进程控制:exec 函数族

进程控制:exec 函数族

1. exec 函数族概述

exec 函数族是 Linux/Unix 系统中用于进程控制的核心函数之一,其主要功能是用指定的程序替换当前进程的内存映像

1.1 核心概念

  • 进程替换:调用 exec 函数后,当前进程的代码段、数据段、堆栈等都被新程序完全替换
  • 进程标识保留:进程 ID(PID)、父进程 ID、优先级等属性保持不变
  • 文件描述符:默认情况下,打开的文件描述符会被继承(除非设置了 FD_CLOEXEC 标志)

1.2 典型应用场景

  1. Shell 命令执行:当你在终端输入ls时,Shell 会 fork 子进程,然后 execls程序
  2. 服务器进程管理:守护进程 fork 子进程处理请求,子进程 exec 具体的服务程序
  3. 程序加载器:动态加载并执行其他可执行文件

2. exec 函数族分类

exec 函数族包含 6 个主要函数,它们都声明在<unistd.h>头文件中:

#include<unistd.h>intexecl(constchar*path,constchar*arg,...);intexeclp(constchar*file,constchar*arg,...);intexecle(constchar*path,constchar*arg,...,char*constenvp[]);intexecv(constchar*path,char*constargv[]);intexecvp(constchar*file,char*constargv[]);intexecve(constchar*path,char*constargv[],char*constenvp[]);

2.1 命名规则解析

  • l(list):参数以可变参数列表形式传递,最后一个参数必须是NULL
  • v(vector):参数以字符串数组argv[]形式传递
  • p(path):在PATH环境变量指定的目录中搜索可执行文件
  • e(environment):可以指定自定义的环境变量数组

2.2 返回值说明

  • 成功:不返回(进程已被新程序替换)
  • 失败:返回 -1,并设置errno,原进程继续执行

3. execl 和 execlp 函数

3.1 函数原型

intexecl(constchar*path,constchar*arg,...);intexeclp(constchar*file,constchar*arg,...);

3.2 参数说明

  • path(execl):可执行文件的完整路径
  • file(execlp):可执行文件名,系统会在PATH环境变量中查找
  • arg…:可变参数列表,第一个参数通常是程序名,最后必须以NULL结束

3.3 使用示例:执行 ls 命令

#include<stdio.h>#include<unistd.h>#include<errno.h>intmain(){// 使用 execl - 需要指定完整路径printf("使用 execl 执行 ls 命令...\n");if(execl("/bin/ls","ls","-l","-a","/etc",NULL)==-1){perror("execl 执行失败");return1;}// 这行代码不会被执行,除非 execl 失败printf("这行不会输出\n");return0;}
#include<stdio.h>#include<unistd.h>intmain(){// 使用 execlp - 自动在 PATH 中查找printf("使用 execlp 执行 ls 命令...\n");if(execlp("ls","ls","-l","-a","/etc",NULL)==-1){perror("execlp 执行失败");return1;}return0;}

3.4 区别对比

特性execlexeclp
路径查找必须提供完整路径自动在 PATH 中查找
使用便捷性较低,需知道路径较高,类似命令行
安全性较高,路径明确较低,可能受 PATH 篡改影响

4. execv 和 execvp 函数

4.1 函数原型

intexecv(constchar*path,char*constargv[]);intexecvp(constchar*file,char*constargv[]);

4.2 参数说明

  • argv[]:字符串指针数组,包含所有参数
  • argv[0]:通常是程序名
  • 最后一个元素:必须是NULL指针

4.3 使用示例

#include<stdio.h>#include<unistd.h>intmain(){// 准备参数数组char*args[]={"ls","-l","-a","/etc",NULL};// 使用 execv - 需要完整路径printf("使用 execv 执行 ls 命令...\n");if(execv("/bin/ls",args)==-1){perror("execv 执行失败");return1;}return0;}
#include<stdio.h>#include<unistd.h>intmain(){// 准备参数数组char*args[]={"ls","-l","-a","/etc",NULL};// 使用 execvp - 自动查找printf("使用 execvp 执行 ls 命令...\n");if(execvp("ls",args)==-1){perror("execvp 执行失败");return1;}return0;}

4.4 适用场景

  • 动态参数:当参数数量或内容在运行时确定时
  • 封装调用:便于将参数封装为数组传递
  • 批量处理:适合需要处理多个相似命令的情况

5. execle 和 execve 函数

5.1 函数原型

intexecle(constchar*path,constchar*arg,...,char*constenvp[]);intexecve(constchar*path,char*constargv[],char*constenvp[]);

5.2 环境变量控制

#include<stdio.h>#include<unistd.h>intmain(){char*env[]={"PATH=/usr/local/bin:/usr/bin:/bin","HOME=/home/user","MYVAR=custom_value",NULL};char*args[]={"env",NULL};// 使用自定义环境变量执行 env 命令if(execve("/usr/bin/env",args,env)==-1){perror("execve 执行失败");return1;}return0;}

6. system 函数

6.1 函数原型

#include<stdlib.h>intsystem(constchar*command);

6.2 工作原理

  1. 调用fork()创建子进程
  2. 子进程中调用execl("/bin/sh", "sh", "-c", command, NULL)
  3. 父进程调用wait()等待子进程结束

6.3 使用示例

#include<stdio.h>#include<stdlib.h>intmain(){printf("使用 system 执行命令...\n");intret=system("ls -la /etc | head -5");if(ret==-1){perror("system 调用失败");}elseif(WIFEXITED(ret)){printf("命令退出状态: %d\n",WEXITSTATUS(ret));}return0;}

6.4 与 exec 的区别

特性exec 函数族system 函数
进程替换是,当前进程被替换否,创建新进程执行
控制权不返回(除非失败)返回命令执行状态
Shell 解析无,直接执行程序有,通过/bin/sh -c
安全性较高,无 Shell 注入风险较低,可能受 Shell 特性影响
使用便捷性较低,需处理参数较高,类似命令行

7. 实战:fork + exec 模式

7.1 典型模式

#include<stdio.h>#include<unistd.h>#include<sys/wait.h>#include<stdlib.h>intmain(){pid_tpid=fork();if(pid<0){perror("fork 失败");exit(1);}elseif(pid==0){// 子进程:执行 ls 命令printf("子进程 PID: %d\n",getpid());char*args[]={"ls","-l","/tmp",NULL};if(execvp("ls",args)==-1){perror("execvp 失败");exit(1);}}else{// 父进程:等待子进程结束printf("父进程 PID: %d,创建子进程 PID: %d\n",getpid(),pid);intstatus;waitpid(pid,&status,0);if(WIFEXITED(status)){printf("子进程正常退出,状态码: %d\n",WEXITSTATUS(status));}}return0;}