1. 程式人生 > >Shell I/O重定向的原理解釋

Shell I/O重定向的原理解釋

在Unix系統中,每個程序都有STDIN、STDOUT和STDERR這3種標準I/O,它們是程式最通用的輸入輸出方式。幾乎所有語言都有相應的 標準I/O函式 ,比如,C語言可以通過scanf從終端輸入字元,通過printf向終端輸出字元。熟悉Shell的朋友都知道,我們可以方便地對 Shell命令進行I/O重定向,比如 $find -name "*.java" >testfile.txt 把當前目錄下的Java檔案列表重定向到testfile.txt。

多數情況下,我們只需要瞭解I/O重定向的使用就夠了,但是如果要程式設計實現類似Shell的I/O重定向以及管道功能,那麼就需要清楚它的原理和實現。

下面本文就以Linux系統為具體例子,介紹I/O重定向的原理和實現(文中實驗環境為Ubuntu 12.04,核心版本3.2.0-59)。

檔案描述符表
理解I/O重定向的原理需要從Linux核心為程序所維護的關鍵資料結構:檔案描述符表 入手。對Linux程序來講,每個開啟的檔案都是通過檔案描述符(File Descriptor)來標識的,核心為每個程序維護了一個檔案描述符表,這個表以FD為索引,再進一步指向檔案的詳細資訊。在程序建立時,核心為程序默 認建立了 0、1、2 三個特殊的FD,這就是STDIN、STDOUT和STDERR
如下圖所示意:
這裡寫圖片描述

所謂的I/O重定向也就是讓已建立的FD指向其他檔案。


比如,下面是對STDOUT重定向到testfile.txt前後核心檔案描述符表變化的示意圖
重定向前:
這裡寫圖片描述
重定向後:
這裡寫圖片描述

在I/O重定向的過程中,不變的是FD 0/1/2代表STDIN/STDOUT/STDERR,變化的是檔案描述符表中FD 0/1/2對應的具體檔案,應用程式只關心前者。本質上這和介面的原理是相通的,通過一個間接層把功能的使用者和提供者解耦。

下面我們通過strace命令跟蹤一下echo命令的系統呼叫:

dagang@ubuntu12:~$ strace echo hello 2>&1 >/dev/null | grep write
write(1, "hello\n"
, 6) = 6

我們可以看到 write(1, “hello\n”, 6) 這樣一個系統呼叫,它的第一個引數1就是代表的STDOUT的FD,這說明對於echo程式,它只管(通過標準I/O函式從STDOUT)向FD 1寫入,而不關心它們FD 1到底對應的是哪個檔案。
Shell正是通過I/O重定向和管道這種特殊的檔案把多個程式的STDIN和STDOUT串聯在一起組成更復雜功能的。
下面是Shell中通過管道的示意圖:
這裡寫圖片描述

下面我們用一個實際的例子來體驗一下:

dagang@ubuntu12:~$ sleep 30 | sleep 40 &
[1] 5584
dagang@ubuntu12:~$ pgrep -l sleep
5583 sleep
5584 sleep
dagang@ubuntu12:~$ ll /proc/5583/fd
total 0
lrwx------ 1 dagang dagang 64 Feb 27 13:41 0 -> /dev/pts/3
l-wx------ 1 dagang dagang 64 Feb 27 13:41 1 -> pipe:[246469]
lrwx------ 1 dagang dagang 64 Feb 27 13:41 2 -> /dev/pts/3
dagang@ubuntu12:~$ ll /proc/5584/fd
total 0
lr-x------ 1 dagang dagang 64 Feb 27 13:41 0 -> pipe:[246469]
lrwx------ 1 dagang dagang 64 Feb 27 13:41 1 -> /dev/pts/3
lrwx------ 1 dagang dagang 64 Feb 27 13:41 2 -> /dev/pts/3

上面我們啟動了兩個程序5583和5584,通過檢視/proc/fd,我們看到程序5583的STDOUT和5584的STDIN都被重定向到了pipe:[246469],如此達到了連線兩個程序標準I/O的目的。

dup2()系統呼叫
上面介紹了檔案描述符表和I/O重定向的原理,那麼在Linux系統中如何通過C程式實現I/O重定向呢?
主要用到了dup2()這個系統呼叫,man中關於dup2是這樣說的:

int dup2(int oldfd, int newfd);
dup2() create a copy of the file descriptor oldfd. After a successful return from dup() or dup2(), the old and new file descriptors may be used interchangeably. They refer to the same open file description (see open(2)) and thus share file offset and file status flags; for example, if the file offset is modified by using lseek(2) on one of the descriptors, the offset is also changed for the other.

這裡我們通過一個實際的問題來說明它的使用方法:
編寫一個C程式,通過呼叫sort這個Shell命令進行排序,要求把in.txt和out.txt分別重定向到sort的STDIN,STDOUT。
參考實現:

int main() {
    int pid = 0;
    // fork a worker process
    if (pid = fork()) {
        // wait for completion of the child process
        int status; 
        waitpid(pid, &status, 0);
    }
    else {
        // open input and output files
        int fd_in = open("in.txt", O_RDONLY);
        int fd_out = open("out.txt", O_CREAT | O_RDWR, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
        if (fd_in > 0 && fd_out > 0) {
            // redirect STDIN/STDOUT for this process
            dup2(fd_in, 0);
            dup2(fd_out, 1);  
            // call shell command
            system("sort");
            close(fd_in);
            close(fd_out);
        }
        else {
            // ... error handling
        }
    }
    return 0;
}

上面的主要步驟包括:
1. 首先fork一個子程序,後續步驟都在子程序中完成,父程序通過waitpid()系統呼叫等待子程序結束;
2. 開啟open()系統呼叫開啟in.txt和out.txt,得到它們的描述符(在我的測試中,這兩個值通常為3和4);
3. 通過dup2()系統呼叫把STDIN重定向到fd_in,把STDOUT重定向到fd_out(注意,重定向的影響範圍是整個子程序);
4. 通過system()系統呼叫執行shell命令sort。

通過上面的例子我們就瞭解最基本的I/O重定向的實現方法,接下來,你能否根據這些知識進一步實現出Shell的管道特性呢?

總結
本文介紹了Linux系統I/O重定向的原理和實現方式。
最重要的是理解檔案描述符和檔案描述符表的概念,以及標準I/O所對應的特殊 FD;實現方面主要是瞭解dup2()系統呼叫的功能和用法。
最後需要注意的是,dup2()不僅可以用來對標準I/O重定向,對任何FD都是可以的,這是 習慣使用Shell進行標準I/O重定向的朋友容易忽略的。