linux 设计与实现 — 进程管理
进程
- 进程:处于执行期的程序的实时结果
- 文件描述符
- 挂起的信号
- 内核内部数据
- 处理器状态
- 一个或多个内存映射地址 一个或多个执行线程
- (执行)线程: 进程中的活动对象
- 程序计数器
- 线程栈
- 寄存器
- 私有数据
进程描述符及任务结构
内核把进程的列表存放在任务队列中,队列中的条目为 task_struct
类型,称为进程描述
符,定义在 <linux/sched.h>
中。
分配进程描述符
Linux通过 slab
分配器分配 task_struct
结构,达到对象复用和缓存着色的目的。在
2.6 版本之前,各个进程的 task_struct
存放在它们的内核栈的尾部,这样是为了寄存
器少(如X86)的硬件体系结构只需通过栈指针就可以计算出其地址,避免专门的寄存器记录。
现在使用 slab
分配器动态生成 task_struct
,所以只需在栈低(向下增长)或栈顶
(向上增长)创建一个 thread_info
即可,该结构放在 <asm/thread_info.h>
中。
进程描述符的存放
内核通过 PID 标识每个进程,在 <linux/threads.h>
中定义 PID 的最大值。
访问任务通常需要访问其 task_struct
指针,为了加快找到当前进程的 task_struct
,
使用 current 宏保存。对于不同的硬件体系结构,实现方式不一样。
- 寄存器多:使用专门的寄存器存放
- 寄存器少:在内核栈尾创建
thread_info
,通过计算偏移量查找
进程状态
task_struct
中的 state
描述进程当前的状态。
- TASK_RUNNING: 可执行的;正在执行或运行队列中等待
- TASK_INTERRUPTIBLE: 可中断睡眠
- TASK_UNINTERRUPTIBLE: 不可中断
- __TASK_TRACED: 被跟踪
- __TASK_STOPPED: 停止,收到 SIGSTOP,SIGSTP,SIGINT,SIGTOU 等信号时
设置当前进程状态
// 调整状态
set_task_state(task, state);
// set_current_state(state) = set_task_state(current, state);
// 指定状态
task->state = state;
进程上下文
进程切换恢复到某进程所需要的所有信息。
进程家族树
系统中每个进程都有一个父进程,也可以拥有零个或多个子进程。指向父进程的指针为
parent
指针。还有一个 children
的子进程列表。
init 进程的进程描述符是作为 init_task
静态分配的。
// 访问父进程
struct task_struct *my_parent = current->parent;
// 依次访问子进程
struct task_struct *task;
struct list_head *list;
list_for_each(list, ¤t->children){
task = list_entry(list, struct task_struct, sibling);
/* task 指向当前的某个子进程 */
}
// 访问所有进程
struct task_struct *task;
for(task=current; task != &init_task; task = task->parent){
list_entry(task->tasks.next, struct task_struct, tasks);
list_entry(task->tasks.prev, struct task_struct, tasks);
}
// 访问整个任务队列
struct task_struct *task;
for_each_process(task){
...
}
进程创建
- 其它系统: spawn 机制,在新的地址空间里创建进程,读入可执行文件,最后开始执行
- Unix系统: 分解为两步:
fork
和exec
- fork 拷贝当前进程创建子进程
- exec 读取可执行文件并载入地址空间运行
写时拷贝
一种推迟甚至免拷贝数据的即技术,内核创建紧进程时并不复制整个进程地址空间,而是让 父子共享同一个拷贝,在需要写入的时候数据在复制,从而各个进程有各自的拷贝。
fork
Linux通过 clone() 实现 fork(). fork(),vfork() 以及 __clone() 库函数通过各自所需 的参数标志调用 clone(), 然后由 clone() 调用 do_fork().
do_fork 完成大部分工作,定义在 kernel/fork.c
, 完成如下工作:
- dup_task_struct: 创建内核栈, thread_info 和 task_struct. 这些和父进程一样。 描述符也一样。
- 检查:用户进程数未超分配的资源限制
- 子进程开始与父进程区分开来,进程描述符中许多值设为0或初始值。 task_struct 中的 大多数据未改。
- 状态设为 TASK_UNINTERRUPTIBLE, 确保不运行
- copy_process() 调用 copy_flags() 更新 task_struct 的 flags 成员。包括用户权限 标志,还没有调用 exec 的标志等
- 调用 alloc_pid() 分配有效 PID
- 根据 clone() 标志, copy_process 拷贝或共享打开的文件,文件系统信息,信号处 理函数,进程地址空间以及命名空间等
- copy_process 扫尾并返回指向子进程的指针
回到 do_fork(),如果 copy_process() 函数返回成功,子进程被唤醒并运行。内核有意先 运行子进程,因为子进程很可能执行 exec, 这样可以避免 COW 开销。
vfork
vfork() 现在几乎不用了,因为有了 COW, 它的作用就是将子进程作为一个单独的线程在它 的地址空间运行,父进程被阻塞,直到子进程推出。主要用于马上调用 exec 的情况,用来 避免拷贝。
- 调用 copy_process 时, task_struct 的 vfor_done 置为 NULL
- 执行 do_fork 时,如果给定标志, vfork_done 会指向特定地址
- 子进程执行,父进程等待
- 调用 mm_release() , 子进程退出地址空间
- 回到 do_fork, 父进程唤醒
线程实现
创建线程
和创建普通进程一样,只是参数标志不一样。
clone(CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND, 0);
内核线程
内核线程通过 kthreadd 内核进程调用 kthread_create 创建。定义在 <linux/kthread.h>
中。
struct task_struct *kthread_create(int (*threadfn)(void *data),
void *data,
const char namefmt[],
...);
新创建的线程处于不可运行状态,需调用 wake_up_process()
唤醒。
创建新线程并运行:
struct task_struct *kthread_run(int (*threadfn)(void *data),
void *data,
const char namefmt[],
...);
该例程通过宏实现的,只是简单的调用 kthread_create 和 wake_up_process 函数.
内核线程启动后会一直运行,直到调用 do_exit() 退出,或者内核其他部分调用 kthread_stop() 退出。
int kthread_stop(struct task_struct *k);
进程终止
进程终结的大部分任务由 do_exit() 完成,定义在 <kernel/exit.c>
- 将 task_struct 中的标志设置为 PF_EXITING.
- del_timer_sync 删除任一内核定时器,确保没有定时器在排队,也没有定时器处理程序正在运行
- acct_update_integrals() 输出统计信息
- exit_mm() 释放占用的 mm_struct. 没有进程占用就释放
- sem_exit() , 如果进程排队等候 IPC 信号则离开队列
- exit_files()、exit_fs() 递减文件描述符和文件系统的引用计数
- 将 task_struct 中的 exit_code 设为 exit() 中的退出码
- exit_notify() 向父进程发送信号,給子进程找父进程,一般为线程组其他线程或 init 进程,并设置状态为 EXIT_ZOMBIE
- schedule() 切换到新的进程