深入理解 Linux 内核—进程

释放双眼,带上耳机,听听看~!

进程是程序执行时的一个实例。

从内核观点看,进程的目的就是担当分配系统资源(CPU 时间、内存等)的实体。

一个进程创建时,几乎与父进程相同。父子进程共享包括程序代码(正文)的页,但有各自独立的栈和堆。

实现多线程应用程序的一个简单方式是把轻量级进程与每个线程关联起来,既可以共享一些资源,如地址空间、打开的文件等,又能使得每个线程被内核独立调度。

进程描述符

为了管理进程,内核通过进程描述符描述每个进程所做的事情,如进程的优先级,是运行还是阻塞,分配的地址空间,可访问哪个文件等。

进程描述符是 task_struct 类型的结构。

进程状态

进程描述符中的 state 字段描述了进程当前所处的状态:

  • 可运行状态(TASK_RUNNING)
  • 可中断的等待状态(TASK_INTERRUPTIBLE)
  • 不可中断的等待状态(TASK_UNINTERRUPTIBLE)
  • 暂停状态(TASK_TRACECD)
  • 僵死状态(EXIT_ZOMBIE)
  • 僵死撤销状态(EXIT_DEAD)

标识一个进程

一方面,内核对进程的大部分引用是通过进程描述符指针进行的。

另一方面,类 Unix 操作系统中,用户可使用PID(process ID)标识进程,PID 在进程描述符的 pid 字段中。

由于循环使用 PID 编号,内核通过 pidmap-array 位图表示已分配的 PID 号和闲置的 PID 号。

一个线程组中,所有线程的进程描述符的 tgid 字段中存放该线程组的第一个轻量级进程(thread group leader)的 PID。

如何从进程的 PID 字段中导出其描述符指针

进程是动态实体,生命周期不定,为方便内核管理,进程描述符存放在动态内存中。

对于每个进程,Linux 将内核态的进程堆栈进程描述符中的 thread_info 字段放在一起,通常占两个页框。如果没有连续的两个页框,编译时可设置跨一个页框。

thread_info 在内存区的开始,而栈从内存区的末端向下增长。thread_info 结构与 task_struct 结构分别通过 task 字段和 thread_info 字段相互关联。

esp 寄存器存放 CPU 栈的栈顶单元的地址。将 esp 寄存器的值的低 13 位(2$^{13}$ = 8K) 清零就可以得到 thread_info 的地址,task 字段在 thread_info 结构中的偏移量为 0,而 task 指向 task_struct 结构,从而得到进程描述符的地址。

Linux内核中使用一个联合体来表示一个进程的线程描述符和内核栈:


1
2
3
4
5
6
1union task_union {
2        struct task_struct task;
3        unsigned longstack[INIT_TASK_SIZE/sizeof(long)];
4    };
5
6

进程链表

进程链表将所有进程的进程描述符链接起来。每个 task_struct 的 tasks 字段(list_head)类型的 prev 字段和 next 字段分别指向前面和后面的 task_struct 元素。

进程链表的头时 init_task 描述符,是 0 进程 或 swapper 进程的进程描述符,其 prev 字段指向最后插入的进程描述符的 tasks 字段。

TASK_RUNNING 状态的进程链表

为提高调度程序运行的速度,建立多个可运行进程链表,每种进程优先级对应一个链表。task_struct 包含类型为 list_head 的 run_list 字段。

每个 CPU 都有自己的运行队列,即自己的进程链表集。

enqueue_task(p, array) 本质上等同于:


1
2
3
4
5
6
1list_add_tail(&p->run_list, &array->queue[p->prio]);
2__set_bit(p->prio, array->bitmap);
3array->nr_active++;
4p->array = array;
5
6

进程间的关系

进程间的亲属关系除了父子关系,子进程间还有兄弟关系。init 进程是所有进程的祖先。进程描述符中引入几个字段表示这些关系:

  • real_parent
  • parent,调试用
  • children
  • sibling

