【Linux】:进程创建与进程fork本质

朋友们、伙计们,我们又见面了,本期来给大家解读一下有关Linux进程创建与进程fork本质的知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!

C 语 言 专 栏:C语言:从入门到精通

数据结构专栏:数据结构

个  人  主  页 :stackY、

C + + 专 栏   :C++

Linux 专 栏  :Linux

目录

前言

1. 进程的创建

1.1 getpid与getppid 

1.2 getpid与getppid原理 

1.3 查看进程的第二种方式

1.4 fork创建进程

1.4.1 初识fork

1.4.2 fork的返回值

1.4.3 fork的一般写法 

2. fork原理(基本理解)


前言

回顾上文,我们可以通过系统调用getpid来获取当前进程的ID,同时也可以通过ps ajx 搭配grep 来查看我们运行起来的进程:

1. 进程的创建

在Linux中创建进程的方式:

1. 命令行中直接启动进程 --- 手动启动

2. 通过代码来创建进程

1.1 getpid与getppid 

启动进程的本质就是创建进程,这个新启动的进程一般是由父进程创建的! 

那么进程与进程之间就注定了存在一种关系:父子关系

通过之前的代码可以发现,在Linux中,登录之后,命令行启动的进程,它的进程id一直在变,但是它的ppid(父进程)一直不变,那么这个父进程是谁呢?

可以使用pa ajx 搭配 grep来查看一下这个进程到底是什么

居然是命令行解释器!!!

所以我们命令行启动的进程都是bash的子进程;

可以使用系统调用getppid来查看当前进程的ppid;

1.2 getpid与getppid原理 

那么getpid与getppid的原理是什么呢?

当进程被创建好之后,那么在操作系统内部就会为进程创建一个task_struct的内核数据结构(PCB),那么根据管理的先描述再组织,这个PCB里面肯定保存着该进程的pid与ppid,但是用户并不能直接访问操作系统得到它,需要通过上层系统调用接口来间接访问,因此使用getpid与getppid可以获取到PCB中的pid与ppid。

1.3 查看进程的第二种方式

Linux系统中的进程信息保存在/proc系统文件中,所以可以通过ls指令来查看一下这个系统文件:

可以看到有很多蓝色的数字,那么将我们自己写的程序运行起来,然后再查看系统文件

如果这样子感觉难找的话可以搭配grep指令

总结:Linxu系统中会存在一个proc系统文件,它是一个动态目录结构,用于存放所有存在的进程,目录名称就是以该进程的id来命名。

那么这个以进程id命名的目录里面保存着什么呢?

可以使用指令:ls /proc/进程id -l

里面最显眼的就是一个cwd和一个exe

① exe:可以发现这个exe正好指向的就是我们自己写的代码然后生成的可执行程序的路径。

当可执行程序能运行起来之后,我们可以在proc系统文件中找到这个可执行程序的路径,那么在程序运行期间,我们将可执行程序删掉之后,会发生什么呢?

当程序运行期间,将可执行程序删掉,可以发现:程序并没有被终止,还在正常运行,但是在系统文件中的exe被删掉了,这同样的证明了进程 != 可执行程序,那么为什么删掉可执行程序之后,运行起来的并不会收到影响?

可执行程序即使变成了进程,但是在它的PCB里面还会存储自己的可执行程序在哪个路径,当可执行程序运行之后,将可执行程序删掉,并不会影响已经运行起来的可执行程序,因为删掉的是磁盘中的数据,运行之后已经被加载拷贝到了内存中,但是系统文件proc中的exe已经被删掉了。

一个进程,可以找到自己的可执行程序。

② cwd: 当前目录(当前工作目录)

在一些文件接口中,我们通常会创建文件或者打开文件,当不给定制定路径时,打开或者创建的文件就在当前目录下,它的原理就是cwd在进程PCB中维护了一串路径,当需要在当前目录下打开或者创建文件时就会在前面自动拼接上cwd,默认情况下,进程启动时所处的路径就是当前路径。

更改当前目录的系统调用:chdir(指定路径)

每一个进程,都要有自己的工作目录

1.4 fork创建进程

如何理解启动一个进程呢?
启动一个进程,本质就是系统多了一个进程,操作系统要管理的进程也就多了一个,进程 = 可执行程序 + 内核数据结构(task_struct),创建一个进程就是要申请内存,保存当前进程的可执行程序 + task_struct对象,并将task_struct对象添加到进程的列表中。

1.4.1 初识fork

fork是创建进程的,那么也就是说,在fork之前只有一个执行流(只有父进程执行),fork之后会变成两个执行流(父进程和子进程都会执行)。

1.4.2 fork的返回值

失败返回-1,成功后会将子进程的pid返回给父进程,将0返回给子进程。

看到这里会发现很震惊的事情:一个函数居然会有两个返回值?(后面解释)

1.4.3 fork的一般写法 

1. 为什么要创建子进程?

        想让子进程协助父进程完成一些工作,这些工作是单进程解决不了的,例如边下载,边播放,这样做也可以提成效率和用户体验。

