在 Linux 中,通常进程将在下面几种状态之间迁移:
- 可运行状态和运行状态
- 睡眠状态
- 不可中断的睡眠
- 仅能被致命信号中断的睡眠
- 可中断的睡眠
- 停止态
- 僵尸态和死亡态
1 可运行状态和运行状态
均用 TASK_RUNNING 表示:
可运行状态
在等待队列上等待调度器调度来获取 CPU 时间片。
运行状态
已经被调度器选中并分配到 CPU 时间片,正在 CPU 上运行。
2 僵尸态和死亡态
在 linux 系统中,资源是以进程为单位分配的,处于僵尸态的进程在父进程回收资源后进入死亡态。
僵尸态 (TASK_DEAD, EXIT_ZOMBIE)
当进程完成执行或终止时,它会向父进程发送 SIGCHLD 信号并进入僵尸状态。如果父进程没有回收僵尸进程,那么这些进程就会一直占用系统资源。
父进程必须使用 wait() 或 waitpid() 系统调用用来获取子进程的退出状态并回收资源。
死亡态 (TASK_DEAD ,EXIT_DEAD)
父进程调用 wait() 或 waitpid() 系统调用获取了子进程的退出状态并将其系统资源释放,子进程进入死亡态。死亡态的进程在进程表中不再存在。
僵尸态是一个过渡状态,表示进程已经结束但是父进程还没有完全释放它,而死亡态状态则是一个最终状态,表示进程已经完全结束并且被释放。
进程回收子进程资源:
int main()
{
pid_t pid = fork();
if (pid == 0) {
// 子进程执行的代码
return 0;
}
else if (pid > 0) {
// 父进程执行的代码
std::cout << "Parent process starts\n";
int status;
waitpid(pid, &status, 0); // 等待子进程退出,回收其系统资源
if (WIFEXITED(status)) {
std::cout << "Child process exited with status: " << WEXITSTATUS(status) << "\n";
}
std::cout << "Parent process ends\n";
return 0;
}
}
进程回收子线程资源:
void *childThreadFunc(void *arg) {
// 子线程运行
}
int main() {
pthread_t childThread;
pthread_create(&childThread, NULL, childThreadFunc, NULL);
pthread_join(childThread, NULL); // 等待子线程退出,即使回收它的系统资源
return 0;
}
注:
如果调用 pthread_detach() 函数可以将子线程设置为 "DETACHED" 状态后,子线程退出时可以自动回收资源,而不需要使用 pthread_join() 函数等待线程退出。
非 "DETACHED" 状态子线程退出时,即使进程没有调用 pthread_join() 回收其资源,其占用的资源将在进程退出后,在回收其资源时统一回收,但是会导致资源回收不及时。
如果父进程提前子进程退出呢?子进程将变成孤儿进程,由 init 进程统一回收孤儿进程的资源。
3 停止态
TASK_STOPPED
处于可运行状态或运行状态的 task,可以使用 SIGSTOP 或 SIGTSTP 信号将 task 置于停止状态 (T),这两个信号的区别:
- SIGSTOP :强制停止 task 的信号,该 task 不能忽略此信号并将进入停止状态。
- SIGTSTP :也是停止进程的信号,但 task 可以通过信号处理程序来忽略该信号。
在这种状态下,可以通过发送 SIGCONT 信号使进程恢复到可运行状态或运行状态。
4 休眠状态
Linux Task 有三种休眠状态,主要区别在于唤醒的方法,包括:
TASK_INTERRUPTIBLE
将其唤醒再次运行的方法有两种:
- 显式唤醒
- 收到非屏蔽信号
比如 mutex_lock_interruptible() 申请互斥锁失败会导致进程进入该状态。
TASK_UNINTERRUPTIBLE
将其唤醒再次运行的方法只有一种:
- 会忽略所有信号,只能显式唤醒
比如等待 IO 资源或 mutex_lock() 申请互斥锁失败会导致进程进入该状态。
TASK_KILLABLE
将其唤醒再次运行的方法有两种:
- 显式唤醒它
- 致命信号(fatal signals)
比如 mutex_lock_killable() 申请互斥锁失败会导致进程进入该状态。
4.1 为什么存在 TASK_KILLABLE?
//include/linux/sched.h
#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)
TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 睡眠方式都有优点和缺点。
TASK_INTERRUPTIBLE 状态的 Task
优点:
可以更快地响应信号。
缺点:
它使编程更加困难。 使用可中断睡眠的内核代码必须始终检查它是否因信号而醒来:
- 如果是,则清理它正在做的任何事情并将 -EINTR 返回给用户空间;
- 用户空间端也必须意识到系统调用被中断并做出相应的响应;但是并非所有用户空间程序员都以会认真和正确的处理 -EINTR 信号;
TASK_UNINTERRUPTIBLE 状态的 Task
优点:
使睡眠不可中断可以消除“可中断休眠”带来的这些问题,它会忽略所有信号,不会将错误的处理抛到用户空间。
缺点:
代价是不可中断。如果预期的唤醒事件没有出现,该过程将永远等待,并且除了重新启动系统之外,通常没有任何人可以对此做任何事情。这是可怕的、无法杀死的进程的根源,它被 ps 显示为处于“D”状态。
考虑到不可终止进程的高度令人讨厌的性质,人们会认为应该尽可能使用可中断睡眠。这个想法的问题在于,在许多情况下,引入可中断睡眠很可能会导致应用程序错误。正如 Alan Cox 最近指出的那样:
Unix 传统(因此几乎所有应用程序)认为文件存储写入是非信号中断的。更改该保证既不安全也不切实际。
Matthew Wilcox 意识到,如果应用程序无论如何都将被终止,那么许多关于应用程序错误的担忧并不适用。如果系统调用注定永远不会返回到用户空间,开发人员是否考虑过系统调用中断的可能性并不重要。所以Matthew创建了一个新的休眠状态,叫做TASK_KILLABLE;它的行为类似于 TASK_UNINTERRUPTIBLE,除了致命信号(fatal signals)会中断睡眠。
TASK_KILLABLE 带来了一组新的原语,用于等待事件和获取锁:
int wait_event_killable(wait_queue_t queue, condition);
long schedule_timeout_killable(signed long timeout);
int mutex_lock_killable(struct mutex *lock);
int wait_for_completion_killable(struct completion *comp);
int down_killable(struct semaphore *sem);
对于这些函数中的每一个,正常、成功返回的返回值将为零,或者在出现致命信号的情况下为负错误代码。在后一种情况下,内核代码应该清理并返回,使进程能够被终止。
TASK_KILLABLE 补丁已为 2.6.25 内核合并,但这并不意味着无法杀死的进程问题已经消失。内核中(从 2.6.26-rc8 开始)实际使用这种新状态的地方数量非常少——例如,人们不必担心在数手指时会用完手指。 NFS 客户端代码已经转换,这只能是一个可喜的发展。但是 TASK_KILLABLE 的其他用途很少,而且在设备驱动程序中根本没有,这通常是进程陷入困境的地方。
新的 API 在内核中得到广泛使用可能需要一些时间,尤其是当它补充了大多数情况下运行良好的现有功能时。此外,将现有代码大量转换为可终止睡眠的好处并不完全清楚。但几乎可以肯定,内核中的某些地方可以通过此更改得到改进,前提是用户和开发人员能够识别进程挂起(hung)的位置。在新代码中使用 killable sleep 也是有意义的,除非有一些紧迫的理由完全禁止中断。
关于更多 TASK_KILLABLE 的介绍: