process与sleep/wakeup机制详解

  1. 进程控制块: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;: 这个字段就是解开你疑惑的钥匙。
    • 它的作用是:当且仅当进程的 stateSLEEPING 时,这个字段会保存它正在等待的那个“等待频道 (Wait Channel)”的地址。
    • 如果进程不是 SLEEPING 状态,这个字段的值是 0。
  1. 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)

  1. 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) 会遍历所有进程,寻找同时满足以下两个条件的进程:

  1. p->state 是 SLEEPING。
  2. p->chan 的值等于 &cons.r。

一旦找到,就将其状态改为 RUNNABLE,让它有机会重新被调度运行。

总结:一个完整的流程

让我们把所有知识点串起来,回顾 consoleread 的场景:

  1. Shell 进程 (P1) 调用 read():
    • 进入内核态,执行 consoleread()。
    • 发现 cons.r == cons.w,没有数据可读。
    • 调用 sleep(&cons.r, &cons.lock)。
  2. sleep 函数执行:
    • 获取到 P1 的 proc 结构体指针。
    • 执行 P1->chan = &cons.r; (把 &cons.r 这个地址存入 P1 的 chan 字段)。
    • 执行 P1->state = SLEEPING;。
    • P1 进程停止运行。
  3. 用户按下回车:
    • 中断发生,最终 consoleintr() 被调用。
    • consoleintr() 发现输入了一整行,于是调用 wakeup(&cons.r)。
  4. wakeup 函数执行:
    • wakeup 遍历进程表,找到了 P1。
    • 它检查发现 P1->state == SLEEPING 并且 P1->chan == &cons.r。条件匹配!
    • 执行 P1->state = RUNNABLE;。
  5. 调度器运行:
    • 在未来的某个时刻,调度器发现 P1 是 RUNNABLE 的,于是让它重新在 CPU 上运行。
    • P1 从 sleep 函数中返回,清除自己的 p->chan 字段,重新获取锁,然后继续执行 consoleread 的后续代码,此时它就能成功读到数据了。