首页 > 其他分享 >MIT 6.1810 Lab: Multithreading

MIT 6.1810 Lab: Multithreading

时间:2024-02-12 20:11:52浏览次数:21  
标签:thread lock void 线程 context pthread 6.1810 Multithreading MIT

lab网址:https://pdos.csail.mit.edu/6.828/2022/labs/cow.html

xv6Book:https://pdos.csail.mit.edu/6.828/2022/xv6/book-riscv-rev3.pdf

schedule代码分析

scheduler在内核初始化的最后调用,内核初始化由main函数承担,运行在特权模式,main函数由start函数调用,start函数运行在机器模式。main函数会先用一个核心完成内核初始化,然后开启多CPU模式,每个核心在结束时调用scheduler。在entry.S中提到qemu会将每个核心都加载到start函数的入口处。

// start() jumps here in supervisor mode on all CPUs.
void
main()
{
  if(cpuid() == 0){
    consoleinit();
    printfinit();
    printf("\n");
    printf("xv6 kernel is booting\n");
    printf("\n");
    kinit();         // physical page allocator
    kvminit();       // create kernel page table
    kvminithart();   // turn on paging
    procinit();      // process table
    trapinit();      // trap vectors
    trapinithart();  // install kernel trap vector
    plicinit();      // set up interrupt controller
    plicinithart();  // ask PLIC for device interrupts
    binit();         // buffer cache
    iinit();         // inode table
    fileinit();      // file table
    virtio_disk_init(); // emulated hard disk
    userinit();      // first user process
    __sync_synchronize();
    started = 1;
  } else {
    while(started == 0)
      ;
    __sync_synchronize();
    printf("hart %d starting\n", cpuid());
    kvminithart();    // turn on paging
    trapinithart();   // install kernel trap vector
    plicinithart();   // ask PLIC for device interrupts
  }

  scheduler();        
}

之后scheduler采用轮询的方法依次激活进程,swtch函数改变CPU的context,当CPU的PC、和其他寄存器都切换后,进程切换已经实质上的完成了。swtch定义在汇编文件中,将当前的context保存在a0寄存器指向的位置,即&c->context,加载新的contexta1寄存器指向的位置&p->context

// Per-CPU process scheduler.
// Each CPU calls scheduler() after setting itself up.
// Scheduler never returns.  It loops, doing:
//  - choose a process to run.v
//  - swtch to start running that process.
//  - eventually that process transfers control
//    via swtch back to the scheduler.
void
scheduler(void)
{
  struct proc *p;
  struct cpu *c = mycpu();
  
  c->proc = 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);

        // Process is done running for now.
        // It should have changed its p->state before coming back.
        c->proc = 0;
      }
      release(&p->lock);
    }
  }
}

每个CPU拥有一个schedulerscheduler的上下文储存在struct cpu -> context中。当想激活scheduler时,只需要将struct cpu中的context写入CPU。调度程序scheduler通过swtch(&c->context, &p->context);退出执行,将执行流让给新进程。

// Per-CPU state.
struct cpu {
  struct proc *proc;          // The process running on this cpu, or null.
  struct context context;     // swtch() here to enter scheduler().
  int noff;                   // Depth of push_off() nesting.
  int intena;                 // Were interrupts enabled before push_off()?
};

sched函数用于激活调度程序,通过swtch(&p->context, &mycpu()->context);完成。sched函数切换前验证当前进程状态设置以完成。保护intena,局部变量保存在栈中,当前进程再次执行时的第一条语句为mycpu()->intena = intena;。因此用户进程的切换发生在用户进程的内核态,sched函数处。

// Switch to scheduler.  Must hold only p->lock
// and have changed proc->state. Saves and restores
// intena because intena is a property of this
// kernel thread, not this CPU. It should
// be proc->intena and proc->noff, but that would
// break in the few places where a lock is held but
// there's no process.
void
sched(void)
{
  int intena;
  struct proc *p = myproc();

  if(!holding(&p->lock))
    panic("sched p->lock");
  if(mycpu()->noff != 1)
    panic("sched locks");
  if(p->state == RUNNING)
    panic("sched running");
  if(intr_get())
    panic("sched interruptible");

  intena = mycpu()->intena;
  swtch(&p->context, &mycpu()->context);
  mycpu()->intena = intena;
}

