当前位置: 首页 > news >正文

Linux系统编程(5):进程进阶——exec 函数族、进程退出与资源回收

前言

本文整理自 Linux 系统编程中关于exec 函数族strtok 字符串解析进程退出方式(exit / _exit / atexit)、wait / waitpid 进程回收的学习笔记与代码示例。所有代码均附有详细注释。

一、exec 函数族——替换进程镜像

1. 概述

fork 之后,子进程和父进程执行的是相同的代码。如果希望子进程去执行一个全新的程序,就需要用到 exec 函数族。

exec 函数族的核心功能:用新进程的镜像替换原有进程的镜像

进程 = [text | data | bss | 堆栈] + PCB

其中 [text | data | bss | 堆栈] 就是镜像部分。

exec 调用成功后,原有的代码、数据、堆栈全部被新程序替换,但PID 不变——本质上还是同一个进程,只是"脱胎换骨"了。

2. exec 函数族总览

int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg, ..., char *const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execvpe(const char *file, char *const argv[], char *const envp[]);

3. 命名规律与区分

函数名由以下几个字母组合而成,记住规律就很容易区分:

字母含义说明
l(list)逐个罗列参数从第二个参数开始,逐个写出来,以 NULL 结尾
v(vector)参数用数组传入从第二个参数开始,组织成指针数组
p(PATH)从环境变量 PATH 中查找不需要指定完整路径,只需文件名
e(environ)传递自定义环境变量通过 envp 数组传递环境变量给新程序

l vs v 的区别

// l —— list,逐个罗列参数 execl("/bin/ls", "ls", "-l", "/", NULL); // v —— vector,用数组传入参数 char *const args[] = {"ls", "-l", "/", NULL}; execv("/bin/ls", args);

两者功能完全相同,只是参数传递方式不同。

带 p —— 在 PATH 中查找

// 不带 p,需要指定完整路径 execl("/bin/ls", "ls", "-l", "/", NULL); // 带 p,系统会到 PATH 环境变量中查找,不需要指定路径 execlp("ls", "ls", "-l", "/", NULL);

可以用which ls命令查看可执行文件的完整路径。

带 e —— 传递自定义环境变量

// 什么时候用? // 如果要执行的程序需要用到自定义的环境变量 // 方式一:使用系统默认环境变量 extern char **environ; execle("/usr/bin/env", "env", NULL, environ); // 方式二:使用自定义环境变量 char *const envp[] = {"USR=linux", "PASSWD=123456", NULL}; execle("/usr/bin/env", "env", NULL, envp);

其中extern char **environ是系统环境变量的指针,环境变量存放在一个char *的指针数组中。

4. exec 调用成功后的特征

exec 调用成功后,原进程中 exec 之后的代码将不再被执行,因为整个进程的镜像已经被替换了。只有调用失败时才会继续执行后续代码。

5. 基本代码示例

