多线程的实现和信号量
多线程
前言
进程是资源分配的基本单位,线程是调度的基本单位。因为已经实现了多进程,资源分配的事情已经结束。所以,创建一个线程,大致上只需要给予它:
struct Tcb {
// 线程本身的标识码
// 线程自己的现场
// 用于线程调度 状态,优先级,调度队列指针
// 用于pthread_exit()
// 用于pthread_join()
// 用于pthread_cancel()
}
第一步 环境改造
因为整个系统,从多进程转向多线程,进程不在被调度和运行,所以,所有关于进程的东西都得移交给线程。
修改
- env:修改 struct env,将运行环境移交给线程
- 调度:sched.c 调度的对象改为线程调度,从线程中搜寻可调度的线程。
- 运行:env_run() 和 exit() 函数对象修改为线程,特别是 exit(),其指向线程的销毁函数
- 通信:进程间通信的函数要改为线程通信,同一进程的多个线程共用进程来进行通信,但是一次只能由一个在等待接受,所有如果多个线程进入接受,其余线程需等待上一个线程通信完毕。
- fork: fork函数创建进程的同时还要创建一个线程,并把当前线程的环境复制过去。
新增
- 线程调度队列
- 声明 join 队列
- 完成 tcb_id 的创建函数,和通过 tcb_id 索引 tcb 的函数
- 完成线程创建,销毁函数
- 创建线程相关宏定义
- 创建 curtcb 并维护
第二步 接口实现
pthread_create
函数原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_rountine)(void *), void *arg)
pthread_t
在 linux 里面其实是 unsigned long
,在我的设计中,这个 thread
返回的是线程的 id,这样利于后面寻找线程.
返回值为是否线程创建成功.
pthread_exit
函数原型:
void pthread_exit(void *value_ptr)
该函数终止当前的线程,并设置当前线程的 exit_value,并唤醒该线程 join 队列中的线程,把 exit_value 传给被唤醒的线程。
pthread_cancel
函数原型:
int pthread_cancel(pthread_t thread)
该函数终止指定的线程,该终止并非立马终止,具体行为可以由线程自己决定是否终止,以及什么时候终止。因此,cancel 的时候需要判断线程的 state 和 type。
其分类如下:
- state:
- PTHRAED_CANCEL_ENABLE:响应 cancel
- PTHREAD_CANCEL_DISABLE:不响应 cancel
- type:
- PTHREAD_CANCEL_DEFERRED:当进入 cancel point 的时候 cancel
- PTHREAD_CANCEL_ASYNCHRONOUS:立即响应 cancel
其中我实现的 cancel point 如下:
- pthread_testcancel
- sem_wait
其余应该还有文件系统相关函数,但是因为我是基于 lab4 写的,系统里面还未实现文件系统,故放弃
pthread_join
函数原型:
int pthread_join(pthread_t thread, void **value_ptr)
该函数将当前线程挂载到指定线程,待目标线程结束后,才被唤醒,用于线程回收资源。
可以认为,线程的创建是为了实现一个要求,线程做完工作之后需要告知其他线程运行结果,所以需要 join 来回收资源,该资源通过指定的 value_ptr
进行回收,进入函数之后,线程的 tcb 维护该指针,待到目标线程结束,将资源放入其中.
但是,也可以让线程自己释放资源,通过设置 detach 能让线程无法被其他线程 join.
根据手册,应该只能被一个线程挂载,并且目标线程的 detach 不能为1。
pthread_setcancelstate
函数原型:
int pthread_setcancelstate(int state, int *oldvalue)
设置当前线程的 cancelstate,其介绍可见 pthread_cancel。
pthread_setcanceltype
函数原型:
int pthread_setcanceltype(int type, int *oldvalue)
设置当前线程的 canceltype,其介绍可见 pthread_cancel。
pthread_testcancel
函数原型:
void pthread_testcancel()
尝试终止当前线程,如果当前线程可以被 cancel,且 cancelmarked 为1,则 cancel 当前线程。
pthread_detach
函数原型:
int pthread_detach(pthread_t thread)
设置目标线程的 detach,使得线程无法被 join。
测试
- 测试线程的创建和销毁
本次测试都是一些基础的测试,判断标准为是否有如下输出:
同时,所有线程都正常退出the exit code is -2 son exit ret is -2
- pingpong.c 测试 fork 和 ipc 是否正常运行
查看是否发出的数据都被接受,且线程正常结束
- 测试在子线程调用 fork 是否按照预期执行
判断标准为是否有如下输出:
2001:this is father
6008:this is son
并且线程正常退出
- canceltest 测试线程的cancelstate 和 canceltype
本次测试正确判断为:
上述循环在输出99之前退出for(i = 0;i<100;i++) { writef("%d ",i); pthread_testcancel(); if (i == 80) { pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,&oldvalue); } }
while (1) {
pthread_testcancel();
writef("%x:son cannot be canceled\n",syscall_gettcbid());
syscall_yield();
writef("next loop\n");
}
上诉循环能 testcancel 结束
本次任务只需要实现无名信号量,也就是说,这个信号量只能在当前进程使用.所以,可以偷懒把信号量存储在用户空间,内核并不对信号量进行管理.但是为了保证操作的原子性,所以,还是需要系统调用实现.
结构体定义
typedef sem sem_t;
LIST_HEAD(Sem_wait_list,Tcb);
struct sem {
u_int sem_value; //信号量的值
char[20] sem_name; // 信号量的名字
u_int sem_shared; //信号量类型
u_int sem_status; //信号量状态
Sem_wait_list sem_wait_list; //信号量的等待队列
u_int sem_envid; // 对于无名信号量,其属于的进程
}
接口实现
sem_init
函数原型:
int sem_init(sem_t *sem,int shared,unsigned int value)
初始化一个信号量
sem_wait
函数原型:
int sem_wait(sem_t *sem)
P操作,如果无法获取资源,则进入资源的等待队列,并放弃 cpu
sem_trywait
函数原型:
int sem_wait(sem_t *sem)
P操作,如果无法获取资源,则进入资源的等待队列,并放弃 cpu。
sem_post
函数原型:
int sem_post(sem_t *sem)
V操作,释放资源,如果有线程在该资源的等待队列则唤醒该线程。
sem_getvalue
函数原型:
int sem_getvalue(sem_t *sem,int *valp)
获取信号量的值,并把该值通过 valp 返回,函数返回值为是否操作成功。
sem_destroy
函数原型:
int sem_destroy(sem_t *sem)
销毁指定信号量,信号量销毁之后,所有对该信号量的操作都是非法的。
测试
生产者消费者模型 customer.c
测试的正确性为两个生产者生产了一共10个数并且消费者输出了这十个数字.
实验难点
这个实验最难的我觉得是对源代码的修改,单纯写多线程其实基本很多都能照搬多进程的写法,难得是将原本多进程的环境修改为多线程,保证多线程的运行。需要解决线程栈的问题,修改很多之前进程的函数。
实验收获
整个实验下来,感觉对 MOS 代码理解更加深厚,之前的线下作业基本就是照着注释写就完事了,这一次我至少完成了一个完整的线程运行的流程,从创建,调度,运行到死亡。在完成挑战性任务之后,整个线程的框架更加的清晰。