原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/

概念知识点理解

  • 进程控制块

    进程控制块:PCB是操作系统管理控制进程运行所有的信息集合,主要包括进程描述信息、进程控制和管理信息、资源分配清单和处理机相关信息等,是进程实体的一部分,进程存在的唯一标志。
    PCB是进程在内存中的静态存在方式,因此进程的静态描述符必须保证一个进程在获得CPU并重新进入运行态时,能够精确的接着上次运行的位置继续运行,相关的程序段、数据以及CPU现场信息都要保存下来,CPU的现场信息主要包括内部寄存器和堆栈的基本数据。

  • 进程上下文

    进程上下文:当程序执行了系统调用或中断而进入内核态时,进程切换现场就称为进程上下文,包含了一个进程所具有的全部信息,一般包括:进程控制块(PCB)、有关程序段和相应的数据集。

  • 进程堆栈

    进程的堆栈:内核在创建进程的时候,会为进程创建相应的堆栈。每个进程会有两个栈,一个用户栈,存在于用户空间,一个内核栈,存在于内核空间。当进程在用户空间运行时,cpu堆栈指针寄存器里面的内容是用户堆栈地址,使用用户栈;当进程在内核空间时,cpu堆栈指针寄存器里面的内容是内核栈空间地址,使用内核栈。进程用户栈和内核栈的切换:当进程因中断或系统调用而陷入内核态运行时,进程所使用的堆栈也要从用户栈转到内核栈。进程进入内核态后,先把用户态堆栈的地址保存在内核栈之中,然后设置堆栈指针寄存器的内容为内核栈的地址,完成用户栈向内核栈的转换;当进程从内核态恢复到用户态之行时,在内核态之行的最后将保存在内核栈里面的用户栈的地址恢复到堆栈指针寄存器,实现内核栈和用户栈的互转。那么,我们知道从内核转到用户态时用户栈的地址是在陷入内核的时候保存在内核栈里面的,但是在陷入内核的时候,我们是如何知道内核栈的地址的呢?关键在进程从用户态转到内核态的时候,进程的内核栈总是空的。这是因为,当进程在用户态运行时,使用的是用户栈,当进程陷入到内核态时,内核栈保存进程在内核态运行的相关信息,但是一旦进程返回到用户态后,内核栈中保存的信息无效,会全部恢复,因此每次进程从用户态陷入内核的时候得到的内核栈都是空的。所以在进程陷入内核的时候,直接把内核栈的栈顶地址给堆栈指针寄存器就可以了。

进程的创建

Linux创建进程是通过子进程复制父进程所拥有的资源来实现的。现代Linux通过写时复制、共享数据等方法优化这一过程,提高创建子进程的效率。
在Linux中,进程创建实际上是通过do_fork函数处理的。do_fork函数的功能相对简单(kernel/fork.c):

  • 检查是否或者哪个事件应该汇报给ptracer。
  • 通过copy_process创建进程描述符和子进程执行所需要的其它数据结构。
  • 执行wake_up_new_task函数,唤醒新进程。
  • 结束并返回子进程的ID
    copy_process则负责对进程创建的相关资源的申请(kernel/fork.c):
  • 调用security_task_create以及稍后调用的security_task_alloc执行附加的安全检查。
  • 执行dup_task_struct复制父进程的task_struct描述符
  • 初始化新结构体的各个字段:did_exec,utime,stime,gtime,irq_events,hardirqs_enabled等等
  • 进行调度相关的初始化:perf_event_init_task,audit_alloc.
  • 复制父进程的信息到子进程:copy_semundo,copy_files,copy_fs,copy_mm等
  • 初始化其它进程相关字段
  • 将total_forks增加1

从上可得task_struct进程控制块与进程地址空间的联系:
在task_struct结构体内的struct mm_struct成员执行内存区描述符的指针。在进程描述符中,还应该存储进程空间的页表信息,和将逻辑地址转换成页号和页内偏移地址所需的相关信息。
通过总结可以得到:进程的创建的系统调用clone fork vfork都是调用do_fork实现的,而do_fork在做了一些参数检查之后。调用了copy_process函数,copy_process函数在进行安全性检查之后,使用dup_task_struct复制父进程的结构体。对新进程描述符的一些标志信息和时间信息进行初始化,之后将父进程的所有进程信息拷贝到子进程空间,包括IO、文件、内存信息等。然后,设置新进程的pid,将新进程加入进程调度队列中。子进程的eax设置为0,父进程则返回新进程的pid,所以在fork调用中,子进程返回的是0,父进程返回的是新进程的pid。详细见后续代码分析。