Uthread: switching between threads

这个实验要求我们实现用户级的线程,用户级线程与内核线程由很多相似之处。考虑以下问题:1.何时发生调度,2.如何保存和恢复现场。内核线程每个时钟中断发生调度,将寄存器的值保存在内存中来保护和恢复现场。在xv6中,用户线程的调度可以通过程序显示调用,即thread_schedule()。每个进程的栈保存在struct thread中,线程切换同样需要更改寄存器,修改这些寄存器不需要特权模式。

struct thread {
  char       stack[STACK_SIZE]; /* the thread's stack */
  int        state;             /* FREE, RUNNING, RUNNABLE */
  struct context context;
};

因此可以如下设计。程序的主线程始终使用系统栈,将每个线程的栈储存在thread->stack中,主线程加载新线程时,将栈指针指向线程栈,控制流的通过ra寄存器完成。相比与内核调度,由于调度程序是被显示调用的,因此不需要保存调度程序的现场,这样调度程序也必须每次从头运行。

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(??, ??);
     */
 +++   thread_switch(&t->context,&current_thread->context);
    //here to pause
  } else
    next_thread = 0;
}

thread_create函数用于创建新线程,记录线程对应的函数地址,线程栈顶,注意sp寄存器保存栈顶,栈向低地址延申,RISC-V无需栈底寄存器。

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
  t->context.ra = (uint64)func;
  t->context.sp = (uint64)t->stack+STACK_SIZE;
}

线程切换只需保护callee-saved,在RISC-V中,部分寄存器由调用函数保护到栈中,因此无需保护这些寄存器。

diff --git a/user/uthread.c b/user/uthread.c
index 06349f5..5bc255c 100644
--- a/user/uthread.c
+++ b/user/uthread.c
@@ -10,14 +10,34 @@
 #define STACK_SIZE  8192
 #define MAX_THREAD  4
 
+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 thread {
   char       stack[STACK_SIZE]; /* the thread's stack */
   int        state;             /* FREE, RUNNING, RUNNABLE */
+  struct context context;
 };
+
 struct thread all_thread[MAX_THREAD];
 struct thread *current_thread;
-extern void thread_switch(uint64, uint64);
+extern void thread_switch(struct context *, struct context *);
               
 void 
 thread_init(void)
@@ -62,6 +82,8 @@ thread_schedule(void)
      * Invoke thread_switch to switch from t to next_thread:
      * thread_switch(??, ??);
      */
+    thread_switch(&t->context,&current_thread->context);
+    //here to pause
   } else
     next_thread = 0;
 }
@@ -76,6 +98,8 @@ thread_create(void (*func)())
   }
   t->state = RUNNABLE;
   // YOUR CODE HERE
+  t->context.ra = (uint64)func;
+  t->context.sp = (uint64)t->stack+STACK_SIZE;
 }
 
 void 
diff --git a/user/uthread_switch.S b/user/uthread_switch.S
index 5defb12..65eec2c 100644
--- a/user/uthread_switch.S
+++ b/user/uthread_switch.S
@@ -8,4 +8,34 @@
 	.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 */

Using threads

这部分希望我们利用本机完成线程的使用,解决给出代码的线程竞争问题。

pthread_create 是 POSIX 线程库中的一个函数,用于创建新的用户线程,这个函数的原型如下

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine)(void *), void *arg);
  1. pthread_t *thread:指向新创建的线程 ID 的指针。
  2. const pthread_attr_t *attr:指定线程的属性,通常传入 NULL 以使用默认属性。
  3. void *(*start_routine)(void *):指定新线程要执行的函数,这个函数的返回类型是 void*,接受一个 void* 类型的参数。
  4. void *arg:传递给 start_routine 函数的参数

