一.前言进程只有被操作系统管理好了才能发挥它的全部作用 而系统中存在多个进程操作系统无法做到面面俱到因此为了更好的管理进程操作系统把进程分成了几种状态运行 阻塞 挂起 休眠等等。至于每种状态的 应用场景是什么有什么用本文将会带大家认识各种进程状态在谈进程状态之前我们需要了解什么是进程、怎么去描述并组织进程、如何创建一个进程。操作系统OS的本质是先描述在组织操作系统并非直接管理 --进程而是管理进程 的 PCBtask_structPCB中有着进程的各种信息包括PID、PPID 、进程状态等我们可以通过函数getpid()获取当前进程的PID进程间存在父子关系可以通过fork()主动创建子进程父子进程相互独立共享一份代码时具有写诗拷贝机制二、什么是进程状态所谓的进程状态其实就是PCBtask_struct内部的一个整形变量-----int status操作系统OS要更改一个进程状态只要更改进程的PCBtask_struct中的状态属性它只是一个标志位用来表明进程的状态仅此而已。那么状态决定了什么呢进程后续的动作---- 而操作系统OS中可能会存在多个进程都要根据它的状态执行后续动作所以说明 --状态-- 对于操作系统后续如何管理 进程十分重要三、操作系统(OS)下的 --- 进程状态对于进程而言呢它是操作系统中的概念如果有学习过《操作系统》这门学科的话应该可以很清楚对于进程而言的话是存在着许许多多的状态如果一开始刚刚接触的小伙伴一定会感觉这么多状态要怎么区分呀其实那么多的状态真正主要的也就那么几个所以接下去我会中重点讲解以下几种进程的状态运行状态首先我们要谈到的是【运行状态】这个状态是最普遍的:进程 PCB 被调度到 CPU 运行队列中且已被分配 CPU 资源就叫做 ------ 运行状态一个CPU一个运行队列也就是说这些进程都需要在CPU这里进行排队逐个执行而排队的是进程么其实不是排队的是进程的PCB面试时不是你在排队而是你的简历信息在排队首先对于一个进程而言我们知道它是由内核 数据结构 所对应的代码和数据所组成的所以当系统中存在多个进程的时候就势必会存在多个结构体当然我们需要将这些进程给链接组织起来双向链表链接那么这些进程就相当于是在处在一个运行队列中我们如果要找到这个队列中的某个进程的话只需要找到这个进程的头部即可那我们就可以对应地去调度某个进程把这个进程所对应的代码和数据放到CPU上去执行因为每个进程是需要去竞争CPU资源的但是呢CPU不可能同时给这么多进程分配资源所以每一个CPU都会去维护一个运行队列里面的队头指针head所指向就是第一个进程所对应的【PCB】队尾指针tail所指向就是最后一个所对应的【PCB】。所以我们要运行某一个进程只需要将 head 所指向的那个进程放到 CPU上去运行即可提问一个进程只要把自己放到 CPU 上开始运行了是不是一直要到执行完毕才把自己放下来不是每一个进程都有一个叫做时间片的概念其时间大概是在10 ms左右。所以并不是一个进程一直在执行而是这多个进程在一个时间段内所有代码都会被执行—— 这就叫做【并发执行】所以呢这就一定会存在大量的进程被CPU放上去、拿下来的动作—— 这就叫做【进程切换】所以呢我们不要拿自己的时间感受去衡量CPU其运行一遍速度是非常快的你根本感受不到这种进程切换的效果阻塞状态阻塞就是进程因等待某种条件就绪而导致的一种不推进状态比如等待 键盘输入。通俗的来说阻塞就是进程卡住了原因就是缺少资源比如在我们日常生活中常常发生堵车原因就是道路资源不够用了车辆这个进程就需要原地等待那么进程需要什么资源呢比如磁盘、网卡、显卡等各种外设假设你现在想在steam上下载游戏当你点击下载按钮后提示磁盘空间不足此时是无法运行steam下载这个进程的因为此进程需要等待足够大的磁盘资源此时我们就称此进程为阻塞状态总结进程 阻塞 就是不被调度此时PCB(task_struct)就会被设置为 阻塞状态并链入等待的资源提供的等待队列。没错这里的等待队列 类似于 CPU 运行队列挂起状态挂起阻塞挂起当CPU资源紧张时将进程的数据和代码交换至磁盘中挂起此时内存中只有PCB挂起可以看作一种特殊的 --阻塞状态详解当计算机资源比较吃紧时操作系统一定要确保自身不会因为资源的紧张而崩溃所以就会将一些等待资源阻塞的进程的代码和 数据交换到磁盘的swap分区中这个过程称为唤出。当需要调度此进程时就会将磁盘的swap分区中保存的内容换回到内存中这个过程称为唤入。注意交换的是进程的代码和数据不是PCB如果PCB被交换出内存了那操作系统如何管理呢所以当某个进程的代码和数据不在内存中而被换出到磁盘上时该进程就为挂起状态。思考swap分区是越大越好么磁盘本质是输入输出设备每次唤入唤出其实都是非常低效的操作如果swap分区设置的过大那么操作系统就会十分依赖它导致出现更多低效IO这本身就是一种牺牲效率来确保操作系统能够正常运行的行为。所以swap分区不宜设置的过大一般为内存大小或内存大小的一半具体要看不同的操作系统举个--挂起-- 例子 在我们生活中一边走路一边玩手机很危险所以此时我们会将玩手机这个进程挂起即把手机揣进兜里然后专心执行走路这个进程总结看了上面的三种基本的进程状态后我们可以来总结一下如果要看进程是什么状态的话一般看这个进程在哪里排队位于【运行队列】的话它就是处于运行状态位于【阻塞队列】的话它就是处于阻塞状态四、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首先我们要来聊的是【运行状态R】来看下下面的这段代码是一个死循环去printf打印循环语句#include stdio.h #include unistd.h int main(void) { while(1); { printf(Hello process, pid: %d\n,getpid()); sleep(1); } return 0; }然后我们将下面的代码给运行起来观察这个进程的状态时看到其显示为S不过呢读者想看到的应该是R才对接下去呢我们把代码修改一下再来看看不使用printf打印语句了而且直接使用一个while(1)去做循环#include stdio.h #include unistd.h int main(void) { while(1); { //printf(Hello process, pid: %d\n,getpid()); //sleep(1); } return 0; }然后我们看到此时再运行起来时这个进程的状态就改变了变成了R这才是我们想要的【进程状态】那有读者就要问了为什么把printf打印语句给去掉之后就变成这样了呢原因就在于printf打印语句它是属于IO流的一种第一次因为是循环的缘故它一直在等IO设备就绪所以其进程状态就一直为S对应的即是在操作系统中的【阻塞状态】但是当我们去掉printf这种IO流之后呢它就是在纯纯运行没有IO那也就变成了R状态这里再补充说明一下这个S和R后面的这里的R代表的就是这个进程是在前台运行的所以我们在输入任何指令后不会对其造成 任何的影响那若是我们不以正常的方式去启动这个进程的话其进程的状态就会不一样了可以看到我在./mytest的后面加上了一个那么其状态变成了R此代表的意思就是这个进程它是运行在了【后台】的总结代表是前台运行无代表后台运行后台运行时可在命令行继续输入指令并执行但无法用ctrlc结束需要用kill -9 pid杀死。想要后台运行某个程序就在后面加如./test 浅度睡眠状态 -- S我们再通过一段代码来看看#include stdio.h #include unistd.h int main(void) { int a 0; printf(Enter# ); scanf(%d, a); printf(echo : %d\n, a); return 0; }将该进程运行起来我们可以看到其是出于S的状态因为【shell】此时正在等待用户的输入这个就对应到了我们上面所讲到的阻塞状态深度睡眠状态 -- D除了【浅度睡眠】之外呢还有一种叫做【深度睡眠】它们俩呢都是阻塞状态对于浅度睡眠来说之所以称为“浅度”是有原因的也就是处于这种状态的进程容易被唤醒。例如说我们在上面所讲到的这个处于阻塞状态的进程我们使用kill -9 PID向这个进程发送【9号信号】那么这个进程就被杀死了你也可以认为被唤醒了深度睡眠状态/不可中断睡眠状态/磁盘休眠状态顾名思义在这个状态的进程不会被杀掉哪怕是操作系统也不行通常会等待IO的结束。例如某一进程要求对磁盘进行写入操作那么在磁盘进行写入期间该进程就处于深度睡眠状态是不会被杀掉的因为该进程需要等待磁盘的回复是否写入成功以做出相应的应答。如果在这个过程中操作系统能够杀死该进程那么就有可能丢失 数据停止状态 -- T好接下去呢我们来讲讲【停止状态T】好接下去呢我们来讲讲【停止状态T】kill -l此处我们要使用到的是18、19号信号接下去我们就来试一试如何让这个进程暂停之后又重新启动会是怎样的#include stdio.h #include unistd.h int main() { while(1) { printf(hello my process: %d\n,getpid()); sleep(1); } return 0; }所以我们来总结一下暂停进程kill -19 PID启动进程kill -18 PID所以呢如果我们要将一个进程给终止的话发送19号信号即可要让其继续启动起来则发起18号信号那我现在要问了这个T停止状态和S睡眠状态有什么不同呢stopped状态 进程完全暂停了, 其不会再接收任何信号了一个进程通过 stopped 状态可以控制另一个S 和 D 一定是在等待某种资源而T状态可能在等待某种资源也可能被其他进程控制进程跟踪状态 -- t接下去呢我们再来说说进程的跟踪状态t还记得我们在基础篇中所学习的 GDB 调试 吗首先我们在正常状态下查看这个进程的状态其为S睡眠状态。接下去呢我进行了【gdb】调试在行内打上断点后输入r运行起来后我们再去查看这个进程的状态时就发现其状态变成了t原因就在于这个进程在调试的时候停了下来死亡状态 -- X对于【死亡状态X】来说呢这个状态只是一个返回状态你不会在任务列表里看到这个状态第一种方法就是向这个进程发送9号信号就可以杀掉这个进程kill -9 PID第二种方法就是通过这个进程的名称来杀掉它killall 进程名僵死状态Z —— 两个特殊进程接下去我们来介绍一种状态叫做【僵死状态Z】对于这个状态我们要涉及到两个特殊的进程叫做僵尸进程与孤儿进程① 僵尸进程首先我们要来介绍的是僵尸进程这里呢通过一个故事来进行引入你呢很喜欢晨跑这一天早晨6点又起来跑步了当你路过一个公园的时候遇到了一个晨练的【程序员】边跑边掉头发 但是呢跑着跑着此时突然他就“啪叽”倒了下来。那你此时就非常担惊受怕了马上拨打了110和120的电话然后守在他的身边等待救援来到周边的人看到也都纷纷围了过来其中不免有一些人会紧急救援。不过等了一会这个人就没了呼吸过了十几分钟后救护车和警车都来了警察先封锁了让法医过来检验一下其状况就说“这个人已经没救了赶紧通知家属准备后事吧~。好我们回归正题来说说这个【僵尸进程】因为在救护车来之前这个人其实就已经死亡了但是其状态还没有被检测当前并不知道它的死因所以我们操作系统就可能会维护当前的这个状态这个状态即为Z状态即[僵死状态]那我现在想问了有一个进程暂时退出了它要将它的状态暂时维持一段时间问题是它维持给谁看呢答父进程当一个进程退出的时候那最关心它的便是【父进程】。因为这个父进程费了很大的劲才将这个子进程fork出来此时呢它突然挂掉了那么此时父进程就必须去关心一下对应的子进程退出时的原因就上面这样生冷的文字来叙述还不太行接下去我们通过实际的案例来观察一下#include stdio.h 2 #include unistd.h 3 #include stdlib.h 4 #include sys/types.h 5 6 int Test1() 7 { 8 pid_t id fork(); 9 if(id 0) 10 { 11 //创建失败 12 printf(创建fork失败\n); 13 return 1; 14 } 15 else if(id 0) 16 { 17 //子进程暂停3s后终止 18 printf(Im child process, ); 19 printf(pid:%d , getpid()); 20 printf(ppid:%d \n, getppid()); 21 sleep(3); 22 return 0; 23 } 24 else{ 25 //父进程死循环 26 while(1) 27 { 28 printf(Iam parent process, ); 29 printf(pid: %d , getpid()); 30 printf(ppid: %d\n, getppid()); 31 sleep(1); 32 } 33 return 0; 34 } 35 } 36 37 38 39 40 int main() 41 { 42 Test1(); 43 return 0; 44 }运行起来可以看到在一开始的 3s 内父子进程还是同时在跑的但是当子进程在循环结束后退出了。不过此时呢父进程还在运行并没有退出。所以我们在右侧查看这两个进程的状态时间就发生了一定的变化仔细地来看一下这个状态的变化其中【PID】为2815其父进程为2814一开始它们均为S 睡眠状态但是呢到了后面子进程的状态就变成了Z僵死状态。也就意味着此时子进程已经死亡了但是呢父进程还不知道这里还可以通过右侧的这个defunct这个来看它表示【失效的、不再存在的】所以我们总结一下 进程一般退出的时候一般其不会立即彻底退出。如果父进程没有主动回收子进程信息子进程会一直让自己处于Z状态这也是为了方便后续父进程读取子进程的相关退出结果那对于上面的这种子进程我们就将其称作为是【僵尸进程】不过呢这种进程是存在一定危害的那一个父进程创建了很多子进程就是不回收是不是就会造成内存资源的浪费是的因为 数据结构对象本身就要占用内存想想C中定义一个结构体变量对象是要在内存的某个位置进行开辟空间那么对于上面的这种危害我们就称作为是【内存泄漏】要如何去进行避免呢之后的文章会做讲解~② 孤儿进程上面我们讲到当一个子进程突然退出但是父进程并没有去主动回收的话那么此时这个子进程就会变成【僵尸进程】那看到下面我们将这个进程突然终止之后父子进程都不见了因为当前这个进程是bash的子进程当其一退出之后bash就将其回收了。可是这个子进程为什么也没了呢这个的话我们就要聊聊【孤儿进程】了接下去我们来将上面测试僵尸进程的代码做个修改让父进程先于子进程退出#include stdio.h 2 #include unistd.h 3 #include stdlib.h 4 #include sys/types.h 5 6 int Test1() 7 { 8 pid_t id fork(); 9 if(id 0) 10 { 11 //创建失败 12 printf(创建fork失败\n); 13 return 1; 14 } 15 else if(id 0) 16 { 17 //子进程进入死循环 18 while(1) 19 { 20 printf(Im child process, ); 21 printf(pid:%d , getpid()); 22 printf(ppid:%d \n, getppid()); 23 sleep(1); 24 } 25 return 0; 26 } 27 else{ 28 //父进程暂停3s后终止 29 printf(Iam parent process, ); 30 printf(pid: %d , getpid()); 31 printf(ppid: %d\n, getppid()); 32 sleep(3); 33 return 0; 34 } 35 } 36 37 38 39 40 int main() 41 { 42 Test1(); 43 return 0; 44 }然后通过进程状态追踪来看看我们观察到一开始两个父进程和子进程是同步在走的但是呢过了一会父进程终止了只有子进程还在跑此时我们需要留意的不是其进程状态而是这个子进程的PPID即父进程的PID它由原先的【22677】变成了【1】这是为什么呢这里的话就要给读者普及一下了对于PID的值为1的进程我们一般将其称作为是【系统进程】。我们可以使用下面这句指令去查找一下ps ajx | head -1 ps ajx | grep systemd然后我们看到这个【系统进程】的PID即为1那为何会出现上面这种现象呢是这个子进程突然换父进程了吗对于父子进程来说如果父进程先于子进程结束的话那么这个子进程就会被称作为是【孤儿进程】对于孤儿进程来说呢它会有1号进程即【系统进程】所领养因为此时这个进程没有了父进程了所以需要有人去带领它但是这个孤儿进程为什么要被领养呢其实很简单既然它是一个进程的话最终都是要退出的但是没了父亲的话就只能由系统进程来进行维护了五、总结与提炼在本文中我们主要讲解了两个大点一个是在操作系统中所常见的一些进程状态它们分别为运行状态、阻塞状态、挂起状态。所被调度的、处于运行队列里的这些进程所处的状态我们称之为运行状态R处于等待队列中同时在等待外设相应的进程所处的状态我们称之为阻塞状态R当把一个进程的 数据和代码都重新交换到外设当中进程所处的状态我们称之为挂起状态R接下去我们又介绍了在Linux系统下的七种进程状态分别是运行状态R、浅度睡眠状态S、深度睡眠状态D、停止状态T、进程跟踪状态t、死亡状态X、僵死状态Z当一个进程没有在等待IO流的时候其就会处于运行状态R使用sleep()函数可以很好地使一个进程处于浅度睡眠状态S如果不想让一个进程在等待磁盘操作的时候被操作系统杀掉则可让其处于深度睡眠状态D可以向一个进程发送【19】号信号使其暂停处于停止状态T继续发送【18】号信号的话则可以使其重新启动在【gdb】的环境下去运行一个断点的话则可以使其处于进程跟踪状态t使用kill -9 PID就可以杀掉一个进程使其处于死亡状态X如果让一个子进程在父进程不知晓的时候退出那么其就会处于僵死状态Z变为【僵尸进程】若是在父子进程中父进程先于子进程退出的话那么这个子进程就会变成【孤儿进程】
郑州网站建设
网页设计
企业官网