【Linux修炼】8.进程概念

在这里插入图片描述每一个不曾起舞的日子,都是对生命的辜负。

本节目标

1. 进程概念

2. 进程的基本操作

3. 进程状态

4. 特殊进程

5. 进程优先级

6. 进程切换

那在还没有学习进程之前,就问大家,操作系统是怎么管理进行进程管理的呢?很简单,先把进程描述起来,再把进程组织起来!

1. 进程概念

1.1 进程的概念

对于我们的了解来说,什么是进程呢? 有的资料是这么说:一个运行起来的程序(加载到内存)叫做进程;在内存中的程序叫做进程。也就是说,进程和程序相比具有动态属性。

对于之前我们通过C写出的进度条程序来说,其本质就是一个文件并且存放在磁盘中。但是其并没有真正的运行,当我们运行程序的时候,文件就会从磁盘加载到内存,但是磁盘中那么多的文件全部加载到内存中明显是不现实的并且我们也不需要其他文件加载到内存,这时候就需要操作系统对文件进行管理从而只让我们想要执行的程序加载到内存,那操作系统是如何管理的呢?

即上篇提到的:先描述,再组织

1.2 描述进程—PCB

通过上述的概念,我们了解的并不多,那么接下来就来分析一下:如果有很多这样的进程加载到内存中,操作系统要如何进行管理呢? 即利用先描述再组织的思想。

而所谓的先描述,这里引进了一个新的概念:PCB :进程控制块 struct task_struct{}

1. 那么什么是进程控制块呢?

进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,称之为PCB(process control block)在Linux中描述进程的结构体叫做task_struct。

在磁盘中的程序中,并没有进程控制块以及内部属性信息的存在,而是加载到内存之后通过操作系统的一系列的管理才出现的。

//进程控制块
struct task_struct
{
    //该进程的所有属性
    //该进程对应的代码和属性地址
    struct task_struct *next;
};

2. 进程控制块如何对进程进行管理的呢?

  • 磁盘中的可执行程序在将要运行时,所有的进程中的数据(并不是程序本身)加载到内存中,此时操作系统会建立起一个PCB来保存每一个程序的信息

image-20221101164505346

  • 这个时候PCB就会对每一个进程都建立起相应的结构体(即进程控制块)将对应的进程的属性、代码等匹配的传到这个结构体中:(这就是先描述)image-20221101165048492

  • 此时,操作系统就会将每一个进程控制块都连接起来,形成链表结构,并返回头结点。这样便通过数据结构的形式将进程的信息组织起来。

通过这样的先描述再组织的思想,当我们处理优先级高的进程时,我们就可以通过遍历头结点,找到优先级最高的那个节点的信息,并将这个进程的代码执行。

因此通过上面的描述,我们就可以回答1.2开始时所问的问题:操作系统要如何进行管理呢?

答:所谓的对进程进行管理,会变成对进程对应的PCB进行相关的管理,也就是说:对进程的管理变成了对链表的增删查!

最终,通过上述的解释,进程究竟是什么我们也就知晓了:进程 = 内核数据结构(task_struct) + 进程对应的磁盘代码

3.为什么要有进程控制块(PCB)呢?

通过上一篇介绍的软硬件体系结构以及刚才的描述,对于为什么要有PCB进行了解答:

对进程管理的核心是对数据进行管理,因此当我们加载程序到内存之前,我们必须拿到所有程序的数据,由于拿到的数据杂乱无章并且未进行分类,这时候就需要PCB将其归类,将对应的数据放到相应的进程控制块里!

image-20221110160400676

即加载进程的时候,操作系统为了方便管理会new一个struct task_struct也就是进程控制块的结构体,然后一点点的将上面加载的数据填充到这里的内部属性(状态,标记,追踪等),因此这里再一次强调了:进程不是程序加载到内存,而是在内存中new了task_struct结构体!

2.进程的基本操作

2.1 查看进程

