在早期的操作系统中进程一直是独立运行的基本单位,但到了上世纪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号