【Linux】12:进程状态
摘要本文系统性地阐述了进程状态的概念及其在操作系统中的核心地位。首先明确了进程状态本质上是进程控制块PCB中的一个状态变量是操作系统调度和管理进程的关键依据。接着深入剖析了操作系统理论中的三种基本进程状态运行状态进程在CPU运行队列中、阻塞状态进程等待资源就绪和挂起状态进程代码和数据被换出到磁盘。然后重点讲解了Linux系统下的七种具体进程状态运行R、可中断睡眠S、不可中断睡眠D、停止T、跟踪停止t、死亡X和僵尸Z并通过代码示例和命令行操作演示了各状态的触发条件和观察方法。文章最后总结了进程状态转换的本质是PCB在不同队列运行队列、等待队列间的移动并强调了僵尸进程和孤儿进程对系统资源的影响。目录一、什么是进程状态二、操作系统的进程状态2.1 运行状态2.2 阻塞状态​编辑2.3 挂起状态2.4 总结2.5 理解内核链表的话题三、linux下的7种进程状态3.1 运行状态R3.2 浅度睡眠状态S3.3 深度睡眠状态D3.4 停止状态T3.5 进程追踪状态t3.6 死亡状态X3.7 僵尸状态Z3.7.1 僵尸进程3.7.2 孤儿进程四、总结一、什么是进程状态在谈 进程状态 之前我们需要了解 什么是进程、怎么去描述并组织进程、如何创建一个进程。操作系统OS的本质是先描述在组织操作系统并非直接管理 -- 进程而是管理 进程 的 PCBtask_structPCB 中有着进程的各种信息包括PID、PPID 、进程状态 等我们可以通过函数 getpid() 获取当前进程的 PID进程 间存在父子关系可以通过 fork() 主动创建 子进程父子进程 相互独立共享一份代码时具有 写时拷贝 机制所谓的 进程状态 其实就是 PCBtask_struct内部的一个整形变量 ----- int status操作系统OS要更改一个进程状态只要更改进程的 PCBtask_struct中的状态属性它只是一个标志位用来表明进程的状态仅此而已。那么 状态 决定了什么呢进程后续的动作 ---- 而 操作系统OS中可能会存在多个进程都要根据它的状态执行后续动作所以说明 --状态-- 对于 操作系统后续如何管理 进程十分重要二、操作系统的进程状态对于进程而言呢它是操作系统中的概念如果有学习过《操作系统》这门学科的话应该可以很清楚对于进程而言的话是存在着许许多多的状态如果一开始刚刚接触的小伙伴一定会感觉这么多状态要怎么区分呀其实那么多的状态真正主要的也就那么几个所以接下去我会中重点讲解以下几种进程的状态2.1 运行状态首先我们要谈到的是【运行状态】这个状态是最普遍的: 进程 PCB 被调度到 CPU 运行队列中且已被分配 CPU 资源就叫做 ------ 运行状态一个CPU一个运行队列也就是说这些进程都需要在CPU这里进行排队逐个执行。而排队的是进程么其实不是排队的是进程的PCB面试时不是你在排队而是你的简历信息在排队。首先对于一个进程而言我们知道它是由 内核数据结构 所对应的代码和数据 所组成的所以当系统中存在多个进程的时候就势必会存在多个结构体当然我们需要将这些进程给链接组织起来双向链表链接那么这些 进程 就相当于是在处在一个运行队列中我们如果要找到这个队列中的某个进程的话只需要找到这个进程的头部即可那我们就可以对应地去调度某个进程把这个进程所对应的代码和数据放到CPU上去执行因为每个进程是需要去竞争CPU资源的但是呢CPU不可能同时给这么多进程分配资源所以每一个CPU都会去维护一个运行队列里面的队 头指针 head 所指向就是第一个进程所对应的【PCB】队尾指针tail 所指向就是最后一个所对应的【PCB】。所以我们要运行某一个进程只需要将 head 所指向的那个进程放到 CPU上去运行即可提问一个 进程 只要把自己放到 CPU 上开始运行了是不是一直要到执行完毕才把自己放下来不是每一个进程都有一个叫做时间片的概念 其时间大概是在10 ms左右。所以并不是一个进程一直在执行而是这多个进程在一个时间段内所有代码都会被执行 —— 这就叫做【并发执行】所以呢这就一定会存在大量的进程 被CPU放上去、拿下来的动作 —— 这就叫做【进程切换】所以呢我们不要拿自己的时间感受去衡量CPU其运行一遍速度是非常快的你根本感受不到这种进程切换的效果2.2 阻塞状态阻塞 就是 进程 因等待某种条件就绪而导致的一种不推进状态比如等待 键盘输入。通俗的来说 阻塞 就是 进程卡住了原因就是缺少资源比如在我们日常生活中常常发生堵车原因就是道路资源不够用了车辆这个进程就需要原地等待那么进程需要什么资源呢比如磁盘、网卡、显卡等各种外设假设你现在想在steam上下载游戏当你点击下载按钮后提示磁盘空间不足此时是无法运行steam下载这个进程的因为此进程需要等待足够大的磁盘资源此时我们就称此进程为阻塞状态总结 进程 阻塞 就是不被调度此时 PCB(task_struct) 就会被设置为 阻塞状态并链入等待的资源提供的等待队列。没错这里的等待队列 类似于 CPU 运行队列2.3 挂起状态挂起阻塞挂起当 内存资源紧张时将进程的数据和代码交换至磁盘中挂起此时内存中只有PCB挂起 可以看作一种特殊的 -- 阻塞状态详解当计算机资源比较吃紧时操作系统一定要确保自身不会因为资源的紧张而崩溃所以就会将一些等待资源阻塞的进程的代码和数据交换到磁盘的 swap分区 中这个过程称为唤出。当需要调度此进程时就会将磁盘的 swap分区 中保存的内容换回到内存中这个过程称为唤入。注意交换的是进程的代码和数据不是PCB如果PCB被交换出内存了那操作系统如何管理呢所以当某个进程的代码和数据不在内存中而被换出到磁盘上时该进程就为挂起状态。思考swap分区是越大越好么磁盘本质是输入输出设备每次唤入唤出其实都是非常低效的操作如果swap分区设置的过大那么操作系统就会十分依赖它导致出现更多低效IO这本身就是一种牺牲效率来确保操作系统能够正常运行的行为。所以swap分区不宜设置的过大一般为内存大小或内存大小的一半具体要看不同的操作系统。2.4 总结看了上面的三种基本的进程状态后我们可以来总结一下如果要看进程是什么状态的话一般看这个进程的PCB在哪里排队位于【运行队列】的话它就是处于运行状态位于【阻塞队列】的话它就是处于阻塞状态一个进程从一个状态切换到另一个状态本质上就是进程的PCB从一个队列换到另外一个队列的过程2.5 理解内核链表的话题之前我们说明进程之间的PCB是使用双向链表链接起来的但是我们上面又说运行队列和阻塞队列里面存放的是进程的PCB一个进程的PCB能属于多种数据结构嘛一个进程有多个PCB嘛我们之前定义结点的方式如下这是我们之前学习到的双链表但是linux内核里面的双链表并没有这样设计。linux设计如下首先linux内核里面设计了list_head类型的结点如下所示struct list_head{struct list_head* next,*prev;}list_head类型的结点里面包含了两个指针next执行了下一个list_head类型的数据而prev指向了前一个list_head类的数据。我们在定义PCB的时候可以在PCBl里面存放一个list_head类型的对象让这个对象指向下一个和前一个PCB里面的list_head类型的对象如下所示这样定义之后问题就来了现在我们只有指向list_head对象的地址list_head对象是属于task_struct内部的那我们如何找到对应的task_struct地址呢我们有了task_struct类型的地址才能访问PCB才能访问PCB里面的其他属性啊。首先需要有一个c语言的基础知识一个结构体对象的地址在数字上结构体对象中第一个属性的地址的。有了这个基础之后我们认为在0号地址存放了一个PCB类型的变量现在我们要访问0号地址对应的PCB对象中的links结构体对象((struct task_struct*)0-links)。这就是相当于links对象距离task_struct对象的偏移量了有了这个偏移量之后现在我知道了下一个PCB的links地址我们通过下一个下一个PCB的links地址减去这个偏移量就可以知道下一个PCB的地址了。(struct task_struct*)(next/list - ((struct task_struct*)0-links))我们得到PCB的地址之后就可以访问PCB对象中的所有属性了。现在我们就回答了上面的问题这样定义之后问题就来了现在我们只有指向list_head对象的地址list_head对象是属于task_struct内部的那我们如何找到对应的task_struct地址呢我们有了task_struct类型的地址才能访问PCB才能访问PCB里面的其他属性啊。在一个PCB类型里面定义了一个list_head类型的对象之后我们当然就可以将所有的PCB通过双链表的方式链接起来。那么我们可以在PCB里面定义一个list_head了是不是可以多定义几个list_head类型的对象呢第一个list_head表示将所有的双向链表链接起来第二个list_head表示运行队列第三个list_head表示阻塞队列。。。。这样一个PCB是不是即属于链表又属于队列了呢。让PCB即属于全局链表又属于运行队列。如果我们想要将一个运行进程切换到阻塞队列就可以将PCB中的list_head断开给他链接到阻塞队列的list_head中。如下所示所以进程的PCB只有一份并且属于多种数据结构三、linux下的7种进程状态在介绍完操作系统学科下的三种最主要进程状态后我们对进程的状态有了基本的概念接下去就让我们正式地来学习一下Linux 系统下7种进程状态先来小结并回顾一下上面所学如果当前是【运行状态】那么接下来就需要被调度运行如果当前是【阻塞状态】那就等条件就绪等设备准备好就把当前进程投递到运行队列里然后再被CPU调度运行如果当前是【挂起状态】要做的就是把当时换出的代码和数据重新换入然后再把所对应的进程列入到运行队列中以下就是关于进程的所有状态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这个状态只是一个返回状态你不会在任务列表里看到这个状态。3.1 运行状态R测试代码如下1 #include stdio.h 2 #include unistd.h 3 int main() 4 { 5 while(1) 6 { 7 printf(hello world\n); 8 sleep(1); 9 } 10 return 0; 11 }编译运行如下我们再开一台xshell来显示进程信息并且每隔一秒显示进程信息。命令如下while :; do ps axj | head -1 ;ps axj |grep mycode;sleep 1 ;done如下所示我们为什么查看mycode进程的状态是S呢我们的mycode进程正在运行这个进程的状态应该是R嘛这是为什么呢我们将代码修改一下让这个代码不要想屏幕打印如下所示1 #include stdio.h 2 #include unistd.h 3 int main() 4 { 5 while(1) 6 { 7 //printf(hello world\n); 8 //sleep(1); 9 } 10 return 0; 11 } ~我们让他什么都不用做只循环编译运行如下所示此时进程的状态就变成了R状态了。为什么我们将printf去掉之后再次编译运行就从S状态变成R状态了呢原因就在于printf打印语句它是属于IO流的一种我们往显示器打印字符它一直在等设备就绪所以其进程状态就为S对应的即是在操作系统中的【阻塞状态】这个进程就一直在运行状态和阻塞状态来回切换那为什么我们看不到R呢这是因为我们每隔1s查看一次进程代码运行的是非常快的大部分时间都在等待IO,所以监测的状态为S状态但是当我们去掉printf这种lO流之后呢它就是在纯纯运行没有lO那也就变成了R状态这里再补充说明一下这个S和R后面的这里的R代表的就是这个进程是在前台运行的所以我们在输入任何指令后不会对其造成 任何的影响。如下所示我们可以在执行程序的时候将代码放到后台去运行如下所示放到后台运行之后此时我们就可以执行命令了然后再次查看进程状态如下所示此时就从R变成R了。总结代表是前台运行无代表后台运行后台运行时可在命令行继续输入指令并执行但无法用ctrlc结束需要用kill -9 pid杀死。想要后台运行某个程序就在后面加如./test 3.2 浅度睡眠状态S测试代码如下1 #include stdio.h 2 #include unistd.h 3 #include sys/types.h 4 5 int main() 6 { 7 printf(我是一个进程我的pid%d\n,getpid()); 8 int x; 9 scanf(%d,x); 10 printf(x%d\n,x); 11 return 0; 12 }运行如下所示我们在另外一个xshell查看状态如下所示将该进程运行起来我们可以看到其是出于S的状态因为【shell】此时正在等待用户的输入这个就对应到了我们上面所讲到的阻塞状态。这个进程在等待键盘设备的输入这个进程的PCB就会链入键盘的阻塞队列中当我们从键盘中输入了一个整数就会把这个进程给唤起放入运行队列继续运行。3.3 深度睡眠状态D除了【浅度睡眠】之外呢还有一种叫做【深度睡眠】它们俩呢都是阻塞状态对于浅度睡眠来说之所以称为“浅度”是有原因的也就是处于这种状态的进程容易被唤醒。例如说我们在上面所讲到的这个处于阻塞状态的进程我们使用kill -9 PID向这个进程发送【9号信号】那么这个进程就被杀死了虽然你在浅度睡眠状态但是是可以被杀死的。深度睡眠状态/不可中断睡眠状态/磁盘休眠状态顾名思义在这个状态的进程不会被杀掉哪怕是操作系统也不行通常会等待IO的结束。例如某一进程要求对磁盘进行写入操作那么在磁盘进行写入期间该进程就处于深度睡眠状态是不会被杀掉的因为该进程需要等待磁盘的回复是否写入成功以做出相应的应答。如果在这个过程中操作系统能够杀死该进程那么就有可能丢失数据。总结一句就是浅度睡眠状态S可以被杀死深度睡眠状态D不可以被杀死3.4 停止状态TT和下面的t都属于暂停状态。测试代码如下1 #include stdio.h 2 #include unistd.h 3 #include sys/types.h 4 5 int main() 6 { 7 while(1) 8 { 9 printf(hello world\n); 10 } 11 return 0; 12 }我们编译运行代码然后按下ctrlz暂停进程此时就会从S状态换成T状态了如下所示我们按下ctrlz之后进程就从S状态换成T状态了暂停状态又是什么呢暂停状态和S状态不一样。暂停状态时linux系统的特有的一种状态暂停是操作系统怀疑这个进程有问题给这个进程暂停然后告知用户让用户来决定这个进程是否要继续。我们除了使用ctrlz来暂停进程还可以使用信号来暂停和启动进程如下所示我们也可以使用信号来启动进程如下所示总结kill -19 pid 暂停进程kill -18 pid启动刚刚暂停的进程kill -9 pid杀死进程3.5 进程追踪状态t我们在编译的时候加入-g选项然后加入断点信息然后按r运行此时进程就停在断点信息那一行了此时我们查看进程状态就是t状态如下所示状态信息如下所示此时在断点这一行进程被暂停了。3.6 死亡状态X对于【死亡状态X】来说呢这个状态只是一个返回状态你不会在任务列表里看到这个状态3.7 僵尸状态Z接下去我们来介绍一种状态叫做【僵死状态Z】对于这个状态我们要涉及到两个特殊的进程叫做僵尸进程与孤儿进程3.7.1 僵尸进程首先我们要来介绍的是僵尸进程这里呢通过一个故事来进行引入你呢很喜欢晨跑‍这一天早晨6点又起来跑步了当你路过一个公园的时候遇到了一个晨练的【程序员】边跑边掉头发 但是呢跑着跑着此时突然他就“啪叽”倒了下来。那你此时就非常担惊受怕了马上拨打了110和120的电话然后守在他的身边等待救援来到周边的人看到也都纷纷围了过来其中不免有一些人会紧急救援。不过等了一会这个人就没了呼吸过了十几分钟后救护车和警车都来了警察先封锁了让法医过来检验一下其状况就说“这个人已经没救了赶紧通知家属准备后事吧~。”好我们回归正题来说说这个【僵尸进程】因为在救护车来之前这个人其实就已经死亡了但是其状态还没有被检测当前并不知道它的死因所以我们操作系统就可能会维护当前的这个状态这个状态即为Z状态即[僵尸状态]那我现在想问了有一个进程暂时退出了它要将它的状态暂时维持一段时间问题是它维持给谁看呢答父进程当一个进程退出的时候那最关心它的便是【父进程】。因为这个父进程费了很大的劲才将这个子进程fork出来此时呢它突然挂掉了那么此时父进程就必须去关心一下对应的子进程退出时的原因在子进程结束之后父进程得到子进程的状态之前子进程的状态就为Z状态。测试代码如下1 #include stdio.h 2 #include unistd.h 3 #include sys/types.h 4 5 int main() 6 { 7 pid_t idfork(); 8 if(id0) 9 { 10 int count5; 11 while(count) 12 { 13 printf(我是子进程我正常运行%d\n,count); 14 sleep(1); 15 count--; 16 } 17 }else 18 { 19 while(1) 20 { 21 printf(我是父进程我正在运行...\n); 22 sleep(1); 23 } 24 } 25 return 0; 26 }子进程会执行5次printf语句然后退出父进程会一直运行运行如下所示子进程结束之后父进程得到子进程结束信息之前子进程就是Z状态。一旦父进程得到了子进程的结束信息处理子进程的退出信息之后子进程就会变为X状态就看不到这个子进程的信息了。如果父进程一直不管子进程的结束信息不回收不获取子进程的退出信息那个子进程就一直是Z状态。子进程的PCB一直存在在内存中OS需要一直维护PCB会一直占用内存导致了内存泄漏。所以僵尸进程就会导致内存泄露。所以父进程需要处理子进程的退出信息将子进程的PCB数据释放。我们总结一下 进程一般退出的时候一般其不会立即彻底退出。如果父进程没有主动回收子进程信息子进程会一直让自己处于Z状态这也是为了方便后续父进程读取子进程的相关退出结果。那对于上面的这种子进程我们就将其称作为是【僵尸进程】不过呢这种进程是存在一定危害的那一个父进程创建了很多子进程就是不回收是不是就会造成内存资源的浪费是的因为数据结构对象本身就要占用内存那么对于上面的这种危害我们就称作为是【内存泄漏】要如何去进行避免呢之后的文章会做讲解~补充一个知识点如果进程退出了内存泄漏的问题就不存在了。什么样的进程具有内存泄漏问题是比较麻烦的一个进程是永远在运行并且从来不退出的我们叫这种进程叫常驻进程这样的进程最怕内存泄漏3.7.2 孤儿进程上面我们讲到当一个子进程突然退出但是父进程并没有去主动回收的话那么此时这个子进程就会变成【僵尸进程】父子进程关系中如果父进程先退出子进程要被1号进程领养这个被领养的进程子进程就叫做孤儿进程测试代码如下1 #include stdio.h 2 #include unistd.h 3 #include sys/types.h 4 5 int main() 6 { 7 pid_t idfork(); 8 if(id0) 9 { 10 perror(fork fail); 11 return 1; 12 }else if(id0) 13 { 14 //子进程 15 while(1) 16 { 17 printf(我是一个子进程pid%d,ppid:%d\n,getpid(),getppid()); 18 sleep(1); 19 } 20 21 }else 22 { 23 //父进程 24 int cnt5; 25 while(cnt) 26 { 27 printf(我是一个父进程pid%d,ppid:%d\n,getpid(),getppid()); 28 sleep(1); 29 cnt--; 30 } 31 } 32 return 0; 33 }代码解释我们在父进程中创建一个子进程子进程一直循环打印父进程执行5次打印就退出父进程退出之后子进程还在一直执行。监视进程命令while :; do ps axj | head -1 ;ps axj |grep mycode;sleep 1 ;done运行如下所示我们使用命令来查看1号进程是什么ps axj | head -1 ps axj | grep systemd如下所示我们可以直接理解1号进程就是操作系统。为什么子进程要被1号进程领养呢如果1号进程不领养子进程子进程结束之后子进程将退出信息给父进程然后父进程来处理子进程的结束状态然后释放子进程的PCB.但是我的父进程已经退出了啊已经接受不到子进程结束信息了那么子进程就会一直处在Z状态导致内存泄漏。所以需要被1号进程领养四、总结在本文中我们主要讲解了两个大点一个是在操作系统中所常见的一些进程状态它们分别为运行状态、阻塞状态、挂起状态。所被调度的、处于运行队列里的这些进程所处的状态我们称之为 运行状态处于等待队列中同时在等待外设相应的进程所处的状态我们称之为 阻塞状态当把一个进程的 数据和代码都重新交换到外设当中进程所处的状态我们称之为 挂起状态接下去我们又介绍了在Linux系统下的七种进程状态分别是运行状态R、浅度睡眠状态S、深度睡眠状态D、停止状态T、进程跟踪状态t、死亡状态X、僵死状态Z当一个进程正在运行没有在等待IO流的时候其就会处于 运行状态R当一个进程请求服务IO时会使一个进程处于浅度睡眠状态S可以被杀死如果不想让一个进程在等待磁盘操作的时候被操作系统杀掉则可让其处于 深度睡眠状态D可以向一个进程发送【19】号信号使其暂停处于停止状态T继续发送【18】号信号的话则可以使其重新启动在【cgdb】的环境下去运行一个断点的话则可以使其处于进程跟踪状态t使用kill -9 PID就可以杀掉一个进程使其处于死亡状态X如果让一个子进程在父进程不知晓的时候退出那么其就会处于僵死状态Z变为【僵尸进程】若是在父子进程中父进程先于子进程退出的话那么这个子进程就会变成【孤儿进程】

相关新闻

最新新闻

日新闻

周新闻

月新闻