进程间还存在其他关系:一个进程可能是一个进程组或登录会话的领头进程,也可能是一个线程组的领头进程,还可能跟踪其他进程。进程描述符的以下字段表示非亲属关系:

  • group_leader 领头进程的描述符指针
  • signal->pgrp 进程组领头进程的 PID
  • tgid 线程组领头进程的 PID
  • signal->session 登录会话领头进程的 PID
  • ptrace_children 链表的头,该链表包含所有被 debugger 追踪的子进程
  • ptrace_list 所跟踪进程的实际父进程链表的前一个和下一个元素

pidhash 表及链表

顺序扫描进程链表并检查进程描述符的 pid 字段比较低效,为了加速内核从进程的 PID 导出对应的进程描述符指针,引入了 4 个散列表,表示不同类型 PID 的字段:
内核初始化期间动态地位 4 个散列表分配空间,并将其地址存入 pid_hash 数组。

pid_hashfn 宏把 PID 转换为表索引。

Linux 利用链表法处理冲突的 PID。

PID 散列表还可跟踪进程间的关系。

如何组织进程

  • 运行队列链表把处于 TASK_RUNNING 状态的所有进程组织在一起。
  • 没有为处于 TASK_STOPPED、EXIT_ZOMBIE 或 EXIT_DEAD 状态的进程建立专门的链表。
  • 处于 TASK_INTERUPTIBLE 或 TASK_UNINTERUPTIBLE 状态的进程被分为多种类型,每种对应一个事件,通过 wait queues 组织。

wait queues 等待队列

等待队列表示一组睡眠的进程,当某一条件变为真时,由内核唤醒。

等待队列由双向链表实现。

等待队列头:


1
2
3
4
5
6
7
1struct  _wait_queue_head {
2   spinlock_t  lock;   // 自旋锁,用于同步
3   struct list_head  task_list;  // 把一个元素链接到等待相同事件的链表
4};
5typedef struct _wait_queue_head wait_queue_head_t;
6
7

等待队列链表中的元素:


1
2
3
4
5
6
7
8
9
1struct  _wait_queue{
2   unsigned int flags;            //  1:互斥访问某一要释放资源的进程;0:非互斥进程
3   struct task_struct *task;      // 进程描述符地址
4   wait_queue_func_t func;        // 唤醒函数的地址
5   struct list_head task_list;
6};
7typedef struct _wait_queue wait_queue_t;
8
9

等待队列的操作

等待特定条件的进程可调用如下函数:

  • sleep_on()


1
2
3
4
5
6
7
8
9
10
11
1void sleep_on(wait_queue_head_t *wq)
2{
3   wait_queue_t wait;
4   init_wait_queue_entry(&wait, current);  // 初始化等待队列的头变量
5   current->state = TASK_UNINTERUPTIBLE;   // 将进程状态设置为 TASK_UNINTERUPTIBLE
6   add_wait_queue(wq, &wq);  // 插入到特定的等待队列
7   schedule();  // 调用调度程序,调度程序执行另一个程序
8   remove_wait_queue(wa, &wait);  // 睡眠进程被唤醒时,调度程序程序执行 sleep_on,将该进程从等待队列中删除
9}
10
11
  • interuptible_sleep_on()
  • sleep_on_timeout() 、interruptible_sleep_on_timeout()
  • prepare_to_wait()、prepare_to_wait_exclusive()、finish_wait()
  • wait_event、wait_event_interruptible 宏

内核通过一些宏唤醒等待队列中的进程并把其状态设置为 TASK_RUNNING。

wake_up 宏等价于如下代码:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1void wake_up(wait_queue_head_t *q)
2{
3   struct list_head *tmp;
4   wait_queue_t *curr;
5
6   // 扫描双向链表 q->task_list 中的所有项
7   list_for_each(tmp, &q->task_list){
8  
9       // 计算 wait_queue_t 变量的地址,该变量的 func 存放唤醒函数的地址
10      curr = list_entry(tmp, wait_queue_t, task_list);
11     
12      // 唤醒 task 字段标识的进程,如果被唤醒(函数返回 1),且进程是互斥的(curr->flags = 1),结束
13      if(curr->func(curr, TASK_INTERRUPTIBLE | TASK_UNINTERUPTIBLE, 0, NULL) && curr->flags)
14          break;
15  }
16}
17
18

