process与sleep/wakeup机制详解
- 进程控制块:struct proc
在 xv6 中,每一个进程(包括内核中运行的特殊进程和所有用户进程)都由一个 struct proc 结构体来描述。这个结构体就是我们常说的进程控制块 (Process Control Block, PCB)。它包含了操作系统管理一个进程所需的所有信息。
你可以在 kernel/proc.h 中找到它的定义:
// kernel/proc.h
// Per-process state
struct proc {
struct spinlock lock;
// p->lock must be held when using these:
enum procstate state; // Process state (核心)
void *chan; // If non-zero, sleeping on chan (核心)
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID
// wait_lock must be held when using this:
struct proc *parent; // Parent process
// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
};
我们重点关注与你问题最相关的两个字段:
enum procstate state;: 这个字段表示进程当前所处的状态。xv6 中的状态有:- UNUSED: 这个 proc 结构体是空闲的,可以被新进程使用。
- USED: 结构体已被分配,正在初始化中。
- SLEEPING: 进程正在睡眠,等待某个事件发生。
- RUNNABLE: 进程已就绪,可以运行,正在等待 CPU 时间。
- RUNNING: 进程当前正在某个 CPU 上执行。
- ZOMBIE: 进程已执行完毕,但其父进程还未调用 wait() 来回收它。
void *chan;: 这个字段就是解开你疑惑的钥匙。- 它的作用是:当且仅当进程的
state是SLEEPING时,这个字段会保存它正在等待的那个“等待频道 (Wait Channel)”的地址。 - 如果进程不是 SLEEPING 状态,这个字段的值是 0。
- 它的作用是:当且仅当进程的
- sleep(void *chan, …) 函数的内部工作流程
现在,我们把 struct proc 和 sleep 函数联系起来。当你调用 sleep(some_channel, …) 时,内核会执行以下操作:
// kernel/proc.c
void
sleep(void *chan, struct spinlock *lk)
{
struct proc *p = myproc(); // 1. 获取当前正在运行的进程的 proc 结构体指针
// ...
// Go to sleep.
p->chan = chan; // 2. 将传入的频道地址存入进程自己的 chan 字段!
p->state = SLEEPING; // 3. 将进程的状态设置为 SLEEPING
sched(); // 4. 调用调度器,放弃 CPU
// Tidy up.
p->chan = 0; // 5. 当被唤醒并再次运行时,清除 chan 字段
// ...
}
内核并不关心 &cons.r 这个地址指向的内容是什么(是0还是100),它只把这个地址本身当作一个独一无二的“标签”或“ID”. (用来标记当前这个sleeping process)
- wakeup(void *chan) 的工作流程
wakeup 的实现现在应该非常清晰了:
// kernel/proc.c
void
wakeup(void *chan)
{
struct proc *p;
for(p = proc; p < &proc[NPROC]; p++) { // 1. 遍历系统中的所有 proc 结构体
if(p->state == SLEEPING && p->chan == chan) { // 2. 检查进程是否正在指定的频道上睡眠
p->state = RUNNABLE; // 3. 如果是,就唤醒它!
}
}
}
wakeup(&cons.r) 会遍历所有进程,寻找同时满足以下两个条件的进程:
- p->state 是 SLEEPING。
- p->chan 的值等于 &cons.r。
一旦找到,就将其状态改为 RUNNABLE,让它有机会重新被调度运行。
总结:一个完整的流程
让我们把所有知识点串起来,回顾 consoleread 的场景:
- Shell 进程 (P1) 调用
read():- 进入内核态,执行 consoleread()。
- 发现 cons.r == cons.w,没有数据可读。
- 调用 sleep(&cons.r, &cons.lock)。
sleep函数执行:- 获取到 P1 的 proc 结构体指针。
- 执行 P1->chan = &cons.r; (把 &cons.r 这个地址存入 P1 的 chan 字段)。
- 执行 P1->state = SLEEPING;。
- P1 进程停止运行。
- 用户按下回车:
- 中断发生,最终 consoleintr() 被调用。
- consoleintr() 发现输入了一整行,于是调用 wakeup(&cons.r)。
wakeup函数执行:- wakeup 遍历进程表,找到了 P1。
- 它检查发现 P1->state == SLEEPING 并且 P1->chan == &cons.r。条件匹配!
- 执行 P1->state = RUNNABLE;。
- 调度器运行:
- 在未来的某个时刻,调度器发现 P1 是 RUNNABLE 的,于是让它重新在 CPU 上运行。
- P1 从 sleep 函数中返回,清除自己的 p->chan 字段,重新获取锁,然后继续执行 consoleread 的后续代码,此时它就能成功读到数据了。