轉載至http://www.javashuo.com/article/p-msgyixni-bs.htmlhtml
首先,先來說一下fork以後,發生了什麼事情。node
由fork建立的新進程被稱爲子進程(child process)。該函數被調用一次,但返回兩次。兩次返回的區別是子進程的返回值是0,而父進程的返回值則是新進程(子進程)的進程 id。將子進程id返回給父進程的理由是:由於一個進程的子進程能夠多於一個,沒有一個函數使一個進程能夠得到其全部子進程的進程id。對子進程來講,之因此fork返回0給它,是由於它隨時能夠調用getpid()來獲取本身的pid;也能夠調用getppid()來獲取父進程的id。(進程id 0老是由交換進程使用,因此一個子進程的進程id不可能爲0 )。linux
fork以後,操做系統會複製一個與父進程徹底相同的子進程,雖然說是父子關係,可是在操做系統看來,他們更像兄弟關係,這2個進程共享代碼空間,可是數據空間是互相獨立的,子進程數據空間中的內容是父進程的完整拷貝,指令指針也徹底相同,子進程擁有父進程當前運行到的位置(兩進程的程序計數器pc值相同,也就是說,子進程是從fork返回處開始執行的),但有一點不一樣,若是fork成功,子進程中fork的返回值是0,父進程中fork的返回值是子進程的進程號,若是fork不成功,父進程會返回錯誤。
能夠這樣想象,2個進程一直同時運行,並且步調一致,在fork以後,他們分別做不一樣的工做,也就是分岔了。這也是fork爲何叫fork的緣由算法
至於那一個最早運行,可能與操做系統(調度算法)有關,並且這個問題在實際應用中並不重要,若是須要父子進程協同,能夠經過原語的辦法解決。shell
1. 管道pipe:管道是一種半雙工的通訊方式,數據只能單向流動,並且只能在具備親緣關係的進程間使用。進程的親緣關係一般是指父子進程關係。
2. 命名管道FIFO:有名管道也是半雙工的通訊方式,可是它容許無親緣關係進程間的通訊。
4. 消息隊列MessageQueue:消息隊列是由消息的鏈表,存放在內核中並由消息隊列標識符標識。消息隊列克服了信號傳遞信息少、管道只能承載無格式字節流以及緩衝區大小受限等缺點。
5. 共享存儲SharedMemory:共享內存就是映射一段能被其餘進程所訪問的內存,這段共享內存由一個進程建立,但多個進程均可以訪問。共享內存是最快的 IPC 方式,它是針對其餘進程間通訊方式運行效率低而專門設計的。它每每與其餘通訊機制,如信號兩,配合使用,來實現進程間的同步和通訊。
6. 信號量Semaphore:信號量是一個計數器,能夠用來控制多個進程對共享資源的訪問。它常做爲一種鎖機制,防止某進程正在訪問共享資源時,其餘進程也訪問該資源。所以,主要做爲進程間以及同一進程內不一樣線程之間的同步手段。
7. 套接字Socket:套解口也是一種進程間通訊機制,與其餘通訊機制不一樣的是,它可用於不一樣及其間的進程通訊。
8. 信號 ( sinal ) : 信號是一種比較複雜的通訊方式,用於通知接收進程某個事件已經發生。異步
信號是Linux系統中用於進程之間通訊或操做的一種機制,信號能夠在任什麼時候候發送給某一進程,而無須知道該進程的狀態。若是該進程並未處於執行狀態,則該信號就由內核保存起來,知道該進程恢復執行並傳遞給他爲止。若是一個信號被進程設置爲阻塞,則該信號的傳遞被延遲,直到其阻塞被取消時才被傳遞給進程。函數
Linux提供了幾十種信號,分別表明着不一樣的意義。信號之間依靠他們的值來區分,可是一般在程序中使用信號的名字來表示一個信號。在Linux系統中,這些信號和以他們的名稱命名的常量被定義在/usr/includebitssignum.h文件中。一般程序中直接包含<signal.h>就好。spa
信號是在軟件層次上對中斷機制的一種模擬,是一種異步通訊方式,信號能夠在用戶空間進程和內核之間直接交互。內核也能夠利用信號來通知用戶空間的進程來通知用戶空間發生了哪些系統事件。信號事件有兩個來源:操作系統
1)硬件來源,例如按下了cltr+C,一般產生中斷信號sigint線程
2)軟件來源,例如使用系統調用或者命令發出信號。最經常使用的發送信號的系統函數是kill,raise,setitimer,sigation,sigqueue函數。軟件來源還包括一些非法運算等操做。
一旦有信號產生,用戶進程對信號產生的相應有三種方式:
1)執行默認操做,linux對每種信號都規定了默認操做。
2)捕捉信號,定義信號處理函數,當信號發生時,執行相應的處理函數。
3)忽略信號,當不但願接收到的信號對進程的執行產生影響,而讓進程繼續執行時,能夠忽略該信號,即不對信號進程做任何處理。
有兩個信號是應用進程沒法捕捉和忽略的,即SIGKILL和SEGSTOP,這是爲了使系統管理員能在任什麼時候候中斷或結束某一特定的進程。
上圖表示了Linux中常見的命令
信號發送的關鍵使得系統知道向哪一個進程發送信號以及發送什麼信號。下面是信號操做中經常使用的函數:
例子:建立子進程,爲了使子進程不在父進程發出信號前結束,子進程中使用raise函數發送sigstop信號,使本身暫停;父進程使用信號操做的kill函數,向子進程發送sigkill信號,子進程收到此信號,結束子進程。
當某個信號被髮送到一個正在運行的進程時,該進程即對次特定的信號註冊相應的信號處理函數,以完成所需處理。設置信號處理方式的是signal函數,在程序正常結束前,在應用signal函數恢復系統對信號的
默認處理方式。
有時候既不但願進程在接收到信號時馬上中斷進程的執行,也不但願此信號徹底被忽略掉,而是但願延遲一段時間再去調用信號處理函數,這個時候就須要信號阻塞來完成。
例子:主程序阻塞了cltr+c的sigint信號。用sigpromask將sigint假如阻塞信號集合。
管道容許在進程之間按先進先出的方式傳送數據,是進程間通訊的一種常見方式。
管道是Linux 支持的最初Unix IPC形式之一,具備如下特色:
1) 管道是半雙工的,數據只能向一個方向流動;須要雙方通訊時,須要創建起兩個管道;
2) 匿名管道只能用於父子進程或者兄弟進程之間(具備親緣關係的進程);
3) 單獨構成一種獨立的文件系統:管道對於管道兩端的進程而言,就是一個文件,但它不是普通的文件,它不屬於某種文件系統,而是自立門戶,單獨構成一種文件系統,而且只存在與內存中。
管道分爲pipe(無名管道)和fifo(命名管道)兩種,除了創建、打開、刪除的方式不一樣外,這兩種管道幾乎是同樣的。他們都是經過內核緩衝區實現數據傳輸。
管道的實質是一個內核緩衝區,進程以先進先出的方式從緩衝區存取數據:管道一端的進程順序地將進程數據寫入緩衝區,另外一端的進程則順序地讀取數據,該緩衝區能夠看作一個循環隊列,讀和寫的位置都是自動增長的,一個數據只能被讀一次,讀出之後再緩衝區都不復存在了。當緩衝區讀空或者寫滿時,有必定的規則控制相應的讀進程或寫進程是否進入等待隊列,當空的緩衝區有新數據寫入或慢的緩衝區有數據讀出時,就喚醒等待隊列中的進程繼續讀寫。
pipe的例子:父進程建立管道,並在管道中寫入數據,而子進程從管道讀出數據
和無名管道的主要區別在於,命名管道有一個名字,命名管道的名字對應於一個磁盤索引節點,有了這個文件名,任何進程有相應的權限均可以對它進行訪問。
而無名管道卻不一樣,進程只能訪問本身或祖先建立的管道,而不能訪任意訪問已經存在的管道——由於沒有名字。
Linux中經過系統調用mknod()或mkfifo()來建立一個命名管道。最簡單的方式是經過直接使用shell
mkfifo myfifo
等價於
mknod myfifo p
以上命令在當前目錄下建立了一個名爲myfifo的命名管道。用ls -p命令查看文件的類型時,能夠看到命名管道對應的文件名後有一條豎線"|",表示該文件不是普通文件而是命名管道。
使用open()函數經過文件名能夠打開已經建立的命名管道,而無名管道不能由open來打開。當一個命名管道再也不被任何進程打開時,它沒有消失,還能夠再次被打開,就像打開一個磁盤文件同樣。
能夠用刪除普通文件的方法將其刪除,實際刪除的是磁盤上對應的節點信息。
例子:用命名管道實現聊天程序,一個張三端,一個李四端。兩個程序都創建兩個命名管道,fifo1,fifo2,張三寫fifo1,李四讀fifo1;李四寫fifo2,張三讀fifo2。
用select把,管道描述符和stdin假如集合,用select進行阻塞,若是有i/o的時候喚醒進程。(粉紅色部分爲select部分,黃色部分爲命名管道部分)
在linux系統中,除了用pipe系統調用創建管道外,還可使用C函數庫中管道函數popen函數來創建管道,使用pclose關閉管道。
例子:設計一個程序用popen建立管道,實現 ls -l |grep main.c的功能
分析:先用popen函數建立一個讀管道,調用fread函數將ls -l的結果存入buf變量,用printf函數輸出內容,用pclose關閉讀管道;
接着用popen函數建立一個寫管道,調用fprintf函數將buf的內容寫入管道,運行grep命令。
popen的函數原型:
FILE* popen(const char* command,const char* type);
參數說明:command是子進程要執行的命令,type表示管道的類型,r表示讀管道,w表明寫管道。若是成功返回管道文件的指針,不然返回NULL。
使用popen函數讀寫管道,實際上也是調用pipe函數調用創建一個管道,再調用fork函數創建子進程,接着會創建一個shell 環境,並在這個shell環境中執行參數所指定的進程。
消息隊列,就是一個消息的鏈表,是一系列保存在內核中消息的列表。用戶進程能夠向消息隊列添加消息,也能夠向消息隊列讀取消息。
消息隊列與管道通訊相比,其優點是對每一個消息指定特定的消息類型,接收的時候不須要按照隊列次序,而是能夠根據自定義條件接收特定類型的消息。
能夠把消息看作一個記錄,具備特定的格式以及特定的優先級。對消息隊列有寫權限的進程能夠向消息隊列中按照必定的規則添加新消息,對消息隊列有讀權限的進程能夠從消息隊列中讀取消息。
消息隊列的經常使用函數以下表:
進程間經過消息隊列通訊,主要是:建立或打開消息隊列,添加消息,讀取消息和控制消息隊列。
例子:用函數msget建立消息隊列,調用msgsnd函數,把輸入的字符串添加到消息隊列中,而後調用msgrcv函數,讀取消息隊列中的消息並打印輸出,最後再調用msgctl函數,刪除系統內核中的消息隊列。(黃色部分是消息隊列相關的關鍵代碼,粉色部分是讀取stdin的關鍵代碼)
共享內存容許兩個或多個進程共享一個給定的存儲區,這一段存儲區能夠被兩個或兩個以上的進程映射至自身的地址空間中,一個進程寫入共享內存的信息,能夠被其餘使用這個共享內存的進程,經過一個簡單的內存讀取錯作讀出,從而實現了進程間的通訊。
採用共享內存進行通訊的一個主要好處是效率高,由於進程能夠直接讀寫內存,而不須要任何數據的拷貝,對於像管道和消息隊裏等通訊方式,則須要再內核和用戶空間進行四次的數據拷貝,而共享內存則只拷貝兩次:一次從輸入文件到共享內存區,另外一次從共享內存到輸出文件。
通常而言,進程之間在共享內存時,並不老是讀寫少許數據後就解除映射,有新的通訊時在從新創建共享內存區域;而是保持共享區域,直到通訊完畢爲止,這樣,數據內容一直保存在共享內存中,並無寫回文件。共享內存中的內容每每是在解除映射時才寫回文件,所以,採用共享內存的通訊方式效率很是高。
共享內存有兩種實現方式:一、內存映射 二、共享內存機制
內存映射 memory map機制使進程之間經過映射同一個普通文件實現共享內存,經過mmap()系統調用實現。普通文件被映射到進程地址空間後,進程能夠
像訪問普通內存同樣對文件進行訪問,沒必要再調用read/write等文件操做函數。
例子:建立子進程,父子進程經過匿名映射實現共享內存。
分析:主程序中先調用mmap映射內存,而後再調用fork函數建立進程。那麼在調用fork函數以後,子進程繼承父進程匿名映射後的地址空間,一樣也繼承mmap函數的返回地址,這樣,父子進程就能夠經過映射區域進行通訊了。
IPC的共享內存指的是把全部的共享數據放在共享內存區域(IPC shared memory region),任何想要訪問該數據的進程都必須在本進程的地址空間新增一塊內存區域,用來映射存放共享數據的物理內存頁面。
和前面的mmap系統調用經過映射一個普通文件實現共享內存不一樣,UNIX system V共享內存是經過映射特殊文件系統shm中的文件實現進程間的共享內存通訊。
例子:設計兩個程序,經過unix system v共享內存機制,一個程序寫入共享區域,另外一個程序讀取共享區域。
分析:一個程序調用fotk函數產生標準的key,接着調用shmget函數,獲取共享內存區域的id,調用shmat函數,映射內存,循環計算年齡,另外一個程序讀取共享內存。
(fotk函數在消息隊列部分已經用過了,
根據pathname指定的文件(或目錄)名稱,以及proj參數指定的數字,ftok函數爲IPC對象生成一個惟一性的鍵值。)
key_t ftok(char* pathname,char proj)