这里演示过程:

  1. 创建文件(Makefile、myproc.c、myproc)

    image-20221101170553126

  2. 执行,并通过打开复制ssh渠道,输入指令:

    ps ajx | head -1 && ps ajx | grep "myproc"
    

    (//&&的左右是两个指令,通过&&可以将两个指令一起执行。左面是通过管道将进程的第一行显示出来,右面是将myproc相关的进程信息打印出来,不利用管道将会出现其他没必要的信息)

    动图:(在运行中,进程就具有动态属性)11.1_1

    image-20221101171753104

2.2 结束进程

结束进程有两种方式,下面就来介绍:

1.通过指令结束进程

kill -9 PID

image-20221101172012829

这样就可以结束掉我们的进程了。

2.通过ctrl + c快捷键

通过快捷键ctrl + c也可以结束进程。

2.3 查看进程的另一种方式(了解)

通过上面的描述,查看进程和结束进程我们都已经了解,在这里还要引入一个新的关于查看进程的知识。

image-20221110170215972

仍是对于这个程序,我们让其运行(实际上进程在调度运行的时候,就有了动态属性)。

通过指令:ls /proc/5058我们就可以看到这个进程中的信息。

image-20221110170745366

这也可以说明,进程实际上也是一个Linux中能够保存的文件。我们进入到5058:

image-20221110170944967

我们发现,其中生成了一个这样的.exe文件,这实际上就是我们正在运行的程序。

那如果我们把这个文件删除了,程序还会不会运行呢?我们接下来试一试:

image-20221110171720531

我们发现,左面的颜色变红提示了已经删掉,但是右下角的程序仍然在进行,这也恰恰说明了加载到内存的数据不会受到磁盘文件的影响!

2.4 进程的系统调用(getpid)

我们发现,上面的执行过程中我们如果想要结束进程,就需要kill 9 PID,而这个PID的值我们该如何获取呢?我们可以通过getpid函数获取。

我们打开手册 man getpid:

image-20221101172715981

我们发现,getpid()函数的返回类型是pid_t,这是我们在C/C++所不曾见到的类型,而getpid()的功能就是:返回这个进程的pid。

因此我们就可以根据这个信息编写代码:

image-20221101173033670

image-20221101173412044

我们发现,确实是一样的。也就是说,当我们想知道一个进程的PID,就可以通过getpid获取。

2.5 常见进程调用(父进程、子进程)

1. 父进程与子进程

我们将上述myproc.c增加一个父进程的打印:image-20221101191004690

image-20221101192225844

当我们结束时,发现父进程的PID并没有发生变化,事实上父进程的PID就是bash即命令行的PID,父进程本身因此在下一次登陆之前,父进程的PID不会发生变化。

2. 子bug父进程的变化

为了解释这个,我们将myproc.c内部增加一个bug:image-20221101192910792

我们看看结果:image-20221101193055032

这说明这个程序执行了,并且报错了,但是仍然可以通过命令提示错误,因此我们可以看出,程序执行是以子进程在执行,其出错并不会导致父进程错误,父进程也就是命令行的进程,因为提示的错误就是父进程在提示!因此父进程并没有错误!

2.6 通过系统调用创建进程-fork初识

1. fork创建子进程

我们通过man fork了解到,fork是创建子进程的函数,但是当这个函数执行前,只有自己本身这个进程和他的父进程,执行之后,这个自己本身的进程就会变成子进程的父进程,而相应的这个进程的父进程也就变成了父进程的父进程。

image-20221101195312940

image-20221101200113612

image-20221101200252035

我们发现,这就相当于bash是爷爷进程,而程序本身是父进程,fork创建出的的是子进程。

2. fork返回值

image-20221101201032505

通过man查看fork手册/return val,我们发现fork有两个返回值,那具体是什么含义呢?接下来我们实验一下:image-20221101200832164

结果:image-20221101201149753

我们发现,同一个变量id,在后续不会被修改的情况下,竟然有不同的内容!但是这里我们还不知道为什么也没办法解释,因此在这里我们打不过,就加入他,利用这个规则,我们来看看下面的程序:(注意sleep这里的细节)

代码:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
int main()
{
    // 创建子进程 -- fork是一个函数 -- 函数执行前:只有一个父进程 -- 函数执行后:父进程+子进程
    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        while(1)
        {
            printf("子进程,pid:%d, ppid:%d,id:%d\n",getpid(), getppid(), id);
            sleep(1);
        }
    }
    else if(id > 0)
    {
        // parent父进程
        while(1)
        {
            printf("父进程,pid:%d, ppid:%d, id: %d\n",getpid(),getppid(),id);
            sleep(2);
        }
    }
    else
    {

    }
    printf("我是一个进程,pid:%d, ppid:%d id:%d\n", getpid(), getppid(), id);
    return 0;
}