execl 示例
#include <stdio.h> #include <unistd.h> int main(int argc, const char *argv[]) { printf("exec start\n"); // execl:逐个罗列参数,第一个参数是完整路径 // "/bin/ls" —— 可执行文件的路径(必须包含文件名) // "ls" —— argv[0],程序名 // "-l", "/" —— 命令行参数 // NULL —— 参数列表结束标志 execl("/bin/ls", "ls", "-l", "/", NULL); // 以下代码只有 execl 调用失败才会执行 // 因为成功后整个进程镜像已被替换为 ls printf("exec end\n"); return 0; }
execlp 示例
#include <stdio.h> #include <unistd.h> int main(int argc, const char *argv[]) { printf("exec start\n"); // execlp:带 p,系统会到 PATH 环境变量中查找 cp // 不需要写 "/bin/cp",只需写 "cp" execlp("cp", "cp", "1.txt", "2.txt", NULL); printf("exec end\n"); return 0; }
execv 示例
#include <stdio.h> #include <unistd.h> int main(int argc, const char *argv[]) { printf("exec start\n"); // execv:参数用数组方式传入 // args[0] = "ls" → 程序名 // args[1] = "-l" → 参数 // args[2] = "/" → 参数 // args[3] = NULL → 结束标志 char *const args[] = {"ls", "-l", "/", NULL}; execv("/bin/ls", args); printf("exec end\n"); return 0; }
execvp 示例
#include <stdio.h> #include <unistd.h> int main(int argc, const char *argv[]) { printf("exec start\n"); // execvp:带 p + v // p:在 PATH 中查找,不需要完整路径 // v:参数用数组传入 char *const args[] = {"ls", "-l", "/", NULL}; execvp("ls", args); printf("exec end\n"); return 0; }
execle / execvpe 示例(传递环境变量)
#include <stdio.h> #include <unistd.h> // 系统默认环境变量 extern char **environ; // 自定义环境变量数组 char *const envp[] = {"USR=linux", "PASSWD=123456", NULL}; int main(int argc, const char *argv[]) { // 方式一:execle + 系统默认环境变量 // execle("/usr/bin/env", "env", NULL, environ); // 方式二:execle + 自定义环境变量 // execle("/usr/bin/env", "env", NULL, envp); // 方式三:execvpe + 自定义环境变量 // 带 p:在 PATH 中查找;带 v:参数数组;带 e:自定义环境变量 char *const args[] = {"env", NULL}; execvpe("env", args, envp); // 也可用于登录程序 // execle("/bin/login", "login", NULL, envp); return 0; }

二、strtok 字符串解析函数

在实现后续的"简易 shell"之前,需要先掌握strtok函数,用于将用户输入的命令行字符串按空格拆分成各个参数。

函数原型

#include <string.h> char *strtok(char *str, const char *delim);
  • 功能:提取字符串中的子字符串(按分隔符拆分)。
  • 参数
    • str:要解析的字符串;连续拆分时传 NULL,表示接着上次的位置继续拆分。
    • delim:分隔符字符串。
  • 返回值
    • 成功返回子字符串的首地址。
    • 失败或字符串解析完毕返回 NULL。
  • 注意:strtok 会修改原始字符串(将分隔符替换为\0)。

strtok 示例代码

#include <stdio.h> #include <string.h> int main(int argc, const char *argv[]) { int i = 0; char buf[] = "ls;-l:/"; // 定义指针数组,用于存放拆分后的各子串 char *ret[10] = {NULL}; // 第一次调用:传入原始字符串 buf,分隔符为 " :;" ret[0] = strtok(buf, " :;"); // 后续调用:传入 NULL,表示继续从上次位置拆分 // 直到返回 NULL 为止 while (ret[++i] = strtok(NULL, " :;")) ; // 打印拆分结果 for (i = 0; i < 3; ++i) { printf("ret[%d] = %s\n", i, ret[i]); } // 输出: // ret[0] = ls // ret[1] = -l // ret[2] = / // 验证:strtok 会修改原始字符串 // 原始的 "ls;-l:/" 中的分隔符会被替换为 '\0' for (i = 0; i < sizeof(buf); ++i) { printf("buf[%d] = %d:%c\n", i, buf[i], buf[i]); } // 可以看到 ';', ':', '/' 都被替换成了 '\0'(ASCII 码 0) return 0; }

三、进程退出(结束)

1. 进程终止的 8 种情况

分为正常终止异常终止两大类:

正常终止(3~5 种)
编号方式说明
1main函数中return最常见的方式
2exit(status)库函数,会刷新 I/O 缓冲区,关闭所有流和打开的文件,并调用 atexit 注册的清理函数
3_exit(status)/_Exit(status)系统调用,会关闭所有文件,但不刷新缓冲区不执行清理函数
4主线程退出(多线程场景)
5主线程调用pthread_exit(多线程场景)
异常终止(6~8 种)
编号方式说明
6abort()发送 SIGABRT 信号终止进程
7signal/kill发信号结束进程,如kill -9 PID
8最后一个线程被pthread_cancel(多线程场景)

2. exit 函数详解

