于2022年3月6日2022年3月6日由Sukuna发布
在本实验中,您将为用户级线程系统设计上下文切换机制,然后实现它。你的 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可能会有所帮助。 您可以通过以下方式开始:
(gdb) file user/_uthread
Reading symbols from user/_uthread...
(gdb) b uthread.c:60
这将在uthread.c的第60行设置一个断点。 甚至在运行uthread之前,可能会(或可能不会)触发断点。 一旦您的xv6 shell运行,键入“ uthread”,gdb将在第60行中断。现在,您可以键入以下命令来检查uthread的状态:
(gdb) p/x *next_thread
使用“ x”,您可以检查存储位置的内容:
(gdb) x/x next_thread->stack
您可以跳到thread_switch的开头,这样:
(gdb) b thread_switch
(gdb) c
您可以使用以下步骤进行单步组装说明:
(gdb) si
ni是下一条汇编语句,n是下一条C语言语句.
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的结构体的偏移.
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{
栈帧
栈
状态
}
t->ra=(uint64)func;
t->sp=(uint64)&t->stack[STACK_SIZE-1];
其中栈是从高到低的,所以说初始的栈顶指针是指向最高的地址的.
thread_switch((uint64)t,(uint64)next_thread);
文件notxv6 / ph.c包含一个简单的哈希表,该哈希表从单个线程使用时是正确的,但从多个线程使用时则是错误的。 在您的主要xv6目录(可能是〜/ xv6-labs-2020)中,键入以下命令:
$ make ph
$ ./ph 1
请注意,要生成ph,Makefile使用操作系统的gcc,而不是6.S081工具。 ph的参数指定在哈希表上执行放置和获取操作的线程数。
ph运行两个基准。 首先,它通过调用put()向哈希表添加很多键,并输出每秒达到的puts速率。 它使用get()从哈希表中获取密钥。 它打印由于puts而应在哈希表中但丢失的数字键(在这种情况下为零),并打印每秒获得的获取次数。
您可以通过给它一个大于1的参数来告诉ph同时使用多个线程的哈希表。 尝试ph 2:
$ ./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调用为:
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
其实本质上,线程之间也会有互斥的关系,两个线程不能同时访问同一个缓冲区,如果同时当文同一个缓冲区的同一个位置就导致了写重复的问题,就需要对访问缓冲区的代码块上锁,只允许一个线程访问缓冲区.也就是只允许一个进程访问数组的某一个位置.
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]);
}
跟我们学过的操作系统一样,线程在进入访问缓冲区的代码之前上锁,线程离开访问缓冲区的代码后解锁.
在此分配中,您将实现一个障碍:应用程序中的一个点,所有参与线程必须在该点等待,直到所有其他参与线程也都到达该点。 您将使用pthread条件变量,这是一种类似于xv6的睡眠和唤醒的序列协调技术。
文件notxv6 / barrier.c。
$ make barrier
$ ./barrier 2
barrier: notxv6/barrier.c:42: thread: Assertion `i == t' failed.
2指定在屏障上同步的线程数(barrier.c中的nthread)。 每个线程执行一个循环。 在每个循环迭代中,线程都会调用barrier(),然后睡眠随机数微秒。 断言触发,因为一个线程在另一线程到达屏障之前就离开了屏障。 理想的行为是每个线程都在barrier()中阻塞,直到它们的所有nthread都调用了barrier()。
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
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函数即可.