LInux: fork()究竟是如何工作的?为何一个变量能够接受两个返回值?

本文详细解析了Linux中的fork函数,包括其工作原理、为何存在两个返回值、子进程创建过程、父子进程关系以及创建失败的原因。通过实例展示了fork函数的用法和实际操作效果。
摘要由CSDN通过智能技术生成

前言

 在Linux中,创建进程有两种方式:

  1. 在命令行中直接启动进程。例如在命令行中输入pwd、tar等命令后,操作系统会直接加载,运行相应的进程。
  2. 通过代码创建进程 —— fork()函数。

 在Linux中,查看进程最常用的两种方式:

  1. 使用ps axj 或 ps aux指令查看进程。
  2. 使用ls /proc指令查看当前已加载到内存中所有的进程。

一、fork()用法

 fork()是一个系统调用接口函数

在这里插入图片描述

 fork()的原型是pid_t fork(void),其中pid_t是一个整型int的别名
 使用fork(),系统会以当前进程为模板,创建子进程(创建子进程时,OS会将当前进程的大部分属性来初始化子进程,而对于子进程的pid、ppid等属性则是单独实现)。同时对于父进程,fork()函数会返回子进程的pid;而对于子进程来说,fork()函数直接返回0!并且文档中明确表示fork()函数创建进程通常是成功的,所有返回值为负数一般忽略。

二 、fork()应用实例展示

 下面我们在process.c中有这样一段代码:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
 
int main()
{//getpid()、getppid()都是系统调用,分别返回当前进程的pid和ppid
     printf("我是一个进程,我的pid:%d, ppid:%d\n", getpid(), getppid());
	 pid_t id = fork();
     if(id == 0)
     {
       while(1)
        {
             printf("我是子进程,我的pid:%d, ppid:%d\n", getpid(), getppid()); 
             sleep(1);
         }
     }
     else if(id > 0)
     {
         while(1)
         {
            printf("我是父进程,我的pid:%d, ppid:%d\n", getpid(), getppid());
             sleep(1);
         }
     }
     else
     {//返回失败
     	return 1:
     }
     return 0;
}

 我们用Makefile、make对上述代码进行编译、运行看看会发生什么?

【动画展示】:
请添加图片描述

 我们发现fork()前的代码只执行了一次(一定是父进程执行),但fork()后的两个判断条件中的代码都执行了(即两个while循环)这也进一步说明了fork()后,操作系统会创建一个子进程!!

tips:

  • 上述展示视频中左边是一个监视进程的脚本,可以不断循环打印目标进程的相关属性信息。脚本如下:(如果需要监视其他进程信息,只需将脚本中的myprocess改成目标可执行程序即可!
while :; do ps axj | head -1 && ps axj | grep myprocess | grep -v "grep"; sleep 1; echo "#########################################################"; done

三、fork()工作原理

3.1 为什么要创建子进程?

父进程创建子进程通常是希望子进程来协助父进程来完成一些工作,这些工作是单进程无法实现的。

 例如:用户在下载某一款软件时,同时存在播放该款软件的官方宣传视频的需求。这就需要子进程来协助父进程来达到边下载边播放的目的。即通过一定手段(通常时fork()的返回值),让父子进程分别执行不同的代码!

3.2 fork()究竟干了些什么?

 fork()创建进程后,OS中会多一个进程(子进程)。在创建过程中,Linux操作系统会为子进程创建对应的PCB,并用父进程的大部分属性来初始化子进程的相关属性(如子进程的pid、ppid、所在路径等属性信息则是单独实现)。最后将该进程链入到运行队列中,等待CPU的调度!!

 而进程 = 代码 + 内核数据结构和数据,并且进程间时是相互独立的。而进程中的代码和数据等是操作系统在创建该进程时,从磁盘上加载拷贝到内存中的。但创建的子进程是直接在内存中创建的,子进程并没有相应的代码和数据,那要怎么解决这个问题呢?

 实际上,代码只是用于读取的。所以Linux中fork()创建的子进程和父进程共用同一段代码。但对于数据来说,父/子进程间必然会存在差异(比如pid、ppid等)。同时为了保证父/子进程间的独立性,必须在父/子进程中各自独立私有一份。而在Linux中采用了写时拷贝的方式来解决这个问题

 下一个问题就是为啥我们在前面动画展示中,fork()创建出的子进程只执行fork()后的代码?在fork()前的代码子进程能否“看见”呢?

 答案是子进程能看见fork()前的所有代码!至于为啥子进程只执行fork()后的代码,这是由于代码运行过程中,存在诸如epi、pc等寄存器。这些寄存器中会保存当前指令要执行的下一条指令的地址。而fork()创建子进程过程中,父进程中pc、epi等寄存器的结果也“继承”给了子进程。所以才出现子进程只运行执行fork()函数后的代码!

3.3 fork为什么会存在两个返回值?

 fork()是一个函数,其存在返回值。fork()在执行是,操作系统内核做了如下工作:分配新的内存块和数据结构给子进程、将父进程的部分内核数据结构拷贝给子进程、添加子进程到系统进程列表中,调度器开始调度。

fork()函数执行完后,已经完成了创建子进程、将子进程添加到调度队列中等工作。当父进程和子进程在调度队列中被调度时,两个进程都需要执行return语句,都需要返回一个值!所以fork存在两个返回值!(操作系统通过寄存器来实现返回值返回两次)(真正原因其实在于地址空间的实现)

3.5 为何fork函数中父进程返回子进程的pid、子进程返回0?

 对于一个进程,其父进程是唯一确定的,但其子进程可能存在多个。就像我们生活中,一个孩子的爹是唯一确定的;但对于一个父亲,其可能存在多个孩子。
 而父进程和子进程之间是管理和被管理的关系。父进程为了更好的管理好子进程,所以fork函数在创建子进程后返回子进程pid;对于子进程来说,其只需管理好自身即可,所以返回0。

3.5 父进程和子进程谁先运行?

 我们已经看到了fork()函数会创建一个子进程。创建完子进程后,子进程会被加载链接到系统进程列表中等待CPU调度运行。
 至于父进程和子进程谁先被CPU调度是不确定的。进程被调度的先后顺序由各自的PCB中的调度信息(时间片 + 优先级等)+ 调度器算法确定。换句话说,父进程和子进程谁先运行是由操作系统决定的!

3.6 为何同一个变量接收两个返回值

 我们前面已经提到过了进程是相互独立的,为例保存fork()创建出的子进程和父进程之间的独立性,我们所采用的解决办法是:父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
在这里插入图片描述
 操作系统在创建子进程时,会先将父进程页表中的访问权限大部分改为只读权限。然后以父进程为模板,将数据拷贝给子进程。当子进程或父进程对数据试图修改数据,由于页表中的访问权限为只读。此时该行为会被操作系统识别到。并分为以下两种情况:

  1. 如果代码和数据所处区域本身就是只读的(如:代码区、字符常量区),此时OS认为该行为非法被拦截。
  2. 原本数据可读可写,但操作系统将对应页表中的访问权限设置为只读。当操作系统识别到该信息时,操作系统并不会将行为视为错误,而是作为重新申请内存拷贝内容的一种策略机制。此时操作系统会重新申请开辟空间,拷贝和修改数据,并修改页表中的映射关系。

四、fork创建子进程失败原因

在Linux中,fork()创建子进程失败通常由有以下两种原因:

  1. 系统中有太多的进程。操作系统内存不足。
  2. 实际上,用户使用fork()创建子进程是有次数限制的。当超过该范围时,fork创建子进程失败!
  • 23
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

独享你的盛夏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值