写在前面
进程调度:进程由于需要进行磁盘读写或者时钟中断等原因需要让出CPU,此时操作系统会选择可运行的其他的进程上CPU运行。进程调度的过程首先是进程通过系统调用等从用户态进入到内核态,这个过程会将用户态的现场保存到trapframe中,然后通过swtch()进行上下文切换,将进程的上下文保存至KSTACK中,并恢复调度器线程的上下文,之后由调度器线程获取可以运行的其他的线程,再进行一次上下文切换,再恢复该线程的用户态现场,即完成了一次进程调度。
锁与临界区:对于一个具有多核的机器,可以同时运行多个线程。如果多个线程需要对某个共享变量进行读写操作时,由于并发性的存在,可能会导致读写的丢失和出错。为了避免这种问题,需要用锁让某些代码在某个时刻只允许一个线程执行,用锁包围起来的代码称为临界区,其余的线程必须等待临界区的代码执行完后才能运行临界区的代码,从而实现了线程间的互斥。
信号量与同步:信号量由一个计数器和两个操作(P和V)组成,用于生产者和消费者问题,V操作会让计数器值加1,而P操作则会让计数器值减一。为了实现P、V操作,引入了sleep和wakeup。sleep是一个条件锁,对于未满足条件的线程会被阻塞,同时释放该线程互斥锁,而wakeup则是唤醒在某个条件下被阻塞的线程,当线程被唤醒后需要重新获取互斥锁。这种机制确保了线程间同步问题。
实验部分
一 Uthread: switching between threads
该实验主要是为了实现用户线程间的切换,实现用户线程的上下文切换。
1 在user/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 */
2 在user/uthread.c中,在struct thread中添加context的数组,用于存储上下文。并在初始化线程时,将ra初始化为func的函数地址,将sp初始化为线程对应的栈顶指针。
//添加上下文变量
struct thread {
char stack[STACK_SIZE]; /* the thread's stack */
int state; /* FREE, RUNNING, RUNNABLE */
uint64 context[14];
};
//上下文切换
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)next_thread->context);
}
//线程的初始化
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[0]=(uint64)func;
t->context[1]=(uint64)t->stack+STACK_SIZE;
}
二 Using threads
该实验是为了实现多个线程向哈希表中添加关键字,若对应关键字存在则需要对值进行修改,若关键字不存在则需要添加关键字和值到哈希表中。为了避免两个线程同时对一个关键字值进行修改,需要对每一个entry添加一个锁,从而保证了只有一个线程能够修改entry的值;为了避免两个线程同时添加entry到哈希表中,从而导致其中一个entry丢失,insert操作需要原子化。具体代码实现如下:
//声明一个hash锁,为每个entry添加锁
pthread_mutex_t hashlock;
struct entry {
int key;
int value;
struct entry *next;
pthread_mutex_t lock;
};
//初始化entry锁
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;
pthread_mutex_init(&e->lock,NULL);
e->next = n;
*p = e;
}
//main中初始化hash锁
pthread_mutex_init(&hashlock,NULL);
nthread = atoi(argv[1]);
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;
}
if(e){
// update the existing key.
pthread_mutex_lock(&e->lock);
e->value = value;
pthread_mutex_unlock(&e->lock);
} else {
// the new is new.
pthread_mutex_lock(&hashlock);
insert(key, value, &table[i], table[i]);
pthread_mutex_unlock(&hashlock);
}
}
三 Barrier
该实验是为了实现一个barrier,使线程阻塞在barrier前,直到所有线程都抵达barrier后,才能执行下一轮。为了确保每一个线程阻塞在barrier前,需要使用条件锁,每个线程抵达barrier后需要对bstate.nthread++,如果bstate.nthread不等于nthread,那么说明最后一个线程还未抵达barrier,需要将线程阻塞,当最后一个线程抵达时,再唤醒所有被阻塞的线程,出barrier要将bstate.nthread--。另外当某个线程完成了一轮循环,进入到下一轮循环时,由于其他线程还没有出barrier,此时bstate.nthread还属于上一轮,因此线程需要进行阻塞。
//定义一个条件锁
pthread_cond_t round_lock;
static void
barrier()
{
// YOUR CODE HERE
//
// Block until all threads have called barrier() and
// then increment bstate.round.
//
pthread_mutex_lock(&bstate.barrier_mutex);
if(bstate.round!=round) pthread_cond_wait(&round_lock,&bstate.barrier_mutex);
bstate.nthread++;
if(bstate.nthread!=nthread)
pthread_cond_wait(&bstate.barrier_cond,&bstate.barrier_mutex);
else{
pthread_cond_broadcast(&bstate.barrier_cond);
bstate.round++;
}
bstate.nthread--;
if(bstate.nthread==0){
round++;
pthread_cond_broadcast(&round_lock);
}
pthread_mutex_unlock(&bstate.barrier_mutex);
}