【Linux取经路】揭秘进程的父与子

简介: 【Linux取经路】揭秘进程的父与子

6800824df19847578603dd7007100ff2.gif

1、进程PID

在上一篇文章(【Linux取经路】进程的奥秘)的结尾提到过,PID 是用来唯一标识一个进程的属性,我们可以通过 ps 指令来查看进程的部分属性。因为进程的属性信息是由操作系统来维护的,这些信息被存储在一个 task_struct 结构体对象中,属于操作系统内核中的数据,操作系统本身是不相信用户的,所以我们无法直接去访问一个 task_struct 对象中的成员,因此 ps 指令显示进程的属性信息,本质是通过系统调用接口去实现的。

1.1 通过系统调用接口查看进程PID

要获取进程的 PID 需要用到系统调用接口 getpid()。该函数会返回调用这个函数的进程的 PID。返回值类型是 pid_t。

a7ff78fc86104de38ecec2709448f70b.png

#include <stdio.h>    
#include <unistd.h>    
#include <sys/types.h>    
int main()    
{    
    while(1)    
    {    
        printf("我的PID是:%d\n",getpid());                                                                                                                                                   
        sleep(1);    
    }    
    return 0;    
}

我们可以写一个脚本来实时获取上面这段代码执行起来后的进程信息。

while :; do ps axj | head -1 ; ps axj |grep process | grep -v grep ; echo "---------------------------------------------------------------"; sleep 1 ; done

8bea3cad65af41829a05488fc006229b.gif

通过动图可以看到,我们用 getpid 得到的进程 PID 和 ps 指令获取到的进程 PID 是一样的,都是15058,并且当我们终止掉进程后 ps 右边也监测不到 process 进程了。

小Tips:一个进程属性中除了有自己进程的 PID 还有父进程 PID,ps指令查询结果中的 PPID 就是当前进程父进程的 PID。我们亦可以通过系统调用接口 getppid 来获取父进程的 PID。

1.2 父进程与子进程

ecaec4a61f0e4ba1b462965beb062940.png

对比上面这张图片上的 PPID 和动图中 ps 指令查询到的 PPID,可以发现,终止掉 process 进程,再重新启动,操作系统系统会给它重新分配一个 PID ,第一次是15058,第二次是22389,但是 process 进程的父进程的 PID 始终都没有发生变化。一直都是12456。下面我们通过 ps 指令来查看一下12456这个进程究竟是什么?

ps axj |head -1 ; ps axj | grep 12456

ecd994c03b2149b09a2cd9ed0bff5487.png

12456是 bash 进程的 PID,bash 是命令行解释器,它会将用户输入的指令翻译给操作系统核心(kernel)去处理,而指令本质上就是可执行程序。因此我们可以得出一个结论:当我们在命令行输入指令去执行的时候,bash会帮我们创建一个子进程去执行该指令。子进程出问题了是不会影响到父进程的。

小Tips:当我们退出 Xshell 再重新登陆,系统会重新为我们分配一个 bash 进程。

2、通过系统调用创建进程-fork初始

之前我们自己创建进程都是通过写一份源代码,然后去编译运行,最终得到一个进程,今天给大家介绍另一种通过系统调用接口 fork 去创建进程的方式。


124774d8591143239f195f2f4cc9987d.png

2.1 调用fork函数后的现象

#include <stdio.h>      
#include <unistd.h>      
#include <sys/types.h> 
int main()                                                                                                                                                                                    
{    
    printf("before:only one line\n");    
    fork();    
    printf("after:only one line\n");    
    return 0;    
}

b0a02494c6764297946db151c0ba47cb.png

通过结果可以看出,fork 后面的代码执行了两次。在 fork 之前只有一个执行流,fork 之后变成了两个执行流。

