操作系统 MIT6.S081 Lab5 Multithreads

操作系统 MIT6.S081 Lab5 Multithreads

实验原理

① Xv6 通过两种方式实现多路复用:

  • 当一个进程带等待外设完成动作、等待一个子进程终结或者处于 sleep 时,Xv6 会将其从 wakeup 状态切换为 sleep 状态
  • Xv6 定期切换一些占用 CPU 时间的进程的 sleep/wakeup 状态

② 具体实现:

  • Xv6 的每个 CPU 都有一个专门用于进程切换的调度器线程(运行函数 scheduler),因为在老进程的内核栈上做进程切换是不安全的;通过 shedsheculer 的协程实现这个过程

  • Xv6 通过函数 swtch 来实现进程上下文的保存和切换

③ 进程切换的过程:
请添加图片描述

  • 老的用户进程进程由于中断、系统调用等进入 trap,执行该进程对应的内核线程
  • 内核线程认为应当 yeild 出 CPU,然后调用函数 shed ,并通过 swtch 函数保存上下文、切换到调度器线程
  • 调度器通过一个循环找出另一个可以运行的新进程,再通过 swtch 函数恢复上下文、切换到它对应的内核线程,再由内核线程返回到用户进程

Part 1 Uthread: switching between threads

实验目的: 为用户级线程系统设计并实现上下文切换机制

实验步骤:

① 给 struct thread 添加新的成员 struct context ,用于保存 PC、栈指针和 callee-saved 寄存器:

// kernel/uthread.c
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;       // pc, sp and callee-saved register
};

② 实现函数 swtch ,用于切换进程上下文:由于 struct context 在内存中是连续排布的,每个寄存器占 8B ,且栈向低地址方向增长,所以各个寄存器对应的位置分别是基址 + 8,+16 …

/* kernel/uthread_switch.S */
	.text
	/*
         * save the old thread's registers,
         * restore the new thread's registers.
         */
	.globl thread_switch
thread_switch:
	/* YOUR CODE HERE */
	/* TODO */
	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 */

③ 在 schedule 函数里调用 swtch 函数,切换上下文:

// kernel/uthread.c
void 
thread_schedule(void)
{
  ...
  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((uint64)(&(t->context)), (uint64)(&(current_thread->context)));
  } else
    next_thread = 0;
}

④ 补全 thread_create() :创建线程时,要设置其入口地址和栈的起始位置;由于栈式向低地址方向增长,因此要取栈的最高位作为栈底:

// kernel/uthread.c
void 
thread_create(void (*func)())
{
  ...
  t->context.ra = (uint64)func;
  t->context.sp = (uint64)(t->stack + STACK_SIZE);
}

实验结果:

① 执行 uthread

请添加图片描述
请添加图片描述

make grade 的 uthread 部分:

请添加图片描述

遇到问题

proc.h 中已经有 struct context ,本来想直接 #include "kernel/proc.h" 来引用 struct context,但是出现了其他错误:
请添加图片描述

proc.h 和其他文件还有依赖关系,比较复杂,所以最终直接在 uthread.c 中重新定义了一遍 struct context

② 调用 thread_swtch 时没有注意到保存进程的变量已经发生交换,原来的 current_thread 变成了 t ,现在的 current_thread 是原来的 new_thread ,结果写成了:

// kernel/uthread.c thread_schduler()
thread_switch((uint64)(&(current_thread->context)), (uint64)(&(new_thread->context)));

问题回答thread_switch 只需要保存/恢复 callee-save registers,思考下为什么

因为调用者需要保存的寄存器,在调用 swtch 函数之前就已经被压入栈了,并不会受到寄存器的影响

Part 2 Using threads

实验目的: 为哈希表加锁,防止 race condition 造成的损失

实验步骤:

① 为每个桶都创建锁:

// notxv6/ph.c
#include <pthread.h>
pthread_mutex_t bucket_locks[NBUCKET];
int
main(int argc, char *argv[])
{
  ...
  // initialize locks
  for (int i = 0; i < NBUCKET; ++i) {
    pthread_mutex_init(&bucket_locks[i], NULL);
  }
  ...
}

② 在 put() 函数插入 key 前后加上锁:

// notxv6/ph.c
static 
void put(int key, int value)
{
  ...
    pthread_mutex_lock(&bucket_locks[i]);
    insert(key, value, &table[i], table[i]);
    pthread_mutex_unlock(&bucket_locks[i]);
  ...
}

实验结果:

① 没有加锁之前,多线程的情况下会出现某些 key 添加失败的情况:

请添加图片描述

② 加锁之后,即使是多线程也不会出现 key 添加失败的情况:
请添加图片描述

并且能够通过 make gradeph_test 部分:
请添加图片描述

问题回答:为什么两个线程会丢失 keys,但是一个线程不会?确定一种两个线程
的执行序列,可以使得 key 丢失

当两个线程同时向同一个桶添加 key 时,有可能会出现 race condition,使得某个 key 丢失;向哈希表中插入结点分为两步:e->next = (*p)->next(*p)->next = e ;例如,当线程 1 和 2 同时向桶 *p 插入 key 时,线程 1 执行了 e1->next = (*p)->next 后被调度走,接着线程 2 执行 e2->next = (*p)->next(*p)->next = e2 ,最后调度回线程 1 完成 (*p)->next = e1 ,此时e2 结点就被丢失了

Part 3 Barrier

实验目的: 实现一个 barrier:当一个线程到这个点后,必须等待其余所有线程都到达这点

实验步骤:barrier.c 中添加以下代码:当线程进入 barrier() 时,先对 bstate 上锁,接着增加进入 barrier() 的线程个数;当发现所有线程都进入时,就广播唤醒所有线程,否则自己也进入睡眠

// notxv6/barrier.c
static void 
barrier()
{
  pthread_mutex_lock(&(bstate.barrier_mutex));
  ++bstate.nthread;
  if (bstate.nthread == nthread) {
    bstate.nthread = 0;
    ++bstate.round;
    pthread_cond_broadcast(&(bstate.barrier_cond));
    pthread_mutex_unlock(&(bstate.barrier_mutex));
  } else {
    pthread_cond_wait(&(bstate.barrier_cond), &(bstate.barrier_mutex));
  }
  pthread_mutex_unlock(&(bstate.barrier_mutex));
}

实验结果:

① 可以通过 barrier 测试:
请添加图片描述

并且能够通过 make gradeph_test 部分:
请添加图片描述
请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Air浩瀚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值