目录

🧠 一、fork() 是什么?

⚙️ 二、函数原型

🧩 三、fork() 的执行流程

🔍 四、简单示例代码

🧮 五、fork 的内存复制机制(Copy-on-Write)

🧰 六、常见用法

示例1:基础用法与进程区分

示例2:循环创建子进程及控制

示例3:fork + exec 执行新程序

示例4:简易并发网络服务器模型

⚠️ 关键注意事项总结

七、看门狗及守护进程示例(重点)

1、实现父子进程看门狗

2、守护进程

八、面试常见问题​


🧠 一、fork() 是什么?

fork() 是 Linux/Unix 系统调用,用于 创建一个新的进程

  • <unistd.h> 提供(在 Linux 系统中使用)

  • 新创建的进程称为 子进程(child process)

  • 调用 fork() 的原进程称为 父进程(parent process)

简单说:fork()复制当前进程,从此系统中存在两个几乎完全相同的进程。

  • ​"一次调用,两次返回"​​:这是 fork()最核心也最令人困惑的特性。调用一次 fork(),它会在父进程和子进程中各返回一次。

    • 在​​父进程​​中,fork()返回新创建的​​子进程的进程ID(PID)​​(一个大于0的整数)。

    • 在​​子进程​​中,fork()返回 ​​0​​。

    • 如果​​创建子进程失败​​(例如系统进程数已达上限或内存不足),fork()返回 ​​-1​​。

      通过判断返回值,程序可以确定当前是在父进程还是子进程中,从而执行不同的代码分支。

  • ​执行顺序​​:调用 fork()之后,父进程和子进程是​​并发执行​​的,它们的运行顺序是​​不确定的​​,取决于内核的调度策略。


⚙️ 二、函数原型

#include <unistd.h>

pid_t fork(void);

返回值说明:

返回值 含义
> 0 父进程中返回子进程的 PID
= 0 子进程中返回 0
< 0 创建失败,返回 -1(通常是资源不足)

🧩 三、fork() 的执行流程

调用 fork() 后,系统会:

  1. 复制当前进程的 地址空间(包括代码段、堆、栈等)

  2. 为子进程分配一个新的 PID

  3. 子进程与父进程从 fork()继续执行后续语句

📘 注意:
虽然两者“代码一样”,但它们有 独立的内存空间。互不影响。


🔍 四、简单示例代码

#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        // 创建失败
        perror("fork failed");
        return 1;
    } 
    else if (pid == 0) {
        // 子进程逻辑
        printf("我是子进程,PID=%d,父PID=%d\n", getpid(), getppid());
    } 
    else {
        // 父进程逻辑
        printf("我是父进程,PID=%d,子PID=%d\n", getpid(), pid);
    }

    return 0;
}

运行输出示例:

我是父进程,PID=1000,子PID=1001
我是子进程,PID=1001,父PID=1000

注意:执行顺序不确定,因为父子进程是并发运行的


🧮 五、fork 的内存复制机制(Copy-on-Write)

现代 Linux 中并不会立即复制整个内存空间,而是使用 COW(Copy on Write) 技术:

  • 父子进程 共享同一份物理内存页(只读)

  • 当任意一方尝试写入该内存页时,才复制出一份独立副本

✅ 优点:显著提高性能,避免浪费内存。
💡 示例:vfork()posix_spawn() 也是为性能优化的变体。

  1. ​COW 的核心思想​​:在调用 fork()的瞬间,内核并不会立即复制父进程的整个数据空间、堆和栈。相反,它让​​父子进程共享所有的物理内存页,并将这些页标记为只读​​。

  2. ​触发复制​​:当任何一个进程(父或子)尝试​​修改​​这些共享的内存页时,CPU会触发一个页错误异常。此时,内核才会​​为该进程复制一份需要修改的内存页的副本​​,然后在这个新副本上进行修改。这样,另一个进程仍然使用原来的未修改的页。

  3. ​COW 的优势​​:

    • ​高效创建​​:fork()调用本身非常快,因为避免了不必要的数据复制。

    • ​节省内存​​:如果子进程在创建后立即调用 exec系列函数来执行另一个程序,那么之前共享的、可能很大的内存空间就会被完全替换,COW机制避免了复制这些根本不会被用到的数据,极大地节省了内存开销。

特性

父子进程关系

说明

​代码段​

共享

代码是只读的,因此父子进程共享同一份代码。

