进程概念

基本概念与基本操作

  • 课本概念:程序的一个执行实例,正在执行的程序等
  • 内核观点:担当分配系统资源(CPU 时间,内存)的实体如果是这样讲解进程,我们肯定是不理解的,对学习过进程的人来说,是理解的,我们换个方式讲解

我们这里提到内存和磁盘,一个程序运行起来叫做进程,那这个程序没运行起来在哪里呢?当我们编译好了可执行程序cmd,里面的代码和数据,说到底可执行程序是二进制文件,文件都是存储在磁盘中的,而当我们要运行这个可执行程序的时候就需要把磁盘中的可执行程序加载到内存中,这是根据冯·诺伊曼体系得出的结论,在内存中的是可执行程序的代码和数据
那这个可执行程序加载到内存,从磁盘加载到内存的东西就是进程吗?
我们知道磁盘中有多个程序,计算机一般也是多个程序同时进行的,这些程序都会被加载到内存中

在这里插入图片描述

而在这些程序被加载到内存运行之前,有一个软件是最早运行,那就是操作系统(OS)
现在有个问题,在系统内部有这么多的被加载进来的程序,那这些程序在内存的什么位置加载呀?这些程序有没有被CPU执行完了呢?也就是执行完的程序的代码和数据所在的空间要被释放掉,有没有可能这个程序需要暂时停止调度呢?有没有一些进程的空间不够了,需要扩容
所以,这些被加载到内存的程序,是需要为他们申请内存和释放内存,甚至进程本身就需要被管理被调度,也就是我们操作系统需要管理进程,但目前操作系统是不能管理进程的,在操作系统目前的视角,cmd的代码和数据,操作系统是不知道是哪个进程的,只能知道这个代码是什么,这个数据有什么,但是并不知道这个进程是谁,而多个程序被加载到内存,操作系统必然要管理这些进程!

那操作系统要如何管理呢?先描述再组织,操作系统为了管理这些加载进来的代码和数据,需要在操作系统内部对每一个程序封装一个struct结构体,而这个封装程序属性的结构体的属性有代码地址、数据地址、id、优先级、状态等等!再加一个这个结构体指针,之后在内核内构造一个数据结构
当有一个程序加载到内存的同时,操作系统内部也会用这个结构体类型创建一个对象,并完善属性,这样就有了一个节点,以此类推,多个程序运行的程序就有多个节点,这些节点也有对应的指针,指向对应的内存中,此时,被加载进来的程序的代码和数据,这些属性都能在这个结构体对象内找到,并且节点内还可以通过结构体指针指向下一个节点,这样形成了一个被加载到内存的程序列表,这个程序列表叫做进程列表,所以进程并不是程序被加载到内存那部分,也不仅仅是,进程应该是内核数据结构对象 + 对应的代码和数据

而在操作系统学科中,这个结构体是叫PCB(process control block,进程控制块),PCB是在操作系统称呼这个结构体的总称,在Linux下这个结构体叫task_struct,在内核里是结构体,叫内核结构体,PCB和task_struct的关系和之前shell和bash之间的关系,一个是抽象,一个是具体,进程的所有属性,都可以直接或间接通过task_struct找到

进程 = PCB(task_struct)+ 自己的代码和数据 (先描述再组织)

所以任何一个进程的数据和代码加载到内存的时候,操作系统都要为该代码和数据创建一个task_struct结构体,这个结构体可以找到对应的代码和数据,并且结构体在操作系统还可以链表的形式,把所有的PCB管理起来,未来操作系统内部管理所有的进程,最后全部都会转换成对进程链表的增删查改
而之后CPU调度进程,我们可以把这个链表当成一个队列先进先出,CPU在这个队列中,找到一个PCB,再根据PCB找到对应的代码和数据进行调度,一个进程运行结束了要怎么办!在内存中释放这块内存,在内核中将这个结构体节点释放掉,

在这里插入图片描述
内存指针

