IPC:進程間的通信方式

前言

由於進程的獨立性,當我們要使兩個進程間進行數據交互的時候就得通過介質來實現。

進程間進行通信的介質我們把它們稱之爲進程間通信方式(IPC)
在這裏插入圖片描述
根據進程間通信場景的不同,操作系統爲用戶提供了幾種不同的進程間通信方式:管道、共享內存、消息隊列、信號量

管道(數據傳輸)

管道是一種傳輸數據的媒介,所謂管道是操作系統在內核創建的一塊緩衝區域,進程只要能夠訪問到這塊緩衝區就能通過緩衝區實現相互通信。

同時操作系統會給進程一個操作句柄用於進程操作管道,在Linux下其實是給了進程兩個文件描述符,用於負責管道的讀和寫。

  • 管道的本質:

內核中的一塊緩衝區

  • 管道通信的原理:

讓多個進程都能訪問到同一塊緩衝區從而實現進程間通信

  • 管道的特性:

1.管道自帶同步與互斥(保證多個進程操作管道不發生數據的二義性)
同步:通過條件判斷保證臨界資源訪問時序的合理性
互斥:通過同一時間的唯一訪問保證臨界資源訪問的安全性(原子操作)

2.管道的生命週期隨進程

3.管道實現方向可選擇的、單向的、有序的數據傳輸

4.管道提供字節流服務(有序,連接,可靠的字節流傳輸)

讀寫特性:

1.管道中沒有數據則讀端阻塞,管道中寫滿了數據則寫端阻塞
2.管道讀端被關閉繼續寫入會觸發異常發起SIGPIPE信號導致進程退出
3.寫端被關閉繼續讀完管道中的數據後返回0

注意:關閉讀端或寫端的時候,所有進程的讀端或寫端都得關閉

匿名管道

匿名管道顧名思義就是內核中的這塊緩衝區沒有標識符

當前進程只能通過返回的操作句柄來操作匿名管道,其他進程無法找到匿名管道,所以匿名管道只能用於具有親緣關係的進程間通信。

  • 匿名管道通信流程:

操作句柄保存在pcb當中,先創建管道再創建子進程,子進程通過複製父進程的方式獲取管道的操作句柄,進而訪問同一管道來實現進程間的通信。

  • 匿名管道的創建:

int pipe(int pipefd[2]):具有兩個int型結點的數組的首地址,用於接收創建管道返回的操作句柄

創建一個匿名管道,向用戶通過參數pipefd返回管道的操作句柄。返回值 0(成功),-1(失敗)

pipefd[0]: 用於從管道讀取數據
pipefd[1]: 用於向管道寫入數據

  • 匿名管道的實現:
int main(){
    // 匿名管道只能用於具有親緣關係的進程間通信
    // 通過子進程複製父進程的方式來獲取管道的操作句柄
    // 創建管道一定要放到子進程之前
    int pipefd[2] = {-1};
    int ret = pipe(pipefd);
    if(ret < 0){
        perror("pipe error");
        return -1;
    }

    pid_t pid = fork();
    if(pid == 0){
        // 子進程從管道讀取數據
        char buf[1024] = {0};
        read(pipefd[0], buf, 1023);
        printf("子進程讀到的數據:%s", buf);
    }
    else if(pid > 0){
        // 父進程向管道寫入數據
        char* ptr = "父進程寫入\n";
        write(pipefd[1], ptr, strlen(ptr));
    }

    return 0;
}
命名管道

命名管道顧名思義就是內核中的這塊緩衝區有標識符

  • 命名管道的通信流程:

多個進程可以通過相同的標識符找到同一塊緩衝區,打開同一個管道文件,進而實現同一主機上任意進程間通信。

  • 命名管道的創建:

mkfifo 創建命名管道(生成test.fifo的管道文件):int mkfifo(const char* pathname管道文件名稱,mode_t mode文件權限)

成功:返回 0 | 失敗:返回-1

  • 命名管道的打開特性:

1.若管道文件以只讀的方式打開,則會阻塞,直到這個管道文件被以寫的方式打開。
2.若管道文件以只寫的方式打開,則會阻塞,直到這個管道文件被以讀的方式打開。
3.若管道文件以讀寫的方式打開,則不會阻塞。

  • 命名管道的實現:

Fifowrite.c

int main(){
    // 創建命名管道
    umask(0);
    int ret = mkfifo("./test.fifo", 0664);
    if(ret < 0 && errno != EEXIST){
        perror("mkfifo error");
        return -1;
    }

    // 操作管道
    int fd = open("./test.fifo", O_WRONLY);
    if(fd < 0 ){
        perror("open fifo error");
        return -1;
    }
    printf("open fifo success\n");
    int cur = 0;
    while(1){
        char buf[1024] = {0};
        sprintf(buf, "寫端寫入數據 [%d]", cur++);
        write(fd, buf, strlen(buf));
        printf("寫入數據成功\n");
        sleep(1);
    }
    close(fd);

    return 0;
}

