【Linux】进程概念、fork() 函数 (干货满满)
创始人
2024-06-03 10:41:24
0

文章目录

  • 📕 前言
  • 📕 进程概念
  • 📕 Linux下查看进程的两种方法
    • 方法一
    • 方法二
  • 📕 pid() 、ppid() 函数
  • 📕 fork() 函数、父子进程
    • 初识
    • 再理解
  • 📕 fork做了什么
  • 📕 如何理解 fork 有两个返回值


📕 前言

我们已经知道,操作系统进行“管理”的本质是——先描述、再组织。描述是一个面向对象的过程,组织是使用数据结构的过程。而进程也是被操作系统管理的,操作系统也要对其先描述,后组织。
描述无疑就是将进程的各种信息存储在一个结构体里面,描述好之后,将描述进程的结构体用链表(也可能是其他)维护,所以查找、删除进程等等的操作,就变成了对链表的增删查改。所以,管理者实际上不需要和被管理者进行直接交流,只需要对被管理对象的数据做管理!!

操作系统对进程的管理也和上面一样,要“先描述,后组织”。

📕 进程概念

当我们打开QQ、微信 ,是为了和朋友聊天,刷朋友圈等;打开vscode,是为了进行编程或者其他操作……任何启动并运行程序的行为,操作系统都会将程序转换为进程,从而完成特定的任务!!!

如下,磁盘中有某个可执行文件(由于是在磁盘中,所以该文件关机再开机还是保存着的)。可执行文件本质上还是一个文件,也有文件的内容和属性。当我们在Linux 环境下使用 ./a.out 的时候(假设该文件名为 a.out),该文件对应的代码和数据(文件的内容)就会被加载进内存,所以CPU才能访问该文件的代码和数据。
但是,这样就算创建一个进程了吗?
请添加图片描述

答案是否定的。以学校为例子,如何才能成为一个学校的学生,只要进入学校,在学校自由行走就算是学生了吗?当然不是啦,不然学校的保安、保洁阿姨、参观人员岂不都是学生。是不是学生,最重要的就是学校是否对你进行“管理”——即学校是否拥有你的学籍档案,你的学生信息是否在学校的教务系统里面。

所以,创建一个进程,不仅要将代码和数据加载到内存里面,还要将进程的一些信息存储在结构体里面,并用某种数据结构进行维护,让操作系统进行管理

如下图,我们绝大多数情况下,都不会只运行一个程序,所以也不会只创建一个进程。用来描述进程的结构体,在Linux环境下被称作 task_struct (早期的进程被称作任务),当然,也可以称它为pcb。每创建一个进程,就会创建一个对应的pcb存储进程独有的一些信息。然后将这些pcb以链表的形式进行维护
比如现在有一个进程A,要对里面的数据进行修改,那么就会遍历链表,找到进程A对应的pcb,然后通过pcb对其数据进行修改。又或者,现在要运行优先级最高的进程,那么操作系统就会遍历pcb链表,找到优先级最高的进程(pcb里面包含进程的所有属性,当然也包含优先级),确定该进程要被调度,通过pcb找到该进程的代码和数据,将它的入口代码放到CPU里面,这样该进程就被运行了。

所以,对进程的管理,并不是直接对代码和数据进行管理,而是将代码和数据提取出一些特定信息,存入task_struct 结构体里面,成为一个节点,再将节点存入链表,从而管理链表。这样,对进程的管理实际上就是对pcb形成的结构体的增删查改,这样就完成了对进程管理的建模的过程。

综上所述,进程 = 当前进程的代码和数据 + pcb/task_struct
当然,上述都是对软件进行管理的解释,但是,操作系统对硬件进行管理也是类似的。
请添加图片描述
pcb提取了进程的属性,这个进程的属性和文件的属性有一点点关系,并不大。文件的属性存储的是:这个文件是谁创建的,什么时候创建的,有什么权限等等。但是为进程创建的pcb结构体,是一种内核数据结构,是由操作系统动态创建和维护的,和磁盘文件的属性没有关系。
所以,pcb里面的属性相当于是另起炉灶,里面的属性基本上和文件属性无关。

