文章目录
Lec11: Thread Switching学习笔记
- 在RISC-V中,每个CPU都有一个hartid,且这个hartid保存在每个CPU的tp寄存器里。
- 在CPU的boot时,会将hartid保存至tp寄存器中;
- 在从内核态进入用户态时,在usertrapret函数中,会将hartid(也就是tp寄存器中的值)保存至trampoline page;
- 在从用户态进入内核态时,也就是在uservec中(文件trampoline.S),会restore寄存器tp的值;
- 线程共享内存问题:
- xv6的内核有多个内核线程,每个内核线程对应用户态的一个进程;
- xv6内核的线程共享内核内存;
- xv6的用户进程之间不会共享内存,且每个用户进程只有一个线程;
- 在linux中,用户进程之间不会共享内存,且每个用户进程可拥有多个线程,线程之间共享内存。(Linux比xv6更加复杂,但是我们这里重点理解工作原理)
- 还有一些系统不使用线程也能同时运行多个程序,感兴趣的可以搜索
state-machine
和event-driven programming
。
- 线程调度:
- xv6为每个CPU都创建了一个线程调度器;
- 定时器中断,会将程序的控制权从用户空间转换至内核空间,因为中断处理程序的优先级更高;
- 中断处理的基本流程:定时器中断将CPU控制权从用户态转交为内核态,内核态再将控制权交给内核的线程调度器;
- 寄存器保存
- 用户寄存器保存在trapframe中;即trapframe保存的是用户进入和离开内核时的数据;
- 内核线程的寄存器保存在contex中;(通过函数swtch进行保存和交换,详细代码位于swtch.S中);即contex保存的是内核线程和线程调度器切换时的数据。
- 调度器线程
- 每个CPU都有一个内核调度器线程,它有自己的contex对象,用于保存自己的寄存器;
- 当一个CPU上的内核线程进行切换时,首先old的内核线程会切换至内核调度器线程,之后再由内核调度器切换至new的内核线程;
- 每个线程调度器都有一个独立的栈;
- 线程调度器的栈和contex,是在启动时就设置好了
- contex对象保存位置:
- 内核线程分为两类:普通的内核线程和内核调度器线程;
- 普通的内核线程的contex,保存在与其对应的用户进程proc的字段contex中(也就是struct proc的contex字段);
- 内核调度器的线程,保存在与其对应的CPU结构体的contex字段中(也就是struct cpu的contex字段);
- CPU的工作:
- 一个CPU在一个时间点只会做以下三件事情中的一件;
- 在运行内核线程;
- 在运行用户进程;
- 在运行线程调度器;
- 线程的切换造成了CPU可运行多个线程的假象;
- 一个线程要么运行在一个CPU上,要么其状态保存在contex中;
- 一个CPU在一个时间点只会做以下三件事情中的一件;
- switch
-
每次调用switch,代码都会进行跳转,当内核再次调用switch并切换时,会跳转回来。
-
如果是从内核线程跳转至调度器线程,则一般是跳转至scheduler函数内;
-
如果是从调度器线程跳转至内核线程,则一般是跳转至内核线程执行switch函数的下一条语句。
-
-
内核第一次调用switch时,其中一个contex是伪造的,因为并不存在与之交换的contex。
-
实验链接
https://pdos.csail.mit.edu/6.S081/2020/labs/thread.html
实验
Lab: Multithreading
该实验的主要目标是实现用户级的线程管理和切换。相关代码位于user/uthread.c
文件中。该文件一共涉及4个线程,main
线程,thread_a
线程,thread_b
线程,thread_c
线程。需要注意的点是:
- 本质上还是单个CPU在分时执行多个线程;
- 栈是从大地址往小地址增长的,即栈底是大地址,栈顶是小地址,如果程序搞反了,可能会导致各个线程之间的数据产生错乱。
具体实现如下:
-
文件
user/uthread.c
// 为每个线程设置contex,为了进行线程切换时保存寄存器的现场 struct thread { char stack[STACK_SIZE]; /* the thread's stack */ int state; /* FREE, RUNNING, RUNNABLE */ struct context* context; }; // 直接从内核的context拷贝过来 // Saved registers for context switches. struct context { uint64 ra; uint64 sp; // callee-saved uint64 s0; uint64 s1; uint64 s2; uint64 s3; uint64 s4; uint64 s5; uint64 s6; uint64 s7; uint64 s8; uint64 s9; uint64 s10; uint64 s11; }; struct context all_context[MAX_THREAD];
-
文件
user/uthread.c
void thread_init(void) { // 增加初始化context的内容 for (int i = 0; i < MAX_THREAD; i++) { all_thread[i].context = &all_context[i]; } current_thread = &all_thread[0]; current_thread->state = RUNNING; }
-
文件
user/uthread.c
void thread_schedule(void) { struct thread *t, *next_thread; /* Find another runnable thread. */ next_thread = 0; t = current_thread + 1; for(int i = 0; i < MAX_THREAD; i++){ if(t >= all_thread + MAX_THREAD) t = all_thread; if(t->state == RUNNABLE) { next_thread = t; break; } t = t + 1; } if (next_thread == 0) { printf("thread_schedule: no runnable threads\n"); exit(-1); } if (current_thread != next_thread) { /* switch threads? */ next_thread->state = RUNNING; t = current_thread; current_thread = next_thread; /* YOUR CODE HERE * Invoke thread_switch to switch from t to next_thread: * thread_switch(??, ??); */ // +++++ begin +++++++ thread_switch((uint64)(t->context), (uint64)(next_thread->context)); // ------ end ---------- } else next_thread = 0; }
-
文件
user/uthread.c
,更改thread_create
函数void thread_create(void (*func)()) { struct thread *t; for (t = all_thread; t < all_thread + MAX_THREAD; t++) { if (t->state == FREE) break; } t->state = RUNNABLE; // YOUR CODE HERE memset(t->stack, 0, sizeof(char) * MAX_THREAD); t->context->ra = (uint64)(*func); // 注意栈的方向,否则会导致数据错乱 t->context->sp = (uint64)(&(t->stack[STACK_SIZE - 1])); }
-
文件
user/uthread_switch.S
.text /* * save the old thread's registers, * restore the new thread's registers. */ .globl thread_switch thread_switch: /* YOUR CODE HERE */ sd ra, 0(a0) sd sp, 8(a0) sd s0, 16(a0) sd s1, 24(a0) sd s2, 32(a0) sd s3, 40(a0) sd s4, 48(a0) sd s5, 56(a0) sd s6, 64(a0) sd s7, 72(a0) sd s8, 80(a0) sd s9, 88(a0) sd s10, 96(a0) sd s11, 104(a0) ld ra, 0(a1) ld sp, 8(a1) ld s0, 16(a1) ld s1, 24(a1) ld s2, 32(a1) ld s3, 40(a1) ld s4, 48(a1) ld s5, 56(a1) ld s6, 64(a1) ld s7, 72(a1) ld s8, 80(a1) ld s9, 88(a1) ld s10, 96(a1) ld s11, 104(a1) ret /* return to ra */
Lab: Using threads
该实验比较简单,主要目的是为Hashtable加锁,为了加快速度,这里我们主要在桶级别进行加锁。代码如下:
-
文件
ph.c
,首先初始化锁struct entry { int key; int value; struct entry *next; }; struct entry *table[NBUCKET]; // ++++ begin +++++++++ pthread_mutex_t locks[NBUCKET]; // ---- end ------------ // 增加函数初始化锁 void initlock() { for (int i = 0; i < NBUCKET; i++) { pthread_mutex_init(&locks[i], NULL); } } // 在main函数中初始化锁 int main(int argc, char *argv[]) { assert(NKEYS % nthread == 0); for (int i = 0; i < NKEYS; i++) { keys[i] = random(); } // +++++ begin ++++++ initlock(); // ------ end ------- // // first the puts // t0 = now(); }
-
桶级加锁
static void put(int key, int value) { int i = key % NBUCKET; // is the key already present? struct entry *e = 0; pthread_mutex_lock(&locks[i]); // +++++ for (e = table[i]; e != 0; e = e->next) { if (e->key == key) break; } if(e){ // update the existing key. e->value = value; } else { // the new is new. insert(key, value, &table[i], table[i]); } pthread_mutex_unlock(&locks[i]); // +++++ }
-
结果
-
可以看到速度提升了,但是不到2倍。
Barrier
该题主要是练习pthread_cond的使用,设置barrier主要目标是为了同步线程。
-
文件
user/barrier.c
,更改函数barrier
为如下内容static void barrier() { // YOUR CODE HERE // // Block until all threads have called barrier() and // then increment bstate.round. // pthread_mutex_lock(&bstate.barrier_mutex); bstate.nthread++; if (bstate.nthread == nthread) { pthread_cond_broadcast(&bstate.barrier_cond); bstate.nthread = 0; bstate.round++; } else { // go to sleep on cond, releasing lock mutex, acquiring upon wake up pthread_cond_wait(&bstate.barrier_cond, &bstate.barrier_mutex); } pthread_mutex_unlock(&bstate.barrier_mutex); }
-
执行结果
结果
$ make grade
提交结果
$ git commit -m "lab multithreading"
$ make handin
查看结果
登录网站https://6828.scripts.mit.edu/2020/handin.py/student
,可以看到提交的结果。
参考链接
https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081
https://pdos.csail.mit.edu/6.S081/2020/schedule.html
Github
源码: https://github.com/aerfalwl/mit-xv6-labs-2020