可执行程序的加载

在Linux中提供了一系列的函数,这些函数能用可执行文件所描述的新上下文代替进程的上下文。这样的函数名以前缀exec开始。所有的exec函数都是调用了execve()系统调用。
sys_execve接受参数:

  • 可执行文件的路径
  • 命令行参数字符串
  • 环境变量字符串
    sys_execve是调用do_execve实现的。do_execve则是调用do_execve_common实现的,依次执行以下操作(fs/exec.h):
  • 在堆上分配一个linux_binprm结构。
  • 调用open_exec读取可执行文件。
  • 调用sched_exec(),确定最小负载的CPU以执行新程序,并把当前进程转移过去。
  • 调用bprm_mm_init()函数,为新程序初始化内存管理。
  • 调用prepare_bprm()函数填充linux_binprm数据结构。
  • 拷贝命令行参数argv,环境变量envp,可执行文件名filename到新进程中
  • 调用search_binary_handler()函数对formats链表进行扫描,并尝试每个load_binary函数,如果成功加载了文件的执行格式,对formats的扫描终止。
  • 释放linux_binprm数据结构,返回从该文件可执行格式的load_binary中获得的代码。
    然后是load_binary分析如下:
  • 检查存放在文件前128字节的一些魔数进行匹配以确认文件格式。
  • 读可执行文件的首部。
  • 从可执行文件中确定动态链接程序的路径名,并用它来确定共享库的位置并把他们映射到内存。
  • 获得动态链接程序的目录项对象。
  • 检查动态链接的执行许可权。
  • 把动态链接程序的前128字节拷贝到缓冲区。
  • 对动态链接程序类型执行一致性检查。
  • 调用arch_pick_mmap_layout(),以选择进程线性区的布局。
  • 调用setup_arg_pages()函数为进程的用户态堆栈分配一个新的线性区描述符。
  • 调用do_mmap()函数创建一个新线性区来对可执行文件正文段进行映射。
  • 调用装入动态链接程序的函数
  • 把可执行格式的linux_binfmt对象的地址放在进程描述符的binfmt字段。
  • 创建动态链接程序表,并把它们放在用户态堆栈。
  • 调用do_brk()函数创建一个新的匿名线性区来映射程序的bss段。
  • 调用start_thread()宏修改保存在内核态堆栈。
  • 返回0.
    以上分析可得:
    Linux内核使用execve系统调用就开始进行程序的装载。execve()系统调用相应的实现是sys_execve()。
    sys_execve()进行一些参数检查复制之后,调用do_execve()。
    do_execve会首先查找被执行的文件,如果找打文件。则读取文件的前128个字节以确定被检查的文件的格式。然后,继续调用search_binary_handle()去搜索合适的可执行文件装载处理过程。
    对ELF可执行文件的装载处理工程是load_elf_binary()。步骤是:检查elf文件的有效性,寻找动态链接库的“.interp”段,设置动态链接器的路径,根据ELF的可执行文件的程序头表的描述,对ELF文件进行映射,初始化ELF进程环境。
    最后,将系统调用的返回地址修改为ELF可执行文件的入口点。在设置了eip之后,sys_execve返回到用户态时,程序就返回到新的程序开始执行,ELF可执行文件装载完成。详细代码见后续分析
    ELF文件格式与进程地址空间的联系:在壮载ELF文件时,会将文件头部的.init、.text、.rodata段装载到进程的代码段中,将ELF中数据相关的部分,装载到数据段中。在动态链接的过程中,ELF文件全局变量的数据区的部分会在进程空间中拷贝一份,代码段则多个进程进行共享。exec执行之后,EIP指向的地方:EIP指向新程序的入口点,在sys_execve执行完成进入用户态之后,将从新的程序入口点继续执行。

源码分析

fork调用

