孤儿进程、僵尸进程和守护进程

发布于:2025-07-29 ⋅ 阅读:(15) ⋅ 点赞:(0)

孤儿进程

如果父进程先于子进程退出,则子进程成为孤儿进程,此时将自动被PID为1的进程收养, PID为1的进程就成为了这个进程的父进程。

当一个孤儿进程退出以后,它的资源清理会交给它的父进程来处理。

#include <testfun.h>
int main(){
    if(fork() == 0){
        while(1){
            sleep(1);
        }
    }else{
    }
    return 0;
}

ps -elf查看 

再使用 kill -9 <PID>  # 强制终止进程   我这里是kill -9 28667

 僵尸进程

如果子进程先退出,系统不会自动清理掉子进程的环境,而必须由父进程调用wait或waitpid函数来完成清理工作。

如果父进程不做清理工作,则已经退出的子进程将成为僵尸进程(defunct),在系统中如果存在的僵尸进程过多,将会影响系统的性能, 所以必须对僵尸进程进行处理。

当一个进程执行结束时,它会向它的父进程发送一个SIGCHLD/终止信号,从而父进程可以根据子进程的终止情况进行处理。

在父进程处理之前,内核必须要在进程队列当中维持已经终止的子进程的PCB。如果僵尸进程过多,将会占据过多的内核态空间。并且僵尸进程的状态无法转换成其他任何进程状态。

#include <unistd.h>  // 提供 fork() 函数声明

int main() {
    // 调用 fork() 创建子进程
    if (fork() == 0) {  // fork() 返回 0 表示当前是子进程
        // 子进程执行的代码块
        // 这里为空,子进程会直接执行后面的 return 0
    } else {
        // 父进程执行的代码块(fork() 返回子进程的 PID)
        while (1) {  // 无限循环,使父进程持续运行
            // 循环体为空,父进程会一直占用 CPU
        }
    }

    // 子进程会执行到这里(因为 if 块为空)
    // 父进程不会执行到这里(因为 while(1) 是无限循环)
    return 0;  // 子进程正常退出
}

ps -elf查看 

孤儿进程 vs 僵尸进程

类型 定义 如何终止
孤儿进程 父进程已终止,由 init 接管 kill -9 <PID>
僵尸进程 已终止但未被父进程 wait() 需终止其父进程或重启系统

 wait函数

wait函数是一个系统调用函数。

wait函数的作用是等待一个已经退出的子进程,并进行清理回收子进程资源工作。

  1. wait 随机地等待一个已经退出的子进程,并返回该子进程的PID, 并给子进程进程资源清理和回收
  2. wait是一个阻塞函数, wait会一直阻塞直到等到一个结束的子进程, 解除阻塞.(前提是有子进程)(没有子进程的时候, 直接返回-1)

其函数声明如下:

// man 2 wait

#include <sys/types.h>
#include <sys/wait.h>
//wait for process to change state
pid_t wait(int *wstatus);
// 返回值, 成功返回子进程的ID, 失败返回-1

wstatus: 了解

  1. 该参数是一个整型指针, 用来存储进程的退出状态(可以用宏解析)
  2. 这个wstatus的整型内存区域中由两部分组成,其中一些位用来表示退出状态(当正常退出时),而另外一些位用来指示发生异常时的信号编号。
  3. 我们可以通过一些宏检查状态的情况。 (参考: man 2 wait)
说明
WIFEXITED(wstatus) 子进程正常退出的时候返回真,此时可以使用WEXITSTATUS(wstatus),获取子进程的返回情况
WIFSIGNALED(wstatus) 子进程异常退出的时候返回真,此时可以使用 WTERMSIG(wstatus)获取信号编号,可以使用 WCOREDUMP(wstatus)获取是否产生core文件
WIFSTOPPED(wstatus) 子进程暂停的时候返回真,此时可以使用WSTOPSIG(wstatus)获取信号编号
...

EgCode:

#include <testfun.h>  // 自定义头文件(假设包含必要的函数声明)
#include <stdio.h>    // 提供 printf() 函数声明
#include <unistd.h>   // 提供 fork(), sleep(), wait() 等函数声明
#include <sys/wait.h> // 提供 wait() 相关的宏(WIFEXITED等)

int main() {
    // 创建子进程
    if (fork() == 0) {  
        // 子进程执行的代码块(fork()返回0)
        printf("child process \n");  // 打印子进程标识
        sleep(20);                  // 子进程休眠20秒(模拟耗时操作)
        return 100;                 // 子进程退出,返回状态码100
    } else {
        // 父进程执行的代码块(fork()返回子进程PID)
        int status;                // 存储子进程退出状态
        int s_pid = wait(&status);  // 阻塞等待任意子进程结束,并获取其PID和状态
        
        printf("wait child: child pid=%d \n", s_pid);  // 打印被回收的子进程PID
        
        // 判断子进程退出方式
        if (WIFEXITED(status)) {  
            // 子进程正常退出(调用return或exit)
            printf("child status: %d \n", WEXITSTATUS(status));  // 打印子进程退出码(100)
        } else if (WIFSIGNALED(status)) {  
            // 子进程因信号终止(如kill -9)
            printf("child signed: %d \n", WTERMSIG(status));  // 打印终止信号的编号
        }
    }
    
    return 0;  // 父进程退出
}