image-20221101202351955

结果(动图):11.1_2

我们发现,两个进程同时执行,即if可以执行,elseif的也一起执行,没错,if和elseif竟然同时执行了,这就是引入fork的缘故(多进程)。

即fork()之后,因为pid既可以是0也可以是1,会有父进程+子进程两个进程在执行后续代码(即两条路线去执行if和else if两个分支),fork()的后续代码,被父子进程共享,让父子进程执行后续共享代码的一部分,这就叫做并发式的编程!

3. 进程状态

对于运行状态,有以下几种:

image-20221110200944889

对于这些运行状态,都是操作系统层面上的说法,为了让理解的更加深刻,接下来以两方面去解释:

3.1 普遍的操作系统层面

进程状态的概念: 通过上面的学习我们知道,当一个程序执行加载到内存时,操作系统就会创建对应的PCB(进程控制块:struct),而在这个PCB结构体中,有一个位置存放着整型的成员变量,而这个整形变量的不同数值,就代表不同的进程状态。

进程状态一共有九种:运行、新建、就绪、挂起、阻塞、等待、停止、挂机、死亡 ;其中运行、阻塞、挂起是最为重要的进程状态。下面就讲解这三个最重要的状态:

1.运行状态R

对于运行状态来说,并不是在CPU中正在运行才是运行状态,只要是进程在CPU的等待队列中,那么就可以称之为进程的运行状态(R)

2.阻塞状态(T)

对于阻塞状态来说,进程在磁盘中的阻塞队列中,就称之为进程的阻塞状态。

通过上面的描述,事实上等于没说什么,什么是运行队列?什么是阻塞队列?为什么会有运行队列和阻塞队列?二者之间存在着什么关系? 事实上这才是运行状态和阻塞状态的切入点,那我们带着疑问一起了解:

当一个进程开始执行时,会将二进制代码从磁盘加载到CPU中去执行,但是我们知道,进程的数量远远多于CPU的数量,这个时候操作系统就会将这些进程进行管理,让一些进程去等待,另一些进程去运行。假设我们只有一个CPU。当一个进程正在执行是,那么其他进程就需要去排队等待CPU资源(CPU会维护一个运行队列让这些要执行的进程去排队,这个运行队列是内核为CPU准备的,一个CPU,一个运行队列)(注:让进程入队列的本质就是将该进程的PCB结构体对象放入运行队列中,而不是让进程(程序)自己去排队)而等待的这些进程都在运行队列中,那么他们就都处于运行状态!

我们知道,冯诺依曼体系结构中的CPU很快,但是(外设)磁盘相对较慢,但是进程除了访问CPU,也避免不了访问磁盘(外设),比如fwrite就是将进程与磁盘之间相联系(而对于scanf、cin这些函数也会访问显示器,一些代码也会通过网络访问网卡,这些都是与外设想交互)但是磁盘很慢,不像CPU一样,所以当A用着,其他的进程就需要等待,因此这样看来进程也可能在外设上等待,也就是说,不要只意味着进程只会占用CPU资源,进程同样也会占用外设资源,因此外设也有自己的描述结构体,也可以维护自己的等待队列! 进程在外设的等待队列中就是进程的阻塞状态!

由于外设的等待队列过慢,CPU因为会执行代码但被外设的速度限制,这时CPU就会对外设说,不好意思,你太慢了,我不想等你了!因此这个时候CPU就会将对应的代码的进程从CPU的等待队列(也就是运行队列)中放到外设的等待队列中,从而去执行CPU运行队列中的下一个进程。

