《Linux系统编程篇》exec族函数——基础篇
文章目录
- 引言
- 探索 `exec()` 系列函数:Linux 进程替换
- 1. 什么是 `exec()` 系列函数?
- 2. `exec()` 系列函数的函数原型
- 3. `exec()` 系列函数的使用
- 4. `exec()` 系列函数的工作原理
- 5. `exec()` 系列函数的常见用法
- 6. `exec()` 与 `fork()` 的配合
- 7. 常见的错误与注意事项
- 结论
当你知道越来越多的时候,随着你的知识量的增加,所而做的事以及解决问题的方法就越多,越丰富!
——家驹
引言
《Linux系统编程篇》——基础篇首页传送门
当我们介绍完fork之后,你会发现虽然我可以同时跑俩个程序,但是还是太过于局限了,而且细心的学员们发现,进程直接是完全不互通的,好像fork之后什么也做不了,是的,只学完fork
就是这样的,所以我们再来介绍新的知识。exec
族函数。
探索 exec()
系列函数:Linux 进程替换
在 Linux 中,exec()
系列函数是用于进程的重载(或替换)的系统调用。与 fork()
不同,exec()
不会创建新的进程,而是将当前进程的代码和数据替换为一个新的程序,从而实现进程功能的“转变”。exec()
在构建多任务系统、实现子进程执行新任务中非常重要。本文将介绍 exec()
系列函数的使用方式和常见应用场景。
1. 什么是 exec()
系列函数?
exec()
系列函数提供了在当前进程上下文中执行其他程序的能力。当调用 exec()
函数时,当前进程的代码和内存会被新程序替换,但进程 ID(PID)保持不变。这意味着在进程级别,它仍然是原进程,但其任务和行为已经完全改变。
当我的程序调用exec
的时候,他就不在执行exec
以下的代码了,而是去执行exec
系列函数指定程序,可以理解为进程替换。
生活中,比如我今天想安安稳稳敲一天的代码,这本来是我今天应该执行的程序,但是我的大脑又想干其他的事情,此时你完全可以不敲代码,改变我今天的行程,当我决定改变日程的时候,其实就相当于在linux系统中调用了exec(),比如我不想敲代码,我想打一天游戏,那么我可以这么做execl(打一天游戏)。是的,就是这样,那么我也不用在思考敲代码的事情了,专心打游戏就可以了。
我们就会想,既然fork
出来一个崭新子进程,我可以让这个子进程,通过调用exec
系列去执行其他程序,而不是局限和父进程执行一样的东西了。
exec()
系列函数主要包括以下几种变体:
execl()
:适用于已知固定参数的情况,参数通过变长列表传递。execv()
:适用于参数数量动态的情况,参数以数组形式传递。execlp()
和execvp()
:与前两者类似,但会在PATH
环境变量指定的路径中寻找可执行文件。execle()
和execve()
:允许传递特定的环境变量。
2. exec()
系列函数的函数原型
以下是 exec()
系列函数的原型及其含义:
// 常见的 exec 系列函数
int execl(const char *path, const char *arg, ...);
int execv(const char *path, char *const argv[]);
int execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[]);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execve(const char *path, char *const argv[], char *const envp[]);
参数说明:
path
:要执行的程序路径(绝对路径或相对路径)。arg
:程序的参数列表(arg[0]
通常是程序的名称)。argv
:参数数组,最后一个元素需为NULL
。envp
:环境变量数组,NULL
表示使用当前环境变量。
3. exec()
系列函数的使用
不同的 exec()
变体在调用时各有特点。下面的例子不仅仅能调用系统的命令(系统命令本质也是程序代码编写而来),可以调用我们的其它已经写好的代码。
以下是几种常见的用法:
(1)execl()
execl()
需要将参数以变长列表的形式传递,适合参数固定的情况。
#include <unistd.h>
#include <stdio.h>
int main() {
printf("Running execl...\n");
execl("/bin/ls", "ls", "-l", "/home", NULL); // 指定路径执行 ls 命令
perror("execl failed");
return 1;
}
执行上面的代码就发现,我好像发命令ls -l 函数?,是的做一层封装,就是你的ls函数,通过argc,argv来配合,这就由学员自由发挥了。
(2)execv()
execv()
适合参数数量动态的情况,参数通过数组传递。
#include <unistd.h>
#include <stdio.h>
int main() {
char *args[] = {"ls", "-l", "/home", NULL};
printf("Running execv...\n");
execv("/bin/ls", args); // 执行 ls 命令
perror("execv failed");
return 1;
}
(3)execlp()
和 execvp()
execlp()
和execvp()
会在环境变量PATH
中查找可执行文件。- 如果不知道命令的完整路径(例如
ls
),可以使用execlp()
或execvp()
。
#include <unistd.h>
#include <stdio.h>
int main() {
printf("Running execlp...\n");
execlp("ls", "ls", "-l", "/home", NULL); // 使用 PATH 查找 ls 命令
perror("execlp failed");
return 1;
}
(4)execle()
和 execve()
execle()
和execve()
可以传入特定的环境变量,适合在新环境下运行程序。
#include <unistd.h>
#include <stdio.h>
int main() {
char *args[] = {"ls", "-l", "/home", NULL};
char *env[] = {"PATH=/usr/bin:/bin", "USER=guest", NULL};
printf("Running execve...\n");
execve("/bin/ls", args, env); // 在指定环境下执行 ls 命令
perror("execve failed");
return 1;
}
4. exec()
系列函数的工作原理
调用 exec()
系列函数后,当前进程会被新程序替换:
- 替换执行:当前进程的代码段、数据段和堆栈被清空,加载新程序。
- PID 不变:虽然代码被替换,但 PID 保持不变。因此,它仍然被认为是原来的进程。
- 关闭不再使用的文件描述符:默认情况下,父进程中的打开文件描述符会被继承。如果不需要,可以设置
FD_CLOEXEC
标志来关闭文件描述符。
5. exec()
系列函数的常见用法
- 执行外部程序:常用于在 shell 或父进程中调用外部程序。
- 多进程程序的子进程初始化:与
fork()
配合使用,通过fork()
创建子进程,然后使用exec()
在子进程中执行不同的任务。 - 服务器编程:许多服务器会使用
fork()
+exec()
来处理每个客户端请求,以便并行执行不同的任务。
6. exec()
与 fork()
的配合
fork()
用于创建新进程,而 exec()
用于执行新任务。这种组合在 shell 和服务器编程中非常常见。以下示例展示了一个父进程通过 fork()
创建子进程,并在子进程中调用 exec()
执行其他程序的过程:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
return 1;
} else if (pid == 0) { // 子进程
printf("In child process...\n");
execlp("ls", "ls", "-l", NULL); // 子进程执行 ls 命令
perror("exec failed");
return 1;
} else { // 父进程
wait(NULL); // 等待子进程结束
printf("Child process finished.\n");
}
return 0;
}
在这个例子中,父进程创建子进程,子进程在调用 exec()
后执行 ls
命令。exec()
执行成功后,子进程的代码被替换,继续执行新任务。父进程则通过 wait()
等待子进程结束。
7. 常见的错误与注意事项
exec()
执行失败:如果文件路径错误或权限不足,exec()
调用将失败,并返回 -1。- 未结束的父进程:在
fork()
后不使用exec()
会导致子进程复制了父进程的代码,可能引起资源浪费。 - 文件描述符继承:
exec()
调用后,父进程的文件描述符通常会被继承。使用FD_CLOEXEC
标志可以避免此问题。
结论
exec()
系列函数为进程提供了执行新程序的能力,允许在进程上下文中运行不同的任务。它与 fork()
配合使用,为 Linux 多任务处理提供了灵活性。理解和使用 exec()
系列函数有助于构建高效的多任务应用,提高系统性能。在实际应用中,使用 exec()
系列函数时务必注意文件路径、参数格式及环境变量的正确传递,以确保程序运行的稳定性。