Unix的標(biāo)準(zhǔn)I/O與重定向的若干概念解析
標(biāo)準(zhǔn)I/O與重定向的若干概念
3個(gè)標(biāo)準(zhǔn)文件描述符
所有的Unix工具都使用文件描述符0、1和2。如下圖所示,標(biāo)準(zhǔn)輸入文件的描述符是0,標(biāo)準(zhǔn)輸出的文件描述符是1,標(biāo)準(zhǔn)錯(cuò)誤輸出的文件描述符則是2。Unix假設(shè)文件描述符0、1和2都已經(jīng)被打開(kāi),可以分別進(jìn)行讀、寫和寫的操作。
重定向I/O的是shell而不是程序
通過(guò)使用輸出重定向標(biāo)志,命令cmd>filename告訴shell將文件描述符1定位到文件。于是shell就將文件描述符與指定的文件連接起來(lái)。程序持續(xù)不斷地將數(shù)據(jù)寫到文件描述符1中,根本沒(méi)有意識(shí)到數(shù)據(jù)的目的地已經(jīng)改變了。listargs.c展示了程序甚至沒(méi)有看到命令行中的重定向符號(hào)。
- #include <stdio.h>
- int main(int ac, char* av[]) {
- int i;
- printf("Number of args: %d, Args are: \n", ac);
- for(i = 0; i < ac; i++) {
- printf("args[%d] %s\n", i, av[i]);
- }
- fprintf(stderr, "This message is sent to stderr.\n");
- }
程序listargs將命令行參數(shù)打印到標(biāo)準(zhǔn)輸出。注意listargs并沒(méi)有打印出重定向符號(hào)和文件名。
如上圖所示驗(yàn)證了關(guān)于shell輸出重定向的一些重要概念。
- shell并不將重定向標(biāo)記和文件名傳遞給程序。
- 重定向可以出現(xiàn)在命令行中的任何地方,并且在重定向標(biāo)識(shí)符周圍并不需要空格來(lái)區(qū)分。例如上圖命令./listargs testing >xyz one two 2>oops也可以寫成./listargs >xyz testing one two 2>oops,如下圖所示。
***可用文件描述符(Lowest-Available-fd)原則
文件描述符是一個(gè)數(shù)組的索引號(hào)。每個(gè)進(jìn)程都有其打開(kāi)的一組文件,這些打開(kāi)的文件被保持在一個(gè)數(shù)組中。文件描述符即為某文件在此數(shù)組中的索引。并且,當(dāng)打開(kāi)文件時(shí),為此文件安排的文件描述符總是此數(shù)組中***可用位置的索引。
將stdin重定向到文件
考慮如何將標(biāo)準(zhǔn)輸入重定向以至可以從文件中讀取數(shù)據(jù)。更加精確的說(shuō),進(jìn)程并不是從文件讀數(shù)據(jù),而是從文件描述符讀取數(shù)據(jù)。如果將文件描述符0重定向到一個(gè)文件,那么此文件就成為標(biāo)準(zhǔn)輸入的源。
方法1:close-then-open
***種放方法是close-then-open策略,具體步驟如下:
- 開(kāi)始時(shí),系統(tǒng)中采用的是典型的設(shè)置,即三種標(biāo)準(zhǔn)流是被連接到終端設(shè)備上的。輸入的數(shù)據(jù)流經(jīng)過(guò)文件描述符0而輸出的流經(jīng)過(guò)文件描述符1和2。
- 接下來(lái),調(diào)用close(0),將標(biāo)準(zhǔn)輸入與終端設(shè)備的連接切斷。
- ***,使用open(filename, O_RDONLY)打開(kāi)一個(gè)想連接到stdin上的文件。當(dāng)前的***可用文件描述符是0,因此所打開(kāi)的文件將被連接到標(biāo)準(zhǔn)輸入上。任何從標(biāo)準(zhǔn)輸入讀取數(shù)據(jù)的函數(shù)都將從此文件中讀取數(shù)據(jù)。
方法2:open-close-dup-close
Unix系統(tǒng)調(diào)用dup建立指向已經(jīng)存在的文件描述符的第二個(gè)連接,這種方法需要4個(gè)步驟。
- open(file),打開(kāi)stdin將要重定向的文件。這個(gè)調(diào)用返回一個(gè)文件描述符fd,這個(gè)描述符并不是0,因?yàn)?在當(dāng)前已經(jīng)被打開(kāi)了。
- close(0),將文件描述符0關(guān)閉,現(xiàn)在文件描述符0已經(jīng)空閑了。
- dup(fd),系統(tǒng)調(diào)用dup(fd)將文件描述符fd做了一個(gè)復(fù)制。此處復(fù)制使用***可用的文件描述符號(hào)。因此獲得的文件描述符是0。這樣,就將磁盤文件與文件描述符0連接在一起了。
- close(fd),使用close(fd)來(lái)關(guān)閉原始連接,只留下文件描述符0的連接。
dup在學(xué)習(xí)管道的時(shí)候非常重要,一個(gè)簡(jiǎn)單一點(diǎn)的方案是將close(0)和dup(fd)結(jié)合在一起作為一個(gè)單獨(dú)的系統(tǒng)調(diào)用dup2。
重定向I/O:who>userlist
當(dāng)輸入who>userlist時(shí),shell運(yùn)行who程序,并將who的標(biāo)準(zhǔn)輸出重定向到名為userlist的文件上。shell實(shí)現(xiàn)該重定向的關(guān)鍵之處在于fork和exec之間的時(shí)間間隙。在fork執(zhí)行完后,子進(jìn)程仍然在運(yùn)行父進(jìn)程也就是shell程序,并準(zhǔn)備執(zhí)行exec。exec將替換進(jìn)程中運(yùn)行的程序,但是它不會(huì)改變進(jìn)程的屬性和進(jìn)程中所有的連接。也就是說(shuō),在運(yùn)行exec之后,進(jìn)程的用戶ID不會(huì)改變,其優(yōu)先級(jí)也不會(huì)改變,并且其文件描述符也和運(yùn)行exec之前一樣。因此,利用這個(gè)原則來(lái)實(shí)現(xiàn)重定向標(biāo)準(zhǔn)輸出。
此時(shí)who就是子進(jìn)程要執(zhí)行的命令,當(dāng)執(zhí)行fork前,父進(jìn)程的文件描述符1指向終端。當(dāng)執(zhí)行fork之后,子進(jìn)程的文件描述符也喜歡指向終端,此時(shí),子進(jìn)程嘗試執(zhí)行close(1),close(1)之后,文件描述符1成為***未用文件描述符,子進(jìn)程現(xiàn)在再執(zhí)行creat(userlist, mode)打開(kāi)文件userlist,文件描述符1被連接到文件userlist。因此,子進(jìn)程的標(biāo)準(zhǔn)輸出被重定向到文件userlist,子進(jìn)程然后調(diào)用exec執(zhí)行who。
子進(jìn)程執(zhí)行了who程序,于是子進(jìn)程中的代碼和數(shù)據(jù)都被who程序的代碼和數(shù)據(jù)所替換了,然而文件描述符被保留下來(lái)。因?yàn)榇蜷_(kāi)的文件并非是程序的代碼也不是數(shù)據(jù),它們屬于進(jìn)程的屬性,因此exec調(diào)用并不改變它們。
管道編程
管道是內(nèi)核中一個(gè)單向的數(shù)據(jù)通道,管道有一個(gè)讀取端和一個(gè)寫入端,可以用來(lái)連接一個(gè)進(jìn)程的輸出和另一個(gè)進(jìn)程的輸入。
創(chuàng)建管道
使用系統(tǒng)調(diào)用result = pipe(int array[2])來(lái)創(chuàng)建管道,并將其兩端連接到兩個(gè)文件描述符。如下圖所示,array[0]為讀取數(shù)據(jù)端的文件描述符,而array[1]則為寫數(shù)據(jù)端的文件描述符。類似與open調(diào)用,pipe調(diào)用也使用***可用文件描述符。
程序pipedemo.c展示了如何創(chuàng)建管道并使用管道向自己發(fā)送數(shù)據(jù)。核心代碼如下:
- int len, i, apipe[2];
- char buf[BUFSIZ];
- if(pipe(apipe) == -1) {
- perror("could not make pipe.");
- exit(1);
- }
- printf("Got a pipe! It is file descriptors: {%d %d}\n", apipe[0], apipe[1]);
- while(fgets(buf, BUFSIZ, stdin)) {
- len = strlen(buf);
- if(write(apipe[1], buf, len) != len) {
- perror("writing to pipe.");
- break;
- }
- for(i = 0; i < len; i++) {
- buf[i] = 'X';
- }
- len = read(apipe[0], buf, BUFSIZ);
- if(len == -1) {
- perror("reading from pipe.");
- break;
- }
- if(write(1, buf, len) != len) {
- perror("writing to stdout");
- break;
- }
- }
數(shù)據(jù)流從鍵盤到進(jìn)程,從進(jìn)程到管道,再?gòu)墓艿赖竭M(jìn)程以及從進(jìn)程回到終端。
使用fork來(lái)共享管道
當(dāng)進(jìn)程創(chuàng)建一個(gè)管道之后,該進(jìn)程就有了連向管道兩端的連接。當(dāng)這個(gè)進(jìn)程調(diào)用fork的時(shí)候,它的子進(jìn)程也得到了這兩個(gè)連向管道的連接。父進(jìn)程和子進(jìn)程都可以將數(shù)據(jù)寫到管道的寫數(shù)據(jù)端口,并從讀數(shù)據(jù)端口將數(shù)據(jù)讀出。但是當(dāng)一個(gè)進(jìn)程讀,而另一個(gè)進(jìn)程寫的時(shí)候,管道的使用效率是***的。程序pipedemo2.c說(shuō)明了如何將pipe和fork結(jié)合起來(lái),創(chuàng)建一對(duì)通過(guò)管道來(lái)通信的進(jìn)程,核心代碼如下:
- int pipefd[2];
- int len;
- char buf[BUFSIZ];
- int read_len;
- if(pipe(pipefd) == -1) {
- oops("cannot get a pipe", 1);
- }
- switch(fork()) {
- case -1:
- oops("cannot fork", 2);
- /*子進(jìn)程*/
- case 0:
- len = strlen(CHILD_MESS);
- while(1) {
- if(write(pipefd[1], CHILD_MESS, len) != len) {
- oops("write", 3);
- }
- sleep(5);
- }
- /*父進(jìn)程*/
- default:
- len = strlen(PAR_MESS);
- while(1) {
- if(write(pipefd[1], PAR_MESS, len) != len) {
- oops("write", 4);
- }
- sleep(1);
- read_len = read(pipefd[0], buf, BUFSIZ);
- if(read_len <= 0) {
- break;
- }
- write(1, buf, read_len);
- }
- }
技術(shù)細(xì)節(jié)
- 從管道中讀取數(shù)據(jù)
當(dāng)進(jìn)程試圖從管道讀取數(shù)據(jù)時(shí),進(jìn)程被掛起直到數(shù)據(jù)被寫進(jìn)管道。
當(dāng)所有的寫進(jìn)程關(guān)閉了管道的寫數(shù)據(jù)端時(shí),試圖從管道中讀取數(shù)據(jù)的調(diào)用會(huì)返回0,這意味這文件的結(jié)束。 - 向管道中寫數(shù)據(jù)
寫入數(shù)據(jù)阻塞直到管道有空間去容納新的數(shù)據(jù)。
如果所有的讀進(jìn)程都已關(guān)閉了管道的讀數(shù)據(jù)端,那么對(duì)管道的寫入調(diào)用將會(huì)執(zhí)行失敗。
總結(jié)
- Unix默認(rèn)從文件描述符0讀取數(shù)據(jù),寫數(shù)據(jù)到文件描述符1,將錯(cuò)誤信息輸出到文件描述符2。
- 創(chuàng)建文件描述符的系統(tǒng)調(diào)用總是使用***可用文件描述符號(hào)。
- 重定向標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出和錯(cuò)誤輸出意味著改變文件描述符0、1和2的連接。
- 管道是內(nèi)核中的一個(gè)數(shù)據(jù)隊(duì)列,其每一端連接一個(gè)文件描述符。程序通過(guò)pipe系統(tǒng)調(diào)用來(lái)創(chuàng)建管道。
- 當(dāng)父進(jìn)程調(diào)用fork的時(shí)候,管道的兩端都被復(fù)制到子進(jìn)程中。
- 只有有共同父進(jìn)程的進(jìn)程之間才可以用管道連接。
代碼
相關(guān)代碼見(jiàn)Github。
參考