type
status
slug
date
summary
tags
category
password
icon
实验部分
在线程的眼里, 互斥 是一种每个线程能看到的资源,且在一个进程中,可以存在多个不同互斥资源, 所以我们可以把所有的互斥资源放在一起让进程来管理,如下面代码第 9 行所示。这里需要注意的是:
mutex_list: Vec<Option<Arc<dyn Mutex>>>
表示的是实现了 Mutex
trait 的一个“互斥资源”的向量。而 MutexBlocking
是会实现 Mutex
trait 的内核数据结构,它就是我们提到的 互斥资源 即 互斥锁 。操作系统需要显式地施加某种控制,来确定当一个线程释放锁时,等待的线程谁将能抢到锁。 为了做到这一点,操作系统需要有一个等待队列来保存等待锁的线程,如下面代码的第 20 行所示。这样,在操作系统中,需要设计实现三个核心成员变量。互斥锁的成员变量有两个:表示是否锁上的
locked
和管理等待线程的等待队列 wait_queue
;进程的成员变量:锁向量 mutex_list
。这个题目不是银行家算法,是魔改成用于检测死锁而不是避免死锁
我从信号量处理这边来阐释:
首先我们看看是把申请的资源放到need区还是alloc区(取决于够不够)
我整个实验遇到了一个很奇怪很奇怪的错误,我原本想有几个线程就使用几个对应的表,让表(我这里用的vec)大小刚好和线程数量一致,然后可以动态增加,但是就出现检测算法有时候检测不到死锁,我怀疑是某个线程顺序会导致这个情况,但是后面我一气之下直接舍弃这种设计,给一个初始的大的vector,然后填入0,原本我以为这样完全没问题,事实上我测试了很多次也确实没什么问题,就是多得哪些没有存在的线程,他们的need和alloc就是0,每次算法遍历的时候他们也会满足ava≥need从而填入1到finish
但是这样过后居然我在有几次测试出现了错误,但是大部分情况又是正常的,我甚至都没办法debug,因为很久过后才会出现没过测试这种情况。
我后面整理了一下思路,感觉的确最开始的实现不太好,有些的地方设计有问题,应该把一些数据比如need和alloc放到线程控制块里面,这样更合理,找起来也方便,由于半期和期末告急,暂且过了这部分测试,等到我后续有时间我打算重写一下这部分的代码。
还有在这之前我还得补一下问答题。
续:
原来之前的bug是我自己写work向量更新的操作的时候代码写错了,没有把原来线程占据的全部资源加到work里面,只是加了这次请求down的资源,难崩。
问答题
1.
在我们的多线程实现中,当主线程 (即 0 号线程) 退出时,视为整个进程退出, 此时需要结束该进程管理的所有线程并回收其资源。 - 需要回收的资源有哪些? - 其他线程的 TaskControlBlock 可能在哪些位置被引用,分别是否需要回收,为什么?
先直接开门见山给出所有问题的答案:
需要回收的与线程相关的资源有:
tid、线程控制块(包含了KernelStack(线程的内核栈)/trap_cx(trap上下文)/TaskUserRes(这里面又包含了用户栈、tid内容)),所以把括号内的东西打散开,也就是释放的所有具体内容
间接联系的还有线程共用的地址空间
除了和线程关联的,这里主线程还会释放作为进程拥有的子进程信息。
其它线程的TaskControlBlock 可能这些地方引用:
- exit_current_and_run_next里面
这里肯定是要回收的,这里就是tid等于0的时候,这个进程需要释放,所以要回收线程的所有资源(核心依赖于remove_inactive_task这个函数来回收)
- 在
sys_waittid
等待指定的task的时候,取出了其它线程的task,判断是否它exit了,如果是就需要释放进程管理这边占用的task tid的资源,只是释放一个id号其实,不过也是需要释放资源的,如果wait的task(线程)没有exit,那么就不会释放资源,这部分代码如下:
看起来,我是根据 process_inner.tasks这个变量全局搜索看的,(process_inner)这个局部变量的取名我检查了一下应该都是一致的,就是进程的inner属性,这里面存的有进程包含的所有线程,所以要拿到其它线程,肯定要经过这一层取inner的操作,全局搜索下来就这两个地方。
下面是分析:
我觉得主要是参考tutorial和sys_waittid和exit_current_and_run_next这两个函数的实现:
综述
一般情况下进程/主线程要负责通过
waittid
来等待它创建出来的线程(不是主线程)结束并回收它们在内核中的资源 (如线程的内核栈、线程控制块等)。如果进程/主线程先调用了 exit
系统调用来退出,那么整个进程 (包括所属的所有线程)都会退出,而对应父进程会通过 waitpid
回收子进程剩余还没被回收的资源。先说waittid(有一种规范的操作就是用waittid来等待所有线程结束)
在sys_waittid的实现里面,代码是:
我们的主线程退出前,会主动调用这个函数释放掉该进程管理的所有线程(通过比如for循环依次传入该进程的所有tid编号)
这里关键点释放是在:
process_inner.tasks[tid] = None;
exit_code
由于线程0会正常情况退出会采用这种方式,所以会循环的释放掉所有的在该进程记录下的taskcontrolblock(这个同时也是释放了tid),其它资源如内核栈,是在线程自身调用exit的时候回收的。不过哪怕是这样,最后thread 0这个线程释放的时候,还是会去调用exit然后使用到我们exit_current_and_run_next,只是说对于线程资源回收的部分是没有的,实际的回收是在这个waittid
直接退出,没有waittid完全
而对于直接的线程0退出,没有去使用waittid,那么核心就在exit_current_and_run_next的这部分代码:
算了,感觉这样写不美观,我摘抄出来一节节说:
僵尸进程的处理是很常见的(在linux上):
任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个 子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。 如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。
一个进程如果只复制fork子进程而不负责对子进程进行wait()或是waitpid()调用来释放其所占有资源的话,那么就会产生很多的僵死进程,如果要消灭系统中大量的僵死进程,只需要将其父进程杀死,此时所有的僵死进程就会编程孤儿进程,从而被init所收养,这样init就会释放所有的僵死进程所占有的资源,从而结束僵死进程。
所以下面你可以看到rcore这里写的:
删除任务控制块(taskcontrolblock):
释放进程相关的资源(地址空间、子进程的记录、文件系统的资源、以及清除在进程里面记录的线程列表)
2.
对比以下两种
Mutex.unlock
的实现,二者有什么区别?这些区别可能会导致什么问题?代码
Mutex2
Mutex2就是我们实验代码里面的MutexBlocking的实现
我先解释它,(文档里面其实已经说的很清楚了):
16 if let Some(waking_task) = mutex_inner.wait_queue.pop_front() {
17 add_task(waking_task);
18 } else {
19 mutex_inner.locked = false;
20 }
- 16-17行 如果有等待的线程,唤醒等待最久的那个线程,相当于将锁的所有权移交给该线程。
- 而如果没有,就在19行的位置释放锁
ok,那我们再看看Mutex1和它有哪里不同(这里先给出答案,Mutex1是有大问题的)
最明显的就是Mutex1一来就先释放了锁,并且没有任何判定的执行唤醒任务的功能(然而两个操作只应该是一次解锁对应一个),我这里直接给出一个会产生问题的例子(Mutex1):
假如现在有三个线程(主线程0,其它线程1,其它线程2),主线程拿到锁,另外其它线程1等待锁,而其它线程2正常运行(后续会需要锁,目前还没有)。
- 主线程释放锁,此时线程2没有等锁(正常运行),线程1等待锁(阻塞)
- 主线程释放锁(目前锁为空的,无人占用)
- 主线程执行到第7行(因为1等待锁,所以wait_queue中可以弹出一个需要唤醒的线程1),然后线程1被唤醒,得到互斥锁,执行临界区内容
- 这个时候,如果2执行到临界区,想要获得锁(此时线程1还在临界区),是能够得到的,因为锁是空的,所以会虽然使用了互斥锁,但是线程1、2同时进入了临界区,并没有满足最基本的互斥锁的要求: 互斥性(mutual exclusion),即锁是否能够有效阻止多个线程进入临界区,这是最基本的属性。
补充
不过对于Mutex1,其实也是有一点小问题,参考rocore里面的代码,这里没有更新任务的状态为Ready,使得任务仍然为block,这一点是不太好的
- 作者:liamY
- 链接:https://liamy.clovy.top/article/OS_Tutorial/lab8
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。