waitpid函数 

函数 区别
wait(&status) 等待 任意一个子进程 结束。
waitpid(pid, &status, options) 可以指定等待某个子进程(pid),并支持非阻塞模式(WNOHANG)。

waitpid函数也是一个系统调用函数。

waitpid函数的作用是等待一个已经退出的子进程,并进行清理工作。

  1. waitpid 等待一个指定退出的子进程,并返回该子进程的PID
  2. waitpid 是一个阻塞函数

其函数声明如下:

#include <sys/types.h>
#include <sys/wait.h>
// wait for process to change state
pid_t waitpid(
    pid_t pid,      // 指定等待的PID的子进程
    int *wstatus,   // 存储进程的退出状态
    int options     // 修改 waitpid 的行为的选项, 默认0
);
// 返回值:  返回值, 成功返回子进程的ID, 失败返回-1。
// 返回值:  如果在options参数上使用WNOHANG选项,且没有子进程退出:返回0

 pid参数:pid参数可以控制支持更多种模式的等待方式。

表 3. PID数值效果

PID数值 效果
< -1 等待进程PID和pid绝对值的进程
== -1 等待任一个子进程, 等价于wait
== 0 等待同一进程组的任意子进程

什么是进程组 ?

进程组是一个或多个进程的集合:

  1. 每个进程除了是一个单独的进程,还归属于某一个进程组; 所以进程不仅有进程ID, 还有进程组ID
  2. 当使用shell创建进程的时候,除了这个进程被创建,这个进程将会创建一个进程组, 并作为进程组的组长。
  3. 组长的PID就是进程组ID。
  4. 通过fork创建一个子进程时,子进程默认和父进程属于同一个进程组。
  5. 只要进程组当中存在至少一个进程(这个进程即使不是组长),该进程组就存在。
  6. getpgrp()函数获得进程组ID。

options参数:

  1. waitpid是阻塞函数,如果给waitpid 的options参数设置一个名为WNOHANG的宏,则系统调用会变成非阻塞模式。
  2. 如果默认阻塞: 填0。

EgCode:

#include <testfun.h>  // 自定义头文件(假设包含必要的函数声明)
#include <stdio.h>    // 提供 printf() 函数声明
#include <unistd.h>   // 提供 fork(), sleep() 函数声明
#include <sys/wait.h> // 提供 waitpid() 及相关宏定义

int main() {
    // 创建子进程
    if (fork() == 0) {
        // 子进程执行的代码块
        printf("child process \n");  // 打印子进程标识信息
        sleep(20);                  // 子进程休眠20秒(模拟耗时操作)
        return 100;                 // 子进程退出,返回状态码100
    } else {
        // 父进程执行的代码块
        int status;                 // 用于存储子进程退出状态
        /* 两种waitpid调用方式:
         * 1. 阻塞方式(0):父进程会一直等待子进程结束
         * 2. 非阻塞方式(WNOHANG):立即返回,不等待
         */
        // int s_pid = waitpid(-1, &status, WNOHANG); // 非阻塞方式
        int s_pid = waitpid(-1, &status, 0);       // 阻塞方式(常用)
        
        // 判断waitpid的返回结果
        if (s_pid == 0) {
            // 只有在WNOHANG模式下可能返回0,表示没有子进程结束
            printf("no child process end \n");
        } else {
            // 正常获取到结束的子进程
            printf("wait child: child pid=%d \n", s_pid);  // 打印被回收的子进程PID
            
            // 解析子进程退出状态
            if (WIFEXITED(status)) {
                // 子进程正常退出
                printf("child status: %d \n", WEXITSTATUS(status));  // 打印退出码
            } else if (WIFSIGNALED(status)) {
                // 子进程被信号终止
                printf("child signed: %d \n", WTERMSIG(status));    // 打印信号编号
            }
        }
    }
    
    return 0;  // 父进程退出
}

守护进程 

