请解释 exec()函数及其家族

什么是 exec()函数及其家族? 为什么使用这个函数,它是如何工作的?

请大家解释一下这些功能。

184441 次浏览

exec(3,3p)函数 更换与另一个当前进程。也就是说,当前的进程 停止和另一个进程运行,接管了原始程序所拥有的一些资源。

exec系列函数使您的进程执行一个不同的程序,取代它正在运行的旧程序。也就是说,如果你打电话的话

execl("/bin/ls", "ls", NULL);

然后使用进程 id、当前工作目录和调用 execl的进程的用户/组(访问权限)执行 ls程序。然后,原来的程序就不再运行了。

要启动一个新进程,使用 fork系统调用。要在不替换原始程序的情况下执行程序,需要先执行 fork,然后执行 exec

what is the exec function and its family.

exec函数族是所有用于执行文件的函数,如 execlexeclpexecleexecvexecvp。它们都是 execve的前端,并提供不同的调用方法。

为什么使用这个函数

执行函数用于执行(启动)文件(程序)。

以及它是如何工作的。

它们的工作原理是用您启动的流程映像覆盖当前流程映像。它们用已启动的新进程替换(通过结束)当前运行的进程(调用 exec 命令的进程)。

详情请浏览: 看看这个链接

简单地说,在 UNIX 中,您有进程和程序的概念。进程是程序执行的环境。

UNIX“执行模型”背后的简单思想是,您可以执行两个操作。

第一个是到 fork(),它创建一个全新的进程,其中包含当前程序的副本(大部分) ,包括其状态。这两个进程之间有一些区别,它们可以分辨出哪个是父进程,哪个是子进程。

第二个是 exec(),它用一个全新的程序代替当前过程中的程序。

From those two simple operations, the entire UNIX execution model can be constructed.


为了增加上述内容的更多细节:

fork()exec()的使用体现了 UNIX 的精神,因为它提供了一种非常简单的启动新进程的方法。

fork()调用与当前进程几乎重复,几乎在所有方面都是相同的(例如,在某些实现中,一切没有被复制到资源限制上,但其想法是创建尽可能接近的副本)。只有一个进程 电话 fork()但是 进程从这个调用返回-听起来很奇怪,但是它真的很优雅

新进程(称为子进程)获取不同的进程 ID (PID) ,并将旧进程(父进程)的 PID 作为其父进程 PID (PPID)。

因为这两个进程现在正在运行完全相同的代码,它们需要能够分辨哪个是哪个—— fork()的返回代码提供了这个信息——子进程得到0,父进程得到子进程的 PID (如果 fork()失败,没有子进程被创建,父进程得到一个错误代码)。

这样,父进程知道子进程的 PID,并且可以与它通信、杀死它、等待它等等(子进程总是可以通过调用 getppid()找到它的父进程)。

exec()调用用一个新程序替换进程的整个当前内容。它将程序加载到当前进程空间并从入口点运行它。

因此,通常按顺序使用 fork()exec()来获得作为当前进程的子进程运行的新程序。Shell 通常会在您尝试运行像 find这样的程序时这样做—— shell 分叉,然后子程序将 find程序加载到内存中,设置所有命令行参数、标准 I/O 等等。

但是它们不需要一起使用。例如,如果程序同时包含父代码和子代码,那么在没有后续 exec()的情况下调用 fork()是完全可以接受的(您需要注意所做的事情,每个实现可能都有限制)。

对于只是侦听 TCP 端口并在父端口返回侦听时分叉自己的副本以处理特定请求的守护进程来说,这种方法使用得相当多(现在仍然如此)。对于这种情况,程序同时包含父 还有和子代码。

类似地,那些知道它们已经完成并且只想运行另一个程序的程序不需要为子程序运行 fork()exec()wait()/waitpid()。它们可以使用 exec()将子进程直接加载到当前进程空间中。

一些 UNIX 实现有一个优化的 fork(),它使用所谓的写上复制(copy-on-write)。这是一个延迟复制 fork()中进程空间的技巧,直到程序试图更改该空间中的某些内容。这对于那些只使用 fork()而不使用 exec()的程序非常有用,因为它们不需要复制整个进程空间。在 Linux 下,fork()只复制页表和一个新的任务结构,exec()将完成“分离”这两个进程的内存的繁重工作。

如果 exec fork之后调用(这是大多数情况下发生的) ,那么会导致向进程空间写入,然后在允许修改之前为子进程复制该进程。

Linux also has a vfork(), even more optimised, which shares just about 一切 between the two processes. Because of that, there are certain restrictions in what the child can do, and the parent halts until the child calls exec() or _exit().

必须停止父进程(并且不允许子进程从当前函数返回) ,因为这两个进程甚至共享同一个堆栈。对于紧接着是 exec()fork()的经典用例来说,这稍微更有效一些。

