🏠关于专栏:Linux的浅学到熟知专栏用于记录Linux系统编程、网络编程等内容。
🎯每天努力一点点,技术变化看得见
进程状态
操作系统进程状态概览(理论版),实际的一款操作系统进程状态与理论状态会有一定区别。↓↓↓
新建状态:字面意思,当进程刚创建时,就是新建状态。
运行状态:等待CPU资源时,当还没有轮到某个进程时,该进程会被链接在CPU的等待队列中(该队列被称为run_queue,操作系统会按一定策略调度该队列中的进程到CPU上执行),位于该队列中的进程都处于运行状态,而不是正在CPU上运行的程序才叫做运行状态。
阻塞状态:等待非CPU资源就绪,该进程被从运行队列上取下,被挂接到等待的资源的等待队列中,该进程就称为阻塞状态。
挂起状态:当内存不足的时候,操作系统通过适当的置换进程的代码和数据到磁盘,进程的状态就称为挂起状态。
关于进程状态,我们来看看Linux源代码中总共有几种状态↓↓↓
static const char* cinst task_state_array[] = {
"R(runnning)",/* 0 */
"S(sleeping)",/* 1 */
"D(disk sleep)", /* 2 */
"T(stopped)",/* 3 */
"t(tracing stop)",/* 4 */
"X(dead)", /* 16 */
"Z(zombie)" /* 32 */
};
在介绍这些状态时,将使用使用描述+代码验证的方式(但部分状态无法使用代码验证)。在开始介绍前,我们需要了解如何查看进程状态↓↓↓
进程状态查看
ps aux / ps axj
查看进程状态总共有两种方式,分别是ps aux
及ps axj
。使用它们查看进程的效果如下图所示↓↓↓
★ps:关于ps命令的更多用法,可以查询man手册。
R运行状态(running)
我们在创建了进程之后,操作系统会给该进程创建一个task_struct结构体,该结构体中包含进程状态、pid、ppid、优先级等字段,该结构体就是PCB(进程控制块),用于记录进程各类信息。
管理好这些进程,我们需要只要管理好task_struct结构体即可。因此,操作系统将task_struct链成一个链表(队列)。
由于计算机中的各类资源(包括CPU、内存、外部设备等)均十分宝贵,各个进程在获取某个资源时,可能需要到某个资源上排队。而等待CPU资源的进程将被链成一个队列,这个队列叫做运行队列(run_queue)。而处于运行队列上的进程的状态就是运行状态,即R状态。
下面,我们编写一个死循环,并查看该进程的状态↓↓↓
#include <stdio.h>
int main()
{
while(1)
{}
return 0;
}
上面的STAT的R就是运行状态。
S睡眠状态(sleeping)
如果我们编写一个循环打印"hello world"的程序,则执行该程序的进程的状态是R状态吗?
#include <stdio.h>
int main()
{
while(1)
{
printf("hello world\n");
}
retrun 0;
}
这里我执行ps axj | head -1 && ps axj | grep test
命令,得到的进程状态是S状态,即睡眠状态。这是为什么呢?那什么是睡眠状态呢?
我们在执行上面的程序时,进程需要访问外设,而外设相比与cpu而言,速度非常慢。该进程为了打印"hello world",它需要到对应的外设上等待(这里的外设是显示器),在它需要使用外设资源时,cpu将该进程的PCB从运行队列中取下来,并链入显示器的等待队列中。在除了cpu以外的队列中等待时,这时的状态就是S休眠状态(也就是理论状态中的阻塞状态)。等该进程打印完毕后,再链入cpu,继续向下执行。但由于上面的程序频繁访问外设,导致它在运行队列中的时间非常短。因而,我们在查看该进程状态时,绝大多数情况下,它都处于睡眠状态。
D磁盘休眠状态(Disk sleep)
在操作系统中,如果一个进程处于睡眠状态,即S状态。如果此时操作系统负载过大,则可能杀死该进程。这种睡眠状态也被称为浅度睡眠,因为它可以被操作系统终止。这也就是为什么某些软件服务在用户量过大时,出现某些用户无法获取服务的原因,因为应用服务器的操作系统覆盖过大时,这些用户的进程被操作系统终止了。
从操作系统角度来说,操作系统为了维护服务器能稳定运行,不得不杀死某些进程;从进程角度来说,进程正常运行而被操作系统强制关闭,进程也无能为力。
如果我们希望某些关键性进程,即使在操作系统负载过大时也不会被杀死,则可以将该进程设置为D状态,即磁盘休眠状态(也称为磁盘睡眠状态、深度睡眠状态)。处于该状态的进程不能被操作系统中断,也不能被操作系统唤醒。该进程只有自己醒来,即该进程不再处于D状态,才能被操作系统调度执行、回收等。
★ps:该状态无法使用程序演示
T停止状态(stopped)
在操作系统中,可以给某些进程发送信号,发送信号的格式为:
kill -[信号编号] [进程pid]
下面,我们使用kill -l
查看所有信号及其对应的编号↓↓↓
上图的18号信号SIGCONT为进程继续执行信号,19号信号为SIGSTOP为进程暂停信号。如果我们给某个进程发送19号信号,则它将处于T状态,即暂停状态。
下面,我们给上面循环打印"hello world"的进程发送19号信号,再对比发送信号前后的状态变化
kill -19 19093
在接收到19号SIGSTOP信号后,19093号进程停止打印"hello world",并且此时它的状态为T状态,即暂停状态。
如果我们给19093号进程发送18号SIGCONT信号,会是什么效果呢?
发送信号后,19093号进程继续打印"hello world",并且它的状态又变回S状态,即睡眠状态。但这里不同的是,原先的状态是S+,而此时的状态是S。这两者有什么区别呢?
S+状态下的进程,在使用ctrl+C时,可以被终止;而S状态下的进程使用ctrl+C却无法被终止。这里带有+号的称为前台进程,不带+号的称为后台进程。前台进程占用用户当前的bash命令行;后台进程不占用用户的bash命令行,但后台进程需要使用kill -9 [进程号]
发送9号信号来终止。
★ps:T状态与S状态的区别:T状态单纯暂停,并不等待某种资源;而S状态是为了等待某种资源。
t调试/追踪状态(tracing stop)
我们在编写完程序后,可以在使用gcc编译,如果在编译命令的末尾加上-g
选项,则会生成一个debug版本的程序。如果不带-g
选项,gcc默认生成的是release版本。
我们使用gcc生成下面程序的release和debug版本↓↓↓
#include <stdio.h>
int Add(int left, int right)
{
return left + right;
}
int main()
{
int num1 = 10;
int num2 = 20;
printf("%d + %d = %d\n", num1. num2, Add(num1, num2));
return 0;
}
从上图可以发现,debug版本所占的内存空间会大于release版本(因为debug版本中包含调试信息)。
下面我们使用gdb对test_g进行调试↓↓↓
由于在12行处打了断点,此时程序停止在12行处。我们使用ps axj | head -1 && ps axj | grep test
查看当前进程状态↓↓↓
此时的进程状态为t状态,即调试状态(也称为追踪状态)。
X死亡状态/终止状态(dead)
如果进程执行结束了,操作系统会马上回收该进程的资源吗(进程此时占用内存等资源)?不一定。如果此时cpu上此时正在处理更加重要、紧急的进程,则操作系统此时不会马上回收已经执行结束的进程,而是将该进程标识为X状态,即死亡状态(也成为终止状态)。
标记为X状态的进程,表示该进程可以被操作系统回收。但具体什么时候回收,取决于操作系统。由于X状态瞬时性较强,难以使用程序演示,这里就不使用程序演示了。
Z僵尸状态/僵尸进程(zombie)
僵尸状态(zombie,也称为僵死状态)是一种比较特殊的状态。该状态发生在子进程退出,而父进程没有回收子进程资源时(即父进程没有使用waitpid读取子进程的退出信息),此时子进程就会进入僵尸状态。
僵尸进程会以终止状态保持在进程表中,并且会一直等待父进程读取退出状态码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,则子进程会进入Z状态。
下面创建的程序,使得子进程保持5秒的僵尸状态↓↓↓
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
printf("I am child process! pid = %d, ppid = %d\n", getpid(), getppid());
sleep(5);
exit(0);
}
printf("I am parent process! pid = %d\n", getpid());
sleep(10);
return 0;
}
执行上述程序并使用while :; do ps axj | head -1 && ps axj | grep test; sleep 1; echo "#############" done;
脚本,每1秒钟对执行内容做监视。
我们可以发现,在子进程退出后,父进程没有退出,此时的子进程的状态变为Z状态,即僵尸状态。
僵尸进程的危害:
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态。
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护。
那一个父进程创建了很多子进程,如果不回收,就会造成内存资源的浪费(内存泄漏)。因为数据结构对象本身就要占用内存,想想C语言中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
★ps:关于僵尸进程如何被处理的问题,将在后序文章中的介绍。
孤儿进程
上面已经将进程的各种状态讲述完毕,接下来,我们再了解另一种进程——孤儿进程。
父进程如果提前退出,子进程后退出,则此时父进程无法再回收子进程资源了,那该如何处理呢?
如果让子进程一直保持Z状态,则会造成内存泄漏;但此时子进程的父进程已经执行结束,没有进程可以来清理子进程的资源了,这种子进程被称为孤儿进程。操作系统为了解决这个问题,对于父进程已经执行结束,而子进程后退出的,该子进程将被1号init进程领养,其资源将由init进程进行回收。
下面代码中,父进程比子进程执行结束前5秒就退出↓↓↓
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
int cnt = 10;
while(cnt > 0)
{
printf("I am child process, pid = %d, ppid = %d\n", getpid(), getppid());
sleep(1);
cnt--;
}
exit(0);
}
printf("I am parent process, pid = %d\n", getpid());
sleep(5);
return 0;
}
由程序执行结果可以看出,子进程前5秒的父进程pid为15480,由于父进程在子进程推出前5秒就推出了,此时子进程被1号init进程领养,故此时该进程的父进程pid为1。
进程优先级
基本概念
优先权高的进程有优先执行权利,可以优先获得cpu资源。
配置进程优先权对多任务环境的linux很有用,可以改善系统性能。对于多核计算机,可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
★ps:优先级是重要的调度指标,CPU中的调度器依据优先级选择哪些进程优先到CPU运行
查看系统进程
我们可以执行ps -le
来查看系统中所有进程的详细信息(-e选项表示所有进程,-l选项表示显示进程详细信息)
上图中:
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
PRI及NI
PRI是进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高。NI就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值。PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice。
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行。所以,调整进程优先级,在Linux下,就是调整进程nice值。nice其取值范围是-20至19,一共40个级别。
那如果PRI(new)=PRI(old)+nice,那么我的程序起始优先级PRI为80,我对它重复设置10000次nice值为-20,它的优先级PRI是不是变成80-20*10000呢?答案是否定的。在Linux操作系统中,PRI起始都为80,nice值得范围为-19到20。当我们设置了新的nice值时,该进程的PRI=80+nice值。也就是说PRI的范围在[80-20,80+19]之间。
★ps:进程的nice值不是进程的优先级,进程优先级与nice值不是一个概念,但是进程nice值会影响到进程的优先级变化。可以理解nice值是进程优先级的修正修正数据。
修改进程优先级
top命令
首先,我们运行一个名为test的死循环程序。此时它的PRI为80,NI为0。
top命令修改进程优先级的方法(如果要设置小于0的nice值,即提高进程优先级,此时需要使用sudo提权):
①执行top命令
②输入r,并输入待修改进程优先级的进程pid。
③输入nice值,这里输入10。
我们使用ps -el | head -1 && ps -el | grep test
查看进程优先级发现,test的PRI变为90,NI变为10。
★ps:上图中,我们将nice值设置为10,则该进程的PRI=80+10=90。如果我们在此基础上设置nice值为15,则该进程的优先级为PRI=80+15=95。
renice命令
renice命令使用格式为:
renice [nice值] -p [进程pid]
下图演示将20269号进程的nice值修改为15。
我们使用ps -el | head -1 && ps -el | grep test
查看进程优先级发现,test的PRI变为95,NI变为15。
★ps:如果要给进程设置比原nice值更小的nice值,需要使用sudo提权。
关于进程的相关概念
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行。
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
关于并行和并发这里做一下更详细的解释:
并行就是多个进程同时执行,例如某计算机有3个CPU,3个进程各占用一个CPU,同时执行,这就是并行。而并发并不是,例如某计算机只有1个CPU,我们有3个进程在一个CPU上并发执行,第1个进程先执行1ms,接下来由第2个进程执行1ms,再由第3个进程执行1ms,然后又由第1个进程执行,以此类推…像这种明明各个进程是交替执行的,但由于各个进程都在向前运行,而进程交替运行的操作用户感知不到,用户以为这3个进程是各占用1个CPU并同时执行的,这种被称为并发。
★ps:关于进程的切换问题↓↓↓
计算机中的CPU内有大量的寄存器,它保存着正在执行的进程的临时数据。如果进程A正在被执行,CPU内的寄存器里面一定保存的是进程A的临时数据。寄存器中的临时数据,就叫做A的上下文。
上下文数据可以丢弃吗?绝对不可以!由于计算机CPU数量小于进程数量,则当进程A暂时被切换下来的时候,进程A需要顺便带走自己的上下文数据。带走暂时保存的目的就是为了下次回来的时候,能恢复上去,就能继续按照之前的逻辑继续向后运行,就如同没有中断过一样。但如果没有保存上下文数据,进程A回来再执行时,就无法判断进程A原先执行到哪里了。
注意:CPU内的寄存器只有一份,但是上下文可以有多份,分别对应不同的进程!!
文章来源:https://www.toymoban.com/news/detail-854994.html
🎈欢迎进入从浅学到熟知Linux专栏,查看更多文章。
如果上述内容有任何问题,欢迎在下方留言区指正b( ̄▽ ̄)d文章来源地址https://www.toymoban.com/news/detail-854994.html
到了这里,关于【从浅学到熟知Linux】进程状态与进程优先级(含进程R/S/T/t/D/X/Z状态介绍、僵尸进程、孤儿进程、使用top及renice调整进程优先级)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!