进程状态概念详解

进程状态

进程状态分为:运行,新建,就绪,挂起,阻塞,等待,停止,挂起,死亡.........

上面的状态都是操作系统的说法!

所以我们要从普遍的操作系统的层面去理解上面的概念!进而去理解linux下面的进程状态的概念!

为什么会有这些状态

首先我们就要对操作系统有个宏观的概念!

image-20221216221136958

==怎么多状态的本质就是为了满足不同的运行场景!==

==上面的所有结构体和数据都是存放在内存里面的!因为操作系统要一开始就被加载在内存中运行!==

运行

因为cpu的核心个数是远少于进程个数的那么进程是如何在CPU里面运行的呢?

就是通过CPU在内核中维护的运行队列来实现的!

==一般来说一个CPU对应一个运行队列!==

让进程入CPU的本质就是将进程入这个运行队列!——但是也不是让进程的代码入队列!而是让进程对应的PCB(task_struct结构体对象)入这个运行队列中!!

所以让进程去排队本质就是让进程的PCB去排队!

举个例子 我们去面试我们是将描述我们的信息的简历投到公司的邮箱里面!

公司也是通过看一个个的已经排序的简历来挑选的,最后也是通过简历来联系到我们的!

CPU的运行逻辑也是一样的!

CPU的运行速度是非常快的!所以CPU将运行队列里面的程序全部轮转完一遍也是很快的!所运行队列上进程就必须随时都准备好让CPU来调度运行它!

==所以将运行队列里面的一个个进程(PCB)就叫做运行状态!==

可能这样说很奇怪但是说白的讲 只要在运行队列里面的进程就是运行状态!

image-20221216155049731

==要区分一下!不是进程在CPU上跑了才叫运行状态!==

==只要进程在运行队列里面!那么这个进程就叫运行状态!==

==进程状态是为了来区分进程是否在运行队列里面!不是为了区分是否则在被CPU运行!因为CPU运算速度是非常快的所以没有意义!==

总结

一个CPU对应一个运行队列!

让进程进入队列,本质:将该进程的task_struct结构体对象放入运行队列中

进程PCB在runqueue中就是R(linux下的运行状态表示),不是这个进程正在被运行就是运行状态!

那么这个“状态”是保存再哪里的呢?

状态是一种进程的内部属性!那么进程状态也是在task_struct(PCB)里面!

一般在内核里面都是用整数来表示一种状态这个整数是几

例如 (1:run 2:stop 3:hug .......)

阻塞

根据冯诺依曼体系 CPU虽然十分的快!但是外设的速度相比起来就十分的慢了!

==但是进程或多或少的要去访问外设==!例如:我们要想写一个程序想让显示器显示一句话

==而且外设也是少量的!==例如显示器,键盘,显卡这些一般都是只有一个的!

但是访问这些少量外设的往往都不止有一个进程!所以这时候进程就要进入等待!

这样意味着进程不止要占用CPU资源 同时也需要占用外设资源!

所以为了应对多个进程的访问!那么每种外设的结构体里面都有和CPU运行队列相似的的等待队列!

image-20221216162256141

这些队列都有各自的硬件维护!

当一个CPU正在执行某个进程!而这个进程的某个代码要进行写入操作!那么就要访问磁盘可是现在磁盘的等待队列已经有好几个进程了!

这时候CPU就不会选择等待这个进程在磁盘中写入完毕,会直接执行下一个进程!(如果等待的话那么就很拖累cpu的速度!)这样就可以保证CPU永远执行运行的进程!

同时原先的这个进程会被从CPU的运行队列中剥离,然后放入磁盘的等待队列中!

==而此时这个被放在外设的等待队列的进程我们就称之为阻塞状态==

image-20221216163238837

==阻塞状态的意义为当前资源不能直接被调度!要等待某种资源!==

==其实状态修改的本质就是将进程的task_struct 对象放到不同的队列中!==

当磁盘执行完一个进程,就会告知操作系统!如果等待队列里面还有阻塞进程

那么首先操作系统就会将这个进程状态改回运行状态然后放回CPU的运行队列里面!

这样子等轮到这个进程重新被CPU执行的时候就可以顺利的访问磁盘了!

因为是先入的队列!所以在这个进程执行完之前是不会有新的进程去访问磁盘!

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

就绪/新建

进程刚刚被创建出来的状态就是就绪或者新建!(这就是进程的初始状态!)

