进程状态

  • 就绪态:万事俱备,只欠东风,这里的东风指的是CPU
  • 运行态:指的是正在干活的进程
  • 浅度睡眠(S):可中断睡眠,可被信号唤醒:收到信号(如SIGINT、SIGTERM)会立即唤醒
    • 常见场景:
      • 等待用户输入(read())
      • 等待网络数据(recv())
      • sleep()函数调用
      • 等待子进程结束(wait())
  • 深度睡眠(D):不可中断睡眠,不能被信号唤醒,即使是kill -9也无法终止,通常很短暂,但可能长时间阻塞
    • 常见场景:
      • 磁盘I/O操作
      • 某些网络文件系统操作
      • 等待某些内核操作完成
特性 浅度睡眠 (S) 深度睡眠 (D)
状态标志 S D
可中断性 ✓ 可被信号中断 ✗ 不可被信号中断
kill -9 可以杀死 无法立即杀死
常见原因 等待I/O、sleep、wait 磁盘I/O、硬件操作
持续时间 通常较短 可能很长(依赖硬件)
系统负载 不计入负载 会计入负载平均
解决方法 发信号、等待 修复硬件/重启
  • 停止态:进程处于暂停状态,被称为停止态
  • 僵尸态父进程还没执行完毕,子进程提前退出了,这种情况下会进入僵尸态。僵尸态会释放绝大部分资源,但在内核会保留一条用于让父进程 wait() 的记录

进程状态实验

  • 快捷键:
    • Ctrl+c: 终止当前运行的前台进程
    • Ctrl+z: 停止当前运行的前台进程
  • 命令:
    • fg命令:将进程恢复到前台继续运行
    • bg命令:将进程恢复到后台继续运行
第一步:准备一个简单的C程序
  • 代码如下:
#include <stdio.h>
void delay(int i) {
    int t;
    while(i--) {
        t = 1000*1000;
        while(t--);
    }
}
int main(int argc, const char *argv[])
{
    while(1) {
        printf("1"); 
        fflush(stdout);
        delay(1000);
    }
    return 0;
}
  • gcc编译生成一个test的可执行程序文件:
gcc test.c -o test -Wall
第二步:运行test,并监控这个进程
  • 可以后台方式运行:
$ ./test &
[1] 3351
  • 另外打开一下终端,运行top -p 3351(3351进程号)
  • 调整窗口后看到类似下面的界面:

第三步:观察进程的运行状态
  • fg将进程恢复到前台继续运行,Ctrl+z停止进程的运行,此是看到的应该是进程状态应该是T

备注:Ctrl+z无法停止后台运行的程序

  • 再将输入fg命令将进程恢复到前台继续运行,输入Ctrl+c终止进程,此时进程消失

top命令可以按q退出

第四步:准备另一下进程观察阻塞态的进程
  • 刚才的代码修改为:
#include <unistd.h>
int main(int argc, const char *argv[])
{
    while(1)sleep(1);
    return 0;
}
  • 再次编译:
gcc test.c -o test -Wall
  • 后台运行,并且用top命令进行监控
$ ./test &
[1] 3410
$ top -p 3410

  • 注意进程的状态是S

僵尸进程

  • 一个已经终止执行,但其退出状态还没有被父进程读取的进程,即父进程没调用 wait()
  • 就像人死了但没下葬z
正常进程 → 结束执行 → 变成僵尸 → 父进程读取状态 → 完全消失
        (已死)      (没埋)     (收尸)        (埋了)
  • 制造僵尸进程
$ ./test &
[1] 3809
$ ps
    PID TTY          TIME CMD
   2507 pts/0    00:00:00 bash
   3809 pts/0    00:00:00 test
   3810 pts/0    00:00:00 test <defunct>  // 僵尸进程
   3811 pts/0    00:00:00 ps
$ cat test.c 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
    switch( fork() ) {
    case -1:
        perror("fork");
        exit(EXIT_FAILURE);
        break;
    case 0:
        return 1;
        break;
    default:
    }
    while(1) sleep(1);
}

特点

  • 释放了内存
  • 关闭了打开的文件
  • 停止了CPU使用
  • 无法被kill杀死(因为僵尸进程已经死了,无法再"杀死"一次)
  • 但仍保留最小信息:
    • 在进程表中占有一个条目
    • 保留进程ID(PID)
    • 保留退出状态码
    • 保留资源使用统计信息

