exec()
函数及其系列是什么?为什么使用此功能以及它如何工作?
请任何人解释这些功能。
答案 0 :(得分:222)
简单地说,在UNIX中,您拥有流程和程序的概念。进程是程序执行的过程。
UNIX“执行模型”背后的简单想法是你可以做两个操作。
第一个是fork()
,它会创建一个包含当前程序副本的全新流程,包括其状态。这些过程之间存在一些差异,使他们能够找出哪个是父母,哪个是孩子。
第二个是exec()
,它用一个全新的程序取代当前流程中的程序。
从这两个简单的操作中,可以构建整个UNIX执行模型。
要在上面添加更多细节:
fork()
和exec()
的使用体现了UNIX的精神,因为它提供了一种非常简单的方法来启动新进程。
fork()
调用与当前进程几乎完全相同,几乎在所有方面都相同(不是所有都被复制过来,例如,某些实现中的资源限制,但这个想法是创建尽可能接近的副本)。一个进程调用fork()
,而两个进程从它返回 - 听起来奇怪,但它非常优雅
新进程(称为子进程)获取不同的进程ID(PID),并将旧进程的PID(父进程)作为其父PID(PPID)。
因为这两个进程现在运行完全相同的代码,所以他们需要能够分辨哪个是哪个 - fork()
的返回代码提供了这个信息 - 子得到0,父得到了PID子项(如果fork()
失败,则不创建子项,父项获取错误代码)。这样,父母知道孩子的PID并且可以与孩子进行交流,杀死它,等待它等等(孩子总是可以通过调用getppid()
找到其父进程)。
exec()
调用用新程序替换进程的整个当前内容。它将程序加载到当前进程空间并从入口点运行它。
因此,fork()
和exec()
通常按顺序使用,以使新程序作为当前进程的子进程运行。每当你尝试运行像find
这样的程序时,shell通常会这样做 - shell分叉,然后子程序将find
程序加载到内存中,设置所有命令行参数,标准I / O等等
但它们不需要一起使用。如果程序包含父代码和子代码,那么程序在没有跟随fork()
的情况下调用exec()
是完全可以接受的(你需要小心你做什么,每个实现可能有限制) 。这对于守护进程使用了很多(现在仍然如此),它们只是在TCP端口上侦听并派生自己的副本来处理特定请求,而父进程则回去监听。对于这种情况,程序包含父和子代码。
同样地,知道他们已经完成并且只想运行另一个程序的程序不需要fork()
,exec()
然后wait()/waitpid()
为孩子。他们可以使用exec()
将孩子直接加载到当前流程空间。
某些UNIX实现具有优化的fork()
,它使用了他们称之为copy-on-write的内容。这是在fork()
中延迟复制进程空间的技巧,直到程序尝试更改该空间中的某些内容。这对于仅使用fork()
而非exec()
的程序非常有用,因为它们不必复制整个进程空间。在Linux下,fork()
只复制页表和新的任务结构,exec()
将完成“分离”两个进程的内存的繁重工作。
如果在exec
之后调用fork
(这是主要发生的事情),则会导致写入进程空间,然后为子进程复制过程
Linux还有一个vfork()
,甚至更优化,它在两个进程之间共享所有。因此,孩子可以做什么有一些限制,父母会停止,直到孩子打电话给exec()
或_exit()
。
必须停止父级(并且不允许子级从当前函数返回),因为这两个进程甚至共享相同的堆栈。对于fork()
的经典用例,紧随其后的是exec()
。
请注意,有一整套exec
次来电(execl
,execle
,execve
等等,但这里的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
答案 1 :(得分:28)
exec()系列中的函数有不同的行为:
你可以混合它们,因此你有:
对于所有这些,初始参数是要执行的文件的名称。
有关详细信息,请参阅exec(3) man page:
man 3 exec # if you are running a UNIX system
答案 2 :(得分:16)
exec
系列函数使您的进程执行不同的程序,替换它正在运行的旧程序。即,如果你打电话
execl("/bin/ls", "ls", NULL);
然后使用调用ls
的进程的进程ID,当前工作目录和用户/组(访问权限)执行execl
程序。之后,原始程序不再运行了。
要启动新流程,请使用fork
系统调用。要在不替换原始程序的情况下执行程序,您需要fork
,然后exec
。
答案 3 :(得分:7)
exec
通常与fork
一起使用,我看到你也问过这个问题,所以我会考虑到这一点。
exec
将当前进程转换为另一个程序。如果你曾经看过神秘博士,那就像他再生一样 - 他的旧身体被一个新的身体所取代。
您的程序和exec
发生这种情况的方式是,操作系统内核检查的许多资源是否将您传递给exec
的文件作为程序参数(第一个参数) )是当前用户可执行的(进行exec
调用的进程的用户ID),如果是,它将当前进程的虚拟内存映射替换为虚拟内存新进程并复制argv
和envp
在exec
调用中传递到此新虚拟内存映射区域的数据。此处还可能发生其他一些事情,但为调用exec
的程序打开的文件仍将为新程序打开,并且它们将共享相同的进程ID,但调用{{1}的程序将停止(除非执行失败)。
这样做的原因是将正在运行 程序分成两步像这样你可以在两个步骤之间做一些事情。最常见的做法是确保新程序将某些文件作为特定文件描述符打开。 (请记住,文件描述符与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 */
来自命令shell。
答案 4 :(得分:5)
什么是exec函数及其系列。
exec
函数系列是用于执行文件的所有函数,例如execl
,execlp
,execle
,execv
和{{1它们都是execvp
的前端,并提供了不同的调用方法。
为什么使用这个功能
当您要执行(启动)文件(程序)时,将使用Exec函数。
以及它是如何运作的。
他们通过使用您启动的过程映像覆盖当前过程映像来工作。它们用已启动的新进程替换(通过结束)当前正在运行的进程(调用exec命令的进程)。
有关详细信息:see this link。
答案 5 :(得分:4)
exec(3,3p)
函数将当前进程替换为。也就是说,当前进程停止,而另一个进程运行,接管原始程序的一些资源。
答案 6 :(得分:4)
当进程使用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()之后,父级和子级都共享父级的内存地址空间。在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等。将与这些命令关联的程序代码包含在一个程序中,或者在需要时让这些子程序将这些程序加载到内存中,是否正确?
这完全取决于您的用例。您有一个提供给定输入x的Web服务器,该输入x将2 ^ x返回给客户端。对于每个请求,Web服务器都会创建一个新的子代,并要求其进行计算。您会编写一个单独的程序来计算该值并使用exec()吗?还是只是在父程序中编写计算代码?
通常,流程创建涉及fork(),exec(),wait()和exit()调用的组合。