MIT 6.828 操作系统课程系列6 Multithreading#
进程调度流程#
看手册第7章。经过之前的lab,其实已经了解了不少。
每个cpu都会跑最初的entry.S。
cpu0会执行userinit
,分配proc,设置好第一个进程,状态设为RUNNABLE。见lab0。
然后每个cpu都会执行scheduler(),死循环不断取出能跑的进程,跑它的代码。
一个进程可以跑在任何cpu上。
一个进程可以说是某一套程序/指令的运行环境,整合了cpu/内存等资源,记录各种状态,让cpu方便地切换进这个环境来执行代码。
// 进程的状态
enum procstate { UNUSED, USED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };
// 进程数据
struct proc {
struct spinlock lock; // 每个进程有一个锁
// p->lock must be held when using these:
enum procstate state; // 进程状态
void *chan; // If non-zero, sleeping on chan
int killed; // 标志为killed
int xstate; // 退出状态。返回给parent
int pid; // Process ID
// wait_lock must be held when using this:
struct proc *parent; // Parent process
// these are private to the process, so p->lock need not be held.
uint64 kstack; // kernel stack的虚拟地址
uint64 sz; // 该进程使用的内存大小。单位字节
pagetable_t pagetable; // User页表
struct trapframe *trapframe; // 存进程各种寄存器等数据。常用在syscall,kernel/user切换等流程。
struct context context; // 存进程的上下文,各种寄存器值。swtch()用到。一键切换进程。
struct file *ofile[NOFILE]; // 打开的文件列表
struct inode *cwd; // 当前目录
char name[16]; // 进程名字
};
#define NPROC 64 // maximum number of processes
struct proc proc[NPROC]; // 系统全局进程列表。最多64个进程。
// cpu数据
struct cpu {
struct proc *proc; // 记录此cpu正在跑的进程
struct context context; // 存此cpu的context。swtch()用到。一键切换进程。
int noff; // Depth of push_off() nesting.
int intena; // Were interrupts enabled before push_off()?
};
#define NCPU 8
struct cpu cpus[NCPU]; // 系统全局cpu列表。最多8个cpu。
scheduler(void)
{
struct proc *p;
struct cpu *c = mycpu();
int id = cpuid();
return r_tp()
// tp = thread pointer寄存器
// 初始化时已经存入了hartid也就是cpu的id
c = &cpus[id];
c->proc = 0; // 初始状态进程为0
for(;;){
// Avoid deadlock by ensuring that devices can interrupt.
intr_on();
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state == RUNNABLE) {
// Switch to chosen process. It is the process's job
// to release its lock and then reacquire it
// before jumping back to us.
p->state = RUNNING;
c->proc = p;
swtch(&c->context, &p->context);
// 切换到userinit创建的进程
// swtch.S
// context包含ra/sp/s0-s11一共14个寄存器
// sd ra, 0(a0)
// sd sp, 8(a0)
// ...
// ld ra, 0(a1)
// ld sp, 8(a1)
// ...
// ret
// 根据riscv的calling convention。函数调用的前两个参数会存在a0和a1。
// sd把寄存器值存到内存地址
// ld把内存地址上的值存到寄存器
// 最后的效果就是把cpu当前的context寄存器保存到c->context,
// 把进程p->context的寄存器值加载到cpu当前寄存器。
// 跑到这里说明cpu从进程中切换出来了。那么当前proc=0。继续找RUNNABLE的进程。
c->proc = 0;
}
release(&p->lock);
}
}
}
哪些情况导致进程切换回kernel的scheduler?
代码里最终是sched()切换回scheduler。
三个入口
exit
进程设为ZOMBIE。再sched()。
ZOMBIE进程必须用wait来回收。yield
进程设为RUNNABLE。再sched()。
cpu每次时钟中断都会导致进程yield。sleep
进程设为SLEEPING。再sched()。
wakeup和kill会把SLEEPING的改为RUNNABLE。
实现线程切换#
每个进程是一套独立的资源环境,直接使用系统资源。而线程是由进程生成,使用所在进程的内存。
需要完成uthread.c
和uthread_switch.S
实现线程的切换。
整体可仿照进程的切换流程。
thread结构体
包含线程的stack,状态,context,要执行的函数等。
三个状态FREE
/RUNNING
/RUNNABLE
。all_thread
为线程数组current_thread
为当前线程指针,指向all_thread
其中一个。初始状态下自己就是线程0。状态
RUNNING
。thread_create
创建新线程
在线程数组中找一个FREE
状态的,改为RUNNABLE
。
仿照进程的启动流程进行配置。thread_schedule
调度线程
从all_thread
里找一个RUNNABLE
状态的。如果找不到说明所有线程都跑完了,它直接exit()。
如果找到的与current_thread
不同,切换到找到的新线程。
如果找到自己,那么继续执行本线程。thread_yield
把当前线程设为RUNNABLE
,再走thread_schedule
。
即尝试交出cpu让别的线程执行。线程函数中可在一些节点使用
thread_yield
交出cpu。
线程函数结束时状态设为FREE
,并thread_schedule
。stack的布局要搞清楚 每个进程有一个
kernel stack
,排在kernel的va末尾处,4k字节。procinit()
中确定。
每个进程有一个user stack
排在elf数据之后,4k字节。在exec()
中确定。
每个线程有一个thread stack
,在all_thread中,all_thread是user程序的全局变量,在进程的elf数据区域中。每个thread stack
有8k字节。
三种stack,完全不同的区域。汇编中stack的使用方式是重点
如果没处理好可能数据会被篡改,查到天荒地老。有用的gdb命令
启动qemumake qemu-gdb
启动gdbgdb-multiarch --command=.gdbinit
导入symbol-file
add-symbol-file user/_uthread
add-symbol-file kernel/kernel
设置断点
b uthread.c:123
b thread_init
跳过某函数
skip function printf
watch系列可监听数据的读写
watch all_thread[0].state
watch all_thread[0].context.sp
使用pthread#
学习pthread
pthread资料
http://lemuria.cis.vtc.edu/~pchapin/TutorialPthread/pthread-Tutorial.pdf
https://docs.oracle.com/cd/E53394_01/html/E54803/tlib-1.html
https://www.ibm.com/docs/en/i/7.2?topic=category-pthread-apis
https://man7.org/linux/man-pages/man7/pthreads.7.html
https://www.cs.cmu.edu/afs/cs/academic/class/15492-f07/www/pthreads.html
https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/pthread.h.html把写操作锁起来即可
Barrier#
Barrier#
可继续学习pthread-Tutorial.pdf
是一种同步机制,让所有线程必须到达某个节点,才能继续执行。
先到的线程必须等待。等所有线程都到达时,一起释放往下走。
一个使用场景是,一个大任务分给多个线程,规定必须完成整个任务才能继续。
那么就要做一个barrier放在子任务完成之后,等待所有子任务完成。
Conditional Variables#
如果想在线程之间发信号,可以用cv。
一个线程用pthread_cond_wait
等待一个cv,另一个线程用pthread_cond_signal
唤醒等待者。
然后配合一个等待条件和mutex完成功能。
pthread_cond_signal
只唤醒一个等待线程,pthread_cond_broadcast
唤醒所有等待线程。
最后的代码比较简洁。可看上述链接的教程获取灵感。