task_struct 存储的一些信息:

  • 标示符: 描述本进程的唯一标示符,用来区别其他进程。
  • 状态: 任务状态,退出代码,退出信号等。
  • 优先级: 相对于其他进程的优先级。
  • 程序计数器: 程序中即将被执行的下一条指令的地址。
  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
  • 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
  • I/ O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
  • 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
    其他信息

📕 Linux下查看进程的两种方法

方法一

ps axj | head -1 && ps ajx | grep myprocess | grep -v grep

上述指令是查看自己创建的进程
如下是系统中所有的进程,ps axj | head -1 是将第一行拿出来,即 PPID 、PID 等等。 PID (process id)表示进程的唯一标识,PPID表示该进程的父进程的唯一标识……这样方便查看各列信息代表上面意思。
&& 是逻辑与,相当于前面的指令执行成功,然后执行后面的。
ps ajx | grep myprocess 代表的是,选出自己创建的进程(可执行文件名要为myprocess,也可以根据自己的文件名修改)。
grep -v grep ,由于进程信息是 ps 打印出来,然后通过管道传给 grep ,由 grep 进行过滤,但是 grep 本身也是一个进程,所以 grep -v grep 是将 grep 的进程信息去掉。
请添加图片描述
如下图,是上述指令执行结果。我们可以看到,同一个可执行程序被执行了两次,但是其PID是不一样的,说明创建了两个进程。也就是说,如果一个可执行程序可以多次被加载到内存,那么该可执行程序内部就可以有多个进程。
请添加图片描述

方法二

ls /proc

根目录下的 proc 目录,这个目录和普通的目录并不一样。proc 是一个内存级的文件系统,只有当操作系统启动的时候,该目录才会存在,在磁盘上并不会存在这个目录
ls -l /proc ,使用该指令查看到的信息如下,proc 目录下面也是一个个目录。目录名字就是特定进程的PID。但是用这个指令查看进程并不直观。所以一般用 ls /proc 。

请添加图片描述
如下,使用 ls /proc 查看进程就非常直观了。
请添加图片描述

当然,由于 /proc 目录以及它目录里的内容,是操作系统动态创建的,所以,当一个进程终止之后,它在 /proc 里面对应的文件夹也会被删除。
如下图,当PID为 28201 的进程还在运行的时候,是可以进入 /proc/28201 的,该目录下面存储的是 28201 这个进程的相关信息。但是使用 ctrl+c 终止进程之后,就看不到该目录下任何信息,甚至无法返回上级目录。这是因为这个目录已经被删除,/proc 下没有 28201 为目录名的文件了。
请添加图片描述

📕 pid() 、ppid() 函数

pid() 函数是获取当前进程的 pid,ppid() 函数是获取当前进程的父进程的 pid 。

如下,linux环境下执行下列的C语言代码,其运行结果如下图。

  1 #include                                                                                   2 #include3 int main()4 {5   while(1)6   {7   printf("我是一个进程,我的pid是:%d,我的ppid是:%d \n",getpid(),getppid());8   sleep(1);9   }10   return 0;11 }

请添加图片描述
./myprocess 这个进程的父进程 28071 ,实际上代表的是命令行解释器,也叫 bash,它本质上也是一个进程。我们在 linux 下敲的命令 ,比如 ls ,ll,grep 等等,都是由 bash 创建子进程去实现的,当然这些命令大部分也是由 C 语言实现,我们自己也可以写类似的命令,只是需要配置PATH(后面的文章会解释)。
这样设置是有很多好处的,比如,上述的这些指令,如果不是由 bash 创建子进程去完成,而是由 bash 本身完成,那么如果运行失败,bash 也就挂了。但是如果是子进程挂了,那么也不影响 bash 的运行。

📕 fork() 函数、父子进程

初识

如果不想用上述的两种方法创建子进程(./myporocess 、 指令),可以使用 fork() 在代码层面上直接创建新的子进程。可以使用 man 手册查看具体细节。

  1 #include2 #include3 int main()4 {5   printf("AAAAAAAAAAA:\n");6   fork();7   printf("BBBBBBBBBBB:pid:%d,ppid:%d \n",getpid(),getppid());8   sleep(1);                                                                                                                  9   return 0;10 }

