exec()
函数及其家族是什么?为什么要使用这个函数,它是如何工作的?
请任何人解释这些功能。
简单地说,在 UNIX 中,您有进程和程序的概念。进程是程序执行的环境。
UNIX“执行模型”背后的简单想法是您可以执行两种操作。
第一个是 to fork()
,它创建一个全新的进程,其中包含(大部分)当前程序的副本,包括其状态。这两个进程之间存在一些差异,这使他们能够确定哪个是父进程,哪个是子进程。
第二个是 to exec()
,将当前进程中的程序替换为全新的程序。
从这两个简单的操作,就可以构建出整个 UNIX 执行模型。
要在上面添加更多细节:
使用fork()
并exec()
体现了 UNIX 的精神,因为它提供了一种非常简单的方式来启动新进程。
该fork()
调用几乎复制了当前进程,几乎在所有方面都相同(并非所有内容都被复制,例如,某些实现中的资源限制,但想法是创建尽可能接近的副本)。只有一个进程调用 fork()
,但有两个进程从该调用返回 - 听起来很奇怪,但它真的很优雅
新进程(称为子进程)获得不同的进程 ID (PID),并将旧进程(父进程)的 PID 作为其父进程 PID (PPID)。
因为这两个进程现在运行完全相同的代码,所以它们需要能够分辨哪个是哪个 - 的返回码fork()
提供此信息 - 子进程获取 0,父进程获取子进程的 PID(如果fork()
失败,则不child 被创建并且 parent 得到一个错误代码)。
这样,父进程知道子进程的 PID 并可以与它通信、杀死它、等待它等等(子进程总是可以通过调用找到它的父进程getppid()
)。
该exec()
调用用一个新程序替换进程的全部当前内容。它将程序加载到当前进程空间并从入口点运行它。
因此,fork()
经常exec()
按顺序使用,以使新程序作为当前进程的子进程运行。当你尝试运行一个程序时,shell 通常会这样做find
——shell fork,然后子进程将find
程序加载到内存中,设置所有命令行参数、标准 I/O 等等。
但它们不需要一起使用。例如,如果程序同时包含父代码和子代码(您需要小心您所做的事情,每个实现都可能有限制),则程序在fork()
没有跟随的情况下调用是完全可以接受的。exec()
这在守护进程中被大量使用(现在仍然如此),这些守护进程只是在 TCP 端口上侦听并派生自己的副本以处理特定请求,而父进程则返回侦听。对于这种情况,程序包含父代码和子代码。
同样,知道自己已经完成并且只想运行另一个程序的程序不需要fork()
,exec()
然后wait()/waitpid()
为孩子。他们可以直接将孩子加载到他们当前的进程空间中exec()
。
一些 UNIX 实现有一个优化fork()
,它使用他们所谓的写时复制。这是一种延迟复制进程空间的技巧,fork()
直到程序尝试更改该空间中的某些内容。这对于那些只使用fork()
而不是exec()
因为它们不必复制整个进程空间的程序很有用。在 Linux 下,fork()
只需复制页表和新的任务结构,exec()
就会完成“分离”两个进程的内存的繁重工作。
如果在exec
后面调用fork
(这是最常发生的情况),则会导致写入进程空间,然后在允许修改之前将其复制给子进程。
Linux 也有一个vfork()
更加优化的,它在两个进程之间共享几乎所有的东西。正因为如此,孩子可以做的事情有一定的限制,父母会停下来,直到孩子打电话exec()
或_exit()
。
必须停止父进程(并且不允许子进程从当前函数返回),因为这两个进程甚至共享同一个堆栈。fork()
这对于紧随其后的经典用例稍微高效一些exec()
。
请注意,有一个完整的exec
调用系列(execl
、execle
等execve
),但exec
在上下文中,这里指的是它们中的任何一个。
下图说明了使用 shell 使用命令列出目录的典型fork/exec
操作:bash
ls
+--------+
| pid=7 |
| ppid=4 |
| bash |
+--------+
|
| calls fork
V
+--------+ +--------+
| pid=7 | forks | pid=22 |
| ppid=4 | ----------> | ppid=7 |
| bash | | bash |
+--------+ +--------+
| |
| waits for pid 22 | calls exec to run ls
| V
| +--------+
| | pid=22 |
| | ppid=7 |
| | ls |
V +--------+
+--------+ |
| pid=7 | | exits
| ppid=4 | <---------------+
| bash |
+--------+
|
| continues
V
exec() 系列中的函数具有不同的行为:
你可以混合它们,因此你有:
对于所有这些,初始参数是要执行的文件的名称。
有关更多信息,请阅读exec(3) 手册页:
man 3 exec # if you are running a UNIX system
该exec
系列功能使您的进程执行不同的程序,替换它正在运行的旧程序。即,如果你打电话
execl("/bin/ls", "ls", NULL);
然后ls
程序使用调用的进程的进程 ID、当前工作目录和用户/组(访问权限)执行execl
。之后,原始程序不再运行。
要启动一个新进程,fork
需要使用系统调用。要在不替换原始程序的情况下执行程序,您需要fork
,然后exec
。
什么是 exec 函数及其家族。
函数族是用于执行文件的exec
所有函数,例如execl
、execlp
、execle
、execv
和execvp
。它们都是前端execve
并提供不同的调用方法。
为什么使用这个功能
当您想要执行(启动)文件(程序)时,使用 Exec 函数。
以及它是如何工作的。
它们通过用您启动的过程映像覆盖当前过程映像来工作。他们用已启动的新进程替换(通过结束)当前运行的进程(调用 exec 命令的进程)。
有关更多详细信息:请参阅此链接。
exec
经常与 一起使用fork
,我看到你也问过这个问题,所以我会考虑到这一点来讨论这个。
exec
将当前进程变成另一个程序。如果你看过神秘博士,那么这就像他重生的时候——他的旧身体被新的身体取代了。
您的程序发生这种情况的方式exec
是操作系统内核检查的许多资源,以查看您exec
作为程序参数(第一个参数)传递给的文件是否可由当前用户(进程的用户 ID)执行进行exec
调用),如果是这样,它将用新进程的虚拟内存替换当前进程的虚拟内存映射,并将调用中传递的数据argv
复制到这个新的虚拟内存映射的区域中。这里也可能发生其他一些事情,但为调用的程序打开的文件仍将为新程序打开,并且它们将共享相同的进程 ID,但调用的程序将停止(除非 exec 失败)。envp
exec
exec
exec
这样做的原因是通过像这样将运行 一个 新 程序分成两个步骤,您可以在两个步骤之间做一些事情。最常见的做法是确保新程序将某些文件作为某些文件描述符打开。(请记住,文件描述符与 不同FILE *
,而是int
内核知道的值)。这样做你可以:
int X = open("./output_file.txt", O_WRONLY);
pid_t fk = fork();
if (!fk) { /* in child */
dup2(X, 1); /* fd 1 is standard output,
so this makes standard out refer to the same file as X */
close(X);
/* I'm using execl here rather than exec because
it's easier to type the arguments. */
execl("/bin/echo", "/bin/echo", "hello world");
_exit(127); /* should not get here */
} else if (fk == -1) {
/* An error happened and you should do something about it. */
perror("fork"); /* print an error message */
}
close(X); /* The parent doesn't need this anymore */
这样就完成了运行:
/bin/echo "hello world" > ./output_file.txt
从命令外壳。
当一个进程使用 fork() 时,它会创建一个自身的副本,这个副本成为该进程的子进程。fork() 是使用 linux 中的 clone() 系统调用实现的,它从内核返回两次。
让我们通过一个例子来理解这一点:
pid = fork();
// Both child and parent will now start execution from here.
if(pid < 0) {
//child was not created successfully
return 1;
}
else if(pid == 0) {
// This is the child process
// Child process code goes here
}
else {
// Parent process code goes here
}
printf("This is code common to parent and child");
在示例中,我们假设 exec() 未在子进程中使用。
但是父子节点在某些 PCB(过程控制块)属性上有所不同。这些是:
但是孩子的记忆呢?是否为孩子创建了新的地址空间?
没有答案。在 fork() 之后,parent 和 child 共享 parent 的内存地址空间。在linux中,这些地址空间被划分为多个页面。只有当孩子写入其中一个父内存页面时,才会为孩子创建该页面的副本。这也称为写时复制(仅在子页面写入时复制父页面)。
让我们通过一个例子来理解写时复制。
int x = 2;
pid = fork();
if(pid == 0) {
x = 10;
// child is changing the value of x or writing to a page
// One of the parent stack page will contain this local variable. That page will be duplicated for child and it will store the value 10 in x in duplicated page.
}
else {
x = 4;
}
但是为什么需要写时复制呢?
典型的进程创建是通过 fork()-exec() 组合进行的。让我们首先了解 exec() 的作用。
Exec() 函数组将子地址空间替换为新程序。一旦在子进程中调用 exec(),将为子进程创建一个与父进程完全不同的单独地址空间。
如果没有与 fork() 关联的写时复制机制,则会为子页面创建重复页面,并且所有数据都将复制到子页面。分配新内存和复制数据是一个非常昂贵的过程(占用处理器的时间和其他系统资源)。我们还知道,在大多数情况下,孩子会调用 exec(),这会用新程序替换孩子的记忆。因此,如果没有写入时复制,我们所做的第一个副本将是一种浪费。
pid = fork();
if(pid == 0) {
execlp("/bin/ls","ls",NULL);
printf("will this line be printed"); // Think about it
// A new memory space will be created for the child and that memory will contain the "/bin/ls" program(text section), it's stack, data section and heap section
else {
wait(NULL);
// parent is waiting for the child. Once child terminates, parent will get its exit status and can then continue
}
return 1; // Both child and parent will exit with status code 1.
为什么父进程等待子进程?
为什么需要 exec() 系统调用?
不必将 exec() 与 fork() 一起使用。如果子程序将执行的代码在与父程序关联的程序中,则不需要 exec()。
但是想想孩子必须运行多个程序的情况。让我们以shell程序为例。它支持多个命令,如 find、mv、cp、date 等。将与这些命令相关的程序代码包含在一个程序中或让孩子在需要时将这些程序加载到内存中是否正确?
这完全取决于您的用例。您有一个 Web 服务器,它给出了一个输入 x,它将 2^x 返回给客户端。对于每个请求,Web 服务器都会创建一个新的子节点并要求它进行计算。你会编写一个单独的程序来计算这个并使用 exec() 吗?或者你只是在父程序中编写计算代码?
通常,进程创建涉及 fork()、exec()、wait() 和 exit() 调用的组合。
这些exec(3,3p)
函数用另一个替换当前进程。也就是说,当前进程停止,而另一个进程运行,接管了原始程序拥有的一些资源。