|
自学网站
推荐给老铁们两款学习网站:
面试利器&算法学习:牛客网
风趣幽默的学人工智能:人工智能学习
首个付费专栏:《C++入门核心技术》
上文回顾
前面我们说了进程的概念, 掌握了 fork 系统接口的基本使用, 那这里有一个问题就是: 我们在调用一个函数, 当这个函数准备 return 的时候, 这个函数的核心功能完成了吗?
已经完成了, 此时:
- 子进程已经被创建了;
- 将子进程放入运行队列中.
如何理解进程被运行?(了解大概即可)
这个现在暂时看不懂也没关系, 后面我会详细讲解的.
进程状态
为了搞清楚正在运行的进程是什么意思, 我们需要知道进程的不同状态.
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 */
};
运行态
运行态指的是进程正在CPU上运行, 还是进程只要在运行队列中就叫做运行态?
答案是进程只要在运行队列中就叫做运行态, 代表我已经准备好了, 随时可以被调度器调度.(正所谓, 时刻准备着!)
终止态
终止状态指的是这个进程已经被释放了, 就叫做终止态, 还是该进程还在, 只不过永远不运行了, 随时等待被释放?
答案是该进程还在, 只不过永远不运行了, 随时等待被释放.
可能有老铁会问, 进程都终止了, 为什么不立马释放对应的资源, 而要维护一个终止态?
因为释放要花费时间, 操作系统当时可能很忙.(想想结账的例子)
阻塞态
关于进程阻塞, 我们需要注意的是一个进程, 在使用资源的时候, 可不仅仅是在申请CPU资源, 进程可能申请更多的其他资源, 如磁盘, 网卡, 显卡, 显示器资源, 声卡, 音响等, 所以也就是说, 关于进程阻塞, 是因为会访问外设.
如果我们申请CPU资源, 则是无法得到满足, 是需要排队的(在运行队列中排队). 同样的, 我们在申请其他慢设备资源的时候, 也是需要排队的(进程的 task_struct 在对应设备的等待队列中排队)
正如上图所示, 需要IO读取时, 可能此时的磁盘并没有就绪, 所以进程需要在磁盘的等待队列中排队, CPU从而执行其他进程.
当进程访问某些资源(如磁盘, 网卡), 该资源如果暂时没有准备好, 或者正在给其他进程提供服务, 此时:
- 当前进程要从 runqueue 中移除;
- 将当前进程放入对应设备的等待队列中.
这都是操作系统干的事!
当我们的进程在等待外部资源的时候, 此时该进程的代码不会被执行, 所以用户层显示的是: 我的进程卡住了, 也就是所谓的进程阻塞. 进程等待某种资源(非CPU), 该资源没有就绪的时候, 进程需要在该资源的等待队列中进行排队, 此时进程的代码并没有执行, 该进程所处的状态就叫阻塞.
好, 下面我们做一个小实验:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
while(1)
{
printf("%d\n", getpid());
}
return 0;
}
很明显上面的代码目的是死循环打印, 好, 下面我们运行程序, 查看当前进程所处的状态:
我们发现当前正在运行的进程是阻塞态, 诶? 不是很奇怪吗, 它不应该是运行态吗?
这是因为啊, 显示器是外设, 读写速度相比于CPU来说太慢了, 也就是说显示器处于一直没有就绪的状态, 所以才会出现上述的现象.
下面我们对代码进行改动, 不让它访问外设试试:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
while(1)
{
//printf("%d\n", getpid());
}
return 0;
}
运行程序:
我们发现此时进程状态变成了运行态.
这是为什么呢, 因为此时的进程没有访问外设, 只在CPU里面, 一直在运行队列中, 所以它不可能被阻塞.
挂起态
如果内存不足了怎么办?
此时OS会帮我们进行辗转腾挪, 因为短期内不会被调度的进程(它等的资源短期内不会就绪), 但是它的代码和数据依然在内存里面, 就是在白白浪费空间, 所以OS会把该进程的代码和数据临时置换到磁盘上.
这也就意味着, 内存不足的时候, 往往伴随着磁盘被高频访问.
僵尸状态
当一个进程退出的时候, 一般不会直接进入 X 状态(死亡, 资源可以立马回收), 而是进入 Z 状态.
这是为什么呢? 我们的进程又是为什么被创建出来呢? 一定是因为有任务需要被这个进程执行, 我们怎么知道, 这个进程把任务给我们完成的如何呢?
所以, 一般需要将进程的执行结果告知给父进程或者OS. 进入 Z 状态, 就是为了维护退出信息, 可以让父进程或者OS读取的(后面我们会讲到, 通过进程等待来读取)
那我们如何模拟僵尸进程呢?
如果创建子进程, 子进程退出了, 父进程不退出, 也不等待(回收)子进程, 那么此时子进程退出之后所处的状态就是 Z.
好, 我们做一个小实验:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程
int cnt = 5;
while(cnt)
{
printf("我是子进程, 我还剩下%d S\n", cnt--);
sleep(1);
}
printf("我是子进程, 我已经僵尸了, 等待被检测\n");
exit(0);
}
else
{
//父进程
while(1)
{
sleep(1);
}
}
return 0;
}
运行程序:
可能有老铁会问, 长时间僵尸有什么问题?
如果没有人回收子进程的僵尸, 该进程会一直维护, 该进程的相关资源(task_struct), 不会被释放, 导致内存泄露, 一般要求父进程进行回收(具体内容咱们后面会详细介绍)
总结僵尸进程的危害:
- 进程的退出状态必须被维护下去, 因为它要告诉关心他的父进程(你交给我的任务, 我完成的怎么样了), 可是如果父进程一直不读取, 那么子进程将会一直处于 Z 状态;
- 维护退出状态本身就要用数据维护, 也是属于进程的基本信息, 所以会被保存在 task_struct 中, 换句话说, Z 状态一直不退出, task_struct 就要一直维护;
- 那么如果一个父进程创建了很多个子进程, 但是就是不回收, 会不会造成内存资源的浪费? 肯定会的, 因为数据结构对象本身就是要占用内存的, 所以如果父进程回收, 是会导致内存泄漏的.
孤儿进程
前面说的僵尸进程指的是, 子进程退出了, 但是父进程不回收它, 导致子进程僵尸了.
那么孤儿进程又是什么呢?
父进程先退出, 子进程就称之为孤儿进程.
看下面一段代码:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程
while(1)
{
sleep(1);
}
}
else
{
//父进程
int cnt = 3;
while(cnt)
{
printf("我是父进程, 我还剩下%d S\n", cnt--);
sleep(1);
}
exit(0);
}
return 0;
}
运行程序:
我们看到实验现象, 父进程直接就没了, 他为什么没有 Z 呢?
因为父进程的父进程是 bash, 如果父进程提前结束是会被bash 自动回收的.
如果父进程提前退出, 子进程还在运行, 此时的子进程是孤儿进程, 会被 1号进程(init进程, 也就是OS) 领养.
好的, 概念基本讲的差不多了, 下面我们再回到原点, 看看一开始的 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 */
};
下面我们一一解析:
- R : 运行态
- S : 阻塞态(属于浅度睡眠, 即可中断睡眠)
- D : 阻塞态, 一般而言, Linux中如果我们等待的是磁盘资源, 进程阻塞就是D
- T : 暂停
- t : 暂停, 进程调试的时候, 遇到断点所处的状态
- X : 死亡状态
- Z : 僵尸状态