《Linux内核设计与实现》笔记3 - 进程管理
进程⌗
进程是处于执行期的程序,在内核中通常称为task
,包含以下资源:
- 可持续程序代码(text section)
- 打开的文件
- 挂起的信号
- 内核内部数据
- 处理器状态
- 一个或多个具有内存映射的内存地址空间
- 一个或多个线程
- 全局变量的数据段
- 等等
进程提供2种虚拟机制:
- 虚拟处理器
- 虚拟内存
线程是在进程中活动的对象,内核的调度对象是线程,而不是进程。线程之间有独立的虚拟处理器,但是共享虚拟内存。Linux对进程和线程不做特别区分,在Linux中线程只是一种特殊的进程。
线程拥有以下独立资源:
- 程序计数器
- 进程栈
- 进程寄存器
相关系统调用:
- 常用
fork()
系统调用创建进程,调用后从内核返回两次,一次回到父进程,一次回到子进程。 - 现代Linux内核中
fork()
由clone()
系统调用实现。 fork()
后调用exec()
系列函数创建新的地址空间,载入程序。- 调用
exit()
退出执行,并释放资源。 - 父进程通过
wait4()
查询子进程是否终结。 - 子进程推出后变为
zombie
状态,直到父进程调用wait()
或waitpid()
。
进程描述符⌗
任务结构⌗
进程描述符存放在内核的任务队列(task list)双向链表中,链表的每一项是一个task_struct
结构体,每个task_struct
在32位机器上大约占1.7KB内存。
// linux/sched.h
struct task_struct{
unsigned long state;
int prio;
unsigned long policy;
struct task_struct *parent;
struct list_head tasks;
pid_t pid;
// ...
}
分配进程描述符⌗
内核通过slab分配器动态分配task_struct
,在每个内核栈的栈底创建一个thread_info
结构体,其中的task
字段存放指向实际task_struct
的指针。
// asm/thread_info.h
struct thread_info{
struct task_struct *task;
// ...
}
在內存中的结构如下图所示:
进程描述符的存放⌗
PID是pid_t
类型,一般就是int
。可以通过/proc/sys/kernel/pid_max
查看和修改PID最大值,这个值通常是400多万,足够使用。
获取当前进程描述符使用current()
宏,等价于以下语句:
current_thread_info()->task
进程状态⌗
task_struct
的state字段表示进程状态
进程有5种状态:
TASK_RUNNING
:可执行,正在执行或在运行队列中等待执行TASK_INTERRUPTIBLE
:被阻塞,等待某些条件完成TASK_UNINTERRUPTBLE
:不可中断的阻塞,对信号不做响应,较少使用__TASK_TRACED
:被其他进程跟踪,例如通过ptrace
对程序跟踪调试__TASK_STOPPED
:停止执行,不能投入运行,在收到信号SIGSTOP
,SIGTSTP
,SIGTIN
,SIGTOUT
等信号后。
进程上下文⌗
当程序执行了系统调用或出发了异常,就陷入内核空间,此时内核处于进程上下文中,内核代表进程执行。进程上下文中current
宏是有效的
进程家族树⌗
task_struct
中的parent
字段记录了父进程的指针,所有进程都是PID为1的init进程的后代所有组成一颗树的结构,命令pstree
打印进程树。
进程创建⌗
其他操作系统都有spawn产生进程的机制,Linux将spawn过程拆成了fork()
和exec()
两步。
fork()
拷贝当前进程创建子进程,父子进程的区别在于PID、PPID、某些资源(挂起的信号)和统计量。
exec()
加载可执行文件到虚拟地址空间,开始执行。
Copy-on-Write⌗
fork()
时不会复制整个进程地址空间,而是将地址空间以只读的方式共享,延迟到写入时再拷贝。
fork()
⌗
通过clone()
系统调用实现,其他的函数如vfork()
,__clone()
都会调用clone()
,clone()
再调用do_fork()
。
vfork()
⌗
不拷贝父进程的页表项,其他和fork()
相同。子进程作为父进程的一个单独的线程在地址空间里运行,不能向地址空间写入,父进程被阻塞,直到子进程退出或执行exec()
。
线程的实现⌗
内核中并没有线程的概念,把所有线程当作进程实现,线程仅仅是共享某些资源的进程。每个线程有自己的task_struct
。
举个例子,创建1个包含4线程的进程,在Linux中只需要创建4个进程,分别分配4个对应的task_struct
,设置共享某些资源。
线程创建⌗
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
共享虚拟内存地址空间、文件系统、文件名描述符、信号处理程序。
对比fork()
:
clone(SIGCHLD, 0);
对比vfork()
:
clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);
clone()
的参数标志都定义在linux/sched.h
头文件中。
内核线程 kernel thread⌗
- 负责执行一些内核的后台操作
- 没有独立的地址空间
- 只在内核空间运行,不会切换到用户空间
- 和普通进程一样可以被调度,被抢占
用命令ps -ef
查看,进程名在[]
中的都是内核进程。
内核线程只能由其他内核线程创建,从kthreadadd
内核进程中衍生出所有新的内核线程。
- 创建内核线程
// linux/kthread.h
struct task_struct *kthread_create(...); // 创建后需要调用wake_up_process()运行
struct task_struct *kthread_run(...); // 宏,创建内核线程并运行
- 推出内核线程,可以调用
do_exit()
,或
int kthread_stop(struct task_struct *k);
进程终止⌗
进程可以主动终止或被动终止
- 主动终止,进程调用
exit()
系统调用- 显式调用
- 隐式调用,例如从
main()
函数返回
- 被动终止
- 接收到不可忽略且不能处理的信号和异常
大部分情况需要调用linux/exit.c
中的do_exit()
函数,这个函数不会返回,将进程的退出状态exit_state
设置为EXIT_ZOMBIE
僵尸态,进程不会再被调度运行。与进程相关的资源都被释放,仅剩下内核栈、thread_info
、task_struct
。
删除进程描述符⌗
调用wait()
系列函数释放僵尸子进程的剩余资源。
孤儿进程⌗
如果父进程在子进程之前终止,init
进程会作为新的父进程,init
进程会清理掉僵尸进程。