以进程的角度分析fork()为什么会“返回两次”?

前言

网上很多文章在介绍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!

猜你喜欢

转载自blog.csdn.net/rockrockwu/article/details/82862681
今日推荐