在早期的操作系统中进程一直是独立运行的基本单位,但到了上世纪80年代,人们又提出了比进程更小的,并且可以独立运行的基本单位,这个基本单位被称作为线程。提出线程的目的是为了提高程序的并发执行的程度,从而提高系统的吞吐量。由于线程与线程之间需要进程数据共享和通信,所以一个进程中的多个线程都可以访问其父进程的资源。同时又采用信号量机制来控制多个线程之间的资源共享问题。我们先来学习如何为进程创建一个线程,再通过一个实际的例子来理解它的原理与应用。
为进程创建一个线程,首先要为使这个线程能够立运行,所以要先为其分配一个pcb,并且与它的父进程共享代码段和页表一页目录,这样做的目的是为了让线程能够调用其它函数和全局资源:
void create_pthread(s_pcb *parent_pcb, s_pthread *p, void *run, void *args) { //进程控制块 s_pcb *pcb = alloc_page(process_id, pages_of_pcb(), 0, 0); //页目录 pcb->page_dir = parent_pcb->page_dir; //页表 pcb->page_tbl = parent_pcb->page_tbl; //申请栈 pcb->stack = alloc_page(process_id, P_STACK_P_NUM, 1, 0); if (args != NULL) { //设置传入参数 u32 *args_addr = pcb->stack + P_STACK_P_NUM - 4; *args_addr = (u32) args; } //申请0级栈 pcb->stack0 = alloc_page(process_id, P_STACK0_P_NUM, 0, 0); //初始化pcb init_pthread(pcb, process_id, run); //将此进程加入链表 pcb_insert(pcb); //进程号加一 process_id++; }
初始化线程,为其分配任务号,指定eip运行多线程函数地址,并设定esp为栈地址,设定cr3为页目录地址:
void init_pthread(s_pcb *pcb, u32 pid, void *run) { init_pcb(pcb); //进程号 pcb->process_id = pid; //程序入口地址 pcb->tss.eip = (u32) run; //程序栈 pcb->tss.esp = (u32) pcb->stack + P_STACK_P_NUM - 8; //程序0级栈 pcb->tss.esp0 = (u32) pcb->stack0 + P_STACK0_SIZE; //页目录存入到cr3中 pcb->tss.cr3 = (u32) pcb->page_dir; //初始化pcb所在的内存页 init_process_page((u32) pcb, pages_of_pcb(), pcb->page_dir); //初始化pcb->stack0所在的内存页 init_process_page((u32) pcb->stack0, pages_of_pcb(), pcb->page_dir); }
为普通程序提供创建线程的系统调用:
void pthread_create(s_pthread *p, void *function, void *args) { int params[4]; params[0] = 3; params[1] = (int) p; params[2] = (int) function; params[3] = (int) args; __asm__ volatile("int $0x80" :: "a"(params)); }
最后来编写一个多线程的例子:一个车站要出售20张车票,需要有2个售票窗口来执行售票工作,这2个窗口售票的过程是相互独立的,但共享同一个票库。每当窗口出售了一张车票,剩余票数减1,当剩余票数为0时停止售票。这是一个非常典型的多线程例子,我们先来看一下它的实现过程:
#define PNUM (2) void sell_ticket(int num) { //调用0x82号中断程序,显示一个数字 int params[2]; params[0] = 1; params[1] = num; __asm__ volatile("int $0x82" :: "a"(params)); } void myfunc(void *args) { int *num = (int *) args; while (1) { if ((*num) <= 0) { break; } //模拟等待了一小会 msleep(10); sell_ticket(*num); (*num)--; } for (;;) { } } int main(int argc, char **args) { int num = 20; s_pthread p[PNUM]; for (int i = 0; i < PNUM; i++) { pthread_create(&p[i], &myfunc, &num); } for (;;) { } return 0; }
上面代码中msleep(10);是模拟一个现实的过程:售票时,售票员先要查询剩余票数,当剩余票数大于0时,售票员按下出售健,剩余票数减1。也就是说在查询到有剩余票后,再对剩余票数减1,这一个过程会有一小会儿的等待过程。编译运行程序并查看程序运行结果:
源代码的下载地址为:
https https://github.com/magicworldos/lidqos.git git git@github.com:magicworldos/lidqos.git subverion https://github.com/magicworldos/lidqos branch v0.21
下面让我们把售票窗口数由2修改成10,再来看一下运行结果:
奇怪的事情发生了:总票数为20张,但是10个售票窗口居然一共出售了28张票。并且剩余票数被减为-7。发生这一现象的原因是售票员在“查看剩余票”到“出售”再到“剩余票数减1”这个过程中经过了一小段时间,当售票员A看到剩余票数为1时,A可以出售此车票,但当A还没有按下“出售”按钮时,另外一个售票员B也看到了这张车票,于是他也可以出售此车票。于是就产生了上面的错误,多个售票员同时看到了剩余票数为1,同时“出售”则车票数被减为负数。这就是多线程共享数据时发生的错误。但车票数被减为负数也只是小问题,更重要的问题是对于买票者来说,可能会出现多个人买到了同一张座位的车票。
为了解决上述问题,我们可以结合上一节所学习的信号量机制来解决这个错误。由于信号量的增减过程是在系统中断服务中完成的,也就是说这是一个不可再进行中断或分割的过程。我们称这个操作过程为“原子操作”,所以对于信号量的增减不会有上述问题中“等待一小会”的问题。我们来修改一下代码,在售票前后加入信号量的P/V操作:
//全局售票信号量 #define PNUM (10) s_sem sem; void sell_ticket(int num) { //调用0x82号中断程序,显示一个数字 int params[2]; params[0] = 1; params[1] = num; __asm__ volatile("int $0x82" :: "a"(params)); } void myfunc(void *args) { int *num = (int *) args; while (1) { //信号量P操作 sem_wait(&sem); //剩余票数为0时停止售票 if ((*num) <= 0) { break; } //模拟等待了一小会 msleep(10); //售票 sell_ticket(*num); //剩余票数减1 (*num)--; //信号量V操作 sem_post(&sem); } //信号量V操作 sem_post(&sem); for (;;) { } } int main(int argc, char **args) { //初始化信号量 sem_init(&sem, 1); //剩余票数 int num = 20; s_pthread p[PNUM]; //创建多个线程 for (int i = 0; i < PNUM; i++) { pthread_create(&p[i], &myfunc, &num); } for (;;) { } return 0; }
再来看一下运行结果:
运行结果正确。
源代码的下载地址为:
https https://github.com/magicworldos/lidqos.git git git@github.com:magicworldos/lidqos.git subverion https://github.com/magicworldos/lidqos branch v0.22
Copyright © 2015-2023 问渠网 辽ICP备15013245号