《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_RUNNING
TASK_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
返回两次exec
family,创建一个新的地址空间,并加载可执行程序开始执行
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_EXITING
flags 成员 - 调用
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 进程成为其父级