5. 进程创建与管理.md 24 KB

5 进程创建与管理

5.1 完整概念讲解

5.1.1 进程的基本概念

进程是程序的一次动态执行实例,是操作系统资源分配的基本单位。在Linux系统中,每个进程都有唯一的进程ID(PID),进程之间通过进程控制块(PCB)来管理。

Linux采用进程树的组织方式:

  • init进程(PID=1):系统启动后的第一个用户态进程,是所有进程的祖先
  • systemd:现代Linux发行版中init进程的实际实现
  • 父进程与子进程:通过fork创建的进程关系

5.1.2 fork()详解

fork()是Linux/Unix系统中最核心的进程创建系统调用,它的特殊之处在于调用一次,返回两次

工作原理:

  1. 内核为新进程分配新的进程控制块(PCB)和唯一的PID
  2. 复制父进程的地址空间、文件描述符表、信号处理等资源
  3. 在父进程中返回子进程的PID(大于0)
  4. 在子进程中返回0

写时复制(Copy-On-Write, COW)机制:

传统fork实现会立即复制整个父进程地址空间,效率极低。现代Linux采用写时复制技术:

  1. fork瞬间:父子进程共享同一物理内存页,页表项标记为只读
  2. 写入时触发缺页异常:当任一进程尝试修改内存时,内核捕获异常
  3. 按需复制:仅复制被修改的页,其余继续共享
  4. 优势:大幅减少fork的开销,特别是对于大型进程

    // COW机制示意(伪代码)
    pid_t pid = fork();
    if (pid == 0) {
    // 子进程修改变量时,内核才会真正复制该页
    variable = 100;  // 触发COW,复制该页
    }
    

5.1.3 fork后父子进程的区别

特性 父进程 子进程
PID 原PID不变 新分配的唯一PID
PPID 父进程的PID 父进程的PID(两者相同)
fork返回值 子进程的PID(>0) 0
文件描述符 保持原有FD 复制父进程的FD表
信号处理 原有设置 复制父进程的设置
资源限制 原有限制 继承父进程的限制

文件描述符共享的关键点:

  • fork后,父子进程共享文件偏移量(通过同一个文件表项)
  • 如果父进程打开文件,子进程读写同一文件,会影响彼此的偏移量
  • 这一特性可用于父子进程间通信

5.1.4 exec族函数

exec函数族用于替换当前进程的映像,加载并执行另一个程序。调用exec后,进程的代码段、数据段、堆栈都会被新程序替换,但PID保持不变。

exec函数族分类:

函数 参数形式 搜索路径 使用场景
execl 列表参数 需指定完整路径 固定路径执行
execv 字符串数组 需指定完整路径 动态构建参数
execlp 列表参数 自动搜索PATH 执行常见命令
execvp 字符串数组 自动搜索PATH 动态构建+PATH搜索
execle 列表参数+环境变量 需指定完整路径 自定义环境变量
execve 字符串数组+环境变量 需指定完整路径 系统调用原语

exec返回值:

  • 成功:不返回(直接跳转到新程序入口)
  • 失败:返回-1,设置errno

5.1.5 wait/waitpid

wait()waitpid()用于回收子进程资源,获取子进程退出状态。

僵尸进程(Zombie Process):

  • 子进程退出后,内核保留其PCB信息(PID、退出状态等)
  • 父进程通过wait调用读取这些信息后,子进程资源才会被完全释放
  • 如果父进程不调用wait,子进程会变成僵尸进程,占用进程表项

wait与waitpid的区别:

特性 wait waitpid
阻塞行为 一直阻塞直到有子进程退出 可设置WNOHANG非阻塞
目标 任意子进程 可指定特定PID的子进程
并发处理 一次只能处理一个 可处理多个子进程

退出状态宏:

  • WIFEXITED(status):子进程正常退出时返回非零
  • WEXITSTATUS(status):获取子进程的退出码(0-255)
  • WIFSIGNALED(status):子进程被信号杀死时返回非零
  • WTERMSIG(status):获取导致子进程终止的信号

5.1.6 进程状态

Linux系统中进程有以下主要状态:

状态 符号 描述
运行/就绪 R (Running/Runnable) 正在运行或在运行队列等待CPU
可中断睡眠 S (Sleeping) 等待事件(如I/O完成),可被信号唤醒
不可中断睡眠 D (Disk Sleep) 等待I/O完成,不可被信号唤醒
停止 T (Stopped) 被信号(SIGSTOP/SIGTSTP)暂停
僵尸 Z (Zombie) 已退出但资源未被父进程回收

