《Linux 内核设计与实现》读书笔记-进程管理
Process Management
process 是运行中的程序以及其相关资源,它是程序代码运行的结果
thread 是 objects 在进程内的活动,是内核调度的基本单位。每个 thread 包含独自地程序计数器、栈、寄存器等资源,thread 共享进程的资源,比如虚拟内存。对 Linux 来说,并不区分线程与进程,线程只是一种特殊的进程,可以与其他进程共享资源
对其它系统来说,线程相当于轻量级的进程,相比于进程消耗更小,并且可以快速执行
进程提供了两种虚拟化:
- virtualized processor,让进程认为自己独占系统
- virtual memory,让进程认为自己独占整个内存
进程 API
exit,退出进程并释放资源wait,父进程等待子进程退出,可以利用wait获取子进程的退出状态。子进程退出后,父进程调用wait前处于zombie state
Process Descriptor
进程相关信息被封装到 task_struct 中,并通过进程描述符(process descriptor)。进程描述符通过引用双向链表进行连接,被称为 task list(linux/sched.h)。
task_struct 是通过 slab allocator 来分配的, thread_info 存储这其引用。为了快速获取当前运行中的进程,定义了 current macro。一些系统使用寄存器保存其位置,但是 x86 为了节省寄存器,将 thread_info 存储在栈底部(取 sp 寄存器的低 13 位),并通过 thread_info 获取 task_struct。
1 | movl $-8192, %eax |
进程通常在用户空间运行,当触发 exception 或 system call 是会进入内核空间,此时 kernel 处于 process context 中,可以通过 current macro 获取当前进程

进程描述符的最大数量可以通过修改 /proc/sys/kernel/pid_max 来进行修改,默认为 32771。进程描述符的最大数量限制了系统能够同时存在的进程数量。
Process State
系统中每个进程都处于以下 5 中状态中的一种
TASK_RUNNING:The Process is runnable。要么正在运行,要么在运行队列中等待运行。在用户空间执行的进程的唯一状态TASK_INTERRUPTIBLE:The Process is sleeping(blocked), 等待某个条件。一旦条件成立,转变为TASK_RUNNINGTASK_UNINTERRUPTIBLE:与TASK_INTERRUPTIBLE等价,除了并不会被唤醒。用于必须等待并且不能被中断或者 event 很快就会完成的场景__TASK_TRACED:进程正在被另一个进程 traced,比如 debugptrace__TASK_STOPPED:进程停止执行。接收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU时会进入该状态

可以通过 set_task_state(task, state) 修改进程的状态
Process Creation
大多数 OS 采用 spawn 机制来创建进程并执行进程。Unix 将这两个行为拆分为两个函数 fork(), exec()
fork,创建一个进程,其内容是父进程的复制,仅有 PID, PPID 和某些资源和统计信息(比如 pending signals 不继承)不同。fork返回两次execfamily,创建一个新的地址空间,并加载可执行程序开始执行
copy-on-write:fork 时延迟或或组织数据的复制,其数据对读操作来说是共享的。对于写操作,会为父子进程分别复制。如果 fork 立马 exec,那么就不需要复制。因此采用 COW 机制的 fork 只需要复制父进程的 page table 并为子进程创建进程描述符
- 调用
dup_task_struct()创建内核栈、thread_info、task_struct,这些值与当前进程一样 - 检查子进程是否会超过资源的限制
- 将进程描述符设置为初始值,使之与父进程区分开来
- 将子进程的状态设置为
TASK_UNINTERRUPTIBLE copy_process调用copy_flags更新task_struct的 flags 成员- 调用
alloc_pid()分配新的 PID - 拷贝或共享(根据传递给
clone的 flags)相关资源 copy_process清理并返回子进程的指针
子进程创建完成后会被唤醒,通常会让子进程先运行(因为通常子进程会调用 exec,这样就不需要复制 data 了。如果先让父进程写数据,那么会产生复制开销)
vfoke 与 fork 类似,但是不会复制 page table entries,直到子进程调用 exec 或退出前,父进程会一直阻塞
线程创建与进程相同,只不过调用 clone 时传递的 flags 不同(clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);),其地址空间、文件系统资源、文件描述符和 信号 handlers 都是共享的
kernel 通过 kernel thread 完成某些后台工作。kernel thread 存在于内核空间中,他们没有地址空间(指向 NULL),他们只在内核空间中进行操作,不会切换回用户空间。可以通过 ps -ef 显示 kernel thread
Process Termination
当进程终止时,会释放所拥有的资源,并向其父进程发送通知
- 设置
task_struct的PF_EXITINGflags 成员 - 调用
del_timer_sync()移除 kernel timer。 - 如果 BSD Process accounting is enable,调用
acct_update_integrals() - 调用
exit_mm()释放进程所持有的mm_struct,如果没有其他进程在使用这个地址空间,kernel 会将其销毁 - 调用
exit_sem(),如果进程重在排队等待 IPC 信号,将其出队 - 调用
exit_files()和exit_fs减少对文件描述符和文件系统数据的引用,如果引用数减为 0 就销毁 - 设置 exit code(存储在
task_struct中),这个 exit code 会被父进程获取 - 调用
exit_notify()给父进程发送信号,并将其下的 children 的 parent 定义为线程组的另一个线程或 init 进程。然后将自己的exit_state设置为EXIT_ZOMBIE do_exit()调用schedule()切换到新的进程
总结一下就是移除 kernel timer、释放内存、文件描述符、IPC 信号,并设置退出 code,并传递给父进程。进程终止后会变为僵尸进程,但是还保留着 内核栈、thread_info、task_struct,这是为了向其父进程提供信息。当父进程接收到信号后,会将这些信息也释放掉
如果父进程在子进程之前退出,这些子进程就会变为孤儿进程(Parentless),因此需要为这些孤儿进程重新定义父级,否则这些孤儿进程就会一直被保留,占用资源。有两种解决方法
- 让当前线程组中的一个线程称为其父级
- 让 init 进程成为其父级