这就好比什么呢?就相当于当你去银行办理业务,当到你到指定柜台去填表时,由于你填的太慢,这时候工作人员为了不让你占用过多时间,就会让你离开窗口,去旁边的桌子上继续填表,为的就是不耽误你后面人的时间让他们继续办理业务。因此这就将这个进程从CPU的运行队列里强行拉到了外设的等待队列中。这个进程也就从运行状态切换到了阻塞状态!

而当你填完表之后,你通过其他工作人员的告知,就会直接进入窗口处理业务,也就是从阻塞状态直接变成运行状态,这个操作就是CPU自动调度而不是操作系统处理的,因为操作系统之前已经处理完你,也就是把你从窗口移到了附近的桌子,就不会继续管你了!

需要注意的是:我们所提到的这些拖拽,排队,不是进程本身的行为,而是他们PCB的行为!

因此所谓的进程的不同状态,本质就是进程在不同的队列中,等待某种资源!

对于新建和就绪状态其实是同一种状态,很好理解,就是这个进程刚刚被创建好,也就是我们所谓的make,因此我们不需要去描述。

3.挂起状态

将内存中进程的数据和代码转移到磁盘的状态被称为挂起状态。

为了解释挂起状态,我们建立这样一个场景:如果阻塞的进程过多,那么他们是不会被立即调度的,也就是说不会将这些进程从阻塞状态同时转换成运行状态,因为这些阻塞的进程本身也需要彼此之间进程排队。那么我们知道,在进程阻塞时,其对应的数据和代码还在内存中,万一内存不够了,怎么办?

由于过多的进程站着茅坑不拉屎的不良行为,这时操作系统就会体现他的义务将这些有不良行为的阻塞进程体验社会的毒打,因此为了不让阻塞的进程占用内存,操作系统就会将阻塞状态的进程的数据和代码转移到磁盘上储存,这样这个进程就从阻塞状态变成了挂起状态!

此时内存中仍有挂起状态进程的PCB,只是其中的代码和数据转移到了磁盘,减少了内存占用的空间。将内存的相关数据加载或保存到磁盘,叫做内存数据的换入换出。

阻塞状态和挂起状态的区别:阻塞不一定挂起,但挂起一定是阻塞!

就好比几年前用过的安卓手机,比如王者荣耀,如果手机用的时间久或者内存小的话,每一次后台运行之后点进去,就有可能重新开始而不是接着上次的界面,这就是因为占用内存过多,内存不够,于是操作系统(安卓系统)将这个进程挂起了。

3.2 具体的Linux操作系统层面

为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
下面的状态在kernel源代码里定义:

/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};

其中,这个结构体中的各种状态就一一对应着我们在普遍的操作系统层面上的状态,分别是:运行状态,睡眠状态,深度睡眠状态,停止状态,停止追踪,死亡状态,僵尸状态。下面先从定义上描述一下一些状态,之后再具体分析。

  • R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
  • S睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
    (interruptible sleep))。
  • D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
  • T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
  • X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

1. R运行状态

先来看看执行的代码:

image-20221111133554188

那当我们执行,看生成的可执行程序是什么状态:(STAT就代表这个进程的状态)

image-20221111133852468

通过./myprocess,执行后这个程序便处于运行状态(R+)。

2. S睡眠状态

我们将上面的代码加上一行printf输出的形式:

image-20221111134129741

有了printf,就需要动图展示一下具体的状态了:(动图)

11.1_3

我们发现,在打印的过程中,myprocess的状态不是运行状态,而是S+休眠状态。事实上,这是由于我们通过printf访问的是外设,而我们知道外设很慢,因此进程等待显示器就绪需要花费很长时间,于是就会把该进程的PCB转移到外设的PCB上排队,这也就对应了普遍操作系统层面上的阻塞状态。

当然,这也不是绝对的,只是对大多数进程而言(99%),因为大多数都是在等待显示器就绪,如果我们想访问变成R状态,可以sleep一个很大的秒数,这样就有机会看到是R状态了

3. D深度睡眠状态

我们之前用的printfscanf事实上都算是浅度睡眠,即S即为浅度睡眠状态,浅度睡眠状态是可以终止的,也就是ctrl c就可以直接终止。而深度睡眠状态是不可以终止的,即无法被操作系统杀死,只能通过断电解决。