查看进程状态:

# 使用ps命令查看
ps -eo pid,stat,cmd | head -20

# 使用top命令实时查看
top

5.1.7 多进程编程模式

典型模式:fork + exec + wait

这是Unix/Linux系统中最经典的进程创建模式:

  1. fork()创建子进程
  2. 子进程中调用exec()执行新程序
  3. 父进程中调用wait()等待子进程结束

    pid_t pid = fork();
    if (pid == 0) {
    // 子进程
    execl("/bin/ls", "ls", "-l", NULL);
    perror("execl failed");
    exit(1);
    } else if (pid > 0) {
    // 父进程
    int status;
    wait(&status);
    // 处理退出状态
    }
    

5.1.8 嵌入式应用场景

在嵌入式Linux应用开发中,多进程架构广泛应用于:

  1. 命令执行:Shell程序通过fork+exec执行外部命令
  2. 守护进程:后台服务进程,如Web服务器、数据库服务
  3. 进程隔离:不同功能模块运行在独立进程中,故障隔离
  4. 并发处理:处理多个客户端连接(如FTP服务器)
  5. 任务调度:定时执行特定任务(如crond)
  6. 看门狗:监控主程序运行状态,异常时重启

5.2 核心API/语法

5.2.1 fork()函数原型

#include <unistd.h>

pid_t fork(void);

参数说明:

  • 无参数

返回值:

  • 成功:在父进程中返回子进程的PID(>0),在子进程中返回0
  • 失败:返回-1,设置errno

errno常见值:

  • EAGAIN:系统进程表已满或用户进程数达到上限
  • ENOMEM:内核内存不足

5.2.2 exec族函数原型

#include <unistd.h>

// execl - 执行文件(列表参数)
int execl(const char *path, const char *arg, ... /* (char *) NULL */);

// execv - 执行文件(字符串数组)
int execv(const char *path, char *const argv[]);

// execlp - 按PATH搜索执行(列表参数)
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);

// execvp - 按PATH搜索执行(字符串数组)
int execvp(const char *file, char *const argv[]);

// execle - 执行文件(带环境变量)
int execle(const char *path, const char *arg, ... /* (char *) NULL, char *const envp[] */);

// execve - 执行文件(系统调用原语)
int execve(const char *path, char *const argv[], char *const envp[]);

参数说明:

  • path:要执行的程序的完整路径
  • file:程序名,execvp/execle会自动搜索PATH环境变量
  • arg:第一个参数通常是程序名
  • argv:字符串数组,以NULL结尾
  • envp:环境变量数组,以NULL结尾

返回值:

  • 成功:不返回
  • 失败:返回-1,设置errno

5.2.3 wait/waitpid函数原型

#include <sys/wait.h>

// wait - 等待任意子进程退出
pid_t wait(int *wstatus);

// waitpid - 等待指定子进程退出
pid_t waitpid(pid_t pid, int *wstatus, int options);

waitpid参数说明:

  • pid:指定要等待的子进程PID
    • pid > 0:等待特定PID的子进程
    • pid = -1:等待任意子进程(等同于wait)
    • pid = 0:等待与调用进程同组ID的子进程
    • pid < -1:等待组ID等于|pid|的子进程
  • options:选项标志
    • WNOHANG:非阻塞模式,无子进程退出立即返回
    • WUNTRACED:报告停止的子进程状态
    • WCONTINUED:报告继续的子进程状态

返回值:

  • 成功:退出的子进程PID,或0(WNOHANG且无子进程退出)
  • 失败:返回-1,设置errno

5.2.4 退出状态宏定义

#include <sys/wait.h>

// 判断子进程是否正常退出
int WIFEXITED(int status);

// 获取子进程退出码(仅当WIFEXITED非零时有效)
int WEXITSTATUS(int status);

// 判断子进程是否被信号杀死
int WIFSIGNALED(int status);

// 获取导致终止的信号编号
int WTERMSIG(int status);

// 判断子进程是否被信号停止
int WIFSTOPPED(int status);

// 获取导致停止的信号编号
int WSTOPSIG(int status);

5.3 代码示例

5.3.1 fork基础示例

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

int main() {
    pid_t pid;
    int counter = 100;  // 共享变量

    printf("Before fork: pid=%d, counter=%d\n", getpid(), counter);

    pid = fork();

    if (pid < 0) {
        // fork失败
        perror("fork failed");
        exit(1);
    } else if (pid == 0) {
        // 子进程
        counter += 10;
        printf("[Child] pid=%d, ppid=%d, counter=%d\n",
               getpid(), getppid(), counter);
        exit(0);
    } else {
        // 父进程
        counter += 20;
        printf("[Parent] pid=%d, child_pid=%d, counter=%d\n",
               getpid(), pid, counter);
        wait(NULL);  // 等待子进程结束
    }

    return 0;
}