挂起

从上面我们知道一旦一个进程需要访问外设但是无法立刻访问到的时候!

进程PCB就会进入外设的等待队列变成等待状态!

那么如果有很多个进程同时进入阻塞状态呢?

从上面我们知道

  1. 我们知道一旦磁盘空出来进程也不会被立刻调度!而是操作系统先把进程状态修改

    然后放入CPU的运行队列里面!

  2. 有大量的阻塞状态的进程那么就意味着要等待很长时间(短期内不会被使用!)

PCB和程序代码都是要占内存的!运行的程序也要占内存

万一存在了大量的阻塞状态的进程导致了内存不够的时候又想要运行新进程应该怎么办?

所以作为调度资源的操作系统 ==操作系统会将这些阻塞的进程的PCB保留在内存但是代码和数据暂时保存在磁盘上!不放在内存里面==这样就可以节省一部分的空间!

image-20221216171847872

==这种将一个进程的代码和数据换出到磁盘中,我们就称该进程被挂起了!==

要区分挂起该进程不是释放该进程!因为该进程的内核数据结构(PCB)仍然在内存里面!

只是因为代码和数据暂时不用所以置换出去!

等到资源准备完毕了!那么操作系统就会将该进程的代码和数据重新加载到内存里面!然后修改内存状态改为运行状态!

我们一般将进程的相关数据保存或者加载到磁盘中 称为内存数据的换入换出!

阻塞和挂起的区别!

阻塞不一定挂起!

但是挂起一定阻塞!

阻塞就是要等待资源

挂起就是要我们操作系统进行相关的换入换出的操作!

但是挂起状态也有很多种

例如 :阻塞挂起状态——就是该进程阻塞了然后数据被挂起!

新建挂起状态——该进程一新建就被挂起了!

==但是只要不是运行状态,上面我们谈到的所有状态都可能会被挂起!所以挂起状态也经常和其他状态组合==

linux下的进程状态

image-20221216202605161

这就是linux内核中的进程状态

运行——R

int main()
{
    while(1)
    {
        
    }
    return 0;
}

我们在linux下运行一个简单的小程序

查看一下进程

image-20221216203747708

红线画出的便是我们执行的进程! stat就是我们的进程状态!

stat显示 R+ 这就是一个典型的运行状态!

睡眠——S

int main()
{
    int a = 0;
    int cnt =0;
    while(1)
    {
        a = 1+1;
        printf("当前a的值为 %d,cnt = %d",a,cnt++);
    }
    return 0;
}

image-20221216204715771

我们发现我们加了一行打印后运行状态一下子从R变成立S

这是为什么呢明明左边的代码是一直在跑的!

但是却不是R(运行状态)反倒是S(睡眠状态)

这是因为我们这个程序它访问了显示器——外设!

而我们知道外设是比较慢的!打印数据是很快的!但是等显示器就绪是要花很长时间(相比CPU)所以这个程序就是S状态

==这个程序绝大部分时间都是在等待IO就绪 只有极少部分时间在执行打印代码!==

==所以我们查过去的话有很大概率是S状态!==

==所以大部分进程在访问外设的时候都大概是S状态!==

==这个S状态其实就是linux下面的阻塞状态的一种!==

暂停——T

暂停状态大部分教材会把这个状态归结到挂起和阻塞里面,但是linux下是有这个状态的!

int main()
{
    int a = 0;
    int cnt =0;
    while(1)
    {
        a = 1+1;
    }
    return 0;
}

image-20221216210253598

我们发现一开始运行的时候程序就是R状态!

但是我们可以通过linux下面的kill 指令让程序进入T状态!

==此时这个T状态可能被挂起,也可能不被挂起!因为是否挂起是由操作系统决定!==

==但是它是属于阻塞的一种!==

==因为此时没有代码在运行!==

image-20221216210627288

然后我们也可以通过kill指令让代码重新跑起来!

==之所以看不到挂起状态是因为挂起状态完全有操作系统来决定!我们是无法进行干涉的知道了也没有作用!我们也只需要关心进程有没有运行就行!所以一般都是看不到挂起状态的!==

关于+号

我们上面也看到了有的进程状态后面有个+号 而有的后面突然就没有了这是怎么回事?

image-20221216211232235

我们可以看到在有加号的情况下我们在它运行的时候无论我们输入什么都是没有反应的!

