Linux内核 | 进程管理
1. 进程和线程
1.1 定义
进程是处于运行状态的程序和相关资源的总称,是资源分配的最小单位。
- 有一段 可执行程序代码 。
- 有一段进程专用的 系统堆栈空间 和 系统空间堆栈 。
- 有 进程描述符 ,用于描述进程的相关信息。
- 有 独立的存储空间 ,也就是专有的用户空间,相应的又会有 用户空间堆栈 。
1.2 主要区别
进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
总结:linux中,进程和线程唯一区别是有没有独立的地址空间。
2. 进程描述符及任务结构
32位机器上,大约有1.7KB,进程描述符完整描述一个正在执行的进程的所有信息。
进程描述符struct task_struct( 源代码 | linnux/sched.h | v5.4 )
struct task_struct {
volatile long state; // -1为不可运行, 0为可运行, >0为已中断
int lock_depth; // 锁的深度
unsigned int policy; // 调度策略:一般有FIFO,RR,CFS
pid_t pid; // 进程标识符,用来代表一个进程
struct task_struct *parent; // 父进程
struct list_head children; // 子进程
struct list_head sibling; // 兄弟进程
}
2.1 分配进程描述符
2.1.1 slab分配器
slab分配器动态生成task_struct,只需在栈底(相对于向下增长的栈)或栈顶(相对于向上增长的栈)创建一个新结构struct thread_info。
2.1.2 进程描述符存放
PID最大值默认为32768(short int 短整形的最大值<linux/threads.h>)可通过修改/proc/sys/kernel/pid_max提高上限。
x86系统中,current把栈指针后13个有效位屏蔽掉,用来计算出thread_info的偏移。
movl $-8192,%eax
andl %esp,%eax
2.1.3 进程状态
- TASK_RUNNING:1. 正在执行 2. 在运行队列中等待执行
- TASK_INTERRUPTIBLE:阻塞(可中断)
- TASK_UNINTERRUPTIBLE:阻塞(不可中断)
- __TASK_TRACED:被其他进程跟踪的进程
- __TASK_STOPPED:进程停止
2.1.4 进程家族树
init进程目的:读取系统的初始化脚本,并执行其他的相关程序,最终完成系统启动的整个过程。
3. 进程创建
其他操作系统提供产生(spawn)进程机制,首先在新地址空间里创建进程,读入可执行文件,最后开始执行。
3.1 写时拷贝(copy-on-write)
原理:如果有进程试图修改一个页,就会产生一个缺页中断。内核处理缺页中断的方式就是对该页进行一次透明复制。这时会清除页面的COW属性,表示着它不再被共享。
3.2 fork()函数
fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。
在现在linux内核中,fork()实际上是由clone()系统调用实现的
3.2.1 copy_process()函数
- dup_task_struct()为新进程创建一个内核栈,thread_info结构和task_struct与当前进程相同。父子进程描述符是完全相同的。(分配空间)
- 检查并确保新创建这个进程后,当前用户所拥有的进程数目没有超出给它分配的资源的限制。(检查边界)
- 子进程与父进程区别开。进程描述符的许多成员都要被清0或设初始值,那些不是继承来的进程描述符的成员,主要是统计信息。task_struct中的大多数数据都依然未被修改。(子进程初始化)
- 子进程的状态被设置为TASK_UNINTERRUPTIBLE(不可中断,阻塞状态),以保证它不会投入运行。(设置子进程状态)
- 调用alloc_pid()为新进程分配一个有效的PID。(为子进程分配pid)
- 根据传递给clone()的参数,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。一般情况下,这些资源会被给定的进程的所有线程共享;否则,这些资源对每个进程是不同的,因此被拷贝到这里。(将资源参数标志赋值给结构体)
- copy_process()做扫尾工作并返回一个指向子进程的指针,再回到do_fork()函数,如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行。(返回子进程指针,并唤醒子进程执行)
注:内核有意让子进程先执行,并非总能如此,因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销。因为父进程先执行,可能往地址空间写入。
3.3 vfork函数
vfork()和fork()区别:vfork()不拷贝父进程的页表项。
vfork():子进程作为父进程的一个单独线程在它的地址空间里运行,父进程被阻塞,直到子进程退出或执行exec(),子进程不能向地址空间写入。
4. 线程创建
线程创建和进程创建基本一致,通过调用clone()函数传递的参数标志,指明需要共享的资源。
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
// CLONE_VM : 地址空间
// CLONE_FS : 文件系统
// CLONE_FILES : 文件描述符
// CLONE_SIGHAND : 信号处理程序及被阻断的信号
clone(SIGCHLD,0);
clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0)
4.1 内核线程
内核线程和普通进程的区别:内核线程没有独立的地址空间。(task_struct的mm指针被设置为NULL)
内核线程只能由其他内核线程创建,通过kthreadd内核线程衍生出所有新的内核线程。(kthreadd是所有内核线程的祖宗)
4.1.1 kthreadd内核线程
kthreadd内核线程是在内核初始化时被创建,循环执行kthreadd函数,它的作用是管理调度其它的内核线程。
kthread_create函数( 源代码 | linux/kthread.h | v5.4 )是通过clone()系统调用,创建一个内核线程,但新创建的线程处于不可运行状态。
struct task_struct * kthread_create(threadfn, data, namefmt, arg...)
创建内核线程,并运行
kthread_run函数( 源代码 | linux/kthread.h | v5.4 ),通过调用kthread_create函数创建内核线程,然后调用wake_up_process()进行唤醒。
#define kthread_run(threadfn, data, namefmt, ...) \
({ \