Linux/Unix系統IPC是各種進程間通信方式的統稱,但是其中極少能在所有Linux/Unix系統實現中進行移植。隨着POSIX和Open Group(X/Open)標準化的推進呵護影響的擴大,情況雖已得到改善,但差別仍然存在。一般來說,Linux/Unix常見的進程間通信方式有:管道、消息隊列、信號、信號量、共享內存、套接字等。博主將在《進程間通信方式總結》系列博文中和大家一起探討學習進程間通信的方式,並對其進行總結,讓我們共同度過這段學習的美好時光。這裏我們就以其中一種方式共享內存展開探討,共享內存是IPC中最常用也是最快的一種進程間通信的方式。顧名思義,共享內存就是允許兩個不相關的進程訪問同一個邏輯內存。共享內存是在兩個正在運行的進程之間共享和傳遞數據的一種非常有效的方式。不同進程之間共享的內存通常安排爲同一段物理內存。進程可以將同一段共享內存連接到它們自己的地址空間中,所有進程都可以訪問共享內存中的地址,就好像它們是由用C語言函數malloc分配的內存一樣。而如果某個進程向共享內存寫入數據,所做的改動將立即影響到可以訪問同一段共享內存的任何其他進程。特此提醒,共享內存內有提供同步訪問機制,這個需要使用着自己去實現。可能有的小夥伴不是很理解,共享內存並未提供同步機制,也就是說,在第一個進程結束對共享內存的寫操作之前,並無同步機制可以阻止第二個進程開始對它進行讀取操作,這樣讀取的就是髒數據,如果此時再來一個進程對該共享內存進行寫操作,你想想共享內存將會亂成一鍋粥,通信將成爲災難。所以我們通常需要用其他的機制來同步對共享內存的訪問,例如前面說到的信號量。有關信號量的更多內容,可以查閱我的另一篇文章:《進程間通信方式總結——信號量》。在那裏,博主將共享內存和信號量結合在一起使用,通過信號量使共享內存擁有同步機制。當然,你可以對共享內存進行封裝,實現一個擁有同步機制的共享內存API,這個就由你自己去實現咯。好了,廢話不多說,下面就讓我們一起進行共享內存的學習吧。
函數介紹
shmget
shmget根據給定的key值創建指定大小的內存空間
函數原型:int shmget(key_t key, size_t size, int shmflg);
key:與信號量的semget函數一樣,程序需要提供一個參數key(非0整數),它有效地爲共享內存段命名,shmget函數成功時返回一個與key相關的共享內存標識符(非負整數),用於後續的共享內存函數。調用失敗返回-1。
size:共享內存的容量,以字節爲單位
shmflg:shmflg是權限標誌,它的作用與open函數的mode參數一樣,如果要在key標識的共享內存不存在時,創建它的話,可以與IPC_CREAT做或操作。共享內存的權限標誌與文件的讀寫權限一樣,舉例來說,0644,它表示允許一個進程創建的共享內存被內存創建者所擁有的進程對該共享內存有讀取和寫入數據的權利,同時其他用戶創建的進程只能讀取共享內存。shmflg爲0,取共享內存標識符,若不存在則函數會報錯;IPC_CREAT:當shmflg&IPC_CREAT爲真時,不管是否已存在該塊共享內存,則都返回該共享內存的ID,若不存在則創建共享內存;如果存在這樣的共享內存,返回此共享內存的標識符;IPC_CREAT|IPC_EXCL:如果內核中不存在鍵值與key相等的共享內存,則新建一個消息隊列;如果存在這樣的共享內存則報錯。
返回值:成功返回共享內存標識符,失敗返回-1,錯誤原因存於error中。
錯誤碼:
EINVAL:參數size小於SHMMIN或大於SHMMAX
EEXIST:預建立key所指的共享內存,但已經存在
EIDRM:參數key所指的共享內存已經刪除
ENOSPC:超過了系統允許建立的共享內存的最大值(SHMALL)
ENOENT:參數key所指的共享內存不存在,而參數shmflg未設IPC_CREAT位
EACCES:沒有權限
ENOMEM:核心內存不足
shmat
連接共享內存標識符爲shmid的共享內存,連接成功後把共享內存區對象映射到調用進程的地址空間,隨後可像本地空間一樣訪問。每當調用shmat函數成功返回,共享內存連接數便加1。
函數原型:void *shmat(int shm_id, const void *shm_addr, int shmflg);
shm_id:shmget函數返回的共享內存標識。
shm_addr:指定共享內存出現在進程內存地址的什麼位置,直接指定爲NULL讓內核自己決定一個合適的地址位置
shmflg:一組標誌位,通常爲0。SHM_RDONLY只讀, 0可讀寫;(SHM_COPY、SHM_MAP、SHM_RND不在此說明)
返回值:成功返回共享內存地址,失敗返回-1,錯誤碼存於error中。
錯誤碼:
EACCES:無權限以指定方式連接共享內存
EINVAL:無效的參數shmid或shmaddr
ENOMEM:核心內存不足
shmdt
shmdt函數用於將共享內存從當前進程中分離。注意,將共享內存分離並不是刪除它,只是使該共享內存對當前進程不再可用,刪除共享內存需要shmctl來完成。Shmdt只是將共享內存連接數減1,並使進程中的共享內存連接地址無效,不會將共享內存狀態置爲刪除狀態。
函數原型:int shmdt(const void *shm_addr);
shm_addr:shmat函數返回的地址指針(連接的共享內存的起始地址),調用成功時返回0,失敗時返回-1。
返回值:成功返回0,失敗返回-1,錯誤碼存於error中。
錯誤碼:EINVAL:無效的參數shmaddr
shmctl
函數原型:int shmctl(int shm_id, int command, struct shmid_ds *buf);
shm_id:shmget函數返回的共享內存標識符。
command:採取的操作,它可以取下面的三個值
IPC_STAT:得到共享內存的狀態,把共享內存的shmid_ds結構複製到buf中
IPC_SET:改變共享內存的狀態,把buf所指的shmid_ds結構中的uid、gid、mode複製到共享內存的shmid_ds結構內
IPC_RMID:刪除共享內存段。
buf:共享內存狀態結構體指針,它指向共享內存模式和訪問權限的結構。
返回值:成功返回0,失敗返回-1,錯誤碼存於error中。
錯誤碼:
EACCESS:參數cmd爲IPC_STAT,確無權限讀取該共享內存
EFAULT:參數buf指向無效的內存地址
EIDRM:標識符爲shmid的共享內存已被刪除
EINVAL:無效的參數cmd或shmid
EPERM:參數cmd爲IPC_SET或IPC_RMID,卻無足夠的權限執行
struct shmid_ds {
struct ipc_perm shm_perm;
int shm_segsz;
time_t shm_atime;
time_t shm_dtime;
time_t shm_ctime;
unsigned short shm_cpid;
unsigned short shm_lpid;
short shm_nattch;
unsigned short shm_npages;
unsigned long *shm_pages;
structvm_area_struct *attaches;
};
程序實例
shm_write.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/shm.h>
#include <unistd.h>
#include <errno.h>
//定義共享內存標識符
int shmid = -1;
//信號處理函數
void handler(int s)
{
fprintf(stdout, "exit\n");
//將共享內存置爲刪除狀態,且引用計數減1
shmctl(shmid, IPC_RMID, 0);
exit(EXIT_SUCCESS);
}
int main(int argc, char **argv)
{
if (argc < 2) fprintf(stderr, "usage:%s key\n", argv[0]),exit(EXIT_FAILURE);
//安裝信號
signal(SIGINT, handler);
char * addr = NULL;
//創建共享內存,如果失敗打印並退出
if (-1 == (shmid = shmget(atoi(argv[1]), 256, IPC_CREAT | 0666)))
{
perror("shmget");
exit(EXIT_FAILURE);
}
//將共享內存映射到當前進程
//參數1:shmget函數的返回值,共享內存的標識符
//參數2:指定共享內存映射到進程中的地址(NULL:有內核分配)
//參數3:一組標誌位,通常爲0
if (-1 == (int)(addr = (char *)shmat(shmid, NULL, 0)))
{
fprintf(stderr, "addr == NULL");
exit(EXIT_FAILURE);
}
fprintf(stdout, "share:");
//從標準輸入讀入數據
fgets(addr, 256, stdin);
//fork創建子進程(讀時共享,寫時拷貝)
pid_t pid = fork();
//子進程
if (0 == pid)
{
//子進程繼承父進程已連接的共享內存地址
//輸出共享內存中內容
fprintf(stdout, "share: %s\n", addr);
exit(EXIT_SUCCESS);
}
//創建子進程(由於vfork創建的子進程完全共享父進程空間,相當於創建了線程)
pid = vfork();
if (0 == pid)
{
//子進程調用exec函數替換進程空間
//子進程與已連接的共享內存自動脫離
execlp("ls", "ls", "-l", NULL);
exit(EXIT_FAILURE);
}
//讓進程掛起(此時不佔用CPU資源)
for (; ;)
{
pause();
}
return0;
}
shm_read.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/shm.h>
#include <errno.h>
//定義共享內存標識符
int shmid = -1;
//信號處理函數
void handler(int s)
{
fprintf(stdout, "exit\n");
//將共享內存置爲刪除狀態,並使引用計數減1
shmctl(shmid, IPC_RMID, 0);
exit(EXIT_SUCCESS);
}
int main(int argc, char **argv)
{
if (argc < 2) fprintf(stderr, "usage:%s key\n", argv[0]),exit(EXIT_FAILURE);
//安裝信號
signal(SIGINT, handler);
char * addr = NULL;
//打開共享內存
//由於寫進程已經創建了共享內存空間,讀進程只需連接即可(後面兩個參數傳0即可)
if ((shmid = shmget(atoi(argv[1]), 0, 0)) == -1)
{
strerror(errno);
exit(EXIT_FAILURE);
}
//將共享內存映射到當前進程
if (-1 == (int)(addr = (char *)shmat(shmid, NULL, 0)))
{
fprintf(stderr, "addr == NULL");
exit(EXIT_FAILURE);
}
//讀取共享內存數據
fprintf(stdout, "share:%s", addr);
//將當前進程掛起
for (; ;)
{
pause();
}
return 0;
}
程序運行結果:
我們可以看到使用共享內存進行進程間的通信真的是非常方便,而且函數的接口也簡單,數據的共享還使進程間的數據不用傳送,而是直接訪問內存,也加快了程序的效率。同時,它也不像匿名管道那樣要求通信的進程有一定的父子關係。需要注意的是,共享內存沒有提供同步的機制,這使得我們在使用共享內存進行進程間通信時,往往要藉助其他的手段來進行進程間的同步工作,如信號量等。
此外還有幾個需要注意的地方,fork和vfork後,子進程繼承父進程已連接的共享內存地址,fork會使共享內存連接數nattch加1,而vfork則不會,這也體現了fork與vfork之間的區別。exec後該子進程與已連接的共享內存地址自動脫離(detach),即連接計數自動減1。進程結束後,已連接的共享內存地址會自動脫離(detach),即引用計數減1(如果你已經調用過shmctl(shmid,IPC_RMID,0)刪除共享內存,或shmdt卸載共享內存,那麼引用計數是不會重複減1的,這個你大可放心)。當然,僅僅是脫離,並沒有刪除共享內存。shmdt函數調用並不刪除所指定的共享內存區,而只是將先前用shmat函數連接(attach)好的共享內存脫離(detach)當前的進程。也就是說,shmat將共享內存連接到進程地址空間,並將共享內存連接計數nattch加1,而shmdt是進程中連接的工作內存地址無效,並使共享內存連接計數nattch減1。shmctl(shmid,IPC_RMID,0)刪除共享內存,將共享內存標記爲刪除,並使共享內存連接計數nattch減1。只有在引用計數爲0且標記爲刪除狀態時才真正刪除該共享內存。程序退出後,引用計數nattch減1,如果沒有將共享內存置爲刪除狀態,即使共享內存連接計數nattch爲0,也不會被刪除。此時你必須使用ipcrm命令來刪除該共享內存,釋放共享內存空間。因此,在創建共享內存實現多個進程間通信,至少需要一個進程將共享內存置爲刪除狀態。
關於共享內存的學習我們就到此結束了,相信大家都有所收穫,希望小夥伴們都已經理解並掌握了共享內存的常用方法。如果你覺得對進程間通信的方式不勝瞭解,還有些許疑惑,請關注博主《進程間通信方式總結》系列博文,相信你在那裏能找到答案。