2. 创建子进程的目的就是为了执行和父进程不一样的代码。

3. 可以通过fork之后的返回值,让父子进程执行不同的代码片段。

4. fork之后,用if进行分流。

#include <stdio.h>
#include <unistd.h>

int main()
{
    pid_t id = fork();
    if(id < 0 ) return 1;
    else if(id == 0)
    {
        // 子进程
        while(1)
        {
            printf("我是一个子进程: pid: %d, ppid: %d, fork return id: %d, 我正在执行下载任务!\n",getpid(), getppid(),id);
            sleep(1);
        }
    }
    else
    {
        // 父进程
        while(1)
        {
            printf("我是一个父进程: pid: %d, ppid: %d, fork return id: %d, 我正在执行播放任务!\n", getpid(), getppid(), id);
            sleep(1);
        }
    }
    
    return 0;
}

2. fork原理(基本理解)

1. fork都干了写什么事情?

fork创建子进程,系统中会多一个子进程,创建子进程,会以父进程的PCB为模版,为子进程创建PCB,但是子进程没有属于自己的代码和数据,它会和父进程共享代码和数据!所以fork之后,父子进程会执行一样的代码。

那么这里就存在一个问题,那么既然是共享的代码和数据,在fork之前的代码子进程也是可以看到的,那么子进程为什么不会从头到尾将代码重新执行一遍呢?

我们都知道,程序是按照从上往下按照顺序执行的,那么为什么呢?当我们的程序在运行时,OS内会有一个寄存器pc指针/eip程序计数器,来记录代码执行到了哪一步,那么在pc/eip执行fork之后,eip指向的是fork后续的代码片段,那么既然父子进程共用代码和数据,同样的pc/eip也会被子进程继承下来。

2. 为什么fork给父进程返回子进程的pid,给子进程返回0?

一个父进程不仅仅只存在一个子进程,那么父进程与子进程之间的比例是:父 : 子 = 1 : n

所以父进程为了区分这么多的子进程,就需要一个特定的唯一值来标志每一个子进程,因此每创建一个子进程都会有一个唯一的子进程的pid返回给父进程,同样的子进程只有一个父进程,不需要特定的标记,所以返回0即可。

3. fork之后父子进程谁先运行

子进程创建完成只是一个开始,系统的其他进程、父进程和子进程都是要被调度执行的,这里就需要引入一个新的概念:运行队列;

当父子进程的PCB都被创建并且在运行队列中排队的时候,哪一个进程的PCB先被CPU选择调度,哪一个进程就先运行;

所以父子进程哪一个先运行是不确定的,由各自PCB中的调度信息(时间片、优先级等)+ 调度器的算法共同决定,简单的说就是由操作系统自主决定的。

父子进程执行后续代码的本质就是被CPU调度运行 

4. 为什么fork会有两个返回值?

fork是一个系统调用的接口,同时也是一个函数,那么在它的内部会完成子进程的创建工作。

fork之后代码共享,其实在创建子进程完成之后的代码就共享了,那么retutn语句也是一条代码,因此它也会被共享且执行,在创建完成子进程之前只有一个执行流执行return语句,那么在创建完成之后就有两个执行流来执行return语句了,所以子进程和父进程都会返回,因此fork就会有两个返回值了。(有助于理解)

其实真实的情况是:操作系统会通过一些寄存器的手段来做到返回值返回两次。

5. 如何理解同一个变量会有不同的值

当我们平时使用一些app,先打开QQ,再打开微信,然后再打开浏览器,此时将这些进程随便干掉一个,都是不会影响其他进程的运行情况的,同样的,在父子进程的关系中也是不会互相影响的。

 

所以,进程之间运行的时候是具有独立性的,无论是什么关系!

那么系统是怎么做到的呢?

首先表现在每个进程都有自己独一份的PCB;

进程之间不会互相影响彼此。

但是父子进程是共享代码和数据的,那么怎么能做到父子进程互不影响呢?

代码本身是只读的,不会修改,所以不会产生影响,但是数据是可以的!!!

比如我们的代码中存在一个全局变量ret,父进程运行的条件是ret大于0,那么如果在子进程中添加了修改ret的代码,将ret修改到小于0,那么父进程就会退出!!,所以这种情况是不被允许的,所以父子进程的代码可以共享,但是数据必须各自私有一份。

因此OS就使用写时拷贝的方法将父子进程的数据各自私有一份

return的本质就是写入,id是父进程定义的局部变量,那么它就一份数据,既然是数据,在return时会发生写时拷贝,所以才产生了同一个变量具有不同的值。

那么既然发生了写时拷贝,会有两个值,那么这两个指的地址是不是一样的呢?我们可以使用代码来看一下:

可以发现,地址相同的两个值居然值不一样,这怎么可能呢?唯一的解释:这个地址绝对不是物理地址!!!(后面的进程地址空间再详细介绍)

朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,欲知后事如何,请听下回分解~,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持!  

  • 12
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

stackY、

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

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

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

打赏作者

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

抵扣说明:

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

余额充值