Linux 操作系統:進程間通信 IPC 方式
進程間通信,即 Interprocess communication(IPC),在現代操作系統中起着至關重要的作用。在計算機系統中,每個進程都擁有獨立的地址空間,這意味着它們不能直接訪問彼此的數據。然而,在許多實際應用場景中,不同的進程需要進行數據交互和協同工作,這就需要進程間通信機制來實現。
進程間通信的重要性主要體現在以下幾個方面。首先,數據傳輸是一個關鍵需求。例如,一個進程可能需要將大量的數據發送給另一個進程,數據量可以從一個字節到幾兆字節不等。其次,共享數據也是常見的需求。多個進程可能需要操作同一份共享數據,當一個進程對共享數據進行修改時,其他進程應能立即看到這些變化。再者,通知事件的功能也很重要。一個進程需要向另一個或一組進程發送消息,通知它們發生了某種特定事件,比如進程終止時要通知父進程。此外,資源共享需要內核提供鎖和同步機制,確保多個進程能夠安全地共享資源。
一、簡介
在 Linux 系統中,進程間通信至關重要。不同的進程通常擁有獨立的虛擬地址空間,這保證了進程的獨立性,但也使得進程間無法直接通信。爲了解決這個問題,Linux 內核提供了多種進程間通信方式。
-
數據傳輸:一個進程可能需要將數據發送給另一個進程。例如,在分佈式計算環境中,不同的計算節點可能需要相互傳遞數據以完成複雜的任務。
-
共享數據:多個進程可能希望操作共享的數據。當一個進程修改了共享數據時,其他共享該數據的進程應能立即看到這個變化。這在多線程編程或並行計算中非常常見,能夠提高系統的效率和協同工作能力。
-
信息傳遞:進程間需要傳遞信息來通知某些事件的發生。比如,當一個進程終止時,它需要通知它的父進程,以便父進程可以採取相應的措施。
-
資源共享:多個進程可能需要共享系統資源,如內存、文件等。爲了實現資源共享,需要內核提供鎖和同步機制,以確保資源的正確使用和避免衝突。
-
進程控制:有些進程,如調試器,希望完全控制另一個進程的執行。控制進程需要能夠攔截目標進程的所有陷入和異常,並及時瞭解目標進程的狀態改變。
⑴進程隔離
進程隔離是爲保護操作系統中進程互不干擾而設計的一組不同硬件和軟件的技術。在 Linux 系統中,進程隔離是通過虛擬地址空間實現的。當創建一個進程時,操作系統會爲該進程分配一個 4GB 大小的虛擬進程地址空間。在 32 位的操作系統中,這樣的設計是因爲一個指針長度是 4 字節,而 4 字節指針的尋址能力是從 0x00000000 到 0xFFFFFFFF,最大值 0xFFFFFFFF 表示的即爲 4GB 大小的容量。針對 Linux 操作系統,將最高的 1G 字節(從虛擬地址 0xC0000000 到 0xFFFFFFFF)供內核使用,稱爲內核空間,而較低的 3G 字節(從虛擬地址 0x00000000 到 0xBFFFFFFF),供各個進程使用,稱爲用戶空間。通過這種方式,不同進程的虛擬地址不同,防止了進程 A 寫入進程 B 的情況發生,實現了進程間的隔離。
⑵虛擬地址空間
虛擬地址空間是爲了解決進程地址空間隔離的問題而創建的。在 Linux 系統中,每個進程都有自己獨立的虛擬地址空間。這個地址空間是 “虛擬” 的,並不是真實存在的,而且每個進程只能訪問自己虛擬地址空間中的數據,無法訪問別的進程中的數據。要使程序在真實的內存上運行,必須在虛擬地址與物理地址間建立一種映射關係。操作系統通過分段、分頁的方法,保證不同進程的地址空間被映射到物理地址空間中不同的區域上,實現了進程間的地址隔離。例如,在 Linux 系統中,進程的用戶空間是獨立的,而內核空間是共有的,進程切換時,用戶空間切換,內核空間不變。
⑶系統調用與內核態 / 用戶態
用戶空間訪問內核空間的唯一方式就是系統調用。當一個任務(進程)執行系統調用而陷入內核代碼中執行時,我們就稱進程處於內核運行態(或簡稱爲內核態),此時處理器處於特權級最高的(0 級)內核代碼中執行。當進程在執行用戶自己的代碼時,則稱其處於用戶運行態(用戶態),即此時處理器在特權級最低的(3 級)用戶代碼中運行。處理器在特權等級高的時候才能執行那些特權 CPU 指令。例如,應用程序訪問文件、網絡等資源時,就需要通過系統調用進入內核態,由內核來執行這些操作,以保障系統的安全和穩定。
二、IPC 通信原理
每個進程各自有不同的用戶地址空間,任何一個進程的全局變量在另一個進程中都看不到,所以進程之間要交換數據必須通過內核, 在內核中開闢一塊緩衝區, 進程 1 把數據從用戶空間拷到內核緩衝區, 進程 2 再從內核緩衝區把數據讀走, 內核提供的這種機制稱爲進程間通信機制。
通常的做法是消息發送方將要發送的數據存放在內存緩存區中,通過系統調用進入內核態。然後內核程序在內核空間分配內存,開闢一塊內核緩存區,內核空間調用 copy_from_user() 函數將數據從用戶空間的內存緩存區拷貝到內核空間的內核緩存區中。
同樣的,接收方進程在接收數據時在自己的用戶空間開闢一塊內存緩存區,然後內核程序調用 copy_to_user() 函數將數據從內核緩存區拷貝到接收進程的用戶空間內存緩存區。這樣數據發送方進程和數據接收方進程就完成了一次數據傳輸,我們稱完成了一次進程間通信。
主要的過程如下圖所示:
三、進程間通信常見的方式
3.1 管道 pipe
⑴匿名管道
匿名管道具有半雙工通信的特點,數據在同一時刻只能在一個方向流動,只能從管道的一端寫入,從另一端讀出。
只能用於具有親緣關係的進程之間進行通信,通常用於父子進程。不會讀到錯誤和亂碼,因爲管道自帶同步機制。
-
管道在進行通信的時候,對外界提供的服務是面向字節流的。
-
管道是依賴於文件系統的,生命週期隨進程,即進程退出,則管道消失。
創建匿名管道使用 pipe 函數,該函數會返回兩個文件描述符,分別指向管道的讀端和寫端。讀寫方式爲父進程調用 pipe 開闢管道,得到兩個文件描述符指向文件的兩端,然後調用 fork 創建子進程,子進程也產生兩個文件描述符指向文件的兩端。由於管道只能實現單向通信,因此需要父子進程關閉適當的讀寫端,父進程關閉讀端,子進程關閉寫端,實現進程間通信。
⑵命名管道
命名管道克服了匿名管道只能在有親緣關係進程間通信的限制,可以在無親緣關係的進程間通信。
命名管道以 FIFO(first input first output)的文件形式存儲於文件系統中,是一個設備文件。即使進程與創建 FIFO 的進程不存在親緣關係,只要可以訪問該路徑,就能夠通過 FIFO 相互通信。
創建命名管道使用 mkfifo 函數,命名管道創建後,使用時必須先調用 open 將其打開。因爲命名管道是一個存在於硬盤上的文件,而管道是存在於內存中的特殊文件。調用 open 打開命名管道的進程可能會阻塞,若同時用讀寫方式(O_RDWR)打開,則一定不會導致阻塞;以只讀方式(O_RDONLY)打開,則調用 open 函數的進程將會被阻塞直到有寫方打開管道;以寫方式(O_WRONLY)打開也會阻塞直到有讀方式打開管道。
3.2 消息隊列 (message queue)
消息隊列是一種通過消息傳遞進行進程間通信的方式。在 Linux 中,消息隊列是由內核維護的消息鏈表,每個消息都有一個唯一的標識符和一個優先級,進程可以通過標識符來發送和接收消息。
消息隊列實現了進程的異步通信,發送和接收消息的進程可以併發執行,不需要等待對方的響應,提高了系統的併發性能。消息隊列中的消息可以持久化存儲,即使接收進程暫時不可用,消息也不會丟失。當接收進程重新啓動後,可以繼續接收未讀取的消息。消息隊列可以支持多對多的通信方式,多個進程可以同時向一個消息隊列發送消息,也可以從同一個消息隊列接收消息。
消息傳遞分爲直接通信傳遞和間接通信方式:
-
直接通信傳遞:發送進程消息,利用 OS 所提供的發送原語,直接把消息發給目標進程。
-
間接通信方式:發送和接收進程都通過共享實體(郵箱)的方式進行消息的發送和接收。可以多個進程往同一個信箱 send 消息,也可以多個進程從同一個信箱中 receive 消息。
消息隊列函數的原型:
// 創建或打開消息隊列:成功返回隊列ID,失敗返回-1
int msgget(key_t key, int flag);
// 添加消息:成功返回0,失敗返回-1
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// 讀取消息:成功返回消息數據的長度,失敗返回-1
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
// 控制消息隊列:成功返回0,失敗返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
代碼演示:
msgSend.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
// int msgget(key_t key, int msgflg);
// int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
// ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
struct msgbuf{
long mtype; /* message type, must be > 0 */
char mtext[128]; /* message data */
};
int main()
{
struct msgbuf sendbuf={888,"message from send"};
struct msgbuf readbuf;
key_t key;
if((key = ftok(".",'z')) < 0){
printf("ftok error\n");
}
int msgId = msgget(key,IPC_CREAT|0777);
if(msgId == -1){
printf("get quen failed\n");
}
msgsnd(msgId,&sendbuf,strlen(sendbuf.mtext),0);
printf("send over\n");
msgrcv(msgId,&readbuf,sizeof(readbuf.mtext),999,0);
printf("read from get is:%s\n",readbuf.mtext);
msgctl(msgId,IPC_RMID,NULL);
return 0;
}
msgGet.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
// int msgget(key_t key, int msgflg);
// int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
// ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
struct msgbuf{
long mtype; /* message type, must be > 0 */
char mtext[128]; /* message data */
};
int main()
{
struct msgbuf readbuf;
memset(readbuf.mtext, '\0', sizeof(readbuf.mtext));
struct msgbuf sendbuf={999,"thank for your reach"};
key_t key;
//獲取key值
if((key = ftok(".",'z')) < 0){
printf("ftok error\n");
}
int msgId = msgget(key,IPC_CREAT|0777);
if(msgId == -1){
printf("get quen failed\n");
perror("why");
}
msgrcv(msgId,&readbuf,sizeof(readbuf.mtext),888,0);
printf("read from send is:%s\n",readbuf.mtext);
msgsnd(msgId,&sendbuf,strlen(sendbuf.mtext),0);
msgctl(msgId,IPC_RMID,NULL);
return 0;
}
3.3 共享內存 (shared memory)
在操作系統中開闢一塊共享空間,允許多個進程共同訪問這一塊共享內存(shared memory)。這裏通過增加頁表項 / 段表項即可將同一片共享內存區映射到各個進程的地址空間中,共享內存是一種最爲高效的進程間通信方式,進程可以直接讀寫內存,而不需要任何數據的拷貝。爲了在多個進程間交換信息,內核專門留出了一塊內存區,可以由需要訪問的進程將其映射到自己的私有地址空間,進程就可以直接讀寫這一內存區而不需要進行數據的拷貝,從而大大提高了效率。
由於多個進程共享一段內存,因此也需要依靠某種同步機制,如互斥鎖和信號量等,防止多個進程同時讀寫共享內存時出現數據混亂。
以 Linux 系統爲例:
// Linux 中,如何實現共享內存:
// 通過 shm_open 系統調用,申請一片共享內存區
int shm_open(const char *name, int oflag, mode_t mode);
// 通過 mmap 系統調用,將共享內存區映射到進程自己的地址空間
void* mmap (void *addr, size_t length, int prot, int flags,int fd, off_t offset);
共享存儲的方式有兩類,一種是基於數據結構的共享,還有一類是基於存儲區的共享。
-
基於數據結構的共享:比如共享空間裏只能放一個長度爲 10 的數組。這種共享方式速度慢、限制多,是一種低級通信方式;
-
基於存儲區的共享:操作系統在內存中劃出一塊共享存儲區,對於數據的形式、存放位置都由通信進程控制,而不是操作系統。這種共享方式內存速度很快,是一種高級通信方式。
共享內存函數的原型:
// 創建或獲取一個共享內存:成功返回共享內存ID,失敗返回-1
int shmget(key_t key, size_t size, int flag);
// 連接共享內存到當前進程的地址空間:成功返回指向共享內存的指針,失敗返回-1
void *shmat(int shm_id, const void *addr, int flag);
// 斷開與共享內存的連接:成功返回0,失敗返回-1
int shmdt(void *addr);
// 控制共享內存的相關信息:成功返回0,失敗返回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
代碼演示:
shmw.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
// int shmget(key_t key, size_t size, int shmflg);
// void *shmat(int shmid, const void *shmaddr, int shmflg);
// int shmdt(const void *shmaddr);
int main()
{
int shmId;
key_t key;
char *shmaddr;
if((key = ftok(".",1)) < 0){
printf("ftok error\n");
}
shmId = shmget(key, 1024*4, IPC_CREAT|0666);//內存大小必須得是MB的整數倍
if(shmId == -1){
printf("shmget error\n");
exit(-1);
}
/*第二個參數一般寫0,讓linux內核自動分配空間,第三個參數也一般寫0,表示可讀可寫*/
shmaddr = shmat(shmId, 0, 0);
printf("shmat OK\n");
strcpy(shmaddr,"I am so cool");
sleep(5);//等待5秒,讓別的進程去讀
shmdt(shmaddr);
shmctl(shmId, IPC_RMID, 0);//寫0表示不關心
printf("quit\n");
return 0;
}
shmr.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
// int shmget(key_t key, size_t size, int shmflg);
// void *shmat(int shmid, const void *shmaddr, int shmflg);
// int shmdt(const void *shmaddr);
int main()
{
int shmId;
key_t key;
char *shmaddr;
if((key = ftok(".",1)) < 0){
printf("ftok error\n");
}
shmId = shmget(key, 1024*4, 0);//內存大小必須得是MB的整數倍
if(shmId == -1){
printf("shmget error\n");
exit(-1);
}
/*第二個參數一般寫0,讓linux內核自動分配空間,第三個參數也一般寫0,表示可讀可寫*/
shmaddr = shmat(shmId, 0, 0);
printf("shmat OK\n");
printf("data : %s\n",shmaddr);
shmdt(shmaddr);
return 0;
}
3.4 信號量 (semophore)
信號量可以用於實現進程間的同步和互斥,控制對共享資源的訪問。它是一個計數器,可以用來控制多個進程對共享資源的訪問。例如,當一個進程正在訪問共享資源時,其他進程可以通過信號量等待,直到該資源被釋放。
信號量 , 在內核中創建一個信號量集合(本質是個數組),數組的元素(信號量)都是 1,使用 P 操作進行 - 1,使用 V 操作 + 1,通過對臨界資源進行保護實現多進程的同步
爲什麼要使用信號量?
用了共享內存通信方式,帶來新的問題,那就是如果多個進程同時修改同一個共享內存,很有可能就衝突了。例如兩個進程都同時寫一個地址,那先寫的那個進程會發現內容被別人覆蓋了。
爲了防止多進程競爭共享資源,而造成的數據錯亂,所以需要保護機制,使得共享的資源,在任意時刻只能被一個進程訪問。正好,信號量就實現了這一保護機制。
信號量其實是一個整型的計數器,主要用於實現進程間的互斥與同步,而不是用於緩存進程間通信的數據。
信號量表示資源的數量,控制信號量的方式有兩種原子操作:
一個是 P 操作,這個操作會把信號量減去 1,相減後如果信號量 <0,則表明資源已被佔用,進程需阻塞等待;相減後如果信號量>=0,則表明還有資源可使用,進程可正常繼續執行。另一個是 V 操作,這個操作會把信號量加上 1,相加後如果信號量 <=0,則表明當前有阻塞中的進程,於是會將該進程喚醒運行;相加後如果信號量 > 0,則表明當前沒有阻塞中的進程。
P 操作是用在進入共享資源之前,V 操作是用在離開共享資源之後,這兩個操作是必須成對出現的。接下來,舉個例子,如果要使得兩個進程互斥訪問共享內存,我們可以初始化信號量爲 1。
具體的過程如下:
-
進程 A 在訪問共享內存前,先執行了 P 操作,由於信號量的初始值爲 1,故在進程 A 執行 P 操作後信號量變爲 0,表示共享資源可用,於是進程 A 就可以訪問共享內存。
-
若此時,進程 B 也想訪問共享內存,執行了 P 操作,結果信號量變爲了 - 1,這就意味着臨界資源已被佔用,因此進程 B 被阻塞。
-
直到進程 A 訪問完共享內存,纔會執行 V 操作,使得信號量恢復爲 0,接着就會喚醒阻塞中的進程 B,使得進程 B 可以訪問共享內存,最後完成共享內存的訪問後,執行 V 操作,使信號量恢復到初始值 1。
可以發現,信號初始化爲 1,就代表着是互斥信號量,它可以保證共享內存在任何時刻只有一個進程在訪問,這就很好的保護了共享內存。
另外,在多進程裏,每個進程並不一定是順序執行的,它們基本是以各自獨立的、不可預知的速度向前推進,但有時候我們又希望多個進程能密切合作,以實現一個共同的任務。
例如,進程 A 是負責生產數據,而進程 B 是負責讀取數據,這兩個進程是相互合作、相互依賴的,進程 A 必須先生產了數據,進程 B 才能讀取到數據,所以執行是有前後順序的。
那麼這時候,就可以用信號量來實現多進程同步的方式,我們可以初始化信號量爲 0。
具體過程:
-
如果進程 B 比進程 A 先執行了,那麼執行到 P 操作時,由於信號量初始值爲 0,故信號量會變爲 - 1,表示進程 A 還沒生產數據,於是進程 B 就阻塞等待;
-
接着,當進程 A 生產完數據後,執行了 V 操作,就會使得信號量變爲 0,於是就會喚醒阻塞在 P 操作的進程 B;
-
最後,進程 B 被喚醒後,意味着進程 A 已經生產了數據,於是進程 B 就可以正常讀取數據了。
可以發現,信號初始化爲 0,就代表着是同步信號量,它可以保證進程 A 應在進程 B 之前執行。
3.5 信號 (sinal)
信號是一種比較複雜的通信方式,通常由錯誤產生,用於通知進程發生某事件。信號可以作爲進程間以及同一進程不同線程之間的同步手段。例如,當一個進程終止時,它可以發送一個特定的信號給它的父進程,以便父進程可以採取相應的措施。
對於 Linux 來說,實際信號是軟中斷,許多重要的程序都需要處理信號。終端用戶輸入了 ctrl+c 來中斷程序,會通過信號機制停止一個程序。
⑴信號的名字和編號:每個信號都有一個名字和編號,這些名字都以 “SIG” 開頭。我們可以通過 kill -l 來查看信號的名字以及序號。
不存在 0 信號,kill 對於 0 信號有特殊的應用。
⑵信號的處理:信號的處理有三種方法,分別是:忽略、捕捉和默認動作。
忽略信號,大多數信號可以使用這個方式來處理,但是有兩種信號不能被忽略(分別是 SIGKILL 和 SIGSTOP);
捕捉信號,需要告訴內核,用戶希望如何處理某一種信號,說白了就是寫一個信號處理函數,然後將這個函數告訴內核。當該信號產生時,由內核來調用用戶自定義的函數,以此來實現某種信號的處理。
系統默認動作,對於每個信號來說,系統都對應由默認的處理動作,當發生了該信號,系統會自動執行。具體的信號默認動作可以使用 man 7 signal 來查看系統的具體定義。
函數原型:
//接收函數,第二個參數指向信號處理函數
sighandler_t signal(int signum, sighandler_t handler);
//發送函數
int kill(pid_t pid, int sig);
接收端:
#include <stdio.h>
#include <signal.h>
// typedef void (*sighandler_t)(int);
// sighandler_t signal(int signum, sighandler_t handler);
/*接受到信號後,讓信號處理該函數*/
void handler(int signum)
{
printf("signum = %d\n",signum);
switch(signum){
case 2:
printf("SIGINT\n");
break;
case 9:
printf("SIGKILL\n");
break;
case 10:
printf("SIGUSR1\n");
break;
}
}
int main()
{
signal(SIGINT,handler);
signal(SIGKILL,handler);
signal(SIGUSR1,handler);
while(1);
return 0;
}
發送端:
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
// int kill(pid_t pid, int sig);
int main(int argc,char **argv)
{
int signum;
int pid;
signum = atoi(argv[1]);//將字符型轉爲整型
pid = atoi(argv[2]);
kill(pid,signum);
printf("signum = %d,pid = %d\n",signum,pid);
return 0;
}
注意:信號發送字符串,只有在父子進程或者是共享內存下才可發送。
3.6 套接字 (socket)
前面提到的管道、消息隊列、共享內存、信號量都是在同一臺主機上進行進程間通信,那要想跨網絡與不同主機上的進程之間通信,就需要 Socket 通信了。
socket 是應用層與 TCP/IP 協議族通信的中間軟件抽象層,它是一組接口,把複雜的 TCP/IP 協議族隱藏在 Socket 接口後面,對用戶來說,一組簡單的接口就是全部,讓 Socket 去組織數據。socket 起源於 UNIX,在 Unix 一切皆文件哲學的思想下,socket 是一種 **” 打開—讀 / 寫—關閉” 模式的實現,服務器和客戶端各自維護一個” 文件”,在建立連接打開後,可以向自己文件寫入內容供對方讀取或者讀取對方內容,通訊結束時關閉文件。是一種可以網間通信的方式。
因此,Socket 通信不僅可以跨網絡與不同主機的進程間通信,還可以在同主機上進程間通信。
四、應用場景分析
4.1 利用管道建立聊天室
在 Linux 中,可以利用管道建立簡單的聊天室實現兩個用戶間的發送和接受消息。原理是需要建立兩個命名管道實現收發。以 client1.c 和 client2.c 爲例,兩個客戶端分別打開兩個不同的管道文件,一個用於發送數據,一個用於接收數據。在 client1.c 中,首先打開管道文件,如果打開失敗會返回錯誤信息並退出。接着使用 fork 創建子進程,子進程用於寫數據,父進程用於讀數據。子進程不斷循環,等待用戶輸入要發送的數據,然後寫入對應的管道。父進程則不斷讀取另一個管道的數據並打印出來。同理,在 client2.c 中也進行類似的操作。這樣就實現了兩個用戶通過管道進行消息的發送和接收,模擬了一個簡單的聊天室。
(1) 匿名管道實現聊天室(父子進程間)
案例:在這個案例中,我們將創建一個父子進程。父進程用於從標準輸入讀取用戶輸入的消息,並通過管道將消息發送給子進程。子進程從管道接收消息並將其輸出到標準輸出,顯示聊天內容。
代碼實現如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
int pipefd[2];
pid_t pid;
char buffer[256];
// 創建匿名管道
if (pipe(pipefd) == -1) {
perror("pipe");
return 1;
}
// 創建子進程
pid = fork();
if (pid == -1) {
perror("fork");
return 1;
} else if (pid == 0) { // 子進程
close(pipefd[1]); // 關閉寫端
while (1) {
ssize_t bytesRead = read(pipefd[0], buffer, sizeof(buffer));
if (bytesRead > 0) {
write(STDOUT_FILENO, buffer, bytesRead);
} else if (bytesRead == 0) {
// 管道關閉,退出
break;
} else {
perror("read");
break;
}
}
close(pipefd[0]);
} else { // 父進程
close(pipefd[0]); // 關閉讀端
while (1) {
ssize_t bytesRead = read(STDIN_FILENO, buffer, sizeof(buffer));
if (bytesRead > 0) {
write(pipefd[1], buffer, bytesRead);
} else {
// 輸入錯誤或結束,退出
break;
}
}
close(pipefd[1]);
wait(NULL); // 等待子進程結束
}
return 0;
}
(2) 命名管道實現聊天室(任意兩個進程間)
案例:首先創建一個命名管道文件。一個進程作爲發送方,打開命名管道進行寫入操作,將用戶輸入的消息寫入管道。另一個進程作爲接收方,打開命名管道進行讀取操作,讀取消息並顯示在終端上,從而實現簡單的聊天功能。
代碼實現如下:
①發送方代碼(sender.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#define FIFO_NAME "mychatfifo"
int main() {
int fd;
char buffer[256];
// 創建命名管道(如果不存在)
mkfifo(FIFO_NAME, 0666);
// 以只寫方式打開命名管道
fd = open(FIFO_NAME, O_WRONLY);
if (fd == -1) {
perror("open");
return 1;
}
while (1) {
fgets(buffer, sizeof(buffer), stdin);
if (strlen(buffer) > 0) {
write(fd, buffer, strlen(buffer));
}
}
close(fd);
unlink(FIFO_NAME); // 刪除命名管道文件
return 0;
}
②接收方代碼(receiver.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#define FIFO_NAME "mychatfifo"
int main() {
int fd;
char buffer[256];
// 以只讀方式打開命名管道
fd = open(FIFO_NAME, O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
while (1) {
ssize_t bytesRead = read(fd, buffer, sizeof(buffer));
if (bytesRead > 0) {
write(STDOUT_FILENO, buffer, bytesRead);
} else if (bytesRead == 0) {
// 管道關閉,退出
break;
} else {
perror("read");
break;
}
}
close(fd);
return 0;
}
在這個案例中,先運行接收方程序,再運行發送方程序。發送方輸入的消息會通過命名管道傳遞到接收方並顯示出來,實現了簡單的聊天功能。需要注意的是,這只是一個基礎的示例,實際的聊天室應用還需要更多的功能,如多用戶支持、消息格式處理等。
4.2 文件傳輸
⑴使用管道實現簡單文件傳輸
命名管道可以用於文件傳輸。假設一個進程作爲發送方,另一個進程作爲接收方。發送方打開要傳輸的文件,逐塊讀取文件內容並寫入命名管道。接收方打開命名管道,逐塊讀取數據並寫入到目標文件中。這樣就實現了文件在不同進程之間的傳輸。
案例思路:本案例通過父子進程間的匿名管道來實現文件傳輸。父進程負責讀取文件內容,並將其寫入管道,子進程從管道中讀取數據並將其輸出到另一個文件中,以此完成文件從一個進程到另一個進程的傳輸。
代碼實現如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#define BUFFER_SIZE 1024
int main() {
int pipefd[2];
pid_t pid;
int source_fd, destination_fd;
char buffer[BUFFER_SIZE];
ssize_t bytesRead;
// 創建匿名管道
if (pipe(pipefd) == -1) {
perror("pipe creation failed");
return 1;
}
// 創建子進程
pid = fork();
if (pid == -1) {
perror("fork failed");
return 1;
} else if (pid == 0) { // 子進程
close(pipefd[1]); // 關閉寫端
// 創建目標文件
destination_fd = open("destination.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (destination_fd == -1) {
perror("open destination file failed");
return 1;
}
while ((bytesRead = read(pipefd[0], buffer, BUFFER_SIZE)) > 0) {
write(destination_fd, buffer, bytesRead);
}
close(pipefd[0]);
close(destination_fd);
} else { // 父進程
close(pipefd[0]); // 關閉讀端
// 打開源文件
source_fd = open("source.txt", O_RDONLY);
if (source_fd == -1) {
perror("open source file failed");
return 1;
}
while ((bytesRead = read(source_fd, buffer, BUFFER_SIZE)) > 0) {
write(pipefd[1], buffer, bytesRead);
}
close(pipefd[1]);
close(source_fd);
wait(NULL); // 等待子進程結束
}
return 0;
}
⑵使用共享內存實現文件傳輸
案例思路:首先創建共享內存區域,一個進程將文件內容讀取到共享內存中,另一個進程從共享內存中獲取數據並寫入到目標文件。爲了實現同步,使用信號量來控制對共享內存的訪問,避免數據衝突。
代碼實現如下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <fcntl.h>
#include <unistd.h>
#define BUFFER_SIZE 1024
// 信號量操作結構體
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
// 初始化信號量
void init_semaphore(int sem_id, int value) {
union semun arg;
arg.val = value;
if (semctl(sem_id, 0, SETVAL, arg) == -1) {
perror("semctl SETVAL failed");
exit(1);
}
}
// P操作(等待信號量)
void P(int sem_id) {
struct sembuf sops = {0, -1, 0};
if (semop(sem_id, &sops, 1) == -1) {
perror("semop P failed");
exit(1);
}
}
// V操作(釋放信號量)
void V(int sem_id) {
struct sembuf sops = {0, 1, 0};
if (semop(sem_id, &sops, 1) == -1) {
perror("semop V failed");
exit(1);
}
}
int main() {
key_t key;
int shmid, semid;
char *shm_addr;
int source_fd, destination_fd;
char buffer[BUFFER_SIZE];
ssize_t bytesRead;
// 生成共享內存和信號量的鍵值
if ((key = ftok(".", 'a')) == -1) {
perror("ftok failed");
return 1;
}
// 創建共享內存
if ((shmid = shmget(key, BUFFER_SIZE, IPC_CREAT | 0666)) == -1) {
perror("shmget failed");
return 1;
}
// 附加共享內存到進程地址空間
shm_addr = (char *)shmat(shmid, NULL, 0);
if (shm_addr == (void *)-1) {
perror("shmat failed");
return 1;
}
// 創建信號量
if ((semid = semget(key, 1, IPC_CREAT | 0666)) == -1) {
perror("semget failed");
return 1;
}
init_semaphore(semid, 1); // 初始化信號量值爲1
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
return 1;
} else if (pid == 0) { // 子進程
// 打開目標文件
destination_fd = open("destination.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (destination_fd == -1) {
perror("open destination file failed");
return 1;
}
P(semid); // 等待信號量,獲取共享內存訪問權
while (*shm_addr!= '\0') {
write(destination_fd, shm_addr, 1);
shm_addr++;
}
V(semid); // 釋放信號量
close(destination_fd);
shmdt(shm_addr); // 分離共享內存
} else { // 父進程
// 打開源文件
source_fd = open("source.txt", O_RDONLY);
if (source_fd == -1) {
perror("open source file failed");
return 1;
}
while ((bytesRead = read(source_fd, buffer, BUFFER_SIZE)) > 0) {
P(semid); // 等待信號量,獲取共享內存訪問權
for (int i = 0; i < bytesRead; i++) {
*shm_addr = buffer[i];
shm_addr++;
}
V(semid); // 釋放信號量
}
*shm_addr = '\0'; // 標記文件結束
close(source_fd);
wait(NULL); // 等待子進程結束
// 標記共享內存可被銷燬
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl IPC_RMID failed");
return 1;
}
// 刪除信號量
if (semctl(semid, 0, IPC_RMID) == -1) {
perror("semctl IPC_RMID failed");
return 1;
}
}
return 0;
}
這些案例展示了不同的 Linux 內核進程間通信方式在文件傳輸中的應用。使用管道方式相對簡單直接,適用於簡單的父子進程間通信場景;而共享內存方式在處理大量數據傳輸時效率更高,但需要額外的同步機制(如信號量)來確保數據的正確性。在實際應用中,可以根據具體需求選擇合適的方法。
4.3 不同場景下的選擇策略
在不同的場景下,需要根據具體需求選擇合適的進程間通信方式。如果是父子進程之間簡單的數據傳遞,匿名管道是一種快速且簡單的選擇。因爲匿名管道創建方便,只需要調用 pipe 函數即可,並且在父子進程中通過文件描述符的繼承可以很容易地實現通信。
對於不相關的進程之間通信,命名管道或者消息隊列可能更爲合適。命名管道可以在文件系統中持久存在,不同的進程可以通過指定管道文件的路徑來進行通信。消息隊列則可以實現多對多的通信,並且可以存儲帶有特定格式的消息,方便不同類型的進程進行數據交換。
當需要高效地共享大量數據時,共享內存是一個很好的選擇。多個進程可以直接訪問同一塊內存區域,避免了數據的複製,大大提高了通信效率。但是需要注意使用信號量等機制來進行同步,防止數據衝突。
如果是在不同機器之間的進程通信,套接字則是唯一的選擇。套接字可以通過網絡進行通信,支持不同的協議和數據格式轉換,可以適應各種複雜的網絡環境。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/YJw6NSSVKXxup544m7btBg