目录
- 前言
- 1. pid && ppid
- 2. fork
- a. 为什么 fork 要给子进程返回 0, 给父进程返回子进程的 pid ?
- b. 一个函数是如何做到两次的?
- c. fork 函数在干什么?
- d. 一个变量怎么做到拥有不同的内容的?
- e. 拓展:fork()之后,父子进程谁先运行?
前言
该篇文章是继 添加链接描述 文章的后续,针对 linux 中的 task_struct 进程的 PID(也即标识符)介绍,和系统调用中的 fork 展开初步的认识。
task_ struct 内容分类:
标识符: 描述本进程的唯一标识符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。(其作用就相当于进程运行了一段时间后,因为系统调度等原因,停止了对该进程的执行而后续回来继续执行的时候,需要知道上次执行到什么地方了。也好比我们看书,今天看完不想看了之后,会在此处做一下标记,方便后续继续向下观看)
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针(比如记录了该进程所匹配的代码和数据的存储位置)
上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
I/O状态信息: 包括显示的I/O请求,分配给进程的 I/O 设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。(保证系统调度的公平等)
其他信息
1. pid && ppid
在前面的进程相关的文章,我们已经知道了,因为用户不擅长直接对操作系统进行访问,并且操作系统也不会相信用户,因此用户无法直接访问操作系统。而在上一篇文章的末尾,我们简单见过了进程的 PID,但是那是通过系统指令获取到的 PID,而作为用户在编程语言上,在无法访问操作系统拿到数据的前提下,如何获取进程的 PID 等进程信息呢?
// 测试demo
#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
while(1)
{
cout << "I am a process, my pid is " << getpid() << ", my parentId is " << getppid() << '\n';
sleep(1);
}
return 0;
}
左右对比,我们是可以得知,在我们通过 c / c++ 编写的程序,运行起来后,系统会自动会该进程创建一个 PID,并且在cpp 中预取这个 PID 和我们在系统中获取的 PID 是一致的。
所以,PID 有什么用呢?? ---- 既然 PID 是每个进程的唯一标识符,那么当我们对进程进行管理的时候,就可以通过其PID 对该进程进行操作,比如杀掉该进程 kill -9 20059
等操作。
而当我们结束上一次进程,重新运行我们的程序时,我们又会发现,它的 pid 不一样了,这也是正常的现象。但是我们可以发现,进程自己的 pid 会变,但是他的父进程的 pid 确不会变!一直是 2742,好奇心驱动我们去查看这个父进程,它到底是谁!
没错!2742 它就是我们的 bash,这个 bash 进程是由 xshell 为我们创建的一个命令行的进程!所以同理,当我们断开对 linux 的链接之后,再一次链接 linux,进程的 ppid 也会随之改变,即我们每一次通过xshell 远程连接我们的服务器时,xshell 都会为我们重新分配一个 bash 进程,用来给我们提供命令行服务!
2. fork
但是上述的这些进程,可都是操作系统给我们创建出来的。那么现在我要手动创建进程,该怎么做呢? ---- 调用 fork 系统函数。那么修改后的 demo 代码如下:
int main()
{
cout << "before the fork!\n";
fork();
cout << "after the fork!\n";
sleep(1)
return 0;
}
很明显,在执行完 fork函数之后,fork 之后的代码,被执行了 两次!为什么?最简单的回答就是,因为 fork 是创建进程的系统函数,因此在执行完 fork 之后,会有两个进程,同时执行这一份代码,所以代码一共被执行了两次!
但是上述仅仅是我们的猜测,为了更清楚的了解 fork 函数干了什么,我们需要通过 man 手册进一步了解 fork 函数!
以上是 fork 函数介绍中的一段信息,但是有一点奇怪的是!这个函数怎么可以返回两个值呢??据 man 手册的介绍,如果进程创建成功,返回这个进程的 pid 给它的父进程,返回 0 给自己;创建失败,则返回 -1 给父进程。这显然是我们无非理解的,在 C/C++ 语言当中,函数的返回值一直都只能有一个啊!而现在,这个 fork 函数,它告诉我有两个返回值!
多的不说,我们再来做一个 demo 测试,到底是不是真的这样,这个函数有两个返回值。
int main()
{
cout << "I am a process, pid: " << getpid() << ", ppid: " << getppid() << '\n';
pid_t pid = fork();
if(pid == 0)
{
// 子进程部分
while(1)
{
cout << "I am a child process, pid: " << getpid() << ", ppid: " << getppid() << "\n";
sleep(1);
}
}
else if(pid > 0)
{
// 父进程部分
while(1)
{
cout << "I am a parent process, pid: " << getpid() << ", ppid: " << getppid() << "\n";
sleep(1);
}
}
else
{
cout << "error\n";
}
return 0;
}
没错!就是这么神奇,如果站在语言上的认知,这是根本无法理解的现象,每个 if 分支里面都是一个死循环,但是,运行起来确出现了两个 if 分支里的内容!所以这可以进一步的说明了,fork 之后,会多一个进程,而这个进程是由原来的进程所创建出来的,它的 ppid (即父进程)就是创建它的进程的 pid !而 fork 作为系统调用接口,也会为创建出来的进程进行属性初始化(分配 pid 等操作)。所以站在系统的角度看待这一段代码的话,那么就能说明原本的进程,作为父进程,在执行完 fork 函数之后,接收到的是创建出来的子进程的 pid,因此会执行第一个 if 分支,而子进程自己接收到 0 的返回值,所以执行第二个 if 分支的内容。
一般而言,fork 之后的代码,是父进程与子进程共享的!所以返回值的不同,恰恰是为了区分,让不同的执行流执行不同的代码块!
接下来,我们要回答几个关于 fork 的问题。
a. 为什么 fork 要给子进程返回 0, 给父进程返回子进程的 pid ?
你知道的,父亲永远只有一个,而孩子可以有很多个,一个孩子也不可能同时有两个父亲。因此在操作系统中同理,一个父进程可以有多个子进程,但是不可能存在一个子进程有多个父进程!所以把子进程的 pid 给父进程,是为了让父进程可以明确的找到它的子进程,而子进程永远只有一个父进程,就不用谈找不到这件事了。假设今天父进程有10个子进程,但是它并不知道这个进程跟那个进程的 pid,也就无法明确指定操作其中某一个子进程了!再者,父进程在创建时,父进程有自己的内核 pcb 数据结构,也有自己的代码和数据,那现在父进程创建出一个子进程之后,系统会为子进程其分配一个 pcb,这没问题,但是子进程应该执行什么样的代码和访问什么样的数据呢? 而开始创建子进程是时候,子进程是没有自己的代码和数据的,所以子进程只能与父进程共享一样的代码(数据另谈)!那问题又来了,当 cpu 在调度的时候,父进程在执行这一份代码,子进程也是执行的这一份代码,这也是上面实验时,fork 之后的代码会重复执行两遍的原因。那这有什么意义呢?或者说,同样的代码为什么要执行两遍呢?所以问题就回归到了 我们为什么要创建这个子进程?! 所以一定是为了让父进程和子进程执行不同的代码,完成不同的工作!所以就需要让 fork 具有不同的返回值,才能达到区别不同的执行流!
b. 一个函数是如何做到两次的?
既然 fork 是用于创建进程的一个系统调用函数,而站在系统层面上,创建进程就要为其创建一个 pcb,并且每个进程需要有与其匹配的代码和数据,这是系统在创建进程时需要做的工作。那么我们就不难猜测,fork 在干什么。
pid_t fork(void)
{
创建子进程
填充 PCB 对应的内容
让父子进程共享同一份代码
到这一步,父子进程都用拥有了自己独立的 task_struct(即 PCB),可以被 cpu 调度运行了
......
return ret;
}
我们知道的是,子进程被创建出来之后,父子进程会共享代码,因此 fork 之后的代码才会被执行两次。现在的问题就是那么 return ret
是不是一条代码? ---- 它是代码是不争的事实!又因为在 return 之前子进程就已经被创建了,并且也完成了其 pcb 的各种初始化工作,同父进程一样拥有独立的 task_struct,所以既然在 return 语句的时候,子进程就已经完全存在了,那么 return 语句就自然是会被父子进程都执行!所以父进程返回一次,子进程返回一次,这个函数一共就返回了两次!
c. fork 函数在干什么?
其实这个问题在上面的介绍中,就已经不难得知了。fork 就是在创建一个进程,并且用其父进程对应的字段来初始化子进程,而因为子进程刚创建出来,没有自己的代码和数据,所以就需要和父进程共享同一份代码(数据另谈)。我们都知道,之所以可以共享代码,是因为代码存储在系统的常量区,它是不能够被修改的,因此父进程并不会因为与子进程进行代码共享而受到影响。但是数据就不一定,数据是可以被修改的!所以假如父进程和子进程共享同一份数据,然后子进程需要对其中的数据进行修改,这个数据又恰恰是父进程的某一个条件判断所需的数据,这就势必会影响到父进程的正常运行!那怎么办呢??所以子进程可以将父进程的数据拷贝一份给自己独立使用,自己怎么修改都不会影响到父进程。但是问题又来了,假设父进程当中有100个全局变量数据,而将来子进程只需要用到其中一个,其它的 99 个甚至更多都不需要修改,这样就会导致系统资源变少,利用率也变低。而实际上,被创建出来的子进程,需要修改到的数据其实是不大的,基本不可能有子进程需要修改父进程的全部数据。因此在面对数据方面,子进程则是采用了 写时拷贝 的策略,当子进程与父进程进行数据共享之后,子进程需要修改数据,系统就会为子进程开辟一块属于子进程自己的空间,并且将要修改的数据拷贝一份,供子进程修改和使用,子进程需要多少空间,系统就开辟多少,而后续子进程访问的也是自己的数据,这样父子进程就不会互相影响(因为我们需要保证让进程之间相互独立,互不影响。总不能是今天我csdn网页的进程奔溃了,连同我的音乐进程也奔溃了吧)。
d. 一个变量怎么做到拥有不同的内容的?
这个问题对 b 问题的一个延续,在上面的问答中,我们已经能够得知,一个函数之所以可以返回两个值,是因为在 return 的时候,子进程就已经被创建出来了,并且开始与父进程共享代码;在数据层面上,子进程并不是完全与父进程进行数据共享,而是采用了 写时拷贝 的策略。所以现在需要确定的是,return 是不是一种对数据的修改,或者是算不算数据的写入? ---- return 将数据进行返回,所以有数据,就必须要接收,也即数据的写入,而写入就是修改的范畴,所以当系统检测到子进程要对父进程的数据进行修改的时候,就会为其开辟一块自己的空间供子进程使用。所以站在系统层面上,这个变量数据在内存中存在两份,一份是父进程的,一份是子进程的,所以当我们站在语言的角度上,才看到了一个变量拥有两个不一样的值。
e. 拓展:fork()之后,父子进程谁先运行?
如果大家有这方面的疑惑的话,就需要深入了解在系统调度器方面的知识,因为谁先运行,这是调度器决定的!在系统层面上,谁先运行,取决于调度器决定先将哪个进程提携给 cpu 执行。而每个系统的调度原理都不太一样,所以这方面又是一门足以压死人的学问,小篇也是无能为力。
进程介绍到这里的时候,还远远没有结束,我们现在只是弄清楚进程是什么,以及与进程相关的系统调用 fork,但是进程还会有所谓的状态,比如进程等待,堵塞等等。但是由于篇幅问题,关于进程的状态等方面的信息,小篇会在后续文章中一一介绍。
如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!
感谢各位观看!