运行上述代码,查看结果如下,说明 fork(); 那一行之后,确实是有两个进程。并且还是父子进程,进程 29333 的父进程 是 29332 。
请添加图片描述
但是,打印 A 是由哪一个进程执行的呢? 修改代码并编译运行之后,如下图,可以看出打印 A 是由父进程执行的。
请添加图片描述
我们可以查看一下上面进程 29758 的父进程 28071 ,它实际上就是 bash 。调用链就是:当运行 ./myprocess ,该程序就成了 bash 的子进程, 然后 ./myprocess 这个进程又创建一个子进程。其实就类似于一个树状结构。当然,进程之间一般只谈父子,不谈爷孙。
此外,父子进程谁先运行,这是由操作系统的调度选择算法来决定的,我们并不能决定。
请添加图片描述

再理解

#include
#include
#include
#includeint main()
{pid_t ret=fork();if(ret == 0){while(1){printf("我是子进程,我的pid是:%d,我的父进程是:%d \n",getpid(),getppid());sleep(1);}}else if(ret > 0) {while(1){printf("我是父进程,我的pid是:%d,我的父进程是:%d \n",getpid(),getppid());sleep(1);}}return 0;
}

执行上述代码,结果如下。但是这里惊奇地发现,if 和 else if 居然同时成立了!!!两个while循环也同时执行了!!!

请添加图片描述
但是不用惊讶,我们先从上面创建子进程的样例中总结一点东西。

  • fork 之后,执行流会变成两个执行流
  • fork 之后,谁先运行由调度器决定。
  • fork 之后,在 fork 函数后面的代码,父子进程是共享的,一般通过 if 和 else if 来把执行流分流。(从 A只打印一份,B打印两份 ;两个循环都执行 可以看出)

📕 fork做了什么

前面说到,进程 = 内核数据结构 + 代码和数据 ,所以,fork 创建一个子进程,也必然要 创建对应的pcb加载代码和数据到内存。但是,实际上只需要创建 pcb 即可,子进程的代码和数据是使用父进程的
如下图,蓝色方框代表父进程的pcb,其代码和数据放在右边的黄色方框(两层来区分代码和数据)。创建子进程之后,只创建了子进程的 pcb ,这个 pcb 绝大部分信息和 父进程的pcb 是一样的,少数不同(pid,ppid 等等),然后用链表连起来。子进程的pcb 里面的指针,指向的代码和数据和父进程的是同一块。

请添加图片描述
同时,进程的运行是具有独立性的!就好像我们同时打开 浏览器、微信、qq、迅雷,随便关掉一个,并不影响其他的使用。父子进程也是两个进程,所以也具有独立性!从下图可以看出,杀掉子进程,父进程依然在执行。
请添加图片描述
可是,既然进程是互相独立的,父子进程都有各自的 pcb ,这当然没话说,但是却共用同一份代码和数据?这如何体现独立性呢?
对于代码而言,代码是只读的,无论父子进程都无法修改,所以可以说是独立的;但是对于数据而言,怎么证明它是独立的呢?

我们可以通过下面的代码来证明。先设置一个变量 x ,在一个执行流中,不改变 x 的值,输出其值和地址;在另一个执行流中,改变其值,输出其值和地址

#include
#include
#include
#includeint main()
{int x=100; pid_t ret=fork();if(ret == 0){while(1){printf("我是子进程,我的pid是:%d,我的父进程是:%d,x的值是 %d ,地址是: %p \n",getpid(),getppid(),x,&x);sleep(1);}}else if(ret > 0) {while(1){printf("我是父进程,我的pid是:%d,我的父进程是:%d,x的值是 %d ,地址是: %p \n",getpid(),getppid(),x,&x);x=666;sleep(1);}}return 0;
}

编译运行之后,结果如下,可以看出,当一个进程修改 x 的值之后,并不会对另一个进程产生干扰
对于数据而言:当有一个执行流想要修改数据的时候,操作系统会自动给该进程触发写时拷贝。写时拷贝就是,当想要写入数据的时候,将这份数据拷贝到另一个地方,将写入的数据放到新的空间,不影响原来的空间。(当然这只是简单概念,具体信息要了解进程地址空间)
但是,变量x 打印出来的地址却是一样的,即使按照写时拷贝的说法,也说不通呀。那么只有一个理由,这里的地址不是物理地址(实际上是进程地址空间 / 虚拟地址)
请添加图片描述