​数据段、堆、栈​

写时复制(COW)

初始共享,修改时分离,保证数据独立性。

​文件描述符​

共享文件表项

子进程会复制父进程打开的文件描述符,但它们​​指向内核中相同的文件表项​​。这意味着父子进程会​​共享文件的当前偏移量​​。如果一个进程移动了文件指针,另一个进程也会受到影响。

​进程ID(PID)​

不同

子进程拥有全新的、唯一的PID。

​父进程ID(PPID)​

不同

子进程的PPID是其父进程的PID。

​运行空间​

独立

拥有独立的虚拟地址空间,互不干扰。


下面是一个简单的示例,演示了 fork()的基本用法和返回值判断:

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

int main() {
    int global_var = 10; // 全局变量,用于演示COW

    printf("Before fork: Process ID = %d, global_var = %d\n", getpid(), global_var);

    pid_t pid = fork(); // 核心调用

    if (pid == -1) {
        perror("fork failed");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 这里是子进程代码块
        printf("Child process: PID = %d, Parent PID = %d\n", getpid(), getppid());
        global_var = 20; // 修改变量,触发COW
        printf("Child process: modified global_var = %d\n", global_var);
        exit(EXIT_SUCCESS); // 子进程退出
    } else {
        // 这里是父进程代码块
        printf("Parent process: PID = %d, Child PID = %d\n", getpid(), pid);
        waitpid(pid, NULL, 0); // 等待子进程结束,避免僵尸进程
        printf("Parent process: after child modified, global_var = %d\n", global_var); // 仍然是10
    }
    return 0;
}

​关键点分析​​:

  • ​返回值判断​​:通过 if-else结构严格区分父子进程的逻辑。

  • ​COW验证​​:子进程对 global_var的修改不会影响父进程中该变量的值,证明了内存空间的独立性。

  • ​等待子进程​​:父进程使用 waitpid()等待子进程结束,这是一种良好的编程实践,可以回收子进程资源,防止其变成“僵尸进程”(Zombie Process)。


🧰 六、常见用法

示例1:基础用法与进程区分

下面的代码展示了最基本的使用方法,并验证了父子进程的独立内存空间。

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

int main() {
    int global_var = 10; // 全局变量,用于演示COW

    printf("【父进程】fork调用前:进程ID = %d, global_var = %d\n", getpid(), global_var);

    pid_t pid = fork(); // 核心调用!

    // 错误处理必须最先判断
    if (pid == -1) {
        perror("fork failed");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 这里是子进程代码块 (pid == 0)
        printf("【子进程】我的PID是:%d,我父进程的PID是:%d\n", getpid(), getppid());
        global_var = 20; // 修改变量,这里会触发写时复制!
        printf("【子进程】我修改了 global_var = %d\n", global_var);
        printf("【子进程】我的任务完成了,即将退出。\n");
        exit(EXIT_SUCCESS); // 子进程退出
    } else {
        // 这里是父进程代码块 (pid > 0, pid就是子进程的ID)
        printf("【父进程】我的PID是:%d,我创建的子进程PID是:%d\n", getpid(), pid);
        // 父进程等待子进程结束,避免其成为"僵尸进程"
        int status;
        waitpid(pid, &status, 0);
        printf("【父进程】等待子进程结束。在我这里,global_var 仍然是:%d\n", global_var); // 验证独立性
        printf("【父进程】子进程的退出状态是:%d\n", WEXITSTATUS(status));
    }

    printf("【进程 %d】这一行会被父子进程都执行一次,但他们是独立的进程!\n", getpid());
    return 0;
}

​编译与运行:​

gcc -o fork_basic fork_basic.c
./fork_basic

​可能的输出:​

【父进程】fork调用前:进程ID = 1234, global_var = 10
【父进程】我的PID是:1234,我创建的子进程PID是:1235
【子进程】我的PID是:1235,我父进程的PID是:1234
【子进程】我修改了 global_var = 20
【子进程】我的任务完成了,即将退出。
【父进程】等待子进程结束。在我这里,global_var 仍然是:10
【父进程】子进程的退出状态是:0
【进程 1234】这一行会被父子进程都执行一次,但他们是独立的进程!

