Linux 進程間通信:管道、共享內存、消息隊列、信號量


進程間通信

https://blog.csdn.net/qq_35423154/article/details/105294963
在之前的一篇博客中講過,因爲每個進程都通過自己的頁表構建物理地址和虛擬地址的映射關係,使每個進程都擁有自己的虛擬地址空間,並通過這個獨立的虛擬地址空間來對物理內存進行操作,所有的進程都只能訪問自己的虛擬地址,而不能直接訪問物理內存,所以多個進程無法訪問同一塊區域,無法實現通信。

因爲這種獨立性,進程之間無法直接進行通信,操作系統就爲了解決這種問題,提出了多重適用於不同情境下的通信方式。

ps:這裏主要講的是systemV的,POSIX的後面再寫

數據傳輸:管道、消息隊列
數據共享:共享內存
進程控制:信號量


管道

原理:管道的本質其實就是內核中的一塊緩衝區,多個進程通過訪問同一個緩衝區就可以實現進程間的通信
在這裏插入圖片描述
管道分爲兩種:匿名管道、命名管道

匿名管道

匿名 管道是內核中的一塊緩衝區,因爲沒有具體的文件描述符,所以匿名管道只能適用於具有親緣關係的進程間通信父進程在創建管道的時候操作系統會返回管道的文件描述符,然後生成子進程時子進程會通過拷貝父進程的pcb來獲取到這個管道的描述符,所以他們可以通過這個文件描述符來訪問同一個管道,來實現進程間的通信。而不具備親緣關係的進程則無法通過這個文件描述符來訪問同一個管道。

接口

#include <unistd.h>
功能:創建一無名管道
原型
int pipe(int fd[2]);
參數
fd:文件描述符數組,其中fd[0]表示讀端, fd[1]表示寫端
返回值:成功返回0,失敗返回-1

一開始父進程創建管道
在這裏插入圖片描述
父進程fork創建子進程
在這裏插入圖片描述
關閉多餘描述符
在這裏插入圖片描述
就這樣,子進程通過寫入端fd[1]向管道寫入數據,父進程通過讀入端fd[0]從管道讀取數據,來實現進程間的通信。

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

