看板 b97902HW 關於我們 聯絡資訊
簡介 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
integritywei:頭推 03/19 01:18
bombom:額頭推 03/19 01:22
avogau:眼睛推 03/19 01:49
iownthegame:以極快的速度推文 03/19 01:55
xflash96:大推。 03/19 07:53
pj2:We will discuss pipe, fork and exec in details later. 03/19 09:26
pj2:So far, you should know why and how to use dup2 03/19 09:27
pj2:Thank 子翔 for the complement 03/19 09:30
hrs113355:推 03/19 17:18
Daniel1147:推 03/19 20:45
dennis2030:今天終於靜下心來看完了 很實用! 03/26 00:05
averangeall:太完美的教學文了!!!!!!!!!!!!! 04/18 16:40
Bingojkt:教學文全消推1@w< 04/19 18:15
happy8155: 朝聖推~~ 10/15 17:30
stitch8467: b02首推 11/07 00:04