(七)進程通信(本機IPC)

目錄

1. 無名管道     

1.1 無名管道的通信原理 

1.2 無名管道的API

1.2.1 函數原型

1.2.2 無名管道特點

1.2.3 父子進程通信

1.3 無名管道有兩個缺點

1.4 什麼時候合適使用無名管道呢

2. 有名管道

2.1 爲什麼叫“有名管道”

2.2 有名管道特點

2.3 有名管道的使用步驟

2.4 有名管道API

System V IPC

(1)什麼是System V IPC

(2)System V IPC的特點

(3)System V IPC標識符

3 System V IPC 之 消息隊列    

3.1 消息隊列的原理

(1)消息隊列的本質

(2)消息是如何存放在消息隊列中的呢?

(3)收發數據的過程

(4)使用消息隊列實現網狀交叉通信

3.2 消息隊列的使用步驟

3.3    消息隊列的函數

(1)msgget函數

(2)msgsnd

(3)msgrcv函數        

(4)進程結束時,自動刪除消息隊列        

3.4 什麼時候合適使用消息隊列

3.5 消息隊列的缺點

4. System V IPC 之共享內存

4.1 共享內存介紹

4.2 共享內存原理

4.3 共享內存的使用步驟

4.4 共享內存的函數

4.4.1 shmget函數

4.4.2 shmat

4.4.3 shmdt函數 

4.4.4 shmctl函數

5. system V IPC  之  信號量(或信號燈)semaphore

5.1 信號量的作用

5.2 資源保護操作的種類

5.3 使用信號量實現互斥    

5.3.1 需要互斥實現“資源保護”的例子

5.3.2 進程信號量實現互斥的原理     

5.3.3 信號量相關的API

5.3 使用信號量實現同步

5.3.1 什麼是同步

5.3.2 同步舉例1

5.3.3 同步例子2


Linux提供的“進程通信”方式有哪些

 

1. 無名管道     

1.1 無名管道的通信原理 

(1)到底什麼是管道

      內核的代碼也是運行在物理內存上的,內核創建一個“管道”,其實就是在內核自己所在的物理內存空間中開闢出一段緩存空間

      

 

(2)如何操作無名管道

       以文件的方式來讀寫管道,以文件方式來操作時

        1)有讀寫用的文件描述符
        2)讀寫時會用write、read等文件Io函數
 

(3)爲什麼叫無名管道

      既然可以通過“文件描述符”來操作管道,那麼它就是一個文件(管道文件),但是無名管道文件比較特殊,它沒有文件名,正是因爲沒有文件名,所有被稱爲無名管道。

1.2 無名管道的API

1.2.1 函數原型

#include <unistd.h>

int pipe(int pipefd[2]);

  

 

1.2.2 無名管道特點

(1)無名管道只能用於親緣進程之間通信,爲什麼?

  由於沒有文件名,因此進程沒辦法使用open打開管道文件,從而得到文件描述符,

  所以只有一種辦法,那就是父進程先調用pipe創建出管道,並得到讀寫管道的文件描述符。

  然後再fork出子進程,讓子進程通過繼承父進程打開的文件描述符,父子進程就能操作同一個管道, 從而實現通信。

(2)讀管道時,如果沒有數據的話,讀操作會休眠(阻塞)

  

1.2.3 父子進程通信

(1)父子進程單向通信

1)實現步驟 

  • (a)父進程在fork之前先調用pipe創建無名管道,並獲取讀、寫文件描述符
  • (b)fork創建出子進程,子進程繼承無名管道讀、寫文件描述符
  • (c)父子進程使用各自管道的讀寫文件描述符進行讀寫操作,即可實現通信

2)SIGPIPE信號

    寫管道時,如果管道的讀端被close了話,向管道“寫”數據的進程會被內核發送一個SIGPIPE信號,

(2)父子進程雙向通信

1)單個無名管道無法實現雙向通信,爲什麼?

     因爲使用單個無名管道來實現雙向通信時,自己發送給對方的數據,就被自己給搶讀到。

2)如何實現無名管來實現雙向通信

    使用兩個無名管道,每個管道負責一個方向的通信。

    

    

1.3 無名管道有兩個缺點

(1)無法用於非親緣進程之間
(2)無法實現多進程之間的網狀通信

1.4 什麼時候合適使用無名管道呢

                         

2. 有名管道

2.1 爲什麼叫“有名管道”

當我們調用相應的API創建好“有名管道”後,會在相應的路徑下面看到一個叫某某名字的 “有名管道文件”。

不管是有名管道,還是無名管道,它們的本質其實都是一樣的,它們都是內核所開闢的一段緩存空間。

2.2 有名管道特點

2.2.1 能夠用於非親緣進程之間的通信(有文件名)

