进程的概念与PCB
进程的概念与PCB
复习定位
进程(w进程是程序的一次运行实例。一个可执行文件可以被多次执行——每执行一次就是创建一个新的进程——有自己的PID、自己的地址空间、自己的文件描述符表。进程是资源分配的最小单位——每个进程之间彼此隔离。理解task_struct(PCB)中存了什么——就理解了操作系统在管理一个进程时要跟踪的所有状态。
程序与进程的本质差异
程序是一个静态文件——存储在磁盘上的ELF可执行文件或脚本——包含代码和数据段。有不变的大小——可以放在磁盘上任意地复制。
进程是程序的一次动态执行实例。当一个程序被执行时——操作系统为其分配PID、创建task_struct、分配虚拟地址空间、加载代码段和数据段、打开默认的文件描述符(0/1/2)、设置环境变量和命令行参数。进程有自己的生命周期——创建fork()→执行execve()→调度运行→等待I/O→终止exit()。
同一个可执行文件可以运行多个进程(同时打开多个终端窗口运行ls命令——每一个都一个独立的进程实例——各有自己的PID、独立地址空间和打开的文件描述符表)。这些进程是独立的——一个ls进程崩溃不会影响另一个。
PCB(进程控制块)的内容
Linux中PCB的结构称为task_struct——定义在include/linux/sched.h——这是一个数百行的巨大结构体。一个task_struct至少包含以下关键信息:
进程标识:pid(进程ID——内核唯一的整数标识)、tgid(线程组ID——同一进程中的多个线程tgid相同而pid不同)、real_parent和parent指针(建立父子进程的继承树)。
状态信息:state(TASK_RUNNING/TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE/TASK_STOPPED/EXIT_ZOMBIE等)、exit_state(EXIT_DEAD/EXIT_ZOMBIE)——这些状态决定了该进程是否在CPU上可调度。
调度信息:prio(动态优先级)、static_prio(静态优先级)、se(调度实体——包含vruntime、rb_node等CFS调度所需字段)、policy(调度策略——SCHED_NORMAL/FIFO/RR/DEADLINE)。
内存管理:mm(指向mm_struct——包含页目录pgd、代码段开始/结束、数据段开始/结束、堆开始/结束、mmap基地址、栈起始地址等)。active_mm(内核线程使用——它们不拥有自己的地址空间)。
文件系统:fs(指向fs_struct——包含当前工作目录cwd、umask、根目录)、files(指向files_struct——包含文件描述符表fdtable——每个fd对应一个struct file*指针——一个进程默认有0/1/2指向终端——程序打开新文件时分配新的fd序号)。
信号处理:pending(等待处理的信号队列)、sighand(指向sighand_struct——包含每个信号对应的处理函数)。
进程关系:tasks(将系统中所有进程链表串联的节点)、children(子进程链表)、sibling(兄弟进程链表)parent(父进程指针)。
时间信息:start_time(进程启动时间)、real_start_time、utime(用户态运行时间)、stime(内核态运行时间)。
fork的执行过程
fork()系统调用复制当前进程创建一个子进程。复制不是把父进程所有资源全拷贝——因为fork+exec是常见模式——立即加载新程序替换地址空间——全拷贝浪费极大。Linux使用写时复制(COW):子进程从父进程继承页表(指向相同的物理页框)但将所有可写页标记为只读。当某一方要写入共享页时——触发缺页异常——内核分配新物理页框——复制数据——更新页表——双方各自拥有独立副本。
fork的返回值特殊之处在于返回两次——父进程中返回子进程的PID(正数)——子进程中返回0——执行相同代码但据此值可做父子分支处理。
pid_t pid = fork();
if (pid > 0) {
// 父进程:pid是子进程的PID
wait(NULL); // 等待子进程结束
} else if (pid == 0) {
// 子进程:fork返回0
execve("program", argv, envp);
} else {
// fork失败(如内存不足或进程数达到上限)
}进程的终止
进程终止统过exit()系统调用。内核在exit中执行以下清理工作:释放mm_struct(如果还有别的线程引用则延迟释放)→关闭所有打开的文件描述符(释放file结构)→释放fs_struct→向父进程发送SIGCHLD信号→将进程状态设置为EXIT_ZOMBIE(僵尸)——此时task_struct仍保留在进程表中——等待父进程调用wait()/waitpid()收集退出状态(status、累计utime+stime等)。如果父进程不wait子进程——子进程变成僵尸(zombie)——占用进程表的slot而不释放——如果父进程长期不wait僵尸积累到上限(约可创建的最大PID)时——新fork()无法分配新PID。
如果父进程比子进程先exit——子进程成为孤儿——init(pid=1)会自动收养所有孤儿进程定期调用wait释放其退出状态的task_struct——防止孤儿逐渐积累为僵尸。
复习检查
PCB(task_struct)中哪个字段记录了进程的当前工作目录?哪个字段记录了根文件系统(是同一个结构体吗)?
fork()返回两次——父进程和子进程分别得到什么值?如何判断当前是父进程还是子进程?
写时复制(Copy-on-Write)在fork后在哪个时间点真正复制物理页框?什么指令触发了缺页?
僵尸进程在系统中占用了哪些资源?如果父进程不wait子进程——系统最终会因什么原因无法创建新进程?
进程的虚拟地址空间退出
exit()后——mm_struct何时完全释放?如果另一个线程还在使用同一个mm——内核如何管理引用计数?