前言
网上很多文章在介绍fork()函数时都会提到,调用一次fork会“返回两次”结果,但又没有深入解释。所以初学者(包括当年的我)看到这句话时就很懵。为什么用fork会返回两次?怎么实现的?为什么需要返回两次?
只要能理解程序和进程的区别,以及有Linux下进程空间的概念,这种现象很好解释,并且不需要涉及分析内核源码。
先放一个常见的测试例子。
1 #include <unistd.h>
2 #include <stdio.h>
3 int main ()
4 {
5 pid_t fpid;
6 int count=0;
9
10 fpid=fork();
11
12 if (fpid < 0) {
13 printf("error in fork!\n");
14 return 0;
15 }
16
17 if (fpid == 0) {
18 printf("In child process. process id:%d\n", getpid());
19 count++;
20 }
21 else {
22 printf("In parent process. process id:%d\n", getpid());
23 count++;
24 }
25
26 printf("count: %d\n",count);
27
28 while(1) {
29 sleep(1);
30 }
31
32 return 0;
33 }
带引号的“返回两次”
为什么要加引号呢?其实是为了强调在程序的角度上看像是返回了两次,因为In child process和In parent process的语句都会被打印出来。
但是在进程的角度看并不能说“返回两次”,因为这两次的返回或者说执行打印的动作并不在同一个进程中。所以理解这种现象的关键是,要区分程序和进程的概念。
Linux进程空间的概念可以参考我以前的博客,或者网上其他博客。下面通过实验的方式来解释这种现象。
一个程序不只有一个进程。
很明显一个程序可以对应多个进程。比如这个例子,编译完毕后只有一个hello_world这个binary,但如果把他运行起来他是可以create出两个进程的。
先把前面的例子修还一下,把sleep(60)去掉。
然后编译运行的结果如下:
binwu@ubuntu:~/hello_world$ ./hello_world &
[1] 56865
binwu@ubuntu:~/hello_world$
In parent process. process id:56865
count: 1
In child process. process id:56866
count: 1
binwu@ubuntu:~/hello_world$ ps -aux | grep hello_world
binwu 56865 0.0 0.0 4508 748 pts/0 S 10:18 0:00 ./hello_world
binwu 56866 0.0 0.0 4508 72 pts/0 S 10:18 0:00 ./hello_world
binwu 56947 0.0 0.0 16180 1148 pts/0 S+ 10:19 0:00 grep --color=auto hello_world
以后台方式运行这个程序可以看到,这个程序会创建两个进程56865和56866。并且看进程的maps可以确定两个进程是完全一模一样的,maps打印如下所示:
binwu@ubuntu:~/hello_world$ cat /proc/56865/maps
55981f8fa000-55981f8fb000 r-xp 00000000 08:01 4457385 /home/binwu/hello_world/hello_world
55981fafa000-55981fafb000 r--p 00000000 08:01 4457385 /home/binwu/hello_world/hello_world
55981fafb000-55981fafc000 rw-p 00001000 08:01 4457385 /home/binwu/hello_world/hello_world
559820fd0000-559820ff1000 rw-p 00000000 00:00 0 [heap]
7fd96e90a000-7fd96eaf1000 r-xp 00000000 08:01 531557 /lib/x86_64-linux-gnu/libc-2.27.so
7fd96eaf1000-7fd96ecf1000 ---p 001e7000 08:01 531557 /lib/x86_64-linux-gnu/libc-2.27.so
7fd96ecf1000-7fd96ecf5000 r--p 001e7000 08:01 531557 /lib/x86_64-linux-gnu/libc-2.27.so
7fd96ecf5000-7fd96ecf7000 rw-p 001eb000 08:01 531557 /lib/x86_64-linux-gnu/libc-2.27.so
7fd96ecf7000-7fd96ecfb000 rw-p 00000000 00:00 0
7fd96ecfb000-7fd96ed22000 r-xp 00000000 08:01 529088 /lib/x86_64-linux-gnu/ld-2.27.so
7fd96ef00000-7fd96ef02000 rw-p 00000000 00:00 0
7fd96ef22000-7fd96ef23000 r--p 00027000 08:01 529088 /lib/x86_64-linux-gnu/ld-2.27.so
7fd96ef23000-7fd96ef24000 rw-p 00028000 08:01 529088 /lib/x86_64-linux-gnu/ld-2.27.so
7fd96ef24000-7fd96ef25000 rw-p 00000000 00:00 0
7fff6ef93000-7fff6efb4000 rw-p 00000000 00:00 0 [stack]
7fff6efc8000-7fff6efcb000 r--p 00000000 00:00 0 [vvar]
7fff6efcb000-7fff6efcd000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
binwu@ubuntu:~/hello_world$ cat /proc/56866/maps
55981f8fa000-55981f8fb000 r-xp 00000000 08:01 4457385 /home/binwu/hello_world/hello_world
55981fafa000-55981fafb000 r--p 00000000 08:01 4457385 /home/binwu/hello_world/hello_world
55981fafb000-55981fafc000 rw-p 00001000 08:01 4457385 /home/binwu/hello_world/hello_world
559820fd0000-559820ff1000 rw-p 00000000 00:00 0 [heap]
7fd96e90a000-7fd96eaf1000 r-xp 00000000 08:01 531557 /lib/x86_64-linux-gnu/libc-2.27.so
7fd96eaf1000-7fd96ecf1000 ---p 001e7000 08:01 531557 /lib/x86_64-linux-gnu/libc-2.27.so
7fd96ecf1000-7fd96ecf5000 r--p 001e7000 08:01 531557 /lib/x86_64-linux-gnu/libc-2.27.so
7fd96ecf5000-7fd96ecf7000 rw-p 001eb000 08:01 531557 /lib/x86_64-linux-gnu/libc-2.27.so
7fd96ecf7000-7fd96ecfb000 rw-p 00000000 00:00 0
7fd96ecfb000-7fd96ed22000 r-xp 00000000 08:01 529088 /lib/x86_64-linux-gnu/ld-2.27.so
7fd96ef00000-7fd96ef02000 rw-p 00000000 00:00 0
7fd96ef22000-7fd96ef23000 r--p 00027000 08:01 529088 /lib/x86_64-linux-gnu/ld-2.27.so
7fd96ef23000-7fd96ef24000 rw-p 00028000 08:01 529088 /lib/x86_64-linux-gnu/ld-2.27.so
7fd96ef24000-7fd96ef25000 rw-p 00000000 00:00 0
7fff6ef93000-7fff6efb4000 rw-p 00000000 00:00 0 [stack]
7fff6efc8000-7fff6efcb000 r--p 00000000 00:00 0 [vvar]
7fff6efcb000-7fff6efcd000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
很明显,在main运行的时候会现创建一个进程56865。当fpid = fork()执行完毕后,系统又创建了一个一模一样的子进程56866。如下图:
所以确切的说那个in parent process是在pid 56865的进程中打印的,in child process是在pid 56866的进程中打印的。然后各自进程中的count分别+1。所以看到的两次count,一次是在父进程中+1 打印,另一次是摘子进程中+1打印,且这两个count都是进程自己私有的,互不影响。
所以这里关键是,执行了fork()系统调用后在返回USER mode时,父进程的返回值是非零,而子进程却不是。
fork()父进程的返回值
fork系统调用内核的实现是do_fork,在do_fork结尾处会设置这个返回值。
1707 /*
1708 * Do this prior waking up the new thread - the thread pointer
1709 * might get invalid after that point, if the thread exits quickly.
1710 */
1711 if (!IS_ERR(p)) {
1712 struct completion vfork;
1713 struct pid *pid;
1714
1715 trace_sched_process_fork(current, p);
1716
1717 pid = get_task_pid(p, PIDTYPE_PID);
1718 nr = pid_vnr(pid);
1719
1720 if (clone_flags & CLONE_PARENT_SETTID)
1721 put_user(nr, parent_tidptr);
1722
1723 if (clone_flags & CLONE_VFORK) {
1724 p->vfork_done = &vfork;
1725 init_completion(&vfork);
1726 get_task_struct(p);
1727 }
1728
1729 wake_up_new_task(p);
1730
1731 /* forking complete and child started to run, tell ptracer */
1732 if (unlikely(trace))
1733 ptrace_event_pid(trace, pid);
1734
1735 if (clone_flags & CLONE_VFORK) {
1736 if (!wait_for_vfork_done(p, &vfork))
1737 ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
1738 }
1739
1740 put_pid(pid);
1741 } else {
1742 nr = PTR_ERR(p);
1743 }
1744 return nr;
1745 }
这里返回值是nr,可以看到nr通过pid_vnr(pid)会获取到pid值,然后fork返回时,这个nr就被带回到USER空间。所以父进程摘fork返回时,能拿到nr,就是子进程的pid号。那子进程从kernel返回到USER得到的是什么呢?这个就需要看fork在拷贝父进程调用的copy_thread函数了。
205 copy_thread(unsigned long clone_flags, unsigned long stack_start,
206 unsigned long stk_sz, struct task_struct *p)
207 {
208 struct thread_info *thread = task_thread_info(p);
209 struct pt_regs *childregs = task_pt_regs(p);
210
211 memset(&thread->cpu_context, 0, sizeof(struct cpu_context_save));
212
213 if (likely(!(p->flags & PF_KTHREAD))) {
214 *childregs = *current_pt_regs();
215 childregs->ARM_r0 = 0;
216 if (stack_start)
217 childregs->ARM_sp = stack_start;
218 } else {
219 memset(childregs, 0, sizeof(struct pt_regs));
220 thread->cpu_context.r4 = stk_sz;
221 thread->cpu_context.r5 = stack_start;
222 childregs->ARM_cpsr = SVC_MODE;
223 }
224 thread->cpu_context.pc = (unsigned long)ret_from_fork;
225 thread->cpu_context.sp = (unsigned long)childregs;
226
227 clear_ptrace_hw_breakpoint(p);
228
229 if (clone_flags & CLONE_SETTLS)
230 thread->tp_value[0] = childregs->ARM_r3;
231 thread->tp_value[1] = get_tpuser();
232
233 thread_notify(THREAD_NOTIFY_COPY, thread);
234
235 return 0;
236 }
这个函数主要是修改栈。因为进程(线程也一样)都有自己独立的栈区,所以即使完全拷贝了父进程,栈区还是要初始化成自己的。
这里会将新进程栈中的r0设置为0,这个就是关键!
因为根据ATPCS规则,r0寄存器是用来存放返回值的。子进程从kernel space返回USER时通过r0拿返回值,所以子进程拿到的就是0!