作用

  • 僵尸进程是一个特性,不是bug
    • 保存退出信息:让父进程知道子进程是怎么结束的
    • 保留PID:防止PID被立即重用导致混乱
    • 资源统计:记录进程的资源使用情况

危害

  • 占用有限的进程号
    • cat /proc/sys/kernel/pid_max
    • 系统最大进程数,通常 32768
    • 如果僵尸进程太多,可能耗尽PID
  • 浪费系统资源
    • 每个僵尸进程占用一个进程表条目
    • 进程表大小有限(通常几千个)
  • 影响系统性能
    • 大量僵尸进程会使 ps、top 等命令变慢
    • 可能影响新进程的创建

wait()函数

函数原型

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);

参数

  • status 包含的信息可以通过宏提取
int status;
pid_t pid = wait(&status);

返回值

  • 成功:
    • 结束的子进程PID (>0)
  • 失败:
    • -1,调用失败设置errno
      • errno == ECHILD // 调用进程没有子进程
      • errno == EINTR // 调用被信号中断

作用

  • 阻塞等待子进程结束
  • 获取子进程退出状态
  • 回收子进程资源(防止僵尸进程)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();
    
    if (pid == 0) {
        // 子进程
        printf("子进程开始工作...\n");
        sleep(3);  // 模拟工作3秒
        printf("子进程结束\n");
        exit(42);  // 以状态码42退出
    } else {
        // 父进程
        printf("父进程等待子进程...\n");
        
        int status;
        pid_t child_pid = wait(&status);  // 阻塞在这里
        
        printf("等到了!子进程 PID=%d 结束\n", child_pid);
        
        if (WIFEXITED(status)) {
            printf("子进程正常退出,退出码: %d\n",  WEXITSTATUS(status));  
        }
    }
    
    return 0;
}

常用状态检查宏

// 判断退出方式
WIFEXITED(status)    // 子进程正常退出返回真
WIFSIGNALED(status)  // 子进程被信号终止返回真
WIFSTOPPED(status)   // 子进程被信号暂停返回真
WIFCONTINUED(status) // 子进程从暂停恢复返回真

// 获取具体信息
WEXITSTATUS(status)  // 获取正常退出时的退出码(0-255)
WTERMSIG(status)     // 获取导致终止的信号编号
WSTOPSIG(status)     // 获取导致暂停的信号编号
WCOREDUMP(status)    // 检查是否生成core dump文件

正常退出

  • WIFEXITED(status)
    • 如果子进程正常终止,则返回 true
  • WEXITSTATUS(status)
    • 返回 Child 的退出状态
    • 包括子节点在调用 exit()_exit() 时指定的 wstatus参数的最低有效 8 位
    • 作为 return 语句的参数指定的返回值
if (WIFEXITED(status)) {
    printf("正常退出,退出码: %d\n", WEXITSTATUS(status));
}

被信号终止

  • WIFSIGNALED(wstatus)
    • 如果子进程被 信号 终止,则返回 true
  • WTERMSIG(wstatus)
    • 返回导致子进程终止的信号的编号
if (WIFSIGNALED(status)) {
    printf("被信号杀死,信号: %d\n", WTERMSIG(status));
    #ifdef WCOREDUMP
    if (WCOREDUMP(status)) {
        printf("产生了core dump文件\n");
    }
    #endif
}
  • WIFSTOPPED(status)
    • 被信号暂停
if (WIFSTOPPED(status)) {
    printf("被信号暂停,信号: %d\n", WSTOPSIG(status));
}
  • WIFCONTINUED(status)
    • 从暂停状态恢复
if (WIFCONTINUED(status)) {
    printf("从暂停状态恢复\n");
}

应用场景

获取子进程退出状态

