chapter 7
发生进程切换的时机:
1.等待pipe或者设备I/O
2.等待子进程退出
3.调用sleep syscall
4.进程运行时间过长
进程切换时需要保存的当前运行的进程的寄存器和恢复即将运行的进程的寄存器
当一个进程想要放弃cpu时需要获取进程锁,更新自身的state,然后调用sched,sched调用swtch保存进程的上下文并切换到scheduler的上下文(cpu->scheduler)。在swtch的过程中process必须要获取锁,否则有可能和其他cpu发生争抢。
Uthread: switching between threads
这个exercise需要实现一个用户级的线程机制,使用该pacakage中的函数使用线程在发生线程切换时不涉及kernel,总体上就是把xv6线程切换的那一套实现搬到user mode。
1.为thread结构体添加context成员,context与proc.h中的context一致
struct thread_context {
uint64 ra; // hold the return address
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;
};
2.实现thread_switch函数,thread_switch函数在uthread_switch.S中实现,逻辑与swtch函数一致
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 */
3.在thread_schedule中添加线程切换的函数
void
thread_schedule(void)
{
....
/* YOUR CODE HERE
* Invoke thread_switch to switch from t to next_thread:
* thread_switch(??, ??);
*/
thread_switch((uint64)&t->context, (uint64)¤t_thread->context);
....
}
4.在thread_create中添加context的初始化,将ra设置为func,将sp设置为t对应的栈顶
void
thread_create(void (*func)())
{
....
// YOUR CODE HERE
t->context.ra = (uint64)func;
t->context.sp = (uint64)t->stack + STACK_SIZE;
}
Using threads
这个exercise的不在xv6里面运行,在Linux上直接运行。在notxv6/ph.c中实现了一个hash table,当使用多个线程运行时会发生key missing的现象,现在要通过pthread中提供的锁来解决这个问题。
显然,key missing发生在插入新key的过程中。在ph.c的hash table中根据key划分kv对到不同的BUCKET中,每个BUCKET都是一个链表,当插入key时如果key已经存在那么更新对应节点的值,如果key不存在则新分配一个节点使用头插法插入到对应的BUCKET中。key missing发在多个线程在同一个BUCKET中同时insert的情况中,如线程1插入了新节点到BUCKET中,并更新BUCKET的头指针为最新的节点,如果线程2在insert时传入的n与线程1在insert时传入的n相同,那么线程1插入的节点就会丢失掉。
解决方法为为每个BUCKET都提供一把锁,调用insert()前获取锁,insert()结束后释放锁
1.声明mutex
pthread_mutex_t mtx[NBUCKET];
2.put时在调用insert()前获取锁,insert()结束后释放锁
static
void put(int key, int value)
{
....
} else {
// the new is new.
pthread_mutex_lock(&mtx[i]);
insert(key, value, &table[i], table[i]);
pthread_mutex_unlock(&mtx[i]);
}
}
3.在main()函数中初始化phread_mutex_t
int
main(int argc, char *argv[])
{
....
for (int i = 0; i < NBUCKET; i++)
pthread_mutex_init(&mtx[i], NULL);
....
Barrier
这个exercise的主要工作是使用pthread_cond_wait()和pthread_cond_broadcast()来对barrier()函数的内容进行补充,这个exercise的程序同样是运行在Linux系统上。
pthread_cond_wait和pthread_cond_broadcast()的功能如下:
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
我们需要对barrier进行实现,使得thread()函数中的断言assert(i == t)可以通过,这就要求在多个线程运行thread()函数的过程中,线程获取到相同的bstate.round后,在barrier中阻塞直到所有的barrier都获取到bstate.round后,对所有线程进行唤醒再进行下一次循环。
在barrier()函数中对bstate.nthread进行递增,对当前round已调用过barrier线程数量进行统计,当bstate.nthread少于运行的线程数量时,调用pthread_mutex_wait进行进行等待并释放mutex。当bstate.ntrhead等于运行的线程数量时,将bstate.nthread置为0开始统计下一轮已经调用过barrier函数的线程数量,更新运行轮次,并使用pthread_cond_broadcast()唤醒所有在等待的线程。
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.nthread = 0;
bstate.round++;
pthread_cond_broadcast(&bstate.barrier_cond);
}
else {
pthread_cond_wait(&bstate.barrier_cond, &bstate.barrier_mutex);
}
pthread_mutex_unlock(&bstate.barrier_mutex);
}