fork()调用创建一个新的进程,该进程几乎是当前进程的一个完全拷贝。由fork()创建的新进程被称为子进程。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值,而父进程中返回子进程ID。子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。注意,子进程持有的是上述存储空间的“副本”,这意味着父子进程间不共享这些存储空间。Linux将复制父进程的地址空间内容给子进程,因此,子进程拥有独立的地址空间。

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
// fork_example.c
#include <memory.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
pid_t pid;
char stack_data[] = "stack_data";
char *heap_data = malloc(10 * sizeof(char));
strcpy(heap_data, "heap_data");
pid = fork();
if (pid == 0) {
printf("CHILD PROCESS: %s, %s\n", stack_data, heap_data);
} else if (pid > 0) {
printf("PARENT PROCESS: %s, %s\n", stack_data, heap_data);
} else {
printf("FORK FAILED.");
}
return 0;
}
// 输出结果
// CHILD PROCESS: stack_data, heap_data
// PARENT PROCESS: stack_data, heap_data

从输出结果可以看出:父进程和子进程的栈和堆的数据是相同的。这些数据在创建子进程时是通过拷贝产生的。

execl调用

系统调用exec是以新的进程去代替原来的进程,但进程的PID保持不变。因此,可以这样认为,exec系统调用并没有创建新的进程,只是替换了原来进程上下文的内容。原进程的代码段,数据段,堆栈段被新的进程所代替。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// execl_example.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
execl("./hello_world", NULL, NULL);
/* We can only reach this code when there is an error in execl */
printf("The execl must be failed!\n");
return 1;
}
// 输出结果:The execl must be failed!

我们执行一个不存在的hello_world程序,会报错。

1
2
3
4
5
6
7
// hello_world.c
#include <stdio.h>
int main(int argc, const char *argv[])
{
printf("Hello World!\n");
}

我们如果添加一个函数,现在我们继续运行execl_example程序,这时输出为:Hello World!
通过比较两次输出,我们发现:当execl成功时,原有的进程执行就会被打断,替换为新的进程继续执行。

使用汇编进行系统调用

我们知道在Linux中,每个系统调用都对应一个系统调用号。这个系统调用号是在unistd.h中定义的。文件的位置是在:
/usr/src/linux-headers-2.6.28-11-generic/arch/x86/include/asm/unistd_32.h

1
2
3
4
5
6
7
8
9
10
11
12
#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
#define __NR_waitpid 7
#define __NR_creat 8
#define __NR_link 9
#define __NR_unlink 10
#define __NR_execve 11

使用汇编调用fork:
可以看到fork的系统调用号是2,我们现在使用汇编代码重新编写fork_example.c

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
#include <memory.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
char stack_data[] = "stack_data";
char *heap_data = malloc(10 * sizeof(char));
strcpy(heap_data, "heap_data");
// pid = fork();
asm volatile(
"mov $0x2, %%eax\n\t" // 将fork的系统调用号2存到eax寄存器
"int $0x80\n\t" // 产生int 0x80中断
"mov %%eax,%0\n\t" // 将结果存入pid中
: "=m" (pid)
);
if (pid == 0) {
printf("CHILD PROCESS: %s, %s\n", stack_data, heap_data);
} else if (pid > 0) {
printf("PARENT PROCESS: %s, %s\n", stack_data, heap_data);
} else {
printf("FORK FAILED.\n");
}
return 0;
}

输出结果为:CHILD PROCESS: stack_data, heap_data
PARENT PROCESS: stack_data, heap_data

可以尝试将调用号替换一下,改成$0x3,得到的结果是:FORK FAILED.

使用汇编调用execl:我们再尝试一下使用汇编调用execl。通过上面的观察我们可以看到execl的系统调用号是11.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
// execl("./hello_world", NULL, NULL);
const char *program = "./hello_world";
asm volatile (
"mov %0,%%ebx\n\t" // 使用program做为参数1
"mov $0,%%ecx\n\t" // 参数2为NULL
"mov $0,%%edx\n\t" // 参数3为NULL
"mov $0xb,%%eax\n\t" // 将execl的系统调用好11存入eax中
"int $0x80\n\t" // 产生0x80中断
: "=m" (program)
);
/* We can only reach this code when there is an error in execl */
printf("The execl must be failed!\n");
return 1;
}

