文章目錄
- 一、引言
- 二、System V IPC的基本概念
- 1、IPC結構的引入
- 2、IPC標識符(IPC ID)
- 3、S ystem V的優缺點
- 三、共享內存(Shared Memory)
- 1、共享內存的基本概念
- 2、共享內存的創建(shmget)
- 3、共享內存的附加(shmat)和分離(shmdt)
- 4、共享內存的控制(shmctl)
- 5、使用案例
- 四、消息隊列(Message Queues)
- 1、消息隊列的基本概念
- 2、消息隊列的創建(msgget)
- 3、消息的發送(msgsnd)和接收(msgrcv)
- 4、消息隊列的控制(msgctl)
- 五、信號量(Semaphores)
- 1、信號量的基本概念
- 2、信號量的創建(semget)
- 3、信號量的初始化(semctl)
- 4、信號量的P操作(semop)和V操作
一、引言
System V IPC是Linux中的一種進程間通信機制。它主要包括消息隊列、信號量和共享內存三種形式。這些機制都通過內核中的IPC設施來實現,允許進程之間進行高效的數據交換和同步。
- 消息隊列:消息隊列允許一個進程向另一個進程發送一個具有特定類型(或稱為“消息類型”)的消息。發送者將消息放入隊列的尾部,而接收者則從隊列的頭部取出消息。消息隊列對于需要異步通信的場景特別有用。
- 信號量:信號量是一個整數變量,主要用于控制對共享資源的訪問。通過操作信號量(如P操作和V操作),進程可以實現對共享資源的互斥訪問或同步操作。信號量在防止死鎖和保證系統穩定性方面起著重要作用。
- 共享內存:共享內存允許兩個或多個進程共享一塊內存區域。通過映射同一塊物理內存到不同進程的地址空間,這些進程可以直接訪問該內存區域中的數據,從而實現高速的數據交換。共享內存是IPC機制中效率最高的一種。
二、System V IPC的基本概念
1、IPC結構的引入
System V IPC的基本概念中,IPC結構的引入是為了解決本地間不同進程之間的通信問題。在Linux系統中,多個進程可能需要相互協作、共享數據或資源,以及協調各自的工作。為了實現這些目標,System V IPC引入了幾種類型的IPC結構,包括消息隊列、共享內存和信號量。
這些IPC結構存在于內核中,而不是文件系統中,這意味著它們由內核直接管理,并且可以通過特定的系統調用來訪問和操作。與管道等其他通信機制不同,IPC結構的釋放不是由內核自動控制的,而是由用戶顯式控制。
System V IPC中的IPC結構并不為按名字為文件系統所知。因此我們不能使用ls
命令看到他們,不能使用rm
命令刪掉它們。因此我們在查看時使用ipcs
命令,刪除時使用ipcrm
命令。
例如,我們可以使用while :; do ipcs -m ; sleep 1 ; done
循環查看當前進程中的共享內存。
ipcrm
命令用于刪除IPC設施。它可以根據設施的標識符或鍵來刪除消息隊列、信號量集或共享內存段。命令的基本語法是:
ipcrm [-M key | -m id | -Q key | -q id | -S key | -s id]
每個IPC結構都有一個唯一的標識符(ID),這是由系統在創建對象時分配的。此外,每個IPC結構還有一個關聯的key
值,用于在不同進程之間唯一標識和訪問同一個IPC結構。通過KEY值,不同的進程可以打開或找到同一個IPC結構,從而實現進程間的通信。
總的來說,System V IPC結構的引入使得不同進程之間可以更加高效、靈活地進行通信和協作。這些IPC結構的存在使得進程間的數據交換、資源共享和同步變得更加簡單和可靠。
2、IPC標識符(IPC ID)
每個內核中的 IPC 結構(消息隊列、信號量或共享存儲段)都用一個非負整數的標識符加以引用。
在System V IPC中,IPC標識符(IPC ID)是一個非負整數,用于唯一地標識一個IPC結構。每個IPC結構(如消息隊列、信號量或共享內存段)在創建時都會被分配一個唯一的IPC ID。
當創建一個消息隊列、信號量或共享內存段時,系統調用(如 msgget()
, semget()
, shmget()
)會返回一個 IPC ID,如果成功的話。這個 IPC ID 是用來在后續的 IPC 操作中引用該 IPC 對象的。
這個IPC ID是操作系統范圍內的全局變量,只要具有相應的權限,任何進程都可以通過這個ID來訪問和操作相應的IPC結構。這種機制使得進程間的通信更加靈活和高效,因為進程可以直接通過IPC ID來引用和操作IPC結構,而無需通過文件路徑或其他標識符。
無論何時創建IPC結構,都應指定一個關鍵字,關鍵字的數據類型由系統規定為key_t
,通常在頭文件<sys/types.h>
中被規定為長整型。關鍵字由內核變換成標識符。
在System V IPC中,獲取IPC ID的通常方式是調用相應的get函數(如msgget
、semget
或shmget
,分別是創-建消息隊列,信號量和共享內存的系統調用函數),并傳遞一個key
值作為參數。這個key值是通過ftok
函數從文件路徑和ID生成的唯一鍵。系統會根據這個key
值來查找或創建對應的IPC結構,并返回其IPC ID。man 3 ftok
:
ftok函數的意義在于為System V IPC 提供唯一的鍵值(key)。在創建共享內存、消息隊列等進程間通信的標識符時,通常需要指定一個ID值。這個ID值通常是通過ftok函數得到的。
ftok
函數是Linux系統中提供的一種比較重要的進程間通信機制,它可以將一個已經存在的文件的路徑名和一個子序號(通常為非負整數)作為輸入,然后返回一個唯一的key_t
類型的鍵值。這個鍵值在系統中是全局唯一的,可以用于標識和訪問特定的IPC結構(如共享內存、消息隊列)。
使用ftok
函數時,需要確保指定的文件路徑下存在一個有效的文件,并且該文件在程序運行期間不會被刪除或移動。否則,ftok函數可能會返回錯誤或生成不同的鍵值,導致進程間通信失敗。
在System V IPC機制中,兩個或多個進程可以通過約定形成同樣的key
,然后使用這個key
來找到和訪問同一個共享內存或消息隊列。
需要注意的是,在System V IPC機制中,IPC ID是系統分配的,并且在系統重啟之前都是有效的。即使創建IPC結構的進程已經退出,只要沒有執行刪除操作或系統重啟,其他進程仍然可以通過IPC ID來訪問和操作該IPC結構。
當系統重啟時,由于內核會重新加載,所有的IPC對象都會被銷毀,因為它們的生命周期并不是永久性的,而是依賴于內核的運行狀態。
IPC標識符(IPC ID)是System V IPC中用于唯一標識IPC結構的非負整數。通過IPC ID,進程可以高效地訪問和操作IPC結構,實現進程間的通信和協作。
key與IPC ID的關系?
key
是在內核角度用于區分共享內存的唯一性。我們以shmid
為例,shmid
是共享內存的ID。此處不明白,可先到后文共享內存處。
首先,key
是長整型的,用于在進程間共享內存、信號量和消息隊列等系統資源之間進行標識和訪問。這個key
值通常是通過ftok()
函數根據給定的路徑名和標識符生成的。在shmget
函數的調用中,key
被用于指定要創建或訪問的共享內存段,也就是將key
和shmid
關聯起來。
而shmid
是共享內存段的用戶級標識符,它是一個非負整數,用于唯一地標識一個共享內存段。當通過shmget
函數成功創建或打開一個共享內存段時,系統會返回一個shmid
,進程可以使用這個shmid
來進行后續的共享內存操作,如shmat
(將共享內存附加到進程的地址空間)和shmdt
(將共享內存從進程的地址空間中分離)。
因此,可以說key
是創建或訪問共享內存段的“鑰匙”,而shmid
則是成功創建或打開共享內存段后獲得的“通行證”。在共享內存的管理中,key
和shmid
共同確保了進程能夠正確地訪問和操作共享內存段。
類似文件inode
和文件fd
的關系
在文件系統中,inode
(索引節點)和文件描述符(fd
)各自扮演了不同的角色,而在共享內存管理中,key
和shmid
也有類似的關系。
- 文件
inode
:在Linux系統中,inode
是文件系統用于存儲文件元數據(如權限、所有者、大小、創建時間等)的數據結構。每個文件(或目錄)在文件系統中都有一個唯一的inode
與之關聯。這個inode
是從內核角度區分文件的唯一性標識。 - 文件描述符(
fd
):文件描述符是一個非負整數,用于在用戶空間程序中引用一個打開的文件。當進程打開一個文件時,內核會分配一個文件描述符給該進程,進程通過這個文件描述符來進行文件的讀寫等操作。
類似地,在共享內存管理中:
key
:key
是用于在內核角度區分共享內存的唯一性標識。它通常通過ftok
函數生成,或者由程序員直接指定。在創建共享內存段時,key
被用來確定要創建或訪問的是哪個共享內存段。shmid
:shmid
(共享內存標識符)是一個非負整數,用于在用戶空間程序中引用一個已創建的共享內存段。當成功創建一個共享內存段后,系統會返回一個shmid
給調用進程。進程通過這個shmid
來進行后續的共享內存操作,如附加(shmat
)、分離(shmdt
)和刪除(shmctl
)。
因此,key
和inode
都是從內核角度區分資源(文件或共享內存)的唯一性標識,而shmid
和文件描述符(fd
)則是從用戶空間角度引用這些資源的標識符。這樣的設計使得內核可以高效地管理資源,同時允許用戶空間程序以更加靈活和直觀的方式使用這些資源。
系統為每一個IPC結構設置一個了ipc_perm
結構。
struct ipc_perm
{__key_t __key; /* Key. */__uid_t uid; /* Owner's user ID. */__gid_t gid; /* Owner's group ID. */__uid_t cuid; /* Creator's user ID. */__gid_t cgid; /* Creator's group ID. */__mode_t mode; /* Read/write permission. */unsigned short int __seq; /* Sequence number. */
};
在創建IPC對象(如通過msgget()
, semget()
, shmget()
等系統調用)時,內核會為該對象分配一個ipc_perm
結構,并初始化其中的字段。除了__seq
字段(它通常由內核管理以跟蹤對象的創建和刪除),其他字段都由創建者或具有適當權限的進程來設置。
為什么要設計該結構體呢?
系統為每一個IPC(進程間通信)結構設置一個ipc_perm
結構,是因為這個結構用于描述IPC對象的權限和所有權信息:
- 權限管理:
ipc_perm
結構中的uid
(用戶ID)、gid
(組ID)和mode
(訪問模式)字段用于定義哪些用戶可以訪問、修改或刪除IPC對象。這種權限管理機制確保了系統的安全性和穩定性,防止未授權的進程訪問或篡改IPC對象。 - 所有權跟蹤:
ipc_perm
結構中的uid
和gid
字段還用于跟蹤IPC對象的所有者。這有助于系統管理員識別和管理IPC對象,例如查找和刪除不再需要的IPC對象。 - 統一接口:在Linux系統中,多種IPC機制(如消息隊列、信號量和共享內存)都使用類似的接口和數據結構。為每種IPC結構都設置一個
ipc_perm
結構,可以確保這些IPC機制在權限管理和所有權跟蹤方面具有一致的接口和行為。 - 內核管理:IPC對象是在內核中創建的,因此內核需要一種方式來跟蹤和管理這些對象的權限和所有權。
ipc_perm
結構為內核提供了一種方便的方式來存儲和檢索這些信息。當系統創建一個新的IPC對象(如一個消息隊列或信號量集)時,它會在內核中為該對象分配內存,并初始化一個ipc_perm
結構來保存該對象的權限和所有權信息。這個ipc_perm
結構通常作為IPC對象特定數據結構(如msgid_ds
、semid_ds
或shmid_ds
)的一部分。內核中,所有IPC對象的ipc_perm
結構被組織成一個數組,以便內核能夠快速地根據IPC對象的標識符(如消息隊列的ID)找到對應的ipc_perm
結構。
3、S ystem V的優缺點
- 訪問計數和垃圾回收:System V IPC結構(如消息隊列、信號量和共享內存)在系統范圍內起作用,但它們沒有訪問計數機制。這意味著,即使不再有任何進程引用這些IPC結構,它們也不會被自動刪除。這可能導致系統資源的浪費,因此需要我們顯式地刪除不再需要的IPC結構。
- 文件系統不可見:System V IPC結構并不通過文件系統來管理,因此它們對于傳統的文件操作命令(如ls、
rm
和chmod
)是不可見的。這增加了管理和調試的復雜性,因為需要使用專門的命令(如ipcs
和ipcrm
)來列出、刪除和修改IPC結構的屬性。 - 編程接口復雜性:System V IPC提供了豐富的功能,但也引入了復雜的編程接口。與基于文件的IPC機制(如管道和
FIFO
)相比,使用System V IPC需要更多的系統調用和更復雜的編程技術。 - 不支持文件描述符:由于System V IPC結構不是通過文件系統來管理的,因此它們沒有文件描述符。這限制了使用基于文件描述符的I/O函數(如
select
和poll
)來監控多個IPC結構的能力。 - 標識符的動態分配:System V IPC結構的標識符是在系統啟動時動態分配的,并且與創建時的系統狀態有關。這增加了在多個進程之間共享IPC結構標識符的難度,因為需要某種形式的通信或配置文件來傳遞這些標識符。
此外,隨著Unix和類Unix系統的發展,出現了其他IPC機制,如POSIX IPC(包括消息隊列、信號量和共享內存),它們在某些方面提供了更好的抽象和更簡單的編程接口。這些新機制可能更適合某些應用場景,并減少了System V IPC的一些限制。
三、共享內存(Shared Memory)
1、共享內存的基本概念
共享存儲允許兩個或多個進程共享一給定的內存區。由于數據不需要在客戶機和服務器之 間復制,所以這是最快的一種通信方式。
**共享內存無進程間協調機制。**這意味著,當多個進程或線程訪問和修改同一塊共享內存區域時,它們必須自己管理對這塊內存的訪問,以防止數據沖突和不一致。
具體來說,當一個進程(我們稱之為寫入方)正在向共享內存寫入數據時,另一個進程(我們稱之為讀取方)可能同時嘗試讀取這塊內存。由于共享內存沒有內置的同步機制,讀取方可能會讀取到寫入方還未完全寫入的數據,或者讀取到寫入方寫入過程中的中間狀態,從而導致數據的不一致性和錯誤。
內核為每個共享存儲段設置了一個shmid_ds
結構:
shmid_ds
結構體用于描述一個共享內存段的屬性。當系統創建一個共享內存段時,內核會為該段分配一個shmid_ds
結構體來保存其相關信息。這個結構體包含了共享內存段的權限、大小、時間戳、附加到該段的進程數等信息。
2、共享內存的創建(shmget)
我們通常使用此函數來創建或者獲取當前key對應的共享內存,當新建一個共享內存時,我們會初始化shmid_ds
結構體的部分成員:
參數:
key
: 這是一個鍵值,用于唯一地標識一個共享內存段。多個進程可以通過這個鍵值來訪問同一個共享內存段。size
: 這是要分配的共享內存段的大小(以字節為單位)。即,我們可以通過該參數來設置共享內存的大小。Linux中,共享內存的大小以4KB為基本單位。若size
的值為4097,共享內存實際大小為8KB。我們只能用4097B。如果正在存訪一個現存的共享內存,則將size
指定為0。shmflg
: 這個標志位用于控制shmget
的行為。它可以是以下值的組合:IPC_CREAT
: 如果指定的共享內存段不存在,則創建它。IPC_EXCL
: 與IPC_CREAT
一起使用時,如果指定的共享內存段已經存在,則調用失敗。- 權限位(如
0666
):這些位指定了新創建的共享內存段的權限。這些權限位與文件系統的權限類似,但它們的解釋略有不同(特別是對于組和其他用戶)。
返回值:
- 如果成功,
shmget
返回一個非負整數,這個整數是共享內存段的標識符(也稱為“鍵”或“句柄”)。 - 如果失敗,
shmget
返回-1
,并設置全局變量errno
以指示錯誤原因。
使用案例:shmid = shmget(key, size, IPC_EXCL | IPC_CREAT | 0666 );
3、共享內存的附加(shmat)和分離(shmdt)
在System V IPC機制中,shmat
和shmdt
是用于附加和分離共享內存段的函數。一旦創建了一個共享存儲段,進程就可調用shmat
將其連接到它的地址空間中。
shmat(附加共享內存)
shmat
是 “shared memory attach” 的縮寫,它的功能是將共享內存區域附加到指定的進程地址空間中。一旦附加成功,進程就可以像訪問自己的內存一樣訪問共享內存。
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
參數說明:
shmid
:由shmget
返回的共享內存標識符。shmaddr
:這是一個可選參數,指定了共享內存附加到進程地址空間的地址。如果設置為NULL,則由系統選擇地址。shmflg
:一組標志,用于控制附加操作的行為。通常設置為0。
如果成功,shmat
返回一個指向共享內存段的指針。如果失敗,返回-1并設置errno
。
shmdt(分離共享內存)
shmdt
函數的功能是將之前附加到進程的共享內存段從進程地址空間中分離。一旦分離,進程就不能再訪問這塊共享內存了。
當對共享存儲段的操作已經結束時,則調用shmdt
脫接該段。注意,這并不從系統中刪除 其標識符以及其數據結構。該標識符仍然存在,直至某個進程(一般是服務器)調用 shmctl
(帶命令IPC_RMID
)特地刪除它。
函數的原型如下:
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
參數shmaddr
是由shmat
返回的指向共享內存段的指針。如果成功,shmdt
返回0。如果失敗,返回-1并設置errno
。
4、共享內存的控制(shmctl)
共享內存的控制通常使用shmctl函數來實現。這個函數的全稱是"shared memory control",用于控制共享內存段的屬性、狀態以及執行一些管理操作。
參數說明:
shmid
:共享內存標識符,即要控制的共享內存段的標識符。cmd
:控制命令,指定了要執行的操作。常見的命令包括:IPC_STAT
:獲取共享內存段的狀態信息,并將其保存在buf中。IPC_SET
:設置共享內存段的狀態信息為buf中的值。IPC_RMID
:刪除共享內存段。因為每個共享存儲段有一個連接計數 (shm_nattch
在shmid_ds
結構中),所以除非使用該段的最后一個進程終止或與該段脫接,否則 不會實際上刪除該存儲段。不管此段是否仍在使用,該段標識符立即被刪除 ,所以不能再用shmat
與該段連接。此命令只能由下列兩種進程執行 :一種是其有效用戶I D等于shm_perm.cuid
或shm_perm.uid
的進程;另一種是具有超級用戶特權的進程。
buf
:一個指向shmid_ds
結構體的指針,用于傳遞或接收共享內存段的狀態信息。這個結構體包含了共享內存的大小、擁有者ID和組ID、權限設置、最后訪問和修改的時間等信息。
調用shmctl
函數并不會直接清除共享內存中的數據,它只是控制共享內存的屬性和狀態。例如,你可以使用IPC_SET
命令來修改共享內存的權限,或者使用IPC_RMID
命令來刪除不再需要的共享內存段。
進程使用共享內存時是如何知道共享內存大小呢
System V共享內存中,當進程想要使用共享內存時,它們會先通過shmget
函數來獲取共享內存的標識符(shmid
),然后再通過該標識符以及其他相關信息來操作共享內存。
具體來說,shmget
函數在創建或獲取共享內存時,會返回一個共享內存的標識符(shmid
)。這個標識符是唯一的,并且可以用來引用特定的共享內存段。同時,shmget
函數還接受一個size
參數,用于指定共享內存的大小(以字節為單位)。當創建新的共享內存段時,這個size
參數就是新段的大小;而當獲取已經存在的共享內存段時,這個參數實際上是被忽略的,因為段的大小已經由之前創建它的進程確定了。
一旦進程獲取了共享內存的標識符(shmid
),它就可以使用其他相關的函數來操作這個共享內存段了。但是,如何知道這個共享內存段的大小呢?
使用shmctl
函數和IPC_STAT
命令來獲取共享內存的狀態信息:shmctl
函數是一個通用的控制函數,可以用于執行各種與共享內存相關的操作。當使用IPC_STAT
命令調用shmctl
時,它會返回一個shmid_ds
結構體,其中包含了共享內存的各種狀態信息,包括大小、權限、連接數等。進程可以通過這種方式來獲取共享內存的大小。
5、使用案例
假設我們有一個客戶端一個服務端,我們需要客戶端向服務端輸入內容。
下面代碼,我們使用FIFO做輔助,來進行讀取控制。
Comm.hpp
#pragma once
#include "Fifo.hpp"
#include <unistd.h>
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <cstring>
#include <string>using namespace std;const char *pathname = "/home/zyb/study_code";
const int proj_id = 0x66;
const int defaultsize = 4097; // 單位是字節
// 我們可以通過size大小,來設置共享內存的值。
// OS中,共享內存的大小以4KB為基本單位。若size為4097,共享內存實際大小為8KB。std::string ToHex(key_t k)
{char buffer[1024];snprintf(buffer, sizeof(buffer), "0x%x", k);return buffer;
}
key_t GetShmKeyOrDie()
{key_t k = ftok(pathname, proj_id);if (k < 0){std::cerr << "ftok error, errno : " << errno << " , error string : " << strerror(errno) << std::endl;exit(1);}return k;
}
int CreateShmOrDie(key_t key, int size, int flag)
{int shmid = shmget(key, size, flag);if (shmid < 0){std::cerr << "shmget error, errno : " << errno << " , error string : " << strerror(errno) << std::endl;exit(2);}return shmid;
}// IPC_CREAT : 不存在就創建,存在就獲取
// IPC_EXCL: 存在就出錯返回
// IPC_EXCL|IPC_CREAT: 不存在就創建,存在就出錯返回
int CreateShm(key_t key, int size)
{return CreateShmOrDie(key, size, IPC_EXCL | IPC_CREAT | 0666 /*指定共享內存的默認權限*/);
}
int GetShm(key_t key, int size)
{return CreateShmOrDie(key, size, IPC_EXCL);
}void DeleteShm(int shmid)
{int n = shmctl(shmid, IPC_RMID, nullptr);if (n < 0){std::cerr << "shmctl error, errno : " << errno << " , error string : " << strerror(errno) << std::endl;}else{std::cout << "shmctl delete shm success, shmid: " << shmid << std::endl;}
}void ShmDebug(int shmid)
{struct shmid_ds shmds;int n = shmctl(shmid, IPC_STAT, &shmds);if (n < 0){std::cerr << "shmctl error, errno : " << errno << " , error string : " << strerror(errno) << std::endl;return;}// 共享內存大小std::cout << "shmds.shm_segsz: " << shmds.shm_segsz << std::endl;// 當前附加到該共享內存段的進程數std::cout << "shmds.shm_nattch:" << shmds.shm_nattch << std::endl;// 表示共享內存段的“更改時間” 通常以時間戳(從1970年1月1日開始的秒數)的形式存儲std::cout << "shmds.shm_ctime:" << shmds.shm_ctime << std::endl;// 共享內存的key值std::cout << "shmds.shm_perm.__key:" << ToHex(shmds.shm_perm.__key) << std::endl;
}void *ShmAttach(int shmid)
{void *addr = shmat(shmid, nullptr, 0);if ((long long int)addr == -1){std::cerr << "shmat error" << std::endl;return nullptr;}return addr;
}void ShmDetach(void *addr)
{int n = shmdt(addr);if (n < 0){std::cerr << "shmdt error" << std::endl;}
}
Fifo.hpp
#ifndef __COMM_HPP__
#define __COMM_HPP__#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <cassert>using namespace std;#define Mode 0666
#define Path "./fifo"class Fifo
{
public:Fifo(const string &path = Path) : _path(path){umask(0);int n = mkfifo(_path.c_str(), Mode);if (n == 0){cout << "mkfifo success" << endl;}else{cerr << "mkfifo failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;}}~Fifo(){int n = unlink(_path.c_str());if (n == 0){cout << "remove fifo file " << _path << " success" << endl;}else{cerr << "remove failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;}}private:string _path; // 文件路徑+文件名
};class Sync
{
public:Sync() : rfd(-1), wfd(-1){}void OpenReadOrDie(){rfd = open(Path, O_RDONLY);if (rfd < 0)exit(1);}void OpenWriteOrDie(){wfd = open(Path, O_WRONLY);if (wfd < 0)exit(1);}bool Wait(){bool ret = true;uint32_t c = 0;ssize_t n = read(rfd, &c, sizeof(uint32_t));if (n == sizeof(uint32_t)){std::cout << "server wakeup, begin read shm..." << std::endl;}else if (n == 0){ret = false;}else{return false;}return ret;}void Wakeup(){uint32_t c = 0;ssize_t n = write(wfd, &c, sizeof(c));assert(n == sizeof(uint32_t));std::cout << "wakeup server..." << std::endl;}~Sync() {}private:int rfd;int wfd;
};#endif
ShmServer.cc
#include "Comm.hpp"int main()
{// 1. 獲取keykey_t key = GetShmKeyOrDie();std::cout << "key: " << ToHex(key) << std::endl;// sleep(2);// 2. 創建共享內存int shmid = CreateShm(key, defaultsize);// ShmDebug(shmid);// 3. 將共享內存和進程進行掛接(關聯)char *addr = (char *)ShmAttach(shmid);std::cout << "Attach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;// sleep(2);// 4. 引入管道Fifo fifo;Sync syn;syn.OpenReadOrDie();// 通信for (;;){if (!syn.Wait())break;cout << " shm content : " << addr << endl;}ShmDetach(addr);std::cout << "Detach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;sleep(2);// 3. 刪除共享內存DeleteShm(shmid);return 0;
}
ShmClient.cc
#include "Comm.hpp"int main()
{key_t key = GetShmKeyOrDie();std::cout << "key: " << ToHex(key) << std::endl;// sleep(2);int shmid = GetShm(key, defaultsize);std::cout << "shmid: " << shmid << std::endl;// sleep(2);char *addr = (char *)ShmAttach(shmid);std::cout << "Attach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;// sleep(5);memset(addr, 0, defaultsize);Sync syn;syn.OpenWriteOrDie();// 通信for (char c = 'A'; c <= 'Z'; c++){addr[c - 'A'] = c;sleep(1);syn.Wakeup();}ShmDetach(addr);std::cout << "Detach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;sleep(2);return 0;
}
最后我們思考:共享內存是所有進程間通信中速度最快的。為什么呢
在共享內存模型中,進程A和進程B都可以直接訪問同一塊物理內存區域(即“共享區”)。這意味著數據不需要通過系統調用或其他中間層進行復制或傳輸,從而減少了數據傳輸的開銷。
共享內存通過允許進程直接訪問同一塊物理內存區域,減少了數據傳輸和I/O操作的開銷,降低了延遲,從而提高了進程間通信的效率。
當A進程需要與B進程通信時,只需要把共享區的虛擬地址與物理地址的映射寫入兩進程的頁表中。因此,進程A可以對該物理地址直接進行寫入;而B進程則是通過頁表的映射關系,從該物理地址直接進行讀取。
傳統的進程間通信(IPC)機制,如管道、消息隊列等,通常涉及到內核空間和用戶空間之間的數據拷貝,這會產生大量的I/O操作。而共享內存允許進程直接訪問內存中的數據,從而避免了這種I/O開銷。
上圖中的管道,我們在使用時,雖然是使用內核緩沖區來進行操作,并沒有將數據寫入到磁盤中,但進行讀寫的兩個進程,當發送方將數據寫入管道時,數據會被拷貝到內核緩沖區中。然后,當接收方從管道中讀取數據時,數據會從內核緩沖區被拷貝到接收方的用戶空間。這個過程中,數據只被拷貝了兩次:一次是從發送方的用戶空間到內核緩沖區,另一次是從內核緩沖區到接收方的用戶空間。
四、消息隊列(Message Queues)
1、消息隊列的基本概念
消息隊列本質上是一個隊列,隊列中存放的是一個個消息。而隊列是一個數據結構,具有先進先出的特點,它存放在內核中并由消息隊列標識符標識。我們稱消息隊列標識符為“”隊列ID”。msgget
用于創建一個新隊列或打開一個現存的隊列。 msgsnd
用于將新消息添加到隊列尾端。每個消息包含一個正長整型類型字段,一個非負長度以及實際 數據字節(對應于長度),所有這些都在將消息添加到隊列時,傳送給msgsnd
。msgrcv
用于從隊列中取消息。我們并不一定要以先進先出次序取消息,也可以按消息的類型字段取消息。
每個隊列中都有一個msqid_ds
結構與其相關:
上述結構規定了消息隊列的當前狀態。
2、消息隊列的創建(msgget)
我們使用消息隊列首先就需要mssget
函數,用來打開一個消息隊列或創建一個新的隊列。
該函數的參數與共享內存相似,我們不再贅述。
若執行成功,則返回非負隊列ID。此后,此值就可被用于消息隊列的其他函數。
3、消息的發送(msgsnd)和接收(msgrcv)
這兩個函數都需要消息隊列ID(msqid)以及特定的結構體struct msgbuf
作為參數。
msgsnd
函數用于將一個新的消息寫入隊列。為了發送消息,調用進程對消息隊列進行寫入時必須有寫權能。
msgp
:指向要發送消息的指針,該消息應該是msgbuf
結構體的實例。msgsz
:消息的大小(不包括mtype
字段)。
msgrcv
函數用于從消息隊列中讀取消息。接收消息時必須有讀權能。
-
msgp
:指向用于存儲接收到的消息的緩沖區的指針,該緩沖區應該是msgbuf
結構體的實例。 -
msgsz
:緩沖區中mtext
字段的最大大小。 -
msgtyp
:要接收的消息的類型。如果msgtyp
為0,則接收隊列中的第一個消息。如果msgtyp
大于0,則接收具有相同類型的第一個消息。如果msgtyp
小于0,則接收類型小于或等于msgtyp
絕對值的最低類型消息。
其中msgp
結構體定義如下:
struct msgbuf {long mtype; /* 消息類型,必須大于0 */char mtext[1]; /* 消息數據,實際大小由msgsz指定 */
};
注意,該結構當中的第二個成員mtext
即為待發送的信息,當我們定義該結構時,mtext
的大小可以自己指定。
4、消息隊列的控制(msgctl)
msgctl
函數類似共享內存的shmctl
,也可以取出消息隊列的結構,設置消息隊列結構,刪除消息隊列。參數同shmctl
,第一個參數表示對哪個消息隊列進行操作:
五、信號量(Semaphores)
1、信號量的基本概念
信號量是一個計數器,用于多進程對共享數據對象的存取,其值表示某個共享資源的可用數量。當一個進程或線程需要訪問這個共享資源時,它必須先請求信號量,并等待信號量變為可用。
為了獲得共享資源,進程需要執行下列操作:
- 測試控制該資源的信號量。
- 若此信號量的值為正,則進程可以使用該資源。進程將信號量值減 1,表示它使用了一 個資源單位。
- 若此信號量的值為 0,則進程進入睡眠狀態,直至信號量值大于 0。若進程被喚醒后, 它返回至(第( 1 )步)。
當進程不再使用由一個信息量控制的共享資源時,該信號量值增 1。如果有進程正在睡眠等待此信號量,則喚醒它們。 為了正確地實現信息量,信號量值的測試及減 1操作應當是原子操作。為此,信號量通常是在內核中實現的。
內核為每個信號量設置了一個semid_ds
結構體:
2、信號量的創建(semget)
返回值是信號量集ID:
3、信號量的初始化(semctl)
該函數包含了多種信號量操作:
semctl
函數常用于對信號量集進行各種操作,包括設置信號量的初始值(即初始化)。
4、信號量的P操作(semop)和V操作
- P操作:也稱為“等待”操作。它用于請求訪問共享資源。如果信號量的值大于0,則將其減1并允許進程繼續執行;如果信號量的值為0,則進程將被阻塞,直到信號量的值變為大于0。這通常通過
semop
函數實現,并設置信號量的值semval為0(如果semval不為0,則阻塞或報錯),然后將其值加1(即semval為0時可以立即通過,否則等待)。 - V操作:也稱為“信號”或“發布”操作。它用于釋放共享資源。當進程完成共享資源的使用后,它會將信號量的值加1,以表示該資源現在可用。這同樣通過
semop
函數實現,并設置信號量的值減1(但只有在信號量值大于或等于要減去的值時才能立即返回,否則進程需要等待)。
在Linux系統中,使用信號量通常涉及以上提到的四個步驟:創建信號量、初始化信號量、進行P/V操作以及(在不再需要時)刪除信號量。