对于这个状态,我们通过想象这样的一个场景去理解他:image-20221111143524815

当一个用户给操作系统、进程、磁盘三个人制定任务时,其中进程和磁盘说这是1万条用户数据,你帮我拷贝一下,磁盘答应了,但是磁盘有可能拷贝成功,也有可能失败。然而这个时候由于进程过多,内存不够了,此时操作系统就会站出来将一些不干活的进程杀掉,但内存仍然不够,于是操作系统秉着他的责任,将所有的进程全部杀掉,自己累的够呛也挂了。但这时磁盘处理数据失败了,就回去找进程,让他重新搞来一份数据,但是此时进程没有回应他,喊了很多次仍无人回应,最后也就这样草草了事。

当用户知道这个任务没有完成,就找到他们三个追究责任,磁盘率先站出来说:这不关我的事,我本来就有可能成功有可能失败,当我失败的时候去找进程发现他不见了,和我没有关系;这时候进程也出来说话了:这和我也没有关系,我是被操作系统杀掉了,怎么能回应你呢?最后操作系统发现他们两个把矛头指向了自己,就气愤的说:我有我的职责,内存不够了,我必须杀掉进程防止内存不够,这就是我的任务,并不是只针对你这一个进程,所有进程在我眼里都是一样的。用户听了他们说的话,却都觉得没什么问题,于是就对操作系统说:下次对于这样的情况,不要杀掉指定的进程,这就相当于给了这个进程一个免死金牌,操作系统也是清楚了具体的规则,同意了,进程和磁盘也表示没问题。这个时候,这个给予免死金牌的进程就处于所谓的深度睡眠状态,不可被操作系统杀死,只能断电处理!

需要注意的是:深度睡眠一般只会在高IO的情况发生下,且如果操作系统中存在多个深度睡眠状态的程序,那么说明该操作系统也即将崩溃了。

4. T停止状态

将printf注释掉:

image-20221111140130669

当我们查看kill 手册,找到对应的19号选项后:

image-20221111140526638

我们发现,状态就从R状态变成了T状态,变成了之后呢,T状态代表什么呢?事实上,T状态也是阻塞状态中的一种,因为其代码不被CPU执行,但是其属不属于挂起状态呢?这个我们无从得知,因为这是操作系统所做的事情。

既然有暂停,那么就一定有继续。我们看上面的kill选项中,18号就是继续的选项,那我们来看看:

image-20221111141001132

我们发现,又变回了R状态,只不过有个细微的差别,和最初的R+相比,+不见了。

如果我们将程序加上printf,让其变成S+状态,当我们再对应的显示器上输入除了ctrl c的其他命令行,我们会发现其并不会执行,而显示器上照常打印,这就是所谓的前台进程。如果我们将其T掉,再kill 18对应的进程让其继续,我们会发现状态变成了S,出现了和运行状态时一样的情况,而这时当我们再输入命令行时,会发现可以显示对应的结果,并且可以继续打印,这就是所谓的后台进程,但是对于后台进程,不能用ctrl c结束,只能通过kil -9 PID的形式结束进程。因此我们也就知道了+的意义,有+的是前台进程,没有+的是后台进程。

5. t追踪暂停状态

对于追踪暂停状态,其实是一种特殊的停止状态,即进程在调试时就处于追踪暂停状态:(gdb)

image-20221111145754245

6. X死亡状态

死亡状态代表着一个进程结束运行,该进程对应的PCB以及代码和数据全部被操作系统回收。

7. Z僵尸状态

  • 僵尸状态是什么?

我们知道,进程被创建出来是为了完成任务的,而完成的结果也是需要被关注的,即进程完成的结果会被其父进程或者操作系统接收,因此在进程退出的时候,不能释放该进程对应的资源,而是保存一段时间,让父进程或者操作系统来进行读取。因此在这个进程退出后到被回收(OS、父进程)前的状态就是僵尸状态!

  • 僵尸状态的危害

