文章目录
- 前言
- 一、进程状态概述
- 1.1 运行状态
- 1.2 阻塞状态
- 1.3 挂起状态
- 二、具体的Linux操作系统中的进程状态
- 2.1 Linux内核源代码
- 2.2 查看进程状态
- 2.3 D磁盘休眠状态(Disk sleep)
- D状态的定义:
- 2.4 T停止状态(stopped)
- 停止状态的概述:
- 停止状态的触发条件:
- 停止状态的行为:
- 三、僵尸进程
- 3.1 僵尸进程的形成原因:
- 3.2 僵尸进程的特点:
- 3.3 如何避免僵尸进程:
- 四、孤儿进程
- 4.1 孤儿进程的形成原因:
- 4.2 孤儿进程的处理:
- 4.3 孤儿进程的特点:
- 4.4 孤儿进程与僵尸进程的区别:
- 结语
前言
在Linux操作系统中,进程状态是操作系统资源调度和管理的重要组成部分。每个进程在其生命周期中都会经历多种状态,常见的有运行、睡眠、停止等,而其中的 僵尸进程 和 孤儿进程 则是我们必须特别关注的特殊进程类型。僵尸进程虽然已终止,但由于父进程未回收其退出状态,依然占据系统资源;而孤儿进程则是在父进程结束后继续存在,并由 init 进程接管。这些特殊进程的存在可能会对系统的资源管理和性能产生影响,了解它们的形成原因、特点及解决方式,对于优化系统、提高效率具有重要意义。本文将深入解析Linux中的进程状态,重点讨论僵尸进程与孤儿进程的形成、影响及处理方法。
一、进程状态概述
操作系统中的进程状态反映了每个进程当前的活动状态。以下是 常见的进程状态,包括其意义、特点及常见场景。
1.1 运行状态
描述: 进程正在 CPU 上执行,或者处于就绪状态等待 CPU 调度。
触发:
- 进程被调度到 CPU 上运行。
- 睡眠态的进程被唤醒并准备运行。
详解:一般的计算机只有一个CPU,但是进程却是有很多个,对 CPU 来说,运行的本质就是被 CPU 调度,所以每个 CPU 都会有一个运行队列,所有的进程要运行都必须在运行队列上排队,而参与排队的是每一个进程对应的PCB 对象
一个进程被 CPU 调度运行,并不是要一直运行直到完毕,假如说一个程序里有一个死循环,导致程序无法终止,进程没法结束,那这时其他的进程就没办法运行,操作系统为了避免一个进程长时间占用 CPU 资源的情况的发生,提出了一个叫做时间片的概念。
时间片(Time Slice)是操作系统中用于实现进程调度的一种机制。在多任务操作系统中,CPU的时间被划分为若干小段,每个进程在一个时间片内运行,时间片用完后,操作系统会暂停该进程的执行,将CPU分配给下一个进程。这种方式是实现多进程并发执行的关键。
时间片的长度对系统性能有很大影响:
- 时间片过短会导致频繁的上下文切换,增加系统开销。
- 时间片过长则会导致响应时间较长,影响系统的实时性。
时间片的调度通常由操作系统内核根据不同的调度算法(如轮转法、优先级调度等)来进行管理。
1.2 阻塞状态
阻塞状态(Blocked State)是操作系统中进程的一种状态,指的是进程由于等待某些事件或资源而无法继续执行的状态。常见的情况包括:
- 等待输入/输出(I/O)操作完成:当一个进程发起I/O操作(如读取文件、等待网络数据)时,如果I/O操作还没有完成,进程会进入阻塞状态,直到I/O操作完成。
- 等待信号量或资源:进程可能需要等待某个资源(如共享内存、锁等)或同步信号(如信号量、条件变量等),如果该资源当前不可用,进程将进入阻塞状态。
- 等待事件发生:某些进程可能需要等待特定事件的发生才能继续执行,例如等待其他进程的结果或信号。
在阻塞状态下,进程不会占用CPU资源,操作系统会调度其他进程进行执行。一旦进程等待的条件满足,操作系统会将该进程从阻塞状态转换回就绪状态,进程就能继续执行。
举个栗子:
在日常编程中,最常见的一种阻塞情况就是一个进程需要通过键盘读取数据,键盘是一种硬件,在冯诺依曼结构体系中属于输入设备,操作系统对硬件资源的管理是先描述再组织,因此每一个硬件都会对应一个结构体对象,该结构体对象中一定会维护一个等待队列,当一个进程需要利用该硬件资源时,进程的 PCB 对象就会被链入该等待队列,此时进程就处于阻塞状态。
1.3 挂起状态
在操作系统中,挂起状态(Suspended State)通常是指某个进程或任务被暂停执行,但并未完全终止。挂起状态允许操作系统在适当的时候恢复该进程的执行。挂起状态的主要目的是为了更高效地管理系统资源,尤其是在多任务处理和进程调度中。
一个进程在没有被 CPU 调度的情况下,其代码和数据都是空闲的,当系统内存空间告急时,操作系统就会把这些没有被 CPU 调度的进程的数据和代码先放到磁盘中存储,只剩下其对应的 PCB 对象在队列中排队,这种状态就叫做进程的挂起状态。
以上介绍的都是操作系统学科的常见知识,不同的操作系统会有不同的实现方案,现在我们就来看看 Linux 操作系统下的各种进程状态。
二、具体的Linux操作系统中的进程状态
以下是 Linux 操作系统下的常见状态:
状态符号 | 状态名称 | 描述 |
---|---|---|
R | 运行(Running) | 进程正在 CPU 上运行或处于可运行状态,等待被调度。 |
S | 可中断睡眠(Sleeping) | 进程正在等待某个事件(如 IO 操作完成),可以通过信号唤醒。 |
D | 不可中断睡眠(Disk Sleep) | 进程正在等待硬件资源(如磁盘 IO),无法被信号中断。 |
T | 停止(Stopped) | 进程已暂停,通常由用户发送 SIGSTOP 或 SIGTSTP 信号。 |
Z | 僵尸(Zombie) | 进程已终止,但其退出状态尚未被父进程读取。 |
t | 挂起(Tracing stop) | 进程被调试器(如 gdb )挂起。 |
X | 死亡(Dead) | 进程已经终止(极少见)。 |
2.1 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 */
};
2.2 查看进程状态
- 先来看看下面这段代码执行起来后的进程状态。
int main()
{ while(1) { printf("Hello \n!"); } return 0;
}
目前这段代码执行出来的进程状态是 S+ 睡眠状态(‘+’ 代表前台运行,暂时不用管),在这段代码中,printf
的执行涉及到终端输出的 I/O 操作。如果在输出期间终端忙碌或缓冲区满,操作系统会将进程暂时挂起,进入 可中断睡眠态(S)。
- 将 while 循环中的打印去掉再去执行代码看看进程状态。
int main()
{ while(1); return 0;
}
这段代码执行出来的进程状态是 R+ 运行状态,该程序会在 while(1);
无限循环中不断运行,占用 CPU,但没有任何实际的操作或输出。由于没有被阻塞、暂停或终止,它始终处于 运行状态(Running),并且在没有外部干预的情况下无法退出。
2.3 D磁盘休眠状态(Disk sleep)
有这样一个场景,一个进程需要向磁盘中写入大量数据。在正常情况下,当进程往磁盘写数据时,它需要等待磁盘写入完成,才能继续执行。这时,进程会处于阻塞状态,也就是在内存中等待外部事件(磁盘操作)完成后,再继续执行。
有一天,进程A就在向磁盘写入大量数据,磁盘正在处理这个请求,进D状态的定义:
当进程进入D状态时,它会处于一种深度睡眠状态,无法响应任何外部的信号,甚至不能被操作系统kill掉。这个状态通常是在进程进行I/O操作时,等待磁盘、网络或其他外部资源的响应。当进程处于D状态时,它实际上是在等待一个长时间的I/O操作(如磁盘写入、读取等)。在此期间,其他任何进程无法干扰它的执行,确保了数据的完整性。程A却在内存中翘着二郎腿,嗑着瓜子在等待磁盘写完后通知它。这时,操作系统发现了进程A,系统检查到内存资源非常紧张,认为进程A占用了大量内存,却什么都不做,导致系统性能下降。
于是,操作系统决定将进程 A kill掉,释放资源。操作系统向进程A发送了kill信号。可是,进程A并没有响应,因为它此时处于D状态(深度睡眠状态),在等待磁盘写入完成。这个状态下,进程无法响应任何外部信号,也无法被终止。于是,进程A就被强行终止,结果是磁盘写入过程被中断,数据没有被完整写入磁盘。
磁盘一时也无法应对这种情况,它原本正等待进程A的写入操作完成,但由于进程被终止,磁盘只能丢弃那部分未完成的数据。最终,数据没有被保存,文件操作失败。
此时,法官出来审问:
- 操作系统:“我只是尽力为系统释放内存资源,确保其他进程能顺利运行。我发现进程A占用了大量内存而没有活动,所以就决定将它kill掉。系统流畅性是我的责任,不能因为它占用内存不干正事就放任它。”
- 磁盘:“我一直按照我该有的规则工作,进程A让我写入数据,结果它在写入中途突然消失。没有进程的指令,数据我只能丢掉。我们磁盘一直是这样的做法,其他磁盘也是如此。”
- 进程A:“法官,我才是受害者呀!我只是在等待磁盘完成任务,结果就被强行kill掉了,哪里有我的错?”
经过审问,法官得出结论:操作系统、磁盘和进程A都没有错。这场悲剧的发生是因为操作系统设计上的一个问题。法官最终判定:为了避免类似情况发生,必须让进程在向磁盘写入数据时不能被随意终止。因此,**D状态(深度睡眠状态)**应运而生。
D状态的定义:
当进程进入D状态时,它会处于一种深度睡眠状态,无法响应任何外部的信号,甚至不能被操作系统kill掉。这个状态通常是在进程进行I/O操作时,等待磁盘、网络或其他外部资源的响应。当进程处于D状态时,它实际上是在等待一个长时间的I/O操作(如磁盘写入、读取等)。在此期间,其他任何进程无法干扰它的执行,确保了数据的完整性。
2.4 T停止状态(stopped)
在Linux操作系统中,**停止状态(stopped)**是进程的一种状态,通常表示进程被挂起,等待某些条件或操作。与“运行状态”和“睡眠状态”不同,进程处于停止状态时,它已经被显式地暂停执行。
停止状态的概述:
- **停止状态(stopped)**指的是进程被暂停,通常是通过发送信号或者由操作系统控制某个进程的运行。这种状态的进程不会继续执行,直到收到适当的信号来恢复运行。
停止状态的触发条件:
进程进入停止状态通常由以下原因触发:
-
用户发送信号:
-
SIGSTOP:这是一个可以让进程进入停止状态的信号,通常是由系统管理员或用户主动发送。使用
kill
命令可以发送此信号,比如:kill -STOP <进程ID>
或者使用更简洁的形式:
kill -19 <进程ID>
19 是SIGSTOP的信号编号,当进程接收到
SIGSTOP
信号时,它将立即进入停止状态,并停止执行。 -
Ctrl+Z:在命令行界面中,用户可以通过按下
Ctrl+Z
来将当前进程暂停。这实际上就是发送了SIGSTOP
信号,令进程进入停止状态。
-
-
调试器控制:
- 调试器(如 gdb):当你使用调试工具进行调试时,调试器会将目标进程暂停,以便开发者检查和修改程序的状态。调试器会发送
SIGSTOP
信号,进程因此进入停止状态。
- 调试器(如 gdb):当你使用调试工具进行调试时,调试器会将目标进程暂停,以便开发者检查和修改程序的状态。调试器会发送
-
父进程发送信号:
- 父进程有时会主动暂停子进程,特别是在进程管理、协调或者资源控制中。可以通过发送
SIGSTOP
或者使用如waitpid()
的系统调用来暂停进程。
- 父进程有时会主动暂停子进程,特别是在进程管理、协调或者资源控制中。可以通过发送
停止状态的行为:
-
暂停执行:当进程处于停止状态时,它不再消耗CPU资源,不会执行任何指令。它就像是被“冻结”了一样,直到它收到继续执行的信号。
-
可以恢复:停止状态的进程并不是永久停止的,它可以通过发送
SIGCONT
信号恢复运行。比如,如果你想恢复一个被暂停的进程,可以使用以下命令:kill -CONT <进程ID>
或者使用更简洁的形式:
kill -18 <进程ID>
这会使进程从停止状态恢复到运行状态。
三、僵尸进程
僵尸进程(Zombie Process)是指已经完成执行的进程,但它的父进程尚未调用 wait()
系统调用来读取它的退出状态,从而没有将它从进程表中清除掉的进程。换句话说,虽然这些进程已经终止,但它们依然在进程表中占据一个条目,等待父进程来回收它们。
3.1 僵尸进程的形成原因:
每个进程在结束时,操作系统会为它保留一定的资源,以便父进程获取子进程的退出状态。如果父进程没有及时通过 wait()
或 waitpid()
来获取这个退出状态,操作系统会将该子进程的状态保留下来,形成一个僵尸进程。
3.2 僵尸进程的特点:
-
状态为“Z”:在
ps
或top
命令的输出中,僵尸进程的状态通常显示为Z
(Zombie)。int main() { pid_t id = fork(); if(id == 0) { int cnt = 5; while(cnt) { printf("I am child, pid: %d,ppid: %d\n",getpid(),getppid(),cnt); sleep(1); cnt--; } _exit(0); } else {while(1){printf("I am father, pid: %d,ppid: %d\n",getpid(),getppid());sleep(1);}}return 0; }
上面这段代码在
myproc
进程中通过调用fork
接口创建了一个子进程,子进程在执行完五次打印后就会被终止掉,其中的exit
函数就是用来终止一个进程,父进程将一直运行。 -
占用进程表项:虽然已经结束执行,但它仍占据一个进程表中的位置。操作系统不会立即回收这些资源,直到父进程调用
wait()
来收集子进程的退出信息。 -
进程号和父进程号:每个僵尸进程都有一个进程ID(PID),但是它不再占用 CPU 时间。它依然保持父进程ID(PPID),直到父进程回收它。
3.3 如何避免僵尸进程:
- 父进程调用
wait()
:这是最常见的清除僵尸进程的方式。父进程需要在子进程结束时调用wait()
来获取子进程的退出状态,从而让操作系统回收相关资源。 - 父进程退出时:如果父进程退出,而没有处理子进程的退出状态,操作系统会将这些孤儿进程的父进程指向
init
进程(PID 1)。init
进程会定期清理这些孤儿进程,防止它们成为僵尸进程。
四、孤儿进程
孤儿进程(Orphan Process)是指其父进程已经终止或退出,但该进程本身依然在运行的进程。孤儿进程并不会因为父进程的死亡而被立刻销毁,它会被操作系统自动收养,由系统中的另一个进程接管,通常是 init
进程(PID 1)。
4.1 孤儿进程的形成原因:
孤儿进程的形成是因为进程的父进程在子进程仍在运行时结束。例如,父进程可能因某些原因(如崩溃、被杀死、退出)而提前终止,这时子进程还在继续运行,就成为了孤儿进程。
4.2 孤儿进程的处理:
操作系统会将孤儿进程的父进程ID(PPID)更改为 init
进程(PID 1)。init
进程会定期检查并回收所有孤儿进程的退出状态,确保它们能被清理,避免成为僵尸进程。
init
进程的作用:init
是系统中的第一个进程,它负责管理系统的启动和关闭。如果一个进程成为孤儿进程,init
会接管并负责清理该进程。它通过调用wait()
系统调用获取孤儿进程的退出状态,防止它们成为僵尸进程。
4.3 孤儿进程的特点:
- 父进程死亡:孤儿进程的父进程在它还在运行时已经退出或崩溃。
- PID变更:当一个进程变成孤儿进程时,它的父进程ID(PPID)会被更新为
init
进程的PID(通常是 1)。 - 仍然运行:尽管父进程已结束,孤儿进程通常会继续运行,直到它完成任务或退出。
4.4 孤儿进程与僵尸进程的区别:
- 孤儿进程:父进程已经结束,子进程仍在运行。孤儿进程最终会被
init
进程收养并清理。 - 僵尸进程:子进程已经结束,但其父进程没有及时回收子进程的退出状态,导致进程表中保留了该进程的条目,直到父进程清理该退出状态。
结语
理解Linux中的进程状态,特别是僵尸进程与孤儿进程的处理,不仅有助于提升你对操作系统内部机制的理解,也为实际工作中的系统管理和性能优化提供了有力支持。虽然僵尸进程和孤儿进程看似对系统运行影响不大,但它们的存在和处理方式往往反映了系统资源的有效管理和调度。在实际的开发与运维过程中,掌握如何发现、避免和清理这些进程,能有效提升系统的稳定性和响应速度。希望本文对Linux进程状态的详细分析,能够帮助你更好地理解Linux系统的运行原理,为后续的调优和故障排查提供实用的指导。
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,17的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是17前进的动力!