一、exec 族函数:进程的 “程序替换” 神器

1.1 核心功能

exec 族函数的核心作用是替换当前进程的代码段、数据段、堆、栈—— 执行 exec 后,进程的 PID 不变,但运行的程序会被完全替换为新的可执行文件;若 exec 执行成功,原进程的后续代码不会执行;若执行失败,才会继续执行原进程代码。

exec 族函数通常与 fork 搭配使用:父进程 fork 创建子进程,子进程执行 exec 替换为目标程序,既保证父进程不被替换,又能通过子进程执行任意可执行文件。

1.2 内存视角的变化

阶段 进程内存状态
exec 执行前 进程运行原程序的代码段、数据段、堆、栈
exec 执行后 原内存区域被新程序覆盖,仅保留 PID、文件描述符等内核态信息
新程序执行结束 整个进程终止(无需返回原程序)

1.3 exec 族 4 个核心函数

exec 族函数有多个变体,核心差异在于参数形式程序路径查找方式,以下是最常用的 4 个函数:

函数原型 核心特点 参数说明
int execl(const char *path, const char *arg, ...); l=list(参数列表),需指定程序绝对 / 相对路径 path:程序路径 + 文件名(如/bin/ls);arg:参数列表,以 NULL 结尾(如execl("/bin/ls", "ls", "-l", NULL)
int execlp(const char *file, const char *arg, ...); l=list,p=PATH(自动从环境变量 PATH 查找程序) file:程序名(如ls),无需写路径;arg:参数列表,以 NULL 结尾(如execlp("ls", "ls", "-l", NULL)
int execv(const char *path, char *const argv[]); v=vector(数组),需指定程序路径 path:程序路径 + 文件名;argv:参数数组,最后一个元素为 NULL(如char *argv[] = {"ls", "-l", NULL}; execv("/bin/ls", argv)
int execvp(const char *file, char *const argv[]); v=vector,p=PATH(自动查找程序) file:程序名;argv:参数数组,以 NULL 结尾(如char *argv[] = {"ls", "-l", NULL}; execvp("ls", argv)
共性说明
  • 返回值:仅执行失败时返回 - 1(成功则无返回);
  • 参数规则:第一个参数(arg/argv [0])通常是程序名(与 file/path 一致),后续为程序的运行参数;
  • 路径规则:若要执行自定义可执行程序(非系统命令),4 个函数的第一个参数都需填写路径 + 文件名(如./myprog)。
示例:execlp 执行 ls 命令

c

运行

#include <unistd.h>
#include <stdio.h>
int main() {
    // 子进程执行ls -l,自动从PATH查找ls
    if (fork() == 0) {
        execlp("ls", "ls", "-l", NULL);
        perror("execlp failed"); // 仅exec失败时执行
        return 1;
    }
    return 0;
}

二、waitpid:子进程资源的 “回收者”

当子进程终止(正常 / 异常),其用户空间内存会释放,但内核中的 PCB(进程控制块)需父进程主动回收,否则会变成僵尸进程。waitpid 是回收子进程资源的核心函数,也是 wait 的增强版。

2.1 函数原型与参数

c

运行

#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
参数 取值与含义
pid -1:回收所有子进程;>0:回收指定 PID 的子进程;0:回收同组的子进程;<-1:回收指定进程组的子进程
status 存储子进程退出状态(不关心则传 NULL);可通过宏解析状态(见 2.3)
options 0:阻塞模式(父进程等待子进程终止);WNOHANG:非阻塞模式(无子进程终止则立即返回)

2.2 返回值说明

返回值 含义
>0 成功回收的子进程 PID
0 WNOHANG 模式下,无子进程终止(需再次尝试回收)
-1 回收失败(如无待回收的子进程、系统错误)

2.3 退出状态解析宏

通过以下宏可解析 status 参数,判断子进程终止方式:

功能 配套使用
WIFEXITED(status) 判断是否正常终止(return/exit/_exit) 是:WEXITSTATUS (status) 获取退出码
WEXITSTATUS(status) 获取正常终止的退出码(0-255) 仅 WIFEXITED 为真时有效
WIFSIGNALED(status) 判断是否被信号异常终止(如 kill -9) 是:WTERMSIG (status) 获取信号编号
WTERMSIG(status) 获取终止子进程的信号编号 仅 WIFSIGNALED 为真时有效

2.4 核心用法示例

示例 1:阻塞回收指定子进程

c

运行

#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
int main() {
    pid_t pid = fork();
    if (pid == 0) {
        execlp("ls", "ls", NULL);
        exit(1);
    }
    // 阻塞等待pid对应的子进程终止
    int status;
    waitpid(pid, &status, 0);
    if (WIFEXITED(status)) {
        printf("子进程正常退出,退出码:%d\n", WEXITSTATUS(status));
    }
    return 0;
}
示例 2:非阻塞回收所有子进程

c

运行

#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
int main() {
    // 创建多个子进程...
    while (1) {
        pid_t ret = waitpid(-1, NULL, WNOHANG);
        if (ret == 0) {
            printf("暂无子进程退出,稍后重试\n");
            usleep(100000); // 100ms后重试
        } else if (ret == -1) {
            printf("所有子进程已回收\n");
            break;
        } else {
            printf("回收子进程PID:%d\n", ret);
        }
    }
    return 0;
}
等价关系

waitpid(-1, status, 0) 完全等价于 wait(status)—— 阻塞回收任意子进程。

三、system 函数:fork+exec 的 “便捷封装”

system 函数是对 fork+exec+waitpid 的封装,可直接执行 shell 命令,无需手动处理进程创建和回收。

3.1 函数原型与功能

c

运行

#include <stdlib.h>
int system(const char *command);
  • 功能:执行指定的 shell 命令(如system("ls -l"));
  • 实现逻辑:
    1. fork 创建子进程;
    2. 子进程执行 exec 调用 shell(如 /bin/sh)执行 command;
    3. 父进程 waitpid 等待子进程终止。

3.2 返回值

  • -1:fork/exec 失败;
  • 其他值:子进程的退出状态(可通过 waitpid 的宏解析)。

3.3 使用限制

system 执行的命令无法修改父进程状态(如 cd 命令)—— 因为命令在子进程中执行,子进程的目录切换、环境变量修改等操作不会影响父进程。

适合场景:执行信息输出、文件操作等无状态修改的命令(如 ls、cp、cat);不适合场景:需要修改父进程状态的操作(如 cd、export)。

3.4 示例:system 执行 cp 命令

c

运行

#include <stdlib.h>
int main() {
    // 执行cp 1.txt 2.txt
    int ret = system("cp 1.txt 2.txt");
    if (ret == -1) {
        perror("system failed");
    }
    return 0;
}

四、工作路径控制:getcwd 与 chdir

在 Shell、进程管理场景中,获取 / 修改当前工作路径是高频操作,核心依赖 getcwd 和 chdir 函数。

4.1 获取当前工作路径:getcwd

c

运行

#include <unistd.h>
char *getcwd(char *buf, size_t size);
  • 功能:将当前工作目录的绝对路径存入 buf;
  • 参数:
    • buf:存储路径的字符数组;
    • size:buf 的最大长度(避免越界);
  • 返回值:成功返回 buf 指针,失败返回 NULL。
示例:打印当前工作路径

c

运行

#include <unistd.h>
#include <stdio.h>
int main() {
    char buf[512];
    if (getcwd(buf, sizeof(buf)) != NULL) {
        printf("当前工作路径:%s\n", buf);
    } else {
        perror("getcwd failed");
    }
    return 0;
}

4.2 切换工作路径:chdir

c

运行

#include <unistd.h>
int chdir(const char *path);
  • 功能:修改当前进程的工作路径;
  • 参数:path 为目标路径(绝对 / 相对路径);
  • 返回值:成功返回 0,失败返回 - 1。
示例:切换到 /tmp 目录

c

运行

#include <unistd.h>
#include <stdio.h>
int main() {
    if (chdir("/tmp") == 0) {
        printf("切换到/tmp成功\n");
        // 验证:打印新路径
        char buf[512];
        getcwd(buf, sizeof(buf));
        printf("新工作路径:%s\n", buf);
    } else {
        perror("chdir failed");
    }
    return 0;
}

关键注意点

chdir 仅修改当前进程的工作路径:

  • 若在子进程中执行 chdir,父进程的路径不会变化;
  • Shell 的 cd 命令必须在主进程执行(而非子进程),否则路径切换不生效。

五、实战:fork+exec+waitpid 实现 MiniShell 核心

结合以上知识点,实现支持 cd/ls/cp/cat 的 MiniShell 核心逻辑(无 system,纯 fork+exec):

c

运行

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

#define MAX_LINE 1024
#define MAX_ARGS 10

int parse_cmd(char *line, char **argv) {
    int argc = 0;
    char *token = strtok(line, " \n");
    while (token != NULL && argc < MAX_ARGS-1) {
        argv[argc++] = token;
        token = strtok(NULL, " \n");
    }
    argv[argc] = NULL;
    return argc;
}

void exec_cmd(char **argv) {
    pid_t pid = fork();
    if (pid < 0) { perror("fork failed"); return; }
    if (pid == 0) {
        execvp(argv[0], argv);
        perror("command not found");
        exit(1);
    } else {
        waitpid(pid, NULL, 0);
    }
}

int main() {
    char line[MAX_LINE], *argv[MAX_ARGS], cwd[512];
    while (1) {
        // 打印带当前路径的提示符
        getcwd(cwd, sizeof(cwd));
        printf("%s > ", cwd);
        fflush(stdout);

        if (fgets(line, MAX_LINE, stdin) == NULL) break;
        int argc = parse_cmd(line, argv);
        if (argc == 0) continue;

        // 内置命令:exit
        if (!strcmp(argv[0], "exit")) exit(0);
        // 内置命令:cd(主进程执行)
        if (!strcmp(argv[0], "cd")) {
            char *dir = argc>1 ? argv[1] : getenv("HOME");
            if (chdir(dir) == -1) perror("cd failed");
            continue;
        }
        // 外部命令:ls/cp/cat(fork+exec)
        exec_cmd(argv);
    }
    return 0;
}

六、核心总结

  1. exec 族:程序替换核心,fork+exec 是 Linux 进程编程的经典组合,exec 成功则进程被替换,失败才返回;
  2. waitpid:子进程资源回收的唯一方式,阻塞 / 非阻塞模式适配不同场景,避免僵尸进程;
  3. system:便捷但受限,无法修改父进程状态,底层是 fork+exec+waitpid;
  4. 路径控制:getcwd 获取当前路径,chdir 修改路径(仅影响当前进程);
  5. 核心原则:需修改父进程状态的操作(如 cd)必须在主进程执行,无需修改状态的命令(如 ls/cp)可通过 fork+exec 在子进程执行。
Logo

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

更多推荐