程序任然会继续运行!

==这样的进程我们称为前台进程!也就是说一旦这个程序运行了shell命令行就无法获得命令行解析了!除了使用ctrl+c来停止进程!==

image-20221216211726655

然后我们我们尝试暂停它 然后在继续!

我们会发现!此时+号消失了!

image-20221216211849182

然后我们会发现命名行解释器又生效了!

但是此时ctrl+c已经无法停止程序了!image-20221216211940559

这是因为此时这个进程已经变成了后台进程!

此时只能使用kill -9 命名来杀掉!

深度睡眠——D

S状态其实是一个浅度睡眠!浅度睡眠是可以被ctrl + C进行终止的!

深度睡眠一般是很难看见的!只在高并发高IO的时候常见!

为了方便理解我们先假设有一个进程A 拿着几十万条的用户数据向磁盘写入

磁盘和其他外设不同保存着数据很重要!其他外设可以休眠后随便终止掉!但是磁盘不一样

一般来说当出现大规模的数据IO的时候,一旦出现了内存紧缺,==操作系统首先会去挂起进程,但是一旦连挂起都无法解决的时候!那么操作系统就会自主的杀掉进程!==

image-20221216213842743

==因为内存紧缺导致了进程A被杀掉!但是当磁盘写入数据完毕应该向进程A报告,如果进程A被杀掉了,那么数据就可能发生丢失!==

所以为了防止这种情况!所以有了D状态!这种状态下操作系统即使是内存紧缺操作系统也不会去杀掉进程!防止数据的丢失!

==D状态直白的讲——该状态下的进程,无法被OS杀掉!只能通过断点,或者进程自己醒来来解决!==

我们发现linux下其实没有写什么新建,就绪,阻塞因为这些状态都是以操作系统的角度来进行的一个总结!

像是D ,T ,S都可以被归类到阻塞或者挂起!

像是运行状态在 linux下就表现为R

追踪暂停 ——t

t也是一种暂停的状态!当我们在使用gdb的时候!程序也是暂停的!但是又与一般的暂停有所区别所以独立出来的一种状态!

int main()    
{    
    printf("hello debug\n");    
    printf("hello debug\n");    
    printf("hello debug\n");    
    printf("hello debug\n");    
    printf("hello debug\n");    
    printf("hello debug\n");    
    printf("hello debug\n");    
    printf("hello debug\n");    
    printf("hello debug\n");    
    printf("hello debug\n");    
    printf("hello debug\n");    
    printf("hello debug\n");    
    printf("hello debug\n");                                                                                                                         
    return 0;    
}    

image-20221217143608683

我们可以发现我们调试的程序处于t状态 而我们的gdb处于S状态 因为此时gdb在等待我们进行输入!

==t状态 表示我们的进程现在正在被追踪==

死亡——X

​ 在死亡后进程会被操作系统给回收,因为一旦进程一旦死亡操作系统的回收是十分的快速的!

所以我们一般也看不见死亡状态!

僵尸——Z

==僵尸状态为什存在?==

首先我们要明白 进程被创建 一定是为了完成某种任务!

当执行任务的时候 我们是不是得知道任务执行的如何呢?但是我们是不是也可以不关心任务的结果呢?

但是无论是那种情况操作系统父进程一定要把任务的结果给保留下来,无论我们是关心还是不关心

==如果我们想要知道进程完成的怎么样,就代表进程退出的时候!不可以立即释放该进程对应的资源!要保存一段时间让父进程或操作系统进行读取!==

==而这个进程从退出到被操作系统/父进程获取读取结果之前保留的资源的这段时间我们称之为Z状态!==再被操作系统/父进程读取结果后才会被从Z状态转换成X状态!最后被资源回收!

举个例子:当我们在路上遇到了一个因为跑步倒下的人,那个人疲劳猝死了,这时候我们绝大多数人应该会选择报警,等警察来了之后,警察还会让人去做一个医学鉴定,确认这个人已经死亡,一旦确定死亡警察就会让家属去处理后事

而我们把进程比作那个猝死的人 从倒下到完成医学鉴定的这段时间 我们就可以称为僵尸状态,等医学检测出来(操作系统/父进程读取玩结果)就是从Z状态转换成X状态

==僵尸状态是一个很大的问题!——即进程已经退出但是资源却没有办法被释放这就是一种内存泄漏!!==

