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可能会有所帮助。 您可以通过以下方式开始:

(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语言语句.

1) 修改thread_switch的代码,添加上关于thread_switch 的代码,只需要保存/恢复callee-save register:
        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的数据结构中添加寄存器信息.
  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
  t->ra=(uint64)func;
  t->sp=(uint64)&t->stack[STACK_SIZE-1];

其中栈是从高到低的,所以说初始的栈顶指针是指向最高的地址的.

4) 调用switch函数,切换.其中t是当前进程,next_thread是下一个进程.
thread_switch((uint64)t,(uint64)next_thread);

Lab6_2 Using 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]);
}

跟我们学过的操作系统一样,线程在进入访问缓冲区的代码之前上锁,线程离开访问缓冲区的代码后解锁.

Lab6_3 Barrier

在此分配中,您将实现一个障碍:应用程序中的一个点,所有参与线程必须在该点等待,直到所有其他参与线程也都到达该点。 您将使用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函数即可.


0 条评论

发表评论

Avatar placeholder

您的电子邮箱地址不会被公开。 必填项已用*标注

隐藏