运行结果为:Hello World!
如果将系统调用号改为0x3,输出结果为:The execl must be failed!

系统调用过程详解

通过上一步的过程,我们了解到,系统调用在内核中的执行是依靠中断实现的。如果我们想进一步定位fork和execl的代码,我们需要先了解系统调用的详细过程。即回答以下两个问题:
1.中断是怎么工作的?
2.int 0x80中断是怎么工作的?

中断是怎么工作的
在Linux操作系统中,中断是通过中断描述符表工作的。中断描述符表(Interrupt Descriptor Table, IDT)是一个系统表,它与每一个中断或者异常向量相联系,每一个向量在表中有相应的中断或者异常处理程序的入口地址。内核在允许中断发生前,必须适当的初始化IDT。对于每个中断,都会有对应的中断处理程序。当产生一个中断时,Linux根据中断向量表中对应的项找到存储中断处理程序的地址,然后调用相应的中断处理程序。中段描述符表在内存中的地址存储在idtr寄存器中。内核在启动中断前,必须初始化IDT,然后将IDT的地址壮载到idtr中。
内核初始化的时候调用trap_init()函数和init_IRQ()函数初始化中断向量表。

int 0x80中断是怎么工作的
通过上面的分析,我们知道每个中断都有对应的处理程序。在系统调用的过程中,会有一个系统调用分派表,每个表项存储了一个系统调用。系统调用中断处理程序,根据系统调用号找到对应的系统调用执行。对于系统调用,参数的传递是通过寄存器ebx ecx edx进行传递的。eax中存储的是系统调用号。系统调用最大为__NR_syscalls个。

在arch/x86/include/asm/irq_vectors.h中定义了

define SYSCALL_VECTOR 0x80

现在我们查找trap_init函数,在arch/x86/kernel/traps.c中
set_system_trap_gate(SYSCALL_VECTOR, &system_call);
现在,查找system_call函数,在arch/x86/kernel/entry_32.s中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ENTRY(system_call)
RING0_INT_FRAME # can't unwind into user space anyway
ASM_CLAC
pushl_cfi %eax # save orig_eax
SAVE_ALL
GET_THREAD_INFO(%ebp)
# system call tracing in operation / emulation
testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
jnz syscall_trace_entry
cmpl $(NR_syscalls), %eax
jae syscall_badsys
syscall_call:
call *sys_call_table(,%eax,4)
movl %eax,PT_EAX(%esp) # store the return value
syscall_exit:
LOCKDEP_SYS_EXIT
DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
TRACE_IRQS_OFF
movl TI_flags(%ebp), %ecx
testl $_TIF_ALLWORK_MASK, %ecx # current->work
jne syscall_exit_work

在include/uapi/asm_generic/unistd.h中找到:
SYSCALL(NR_fork, sys_fork)
fork的系统调用号是2,对应的系统调用分派表中为sys_fork函数。在kernel/fork.c中找到如下代码:

1
2
3
4
5
6
7
8
9
10
11
#ifdef __ARCH_WANT_SYS_FORK
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
return do_fork(SIGCHLD, 0, 0, NULL, NULL);
#else
/* can not support in nommu mode */
return(-EINVAL);
#endif
}
#endif

do_fork源码分析

现在查找do_fork函数,也在kernel/fork.c中:

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
/*
* Ok, 这就是fork例程的主要部分。
*
* 函数执行进程的复制,如果成功则启动新进程。并且等待新进程完成VM的使用。
*/
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p;
int trace = 0;
long nr;
/*
* 在分配之前做一些参数和权限检查。
*/
if (clone_flags & (CLONE_NEWUSER | CLONE_NEWPID)) {
if (clone_flags & (CLONE_THREAD|CLONE_PARENT))
return -EINVAL;
}
/*
* 确定是否需要报告给ptracer,或者哪些需要汇报给ptracer。如果是调用者内核线程
* 或者标志了CLONE_UNTRACED,则不报告任何跟踪信息。否则,报告相应fork的跟踪信息。
*/
if (!(clone_flags & CLONE_UNTRACED)) {
if (clone_flags & CLONE_VFORK)
trace = PTRACE_EVENT_VFORK;
else if ((clone_flags & CSIGNAL) != SIGCHLD)
trace = PTRACE_EVENT_CLONE;
else
trace = PTRACE_EVENT_FORK;
if (likely(!ptrace_event_enabled(current, trace)))
trace = 0;
}
  
