天天看點

Linux管道(pipe)的那些事1 管道(pipe)2 fork3. 檔案描述符(file descriptor)

1 管道(pipe)

Linux 中的管道可用于不同程序之間的通信,其操作符為 “|”。 通常管道隻能在具有新緣關系(父子或擁有相同祖先)的程序間通信。而有名管道克服了管道沒有名字的限制,是以它可允許無親緣關系程序間的通信。

1.1 實作機制

管道是由核心管理的一個緩沖區,管道的一端連接配接一個程序的輸出,這個程序會向管道中放入資訊。管道的另一端連接配接一個程序的輸入,這個程序取出被放入管道的資訊。當管道中沒有資訊的話,從管道中讀取的程序會等待,直到另一端的程序放入資訊。當管道被放滿資訊的時候,嘗試放入資訊的程序會等待,直到另一端的程序取出資訊。當兩個程序都終結的時候,管道也自動消失。

管道的利用POSIX系統中的fork機制建立的。fork機制會将原程序複制到新程序,并且将原程序對其緩沖區的連接配接也一塊複制過來。這樣原程序和新程序都擁有了對同一緩沖區的(即管道)的讀寫能力,同時,每個程序關閉自己不需要的一個連接配接,一個隻留讀連接配接,一個隻留寫連接配接,這樣就形成了管道。

1.2 操作舉例

顯示前三個檔案的檔案名:

  1. ls | head -3
  2. barry.txt
  3. bob
  4. example.png

将一個程式的輸出傳遞給less指令使其易于檢視:

  1. ls -l /etc | less
  2. (Full screen of output you may scroll. Try it yourself to see.)

列出目前目錄下所有具寫權限的檔案:

ls -l | grep '^.....w' drwxrwxr-x 3 ryan users 4096 Jan 21 04:12 dropbox

1.3 命名管道

由于基于fork機制的限制,管道隻能用于父程序和子程序之間,或者有相同祖先的兩個子程序之間的通信。為了解決這一問題,Linux提供了FIFO方式連接配接程序。FIFO又叫做命名管道(named PIPE)。

FIFO (First in, First out)為一種特殊的檔案類型,它在檔案系統中有對應的路徑。FIFO隻是借用了檔案系統來為管道命名。當一個程序以讀(r)的方式打開該檔案,而另一個程序以寫(w)的方式打開該檔案,那麼核心就會在這兩個程序之間建立管道,當删除FIFO檔案時,管道連接配接也随之消失。是以FIFO實際上也由核心管理,不與硬碟打交道。之是以叫FIFO,是因為管道本質上是一個先進先出的隊列資料結構,最早放入的資料被最先讀出來,進而保證資訊交流的順序。命名管道的好處在于我們可以通過檔案的路徑來識别管道,進而讓沒有親緣關系的程序之間建立連接配接。

用ls指令檢視所建立的管道:

$ ls -lF /tmp/my_fifo

prwxr-xr-x 1 root root 0 05-08 20:10 /tmp/my_fifo|

2 fork

2.1 程序

在說fork之前,我們先來複習一下作業系統中程序的相關内容: 程序可以看做程式的一次執行過程,且是擁有資源的最小機關和排程機關(在引入線程的作業系統中,線程是最小的排程機關)。在linux中,每個程序有唯一的PID(程序辨別符)辨別。PID是一個從1到32768的正整數,其中1是特殊程序init,其它程序從2開始依次編号。當用完32768後,從2重新開始。

Linux中有一個叫程序表的結構用來存儲目前正在運作的程序。可以使用“ps aux”指令檢視所有正在運作的程序。

程序在linux中呈樹狀結構,init為根節點,其它程序均有父程序,某程序的父程序就是啟動這個程序的程序,這個程序叫做父程序的子程序。

2.2 fork

在Linux系統中建立程序的方式有兩種:一是由作業系統建立,二是由父程序建立程序(通常為子程序),即fork。一個程序(父程序)調用fork()函數後,系統先給新的程序(子程序)配置設定資源,然後把原來程序的所有資料(變量、環境變量、程式計數器等)都複制到新的新程序中,隻有少數值與原來的程序的值不同,相當于克隆了一個自己。

子程序是父程序的副本,它将獲得父程序資料空間、堆、棧等資源的副本,子程序資料空間中的内容是父程序的完整拷貝。注意,子程序持有的是上述存儲空間的“副本”,這意味着父子程序間不共享這些存儲空間,它們之間共享的存儲空間隻有代碼段。,但隻有一點不同,如果fork成功,子程序中fork的傳回值是0, 父程序中fork的傳回值是子程序的程序号,如果fork不成功,父程序會傳回錯誤。 

從性能方面考慮,父程序到子程序的資料拷貝并不是建立時就拷貝了的,而是采用了寫時拷貝(copy-on -write)技術來處理。用fork建立的子程序和父程序作為異步的并發程序而單獨執行,它們都有獨自的程序辨別符(PID)。異步是指它們各行其事,互相間不進行同步;并發是指它們可同時執行。是以我們無法知道子程序和父程序哪一個先執行完。

2.3 舉例

C語言版fork例子:

  1. #include <unistd.h>  
  2. #include <stdio.h>   
  3. int main ()   
  4. {   
  5.     pid_t fpid; //fpid表示fork函數傳回的值  
  6.     int count=0;  
  7.     fpid=fork();   
  8.     if (fpid < 0)   
  9.         printf("Error in fork!");   
  10.     else if (fpid == 0) {  
  11.         printf("I'm child process, my process id is %d\n",getpid()); 
  12. count++;
  13.     }  
  14.     else {  
  15.         printf("I'm parent process, my process id is %d\n",getpid()); 
  16. count++; 
  17.     } 
  18.     printf("Count value: %d\n", count);
  19.     return 0;  
  20. }  

運作的結果是:

    i'm child process, my process id is 5574

    Count value: 1

    i'm parent process, my process id is 5573

    Count value: 1

每個程序的PID都可以通過getpid()函數獲得,另外還可以通過getppid()函數獲得其父程序的PID.

調用fork()(fpid=fork())後生成一個子程序,在子程序中,fork()函數的傳回值為0,在父程序中,fork()的傳回值為子程序的PID。此後,兩個程序根據不同的判斷條件(fpid<0; fpid==0)執行不同的代碼指令,它們的執行是互相獨立的。這兩個程序都有一個count變量,這兩個變量雖然值相等,但其實它們屬于不同的程序,是不同的變量,存放在不同的記憶體位址中。另外,子程序生成之後是從fork()函數之後的代碼開始執行的,而不是從#include <unistd.h>處。 這是因為fork操作複制并使用了原程序的程式計數器的緣故。

3. 檔案描述符(file descriptor)

當fork函數生成兩個程序之後,這兩個程序可以利用相同的檔案描述符對同一個檔案進行讀/寫操作,這樣就可以在兩個程序之間傳遞資料了。

3.1 檔案描述符

對于linux/Unix而言,任何事物都以檔案的形式存在。通過檔案不僅可以通路正常資料,還可以通路網絡連接配接和硬體裝置。而檔案描述符就是用來通路這些檔案的入口。像TCP和UDP等網絡應用程式,系統在背景都為該應用程式配置設定了一個檔案描述符,無論這個檔案的本質如何。該檔案描述符為應用程式與基礎作業系統之間的互動提供了通用接口。 檔案描述符是一個非負的整數,它是一個索引值,指向核心中每個程序打開檔案的記錄表。當程序打開一個檔案或建立一個檔案時,核心就向程序傳回一個檔案描述符。當程序需要讀寫檔案時,也需要把檔案描述符作為參數傳遞給相應的函數。 每一個檔案描述符會與一個打開的檔案相對應,同時,不同的檔案描述符也可能指向同一個檔案。相同的檔案可以被不同的程序打開也可以在同一個程序中被多次打開。系統為每一個程序維護了一個檔案描述符表,該表的值都是從0開始的,是以在不同的程序中會看到相同的檔案描述符,這種情況下相同檔案描述符有可能指向同一個檔案,也有可能指向不同的檔案。

3.2 與檔案有關的三個表

在Linux系統中,核心維護着3個與檔案有關的資料結構,它們是: 1)程序級檔案描述符表(file descriptor table) 核心為每個程序維護一個程序級别的檔案描述符表,該表記錄了單個檔案描述符的資訊,包括:控制标志,打開檔案指針等。 2)系統級打開檔案表(open file table) 核心為所有打開的檔案維護一個系統級别的打開檔案描述表,簡稱打開檔案表。記錄了每個打開檔案的資訊,包括: 檔案偏移量(file offset), 可調用read()和write()更新,調用lseek()直接修改。 通路模式(status), 可調用open()設定,其狀态有隻讀,隻寫或讀寫等。 i-node 對象指針 3)檔案系統 i-node 表(i-node table) 每個檔案系統會為存儲于其上的所有檔案(包括目錄)維護一個 i-node 表,單個 i-node 包含的資訊有:檔案類型(正常檔案,目錄,套接字或FIFO)、通路權限(讀、寫、執行等)、檔案鎖清單,檔案大小等。 i-node 存儲在磁盤裝置上,核心在記憶體中維護了一個副本,這裡的i-node表為後者。記憶體中的副本除了原有資訊,還包括:引用計數、所在裝置号以及一些臨時屬性(例如檔案鎖)。

這3個資料結構的關系如下圖所示:

Linux管道(pipe)的那些事1 管道(pipe)2 fork3. 檔案描述符(file descriptor)

3.3 輸入輸出重定向

通常,一個程序啟動時,Linux/Unix系統會首先為其配置設定3個檔案描述符: 0,1和2,它們分别對應系統中的3個檔案:标準輸入(STDIN),标準輸出(STDOUT),标準錯誤輸出(STDERR)。

Linux中可以使用重定向操作來指定檔案描述符,這分為輸入重定向和輸出重定向。在使用輸入重定向(>)時,linux會用重定向指定的檔案來替換标準輸入檔案描述符,它會讀取檔案并提取資料,如同是在鍵盤上輸入的。在使用輸出重定向(>)時,linux會用重定向指定的檔案來替換标準輸出檔案描述符。(>>)表示追加到檔案。“&”表示引用檔案描述符。 1.臨時重定向 echo "This is only in the file" > file 此消息将隻輸出到file檔案,而不輸出到螢幕。它的原理就是将标準輸出重定向到了檔案file. 這裡省略了标準輸出檔案描述符“1”。實際應為: echo "This is only in the file" 1>file. 2.永久重定向 exec 1> file将标準輸出重定向到檔案file

echo "This is only in the file"   

3.輸入重定向 cat <file 将file檔案作為cat指令的輸入

4.重定向檔案描述符

exec 3>&1      #将檔案描述符3重定向至1,任何發送給檔案描述符3的内容都将輸出至終端顯示器

exec 1>file     #将發送至檔案描述符1的内容重定向至檔案file

echo "this should be put in the file"

exec 1>&3 #将此時的标準輸出重定向至檔案描述符3,而3指向的是終端顯示器,是以此時正常輸出至顯示器

echo "this is the normal output"

3.4 檔案描述符的設定

為了防止系統資源的耗盡,linux核心對檔案打開的數量進行了限制。這種限制有兩個層面,一個是使用者層面的限制,一個是系統層面的限制。

ulimit指令看到的是使用者級的最大檔案描述符限制,也就是說每一個使用者登入後執行的程式占用檔案描述符的總數不能超過這個限制

[[email protected] ~]# ulimit  -n

10240

設定程序能打開的最大檔案句柄數:ulimit -n xxx

[[email protected] ~]# ulimit  -n 10240

10240

sysctl指令和proc檔案中檢視到的數值是一樣的,這屬于系統級限制,它是限制所有使用者打開檔案描述符的總和

[[email protected] ~]# sysctl -a | grep -i file-max --color

fs.file-max = 392036

[[email protected] ~]# cat /proc/sys/fs/file-max

392036

修改系統層面的限制需要修改/proc/sys/fs/file-max中的值并且使用"sysctl -p"使之永久生效。

3.5 檔案描述符複制與管道的建立

為了生成linux中的管道,首先使用pipe()函數得到一對檔案描述符,它們是隻讀檔案描述符和隻寫檔案描述符。fork()函數執行之後,子程序會将父程序的資料拷貝一份,同樣,子程序也會擁有父程序所有檔案描述符的副本。這時在父程序中關閉讀檔案描述符,隻留下寫檔案描述符;而在子程序則關閉寫檔案描述符,隻留下讀檔案描述符。當父程序進行寫操作而子程序進行讀操作時,就相當于兩個程序在通信,管道就形成了。其實,使用者程式的系統調用仍然是通常的檔案操作,而核心卻利用這種抽象機制實作了管道這一特殊操作。

管道建立圖示:

Linux管道(pipe)的那些事1 管道(pipe)2 fork3. 檔案描述符(file descriptor)

繼續閱讀