请注意,有一个完整的 exec调用家族(execlexecleexecve等等) ,但是这里上下文中的 exec意味着它们中的任何一个。

下图说明了典型的 fork/exec操作,其中使用 bash shell 通过 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经常与 fork结合使用,我看到你们也问过这个问题,所以我将在脑海中讨论这个问题。

exec turns the current process into another program. If you ever watched Doctor Who, then this is like when he regenerates -- his old body is replaced with a new body.

The way that this happens with your program and exec is that a lot of the resources that the OS kernel checks to see if the file you are passing to exec as the program argument (first argument) is executable by the current user (user id of the process making the exec call) and if so it replaces the virtual memory mapping of the current process with a virtual memory the new process and copies the argv and envp data that were passed in the exec call into an area of this new virtual memory map. Several other things may also happen here, but the files that were open for the program that called exec will still be open for the new program and they will share the same process ID, but the program that called exec will cease (unless exec failed).

这样做的原因是,通过将 跑步 new 程序分成这样的两个步骤,您可以在两个步骤之间做一些事情。最常见的做法是确保新程序将某些文件作为某些文件描述符打开。(请记住,文件描述符不同于 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

从命令外壳。

Functions in the exec() family have different behaviours:

  • 参数作为字符串列表传递给 main ()
  • v : arguments are passed as an array of strings to the main()
  • p : path/s to search for the new running program
  • 环境可以由调用者指定

你可以混合它们,因此你有:

  • 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 []) ;

对于所有这些参数,初始参数是要执行的文件的名称。

更多信息请阅读 Exec (3)手册页:

man 3 exec  # if you are running a UNIX system

当一个进程使用 fork ()时,它会创建一个自身的副本,这个副本将成为该进程的子进程。Fork ()是在 Linux 中使用 clone ()系统调用实现的,该调用从内核返回两次。

  • 非零值(子进程 ID)返回给父进程。
  • 将零值返回给子级。
  • 如果由于内存不足等问题而未能成功创建子级,则将 -1返回到 fork ()。

让我们通过一个例子来理解这一点:

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 ()。

But a parent and child differs in some of the PCB(process control block) attributes. These are:

  1. PID-子进程和父进程具有不同的进程 ID。
  2. 挂起信号-子进程不继承父进程的挂起信号。创建子进程时它将为空。
  3. 内存锁——子元素不继承其父元素的内存锁。内存锁是一种锁,它可以用来锁定一个内存区域,然后这个内存区域不能被交换到磁盘。
  4. Record Locks - The child doesn’t inherit its parent’s record locks. Record locks are associated with a file block or an entire file.
  5. 对于子进程,进程资源利用率和 CPU 时间消耗被设置为零。
  6. 子级也不从父级继承计时器。

但是子内存怎么办呢? 是否为子内存创建了一个新的地址空间?

The answers in no. After the fork(), both parent and child share the memory address space of parent. In linux, these address space are divided into multiple pages. Only when the child writes to one of the parent memory pages, a duplicate of that page is created for the child. This is also known as copy on write(Copy parent pages only when the child writes to it).

让我们通过一个例子来理解书面复制。

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() group of functions replaces the child’s address space with a new program. Once exec() is called within a child, a separate address space will be created for the child which is totally different from the parent’s one.

If there was no copy on write mechanism associated with fork(), duplicate pages would have created for the child and all the data would have been copied to child’s pages. Allocating new memory and copying data is a very expensive process(takes processor’s time and other system resources). We also know that in most cases, the child is going to call exec() and that would replace the child’s memory with a new program. So the first copy which we did would have been a waste if copy on write was not there.

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.

Why does parent waits for a child process?

  1. 父级可以将任务分配给它的子级,然后等待它完成它的任务。然后它可以进行一些其他的工作。
  2. 一旦子节点终止,除了流程控制块之外,与子节点关联的所有资源都将被释放。现在,孩子处于僵尸状态。使用 wait () ,父级可以查询子级的状态,然后请求内核释放 PCB。如果父级不使用 wait,则子级将保持僵尸状态。

为什么需要 exec ()系统调用?

没有必要在 fork ()中使用 exec ()。如果子代将执行的代码位于与父级关联的程序中,则不需要 exec ()。

但是想想当孩子必须运行多个程序的情况。让我们以 shell 程序为例。它支持多种命令,如 find、 mv、 cp、 date 等。在一个程序中包含与这些命令相关联的程序代码或者在需要时让子程序将这些程序加载到内存中是正确的吗?

这完全取决于您的用例。你有一个 web 服务器,它给出一个输入 x,然后把2 ^ x 返回给客户端。对于每个请求,Web 服务器创建一个新的子级并请求它进行计算。您是否要编写一个单独的程序来计算并使用 exec () ?或者只是在父程序中编写计算代码?

通常,流程创建涉及 fork ()、 exec ()、 wait ()和 exit ()调用的组合。