• 【Linux】第十二站:进程


    一、windows和linux中的进程

    一个已经加载到内存的程序,叫做进程

    进程也叫做任务

    比如在windows中,这个任务管理器就可以看到进程

    image-20231105143905414

    而在linux中,我们使用

    ps axj
    
    • 1

    就可以看到我们的进程了

    image-20231105144017060

    二、先描述

    我们先使用如下代码

    image-20231105145022218

    image-20231105145038489

    如下所示,就可以查看到进程了

    image-20231105145346868

    在这个过程中,我们的这个可执行程序就会从磁盘中加载到内存中,然后经过CPU,最终输出到显示器上。

    这个加载到内存中的程序就是一个进程

    像我们电脑的操作系统开机的时候也一样,开机的时候,就是操作系统加载到内存的时候。这也是进程

    这些加载到内存,其实就是将这些二进制数据(代码和一些数据)从磁盘中搬到内存中

    如下图所示,当一个程序要运行的时候,他需要先将这些二进制文件从磁盘中搬到操作系统中。

    image-20231105151143404

    但是常识告诉我们,一个操作系统,不仅仅只能运行一个进程,可以同时运行多个进程

    所以说,操作系统必须将这些进程给管理起来

    那么应该如何管理进程呢?

    当然还是先描述,在组织

    所以任何一个进程,在加载到内存的时候,形成真正的进程时,操作系统要先创建描述进程属性的结构体对象----PCB(process ctrl block 进程控制块)

    这就是类似于人是怎么样辨别认识一个事情或者对象的

    当然是都是通过属性认识的。

    当属性够多,这一堆属性的集合,就是目标对象

    这个PCB就是进程属性的集合

    而在C语言中,我们可以用struct结构体来描述这个集合。

    这个结构体里面就有进程编号、进程的状态、优先级、相关的指针信息…等等信息

    然后根据进程的PCB属性,就可以为该进程创建对应的PCB对象了

    不过我们的操作系统除了创建一个PCB对象之外,还要去将代码和数据加载到内存中

    image-20231105154122240

    就好比我们现在是一个学生,那么PCB就是我们的档案,代码和数据就是我们本人。只有当这两部分都在学校的时候,我们才是这个学校的学生。

    所以描述进程的PCB的结构体和该进程的代码和数据合起来才称作进程

    image-20231105154613845

    所以所谓的进程 = 内核PCB数据结构对象 + 你自己的代码和数据

    内核PCB数据结构对象是描述你这个进程的的所有的属性值

    所以操作系统要做管理的时候,不需要对我们自己做出管理,只需要对我们这个内核PCB数据结构对象做出管理即可

    这个PCB对象里面是有相关的指针信息的,所以可以通过这个PCB对象直接找到我们的

    上面这个过程就是一个进程的描述的过程,即先描述

    三、在组织

    可是我们操作系统经常要持续很多个进程的。所以这就需要组织起来了,从而达到对我们的进程做出管理

    为了能够将这些数据组织起来,所以我们可以在每一个PCB对象中加上一个相应的指针,用来找到下一个PCB对象,即如同链表的结构

    image-20231105155647588

    所以在操作系统中,对于进程的管理,就变成了对于这个单链表的增删查改了

    这个PCB就好比我们每个人的简历一样,在我们找工作的时候,HR都是直接对我们的简历做出管理的。当我们投递简历以后,我们会看到在排队中,这个排队就指的是这个PCB在排队。而不是我们本人在排队

    四、具体的Linux系统是如何做的?

    1.基本概念

    课本概念:程序的一个执行实例,正在执行的程序等
    内核观点:担当分配系统资源(CPU时间,内存)的实体

    2.描述进程-PCB

    进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
    课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct

    3.task_struct和PCB的关系

    • task_struct 是PCB具体的一种

    • 在Linux中描述进程的结构体叫做task_struct。

    • task_struct是Linux内核的一种数据结构类型(自定义类型),它会被装载到RAM(内存)里并且包含着进程的信息

    4.task_struct内容分类

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

    5.linux具体如何做的?

    在linux中PCB 就是task_struct 结构体,里面包含进程的所有属性

    Linux中如何组织进程呢?,linux内核中,最基本的组织进程task_struct的方式,是采用双向链表的

    不过要注意的是,有可能这个task_struct既是属于一个双链表,又是属于一个队列的。他是比较复杂的

    6.查看进程

    我们可以使用这个命令查看进程

    top
    
    • 1

    同时我们在前面所演示这个命令也是一个查看进程的命令

    这里这里的&&代表的十左侧的要执行成功,右侧的也要执行成功。即两条都要执行

    ps ajx | head -1 && ps ajx | grep  myprocess
    
    • 1

    除了使用&&,也可以使用;来隔开

    ps ajx | head -1 ; ps ajx | grep myprocess
    
    • 1

    如下所示,我们可以看到两个进程了

    前面两个是这个可执行程序由于跑了两份,所以的两份进程。

    下面的第三个进程是grep本身的进程,因为它本身也带有myprocess,所以就把它自己的进程过滤出来了。而之前的那些指令的进程早就已经结束了,所以不会显示出来。这里是因为正好grep执行的时候把他自己给过滤出来了

    image-20231105165724516

    注意看

    注意这个ID值,虽然这两个程序是一样的,但是他们的ID是不一样的,虽然将同一个程序执行了两遍,但是它还是两个进程,因为他们创建了两个不同的PCB

    image-20231105170443776

    除了上面这种方法还有这样一种方法

    ls /proc
    
    • 1

    image-20231105170748841

    这个是linux系统中比较重要的,也比较奇怪的一个目录

    它在关机的时候数据就全没了,在开机的时候就又会创建这个目录文件

    这个其实因为因为操作系统用文件系统的方式把内存当中的文件,包括进程全部可视化出来了

    它这上面的数据都是内存级的

    我们可以来证明一下

    我们先关掉一个进程

    image-20231105171607930

    当我们再次使用这个

    ls /proc
    
    • 1

    这个指令的时候

    image-20231105171711143

    对于这些蓝色的数字,我们知道它一定是目录,而对于黑色的我们先不管

    而这些蓝色的数字就是当前进程的PID,所以在创建进程的时候,操作系统会创建一个与PID一样名字的文件夹,这个目录里面保存这个进程的大部分属性

    我们可以通过这个找到我们这个进程的目录

    image-20231105172115374

    然后我们可以查看一下这里面的内容

    image-20231105172159326

    如果我们将我们这个进程之间结束掉,那么我们这里就用这种方法找不到这个进程了,这个目录就自动删除了

    image-20231105172307246

    如果我们重启一个进程,那么就又出来了

    image-20231105172419200

    不过这个PID也被改变了。

    所以说每一个程序结束以后在重新启动,PID就会变化了,所以他是系统当中动态运行的相关信息。

    我们可以进入到它里面就可以看到这个东西,这就足以证明这个就是我们的这个可执行程序的进程

    image-20231105173035832

    对于这个cwd,就是我们当前进程的工作目录(current work dir)

    image-20231105173156072

    对于这个cwd,这个是很有用的

    比如说当我们使用touch命令创建一个test.c文件的时候,它是如何找到当前的目录呢?

    就是进程启动的时候已经记录了当前的目录。所以才找到的

    就好比我们之前的文件操作的时候,为什么文件可以创建在当前目录呢?

    就是因为进程有这个cwd,记录了当前工作目录,所以可以直接拼接上去。这就是当前路径

    image-20231106103513975

    7.kill进程

    我们先写出如下代码

    image-20231106104507368

    如下所示,当我们运行了这个代码以后,它就会有它对应的进程

    image-20231106130028262

    并且其中我们发现最下面的这个proc其实是因为grep这条指令也要带上proc,所以也把他过滤出来了,可见指令在运行时也是要有进程的

    那么我们也可以看到这个PID,有了这个PID有什么用呢?在前面我们用它可以找到一个对应的文件。那么还有什么作用呢?其实我们还可以直接杀掉这个进程

    kill -9 13388	//13388是当前程序对应的PID
    
    • 1

    我们可以看到,进程确实被杀死了

    image-20231106130807040

    8.进程的PID

    我们知道对于我们的操作系统,它的内部一定是这样的

    内部有task_struct结构体,然后指向对应的代码块和数据。而pid一定是在它的内部存储着的

    image-20231106131618318

    而我们前面的ps命令其实就是在遍历这个链表。从而获取每个值的

    如果我们想要获得某个进程的pid,由于我们无法直接拿去,所以我们只能通过系统调用来获取

    getpid()
    
    • 1

    如下是man手册中的getpid

    image-20231106131913268

    我们可以知道,它的作用是返回调用这个函数的进程的PID

    然后我们可以去应用一下:

    image-20231106132452444

    我们可以用这段指令来进行检测进程

    while :; do ps ajx | head -1 ; ps ajx | grep  Proc | grep -v grep;echo "------------------------------------------------"; sleep 1; done
    
    • 1

    它一开始的效果是这样的

    image-20231106134102921

    如下所示,我们就可以检测到进程了

    image-20231106134208928

    我们也可以看到这个进程的PID,并且如果我们在中间终止掉了这个进程,然后再重启的时候,它的PID已经被改变了

    相应的,除了PID以外,还有一个PPID,它的意思是父进程

    同样的,我们可以去获取他的进程

    image-20231106134711371

    如下是运行结果

    image-20231106134856406

    如果我们将这个进程杀掉,然后,我们继续重启这个进程

    image-20231106135015218

    我们会发现,父进程没有变化,我们自己的进程的ID变化了

    那么我们可能会好奇这个13200里面到底是什么呢?

    image-20231106135422052

    我们可以看到有很多的13200,但是我们先只关心一下PID为13200的这个

    我们可以看到它后面跟的是bash,也就是命令行解释器

    所以说,运行一个进程时,命令行解释器会把我们这条命令的进程变成bash的子进程,由子进程实行对应的命令,这样的话,当子进程执行命令的时候,一旦这个进程出问题了,就不会影响bash进程

    但是如果我们直接将我们的这个Xshell断开链接了,然后重启,我们就会发现这个PPID也变了

    image-20231106141236598

    但是如果我们只是普通的杀掉这个进程,然后再运行的时候,父进程不改变

    image-20231106141323241

    然后我们可以像前面一样,调出这个父进程所对应的文件

    image-20231106141459229

    我们可以看到,以前的13200也就不见了

    image-20231106141618067

    所以说,我们每次重新登录的时候,系统会为我们重新创建一个bash进程

    我们在命令行中输入的所有进程都是bash的子进程

    bash只负责命令行的解释,具体执行出问题的时候,只会影响它的子进程

    这就是为什么当一个子进程中断的时候,父进程不变的原因

    9.fork

    我们前面一直在说进程,那么如果我们想创建一个进程,该如何做呢?

    一个方法就是我们前面使用的直接./Proc,这样的话就是操作系统为我们创建进程了

    如果我们想要自己创建一个进程,那么我们可以用fork函数

    image-20231106143610555

    我们先用起来这个函数

    当我们用如下代码,不包括fork的时候

    image-20231106143952060

    毫无疑问,打印两行

    image-20231106144012692

    当我们将这个fork加上以后

    image-20231106144154479

    我们会发现打印出来了两次after

    image-20231106144139647

    为什么呢?这个其实就是因为,fork之前只有一个执行流,forK之后就有两个执行流了。

    不过我们还是先看一下fork函数

    image-20231106144651978

    这是它的返回值,意思是如果成功,那么返回一个子进程的PID给父进程,返回一个0给子进程。如果失败,返回-1给父进程,不创建子进程,并适当地设置errno。

    我们发现它好像有两个返回值?

    我们先看一下这个代码

    image-20231106150126373

    如下是我们的运行结果

    image-20231107130400691

    我们发现这个运行结果很奇怪。出现了很诡异的现象。

    这其实就是fork的作用,有了它就有两个进程了

    我们可以关注一下一组pid和ppid的值

    image-20231107130417339

    我们会发现,这两个刚好就是一组父子关系。

    因为父进程的pid等于子进程的ppid

    而前面这个29958是bash命令行的进程,从下图可以看到

    image-20231107130525522

    我们也可以在前面的这个图看到,这个父进程其实就是我们原来的进程,而子进程就是这个父进程所创建的一个新的分支

    image-20231107130946110

    所以这样的话,我们就也可以创建一个进程了

    所以

    ./xxxx------------指令层面创建进程

    fork()-------------代码层面创建进程

    10.对fork的深刻理解

    fork的英文意思是叉的意思,也就是代码一分为二

    如下图所示,当我们使用fork以后,代码就变成了两个执行流,父进程的执行流执行id值大于0的部分,子进程的执行流执行等于0的部分

    image-20231107132344783

    在这里我们就会有如下几个疑问

    1. 为什么fork要给子进程返回0,给父进程返回子进程的pid
    2. 一个函数是如何做到返回两次的?如何理解
    3. 一个变量怎么会有不同的内容?如何理解
    4. fork函数究竟在干什么?干了什么?
    1. 为什么fork要给子进程返回0,给父进程返回子进程的pid?

      返回不同的返回值,是为了区分让不同的执行流,执行不同的代码块

      一般而言,fork之后的代码父子共享

      以上回答了为什么要不同的值,下面回答为什么父进程要返回子进程的pid

      这是因为,一个父亲可以有多个孩子,而一个孩子只有一个父亲

      所以未来父亲需要对孩子做出区分,所以返回一个pid就可以区分各个子进程

      而对于一个孩子,由于父亲是唯一的,所以返回0即可

      所以必须返回子进程的pid,用来标定子进程的唯一性

      子进程只需要调用getppid就能直接获取父进程的pid,所以它直接返回0标记成功即可

    2. fork函数究竟在干什么?干了什么?(上)

      我们知道

      进程 = 内核数据结构(PCB) + 代码和数据

      如下是我们fork之前的样子,只有当前的一个进程,CPU去调用这个进程

      image-20231107134244295
      而创建子进程其实就是:系统里面多了一个进程

      即如下图所示,首先,会先照着原来的task_struct创建一个新的task_struct。不过这个新的tash_struct会将它的pid修改为一个新的pid,将它的ppid改为前面的pid

      image-20231107134551677

      现在子进程有了自己的内核数据结构,也就是PCB,那么它还没有它自己的代码和数据,它应该访问什么呢?

      而父进程是可以访问自己的代码和数据的。子进程没有,所以只能去访问父进程的代码

      image-20231107140518259

      所以说,他们的代码是共享的

      所以CPU在执行的时候,如果是父进程,跑的是它的代码,如果是子进程,跑的也依旧是这个代码

      即fork之后,父子进程代码共享,因为代码是不可以被修改的

      那么既然代码是共享的,那么之前的数据呢?我们先不考虑这个问题

      我们先来看一下这个,既然代码是共享的,那么我们为什么要创建进程呢?

      这是为了让父和子执行不同的事情,所以需要想办法让父和子执行不同的代码块,所以我们就需要让fork具有不同的返回值

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

      在上一个问题中,我们讨论到了代码块一定是共享的,为了让其可以执行不同的代码块,就需要具有不同的返回值。

      那么这个是如何做到的呢?

      我们要注意,fork本身也是一个函数,即它也有自己实现的代码

      当一个函数已经return的时候,它的核心工作已经做完了。这就意味着。在返回之前已经创建好了子进程了。

      而在return之前函数体内部一定会发生下面这些事情

      1. 创建子进程PCB
      2. 填充PCB对应的内容
      3. 让子进程和父进程指向同样的代码
      4. 父子进程都是有独立的task_struct,可以被CPU同时调用运行了

      而创建好子进程后,由于父子进程代码共享,而return也是代码,所以return也是会被共享的

      所以父进程调度执行的时候,返回一次,子进程调度执行的时候,也返回一次

      所以这个fork函数最终返回了两次

      image-20231107163506965

    4. fork函数究竟在干什么?干了什么?(续)

      前面说过,子进程创建好PCB以后,会共享同一块代码,那么数据呢?

      首先在任何平台,进程在运行的时候,是具有独立性的,也就说当我们的qq崩了以后,并不影响其他软件

      所以,父子进程既然是两个进程,那么他们就不能同时访问同样的空间。

      因为数据可能会被修改,所以不能让父进程和子进程共享同一份数据。

      而代码是不可被修改的,所以可以共享同一份代码,因为并不影响独立性。

      所以说,子进程要将这份数据单独拷贝一份

      image-20231107162002496

      可是有可能父进程创建出来的变量,子进程并不会去使用。这样使得子进程的利用率很低

      所以操作系统变决定不这样直接去拷贝,而是一开始先共享代码和数据,但是当子进程要对某个数据进行修改的时候,采取拷贝一份这个这个数据。

      image-20231107162359485

      所以只有当子进程要尝试修改某个数据的时候,才会去拷贝一份。这也叫做数据层面的写时拷贝

    5. 一个变量怎么会有不同的内容?

      当我们理解了fork函数究竟在做了什么,我们也就随之理解了一个变量怎么会有不同的内容

      return就是写入的过程,id就是父进程的数据。

      所以对于子进程的id就是就会专门创建一个空间用来存储,要发生写时拷贝

      image-20231107163637791

      所以最终返回值是不同的

    所以说,我们在看我们之前的代码,我们也就能理解这些现象了

    image-20231107164333270

    1. 当父子进程被创建好,fork以后,谁先运行呢?

      其实谁先运行是不确定的,是由调度器来决定的

      也就是说,现在有很多个进程,CPU应该挑选哪一个进程来运行呢?这个是由调度器来决定的

      所谓调度器,就是一套执行数据结构查找的相关算法,它会在这些进程中找到一个合适的进程放到CPU中。

      像我们现在的计算机,调度器要尽可能保证公平,时间都比较均衡。

    11.bash用fork创建子进程

    我们还是看下面的运行结果,代码同上

    image-20231107165406661

    image-20231107170234376

    我们现在已经差不多了解了这些PID和PPID这些信息了。

    那么对于原来的那一个进程而言,它也是一个子进程为11129,它的父进程8464

    而这个8464正好就是bash命令行解释器的进程。我们也知道这是为了保证该进程不会影响其他进程的

    那么既然这里是通过创建子进程的方式完成的,那么bash如何创建子进程呢?

    其实就是利用fork,让这个fork出来的子进程去执行解释新的命令。而它自己就继续接收用户的输入。

  • 相关阅读:
    flink sql 使用
    JobManager 内存简介
    郑州市管城区工信局局长任华民一行莅临中创算力调研指导工作
    node.js-模块化
    类初始化的顺序
    深度学习系列54:使用 MMDETECTION 和 LABEL-STUDIO 进行半自动化目标检测标注
    解决vscode大量ts文件报错问题
    groupby的复杂用法
    Golang GUI框架
    数据结构和算法
  • 原文地址:https://blog.csdn.net/jhdhdhehej/article/details/134232565