父子历程的故事:解读Linux中的fork机制
https://i-blog.csdnimg.cn/direct/b51621597a6146b3926f91c6a49f1c14.gif#pic_center前言
在Linux体系中,历程是操作体系最重要的执行单元,而父子历程的创建与管理更是体系资源分配和任务并行的关键。通过fork函数,Linux能够快速高效地复制一个历程,使得父子历程协同工作成为大概。理解父子历程的运行机制不但有助于掌握体系编程的核心技能,更能为优化资源利用与进步步伐性能提供理论底子。本文将带你从底子原理出发,剖析Linux父子历程的运行特性、fork的核心机制及其在实际开发中的应用。
一、历程PID
PID 是用来唯一标识一个历程的属性,我们可以使用 ps 指令检察一个历程的部分属性。历程的属性信息是由操作体系来维护的,这些信息被存储在一个 task_struct 结构体中,属于操作体系内核中的数据。由于操作体系本身是不相名誉户的,所以用户无法直接去访问 task_struct 对象中的成员,因此 ps 指令能够表现历程的属性信息,本质上是通过体系调用接口去实现的。
1.1 通过体系调用接口检察历程PID
获取历程的 PID 需要用到体系调用接口 getpid() ,该函数会返回调用该函数的历程的 PID,返回值类型为 pid_t 。如下图我们使用 man getpid 指令去检察 getpid 的底子文档:
https://i-blog.csdnimg.cn/direct/fbe5992033434fb69283d12db060a77c.png
注意上图中另有一个 getppid 是什么呢?不难猜到,这应该是用来获取父历程 PID 的体系调用接口,接下来我们写段代码来具象化 PID 吧。
注意上图中另有一个 getppid 是什么呢?不难猜到,这应该是用来获取父历程 PID 的体系调用接口,接下来我们写段代码来具象化 PID 吧。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
while(1)
{
printf("I am a process, my id is: %d, parent id is: %d\n", getpid(), getppid());
sleep(1);
}
return 0;
}
我们可以写一个脚原来实时获取上面这段代码执行起来后的历程信息。
https://i-blog.csdnimg.cn/direct/8eccfc20a16f48e28eccd12e4c725b93.png
https://i-blog.csdnimg.cn/direct/1286a23aa8a84a04895fbbbd26165342.png
可以看到,我一个将这段代码执行了两次,每一次的子历程 PID 都在发生变化,但是父历程的 PID 从未更改。
为了包管数据的准确性,我们再使用 ps 指令对比以下获取到的历程 PID 是否真的一样。
while :; do ps axj | head -1 ; ps axj |grep process | grep -v grep ;sleep 1 ; done
https://i-blog.csdnimg.cn/direct/762f01951bb446beafb1486809902870.pnghttps://i-blog.csdnimg.cn/direct/34260efbd18546f29bac8dab2200d6bc.png
结论:我们用 getpid 和 getppid 得到的父子历程的 PID 和 ps 指令获取到的历程 PID 是一样的
二、通过体系调用创建历程-fork初识
之前我们本身创建历程都是通过写一份源代码,然后去编译运行,终极得到一个历程,本日给各人介绍另一种通过体系调用接口 fork 去创建历程的方式。一样的,我们使用 man fork 去检察一下 fork 的相干文档:
https://i-blog.csdnimg.cn/direct/b66d36c10f42428b8a73ccb5e0c339a7.png
大抵意思就是:fork 函数会以调用该函数的历程作为父历程去创建一个子历程.
https://i-blog.csdnimg.cn/direct/62eb15029a724879b4353ea7bcc67280.png
创建成功时,会在父历程中返回子历程的 PID ,在子历程中返回 0 。否则就在父历程中返回 -1 ,子历程创建失败。
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;
}
https://i-blog.csdnimg.cn/direct/f8c0bd0cb5b34c08aa20aa83ff3efa09.png
如上图所示,fork 背面的代码执行了两次!这是什么缘故原由呢?我们再写一段代码跑跑。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
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;
}
https://i-blog.csdnimg.cn/direct/ef7dfd69e7844c5d986999a0b7c2261a.png
通过结果我们可以得出,在上面的一份代码中 id 大于0和 id 等于0同时存在, if 和 else if 同时满足,并且有两个死循环在同时跑。这个现象说明此时肯定存在两个历程,即原来的 myprocess 历程和在 myprocess 历程中创建的子历程,因为在一个历程中 if 和 else if 是不大概同时满足的。这也符合 fork 函数创建子历程的目的,fork 函数创建子历程后,会从原来的一个执行流酿成两个执行流。
2.2 为什么fork要给子历程返回0,给父历程返回子历程 pid?
1. fork 返回值的计划目的
fork 是 UNIX 体系中用于创建新历程的核心体系调用。调用一次 fork,体系会“分裂”出两个历程:父历程和子历程。它的返回值有以下特点:
[*]在父历程中:fork 返回新创建的子历程的 PID,使得父历程可以通过该 PID 来管理和操作子历程(如使用 wait 或 kill 等操作)。
[*]在子历程中:fork 返回 0,标识本身是子历程,无需再通过 PID 区分。
这种计划的核心目的正如您提到的,用于区分差异执行流,即便父子共享同一套代码,也可以根据返回值选择性地执行差异代码。
2. 实际类比的深入解读
[*]父亲喊“儿子”:假如不区分,所有子历程都会响应,导致混乱。通太过配唯一的 PID,每个子历程可以被单独辨认。
[*]子历程喊“爸爸”:由于每个子历程只能有一个父历程,所以子历程通过调用 getppid() 即可找到其唯一的父历程。
3. 为什么子历程返回值为 0
[*]简单区分:子历程无需知道本身的 PID 来执行本身的任务,而只需通过返回值 0 知道本身是子历程。
[*]服从和逻辑一致性:假如子历程也返回本身的 PID,会引入额外的复杂性,而且父历程需要一个单独机制区分这些值。
2.3 一个函数是如何做到返回两次的?如何理解?
在调用 fork 函数之前就只有一个历程,我们先往返首一下什么是历程?历程 = 内核数据结构 + 代码和数据,其中的内核数据结构就是历程对应的 PCB 对象。
https://i-blog.csdnimg.cn/direct/9358e21e768c4596888c140e517360c8.png
历程的 PCB 对象会找到相应的代码和数据,然后 CPU 就要去调理这个历程,也就是找到该历程的代码和数据去执行。调用 fork 函数创建子历程,本质上是操作体系多了一个历程,因此 fork 函数创建出来的子历程,它要先创建本身的 PCB 对象,子历程的 PCB 对象大部分都是以父历程的 PCB 对象为模板创建的,即从父历程的 PCB 对象中拷贝过来,再对部分属性稍作修改,子历程的 PCB 对象就有了。但是它没有本身的代码和数据,所以只能用父历程的,所以 fork 函数之后,父子历程的代码共享,这就表明了为什么上面 fork 函数之后的代码输出了两次,实在就是父子历程各自执行了一次。
创建子历程的目的就是为了资助父历程做差异的事情,但是父子历程共享一份代码,所以我们应该在代码中对它们加以区分。fork 函数就帮我们完成了这个需求,它会在父子历程中返回差异的值,用户只需要根据返回值的差异让父子历程执行差异的代码。
fork 函数的实现过程:
pid_t fork():
[*]创建子历程
[*]创建子历程的PCB
[*]添补PCB对应的内容
[*]让子历程和父历程指向同样的代码
[*]此时父子历程都有独立的task_struct对象,可以被CPU调理运行了
[*]return ret;
由于父子历程会共享一份代码,所以在 fork 函数执行 return 语句之前,子历程的 PCB 对象就已经被创建出来了,CPU 已经可以去同时调理父子历程。由于 fork 函数中的 return 语句也是被共享的,所以 fork 函数有两个返回值。
2.4 一个变量怎么会有差异的内容?
1. fork 的返回值如何写入差异的变量空间
当调用 fork 时,父历程与子历程会各自吸收一个返回值,并且写入同名变量 id。但这并不意味着他们共享同一块内存,而是因为:
[*]独立的历程地址空间
每个历程都有本身独立的假造地址空间。在 fork 之后,父历程与子历程的地址空间是彼此独立的。尽管子历程初始时看起来与父历程完全相同,但实际上它们的数据是分离的。
[*]写时拷贝(COW)机制
操作体系为进步服从并节省资源,采用了写时拷贝技术。在 fork 之后:
[*]父子历程共享同一份内存数据,直到有一方尝试修改这些数据。
[*]当某个历程试图修改数据时,操作体系会为该历程分配新的物理内存空间,并将被修改的数据复制到新分配的空间中。
2. fork 中变量 id 的本质
在代码中,变量 id 是存储 fork 返回值的地方。以下几点表明了为什么同名变量可以存储差异的值:
[*]父子独立运行
fork 返回后,父子历程的执行路径分开。父历程的 id 变量存储的是子历程的 PID,而子历程的 id 变量存储的是 0。
[*]差异的内存空间
由于父子历程的地址空间独立,id 实际上存在于两块差异的内存地区,即父历程的 id 和子历程的 id 是完全独立的变量。
[*]赋值过程
fork 的返回值通过操作体系写入到父子历程各自的 id 变量中:
[*]父历程在 return 时向 id 写入子历程的 PID。
[*]子历程在 return 时向 id 写入 0。
结语
Linux父子历程的运行机制展示了操作体系计划的高效性与机动性。从fork的返回值计划到写时拷贝(COW)的优化方案,这一切都体现了Linux在性能与资源利用上的奇妙平衡。通过深入理解父子历程的特性,不但能够提升体系编程的能力,还能为并发和并行步伐计划提供坚实的理论支持。希望本文能为你的学习和实践带来启发,在Linux体系的探索中迈向更高的条理。
https://i-blog.csdnimg.cn/direct/08aa28daeaf84238b6923679665d72dd.gif#pic_center
本日的分享到这里就竣事啦!假如以为文章还不错的话,可以三连支持一下,17的主页另有许多风趣的文章,欢迎小搭档们前去点评,您的支持就是17进步的动力!
https://i-blog.csdnimg.cn/direct/fee4a8a1f2904e6680b150b45983b0ab.png#pic_center
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]