一文带你轻松入门Linux中的『进程』-2

简介: 一文带你轻松入门Linux中的『进程』

三、创建进程

刚才我们在Linux下启动一个进程的时候利用的是./可执行程序,那是否有其他办法去启动一个进程呢?

1、fork初识

  • 当然是有的,那就是使用fork()这个函数。在使用之前呢我们要先去查看一下这个函数该如何使用


man fork
  • 可以看到,这个函数的功能就是去创建一个子进程,其返回值为pid_t

image.png然后我们来测试一段代码:


printf("before: only one line\n");
fork();
printf("after: only one line\n");
sleep(1);
  • 通过执行结果我们可以看到,虽然只有一句after: only one line,但是在【fork】之后却打印了两句

image.png

💬 那有的同学就会感到非常地好奇,这是为什么呢?

  • 因为在【fork】之后会产生两个执行的进程。但有同学还是会觉得很怪,这怎么就变成了两个进程了呢?我们可以去查询一下这个单词的意思,发现其确实是有分叉的意思。所以在执行了这个函数后,就会存在两个执行流

image.png

  • 如果想要更加清楚地了解这个函数,我们还需要再查看一下man手册,然后看到
  • 如果成功则会给父进程返回子进程的PID,给子进程返回0
  • 如果失败的话则会给父进程返回-1

image.png

那接下去我们就根据这个返回值去举个例子看看

下面是测试的代码:


1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <sys/types.h>
  4 
  5 int main(void)
  6 {
  7     printf("begin: 我是一个进程, pid: %d, ppid: %d\n", getpid(), getppid());
  8 
  9     pid_t id = fork();
 10     if(id == 0){
 11         // 子进程
 12         while(1)
 13         {
 14             printf("我是一个子进程, pid: %d, ppid: %d\n", getpid(), getppid());
 15             sleep(1);
 16         }
 17     }
 18     else if(id > 1){
 19         // 父进程
 20         while(1)
 21         {
 22             printf("我是一个父进程, pid: %d, ppid: %d\n", getpid(), getppid());          
 23             sleep(1);
 24         }
 25     }
 26     else{
 27         printf("error, fork创建子进程失败\n");
 28     }
 29     return 0;
 30 }
  • 然后将进程挂起后我们来看看,在第一句执行完后父子进程竟然是一起执行的,if...else...分支可以同时进去,并且还有两个死循环在同时跑。这是为什么呢?

2、疑难解答

读者一定会对上面种种现象感到非常地疑惑,在本小节我将会为你解答这些疑惑

  • 我们来分析一下这个进程的创建过程:首先我们可以看到我们在这个PPID为【18152】的 bash 上执行了一个进程,那么操作系统就会为这个进程分配一个PID为【27013】
  • 接下去这个进程被操作系统调度,执行自己的代码,执行到内部代码的fork函数时,执行流被一分为二,变成了两个执行分支:一个是父进程(它自己),另一个则是子进程(新的分支)

image.png💬 所以现在我们可以得出创建进程的两种方式:

  1. ./运行我们的程序 - - 指令层面
  2. fork() - - 代码层面

image.png

接下去我们就通过一些具体的问题来更加深入地了解一下fork()与进程之间的关系?

一、为什么fork()要给子进程返回0,给父进程返回子进程pid

  • 上面我们说到当进程的代码执行到fork()函数的时候,会将执行流一分为二,父子进程通过不同的 id 返回值来区分,以此执行不同的代码块。那其实很好理解了:因为父子进程是两个不同的进程,所以需要根据这个不同的返回值来进程区别

image.png💬 那有同学说:你这不说了跟没说一样嘛,要区分的话当然得不同了,那为什么父进程得到的是子进程的PID,但是子进程却是0呢,为什么不可以倒过来?

  • 这位同学,你问到点子上了,确实这是它们最大的区别,不过呢这样的返回值还是有原因的。读者可以这么来理解:一个父亲可以有多个孩子👦,但是呢一个孩子却只能有一个父亲👨 父亲所获取到的返回值是子进程的PID是由于他要靠不同的PID值来区分不同的孩子;但子进程的返回值都是0的原因在于他一定只对应着某一个父进程,只需让父进程知道它被成功创建出来了即可