Fiforead.c

int main(){
    // 創建命名管道
    umask(0);
    int ret = mkfifo("./test.fifo", 0664);
    if(ret < 0 && errno != EEXIST){
        perror("mkfifo error");
        return -1;
    }

    // 操作管道
    int fd = open("./test.fifo", O_RDONLY);
    if(fd < 0 ){
        perror("open fifo error");
        return -1;
    }
    printf("open fifo success\n");

    while(1){
        char buf[1024] = {0};
        int ret = read(fd, buf, 1023);
        if(ret < 0){
            perror("read error");
            return -1;
        }
        else if(ret == 0){
            perror("all write closed");
            return -1;
        }
        printf("read buf:[%s]\n", buf);
    }
    close(fd);

    return 0;
}

共享內存(數據共享)

  • 共享內存的本質:

共享內存實際是在物理內存上開闢的一塊具有標識符的內存空間

  • 共享內存的通信原理:

將這塊內存空間映射到進程的虛擬地址空間中,進程則可以通過虛擬地址進行訪問操作;多個進程映射同一塊物理內存,那麼多個進程訪問同一塊內存空間,進而實現進程間通信。

  • 共享內存的特性:

1.共享內存生命週期隨內核
在物理內存上開闢,將信息儲存在內核中,生命不會隨進程結束而結束,內核重啓或手動釋放共享內存

2.最快的進程間通信方式
不涉及用戶態和內核態的兩次數據拷貝

3.共享內存操作是不安全
不自帶同步和互斥,需要操作用戶進行控制

4.共享內存的寫入是一種針對地址的覆蓋式寫入

  • 共享內存的通信流程:

Shmwrite.c

#define IPC_KEY 0x12345678

int main(){
    // 1.創建共享內存
    int shm_id = shmget(IPC_KEY, 32, IPC_CREAT|0664);
    if(shm_id < 0){
        perror("shmget error");
        return -1;
    }

    // 2.建立映射關係
    void* shm_start = shmat(shm_id, NULL, 0);
    if(shm_start == (void*)-1){
        perror("shmat error");
        return -1;
    }

    // 3.進行內存操作
    int cur = 0;
    while(1){
        sprintf(shm_start, "%s:%d", "寫入端寫入", ++cur);
        sleep(1);
    }

    // 4.解除映射關係
    shmdt(shm_start);

    // 5.刪除共享內存
    shmctl(shm_id, IPC_RMID, NULL);

    return 0;
}

Shmread.c

#define IPC_KEY 0x12345678

int main(){
    // 1.創建共享內存
    int shm_id = shmget(IPC_KEY, 32, IPC_CREAT|0664);
    if(shm_id < 0){
        perror("shmget error");
        return -1;
    }

    // 2.建立映射關係
    void* shm_start = shmat(shm_id, NULL, 0);
    if(shm_start == (void*)-1){
        perror("shmat error");
        return -1;
    }

    // 3.進行內存操作
    while(1){
        printf("[%s]\n", shm_start);
        sleep(1);
    }

    // 4.解除映射關係
    shmdt(shm_start);

    // 5.刪除共享內存
    shmctl(shm_id, IPC_RMID, NULL);

    return 0;
}

刪除共享內存中值得注意:

共享內存並不會立即被刪除(因爲可能造成正在訪問的進程崩潰),而是將key修改爲0,表示這塊共享內存不再繼續接收映射連接,當這塊共享內存的映射連接數爲0時,則自動被釋放。

消息隊列(數據傳輸)

  • 消息隊列的本質:

消息隊列是內核中的一個優先級隊列

  • 消息隊列的通信原理:

多個進程通過向隊列中添加結點或者獲取結點實現通信

  • 消息隊列的特性:

1.生命週期隨內核
2.自帶同步與互斥
3.數據傳輸自帶優先級(傳輸一個有類型(優先級)的數據塊)

信號量(進程控制)

  • 信號量的本質:

內核當中的一個計數器+pcb等待隊列(對資源進行計數)

  • 信號量的作用:

用於實現進程間的同步與互斥

同步的實現:

訪問前進行條件判斷

信號量是對資源的計數,通過計數判斷是否能夠獲取一個資源進行處理,若計數器<0則表示不能獲取(並且對計數器-1),需要等待(加入pcb隊列),並不保證獲取資源的安全性

這時若其他進程生產一個資源,則會對計數進行+1,若計數<=0則喚醒一個進程。 >0社麼都不幹

將pcb置爲可中斷休眠狀態加入隊列

互斥的實現:

同一時間的唯一訪問

起始爲1

通過不大於1的計數器,實現對臨界資源訪問狀態的標記,在訪問臨界資源之前先獲取信號量

計數 - 1後:

若計數 <= 0則進程等待(將進程pcb加入隊列中);否則可以對臨界資源進行訪問(已經將臨界資源的狀態置爲不可訪問狀態,完畢之後,對計數+1,喚醒一個進程(將一個pcb出隊,置爲運行狀態)

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