2.2.2 讀管道時,如果管道沒有數據的話,讀操作同樣會阻塞(休眠)

2.2.3 當進程寫一個所有讀端都被關閉了的管道時,進程會被內核返回SIGPIPE信號

2.3 有名管道的使用步驟

(1)進程調用mkfifo創建有名管道

(2)open打開有名管道

(3)read/write讀寫管道進行通信


對於通信的兩個進程來說,創建管道時,只需要一個人創建,另一個直接使用即可。

爲了保證管道一定被創建,最好是兩個進程都包含創建管道的代碼,誰先運行就誰先創建,

2.4 有名管道API

#include <sys/types.h>
#include <sys/stat.h>
			
int mkfifo(const char *pathname, mode_t mode);

  

通信

同樣的,使用一個“有名管道”是無法實現雙向通信的,因爲也涉及到搶數據的問題。

所以雙向通信時需要兩個管道。

什麼時候使用有名管

(1)實現網狀通信 (實現起來很困難)

(2)什麼時候合適使用有名管道

  •       當兩個進程需要通信時,不管是親緣的還是非親緣的,我們都可以使用有名管道來通信。
  •       至於親緣進程,你也可以選擇前面講的無名管道來通信。
     

System V IPC

(1)什麼是System V IPC

無名管道和有名管道,都是UNIX系統早期提供的比較原始的一種進程間通信(IPC)方式,早到Unix系統設計之初就有了

後來Unix系統升級到第5版本時,又提供了三種新的IPC通信方式: 消息隊列 信號量 共享內存

System V就是系統第5版本的意思,後來的Linux也繼承了unix的這三個通信方式

(2)System V IPC的特點

管道的本質就是一段緩存,不過Linux OS內核是以文件的形式來管理的,

System V IPC與管道有所不同,它完全使用了不同的實現機制,與文件沒任何的關係

使用System V IPC時,不存在親緣進程一說

(3)System V IPC標識符

System V IPC不再以文件的形式存在,因此沒有文件描述符這個東西,但是它有類似的“標識符”。

3 System V IPC 之 消息隊列    

3.1 消息隊列的原理

(1)消息隊列的本質

消息隊列的本質就是由內核創建的用於存放消息的鏈表,由於是存放消息的,所以我們就把這個鏈表 稱爲了消息隊列

通信的進程通過共享操作同一個消息隊列,就能實現進程間通信。

(2)消息是如何存放在消息隊列中的呢?

消息隊列這個鏈表有很多的節點,鏈表上的每一個節點就是一個消息。

從圖中可以看出,每個消息由兩部分組成,分別是消息編號(消息類型)和消息正文。

1)消息編號:識別消息用
2)消息正文:真正的信息內容

(3)收發數據的過程

1)發送消息

(a)進程先封裝一個消息包

     這個消息包其實就是如下類型的一個結構體變量,封包時將消息編號和消息正文寫到結構體的成員中。

struct msgbuf
{
			long mtype;         /* 放消息編號,必須> 0 */
			char mtext[msgsz];  /* 消息內容(消息正文) */
};	

(b)調用相應的API發送消息

       調用API時通過“消息隊列的標識符”找到對應的消息隊列,然後將消息包發送給消息隊列,消息包(存放消息的結構體變量)會被作爲一個鏈表節點插入鏈表。

 

2)接收消息

  調用API接收消息時,必須傳遞兩個重要的信息,
(a)消息隊列標識符    
(b)你要接收消息的編號

  有了這兩個信息,API就可以找到對應的消息隊列,然後從消息隊列中取出你所要編號的消息

“消息隊列”有點像信息公告牌,發送信息的人把某編號的消息掛到公告牌上,

 接收消息的人自己到公告牌上 去取對應編號的消息,如此,發送者和接受者之間就實現了通信。

(4)使用消息隊列實現網狀交叉通信

管道很難實現網狀交叉通信,但是使用消息隊列確非常容易實現。

3.2 消息隊列的使用步驟

(1)使用msgget函數創建新的消息隊列、或者獲取已存在的某個消息隊列,並返回唯一標識消息隊列的標識符(msqID),後續收發消息就是使用這個標識符來實現的。

 

(2)收發消息

                · 發送消息:使用msgsnd函數,利用消息隊列標識符發送某編號的消息
                · 接收消息:使用msgrcv函數,利用消息隊列標識符接收某編號的消息

 

(3)使用msgctl函數,利用消息隊列標識符刪除消息隊列

3.3    消息隊列的函數

(1)msgget函數

   1)函數原型

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);

(a)功能:利用key值創建、或者獲取一個消息隊列

(b)返回值

(c)參數

(d)多個進程是如何共享到同一個消息隊列的    

2)通信

(a)如何驗證消息隊列是否被創建成功?

        

(b)system v ipc的缺點

     進程結束時,system v ipc不會自動刪除,進程結束後,使用ipcs依然能夠查看到。

     如何刪除?

       

(2)msgsnd

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

(3)msgrcv函數        

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

  

(4)進程結束時,自動刪除消息隊列        

         我們需要調用msgctl函數來實現。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

(a)功能

(b)參數

struct msqid_ds 
{
		struct ipc_perm  msg_perm; /* 消息隊列的讀寫權限和所有者 */
		time_t  msg_stime;    /* 最後一次向隊列發送消息的時間*/
		time_t  msg_rtime;    /* 最後一次從消息隊列接收消息的時間 */
		time_t  msg_ctime;    /* 消息隊列屬性最後一次被修改的時間 */
		unsigned  long __msg_cbytes; /* 隊列中當前所有消息總的字節數 */
		msgqnum_t  msg_qnum;     /* 隊列中當前消息的條數*/
		msglen_t msg_qbytes;  /* 隊列中允許的最大的總的字節數 */
		pid_t  msg_lspid;     /* 最後一次向隊列發送消息的進程PID */
		pid_t  msg_lrpid;     /* 最後一次從隊列接受消息的進程PID */
};
		
struct ipc_perm 
{
	key_t          __key;       /* Key supplied to msgget(2):消息隊列的key值 */
	uid_t          uid;         /* UID of owner :當前這一刻正在使用消息隊列的用戶 */
	gid_t          gid;         /* GID of owner :正在使用的用戶所在用戶組 */
	uid_t          cuid;        /* UID of creator :創建消息隊列的用戶 */
	gid_t          cgid;        /* GID of creator :創建消息隊列的用戶所在用戶組*/
	unsigned short mode;        /* Permissions:讀寫權限(比如0664) */
	unsigned short __seq;       /* Sequence number :序列號,保障消息隊列ID不被立即
																	重複使用 */
};

 

3.4 什麼時候合適使用消息隊列

當你的程序必須涉及到多進程網狀交叉通信時,消息隊列是上上之選。

3.5 消息隊列的缺點

與管道一樣,不能實現大規模數據的通信,大規模數據的通信,必須使用後面講的“共享內存”來實現。

4. System V IPC 之共享內存

4.1 共享內存介紹

共享內存的API與消息隊列的API非常相似,應該System V IPC的API都是差不多的

共享內存就是OS在物理內存中開闢一大段緩存空間,不過與管道、消息隊列調用read、write、msgsnd、msgrcv等API來讀寫所不同的是,使用共享內存通信時,進程是直接使用地址來共享讀寫的。

直接使用地址來讀寫緩存時,效率會更高,但是如果是調用API來讀寫的話,中間必須經過重重的OS函數調用之後,直到調用到最後一個函數時,該函數纔會通過地址去讀寫共享的緩存,中間的調用過程會降低效率。

4.2 共享內存原理

以兩個進程使用共享內存來通信爲例,實現的方法就是:

(1)調用API,讓OS在物理內存上開闢出一大段緩存空間。
(2)讓各自進程空間與開闢出的緩存空間建立映射關係
          

4.3 共享內存的使用步驟

4.4 共享內存的函數

4.4.1 shmget函數

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);

  

4.4.2 shmat

#include <sys/types.h>
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);

  

4.4.3 shmdt函數 

#include <sys/types.h>
#include <sys/shm.h>

int shmdt(const void *shmaddr);	

  

4.4.4 shmctl函數

#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

    

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:最後一次修改屬性信息的時間 */
	pid_t shm_cpid;    /* PID of creator:創建進程的PID */
	pid_t shm_lpid;    /* PID of last shmat(2)/shmdt(2) :當前正在使用進程的PID*/
	shmatt_t shm_nattch;  /* No. of current attaches:映射數量,
												 * 標記有多少個進程空間映射到了共享內存上
												 * 每增加一個映射就+1,每取消一個映射就-1 */ 
	...
};

struct ipc_perm,這個結構體我們在講消息隊列時已經講過,這裏不再重複講。
struct ipc_perm 
{
	 key_t          __key;    /* Key supplied to shmget(2) */
	 uid_t          uid;      /* UID of owner */
	 gid_t          gid;      /* GID of owner */
	 uid_t          cuid;     /* UID of creator */
	 gid_t          cgid;     /* GID of creator */
	 unsigned short mode;     /* Permissions + SHM_DEST andSHM_LOCKED flags */
	 unsigned short __seq;    /* Sequence number */
};

 

5. system V IPC  之  信號量(或信號燈)semaphore

5.1 信號量的作用

簡潔一點,信號量用於“資源的保護“。

5.2 資源保護操作的種類