​关键点分析:​

  • ​返回值判断​​:通过 if-else结构严格区分父子进程的逻辑 。

  • ​COW验证​​:子进程对 global_var的修改不会影响父进程中该变量的值,证明了内存空间的独立性 。

  • ​等待子进程​​:父进程使用 waitpid()等待子进程结束,这是一种良好的编程实践,可以回收子进程资源,防止其变成“僵尸进程”(Zombie Process)。

示例2:循环创建子进程及控制

如果想创建多个子进程,直接使用循环可能会产生意想不到的结果。

​错误示范:​

#include <stdio.h>
#include <unistd.h>

int main() {
    int i;
    pid_t pid;
    printf("【父进程】开始循环创建子进程...\n");

    for (i = 0; i < 3; i++) {
        pid = fork();
        if (pid == 0) {
            printf("子进程 %d 被创建,它的 i = %d\n", getpid(), i);
            // 注意:子进程没有退出,它会继续循环!
        } else if (pid > 0) {
            printf("父进程创建了第 %d 个子进程,PID=%d\n", i, pid);
        }
    }
    sleep(1); // 让所有进程有时间输出
    return 0;
}

运行这个程序,你会发现创建的进程数量远多于3个(实际上是2^3=8个),因为子进程也会继续执行循环,创建"孙进程" 。

​正确做法:​​ 在子进程中及时使用 breakexit跳出循环。

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

int main() {
    int i;
    pid_t pid;

    printf("【父进程 PID=%d】开始创建3个子进程...\n", getpid());

    for (i = 0; i < 3; i++) {
        pid = fork();
        if (pid == -1) {
            perror("fork");
            exit(1);
        } else if (pid == 0) {
            // 子进程:执行自己的任务后立即跳出循环,不再参与fork
            printf("    【子进程%d】我的PID是%d,我负责第%d项任务\n", i+1, getpid(), i+1);
            sleep(1); // 模拟任务执行时间
            break; // 或者直接 exit(0); 关键是要阻止子进程继续循环
        }
        // 父进程继续循环,创建下一个子进程
    }

    // 等待所有子进程结束 (父进程需要做的事情)
    if (pid > 0) { // 只有父进程的pid才大于0
        for (i = 0; i < 3; i++) {
            wait(NULL); // 等待任何一个子进程结束
        }
        printf("【父进程】所有子进程任务已完成。\n");
    } else {
        // 子进程可以在这里执行更复杂的操作,然后退出
        printf("    【子进程%d, PID=%d】任务完成,退出。\n", i+1, getpid());
    }

    return 0;
}

示例3:fork + exec 执行新程序

这是 fork()最经典的用法之一:Shell就是这样运行命令的。子进程通过 exec系列函数来加载并执行一个全新的程序,替换掉自己原有的代码段、数据段等 ,但pid没有变。子进程调用 exec()成功后,它并没有变成一个“新的进程”,而是​​同一个进程​​“脱胎换骨”,执行了全新的程序。父进程 wait等待的,正是这个“改头换面”后的​​子进程的结束。

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

int main() {
    pid_t pid = fork();

    if (pid == -1) {
        perror("fork failed");
        exit(1);
    } else if (pid == 0) {
        // 子进程:我要去执行 ls -l 命令了
        printf("【子进程】我要变身去执行 'ls -l' 命令了!\n");
        // 使用 execvp 执行命令,第一个参数是命令名,第二个是参数数组
        char *args[] = {"ls", "-l", NULL}; // 参数列表必须以NULL结束
        execvp("ls", args);

        // 如果 execvp 成功,下面的代码绝不会被执行!
        perror("execvp failed"); // 只有 exec 失败时才会执行到这里
        exit(1); // 如果exec失败,子进程需要退出
    } else {
        // 父进程
        printf("【父进程】我正在等待子进程执行完 'ls' 命令...\n");
        waitpid(pid, NULL, 0); // 等待子进程结束
        printf("【父进程】子进程的任务完成了。\n");
    }
    return 0;
}

示例4:简易并发网络服务器模型

这是 fork()另一个极其重要的应用场景:实现并发服务器 。下面是一个高度简化的逻辑模型:

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

// 模拟处理客户端请求的函数
void handle_client(int client_id, const char *message) {
    printf("    【工作进程%d】开始处理请求:%s\n", client_id, message);
    sleep(2); // 模拟处理请求需要时间
    printf("    【工作进程%d】请求处理完毕。\n", client_id);
}