守护进程是 在后台运行的特殊进程,它脱离终端控制,通常用于提供系统服务(如网络、日志、定时任务等)。它没有控制终端(TTY),生命周期长,随系统启动而运行,直到系统关闭才结束。 

    特征 说明
    后台运行 不占用终端,用户无法直接交互(如 sshdnginx)。
    脱离终端控制 不受用户登录/注销影响(即使终端关闭,守护进程仍运行)。
    生命周期长 通常在系统启动时运行,直到系统关闭。
    无控制终端 其 stdin/stdout/stderr 通常重定向到 /dev/null 或日志文件。
    独立会话组 调用 setsid() 创建新会话,脱离原进程组和终端关联。

     守护进程的创建流程

    1. 父进程创建子进程,然后让父进程终止。
    2. 在子进程当中创建新会话。
    3. 修改当前工作目录为根目录。(因为如果使用当前目录, 意味着当前目录活跃, 则当前目录无法在文件系统中卸载; 而根目录所在的文件系统正常来讲是一直挂载的)
    4. 重设文件权限掩码为0,避免创建文件的权限受限。
    5. 关闭不需要的文件描述符,比如0、1、2。
    #include <unistd.h>    // 提供 fork(), setsid(), chdir(), close(), sleep() 等函数
    #include <sys/stat.h>  // 提供 umask() 函数
    
    int main(int argc, char* argv[]) {
        // 创建子进程
        if (fork() == 0) {  // 子进程进入该分支(fork()返回0)
            // 1. 创建新会话,脱离终端控制
            setsid();  // 使子进程成为新会话的领头进程,脱离原终端
            
            // 2. 更改工作目录到根目录
            chdir("/");  // 避免守护进程阻止文件系统卸载
            
            // 3. 重设文件权限掩码
            umask(0);  // 确保守护进程创建文件时有最大权限(模式0777)
            
            // 4. 关闭所有文件描述符(防止资源泄漏)
            for (int i = 0; i < 1024; i++) {  // 遍历可能的文件描述符
                close(i);  // 关闭每个描述符(包括标准输入/输出/错误)
            }
            
            // 5. 守护进程主循环
            while (1) {  // 无限循环保持守护进程运行
                sleep(1);  // 避免CPU占用过高(实际应用会替换为任务逻辑)
            }
        }
        
        // 父进程直接退出(子进程由init接管)
        return 0;  
    }

    补充:job、bg、fg

    关键区别总结

    特性 jobs bg  fg 
    主要作用 查看作业状态 恢复暂停的任务到后台back 将任务调回前台front
    依赖场景 需先有后台/暂停的作业 需先用 Ctrl+Z 暂停任务 需先用 & 或 Ctrl+Z
    输出示例 [1] Running sleep 100 & [1] + Continued vim & 前台占用终端(如 vim

     +:最近一个被放入后台的作业。(fg  %+=fg)

    -:倒数第二个被放入后台的作业。

    &: 表示放到后台运行

    快捷键 功能 信号 进程状态 恢复方式 适用场景
    Ctrl+C 强制终止当前前台进程 SIGINT 进程直接退出 无法恢复,需重新启动 想立即结束任务(如卡死的程序)
    Ctrl+Z 暂停当前前台进程 SIGTSTP 进程暂停(Stopped) 可通过 fg 或 bg 恢复 临时释放终端(如暂停 vim 去执行其他命令)

    符号 含义 等价命令
    %1 作业编号 1 fg %1bg %1
    %+ 或 %% 最近操作的任务(带 + 标记) fg(不指定编号)
    %- 倒数第二个操作的任务 fg %-

    fg %1:将作业 1 调回前台运行。

    bg %1:将作业 1 在后台继续运行(针对已暂停的任务)。 

    补充:Kill 

    kill 是 Linux/Unix 系统中用于 终止进程 的核心命令,通过向目标进程发送信号(Signal)来实现控制。

    kill命令可以用来给指定的进程发送信号。

    1. 通常用户经常会从终端启动shell再启动进程,当进程正在运行时,它可以接受一些键盘发送的信号:比如ctrl+c表示终止信号, ctrl+z表示暂停信号。这种可以直接接受键盘信号的状态被称为前台,否则称为后台。
    2. 当进程处于后台的时候,只能通过kill命令发送信号 给它。 
    kill [选项] <PID或作业号>  # 通过进程ID或作业号终止
    kill -<信号名或编号> <PID> # 发送指定信号
    信号编号 信号名 作用 是否可捕获
    1 SIGHUP 终端挂断(重启配置)
    2 SIGINT 键盘中断(Ctrl+C)
    9 SIGKILL 强制终止(不可捕获/忽略)
    15 SIGTERM 默认终止信号(允许清理)
    19 SIGSTOP 暂停进程(Ctrl+Z)
    kill -15 1234      # 先尝试正常退出(如保存数据)
    kill -9 1234       # 若无效,强制终止
    kill -9 -1        # 终止当前用户的所有进程(危险!)
    kill %1           # 终止作业编号为1的任务(Shell作业控制)


    网站公告

    今日签到

    点亮在社区的每一天
    去签到