
|
|
推荐给老铁们两款学习网站:
面试利器&算法学习:牛客网
风趣幽默的学人工智能:人工智能学习
首个付费专栏:《C++入门核心技术》
前面我们说了进程的概念, 掌握了 fork 系统接口的基本使用, 那这里有一个问题就是: 我们在调用一个函数, 当这个函数准备 return 的时候, 这个函数的核心功能完成了吗?
已经完成了, 此时:


为了搞清楚正在运行的进程是什么意思, 我们需要知道进程的不同状态.
/*
* 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从而执行其他进程.
当进程访问某些资源(如磁盘, 网卡), 该资源如果暂时没有准备好, 或者正在给其他进程提供服务, 此时:
这都是操作系统干的事!
当我们的进程在等待外部资源的时候, 此时该进程的代码不会被执行, 所以用户层显示的是: 我的进程卡住了, 也就是所谓的进程阻塞. 进程等待某种资源(非CPU), 该资源没有就绪的时候, 进程需要在该资源的等待队列中进行排队, 此时进程的代码并没有执行, 该进程所处的状态就叫阻塞.
好, 下面我们做一个小实验:
#include
#include
#include
int main()
{
while(1)
{
printf("%d\n", getpid());
}
return 0;
}
很明显上面的代码目的是死循环打印, 好, 下面我们运行程序, 查看当前进程所处的状态:

我们发现当前正在运行的进程是阻塞态, 诶? 不是很奇怪吗, 它不应该是运行态吗?
这是因为啊, 显示器是外设, 读写速度相比于CPU来说太慢了, 也就是说显示器处于一直没有就绪的状态, 所以才会出现上述的现象.
下面我们对代码进行改动, 不让它访问外设试试:
#include
#include
#include
int main()
{
while(1)
{
//printf("%d\n", getpid());
}
return 0;
}
运行程序:

我们发现此时进程状态变成了运行态.
这是为什么呢, 因为此时的进程没有访问外设, 只在CPU里面, 一直在运行队列中, 所以它不可能被阻塞.
如果内存不足了怎么办?
此时OS会帮我们进行辗转腾挪, 因为短期内不会被调度的进程(它等的资源短期内不会就绪), 但是它的代码和数据依然在内存里面, 就是在白白浪费空间, 所以OS会把该进程的代码和数据临时置换到磁盘上.
这也就意味着, 内存不足的时候, 往往伴随着磁盘被高频访问.

当一个进程退出的时候, 一般不会直接进入 X 状态(死亡, 资源可以立马回收), 而是进入 Z 状态.
这是为什么呢? 我们的进程又是为什么被创建出来呢? 一定是因为有任务需要被这个进程执行, 我们怎么知道, 这个进程把任务给我们完成的如何呢?
所以, 一般需要将进程的执行结果告知给父进程或者OS. 进入 Z 状态, 就是为了维护退出信息, 可以让父进程或者OS读取的(后面我们会讲到, 通过进程等待来读取)
那我们如何模拟僵尸进程呢?
如果创建子进程, 子进程退出了, 父进程不退出, 也不等待(回收)子进程, 那么此时子进程退出之后所处的状态就是 Z.
好, 我们做一个小实验:
#include
#include
#include
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), 不会被释放, 导致内存泄露, 一般要求父进程进行回收(具体内容咱们后面会详细介绍)
总结僵尸进程的危害:
前面说的僵尸进程指的是, 子进程退出了, 但是父进程不回收它, 导致子进程僵尸了.
那么孤儿进程又是什么呢?
父进程先退出, 子进程就称之为孤儿进程.
看下面一段代码:
#include
#include
#include
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 */
};
下面我们一一解析:
1.下面有关孤儿进程和僵尸进程的描述,说法错误的是?
A.孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。
B.僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
C.孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
D.孤儿进程和僵尸进程都可能使系统不能产生新的进程,都应该避免
解析:
僵尸进程:子进程先于父进程退出,父进程没有对子进程的退出进行处理,因此子进程会保存自己的退出信息而无法释放所有资源成为僵尸进程导致资源泄露。
孤儿进程:父进程先于子进程退出,子进程成为孤儿进程,运行在后台,父进程成为1号进程(而孤儿进程的退出,会被1号进程负责任的进行处理,因此不会成为僵尸进程)
2.关于僵尸进程,以下描述正确的有?
A.僵尸进程必须使用waitpid/wait接口进行等待
B.僵尸进程最终会自动退出
C.僵尸进程可以被kill命令杀死
D.僵尸进程是因为父进程先于子进程退出而产生的
解析:
僵尸进程是指先于父进程退出的子进程程序已经不再运行,但是因为需要保存退出原因,因此资源没有完全释放的进程,它不会自动退出释放所有资源,也不会被kill命令再次杀死;
僵尸进程会产生资源泄露,需要避免;
避免僵尸进程的产生采用进程等待(wait/waitpid)方式完成.
3.以下关于孤儿进程的描述正确的有
A.父进程先于子进程退出,则子进程成为孤儿进程
B.孤儿进程会产生资源泄漏
C.孤儿进程运行在系统后台
D.孤儿进程没有父进程
解析:
孤儿进程:父进程先于子进程退出,子进程成为孤儿进程, 运行在后台,父进程成为1号进程,退出后由1号进程回收资源,因此不会成为僵尸进程,而是直接释放所有资源;
孤儿进程的产生一般都会带有目的性,比如我们需要一个程序运行在后台,或者我们不想一个进程退出后成为僵尸进程之类的, 所以本题A,C均可.
4.以下描述错误的有
A.守护进程:运行在后台的一种特殊进程,独立于控制终端并周期性地执行某些任务。
B.僵尸进程:一个进程 fork 子进程,子进程退出,而父进程没有 wait/waitpid子进程,那么子进程的进程描述符仍保存在系统中,这样的进程称为僵尸进程。
C.孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,这些子进程称为孤儿进程。(孤儿进程将由 init 进程收养并对它们完成状态收集工作)
D.精灵进程:精灵进程退出后会成为僵尸进程
解析:
僵尸进程:子进程先于父进程退出,父进程没有对子进程的退出进行处理,因此子进程会保存自己的退出信息而无法释放所有资源成为僵尸进程导致资源泄露。
孤儿进程:父进程先于子进程退出,子进程成为孤儿进程,运行在后台,父进程成为1号进程(而孤儿进程的退出,会被1号进程负责任的进行处理,因此不会成为僵尸进程)
守护进程&精灵进程:这两种是同一种进程的不同翻译,是特殊的孤儿进程,不但运行在后台,最主要的是脱离了与终端和登录会话的所有联系,也就是默默的运行在后台不想受到任何影响