【操作系统】以fork()为例详解进程的创建过程与父子进程关系

目录

写在开头

1. fork()函数

2. 子进程的创建

3. 让父子进程执行不同的语句

4. 父子进程的并发执行

5. 父子进程的内存空间独立

6. 孤儿进程

写在最后

写在开头

   近期相对进程有更深入的理解,故重学操作系统,发现确实有很多值得学的知识。本文将以C语言实验为例,通过fork()函数详解进程的创建过程父子进程关系,理解fork函数干了什么、父子进程执行时的并发关系、父子进程运行时的内存独立、孤儿进程等。

   本文主要基于杨一涛老师的操作系统教程,许多展示理论的图片都源于该课程。杨老师对于操作系统的讲解深入浅出,很适合初学者学习(唯一的缺点可能就是课程的录制稍显粗糙,屏闪严重),教程详见:

【操作系统原理】考研 408 必备,坚持看完两集,学不会找 up 主_哔哩哔哩_bilibili

1. fork()函数

   首先解释一下C语言中的fork函数干了什么。

fork()函数:用于创建一个新的进程,创建的新进程是原来进程的子进程,创建的方法是将当前进程的内存内容完整的复制到内存的另一个区域(换句话说,原本的父进程执行到了代码的某个位置,fork(),创建的子进程也会从此位置开始执行,内存情况是完全相同的)。

返回值:如果子进程创建失败,返回值是-1。如果子进程创建成功,对于父进程而言,fork的返回值是子进程的pid(子进程的进程号);对于子进程而言,fork的返回值是0。

2. 子进程的创建

  下面通过一个简单的fork()实验演示子进程的创建方式,并理解fork函数对于父子进程不同的返回值。编辑一段C语言代码helloParentProcess1.c如下:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(){
        pid_t pid;      //parent_id
        pid_t cid;      //child_pid
        printf("Before fork Process id: %d\n", getpid());
        cid = fork();
        printf("After fork, Process id: %d\n", getpid());
        printf("fork_function return: %d\n", cid);
        pause();
        return 0;
}

  上述代码中的数据类型pid_t其实就相当于int,在头文件sys/types.h中重新定义了一下而已。getpid()函数用于返回当前进程的pid。代码末尾添加了函数pause()让父子进程暂停下来,否则难以观察其以运行状态。把代码用gcc编译一下,再运行,结果如下:

gcc helloParentProcess1.c -o helloParentProcess1

  由于有pause,程序不会结束,需要手动ctrl + c退出。为什么会输出两次 After fork...和fork_function...呢?别着急,我们慢慢讲讲整个代码的执行逻辑,注意由于存在并发问题,以下的描述并不是完全按照代码的执行顺序:

1.首先fork之前,输出当前进程的进程号20489

2.执行fork,创建子进程,子进程的内存状态就是fork时父进程的内存状态,因此子进程也会从fork之后的语句开始执行,子进程并不会输出before fork Process id...

3.对于子进程,其进程号是父进程的pid+1,即20490

4.父子进程是并发执行的,After fork和fork_function return这两句话都会执行,分别输出父子进程各自的进程号,和fork的返回值。而fork函数的返回值,对于父进程(20489),对应是上图中的第三行输出,返回子进程的pid(20490),而对于子进程(20490),返回值是0。

3. 让父子进程执行不同的语句

   由于fork的返回值在父子进程中是不同的,因此可以将fork的返回值作为if判断的条件,让父子进程执行不同的语句,代码helloParentProcess2_1.c如下:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(){
        pid_t pid;      //parent_id
        pid_t cid;      //child_pid
        printf("Before fork Process id: %d\n", getpid());
        cid = fork();
        if(cid==0){     //子进程会执行的代码
                printf("Child process id(my parent id is %d): %d\n", getppid(), getpid());
                for(int i=0; i<3; i++){
                        printf("Hello %d\n", i);
                }
        }else {         //父进程会执行的代码
                printf("Parent process id: %d\n", getpid());
                for(int i=0; i<3; i++){
                        printf("World %d\n", i);
                }
        }
        printf("After fork, Process id: %d\n", getpid());
        pause()
        return 0;
}

  这个代码的逻辑也很明显,fork前输出父进程的进程号pid,然后fork,在父进程中fork的返回值是子进程的pid,不为0,进入else分支,在else分支中输出父进程的pid,并打印三次world。对于子进程,fork的返回值是0,进入if分支,输出子进程对应父进程的pid(即函数getppid())已经子进程自身的pid,并打印三次hello。运行结果如下:

   果然如我们所料,根据fork的返回值使得一段程序同时执行了if语句的不同分支。同时由于有pause在,父子进程都没有结束,我们可以另起一个终端,查看当前终端存在的进程:

ps -al

  可以明显的看到父子进程同时存在,第二行是子进程32728,其父进程号PPID是33727。从父子进程的执行过程我们看到,貌似是先执行了父进程输出三个world,然后才执行了子进程输出三个hello,然而事实真的是这样吗?父子进程应该是并发执行的,也就是说if语句的两个分支应当是并发执行,在这个例子中我们好像没有看到并发的特点,下段代码将改进。

4. 父子进程的并发执行

  在上一段代码中,由于父子进程进入的条件分支的代码都很短,很快就执行完了,因此貌似好像是顺序执行的。如果我们让父子进程各自执行的代码量提升,在一个时间片内无法执行完,就可以看到父子进程切换的效果。简单起见,我们仅需要让循环语句的次数增加即可。比如设置到100,代码详见helloParentProcess2_2.c:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(){
        pid_t pid;      //parent_id
        pid_t cid;      //child_pid
        printf("Before fork Process id: %d\n", getpid());
        cid = fork();
        if(cid==0){     //子进程会执行的代码
                printf("Child process id(my parent id is %d): %d\n", getppid(), getpid());
                for(int i=0; i<100; i++){
                        printf("Hello %d\n", i);
                }
        }else {         //父进程会执行的代码
                printf("Parent process id: %d\n", getpid());
                for(int i=0; i<100; i++){
                        printf("World %d\n", i);
                }
        }
        printf("After fork, Process id: %d\n", getpid());
        pause()
        return 0;
}