pthread_join 是 POSIX 线程库中的一个函数,用于等待一个指定的线程终止,并获取该线程的退出状态。

int pthread_join(pthread_t thread, void **retval);
  1. pthread_t thread:要等待的线程的标识符。
  2. void **retval:用于存储目标线程的返回值(退出状态)的指针。这是一个指向指针的指针,因为线程的返回值是一个指针类型。
  3. pthread_join 函数的返回值为 0 表示成功,非零值表示错误。成功调用 pthread_join 会阻塞当前线程,直到指定的线程终止。

为什么程序在多线程时,发生了异常呢。我们可以在insert中,n是表头,p指向表头,在多线程运行时,e->next = n;中的n可能已经不是当前的表头了,最后*p = e设置新表头时,就有可能漏掉了其他线程添加的表项。

static void 
insert(int key, int value, struct entry **p, struct entry *n)
{
  struct entry *e = malloc(sizeof(struct entry));
  e->key = key;
  e->value = value;
  e->next = n;
  *p = e;
}

因此可以通过添加锁来解决这个问题。为每个剪纸桶添加一个锁,在获取表头前获取锁,设置新表头后释放。

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
diff --git a/notxv6/ph.c b/notxv6/ph.c
index 82afe76..5f8ab08 100644
--- a/notxv6/ph.c
+++ b/notxv6/ph.c
@@ -17,6 +17,8 @@ struct entry *table[NBUCKET];
 int keys[NKEYS];
 int nthread = 1;
 
+pthread_mutex_t locks[NBUCKET];            // declare a lock
+
 
 double
 now()
@@ -52,7 +54,9 @@ void put(int key, int value)
     e->value = value;
   } else {
     // the new is new.
+    pthread_mutex_lock(&locks[i]);       // acquire lock
     insert(key, value, &table[i], table[i]);
+    pthread_mutex_unlock(&locks[i]);     // release lock
   }
 
 }
@@ -117,7 +121,11 @@ main(int argc, char *argv[])
   for (int i = 0; i < NKEYS; i++) {
     keys[i] = random();
   }
-
+  for (int i = 0; i < NBUCKET; i++)
+  {
+    pthread_mutex_init(&locks[i], NULL); // initialize the lock
+  }
+  
   //
   // first the puts
   //

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

修改bstate时先获取锁,如果不满足条件,则调用pthread_cond_wait(&cond, &mutex);,该函数会阻塞线程并释放锁,当满足条件后,每个线程依次获取锁退出函数。

diff --git a/notxv6/barrier.c b/notxv6/barrier.c
index 12793e8..f224554 100644
--- a/notxv6/barrier.c
+++ b/notxv6/barrier.c
@@ -30,7 +30,16 @@ barrier()
   // Block until all threads have called barrier() and
   // then increment bstate.round.
   //
-  
+  pthread_mutex_lock(&bstate.barrier_mutex);       // acquire lock
+  bstate.nthread++;
+  if(bstate.nthread == nthread){
+    bstate.round++;
+    bstate.nthread = 0;
+    pthread_cond_broadcast(&bstate.barrier_cond);     // wake up every thread sleeping on cond
+  } else {
+    pthread_cond_wait(&bstate.barrier_cond, &bstate.barrier_mutex);  // go to sleep on cond, releasing lock mutex, acquiring upon wake up
+  }
+  pthread_mutex_unlock(&bstate.barrier_mutex);       // release lock
 }
 
 static void *

结果

最后成功通过所有测试

标签:thread,lock,void,线程,context,pthread,6.1810,Multithreading,MIT
From: https://www.cnblogs.com/benoqtr/p/18014085