非互斥进程在双向链表开始的位置,所以总是先唤醒非互斥进程。

进程资源限制

当前进程的资源限制存放在 current->signal->rlim 字段,即进程的信号描述符的一个字段。


1
2
3
4
5
6
1struct rlimit {
2   unsigned long rlim_cur;    
3   unsigned long rlim_max;  // 资源限制所允许的最大值
4};
5
6

限制的资源有:进程地址空间、堆、栈、拥有页框数、CPU 使用最长时间、文件大小、文件锁、打开的文件描述符数等

进程切换

硬件上下文

进程恢复执行前必须装入寄存器的一组数据称为硬件上下文。硬件上下文是进程可执行上下文的一个子集。

prev 表示切换出的进程描述符,next 表示切换进的进程描述符。

80×86 采用 far jmp 指令跳到 next 进程的 TSS 描述符的选择符执行进程切换,但 Linux 2.6 采用软件执行进程切换。

进程切换只发生在内核态。执行进程切换前,用户态进程使用的所有寄存器内容都已保存在内核态堆栈上。

任务状态段

任务状态段(Task State Segment,TSS)存放硬件上下文。Linux 不使用硬件进行上下文切换,但为每个 CPU 创建一个 TSS,原因:

  • CPU 从用户态切换到内核态时,可从 TSS 中获取内核态堆栈地址。
  • 用户态进程通过 in 或 out 指令访问 I/O 端口时,CPU 需访问 TSS 中的 I/O 许可权位图以检查该进程是否有访问端口的权力。

每个 TSS 有 8 字节的任务状态段描述符(Task State Segment Descirptor, TSSD),包括指向 TSS 起始地址的 32 位 Base 字段。

TSSD 存放在 GDT 中。GDT 存放在 gdtr 寄存器。TSSD 选择符存放在 tr 寄存器。

thread 字段

进程描述符中包含类型为 thread_struct 的 thread 字段,进程切换时,内核就把其硬件上下文保存在该结构。

thread 包含大部分 CPU 寄存器,但不包括通用寄存器,如 eax,ebx 等,因为通用寄存器的值保存在内核堆栈中。

执行进程切换

本质上,进程切换包括两步:

  • 切换页全局目录。
  • 切换内核堆栈和硬件上下文。

switch_to 宏

三个参数,prev,next,last。prev 为被替换进程的描述符地址,next 表示新进程描述符的地址,last 切换后从哪个进程切换过来的。

采用标准汇编语言描述 switch_to 宏:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
1// eax 和 edx 寄存器分别保存 prev 和  next 的值
2movl prev, %eax  
3movl next, %edx
4
5// eflags 和 ebp 寄存器的内容保存在 prev 内核栈
6pushfl
7push %ebp
8
9// prev->thread.esp 指向 prev 内核栈的栈顶
10movl %esp, 484(%eax)
11
12// next->thread.esp 装入 esp,内核开始在 next 的内核栈上操作,完成从 prev 到 next 的切换
13movl 484(%edx), %esp
14
15// 标记为 1 的地址存入 prev->thread.eip。被替换的进程重新执行时,执行被标记为 1 的指令
16movl $1f, 480(%eax)
17
18// next->thread.eip(大多数情况下为标记为 1 的地址)压入 next 的内核栈
19pushl 480(%edx)
20
21// 跳到 __switch_to() 函数
22jmp __switch_to
23
24// 被替换的进程再次获得 CPU:esp 寄存器指向 prev 的内核栈
251:
26  popl %ebp
27  popfl
28
29// 拷贝 eax 寄存器的内容到 switch_to 宏的第三个参数 last 标识的内存区域中
30// eax 寄存器指向刚被替换的进程描述符
31movl %eax, last
32
33

总结:将一些寄存器保存在 prev 内核栈,如 eflags,ebp。将另一些用寄存器中内容保存在 prev 的 thread 字段,如 esp,eip,然后将 next 的 thread 的字段的值装入寄存器,如 esp,完成内核栈的切换。

