2.3 D磁盘休眠状态(Disk sleep)
D状态也是一种阻塞状态,在 Linux 系统层面我们称作深度睡眠,S状态称作浅度睡眠。浅度睡眠是可以被唤醒的,即可以响应外部的变化,我们可以通过 kill 指令(其他进程)将浅度睡眠的进程终止掉。下面通过一个情景剧来给大家介绍为什么要有 D 状态,以及 D 状态的作用。
有这样一个场景,一个进程需要向磁盘中写入大量数据。在正常情况下往磁盘中写入数据,进程是需要等待的,等磁盘写完后给进程一个信号,然后进程才能继续去运行。有一天进程A就在向磁盘中写入大量数据,磁盘在写入的过程中,进程A就在内存中翘着二郎腿,嗑着瓜子在等待磁盘写完了给它发信息,此时路过的操作系统发现了进程A,它对进程A说:“我这内存压力都大的不行了,你小子倒好,占着内存不干正事,还在这嗑瓜子!”。于是乎操作系统就将进程A kill 掉了。此时磁盘傻眼了,数据写到一半进程没了,因为进程没了,所以磁盘就把写入的数据删除了,最终结果就是数据没有被写入磁盘。究竟是谁导致了这场悲剧的发生呢?于是乎法官就出来,它先审问操作系统,进程是你 kill 掉的,你怎么解释?操作系统说,我命苦呀,我只是完成了我的本职工作呀,为了给用户提供流畅的运行环境,将一些进程 kill 掉是我的职责呀,这不是我的问题呀。接着法官又来问磁盘,数据是你丢失的,你该如何解释?磁盘说,我祖祖辈辈都是这样工作的呀,进程它让我写入数据,结果自己不见了,其它磁盘遇到这种情况也是将数据丢弃掉呀,你如果判我有罪,那岂不是我的父亲、母亲都有罪呀。最后法官来问进程A,进程还没等法官开口就扑通跪下说,法官大人您明察秋毫呀,我才是被 kill 掉的那个,我属于被害人呀,我怎么会有罪呢。法官听了一圈,感觉大家都没罪,最终法官宣判了,你们三个都没罪,是制度问题,回去我改改操作系统,当进程在向磁盘中写入数据的时候任何人都不能将该进程 kill 掉。于是 D 状态就诞生了。当一个进程处于 D 状态的时候,它不会响应任何请求,任何人和操作系统都不能将该进程 kill 掉。
小Tips:结束掉 D 状态的方法有两种,一是等待某个条件满足,如等待数据写完,二是直接断电。如果被用户查到 D 状态的进程,那就预示着这个操作系统离崩溃不远啦。所以 D 状态会有,但是一般出现的时间都非常短。
2.4 T停止状态(stopped)
在 Linux 内核源代码中我们可以看到连个 T 状态,一个是 T ,一个是 t,我们可以认为这两个 T 状态是一样的,对于一个进程,我们可以通过下面这条指令将它设置成停止状态。
kill -19 进程PID
可以通过下面这条指令来结束停止状态。
kill -18 进程PID//
小Tips:结束停止状态的进程会到后台运行,要终止掉这个进程只能通过 kill -9指令。T状态和S状态很像,其中S状态的进程一定是在等待某种资源,而T状态的进程可能是在等待某种资源,也可能是在被其他进程控制。我们在打断点调试一段代码的时候,该进程就会处于T状态。
三、僵尸进程
一个进程在退出时并不是立即将自己所有资源全部释放,当一个进程退出时,操作系统会把当前进程的各种信息维持一段时间,这个状态就叫做 Z 僵尸状态。维持信息是给关心它的“人”,也就是父进程来查看的。如果父进程一直没有来关心退出的子进程,那么这个子进程将长时间处于 Z 状态。
int main() { pid_t id = fork(); if(id == 0) { int cnt = 5; while(cnt) { printf("我是子进程,PID是:%d,PPID:%d,cnt:%d\n",getpid(),getppid(),cnt); sleep(1); cnt--; } _exit(0); } else { while(1) { printf("我是父进程,PID是:%d,PPID:%d\n",getpid(),getppid()); sleep(1); } } return 0; }
上面这段代码在 process 进程中通过调用 fork 接口创建了一个子进程,子进程在执行完五次打印后就会被终止掉,其中的 exit 函数就是用来终止一个进程,父进程将一直运行。
子进程执行完5次打印后就处于 Z 状态并且后面跟了一个单词 defunct,该单词有死了的,不存在的意思,只不过它还再等父进程来回收它的资源。处于 Z 状态的进程的相关资源尤其是 task_struct 结构体不能被释放。只有当父进程把子进程的相关资源回收后,子进程才能变成 X死亡状态。我们将这种处于 Z 状态的进程就叫做僵尸进程,如果父进程一直不来回收,那这种进程会长时间占用内存资源,造成内存泄漏。
3.1 僵尸进程危害总结
- 进程的退出状态必须被维持下去,因为它要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就将一直处于 Z 状态。
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在 PCB 对象中,换句话说,Z状态一直不退出,PCB一直都要维护。
- 一个父进程如果创建了很多的子进程,就是不回收,会造成内存资源的浪费,因为 PCB 对象本身就要占用内存。
- 造成内存泄漏。
四、孤儿进程
上面我们是让子进程先退出,父进程一直运行,接下来我们让父进程先退出,子进程一直运行,看看会有什么结果。
int main() { pid_t id = fork(); if(id == 0) { //子进程 int cnt = 500; while(cnt) { printf("我是子进程,PID是:%d,PPID:%d,cnt:%d\n",getpid(),getppid(),cnt); sleep(1); cnt--; } _exit(0); } else { //父进程 int cnt = 5; //这里的cnt是5,意味着父进程会先执行结束 while(cnt--) { printf("我是父进程,PID是:%d,PPID:%d,cnt:%d\n",getpid(),getppid(),cnt); sleep(1); } } return 0; }
可以看到父进程在执行结束后就只剩下子进程,为什么父进程不会处在 Z僵尸状态呢?答案是父进程也是 bash 的子进程,父进程在执行结束后,它的父进程 bash 会将其回收掉,并且过程非常快,所以我们我们没有看到父进程处在 Z僵尸状态。其次我们发现,当父进程结束后,它的子进程的父进程会变成1号进程,即操作系统。我们将父进程是1号进程的进程叫做孤儿进程,该进程被系统领养。因为孤儿进程未来也会退出,也要被释放,所以它需要被领养。
小Tips:所有的进程只对它的“儿子”,即子进程负责,不会对它的孙子进程负责,因为代码中只有创建子进程的逻辑,并没有创建孙子进程的逻辑,所以并不是不想让爷爷进程来回收孙子进程的资源,是因为爷爷进程没有这个本事,而操作系统会直接从内核层面进行回收,所以当一个进程的父进程结束后,会把该进程交给操作系统,让操作系统来充当它的父进程。
五、结语
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,春人的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是春人前进的动力!