相关文章

  • [MIT 6.S081] Lab: Copy-on-Write Fork for xv6
    Lab:Copy-on-WriteForkforxv6在这个实验中,我们要为xv6实现cowfork。Implementcopy-onwrite根据写时复制的方式,我们在复制页面的时候应该采用的是将父级的物理页面映射到子级的页面,因此我们需要修改kernel/vm.c中的uvmcopy,将页面复制修改为映射的方式,同时应当将......
  • form里面有多个对象,对象里面的每一项都是formItem,rules应该怎么配置
    <template><el-form:model="formData":rules="formRules"ref="form"label-width="100px"><el-form-itemlabel="对象1"><el-inputv-model="formData.object1.prop1"pla......
  • MIT 6.1810 Lab: Copy-on-Write Fork for xv6
    lab网址:https://pdos.csail.mit.edu/6.828/2022/labs/cow.htmlxv6Book:https://pdos.csail.mit.edu/6.828/2022/xv6/book-riscv-rev3.pdfImplementcopy-on-writefork这部分需要我们实现写时拷贝,题目给出解决方案为,当fork时,将父子进程的页表项都设置为只度,当发生写错误时,在处......
  • Omit 用于创建一个新的类型,该类型包含了原始类型`T`的所有属性,但排除了指定的键`K`
    在TypeScript中,Omit<T,K>是一个内置的实用类型(从TypeScript3.5版本开始提供),用于创建一个新的类型,该类型包含了原始类型T的所有属性,但排除了指定的键K。其定义如下:typeOmit<T,Kextendskeyofany>=Pick<T,Exclude<keyofT,K>>;这个类型的工作原理是首先找出T的所有键(......
  • [MIT 6.S081] Lab: xv6 lazy page allocation
    Lab:xv6lazypageallocationEliminateallocationfromsbrk()这一步比较简单,直接在sys_sbrk中将分配内存改为对内存大小进行修改而不分配内存即可。uint64sys_sbrk(void){intaddr;intn;if(argint(0,&n)<0)return-1;addf=myproc()->sz;my......
  • MIT 6.1810 Lab: traps
    lab网址:https://pdos.csail.mit.edu/6.828/2022/labs/traps.htmlxv6Book:https://pdos.csail.mit.edu/6.828/2022/xv6/book-riscv-rev3.pdf写前思考我们都知道,在用户进程执行系统调用时,内核代替用户进程执行,那么这种代替是一种什么样的代替。这个过程是还属于用户进程,只是执......
  • mit6.824lab2D-Debug记录
    1.死锁要提交快照的时候由于没有人取走applyCh通道里面的东西,导致死锁。具体解释:2D的测试代码中在日志达到一定大小时会调用snapshot,该函数需要申请rf.mu这个互斥锁。而在提交普通的日志条目时,错误地没有先释放锁,导致snapshot无法进行下去,相关的进程卡在rf.mu这个锁上,无法完成......
  • 详解torch The “freeze_support()” line can be omitted if the programis not goin
    详解torchThe“freeze_support()”linecanbeomittediftheprogramisnotgoingtobefrozentoproduce在使用torch进行多进程编程时,我们可能会遇到一行代码freeze_support()。这行代码通常在Windows操作系统下使用,用于确保在运行多进程之前对Python解释器进行初始化。然......
  • P9612 [CERC2019] Light Emitting Hindenburg 题解
    题目传送门题目大意这个题目简化一下就是求\(n\)个数中取\(k\)个数按位与的最大值思路很容易想到贪心。题中说道输入的数在二进制下最多\(29\)位,所以我们从\(29\)开始遍历二进制位,如果当前位有大于等于\(k\)个\(1\),那么标记一下这些数,可以发现剩下的比当前位低的......
  • MIT 6.1810 Lab: page tables
    lab网址:https://pdos.csail.mit.edu/6.828/2022/labs/pgtbl.htmlxv6Book:https://pdos.csail.mit.edu/6.828/2022/xv6/book-riscv-rev3.pdfBooklearningXv6使用Sv39RISC-V标准,即使用39位的虚拟地址。前27位作为页表项索引,后12位作为页内偏移。页表项记录一个44位的实页号......