__switch_to () 函数

该函数从 eax 和 edx 取参数 prev_p 和 next_p,而不是从栈中取参数。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
1// 有选择地保存 prev_p 进程的 FPU、MMX 及 XMM 寄存器的内容
2__unlazy_fpu(prev_p);
3
4// 获得本地 CPU 的下标
5smp_processor_id()
6
7// 把 next_p->thread.esp0 装入本地 CPU 的 TSS 的 esp0 字段
8init_tss[cpu].esp0 = next_p->thread.esp0
9
10// 把 next_p 进程使用的线程局部存储(TLS)段装入本地 CPU 的全局描述符表
11cpu_gdt_table[cpu][6] = next_p->thread.tls_array[0]
12cpu_gdt_table[cpu][7] = next_p->thread.tls_array[1]
13cpu_gdt_table[cpu][8] = next_p->thread.tls_array[2]
14
15// 保存 fs 和 gs 段寄存器的内容到 prev_p->thread.fs、prev_p->thread.gs
16movl %fs 40(%esi)
17movl %gs 44(%esi)
18
19// 将 next_p 进程的 thread_struct 中保存的值装入 fs、gs 寄存器
20movl 40(%ebx), %fs
21movl 44(%ebx), %gs
22
23// 如果 next_p 被挂起时正在使用调试寄存器,
24// 用 next_p->thread.debugreg 数组的内容装载 dr0 等 6 个调试寄存器
25if(next_p->thread.debugreg[7]){
26  loaddebug(&next_p->thread, 0);
27  loaddebug(&next_p->thread, 1);
28  loaddebug(&next_p->thread, 2);
29  loaddebug(&next_p->thread, 3);
30  /* 没有 4 和 5 */
31  loaddebug(&next_p->thread, 6);
32  loaddebug(&next_p->thread, 7);
33
34// 当 next_p 或 prev_p 有自己定制的 I/O 权限位图时,
35// 更新 TSS 中的 I/O 位图
36// 懒模式:当进程在当前时间片内访问 I/O 端口时,
37// 真实位图才被拷贝到本地 CPU 的 TSS 中
38if (prev_p->thread.io_bitmap_ptr || next_p->thread.io_bitmap_ptr)
39  handle_io_bitmap(&next_p->thread, &init_tss[cpu]);
40
41// 结束
42// prev_p 参数被拷贝到 eax
43// 找到标号为 1 的指令的地址
44return prev_p
45
46

总结:保存 prev 的浮点寄存器、获得 CPU、根据 next 修改 TSS,装载 next 的 TLS。切换 prev、next 寄存器、返回。

保存和加载 FPU、MMX 及 XMM 寄存器

算术浮点单元(floating-point unit,FPU)已被集成到 CPU 中。进程使用 ESCAPE 指令时,浮点寄存器的内容为其硬件上下文,应被保存。

MMX 指令引入单指令多数据(SIMD)流水线,作用于 FPU 的浮点寄存器,无法与浮点指令混在一起使用。

SSE,SIMD 的扩展,使用 XMM 寄存器,不与 FPU 和 MMX 寄存器重叠,可与 FPU/MMX 指令混合使用。

80×86 不在 TSS 中自动保存 FPU、MMX 和 XMM 寄存器。但通过某种硬件支持保存—cr0 寄存器中的 TS(Task-Switching)标志:

  • 执行硬件上下文切换时,设置 TS 标志。
  • TS 标志被设置后,执行 ESCAPE、MMX、SSE 或 SSE2 指令,控制单元产生“Device not available”异常。

使得内核在真正需要时才保存和恢复 FPU、MMX 和 XMM 寄存器。

为保存 FPU、MMX 和 XMM寄存器引入的数据结构:


1
2
3
4
5
6
7
1union i387_union{
2   struct i387_fsave_struct fsave;   // 由无数学协处理器使用
3   struct i387_fxsave_struct fxsave;  // 由具有协处理器或 MMX 单元的 CPU 模型使用
4   struct i387_soft_struct soft;  // 由具有 SSE 和 SSE2 扩展的 CPU 模型使用
5};
6
7

进程描述符包含两个附加的标志:

  • task_struct 描述符的 thread_info 字段中的 status 字段为 TS_USEDFPU 标志时,表示当前进程是否使用过 FPU、MMX 和 XMM 寄存器。
  • task_struct 描述符的 flags 字段中的 PF_USED_MATH 标志,表示 thread.i387 字段的内容是否有意义。没有意义的情况:a、进程调用 execve() 执行一个新程序时;b、用户态下的进程开始执行一个信号处理程序时。

保存 FPU 寄存器


1
2
3
4
5
6
7
8
1// __switch_to() 把 prev 的描述符作为参数传递给 __unlazy_fpu 宏,
2// 该宏检查 prev 的 TS_USEDFPU 标志,
3// 如果被设置,说明使用了 FPU、MMX、SSE 或 SSE2 指令,
4// 内核必须保存相关硬件上下文
5if(prev->thread_info->status & TS_USEDFPU)
6   save_init_fpu(prev);
7
8

save_init_fpu():


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1// FPU 寄存器内容保存到 prev 检查描述符,并重新初始化 FPU。
2
3// 如果 CPU 使用 SSE/SSE2 扩展,还应保存 XMM 寄存器内容,并重新初始化 SSE/SSE2 单元。
4asm volatile("fxsave %0; fnclex"
5   : "=m" (tsk->thread.i387.fxsave));
6
7// 否则
8asm volatile("fnsave %0; fwait"
9   : "=m" (tsk->thread.i387.fsave));
10
11// 重置 prev 的 TS_USEDFPU 标志
12prev->thread_info->status &= ~TS_USEDFPU;
13
14// stts() 宏设置 cr0 的 TS 标志,产生如下汇编指令
15movl %cr0, %eax
16orl $8, %eax
17movl %eax, %cr0
18
19

总结:切换完浮点寄存器后,设置相关标志。

装载 FPU 寄存器

__unlazy_fpu() 设置了 cr0 的 TS 标志后,next 第一次试图执行 ESCAPE、MMX 或 SSE/SSE2 指令时,产生异常,执行如下函数:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1void math_state_restore()
2{
3   // 清除 cr0 的 TS 标志,避免产生异常
4   asm volatile ("cls);  
5  
6   // 如果 thread.i387 字段内容无效
7   // 重新设置 thread.i387 字段,并将 PF_USED_MATH 标志设置为 1
8   if(!(current->flags & PF_USED_MATH))  
9       init_fpu(current);  
10 
11  // 将保存在 thread.i387 字段中的值载入 FPU 寄存器,
12  // 根据 CPU 是否支持 SSE/SSE2 扩展使用 fxrstor 或 frstor 汇编指令
13  restore_fpu(current);  
14
15  // 设置 TS__USEDFPU 标志
16  current->thread.status |= TS_USEDFPU;  
17}
18
19

总结:恢复完浮点寄存器后,设置相关标志。

在内核态使用 FPU、MMX 和 SSE/SSE2 单元

应避免干扰用户态进程的计算。 因此:

  • 如果用户态进程使用了 FPU,内核必须调用 kernel_fpu_begin() 保存寄存器内容,并重新设置 cr0 寄存器的 TS 标志。
  • 使用完协处理器后,内核必须调用 kernel_fpu_end() 设置 cr0 寄存器的 TS 标志。

创建进程

clone(),fork(),vfork() 系统调用

Linux 中,轻量级进程由 clone() 创建。clone() 是 C 语言库中定义的封装,调用 sys_clone()。

fork() 由 clone() 实现,flags 参数指定为 SIGCHLD 信号及所有清 0 的 clone 标志,child_stack 参数是父进程堆栈指针,写时复制。

vfork() 也是由 clone() 实现,flags 参数指定为 SIGCHLD 信号和 CLONE_VM 及 CLONE_VFORK 标志,child_stack 参数是父进程的栈指针。

do_fork()

处理 clone(),fork(),vfork() 系统调用。

do_fork():

  • 根据 pidmap_array 位图为新进程分配 PID。
  • 如果父进程被跟踪(current->ptrace != 0),还想跟踪子进程,且子进程不是内核线程,设置 CLONE_PTRACE 标志。
  • 关键步骤:调用 copy_procecss() 复制进程描述符。如果资源都可用,返回创建的 task_struct 描述符地址。
  • 如果设置了 CLONE_STOPPED 标志,或必须跟踪子进程(p->ptrace 设置了 PT_PTRACED 标志),则子进程的状态被设置成 TASK_STOPPED,并增加 SIGSTOP 信号。
  • 如果没有设置 CLONE_STOPPED 标志,则调整父子进程的调度参数,根据不同情况将子进程插入父进程运行队列的不同位置。
  • 如果父进程被跟踪,则把子进程的 PID 存入 current ->ptrace_message,并通过 SIGCHLD 信号通知 debugger 进程,已经创建了一个子进程。
  • 如果设置了 CLONE_VFORK 标志,则把父进程插入等待队列,并挂起直到子进程释放自己的内存地址空间。
  • 结束并返回子进程的 PID。

copy_process()

do_fork() 使用 copy_process() 创建进程描述符及子进程执行所需的其他内核数据结构。

参数与 do_fork() 相同,外加子进程的 PID。

copy_process():

  • 如果 clone_flags 所传标志不一致,返回错误代号。
  • 执行安全检查。
  • 获取子进程描述符指针 tsk。如果需要,保存浮点寄存器。
  • 检查用户拥有的进程数是否超出限制。
  • 递增用户拥有的进程计数器。
  • 检查系统中的进程数是否超过限制。
  • 如果实现新进程的执行域和可执行格式的内核函数都包含在内核模块中,递增其使用计数器。
  • 设置与进程状态相关的几个关键字段。
  • 新进程的 PID 存入 tsk->pid。
  • 根据 clone_flags 决定是否复制子进程的 PID。
  • 初始化子进程描述符的一些字段。
  • 根据 clone_flags,创建新的数据结构,如页、信号量、文件系统等,并将父进程的数据拷入。
  • 初始化子进程内核栈。
  • 根据 clone_flags,改变子进程用户态地址空间的 child_tidptr 指向的变量的值,实际的写操作稍后执行。
  • 进行设置,使得子进程不将系统调用结束消息通知给调试进程。
  • 根据 clone_flags 参数低位的信号数字初始化 tsk->exit_signal,如果 CLONE_THREAD 被设置,则 tsk->exit_signal 初始化为 -1。
  • 初始化新进程的调度程序数据结构。
  • 设置新进程的 threa_info 的 cpu 字段。
  • 初始化亲子关系。
  • 如果不需要跟踪子进程,tsk->ptrace = 0。
  • 将新进程描述符插入进程链表。
  • 如果需要跟踪子进程,tsk->ptrace = current->parent,将子进程插入调试程序的跟踪链表。
  • 将新进程描述符的 PID 插入 pidhash[PIDTYPE_PID] 散列表。
  • 如果子进程是线程组的领头进程,进行一些设置,并插入各种 pidhash 散列表。
  • 否则,如果子进程属于父进程的线程组,进行一些设置,插入 PIDTYPE_TGID 散列表。
  • 递增 nr_threads。
  • 递增被创建的进程数。
  • 终止并返回子进程描述符 tsk。

总结:检查,获得子进程描述符,初始化子进程相关数据结构、状态,设置子进程属性,设置亲子关系,将子进程插入相应的链表、散列表,递增相关计数。

内核线程

内核线程:

  • 只运行于内核态。
  • 只用大于 PAGE_OFFSET 的线性地址空间。

kernel_thread() 创建一个内核线程,本质调用 do_fork()。

  • CLONE_VM 标志避免复制调用进程的页表(用户态地址空间)。
  • CLONE_UNTRACED 标志保证内核线程不会被跟踪。

进程 0:所有进程的祖先叫做进程 0,idle 进程,或 swapper 进程,是 Linux 的初始化阶段从无到由创建的 一个内核线程。

进程 1:进程 0 创建的内核线程执行 init() 函数完成内核初始化,init() 调用 execve() 装入可执行程序 init,init 内核线程变为一个普通进程。

撤销进程

进程终止

终止用户态应用的系统调用:

  • exit_group(),调用内核函数 do_group_exit()。
  • exit(),调用内核函数 do_exit()。

do_group_exit()

  • 1、如果退出进程的 SIGNAL_GROUP_EXIT 标志不为 0,将 current->signal->group_exit_code 值当作退出码,跳到 4。
  • 2、否则,设置 SIGNAL_GROUP_EXIT 标志,并把终止代号放到 current->signal->group_exit_code。
  • 3、调用 zap_other_threads() 杀死 current 线程组中的其他进程,为此,该函数会扫描与 current->tgid 对应的 PIDTYPE_TGID 类型的散列表中的每个 PID 链表,并发送SIGKILL 信号,使得所有进程执行 do_exit()。
  • 4、调用 do_exit()。

do_exit()

  • 将进程描述符的 flag 字段设置为 PF_EXITING 标志,表示进程正在被删除。
  • 如果需要,从动态定时器队列中删除进程描述符。
  • 从进程描述符中分离出资源,如分页、信号量、文件系统、打开文件描述符、命名空间及 I/O 权限位图,如果无其他进程共享,删除。
  • 如果实现了被杀死进程的执行域和可行域格式的内核函数包含在内核模块中,则递减其使用计数器。
  • 将进程描述符的 exit_code 字段设置为进程的终止代号。
  • exit_notify()
    • 更新父子进程的亲属关系。如果该组有其他运行的进程,则终止进程的子进程成为另外进程的子进程,否则,成为 init 的子进程。
    • 根据终止进程的进程描述符的 exit_signal 值,是否进程组的最后一个成员,是否被跟踪等情况,或向父进程发送信号,或更改进程状态,或回收内存,或递减进程描述符计数器。
    • 将进程描述符的 flags 字段设置为 PF_DEAD 标志。
  • 调度一个新进程运行。

删除进程

父进程在子进程之间结束,则孤儿进程成为 init 进程的子进程,init 调用 wait(),撤销僵死进程。

release_task() 从僵死进程的描述符中分离出最后的数据结构,对僵死进程有两种处理:

  • 如果父进程不需要接收来自子进程的信号,调用 do_exit(),进程描述符所占内存的回收由进程调度程序完成。
  • 如果已经给父进程发送了一个信号,调用 wait4() 或 waitpid(),并回收进程描述符所占的内存空间。

release_task():

  • 递减终止进程拥有者的进程个数。
  • 如果进程正被跟踪,从调度程序的 ptrace_children 链表中删除,让该进程重新属于初始父进程。
  • 删除所有挂起的信号,并释放进程的 signal_struct 描述符。如果不被其他进程使用,删除该结构。剥离所有的定时器。
  • 删除信号处理函数。
  • __unhash_process()
    • nr_threads 减 1。
    • 分别从 PIDTYPE_PID 和 PIDTYPE_TGID 类型的 PID 散列表中删除进程描述符。
    • 如果为线程组的领头进程,分别从 PIDTYPE_PGID 和 PIDTYPE_SID 类型的散列表中删除进程描述符。
    • 从进程链表中解除进程描述符的链接。
  • 如果进程不是线程组的领头进程,领头进程僵死,且进程是线程组的最后一个,向领头进程的父进程发送信号,通知它进程已死亡。
  • 调整父进程的时间片。
  • 递减进程描述符的使用计数器,如果变为 0,终止所有残留的对进程的引用。
    • 递减进程所有者的 user_struct 结构的使用计数器,如果为 0,释放。
    • 释放进程描述符及 thread_info 描述符和内核态堆栈。

给TA打赏
共{{data.count}}人
人已打赏
安全运维

WordPress网站专用docker容器环境带Waf

2020-7-18 20:04:44

安全运维

运维安全-Gitlab管理员权限安全思考

2021-9-19 9:16:14

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索