程序間通信 —— IPC
目錄:
- 1.管道
- 2.消息隊列
- 3.共享記憶體
- 4.信号量
為什麼要有程序間通信?
每個程序各自有不同的使用者位址空間,任何一個程序的全局變量在另一個程序中都看不到,是以程序之間要交換資料必須通過核心,在核心中開辟一塊緩沖區,程序 1 把資料從使用者空間拷到核心緩沖區,程序 2 再從核心緩沖區把資料讀走,核心提供的這種機制稱為程序間通信。
程序間通信的目的:
- 資料傳遞:一個程序将資料傳送到另一個程序
- 資源共享:多個程序共享同樣的資源
- 事件通知:一個程序需要向另外一個程序發送消息,通知他們發生了某事件
1.管道
匿名管道 由pipe函數建立:
#include <unistd.h>
int pipe(int fileded[]);
調用 pipe 函數時在核心中開辟一塊緩沖區(稱為管道)用于通信,它有一個讀端一個寫端,然後通過 filedes 參數傳出給使用者程式兩個檔案描述符,filedes[0]指向管道的讀端,filedes[1]指向管道的寫端(很好記,就像 0 是标準輸入 1是标準輸出一樣)。
是以管道在使用者程式看起來就像一個打開的檔案,通過read(filedes[0]);或者 write(filedes[1]);向這個檔案讀寫資料其實是在讀寫核心緩沖區。
pipe 函數調用成功傳回 0,調用失敗傳回-1。
管道的最典型的用途就是為兩個不同的程序(一個是父程序,一個是子程序)提供程序間通信的手段。首先由一個程序建立一個管道後調用fork建立一個子程序。接着,父程序關閉這個管道的讀端,子程序關閉這個管道的寫端。這就在父子之間提供了一個單向的資料流。
例:建立管道,進行父子間通信
使用管道需要注意以下4種特殊情況:
1、如果所有指向管道寫端的檔案描述符都關閉了,而仍然有程序從管道的讀端讀資料,那麼管道中剩餘的資料都被取走後,再次read時會傳回0,就像讀到檔案末尾一樣。
2、如果有指向管道寫端的檔案描述符沒關閉,而持有管道寫端的程序也不往管道
中寫資料,這時有程序從管道中讀資料時,管道中的資料被讀取之後,就會再次read阻塞,直到管道中有資料可讀才讀取資料并傳回。
3、如果所有指向管道讀端的檔案描述符都關閉了,這時有程序向管道的寫端write,那麼該程序會受到SIGPIPE信号。通常會導緻程序異常終止。
4、如果有指向管道讀端的檔案描述符沒關閉,而持有管道讀端的程序也沒有從管道中讀資料,這時有程序向管道的寫端寫資料,那麼在管道被寫滿時會再次write阻塞,直到管道中有空位置了才寫入資料并傳回。
管道的限制:
- 資料的流動方向是單向的
- 隻能在有共同祖先的程序之間通信
也有一種特殊的管道稱為FIFO,也叫命名管道,它也能用于無關聯程序間的通信。
FIFO檔案在磁盤上沒有資料塊,僅用來辨別核心中的一條通道,各程序可以打開這個檔案進行read/write,實際上是在讀寫核心通道。
例子:從鍵盤讀取資料,寫入管道,讀取管道,寫到螢幕
2.消息隊列
消息隊列是兩個程序之間傳遞資料的一種簡單有效的方式。每個資料塊都有一個特定的類型,接收方可以根據類型來有選擇的接收資料。而不一定像管道和命名管道那樣必須以先進先出的方式接收資料。
建立消息隊列:msgget()
int msgget(key_t key, int msgflg);
參數:
key:某個消息隊列的名字
msgflg: / IPC_CREAT |
傳回值:
成功傳回該消息隊列的辨別碼;失敗傳回-
key_t ftok(const char* pathname, int proj id)
id: 低位不能是
一般寫為:msgget(ftok(".", 'a'), IPC_CREAT|)自動生成key
往消息隊列裡發送資料:msgsnd()
struct msgbuf
{
long channel; 消息類型(通道号),必須>=
char mtext[];//寫上自己的消息内容
}
int msgsnd(int msqid, //msgget的傳回值
const void *msgp, //要發送的消息在哪裡
size_t msgsz, //消息的位元組數,不包括channel的大小
int magflg);//IPC_NOWAIT表示隊列滿,不等待,傳回EAGAIN錯誤//0
傳回值:
成功傳回,失敗傳回-
從消息隊列中取資料:msgrcv()
ssize_t msgrcv(int msqid, //id
void *msgp, //取出來的消息放在這裡
size_t msgsz, //裝消息的地方的大小,不包括類型
long msgtyp, //取哪個類型的消息
int msgflg);//0
傳回值:
成功傳回實際放到接收緩沖區裡去的字元個數,失敗傳回-
删除消息隊列:msgctl()
int magctl(int msqid,//由msgget函數傳回
int cmd,//IPC_RMID(删除消息隊列)
struct msqid_ds *buf); //0
傳回值: 成功傳回,失敗傳回-
1、系統中最多能建立多少個消息隊列?
cat /proc/sys/kernel/msgmni
2、一條消息最多能夠裝多少位元組?
cat /proc/sys/kernel/msgmax
/3、一個消息隊列中所有的消息的總位元組數是多少?
cat /proc/sys/kernel/msgmnb
ipcs :檢視消息隊列、共享記憶體、信号量全部的
檢視目前已經存在的消息隊列:pcs -q
删除消息隊列:ipcrm -q + key
消息隊列的缺點:
- 系統能建立的隊列個數有限
- 每個消息能裝的内容有限
- 一個消息隊列中所有消息的總位元組數也有限
- 每一次通路的時候需要調用系統的函數,意味着要從使用者空間切換到核心空間,發送完畢後,再從核心空間切換到使用者空間,開銷比較大。
例子:用消息隊列實作用戶端和伺服器之間的消息發送
每一個程序的程序id是唯一的,收消息時從id那收。最終将消息顯示到用戶端的顯示器上
伺服器:收1号通道的資料
發送到pid通道号
用戶端:往1号通道發(帶上自己的程序id)
3.共享記憶體—— 最快的程序間通信方式
将其映射到自己的虛拟位址空間,程序間的資料傳遞不用從使用者空間到核心。
System V 共享記憶體方式
//建立或打開共享記憶體
int shmget(key_t key, //共享記憶體的名字
size_t size,//要建立的共享記憶體段的大小,向上對齊到記憶體頁的大小
int shmflg) // 打開:0 建立:IPC_CREAT|0644
//将共享記憶體挂載到自己的位址空間
void* shmat(int shmid, //shmget得到的id
const void* shmaddr, //NULL(讓作業系統自己選擇)想讓作業系統挂載到這個位址空間
int shmflg); //0
傳回值:實際挂載到的虛拟位址的起始位置
//解除安裝掉共享記憶體,但不删除共享記憶體段
int shmdt(const void* shmaddr);
//删除共享記憶體段
int shmctl(int shmid,
int cmd, //IPC_RMID 删除共享記憶體
struct shmid_ds* buf); // NULL/0
檢視共享記憶體:ipcs -m
删除共享記憶體 : ipcm -m + key
例:将id和name映射到自己的虛拟位址空間,然後讀出來。
共享記憶體為程序間通信提供了一種效率很高的手段,但是這種手段并不提供程序間同步和互斥的功能,是以,共享記憶體作為廣義的程序間通信手段還必須有其它機制來配合。是以我們一般将信号量和共享記憶體來搭配使用。
4.信号量—— 解決同步互斥問題
同步:多個程序為了完成一件事情需要互相協同工作。
互斥:多個程序通路共享資源,共享資源具有排它性,一次僅允許一個人通路,由于各個程序都要使用共享資源。而且這個資源必須同時隻能有一個程序使用。
信号量的使用
互斥通路:臨界區的互斥通路控制
條件同步:線程間的事件等待
用信号量實作臨界區的互斥通路:
每類資源設定一個信号量,其初值為1
必須成對使用P()操作和V()操作
- P()操作保證互斥通路臨界資源
- V()操作在使用後釋放臨界資源
- PV操作不能次序錯誤。重複或遺漏
用信号量實作條件同步:
條件同步設定一個信号量,其初值為0
信号量值的含義
struct semphore{
int value;
struct PCB* queue;//等待隊列,有哪些程序在等待該資源
}
s.value > 表示資源的個數
s.value = 表示沒有資源可以使用,也沒有程序等待該資源
s.value < |s.value|個程序在等待該資源
P、V原語
//P原語 //V原語
P(s){ V(s){
s.value--; s.value++;
if(s.value < ){ if(s.value <= ){
将該程序置為等待狀态 喚醒等待隊列的程序
加入等待隊列queue 将程序的狀态改為就緒态,放到就緒隊列
} }
} }
//建立或打開信号量
int semget(key_t key,
int nsems, //在信号量集中信号量的個數
int semflg); //打開是0 建立是 IPC_CREAT|0644
//删除
int semctl(int semid, //segmet的傳回值
int semnum, //
int cmd, //IPC_RMID
...);
//設定信号量初值
union semun{
int val;
};
int semctl(int semid, //semget的傳回值
int semnum, // 對信号量集的第幾個信号量操作
int cmd, //SETVAL
su); //設定信号量初值
//檢視信号量的值
int semctl(int semid, //semget的傳回值
int semnum, // 對信号量集的第幾個信号量操作
int cmd, //GETVAL
);
傳回值:目前信号量的值
//PV操作
struct sembuf{
unsigned short sem_num;//信号量集中的第幾個信号量,下标
short sem_op; //P -1 , V +1
short sem_flg //0
};
int semop(int semid,
struct sembuf* buf,
unsigned nsops);
檢視信号量:ipcs -s
删除信号量:ipcrm -s + key
例子:哲學家進餐