MIT_6.s081_Lab6:Xv6 and MultiThread
于2022年3月6日2022年3月6日由Sukuna发布
Lab6_1 Uthread: switching between threads
在本实验中,您将为用户级线程系统设计上下文切换机制,然后实现它。你的 xv6 有两个文件 user/uthread.c 和 user/uthread_switch.S,以及 Makefile 中的一个规则来构建一个 uthread 程序。uthread.c 包含大部分用户级线程包,以及三个测试线程的代码。但线程包中缺少一些用于创建线程和线程间切换的代码。
您需要创建一个函数,这个函数可以创建一个进程,在切换进程的时候保存和恢复寄存器.
您需要在 user/uthread.c 中的thread_create()和thread_schedule()以及user/uthread_switch.S中的thread_switch中添加代码。
一个目标是确保当thread_schedule()第一次运行给定线程时,该线程在自己的堆栈上执行传递给thread_create()的函数(这个函数作为一个参数传递给thread_create()函数调用)。换句话说,就是创建线程的时候,有一个参数就是一个地址,这个地址指向一个函数,当这个线程第一次启动的时候就从这个地址对应的指令开始执行.
另一个目标是确保thread_switch保存被切换的线程的寄存器,恢复被切换到的线程的寄存器,并返回到后一个线程的指令中上次停止的点。您必须决定在哪里保存/恢复寄存器;修改struct thread以保存寄存器是一个不错的计划。
您需要在thread_schedule中添加对thread_switch的调用;您可以将所需的任何参数传递给thread_switch,但目的是从线程t切换到next_thread。
thread_switch 只需要保存/恢复callee-save registers。
要测试您的代码,使用riscv64-linux-gnu-gdb单步执行thread_switch可能会有所帮助。 您可以通过以下方式开始:
代码语言:javascript复制(gdb) file user/_uthread
Reading symbols from user/_uthread...
(gdb) b uthread.c:60
这将在uthread.c的第60行设置一个断点。 甚至在运行uthread之前,可能会(或可能不会)触发断点。 一旦您的xv6 shell运行,键入“ uthread”,gdb将在第60行中断。现在,您可以键入以下命令来检查uthread的状态:
代码语言:javascript复制(gdb) p/x *next_thread
使用“ x”,您可以检查存储位置的内容:
代码语言:javascript复制 (gdb) x/x next_thread->stack
您可以跳到thread_switch的开头,这样:
代码语言:javascript复制(gdb) b thread_switch
(gdb) c
您可以使用以下步骤进行单步组装说明:
代码语言:javascript复制(gdb) si
ni是下一条汇编语句,n是下一条C语言语句.
1) 修改thread_switch的代码,添加上关于thread_switch 的代码,只需要保存/恢复callee-save register:
代码语言:javascript复制 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)
其中a0代表了被打断的线程的栈帧,a1代表了要切换到的线程的栈帧.其中栈帧就是相对于thread的结构体的偏移.
2) 在Thread的数据结构中添加寄存器信息.
代码语言:javascript复制 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;
//修改后
thread{
栈帧
栈
状态
}
3) 修改创建进程的代码.对ra和sp寄存器进行初始化.其中ra本质上就是断点寄存器.有一个参数就是一个地址,这个地址指向一个函数,当这个线程第一次启动的时候就从这个地址对应的指令开始执行.把这个参数传递给ra
代码语言:javascript复制 t->ra=(uint64)func;
t->sp=(uint64)&t->stack[STACK_SIZE-1];
其中栈是从高到低的,所以说初始的栈顶指针是指向最高的地址的.
4) 调用switch函数,切换.其中t是当前进程,next_thread是下一个进程.
代码语言:javascript复制thread_switch((uint64)t,(uint64)next_thread);
Lab6_2 Using Thread.
文件notxv6 / ph.c包含一个简单的哈希表,该哈希表从单个线程使用时是正确的,但从多个线程使用时则是错误的。 在您的主要xv6目录(可能是〜/ xv6-labs-2020)中,键入以下命令:
代码语言:javascript复制$ make ph
$ ./ph 1
请注意,要生成ph,Makefile使用操作系统的gcc,而不是6.S081工具。 ph的参数指定在哈希表上执行放置和获取操作的线程数。
ph运行两个基准。 首先,它通过调用put()向哈希表添加很多键,并输出每秒达到的puts速率。 它使用get()从哈希表中获取密钥。 它打印由于puts而应在哈希表中但丢失的数字键(在这种情况下为零),并打印每秒获得的获取次数。
您可以通过给它一个大于1的参数来告诉ph同时使用多个线程的哈希表。 尝试ph 2:
代码语言:javascript复制$ ./ph 2
100000 puts, 1.885 seconds, 53044 puts/second
1: 16579 keys missing
0: 16579 keys missing
200000 gets, 4.322 seconds, 46274 gets/second
此ph 2输出的第一行表明,当两个线程同时向哈希表添加条目时,它们每秒的总插入率为53,044。 这大约是运行ph 1时单线程的速度的两倍。这是大约2倍的出色“并行加速”,这是人们可能希望的(即两倍的内核,每单位时间产生两倍的工作量)。
但是,两行说缺少16579键,表明哈希表中不应该存在大量键。 就是说,puts应该将这些键添加到哈希表中,但是出了点问题。 看一下notxv6 / ph.c,尤其是put()和insert()。
为什么缺少2个线程而不是1个线程的键? 用2个线程标识一系列事件,这些事件可能导致键丢失。 提交您的序列,并在answer-thread.txt中提供简短解释。为避免发生此序列的事件,请在putx和get.not.v6 / ph.c中插入lock和unlock语句,以便两个线程始终缺少0的键数。 相关的pthread调用为:
代码语言:javascript复制pthread_mutex_t lock; // declare a lock
pthread_mutex_init(&lock, NULL); // initialize the lock
pthread_mutex_lock(&lock); // acquire lock
pthread_mutex_unlock(&lock); // release lock
其实本质上,线程之间也会有互斥的关系,两个线程不能同时访问同一个缓冲区,如果同时当文同一个缓冲区的同一个位置就导致了写重复的问题,就需要对访问缓冲区的代码块上锁,只允许一个线程访问缓冲区.也就是只允许一个进程访问数组的某一个位置.
代码语言:javascript复制pthread_mutex_t locks[NBUCKET];
static
void put(int key, int value)
{
int i = key % NBUCKET;
// is the key already present?
struct entry *e = 0;
for (e = table[i]; e != 0; e = e->next) {
if (e->key == key)
break;
}
pthread_mutex_lock(&locks[i]);
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]);
}
跟我们学过的操作系统一样,线程在进入访问缓冲区的代码之前上锁,线程离开访问缓冲区的代码后解锁.
Lab6_3 Barrier
在此分配中,您将实现一个障碍:应用程序中的一个点,所有参与线程必须在该点等待,直到所有其他参与线程也都到达该点。 您将使用pthread条件变量,这是一种类似于xv6的睡眠和唤醒的序列协调技术。
文件notxv6 / barrier.c。
代码语言:javascript复制$ make barrier
$ ./barrier 2
barrier: notxv6/barrier.c:42: thread: Assertion `i == t' failed.
2指定在屏障上同步的线程数(barrier.c中的nthread)。 每个线程执行一个循环。 在每个循环迭代中,线程都会调用barrier(),然后睡眠随机数微秒。 断言触发,因为一个线程在另一线程到达屏障之前就离开了屏障。 理想的行为是每个线程都在barrier()中阻塞,直到它们的所有nthread都调用了barrier()。
代码语言:javascript复制pthread_cond_wait(&cond, &mutex); // go to sleep on cond, releasing lock mutex, acquiring upon wake up
pthread_cond_broadcast(&cond); // wake up every thread sleeping on cond
代码语言:javascript复制 pthread_mutex_lock(&bstate.barrier_mutex);
bstate.nthread =1;pthread_mutex_unlock(&bstate.barrier_mutex);
if(bstate.nthread<nthread)
{
pthread_cond_wait(&bstate.barrier_cond,&bstate.barrier_mutex);//go to sleep on cond,releasing lock mutes acquirint upon wake up
}
else{
bstate.round =1;//next round
bstate.nthread=0;//the thread that have entered the barrier() function -> 0
pthread_cond_broadcast(&bstate.barrier_cond);
}
pthread_mutex_unlock(&bstate.barrier_mutex);
因为要修改临界量nthread,所以说进入之前所以要加上锁.如果没到就停止,释放锁,如果到了的话就nthread=0,round 1,调用broadcast函数即可.