作者LoganChien (简子翔)
看板b97902HW
标题[系程] 教学: 简介 fork, exec*, pipe, dup2
时间Fri Mar 19 01:08:46 2010
简介 fork, exec*, pipe, dup2
前言
郑卜壬老师在上课的时候,以极快的速度讲过了 fork, exec*,
pipe, dup2 几个指令,并以下面的程式示范了一个 Shell
如何实作 Redirection。我自己觉得这一部分很有趣,所以
花了一些时间去做一些实验,以下就把我的心得和大家分享。
Redirection
我们先从老师课堂上的那一个 Redirection 程式谈起。
/* 程式码 red.c */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char **argv)
{
/* -- Check Arguments -------- */
if (argc < 4)
{
fprintf(stderr, "Error: Incorrect Arguments.\n");
fprintf(stderr, "Usage: red STDIN_FILE STDOUT_FILE EXECUTABLE ARGS\n");
exit(EXIT_FAILURE);
}
/* -- Open the Files for Redirection -------- */
int fd_in =
open(argv[1], O_RDONLY);
if (fd_in < 0)
{
fprintf(stderr, "Error: Unable to open the input file.\n");
exit(EXIT_FAILURE);
}
else
{
dup2(fd_in, STDIN_FILENO);
close(fd_in);
}
int fd_out =
open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd_out < 0)
{
fprintf(stderr, "Error: Unable to open the output file.\n");
exit(EXIT_FAILURE);
}
else
{
dup2(fd_out, STDOUT_FILENO);
close(fd_out);
}
/* -- Replace the Executable Image of the Process -------- */
if (
execvp(argv[3], argv + 3) == -1)
{
fprintf(stderr, "Error: Unable to load executable image.\n");
exit(EXIT_FAILURE);
}
return EXIT_SUCCESS;
}
大家可以用 gcc red.c -o red 来编译这一个程式,然後用
./red 标准输入 标准输出 可执行档 其他参数
来执行他。例如:
touch empty.txt
./red empty.txt output.txt /bin/echo "Hello, redirection."
我们可以发现 /bin/echo 执行时,其标准输出被我们重新导向到
output.txt,而不是直接显示到萤幕上。接下来我们来细看 red
这一个程式。我们先去除所有的错误检查:
int main(int argc, char **argv)
{
int fd_in = open(argv[1], O_RDONLY);
/* 很明显地,这一行是要开启一个档案做为标准输入的来源。
fd_in = 3
fd[0] --------> file_table[i] (inherit from shell)
fd[1] --------> file_table[j] (inherit from shell)
fd[2] --------> file_table[k] (inherit from shell)
fd[3] --------> file_table[m] (open argv[1])
*/
dup2(fd_in, STDIN_FILENO);
/*
接着我们会先关掉原本由 bash 之类的 Shell 自动帮我们开启的 STDIN。
这里的 STDIN_FILENO 事实上就是被 define 成 0。
然後,把第 fd_in 个 File descriptor 复制一份到第 0 个 File descriptor。
这时,同时会有二个 File descriptor 指向同一个档案(Kernel File Table
同一个 entry)。
fd[0] ----------------------------+
fd[1] --------> file_table[j] |
fd[2] --------> file_table[k] |
fd[3] --------> file_table[m] <---+
*/
close(fd_in);
/*
关闭第 fd_in 个 File descriptor。或者精确地说,回收第 fd_in 个
File descriptor。因为我们不需要这个 File descriptor 了。此时,
就只剩一个 File descriptor 指向 file_table[m]。
fd[0] --------> file_table[m] (open argv[1])
fd[1] --------> file_table[j] (inherit from shell)
fd[2] --------> file_table[k]
*/
int fd_out = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644);
/*
开启一个档案准备做为标准输出。如果档案不存在,就自动建立他,并将
其属性设为 644。若档案已经存在,记得要清空整个档案。
fd_out = 3
fd[0] --------> file_table[m] (open argv[1])
fd[1] --------> file_table[j] (inherit from shell)
fd[2] --------> file_table[k]
fd[3] --------> file_table[n] (open argv[2])
*/
dup2(fd_out, STDOUT_FILENO);
/* 和 STDIN 一样,我们关闭原有的 STDOUT,把我们开启的 fd_out 复制过去。
fd[0] --------> file_table[m] (open argv[1])
fd[1] --------------------------------+
fd[2] --------> file_table[k] |
fd[3] --------> file_table[n] <-------+
*/
close(fd_out);
/* 和 STDIN 一样,把 fd_out 回收掉。
fd[0] --------> file_table[m] (open argv[1])
fd[1] --------> file_table[n] (open argv[2])
fd[2] --------> file_table[k] (inherit from shell)
*/
execvp(argv[3], argv + 3);
/*
重新载入另一个执行档。我们下面会再谈到他。
所有的 File descriptor 都
不会被改动(注1)。而 argv[3] 是可执行档的位置,argv + 3 是因为除了前
三个 argument 之外,我要把剩下的 argument 传给另一个执行档的 main() */
}
注1: 对於没有设定 FD_CLOEXEC 的 File descriptor,File descriptor 就不会因为
exec* 函式而被更动。本例之中,所有的 File descriptor 都没有设 FD_CLOEXEC。
exec* 重新载入可执行档
所谓的可执行档(Executable),就是我们在 Windows 上常见的 .exe,
或者是 Linux 上的 elf executable。这种档案会储存许多 instruction
的 machine code。而
exec* 系统呼叫就是帮我们把可执行档的
machine code 搬进 Process 的 Memory,然後呼叫可执行档之中的
main 函式。所以 exec* 系统呼叫的第一个参数通常都是可执行档的
位置。
另外,直得注意的是:
如果 exec* 系统呼叫成功地被执行,这个 process
就好像一个人完全失忆,然後大脑被载入不同的记忆,他会完全忘记他本
来要执行的指令,从新的 main 函式开始。不过应该属於他的东西还会是
他的,例如:Process ID、File descriptor 等等。
想像一下,如果今天 LxxxxCxxxx 失忆了,然後有一个很强的催眠师
透过一些暗示,让 LxxxxCxxxx 有外星人的记忆。於是我就完全忘了
我是地球人,也忘了我写过这一篇教学文,也忘了我等一下要去睡觉。
我说起话来像是外星人,我的动作看起来像是外星人,事实上,我就
是外星人。不过我身边的东西,例如身分证上的身分证字号还是一样,
我手边的书还是 Alho 的 Compiler,不会因为很强的催眠师而改变。
如果今天,有一个 process 本来他的记忆是 a.out,不过不久之後
被 exec* 系统呼叫载入 /bin/ls。於是这个 process 完全忘了他
是 a.out,忘了他曾经呼叫过 exec*。他的输出结果像是 /bin/ls,
他的执行步骤看起来像是 /bin/ls,事实上他就是 /bin/ls。不过
这个 process 有的东西:Process ID、File Descriptor 不会因为
exec 而改变。
所以上面的 red.c 在 exec* 载入程式之後,如果直接把 fd[1]
当作 stdout,则所有的标准输出就会被写到我们开启的档案。
XD
还有,眼尖的同学可能已经注意到了!我上面所有的 exec 都加上了 *,
而在我的程式码之中,我呼叫的是 execv 的函式。事实上,为了方便
programmer 使用,exec* 有很多变体,如 execl, execv, execle,
execve, execlp, execvp, execlp。他们的差异是:
结尾有 l 的,就是函式长度不固定(va_arg)的版本。这方便我们在
程式之中直接呼叫 exec,例如:
execl("/bin/echo", "echo", "abc", "def", "ghi", (char *)0);
注意,一定要有一个 0 做为 argument 的结尾。如果是用以上的系
统呼叫,argv[0] 会是 "echo",argv[1] 会是 "abc",等等,而
argc 会是 4。
结尾有 v 的,就是 char ** 的版本。一样地,char ** 的最後一个
参数要以 0 做为结尾。
char *argvs[] = { "echo", "abc", "def", "ghi", 0 };
execv("/bin/echo", argvs);
结尾有 e 的就是可以设定 environment variable 的版本。例如:
extern char **environ;
int main()
{
execle("/bin/echo", "echo", "abc", "def", "ghi", (char *)0, environ);
}
结尾有 p 的就是会自动去从 $
PATH 找出 executable 的版本。
/*file*/
execlp("echo", "echo", "abc", "def", "ghi", (char *)0);
execlp("ls", "ls", "-al", (char *)0);
ref. http://www.opengroup.org/onlinepubs/000095399/functions/exec.html
fork 建立一个 Child Process
看完上面的 exec 你可能会想:不对呀,我不过是想要呼叫其他的
程式,怎麽我就因此被取代了?
这时 fork 就要派上用场了!
fork 这一个函式的功用就是完整地复制一份 Process,不论是他们的
Calling stack、Register、File descriptor 等等全部都复制一份,
然後回传 Process ID。这里,我们说完整的意思是这二个 Process
的执行的状态(Global Variable、Local Variable、Function Call
等等)几乎可以直接互换。而 File Descriptor 也会被 dup 一份。
唯一的不同就是呼叫 fork 的 Process,也就是正本,其 fork 函式
的回传值会是复本的 Process ID。而复本的 fork 的回传值一定是 0。
(如果没有错误产生的话)
我们可以把 fork 想像成一个很强大的人类复制机。今天 LoganChien
可以 fork 出一个 LxxxxCxxxx,他们二个不论是 DNA、记忆、外型都
一样。甚至包含按下复制前一刻的记忆都一样。唯一的不同就是我知
道复制人是 LxxxxCxxxx,而 LxxxxCxxxx 只知道他是一个复制人,不
知道他是由 LoganChien 复制出来的。
为什麽要这样设计?答案很简单!
因为不这样设计,LxxxxCxxxx 不就
造反了?LoganChien 说 LxxxxCxxxx 是他的复制人,LxxxxCxxxx 说
LoganChien 是他的复制人。
而作业系统中的 Process 也是这样。我们 fork 之後,会产生二个
Process。基本上这二个 Process 是很像是,除了 Process ID 与
fork 的回传值,基本上这二个 Process 是一样的!所以为了让
Parent Process 可以掌控 Child Process,所以 Parent Process
的 fork 回传值是 Child Process 的 Process ID。而 Child
Process 只会得到 0。
这样设计还有另一个用意,我们可以再同一份程式码处理 Parent
Process 与 Child Process 的情况。我们通常是这样写 fork
的函式呼叫的:
/* 程式码: fork.c */
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h> /* Required for wait() */
#include <unistd.h>
int main()
{
pid_t proc = fork(); /* Create Child Process */
if (proc < 0) /* Failed to Create Child Process */
{
printf("Error: Unable to fork.\n");
exit(EXIT_FAILURE);
}
else if (proc == 0) /* In the Child Process */
{
printf("In the child process.\n");
exit(25);
}
else /* In the Parent Process */
{
printf("In the parent process.\n");
int status = -1;
wait(&status); /* Wait for Child Process */
printf("The Child Process Returned with %d\n", WEXITSTATUS(status));
}
return EXIT_SUCCESS;
}
我们会先用 fork 建立一个 Child Process。之後二个 Process 就
是各跑各的,除非 Parent Process 用 wait 来等待 Child Process
执行完毕。当然,照 POSIX API 的惯例,fork() 回传负的值,就代
表有错误发生。
在我们有了 fork 与 exec* 之後,我们就可以执行、呼叫另一个可
执行档了!原理很简单,我们只要在 Child Process 呼叫 exec*
就没有问题了。范例如下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
printf("Let's invoke ls command!\n\n");
pid_t proc = fork();
if (proc < 0)
{
printf("Error: Unable to fork.\n");
exit(EXIT_FAILURE);
}
else if (proc == 0)
{
if (
execlp("ls", "ls", "-al", (char *)0) == -1)
{
printf("Error: Unable to load the executable.\n");
exit(EXIT_FAILURE);
}
/* NEVER REACHED */
}
else
{
int status = -1;
wait(&status);
printf("\nThe exit code of ls is %d.\n", WEXITSTATUS(status));
}
return EXIT_SUCCESS;
}
(未完,待续)
--
LoganChien ----- from PTT2 个板 logan -----
--
※ 发信站: 批踢踢实业坊(ptt.cc)
◆ From: 140.112.247.159
1F:推 integritywei:头推 03/19 01:18
2F:推 bombom:额头推 03/19 01:22
3F:推 avogau:眼睛推 03/19 01:49
4F:推 iownthegame:以极快的速度推文 03/19 01:55
5F:推 xflash96:大推。 03/19 07:53
6F:推 pj2:We will discuss pipe, fork and exec in details later. 03/19 09:26
7F:→ pj2:So far, you should know why and how to use dup2 03/19 09:27
8F:→ pj2:Thank 子翔 for the complement 03/19 09:30
9F:推 hrs113355:推 03/19 17:18
10F:推 Daniel1147:推 03/19 20:45
11F:推 dennis2030:今天终於静下心来看完了 很实用! 03/26 00:05
12F:推 averangeall:太完美的教学文了!!!!!!!!!!!!! 04/18 16:40
13F:推 Bingojkt:教学文全消推1@w< 04/19 18:15
14F:推 happy8155: 朝圣推~~ 10/15 17:30
15F:推 stitch8467: b02首推 11/07 00:04