lab4挑战性任务-多线程的实现


多线程的实现和信号量

多线程

前言

进程是资源分配的基本单位,线程是调度的基本单位。因为已经实现了多进程,资源分配的事情已经结束。所以,创建一个线程,大致上只需要给予它:

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。

测试

  1. 测试线程的创建和销毁
    本次测试都是一些基础的测试,判断标准为是否有如下输出:
    the exit code is -2
    son exit ret is -2
    同时,所有线程都正常退出
  2. pingpong.c 测试 fork 和 ipc 是否正常运行

查看是否发出的数据都被接受,且线程正常结束

  1. 测试在子线程调用 fork 是否按照预期执行

判断标准为是否有如下输出:

2001:this is father
6008:this is son

并且线程正常退出

  1. canceltest 测试线程的cancelstate 和 canceltype
    本次测试正确判断为:
    for(i = 0;i<100;i++) {
        writef("%d ",i);
        pthread_testcancel();
        if (i == 80) {
            pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,&oldvalue);
        }
    }
    上述循环在输出99之前退出
  while (1) {
    pthread_testcancel();
    writef("%x:son cannot be canceled\n",syscall_gettcbid());
    syscall_yield();
    writef("next loop\n");
}

上诉循环能 testcancel 结束

  1. stacktest 测试栈是否共享
    测试思路:将主线程局部变量的地址通过 pthread_create 传个新线程,新线程访问该地址,看是否查询到该变量

    正确判断:
    是否输出正确的值

    信号量

本次任务只需要实现无名信号量,也就是说,这个信号量只能在当前进程使用.所以,可以偷懒把信号量存储在用户空间,内核并不对信号量进行管理.但是为了保证操作的原子性,所以,还是需要系统调用实现.

结构体定义

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 代码理解更加深厚,之前的线下作业基本就是照着注释写就完事了,这一次我至少完成了一个完整的线程运行的流程,从创建,调度,运行到死亡。在完成挑战性任务之后,整个线程的框架更加的清晰。


Author: Dovahkiin
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Dovahkiin !
  TOC