#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    
    if (pid == 0) {
        // 子进程:模拟不同退出方式
        printf("子进程 PID=%d 启动\n", getpid());
        
        // 测试不同场景(取消注释一个):
        
        // 场景1:正常退出
        // exit(100);
        
        // 场景2:除零错误(产生SIGFPE信号)
        // int a = 1 / 0;
        
        // 场景3:段错误(产生SIGSEGV信号)
        // int *p = NULL;
        // *p = 42;
        
        // 场景4:自己发送信号
        // kill(getpid(), SIGTERM);
        
        sleep(1);
        exit(100);
        
    } else {
        // 父进程
        int status;
        wait(&status);
        
        printf("\n=== 子进程状态分析 ===\n");
        printf("子进程 PID: %d\n", pid);
        
        if (WIFEXITED(status)) {
            printf("退出方式: 正常退出\n");
            printf("退出码: %d\n", WEXITSTATUS(status));
        }
        else if (WIFSIGNALED(status)) {
            printf("退出方式: 被信号终止\n");
            printf("信号编号: %d\n", WTERMSIG(status));
            printf("信号名称: %s\n", strsignal(WTERMSIG(status)));
            
            #ifdef WCOREDUMP
            if (WCOREDUMP(status)) {
                printf("生成core文件: 是\n");
            }
            #endif
        }
        else if (WIFSTOPPED(status)) {
            printf("退出方式: 被信号暂停\n");
            printf("暂停信号: %d\n", WSTOPSIG(status));
        }
    }
    
    return 0;
}

等待多个子进程

#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    int num_children = 5;
    pid_t children[5];
    
    // 创建5个子进程
    for (int i = 0; i < num_children; i++) {
        pid_t pid = fork();
        
        if (pid == 0) {
            // 子进程
            printf("子进程 %d (PID=%d) 开始\n", i, getpid());
            sleep(i + 1);  // 第i个子进程睡眠i+1秒
            printf("子进程 %d 结束\n", i);
            exit(i * 10);  // 返回不同的退出码
        } else {
            // 父进程记录子进程PID
            children[i] = pid;
        }
    }
    
    // 父进程等待所有子进程
    printf("\n父进程 (PID=%d) 开始等待...\n", getpid());
    
    int completed = 0;
    while (completed < num_children) {
        int status;
        pid_t child_pid = wait(&status);  // 等待任意子进程
        
        // 找出是哪个子进程结束了
        for (int i = 0; i < num_children; i++) {
            if (children[i] == child_pid) {
                printf("子进程 %d (PID=%d) 结束,退出码: %d\n",
                       i, child_pid, WEXITSTATUS(status));
                children[i] = -1;  // 标记为已结束
                break;
            }
        }
        
        completed++;
    }
    
    printf("所有子进程都已回收\n");
    return 0;
}

等待全部子进程结束

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

int main(int argc, const char *argv[])
{
	int time = 4; //进程数量,可以随意调整
	pid_t pid = 1;
	while(time-- && pid > 0) {
		switch( (pid = fork()) ){
    		case -1:
    			perror("fork");
    			exit(EXIT_FAILURE);
    			break;
    		case 0:
    			printf("做点别的事情\n");
    			sleep(1);
    			exit(EXIT_FAILURE);
    			break;
    		default:
		}
	}
	//....
	while( wait(NULL) > 0) {
		perror("wait");
	}
	return 0;
}

waitpid()函数

函数原型

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *wstatus, int options);

参数

  • pid:
    • 0,等待进程ID为pid的特定子进程

    • 0,等待与调用进程同一进程组的任意子进程
    • -1,等待任意子进程,等价于 wait(&status)
    • < -1,等待进程组ID等于pid的所有子进程
  • wstatus:
    • 同wait函数
// 和wait()的status参数完全一样
int status;
pid_t result = waitpid(pid, &status, options);

// 使用相同的宏提取信息
if (WIFEXITED(status)) {
    printf("退出码: %d\n", WEXITSTATUS(status));
}
  • options:
    • 0 ,默认选项,阻塞等待,子进程不结束就卡住
    • WNOHANG非阻塞,立即返回,不等待
    • WUNTRACED返回停止的子进程状态信息
    • WCONTINUED返回恢复执行的子进程的状态信息
// 阻塞等待任意子进程结束(默认)
waitpid(-1, &status, 0);

// 非阻塞检查(立即返回)
waitpid(-1, &status, WNOHANG);

// 非阻塞,同时监控停止和恢复状态
waitpid(-1, &status, WNOHANG | WUNTRACED | WCONTINUED);

