pax*_*blo 230
简单地说,在UNIX中,您拥有流程和程序的概念.进程是程序执行的过程.
UNIX"执行模型"背后的简单想法是你可以做两个操作.
第一个是fork()创建一个包含当前程序副本的全新流程,包括其状态.这些过程之间存在一些差异,使他们能够找出哪个是父母,哪个是孩子.
第二个是exec(),用一个全新的程序取代当前流程中的程序.
从这两个简单的操作中,可以构建整个UNIX执行模型.
要为上面添加更多细节:
使用fork()和exec()体现UNIX的精神,它提供了一种非常简单的方法来启动新进程.
该fork()调用几乎在每个方面都与当前进程几乎相同(并非所有内容都被复制,例如,某些实现中的资源限制,但想法是尽可能创建尽可能接近的副本).一个进程调用,fork()而两个进程从它返回 - 听起来奇怪,但它真的非常优雅
新进程(称为子进程)获取不同的进程ID(PID),并将旧进程的PID(父进程)作为其父PID(PPID).
因为这两个进程现在运行完全相同的代码,所以它们需要能够分辨出哪个 - fork()提供此信息的返回代码- 子获取0,父获取子进程的PID(如果fork()失败,则不child已创建,父级获取错误代码).这样,父母知道孩子的PID,并且可以与孩子进行交流,杀死它,等待它等等(孩子总是可以通过调用来找到它的父进程getppid()).
该exec()调用使用新程序替换进程的整个当前内容.它将程序加载到当前进程空间并从入口点运行它.
因此,fork()与exec()通常的顺序来获取所有正在运行的当前进程的孩子一个新的程序.每当你试图运行像findshell 这样的程序时,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在上下文中这里是指任何人.
下图说明了使用shell通过命令列出目录的典型fork/exec操作:bashls
+--------+
| 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
Run Code Online (Sandbox Code Playgroud)
T04*_*435 32
exec()系列中的函数具有不同的行为:
你可以混合它们,因此你有:
对于所有这些,初始参数是要执行的文件的名称.
有关更多信息,请阅读exec(3)手册页:
man 3 exec # if you are running a UNIX system
Run Code Online (Sandbox Code Playgroud)
Fre*_*Foo 16
该exec系列函数使你的程序执行不同的程序,取代旧的程序它运行.即,如果你打电话
execl("/bin/ls", "ls", NULL);
Run Code Online (Sandbox Code Playgroud)
然后ls使用进程id,当前工作目录和被调用进程的用户/组(访问权限)执行程序execl.之后,原始程序不再运行.
要启动新进程,fork请使用系统调用.要在不更换原件的情况下执行程序fork,则需要exec.
exec经常一起使用fork,我看到你也问过这个问题,所以我会考虑到这一点.
exec将当前进程转换为另一个程序.如果你曾经看过神秘博士,那就像他再生一样 - 他的旧身体被一个新的身体所取代.
您的程序发生这种情况的方式exec是,OS内核检查的许多资源是否可以exec由当前用户(进程的用户ID)执行,以查看您作为程序参数(第一个参数)传递的文件进行exec调用)如果是这样,它将当前进程的虚拟内存映射替换为虚拟内存新进程,并将调用中传递的数据argv和envp数据复制exec到此新虚拟内存映射的区域中.此处还可能发生其他一些事情,但为调用的程序打开的文件exec仍将为新程序打开,并且它们将共享相同的进程ID,但调用的程序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 */
Run Code Online (Sandbox Code Playgroud)
这完成了运行:
/bin/echo "hello world" > ./output_file.txt
Run Code Online (Sandbox Code Playgroud)
从命令shell.
当一个进程使用 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");
Run Code Online (Sandbox Code Playgroud)
在示例中,我们假设 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;
}
Run Code Online (Sandbox Code Playgroud)
但为什么写时复制是必要的?
典型的进程创建是通过 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.
Run Code Online (Sandbox Code Playgroud)
为什么父进程等待子进程?
为什么需要 exec() 系统调用?
没有必要将 exec() 与 fork() 一起使用。如果子进程将执行的代码在与父进程关联的程序中,则不需要 exec()。
但是想想孩子必须运行多个程序的情况。让我们以shell程序为例。它支持多种命令,如 find、mv、cp、date 等。将与这些命令关联的程序代码包含在一个程序中或在需要时让孩子将这些程序加载到内存中是否正确?
这一切都取决于您的用例。您有一个 Web 服务器,它给出了一个输入 x,它将 2^x 返回给客户端。对于每个请求,Web 服务器都会创建一个新子项并要求它进行计算。你会写一个单独的程序来计算这个并使用 exec() 吗?或者你只是在父程序中编写计算代码?
通常,进程创建涉及 fork()、exec()、wait() 和 exit() 调用的组合。
这些exec(3,3p)函数用另一个进程替换当前进程。也就是说,当前进程停止,另一个进程运行,接管原始程序拥有的一些资源。