Linux之进程状态和进程优先级

前言

在前面的学习中,我们已经学习了进程的概念和基本创建,以及如何通过相关的系统调用创建进程和获取进程标识符。

那为了弄明白正在运行的进程是什么意思,我们需要了解进程的不同状态


 准备工作

我们使用一个应用的时候,比如我们打开电脑上的爱奇艺看电影,那在看电影的过程中这个应用对应的进程是否是一直在不停的运行呢?

其实它并不是一直在不停运行的。
我们来举个极端一点的场景:
假设现在这里只有一个CPU,但是我们同时打开了多个进程,比如QQ、微信、爱奇艺、网易云音乐等,然后浏览器还有一些下载任务。那这么多的进程在操作系统内被CPU调度运行的时候呢其实并不是从一个进程运行开始,一直不停直到运行结束的,而是每个进程被CPU运行一会儿,操作系统都会把它从操作系统上拿下来,然后把另一个放上来运行,这样重复的快速交替运行的。
一般呢我们把它叫做基于进程切换的分时操作系统,即不同的进程快速切换交替运行,同一时间段内它们的代码都可以得以推进,使得用户感觉多个应用程序几乎同时在运行,因为我们的感官和CPU的运行速度差的是很大的。

所以进程在运行的时候是可以被操作系统管理和调度的:

那这样的话就涉及一个问题,就是在某个时刻操作系统凭什么调度这个进程,让这个进程在CPU上运行而不是其它的进程呢?

那这就取决于进程状态相关的概念。
那在正式学习进程状态之前,我们先来了解两个概念——阻塞和挂起。


阻塞、挂起状态
阻塞

那我们先来了解一下阻塞:

阻塞即进程因为正在等待某种条件就绪,而导致的一种不推进(不被调度)的状态。

比如现在有一个进程被创建了(我们打开一个应用或运行一个程序),但是一直没有被CPU执行,那大家想一下这种情况在我们用户层面看到的是一个什么情况呢?
再比如我们有时候在Windows上启动了好多个程序,就可能会出现“卡”的情况。
那这种情况呢其实就可能是进程太多了,操作系统调度不过来了,目前操作系统正在调度的,就正在运行,没有被调度的,就卡在那了。
所以呢,说成大白话,阻塞就是进程“卡住”了。

再比如:

我们下载一些东西的时候,如果出现了断网或者0KB了,那这个时候这个下载的进度条就也卡住了。当然这个卡跟我们上面说的有的不一样。但是这种情况其实也可以认为是阻塞状态。
所以,我们又得出:阻塞一定是在等待某种资源。

那如何理解这里的等待某种资源呢?首先这里等待的资源可能是什么呢?

比如:磁盘、网卡、显卡各种外设等。

举个例子:

我们在下载某个东西的时候,突然断网了,那对应的进程就会被设置成阻塞状态了,CPU就不会再继续执行你了,你这个进程就要等到网络好了的时候才会被操作系统调度,被CPU继续执行。就好比你去银行办理某个业务,办理之前你需要填一个单子,但是此时单子用完了,相应的工作人员去取了,然后你所在的这个柜台的工作人员对你说,那您先去旁边等一会吧,先让后面的人办理它们的业务,等您拿到单子填好之后再来办理您的业务吧。

那现实生活中的等待我们可能很好理解,那你就搬个凳子坐那里等一会呗,可是这里等待某种资源,它具体是如何等待呢?

首先,对于这些资源,操作系统肯定要管理起来,怎么管理的?

先描述,再组织!先用一个结构体把它们的属性都封装起来,然后再用一个链表或其它高效的数据结构组织起来。

那进程呢?操作系统里面可能存在很多进程,那也要管理起来,如何管理?

先描述,再组织。那就是一个task_struct的链表。

那某个进程在等待某种资源的时候其实就是把自己的task_struct放到对应资源的等待队列中。在操作系统中,每个资源对应的描述数据结构通常会包含一个等待队列。这个等待队列用于存储等待该资源的进程或线程。当一个进程请求某个资源时,如果资源当前不可用,操作系统会将该进程标记为阻塞状态,并将其对应的 PCB(task_struct)移动到相应资源的等待队列后面。这样,CPU就可以调度其他可执行的进程来继续执行。

挂起

