Linux内核学习——Linux进程的创建与撤销
Linux的进程除了PID为0的第一个进程以外,所有的其他进程都是复制出来的,这和Windows凭空捏造不太一样。另一方面,Linux进程在复制时也有很多细节处理的设计,旨在更高效、更简洁。近来在研究Linux进程活动的整个生命周期,而本文仅仅对进程的出生与死亡进行了概略性描述,毕竟一个进程的前世今生绝不是一两篇文章可以hold住的。
Linux进程的创建与撤销
Linux进程的创建有三个函数。fork,vfork和clone。不要把exec系列的函数和这三个函数混为一谈,exec系列函数常用于三者之后,用于创建一个新的程序运行环境。
clone(), fork(), vfork()
Linux既然有各种各样的机制,那么复制也有着多种手法。这也就对应了这三种创建进程的系统调用。
这三个函数实际上都是C库的封装,内部分别调用了sys_clone, sys_fork, sys_vfork三个系统调用。这三个系统调用又统一调用了do_fork函数,只是携带的参数和标志不同,从而在do_fork中有选择的完成某些任务。
便于理解,先大体来谈谈三者的区别: - fork - 子进程复制父进程的所有资源 - clone - 轻量级进程使用clone - vfork - 子进程和父进程共享数据段,子进程每次都优先于父进程执行(这很有意义,后面你会看到)
fork()
既然Linux的进程是复制的,那么大多数人的第一想法一定是子进程复制整个父进程的资源。诸如最简单的fork,是否只要在调用时完全的深度拷贝即可?实际上深度拷贝所有的资源是没有必要的,Linux指定了3个机制,用于提升性能: - COW,即写时复制。这一技术的引用就使得父子进程可以在一开始的时候共享物理页(一开始是相同的),当二者其一想要写该物理页时,再真正的copy出页的内容到新分配的物理页,然后进行修改。 - 轻量级进程允许父子进程共享内核的大部分数据结构(页表(用户态地址空间)、打开文件表和信号处理),所以如同在《Linux进程概述》中所说,它更像一个线程。 - vfork()系统调用创建的进程能共享父进程的内存地址空间。为了防止父进程重写子进程所需要的数据,阻塞父进程的执行,直到子进程推出或执行一个新的程序为止。
无论是哪一种机制,其根本的思想是一致的:能省则省,延迟拷贝。
对于fork来说,进程A调用fork创建一个子进程B时,B和A拥有相同的物理页面,为了节约内存和提速,fork()会把A和B的物理页面设置成只读。此后,A或B想要执行写操作时,都会产生页面出错异常(page_fault int14)中断,此时CPU执行异常处理函数do_wp_page()解决该异常。do_wp_page()实际上非常简单,无非就是取消共享,为写进程复制一个新的物理页面(此时才真正进行了开辟和复制),设置权限。异常返回后,继续执行写操作。
vfork()
比起fork()的延迟懒惰处理,vfork()更加粗暴。内核在复制子进程时,连子进程自身的虚拟地址空间都不创建了,干脆使用了父进程的虚拟空间。既然虚拟空间都霸占了,物理页面也当然共享了(这意味着修改子进程的变量值也会影响父进程)。 于是,vfork为了防止父进程overwrite,设计上会把父进程先挂起,子进程exit或execve时父进程才会被唤起。vfork创建的子进程不应该用return或exit(),但可以用_exit()退出(参考man vfork,exit是_exit()或_exitgroup()的封装,结束子进程,他不会修改函数栈,所以我在测试中exit()没有出错,这应该是一种严格的说法,当然vfork不接exec,要么脑子有坑,要么想干坏事。至于exit()封装的额外操作还有待研究)。
vfork的子进程如果return就意味着父进程return(共享内存、栈),这看起来没问题,但是接下来父进程再次return就崩了,系统表示很困惑。(return后会自动接exit(),而此时栈已经被破坏了,有些系统会无限循环,再次调用main(),有些直接就segmentation fault)。
那么问题来了,我要这vfork有何用?实际上,vfork只是一个中间步骤,vfork的存在是为了exec的调用。exec是重新开辟空间,那么如果没有vfork,就只得用笨重的fork,而因为下一步想要exec,所以fork的复制过程就毫无意义。
clone()
clone是给轻量级进程的,但clone本身的设计很强大,他可以有选择性的让子进程继承资源。
clone结构:int clone(int (fn)(void ), void *child_stack, int flags, void *arg, ...);
fn是函数指针,指向程序的指针,函数返回时子进程终止并返回一个退出码;child_stack是子进程堆栈;flags表示从父进程继承哪些资源;arg为传递给子进程的参数。
flags取值是一组宏,几个常见的标志: - CLONE_VM - 共享内存描述符和所有的页表 - CLONE_FS - 共享根目录和当前工作目录所在的表,以及用于屏蔽新文件初始许可权的位掩码值 - CLONE_FILES - 共享打开文件表 - CLONE_SIGHAND - 共享信号处理程序的表、阻塞信号表和挂起信号表。如果标记为true,必须设置CLONE_VM - CLONE_PTRACE - 如果父进程被跟踪,那么子进程也被跟踪。 - CLONE_VFORK - vfork()系统调用时设置 - CLONE_PARENT - 设置子进程的父进程为调用进程的父进程 - CLONE_THREAD - 把子进程插入到父进程的同一线程组中,使子进程共享父进程信号描述符。因此子进程的tgid和group_leader字段也被设置。如果该标记为true,必须设置CLONE_SIGHAND - CLONE_NEWNS - 当clone需要自己的命名空间时设置这个标志。CLONE_NEWNS和CLONE_FS互斥。 - CLONE_PID - 子进程创建时PID和父进程一致
clone()是libc定义的封装函数,clone()系统调用的服务例程是sys_clone(),它没有fn和arg参数(clone()把fn指针放在了子进程堆栈的某个位置,arg在fn下面),clone()返回后,取出的地址就是fn,参数就是arg,顺理成章执行fn(arg)。
系统调用服务例程
实际上我在看glibc 2.19时发现整个流程非常的复杂,有兴趣的可以自己跟一下。但无论如何,我只需要知道,最终的系统调用对应的服务例程是sys_fork等函数就够了。
1 |
|
可以看到vfork和fork最终也都是通过do_fork()进行的,只是提前给定了clone_flags。
do_fork()处理的事情: 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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99/**
* 负责处理clone,fork,vfork系统调用。
* clone_flags-与clone的flag参数相同
* stack_start-与clone的child_stack相同
* regs-指向通用寄存器的值。是在从用户态切换到内核态时被保存到内核态堆栈中的。
* stack_size-未使用,总是为0
* parent_tidptr,child_tidptr-clone中对应参数ptid,ctid相同
*/
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p;
int trace = 0;
/**
* 通过查找pidmap_array位图,为子进程分配新的pid参数.
*/
long pid = alloc_pidmap();
if (pid < 0)
return -EAGAIN;
/**
* 如果父进程正在被跟踪,就检查debugger程序是否想跟踪子进程.并且子进程不是内核进程(CLONE_UNTRACED未设置)
* 那么就设置CLONE_PTRACE标志.
*/
if (unlikely(current->ptrace)) {
trace = fork_traceflag (clone_flags);
if (trace)
clone_flags |= CLONE_PTRACE;
}
/**
* copy_process复制进程描述符.如果所有必须的资源都是可用的,该函数返回刚创建的task_struct描述符的地址.
* 这是创建进程的关键步骤.
*/
p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid);
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
*/
if (!IS_ERR(p)) {
struct completion vfork;
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
}
/**
* 如果设置了CLONE_STOPPED,或者必须跟踪子进程.
* 就设置子进程为TASK_STOPPED状态,并发送SIGSTOP信号挂起它.
*/
if ((p->ptrace & PT_PTRACED) || (clone_flags & CLONE_STOPPED)) {
/*
* We'll start up with an immediate SIGSTOP.
*/
sigaddset(&p->pending.signal, SIGSTOP);
set_tsk_thread_flag(p, TIF_SIGPENDING);
}
/**
* 没有设置CLONE_STOPPED,就调用wake_up_new_task
* 它调整父进程和子进程的调度参数.
* 如果父子进程运行在同一个CPU上,并且不能共享同一组页表(CLONE_VM标志被清0).那么,就把子进程插入父进程运行队列.
* 并且子进程插在父进程之前.这样做的目的是:如果子进程在创建之后执行新程序,就可以避免写时复制机制执行不必要时页面复制.
* 否则,如果运行在不同的CPU上,或者父子进程共享同一组页表.就把子进程插入父进程运行队列的队尾.
*/
if (!(clone_flags & CLONE_STOPPED))
wake_up_new_task(p, clone_flags);
else/*如果CLONE_STOPPED标志被设置,就把子进程设置为TASK_STOPPED状态。*/
p->state = TASK_STOPPED;
/**
* 如果进程正被跟踪,则把子进程的PID插入到父进程的ptrace_message,并调用ptrace_notify
* ptrace_notify使当前进程停止运行,并向当前进程的父进程发送SIGCHLD信号.子进程的祖父进程是跟踪父进程的debugger进程.
* dubugger进程可以通过ptrace_message获得被创建子进程的PID.
*/
if (unlikely (trace)) {
current->ptrace_message = pid;
ptrace_notify ((trace << 8) | SIGTRAP);
}
/**
* 如果设置了CLONE_VFORK,就把父进程插入等待队列,并挂起父进程直到子进程结束或者执行了新的程序.
*/
if (clone_flags & CLONE_VFORK) {
wait_for_completion(&vfork);
if (unlikely (current->ptrace & PT_TRACE_VFORK_DONE))
ptrace_notify ((PTRACE_EVENT_VFORK_DONE << 8) | SIGTRAP);
}
} else {
free_pidmap(pid);
pid = PTR_ERR(p);
}
return pid;
}
copy_process()
这个函数创建了进程描述符以及子进程执行所要的所有其他数据结构。它的参数与do_fork()相同,外加子进程的PID。
1 |
|
做完do_fork后,我们的子进程整装待发,等待CPU调度。后续进程调度时,调度程序会把进程描述符thread字段的值装入几个CPU寄存器(thread.esp装入esp寄存器,ret_from_fork()地址装入eip寄存器)。关于此,可以参考《Linux进程切换》一文,这一处理刚好和_switch_to的设计一致。
内核线程
Linux还有一组内核线程,它们只运行在内核态,因此也就不必受用户态上下文的拖累。内核线程只使用大于PAGE_OFFSET的内核地址空间。
创建内核线程使用kernel_thread(),实际上本质依然是do_fork():
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/**
* 创建一个新的内核线程
* fn-要执行的内核函数的地址。
* arg-要传递给函数的参数
* flags-一组clone标志
*/
int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)
{
struct pt_regs regs;
memset(®s, 0, sizeof(regs));
/**
* 内核栈地址,为其赋初值。
* do_fork将从这里取值来为新线程初始化CPU。
*/
regs.ebx = (unsigned long) fn;
regs.edx = (unsigned long) arg;
regs.xds = __USER_DS;
regs.xes = __USER_DS;
regs.orig_eax = -1;
/**
* 把eip设置成kernel_thread_helper,这样,新线程将执行fn函数。如果函数结束,将执行do_exit
* fn的返回值作为do_exit的参数。
*/
regs.eip = (unsigned long) kernel_thread_helper;
regs.xcs = __KERNEL_CS;
regs.eflags = X86_EFLAGS_IF | X86_EFLAGS_SF | X86_EFLAGS_PF | 0x2;
/* Ok, create the new process.. */
/**
* CLONE_VM避免复制调用进程的页表。由于新的内核线程无论如何都不会访问用户态地址空间。
* 所以复制只会造成时间和空间的浪费。
* CLONE_UNTRACED标志保证内核线程不会被跟踪,即使调用进程被跟踪。
*/
return do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, ®s, 0, NULL, NULL);
}
进程0
一直在折腾clone,但总归有第一个进程,那就是进程0——idle或叫swapper。这是个Linux开天辟地时捏出来的内核线程。他使用的数据结构是静态分配的,换句话说,其他所有进程的数据结构都是动态分配的。
- init_task变量中存放进程描述符,INIT_TASK宏初始化。 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
47
48
49
50
51
52
53
54
55/**
* 进程0的描述符, 也是进程链表的头
*/
struct task_struct init_task = INIT_TASK(init_task);
/**
* 初始化进程0的任务描述符。
*/
#define INIT_TASK(tsk) \
{ \
.state = 0, \
.thread_info = &init_thread_info, \
.usage = ATOMIC_INIT(2), \
.flags = 0, \
.lock_depth = -1, \
.prio = MAX_PRIO-20, \
.static_prio = MAX_PRIO-20, \
.policy = SCHED_NORMAL, \
.cpus_allowed = CPU_MASK_ALL, \
.mm = NULL, \
.active_mm = &init_mm, \
.run_list = LIST_HEAD_INIT(tsk.run_list), \
.time_slice = HZ, \
.tasks = LIST_HEAD_INIT(tsk.tasks), \
.ptrace_children= LIST_HEAD_INIT(tsk.ptrace_children), \
.ptrace_list = LIST_HEAD_INIT(tsk.ptrace_list), \
.real_parent = &tsk, \
.parent = &tsk, \
.children = LIST_HEAD_INIT(tsk.children), \
.sibling = LIST_HEAD_INIT(tsk.sibling), \
.group_leader = &tsk, \
.real_timer = { \
.function = it_real_fn \
}, \
.group_info = &init_groups, \
.cap_effective = CAP_INIT_EFF_SET, \
.cap_inheritable = CAP_INIT_INH_SET, \
.cap_permitted = CAP_FULL_SET, \
.keep_capabilities = 0, \
.user = INIT_USER, \
.comm = "swapper", \
.thread = INIT_THREAD, \
.fs = &init_fs, \
.files = &init_files, \
.signal = &init_signals, \
.sighand = &init_sighand, \
.pending = { \
.list = LIST_HEAD_INIT(tsk.pending.list), \
.signal = {{0}}}, \
.blocked = {{0}}, \
.alloc_lock = SPIN_LOCK_UNLOCKED, \
.proc_lock = SPIN_LOCK_UNLOCKED, \
.switch_lock = SPIN_LOCK_UNLOCKED, \
.journal_info = NULL, \
}1
2
3
4
5
6
7
8
9
10
11
12
13
14#define init_thread_info (init_thread_union.thread_info)
#define INIT_THREAD_INFO(tsk) \
{ \
.task = &tsk, \
.exec_domain = &default_exec_domain, \
.flags = 0, \
.cpu = 0, \
.preempt_count = 1, \
.addr_limit = KERNEL_DS, \
.restart_block = { \
.fn = do_no_restart_syscall, \
}, \
}
start_kernel()初始化内核需要的所有数据结构,激活中断,创建另一个叫进程l的内核线程(一般叫init进程):
kernel_thread(init, NULL, CLONE_FS|CLONE_SIGHAND);
新创建内核线程的PID为1,并与进程0共享每进程所有的内核数据结构。此外,当调度程序选择到它时,init进程开始执行init()函数。
创建init进程后,进程0执行cpu_idle()函数,该函数本质上是在开中断的情况下重复执行hlt汇编语言指令。只有当没有其他进程处于TASK_RUNNING状态时,调度程序才选择进程0。
进程1
0创建的内核线程执行init(),init()完成内核初始化。init()调用execve()系统调用装入可执行程序init。然后init内核线程变为一个普通进程,且拥有自己的每进程内核数据结构。系统关闭之前,init进程一直存活,因为它创建和监控在OS外层执行的所有进程的活动。
linux还有很多其他内核线程,到具体模块时再看。
撤销进程
进程终止时,必须通知内核释放进程的资源,包括内存、打开文件以及其他零碎的东西,比如信号量。进程终止一般通过调用exit()库函数,该函数释放C函数库所分配的资源,执行编程者所注册的每个函数,并结束从系统回收进程的那个系统调用。exit()可以显示的插入,C编译程序总是把exit()插入到main()的最后一条语句之后。
exit_group()和exit()都可以终止进程,前者终止的是整个线程组。do_group_exit()内核函数实现了这个系统调用,它对应C库的exit()。后者终止某一个线程,而不管线程所属线程组中的所有其他线程。do_exit()是实现这个系统调用的内核函数,它对应Linux线程库的pthread_exit()。
注意区分C库的exit()和系统调用exit()。
exit()的处理: 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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121asmlinkage long sys_exit(int error_code)
{
do_exit((error_code&0xff)<<8);
}
/**
* 所有进程的终止都是本函数处理的。
* 它从内核数据结构中删除对终止进程的大部分引用(注:不是全部,进程描述符就不是)
* 它接受进程的终止代码作为参数。
*/
fastcall NORET_TYPE void do_exit(long code)
{
struct task_struct *tsk = current;
int group_dead;
profile_task_exit(tsk);
if (unlikely(in_interrupt()))
panic("Aiee, killing interrupt handler!");
if (unlikely(!tsk->pid))
panic("Attempted to kill the idle task!");
if (unlikely(tsk->pid == 1))
panic("Attempted to kill init!");
if (tsk->io_context)
exit_io_context();
if (unlikely(current->ptrace & PT_TRACE_EXIT)) {
current->ptrace_message = code;
ptrace_notify((PTRACE_EVENT_EXIT << 8) | SIGTRAP);
}
/**
* PF_EXITING表示进程的状态:正在被删除。
*/
tsk->flags |= PF_EXITING;
/**
* 从动态定时器队列中删除进程描述符。
*/
del_timer_sync(&tsk->real_timer);
if (unlikely(in_atomic()))
printk(KERN_INFO "note: %s[%d] exited with preempt_count %d\n",
current->comm, current->pid,
preempt_count());
acct_update_integrals();
update_mem_hiwater();
group_dead = atomic_dec_and_test(&tsk->signal->live);
if (group_dead)
acct_process(code);
/**
* exit_mm从进程描述符中分离出分页相关的描述符。
* 如果没有其他进程共享这些数据结构,就删除这些数据结构。
*/
exit_mm(tsk);
/**
* exit_sem从进程描述符中分离出信号量相关的描述符
*/
exit_sem(tsk);
/**
* __exit_files从进程描述符中分离出文件系统相关的描述符
*/
__exit_files(tsk);
/**
* __exit_fs从进程描述符中分离出打开文件描述符相关的描述符
*/
__exit_fs(tsk);
/**
* exit_namespace从进程描述符中分离出命名空间相关的描述符
*/
exit_namespace(tsk);
/**
* exit_thread从进程描述符中分离出IO权限位图相关的描述符
*/
exit_thread();
exit_keys(tsk);
if (group_dead && tsk->signal->leader)
disassociate_ctty(1);
/**
* 如果实现了被杀死进程的执行域和可执行格式的内核函数在内核模块中
* 就递减它们的值。
* 注:这应该是为了防止意外的卸载模块。
*/
module_put(tsk->thread_info->exec_domain->module);
if (tsk->binfmt)
module_put(tsk->binfmt->module);
/**
* 设置退出代码
*/
tsk->exit_code = code;
/**
* exit_notify执行比较复杂的操作,更新了很多内核数据结构
*/
exit_notify(tsk);
#ifdef CONFIG_NUMA
mpol_free(tsk->mempolicy);
tsk->mempolicy = NULL;
#endif
BUG_ON(!(current->flags & PF_DEAD));
/**
* 完了,让其他线程运行吧
* 因为schedule会忽略处于EXIT_ZOMBIE状态的进程,所以进程现在是不会再运行了。
*/
schedule();
/**
* 当然,谁还会让死掉的进程继续运行,说明内核一定是错了
* 注:难道schedule被谁改了,没有判断EXIT_ZOMBIE???
*/
BUG();
/* Avoid "noreturn function does return". */
/**
* 仅仅为了防止编译器报警告信息而已,仅此而已。
*/
for (;;) ;
}
再看exit_group(): 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
47
48
49asmlinkage void sys_exit_group(int error_code)
{
do_group_exit((error_code & 0xff) << 8);
}
/**
* 杀死属于current线程组的所有进程.它接受进程终止代码作为参数.
* 这个参数可能是系统调用exit_group()指定的一个值,也可能是内核提供的一个错误代号.
*/
NORET_TYPE void
do_group_exit(int exit_code)
{
BUG_ON(exit_code & 0x80); /* core dumps don't get here */
/**
* 检查进程的SIGNAL_GROUP_EXIT,如果不为0,说明内核已经开始为线程组执行退出的过程.
*/
if (current->signal->flags & SIGNAL_GROUP_EXIT)
exit_code = current->signal->group_exit_code;
else if (!thread_group_empty(current)) {
/**
* 设置进程的SIGNAL_GROUP_EXIT标志,并把终止代号放在sig->group_exit_code
*/
struct signal_struct *const sig = current->signal;
struct sighand_struct *const sighand = current->sighand;
read_lock(&tasklist_lock);
spin_lock_irq(&sighand->siglock);
if (sig->flags & SIGNAL_GROUP_EXIT)
/* Another thread got here before we took the lock. */
exit_code = sig->group_exit_code;
else {
sig->flags = SIGNAL_GROUP_EXIT;
sig->group_exit_code = exit_code;
/**
* zap_other_threads杀死线程组中的其他线程.
* 它扫描PIDTYPE_TGID类型的散列表中的每个PID链表,向表中其他进程发送SIGKILL信号.
*/
zap_other_threads(current);
}
spin_unlock_irq(&sighand->siglock);
read_unlock(&tasklist_lock);
}
/**
* 杀死当前进程,此过程不再返回.
*/
do_exit(exit_code);
/* NOTREACHED */
}
进程删除
Linux的子进程可以通过查询内核获取父进程的PID,或者它的子进程的执行状态。考虑到这一设计的完整性,Linux不允许内核在进程终止后直接丢弃包含在进程描述符字段中的数据,而是当父进程发出了与被终止的进程相关的wait()系列系统调用后,才可以这样做。这就是引入僵死状态的原因(参考《Linux进程概述》)。
如果父进程死在子进程前,那么所有孤儿都交给init。init在用wait()系列系统调用检查到其终止时,撤销僵死的进程。
负责干活的是release_task()函数,他从僵死进程的描述符中分离出最后的数据结构;对僵死进程的处理有两种可能的方式:如果父进程不需要接收子进程的信号,就调用do_exit();如果已经给父进程发了新号,就调用wait4()或waitpid()系统调用。后者中函数还会回收进程描述符所占用的内存空间,而前者中内存的回收由进程调度程序完成。
1 |
|
进程终止的处理函数: 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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116/**
* 所有进程的终止都是本函数处理的。
* 它从内核数据结构中删除对终止进程的大部分引用(注:不是全部,进程描述符就不是)
* 它接受进程的终止代码作为参数。
*/
fastcall NORET_TYPE void do_exit(long code)
{
struct task_struct *tsk = current;
int group_dead;
profile_task_exit(tsk);
if (unlikely(in_interrupt()))
panic("Aiee, killing interrupt handler!");
if (unlikely(!tsk->pid))
panic("Attempted to kill the idle task!");
if (unlikely(tsk->pid == 1))
panic("Attempted to kill init!");
if (tsk->io_context)
exit_io_context();
if (unlikely(current->ptrace & PT_TRACE_EXIT)) {
current->ptrace_message = code;
ptrace_notify((PTRACE_EVENT_EXIT << 8) | SIGTRAP);
}
/**
* PF_EXITING表示进程的状态:正在被删除。
*/
tsk->flags |= PF_EXITING;
/**
* 从动态定时器队列中删除进程描述符。
*/
del_timer_sync(&tsk->real_timer);
if (unlikely(in_atomic()))
printk(KERN_INFO "note: %s[%d] exited with preempt_count %d\n",
current->comm, current->pid,
preempt_count());
acct_update_integrals();
update_mem_hiwater();
group_dead = atomic_dec_and_test(&tsk->signal->live);
if (group_dead)
acct_process(code);
/**
* exit_mm从进程描述符中分离出分页相关的描述符。
* 如果没有其他进程共享这些数据结构,就删除这些数据结构。
*/
exit_mm(tsk);
/**
* exit_sem从进程描述符中分离出信号量相关的描述符
*/
exit_sem(tsk);
/**
* __exit_files从进程描述符中分离出文件系统相关的描述符
*/
__exit_files(tsk);
/**
* __exit_fs从进程描述符中分离出打开文件描述符相关的描述符
*/
__exit_fs(tsk);
/**
* exit_namespace从进程描述符中分离出命名空间相关的描述符
*/
exit_namespace(tsk);
/**
* exit_thread从进程描述符中分离出IO权限位图相关的描述符
*/
exit_thread();
exit_keys(tsk);
if (group_dead && tsk->signal->leader)
disassociate_ctty(1);
/**
* 如果实现了被杀死进程的执行域和可执行格式的内核函数在内核模块中
* 就递减它们的值。
* 注:这应该是为了防止意外的卸载模块。
*/
module_put(tsk->thread_info->exec_domain->module);
if (tsk->binfmt)
module_put(tsk->binfmt->module);
/**
* 设置退出代码
*/
tsk->exit_code = code;
/**
* exit_notify执行比较复杂的操作,更新了很多内核数据结构
*/
exit_notify(tsk);
#ifdef CONFIG_NUMA
mpol_free(tsk->mempolicy);
tsk->mempolicy = NULL;
#endif
BUG_ON(!(current->flags & PF_DEAD));
/**
* 完了,让其他线程运行吧
* 因为schedule会忽略处于EXIT_ZOMBIE状态的进程,所以进程现在是不会再运行了。
*/
schedule();
/**
* 当然,谁还会让死掉的进程继续运行,说明内核一定是错了
* 注:难道schedule被谁改了,没有判断EXIT_ZOMBIE???
*/
BUG();
/* Avoid "noreturn function does return". */
/**
* 仅仅为了防止编译器报警告信息而已,仅此而已。
*/
for (;;) ;
}