Linux:IPC - System V
- 共享內存 shm
- 創建共享內存
- shmget
- shmctl
- ftok
- 掛接共享內存
- shmat
- shmdt
- shm特性
- 消息隊列 msg
- msgget
- msgctl
- msgsnd
- msgrcv
- 信號量 sem
- System V 管理機制
System V IPC
是Linux系統中一種重要的進程間通信機制,它主要包括共享內存 shm
,消息隊列 msg
,信號量 sem
。本博客講解基于System V
的進程間通信。
共享內存 shm
共享內存,顧名思義就是一塊被多個進程共享的內存,由于進程間具有獨立性,毫無疑問這一塊內存不應該由某一個進程進行開辟,而是由操作系統親自開辟。
如上圖,共享內存會被進程的頁表直接映射到自己的進程地址空間的共享區,從而通過地址空間直接對內存操作,這就是多個進程共享一塊內存的基本原理。
那么我們現在就來看看如何創建出一個共享內存shm
:
創建共享內存
shmget
shmget
函數是 shm
中用于創建或獲取共享內存段的函數。需要頭文件<sys/ipc.h>
和<sys/shm.h>
函數原型如下:
int shmget(key_t key,size_t size,int shmflg);
返回值:
shmget
返回一個整型,這個整型叫做shmid
,用于標識唯一的shm
。
key
: 用于標識要創建或獲取的共享內存段
key
是System V
的唯一標識符,注意不是shm
的,而是所有System V
的,你可以通過一個key
值來標識任意一個system V
。
- 標識唯一的
shm
是shmid
- 標識唯一的
system V
是key
size
: 指定要創建的共享內存段的大小,單位為字節
注意:共享內存以4 kb
為基本單位開辟內存,也就是4096 byte
,因此開辟shm
的時候,這個參數最好設置為4096
的倍數。哪怕你只申請了1 byte
的內存,實際上還是會開辟4096 byte
大小的空間。
shmflg
: 用于指定共享內存段的訪問權限和其他選項
這是一個用于控制共享內存的開辟方式,以及各個屬性的選項,本質是一個位圖。
-
IPC_CREAT
: 如果指定的key
不存在,則創建一個新的共享內存段,如果已經存在,則直接獲得原先的共享內存 -
IPC_EXCL
: 如果指定的key
已經存在,則創建失敗
要注意IPC_EXCL
只能配合IPC_CREAT
一起使用,不能單獨使用IPC_EXCL
。
另外的,共享內存也可以設置讀寫權限,直接將權限值的八進制按位或到第三個參數中即可。
示例:
int main()
{int shmid = shmget(1, 4096, IPC_CREAT | IPC_EXCL | 0666);return 0;
}
以上函數中,就是一個簡單的創建共享內存的過程:
- 第一個參數
1
:即這個共享內存的system V
標識符key = 1
; - 第二個參數傳入
4096
:即開辟的共享內存大小為4096 byte
; - 第三個參數為
IPC_CREAT | IPC_EXCL | 0666
:如果當前的key
不存在,則創建對應的共享內存,共享內存的初始權限為0666
,如果存在,則創建失敗。
那么我們要如何知道成功創建了一個共享內存呢?
通過ipcs
指令,可以看當前所有的system V
的總體情況:
如果只想看共享內存,則輸入ipcs -m
:
可以看到,我們創建了一個共享內存,key = 0x00000001
,shmid = 2
,擁有者onwer = box-he
,初始權限perm = 666
,大小bytes = 4096
。
那么現在有一個問題就是:我們的進程已經結束了,但是進程創建的共享內存還在!
也就是說,共享內存如果不主動釋放,那么共享內存就會一直存在,除非重啟操作系統。這個特性叫做:共享內存的生命周期隨內核。
如果想要刪除一個共享內存,有兩種方式:通過指令 / 通過接口。
通過ipcrm -m xxx
,可以刪除shmid
為xxx
的共享內存。
示例:
一開始存在一個shmid = 2
的共享內存,通過指令ipcrm -m 2
,就可以刪除這個共享內存了。
如果想要通過接口刪除共享內存,則通過shmctl
接口。
shmctl
shmctl
用于控制共享內存的各種屬性。
其包含以下功能:
- 獲取共享內存段的狀態信息
- 修改共享內存段的屬性
- 刪除共享內存段
shmctl
包含在頭文件<sys/ipc.h>
和<sys/shm.h>
中,函數原型如下:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
返回值:
- 成功時返回 0
- 失敗時返回 -1,并設置
errno
變量
參數如下:
shmid
:要操作的共享內存段的標識符
cmd
:要執行的操作,可以是以下值之一:
-IPC_STAT
:獲取共享內存段的狀態信息
-IPC_SET
:設置共享內存段的某些屬性
-IPC_RMID
:刪除共享內存段
buf
:指向shmid_ds
結構體的指針,用于存儲或設置共享內存段的屬性
在講解以上三種模式前,要先介紹一下一個結構體shmid_ds
。
struct shmid_ds {struct ipc_perm shm_perm; /* Ownership and permissions */size_t shm_segsz; /* Size of segment (bytes) */time_t shm_atime; /* Last attach time */time_t shm_dtime; /* Last detach time */time_t shm_ctime; /* Last change time */pid_t shm_cpid; /* PID of creator */pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */shmatt_t shm_nattch; /* No. of current attaches */...
};
shmid_ds
是一個Linux中給共享內存定義的結構體,這個結構體用于存儲一個共享內存的基本信息。
當第二個參數
cmd
為IPC_STAT
,此時就可以獲取一個共享內存的基本信息。
示例:
int main()
{int id = shmget(1, 4096, IPC_CREAT | IPC_EXCL | 0666);struct shmid_ds shm;shmctl(id, IPC_STAT, &shm);cout << "atime:" << shm.shm_atime << endl;cout << "ctime:" << shm.shm_ctime << endl;cout << "cpid:" << shm.shm_cpid << endl;return 0;
}
輸出結果:
以上示例中,通過一個struct shmid_ds
類型的結構體變量shm
,通過調用shmctl(id, IPC_STAT, &shm);
獲取到了這個共享內存的相關信息。
其中shmctl(id, IPC_STAT, &shm);
第一個參數id
:要控制的共享內存的shmid
第二個參數IPC_STAT
:表示當前shmctl
的作用是獲取共享內存相關信息
第三個參數&shm
:表示獲取到的信息存入變量shm
中
當第二個參數
cmd
為IPC_SET
,就可以設置共享內存的某些屬性
示例:
int main()
{int id = shmget(1, 4096, IPC_CREAT | IPC_EXCL | 0666);struct shmid_ds shm;shmctl(id, IPC_STAT, &shm);cout << "atime:" << shm.shm_atime << endl;shm.shm_atime = 1 ;//修改shm信息shmctl(id, IPC_SET, &shm);//重新獲取shm信息shmctl(id, IPC_STAT, &shm);cout << "atime:" << shm.shm_atime << endl;return 0;
}
一開始通過shmctl(id, IPC_STAT, &shm);
把共享內存的相關信息存儲到了結構體shm
中,然后把結構體的shm_atime
成員設置為1
。
再通過shmctl(id, IPC_SET, &shm);
把共享內存的信息設置成和shm
一致,此時第二個參數為IPC_SET
。
第三次調用shmctl(id, IPC_STAT, &shm);
,此時再把共享內存的信息同步到shm
中,最后輸出shm
的shm_atime
成員。
輸出結果:
可以看到,當第二個參數為IPC_SET
時,可以修改共享內存的相關屬性。
當第二個參數為
IPC_RMID
,表示要刪除共享內存段
此時由于我們要刪除共享內存,第三個參數就用不上了,此時設置為空指針即可。
示例:
int main()
{shmctl(3, IPC_RMID, nullptr);return 0;
}
以上代碼中shmctl(3, IPC_RMID, nullptr);
就表示要刪除shmid = 3
的共享內存。
輸出結果:
一開始存在一個shmid = 3
的共享內存,經過./test.exe
后,這個共享內存就被刪除了,也就是執行了shmctl(3, IPC_RMID, nullptr);
。
最后再看一次第二個參數的作用:
cmd
:要執行的操作,可以是以下值之一:
-IPC_STAT
:獲取共享內存段的狀態信息
-IPC_SET
:設置共享內存段的某些屬性
-IPC_RMID
:刪除共享內存段
現在應該可以理解三種情況的作用了。
ftok
創建之前所有示例中,通過shmget
創建共享內存時,第一個參數key
我都設置為了1
,但其實這是非常不符合規范的。操作系統中存在非常多的進程,如果多個進程通過system V
通信,那就不能使用相同的key
值,如果key
設置的太簡單,就很容易沖突。
ftok
函數,就利用算法生成不易重復的key
值。
使用ftok
函數需要頭文件<sys/types.h>
和<sys/ipc.h>
,函數原型如下:
key_t ftok(const char* pathname, int proj_id);
參數:
pathname
:當前操作系統下的某一條路徑proj_id
:一個數字
也就是說,只要傳入一個路徑和一個數字,ftok
就會生成一個key值。
示例:
int main()
{key_t key = ftok("./test.cpp", 1);cout << key << endl;return 0;
}
利用相對路徑,使用了路徑./test.cpp
作為第一個參數,第二個參數設置為了1,最后輸出ftok
生成的key值。
輸出結果:
最后生成的key
值為17003535
。
要注意的是,path
必須是一個存在的路徑,ftok
函數會利用路徑所指向的文件的屬性,以及傳入的第二個參數,一起來產生這個key值。
需要進程間通信的雙方,只需要事先約定好這個path
以及第二個整型,就可以利用ftok
產生相同的key
值,進而訪問同一塊共享內存了。
到目前為止,我們只是講解了如何來開辟一個共享內存,還沒有真正使用這一塊共享內存來實現進程間通信
也就是下圖中的藍色部分:
接下來就看看如何使用這一塊共享內存。
掛接共享內存
shmat
共享內存是被直接映射到進程地址空間的共享區的,進程可以通過訪問進程地址空間來訪問共享內存,那么現在的問題就是:如何讓一個內存映射到進程地址空間中?這個把共享內存映射到進程地址空間的過程,叫做掛接共享內存
。
掛接共享內存需要通過shmat
接口實現,需要頭文件<sys/types.h>
和<sys/shm.h>
,函數原型如下:
void *shmat(int shmid, const void *shmaddr, int shmflg);
參數:
shmid
:即被掛接的共享內存的shmid
shmaddr
:指明這個共享內存要掛接到哪一個地址,一般來說我們不會主動指定地址,這個參數直接傳入空指針即可,操作系統會自動幫我們選擇合適的地址掛接shmflg
:掛接共享內存的模式,- 傳入
0
:以讀寫
的方式掛接共享內存 - 傳入
SHM_RDONLY
:以只讀
的方式掛接共享內存
- 傳入
返回值:
- 如果掛接共享內存出錯,返回
-1
- 如果掛接共享內存成功,返回掛接后共享內存的地址
shmdt
如果你想要取消對共享內存的掛接,使用shmdt
接口即可,需要頭文件<sys/types.h>
和<sys/shm.h>
,函數原型如下:
int shmdt(const void *shmaddr);
只需要把掛接到的共享內存的地址傳入shmdt
即可取消掛接。
現在我們就利用共享內存來完成一次進程間通信,現有A.exe
和B.exe
兩個進程,A負責發送消息,B負責接收消息。
A進程代碼如下:
int main()
{key_t key = ftok("./test.cpp", 1);int shmid = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0666);char* ptr = (char*)shmat(shmid, nullptr, 0);for(int ch = 'A'; ch <= 'Z'; ch++){ptr[ch - 'A'] = ch;sleep(1);}shmctl(shmid, IPC_RMID, nullptr);return 0;
}
一開始A通過ftok
生成了一個key值,然后利用shmget
生成了一個共享內存,再用shmat
將共享內存掛接到進程地址空間,此時ptr
指針就指向這個共享內存了,隨后利用循環將字母A - Z
寫入共享內存中,每秒寫入一個。最后利用shmctl
關閉共享內存。
B進程代碼如下:
int main()
{key_t key = ftok("./test.cpp", 1);int shmid = shmget(key, 4096, IPC_CREAT);char* ptr = (char*)shmat(shmid, nullptr, 0);while(true){cout << ptr << endl;sleep(5);}return 0;
}
B進程也通過ftok
生成了key值,由于參數都是"./test.cpp"
和1
,所以生成的key和A一定是一樣的。
隨后通過shmget
獲得共享內存的shmid
,這個共享內存是由A
維護的,因此B只需要拿到shmid
即可,共享內存的創建和銷毀都由A來控制。
隨后B進程在一個循環中,每隔五秒讀取一次共享內存。
輸出結果:
可以看到,A進程向共享內存寫入的數據,此時就可以被B拿到了,這就是基于共享內存shm
的進程間通信。
shm特性
共享內存有以下一些主要特性:
-
內存共享:多個進程可以同時訪問和修改同一塊共享內存區域。這種共享內存機制可以讓進程之間高效地交換數據,而無需通過系統調用或者其他進程間通信機制。
-
快速訪問:相比于其他進程間通信機制,如管道、消息隊列等,共享內存的訪問速度更快,因為數據直接存儲在內存中,不需要進行數據的拷貝和上下文切換。
-
靈活性:共享內存可以在進程之間自由分配和管理,大小和位置都可以靈活設置。這種靈活性使得共享內存非常適合用于復雜的進程間通信場景。
-
同步問題:多個進程可以并發訪問和修改共享內存,因此需要使用信號量、互斥鎖等同步機制來協調對共享內存的訪問,避免數據競爭和不一致性問題。
-
內存分配:共享內存是由內核管理的,進程無法直接分配和釋放共享內存,必須通過系統調用如
shmget()
和shmctl()
來完成。
system V 的后兩種通信方式消息隊列 msg
和信號量 sem
都非常不常用了,本博客中只是簡單講解,不深入研究。
消息隊列 msg
消息隊列顧名思義,是一個被操作系統維護的隊列:
進程可以向消息隊列中寫入或者讀取消息,上圖中,每個黃色的小方塊就是一條消息,在消息的頭部會有一個區域用于標識,這條消息是哪一個進程發出的。
msgget
msgget
用于創建一個消息隊列,需要頭文件<sys/types.h>
、<sys/ipc.h>
和<sys/msg.h>
,函數原型如下:
int msgget(key_t key, int msgflg);
返回值:
msgget
返回一個整型,這個整型叫做msgid
,用于標識唯一的msg
。
參數:
-
key
:即標識唯一的system V
的key
值 -
msgflg
: 用于指定消息隊列的訪問權限和其他選項-
IPC_CREAT
: 如果指定的key
不存在,則創建一個新的消息隊列,如果已經存在,則直接獲得原先的消息隊列 -
IPC_EXCL
: 如果指定的key
已經存在,則創建失敗
-
IPC_EXCL
只能配合IPC_CREAT
一起使用,不能單獨使用IPC_EXCL
。
消息隊列也可以設置讀寫權限,直接將權限值的八進制按位或到第二個參數中即可。
你會發現,其實消息隊列和共享內存的使用方法幾乎是一摸一樣的!
msgctl
msgctl
用于控制共享內存的各種屬性,需要頭文件<sys/types.h>
、<sys/ipc.h>
和<sys/msg.h>
,函數原型如下:
int msgctl(int msgid, int cmd, struct msgid_ds* buf);
返回值:
- 成功時返回 0
- 失敗時返回 -1,并設置
errno
變量
參數如下:
shmid
:要操作的消息隊列的標識符
cmd
:要執行的操作,可以是以下值之一:
-IPC_STAT
:獲取消息隊列的狀態信息
-IPC_SET
:設置消息隊列的某些屬性
-IPC_RMID
:刪除消息隊列
buf
:指向msgid_ds
結構體的指針,用于存儲或設置消息隊列的屬性
msgid_ds
源碼如下:
struct msqid_ds {struct ipc_perm msg_perm; /* Ownership and permissions */time_t msg_stime; /* Time of last msgsnd(2) */time_t msg_rtime; /* Time of last msgrcv(2) */time_t msg_ctime; /* Time of last change */unsigned long __msg_cbytes; /* Current number of bytes inqueue (nonstandard) */msgqnum_t msg_qnum; /* Current number of messagesin queue */msglen_t msg_qbytes; /* Maximum number of bytesallowed in queue */pid_t msg_lspid; /* PID of last msgsnd(2) */pid_t msg_lrpid; /* PID of last msgrcv(2) */
};
這一塊和共享內存也幾乎是一摸一樣的,不過多解釋了。
剛剛兩個接口解決的是消息隊列的創建與釋放,接下來看看消息隊列如何向隊列中寫入與讀取。
msgsnd
msgsnd
用于向消息隊列寫入,需要頭文件<sys/types.h>
、<sys/ipc.h>
和<sys/msg.h>
,函數原型如下:
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
函數返回值:
- 成功時,返回 0
- 失敗時,返回 -1 并設置
errno
變量
參數:
msqid
:要發送消息的消息隊列的標識符msgp
:指向要發送的消息內容的指針msgsz
:要發送的消息內容的長度,以字節為單位msgflg
:控制msgsnd()
行為的標志位,一般來說傳入0
即可。常用的有:IPC_NOWAIT
: 如果消息隊列已滿,立即返回而不是阻塞MSG_NOERROR
: 如果消息內容太長,截斷后仍然發送
此處要著重講解一下第二個參數msgp
:
msgp
參數是一個指向要發送消息內容的指針。通常情況下,這個消息內容會被存儲在一個自定義的結構體中,這個結構體要滿足以下格式:
struct msgbuf {long mtype; /* message type, must be > 0 */char mtext[1]; /* message data */
};
這個結構體有兩個成員變量:
-
mtype
:這是一個long
類型的消息類型標識符。發送消息時,接收方可以根據消息類型來選擇性地接收消息。 -
mtext
:這是一個字符數組,用于存儲實際的消息內容,它的大小可以根據需要進行調整。
在使用 msgsnd()
函數發送消息時,msgp
參數就是指向這個 msg_buf
結構體的指針。
我們在發送消息時,只需要定義一個結構體,結構體的名稱可以是任意的,第一個成員必須是long
類型,第二個成員必須是char
的數組,數組長度任意。
第一個成員一般用于標識不同進程,比如在一個消息隊列中,A
進程發送的消息,mtype
設置為1
,B
進程發送的消息,mtype
設置為2
,這樣就可以根據這個成員來辨別一條消息是哪個進程發送的了。
msgrcv
msgrcv
用于從消息隊列提取,需要頭文件<sys/types.h>
、<sys/ipc.h>
和<sys/msg.h>
,函數原型如下:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
返回值:
- 成功時,返回實際接收的消息長度
- 失敗時,返回 -1 并設置
errno
變量
參數:
msqid
:要從中接收消息的消息隊列的標識符msgp
:指向用于存儲接收消息內容的緩沖區的指針msgsz
:緩沖區msgp
的大小,以字節為單位msgtyp
:指定要接收的消息類型。可以是以下幾種情況:- 如果
msgtyp > 0
,則接收第一個mtype
等于msgtyp
的消息。 - 如果
msgtyp == 0
,則接收消息隊列中第一個消息,不管mtype
是什么。
- 如果
msgflg
: 控制msgrcv()
行為的標志位,一般設為0
即可。常用的有:IPC_NOWAIT
:如果消息隊列為空,立即返回而不是阻塞MSG_NOERROR
:如果接收的消息內容太長,將其截斷后仍然返回
再簡單講解兩個消息隊列相關的系統指令:
ipcs -q
用于查看消息隊列:
ipcrm -q xxx
:用于刪除msgid
為xxx
的消息隊列
接下來講解system V
的最后一種通信方式信號量 sem
。
信號量 sem
信號量的基本原理,在于把一份資源拆分為很多份小資源:
多個進程可以分別訪問這個資源的一小部分:
但是不允許多個進程同時訪問一個小份資源!
而信號量的作用就是預定資源,信號量本質是一個計數器,用于記錄當前還有多少可以分配的資源。
信號量的申請過程如下:
- 進程訪問資源前,要先申請一個信號量,用于預定資源,一旦預定成功,信號量的數目減少一個,即當前剩余的資源少一個。從預定成功開始,這一份資源就不能被其他進程再訪問了。
- 進程申請到信號量后,就可以正常訪問這一份資源了
- 當進程使用完,于是釋放信號量,此時信號量數目加一,即當前剩余資源增加一個。
關于信號量,本博客不講解接口如何使用了,其使用方式比較麻煩,需要很大篇幅,而且信號量也不常用。
System V 管理機制
同為system V
系列,共享內存 shm
、消息隊列 msg
和信號量 sem
是有共性的,操作系統對這三者進行統一的管理。
Linux
中,描述三者的結構體如下:
其中共享內存 shm
被結構體shmid_kernel
管理,消息隊列 msg
被結構體msg_quque
管理,信號量 sem
被結構體sem_array
管理。不過以上結構體中,成員并不是完全的,我只截取了一小部分。
Linux
是如下對system V
進行管理的:
ipc_ids
結構體的entires
成員指向了結構體ipc_id_ary
,ipc_id_ary
的第二個成員是一個柔性數組,該數組是一個指針數組,指向了不同的system V
結構體。此時Linux
對system V
的管理就變成了對數組的增刪查改。
那么現在有一個問題就是:為什么一個數組可以指向三種不同類型的結構體變量?
我們再回到三個描述system V
的結構體:
它們三個結構體的第一個成員分別是shm_perm
、q_perm
和sem_perm
,這三者其實都是同一個結構體類型struct kern_ipc_perm
,而Linux
就是通過這個struct kern_ipc_perm
來同時管理三種結構體的。
ipc_id_ary
中,第二個成員數組的類型是struct kern_ipc_perm*
,也就是指向struct kern_ipc_perm
指針,這個struct kern_ipc_perm
存儲了三種system V
都具有的屬性。struct kern_ipc_perm
結構體同時也都是三個system V
的結構體的第一個成員,因此在訪問具體的某個結構體時,只需要進行一次指針的強制類型轉換即可。
Linux
就是通過這樣一種方式,把所有的system V
都統一地管理了起來。