编译后执行,可以发现父进程执行到第88次循环后就转而执行子进程,后面又切换回父进程:

 当然了,何时发生切换取决于CPU,读者的运行结果和我不完全相同也很正常,关键是我们看到了父子进程的并发关系。这里附一个杨老师课程中的图,理解并发和并行。

  简单的讲,并发就是同一时刻存在着多个不同的进程,而并行是同一时刻运行着多个不同的进程。本实验的父子进程就是上图中左图的并发情况。

5. 父子进程的内存空间独立

  fork会将当前进程的内存内容完整的复制到内存的另一个区域。因此创建的子进程和原本的父进程的内存空间是独立的,不会相互影响,这里我们可以通过一个简单的实验说明,代码helloParentProcess3.c如下:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(){
        pid_t pid;      //parent_id
        pid_t cid;      //child_pid
        printf("Before fork Process id: %d\n", getpid());
        // 验证父子进程的空间是独立的,value是各自的,互不影响
        int value = 100;
        cid = fork();
        if(cid==0){     //子进程会执行的代码
                printf("Child process id(my parent id is %d): %d\n", getppid(), getpid());
                for(int i=0; i<3; i++){
                        printf("Hello %d\n", value--);
                }
        }else {         //父进程会执行的代码
                printf("Parent process id: %d\n", getpid());
                for(int i=0; i<3; i++){
                        printf("World %d\n", value++);
                }
        }
        printf("After fork, Process id: %d\n", getpid());
        pause();
        return 0;
}

   编译后运行结果如下:

 这里定义了一个变量value,可以明显的看到,对于父进程和子进程,value是独立存在互不影响的,这是因为当fork将当前进程的内存内容完整的复制到内存的另一个区域时,会复制一个新的value变量到对应的子进程的内存区域,与父进程并不共享内存。

6. 孤儿进程

  孤儿就是没有父母。换句话将,如果父进程1创建子进程后2,父进程1ger~了(结束),子进程2还在运行,那么这个子进程2就会沦为孤儿进程。此时子进程的父进程就会被挂在到系统0中,而不是原本的那个挂掉的父进程1。这里做个实验,我们去掉最后的pause()语句,让父进程尽快执行完成ger~掉,看看子进程的情况:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(){
        pid_t pid;      //parent_id
        pid_t cid;      //child_pid
        printf("Before fork Process id: %d\n", getpid());

        // 展示孤儿进程
        cid = fork();
        if(cid==0){     //子进程会执行的代码,子进程执行之时可能已经是孤儿了!
                printf("Child process id(my parent id is %d): %d\n", getppid(), getpid());
                for(int i=0; i<3; i++){
                        printf("Hello %d\n", i);
                }
        }else {         //父进程会执行的代码,很快就会执行完挂掉
                printf("Parent process id: %d\n", getpid());
                for(int i=0; i<1; i++){
                        printf("World %d\n", i);
                }
        }
        return 0;
}

运行结果如下:

   注意在执行子进程代码时,输出的父进程id已经不是创建它的那个父进程(生父pid:50579)了,已经被“过继”给了进程1082,那1082是啥呢,我们可以ps -el显示所有进程:

ps -el

  pid 1082就是systemd,系统的进程,算是个初始的进程(根进程),我用的linux是kali,如果是其他系统可能得到的pid是1(而非1082),也对应的就是systemd。 由此我们可以看出,当一个进程沦为孤儿进程后,依旧会正常运行(不会和创建它的父进程一起ger~),但系统会将该进程托管到系统进程。如果不想让子进程沦为孤儿,可以按照如下的方法,在父进程中添加wait(null);等待子进程结束才返回,代码helloParentProcess4_2.c如下:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(){
        pid_t pid;      //parent_id
        pid_t cid;      //child_pid
        printf("Before fork Process id: %d\n", getpid());
        //避免让子进程沦为孤儿进程
        cid = fork();
        if(cid==0){     //子进程会执行的代码
                printf("Child process id(my parent id is %d): %d\n", getppid(), getpid());
                for(int i=0; i<3; i++){
                        printf("Hello %d\n", i);
                }
                sleep(3); //等待3s才返回
        }else {         //父进程会执行的代码
                printf("Parent process id: %d\n", getpid());
                for(int i=0; i<1; i++){
                        printf("World %d\n", i);
                        wait(NULL);  //等待子进程结束才返回
                }
        }
        return 0;
}

   我们专门让子进程等待3s才返回,同时父进程会等待子进程结束才返回,子进程不会沦为孤儿进程。编译后运行,效果如下:

   可以看到程序最后也等待了一小段时间才运行结束 ,且子进程的父进程号就是原本创建它的57202,子进程没有沦为孤儿进程。

写在最后

   操作系统还真是门挺重要的课程,以前没有系统学习过,感觉还是要补起来。本文以C语言实验为例,通过fork()函数详解进程的创建过程父子进程关系,理解fork函数干了什么、父子进程执行时的并发关系、父子进程运行时的内存独立、孤儿进程等。

   以后还会不定期更新一些计算机基础的文章,网安的文章也会慢慢更新,如果各位读者有什么问题也欢迎评论区指出,我一定知无不言。

  • 45
    点赞
  • 56
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值