linux程序間通訊(IPC)機制總結
阿新 • • 發佈:2019-01-07
在linux下的多個程序間的通訊機制叫做IPC(Inter-Process Communication),它是多個程序之間相互溝通的一種方法。在linux下有多種程序間通訊的方法:半雙工管道、命名管道、訊息佇列、訊號、訊號量、共享記憶體、記憶體對映檔案,套接字等等。使用這些機制可以為linux下的網路伺服器開發提供靈活而又堅固的框架。
1. 管道 (PIPE)
管道實際是用於程序間通訊的一段共享記憶體,建立管道的程序稱為管道伺服器,連線到一個管道的程序為管道客戶機。一個程序在向管道寫入資料後,另一程序就可以從管道的另一端將其讀取出來。 管道的特點: 1、管道是半雙工的,資料只能向一個方向流動;需要雙方通訊時,需要建立起兩個管道; 2、只能用於父子程序或者兄弟程序之間(具有親緣關係的程序)。比如fork或exec建立的新程序,在使用exec建立新程序時,需要將管道的檔案描述符作為引數傳遞給exec建立的新程序。當父程序與使用fork建立的子程序直接通訊時,傳送資料的程序關閉讀端,接受資料的程序關閉寫端。 3、單獨構成一種獨立的檔案系統:管道對於管道兩端的程序而言,就是一個檔案,但它不是普通的檔案,它不屬於某種檔案系統,而是自立門戶,單獨構成一種檔案系統,並且只存在與記憶體中。 4、資料的讀出和寫入:一個程序向管道中寫的內容被管道另一端的程序讀出。寫入的內容每次都新增在管道緩衝區的末尾,並且每次都是從緩衝區的頭部讀出資料。
pipe函式原型:
管道讀寫規則 當沒有資料可讀時 O_NONBLOCK disable:read呼叫阻塞,即程序暫停執行,一直等到有資料來到為止。 O_NONBLOCK enable:read呼叫返回-1,errno值為EAGAIN。 當管道滿的時候 O_NONBLOCK disable: write呼叫阻塞,直到有程序讀走資料 O_NONBLOCK enable:呼叫返回-1,errno值為EAGAIN 如果所有管道寫端對應的檔案描述符被關閉,則read返回0 如果所有管道讀端對應的檔案描述符被關閉,則write操作會產生訊號SIGPIPE 當要寫入的資料量不大於PIPE_BUF(Posix.1要求PIPE_BUF至少512 位元組)時,linux將保證寫入的原子性。
當要寫入的資料量大於PIPE_BUF時,linux將不再保證寫入的原子性。
2. 命名管道(FIFO)
命名管道是一種特殊型別的檔案,它在系統中以檔案形式存在。這樣克服了管道的弊端,他可以允許沒有親緣關係的程序間通訊。
建立管道的兩個系統呼叫原型:
訊號機制是unix系統中最為古老的程序之間的通訊機制,用於一個或幾個程序之間傳遞非同步訊號。訊號可以有各種非同步事件產生,比如鍵盤中斷等。shell也可以使用訊號將作業控制命令傳遞給它的子程序。 在此列出幾個簡單使用方法定義:
訊息佇列是核心地址空間中的內部連結串列,通過linux核心在各個程序直接傳遞內容,訊息順序地傳送到訊息佇列中,並以幾種不同的方式從佇列中獲得,每個訊息佇列可以用IPC識別符號唯一地進行識別。核心中的訊息佇列是通過IPC的識別符號來區別,不同的訊息佇列直接是相互獨立的。每個訊息佇列中的訊息,又構成一個獨立的連結串列。 訊息佇列克服了訊號承載資訊量少,管道只能承載無格式字元流。
訊息佇列標頭檔案:
5. 訊號量(Semaphore)
訊號量是一種計數器,用於控制對多個程序共享的資源進行的訪問。它們常常被用作一個鎖機制,在某個程序正在對特定的資源進行操作時,訊號量可以防止另一個程序去訪問它。
訊號量是特殊的變數,它只取正整數值並且只允許對這個值進行兩種操作:等待(wait)和訊號(signal)。(P、V操作,P用於等待,V用於訊號)
p(sv):如果sv的值大於0,就給它減1;如果它的值等於0,就掛起該程序的執行
V(sv):如果有其他程序因等待sv而被掛起,就讓它恢復執行;如果沒有其他程序因等待sv而掛起,則給它加1
簡單理解就是P相當於申請資源,V相當於釋放資源
訊號量標頭檔案:
6. 共享記憶體(Share Memory)
共享記憶體是在多個程序之間共享記憶體區域的一種程序間的通訊方式,由IPC為程序建立的一個特殊地址範圍,它將出現在該程序的地址空間(這裡的地址空間具體是哪個地方?)中。其他程序可以將同一段共享記憶體連線到自己的地址空間中。所有程序都可以訪問共享記憶體中的地址,就好像它們是malloc分配的一樣。如果一個程序向共享記憶體中寫入了資料,所做的改動將立刻被其他程序看到。 共享記憶體是IPC最快捷的方式,因為共享記憶體方式的通訊沒有中間過程,而管道、訊息佇列等方式則是需要將資料通過中間機制進行轉換。共享記憶體方式直接將某段記憶體段進行對映,多個程序間的共享記憶體是同一塊的物理空間,僅僅對映到各程序的地址不同而已,因此不需要進行復制,可以直接使用此段空間。
注意:共享記憶體本身並沒有同步機制,需要程式設計師自己控制。
共享記憶體標頭檔案:
訊息佇列、訊號量以及共享記憶體的相似之處: 它們被統稱為XSI IPC,它們在核心中有相似的IPC結構(訊息佇列的msgid_ds,訊號量的semid_ds,共享記憶體的shmid_ds),而且都用一個非負整數的識別符號加以引用(訊息佇列的msg_id,訊號量的sem_id,共享記憶體的shm_id,分別通過msgget、semget以及shmget獲得),標誌符是IPC物件的內部名,每個IPC物件都有一個鍵(key_t key)相關聯,將這個鍵作為該物件的外部名。 XSI IPC和PIPE、FIFO的區別: 1、XSI IPC的IPC結構是在系統範圍內起作用,沒用使用引用計數。如果一個程序建立一個訊息佇列,並在訊息佇列中放入幾個訊息,程序終止後,即使現在已經沒有程式使用該訊息佇列,訊息佇列及其內容依然保留。而PIPE在最後一個引用管道的程序終止時,管道就被完全刪除了。對於FIFO最後一個引用FIFO的程序終止時,雖然FIFO還在系統,但是其中的內容會被刪除。 2、和PIPE、FIFO不一樣,XSI IPC不使用檔案描述符,所以不能用ls檢視IPC物件,不能用rm命令刪除,不能用chmod命令刪除它們的訪問許可權。只能使用ipcs和ipcrm來檢視可以刪除它們。
7. 記憶體對映(Memory Map)
記憶體對映檔案,是由一個檔案到一塊記憶體的對映。記憶體對映檔案與虛擬記憶體有些類似,通過記憶體對映檔案可以保留一個地址的區域, 同時將物理儲存器提交給此區域,記憶體檔案對映的物理儲存器來自一個已經存在於磁碟上的檔案,而且在對該檔案進行操作之前必須首先對檔案進行對映。使用記憶體對映檔案處理儲存於磁碟上的檔案時,將不必再對檔案執行I/O操作。每一個使用該機制的程序通過把同一個共享的檔案對映到自己的程序地址空間來實現多個程序間的通訊(這裡類似於共享記憶體,只要有一個程序對這塊對映檔案的記憶體進行操作,其他程序也能夠馬上看到)。 使用記憶體對映檔案不僅可以實現多個程序間的通訊,還可以用於處理大檔案提高效率。因為我們普通的做法是把磁碟上的檔案先拷貝到核心空間的一個緩衝區再拷貝到使用者空間(記憶體),使用者修改後再將這些資料拷貝到緩衝區再拷貝到磁碟檔案,一共四次拷貝。如果檔案資料量很大,拷貝的開銷是非常大的。那麼問題來了,系統在在進行記憶體對映檔案就不需要資料拷貝?mmap()確實沒有進行資料拷貝,真正的拷貝是在在缺頁中斷處理時進行的,由於mmap()將檔案直接對映到使用者空間,所以中斷處理函式根據這個對映關係,直接將檔案從硬碟拷貝到使用者空間,所以只進行一次資料拷貝。效率高於read/write。 記憶體對映標頭檔案:
共享記憶體和記憶體對映檔案的區別: 記憶體對映檔案是利用虛擬記憶體把檔案對映到程序的地址空間中去,在此之後程序操作檔案,就像操作程序空間裡的地址一樣了,比如使用c語言的memcpy等記憶體操作的函式。這種方法能夠很好的應用在需要頻繁處理一個檔案或者是一個大檔案的場合,這種方式處理IO效率比普通IO效率要高
共享記憶體是記憶體對映檔案的一種特殊情況,記憶體對映的是一塊記憶體,而非磁碟上的檔案。共享記憶體的主語是程序(Process),作業系統預設會給每一個程序分配一個記憶體空間,每一個程序只允許訪問作業系統分配給它的哪一段記憶體,而不能訪問其他程序的。而有時候需要在不同程序之間訪問同一段記憶體,怎麼辦呢?作業系統給出了 建立訪問共享記憶體的API,需要共享記憶體的程序可以通過這一組定義好的API來訪問多個程序之間共有的記憶體,各個程序訪問這一段記憶體就像訪問一個硬碟上的檔案一樣。 記憶體對映檔案與虛擬記憶體的區別和聯絡: 記憶體對映檔案和虛擬記憶體都是作業系統記憶體管理的重要部分,兩者有相似點也有不同點。 聯絡:虛擬記憶體和記憶體對映都是將一部分內容載入到記憶體,另一部放在磁碟上的一種機制。對於使用者而言都是透明的。 區別:虛擬記憶體是硬碟的一部分,是記憶體和硬碟的資料交換區,許多程式執行過程中把暫時不用的程式資料放入這塊虛擬記憶體,節約記憶體資源。記憶體對映是一個檔案到一塊記憶體的對映,這樣程式通過記憶體指標就可以對檔案進行訪問。 虛擬記憶體的硬體基礎是分頁機制。另外一個基礎就是區域性性原理(時間區域性性和空間區域性性),這樣就可以將程式的一部分裝入記憶體,其餘部分留在外存,當訪問資訊不存在,再將所需資料調入記憶體。而記憶體對映檔案並不是區域性性,而是使虛擬地址空間的某個區域銀蛇磁碟的全部或部分內容,通過該區域對被對映的磁碟檔案進行訪問,不必進行檔案I/O也不需要對檔案內容進行緩衝處理。
8. 套接字
套接字機制不但可以單機的不同程序通訊,而且使得跨網機器間程序可以通訊。
套接字的建立和使用與管道是有區別的,套接字明確地將客戶端與服務器區分開來,可以實現多個客戶端連到同一伺服器。
伺服器套接字連線過程描述:
首先,伺服器應用程式用socket建立一個套接字,它是系統分配伺服器程序的類似檔案描述符的資源。 接著,伺服器呼叫bind給套接字命名。這個名字是一個標示符,它允許linux將進入的針對特定埠的連線轉到正確的伺服器程序。 然後,系統呼叫listen函式開始接聽,等待客戶端連線。listen建立一個佇列並將其用於存放來自客戶端的進入連線。 當客戶端呼叫connect請求連線時,伺服器呼叫accept接受客戶端連線,accept此時會建立一個新套接字,用於與這個客戶端進行通訊。
客戶端套接字連線過程描述:
客戶端首先呼叫socket建立一個未命名套接字,讓後將伺服器的命名套接字作為地址來呼叫connect與伺服器建立連線。
只要雙方連線建立成功,我們就可以像操作底層檔案一樣來操作socket套接字實現通訊。
幾個基礎函式定義:
參考: 《linux網路程式設計》 《unix環境高階程式設計》
管道實際是用於程序間通訊的一段共享記憶體,建立管道的程序稱為管道伺服器,連線到一個管道的程序為管道客戶機。一個程序在向管道寫入資料後,另一程序就可以從管道的另一端將其讀取出來。 管道的特點: 1、管道是半雙工的,資料只能向一個方向流動;需要雙方通訊時,需要建立起兩個管道; 2、只能用於父子程序或者兄弟程序之間(具有親緣關係的程序)。比如fork或exec建立的新程序,在使用exec建立新程序時,需要將管道的檔案描述符作為引數傳遞給exec建立的新程序。當父程序與使用fork建立的子程序直接通訊時,傳送資料的程序關閉讀端,接受資料的程序關閉寫端。 3、單獨構成一種獨立的檔案系統:管道對於管道兩端的程序而言,就是一個檔案,但它不是普通的檔案,它不屬於某種檔案系統,而是自立門戶,單獨構成一種檔案系統,並且只存在與記憶體中。 4、資料的讀出和寫入:一個程序向管道中寫的內容被管道另一端的程序讀出。寫入的內容每次都新增在管道緩衝區的末尾,並且每次都是從緩衝區的頭部讀出資料。
管道的實現機制:
管道是由核心管理的一個緩衝區,相當於我們放入記憶體中的一個紙條。管道的一端連線一個程序的輸出。這個程序會向管道中放入資訊。管道的另一端連線一個程序的輸入,這個程序取出被放入管道的資訊。一個緩衝區不需要很大,它被設計成為環形的資料結構,以便管道可以被迴圈利用。當管道中沒有資訊的話,從管道中讀取的程序會等待,直到另一端的程序放入資訊。當管道被放滿資訊的時候,嘗試放入資訊的程序會等待,直到另一端的程序取出資訊。當兩個程序都終結的時候,管道也自動消失。
管道只能在本地計算機中使用,而不可用於網路間的通訊。pipe函式原型:
- #include <unistd.h>
- int pipe(
- eg.int fd[2]
- int result = pipe(fd);
管道讀寫規則 當沒有資料可讀時 O_NONBLOCK disable:read呼叫阻塞,即程序暫停執行,一直等到有資料來到為止。 O_NONBLOCK enable:read呼叫返回-1,errno值為EAGAIN。 當管道滿的時候 O_NONBLOCK disable: write呼叫阻塞,直到有程序讀走資料 O_NONBLOCK enable:呼叫返回-1,errno值為EAGAIN 如果所有管道寫端對應的檔案描述符被關閉,則read返回0 如果所有管道讀端對應的檔案描述符被關閉,則write操作會產生訊號SIGPIPE 當要寫入的資料量不大於PIPE_BUF(Posix.1要求PIPE_BUF至少512
命名管道是一種特殊型別的檔案,它在系統中以檔案形式存在。這樣克服了管道的弊端,他可以允許沒有親緣關係的程序間通訊。
建立管道的兩個系統呼叫原型:
- #include <sys/types.h>
- #include <sys/stat.h>
- int mkfifo(constchar *filename,mode_t mode); //建立一個名字為filename的命名管道,引數mode為該檔案的許可權(mode%~umask),若成功則返回0,否則返回-1,錯誤原因存於errno中。
- eg.mkfifo( "/tmp/cmd_pipe", S_IFIFO | 0666 );
- int mknod(constchar *path, mode_t mode, dev_t dev); //第一個引數表示你要建立的檔案的名稱,第二個引數表示檔案型別,第三個引數表示該檔案對應的裝置檔案的裝置號。只有當檔案型別為 S_IFCHR 或 S_IFBLK 的時候該檔案才有裝置號,建立普通檔案時傳入0即可。
- eg.mknod(FIFO_FILE,S_IFIFO|0666,0);
訊號機制是unix系統中最為古老的程序之間的通訊機制,用於一個或幾個程序之間傳遞非同步訊號。訊號可以有各種非同步事件產生,比如鍵盤中斷等。shell也可以使用訊號將作業控制命令傳遞給它的子程序。 在此列出幾個簡單使用方法定義:
- #include <sys/types.h>
- #include <signal.h>
- void (*signal(int sig,void (*func)(int)))(int); //用於擷取系統訊號,第一個引數為訊號,第二個引數為對此訊號掛接使用者自己的處理函式指標。返回值為以前訊號處理程式的指標。
- eg.int ret = signal(SIGSTOP, sig_handle);
- int kill(pid_t pid,int sig); //kill函式向程序號為pid的程序傳送訊號,訊號值為sig。當pid為0時,向當前系統的所有程序傳送訊號sig。
- int raise(int sig);//向當前程序中自舉一個訊號sig, 即向當前程序傳送訊號。
- #include <unistd.h>
- unsigned int alarm(unsigned int seconds); //alarm()用來設定訊號SIGALRM在經過引數seconds指定的秒數後傳送給目前的程序。如果引數seconds為0,則之前設定的鬧鐘會被取消,並將剩下的時間返回。使用alarm函式的時候要注意alarm函式的覆蓋性,即在一個程序中採用一次alarm函式則該程序之前的alarm函式將失效。
- int pause(void); //使呼叫程序(或執行緒)睡眠狀態,直到接收到訊號,要麼終止,或導致它呼叫一個訊號捕獲函式。
訊息佇列是核心地址空間中的內部連結串列,通過linux核心在各個程序直接傳遞內容,訊息順序地傳送到訊息佇列中,並以幾種不同的方式從佇列中獲得,每個訊息佇列可以用IPC識別符號唯一地進行識別。核心中的訊息佇列是通過IPC的識別符號來區別,不同的訊息佇列直接是相互獨立的。每個訊息佇列中的訊息,又構成一個獨立的連結串列。 訊息佇列克服了訊號承載資訊量少,管道只能承載無格式字元流。
訊息佇列標頭檔案:
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <sys/msg.h>
- struct msgbuf{
- long mtype;
- char mtext[1];//柔性陣列
- }
- struct msgbuf{
- long mtype;
- char mtext[1];//柔性陣列
- }
- <div style="font-family: 微軟雅黑; font-size: 14px; line-height: 21px;">struct msgid_ds{</div><div style="font-family: 微軟雅黑; font-size: 14px; line-height: 21px;"> struct ipc_perm msg_perm{</div><div style="font-family: 微軟雅黑; font-size: 14px; line-height: 21px;"> time_t msg_stime;</div><div style="font-family: 微軟雅黑; font-size: 14px; line-height: 21px;"> time_t msg_rtime;</div><div style="font-family: 微軟雅黑; font-size: 14px; line-height: 21px;"> time_t msg_ctime;</div><div style="font-family: 微軟雅黑; font-size: 14px; line-height: 21px;"> unsigned long _msg_cbuyes;</div><div style="font-family: 微軟雅黑; font-size: 14px; line-height: 21px;"> ..........</div><div style="font-family: 微軟雅黑; font-size: 14px; line-height: 21px;"> };</div>
- struct ipc_perm{
- key_t key;
- uid_t uid;
- gid_t gid;
- .......
- };
- key_t ftok( constchar * fname, int id );//引數一為目錄名稱, 引數二為id。如指定檔案的索引節點號為65538,換算成16進製為0x010002,而你指定的ID值為38,換算成16進製為0x26,則最後的key_t返回值為0x26010002。
- eg.key_t key = key =ftok(".", 1);
- int msgget(key_t key,int msgflag); //msgget用來建立和訪問一個訊息佇列。程式必須提供一個鍵值來命名特定的訊息佇列。
- eg.int msg_id = msgget(key, IPC_CREATE | IPC_EXCL | 0x0666);//根據關鍵字建立一個新的佇列(IPC_CREATE),如果佇列存在則出錯(IPC_EXCL),擁有對檔案的讀寫執行許可權(0666)。
- int msgsnd(int msgid,constvoid *msgptr,size_t msg_sz,int msgflg); //msgsnd函式允許我們把一條訊息新增到訊息佇列中。msgptr只想準備傳送訊息的指標,指標結構體必須以一個長整型變數開始。
- eg.struct msgmbuf{
- int mtype;
- char mtext[10];
- };
- struct msgmbuf msg_mbuf;
- msg_mbuf.mtype = 10;//訊息大小10位元組
- memcpy(msg_mbuf.mtext, "測試訊息", sizeof("測試訊息"));
- int ret = msgsnd(msg_id, &msg_mbuf, sizeof("測試訊息"), IPC_NOWAIT);
- int msgrcv(int msgid, void *msgptr, size_t msg_sz, longint msgtype, int msgflg); //msgrcv可以通過msqid對指定訊息佇列進行接收操作。第二個引數為訊息緩衝區變數地址,第三個引數為訊息緩衝區結構大小,但是不包括mtype成員長度,第四個引數為mtype指定從佇列中獲取的訊息型別。
- eg.int ret = msgrcv(msg_id, &msg_mbuf, 10, 10, IPC_NOWAIT | MSG_NOERROR);
- int msgctl(int msqid,int cmd,struct msqid_ds *buf); //msgctl函式主要是一些控制如刪除訊息佇列等操作。 cmd值如下:
- IPC_STAT:獲取佇列的msgid_ds結構,並把它存到buf指向的地址。
- IPC_SET:將佇列的msgid_ds設定為buf指向的msgid_ds。
- IPC_RMID:核心刪除訊息佇列,最後一項填NULL, 執行操作後,核心會把訊息佇列從系統中刪除。
5. 訊號量(Semaphore)
訊號量是一種計數器,用於控制對多個程序共享的資源進行的訪問。它們常常被用作一個鎖機制,在某個程序正在對特定的資源進行操作時,訊號量可以防止另一個程序去訪問它。
訊號量是特殊的變數,它只取正整數值並且只允許對這個值進行兩種操作:等待(wait)和訊號(signal)。(P、V操作,P用於等待,V用於訊號)
p(sv):如果sv的值大於0,就給它減1;如果它的值等於0,就掛起該程序的執行
V(sv):如果有其他程序因等待sv而被掛起,就讓它恢復執行;如果沒有其他程序因等待sv而掛起,則給它加1
簡單理解就是P相當於申請資源,V相當於釋放資源
訊號量標頭檔案:
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <sys/sem.h>
- struct semid_ds{
- struct ipc_perm sem_perm;
- unsigned short sem_nsems;
- time_t sem_otime;
- time_t sem_ctime;
- ...
- }
- union semun{
- int val;
- struct semid_ds *buf;
- unsigned short *array;
- struct seminfo *__buf;
- }
- struct sembuf{
- ushort sem_num;//訊號量的編號
- short sem_op;//訊號量的操作。如果為正,則從訊號量中加上一個值,如果為負,則從訊號量中減掉一個值,如果為0,則將程序設定為睡眠狀態,直到訊號量的值為0為止。
- short sem_flg;//訊號的操作標誌,一般為IPC_NOWAIT。
- }
- int semget(key_t key, int num_sems, int sem_flags); //semget函式用於建立一個新的訊號量集合 , 或者訪問一個現有的集合(不同程序只要key值相同即可訪問同一訊號量集合)。第一個引數key是ftok生成的鍵值,第二個引數num_sems可以指定在新的集合應該建立的訊號量的數目,第三個引數sem_flags是開啟訊號量的方式。
- eg.int semid = semget(key, 0, IPC_CREATE | IPC_EXCL | 0666);//第三個引數參考訊息佇列int msgget(key_t key,int msgflag);第二個引數。
- int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops); //semop函式用於改變訊號量的值。第二個引數是要在訊號集合上執行操作的一個數組,第三個引數是該陣列操作的個數 。
- eg.struct sembuf sops = {0, +1, IPC_NOWAIT};//對索引值為0的訊號量加一。
- semop(semid, &sops, 1);//以上功能執行的次數為一次。
- int semctl(int sem_id, int sem_num, int command,...); //semctl函式用於訊號量集合執行控制操作,初始化訊號量的值,刪除一個訊號量等。 類似於呼叫msgctl(), msgctl()是用於訊息佇列上的操作。第一個引數是指定的訊號量集合(semget的返回值),第二個引數是要執行操作的訊號量在集合中的索引值(例如集合中第一個訊號量下標為0),第三個command引數代表要在集合上執行的命令。
- IPC_STAT:獲取某個集合的semid_ds結構,並把它儲存到semun聯合體的buf引數指向的地址。
- IPC_SET:將某個集合的semid_ds結構的ipc_perm成員的值。該命令所取的值是從semun聯合體的buf引數中取到。
- IPC_RMID:核心刪除該訊號量集合。
- GETVAL:返回集合中某個訊號量的值。
- SETVAL:把集合中單個訊號量的值設定成為聯合體val成員的值。
6. 共享記憶體(Share Memory)
共享記憶體是在多個程序之間共享記憶體區域的一種程序間的通訊方式,由IPC為程序建立的一個特殊地址範圍,它將出現在該程序的地址空間(這裡的地址空間具體是哪個地方?)中。其他程序可以將同一段共享記憶體連線到自己的地址空間中。所有程序都可以訪問共享記憶體中的地址,就好像它們是malloc分配的一樣。如果一個程序向共享記憶體中寫入了資料,所做的改動將立刻被其他程序看到。 共享記憶體是IPC最快捷的方式,因為共享記憶體方式的通訊沒有中間過程,而管道、訊息佇列等方式則是需要將資料通過中間機制進行轉換。共享記憶體方式直接將某段記憶體段進行對映,多個程序間的共享記憶體是同一塊的物理空間,僅僅對映到各程序的地址不同而已,因此不需要進行復制,可以直接使用此段空間。
注意:共享記憶體本身並沒有同步機制,需要程式設計師自己控制。
共享記憶體標頭檔案:
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <sys/shm.h>
- strcut shmid_ds{
- struct ipc_perm shm_perm;
- size_t shm_segsz;
- time_t shm_atime;
- time_t shm_dtime;
- ......
- }
- int shmget(key_t key,size_t size,int shmflg); //shmget函式用來建立一個新的共享記憶體段, 或者訪問一個現有的共享記憶體段(不同程序只要key值相同即可訪問同一共享記憶體段)。第一個引數key是ftok生成的鍵值,第二個引數size為共享記憶體的大小,第三個引數sem_flags是開啟共享記憶體的方式。
- eg.int shmid = shmget(key, 1024, IPC_CREATE | IPC_EXCL | 0666);//第三個引數參考訊息佇列int msgget(key_t key,int msgflag);
- void *shmat(int shm_id,constvoid *shm_addr,int shmflg); //shmat函式通過shm_id將共享記憶體連線到程序的地址空間中。第二個引數可以由使用者指定共享記憶體對映到程序空間的地址,shm_addr如果為0,則由核心試著查詢一個未對映的區域。返回值為共享記憶體對映的地址。
- eg.char *shms = (char *)shmat(shmid, 0, 0);//shmid由shmget獲得
- int shmdt(constvoid *shm_addr); //shmdt函式將共享記憶體從當前程序中分離。 引數為共享記憶體對映的地址。
- eg.shmdt(shms);
- int shmctl(int shm_id,int cmd,struct shmid_ds *buf);//shmctl函式是控制函式,使用方法和訊息佇列msgctl()函式呼叫完全類似。引數一shm_id是共享記憶體的控制代碼,cmd是向共享記憶體傳送的命令,最後一個引數buf是向共享記憶體傳送命令的引數。
訊息佇列、訊號量以及共享記憶體的相似之處: 它們被統稱為XSI IPC,它們在核心中有相似的IPC結構(訊息佇列的msgid_ds,訊號量的semid_ds,共享記憶體的shmid_ds),而且都用一個非負整數的識別符號加以引用(訊息佇列的msg_id,訊號量的sem_id,共享記憶體的shm_id,分別通過msgget、semget以及shmget獲得),標誌符是IPC物件的內部名,每個IPC物件都有一個鍵(key_t key)相關聯,將這個鍵作為該物件的外部名。 XSI IPC和PIPE、FIFO的區別: 1、XSI IPC的IPC結構是在系統範圍內起作用,沒用使用引用計數。如果一個程序建立一個訊息佇列,並在訊息佇列中放入幾個訊息,程序終止後,即使現在已經沒有程式使用該訊息佇列,訊息佇列及其內容依然保留。而PIPE在最後一個引用管道的程序終止時,管道就被完全刪除了。對於FIFO最後一個引用FIFO的程序終止時,雖然FIFO還在系統,但是其中的內容會被刪除。 2、和PIPE、FIFO不一樣,XSI IPC不使用檔案描述符,所以不能用ls檢視IPC物件,不能用rm命令刪除,不能用chmod命令刪除它們的訪問許可權。只能使用ipcs和ipcrm來檢視可以刪除它們。
7. 記憶體對映(Memory Map)
記憶體對映檔案,是由一個檔案到一塊記憶體的對映。記憶體對映檔案與虛擬記憶體有些類似,通過記憶體對映檔案可以保留一個地址的區域, 同時將物理儲存器提交給此區域,記憶體檔案對映的物理儲存器來自一個已經存在於磁碟上的檔案,而且在對該檔案進行操作之前必須首先對檔案進行對映。使用記憶體對映檔案處理儲存於磁碟上的檔案時,將不必再對檔案執行I/O操作。每一個使用該機制的程序通過把同一個共享的檔案對映到自己的程序地址空間來實現多個程序間的通訊(這裡類似於共享記憶體,只要有一個程序對這塊對映檔案的記憶體進行操作,其他程序也能夠馬上看到)。 使用記憶體對映檔案不僅可以實現多個程序間的通訊,還可以用於處理大檔案提高效率。因為我們普通的做法是把磁碟上的檔案先拷貝到核心空間的一個緩衝區再拷貝到使用者空間(記憶體),使用者修改後再將這些資料拷貝到緩衝區再拷貝到磁碟檔案,一共四次拷貝。如果檔案資料量很大,拷貝的開銷是非常大的。那麼問題來了,系統在在進行記憶體對映檔案就不需要資料拷貝?mmap()確實沒有進行資料拷貝,真正的拷貝是在在缺頁中斷處理時進行的,由於mmap()將檔案直接對映到使用者空間,所以中斷處理函式根據這個對映關係,直接將檔案從硬碟拷貝到使用者空間,所以只進行一次資料拷貝。效率高於read/write。 記憶體對映標頭檔案:
- #include <sys.mman.h>
- void *mmap(void*start,size_t length,int prot,int flags,int fd,off_t offset); //mmap函式將一個檔案或者其它物件對映進記憶體。 第一個引數為對映區的開始地址,設定為0表示由系統決定對映區的起始地址,第二個引數為對映的長度,第三個引數為期望的記憶體保護標誌,第四個引數是指定對映物件的型別,第五個引數為檔案描述符(指明要對映的檔案),第六個引數是被對映物件內容的起點。成功返回被對映區的指標,失敗返回MAP_FAILED[其值為(void *)-1]。
- int munmap(void* start,size_t length); //munmap函式用來取消引數start所指的對映記憶體起始地址,引數length則是欲取消的記憶體大小。如果解除對映成功則返回0,否則返回-1,錯誤原因存於errno中錯誤程式碼EINVAL。
- int msync(void *addr,size_t len,int flags); //msync函式實現磁碟檔案內容和共享記憶體取內容一致,即同步。第一個引數為檔案對映到程序空間的地址,第二個引數為對映空間的大小,第三個引數為重新整理的引數設定。
共享記憶體和記憶體對映檔案的區別: 記憶體對映檔案是利用虛擬記憶體把檔案對映到程序的地址空間中去,在此之後程序操作檔案,就像操作程序空間裡的地址一樣了,比如使用c語言的memcpy等記憶體操作的函式。這種方法能夠很好的應用在需要頻繁處理一個檔案或者是一個大檔案的場合,這種方式處理IO效率比普通IO效率要高
共享記憶體是記憶體對映檔案的一種特殊情況,記憶體對映的是一塊記憶體,而非磁碟上的檔案。共享記憶體的主語是程序(Process),作業系統預設會給每一個程序分配一個記憶體空間,每一個程序只允許訪問作業系統分配給它的哪一段記憶體,而不能訪問其他程序的。而有時候需要在不同程序之間訪問同一段記憶體,怎麼辦呢?作業系統給出了 建立訪問共享記憶體的API,需要共享記憶體的程序可以通過這一組定義好的API來訪問多個程序之間共有的記憶體,各個程序訪問這一段記憶體就像訪問一個硬碟上的檔案一樣。 記憶體對映檔案與虛擬記憶體的區別和聯絡: 記憶體對映檔案和虛擬記憶體都是作業系統記憶體管理的重要部分,兩者有相似點也有不同點。 聯絡:虛擬記憶體和記憶體對映都是將一部分內容載入到記憶體,另一部放在磁碟上的一種機制。對於使用者而言都是透明的。 區別:虛擬記憶體是硬碟的一部分,是記憶體和硬碟的資料交換區,許多程式執行過程中把暫時不用的程式資料放入這塊虛擬記憶體,節約記憶體資源。記憶體對映是一個檔案到一塊記憶體的對映,這樣程式通過記憶體指標就可以對檔案進行訪問。 虛擬記憶體的硬體基礎是分頁機制。另外一個基礎就是區域性性原理(時間區域性性和空間區域性性),這樣就可以將程式的一部分裝入記憶體,其餘部分留在外存,當訪問資訊不存在,再將所需資料調入記憶體。而記憶體對映檔案並不是區域性性,而是使虛擬地址空間的某個區域銀蛇磁碟的全部或部分內容,通過該區域對被對映的磁碟檔案進行訪問,不必進行檔案I/O也不需要對檔案內容進行緩衝處理。
8. 套接字
套接字機制不但可以單機的不同程序通訊,而且使得跨網機器間程序可以通訊。
套接字的建立和使用與管道是有區別的,套接字明確地將客戶端與服務器區分開來,可以實現多個客戶端連到同一伺服器。
伺服器套接字連線過程描述:
首先,伺服器應用程式用socket建立一個套接字,它是系統分配伺服器程序的類似檔案描述符的資源。 接著,伺服器呼叫bind給套接字命名。這個名字是一個標示符,它允許linux將進入的針對特定埠的連線轉到正確的伺服器程序。 然後,系統呼叫listen函式開始接聽,等待客戶端連線。listen建立一個佇列並將其用於存放來自客戶端的進入連線。 當客戶端呼叫connect請求連線時,伺服器呼叫accept接受客戶端連線,accept此時會建立一個新套接字,用於與這個客戶端進行通訊。
客戶端套接字連線過程描述:
客戶端首先呼叫socket建立一個未命名套接字,讓後將伺服器的命名套接字作為地址來呼叫connect與伺服器建立連線。
只要雙方連線建立成功,我們就可以像操作底層檔案一樣來操作socket套接字實現通訊。
幾個基礎函式定義:
- #include <sys/types.h>
- #include <sys/socket.h>
- int socket(it domain,int type,int protocal);
- int bind(int socket,conststruct sockaddr *address,size_t address_len);
- int listen(int socket,int backlog);
- int accept(int socket,struct sockaddr *address,size_t *address_len);
- int connect(int socket,conststruct sockaddr *addrsss,size_t address_len);
參考: 《linux網路程式設計》 《unix環境高階程式設計》