(1)互斥

            對於互斥操作來說,多進程共享操作時,多個進程間不關心誰先操作、誰後操作的先後順序問題

            它們只關心一件事,那就是我在操作時別人不能操作。

(2)同步

           所以所謂同步就是,多個共享操作時,進程必須要有統一操作的步調,按照一定的順序來操作。    

(3)實現同步、互斥,其實就是加鎖

           信號量就是一個加鎖機制,通過加鎖來實現同步和互斥。

(4)疑問:信號量既然是一種加鎖機制,爲什麼進程信號量會被歸到了進程間通信裏面呢?

           資源保護時,某個進程的操作沒有完全完成之前,別人是不能操作的,

           那麼進程間必須相互知道對方的操作狀態,必須會涉及到通信過程。

           所以信號量實現資源保護的本質就是,通過通信讓各個進程瞭解到操作狀態,然後查看自己能不能操作。

5.3 使用信號量實現互斥    

進程信號量既能實現進程的互斥,也能實現進程的同步,不過有些“資源保護機制”就只能實現互斥

5.3.1 需要互斥實現“資源保護”的例子

                  

因爲在切換進程時,往往只寫了一個“hello”或者“hhhhh”後,就被切換到另一個進程,該進程會繼續寫數據,如此就對上一個進程所寫數據產生了隔斷。

5.3.2 進程信號量實現互斥的原理     

(1)什麼是進程信號量

簡單理解的話,信號量其實是OS創建的一個共享變量,進程在進行操作之前,會先檢查這個變量的值,這變量的值就是一個標記,通過這個標記就可以知道可不可以操作,以實現互斥。

(2)多值信號量和二值信號量

1)二值信號量

      同步和互斥時使用的都是二值信號量。

      二值信號量的值就兩個,0和1,0表示不可以操作,1表示可以操作。
      通過對變量進行0、1標記,就可以防止出現相互干擾情況。

                   

2)多值信號量

      信號量的最大值>1,比如爲3的話,信號量允許的值爲0、1、2、3。

(3)信號量集合

號量其實是一個OS創建的,供相關進程共享的int變量,只不過我們在調用相關API創建信號量時,我們創建的都是一個信號量集合,所謂集合就是可能會包含好多個信號量。

   用於互斥時,集合中只包含一個信號量。

   用於同步時,集合中會包含多個信號量,至於多少個,需要看情況。

(4)信號量的使用步驟

1)進程調用semget函數創建新的信號量集合,或者獲取已有的信號量集合。

2)調用semctl函數給集合中的每個信號量設置初始值

3)調用semop函數,對集合中的信號量進行pv操作

4)調用semctl刪除信號量集合

 

什麼是pv操作?

 pv操作其實說白了就是加鎖、解鎖操作

 (a)P操作(加鎖):對信號量的值進行-1,如果信號量的值爲0,p操作就會阻塞

 (b)V操作(解鎖):對信號量的值進行+1,V操作不存在阻塞的問題

 總之通過pv操作(加鎖、解鎖),就能夠實現互斥,以防止出現干擾。

5.3.3 信號量相關的API

(1)semget函數

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);
	
//sem就是semaphore的縮寫。

  

(2)semctl函數

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, ...);

(a)功能

根據cmd的要求對集合中的各個信號量進行控制,...表示它是一個變參函數,如果第四個參數用不到的話,可以省略不寫。

(b)返回值

調用成功返回非-1值,失敗則返回-1,errno被設置。

(c)參數說明

       

從以上可以看出,第四個參數對應內容是變着的,爲了應對這種變化就用到了一個聯合體。

union semun {
	 int              val;    //存放用於初始化信號量的值
	 struct semid_ds *buf;    //存放struct semid_ds結構體變量的地址
	 unsigned short  *array;  /* 不做要求 */
	 struct seminfo  *__buf;  /* 不做要求 */
};

這個聯合體類型並沒有被定義在信號量相關的系統頭文件中,我們使用這個聯合體時,我們需要自己定義這個類型,至於聯合體類型名可以自己定,不過一般都是直接沿用semun這個名字

疑問:這個聯合怎麼用?

      

(3)semop函數

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, unsigned nsops);

- 結構體成員

struct sembuf
{
	unsigned short sem_num;  
	short          sem_op;
	short          sem_flg;  
}

這個結構體不需要我們自己定義,因爲在semop的頭文件中已經定義了。

  

5.3 使用信號量實現同步

5.3.1 什麼是同步

讓多個進程按照固定的步調做事,同步本身就是互斥的。

5.3.2 同步舉例1

通過同步讓三個親緣進程按照順序打印出111111、222222、333333。

如何實現同步 (多米諾骨牌

  

可以看出,有多少個進程需要同步,我們在集合中就需要創建對應數量的信號量。

5.3.3 同步例子2

使用信號量來解決共享內存的同步問題

  

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章