全网最最实用--基于Mac ARM 芯片实现操作系统MIT 6.S081-lab7

实验七 多线程

一、代码理解

在编写代码之前,您应该确保您已经阅读了xv6书中的“第7章:调度”并研究了相应的代码。
阅读第 7.4 节“调度”以及kernel/proc.c、kernel/swtch.S

1.swtch.S

sd 是 Store Double 指令,用于将寄存器的值存储到内存中。
ra 是返回地址寄存器,存储了函数返回时要跳转的地址。
sp 是栈指针寄存器,指向当前栈的顶部。
s0 到 s11 是保存寄存器(saved registers),用于存储函数调用过程中需要保留的值。
a0 是函数的第一个参数,指向当前上下文的结构体 struct context。

ld 是 Load Double 指令,用于从内存中加载值到寄存器。
a1 是函数的第二个参数,指向新上下文的结构体 struct context。

swtch 函数的作用是在两个上下文之间进行切换。它首先将当前上下文的寄存器值保存到 old 指向的内存区域,然后将新上下文的寄存器值从 new 指向的内存区域加载到寄存器中,最后返回到新上下文的执行位置。这个过程实现了从一个线程或进程到另一个线程或进程的切换。

2. 线程调度 thread_schedule(void)

void 
thread_schedule(void)
{
  struct thread *t, *next_thread;

 
  /* 寻找另一个可运行的线程 */
  next_thread = 0;
  t = current_thread + 1;
  for(int i = 0; i < MAX_THREAD; i++){
  //循环遍历 all_thread 数组,从 current_thread 的下一个线程开始,查找状态为 RUNNABLE 的线程。
    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) {
  //如果当前线程和下一个线程不同,说明需要进行线程切换。
    next_thread->state = RUNNING;
    t = current_thread;
    current_thread = next_thread;
 
    /* 调用 thread_switch 从 t 切换到 next_thread: 
     * thread_switch(??, ??);
     * 表示需要编写代码来实际调用上下文切换的函数 thread_switch。你需要传递当前线程和下一个线程的上下文信息。
     */
  } else
    next_thread = 0;
}

3.线程切换thread_create(void (*func)())

void 
thread_create(void (*func)())
{
  struct thread *t;

  // 遍历线程列表以找到一个空闲(FREE)状态的线程
  for (t = all_thread; t < all_thread + MAX_THREAD; t++) {
    // 如果找到一个线程的状态为FREE,即未被使用的线程,则停止循环
    if (t->state == FREE) break;
  }
  
  // 将找到的空闲线程设置为可运行(RUNNABLE)状态
  t->state = RUNNABLE;
  
  // 这里是一个待填充的代码块,需要添加代码来初始化线程栈和设置线程的执行上下文
  // 通常包括设置线程的程序计数器(PC)为提供的函数指针,以及其他相关的寄存器和栈信息
  // YOUR CODE HERE
}

4.模拟线程

它利用a,b,c三个线程来演示,对于每个线程来说,逻辑和a线程一样。

thread_a 函数的主要作用是模拟一个简单的线程行为。
、线程 A 启动后,先等待线程 B 和 C 启动,然后进入一个循环执行 100 次,每次执行时打印当前循环计数值,并更新计数变量。
每次循环结束后,线程 A 都会让出 CPU 使用权,让其他线程有机会运行。
循环完成后,线程 A 打印退出信息,并将自身状态设置为 FREE,最后调用调度函数 thread_schedule 切换到下一个可运行的线程。

二、Uthread: switching between threads

1.题目描述

在本练习中,您将设计用户级线程系统的上下文切换机制,然后实现它。为了帮助您入门,您的 xv6 有两个文件 user/uthread.c 和 user/uthread_switch.S,以及 Makefile 中用于构建 uthread 程序的规则。uthread.c 包含用户级线程包的大部分内容,以及三个简单测试线程的代码。线程包缺少一些用于创建线程和在线程之间切换的代码。补全uthread.c,完成用户态线程功能的实现。

2.解答

