一文带你轻松入门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:

相关文章
|
17天前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
51 4
linux进程管理万字详解!!!
|
8天前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
48 8
|
5天前
|
Linux
如何在 Linux 系统中查看进程占用的内存?
如何在 Linux 系统中查看进程占用的内存?
|
17天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
52 4
|
17天前
|
Unix Linux Shell
linux入门!
本文档介绍了Linux系统入门的基础知识,包括操作系统概述、CentOS系统的安装与远程连接、文件操作、目录结构、用户和用户组管理、权限管理、Shell基础、输入输出、压缩打包、文件传输、软件安装、文件查找、进程管理、定时任务和服务管理等内容。重点讲解了常见的命令和操作技巧,帮助初学者快速掌握Linux系统的基本使用方法。
54 3
|
18天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
19天前
|
消息中间件 存储 Linux
|
26天前
|
运维 Linux
Linux查找占用的端口,并杀死进程的简单方法
通过上述步骤和命令,您能够迅速识别并根据实际情况管理Linux系统中占用特定端口的进程。为了获得更全面的服务器管理技巧和解决方案,提供了丰富的资源和专业服务,是您提升运维技能的理想选择。
31 1
|
1月前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
【10月更文挑战第9天】本文将深入浅出地介绍Linux系统中的进程管理机制,包括进程的概念、状态、调度以及如何在Linux环境下进行进程控制。我们将通过直观的语言和生动的比喻,让读者轻松掌握这一核心概念。文章不仅适合初学者构建基础,也能帮助有经验的用户加深对进程管理的理解。
25 1
|
1月前
|
消息中间件 Linux API
Linux c/c++之IPC进程间通信
这篇文章详细介绍了Linux下C/C++进程间通信(IPC)的三种主要技术:共享内存、消息队列和信号量,包括它们的编程模型、API函数原型、优势与缺点,并通过示例代码展示了它们的创建、使用和管理方法。
31 0
Linux c/c++之IPC进程间通信
下一篇
无影云桌面