int main() {
    printf("【服务器主进程 PID=%d】启动,正在监听端口...\n", getpid());
    int client_id = 0;

    // 服务器主循环
    while (client_id < 3) { // 假设只处理3个客户端连接以示例如
        printf("【服务器】接受一个新的客户端连接 (#%d)...\n", ++client_id);

        pid_t pid = fork();

        if (pid == -1) {
            perror("fork failed");
        } else if (pid == 0) {
            // 子进程:专门处理这个客户端的请求
            char msg[100];
            sprintf(msg, "Hello from Client %d", client_id);
            handle_client(client_id, msg);
            exit(0); // 处理完成,子进程退出
        } else {
            // 父进程:继续回到循环开头,监听下一个连接
            printf("【服务器】已创建子进程 %d 来处理客户端 #%d 的请求,父进程继续监听...\n", pid, client_id);
            // 父进程不需要立即wait,否则就变成串行处理了。
            // 可以通过信号或非阻塞wait来避免僵尸进程,这里为简化使用sleep(1)略微等待子进程结束
        }
        sleep(1); // 模拟连接到达的间隔
    }

    // 父进程最后等待所有子进程
    while (wait(NULL) > 0) { // 回收所有子进程
        ; 
    }
    printf("【服务器】所有客户端请求处理完毕,服务器关闭。\n");
    return 0;
}

⚠️ 关键注意事项总结

  1. ​僵尸进程与孤儿进程​​:

    • ​僵尸进程​​:子进程终止后,如果父进程没有调用 wait()waitpid()来回收其退出状态信息,该子进程的进程描述符仍然保留在系统中,成为“僵尸进程”。过多的僵尸进程会消耗系统资源。

    • ​孤儿进程​​:如果父进程先于子进程终止,子进程就会成为“孤儿进程”。孤儿进程会被 ​​init 进程(PID=1)​​ 自动收养,由 init 负责其善后工作。

  2. ​缓冲区问题​​:标准I/O库(如 printf)通常带有缓冲区。如果 fork()之前有未刷新(如没有换行符 \n)的缓冲区数据,子进程会复制这份缓冲区。这可能导致输出内容出现重复或错乱。可以在 fork()前使用 fflush(stdout)来清空缓冲区。

  3. ​连续调用 fork​​:在循环或条件判断中连续调用 fork()需要特别小心,否则容易产生指数级增长的进程数量,造成“fork炸弹”。务必确保子进程在创建后及时 break循环或退出。

  4. waitpid 使用方式详解:

  1. ​阻塞等待 (options = 0)​

    • 父进程暂停执行,直到指定子进程结束

    • 最简单直接的方式,适用于需要等待子进程完成的场景

    • 示例:waitpid(child_pid, &status, 0)

  2. ​非阻塞检查 (options = WNOHANG)​

    • 立即返回,不阻塞父进程

    • 返回值:

      • 0:子进程已结束,返回值为子进程PID

      • 0:子进程仍在运行

      • -1:出错

    • 适用于需要定期检查子进程状态的场景

    • 示例:waitpid(child_pid, &status, WNOHANG)

  3. ​监控停止进程 (options = WUNTRACED)​

    • 报告已停止的子进程状态(通常由SIGSTOP等信号引起)

    • 使用 WIFSTOPPED(status)检查是否停止

    • 使用 WSTOPSIG(status)获取导致停止的信号

    • 示例:waitpid(child_pid, &status, WUNTRACED)

  4. ​监控继续进程 (options = WCONTINUED)​

    • 报告已恢复运行的子进程状态(通常由SIGCONT信号引起)

    • 使用 WIFCONTINUED(status)检查是否恢复运行

    • 示例:waitpid(child_pid, &status, WCONTINUED)

七、看门狗及守护进程示例(重点)

1、实现父子进程看门狗

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <fcntl.h>
#include <string.h>
#include <stdarg.h>
#include <time.h>

#define HEARTBEAT_TIMEOUT 30  // 心跳超时时间(秒)
#define RESTART_DELAY 3       // 重启延迟时间(秒)

// 日志函数
void write_log(const char* format, ...) {
    FILE* log_file = fopen("/tmp/watchdog.log", "a");
    if (!log_file) return;
    
    time_t now = time(NULL);
    char timestamp[64];
    strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", localtime(&now));
    
    va_list args;
    va_start(args, format);
    
    fprintf(log_file, "[%s] PID:%d ", timestamp, getpid());
    vfprintf(log_file, format, args);
    fprintf(log_file, "\n");
    fflush(log_file);
    
    va_end(args);
    fclose(log_file);
}