// copy_process函数创建进程描述符和子进程需要的其他数据结构。
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace);
/* 现在唤醒新线程。*/
if (!IS_ERR(p)) {
struct completion vfork;
trace_sched_process_fork(current, p);
nr = task_pid_vnr(p);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
wake_up_new_task(p);
/* fork已经完成,子进程也已经启动。现在通知ptracer。 */
if (unlikely(trace))
ptrace_event(trace, nr);
if (clone_flags & CLONE_VFORK) {
if (!wait_for_vfork_done(p, &vfork))
ptrace_event(PTRACE_EVENT_VFORK_DONE, nr);
}
} else {
nr = PTR_ERR(p);
}
return nr;
}

可以看到do_fork调用了copy_process完成了绝大部分的工作。copy_process位于同一个文件当中:

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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
/*
* 以复制的方式创建一个新的进程。但不启动运行新创建的进程。
*
* 主要复制寄存器和其它进程环境中的相应的合适部分。真正的
* 启动工作则交由调用者完成。
*/
static struct task_struct *copy_process(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace)
{
int retval;
struct task_struct *p; // 保存新的进程描述符。
/* 删除了对标志位的一致性和合法性的检查 */
// security_task_create和security_task_alloc()执行所有附加的安全检查。
retval = security_task_create(clone_flags);
// dup_task_struct为子进程获取进程描述符。稍后分析。
p = dup_task_struct(current);
// task结构中ftrace_ret_stack结构变量的初始化,即函数返回用的栈。
ftrace_graph_init_task(p);
get_seccomp_filter(p);
// task中互斥变量的初始化。
rt_mutex_init_task(p);
// 第1个if对进程占用的资源数做出限制,task_rlimit(p, RLIMIT_NPROC)
// 限制了改进程用户可以拥有的进程总数。
if (atomic_read(&p->real_cred->user->processes) >= task_rlimit(p, RLIMIT_NPROC)) {
// 第2个if使用了capable()函数来对权限做出检查,检查是否有权对指定
// 的资源进行操作,该函数返回0则代表无权操作。
if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) && p->real_cred->user != INIT_USER)
goto bad_fork_free;
}
current->flags &= ~PF_NPROC_EXCEEDED; // 将当前进程标志位中的PF_NPROC_EXCEEDED置0。
copy_creds(p, clone_flags); // copy_creds()复制证书,应该是复制权限及身份信息。
// 检查创建的线程是否超过了系统进程总量。
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count;
// 增加执行实体的模块引用计数。
if (!try_module_get(task_thread_info(p)->exec_domain->module))
goto bad_fork_cleanup_count;
p->did_exec = 0;
delayacct_tsk_init(p); /* Must remain after dup_task_struct() */
copy_flags(clone_flags, p); // 更新task_struct结构中flags成员
INIT_LIST_HEAD(&p->children); // 初始化task_struct结构中的子进程链表
INIT_LIST_HEAD(&p->sibling); // 初始化task_struct结构中的兄弟进程链表
rcu_copy_process(p); // rcu相关变量的初始化
p->vfork_done = NULL;
spin_lock_init(&p->alloc_lock);
init_sigpending(&p->pending);
p->utime = p->stime = p->gtime = 0;
p->utimescaled = p->stimescaled = 0;
p->prev_cputime.utime = p->prev_cputime.stime = 0;
seqlock_init(&p->vtime_seqlock);
p->vtime_snap = 0;
p->vtime_snap_whence = VTIME_SLEEPING;
memset(&p->rss_stat, 0, sizeof(p->rss_stat));
p->default_timer_slack_ns = current->timer_slack_ns;
task_io_accounting_init(&p->ioac); // 进程描述符中的io数据记录的初始化
acct_clear_integrals(p);
posix_cpu_timers_init(p); // timer初始化
do_posix_clock_monotonic_gettime(&p->start_time);
p->real_start_time = p->start_time;
monotonic_to_bootbased(&p->real_start_time);
p->io_context = NULL;
p->audit_context = NULL;
if (clone_flags & CLONE_THREAD)
threadgroup_change_begin(current);
cgroup_fork(p);
#ifdef CONFIG_NUMA
p->mempolicy = mpol_dup(p->mempolicy);
if (IS_ERR(p->mempolicy)) {
retval = PTR_ERR(p->mempolicy);
p->mempolicy = NULL;
goto bad_fork_cleanup_cgroup;
}
mpol_fix_fork_child_flag(p);
#endif
/* 设置CPU */
p->cpuset_mem_spread_rotor = NUMA_NO_NODE;
p->cpuset_slab_spread_rotor = NUMA_NO_NODE;
seqcount_init(&p->mems_allowed_seq);
/* 设置跟踪中断标志 */
p->irq_events = 0;
p->hardirqs_enabled = 0;
p->hardirq_enable_ip = 0;
p->hardirq_enable_event = 0;
p->hardirq_disable_ip = _THIS_IP_;
p->hardirq_disable_event = 0;
p->softirqs_enabled = 1;
p->softirq_enable_ip = _THIS_IP_;
p->softirq_enable_event = 0;
p->softirq_disable_ip = 0;
p->softirq_disable_event = 0;
p->hardirq_context = 0;
p->softirq_context = 0;
/* 设置锁深度 */
p->lockdep_depth = 0; /* no locks held yet */
p->curr_chain_key = 0;
p->lockdep_recursion = 0;
#ifdef CONFIG_DEBUG_MUTEXES
p->blocked_on = NULL; /* not blocked yet */
#endif
#ifdef CONFIG_MEMCG
p->memcg_batch.do_batch = 0;
p->memcg_batch.memcg = NULL;
#endif
sched_fork(p); // 调度相关初始化,将新进程分配到某个CPU上。
perf_event_init_task(p);
audit_alloc(p);
/* 以下根据clone_flags的设置复制相应的部分,进行重新分配或者共享父进程的内容 */
copy_semundo(clone_flags, p);
copy_files(clone_flags, p);
copy_fs(clone_flags, p);
copy_sighand(clone_flags, p);
copy_signal(clone_flags, p);
copy_mm(clone_flags, p);
copy_namespaces(clone_flags, p);
copy_io(clone_flags, p);
copy_thread(clone_flags, stack_start, stack_size, p);
if (pid != &init_struct_pid) {
retval = -ENOMEM;
pid = alloc_pid(p->nsproxy->pid_ns);
if (!pid)
goto bad_fork_cleanup_io;
}
p->pid = pid_nr(pid);
p->tgid = p->pid;
// 如果设置了同在一个线程组则继承TGID。
// 对于普通进程来说TGID和PID相等,
// 对于线程来说,同一线程组内的所有线程的TGID都相等,
// 这使得这些多线程可以通过调用getpid()获得相同的PID。
if (clone_flags & CLONE_THREAD)
p->tgid = current->tgid;
p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;
/*
* Clear TID on mm_release()?
*/
p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr : NULL;
uprobe_copy_process(p);
/*
* sigaltstack should be cleared when sharing the same VM
*/
if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM)
p->sas_ss_sp = p->sas_ss_size = 0;
/*
* Syscall tracing and stepping should be turned off in the
* child regardless of CLONE_PTRACE.
*/
user_disable_single_step(p);
clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);
#ifdef TIF_SYSCALL_EMU
clear_tsk_thread_flag(p, TIF_SYSCALL_EMU);
#endif
clear_all_latency_tracing(p);
/* ok, now we should be set up.. */
if (clone_flags & CLONE_THREAD)
p->exit_signal = -1;
else if (clone_flags & CLONE_PARENT)
p->exit_signal = current->group_leader->exit_signal;
else
p->exit_signal = (clone_flags & CSIGNAL);
p->pdeath_signal = 0;
p->exit_state = 0;
p->nr_dirtied = 0;
p->nr_dirtied_pause = 128 >> (PAGE_SHIFT - 10);
p->dirty_paused_when = 0;
/*
* Ok, make it visible to the rest of the system.
* We dont wake it up yet.
*/
p->group_leader = p;
INIT_LIST_HEAD(&p->thread_group);
p->task_works = NULL;
/* Need tasklist lock for parent etc handling! */
write_lock_irq(&tasklist_lock);
// 如果这两个标志设定了,那么和父进程有相同的父进程
if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
p->real_parent = current->real_parent;
p->parent_exec_id = current->parent_exec_id;
} else {
// 否则父进程为实际父进程
p->real_parent = current;
p->parent_exec_id = current->self_exec_id;
}
spin_lock(&current->sighand->siglock);
/*
* Process group and session signals need to be delivered to just the
* parent before the fork or both the parent and the child after the
* fork. Restart if a signal comes in before we add the new process to
* it's process group.
* A fatal signal pending means that current will exit, so the new
* thread can't slip out of an OOM kill (or normal SIGKILL).
*/
recalc_sigpending();
if (signal_pending(current)) {
spin_unlock(&current->sighand->siglock);
write_unlock_irq(&tasklist_lock);
retval = -ERESTARTNOINTR;
goto bad_fork_free_pid;
}
// 如果和父进程有相同的线程组
if (clone_flags & CLONE_THREAD) {
current->signal->nr_threads++;
atomic_inc(&current->signal->live);
atomic_inc(&current->signal->sigcnt);
p->group_leader = current->group_leader;
list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);
}
if (likely(p->pid)) {
ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace); // ptrace的相关初始化
// 如果进程p是线程组leader
if (thread_group_leader(p)) {
if (is_child_reaper(pid)) {
ns_of_pid(pid)->child_reaper = p;
p->signal->flags |= SIGNAL_UNKILLABLE;
}
p->signal->leader_pid = pid;
p->signal->tty = tty_kref_get(current->signal->tty);
/* 加入对应的PID哈希表 */
attach_pid(p, PIDTYPE_PGID, task_pgrp(current));
attach_pid(p, PIDTYPE_SID, task_session(current));
list_add_tail(&p->sibling, &p->real_parent->children);
list_add_tail_rcu(&p->tasks, &init_task.tasks); // 加入队列
__this_cpu_inc(process_counts); // 将per cpu变量加一
}
attach_pid(p, PIDTYPE_PID, pid); // 维护pid变量
nr_threads++; // 线程数加1。
}
total_forks++; // 将全局变量total_forks加1.
spin_unlock(&current->sighand->siglock);
write_unlock_irq(&tasklist_lock);
proc_fork_connector(p);
cgroup_post_fork(p);
if (clone_flags & CLONE_THREAD)
threadgroup_change_end(current);
perf_event_fork(p);
trace_task_newtask(p, clone_flags);
return p;
}

