顯示廣告
隱藏 ✕
看板 SuperTree
作者 dick51207 (dick51207.bbs@ptt.cc)
標題 [系程] 教學: 簡介 fork, exec*, pipe, dup2
時間 2017年08月14日 Mon. PM 08:07:53


※ 本文轉寄自 dick51207.bbs@ptt.cc

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

--
※ 看板: SuperTree 文章推薦值: 0 目前人氣: 0 累積人氣: 45 
分享網址: 複製 已複製
r)回覆 e)編輯 d)刪除 M)收藏 ^x)轉錄 同主題: =)首篇 [)上篇 ])下篇