文档的文字不好理解,其实内存指针就是操作系统内的 PCB 一定能够找到自己在内存中的代码和数据,也就是 CPU 调度进程,但真正执行代码的时候不是在 PCB,而是在真实的内存代码和数据,这样好比面试官面试别人,他提问的时候不可能对着简历提问,应该向人提问,所以内存指针其实就是在 PCB 内的某些指针,能够支持我们找到自己代码和数据,所以内存指针在这里有点类似于简历上的电话,通过电话联系面试人来面试
上下文数据、I/O 状态信息 都和切换和调度有关这里先不讲,记账信息也是

组织进程

从内核源代码里找到它,所有运行在系统里面的进程都以 task_struct 双链表 的形式存在内核里!

在这里插入图片描述
重要结论:我们通过一个简单的循环输出字符自己写的程序,此时我们思考,这样简单的程序也是进程,在操作系统内核会有一个PCB来管理这个进程,内存中有这个程序的代码和数据!那现在这个程序运行,我们想查看这个进程的属性值要怎么查看呢?接下来学习我们人生中第一个系统调用!

getpid getppid chdir

在这里插入图片描述
在这里插入图片描述

关键验证:调用得出我们当前程序的进程 pid 了,这样也进一步证明了我们写的程序也是进程

重要指令:如果要查看进程信息要怎么办?我们在另一台设备上使用指令 ps,ps 指令可以查看所有进程的信息,指令:ps axj
在这里插入图片描述

二、还可以通过使用 && 来实现上述功能 ,如:ps axj | head -1 && ps axj | grep myproc

上图中第二行是什么呀?

我们知道,我们自己写的小程序myproc都是个进程 ,那指令grep,指令本身就是可执行程序,使用grep本上就是进程,在加上grep查询的对象是myproc,使用必然会被查询出来

如果不想要这行grep的进程,就可以在增加一个条件, grep -v grep,-v选项是反向匹配,会不匹配要求的数据!所有这样的指令可以只输出需要进程的信息:ps axj | head -1; ps axj | grep myproc | grep -v grep

在这里插入图片描述## 杀进程

所以现在可以查看到进程,那进程要如何取消呢?我们之前错误的使用某个程序,就是直接使用Ctrl + C,所以我们学习进程到现在可以知道,Ctrl + C 是取消进程!
但是我们学了进程,进程的概念应该更加仔细,所以还有别的取消方法!

这里再提一下,程序每次启动,其进程ID是不同的,这很正常,我们进程取消了会有别的进程来取缔这个ID,而系统分配PID 是线性递增的,那为什么 PID 不递增呢?因为有可能一个进程取消的同时,由别的进程启动了

这里循环小程序是在zyx用户启动的,我们可以在root用户下ps查看进程,也有在root用户下取消杀死进程,使用指令 kill -9 PID,-9 是信号,之后再讲,这样的指令可以直接把指定的进程杀掉!

补充 procexe,cwd,chdir