dup_task_struct也在fork.c文件中

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
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk; // 存放新的task_sturct结构体
struct thread_info *ti; // 存放线程信息
unsigned long *stackend;
int node = tsk_fork_get_node(orig);
int err;
tsk = alloc_task_struct_node(node); // 通过alloc_task_struct()函数创建task_struct结构空间
ti = alloc_thread_info_node(tsk, node); // 分配thread_info结构空间
err = arch_dup_task_struct(tsk, orig); // 关于浮点结构的复制
tsk->stack = ti; // task的对应栈
setup_thread_stack(tsk, orig);
clear_user_return_notifier(tsk);
clear_tsk_need_resched(tsk);
stackend = end_of_stack(tsk);
*stackend = STACK_END_MAGIC; /* for overflow detection */
#ifdef CONFIG_CC_STACKPROTECTOR
tsk->stack_canary = get_random_int(); // 金丝雀的设置,用于防御栈溢出攻击
#endif
/*
* One for us, one for whoever does the "release_task()" (usually
* parent)
*/
atomic_set(&tsk->usage, 2); // 设置进程块的使用计数。
#ifdef CONFIG_BLK_DEV_IO_TRACE
tsk->btrace_seq = 0;
#endif
tsk->splice_pipe = NULL;
tsk->task_frag.page = NULL;
account_kernel_stack(ti, 1);
return tsk;
}

通过上面的代码,可以总结出fork的工作的基本流程是: