XV6 Lab5:Thread

Lab5:Thread

【PS:thread_switch 只需要保存被调用者保存寄存器(callee-saved registers),为什么?
caller-saved寄存器由调用的C代码保存在堆栈上(如果需要) 。
Swtch知道struct context中每个寄存器字段的偏移量。它不保存pc。相反,swtch保存了ra寄存器[1],它保存了swtch应该返回的地址。现在,swtch从新的上下文中恢复寄存器,新的上下文中保存着前一次swtch所保存的寄存器值。当swtch返回时,它返回到被恢复的ra寄存器所指向的指令,也就是新线程之前调用swtch**的指令。此外,它还会返回新线程的堆栈。
这个是因为协程切换的过程本质是一个函数调用,因此 caller-save registers 是被调用者(如 thread_a() )保存好的。

本 lab 的任务是理解并实现多线程。

线程是运行在进程上下文中的逻辑流,每个线程都有自己的上下文:唯一的整数线程ID、栈、栈指针、程序计数器、通用目的寄存器和条件码。所有运行在一个进程里的线程共享该线程的整个虚拟地址空间。

阅读指路
user/uthread.c:包含大部分用户级线程包,以及三个简单测试线程的函数。
user/uthread_switch.S:线程切换函数的汇编代码。

Uthread: switching between threads

  • 目标:为用户系统设计并实现一个方案,创建线程,在线程之间进行切换时保存/恢复寄存器。
  • 方法:修改 user/uthread.c 中的 thread_create()
    thread_schedule(),以及 user/uthread_switch.S 中的 thread_switch

修改 struct thread 以保存寄存器;
thread_schedule() 中添加对 thread_switch 的调用,该函数在 uthread_switch.S 中实现;
thread_switch() 保存和还原唤醒线程和被唤醒线程的上下文

主要工作:

首先,在 user/uthread.c 定义线程上下文的数据结构,并在 thread 结构体中加入上下文信息
(此处 struct thread_context 的定义与 kernel/proc.h 里面的进程上下文定义 struct context 是一样的,完全复制)

// [user/uthread.c]
struct thread_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 thread_context context;
};

thread_create():
新建线程要设置相关的上下文context
要求thread_schedule()在运行刚才加入的切换代码后,自动开始运行对应函数。
这就需要在thread_create()初始化线程的时候,把它保存的ra寄存器的值赋成对应函数的入口地址,这样在切换结束后运行汇编代码中的ret就自动跳转到ra指向的位置了;另一个需要初始化的是sp寄存器,因为每个线程都各自有一个栈,所以要让自己的sp寄存器指向自己的栈底,由于栈地址从高向低增长,所以sp寄存器赋为栈数组的最高地址。
ra 寄存器指向线程要运行的函数,switch 结束后,调度到CPU的线程从 ra 处开始运行】
sp 指向线程自己的栈(要注意:压栈时栈指针减小STACK_SIZE,所以一开始在最高处)】

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;// 执行完thread_switch()后会返回到这里
  t->context.sp = (uint64)(t->stack + STACK_SIZE);// 栈从高地址往低地址增长
}

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);
  }
// 【current_thread为公共变量,是指向当前线程的指针】
  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;
}

user/uthread_switch.S
kernel/swtch.S 是进程上下文切换的功能,可以完全复制】

/*	uthread_switch.S	*/	

	.text

	/*
         * save the old thread's registers,
         * restore the new thread's registers.
         */

	.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

目标:利用多线程和锁在哈希表上实现并行编程。
notxv6/ph.c

介绍:
用 UNIX 的 pthread 线程库;
notxv6/ph.c 是对哈希表的操作,单线程时正确,多线程有待完成;
可执行文件./ph后面的参数表示线程数(./ph k 即为k个线程);

ph 运行两个benchmark:首先,调用 put() 将大量 key-value 添加到哈希表中,并打印每秒 put 的次数;然后,调用 get() 从哈希表中获取 key 对应的 value,打印出由于 put 并发而丢失的数字,并打印每秒 get 的次数。

方法:a lock per hash bucket