那下面我们再来了解一下挂起: 假设现在有一个下载任务因为断网进入了阻塞状态

而此时操作系统的内存资源又特别紧张,那操作系统可能会把这些阻塞状态的进程的代码和数据先交换到磁盘的swap分区上,因为你这些阻塞的进程不被调度,但是你的代码和数据还放在内存里,那就太占用资源了。然后等到这些阻塞的进程等待的资源就绪的时候,再把它们的代码和数据交换回内存,然后被CPU运行。那其中操作系统把某些进程的代码和数据交换到磁盘上,此时就可以认为这些进程处在挂起状态。严格意义来讲,我们这里说的这种挂起状态全称可以叫做阻塞挂起状态,可以将挂起理解为一种特殊的阻塞状态

swap分区可以开辟的很大/小吗?

不可以,一般swap分区不超过内存的大小.如果swap分区开的太大,系统可能会过于频繁的与swap分区进行交互,这种频繁的拷贝会导致机器运行效率变低! 

在网上搜进程的状态:可能大部分都是这种,有的可能会有挂起状态。而我们上面了解的内容其实就是基于操作系统这门课程来说的,可以认为它对于所有具体的操作系统都是成立的,可能比较抽象。 

下面对一款具体的操作系统——Linux来学习进程的状态。 

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 */
};

进程状态就是PCB中的一个字段:int status, PCB->status = ...,然后PCB放入...队列等等

进程状态变化的本质:

1.更改PCB status整型变量

2.将PCB链入不同的队列中

R运行状态(running)

R表示运行状态,那我问大家,如果一个进程是R状态,那么它一定是在CPU上运行吗?

不一定, 操作系统里可能有10个8个状态为R的进程,但是它们之中可能只有几个是正在CPU上运行的。所以,其实操作系统维护调度进程也有相应的队列(运行队列),运行队列通常根据不同的调度策略进行管理,处在运行队列中的进程,它的状态就是R.

所以:R运行状态(running):并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里

S休眠状态(sleeping)

 S休眠状态(sleeping): 意味着进程在等待事件完成(这里的休眠有时候也叫做可中断修眠(interruptible sleep))。(S状态就是一种阻塞状态)

写一段代码观察: 

大家看,现在这个进程在运行吗?当然,我们肉眼可见它在不停的运行。

我们看到查出来的进程的状态是S,后面那个+号先不管,那为什么我们查到的是S状态而不是R状态呢?

代码是一个死循环里面有一个打印语句,所以它运行的时候要频繁访问显示器这个外设, 但是,我们每次访问显示器的时候,他一定是就绪的吗?

如果不是就绪的话, 那我们的进程是不是就不在进程的运行队列里面排队了, 而是在显示器这个外设资源的等待队列里面排队。所以这个进程并不是一直在CPU的运行队列里面的, 而是在运行队列和外设资源的等待队列里面不断切换的我们这里只有一行代码, CPU执行的时候是很快的, 而等待外设的过程相对于CPU执行代码的速度是非常慢的。所以我们ps命令查的时候会发现查到的基本都是s状态.

 我们后面把打印注释掉查看到的就是R状态了:

那也很好解释,因为我们把它注释掉,程序里面就没有访问资源的代码, 只有while循环判断, while循环判断就是纯计算, 所以它不需要访问外设, 那只要被调度, 就一直处在运行队列里, 所以我们查到它的状态总是R状态。

 修改代码:

我们看到此时它的状态就是S。因为它此时正在等待资源,等待我们通过键盘输入数据。
所以它此时就不在CPU的运行队列里, 没有被调度, 而是在键盘资源的等待队列, 也就是我们上面说的阻塞状态。所以S休眠状态其实就是阻塞的一种, 而且S这种休眠状态被称为可中断休眠: CTRL+c就可以终止该进程

D不可中断休眠状态

D 磁盘休眠状态(Disk sleep)也叫不可中断休眠状态(uninterruptible sleep),在这个状态的进程通常要等待IO的结束, 也是一种阻塞状态。

那该如何理解这个D 状态呢?我们来看这样一个场景:

