✍作者:阿润菜菜
专栏:Liunx系统编程
引入
其实在我们运行程序时,CPU并不是一个进程一直在运行,而是一个进程跑一会,另一个进程跑一会,这样间替周而复始。----- 分时操作系统
那凭什么要运行这个进程而不是另一个? 答案就取决于 进程状态
我们来看一下在Linux内核源代码中是怎么定义进程状态的:
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
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 */
};
可以看到状态态在kernel源代码里定义的方式大概是用一个数组包装,用不同的数字来标识不同的状态,如 0 代表R(运行)状态
因为操作系统有很多,每个操作系统对进程的管理都有所差别,但基本原理是一样的,我们下面来研究Linux操作系统来观察进程状态
阻塞:进程因为等待某种条件就绪,而导致的一种不推进的状态 ---- 进程卡住了 ---- 一定是在等待某种资源 ( 进程等待某种资源就绪的过程)
就比如我写了个scanf代码,当CPU运行到此处时,需要等待我从键盘进行输入,此时进程的状态就是阻塞状态。
为什么程序多会卡? 因为启动了太多的进程,操作系统难以调度过来,进程卡住了(阻塞!)
为什么要有阻塞? 进程要通过等待的方式,等具体的资源被别人用完之后,再被自己使用
挂起:闲置进程的代码和数据被交换到磁盘中(可以看作成一种特殊的阻塞状态)
注:
阻塞就是不被操作系统调度,一定是当前进程需要等待某种资源就绪,一定是task_struct结构体需要在被某种OS管理下的资源下进行排队
因为我们想要让进程帮我们办事情。进程给我们办事情我们就会关心结果,为了使程序能并发执行,且对并发执行的程序加以描述和控制(PCB),我们引出了进程的概念。
计算机在开机的时候,操作系统就会被加载到内存里面,磁盘中的程序在运行的时候也会被加载到内存里面,实际上是加载到操作系统内部,受操作系统的管理,我们知道程序运行的时候,是需要CPU进行读取进程的代码并计算的,但进程的数量一定会比CPU多,那CPU该怎么一个个的读取进程代码并计算呢?答案是通过运行队列(PCB数据结构)来对进程的运行进行管理。
我们可以通过测试代码进行观察
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4
5 int main()
6 {
7 while(1)
8 {}
9 }
当程序代码只是一个死循环时,我们将程序运行起来,然后查看进程状态,可以很明显的看到状态是R,也就是运行状态。
![在这里插入图片描述](https://img-blog.csdnimg.cn/7c5971a63df344b4bfec735a8b77448a.png
问题1:进程只要是R状态,就一定是在CPU上运行吗?
进程是什么状态,一般也看这个进程在哪里排队 。
也就是说,R状态并不直接代表进程在运行,而代表进程在运行队列中排队。
问题2进程队列是谁维护的?
操作系统自己维护的(即内存中),并不是CPU内维护。运行队列中是task_struct (PCB控制块)
如果我们在刚刚代码中加入一句printf实现在显示屏上打印再来观察进程状态会出现什么?
结果
我们在代码中添加printf语句之后,程序还在运行,但观察到的进程的状态是S休眠状态,这是为什么呢?
因为我们的代码中访问了外设资源,即显示器。我们知道CPU会飞速的运行完进程的所有代码,但是我们写的进程需要占用硬件资源,每一次占用硬件资源都要等显示器就绪,这会花很长的时间(CPU计算的速度和IO的速度差别大概是几十万倍)。大概率99%的时间是进程在等显示器就绪,也就是在等待IO就绪,1%的时间是CPU在运行进程的代码,所以我们在查看进程状态的时候,极大概率上查到的都是S休眠状态。更形象化的说明就是,在进程访问完毕一次显示器的时候,CPU已经将这个死循环代码执行了50、60万次,所以我们在查看进程状态的时候,进程都是在等IO就绪的,所以就会查看到进程是休眠状态,这也是阻塞状态的一种。
S 休眠状态的两种
S 休眠状态本质是一种阻塞状态
S+: 前台运行 可以ctrl C暂停
S:后台运行 不可以Ctrl C暂停 可以直接kill 干掉
S状态是浅度睡眠状态,是可以被终止的,通过ctrl+c或kill -9 pid两种方式进行分别进行前后台终止。
背景
当阻塞进程过多时,操作系统会将一些进程挂起,以此来解决内存空间不足的问题。如果挂起依旧无法解决内存空间不足,Linux操作系统就会将进程杀死,但是这样杀死进程很有可能导致进程对应的IO过程失败,从而丢失大量数据,这会对用户造成巨大的损失,所以就出现了一个新的进程状态,深度睡眠状态,这样的进具有无法被操作系统kill掉的特性。一般情况下,处于D状态的进程,只能等待IO过程结束,让进程自己醒来,重新投入CPU的运行队列,重新继续运行进程。或者万不得已可以通过断电的方式来杀掉深度睡眠的进程!!!
当然深度睡眠的状态一般不会出现,只有高IO的情况下,运行某个程序时,进程才有可能出现深度睡眠的状态。如果处于这种状态,计算机也宕机了。
本质是阻塞状态的一种。
我们可以通过kill命令进行进程暂停状态的操作
kill -19 + 进程id --- 停止运行进程
kill -18 + 进程id --- 继续运行进程
注:状态后面带+,表示前台进程,状态后面不带+,表示后台进程
t 追踪暂停状态
Linux内核源代码中为了区分跟踪暂停和暂停状态,将T改为t来表示追踪暂停状态
适用在我们调试断点处 本质:暂停进程
当我们在调试某个二进制程序的时候,其实就是在调试该进程,当进程中有断点的时候,gdb中按下r进行调试运行,此时就会由于断点的存在而停下来,这其实表示的就是我们当前运行的进程停下来了,等待我们查看当前进程的上下文数据,这就是tracing stop状态,跟踪状态。
当进程结束时会显示X 状态,由于CPU处理的速度之快,我们极难观察到。
想一下如果一个进程退出了,立马进入X状态,你作为父进程,有没有机会拿到退出结果呢?
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)
没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。
所以Linux当进程退出的时候,一般进程不会立即彻底退出,而是要维持一个状态叫做:Z 僵尸状态。—方便后续父进程(OS)读取该子进程退出的退出结果。
只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
代码示例:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4
5 int main()
6 {
7
8 pid_t id = fork();
9
10 if (id==0)
11 {
12 printf("I am a child process,pid:%d,ppid:%d\n",getpid(),getppid());
13 sleep(5);
14 exit(1);
15 }
16 else
17 {
18 while(1)
19 {
20 printf("I am a parent process,pid:%d,ppid:%d\n",getpid(),getppid());
21 sleep(1);
22 }
23 }
24 return 0;
25 }
结果:
僵尸进程危害
那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
进程的退出状态属于进程的基本信息,也是需要数据进行维护的,所以这种信息会被保存在进程对应的PCB里面,如果进程的状态一直是Z状态的话(父进程一直不读取子进程的退出状态),那么PCB就需要一直维护这种状态信息,虽然子进程对应的代码和数据会被释放,但是PCB是不会被释放的,因为他需要维护进程的Z状态,所以这个时候就会产生内存泄露的问题
僵尸进程会产生资源泄露,需要避免避免僵尸进程的产生采用进程等待(wait/waitpid)方式完成
僵尸进程无法被杀掉,即使通过kill信号也无法杀掉,因为它已经死亡了,所以无法被杀掉的进程有三个,深度睡眠进程,僵尸进程,死亡进程,D状态是不能杀,Z和X是无法杀,因为已经死了!
在结束进程之后,操作系统拿到退出码,进行甄别帮我们将父进程和子进程一起回收掉。以免内存泄露的发生。
如果父进程提前退出,那么子进程就被称之为:孤儿进程
我们知道僵尸进程是子进程退出,父进程还在运行,由于父进程需要读取到子进程状态,子进程会进入Z(僵尸)状态,后续会由操作系统进行甄别回收。那么子进程退出,父进程会帮助子进程进行回收。如果父进程提前退出了,那么它的子进程谁来回收?答案是子进程会被OS自动领养(通过1号进程(bash) 成为其新的父进程)
孤儿进程:父进程先于子进程退出,子进程成为孤儿进程,运行在后台,父进程成为1号进程(而孤儿进程的退出,会被1号进程负责任的进行处理,因此不会成为僵尸进程)
#include
#include
#include
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 1;
}
else if(id == 0){//child
printf("I am child, pid : %d\n", getpid());
sleep(10);
}else{//parent
printf("I am parent, pid: %d\n", getpid());
sleep(3);
exit(0);
}
return 0;
}