int main()    
{    
    printf("begin:我是一个进程,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    
    {    
        perror("子进程创建失败!\n");    
    }                                                                                                                                                                                         
    return 0;    
}

80e7a1a4534a4fe6a336713b67056db1.png

通过打印结果我们可以得出,在上面的一份代码中 id 大于0和 id 等于0同时存在, if 和 else if 同时满足,并且有两个死循环在同时跑。这个现象说明此时一定存在两个进程,即原来的 process 进程和在 process 进程中创建的子进程,因为在一个进程中 if 和 else if 是不可能同时满足的。这也符合 fork 函数创建子进程的目的,fork 函数创建子进程后,会从原来的一个执行流变成两个执行流。

2.2 为什么fork给子进程返回0,给父进程返回pid?

一般而言,fork 之后的代码父子共享。返回不同的返回值,是为了区分不同的执行流,让不同的执行流去执行不同的代码块。简单来说就是为了区分父进程和子进程,让父进程和子进程去执行不同的任务,子进程创建出来的目的就是帮助父进程完成一些工作,如果返回值相同那 fork 函数后面的代码父进程和子进程都会执行,那么我们创建子进程的意义何在?

先解释了为什么 fork 函数有两个返回值,下面再来解释为什么 fork 给子进程返回0,给父进程返回 pid。已现实生活为例,一个父亲可以有多个孩子,而一个孩子一定只有一个父亲,父亲给每个孩子都取了不同的名字,名字就成了一个区分孩子的属性,将来父亲就可以通过名字去对孩子做管理。这里的进程也一样,一个父进程可以创建多子进程,将来在父进程中可能要对这多个子进程做管理,为了区分不同的子进程,因此当 fork 函数成功创建子进程后,会给父进程返回子进程的 pid。而子进程要找到父进程只需要调用 getppid 函数即可,所以当 fork 函数成功创建子进程后,只需要给子进程返回一个0用来标识创建成功即可。

2.3 fork函数是如何做到返回两次的?

在调用 fork 函数之前就只有一个进程,我们先来回顾一下什么事进程?进程 = 内核数据结构 + 代码和数据,其中的内核数据结构就是进程对应的 PCB 对象。



913ce207a0214643a9c8874b448dd7b6.png

进程的 PCB 对象会找到对应的代码和数据,然后 CPU 就要去调度这个进程,也就是找到该进程的代码和数据去执行。调用 fork 函数创建子进程,本质上是操作系统中多了一个进程,因此 fork 函数创建出来的子进程,它一定要先创建自己的 PCB 对象,子进程的 PCB 对象中的属性大部分都是以父进程的 PCB 对象为模板创建的,即直接从父进程的 PCB 对象中拷贝过来,再对部分属性稍作修改。子进程的 PCB 对象有了,但是它没有自己的代码和数据,它只能使用父进程的代码,因此 fork 函数之后,父子进程的代码是共享的。这就解释了上例中为什么 fork 函数后的代码执行了两次,其实本质上是父子进程各执行了一次。

小Tips:这里只说了子进程会共享父进程的代码,至于子进程的数据怎么处理将在下文为大家揭晓谜底。

我们创建子进程的目的就是为了让父子进程执行不同的事情,而父子进程会共享同一份代码,因此我们需要在代码中对父进程和子进程加以区分,好让它们能够执行不同的代码块。fork 函数就帮我们实现了这个需求,它会在父子进程中返回不同的值,用户只需要去判断 fork 函数的返回值就可以让父子进程执行不同的代码块。

fork 作为一个系统调用接口,它本质是一个函数,在操作系统内一定有它的具体实现方法,当我们调用 fork 函数的时候,我们是进到 fork 函数的内部去创建子进程。

ad06e3faa7c943a4a69d13bcd4df834b.png


上面说过,父子进程会共用同一份代码,fork 函数在执行 return 语句之前子进程的 PCB 对象就被创建出来了,CPU 已经可以同时去调度父子进程。因此 fork 函数中的 return 语句也是被父子进程所共享的,这就是为什么 fork 函数有两个返回值,本质上是因为父子进程共用同一份代码导致的,父进程会执行 return 返回一个值,子进程也会执行 return 返回一个值。

2.4 一个变量怎么会有不同的值?

fork 函数有两个返回值现在可以理解了,那一个用来接收 fork 函数返回值的变量 id 怎么可能同时表示两个不同的值呢? 这里需要给大家引入一个概念,任何平台,进程在运行的时候,是具有独立性的。即一个进程退出了、崩溃了、出问题了是不会影响其他进程的。那父进程和子进程也是两个进程,它们也是彼此独立的,这就要求父子进程绝对不能访问同一份数据,因为数据在代码执行的过程中可能被修改,因此对于子进程来说,他要把父进程中的数据单独拷贝一份,这个过程是由操作系统来帮我们完成的。子进程可以把父进程中的数据全部拷贝一份,但是子进程可能对拷贝的绝大部分数据都没有访问,这就会造资源的浪费。因此一般的操作系统并不会给子进程把父进程的所有数据全部拷贝一份,而是执行数据层面的写时拷贝,即子进程在被创建后,它会共享父进程的代码和数据,当子进程需要修改父进程的某一数据时,操作系统会检测到并及时的出来制止子进程,说:“你先别急着修改,我先给你重新开一块空间,把数据重新拷贝一份,你去修改拷贝的那一份”。这种用多少拷贝多少的方法会提高系统资源的利用率。

fork 函数在执行 return 返回的时候不就是想往 id 变量里面写入嘛,父进程 return 一次,子进程 return 一次,子进程会执行写时拷贝,这就是为什么一个 id 变量有两个不同的数据,本质上是因为有两块空间。至于为什么同一个变量名可以让父子进程看到不同的内容,这个问题将在后面介绍的地址空间的时候再给大家揭晓,感兴趣的朋友可以先点一个关注👀。

小Tips:共享代码并不影响独立性,因为代码在加载到内存之后是不可能发生改变的。

2.5 fork接口总结

  • fork 有两个返回值。
  • 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)。