// 业务函数 - 用户需要实现
void business_main() {
    while (1) {
        // 模拟业务工作
        sleep(5);
        write_log("业务进程运行中");
    }
}

int main() {
    int pipe_fd[2];
    pid_t child_pid;
    int status;
    time_t last_heartbeat;
    char heartbeat_msg[100];
    
    write_log("=== 看门狗守护进程启动 ===");
    
    // 创建心跳管道
    if (pipe(pipe_fd) < 0) {
        write_log("创建管道失败");
        return 1;
    }
    
    // 设置管道为非阻塞
    fcntl(pipe_fd[0], F_SETFL, O_NONBLOCK);
    
    // 忽略常见信号
    signal(SIGTERM, SIG_IGN);
    signal(SIGINT, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);

    while (1) {
        write_log("启动子进程...");
        
        child_pid = fork();
        
        if (child_pid < 0) {
            write_log("fork失败,5秒后重试");
            sleep(5);
            continue;
        }
        
        if (child_pid == 0) {
            // 子进程:业务逻辑
            close(pipe_fd[0]);  // 关闭读端
            
            // 设置管道写端为非阻塞
            fcntl(pipe_fd[1], F_SETFL, O_NONBLOCK);
            
            write_log("业务进程启动");
            business_main();
            exit(0);
        }
        
        // 父进程:监控逻辑
        close(pipe_fd[1]);  // 关闭写端
        last_heartbeat = time(NULL);
        
        while (1) {
            int wait_result;
            
            // 非阻塞检查子进程状态
            wait_result = waitpid(child_pid, &status, WNOHANG);
            
            if (wait_result == child_pid) {
                // 子进程状态变化
                if (WIFEXITED(status)) {
                    int exit_code = WEXITSTATUS(status);
                    write_log("子进程正常退出,代码:%d", exit_code);
                    break;
                } else if (WIFSIGNALED(status)) {
                    int signal_num = WTERMSIG(status);
                    write_log("子进程被信号终止:%d", signal_num);
                    break;
                }
            } else if (wait_result == -1) {
                write_log("waitpid错误");
                break;
            }
            
            // 检查心跳
            if (read(pipe_fd[0], heartbeat_msg, sizeof(heartbeat_msg)) > 0) {
                last_heartbeat = time(NULL);
            }
            
            // 心跳超时检查
            if (time(NULL) - last_heartbeat > HEARTBEAT_TIMEOUT) {
                write_log("心跳超时,终止子进程");
                kill(child_pid, SIGKILL);
                sleep(1);
                break;
            }
            
            sleep(1);  // 避免CPU占用过高
        }
        
        write_log("等待%d秒后重启子进程", RESTART_DELAY);
        sleep(RESTART_DELAY);
    }
    
    close(pipe_fd[0]);
    write_log("=== 看门狗退出 ===");
    return 0;
}
 

关键特性说明

  1. ​心跳监控机制​​:

    • 使用管道实现父子进程通信

    • 父进程定期检查心跳信号

    • 可配置的超时时间(HEARTBEAT_TIMEOUT)

  2. ​进程状态监控​​:

    • 使用waitpid非阻塞检查子进程状态

    • 自动检测正常退出和信号终止

    • 支持自动重启子进程

  3. ​日志系统​​:

    • 带时间戳的详细日志记录

    • 日志文件保存在/tmp/watchdog.log

    • 包含进程ID和详细状态信息

  4. ​错误处理​​:

    • 完善的fork失败处理

    • 信号忽略防止意外中断

    • 管道创建错误处理

2、守护进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <fcntl.h>
#include <time.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <stdarg.h>  // 添加可变参数支持

#define LOG_FILE "/tmp/watchdog.log"
#define HEARTBEAT_TIMEOUT 30
#define RESTART_DELAY 3

// 日志函数 - 修复可变参数问题
void write_log(const char* format, ...) {
    FILE* log_file = fopen(LOG_FILE, "a");
    if (!log_file) return;
    
    time_t now = time(NULL);
    char timestamp[64];
    strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", localtime(&now));
    fprintf(log_file, "[%s] [PID:%d] ", timestamp, getpid());
    
    va_list args;
    va_start(args, format);
    vfprintf(log_file, format, args);
    va_end(args);
    
    fprintf(log_file, "\n");
    fclose(log_file);
}

