目录
进程状态
进程的先描述,再组织 戳链接( ̄︶ ̄)↗https://blog.youkuaiyun.com/qq_41071068/article/details/103213364
在进程的组织中我们以学校组织管理学生为例子. 那么 在学校中, 学生有着不同的状态, 比如说, 休学, 请假, 毕业, 退学, 学业警
告等各种表示学生学习生活状态的信息 .进程就像学校里的学生一样, 也有着不同的状态. 我们来看 内核原码中对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-- 运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
- S-- 睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep)
- D-- 磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束.
- T-- 停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
- X-- 死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
- Z-- 僵尸状态 (zombie) : 已经死了, 依然占着某些资源
R-- 运行状态
只有该状态下的进程才可能在CPU中执行, 为什么说是可能呢? 因为在Linux下, 正在运行和就绪(在运行队列中, 拿到时间片就能
运行)的进程都视作运行状态. Linux 下 R 状态的进程的task_struct结构 (PCB) 被放入对应的CPU的可执行队列中(一个进程最多
只能出现在一个CPU的可执行队列中). 进程调度器的任务就是从各个CPU的可执行队列中分别选择一个进程在该CPU上行 .
如下面代码运行会进入死循环, 会一直占用CPU, 处于R状态
#include<unistd.h>
int main(){
while(1);
return 0;
}
S-- 睡眠状态(可中断睡眠状态)
处于这个状态的进程因为等待某个事件的发生, 比如被wait()阻塞,而被挂起. 这些进程的task_struct被放入对应事件的等待
队列中. 当等待的事件发生时(由外部中断触发、或由其他进程触发),对应的等待队列中的这个睡眠状态的进程被唤醒.
通过ps命令我们会看到,一般情况下,进程列表中的绝大多数进程都处于S 状态(除非机器正处于高负荷状态下. 毕竟CPU就那
么几个,而进程动辄就是几十上百个,如果不是绝大多数进程都在睡眠,那对于CPU来说, CPU简直太难了 .
如下代码, 子进程由于sleep进入S状态, 父进程由于wait阻塞等待子进程退出, 也处于S状态
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
int main(){
pid_t pid = fork();
if(pid == 0){
sleep(30);
_exit(0);
}
wait(NULL);
return 0;
}
D-- 磁盘休眠状态(不可中断睡眠)
与S状态类似, D状态进程也处于 "睡眠状态", 但是此刻的睡眠时不可中断的. 不可中断,指的并不是CPU不响应外部硬件的中
断,而是指进程不响应异步信号 . 绝大多数情况下,进程处在睡眠状态时,总是应该能够响应异步信号的。否则你将惊奇的发
现,kill -9竟然杀不死一个正在睡眠的进程了!于是我们也很好理解,为什么ps命令看到的进程几乎不会出现D 状态,而总是S状
态. 而 D 状态存在的意义就在于,内核的某些处理流程是不能被打断的. 如果响应异步信号,程序的执行流程中就会被插入一段
用于处理异步信号的流程(这个插入流程可能只存在于内核态,也可能延伸到用户态),于是原有的流程被中断了. 在进程对某
些硬件进行操作时(比如进程调用read系统调用对某个设备文件进行读操作,而read系统调用最终执行到对应设备驱动的代码,
并与对应的物理设备进行交互), 可能需要使用 D 状态对进程进行保护,以避免进程与设备交互的过程被打断,造成设备陷入不
可控的状态 . 这种情况下的 D 状态总是非常短暂的,通过ps命令基本上不可能捕捉到 .
很难捕捉, 但还是可以捕捉的, 让grep 在根目录下搜索字符, grep会对磁盘进行扫描, 即处于不可中断的D状态, 直到IO结束.
T-- 停止状态& t-- (跟踪状态)
向进程发送一个SIGSTOP(停止信号)信号,它就会因响应信号而进入T状态(除非该进程本身处于 D 状态而不响应信号 ).
(SIGSTOP与SIGKILL(无条件终止)信号一样,是非强制的. 不允许用户进程通过signal系统的系统调用重新设置对应的信号处理
函数)向进程发送一个SIGCONT(继续信号)信号,可以让其从 T 状态恢复到 R 状态。
当进程正在被跟踪时,它处于 t 这个特殊的状态。“正在被跟踪”指的是进程暂停下来,等待跟踪它的进程对它进行操作。比如在
gdb中对被跟踪的进程下一个断点,进程在断点处停下来的时候就处于 t 状态。而在其他时候,被跟踪的进程还是处于前面提到
的那些状态。对于进程本身来说,T 和 t 状态很类似,都是表示进程暂停下来。而 t 状态相当于在 T 之上多了一层保护,处于 t
状态的进程不能响应SIGCONT信号而被唤醒. 只能等到调试进程通过ptrace系统调用执行PTRACE_CONT、PTRACE_DETACH
等操作 (通过ptrace系统调用的参数指定操作), 或调试进程退出,被调试的进程才能恢复 R 状态 .
如下代码:
#include<unistd.h>
int main(){
while(1);
return 0;
}
进入死循环后是 R 状态 , 用 kill -STOP PID 来暂停, 如下
如下代码
#include<stdio.h>
int main(){
int flag = 10;
if(flag == 10){
flag = 0;
}
return 0;
}
当执行到断点暂停, 进程此时处于 t 状态
X-- 死亡状态(退出状态)
进程即将被销毁时的状态, 所以X状态是非常短暂的,几乎不可能通过ps命令捕捉到 .
Z-- 僵死状态&僵尸进程
在进程退出过程中, 进程占有的所有资源将被回收, 除了task_struct结构 (以及少数资源) 以外不立即释放.
但当一个进程退出, 但其所占用的资源没有释放回收时, 这个进程就变成了僵尸进程, 处于僵死状态.
例如当子进程先于父进程退出时, 为了保存自身的退出状态(退出码或异常信号), task_struct 以及少数资源不会立即释放, 一直等
待父进程接收其退出状态, 这时操作系统会通知父进程获取子进程的退出状态, 当父进程获取到子进程的退出状态后, 释放子进程
未释放完的资源. 但若父进程没有关注到子进程退出, 一直没有获取子进程退出状态. 这时, 子进程就成为了一个只有着一个空
task_struct(只保存着退出状态, 以及一些统计信息). 而没有真正的灵魂, "有的进程活着, 但它已经死了", 此时这个子进程就处于僵
死状态.
当然,内核也可以将这些信息保存在别的地方,而将task_struct释放掉,以节省一些空间。但是使用task_struct结构更为方
便,因为内核中已经建立了从pid到task_struct查找关系,还有进程间的父子关系。释放掉task_struct,则需要建立一些新的数据
结构,以便让父进程找到它的子进程的退出信息。父进程可以通过wait系列的系统调用(如wait4,waitid)来等待某个或某些子进
程的退出,并获取它的退出信息. 然后wait系列的系统调用会顺便将子进程的尸体(task_struct)也释放掉. 前面说到, 子进程在退出
时操作系统会通知父进程获取退出码, 即内核会给其父进程发送一个信号,通知父进程来收尸. 这个信号默认是SIGCHLD,但是
在通过clone系统调用(fork()和vfork()就是封装了clone)创建子进程时,可以设置这个信号 .
僵尸进程
处于僵死状态的进程称之为僵尸进程.
下面代码
#include <stdio.h>
#include <stdlib.h>
#include<unistd.h>
int main() {
pid_t pid = fork();
if(pid < 0){
perror("fork");
return 1;
}
else if(pid == 0){
printf("child[%d] is begin Z...\n", getpid());
sleep(5);
exit(EXIT_SUCCESS);
}
else{
printf("parent[%d] is sleeping...\n", getpid());
sleep(10);
}
return 0;
}
可以看到, 在父进程处于S状态时, 没有获取子进程退出状态, 子进程就变成了僵尸进程.
僵尸进程危害
- 子进程先于父进程终止,而父进程又没有调用wait或waitpid, 此种情况子进程进入僵死状态,并且会一直保持下去直到系统重启. 内核只保存进程的一些必要信息在task_struct中以备父进程所需.
- 那一个父进程创建了很多子进程,要是都不回收,就会造成内存资源的浪费, 因为task_struct(PCB)数据结构对象本身就要占用内存,要在内存的某个位置进行开辟空间, 这样就会造成资源泄漏.
- Linux操作系统最大进程程数是有限制的, 如果僵尸进程过多, 会导致进程数过多, 创建不了新的进程.
如何避免僵尸进程
- 让要退出的进程的父进程用wait()阻塞等待子进程退出并回收, 或用waitpid() 隔段时间来查询子进程是否结束并回收 .
- 采用信号SIGCHLD通知处理,并在信号处理程序中调用wait()函数
具体方法 戳链接( ̄︶ ̄)↗https://blog.youkuaiyun.com/qq_41071068/article/details/103659853 -
让僵尸进程变成孤儿进程,由init进程回收,就是让父进程先退出
wait()和waitpid()详解在另一篇中,戳链接( ̄︶ ̄)↗https://blog.youkuaiyun.com/qq_41071068/article/details/103302883
还是上面的代码. 稍作修改, 如下 :
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include<sys/wait.h>
int main() {
pid_t pid = fork();
if(pid < 0){
perror("fork");
return 1;
}
else if(pid == 0){
sleep(5);
exit(EXIT_SUCCESS);
}
printf("父进程被wait()阻塞, 等待子进程退出并回收\n");
wait(NULL);
return 0;
}
孤儿进程
那么如果父进程先退出了呢, 谁又来给子进程“收尸”?当一个父进程进程退出的时候,会将它的所有子进程都托管给别的进程(使
之成为别的进程的子进程. 托管给谁呢?可能是退出进程所在进程组的下一个进程 (如果存在的话 ), 或者是1号进程。所以每个进
程每时每刻都有父进程存在. 除非它是1号进程.
1号进程: pid为1的进程, 又称init进程. linux系统启动后, 第一个被创建的用户态进程就是init进程.
它有两项使命:
1、执行系统初始化脚本,创建一系列的进程(它们都是init进程的子孙)
2、在一个死循环中等待其子进程的退出事件,并调用waitid系统调用来完成“收尸”工作 ;
init进程不会被暂停, 也不会被杀死(这是由内核来保证的). 它在等待子进程退出的过程中处于S状态, “收尸”过程
中则处于R状态.
如下代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
pid_t pid = fork();
if(pid < 0){
perror("fork");
exit(-1);
}
else if(pid == 0){
printf("子进程睡一会\n");
sleep(10);
printf("子进程退出\n");
}
else{
printf("父进程先走一步\n");
exit(0);
}
return 0;
}
在子进程睡眠的这10秒内, 可以看到子进程被1号进程收养, 子进程退出后, 由1号进程回收.