image.png


二、fork()函数究竟在干什么?干了什么?

  • 在上面我们讲到过【进程 = 内核数据结构 + 代码与数据】,当我们在执行完fork()函数后,子进程被创建出来,那么它的PCB结构体即 task_struct 会被构建出来,我们知道的是在每个进程的结构体中有PIDPPID这两个成员,而且对于子进程中的PPID恰好就是父进程中的PID 。所以子进程大部分的属性就是以父进程为模版创建的,相当于把父进程拷贝了一份,对部分属性做了修改

image.png

可以看出子进程被创建出来后系统中多了一个进程,那么对于父进程来说它有自己的内核数据结构、代码和数据,子进程也按照父进程的PCB模拟了一块出来

💬 那我现在要问了:请问子进程的数据和代码呢?也是拷贝出来的吗?

  • 那有的同学就说:都属于不同的两个进程了,总会有自己的代码和数据吧。诶,这个说得就不对了,对于子进程来说,虽然它有自己的内核数据结构,但它在一创建出来的时候并没有独属于自己的【代码和数据】,而也是使用和父进程一样的同一份代码和数据

image.png

那对于这个代码而言我们就要有更多的思考了🤔

  • 既然父子进程共享同一段代码的话,我们再来看看fork()之后会有什么现象。可以看到同一句代码被重复执行了两次

image.png💬 那我此时还想问的是:既然跑的都是同一段代码,那还要子进程干嘛呢?直接父进程去跑个两遍不就好了

  • 既然子进程被创建出来的话,那一定是有它的作用的,上面我们所看到的只是一段很简单的逻辑,但是在现实的开发中却会存在很复杂的逻辑,可能需要父子进程去执行不同的两段逻辑,所以这才使得【父进程】与【子进程】得到了两种不同的返回值,我们才可以对其去进行判断

三、一个函数是如何做到返回两次的?如何理解?

上面讲到了因为在某些情况下需要依靠父子进程去执行不同的两段逻辑,所以在创建子进程后父子进程它们分别会得到不同的两个值

  • 那既然在调用了fork()函数后,就肯定需要去返回两次才可以。这里我们再通过画图来分析一下,既然这个fork()是库函数的话,那执行到这一句的时候就一定会跳转到库中的这一逻辑中去执行【创建子进程】的这一步的步骤,但是这还是无法说明他可是有不同的返回值呀?
  • 那我这时候就要问了,最后的这个return语句算是代码吗? 当然了!那我们在上面说到过这个代码呢是父子进程共享的,==那么父进程返回一次,子进程返回一次,也就相当于返回了两次==2️⃣

image.png


四、一个变量怎么会有不同的内容呢?

  • 上面我们讨论到了父子进程会去共享同一段代码,但是呢这个数据子进程该去对待呢?还是和父进程用同一个吗?

image.png👉 这里要提出一点:在任何平台,进程在运行的时候是具有独立性的,不会影响另一个进程

  • 在上面我们有看过这张图,在一个操作系统中是可以同时运行多个进程的,但是呢如果我们的【XShell7】突然闪退了,会影响【Chrome浏览器】吗?—— 这当然是不会的!

image.png

  • 但是呢,也并不是所有的数据都牵扯【独立性】,就好比我们在家里的茶几上都会有水杯,那么家里的每个人都是可以使用水杯的,这个互不影响

image.png

但是呢此时我们的子进程和父进程所维护的数据是同一块,这就免不了出现【并发修改】的问题

  • 所以我们不能让父进程和子进程共享同一块数据。所以子进程呢就会把数据单独拷贝一份。因为父子进程使用的是不同的PCB,所以当CPU调度不同进程访问的是不同的数据,它们在数据上割裂了,那一个进程运行时就不会影响另一个进程了

image.png

那么我现在又要提出疑问了,子进程每次在创建之后都会去拷贝一份这个数据,会存在问题吗?

  • 刚才我们谈到子进程要去拷贝数据的原因是在于【并发修改】的问题,但若是子进程只是读取数据但是不修改呢?也需要去完整地拷贝一份数据吗?不,完全不需要!这会使得资源消耗过大
  • 在一上来不会直接给子进程拷贝一份父进程的数据,在子进程刚被创建的时候,代码和数据全部都是被共享的,只有当操作系统识别到子进程要对父进程中的数据做修改时,才会在系统的某一个位置开辟一段空间,然后在修改的时候不去修改父进程内部的这个数据,而是去修改拷贝出来的这块数据 ——> ==此为父子进程之间数据层面的写时拷贝==

image.png如果对上面这个不太理解的话可以看看 string类中的写时拷贝 ,这两块知识点是联动的🌊

那看完了上面的这些内容后,我们再来谈谈刚才所说到的fork()函数的返回值问题

  • 因为是当前的父进程去调用的这段代码,所以最后在返回的时候父进程直接进行写入即可,到这个id中,但是呢对于子进程来说就不一样了,这里会发生一个写时拷贝。那也就导致了父子进程最后所获取到的值不一样的原因

image.png


【总结一下】:

  • 当我们调用fork()之后,子进程就被创建出来了,父子进程就共享后续的代码了,但是呢父和子会由各自执行return从而造成两次返回,在【id】层面上发生写时拷贝,让父子进程的【id】变成不同的值。使得可以在后续对不同的【id】值进行变换,从而形成一个分流,让父子去执行不同的代码块,所以父进程和子进程就可以去执行不同的逻辑了。 —— 这就叫做fork

四、总结与提炼

最后来总结一下本文所学习的内容:book:

  • 首先的话我们初步了解到了进程的基本概念,知道了进程不仅仅是一个 ==正在执行的程序==,而且要让一个程序成为了一个进程,就必须让其 加载到内存中
  • 除了对进程有一个基本的了解后,我们还需要去理解什么是进程:因为管理的本质是【先描述,再组织】,所以我们要先去描述一个进程,使用的是PCB叫做【进程控制块】,在Linux里为task_struct。由此我们知道了 进程 = 内核PCB数据结构对象 + 你自己的代码和数据;知道如何去描述进程后,我们还要学习如何去组织进程,在Linux中我们采取的【双向链表】进行组织的
  • 那么在描述并组织完多个进程之后,我们就可以去查看这些进程了,所采用的方式有三种:pstopls;当然,在查看进程的时候主要关注的是PIDPPID这两个属性值,分别代表的是 当前进程的标识符当前进程的父进程标识符;当然,这两个标识符不仅仅是可以通过指令来查看,而且还可以通过操作系统提供给我们的【库函数】来查看,查看 man手册 可以发现这两个函数为getpid()getppid()
  • 除了【指令层面】的./运行我们的程序外,我们还可以从【代码层面】的fork()来创建进程,后者可以帮我们去创建出一个子进程,从四个问题来步步分析fork()的底层,我们可以知道在 fork 之后的代码会被父子进程所共享,而且它们所获取到返回值会因为子进程的 写时拷贝 而不同,所以父子进程才得以执行不同的代码逻辑

以上就是本文所要介绍的内容,感谢您的阅读:rose:

相关文章
|
7月前
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
284 67
|
6月前
|
Web App开发 Linux 程序员
获取和理解Linux进程以及其PID的基础知识。
总的来说,理解Linux进程及其PID需要我们明白,进程就如同汽车,负责执行任务,而PID则是独特的车牌号,为我们提供了管理的便利。知道这个,我们就可以更好地理解和操作Linux系统,甚至通过对进程的有效管理,让系统运行得更加顺畅。
200 16
|
6月前
|
Unix Linux
对于Linux的进程概念以及进程状态的理解和解析
现在,我们已经了解了Linux进程的基础知识和进程状态的理解了。这就像我们理解了城市中行人的行走和行为模式!希望这个形象的例子能帮助我们更好地理解这个重要的概念,并在实际应用中发挥作用。
139 20
|
5月前
|
监控 Shell Linux
Linux进程控制(详细讲解)
进程等待是系统通过调用特定的接口(如waitwaitpid)来实现的。来进行对子进程状态检测与回收的功能。
123 0
|
5月前
|
存储 负载均衡 算法
Linux2.6内核进程调度队列
本篇文章是Linux进程系列中的最后一篇文章,本来是想放在上一篇文章的结尾的,但是想了想还是单独写一篇文章吧,虽然说这部分内容是比较难的,所有一般来说是简单的提及带过的,但是为了让大家对进程有更深的理解与认识,还是看了一些别人的文章,然后学习了学习,然后对此做了总结,尽可能详细的介绍明白。最后推荐一篇文章Linux的进程优先级 NI 和 PR - 简书。
181 0
|
5月前
|
存储 Linux Shell
Linux进程概念-详细版(二)
在Linux进程概念-详细版(一)中我们解释了什么是进程,以及进程的各种状态,已经对进程有了一定的认识,那么这篇文章将会继续补全上篇文章剩余没有说到的,进程优先级,环境变量,程序地址空间,进程地址空间,以及调度队列。
126 0
|
5月前
|
Linux 调度 C语言
Linux进程概念-详细版(一)
子进程与父进程代码共享,其子进程直接用父进程的代码,其自己本身无代码,所以子进程无法改动代码,平时所说的修改是修改的数据。为什么要创建子进程:为了让其父子进程执行不同的代码块。子进程的数据相对于父进程是会进行写时拷贝(COW)。
156 0
|
8月前
|
存储 Linux 调度
【Linux】进程概念和进程状态
本文详细介绍了Linux系统中进程的核心概念与管理机制。从进程的定义出发,阐述了其作为操作系统资源管理的基本单位的重要性,并深入解析了task_struct结构体的内容及其在进程管理中的作用。同时,文章讲解了进程的基本操作(如获取PID、查看进程信息等)、父进程与子进程的关系(重点分析fork函数)、以及进程的三种主要状态(运行、阻塞、挂起)。此外,还探讨了Linux特有的进程状态表示和孤儿进程的处理方式。通过学习这些内容,读者可以更好地理解Linux进程的运行原理并优化系统性能。
324 4
|
8月前
|
Linux 数据库 Perl
【YashanDB 知识库】如何避免 yasdb 进程被 Linux OOM Killer 杀掉
本文来自YashanDB官网,探讨Linux系统中OOM Killer对数据库服务器的影响及解决方法。当内存接近耗尽时,OOM Killer会杀死占用最多内存的进程,这可能导致数据库主进程被误杀。为避免此问题,可采取两种方法:一是在OS层面关闭OOM Killer,通过修改`/etc/sysctl.conf`文件并重启生效;二是豁免数据库进程,由数据库实例用户借助`sudo`权限调整`oom_score_adj`值。这些措施有助于保护数据库进程免受系统内存管理机制的影响。
|
8月前
|
Linux Shell
Linux 进程前台后台切换与作业控制
进程前台/后台切换及作业控制简介: 在 Shell 中,启动的程序默认为前台进程,会占用终端直到执行完毕。例如,执行 `./shella.sh` 时,终端会被占用。为避免不便,可将命令放到后台运行,如 `./shella.sh &`,此时终端命令行立即返回,可继续输入其他命令。 常用作业控制命令: - `fg %1`:将后台作业切换到前台。 - `Ctrl + Z`:暂停前台作业并放到后台。 - `bg %1`:让暂停的后台作业继续执行。 - `kill %1`:终止后台作业。 优先级调整:
630 5
下一篇
oss云网关配置