首先熟悉一下ph.c中定义的数据结构(哈希表)

#define NBUCKET 5
#define NKEYS 100000

struct entry {  // element
  int key;
  int value;
  struct entry *next;
};
struct entry *table[NBUCKET];   // 5(NBUCKET) queues of entries
int keys[NKEYS];
int nthread = 1;	// 线程数(由命令行参数传入)

为每一个 bucket 定义一个锁:

pthread_mutex_t lock[NBUCKET];  // 【one lock for each bucket.(according to Hints)】

在main()函数中创建线程前给每一个 bucket 的锁初始化 :

  for (int i = 0; i < NBUCKET; i++){  //【initialize locks】
    pthread_mutex_init(&lock[i], NULL);
  }

创建线程先后执行 put_thrread()get_thread(),分别调用了 put()get()
put() 函数在 insert 操作前后必须保证互斥性,加锁】
get() 函数本身不写入,每个线程各自遍历bucket中的元素,不需要互斥性】

static 
void put(int key, int value)
{
  int i = key % NBUCKET;  // key除以5的余数i决定了插入到table[i]中(每个table中的entry的key对5同余)
  
  // is the key already present?
  struct entry *e = 0;
  for (e = table[i]; e != 0; e = e->next) { // 遍历应该插入的table[i]
    if (e->key == key)  // 找到相同的key已经存在, break
      break;
  }
  
  if(e){  // 存在相同的key(e != 0)
    // update the existing key.
    e->value = value;
  
  } else {  // 不存在相同的key(e==0), 插入新的entry包含传入的参数(key, value)
    // the new is new.
    pthread_mutex_lock(&lock[i]); //【对第i个bucket的操作上锁】
    insert(key, value, &table[i], table[i]);
    pthread_mutex_unlock(&lock[i]); //【对第i个bucket的操作解锁】
  }
}

Barrier

  • 目标:实现多线程的同步(barrier),即在应用程序的一个点,所有线程都必须等待,直到所有其他线程到达此处

notxv6/barrier.c

  • 介绍:
    每个线程执行一个循环。在每次循环迭代中,每个线程调用 barrier(),然后休眠随机一段时间。目标是每个线程都阻塞在 barrier(),直到所有线程都调用了 barrier()
  • 方法:
    用到以下两个pthread条件变量的函数:
    pthread_cond_wait(&cond,&mutex):
    在cond上睡眠,释放锁mutex,醒来时重新占有锁mutex
    pthread_cond_broadcast(&cond):
    唤醒所有睡眠在cond上的线程

Hints:

  • 处理一系列 barrier() 调用,每次调用称为一轮,bstate.round 记录当前调用的轮数;当所有线程都到达barrier() 时,增加它们的调用轮数,保持一致。

分析:

main函数的主要部分,创建多线程并调用 thread() 函数:

  for(i = 0; i < nthread; i++) {
    assert(pthread_create(&tha[i], NULL, thread, (void *) i) == 0);
  }
  for(i = 0; i < nthread; i++) {
    assert(pthread_join(tha[i], &value) == 0);
  }

thread() 循环调用barrier():

static void *
thread(void *xa)
{
  long n = (long) xa;	// thread number
  long delay;
  int i;

  for (i = 0; i < 20000; i++) {
    int t = bstate.round;
    assert (i == t);	// 保证当前轮数正确
    barrier();	// 调用barrier()
    usleep(random() % 100);	// 随机休眠一段时间
  }

  return 0;
}

定义的共享变量:

static int nthread = 1;
static int round = 0;

struct barrier {
  pthread_mutex_t barrier_mutex;
  pthread_cond_t barrier_cond;
  int nthread;      // 【Number of threads that have reached this round of the barrier】
  int round;     // 【Barrier round】
} bstate;

主要工作:

static void 
barrier()
{
  // YOUR CODE HERE
  //
  // Block until all threads have called barrier() and
  // then increment bstate.round.
  //
  pthread_mutex_lock(&bstate.barrier_mutex);  // 上锁

  bstate.nthread++;
  if(bstate.nthread == nthread) {
  	// 所有线程到达这一步 【最后一个到达的线程进入, 负责唤醒所有前面的线程】
    bstate.round++;
    bstate.nthread = 0;
    pthread_cond_broadcast(&bstate.barrier_cond); // 唤醒所有线程
  } else {   // 需要等待 【非最后到达的线程进入】
    // 【必须在上锁状态下调用】: 释放锁, 阻塞当前线程, 等待被唤醒, 返回时重新上锁 
    pthread_cond_wait(&bstate.barrier_cond, &bstate.barrier_mutex);
    // (不过可以看到接下来就会解锁, 所有线程会一个一个退出)
  }
  
  pthread_mutex_unlock(&bstate.barrier_mutex);  // 解锁
}
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: :xv6是一个基于Unix的操作系统,它是一个教学用途的操作系统,旨在教授操作系统的基本概念和实现。它是在MIT的x86架构上开发的,包括了Unix的一些基本功能,如进程管理、文件系统、内存管理等。xv6的源代码是公开的,可以用于学习和研究。 Unix utilities是Unix操作系统中的一些基本工具,如ls、cd、cp、mv、rm等。这些工具可以帮助用户管理文件和目录,执行各种操作。这些工具的实现是基于Unix的系统调用,可以通过编写C程序来调用这些系统调用实现相应的功能。这些工具是Unix操作系统的基础,也是其他操作系统的参考。 ### 回答2: lab: xv6 and unix utilities 实验是一项旨在帮助学生深入理解操作系统和 Unix 工具使用的实验。该实验分为两个部分,第一部分教授学生如何构建和运行 xv6 操作系统;第二部分则重点教授 Unix 工具的使用。 在 xv6 操作系统部分,学生将学习到操作系统内核的基本结构和实现原理。实验将引导学生理解内存管理、进程调度、系统调用等关键操作系统概念。此外,学生还将学习如何编写简单的 shell 以及如何通过修改 xv6 内核代码来实现新的系统调用和功能。 在 Unix 工具部分,学生将探索 Unix 系统中广泛使用的常见工具。这些工具包括 vi 编辑器、grep、awk、sed 等。实验将介绍这些工具的基本使用方法以及它们在处理文本和数据时的实际应用。这部分实验还将让学生深入了解 shell 和 shell 脚本的编写,帮助他们在 Unix 环境中轻松地编写脚本和自动化任务。 lab: xv6 and unix utilities 实验对计算机科学专业的学生具有重要意义。通过完成这个实验,学生将建立起对操作系统和 Unix 工具的深入理解,为他们成为一名优秀的软件工程师奠定坚实的基础。同时,这个实验还将为学生提供实践经验,让他们能够将所学知识应用到真实的软件开发和运维中。 ### 回答3: Lab: xv6 and Unix Utilities是一个计算机科学领域的实验,旨在让学生深入了解Unix操作系统以及操作系统本身的自我管理机制。在这个实验中,学生需要从零开始构建一个类似于Unix的操作系统,在这个操作系统中,学生需要设计一些基本命令,例如ls,cat,grep等等,并且将它们与系统的底层API结合起来,以实现各种功能。此外,学生还需要了解和探索xv6这个开发工具,它是一个轻量级基于Unix的操作系统实现,具有一定的可移植性和简洁性,因此,它可以作为一个基础框架来实现一个完整的Unix操作系统。 这个实验的目标是让学生了解Unix的基本命令结构和API,以及操作系统内部的一些基本机制,例如进程管理,文件系统交互以及进程通信等等。此外,通过实现这些命令,学生还可以学到一些基本的C语言编程技能,例如文件操作,字符串处理以及进程管理等等。还可以学习到如何使用Git等版本控制工具,以及如何进行调试和测试代码的技巧。 在整个实验过程中,学生需要有较强的自我管理能力和综合运用能力,因为在实现这些命令的同时,他们还需要和其他团队成员进行交流和合作,以及不断改进和完善他们的代码。总之,这个实验是一个非常有趣且富有挑战性的计算机科学课程,通过完成这个实验,学生可以更好地了解操作系统的构造和运作机制,以及如何设计和开发高效的系统级应用程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值