🧑💻作者: @情话0.0
📝专栏:《Linux从入门到放弃》
👦个人简介:一名双非编程菜鸟,在这里分享自己的编程学习笔记,欢迎大家的指正与点赞,谢谢!
文章目录
前言
此篇博客依然是在之前的学习基础之上继续探究关于进程的知识。
一、进程状态
首先大家可以思考一个问题,当电脑打开一个客户端程序,它是一直在被运行吗?
答案当然不是,一个进程是不可能被一直运行的,在计算机内部都是多个进程来回被调度的,之所以我们感觉它一直在运行是因为进程之间的切换速度太快了,人是感受不到切换带来的延迟。
进程在运行的时候是可以被操作系统管理、调度的,这里就有一个问题,为什么这个进程被调度呢?为什么不调度其他进程呢?而这就取决于进程状态。
1.1 阻塞和挂起
要理解进程状态咱们先看一下这两种状态:阻塞和挂起
阻塞:进程因为等待某种条件就绪而导致的一种不推进的状态,其实就是我们常说的卡住了。阻塞一定是在等待某种资源,那么进程卡住了其实就是等待着CPU来调度它,站在进程的角度,它就是在等待获取到CPU的资源。所以说为什么阻塞呢?进程就是通过等待的方式,等具体的资源被别的进程使用完之后给自己拿到。
阻塞——进程等待某种资源就绪的过程。
但是,进程是怎么等待的呢?
这里提到的资源不仅有软件资源,也有硬件资源(磁盘、网卡、显卡等外设)。操作系统不仅要对进程管理,也得对这些资源做管理——先描述,再组织。于是也得为每个资源创建对应的结构体并链接起来。假设说某个进程现在缺少网卡这个硬件资源,那么操作系统就会将这个进程的PCB与网卡资源的结构体链接起来,当网卡资源被该进程拿到时,就会将该进程的PCB重新链接到被CPU调度的执行队列中。
挂起:在阻塞的概念中,若一个进程因没有某种资源就会被链接到对应资源的结构队列或链表中,而这些东西(进程PCB、代码数据、资源结构队列)都是存放在内存中的,如果这样的进程太多的话就会一直占据着内存的空间资源造成浪费,为了避免这种浪费操作系统将那些长时间不会被调度的进程的代码数据转移到磁盘中,这样就释放了内存的空间。而那些代码数据被转移到磁盘上的进程就进入了挂起状态。
下面将学习关于Linux操作系统的进程状态变化:
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*/
};
task_struct是一个结构体,内部包含这关于进程的各种属性,其中一个就是状态。进程状态是通过一个数组来表示的,用不同的数字代表不同的状态。
1.2 进程只要是R状态,就一定是在CPU上运行吗?
不一定,一个进程现在处于R状态并不代表你一定在CPU上运行。也就是说,系统中可能会存在多个进程是处于R状态的,但是可能只有那么几个在CPU上运行,所以CPU在调度进程时也一定要维护一个运行队列——将所有待运行的进程维护成一个队列。当CPU要调度进程时就直接从这个队列中挑选进程即可。同时要说明一个进程是什么状态一般也看这个进程在哪里排队。
1.2.1 这样的进程处于R状态吗?
#include <stdio.h>
int main()
{
while(1)
{
printf("我在运行吗???\n");
}
return 0;
}
运行上面的代码发现它陷入了死循环,会不停的给显示器上打印数据。那它是处于R状态吗?
经过查看之后发现该进程状态并不是R,而是S+,该进程不是跑得很欢实吗,为甚么不是R状态呢?
这是因为当执行打印语句得时候是需要访问显示器这样的外设资源,但是外设资源不可能直接被这个进程申请到,因为它还有可能被其他进程所访问,因此该进程就得需要在这个外设等待队列上进行等待,而CPU的执行速度非常快,CPU不允许你在持有CPU资源的同时还去等待某种资源,因此当查看进程状态的时候大部分就是S状态。
可是这个进程总有在CPU运行的时候吧,那我应该能查到R状态吧,可是怎么好像一直查不到呢?因为代码很短,一瞬间就被执行完了,而打印相对比较慢,可能1s的时间 99.9% 都是在等待的,所以在查看时可能得上万次才能查到一次R状态。
1.2.1 为什么这样的进程处于R状态?
当把那一行打印语句注释掉之后就会发现该进程又处于R状态,很明显,问题出现在那一行打印语句上。
而将那行代码注释掉,该进程就处于R状态,这是因为不用访问外设,while循环就是个判断语句,是纯计算的代码。在整个进程调度的生命周期里只会用CPU资源,只要被调度就一直是R状态,只要被查看就一直是R状态。
归根结底就是CPU太快了。
1.3 休眠状态
在上面的内容我们已经了解了S状态,表示可中断休眠,本质就是一种阻塞状态。
而D休眠状态表示的是不可中断休眠,但是对于我们来说一般用不着。对于D状态举个简单例子解释一下:
假设内存中某个进程有1GB的数据要写到磁盘上,于是在与磁盘交涉之后就开始往磁盘上写,毕竟磁盘的读写速度较慢,因此这个进程的状态被设置为阻塞状态,然后在磁盘的内核数据队列中等待排队。可是就在磁盘兢兢业业的拷贝数据的时候操作系统路过发现内存都忙成什么样了,怎么还有进程在这啥也不干还占着空间,于是就杀掉了这个进程。之后呢磁盘因为某种原因导致写失败,于是磁盘就要去找那个进程,可是发现怎么不见了,这就会导致数据的丢失。
那这个后果到底是谁的锅呢?磁盘?内存还是操作系统呢?其实都不是它们的锅,而是操作系统设计者的锅。实际上只要保证这个进程不能被杀死即可。于是就有了D状态。
1.4 暂停状态
T表示暂停状态,可通过指令的方式将一个进程的状态改为T状态。
#include <stdio.h>
#include <unistd.h>
int main()
{
int count=0;
while(1)
{
printf("我再运行吗??? count:%d\n",count++);
sleep(1);
}
return 0;
}
运行上面的代码我们知道会一直打印并且该进程的状态基本上都是S状态,我们可以通过发送19号信号(SIGSTOP)来暂停该进程,然后可再通过18号信号(SIGCONT)恢复该进程。
- 暂停进程
- 恢复进程
此时发现进程又像之前一样开始打印数据了,只是该进程的状态没有之前的 ‘+’,除此之外就是发现ctrl+c
并不能结束掉该进程。这是因为如果状态带了 ‘+’ 表示在前台运行,前台进程你可以ctrl+c
终止进程。但没有 ‘+’ 表示后台运行,就像上面代码一样一直打印数据,此时要想杀掉该进程就可通过9号信号杀死该进程。
1.5 僵尸状态(僵尸进程)
要谈僵尸状态,我们先思考一下为什么要创建进程?——让进程帮助我们办事,完成某项任务。当然对于完成的结果我们可以关心,也可以不关心。今天我们只谈关心结果。
我们一般在写main函数时,最后都会写一句return 0;
,而这个o表示这个main函数的退出码,可以通过这个退出码来判断进程执行后的结果。
如果一个进程退出了,立马X状态,立马退出,那么父进程有没有机会拿到退出结果呢?——在Linux中,当进程退出的时候,一般不会立即彻底退出,而是要维持一个状态叫做Z,也叫做僵尸状态。这种状态方便后续父进程或OS读取子进程的退出结果,所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t id=fork();
if(id==0)
{
//子进程
while(1)
{
printf("i am child,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
else if(id>0)
{
//父进程
while(1)
{
printf("i am father,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
return 0;
}
当通过kill命令杀死了子进程之后,子进程的状态就变成了Z+,此时它就必须一直被维持下去,直到父进程读取到它的退出结果。
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,也就是说,Z状态一直不退出,PCB就要一直被维护。
那一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费,因为数据结构对象本身就要占用内存,从而造成内存泄漏。
二、孤儿进程
在学习了僵尸进程之后明白了子进程在父进程之前退出,如果父进程一直不获取子进程的退出结果,那么子进程就会进入僵尸状态,子进程也就成了僵尸进程。
那什么是孤儿进程呢?
2.1 什么是孤儿进程?
从名字其实就可以感受到,孤儿就是了没有了父母,也就是父进程先退出,而子进程没有了父进程获取它的退出结果,那么子进程就变成了孤儿进程。之所以父进程正常退出没有变成僵尸状态那是因为父进程的父进程是bash,父进程退出的时候bash获取到了父进程的退出结果。
2.2 孤儿进程周边
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t id=fork();
if(id==0)
{
//子进程
while(1)
{
printf("i am child,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
else if(id>0)
{
//父进程
int cnt=10;
while(1)
{
printf("i am father,pid:%d,ppid:%d\n",getpid(),getppid());
if(cnt-- <= 0) break;
sleep(1);
}
}
return 0;
}
代码功能: 通过fork创建子进程,子进程一直循环打印,父进程在十秒之后退出。
//查看进程的状态信息
while :; do ps axj | head -1 && ps axj | grep myproc | grep myproc | grep -v grep; sleep 1; echo "-----------"; done;
上面两幅图分别是代码刚开始运行所查到的进程信息和在十秒之后的进程状态信息,发现前十秒父子进程都在正常运行,父进程PID为2453,子进程PID为2454。十秒之后,父进程退出,关于PID为2453的进程状态信息已没有,只剩下了子进程的状态信息。值得注意的是,此时子进程的父进程PID变成了1,并且它的状态变成了S,为后台进程,需要kill命令将这个进程杀死掉kill -9 2454 或者 killall myproc
。
孤儿进程——在父进程退出之后,子进程就会被操作系统自动领养(让1号进程成为新的父进程)。
为什么操作系统会这样做呢?
如果不这样做,那么子进程在退出之后会因没有父进程读取它的退出结果而处于一种游离状态,一直占据着相关的内存结构(内存泄露)。
总结
此篇博客首先就是关于进程的几种状态做了解释,主要就是对阻塞和挂起的理解。再者就是明对僵尸状态和僵尸进程的理解,如果子进程先退出而父进程又一直不获取子进程的退出结果,那么子进程就变成了僵尸进程。最后就是对孤儿进程的介绍,明白父进程先退出那么子进程就变成了孤儿进程同时会被1号进程领养。