我们可以用去创建一个程序 让父进程一直正常的运行,但是让子进程正常退出!

那么此时这个子进程就会处于一个资源无法被回收的状态!

#include <stdio.h>    
#include<unistd.h>    
#include <stdlib.h>    
int main()    
{    
    pid_t id = fork();    
    if(id == 0 )    
    {    
        printf("我是子进程 pid = %d ,ppid = %d\n",getpid(),getppid());    
        sleep(3);    
        exit(1);    
    }    
    else    
    {    
        while(1)    
        {    
    
            printf("我是父进程 pid = %d ,ppid = %d\n",getpid(),getppid());               sleep(1);
        }                
    }        
    return 0;
} 

image-20221217153100698

我们可以看到子进程进入了僵尸状态!后面还带了一个defunct!说明了这个进程已经是一个失效的进程!

僵尸进程的危害总结
  1. 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎 么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
  2. 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话 说,Z状态一直不退出,PCB一直都要维护?是的!
  3. 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构 对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
  4. 内存泄漏?是的!
  5. Z状态是不能kill指令杀死的!因为Z状态本身就已经是“死”的没法杀死

孤儿进程——S状态

我们上面讲到子进程先退出,然后父进程不退出会导致一个僵尸状态!

那如果反过来!子进程不退出,但是父进程先退出了呢?

那么这个子进程就称为孤儿进程!

但是我们都知道,一个子进程结束后一定要被回收!那么如果父进程已经先结束了,由谁俩回收这个子进程呢?

我们可以用一个例子来演示一下!

#include <stdio.h>    
#include<unistd.h>    
#include <stdlib.h>    
int main()    
{    
    pid_t id = fork();    
    if(id == 0 )    
    {    
        while(1)
        {
          printf("我是子进程 pid = %d ,ppid = %d\n",getpid(),getppid()); 			  sleep(3);    
        }
        exit(1);    
    }    
    else    
    {    
        while(1)    
        {    
          printf("我是父进程 pid = %d ,ppid = %d\n",getpid(),getppid());                 sleep(1);
        }                
    }        
    return 0;
} 

image-20221217155126024

当我们运行上面的程序的时候!我们可以看到父进程和子进程都是正常的运行的

接下来我们使用kill 指令杀掉父进程

kill -9 

image-20221217155455031

我们可以看到父进程已经被杀死了!但是子进程仍然还在运行!

接下里有几个问题

  1. 为什么父进程被杀掉之后不存在僵尸状态呢?——因为父进程也有它的父进程!命令行下面的所有进程都是bash的子进程!==父进程被杀掉后直接被bash回收了!==我们能看到子进程的僵尸状态是因为它对应的父进程没有进程回收!
  2. 我们发现子进程此时的PPID 变成了1——这个1号进程是什么呢?就是操作系统,也就是说当如果不领养那么子进程对应的僵尸进程,就无人回收资源了!
  3. 系统领养后,这个子进程的+号也消失了,说明这个子进程变成了一个后台进程!——如果前台进程创建的子进程被孤儿了,那么会自动变成一个后台进程!

进程优先级

其实我们一般不怎么关心优先级!优先级一般和调度器有很大关系!linux下我们可以设置优先级,但是很有限。

什么叫做优先级?

linux下还有个概念叫做权限!

那么权限和优先级是一回事吗?不是

权限是决定能不能做!

而优先级是==能做!但是先做还是后做!==

为什么会存在优先级

因为资源太少了!像是我们的CPU一般只有一个或者几个!

但是进程却一般友几十个或者几百个!

所以就得确认优先级

linux下的优先级特点!

计算机里面的优先级的本质就是pcb里面的一个整数数字!(可能是几个!)

linux里面的优先级是用两个整数来代替的!

image-20221217161942731

一个是PRI(priority) 一个是NI(nice)

==最终优先级 = 老的优先级 + nice==

linux支持进程运行中,优先级调整的!调整策略就是通过更改nice值完成的!

不过我们一般都是不改的

top指令——修改优先级

image-20221217165204927

输入top后就会进入这个页面!——使用top修改可能需要用到root权限!

输入r就会跳出一个renice 后面显示的pid就是当前进程的pid

我们可以输入我们想要进程pid用来修改nice值!

image-20221217171007692

这时候就可以调整nice值了!

一般nice值越小,优先级越高!