对于僵尸状态的进程,事实上不是数据存在在内存,而是其对应的PCB在内存中占用空间,因此如果这种进程过多导致PCB占用内存过大,并且父进程和操作系统没有及时回收这种资源,那么就极易发生内存泄漏。由此可见。除了malloc或者new,系统层面上也是有可能发生内存泄漏的。

  • 如何解决僵尸状态?

既然有僵尸状态的危害,就一定有解决的方法,解决的方法将在后续讲解,在此只需要知道僵尸状态是什么,有什么危害就是我们这一节的目标。

总结: 具体的Linux操作系统下的进程状态和普遍的操作系统上进程的状态的分类是不同的,Linux操作系统和普通的进程状态相比没有阻塞和挂起状态,普通OS的阻塞状态在LinuxOS中通过睡眠、深度睡眠、暂停、追踪暂停等状态表现出来,而进程处于这些状态时是否会被调整为挂起状态,用户是不可得知的,因为操作系统没必要将挂起状态暴露给用户,用户也不关心一个进程是否会处于挂起状态。

4. 两种特殊的进程

4.1 僵尸进程

上述已经提到过僵尸状态的进程的概念,那么在这里就详细演示一下僵尸进程的具体面貌:(代码)

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>

int main()
{
    int id = fork();
    if(id > 0)
    {
        while(1)
        {
            printf("我是父进程,pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else if(id == 0)
    {
        while(1)
        {
            printf("我是子进程,pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else 
    {
        perror("fork fail");
        exit(-1);
    }
    return 0;
}

image-20221111172840152

我们发现,通过sleep,为4044的子进程被杀掉了,此时父进程并没有处理子进程,因此出现了default(失效)并且子进程变成了Z状态,并且左侧也验证了只剩下父进程,并没有子进程的存在了!如果父进程一直不对这个子进程进行收取,那么这个子进程就会变成僵尸进程。

4.2 孤儿进程

1. 什么是孤儿进程?

上述我们提到,如果一个子进程被杀,那么其暂时就会处于僵尸状态,如果没有父进程回收就会变成僵尸进程。那如果是父进程被杀,父进程和子进程又会发生什么呢?事实上,父进程被杀,即父进程比子进程先退出,那么剩下的子进程就叫做孤儿进程。 这种现象也确实存在。

2.孤儿进程的表现形式

我们知道,如果一个进程变成了僵尸进程,其进程状态就会变成T,那我们来看看孤儿进程是如何表现的。

  1. 首先,我们仍然展示一下具体代码:(和僵尸进程中的代码一致)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>

int main()
{
    int id = fork();
    if(id > 0)
    {
        while(1)
        {
            printf("我是父进程,pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else if(id == 0)
    {
        while(1)
        {
            printf("我是子进程,pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else 
    {
        perror("fork fail");
        exit(-1);
    }
    return 0;
}
  1. 接下来,复制SSH渠道,杀掉父进程,观察子进程的状态。(父进程也关注一下)动图展示:11.1_4

结果:image-20221112113039019

我们发现,有三处发生了变化,其一是父进程被杀之后,变成了S状态,但是这样不就与我们之前的僵尸状态违背了吗?为什么不是T状态呢?事实上我们在上述僵尸状态中讲过,在被父进程回收之前就是T状态,而这个父进程被杀掉,也有其相应的父进程,这个父进程的父进程就是bash,bash相比较普通的父进程,bash进程将他的子进程进行了及时的回收,而这个父进程却不会对其子进程进行及时的回收。因此对比之下bash进程比一般的父进程更加负责。

其二就是我们子进程的状态,由S+变成了S进程,即从前台进程变成了后台进程,这就是孤儿进程最明显的变化,变成后台进程后不能通过ctrl c快捷键暂停,而只能通过kill结束进程(当然,断电处理也可以,但是没必要)

其三我们发现,子进程的PPID也就是这个子进程的父进程因为被杀掉而变成了1,而这个1所对应的其实就是操作系统,即父进程被杀掉之后,这个子进程被操作系统所领养,操作系统就变成了他的父进程,这也正对应了冯诺依曼体系中的进程被操作系统所管理。如果不领养,那么当这个子进程被杀掉时,就会因为没有后续处理从而将变成僵尸进程,这是整个体系都不愿看到的结果,因此操作系统必须领养。

5. 进程优先级(了解范畴)

对于进程优先级,我们采取三个问题将这个概念解释清楚:

1. 什么叫做优先级?

只凭字面意思来说,优先级和权限有没有区别呢?答案是一定有的,即权限是能做或者不能做的问题,而优先级是先做和后做的问题。

2. 为什么会存在优先级?

那为什么会存在优先级呢?那是因为在一定范围内的资源是有限的,为了获得这个资源就必须赶在其他人的前面,否则就有可能最后什么也捞不到。举个例子:我们知道在一个内存中有许多的进程,但是CPU只有一个,这个时候进程为了能够先执行就会产生优先级的概念,即重要的进程先运行,其他的进程后运行。

3. Linux优先级的特点

在Linux操作系统中,在ps ajx 选项中出现的PRI(priority)下的数字就是所谓的优先级,即这个数字和我们现实中的排名一样,数值越低,优先级就越高。而这个数值PRI = 基础数值(80) + nice值,(NI)nice值是可以进行修改的,其修改区间为[-20,19],因此PRI的范围为[60, 99]。

仍然是利用孤儿进程的代码:

演示:通过指令ps -l PIDimage-20221112140055964

我们发现,初始状态下,PRI和NI的值为80和0,加起来就是基础数值。

接下来进行修改:

通过如下指令:进入top后按“r”–>输入进程PID–>输入nice值

第一步:sudo top(改变优先级需要提权)

image-20221112140627619

第二步:输入r,输入对应要修改优先级进程的PID,回车

image-20221112140740039

第三步:输入修改之后的NI值。回车。

image-20221112140820794

这样就修改完成了,接下来我们看一看结果:

image-20221112140907269

这样其PRI就变成了80+19 = 99。

6. 进程的其他概念

  • 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
  • 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
  • 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
  • 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发(一段时间采用:时间片轮转的方式)

7. 进程切换

在定义之前,我们需要举例引入进程切换的概念,对于我们常用的手机、电脑等,一般只有一个CPU,我们知道一个CPU一次只能运行一个进程,但是我们发现,在电脑上,我们可以在打开PDF的同时,去浏览一些其他网页,即同一个时间段内,多个进程可以被CPU同时运行,这是为什么呢?事实上这就是进程切换的效果。

进程切换的理解: 由于Linux的CPU一次只能运行一个进程,但是我们一个时间段内却可以运行多个进程,因为CPU是足够快的,因此我们人感觉的一瞬间就相当于CPU的一个时间段,想一想1ms对于人来说算是一瞬间,但是CPU却是以纳秒为单位计时的,因此在我们自身感觉到的一瞬间也就是CPU的一个时间段内,会将执行的多个进程按照一定的周期分别运行,一个运行到固定周期之后就强行拉入运行队列的末尾等待,就这样直到完成所有执行的进程,这就是进程之间在一定的时间内相互切换,叫做进程切换。而所谓的周期就是时间片。(并发中提到)

进程的上下文保护:

当CPU在进行进程切换的时候,要进行进程的上下文保护,当进程在恢复运行的时候,要进行上下文进程的恢复!

上下文是什么呢?

进程在运行时会产生非常多的临时数据,同时CPU中存在一套寄存器硬件,当进程运行时,进程的PCB会被放入CPU内的寄存器中,此时CPU就可以通过进程PCB(暂时理解成PCB)得到进程代码数据的地址;CPU在运行进程时所产生的大量的临时数据也都会被保存在寄存器中;因此在进行进程切换时需要进行进程的上下文保护与上下文恢复,进程停止运行时将寄存器里面的数据保存起来,进程重新运行时将保存的数据再放入到寄存器中;所以进程的上下文就是一个进程完成他的时间片后在CPU中所保存的临时数据。

CPU寄存器硬件被所有进程共享,但CPU在具体运行某一进程时,CPU寄存器中的数据只属于该进程;同时,我们进行上下文保护时保存的是寄存器中的数据,而不是寄存器硬件。

  • 39
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 37
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 37
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

每天都要进步呀~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值