int main()
{
        int pipefd[2];
        pipe(pipefd);
        
        int pid = fork();
        if(pid == 0)
        {
                close(pipefd[0]);
                while(1)
                {
                        write(pipefd[1], "hello world", 12);
                        sleep(3);
                }
                close(pipefd[1]);
                exit(0);
        }
        else if(pid > 0)
        {
                close(pipefd[1]);

                char buff[1024];
                while(1)
                {
                        read(pipefd[0], buff, 12);
                        printf("%s\n", buff);
                }
                close(pipefd[0]);
        }
        return 0;

這是一個簡單的管道

在這裏插入圖片描述
運行後每三秒寫端會寫入數據,然後讀端立即讀入數據。

因爲父子進程究竟是誰先執行這一點我們無法知道,假設如果子進程還沒寫入,父進程卻已經開始讀了,這時候應該是會讀不到東西的,但是這種情況並沒有發生,這裏就牽扯到了管道的讀寫特性。

管道的讀寫特性:
  1. 如果管道中沒有數據,則調用read讀取數據會阻塞
  2. 如果管道中數據滿了,則調用write寫入數據會阻塞
  3. 如果管道的所有讀端pipefd[0]被關閉,則繼續調用write會因爲無法讀出而產生異常導致進程退出
  4. 如果管道的所有寫端pipefd[1]被關閉,則繼續調用read,因爲無法再次寫入,read讀完管道中的所有數據後不再阻塞,返回0退出

命名管道

匿名管道的限制就是只能在親緣關係的進程間通信,如果我們想爲不相關的進程交換數據,就可以使用命名管道。

原理:命名管道也是內核中的一塊緩衝區,但是它具有標識符。這個標識符是一個可見於文件系統的管道文件,能夠被其他進程找到並打開管道文件來獲取管道的操作句柄,多個進程可以通過打開這個管道文件來訪問同一塊緩衝區來實現通信

接口:

int mkfifo(const char *filename,mode_t mode);
mode:權限掩碼
filename:管道的標識符,通過這個標識符來訪問管道
返回值:若成功則返回0,否則返回-1

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>

int main()
{
        mkfifo("test", 0664);
        int pid = fork();
        if(pid > 0)
        {
                int read_fd = open("test", O_RDONLY);
                char buff[1024];

                read(read_fd, buff, 12);
                printf("buff:%s\n", buff);
                close(read_fd);
        }
        else if(pid == 0)
        {
                int write_fd = open("test", O_WRONLY);

                write(write_fd, "hello world", 12);
                close(write_fd);

                exit(0);
        }
        return 0;
}

在這裏插入圖片描述
試驗一下

open打開命名管道的特性:

1. 若文件以只讀打開,則會阻塞,直到文件被以寫的方式打開
2. 若文件以只寫打開,則會阻塞,直到文件被以讀的方式打開

管道的特性:

1. 管道是半雙工通信(可以選擇方向的單向傳輸),這個可以從上面的示意圖看出來
2. 管道的讀寫特性(無論命名匿名都一樣)
若管道中沒有數據則讀操作堵塞,如果管道中數據滿了寫操作堵塞。
如果管道中所有讀端關閉則寫端觸發異常,如果所有寫端關閉則讀端讀完數據後不堵塞返回0
3. 管道聲明週期隨進程,打開管道的所有進程退出後管道就會被釋放。
4. 管道提供字節流傳輸服務
5. 命名管道額外有一個打開特性,只讀打開會阻塞直到被以寫打開,只寫打開會阻塞直到被以讀打開
6. 管道自帶同步和互斥


共享內存

共享內存即在物理內存上開闢一塊空間,然後多個進程通過頁表將這同一個物理內存映射到自己的虛擬地址空間中,通過自己的虛擬地址空間來訪問這塊物理內存,達到了數據共享的目的。
如圖:

在這裏插入圖片描述
也正是因爲這種特性,使得共享內存成爲了最快的進程間通信的方式因爲它直接通過虛擬地址來訪問物理內存,比前面的管道和後面的消息隊列少了內核態和用戶態的幾次數據拷貝和交互。

共享內存的使用流程:

1. 創建共享內存
2. 將共享內存映射到虛擬地址空間
3. 進行操作
4. 解除映射關係
5. 釋放共享內存

接口:

  1. 創建共享內存
    int shmget(key_t key, size_t size, int shmflg)

key:這個共享內存段名字
size:共享內存大小
shmflg:由九個權限標誌構成,它們的用法和創建文件時使用的mode模式標誌是一樣的
返回值:成功返回共享內存標識符,失敗返回-1
頭文件:
#include <sys/ipc.h>
#include <sys/shm.h>

  1. 將共享內存映射到虛擬地址空間
    void *shmat(int shmid, const void *shmaddr, int shmflg)

shmid: 共享內存標識
shmaddr:指定連接的地址
shmflg:權限標誌
返回值:成功返回指向共享內存映射在虛擬地址空間的指針(即首地址),失敗返回-1
頭文件:
#include <sys/types.h>
#include <sys/shm.h>

  1. 共享內存管理
    int shmctl(int shmid, int cmd, struct shmid_ds *buf)

shmid:由shmget返回的共享內存標識碼
cmd:將要採取的動作
buf:指向一個保存着共享內存的模式狀態和訪問權限的數據結構
返回值:成功返回0,失敗返回-1
頭文件:
#include <sys/types.h>
#include <sys/shm.h>

  1. 解除映射關係
    int shmdt(const void *shmaddr)

shmaddr: 由shmat所返回的指針
返回值:成功返回0,失敗返回-1
頭文件:
#include <sys/types.h>
#include <sys/shm.h>


消息隊列

消息隊列是內核中的一個優先級隊列,多個進程通過訪問同一個隊列,進行添加節點或者獲取節點來實現通信。

接口:

  1. 創建消息隊列
    int msgget(key_t key, int msgflg);

key:消息隊列對象的關鍵字
msgflg:消息隊列的建立標誌和存取權限
返回值:成功執行時,返回消息隊列標識值。失敗返回-1
頭文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

  1. 進程可以向隊列中添加/獲取節點
    添加節點:
    int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
    獲取節點:
    ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
    int msgflg);

msqid:消息隊列對象的標識符
msgp:消息緩衝區指針
msgsz:消息數據的長度
msgtyp:決定從隊列中返回哪條消息
msgflg:消息隊列狀態
返回值:成功執行時,返回0。失敗返回-1
頭文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

  1. 刪除消息隊列
    int msgctl(int msqid, int cmd, struct msqid_ds *buf)

msqid:消息隊列對象的標識符
cmd:函數要對消息隊列進行的操作
buf:取出系統保存的消息隊列的msqid_ds 數據,並將其存入參數buf 指向的msqid_ds 結構中
返回值:成功執行時,返回0。失敗返回-1
頭文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

特性:

1. 自帶同步與互斥
2. 生命週期隨內核


信號量

信號量其實是內核中的一個計數器和阻塞隊列,通過信號量來對臨界資源的訪問進行控制,來實現進程間的同步與互斥

例如有一個能容納n人的餐廳,則用一個計數器表示n,如果有人進入則n - 1,如果有人出來則n + 1,只有n > 0時才能進入,如果n <= 0時,則說明沒有位置,需要將進程掛起並放入阻塞隊列中,直到有人出來使資源釋放時,才能將後續進程從阻塞隊列中喚醒獲取資源

同步:
通過條件判斷實現臨界資源訪問的合理性

互斥:
通過同一時間的唯一訪問來實現臨界資源訪問的安全性

之後會單獨寫一篇,關於這方面的博客,這裏就先粗略介紹一下。

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