编译运行:

gcc -o fork_example fork_example.c
./fork_example

输出示例:

Before fork: pid=12345, counter=100
[Child] pid=12346, ppid=12345, counter=110
[Parent] pid=12345, child_pid=12346, counter=120

说明: fork后,父子进程各有一份counter的副本,互不影响。

5.3.2 文件描述符共享示例

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

#define FILENAME "shared_file.txt"

int main() {
    pid_t pid;
    int fd;
    char buf[128];

    // 创建并写入文件
    fd = open(FILENAME, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    write(fd, "Hello", 5);
    close(fd);

    // 以读写方式打开文件,fork后父子进程共享此fd
    fd = open(FILENAME, O_RDWR);

    pid = fork();
    if (pid < 0) {
        perror("fork failed");
        exit(1);
    } else if (pid == 0) {
        // 子进程:在文件末尾追加写入
        lseek(fd, 0, SEEK_END);  // 移动到文件末尾
        write(fd, " World", 6);
        printf("[Child] wrote ' World'\n");
        close(fd);
        exit(0);
    } else {
        // 父进程:等待子进程写入后读取
        sleep(1);  // 等待子进程写入
        lseek(fd, 0, SEEK_SET);  // 移动到文件开头
        memset(buf, 0, sizeof(buf));
        read(fd, buf, sizeof(buf) - 1);
        printf("[Parent] read: '%s'\n", buf);
        close(fd);
        wait(NULL);
    }

    return 0;
}

编译运行:

gcc -o fd_share fd_share.c
./fd_share

输出:

[Child] wrote ' World'
[Parent] read: 'Hello World'

说明: fork后父子进程共享文件偏移量,因此子进程写入后父进程能读取到完整内容。

5.3.3 exec族函数示例

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

int main() {
    pid_t pid;

    // 示例1:execl执行ls命令
    printf("=== Example 1: execl ===\n");
    pid = fork();
    if (pid == 0) {
        // execl: 需要指定完整路径,参数逐个传递,以NULL结尾
        execl("/bin/ls", "ls", "-l", "-a", NULL);
        perror("execl failed");  // 只有execl失败才会执行到这里
        exit(1);
    }
    wait(NULL);

    // 示例2:execv执行ls命令(参数通过数组传递)
    printf("\n=== Example 2: execv ===\n");
    pid = fork();
    if (pid == 0) {
        char *args[] = {"ls", "-l", "-h", NULL};
        execv("/bin/ls", args);
        perror("execv failed");
        exit(1);
    }
    wait(NULL);

    // 示例3:execlp按PATH搜索执行
    printf("\n=== Example 3: execlp ===\n");
    pid = fork();
    if (pid == 0) {
        // execlp: 自动搜索PATH环境变量
        execlp("ls", "ls", "-l", "/tmp", NULL);
        perror("execlp failed");
        exit(1);
    }
    wait(NULL);

    // 示例4:execvp按PATH搜索执行
    printf("\n=== Example 4: execvp ===\n");
    pid = fork();
    if (pid == 0) {
        char *args[] = {"ls", "-l", "-t", NULL};
        // execvp: 自动搜索PATH,参数通过数组传递
        execvp("ls", args);
        perror("execvp failed");
        exit(1);
    }
    wait(NULL);

    // 示例5:execle设置环境变量
    printf("\n=== Example 5: execle ===\n");
    pid = fork();
    if (pid == 0) {
        char *env[] = {"MY_VAR=hello_world", NULL};
        execle("/bin/env", "env", NULL, env);
        perror("execle failed");
        exit(1);
    }
    wait(NULL);

    printf("\nAll exec examples completed.\n");
    return 0;
}

编译运行:

gcc -o exec_example exec_example.c
./exec_example

5.3.4 waitpid与退出状态处理示例

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

void print_exit_status(int status) {
    if (WIFEXITED(status)) {
        printf("  正常退出,退出码: %d\n", WEXITSTATUS(status));
    } else if (WIFSIGNALED(status)) {
        printf("  被信号杀死,信号: %d\n", WTERMSIG(status));
    } else if (WIFSTOPPED(status)) {
        printf("  被信号停止,信号: %d\n", WSTOPSIG(status));
    }
}

int main() {
    pid_t pid1, pid2;
    int status;

    // 创建第一个子进程:正常退出
    pid1 = fork();
    if (pid1 == 0) {
        printf("[Child1] PID=%d, PPID=%d\n", getpid(), getppid());
        sleep(1);
        printf("[Child1] exiting with code 42\n");
        exit(42);  // 退出码42
    }

    // 创建第二个子进程:被信号杀死
    pid2 = fork();
    if (pid2 == 0) {
        printf("[Child2] PID=%d, PPID=%d\n", getpid(), getppid());
        sleep(2);
        printf("[Child2] being killed by SIGSEGV\n");
        char *p = NULL;
        *p = 'x';  // 故意触发段错误
        exit(1);
    }

    // 父进程使用waitpid等待特定子进程
    printf("[Parent] Waiting for child1 (PID=%d)...\n", pid1);
    waitpid(pid1, &status, 0);  // 阻塞等待pid1
    printf("[Parent] Child1 status:");
    print_exit_status(status);

    printf("[Parent] Waiting for child2 (PID=%d)...\n", pid2);
    waitpid(pid2, &status, 0);  // 阻塞等待pid2
    printf("[Parent] Child2 status:");
    print_exit_status(status);

    // 非阻塞等待示例
    printf("\n[Parent] Non-blocking wait example:\n");
    pid_t pid3 = fork();
    if (pid3 == 0) {
        printf("[Child3] sleeping 3 seconds\n");
        sleep(3);
        exit(0);
    }

    // 轮询等待
    while (1) {
        pid_t result = waitpid(pid3, &status, WNOHANG);
        if (result == 0) {
            printf("  [Parent] Child3 still running...\n");
            sleep(1);
        } else if (result > 0) {
            printf("  [Parent] Child3 exited with code %d\n", WEXITSTATUS(status));
            break;
        } else {
            perror("waitpid failed");
            break;
        }
    }

    return 0;
}

编译运行:

gcc -o waitpid_example waitpid_example.c
./waitpid_example

5.3.5 多进程服务器模型示例(嵌入式应用)

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

#define MAX_CLIENTS 5

// 信号处理函数:回收子进程
void sigchld_handler(int sig) {
    int saved_errno = errno;
    // 使用WNOHANG非阻塞回收,避免阻塞
    while (waitpid(-1, NULL, WNOHANG) > 0) {
        continue;
    }
    errno = saved_errno;
}

void handle_client(int client_id) {
    printf("[Client Handler %d] PID=%d, handling client %d\n",
           client_id, getpid(), client_id);

    // 模拟处理客户端请求
    for (int i = 0; i < 3; i++) {
        printf("  [Client %d] Processing request %d...\n", client_id, i + 1);
        sleep(1);
    }

    printf("  [Client %d] Done\n", client_id);
    exit(0);
}

int main() {
    pid_t pids[MAX_CLIENTS];

    // 注册SIGCHLD信号处理函数
    struct sigaction sa;
    sa.sa_handler = sigchld_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
    sigaction(SIGCHLD, &sa, NULL);

    printf("[Server] PID=%d, starting multi-process server\n", getpid());

    // 模拟接受5个客户端连接,创建子进程处理
    for (int i = 0; i < MAX_CLIENTS; i++) {
        pids[i] = fork();
        if (pids[i] < 0) {
            perror("fork failed");
            exit(1);
        } else if (pids[i] == 0) {
            // 子进程:处理客户端
            handle_client(i);
        } else {
            // 父进程:记录子进程PID
            printf("[Server] Forked child process PID=%d for client %d\n",
                   pids[i], i);
        }
    }

    // 父进程:处理其他任务(如接受新连接)
    printf("[Server] Main server running, waiting for children...\n");

    // 等待一段时间让子进程完成
    sleep(8);

    printf("[Server] Server shutting down\n");
    return 0;
}

编译运行:

gcc -o multiproc_server multiproc_server.c
./multiproc_server

5.4 注意事项与易错点

5.4.1 fork使用注意事项

问题 描述 解决方案
fork后未检查返回值 无法区分父进程和子进程 必须检查返回值:pid<0失败,pid=0子进程,pid>0父进程
fork后未处理错误 可能导致程序行为异常 失败时及时exit或错误处理
fork后子进程未调用exec或exit 子进程继续执行父进程代码 明确子进程的执行路径
fork后未wait子进程 产生僵尸进程 使用wait/waitpid或SIGCHLD处理
fork后全局变量不确定 输出顺序与调度顺序有关 不要依赖父子进程的执行顺序

5.4.2 exec使用注意事项

问题 描述 解决方案
exec失败未处理 后续代码会继续执行 exec成功不返回,失败需检查errno
参数传递错误 最后一个参数必须是NULL 列表形式:execl(..., NULL)
路径错误 找不到可执行文件 使用execlp/execvp自动搜索PATH
权限问题 可执行文件无执行权限 检查文件权限:chmod +x
环境变量问题 execle/execve需要手动传递环境 使用environ或自定义env数组

5.4.3 wait/waitpid使用注意事项

问题 描述 解决方案
忘记调用wait 僵尸进程堆积,占用进程表 及时回收子进程
wait阻塞问题 多个子进程时阻塞等待 使用waitpid+WNOHANG非阻塞
状态宏使用错误 WIFEXITED为假时使用WEXITSTATUS 先判断WIFEXITED再获取退出码
信号竞争 SIGCHLD信号可能在wait前到达 使用SA_RESTART或sigwait
僵尸进程处理 未注册SIGCHLD处理函数 使用sigaction注册信号处理

5.4.4 常见错误代码示例

// 错误示例1:未检查fork返回值
pid_t pid = fork();
// 危险!pid可能是-1

// 正确做法
pid_t pid = fork();
if (pid < 0) {
    perror("fork");
    exit(1);
}

// 错误示例2:exec失败后继续执行
if (pid == 0) {
    execl("/bin/ls", "ls", NULL);
    // 如果execl失败,这里会继续执行
    printf("This will execute if execl fails\n");
    exit(1);  // 必须exit,否则子进程会继续执行父进程代码
}

// 错误示例3:僵尸进程未回收
if (pid == 0) {
    exit(0);  // 子进程退出
}
// 父进程未调用wait,子进程变成僵尸
sleep(10);
// 僵尸进程占用进程表项

5.4.5 嵌入式场景注意事项

  1. 资源限制:嵌入式系统资源有限,避免创建过多子进程
  2. 内存管理:注意COW机制可能的内存开销
  3. 信号处理:使用sigaction替代signal,避免竞态条件
  4. 守护进程:使用double fork创建守护进程
  5. 进程监控:实现看门狗机制,监控子进程健康状态

5.5 面试要点

Q1: fork()的返回值是什么?为什么能返回两次?

答:

  • fork()成功后,父进程返回子进程的PID(>0)子进程返回0
  • 失败时返回-1
  • 能返回两次的原因:fork通过复制父进程创建子进程,两个进程都从fork调用点继续执行,但内核在不同进程中设置不同的返回值

Q2: 什么是写时复制(COW)?有什么优势?

答:

  • 定义:fork时不立即复制整个地址空间,而是让父子进程共享物理内存页,页表项标记为只读
  • 触发条件:当任一进程尝试写入共享页时,触发缺页异常,内核才真正复制该页
  • 优势:大幅减少fork的开销,特别是对于大型进程(如占用GB内存的程序)
  • 实现:Linux通过页表项的写保护位实现,是现代操作系统的标准优化

Q3: fork后父子进程有哪些区别?

答:

区别 父进程 子进程
PID 原PID 新分配的唯一PID
fork返回值 子进程PID 0
文件描述符 独立的FD表 复制父进程的FD表(共享文件偏移量)
信号处理 原有设置 复制父进程的设置
资源限制 原有限制 继承父进程的限制

Q4: exec函数族的作用是什么?为什么成功不返回?

答:

  • 作用:替换当前进程的映像,加载并执行另一个程序
  • 为什么不返回:成功时,进程的代码段、数据段、堆栈都被新程序替换,控制权转移到新程序入口点,原程序的返回地址已不存在
  • 失败情况:如果找不到程序、权限不足等,exec返回-1,原程序继续执行

Q5: 僵尸进程是如何产生的?如何避免?

答:

  • 产生原因:子进程退出后,内核保留其PCB信息等待父进程读取;如果父进程不调用wait,子进程变成僵尸
  • 危害:占用进程表项,长期积累可能导致系统无法创建新进程
  • 避免方法
    1. 父进程及时调用wait/waitpid回收
    2. 注册SIGCHLD信号处理函数,自动回收
    3. 使用double fork:孙进程由init回收
    4. 父进程忽略SIGCHLD信号:signal(SIGCHLD, SIG_IGN)

Q6: wait和waitpid有什么区别?

答:

特性 wait waitpid
阻塞 一直阻塞 可设置WNOHANG非阻塞
目标 任意子进程 可指定特定PID
并发 一次处理一个 可处理多个子进程
使用场景 简单的单子进程 复杂的多子进程管理