返回值

  • 0是结束的子进程PID

  • 0,仅当options包含WNOHANG,没有子进程状态改变
  • -1,调用失败设置errno
    • ECHILD // 指定的pid没有对应的子进程
    • EINTR // 调用被信号中断
    • EINVAL // 无效的options参数
int status;
pid_t result;

// 场景1:阻塞等待,等到子进程结束
result = waitpid(pid, &status, 0);
// result > 0: 子进程PID
// result = -1: 错误

// 场景2:非阻塞检查
result = waitpid(pid, &status, WNOHANG);
// result > 0: 有子进程结束,返回PID
// result = 0: 子进程还在运行,没结束
// result = -1: 错误

// 场景3:等待特定进程组
result = waitpid(-1234, &status, 0);  // 等待PGID=1234的所有子进程
// result > 0: 返回结束的子进程PID(可能是组内任何一个)

作用

  • 加强版wait()函数
  • 等待指定子进程
  • 非阻塞等待(WNOHANG选项)
  • 等待进程组(pid < -1)

wait(&wstatus)的调用与 waitpid(-1, &wstatus, 0) 等价

应用场景

等待指定子进程

#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    pid_t child1, child2;
    
    child1 = fork();
    if (child1 == 0) {
        // 第一个子进程
        printf("子进程1 (PID=%d) 启动\n", getpid());
        sleep(2);
        exit(10);
    }
    
    child2 = fork();
    if (child2 == 0) {
        // 第二个子进程
        printf("子进程2 (PID=%d) 启动\n", getpid());
        sleep(4);
        exit(20);
    }
    
    // 父进程:先等待子进程2
    printf("父进程先等待子进程2 (PID=%d)\n", child2);
    
    int status;
    pid_t result = waitpid(child2, &status, 0);
    
    if (result == child2) {
        printf("子进程2 结束,退出码: %d\n", WEXITSTATUS(status));
    }
    
    // 再等待子进程1
    printf("父进程再等待子进程1 (PID=%d)\n", child1);
    waitpid(child1, &status, 0);
    printf("子进程1 结束,退出码: %d\n", WEXITSTATUS(status));
    
    return 0;
}

等待任意子进程

// 等待任意子进程,等价于 wait(&status)
waitpid(-1, &status, 0);

非阻塞等待

#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    
    if (pid == 0) {
        // 子进程长时间工作
        printf("子进程开始长时间工作...\n");
        sleep(10);
        printf("子进程结束\n");
        exit(100);
    } else {
        // 父进程:非阻塞轮询
        printf("父进程开始非阻塞等待\n");
        
        int status;
        int attempts = 0;
        
        while (1) {
            pid_t result = waitpid(pid, &status, WNOHANG);
            
            if (result == 0) {
                // 子进程还没结束
                attempts++;
                printf("轮询第 %d 次: 子进程还在运行\n", attempts);
                sleep(1);  // 做其他工作
                
                // 模拟父进程的工作
                if (attempts % 3 == 0) {
                    printf("父进程正在处理其他任务...\n");
                }
            }
            else if (result == pid) {
                // 子进程结束
                printf("子进程结束!退出码: %d\n", 
                       WEXITSTATUS(status));
                printf("总共轮询了 %d 次\n", attempts);
                break;
            }
            else {
                // 错误
                perror("waitpid");
                break;
            }
        }
    }
    
    return 0;
}

检查是否有子进程结束

int status;
pid_t result = waitpid(-1, &status, WNOHANG | WUNTRACED | WCONTINUED);

if (result == 0) {
    // 没有子进程状态改变
} else if (result > 0) {
    if (WIFEXITED(status)) {
        // 子进程正常退出
    } else if (WIFSTOPPED(status)) {
        // 子进程被暂停
    } else if (WIFCONTINUED(status)) {
        // 子进程从暂停恢复
    } else if (WIFSIGNALED(status)) {
        // 子进程被信号杀死
    }
}

处理所有结束的子进程

// 使用循环回收所有僵尸进程
while (waitpid(-1, &status, WNOHANG) > 0) {
    // 处理每个结束的子进程
}

有 fork 就要有 wait,不然会产生僵尸进程

  • wait() 是父进程的"收尸官"
    • 阻塞等待子进程结束
    • 回收资源防止僵尸进程
    • 获取状态了解子进程"死因"
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