运行,阻塞,挂起
运行 (Running)
当一个进程处于运行状态时,它正在使用CPU执行指令。进程在以下两种情况下可能被认为是运行状态:
- 实际运行(Running on CPU): 进程当前正在CPU上执行。
- 可运行(Runnable): 进程准备好运行,只是在等待CPU资源。
在任务管理器中,处于运行状态的进程通常会显示为R
。
阻塞 (Blocked)
当一个进程被阻塞时,它正在等待某些事件完成,无法继续执行。阻塞通常发生在进程需要等待某种资源的情况,例如:
- 等待I/O操作完成(如读取文件或网络数据)
- 等待某个信号或条件变量
在任务管理器中,阻塞状态的进程通常会显示为D
(不可中断的睡眠)或S
(可中断的睡眠)。
挂起 (Suspended)
挂起状态表示进程已被暂停,暂时停止了运行。挂起可以通过以下方式发生:
- 显式挂起(Suspended by signal): 进程收到一个挂起信号(如
SIGSTOP
),并停止执行,直到收到继续信号(如SIGCONT
)。 - 后台挂起(Suspended by job control): 用户在shell中使用命令(如
Ctrl+Z
)将前台进程挂起并转入后台。
在任务管理器中,挂起状态的进程通常会显示为T
(stopped by job control signal)。
总结
- 运行(Running): 进程正在使用CPU或准备使用CPU。
- 阻塞(Blocked): 进程在等待某个事件或资源。
- 挂起(Suspended): 进程已被暂停,等待继续执行的信号。
进程的状态与切换
-
运行(Running):
R (Running): 进程正在CPU上运行或准备运行。 -
可中断睡眠(Interruptible Sleep):
S (Sleeping): 进程正在等待某个事件(如I/O操作)完成,可以被信号中断唤醒。 -
不可中断睡眠(Uninterruptible Sleep):
D (Uninterruptible sleep): 进程在等待某个无法中断的事件(如设备I/O操作)完成,通常不会响应信号。 -
停止(Stopped):
T (Stopped): 进程已被停止,可以由用户或调试器发出信号(如SIGSTOP
)造成。 -
僵尸(Zombie):
Z (Zombie): 进程已终止,但其进程表项仍然保留,等待父进程读取其退出状态。 -
挂起(Suspended):
挂起状态是停止状态的一种表现,通常是通过Ctrl+Z
或SIGSTOP
信号实现的。
进程状态切换的方法
进程状态之间的切换是由内核根据进程的行为和系统事件进行管理的。常见的状态切换及其触发条件如下:
-
从运行(Running)到可中断睡眠(Sleeping):
- 进程执行阻塞I/O操作,如读写文件,等待网络数据等。
- 调用系统调用如
sleep()
,select()
,poll()
或等待某个事件。
-
从可中断睡眠(Sleeping)到运行(Running):
- I/O操作完成或事件发生,唤醒等待的进程。
- 进程收到一个信号,导致其从睡眠状态被中断。
-
从运行(Running)到不可中断睡眠(Uninterruptible sleep):
- 进程执行某些不可中断的系统调用或等待硬件设备响应。
-
从不可中断睡眠(Uninterruptible sleep)到运行(Running):
- 不可中断的系统调用或硬件设备操作完成,进程重新变为可运行。
-
从运行(Running)到停止(Stopped):
- 进程收到
SIGSTOP
,SIGTSTP
(由Ctrl+Z
触发)等信号。 - 用户在调试过程中手动暂停进程。
- 进程收到
-
从停止(Stopped)到运行(Running):
- 进程收到
SIGCONT
信号,继续执行。
- 进程收到
-
从运行(Running)到僵尸(Zombie):
- 进程正常退出(调用
exit()
),但其父进程尚未调用wait()
或waitpid()
读取其退出状态。
- 进程正常退出(调用
-
从僵尸(Zombie)到终止(Terminated):
- 父进程调用
wait()
或waitpid()
读取僵尸进程的退出状态后,僵尸进程彻底从系统中移除。
- 父进程调用
僵尸进程
什么是僵尸进程?
僵尸进程是已经终止执行但仍然在进程表中保留的进程。它保留在进程表中的原因是为了让其父进程读取它的退出状态。这种状态的进程不会占用系统资源(如CPU和内存),但会占用一个进程表项。
僵尸进程的产生
当一个进程结束时(通过调用exit()
或返回main()
函数),操作系统会清理它所占用的大部分资源,但仍会保留一个记录,包括进程的退出状态。这是因为Unix-like系统采用了父子进程关系的机制,父进程需要通过调用wait()
或waitpid()
系统调用来读取子进程的退出状态。
具体步骤如下:
- 子进程终止: 子进程调用
exit()
终止运行。 - 内核保留进程信息: 内核会保留子进程的退出状态和其他信息,以便父进程读取。
- 子进程进入僵尸状态: 子进程的状态变为僵尸状态,直到父进程读取它的退出状态。
- 父进程读取退出状态: 父进程调用
wait()
或waitpid()
读取子进程的退出状态,内核随即清理僵尸进程的剩余信息,从进程表中移除该进程。
为什么会有僵尸进程?
僵尸进程存在的主要原因是确保父进程能够得到子进程的退出信息。这对于进程间通信和进程管理非常重要。通过这种机制,父进程可以知道子进程是正常终止还是由于错误终止,并可以基于此信息进行相应的处理。
僵尸进程的问题
虽然僵尸进程本身不占用大量系统资源,但大量的僵尸进程会导致进程表项耗尽,从而阻止系统创建新进程。因此,管理僵尸进程非常重要。
如何处理僵尸进程?
- 确保父进程读取子进程的退出状态: 父进程应该使用
wait()
或waitpid()
系统调用来处理子进程的退出状态。如果父进程没有执行这一步,子进程就会变成僵尸进程。 - 孤儿进程和init进程: 如果父进程终止而没有处理子进程的退出状态,子进程会成为孤儿进程。孤儿进程会被init进程(PID为1)接管,init进程会自动处理这些子进程的退出状态,从而避免僵尸进程的产生。
- 信号处理: 可以通过设置父进程的信号处理函数,当子进程终止时捕获
SIGCHLD
信号并调用waitpid()
来处理退出状态。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>void handle_sigchld(int sig) {// 使用waitpid处理所有已终止的子进程while (waitpid(-1, NULL, WNOHANG) > 0) {// Nothing to do here}
}int main() {signal(SIGCHLD, handle_sigchld); // 注册SIGCHLD信号处理程序pid_t pid = fork();if (pid < 0) {perror("fork failed");exit(1);} else if (pid == 0) {// 子进程printf("Child process: %d\n", getpid());exit(0);} else {// 父进程printf("Parent process: %d\n", getpid());sleep(10); // 让父进程等待足够长的时间}return 0;
}
孤儿进程
孤儿进程(Orphan Process)是指其父进程已经终止,而它自身仍然在运行的进程。当一个进程终止时,其子进程不会自动终止,而是会变成孤儿进程。为了确保系统的稳定性和资源的合理利用,孤儿进程会被系统中的一个特殊进程,即init
进程(PID为1)接管。
孤儿进程的处理
当孤儿进程被init
进程接管后,init
进程会成为这些孤儿进程的新父进程,并负责处理它们的退出状态,避免它们变成僵尸进程。
具体过程
- 父进程终止: 一个进程终止而其子进程仍在运行。
- 孤儿进程产生: 子进程因为其父进程终止而成为孤儿进程。
init
进程接管: 操作系统会将孤儿进程的父进程重新设置为init
进程。init
进程处理: 当这些孤儿进程终止时,init
进程会调用wait()
或waitpid()
读取它们的退出状态,从而防止它们变成僵尸进程。
示例
下面是一个简单的C语言示例,演示了孤儿进程的产生过程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>int main() {pid_t pid = fork();if (pid < 0) {// fork失败perror("fork failed");exit(1);} else if (pid == 0) {// 子进程printf("Child process, PID: %d, Parent PID: %d\n", getpid(), getppid());sleep(5); // 保持子进程运行一段时间,确保父进程先终止printf("Child process after sleep, PID: %d, New Parent PID: %d\n", getpid(), getppid());exit(0);} else {// 父进程printf("Parent process, PID: %d\n", getpid());exit(0); // 父进程立即终止}return 0;
}
在这个示例中:
- 父进程创建一个子进程。
- 父进程终止,而子进程保持运行。
- 子进程在睡眠5秒后醒来,并输出其新的父进程PID,此时父进程已经是
init
进程。输出示例:Parent process, PID: 12345 Child process, PID: 12346, Parent PID: 12345 Child process after sleep, PID: 12346, New Parent PID: 1
在输出中可以看到,子进程最初的父进程PID是原父进程的PID,当原父进程终止后,子进程的父进程PID变为1,即
init
进程。