📕 如何理解 fork 有两个返回值

当函数内部准备执行 return 的时候,函数的主体功能是已经完成了的。
fork 它本质上就是操作系统提供的一个函数,提供接口给外部使用,创建子进程。当 fork 函数执行到return 语句的时候,主体功能已经完成,子进程已经被创建,共享下面的代码。而return 也是语句,所以 return 既要被父进程使用,又要被子进程使用。相当于return了两次。

至于一个变量存储了两个值,实际上是写时拷贝。
请添加图片描述

相关内容

热门资讯

我喜欢的一本书作文初二【优秀... 我喜欢的一本书作文初二 篇一《鲁滨逊漂流记》《鲁滨逊漂流记》是我非常喜欢的一本书。这本书是由英国作家...
杭州市中考时间表【精彩3篇】 杭州市中考时间表 篇一杭州市中考时间表已发布,为了帮助考生和家长做好准备,以下是关于杭州市中考时间表...
2011年中考作文【经典3篇... 2011年中考作文 篇一我的梦想我的梦想是成为一名优秀的科学家。科学是一门神奇的学科,它可以帮助我们...
我们的初中生活作文(优选3篇... 我们的初中生活作文 篇一初中生活是人生中一个重要的阶段,对于每个学生来说都是难忘的。我也有着属于自己...
同桌的五大酷刑初二作文(推荐... 同桌的五大酷刑初二作文 篇一同桌的五大酷刑初二作文初中生活中,一个人最常接触到的人就是同桌了。同桌对...
八年级作文800字民族团结【... 八年级作文800字民族团结 篇一民族团结的重要性民族团结是指不同民族之间相互尊重、和谐相处的一种状态...
再一次遇见初二作文【优质6篇... 再一次遇见初二作文 篇一初二的时光,仿佛就在昨天。那些快乐、烦恼、成长、友情,都深深地刻在我心底。如...
初二作文:在那梦想放飞的地方... 初二作文:在那梦想放飞的地方 篇一在那梦想放飞的地方每个人都有自己的梦想,而在梦想放飞的地方,我们可...
什么的滋味初二作文(实用6篇... 什么的滋味初二作文 篇一初中生活的滋味初中生活,是我们人生中一段难忘而充实的时光。在这三年里,我们经...
走出校园初中作文初二7篇【通... 走出校园初中作文初二7篇 篇一:我的暑假计划初二暑假即将到来,我计划走出校园,积极参与各种有益的活动...
认识自己初二写人作文(精彩3... 认识自己初二写人作文 篇一:我与自己的相识我是一个初二的学生,但我却认识自己的时间并不长。以前,我总...
我看了一本好书初二水平作文8... 我看了一本好书初二水平作文800字 篇一《红楼梦》是我最近读的一本好书。这是一部中国古典小说,被誉为...
难忘你的眼神作文开头初二【实... 难忘你的眼神作文开头初二 篇一那是一个阳光明媚的春天,我和我的好朋友小明一起去参加学校的春游活动。我...
不为人知的我初中作文(推荐3... 不为人知的我初中作文 篇一我内心的小宇宙每个人都有一个内心的世界,那是一个只属于自己的空间。而我,作...
父亲的“雪花”初二作文【精彩... 父亲的“雪花”初二作文 篇一父亲的“雪花”我叫李明,今年初二。今天,我想给大家讲一个关于我的父亲的故...
游记作文600字初二【通用6... 游记作文600字初二 篇一:探索之旅这个寒假,我和家人一起去了福建。福建是一个风景优美的省份,有着许...
观奥运会闭幕式有感-初二【精... 观奥运会闭幕式有感-初二 篇一观奥运会闭幕式有感今年的奥运会闭幕式让我感受到了无比的激动和自豪。作为...
生活.人生-初二作文【优质3... 生活.人生-初二作文 篇一我对生活的理解生活是一种独特的体验,它是我们与世界互动的方式。对我而言,生...
八年级数学相似多边形的周长比... 八年级数学相似多边形的周长比和面积同步检测题 篇一相似多边形是初中数学中的重要概念之一,它们在几何学...
初二周记(精选6篇) 初二周记 篇一这一周对我来说是非常充实而有意义的一周。在这周里,我经历了许多新的事情,学到了很多新的...