补充两个小知识(proc)(execwdchdir

在 Linux 中,我们执行的指令都是进程,只是有的进程执行完就结束了,有的需要用户操作才能结束,所以在 Linux 下处理任务都是以进程的形式执行的,即 Linux 用户是通过进程来访问操作系统的!所以我们用户可以是一名老师,而操作系统是一名学生,老师是可以对学生下任务的,也可以下多个任务,所以进程其实也叫任务,所以 PCB 在 Linux 中叫 task_struct!

proc

我们查看进程,不仅仅是用ps指令来查看,还有其他方式来查看进程,查看目录proc,proc是process的简称,在根目录下,在Linux下,不仅仅可以通过ls这样的方式用目录结构来查看磁盘中的文件信息,它把内存的相关数据也以文件的形式呈现出来,可以动态的看到内存的相关数据,这里的proc就是内存级的文件系统,和磁盘没有关系,都是内存内的数据
举个例子加深印象:一个学校,有几百名同学出国留学,教育局要求学生上交学生名单,学校还是要给教育局上交全部同学的信息,学校不管学生去哪了,上交名单肯定都要算上,同理,不管数据是从磁盘还是从内存来的,其结果都是以文件结构的方式为用户动态呈现的。
这也符合Linux一切皆文件的思想!甚至把所有的进程都分为一个一个的文件!

在这里插入图片描述

目录下,编号为目录名的目录,这个编号就是进程的PID,综上,根目录下的proc目录里记录了当前系统所有进程的信息,proc里的数字目录就是每个进程的PID,每个数字目录里储存的是该进程在运行时的动态属性,一旦进程退出,该目录就会自动被系统删除,有新进程运行,系统就会在proc中自动创建对应PID的目录文件

exe

我们进到我们小程序的进程文件里看看

在这里插入图片描述

我们现在还不理解文件内部的属性含义,但在这里要提两个属性。
目录下exe超链接文件,链接的是启动这个进程的可执行文件的文件路径,所以一个进程启动,是知道自己是从哪里来的,启动了那个指令才有了这个进程,所有进程会记录其对应可执行文件的绝对路径+文件名

如果这个时候,把该进程对应的可执行文件删除了进程会结束吗?根据实践可以知道,进程并没要结束,进程还在执行!这是为什么呢?我们删除的是磁盘里的可执行文件,但进程启动拷贝磁盘中的可执行文件数据并存储在内存中,这也充分证明了这一点!这个时候我们再次查看该进程文件,就会看到exe超链接在闪烁报错可执行程序已经被delete了

cwd chdir

还有一个超链接cwd(current work dir 当前工作目录)这个cwd链接的是可执行程序所在的工作目录,即进程会记录它是在那个路径上启动的!我们之前学习文件操作的时候,使用fopen("/a/b/c/d.txt"","w");时, 我们会有通过这个语句创建一个文件,我们通常在文件路径处,直接写文件名,这个文件会在当前目录下创建,那么程序为什么会在当前路径下创建?我们在fopen创建新文件,你以为输入一个文件就可以直接创建文件,但是我们要知道fopen是程序内的代码,代码实际的运行是在进程进行的,在运行的时候,我们肯定不能只依靠的文件名就创建文件,第一我们不知道文件要在哪里创建,那我们只能默认在可执行文件的目录下创建了,而我们也查看了进程文件,进程中只有cwd和exe有记录有意义的文件路径,所以在fopen内部,要想办法找到其工作目录再拼接,以达成文件的创建.

那这个cwd可以改吗?使用chdir

在这里插入图片描述

chdir是系统调用,在那个进程调用就改那个进程的cwd,在参数部分输入要修改的路径,chdir(".../.../...");

在这里插入图片描述
在这里插入图片描述
这也是为什么,使用cd指令可以切换路径,可以根据绝对和相对路径切换

父进程

getppid

在上图216,我们在学习getpid系统调用的时候,下面还有一个系统调用getppid,这个是获取父进程PID的系统调用,接下来要学习如何通过代码来创建进程,而不是通过指令,即如何创建子进程。Linux 系统里所有的进程都是通过其父进程创建的,Linux 系统的进程是单亲繁殖的系统,没有母进程,子进程由父进程创建,而一个进程会有自己的父进程,一个父进程也会有多个子进程,所以 Linux 系统的所有进程组成一个多叉树

在这里插入图片描述

只能查自己和父进程的PID!
在这里插入图片描述

我们调用程序,可以发现子进程重新调用是会一直改变的,之前也有提供,当结束后会有别的进程顶上去

我们每次调用进程都是向系统重新加载,就像高考复读生,第一年参加高考考号可能是1开头,第二次高考的考号可能又不一样,人数和年级以及不同了,很正常!

但是我们发现父进程的PID是没有发生改变的!
我们来查看一下父进程!
在这里插入图片描述

这个父进程是一个bash!bash 是命令行解释器(命令行解释器(王婆):本质是一个进程! 王婆和实习生!)

知识点:OS会给每个登录用户分配一个bash,而上图bash前面有个-,代表着远程登录的!
在这里插入图片描述

上图为通过while语句监控 用ps 查看bash进程,我们在中途用xshell新窗口登录root用户,我们能够明显看到新增了一个-bash进程,这也证明了上面观点!

那接下来一个问题
在这里插入图片描述

这个是什么?这个其实是bash打印出来的字符串,后面会卡着,等待输入,凭什么这样!这也说明bash是用C语言写的,我们C语言程序输出到终端,不是可以先printf打印字符串再scanf输入数据吗!所以我们在命令行上输入的所以指令都是输入给bash,以字符串输入给bash的,bash拿到命令就可以做分析了,分析我们后面讲!

那王婆和实习生是怎么回事! 第5课两小时部分,对shell外壳程序的初步讲解的例子,王婆就是Linux的bash命令行解释器,而实习生是王婆请来处理事情的!比如小王(用户)对婚恋的要求告诉王婆(bash),王婆(bash)派实习生去找如花(操作系统)说媒,我们之前讲到在命令行上我们自己启动的命令,它的父进程就bash,所以这里通过命令行启动的命令就是上面例子的实习生(ls、pwd、top、mkdir、touch 这些命令都是进程,这些进程的父进程都是bash)

这里就有新的问题 : 一个进程(比如bash)是如何做到创建一个子进程呢!即:用代码创建子进程

代码创建子进程(fork)、进程具有独立性

我们现在也要逐步去理解一个进程是如何在计算机中被创造的,我们知道命令行直接启动是由父进程bash创建的,这个父进程bash是怎么做到的,我们自己可不可以创建子进程?

fork
在Linux中,如果想创建一个进程!就可以使用fork系统调用!我们来看看man fork是如何描述fork的!
在这里插入图片描述

fork 为系统调用!功能是创建一个子进程!其头文件为 #include <unistd.h>#include <sys/types.h>,从 fork 的函数声明中可以知道,fork 系统调用没有参数,返回值为 pid_t

例子:

在这里插入图片描述

如果fork在这里是创建子进程的!正常情况下,我们代码执行是只有一个执行流的,但是一旦执行完fork后,此时就会有两个执行流了,并且这两个执行流都会执行后面的代码,所以fork后面的printf会打印两遍,一个是输出父进程的PID,一个是输出新的子进程的PID

在这里插入图片描述

在这里插入图片描述

上图也可以证实上面的说法,第一个进程为父进程PID,第二个进程为子进程PID,有两个进程在运行,验证了会创建一个新进程!
接下来讲讲原理!

在这里插入图片描述

我们知道 进程 = PCB(内核数据结构)+ 代码和数据(内存),上图第一行就是父进程的结构展示,而创建一个子进程,必然要在操作系统里也创建一个对应的PCB结构对象,而对象需要初始化!这里是默认拷贝父进程的属性,但有个别属性不同,比如:PID 和 PPID,所以子进程默认是指向父进程的数据和父进程的代码的!所以子进程被调度的时候,会调用父进程的代码!
这就好比,有个人抄了你的简历,把名字改了,但是没改电话!
子进程没有自己的代码和数据,因为目前,没有程序新加载!程序的代码和数据只有一份啊,我们创建了子进程,但是没有给它加载新的数据和代码呀!所以子进程会默认共享父进程的代码和数据

接下来在讲讲fork 的 返回值
在这里插入图片描述

fork返回两个返回值??? 两个返回值,一个是把子进程的PID返回给父进程,一个是把0返回给子进程
现在我们提新的需求: 我想让父子进程未来执行不同的代码逻辑!就是不是和上面那样子进程执行父进程的代码,而是执行其他的,不同于父进程的代码!

基于上面fork系统调用成功创建子进程会返回两个返回值,以一个例子来看看是如何实现上面要求的!
在这里插入图片描述

因为fork成功创建子进程会返回两个值,父进程返回的是子进程PID,给子进程返回的是0! 父子进程的id变量收到的数据是不一样的,所以可以让两个进程分别执行不同的代码
在这里插入图片描述

第一行是在fork之前,输出的父进程pid,后面开始是父进程输出其PID,其父进程为bash进程,第三行是子进程,输出其pid,子进程的父进程为对应父进程的PID,达成了上面要求,父子执行了不同的代码块

总结:
在fork之后,父子进程共享之后的代码,只是父进程认为从fork返回的是大于0的数,而子进程认为从fork返回的是0!所以父子进程会进入对应条件的执行流,从而实现了父子进程执行不同的代码块

为什么!!!
1、为什么fork给父子返回各自的不同返回值?
2、为什么一个函数会返回两次??
3、为什么一个变量,即 == 0 ,又大于0 ? 导致if else同时成立???

第三个问题还需要几节课才能解决,在讲解虚拟地址空间的时!

第一个问题
为什么给子进程返回的是0 ,给父进程返回的是子进程的PID呢?
实际上在Linux系统中,父进程 : 子进程 = 1 :n ,说人话就是任何一个父进程可以又0个 到多个孩子,因为父进程可以有多个子进程,所以一定要把子进程的PID返回给父进程,因为父进程需要通过这些不同的PID来查找对应不同的子进程,而子进程不需要获得父进程的PID,因为getppid可以获得了,子进程只需要表明自己成功建立就行,父进程对于子进程来说有唯一性,而父进程需要通过子进程的PID来管理不同的子进程

第二个问题
为什么函数要返回两次?
一个函数已经到return XX了,核心功能是已经做完了的!
而fork函数为系统调用,调用fork会进入fork函数对应的系统调用,当fork函数执行到最后会做return操作,函数执行完会返回到调用fork的函数当前行并继续向后执行 ,而fork函数要做什么操作(申请新的pcb——拷贝父进程pcb给子进程——子进程pcb放入进程list——甚至放入调度队列中),所以说,当fork执行到return时,上面的核心工作已经做完了(子进程被创建,甚至被调度了!),而return 也是个语句,return语句现在已经被父子进程共同共享了!所以父进程子进程都会走return语句,所以return会返回两次!!!

在这里插入图片描述

第三个问题
这个问题现在只能讲一半!为什么一个变量,即 == 0 ,又大于0 ? 导致if else同时成立?
数据之间是相互共享的吗?
结论:进程具有独立性!(一个进程挂了,不会影响其他进程,在爱奇艺看电影挂了,会影响在b站看番吗?
所以父进程挂了,子进程不会挂!
父子进程的PCB各自为一个对象在OS中,独立性可以满足
而代码是只读的,是共享的,父进程挂了,对代码也不能有什么操作,所以子进程代码也不会受到影响
那么数据呢?父进程是有一个全局变量来确认进程是否退出,如果子进程可以对父进程这个变量进行修改,那不就影响到其他进程了吗?所以是不能保证独立性的!所以父子进程在数据层面,默认是共享的,但**一旦父子进程任何一方有尝试修改这个数据,在系统内部就会把被修改的数据在底层拷贝一份,让目标进程去修改这个拷贝数据!**不会影响其他进程,保持了独立性。这个也将写时拷贝
例子实验:
在这里插入图片描述

上面例子是子进程对全局变量修改,每次加10,再看父进程输出全局变量,我们可以看到父进程输出的全局变量并没有被修改,有证明了进程的独立性
·

上图也是写时拷贝的原理,未对全局变量gval,进行修改时,都指向原本的位置,当子进程要修改gval时,就会内存中拷贝gval的数据,让子进程对拷贝数据修改,以实现数据独立!
综上,再数据结构独立,代码独立,以及数据写时拷贝实现数据独立,保证了进程的独立性

而return过后是对pid做赋值,父子进程,看谁先返回数据,先返回的就做写时拷贝,这样父子进程都以写时拷贝分别获得不同的数据

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