假设现在这个进程想往磁盘上写100MB的数据, 那它就告诉磁盘, 我想往你身上写100MB的数据;然后磁盘说, 我的速度比较慢, 你要等等我;那此时进程就被设置成了阻塞状态, 就去磁盘的等待队列里面排队了.
那CPU就对这个进程说, 那你先写数据吧, 我先运行其它的进程, 等你资源准备就绪了, 我再调度你.
此时,操作系统路过. 作为系统的管理者, 它发现此时系统的内存资源已经非常紧张了, 如果再新增进程就要挂了, 但是此时操作系统却发现你这个进程却不在运行队列里, 而是啥也不干在这里等。那操作系统就想,那我把你杀掉吧,于是,这个进程就被干掉了。与此同时呢,又发生了新的状况,这个进程被干掉了,此时磁盘还在写数据,但是,由于某些原因,数据写失败了。
那然后磁盘就要给这个进程说,你的这些数据写入失败了,但是此时却发现人不见了,找不到这个进程了。那现在进程被杀死了,就只剩磁盘拿着这100mb的数据,不知该怎么处理了。可以直接把这些数据丢掉吗,如果这些数据很重要呢!

 那此时这种情况该如何处理呢?

上面的故事呢, 涉及3个比较关键的角色——操作系统、进程、磁盘
那么请问:导致上面那样的情况出现,是谁的问题呢?
它们做的好像都是合理的, 那怎么办呢?如何避免这种情况的出现呢?
其实只要我们保证这个进程不能被杀死就行了.

所以呢,就有了这样一种休眠状态:

即D状态——不可中断休眠状态。
如果一个进程处在这种状态, 它就无法被杀死, 操作系统也不行。
所以如果出现D状态的话,那你的机器可能就快要宕机了, 因为此时磁盘的压力可能已经非常大了,严重变慢,才导致出现这种情况。
所以这种状态大概率遇不到。

 T暂停状态(stopped)

T暂停状态呢其实也是一种阻塞状态:

可以通过发送 SIGSTOP 信号给进程来暂停(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。

那我们怎么让它暂停呢?

kill命令: -19(SIGSTOP) 这个选项,加上对应进程的PID,他就可以暂停一个进程。

我们看到执行之后这个进程就停止了,而且我们再去查看进程状态就变成T暂停状态了。

那我想让他继续运行呢? 

kill -18 (SIGCONT) 加上对应进程的PID 就可以让指定的进程继续执行

我们看到执行之后它就重新运行起来了,此时再去查看进程状态就又变回S了, 但是不是S+

此时我们去CTRL+c无法终止这个进程了:

前面我们查看的状态S字母后面还有一个“+”加号,但是自从上面变成T状态之后,就没有+了 

 那进程状态后面的+表示什么呢?

如果带+的话, 表明该进程是在前台运行的, CTRL+c可以终止掉它
如果没有+, 就表明这个进程变成了在后台运行后台运行的时候我们可以去正常执行我们的shell指令

但是它在后台还会一直运行,且CTRL+c终止不了,那有办法杀掉它吗?

kill -9 + pid 就可以杀掉这个后台进程(前台也可以)


 t 追踪暂停状态 (tracing stop) 

那其实我们GDB调式程序的是时候,如果打了断点,程序在断点处停下来,此时程序就会停止执行进入t状态


X死亡状态(dead)

X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

它是一个瞬时状态,我们也很难查到,不过与之相关的,还有一个重点状态——Z (zombie):僵尸状态

 Z僵尸状态 (zombie)

首先问一个问题,我们为什么要去创建进程

那其实就是为了让进程帮我们办事, 完成某个任务, 那对于进程执行的结果,我们有时候可能是关心的,有时候可能并不关心。

我们创建进程如果关心结果的话,那如何获取这个结果呢?其中一个方式就是通过退出码 

我们平时写的C/C++ 代码, main函数里面最后一般都要有一个返回值return 0,这个返回值其实叫做进程退出码
另外任何命令行上启动的进程,都是bash的子进程,所以我们运行一个程序的时候,可以认为是父进程bash创建了一个子进程,让这个子进程去帮忙办事。
那事办的怎么样,结果如何?父进程是怎么知道的呢?
它是通过退出码来获悉的。

如何获取一个进程的退出码呢?

指令: echo &? 

那么,如果一个进程退出了立即变成了X死亡状态,那父进程bash就没有机会拿到这个退出结果了,

所以, 为了方便子进程退出后父进程或操作系统获取该进程的退出结果, Linux进程退出时, 进程一般不会立即死亡, 而是要维持一个Z状态即——僵尸状态。等这个进程真正被回收了,它的状态就会变成X死亡状态。

 那处在僵尸状态的进程僵尸进程,那首先我们就要来重点理解一下僵尸进程。

那我们来给大家讲一个故事:

假如你是一名自律的学生,每天都有晨跑的习惯。今天早上你正在跑步的时候,忽然后面来了一个人,也在跑步,但他跑的非常快,很快就超过你跑到前面去了,你依然在后面慢慢的跑着。
突然,他跑到在你前面, 口吐白沫,一阵抽搐,就倒下了。
后面不远处的你看到了这个情况,非常慌张,赶紧打了120并报了警。
然后警察很快就到了,到了之后发现这个人已经不行了。
然后请问大家警察会怎么做?
会不会直接把这个人弄走,通知它的家属,然后销毁现场。
那正常情况呢应该是不会这样的。
警察应该会封锁现场,然后刚好120的人过来了,警察说,你帮我们确认了,这个人是不是已经不行了,120的医务人员说确实不行了。
然后呢警察可能会通知法医来确认这个人的死因,是自己死亡,还是被谋杀了啥的,然后进行一些相关的调查啥的。
假设最后发现这个人是自己发病死亡了,那然后警察就通知家属,让它们把人带走,然后就可以撤离现场了。

那上述的故事中:

警察发现这个人死亡后应不应该立即把这个人弄走,然后销毁现场。
不应该,而是要维护好现场,便于调查它的死因啥的…
那其实这就对应了我们上面提到的一个进程退出后不会立即死亡,而是维持一个僵尸状态,便于父进程或者操作系统获取该进程的退出结果。

所以总结来说: 

所以当一个进程退出的时候,退出信息会由OS写入到当前退出进程的PCB中, 可以允许当前进程的代码和数据空间被释放, 但是不能允许进程的PCB被立即释放, 进程退出了, 但还没有被父进程或OS读取,  OS必须维护这个退出进程的PCB结构, 此时这个进程状态就是僵尸进程, 进程被父进程或者OS读取后, PCB状态先被改成X状态, 才会被释放.

演示一下:

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 
  4 int main()
  5 {
  6     pid_t id = fork();
  7     if(id == 0)
  8     {
  9         //子进程
 10         while(1)
 11         {
 12             printf("我是子进程,我在运行,pid:%d,ppid%d\n",getpid(),getppid());
 13             sleep(1);
 14         }
 15     }
 16     else if(id > 0)
 17     {
 18         //父进程
 19         while(1)
 20         {
 21             printf("我是父进程,我在运行,pid:%d,ppid:%d\n",getpid(),getppid());                                                                                
 22             sleep(1);
 23         }
 24     }
 25     return 0;
 26 }

我们看到父子进程两个的状态最开始都是S,那按我们上面讲的,子进程退出,父进程还在运行,且没有回收子进程获取返回码(我们现在也不会),那么子进程就会进入僵尸状态,干掉子进程 再来查看子进程就变成了僵尸进程

 僵尸进程的危害 

进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?

是的!

维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说, Z状态一直不退出, PCB一直都要维护进程基本信息?

是的!

那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存泄漏

是的!因为数据结构对象本身就要占用内存,如果一直不释放那就内存泄漏了。

等这个进程真正被回收了,它的状态才会变成X死亡状态,此时该进程的所有资源才会被释放。如何避免后面再说.

孤儿进程

 先给出孤儿进程的概念:

孤儿进程指的是在其父进程执行完成退出或被终止后仍继续运行的一类进程。

即如果父进程先退出,子进程继续还在运行,那么该子进程就被称为孤儿进程。

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 
  4 int main()
  5 {
  6     pid_t id = fork();
  7     if(id == 0)
  8     {
  9         //子进程
 10         while(1)
 11         {
 12             printf("我是子进程,我在运行,pid:%d,ppid%d\n    ",getpid(),getppid());
 13             sleep(1);
 14         }
 15     }
 16     else if(id > 0)
 17     {
 18         //父进程
 19         int count = 10;
 20         while(count--)                                 
 21         {
 22             printf("我是父进程,我在运行,pid:%d,ppid:%d\    n",getpid(),getppid());
 23             sleep(1);
 24         }
 25     }
 26     return 0;
 27 }
~

 那下面我们就运行这个程序并观察一下对应的现象:

那这里呢我们要用到一条shell语句
while :; do ps axj | head -1&&ps axj | grep mytest|grep -v grep; sleep 1;echo "--------"; done
它的作用其实就是每个1秒去显示一下对应搜索的进程信息,并打印了一个“----------”的分割线

 那我们发现呢?
运行一段时间之后,父进程就结束退出了,后面就只剩子进程在运行了。

父进程也是一个进程啊,进程退出时,并不会立即变为X死亡状态,而是要维持一个僵尸状态,但是这里这个父进程退出之后为啥我们没有看到Z状态呢?

这里的父进程它的父进程是谁?是bash,命令行启动的所有程序的进程的父进程都是bash。所以,我们这里之所以没有看到父进程处于僵尸状态,就是它的父进程bash把它直接回收了。

我们还发现: 

原来这个fork出来的子进程的父进程的PID是13695,但是它的父进程退出后,它的父进程的PID变成了1,它被一个新的爹领养了,那这里我们得出一个结论:

如果一个进程的父进程退出了,而这个进程自己还在运行,那么它将会被1号进程(init进程)自动领养,那么这个被领养的进程即孤儿进程。

为什么操作系统要领养孤儿进程?

如果孤儿进程不被领养,那它就没有父进程了,那这样的话未来这个孤儿进程退出的时候谁来回收它呢?那退出后没有人回收它,它就会一直处于僵尸状态,等待回收而没有人回收,那么就导致内存泄漏。

那再来总结一下:

在操作系统领域中,孤儿进程指的是在其父进程执行完成或被终止后仍继续运行的一类进程。这些孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。


此外退出进程补充一个方法:

killall +进程名称 

就可以杀掉指定名称的进程。


Linux进程优先级

基本概念

cpu资源分配的先后顺序, 就是指进程的优先权(priority). 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用, 可以改善系统性能. 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。

查看系统进程

在linux系统中,用ps –la 命令则会类似输出以下几个内容: 

UID : 代表执行者的身份

PID : 代表这个进程的代号

PPID :代表父进程的代号

PRI :代表这个进程可被执行的优先级,其值越小越早被执行,Linux中的文件默认优先级都是80

NI :代表这个进程的nice值

其中PRI和NI是与进程的优先级有关的属性 


PRI and NI

PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高

那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值 ,PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice,其中PRI(old)默认为80

这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行,所以,调整进程优先级,在Linux下,进程默认的优先级是80, 调整进程nice值,nice其取值范围是-20至19(对应优先级为60~99),一共40个级别。这么做的目的是要把进程优先级控制在一定的范围内,使得操作系统在调度时,能够均衡的调度每一个进程,使得其他优先级较低的进程也有机会得到cpu资源

如果优先级较低的进程长时间得不到cpu资源的状态,也叫作进程饥饿

linux进程优先级数值范围: 60 ~99 , linux中默认进程优先级状态为80


修改进程优先级

这里需要注意,普通用户可以将自己的进程的优先级降低, 但是如果想要提高某个进程的优先级,那么就要用root权限来执行, 还有一个细节需要注意, 假如此时我们的进程PRI被改为了90, 我们将nice改为-10后, PRI会变为70而不是80, PRI每次都是根据Linux中的进程默认权限80和nice值进行修正运算的, 与PRI当前的值是多少无关。

1. top

2. 进入top后按“r”–>输入进程PID–>输入nice值

输入sudo top:

 

再把nice值修改为10, 发现新的优先级并不是70 + 10  , 而是80 + 10

为什么优先级不能随意修改?

为什么修改优先级要有一个范围,不能无下限无上限的修改?

因为操作系统在调度进程时,需要较为均衡的让每一个进程都要得到调度, 如果用户无下限的修改优先级, 会导致优先级较低的进程长时间得不到CPU的资源, 会造成进程饥饿

所以我们要有一个概念:

当一个进程被放在CPU上处理时, 并不是一直在CPU上, 过段时间后操作系统会它取下来放入其他进程! 所以在一秒内, n个进程可能就已经被调度成百上千次了!

具体在下章讲解 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值