剛開始接觸IPC機制時,感覺這個知識點真的時晦澀難懂,因此自己將自己對IPC機制的理解做下總結。
Linux中的IPC機制:信號量, 共享內存, 消息隊列。注意這裡的IPC機制的通信只能在一台主機上的多個進程線程之間進行通信,而跨主機之間的通信用socket, poll, epoll機制。
一:信號量計數信號量,其值在0和某個限制值之間,該值最大為32767, 這裡信號量的值就是可用資源數。
System V對計數信號量又加了一級復雜度,System V 信號量是由一個或多個計數信號量構成的集合。
信號量是什麼:信號量的本質是一種數據操作鎖,它本身不具有數據交換的功能,而是通過控制其他的通信資源(文件,外部設備)來實現進程間通信,它本身只是一種外部資源的標識。信號量在此過程中負責數據操作的互斥、同步等功能。
信號量主要是對臨界資源進行保護的機制,通過p,v操作使信號量結構體內部的成員val數值的變化來標示有沒有可用資源,通常情況下信號量接合共享內存一起使用。信號量集在系統中的數據結構,在/usr/include/linux/sem.h中可以找到。
內核維護的信號量集的結構體
struct semid_ds {
struct ipc_perm sem_perm; //這個ipc_perm結構含有當前這個信號量的訪問權限
__kernel_time_t sem_otime;
__kernel_time_t sem_ctime;
struct sem *sem_base; //sem_base是一個指向信號量結構數組的指針
struct sem_queue *sem_pending;
struct sem_queue **sem_pending_last;
struct sem_undo *undo;
unsigned short sem_nsems;
};
內核管理的信號量集如圖所示
系統調用函數semop()對這個結構體成員數據進行操作,來實現對val值的改變。
struct sembuf {
unsigned short sem_num;
short sem_op;
short sem_flg;
};
信號量結構體
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
void *__pad;
};
通常情況下,通過對信號量進行p,v操作使得信號量的數據成員val的變化,來標示資源是否可用。val值表示幾個進程可以訪問這個可用資源。例如val值設為2, 現在有個進程想訪問這個資源,則進行p操作是的val值減一,如果令個進程訪問這個臨界資源,在進行p操作,此時val值變為0,表示沒有可用資源可以訪問,這時如果有個進程訪問這個資源,那麼進程會被阻塞。直到使用資源的進程放棄繼續訪問資源。如果進程放棄繼續訪問資源,則進行v操作,使得val值加一。
二:共享內存通常大量數據之間的通信用共享內存,小數據用消息隊列。利用共享內存機制進行進程間的通信效率比較快,因為進程可以直接讀取內存,而不需要任何數據的拷貝,例如管道,消息隊列等通信方式,需要在內核空間和用戶空間進行四次的數據拷貝,而共享內存則拷貝兩次數據即可, 一次從輸入文件到共享內存區,另一次從共享內存到輸出文件。
如圖所示,進程間需要共享的數據放入內核的共享內存區,進程可以把共享內存映射到自己進程的地址空間去,所以進程可以直接讀取內存,不需要任何數據的拷貝。
再來看看這個圖,共享內存機制只需要進行兩次的數據拷貝,一次從輸入文件到共享內存區,另一次從共享內存到輸出文件。
共享內存機制沒有同步機制,因此可能會導致多個進程對共享內存中的數據進行修改,這樣會時數據發生錯誤。因此利用信號量來解決這個問題。
為了解決這個問題設置兩個信號量,sem1, sem2. sem1信號量是對服務器給客戶端發消息進行同步,sem2是對客戶端對服務器發消息進行同步。服務器給共享內存區寫數據,寫完後設置sem1信號量進行V操作使val變為1,此時表示有可用資源等待進程訪問。此時客戶端先對sem1信號量進行P操作,使得val變為0,然後客戶端讀共享內存區的數據。這樣做是為了防止多個進程對資源訪問。同理操作sem2即可下面的程序實現服務器,客戶端之間相互進行收發消息
ser.cpp
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/shm.h>
#include <sys/sem.h>
using namespace std;
union semun{
int val;
struct semid_ds *buf;
ushort *array;
struct seminfo *__buf;
void *__pad;
};
#define IPCMODE IPC_CREAT|IPC_EXCL|0666
int main(int argc, char *argv[])
{
key_t key_id;
key_id = ftok(argv[1], ID);
if(key_id == -1)
{
printf("ftok error.\n");
exit(1);
}
printf("key_id = 0x%x\n", key_id);
key_t shm_id;
shm_id = shmget(key_id, 1024, IPC_CREAT|IPC_EXCL|0666);
if(shm_id == -1)
{
printf("shmget error.\n");
exit(1);
}
char *addr;
addr = (char *)shmat(shm_id,NULL, 0);
if(addr == (void*)-1)
{
printf("shmat error.\n");
shmctl(shm_id, IPC_RMID, NULL);
exit(1);
}
//////////////////////////////////////////////////////////
key_t sem_key;
sem_key = ftok(argv[1],ID1);
if(sem_key == -1)
{
printf("sem ftok error.\n");
shmctl(shm_id, IPC_RMID, NULL);
exit(1);
}
key_t sem_id = semget(sem_key, 2, IPC_CREAT|IPC_EXCL|0666);
if(sem_id == -1)
{
printf("semget error.\n");
shmctl(shm_id, IPC_RMID, NULL);
exit(1);
}
union semun init;
init.val = 0;
semctl(sem_id, 0, SETVAL, init);
semctl(sem_id, 1, SETVAL, init);
struct sembuf p = {0,-1,0};
struct sembuf v = {1,1,0};
//////////////////////////////////////////////////////////
while(1)
{
//write
printf("Ser:>");
scanf("%s",addr);
if(strncmp(addr,"quit",4) == 0)
{
shmdt(addr);
shmctl(shm_id, IPC_RMID, NULL);
semctl(sem_id,0,IPC_RMID);
semctl(sem_id,1,IPC_RMID);
break;
}
semop(sem_id, &v, 1); //服務器端寫數據,然後對sem1進行V操作//read
semop(sem_id, &p, 1); //服務器讀數據之前,先對sem0進行P操作printf("Cli:>%s\n",addr);
}
return 0;
}
cli.cpp
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/shm.h>
#include <sys/sem.h>
using namespace std;
union semun{
int val;
struct semid_ds *buf;
ushort *array;
struct seminfo *__buf;
void *__pad;
};
#define IPCMODE IPC_CREAT|IPC_EXCL|0666
int main(int argc, char *argv[])
{
key_t key_id = ftok(argv[1],ID);
if(key_id == -1)
{
printf("ftk error.\n");
exit(1);
}
key_t shm_id = shmget(key_id,0,0);
if(shm_id == -1)
{
printf("shmget error.\n");
exit(1);
}
char *addr = (char *)shmat(shm_id,NULL,0);
if(addr == (void *)-1)
{
printf("shmat error.\n");
exit(1);
}
////////////////////////////////////////////////////////
key_t sem_key;
sem_key = ftok(argv[1],ID1);
if(sem_key == -1)
{
printf("sem ftok error.\n");
exit(1);
}
key_t sem_id = semget(sem_key, 0, 0);
if(sem_id == -1)
{
printf("semget error.\n");
exit(1);
}
struct sembuf p = {1,-1,0};
struct sembuf v = {0,1,0};
////////////////////////////////////////////////////////
while(1)
{
semop(sem_id, &p, 1); //客戶端進行讀數據之前對sem1信號量進行P操作printf("Ser:>%s\n",addr);
printf("Cli:>");
scanf("%s",addr);
if(strcmp(addr,"quit") == 0)
{
shmdt(addr);
break;
}
semop(sem_id, &v, 1); //客戶端進行寫數據之後對sem0信號量進行V操作}
return 0;
}
三:消息隊列當初發明消息隊列的這種IPC機制的時候是為了克服如果客戶端和服務器之間相互進行數據交互的時候(雙向數據流)這種情況就要用到兩個管道進行通信,如果用消息隊列則可以解決這種雙向數據流交互的問題。
內核中關於消息隊列的幾個結構體
在/usr/include/linux/msg.h文件中
struct msqid_ds {
struct ipc_perm msg_perm;
struct msg *msg_first; /* first message on queue,unused */
struct msg *msg_last; /* last message in queue,unused */
__kernel_time_t msg_stime; /* last msgsnd time */
__kernel_time_t msg_rtime; /* last msgrcv time */
__kernel_time_t msg_ctime; /* last change time */
unsigned long msg_lcbytes; /* Reuse junk fields for 32 bit */
unsigned long msg_lqbytes; /* ditto */
unsigned short msg_cbytes; /* current number of bytes on queue */
unsigned short msg_qnum; /* number of messages in queue */
unsigned short msg_qbytes; /* max number of bytes on queue */
__kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */
__kernel_ipc_pid_t msg_lrpid; /* last receive pid */
};
內核中消息隊列的結構如圖
msg_ids{}結構用來方便內核管理消息隊列,由此可見消息隊列是以鏈表的型式進行連接的。下圖中應該還得畫出在msg_ids{}結構中有msg_first和msg_last兩個msg類型的指針分別指向消息隊列的頭和尾。
消息隊列的特點:消息隊列傳送的消息不僅是字符類型,也可以是其他類型,消息隊列發送的消息是個結構體
msgbuf{}這個結構體包含消息類型,消息數據。struct msgbuf {
long mtype; /* type of message */
char mtext[1]; /* message text */
};
由於消息隊列發送的消息都帶有消息類型,所以允許多個進程共同的使用同一個消息隊列進行數據的交互。
注意:消息隊列的讀取不一定按照先進先出的方式。例如,服務器發送的消息設type尾1,客戶端發送的消息的type設尾2,那麼如果顯現一個消息隊列裡面的消息情況為1,1,1,2,2,1,1這個隊列。那麼客戶端要讀取服務器發送的消息則先出隊列的是最右邊的第一個1,客戶端繼續讀取服務器發來的消息,則出隊列的是右邊第二個1,接下來出隊列的是右邊第五個1,,由此可以看出消息的讀取不一定按照先進先出的方式。