2.6 子进程创建后,父子进程谁先运行?

一般而言,一旦一个进程被创建出来,以打开QQ音乐为例,在操作系统层面该进程什么时候被运行我们作为用户是管不了的,我们只负责使用即可,操作系统在底层会去调度每一个进程,至于如何调度我们作为用户也无需关心。因此子进程被创建出来后,父进程和子进程究竟谁先执行是由调度器来决定的,所以谁先谁后并不一定。

小Tips:所有的进程在操作系统中会以链表的形式被组织起来,对进程的管理就变成了对链表的增删查改。挑选一个进程放到 CPU 中去运行,这个工作是由调度器去做的,调度器一般会做到公平调度。一般的计算机中只有一个 CPU,而进程却可能有很多个,这就注定了 CPU 是一个少量的资源,对所有的进程来说,运行的本质,就是把它放到 CPU 上,所以所有的进程,对 CPU 资源本质上是一种竞争关系,此时就必须要有调度器的存取,去保证每个进程被公平的调度。

2.7 此时再来理解bash

bash 作为命令行解释器它自身就是一个进程,而我们在 bash 命令行输入的指令本质上是一个可执行程序,最终加载到内存中也会变成进程,因此在 bash 的源代码实现中一定会调用 fork 这个系统接口去创建子进程,让自己继续去执行命令行解释,让创建出来的子进程去执行我们输入的指令。所以我们在 bash 命令行输入的所有指令,最终执行起来加载到内存变成进程后,都是 bash 进程的子进程。

3、结语

今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,春人的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是春人前进的动力!

目录
相关文章
|
15天前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
本文旨在探讨Linux操作系统中的进程管理机制,包括进程的创建、执行、调度和终止等环节。通过对Linux内核中相关模块的分析,揭示其高效的进程管理策略,为开发者提供优化程序性能和资源利用率的参考。
40 1
|
3天前
|
存储 监控 Linux
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
38 13
|
10天前
|
SQL 运维 监控
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
|
18天前
|
运维 监控 Linux
Linux操作系统的守护进程与服务管理深度剖析####
本文作为一篇技术性文章,旨在深入探讨Linux操作系统中守护进程与服务管理的机制、工具及实践策略。不同于传统的摘要概述,本文将以“守护进程的生命周期”为核心线索,串联起Linux服务管理的各个方面,从守护进程的定义与特性出发,逐步深入到Systemd的工作原理、服务单元文件编写、服务状态管理以及故障排查技巧,为读者呈现一幅Linux服务管理的全景图。 ####
|
1月前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
137 4
linux进程管理万字详解!!!
|
23天前
|
缓存 算法 Linux
Linux内核的心脏:深入理解进程调度器
本文探讨了Linux操作系统中至关重要的组成部分——进程调度器。通过分析其工作原理、调度算法以及在不同场景下的表现,揭示它是如何高效管理CPU资源,确保系统响应性和公平性的。本文旨在为读者提供一个清晰的视图,了解在多任务环境下,Linux是如何智能地分配处理器时间给各个进程的。
|
1月前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
75 8
|
1月前
|
网络协议 Linux 虚拟化
如何在 Linux 系统中查看进程的详细信息?
如何在 Linux 系统中查看进程的详细信息?
65 1
|
1月前
|
Linux
如何在 Linux 系统中查看进程占用的内存?
如何在 Linux 系统中查看进程占用的内存?
|
1月前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
70 4
下一篇
DataWorks