看板 SuperTree
作者 標題 [系程] 教學: 簡介 fork, exec*, pipe, dup2
時間 2017年08月14日 Mon. PM 08:07:53
※ 本文轉寄自 dick51207.bbs@ptt.cc
看板 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] |
*/
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 03/19 01:18
推 :額頭推2F 03/19 01:22
推 :眼睛推3F 03/19 01:49
推 :以極快的速度推文4F 03/19 01:55
推 :大推。5F 03/19 07:53
推 :We will discuss pipe, fork and exec in details later.6F 03/19 09:26
→ :So far, you should know why and how to use dup2
→ :Thank 子翔 for the complement
→ :So far, you should know why and how to use dup2
→ :Thank 子翔 for the complement
推 :推9F 03/19 17:18
推 :推10F 03/19 20:45
推 :今天終於靜下心來看完了 很實用!11F 03/26 00:05
推 :太完美的教學文了!!!!!!!!!!!!!12F 04/18 16:40
推 :教學文全消推1@w<13F 04/19 18:15
推 : 朝聖推~~14F 10/15 17:30
推 : b02首推15F 11/07 00:04
--
※ 同主題文章:
08-14 20:07 Re [系程] 教學: 簡介 fork, exec*, pipe, dup2
● 08-14 20:07 □ [系程] 教學: 簡介 fork, exec*, pipe, dup2
※ 看板: SuperTree 文章推薦值: 0 目前人氣: 0 累積人氣: 45
回列表(←)
分享