// 信号处理
volatile sig_atomic_t g_shutdown = 0;
void signal_handler(int sig) {
    if (sig == SIGTERM || sig == SIGINT) {
        write_log("接收到终止信号 %d,开始关闭", sig);
        g_shutdown = 1;
    }
}

// 守护进程初始化
int daemon_init() {
    pid_t pid = fork();
    if (pid < 0) return -1;
    if (pid > 0) exit(0);
    
    if (setsid() < 0) return -1;
    
    pid = fork();
    if (pid < 0) return -1;
    if (pid > 0) exit(0);
    
    umask(0);
    chdir("/");
    
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);
    
    open("/dev/null", O_RDONLY);
    open("/dev/null", O_RDWR);
    open("/dev/null", O_RDWR);
    
    return 0;
}

// 业务函数(用户实现)
void business_main() {
    // 示例业务逻辑
    while (!g_shutdown) {
        sleep(5);
        write_log("业务进程运行中");
    }
}

// 启动子进程
pid_t start_child_process() {
    pid_t pid = fork();
    if (pid == 0) {
        write_log("子进程启动");
        business_main();
        exit(0);
    }
    return pid;
}

// 监控子进程
void monitor_child_process(pid_t child_pid) {
    int status;
    
    while (!g_shutdown) {
        pid_t wait_result = waitpid(child_pid, &status, WNOHANG);
        
        if (wait_result == child_pid) {
            if (WIFEXITED(status)) {
                write_log("子进程正常退出,状态码: %d", WEXITSTATUS(status));
                break;
            } else if (WIFSIGNALED(status)) {
                write_log("子进程被信号终止: %d", WTERMSIG(status));
                break;
            }
        } else if (wait_result == -1) {
            write_log("waitpid错误: %s", strerror(errno));
            break;
        }
        
        sleep(1);  // 避免CPU占用过高
    }
}

int main() {
    if (daemon_init() < 0) {
        return 1;
    }
    
    signal(SIGTERM, signal_handler);
    signal(SIGINT, signal_handler);
    
    write_log("守护进程启动");
    
    while (!g_shutdown) {
        pid_t child_pid = start_child_process();
        if (child_pid > 0) {
            monitor_child_process(child_pid);
        }
        sleep(RESTART_DELAY);
    }
    
    write_log("守护进程退出");
    return 0;
}
 

关键功能说明

  1. ​守护进程初始化​​:

    • 通过两次fork创建标准守护进程

    • 脱离终端控制,在后台运行

    • 关闭标准输入输出,重定向到/dev/null

  2. ​子进程监控​​:

    • 使用waitpid非阻塞检查子进程状态

    • 自动检测正常退出和信号终止

    • 支持心跳超时检测(需业务代码实现)

  3. ​自动重启机制​​:

    • 子进程退出后自动重启

    • 可配置重启延迟时间(RESTART_DELAY)

    • 支持优雅关闭(SIGTERM/SIGINT)

  4. ​日志系统​​:

    • 带时间戳的详细日志记录

    • 日志文件保存在/tmp/watchdog.log

    • 包含进程ID和详细状态信息

八、面试常见问题​

​1. fork() 的基本特性是什么?​

​答案:​fork()的核心特性是 ​​“一次调用,两次返回”​​。

  • 调用一次 fork(),系统会创建一个与父进程几乎完全相同的子进程。

  • ​返回值区分父子进程​​:

    • 在 ​​父进程​​ 中返回 ​​子进程的 PID(大于0的整数)​

    • 在 ​​子进程​​ 中返回 ​​0​

    • 返回 ​​-1​​ 表示创建失败(如系统资源不足)。

​2. 什么是写时复制(Copy-On-Write, COW)?它有什么优势?​

​答案:​​ 写时复制是现代操作系统优化 fork()性能的关键机制。

  • ​工作原理​​:

    • fork()初始时,父子进程​​共享所有物理内存页​​,并将这些页标记为​​只读​​。

    • 当任一进程(父或子)尝试​​修改​​共享内存时,才会触发真正的内存页复制。

  • ​优势​​:

    • ​高效创建​​:避免启动时复制大量内存,fork()速度极快。

    • ​节省内存​​:未修改的数据始终共享,特别适合子进程后续立即调用 exec()的场景。​

​3. 先 open()fork()与先 fork()open(),文件描述符有何区别?​

