Linux进程通信的四种方式——共享内存、信号量、无名管道、消息队列|实验、代码、分析、总结
Linux進程通信的四種方式——共享內存、信號量、無名管道、消息隊列|實驗、代碼、分析、總結
每個進程各自有不同的用戶地址空間,任何一個進程的全局變量在另一個進程中都看不到,所以進程之間要交換數據必須通過內核,在內核中開辟一塊緩沖區,生產者進程把數據從用戶空間拷到內核緩沖區,消費者進程再從內核緩沖區把數據讀走,內核提供的這種機制稱為進程間通信(IPC,InterProcess Communication).
轉載需注明出處:?? Sylvan Ding ??
文章目錄
- Linux進程通信的四種方式——共享內存、信號量、無名管道、消息隊列|實驗、代碼、分析、總結
- 實驗目標
- 問題描述
- 需求分析
- 實驗環境
- 共享內存
- 實驗設計
- 共享內存原理
- 使用函數介紹
- sprintf()
- ftoc()
- 獲取共享內存
- 關聯共享內存
- 取消關聯共享內存
- 實驗設計與編程
- 生產者進程A
- 消費者進程B
- 實驗結果與分析
- 信號量
- 實驗設計
- 信號量的工作原理
- 使用函數介紹
- 頭文件
- 信號量創建
- 信號量初始化和刪除
- 信號量值修改
- 實驗設計與編程
- 進程A
- 進程B
- uni_clock.h
- 實驗結果與分析
- 管道
- 實驗設計
- 管道的實現機制
- 環形緩沖區
- 管道對象
- 無名管道的實驗設計與編程
- 實驗結果與分析
- 消息隊列
- 實驗設計
- 消息隊列的實現原理
- 用戶消息緩沖區
- 使用函數介紹
- 消息隊列的創建
- 向消息隊列中添加信息
- 從消息隊列中讀取信息
- 消息隊列的控制函數
- ipcs 命令
- 實驗設計與編程
- 消費者進程 A
- 生產者進程 B
- 實驗結果與分析
- 總結和實驗心得
- 參考文獻
- 轉載注意事項
實驗目標
本實驗主要實現單機上不同進程間的通信,故在網絡環境中廣泛應用的客戶機-服務器系統通信的三種實現方法(套接字、遠程過程調用和遠程方法調用)不予實現。
問題描述
生產者一次生成一個元素放入緩沖池中,消費者一次可以從緩沖池中取出一個元素。生產者放入的元素個數要與消費者取出的元素個數一致。實驗的輸出要能跟蹤生產者的每次“生產”行為,以及消費者的每次“消費”行為。
需求分析
利用OS提供的高級通信工具,在單機環境下,設計和編程實現進程間數據的高效傳送,并對比分析各方法的優缺點。編寫生產者和消費者的相關程序,在Linux系統下實現信號量、共享內存、管道(無名管道和有名管道)、消息隊列等四種進程間通信方式。實驗的輸出要能跟蹤生產者的每次“生產”行為,以及消費者的每次“消費”行為。
實驗環境
- 操作系統:Ubuntu
- 編程語言:C
- 編譯器:GCC 7.5.0
共享內存
共享存儲區(Share Memory)是Linux系統中通信速度最高的通信機制,因為數據不需要在客戶機和服務器端之間復制,數據直接寫到內存,不用若干次數據拷貝,所以這是最快的一種IPC。該機制中共享內存空間和進程的虛地址空間滿足多對多的關系。即一個共享內存空間可以映射多個進程的虛地址空間,一個進程的虛地址空間又可以連接多個共享存儲區。當進程間預利用共享存儲區通信時,先要在主存中建立一個共享存儲區,然后將它附接到自己的虛地址空間。該機制只為進程提供了用于實現通信的共享存儲區和對共享存儲區進行操作的手段,然而并未提供對該區進行互斥訪問及進程同步的措施,所以要使用信號量來實現對共享內存的存取的同步。
實驗設計
共享內存原理
通過上圖可知,共享內存是通過將不同進程的虛擬內存地址映射到相同的物理內存地址來實現的。
在Linux內核中,每個共享內存都由一個名為 struct shmid_kernel 的結構體來管理,而且Linux限制了系統最大能創建的共享內存為128個。通過類型為 struct shmid_kernel 結構的數組來管理,如下:
struct shmid_ds {struct ipc_perm shm_perm; /* operation perms */int shm_segsz; /* size of segment (bytes) */__kernel_time_t shm_atime; /* last attach time */__kernel_time_t shm_dtime; /* last detach time */__kernel_time_t shm_ctime; /* last change time */__kernel_ipc_pid_t shm_cpid; /* pid of creator */__kernel_ipc_pid_t shm_lpid; /* pid of last operator */unsigned short shm_nattch; /* no. of current attaches */unsigned short shm_unused; /* compatibility */void *shm_unused2; /* ditto - used by DIPC */void *shm_unused3; /* unused */ };struct shmid_kernel { struct shmid_ds u;/* the following are private */unsigned long shm_npages; /* size of segment (pages) */pte_t *shm_pages; /* array of ptrs to frames -> SHMMAX */ struct vm_area_struct *attaches; /* descriptors for attaches */ };static struct shmid_kernel *shm_segs[SHMMNI]; // SHMMNI等于128struct shmid_kernel 結構體中,shm_npages 字段表示共享內存使用了多少個內存頁,而 shm_pages 字段指向了共享內存映射的虛擬內存頁表項數組。
使用函數介紹
sprintf()
C 庫函數 int sprintf(char *str, const char *format, ...) 發送格式化輸出到 str 所指向的字符串。下面是 sprintf() 函數的聲明:
int sprintf(char *str, const char *format, ...)- str – 這是指向一個字符數組的指針,該數組存儲了 C 字符串。
- format – 這是字符串,包含了要被寫入到字符串 str 的文本。它可以包含嵌入的 format 標簽,format 標簽可被隨后的附加參數中指定的值替換,并按需求進行格式化。
ftoc()
進程間的通訊,必須有個公共的標識來確保使用同一個通訊通道。任何一個進程如果使用同一個通訊標識,則內核就可以通過該標識找到對應的那個信道,這個標識就是IPC鍵值。
函數 ftok 把一個已存在的路徑名和一個整數標識符轉換成一個key_t值,稱為IPC鍵值。函數原型如下:
key_t ftok(const char *pathname, int proj_id)- pathname:指定的文件,此文件必須存在且可存取
- proj_id:計劃代號(project ID)
函數 ftok 把從pathname導出的信息與id的低序8位組合成一個整數IPC鍵,從而避免用戶使用key值的沖突。所需頭文件 #include <sys/types.h> 和 #include <sys/ipc.h>,成功:返回key_t值(即IPC 鍵值),出錯返回-1,錯誤原因存于error中。
給semget、msgget、shmget傳入key值,它們返回的都是相應的IPC對象標識符。IPC鍵值和IPC標識符是兩個概念,后者是建立在前者之上。
上圖畫出了從 IPC 鍵值生成 IPC 標識符圖,其中key為 IPC 鍵值,由ftok函數生成,ipc_id 為IPC標識符,由 semget、msgget、shmget 函數生成。ipc_id 在信號量函數中稱為 semid,在消息隊列函數中稱為 msgid,在共享內存函數中稱為 shmid,它們表示的是各自 IPC 對象標識符。
struct ipc_perm {key_t key ; /* 此IPC對象的key鍵 */uid_t uid ; /* 此IPC對象用戶ID */gid_t gid ; /* 此IPC對象組ID */uid_t cuid ; /* IPC對象創建進程的有效用戶ID */gid_t cgid ; /* IPC對象創建進程的有效組ID */mode_t mode ; /* 此IPC的讀寫權限 */ulong_t seq ; /* IPC對象的序列號 */ };系統為每一個IPC對象保存一個ipc_perm結構體,該結構說明了IPC對象的權限和所有者,并確定了一個IPC操作是否可以訪問該IPC對象。
msgget、semget、shmget 函數最右邊的形參flag(msgget中為msgflg、semget中為semflg、shmget中shmflg)為IPC對象創建權限。IPC對象創建權限(即flag)格式為0xxxxx,其中0表示8位制,低三位為用戶、屬組、其他的讀、寫、執行權限(執行位不使用)。IPC對象存取權限常與下面IPC_CREAT、IPC_EXCL兩種標志進行或(|)運算完成對IPC對象創建的管理,下面是兩種創建模式標志在<sys/ipc.h>頭文件中的宏定義。
#define IPC_CREAT 01000 /* Create key if key does not exist. */ #define IPC_EXCL 02000 /* Fail if key exists. */獲取共享內存
使用 shmget() 函數獲取共享內存,shmget() 函數的原型如下:
int shmget(key_t key, size_t size, int shmflg);- 參數 key 一般由 ftok() 函數生成,用于標識系統的唯一IPC資源。
- 參數 size 指定創建的共享內存大小。
- 參數 shmflg 指定 shmget() 函數的動作,比如傳入 IPC_CREAT 表示要創建新的共享內存。
函數調用成功時返回一個新建或已經存在的的共享內存標識符,取決于shmflg的參數。失敗返回-1,并設置錯誤碼。需要頭文件 #include <sys/shm.h>。
shmget() 函數的實現比較簡單,首先調用 findkey() 函數查找值為key的共享內存是否已經被創建,findkey() 函數返回共享內存在 shm_segs數組 的索引。如果找到,那么直接返回共享內存的標識符即可。否則就調用 newseg() 函數創建新的共享內存。newseg() 函數的實現也比較簡單,就是創建一個新的 struct shmid_kernel 結構體,然后設置其各個字段的值,并且保存到 shm_segs數組 中。
關聯共享內存
shmget() 函數返回的是一個標識符,而不是可用的內存地址,所以還需要調用 shmat() 函數把共享內存關聯到某個虛擬內存地址上。shmat() 函數的原型如下:
void *shmat(int shmid, const void *shmaddr, int shmflg);- 參數 shmid 是 shmget() 函數返回的標識符。
- 參數 shmaddr 是要關聯的虛擬內存地址,如果傳入0,表示由系統自動選擇合適的虛擬內存地址。
- 參數 shmflg 若指定了 SHM_RDONLY 位,則以只讀方式連接此段,否則以讀寫方式連接此段。
函數調用成功返回一個可用的指針(虛擬內存地址),出錯返回-1。
shmget() 主要通過 shmid 標識符來找到共享內存描述符,系統中所有的共享內存到保存在 shm_segs 數組中。接著,找到一個可用的虛擬內存地址,如果在調用 shmat() 函數時沒有指定了虛擬內存地址,那么就通過 get_unmapped_area() 函數來獲取一個可用的虛擬內存地址。通過調用 kmem_cache_alloc() 函數創建一個 vm_area_struct 結構,vm_area_struct 結構用于管理進程的虛擬內存空間。shmat() 函數只是申請了進程的虛擬內存空間,而共享內存的物理空間并沒有申請,當進程發生缺頁異常的時候會調用 shm_nopage() 函數來恢復進程的虛擬內存地址到物理內存地址的映射。shm_nopage() 函數的主要功能是當發生內存缺頁時,申請新的物理內存頁,并映射到共享內存中。由于使用共享內存時會映射到相同的物理內存頁上,從而不同進程可以共用此塊內存。
取消關聯共享內存
當一個進程不需要共享內存的時候,就需要取消共享內存與虛擬內存地址的關聯。取消關聯共享內存通過 shmdt() 函數實現,原型如下:
int shmdt(const void *shmaddr);- 參數 shmaddr 是要取消關聯的虛擬內存地址,也就是 shmat() 函數返回的值。
函數調用成功返回0,出錯返回-1。
實驗設計與編程
設置兩個進程,分別為生產者 進程A和消費者 進程B,進程A 創建一塊共享內存,然后寫入數據(字符串:Process A generated!),進程B 獲取這塊共享內存并且讀取其字符串內容并輸出。
生產者進程A
/* Process A - Producer */#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h>#define SHM_PATH "~/shm" // 指定文件,以此生成 IPC key #define SHM_SIZE 128 // 設置共享內存區大小:128字節int main(int argc, char *argv[]) {int shmid; // 共享內存標識符char *addr; // 虛存中的字符串地址key_t key = ftok(SHM_PATH, 0x0066); // 生成 IPC key// 在進程A的虛存中開辟對應key的共享存儲空間// 共享存儲區大小為 128 字節// 指定 flag 為 IPC_CREAT|IPC_EXCL|0666// 因為有 IPC_EXCL 存在,所以第二次運行時,對應 key 已經存在,所以會失敗shmid = shmget(key, SHM_SIZE, IPC_CREAT|IPC_EXCL|0666);if (shmid < 0) {printf("failed to create share memory\n");return -1;}// 將虛擬內存的共享空間和物理內存的共享空間相關聯// 返回對應的虛存地址addr = shmat(shmid, NULL, 0);if (addr <= 0) {printf("failed to map share memory\n");return -1;}// 向進程A的虛存地址 addr 中寫入字符串// 實際寫入了在A和B的公共物理內存上sprintf(addr, "%s", "Process A created!\n");return 0; }消費者進程B
/* Process B - Consumer */#include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h>#define SHM_PATH "~/shm" // 指定文件,以此生成 IPC key #define SHM_SIZE 128 // 設置共享內存區大小:128字節int main(int argc, char *argv[]) {int shmid; // 共享內存標識符char *addr; // 進程B虛存中的字符串地址key_t key = ftok(SHM_PATH, 0x0066); // 生成 IPC keychar buf[128]; // 接受進程B虛存對應的共享物理內存中的內容// 在進程B的虛存中開辟對應key的共享存儲空間// 對應 key 已經存在,所以 flag 參數不加 IPC_EXCLshmid = shmget(key, SHM_SIZE, IPC_CREAT);if (shmid < 0) {printf("failed to get share memory\n");return -1;}// 將虛擬內存的共享空間和物理內存的共享空間相關聯addr = shmat(shmid, NULL, 0);if (addr <= 0) {printf("failed to map share memory\n");return -1;}// 拷貝出共享物理內存中對應的 128 字節數據// 輸出:Process A created!strcpy(buf, addr);printf("%s", buf);return 0; }實驗結果與分析
共享內存是進程間通信中最簡單的方式之一。共享內存允許兩個或更多進程訪問同一塊內存,當一個進程改變了這塊地址中的內容的時候,其它進程都會察覺到這個更改。因為所有進程共享同一塊內存,共享內存在各種進程間通信方式中具有最高的效率。訪問共享內存區域和訪問進程獨有的內存區域一樣快,并不需要通過系統調用或者其它需要切入內核的過程來完成。同時它也避免了對數據的各種不必要的復制。
但是,系統內核沒有對訪問共享內存進行同步,解決這些問題的常用方法是通過使用信號量進行同步。雖然每個使用者都可以讀取寫入數據,但是所有程序之間必須達成并遵守一定的協議,以防止諸如在讀取信息之前覆寫內存空間等競爭狀態的出現。不幸的是,Linux無法嚴格保證提供對共享內存塊的獨占訪問,甚至是在通過使用IPC_PRIVATE創建新的共享內存塊的時候也不能保證訪問的獨占性。 同時,多個使用共享內存塊的進程之間必須協調使用同一個鍵值。
信號量
信號量是一個計數器,可以用來控制多個線程對共享資源的訪問。它不是用于交換大批數據,而用于多線程之間的同步。它常作為一種鎖機制,防止某進程在訪問資源時其它進程也訪問該資源。因此,主要作為進程間以及同一個進程內不同線程之間的同步手段。
實驗設計
信號量的工作原理
信號量本質上是一個計數器,它不以傳送數據為主要目的,它主要是用來保護共享資源(信號量也屬于臨界資源),使得資源在一個時刻只有一個進程獨享。
信號量有初值(>0),每當有進程申請使用信號量,通過一個P操作來對信號量進行-1操作,當計數器減到0的時候就說明沒有資源了,其他進程要想訪問就必須等待。當該進程執行完這段工作之后,就會執行V操作,對信號量進行+1操作。
當有進程要求使用共享資源時,需要執行以下操作:
- 系統首先要檢測該資源的信號量
- 若該資源的信號量值大于0,則進程可以使用該資源,此時,進程將該資源的信號量值減-1
- 若該資源的信號量值為0,則進程進入休眠狀態,直到信號量值大于0時進程被喚醒,訪問該資源
- 當進程不再使用由一個信號量控制的共享資源時,該信號量值增+1,如果此時有進程處于休眠狀態等待此信號量,則該進程會被喚醒
在信號量進行PV操作時都為原子操作(因為它需要保護臨界資源)。
二元信號量(Binary Semaphore)是最簡單的一種鎖(互斥鎖),它只用兩種狀態:占用與非占用,通常用來替代互斥鎖實現線程同步。所以它的引用計數為1。
使用函數介紹
Linux提供了一組精心設計的信號量接口來對信號進行操作。
頭文件
#include <sys/sem.h> // 有名信號量信號量在進程是以有名信號量進行通信的,在線程中則是以無名信號進行通信的。本實驗主要實現進程間信號量的通信,所以使用 <sys/sem.h> .
信號量創建
作用是創建一個新信號量或取得一個已有信號量,原型為:
int semget(key_t key, int num_sems, int sem_flags);- num_sems指定需要的信號量數目,它的值幾乎總是1
- sem_flags是一組標志,設置了IPC_CREAT標志后,即使給出的鍵是一個已有信號量的鍵,也不會產生錯誤。而IPC_CREAT | IPC_EXCL則可以創建一個新的,唯一的信號量,如果信號量已存在,返回一個錯誤。
信號量初始化和刪除
semctl() 函數用來直接控制信號量信息,它的原型為:
int semctl(int sem_id, int sem_num, int command, ...);- command 通常是下面兩個值中的其中一個:SETVAL 用來把信號量初始化為一個已知的值,p 值通過union semun中的val成員設置,其作用是在信號量第一次使用前對它進行設。IPC_RMID 用于刪除一個已經無需繼續使用的信號量標識符。
如果有第四個參數,它通常是一個union semum結構,定義如下:
union semun {int val;struct semid_ds *buf;unsigned short *arry; };信號量值修改
作用是改變信號量的值,原型為:
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);- sem_id是由semget()返回的信號量標識符
sembuf 結構的定義如下:
struct sembuf{short sem_num; // 除非使用一組信號量,否則它為0short sem_op; // 信號量在一次操作中需要改變的數據,通常是兩個數,一個是-1,即P(等待)操作,// 一個是+1,即V(發送信號)操作。short sem_flg; // 通常為SEM_UNDO,使操作系統跟蹤信號,// 并在進程沒有釋放該信號量而終止時,操作系統釋放信號量 };實驗設計與編程
使用二元信號量對“共享內存”方式進行改進。原本進程A和B能同時訪問并修改共享內存區,進程A寫入字符串,進程B讀出字符串。現在,假定進程A寫入時間需要5s,先啟動進程A,A在執行寫入前進行P操作,此時啟動B,B執行讀取操作,但因為有信號量對臨界區的限制,所以進程B此時應當被掛起。5s后進程A寫入成功,執行V操作,脫離臨界區,進程B從休眠態被喚醒,從共享內存區中取進程A存入的字符串數據。最后,B釋放共享內存區并刪除信號量。
進程A
// // Created by Sylvan Ding on 2022/3/5. ///* Process A - Producer */#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/sem.h> #include "uni_clock.h"#define SHM_PATH "~/shm" #define SHM_SIZE 128#define SEM_PATH "~/sem"union semun {// 信息量的控制單元int val;struct semid_ds *buf;unsigned short *arry; };static int set_semvalue(int semid); static int semaphore_p(int semid); static int semaphore_v(int semid);int main(int argc, char *argv[]) {int shmid;char *addr;key_t key = ftok(SHM_PATH, 0x0066);// 生成信號量標識符int semid;key_t key_sem = ftok(SEM_PATH, 0x0066);semid = semget(key_sem, 1, IPC_CREAT|IPC_EXCL|0666);// 信號量初始化if(set_semvalue(semid)<0) {printf("failed to initialize semaphore\n");return -1;}// 開辟共享存儲空間shmid = shmget(key, SHM_SIZE, IPC_CREAT|IPC_EXCL|0666);if (shmid < 0) {printf("failed to create share memory\n");return -1;}// 將虛擬內存的共享空間和物理內存的共享空間相關聯addr = shmat(shmid, NULL, 0);if (addr <= 0) {printf("failed to map share memory\n");return -1;}// 進入臨界區if(semaphore_p(semid)<0) {printf("A semaphore_p failed\n");return -1;};// 進程A開始向共享內存中寫入字符串printf("%s: Process A is on writing...\n", getTime());sleep(5); // 模擬寫入過程5ssprintf(addr, "%s", "Hello, world!\n");// 進程A提示寫入完畢printf("%s: Process A has finished writing!\n", getTime());// 進程A寫入完畢,退出臨界區if(semaphore_v(semid)<0) {printf("A semaphore_v failed\n");return -1;}// 斷開進程A與共享內存區的連接shmdt(addr);return 0; }static int set_semvalue(int semid) {// 信息量初始化union semun sem_union;sem_union.val = 1; // 二元信息量if(semctl(semid, 0, SETVAL, sem_union) == -1) {return -1;}return 0; }static int semaphore_p(int semid) {// 等待P(sv)struct sembuf sem_b;sem_b.sem_num = 0;sem_b.sem_op = -1; // P()sem_b.sem_flg = SEM_UNDO;if(semop(semid, &sem_b, 1) == -1) {return -1;}return 0; }static int semaphore_v(int semid) {// V(sv)struct sembuf sem_b;sem_b.sem_num = 0;sem_b.sem_op = 1; // V()sem_b.sem_flg = SEM_UNDO;if(semop(semid, &sem_b, 1) == -1) {return -1;}return 0; }進程B
// // Created by Sylvan Ding on 2022/3/5. ///* Process B - Consumer */#include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/sem.h> #include "uni_clock.h"#define SHM_PATH "~/shm" #define SHM_SIZE 128#define SEM_PATH "~/sem"union semun {// 信息量的控制單元int val;struct semid_ds *buf;unsigned short *arry; };static int semaphore_p(int semid); static int semaphore_v(int semid); static int del_semvalue(int semid);int main(int argc, char *argv[]) {int shmid;char *addr;key_t key = ftok(SHM_PATH, 0x0066);// 生成信號量標識符int semid;key_t key_sem = ftok(SEM_PATH, 0x0066);semid = semget(key_sem, 1, IPC_CREAT);char buf[128];// 進程B創建共享存儲空間shmid = shmget(key, SHM_SIZE, IPC_CREAT);if (shmid < 0) {printf("failed to get share memory\n");return -1;}// 將虛擬內存的共享空間和物理內存的共享空間相關聯addr = shmat(shmid, NULL, 0);if (addr <= 0) {printf("failed to map share memory\n");return -1;}// 進程B進入臨界區printf("%s: Process B started reading!\n", getTime());if(semaphore_p(semid)<0) {printf("B semaphore_p failed\n");return -1;};// 進程B讀共享內存中數據sleep(2); // B讀出數據需2sstrcpy(buf, addr);printf("%s: %s", getTime(), buf);// 退出臨界區if(semaphore_v(semid)<0) {printf("B semaphore_v failed\n");return -1;}// 銷毀信號量if(del_semvalue(semid)<0) {printf("failed to delete semaphore\n");return -1;}// 斷開進程與共享內存區的連接shmdt(addr);// 刪除共享內存區shmctl(shmid, IPC_RMID, NULL);return 0; }static int semaphore_p(int semid) {// 等待P(sv)struct sembuf sem_b;sem_b.sem_num = 0;sem_b.sem_op = -1; // P()sem_b.sem_flg = SEM_UNDO;if(semop(semid, &sem_b, 1) == -1) {return -1;}return 0; }static int semaphore_v(int semid) {// V(sv)struct sembuf sem_b;sem_b.sem_num = 0;sem_b.sem_op = 1; // V()sem_b.sem_flg = SEM_UNDO;if(semop(semid, &sem_b, 1) == -1) {return -1;}return 0; }static int del_semvalue(int semid) {// 刪除信號量union semun sem_union;if(semctl(semid, 0, IPC_RMID, sem_union) == -1) {return -1;}return 0; }uni_clock.h
// // Created by Sylvan Ding on 2022/3/5. //#ifndef UNTITLED_UNI_CLOCK_H #define UNTITLED_UNI_CLOCK_H#include <time.h>char *getTime() {time_t t;time(&t);struct tm *tn;tn = localtime(&t);return asctime(tn); }#endif //UNTITLED_UNI_CLOCK_H實驗結果與分析
由于競爭信號量的時候,未能拿到信號的進程會進入睡眠,所以信號量可以適用于長時間持有。而且信號量不適合短時間的持有,因為會導致睡眠的原因,維護隊列、喚醒等各種開銷,在短時鎖定,效率較低。由于睡眠的特性,只能在進程上下文進行調用,無法再中斷上下文中使用信號量。一個進程可以在持有信號量的情況下去睡眠,另外的進程嘗試獲取該信號量時候,不會死鎖。
管道
子進程從父進程繼承文件描述符。來源于早起Unix命令行輸入時的想法:能不能讓上一個進程的輸出重定向為下一個進程的輸入。流水線方式,稱為管道機制,即子進程共享父進程的一些資源。
管道的特點:
實驗設計
管道的實現機制
在Linux中,管道是一種使用非常頻繁的通信機制。從本質上說,管道也是一種文件,但它又和一般的文件有所不同,管道可以克服使用文件進行通信的兩個問題,具體表現為:
在 Linux 中,管道的實現并沒有使用專門的數據結構,而是借助了文件系統的file結構和VFS的索引節點inode。通過將兩個 file 結構指向同一個臨時的 VFS 索引節點,而這個 VFS 索引節點又指向一個物理頁面而實現的。
環形緩沖區
在內核中,管道 使用了環形緩沖區來存儲數據。環形緩沖區的原理是:把一個緩沖區當成是首尾相連的環,其中通過讀指針和寫指針來記錄讀操作和寫操作位置。
管道對象
在 Linux 內核中,管道使用 pipe_inode_info 對象來進行管理,pipe_inode_info 對象的定義,如下所示:
struct pipe_inode_info {wait_queue_head_t wait;unsigned int nrbufs,unsigned int curbuf;...unsigned int readers;unsigned int writers;unsigned int waiting_writers;...struct inode *inode;struct pipe_buffer bufs[16]; };- wait:等待隊列,用于存儲正在等待管道可讀或者可寫的進程。
- bufs:環形緩沖區,由 16 個 pipe_buffer 對象組成,每個 pipe_buffer 對象擁有一個內存頁 。
- nrbufs:表示未讀數據已經占用了環形緩沖區的多少個內存頁。
- curbuf:表示當前正在讀取環形緩沖區的哪個內存頁中的數據。
- readers:表示正在讀取管道的進程數。
- writers:表示正在寫入管道的進程數。
- waiting_writers:表示等待管道可寫的進程數。
- inode:與管道關聯的 inode 對象。
環形緩沖區是由 16 個 pipe_buffer 對象組成,定義如下:
struct pipe_buffer {struct page *page;unsigned int offset;unsigned int len;... };- page:指向 pipe_buffer 對象占用的內存頁。
- offset:如果進程正在讀取當前內存頁的數據,那么 offset 指向正在讀取當前內存頁的偏移量。
- len:表示當前內存頁擁有未讀數據的長度。
無名管道的實驗設計與編程
無名管道一般用于父子進程之間相互通信,具體流程如下:
- 父進程使用 pipe 系統調用創建一個管道
- 父進程使用 fork 系統調用創建一個子進程
- 由于子進程會繼承父進程打開的文件句柄,所以父子進程可以通過新創建的管道進行通信
管道是單向的,所以要將管道分為讀端和寫端,需要兩個文件描述符來管理管道:fd[0] 為讀端,fd[1] 為寫端。
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <stdlib.h> #include <string.h>int main() {int fd[2]; // 用于管理管道的文件描述符pid_t pid; // 子進程pidchar buf[128] = {0};char *msg = "Hello world!!!";// 父進程創建管道// pipe返回值:成功,返回0,否則返回-1.// 參數數組包含pipe使用的兩個文件的描述符.// fd[0]:讀管道,fd[1]:寫管道.if (-1 == pipe(fd)) {printf("failed to create pipe\n");return -1;}// 創建子進程// fork函數將運行著的程序分成2個(幾乎)完全一樣的進程,// 每個進程都啟動一個從代碼的同一位置開始執行的線程。// 返回負值:創建子進程失敗,零返回到新創建的子進程,// 正值返回父進程或調用者。pid = fork();if (pid<0) {printf("failed to fork\n");return -1;}// 子進程-生產者if (0 == pid) {// 關閉管道的讀端close(fd[0]); // 向管道寫端寫入數據if(write(fd[1], msg, strlen(msg))<0) {printf("failed to write data\n");exit(1); }; exit(0); } else { // 父進程-消費者// 關閉管道的寫端close(fd[1]); // 從管道的讀端讀取數據if(read(fd[0], buf, sizeof(buf))<0) {printf("failed to read data\n");return -1;}; printf("Parent read data: %s\n", buf);}return 0; }實驗結果與分析
父子進程通過 pipe 系統調用打開的管道,在內核空間中指向同一個管道對象(pipe_inode_info)。所以父子進程共享著同一個管道對象,那么就可以通過這個共享的管道對象進行通信。
使用 pipe 系統調用打開管道時,并沒有立刻申請內存頁,而是當有進程向管道寫入數據時,才會按需申請內存頁。當內存頁的數據被讀取完后,內核會將此內存頁回收,來減少管道對內存的使用。
消息隊列
消息隊列是消息的鏈表,存放在內核中并由消息隊列標識符標識。消息隊列克服了信號傳遞信息少,管道只能承載無格式字節流以及緩沖區大小受限等特點。消息隊列是UNIX下不同進程之間可實現共享資源的一種機制,UNIX允許不同進程將格式化的數據流以消息隊列形式發送給任意進程。對消息隊列具有操作權限的進程都可以使用msget完成對消息隊列的操作控制。通過使用消息類型,進程可以按任何順序讀信息,或為消息安排優先級順序。
實驗設計
消息隊列的實現原理
- 消息隊列的本質其實是一個內核提供的鏈表,內核基于這個鏈表,實現了一個數據結構;
- 向消息隊列中寫數據,實際上是向這個數據結構中插入一個新結點;從消息隊列匯總讀數據,實際上是從這個數據結構中刪除一個結點;
- 消息隊列提供了一個從一個進程向另外一個進程發送一塊數據的方法;
- 消息隊列也有管道一樣的不足,就是每個數據塊的最大長度是有上限的,系統上全體隊列的最大總長度也有一個上限。Linux用宏MSGMAX和MSGMNB來限制一條消息的最大長度和一個隊列的最大長度.
用戶消息緩沖區
無論發送進程還是接收進程,都需要在進程空間中用消息緩沖區來暫存消息。該消息緩沖區的結構定義如下:
struct msgbuf {long mtype; /* 消息的類型 */char mtext[]; /* 消息正文 */ };- 可通過 mtype 區分數據類型,同過判斷mtype,是否為需要接收的數據
- mtext[] 為存放消息正文的數組,可以根據消息的大小定義該數組的長度
使用函數介紹
消息隊列的創建
通過msgget創建消息隊列,函數原型如下:
#include <sys/msg.h> int msgget(key_t key, int msgflg);- 成功 msgget 將返回一個非負整數,即該消息隊列的標識碼;
- 失敗 則返回“-1”
向消息隊列中添加信息
向消息隊列中添加數據,使用到的是msgsnd()函數,函數原型如下:
int msgsnd(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);- msgid: 由msgget函數返回的消息隊列標識碼
- msg_ptr: 是一個指針,指針指向準備發送的消息,
- msg_sz: 是msg_ptr指向的消息長度,消息緩沖區結構體中mtext的大小,不包括數據的類型
- msgflg: 控制著當前消息隊列滿或到達系統上限時將要發生的事情。比如,msgflg = IPC_NOWAIT 表示隊列滿不等待,返回EAGAIN錯誤
注意,消息的數據結構卻有一定的要求,指針msg_ptr所指向的消息結構一定要是以一個長整型成員變量開始的結構體,接收函數將用這個成員來確定消息的類型。所以消息結構要定義成這樣:
struct my_message {long int message_type;/* The data you wish to transfer */ };注意:msg_sz 是msg_ptr指向的消息的長度,注意是消息的長度,而不是整個結構體的長度,也就是說msg_sz是不包括長整型消息類型成員變量的長度。
從消息隊列中讀取信息
msgrcv() 用來從一個消息隊列獲取消息,它的原型為:
int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg);- msgid: 由msgget函數返回的消息隊列標識碼
- msg_ptr: 是一個指針,指針指向準備接收的消息
- msgsz: 是msg_ptr指向的消息長度
- msgtype: 可以實現接收優先級的簡單形式
- msgtype=0返回隊列第一條信息
- msgtype>0返回隊列第一條類型等于msgtype的消息
- msgtype<0返回隊列第一條類型小于等于msgtype絕對值的消息
- msgflg: 控制著隊列中沒有相應類型的消息可供接收時將要發生的事。比如,msgflg=IPC_NOWAIT,隊列沒有可讀消息不等待,返回ENOMSG錯誤。msgflg=MSG_NOERROR,消息大小超過msgsz時被截斷
調用成功時,該函數返回放到接收緩存區中的字節數,消息被復制到由msg_ptr指向的用戶分配的緩存區中,然后刪除消息隊列中的對應消息。失敗時返回-1。
消息隊列的控制函數
控制函數原型如下:
int msgctl(int msqid, int command, strcut msqid_ds *buf);- command: 是將要采取的動作,它可以取3個值
- IPC_STAT:把msgid_ds結構中的數據設置為消息隊列的當前關聯值,即用消息隊列的當前關聯值覆蓋msgid_ds的值
- IPC_SET:如果進程有足夠的權限,就把消息列隊的當前關聯值設置為msgid_ds結構中給出的值
- IPC_RMID:刪除消息隊列. 注意:若選擇刪除隊列,第三個參數傳NULL
- buf是指向msgid_ds結構的指針,它指向消息隊列模式和訪問權限的結構
- 如果操作成功,返回“0”;如果失敗,則返回“-1”
msgid_ds結構至少包括以下成員:
struct msgid_ds {uid_t shm_perm.uid;uid_t shm_perm.gid;mode_t shm_perm.mode; };ipcs 命令
Linux系統下自帶的ipcs命令,可以查看當前系統下共享內存、消息隊列、信號量等等的使用情況,從而利于定位多進程通信中出現的通信問題。
上圖分別查看了當前unix系統中信號量、共享內存和消息隊列的使用情況。
命令格式如下:
ipcs [resource-option] [output-format]resource選項:
- ipcs -a 顯示系統內所有的IPC信息
- …
輸出格式控制:
- ipcs -p 查看IPC資源的創建者和使用的進程ID
- ipcs -u 查看IPC資源狀態匯總信息
ipcrm 通過指定ID刪除IPC資源,同時將與IPC對象關聯的數據一并刪除,只有超級用戶或IPC資源創建者能夠刪除。
實驗設計與編程
- 編寫消費者進程 A,A 生成 IPC key,并創建消息隊列,使 A 保持對消息隊列的監聽,A 接受類型為 1 的消息;
- 編寫生產者進程 B,B 和 A 使用相同的消息隊列。在另一個終端上運行進程 B,B 向消息隊列中發送自定義類型和內容的消息,觀察 A 所在終端的輸出。
消費者進程 A
// // Created by Sylvan Ding on 2022/3/5. ///* Process A - Consumer */#include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h>#define IPCKEY 666 // ipc key #define MSGSIZE 128 // message sizestruct MyMsg {long msgtype; // msg類型char msg_cont[MSGSIZE]; // msg內容 };int msgid; int ret_val; long msgtype = 1; // msg type to receive struct MyMsg mymsg = {0}; const char *quit_msg = "quit"; // message to quitint main(int argc, char *argv[]) {// 創建消息隊列msgid = msgget((key_t)IPCKEY, IPC_CREAT|IPC_EXCL|0666);if (msgid < 0) {printf("failed to create message queue\n");return -1;}// 從消息隊列中獲取類型為1的消息mymsg.msgtype = msgtype;while (1) {ret_val = msgrcv(msgid, &mymsg, sizeof(mymsg.msg_cont), msgtype, IPC_NOWAIT);if (ret_val > 0) {// 退出監聽if (!strcmp(mymsg.msg_cont, quit_msg)) {printf("Process A received a command to QUIT!\n");break;}// 打印接收信息printf("A received a message from msq: %s\n", mymsg.msg_cont);fflush(stdout);}}// 消息隊列釋放msgctl(msgid, IPC_RMID, NULL);return 0; }生產者進程 B
// // Created by Sylvan Ding on 2022/3/5. ///* Process B - Producer */#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h>#define IPCKEY 666 // ipc key #define MSGSIZE 128 // message sizestruct MyMsg {long msgtype; // msg類型char msg_cont[MSGSIZE]; // msg內容 };int msgid; int ret_val; struct MyMsg mymsg = {0};int main(int argc, char *argv[]) {// 創建消息隊列msgid = msgget((key_t)IPCKEY, IPC_CREAT);if (msgid < 0) {printf("failed to create message queue\n");return -1;}// 輸入消息類型和內容printf("Here is the procedure B: \n");printf("mgs_type = ");scanf("%ld", &(mymsg.msgtype));printf("input your msg:\n");scanf("%s", mymsg.msg_cont);// 向消息隊列發送信息ret_val = msgsnd(msgid, &mymsg, sizeof(mymsg.msg_cont), IPC_NOWAIT);if (ret_val < 0) {printf("failed to send message\n");return -1;}return 0; }實驗結果與分析
總結和實驗心得
本次實驗基于Linux系統實現了進程的共享內存、管道和消息隊列等三種IPC方式。其中,共享內存使各個進程共享一段物理內存空間,管道為父子進程間信息傳遞帶來便捷,消息隊列提供了消息類型選擇機制。實驗還通過信號量改進了共享內存,實現了進程對存儲空間的互斥訪問。通過實驗,初步掌握了Linux系統的IPC操作,了解了他們各自的優缺點,現做總結如下:
如果用戶傳遞的信息較少,或是需要通過信號來觸發某些行為,軟中斷信號機制不失為一種簡捷有效的進程間通信方式。但若是進程間要求傳遞的信息量比較大或者進程間存在交換數據的要求,那就需要考慮別的通信方式了。
無名管道簡單方便。但局限于單向通信的工作方式。并且只能在創建它的進程及其子孫進程之間實現管道的共享:有名管道雖然可以提供給任意關系的進程使用。但是由于其長期存在于系統之中,使用不當容易出錯,所以普通用戶一般不建議使用。
消息緩沖可以不再局限于父子進程,而允許任意進程通過共享消息隊列來實現進程間通信。并由系統調用函數來實現消息發送和接收之間的同步。從而使得用戶在使用消息緩沖進行通信時不再需要考慮同步問題。使用方便,但是信息的復制需要額外消耗CPU的時間。不適宜于信息量大或操作頻繁的場合。
共享內存針對消息緩沖的缺點改而利用內存緩沖區直接交換信息,無須復制,快捷、信息量大是其優點。但是共享內存的通信方式是通過將共享的內存緩沖區直接附加到進程的虛擬地址空間中來實現的。因此,這些進程之間的讀寫操作的同步問題操作系統無法實現。必須由各進程利用其他同步工具解決。另外,由于內存實體存在于計算機系統中。所以只能由處于同一個計算機系統中的諸進程共享。不方便網絡通信。
不同的進程通信方式有不同的優點和缺點。因此。對于不同的應用問題,要根據問題本身的情況來選擇進程間的通信方式。
參考文獻
轉載注意事項
本文為原創文章,轉載需要在文章開頭或結尾注明出處!
?? ?? Sylvan Ding’s Blog ??
總結
以上是生活随笔為你收集整理的Linux进程通信的四种方式——共享内存、信号量、无名管道、消息队列|实验、代码、分析、总结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MYSQL--三种锁
- 下一篇: linux系统安装显卡驱动卡顿,关于Ub