最好的參考資料:
1.師從互聯網。
2.UNP v2 Posix IPC的相關章節3、6、11、14。
3.Linux man 命令。
緬懷Stevens 大師~~~在《深入理解Linux內核架構》中作者說過:POSIX標準已經用更爲現代的方式,引入了類似的結構(即Posix IPC)。我不討論Posix的相關機制了,因爲大多數應用程序仍然在使用System V IPC 。
第一條:System V IPC 的名字
IPC鍵: 一個進程內可能同時使用多個同一種類 System V IPC,比方說信號量。System V IPC使用key_t類型的IPC 鍵值(就是XXXget函數的第一個參數)來區分同種但不相同的IPC 資源。Linux 2.6.35在sys/types.h中把它定義爲int類型。有兩種獲得IPC鍵值的方法,通過ftok函數可生成這樣的鍵值,但可能重複,不唯一,但重複概率極小。另外一個是直接使用IPC_PREVATE宏——唯一的鍵值。IPC關鍵字本質上類似與文件系統的文件路徑名;
IPC標誌符:通過XXXget函數將IPC鍵轉換成IPC標誌符,System V IPC使用IPC標誌符作爲他們的名字。凡知道這個標誌符(即一個魔數)的程序,都能訪問IPC 標誌符對應的IPC資源。IPC 標誌符又類似於文件描述符。
#include <sys/ipc.h>
key_t ftok ( const char * pathname, int proj_id);//在Linux中ftok函數使用proj_id的低8位、st_dev的低8位以及st_ino的低16位生成這個鍵值。
第二條:XXXget函數的參數
這裏重點說一下shmget、semget、msgget函數的最後一個flag參數,他的值是IPC_CREAT、IPC_EXCL、S_IRUGO(無此宏,它代表用戶、組和其他)、S_IWUGO(同前)的或運算組合。當只指定S_IRUSR ,ipcs命令顯示的perms是502。測試777也可以達到。
注:S_IXUGO不需要使用。
第三條:System V IPC 的實現
內核給爲3種 System V IPC分別維護一個(only one)類型如下結構:
struct ipc_ids{
int in_use;//分配的某種(3種之一)IPC資源數
int max_id;//使用的最大索引位置
unsigned short seq;//下一個分配位置的序號
unsigned short seq_max;//序號的最大值溢出歸0
struct semaphore sem;//保護ipc_ids自身
struct ipc_id_ary nullentry;//如果IPC資源無法初始話,則entries指針指向位數據結構 (一般不使用)
struct ipc_id_ary * entries;//所有同一類型(比如信號量)的IPC資源的入口哦。
...
}
entries有兩個成員:p和size。p指向類型爲struct kern_ipc_perm的結構(對應一個IPC資源)的數組。size數組的大小。
struct kern_ipc_perm {
key_t key; /* Key supplied to msgget *///這個key就是用來區分同一類型IPC資源。
unsigned int uid; /* Effective UID of owner */
unsigned int gid; /* Effective GID of owner */
unsigned int cuid; /* Effective UID of creator */
unsigned int cgid; /* Effective GID of creator */
unsigned short mode; //權限位
...
};
這樣內核就能不斷縮小目標範圍,找到每個IPC資源了。其實,p指向的這個struct kern_ipc_perm 實際上是對應3種不同System IPC 的一個描述結構的一個成員而已。這3個描述結構分別是:semid_ds,shmid_ds,msgid_ds.可以在頭文件中找到他們,他們對應的內核中的結構爲sem_array,msg_queue,shmid_kernel,這纔是某個IPC資源的真正描述體。
另外,在ULK上有這樣的描述:所有的System V IPC 函數都必須通過適當的Linux系統調用實現的。實際上,在80X86架構下,只有一個名爲ipc()的IPC系統調用。當進程調用一個IPC函數,比如說msgget(),該函數實際上調用C庫中的一個封裝函數,該封裝函數又通過以msgget()的所有參數加上一個適當的子命令代碼(本例是MSGGET)作爲參數來調用ipc()系統調用,sys_ipc()服務例程檢查子命令代碼,並調用內核函數實現所請求的服務。
若還不能明白,那就多看看《深入LINUX 內核架構》和《ULK》相關章節^_^。
第四條:System V 信號量
System V 的信號量現在指的是信號量的集合,當然可以指定含有一個信號量的信號量集合,就像Posix 信號量。
對於每個信號量集——IPC資源,內核維護一個如下的信息結構,它定義在bits/sem.h中
struct semid_ds {
struct ipc_perm sem_perm; /* Ownership and permissions */
time_t sem_otime; /* Last semop time */
time_t sem_ctime; /* Last change time */
unsigned short sem_nsems; /* No. of semaphores in set */
};
對於每個信號量集中的某個信號量,內核維護一個大概形式如下的結構,這個結構是不透明的內部數據結構,無法在頭文件中找到:
struct sem{
unsigned short semval; /* semaphore value *///該信號量的當前值
unsigned short semzcnt; /* # waiting for zero *///等待該信號量值變爲0的線程數
unsigned short semncnt; /* # waiting for increase *///等待該信號量變爲大於當前值的某個值的線程數
pid_t sempid; /* process that did last op */
unsigned short semadj;//在UNPv2中指出:semadj不必存在!他用於SEM_UNDO標誌。
};
#include<sys/sem.h>
int semget (key_t key, int nsems, int semflg) ;
semget函數的基本原理:進程甲創建一個和key相關信號量集,參數nsems指定相關信號量集含有的信號量個數,一旦信號量集創建了所含有的信號量的個數就更改不了了,進程甲、乙、丙、丁第二次通過相同的key調用semget時nsems可隨意指定,那就設爲0吧。kernel2.6.35驗證內核會初始化這個信號量集的每個信號量爲0,man手冊和Setevns大師 警告:這不可移植!!
union semun{ /* The user should define a union like the following to use it for argumentsfor `semctl'. */
int val; <= value for SETVAL
struct semid_ds *buf; <= buffer for IPC_STAT & IPC_SET
unsigned short int *array; <= array for GETALL & SETALL
struct seminfo *__buf; <= buffer for IPC_INFO
};/*Previous versions of this file used to define this union but this is incorrect. One can test the macro _SEM_SEMUN_UNDEFINED to see whether one must define the union or not. */
int semctl (int semid, int semnum, int cmd, .../*union senum arg */);
semctl函數的基本原理:linux並未提供union semun結構的定義,這是一個模板。該結構是做爲semctl的最後一個參數,配合cmd使用。該函數的每個命令的具體使用參看UNPv2(2010 人郵新版)的第十一章:231頁以及man手冊。
struct sembuf{//成員的順序再不同實現上不同,所以不能靜態初始化該結構。
unsigned short int sem_num; /* semaphore number */
short int sem_op; /* semaphore operation */
short int sem_flg; /* operation flag */
};
int semop (int semid, struct sembuf * sops, size_t nsops) ;
semop函數的基本原理:參數sops指向一個sembuf結構的數組,參數nsops是這個數組的元素的個數。其中sembuf.sem_num成員用來標記操作信號量集中的第幾個信號量(從0開始)。重點說下sembuf.sem_op,其值和上文中的 struct sem結構個相關的。他的值分爲三種情況:
1.大於0:sem_op值直接加到sem.seval上。進程返回。
2.等於零:如果sem.seval等於0.進程返回。如果sem.seval不等於0進程被阻塞直到:第一種情況:sem.seval變爲0,進程返回。第二種情況:被中斷進程返回,errno置爲EINTR。
3.小於0:若sem.seval大於等於sem_op的絕對值,則sem.seval減掉sem_op的絕對值。若sem.seval小於sem_op的絕對值,則sem.seval減掉sem_op的絕對值。進程被阻塞直到:第一種情況:sem.seval大於等於sem_op的絕對值,進程返回並且sem.seval減掉sem_op的絕對值。第二種情況:被中斷進程返回,errno置爲EINTR。
最後sembuf.sem_flg:可設爲0、IPC_NOWAIT、SEM_UNDO的或運算操作。當指定IPC_NOWAIT時,以上所有阻塞都不會發生,取而代之的是,errno被置爲EAGAIN,進程返回。當指定SEM_UNDO時sembuf.sem_op操作的影響只對本次進程操作有影響,當調用進程結束設置了SEM_UNDO的那幾個信號量值將會回覆到進程開始之前。正如楊繼張老師所言:該信號量的值就像變得根本沒有運行過該進程一樣,這句是復舊(undo)的本意。
呼~~~這複雜性絕非Posix信號量之輩,所能比擬的~~~^_^
關於新創建的信號量集的初始化:System V信號量在設計中,創建和初始化信號量集需要兩次函數調用是個致命的缺陷——Stevens.
在man semget命令中有如下聲明:
The values of the semaphores in a newly created set are indeterminate. (POSIX.1-2001 is explicit on this point.) Although Linux, like many other implementations, initializes the semaphore values to 0, a porta‐ble application cannot rely on this: it should explicitly initialize the semaphores to the desired values.
新創建的信號量集中的信號們的值是不確定的。(Posix明確的指出過)雖然Linux和其他的實現上初始化他們爲0,爲了可移植性我們應該明確的初始化他們爲想要的值。
The semaphores in a set are not initialized by semget(). In order to initialize the semaphores, semctl(2) must be used to perform a SETVAL or a SETALL operation on the semaphore set. (Where multiple peers do not know who will be the first to initialize the set, checking for a nonzero sem_otime in the associated data structure retrieved by a semctl(2) IPC_STAT operation can be used to avoid races.)
semget沒有初始化信號集中的信號的話,應該用semctl的SETVAL或SETALL的操作初始化信號集。(多個進程可以通過檢測sem_otime(semctl的IPC_STAT操作可得到)的值(在semget創建新的信號量集時,sem_ids中sem_otime成員被置爲0,只有在semop操作後被置爲當前值),來避免競爭發生)。這意味着創建信號量的那個進程必須初始化他的值,而且必須在任何其他進程可以使用該信號量之前調用semop。
第五條:System V 消息隊列
內核對每個消息隊列,維護一個定義在<bits/msq.h>中的結構
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 in queue (nonstandard) */
msgqnum_t msg_qnum; /* Current number of messages in queue */
msglen_t msg_qbytes; /* Maximum number of bytes allowed in queue */
pid_t msg_lspid; /* PID of last msgsnd(2) */
pid_t msg_lrpid; /* PID of last msgrcv(2) */
};
我第一次看的時候很疑惑:怎麼沒有消息有關的數據結構呢?注意到他的備註/* Structure of record for one message inside the kernel.
The type `struct msg' is opaque. */哦,原來是不透明的。
#include <sys/msg.h>
int msgget (key_t key, int msgflg);
#ifdef __USE_GNU//編譯時加上-D_GNU_SOURCE即可
struct msgbuf {/* Template for struct to be used as argument for `msgsnd' and `msgrcv'. */
long int mtype; /* type of received/sent message */
char mtext[1]; /* text of the message */
};
#endif
ssize_t msgrcv (int msqid, void * msgp, size_t msgsz,long int msgtyp, int msgflg);//msgflag可爲0或者IPC_NOWAIT或者MSG_NOERROR
int msgsnd (int msqid, const void * msgp, size_t msgsz, int msgflg);//msgflag可爲0或者IPC_NOWAIT
int msgctl (int msqid, int cmd, struct msqid_ds *buf) ;
這裏說下msgrcv和msgsnd函數:msgp參數他的類型是struct msgbuf 。linux提供了一個如上的模板,我們可以自行添加其他數據信息,但long int的mtype成員必須要放在最前面。這個成員,非常重要,他是標註這個消息的類型的。消息的收發基本上是圍繞這個類型在轉。
另外,msgsz參數有點特別:他的值指定的是msgbuf結構數據部分的大小即其值應爲sizeof(*msgp)-sizeof(long int)。
關於msgrcv的type參數:
1.當type爲0:返回消息隊列的第一個消息。消息隊列是FIFO鏈表。
2.當type大於0:返回type類型最早的消息。沒有的話將被阻塞,若指定IPC_NOWAIT標誌,errno置爲ENOMSG。特別的若指定MSG_EXCEPT標誌,無論type存在與否,只返回第一個消息。
3.當type小於0:返回小於等於type絕對值的最小消息。
4.相關細節參看man msgrcv。
P.S.關於eclipse的宏提示:我剛纔測試sys/msg.h文件下struct msgbuf 結構宏的是__USE_GNU沒有定義(eclipse 顯示 灰色的)於是我加上了-D_GNU_SOURCE還是灰色,敲代碼時也沒有結構體成員提示,百思不得其解。但是編譯了一下,通過!程序正常運行!你們懂的~~:-)我想是因爲-D_GNU_SOURCE是在我們編譯時才起作用的。平時,eclipse不能感知到他已經定義了~~~~故,__USE_GNU處於未定義狀態下。
第六條:System V 共享內存區
對於每個共享內存區,內核都會維護這樣的結構,在bits/shm.h中可以找到他:
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 of this structure*/
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 */
...
};
最有用的IPC機制就是共享內存,這種機制允許兩個或多個進程通過把公共數據結構放入一個共享內存區 來訪問。如果進程要訪問這種存放在共享內存區的數據結構,就必須在自己的地址空間中增加一個新的內存區,它將映射與這個共享內存區相關的也框。這樣的也框可以很容易地由內核通過請求調頁進行處理——ULK做了這樣的描述。
#include <sys/shm.h>
int shmget (key_t key, size_t size, int shmflg);
int shmctl (int shmid, int cmd, struct shmid_ds *buf);
void *shmat (int shmid, const void * shmaddr, int shmflg);
int shmdt (const void *shmaddr);
這幾個函數的原理是:shmget創建或打開一個共享內存空間對象,shmat函數把這個共享內存空間映射到自己的進程空間。shmdt斷開這種映射。但共享內存區是隨內核持續的,他上面的數據不會隨shmdt而刪除。徹底刪除該共享內存區要到他的引用計數器變爲0時。shmdt發現指定的共享內存區的引用計數爲0,就順便刪除他。shmctl的IPC_RMID用於減少鏈接計數器即刪除共享內存空間對象(原理同文件操作的unlink)。之後通過直接讀寫shmat返回的地址讀寫就可以了~~~和Posix 共享內存空間原理一樣!!!
最後
UNIX網絡編程第二卷:進程間通信,作者:W.Richard Stevens,譯者:楊繼張。
深入理解Linux 內核 第三版 作者:Daniel P.Bovet &Marco Cesati, 譯者:陳莉君&張瓊聲&張宏偉。
深入linux內核架構 作者:WolfgangIMauerer ,譯者:郭旭。
本文引用了互聯網上衆大神的bolg和帖子,非常感謝。
特別感謝廣大的開源社區。
若有侵害到您的利益,及時相告,我將在一個工作日內刪除。