#include <stdlib.h> void exit(int status);
  • 功能:让进程退出,并刷新缓冲区
  • 参数status为进程退出状态。
  • 常用宏:EXIT_SUCCESS(0)、EXIT_FAILURE(1)。
  • 退出状态只取低 8 位(status & 0377)。

exit 的执行流程

exit → 刷新缓冲区 → 调用 atexit 注册的退出函数 → 调用 _exit

3. _exit 函数详解

#include <unistd.h> void _exit(int status);
  • 功能:让进程退出,不刷新缓冲区
  • 参数status为进程退出状态。
  • 是系统调用,比 exit 更"底层"。

exit 与 _exit 的关键区别

// exit 示例 printf("hello world!"); // 注意没有 \n,内容在缓冲区中 sleep(3); exit(0); // exit 会刷新缓冲区 → 屏幕上能看到 "hello world!" // _exit 示例 printf("hello world!"); // 注意没有 \n,内容在缓冲区中 sleep(3); _exit(0); // _exit 不刷新缓冲区 → 屏幕上看不到 "hello world!"

4. atexit 注册退出清理函数

#include <stdlib.h> int atexit(void (*function)(void));
  • 功能:注册进程退出前要执行的清理函数。
  • 参数function为函数指针,指向void返回值、void参数的函数。
  • 返回值:成功返回 0,失败返回非 0。
  • 调用顺序:多个清理函数按照注册的倒序(后注册的先调用)被调用。

重要注意事项

  • exit调用时执行 atexit 注册的函数;_exit调用时不会执行。
  • 只有在进程正常结束时才会被调用(main 中 return 或 exit)。
  • atexit 可以多次注册多个清理函数。
atexit 示例代码
#include <stdio.h> #include <unistd.h> #include <stdlib.h> // 清理函数 1 void cleanup1(void) { printf("1 cleanup free malloc\n"); } // 清理函数 2 void cleanup2(void) { printf("2 cleanup free malloc\n"); } int main(int argc, const char *argv[]) { // 按顺序注册两个清理函数 atexit(cleanup1); // 先注册 atexit(cleanup2); // 后注册 printf("hello world!"); sleep(3); exit(0); // 调用 exit 正常退出 // 以下代码不会执行 while (1) sleep(1); return 0; }

运行结果

hello world!(等待 3 秒后) 2 cleanup free malloc ← 后注册的先调用 1 cleanup free malloc ← 先注册的后调用

执行顺序:先打印hello world!,等待 3 秒后,exit 触发 atexit 注册的清理函数,后注册的 cleanup2 先执行,先注册的 cleanup1 后执行

