程序間通訊

有志者事竟成1019發表於2024-06-10

程序間通訊

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;

相關文章