我们尝试输一个-100和一个100

image-20221217170848987

==我们可以发现linux下nice值的范围是 从 [-20,19]==

为什么要给一个范围呢?这是因为为了防止调优先级导致某个进程过度占用CPU资源!导致调度失衡!

然后我们将这nice值改成9再看看

image-20221217171555770

我们刚刚说过最终优先级 = 老的优先级 + nice值吗?

刚刚我们的优先级是99 那不该是 108吗 或者再不济应该还是 99啊?

原因是如果老的优先级 就是指原先的优先级的话很容易出问题!

所以linux是将这个老的优先级一律设置成80!这样也方便!

总结

  1. PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小 进程的优先级别越高
  2. 那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
  3. PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
  4. 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
  5. 所以,调整进程优先级,在Linux下,就是调整进程nice值
  6. nice其取值范围是-20至19,一共40个级别。
  7. 需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进 程的优先级变化。
  8. 可以理解nice值是进程优先级的修正修正数据

进程其他相关概念!

竞争性

系统进程数目众多,==而CPU资源只有少量==,甚至1个,所以进程之间是具有竞争属性的。为了高 效完成任务,更合理竞争相关资源,便具有了优先级

独立性

多进程运行,需要独享各种资源,多进程运行期间互不干扰

(像是我们一般开启多个软件,其中一个软件闪退是不会影响剩下的软件的!)

==父子进程也是一样!父子进程也是互相独立的!==

并行

多个进程在多个CPU下分别,同时进行运行,这称之为并行

并发

多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为 并发

当一个进程在CPU上执行的时候!CPU是不会将其执行完毕的!当代CPU使用的是时间片轮转的概念!也就是给每个进程一段的时间运行,时间到了就换下一个进程!通过快速的切换的方式就可以在==同一个时间段==里面实现多个进程同时执行!

进程切换

image-20221217175622513

==首先我们要知道CPU一直在执行三件事——1.取指令 2.分析指令 3.执行指令==

CPU里面存在很多寄存器——这些寄存器都是同一套的!

当CPU要执行某个进程的时候!首先会将进程的PCB放到寄存器里面!然后通过PCB来找到该进程的代码!

寄存器的功能有很多

有例如pc(eip)寄存器它的作用就是:存放当前执行指令的下一条指令的地址!——也就是说CPU里面存在的一个寄存器专门用来标记下一次要从当前这个进程的哪一行指令开始执行

参与计算的寄存器,

保存return值的寄存器:我们以前函数执行的时候进行return,但是明明函数过来作用域已经被释放了,主函数是怎么得到的return的值呢,答案也是通过寄存器来进行的!

==当我运行进程的时候 一定会产生十分多的临时数据==

例如我们进行一个加法运算函数

int add(int a,int b,int c)
{
    return a+b+c;
}

那么CPU就要将这些a,b,c变量导入寄存器里面!然后用里面的寄存单元进行计算,然后将结果保存在另一个寄存单元里面,最后计算完毕再拷回去!

所以一定会参数很多的临时数据!

==这这些临时数据是属于当前进程的!==

要明确一点寄存器硬件 不等于 寄存器内的数据!

虽然这些数据是保存在寄存器里面的!

==进程在运行的时候是占用着CPU的!但是进程不能一直占用到进程结束!==

像是我们写个死循环!但是我们仍然能可以使用其他程序

所以进程在运行的试试都有自己的时间片——让程序跑一段时间的概念就是时间片!

==所以可能出现进程没有跑完就被拿下去的情况==——所以就必须考虑下一次进程回来的时候执行到哪里!

==所以对于那些属于进程的临时数据!也必须处理!否则下一次进程再次回到CPU就必须重新开始了!==

所以对于进程离开CPU保留临时数据的过程我们称为==上下文保护!==

当回到进程回到CPU后,恢复==临时数据==的过程就称为==上下文恢复!==

要区分寄存器和寄存器里面的临时数据!——上下文保护是保存寄存器里面的临时数据!(寄存器只有一套,但是里面的数据可以不断的换)

上下文恢复就是将这些临时数据重新写入到寄存器里面!

所以将进程重新回到CPU的时候!要首先进行上下文恢复!

离开后也不是立刻离开!要进行上下文保护!

==寄存器被所有进程共享!但是寄存器里面的数据都是属于当前的进程的!==

一般可以简单的认为是保存在PCB里面