_exit 示例代码(不执行 atexit 清理函数)
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main(int argc, const char *argv[]) { printf("hello world!"); // 没有 \n,内容在缓冲区中 sleep(3); _exit(0); // _exit 不刷新缓冲区,不执行 atexit while (1) sleep(1); return 0; }

运行结果

(等待 3 秒后,直接退出,屏幕上什么都没有)
  • 缓冲区中的hello world!没有被刷新。
  • 即使注册了 atexit 清理函数也不会被调用。

5. return 关键字

  • return出现在main函数中时,效果等同于调用exit,可以结束进程。
  • return出现在其他函数中时,只是结束该函数的调用,不会结束进程。

四、wait 和 waitpid——回收子进程资源

1. 为什么需要 wait

当子进程结束时,如果父进程不回收其资源,子进程就会变成僵尸进程(Z 状态),占用系统资源。waitwaitpid就是用来回收子进程资源获取退出状态的函数。

2. wait 函数

#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *wstatus);
  • 功能阻塞等待任意子进程退出,并回收该进程的资源和获取退出状态值。
  • 参数
    • wstatus:用于接收子进程的退出状态;如果不关心退出状态,传NULL
  • 返回值:成功返回回收到的子进程 PID;失败返回 -1。
  • 注意:wait 是一个阻塞操作——如果所有子进程都在运行,父进程会一直卡在 wait 处等待。
退出状态解析宏
功能
WIFEXITED(wstatus)判断子进程是否正常结束
WEXITSTATUS(wstatus)获取正常结束时的退出状态值
WIFSIGNALED(wstatus)判断子进程是否因信号而结束
WTERMSIG(wstatus)获取导致结束的信号编号
wait 示例代码
#include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <sys/wait.h> #include <stdlib.h> int main(int argc, const char *argv[]) { // 创建子进程 pid_t pid = fork(); if (pid < 0) { perror("fork fail"); return -1; } // 父进程:阻塞等待子进程结束 if (pid > 0) { while (1) { printf("father hello world!\n"); int status; // wait 阻塞等待任意子进程退出 // status 中保存子进程的退出状态 if (wait(&status) < 0) { perror("wait fail"); return -1; } // 判断子进程是正常结束还是信号结束 if (WIFEXITED(status)) { // 正常结束:打印退出状态值 printf("status = %d\n", WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { // 信号结束:打印信号编号 printf("signo = %d\n", WTERMSIG(status)); } } } // 子进程:循环 10 次后退出,退出状态为 99 else if (pid == 0) { int n = 10; while (1) { sleep(1); printf("child hello world!\n"); n--; // 子进程循环打印 10 次后退出 // 注意:n 变为 0 后还会再循环一次(while(1)) // 实际上会死循环,除非 n 到 0 时 break 或用 while(n) } exit(99); // 以状态值 99 退出 } return 0; }

3. waitpid 函数——更灵活的版本

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

参数说明

pid 参数
pid 的值含义
< -1回收**进程组 ID 等于
-1回收任意子进程(不限进程组)
0回收与调用进程同组的任意子进程
> 0回收指定 PID的子进程
waitpid(-1, &status, 0) ← 等价于 wait(&status)
options 参数
options 的值含义
0阻塞模式——没有子进程退出就一直等待
WNOHANG非阻塞模式——如果没有子进程退出,立即返回 0
返回值
情况返回值
成功回收到子进程返回该子进程的 PID
非阻塞模式下没有子进程状态改变返回 0
失败返回 -1
waitpid 三种行为
  1. 1.如果所有子进程都在运行,阻塞等待
  2. 2.如果有子进程终止,立即获得终止状态并返回
  3. 3.如果没有子进程,立即出错退出(返回 -1)。
waitpid + WNOHANG 非阻塞示例代码
#include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <sys/wait.h> #include <stdlib.h> int main(int argc, const char *argv[]) { // 创建子进程 pid_t pid = fork(); if (pid < 0) { perror("fork fail"); return -1; } // 父进程 if (pid > 0) { while (1) { printf("father hello world!\n"); int status; int ret = -1; // waitpid 非阻塞回收 // -1:回收任意子进程 // WNOHANG:非阻塞模式 // 返回值 > 0:成功回收到的子进程 PID // 返回值 == 0:没有子进程状态改变(还在运行) // 返回值 < 0:出错(如没有子进程了) if ((ret = waitpid(-1, &status, WNOHANG)) < 0) { perror("wait fail"); return -1; } // ret != 0 表示成功回收到了一个子进程 if (ret != 0) { printf("ret = %d\n", ret); // 打印回收到的子进程 PID if (WIFEXITED(status)) { // 正常结束:打印退出状态值 printf("status = %d\n", WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { // 信号结束:打印信号编号 printf("signo = %d\n", WTERMSIG(status)); } } sleep(1); // 每隔 1 秒检查一次 } } // 子进程:循环 10 次后退出 else if (pid == 0) { int n = 10; while (n) { sleep(1); printf("child hello world!\n"); n--; } exit(99); // 以状态值 99 退出 } return 0; }

运行结果(子进程运行期间,父进程非阻塞地做自己的事,子进程结束后立即回收):

father hello world! child hello world! father hello world! child hello world! ...(重复 10 次) child hello world! ← 子进程最后一次打印 father hello world! ret = 6038 ← 父进程成功回收到子进程 status = 99 ← 子进程的退出状态值 father hello world! father hello world! ← 父进程继续运行(但子进程已回收完毕,后续 waitpid 返回 -1)

4. wait vs waitpid 对比

对比项waitwaitpid
阻塞/非阻塞只有阻塞版本可以非阻塞(WNOHANG)
选择性回收只能回收任意一个子进程可以指定 PID回收
等价关系wait(&status)waitpid(-1, &status, 0)

注意:waitpid 配合 WNOHANG 实现非阻塞回收时,要想真正完成"收尸"操作,必须使用轮询(while 循环不断检查)。后面还可以学习异步回收的方式——通过信号来通知父进程。


五、fork 之后文件描述符的共享

fork 之后,父子进程共享:

  1. 1.子进程会继承父进程中已经打开的文件描述符
  2. 2.父子进程使用的是同一个文件表
  3. 3.相互之间的文件操作会受到影响

解决方案:如果不想相互间受到影响,可以在 fork之后再打开文件,这样父子进程各自独立操作各自的文件。


六、简易 Shell(myshell)实现

综合 fork、exec、strtok、wait,可以实现一个简易的 shell 程序。

版本 1:使用 sleep 等待(不推荐)

#include <stdio.h> #include <unistd.h> #include <string.h> int main(int argc, const char *argv[]) { char buf[1024] = {0}; while (1) { // 1. 打印提示符 printf("myshell$ "); // 2. 从键盘读取一行输入 fgets(buf, sizeof(buf), stdin); // 3. 判断是否退出 if (strncmp(buf, "exit", 4) == 0 || strncmp(buf, "quit", 4) == 0) { printf("exit myshell ......\n"); return 0; } // 4. 用 strtok 将输入按空格和换行拆分,存入 args 数组 // 例如输入 "ls -l /" → args[0]="ls", args[1]="-l", args[2]="/", args[3]=NULL int i = 0; char *args[10] = {NULL}; args[0] = strtok(buf, " \n"); while (args[++i] = strtok(NULL, " \n")) ; // 5. fork 创建子进程 pid_t pid = fork(); if (pid < 0) { perror("fork fail"); return -1; } if (pid > 0) { // 父进程:用 sleep(1) 等待子进程(不推荐,无法精确等待) sleep(1); continue; } else if (pid == 0) { // 子进程:调用 execvp 执行用户输入的命令 // execvp 带 p:在 PATH 中查找;带 v:参数数组 if (execvp(args[0], args) < 0) { perror("execvp fail"); return -1; } } } return 0; }

问题:父进程用sleep(1)等待子进程,不够精确,可能导致子进程还没执行完父进程就开始下一轮循环了。

版本 2:使用 wait 回收(推荐)

#include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/wait.h> int main(int argc, const char *argv[]) { char buf[1024] = {0}; while (1) { // 1. 打印提示符 printf("myshell$ "); // 2. 读取用户输入 fgets(buf, sizeof(buf), stdin); // 3. 判断退出 if (strncmp(buf, "exit", 4) == 0 || strncmp(buf, "quit", 4) == 0) { printf("exit myshell ......\n"); return 0; } // 4. 解析命令参数 int i = 0; char *args[10] = {NULL}; args[0] = strtok(buf, " \n"); while (args[++i] = strtok(NULL, " \n")) ; // 5. fork 创建子进程 pid_t pid = fork(); if (pid < 0) { perror("fork fail"); return -1; } if (pid > 0) { // 父进程:阻塞等待子进程结束(推荐方式) // 比 sleep(1) 更精确,确保子进程执行完毕 wait(NULL); // 不关心退出状态,传 NULL } else if (pid == 0) { // 子进程:执行命令 if (execvp(args[0], args) < 0) { perror("execvp fail"); return -1; } } } return 0; }

改进点:用wait(NULL)替代sleep(1),父进程会阻塞等待子进程执行完毕后再继续下一轮循环。

myshell 流程图

开始 ↓ 打印提示符 "myshell$" ↓ fgets 从键盘读入字符串 ↓ 判断是否为 "exit"/"quit" ├── 是 → 程序结束 └── 否 ↓ strtok 分割字符串,存入指针数组 ↓ fork 创建子进程 ├── 父进程 → wait 等待子进程结束 → 回到循环开头 └── 子进程 → execvp 执行命令 → 结束

七、总结

本文覆盖了以下核心知识点:

知识点关键内容
exec 函数族execl / execlp / execle / execv / execvp / execvpe
l vs v 区别l 逐个罗列参数,v 用数组传参
p 的含义在 PATH 环境变量中查找,不需要完整路径
e 的含义传递自定义环境变量
strtok字符串拆分函数,首次传原始串,后续传 NULL
exit vs _exitexit 刷新缓冲区并执行 atexit 清理;_exit 不刷新不清理
atexit注册退出清理函数,倒序调用,仅在正常退出时执行
wait阻塞等待任意子进程退出并回收资源
waitpid更灵活,支持指定 PID、支持非阻塞(WNOHANG)
WIFEXITED / WEXITSTATUS判断正常结束并获取退出值
WIFSIGNALED / WTERMSIG判断信号结束并获取信号编号
简易 shellfork + execvp + strtok + wait 组合实现

exec 函数族是 Linux 进程编程的核心,配合 fork 就能实现"创建新进程并执行新程序"的能力。wait / waitpid 则是避免僵尸进程、回收子进程资源的必备工具。建议结合代码动手实践,尤其是简易 shell 的实现,它综合了本篇和上一篇的大部分知识点。

http://www.cnnetsun.cn/news/4071850.html

相关文章:

  • “看小说顺便学会英语是不是很酷”App项目求iOS开发搭档
  • 基于nodejs的二手交易系统(源码+文档+部署讲解等)
  • Qwen3.8-27B大模型本地部署指南:17GB内存即可运行
  • IDM激活脚本完全入门:告别试用到期弹窗,一次冻结永久安心
  • 如何在本地运行Maple-Preview:llama.cpp部署完整教程
  • LLM智能体结构化记忆管理:战略性遗忘机制的设计与实践
  • OOTDiffusion AI 虚拟试衣技术部署与使用教程
  • Gemini 3.5 助力科研全流程提效指南,精准抓住每个核心瓶颈!
  • 博士开题全攻略:从选题到答辩的学术地图绘制指南
  • 理想汽车战略升维:从增程技术到全栈自研的生态布局
  • LLM-Cave: A benchmark and light environment for large language models reasoning and decision-maki...
  • Power Query自定义字段进阶:掌握M语言运算符与if逻辑实现数据转换
  • 魔兽争霸3修复工具WarcraftHelper:老游戏在现代电脑上重获新生的免费方案
  • GaussDB(DCS) 翻车实录:我用 ZREVRANGEBYSCORE 搞排行榜,差点被“大Key”和“Java Stream”联手送走!
  • Arch Linux Python 3.14 安装 PaddlePaddle 报错 “No matching distribution found“ 解决方案
  • Linux 桌面上看哔哩哔哩,为什么值得多装一个客户端?bilibili-linux 从安装到玩转的完整指南
  • 如何找回消失的网页:Wayback Machine 浏览器扩展完整上手攻略
  • Qwen 3.8 27B大模型本地部署与微调实战指南
  • ArmCord快速找回Mac窗口最小化快捷键:Command+M 原生体验的一次小手术
  • 基于TokenSpeed推理引擎的Qwen3.8大模型高性能部署实战指南
  • EndNote与Word协同问题全解析:从样式错乱到PDF转换的终极解决方案
  • TVA-World架构:开启具身智能时代新纪元(11)
  • CMake基础语法
  • 从客厅到笔记本,一篇文章玩转 Jellyfin Desktop 桌面客户端
  • 单链表算法题(二):进阶技巧篇
  • ACE项目解析:自适应上下文弹性扩展器如何解决LLM智能体上下文焦虑
  • 3分钟给《植物大战僵尸》装上免费宽屏,PvZWidescreen让画面从800宽变成1066
  • Oracle数据库核心架构解析与实战入门指南
  • 企业开箱即用 Agent 和自研 Agent 平台怎么选?——看业务标准化程度、系统复杂度与长期扩展需求
  • 第23篇:动态模板 dynamic_table:让大模型自定义表格列定义