程序間通訊
1.什麼是通訊
- 資料傳輸:一個程序需要將自己的資料傳輸給另一個程序
- 資源共享:多個程序同時共享一個資源
- 程序事件:一個程序向一組(或一個)程序通知某一事件,如:子程序結束要通知父程序來回收資源
- 程序控制:有些程序需要知道另一個程序的狀態,控制攔截另一個程序陷入異常等,如:gdb除錯
2.為什麼要有通訊
多個程序之間需要協同來完成某項任務:
eg:
cat log.txt | gerp "hehe"
具備通訊的的前提條件:
-
因為程序具有獨立性,所以不能在兩個程序各自的區域來進行通訊,就好比警察和黑幫的臥底,需要用紙條來通訊,所以不能在各自的地盤,所以OS需要給需要通訊的程序提供一個
記憶體空間
-
並且兩個程序之間都可以看到一個公共的資源(空間)
作業系統的很多模組都可以提供公共空間
3.程序間通訊分類
管道通訊
- 匿名管道
- 命名管道
System V IPC
- System V 訊息佇列
- System V 共享記憶體
- System V 訊號量
POSIX IPC
- 訊息佇列
- 共享記憶體
- 訊號量
- 互斥量
- 條件變數
- 讀寫鎖
4.管道
對於檔案系統來說,被開啟的檔案在檔案描述符表裡有對應的fd (檔案描述符)
當一個程序fork()
後,子程序會複製父程序的大部分資源
,其中就包括檔案struct files_struct
,當然,**file* fd_array
**也複製過去,即檔案描述符表
所以子程序能指向和父程序同一個被開啟的檔案所以子程序建立後,開啟的檔案和父程序指向的是同一個檔案
此時就初步具備了通訊的條件,這個空間是由檔案系統提供的,檔案在磁碟裡
但是,檔案系統需要訪問外設(即磁碟),所以訪問速度相對較慢
4.1管道檔案的定義和實現
有一種記憶體級的檔案,他沒有對應的磁碟檔案,但是有自己的file結構體
,這個實現是作業系統本身用聯合體實現的,這個細節實現是OS去操作的。
對於每個struct file
都有
1.file的操作辦法 2. 核心緩衝區
-
對於一個struct file,可以不指向磁碟中的檔案,因為這個實現是作業系統來實現的
-
所以作業系統在記憶體中建立一個
不指向磁碟中任何檔案的struct file
,即只有一個結構體,這個結構體裡當然也具備了1.file的操作辦法 2. 核心級緩衝區
,所以程序之間可以透過這個匿名檔案的緩衝區來進行通訊 -
當父程序開啟一個記憶體級檔案的時候,
fork
子程序,子程序也具備了指向該記憶體級檔案,所以父子程序可以用這個記憶體級檔案來進行通訊,不需要訪問磁碟就可以完成,所以速度就會大大提升,這個檔案沒有名字,所以叫做匿名管道 -
如下圖
4.2 管道的建立過程
管道在生活中就是用來單向傳輸的,一頭輸入一頭只輸出
管道需要讀和寫,所以一個父程序需要同時具備讀和寫許可權的屬性才能
fork
子程序,要不然子程序沒辦法進行讀或者寫但是又不能父子程序兩個都寫,或者都讀,只能一方寫,一方讀
所以建立過程如下:
- 1.父程序以讀和寫方式分別開啟一個記憶體級檔案
- 2.父程序fork()子程序,這樣一來,子程序也都具備了對該記憶體級檔案讀和寫的方式
- 關掉一個讀的和一個寫的
fd
,如果沒關,萬一沒關可能會被不小心訪問到
⭐管道是一個父程序分別以讀和寫方式開啟一個記憶體級檔案,並透過fork建立一個子程序,各自再關閉對應的讀寫端,進而形成一條通訊通道,這樣的通道是基於檔案的,所以叫做:管道
匿名管道 :目前只能用來進行父子程序間通訊
4.3 pipe建立管道
#include <unistd.h>
//功能:建立一無名管道
//原型
int pipe(int fd[2]);
//輸出型引數
fd:檔案描述符陣列,其中fd[0]表示讀端, fd[1]表示寫端
返回值:成功返回0,失敗返回錯誤程式碼
聯想記憶法 : 1. fd[0]:0比作嘴巴,讀東西,讀端
2. fd[1]: 1比作筆,寫東西,寫端
在fork子程序後,父子程序是讀還是寫,那麼就關閉不用的一個,fork()之後各自關掉不用的描述符
4.4 匿名管道的讀寫特徵
-
讀慢,寫快
寫的速度>讀的速度,管道也是有最大容量的
所以當管道被寫滿時,將不在繼續寫,直至讀端讀走資料有可以寫的空間,寫端才繼續寫
-
讀快,寫慢
讀的速度>寫的速度
因為讀速度大於寫速度,所以當讀端讀完管道內的內容時,此時已經沒有內容可讀了,那麼程序將阻塞在read函式這裡,等待寫端寫,直至管道內有資料可繼續讀
-
寫端關閉,讀端不關閉
當管道寫端關閉時,讀端讀完管道內的資料時,如果再次去讀沒有資料的管道會返回0,相當於讀到了EOF
-
讀端關閉,寫端不關閉
讀關閉,作業系統將給程序法訊號,終止寫端,因為不需要讀的話,就是浪費系統資源,作業系統會強制終止寫端
4.5 命名管道
1.mkfifo函式建立命名管道
#include<sys/types.h>
#include<sys/stat.h>
int mkfifo(const char *pathname,mkde_t mode);
- 引數
pathname : 要建立命名管道的目錄和檔名
-
返回值
成功返回 0 ,失敗返回 -1
-
命名管道的檔案型別是
p
2.命名管道的原理
在程序中開啟命名管道檔案的方法跟普通檔案一樣
- 檔案從磁碟中載入到記憶體,然後建立
struct file
,然後將其地址放到task_struct
中的files_struct
中的檔案描述符表中 - 但是不一樣的是,我們只與struct file中的核心緩衝區互動,讀寫的內容都儲存在struct file中的緩衝區中,自始至終沒有將內容寫到磁碟,磁碟檔案相當於一個載體,只是為了給我們提供一個
struct file
- 如下圖流程
3.兩個無血緣程序間的通訊
讓兩個程序看到同一份命名管道,然後分別選擇一個讀和寫
注意細節:當只有一端開啟命名管道時,eg:只開啟讀端,另一端還沒就緒,此時開啟的一端會阻塞自己,等待另一端就緒
5.共享記憶體
共享記憶體區是最快的
IPC
形式,一旦這個shm與程序地址空間對映,那麼無需透過核心進行通訊,直接透過一個記憶體進行通訊,但是由於共享記憶體只能在本地進行多個程序間通訊,所以就慢慢的被淘汰掉了了
5.2 共享記憶體的原理
- 首先要拿到一個
key
,生成一個獨一無二的key
,其他程序(人)進來需要key
,建立共享記憶體時候要傳這個key
就好比開個房間 - 然後讓程序的程序地址空間與這個共享記憶體區域建立對映關係,這樣程序就拿到了讀寫共享記憶體的功能
- 此時程序之間就具備了通訊的基本能力:看到同一份公共資源,就可以程序程序之間通訊了
如上圖,透過頁表對映到各自的程序地址空間,從而實現兩個程序可以實現程序間通訊
5.3 共享記憶體的實現
- ①建立
key*
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
//pathname放一個指定的路徑即可,proj_id指定一個數值即可,但是記得另一個程序對應得這兩個引數也要一樣
- ②建立共享記憶體
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
//key即上邊得Key , size為開闢共享記憶體得大小單位是位元組
引數:shmflg 是一個用標誌位代表的一個引數有兩個IPC_CREAT
,IPC_EXCL
IPC_CREAT : 如果沒有那麼建立共享記憶體,如果已經有了,那麼返回共享記憶體的
shmid
IPC_EXCL : 該宏必須和IPC_CREAT一起使用,否則沒有意義。當shmget取IPC_CREAT|IPC_EXCL時,表示如果發現訊號集已經存在,則返回-1,錯誤碼為EEXIST。
建立時必須加上建立共享記憶體的許可權碼0600
//1.建立時一般用下邊這個
int shmid=shmget(key,4096,IPC_EXCL|IPC_CREAT|0600);
//2.獲取時用下邊的這個
int _shmid=shmget(key,4096,IPC_CREAT);
返回值 :成功返回共享記憶體的shmid,失敗返回-1
- ③共享記憶體與程序建立聯絡 shmat 函式
void *shmat(int shmid, const void *shmaddr, int shmflg);
引數: hm_addr指定共享記憶體連線到當前程序中的地址位置,通常為空(nullptr),表示讓系統來選擇共享記憶體的地址。
shm_flg是一組標誌位,通常為0
- **④斷開與共享記憶體連結 shmdt()函式 **
int shmdt(const void *shmaddr);
引數shmaddr是shmat()函式返回的地址指標,呼叫成功時返回0,失敗時返回-1.
- ⑤控制共享記憶體 : shmctl()函式
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- IPC_STAT:把shmid_ds結構中的資料設定為共享記憶體的當前關聯值,即用共享記憶體的當前關聯值覆蓋shmid_ds的值。
- IPC_SET:如果程序有足夠的許可權,就把共享記憶體的當前關聯值設定為shmid_ds結構中給出的值
- IPC_RMID:刪除共享記憶體段
- 返回值 : 失敗返回 -1
6.訊號量
- 訊號量:訊號量本質是一個計數器,用來表示公共資源中可用的數量
- 公共資源 :可用被多個程序同時訪問的資源,叫做公共資源
為什麼要讓不同的程序看到同一份公共資源呢---->為了不同程序之間進行通訊,協同工作等------>那麼就讓不同的程序看到同一份資源------->提出產生公共資源的方法----->過程中遇到問題------>資料不一致問題,比如還沒有寫完另一邊就開始讀取了
-
臨界資源 : 被保護起來的公共資源被稱作臨界資源(臨界資源佔少數,因為大部分資源都是各自程序自身的,只有程序要通訊並且防止被打擾才會進行保護,所以臨界資源在這個條件下佔少數)
-
臨界資源(記憶體,檔案,網路等)是要被使用的,如何被程序使用呢?程序存在對這部分資源的使用方法程式碼,由這部分程式碼來實現,那麼這部分程式碼區域被稱作臨界區 ,其他區域則被稱作非臨界區
-
如何保護:互斥和同步
-
原子操作 : 對於一件事情 , 要麼就一開始就不做,要麼做了就做完
-
對於共享資源的使用:1.作為一個整體 2.拆分成若干個部分使用
所有的程序在訪問公共資源的前提下,需要先申請訊號量------>所以必須程序都能看到訊號量,那麼訊號量也是一個公共資源----->所以訊號量也要保證自己的安全------->所以訊號量進行++或者- -操作是原子性的
那麼, 對於訊號量獲取資源進行訊號量--
的操作被稱為P操作
對於訊號量回收資源進行訊號量++
的操作被稱為V操作
7. IPC資源的組織方式
系統的IPC資源常見的有:訊息佇列,共享記憶體,訊號量等
這些資源都包括了兩個結構體:1.自身的結構體例如struct shm_ds或者sem_ds
2.struct ipc_prem
這些資源是由OS統一管理的,OS會建立一個陣列 : ipc_prem *prems[ ];
,由這個陣列統一管理ipc資源
- 在建立對應得ipc資源時,會先建立一個自身型別得結構體,比如shm就會建立
shm_ds
- 對於一個結構體,結構體的起始地址==結構體第一個元素的地址,所以對於每個ipc資源的自身結構體,有以下內容,其中第一個元素為建立了一個ipc_prem物件,然後將其地址放到OS的prems陣列中,以方便統一管理,這樣,OS就會知道管理諸多ipc資源中,所要處理的當前ipc資源型別是什麼了
- 例如 : prems[0]=&semid_ds.sem_prem;