在这个练习中,我们需要实现用户级线程系统的上下文切换机制。我们的目标是完成 thread_create()thread_schedule() 函数,并实现 thread_switch() 函数,以便在线程之间进行上下文切换。以下是完成这个任务的步骤:

a. 修改 struct thread

为了保存和恢复线程的寄存器状态,我们需要在 struct thread 中添加一个数组来保存寄存器。

user/uthread.c 中:

struct thread {
  char stack[STACK_SIZE]; // 线程的栈
  int state;              // 线程的状态
  uint64_t registers[32]; // 寄存器保存区
};
b. 实现 thread_create()

thread_create() 中,我们需要初始化线程的栈和寄存器,并设置线程的入口函数。

user/uthread.c 中:

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;
  t->registers[2] = (uint64_t)(t->stack + STACK_SIZE); // 设置栈指针 (sp)
  t->registers[1] = (uint64_t)func; // 设置返回地址 (ra) 为入口函数
}
c. 实现 thread_schedule()

thread_schedule() 中,我们需要找到一个可运行的线程,并调用 thread_switch() 进行上下文切换。

user/uthread.c 中:

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;
    thread_switch(&t->registers, &next_thread->registers);
  } else {
    next_thread = 0;
  }
}
d. 实现 thread_switch()

thread_switch() 用于保存当前线程的寄存器状态,并恢复下一个线程的寄存器状态。

user/uthread_switch.S 中:

// 保存当前线程的寄存器,恢复下一个线程的寄存器
.global thread_switch
thread_switch:
  // 保存当前线程的寄存器
  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

三、Using threads

1.题目描述

在此作业中,您将使用哈希表探索线程和锁的并行编程。您应该在具有多个内核的真实 Linux 或 MacOS 计算机(不是 xv6,不是 qemu)上完成此作业。大多数最新的笔记本电脑都具有多核处理器。
文件notxv6/ph.c包含一个简单的哈希表,如果从单个线程使用,则正确,但从多个线程使用时则不正确.
这个程序是一个多线程哈希表操作的示例,主要功能是使用多个线程并发地向哈希表中插入键值对,然后使用多个线程并发地从哈希表中读取键值对

2.解答

主要是给插入键值和读取键值加锁。
为了使程序能够正确地从多个线程使用,我们需要确保对共享数据结构的访问是线程安全的。在这个程序中,table 以及其内部的链表是共享数据结构,因此我们需要对它们的访问进行同步。我们可以使用互斥锁(mutex)来实现这一点。

a.修改 struct entry 和全局变量

我们需要为每个桶添加一个互斥锁:

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <assert.h>
#include <pthread.h>
#include <sys/time.h>

#define NBUCKET 5
#define NKEYS 100000

struct entry {
  int key;
  int value;
  struct entry *next;
};

struct entry *table[NBUCKET];
pthread_mutex_t locks[NBUCKET]; // 为每个桶添加一个互斥锁

int keys[NKEYS];
int nthread = 1;

double now() {
  struct timeval tv;
  gettimeofday(&tv, 0);
  return tv.tv_sec + tv.tv_usec / 1000000.0;
}
b.初始化互斥锁

main 函数中,我们需要初始化这些互斥锁:

int main(int argc, char *argv[]) {
  pthread_t *tha;
  void *value;
  double t1, t0;

  if (argc < 2) {
    fprintf(stderr, "Usage: %s nthreads\n", argv[0]);
    exit(-1);
  }
  nthread = atoi(argv[1]);
  tha = malloc(sizeof(pthread_t) * nthread);
  srandom(0);
  assert(NKEYS % nthread == 0);
  for (int i = 0; i < NKEYS; i++) {
    keys[i] = random();
  }

  // 初始化互斥锁
  for (int i = 0; i < NBUCKET; i++) {
    pthread_mutex_init(&locks[i], NULL);
  }

  // first the puts
  t0 = now();
  for (int i = 0; i < nthread; i++) {
    assert(pthread_create(&tha[i], NULL, put_thread, (void *) (long) i) == 0);
  }
  for (int i = 0; i < nthread; i++) {
    assert(pthread_join(tha[i], &value) == 0);
  }
  t1 = now();

  printf("%d puts, %.3f seconds, %.0f puts/second\n",
         NKEYS, t1 - t0, NKEYS / (t1 - t0));

  // now the gets
  t0 = now();
  for (int i = 0; i < nthread; i++) {
    assert(pthread_create(&tha[i], NULL, get_thread, (void *) (long) i) == 0);
  }
  for (int i = 0; i < nthread; i++) {
    assert(pthread_join(tha[i], &value) == 0);
  }
  t1 = now();

  printf("%d gets, %.3f seconds, %.0f gets/second\n",
         NKEYS * nthread, t1 - t0, (NKEYS * nthread) / (t1 - t0));

  // 销毁互斥锁
  for (int i = 0; i < NBUCKET; i++) {
    pthread_mutex_destroy(&locks[i]);
  }

  free(tha);
  return 0;
}
c.修改 putget 函数

putget 函数中,我们需要使用互斥锁来保护对哈希表的访问:

static void put(int key, int value) {
  int i = key % NBUCKET;

  pthread_mutex_lock(&locks[i]); // 加锁
  struct entry *e = 0;
  for (e = table[i]; e != 0; e = e->next) {
    if (e->key == key)
      break;
  }
  if (e) {
    e->value = value;
  } else {
    insert(key, value, &table[i], table[i]);
  }
  pthread_mutex_unlock(&locks[i]); // 解锁
}

static struct entry* get(int key) {
  int i = key % NBUCKET;

  pthread_mutex_lock(&locks[i]); // 加锁
  struct entry *e = 0;
  for (e = table[i]; e != 0; e = e->next) {
    if (e->key == key) break;
  }
  pthread_mutex_unlock(&locks[i]); // 解锁

  return e;
}

四、Barrier

1.题目描述

本作业中,您将实现一个屏障:应用程序中所有参与线程都必须等待的点,直到所有其他参与线程也到达该点。您将使用 pthread 条件变量,这是一种类似于 xv6 的睡眠和唤醒的序列协调技术。

你应该在真实的计算机(不是 xv6,不是 qemu)上完成这个作业。

2.解答

a.关键组件
  1. 结构体barrier: 用于存储与屏障相关的状态,包括:

    • barrier_mutex: 用于保护屏障状态的互斥锁。
    • barrier_cond: 用来在所有线程达到屏障时发送信号的条件变量。
    • nthread: 记录已到达屏障的线程数量。
    • round: 记录屏障的当前轮次。
  2. 函数barrier_init(): 初始化屏障结构体,设置互斥锁和条件变量。

  3. 函数barrier(): 实现屏障功能。当线程到达屏障时,它们需要等待,直到所有其他线程也到达屏障。一旦所有线程都到达,屏障应该让所有线程继续执行,并递增屏障的轮次。

  4. 线程函数thread(): 实现每个线程的工作逻辑。每个线程在进入下一轮循环之前,必须等待其他所有线程都完成当前轮次。

b.实现屏障函数barrier()

这个函数需要完成的工作是:

  • 确保所有线程在继续之前都到达屏障。
  • 一旦所有线程都到达屏障,让它们全部继续执行,并递增屏障的轮次。
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); // 通知所有等待的线程
  } else {
    while (bstate.round == round) {             // 当前线程等待其他线程
      pthread_cond_wait(&bstate.barrier_cond, &bstate.barrier_mutex);
    }
  }

  pthread_mutex_unlock(&bstate.barrier_mutex);  // 解锁
}

五、心得体会

我发现我更这门课的笔记很少人看。很多人问这门课到底值不值得上手。我觉得,如果你有一整个月的时间all in,那就去做,真的可以收获很多!
但是对于时间零散没有意志力的人来说,你可以把这门课当应试课来学,虽然学不到100%,但是50%也比大多数人强。
其实环境搭建好后,就没有啥门槛可以直接开始调试学习啦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值