​答案:​​ 这是面试常见陷阱,关键区别在于文件描述符的继承方式。

操作顺序

文件描述符关系

文件偏移量

​先 open()fork()

父子进程​​共享​​同一个文件描述符表项

​共享​​偏移量(一个进程读写会影响另一个)

​先 fork()open()

父子进程​​各自打开​​文件,有独立的描述符

​不共享​​偏移量(相互独立)

​代码验证要点​​:先openfork后,父子进程交替读取文件,会看到它们轮流读取后续内容,而非重复读取开头,证明偏移量共享。

​4. 循环中调用 fork()会产生多少进程?​

​答案:​​ 进程数量会呈​​指数级增长​​。公式为 ​​2^n​​(n 是 fork()调用次数)。

  • 示例:for (i = 0; i < 3; i++) { fork(); }最终会创建 ​​2^3 = 8​​ 个进程(包括原始进程)。

  • ​关键点​​:每次 fork()后,​​新创建的子进程也会继续执行后续的循环和 fork()调用​​。

​5. 什么是僵尸进程?如何避免和解决?​

​答案:​

  • ​僵尸进程​​:子进程​​终止后​​,其退出状态信息未被父进程通过 wait()waitpid()回收,导致其进程描述符仍占用系统进程表资源的状态。

  • ​解决方案​​:

    1. ​父进程主动回收​​:在父进程中调用 waitpid(pid, &status, 0)

    2. ​信号处理​​:为 SIGCHLD信号设置处理函数,在函数中非阻塞地循环调用 waitpid(-1, NULL, WNOHANG)来回收所有已终止的子进程。

    3. ​终极手段​​:若父进程已无法修改,可杀死父进程(kill -9 PPID),让子进程被 ​​init 进程(PID=1)​​ 收养并自动回收。​

​6. 在多线程程序中调用 fork()安全吗?​

​答案:​​ ​​极其危险,不推荐​​。

  • fork()只会复制调用它的那个线程,而其他线程在子进程中会瞬间“消失”。

  • 这可能导致子进程持有父进程中其他线程已加锁但未释放的锁,从而引发​​死锁​​。

  • ​最佳实践​​:如果必须在多线程环境中创建新进程,应使用 fork()后立即调用 exec()执行新程序。

​7. vfork()fork()有什么区别?​

​答案:​vfork()是一个历史遗留的、为高效执行 exec()而设计的函数。

  • ​关键区别​​:vfork()创建的子进程与父进程​​共享地址空间​​,且保证子进程先运行,直到其调用 exec()_exit()后父进程才被调度。

  • ​使用场景​​:仅在子进程​​立即​​调用 exec()系列函数时,为避免不必要的内存复制开销才考虑使用 vfork()。现代 Linux 中 fork()的 COW 机制已非常高效,vfork()的使用场景已大大减少。

​8. wait()waitpid()有什么区别?WNOHANG选项有什么用?​

​答案:​

  • wait()​:​​阻塞​​调用,父进程暂停执行,直到​​任意一个​​子进程结束。

  • waitpid()​:更灵活,可以指定要等待的​​特定子进程​​(通过PID),并可通过选项控制行为。

  • WNOHANG选项​​:使 waitpid()变为​​非阻塞​​调用。如果指定的子进程没有结束,waitpid()会立即返回0,而不会让父进程等待。这使得父进程可以在等待子进程的同时继续处理其他任务,是实现“看门狗”或监控进程的关键。​

​9. 预测以下代码的输出并解释原因。​

#include <stdio.h>
#include <unistd.h>
int main() {
    fork();
    fork();
    fork();
    printf("Hello\n");
    return 0;
}

​答案:​​ 输出 ​​8​​ 个 "Hello"。

  • ​解释​​:3次 fork()调用,进程总数变为 2^3 = 8 个(包括原始进程)。每个进程都会执行 printf语句。

​10. 如何解析 waitpid()得到的 status信息?​

​答案:​​ 使用预定义的宏来解析 status这个“信息包”:

  • WIFEXITED(status):判断子进程是否​​正常退出​​。

  • WEXITSTATUS(status):如果正常退出,提取子进程的​​退出码​​(即 exit(5)中的5)。

  • WIFSIGNALED(status):判断子进程是否因​​信号​​而异常终止。

  • WTERMSIG(status):如果被信号终止,提取导致终止的​​信号编号​​(如 SIGKILL为9)。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