c/c++: 進程間通信(匿名管道、有名管道、內存映射)

進程間通信的方式?

  • - 管道
    •     - 匿名管道
    •     - 有名管道
  •   - 內存映射
  •   - 本地套接字
  •   - 網絡套接字
  •   - 消息隊列
  •   - 共享內存

 

- 父子進程始終共享什麼東西?

  •   - 文件描述符
  •   - 內存映射區

 

目錄

一、管道

1.1匿名管道

創建匿名管道

匿名管道的原理:

實現過程:

匿名管道的侷限性:

栗子:(使用匿名管道實現進程間通信)

2.2 有名管道

有名管道的原理:

創建方式:

創建栗子:(2個無關的進程通信)

二、內存映射

父子進程使用內存映射通信的栗子:

無關係進程使用內存映射通信的栗子:

三、總結

3.1 進程間通信

3.2 面試問題


 

一、管道

管道的本質:

  • 是內核緩衝區
  • 擁有文件的特質(讀操作、寫操作)
  • 匿名管道 -> 沒有文件的實體
  • 有名管道 -> 有文件實體, 不存儲數據
  • 可以文件操作的方式對管道進行處理

在創建子進程之前, 父進程通過文件操作打開了兩個文件 a, b, 得到了兩個文件描述符: fd3 fd4

父進程通過fork 創建子進程

子進程對應一個虛擬地址空間
    - 文件描述符表: fd3, fd4 -> 從父進程拷貝過來的
    - 在子進程中通過得到的fd3 fd4 來操作 A, B文件

 

1.1匿名管道

 

創建匿名管道

#include <unistd.h>
  int pipe(int pipefd[2]);  char* 
  	參數:
  		pipefd: 傳出參數
  		- pipefd[0] -> 管道的讀端
  		- pipefd[1] -> 管道的寫端
  	返回值: 
  		0: 調用成功
  		-1: 失敗

 

匿名管道的原理:

  • 沒有名字, 在磁盤上沒有實體, 是內存中的一塊緩衝區
  • 這個緩衝區, 由父進程在fork()子進程之前創建得到的
  • 內核緩衝區有兩部分
        - 讀端 -> 可以進行讀操作的文件描述符
        - 寫端 -> 可以進行寫操作的文件描述符
  • 父進程被銷燬, 管道自動釋放
  • 默認阻塞

匿名管道數據結構是一個環形隊列,默認容量4k

 

實現過程:

  • - 讀操作
    •     - 如果管道中有數據
      •       - read讀, read返回值是讀到的字節數
    •     - 管道中沒有數據
      •       - 寫端沒有關閉, 但是寫的慢,read會阻塞
      •       - 寫端關閉了,read解除阻塞, 返回值爲0

 

  •  - 寫數據
    •     - 管道有空間。接着寫, 寫滿之後阻塞
    •     - 沒有空間。直接阻塞
  •     - 寫數據的時候, 讀端關閉了
    •       - 管道破裂
    •       - 進程被 SIGPIPE 信號殺死

 

管道的讀寫兩端都默認阻塞,如何設置爲非阻塞呢?

// 使用 fcntl 函數
// 設置讀端爲非阻塞  -> fd[0]
int flag = fcntl(fd[0], F_GETFL)
flag |= O_NONBLOCK;
fcntl(fd[0], F_SETFL, flag);

 

匿名管道的侷限性:

  • 管道中的數據只能被讀一次
  • 半雙工,數據是單向流動的
  • 只能進程有血緣關係的進程通信
        - 在磁盤上沒有實體
        - 被一個進程創建出來的
          - 得到兩個fd(一讀一寫)

栗子:(使用匿名管道實現進程間通信)

流程:

父子進程間通信, 實現 ps aux 
子進程 -> ps aux
子進程得到的結果, 給到父進程
涉及到有血緣關係的進程通信 -> pipe
創建進程 -> fork()
子進程做的事兒: execlp(), 執行命令數據會默認寫到終端
- 重定向: stdout_fileno  ->   管道的寫端   dup2

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

int main()
{
    int fd[2];
    // 創建匿名管道
    int ret = pipe(fd);
    if(ret == -1)
    {
        perror("pipe");
        exit(0);
    }

    // 創建子進程
    pid_t pid = fork();
    if(pid > 0)
    {
        // 父進程, 讀管道
        char buf[1024];
        while(1)
        {
            read(fd[0], buf, sizeof(buf));
            printf("%s", buf);
        }
        wait(NULL);
    }
    else if(pid == 0)
    {
        // 子進程, 寫管道
        // 文件描述符重定向 stdout_fileno -> fd[1]
        dup2(fd[1], STDOUT_FILENO);
        // 執行shell命令 ps aux
        execlp("ps", "ps", "aux", NULL);
        perror("execlp");
        exit(0);
    }
    else
    {
        perror("fork");
        exit(0);
    }
    return 0;

 

2.2 有名管道

 

有名管道的原理:

  • - 在磁盤上有一個文件 -> 僞文件
       
    • - 通過這個僞文件給不同 的進程搭建一個橋樑 -> 找到同一塊內核緩衝區
    • - 這個磁盤文件大小, 永遠爲0
  • - 內核緩衝區 -> 環形隊列實現的
  • - 數據只能被讀一次
  • - 默認也是阻塞的
  • - 實現沒有血緣關係的進程通信

 

創建方式:

# 通過命令創建
  mkfifo 名字
  
  # 通過函數創建
  #include <sys/types.h>
  #include <sys/stat.h>
  int mkfifo(const char *pathname, mode_t mode);
  	參數: 
  		- pathname: 創建的管道文件對應路徑和名字
  		- mode: 用戶對管道文件的操作權限, 八進制的數, 最終權限: (mode & ~umask)

 

創建栗子:(2個無關的進程通信)

wirte文件

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

int main()
{
    // 創建有名管道
    int ret = mkfifo("test", 0664);
    if(ret == -1)
    {
        perror("mkfifo");
        exit(0);
    }

    // 打開管道文件
    int fd = open("test", O_WRONLY);
    // 寫數據
    for(int i=0; i<100; ++i)
    {
        char buf[1024];
        sprintf(buf, "hello, %d\n", i);
        write(fd, buf, strlen(buf)+1);
        sleep(2);
    }
    // 關閉文件
    close(fd);
    
    return 0;
}

read文件

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

int main()
{
    // 打開管道文件
    int fd = open("test", O_RDONLY);
    // 寫數據
    while(1)
    {
        char buf[1024];
        read(fd, buf, sizeof(buf));
        printf("recv buf: %s\n", buf);
    }
    // 關閉文件
    close(fd);
    
    return 0;
}

 

 

 

二、內存映射

將磁盤文件的數據映射到內存, 用戶通過修改內存就能修改磁盤文件

函數原型:

  • mmap  得到映射內存在共享庫加載的區域(虛擬地址空間)
  #include <sys/mman.h>

  void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
  	參數: 
  		- addr: NULL, 由內核指定
  		- length: 要映射的數據的長度, 這個值不能爲0
  		- prot: 對申請的內存映射區的操作權限
  			- PROT_READ: 讀權限, 這個權限必須要有
  			- PROT_WRITE: 寫權限
  			- PROT_READ | PROT_WRITE: 讀寫權限
  		- flags:
  			- MAP_SHARED: 映射區數據會自動和磁盤文件進行同步, 進程間通信必須設置這個選項
  			- MAP_PRIVATE: 不同步
  		- fd: 文件描述符
  			- 通過open得到的, open的是一個磁盤文件
  				- 文件大小不能爲0  (新文件要用lseek擴展)
  				- open的時候需要指定flags
  					- 這個權限要和prot參數值對應
  						- prot:PROT_READ , flag=只讀/讀寫
  						- prot:PROT_READ | PROT_WRITE , flag=讀寫
  		- offset: 偏移量, 一定得的4k的整數倍, 0是可以的
  	返回值:
  		成功: 指向映射區起始位置的指針
  		失敗: MAP_FAILED (that is,(void *) -1)
  • munmap    釋放申請的內存映射區 
 int munmap(void *addr, size_t length);
  	參數:
  		- addr: mmap的返回值
  		- length: 和mmap函數的第二個參數相同即可

 

父子進程使用內存映射通信的栗子:

#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>

int main()
{
    int fd = open("test.txt", O_RDWR);
    int size = lseek(fd, 0, SEEK_END);
    // 創建內存映射區
    void* ptr = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if(ptr == MAP_FAILED)
    {
        perror("mmap");
        exit(-1);
    }

    // 創建子進程
    pid_t pid = fork();
    if(pid > 0)
    {
        // 父進程
        // 寫內存
        strcpy((char*)ptr, "你是我兒子嗎?");
    }
    else if(pid == 0)
    {
        // 子進程
        // 讀內存
        char buf[64];
        strcpy(buf, (char*)ptr);
        printf("read data: %s\n", buf);
    }

    // 關閉內存映射區
    munmap(ptr, size);

    return 0;
}

 

無關係進程使用內存映射通信的栗子:

wirte文件

#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>

int main()
{
    int fd = open("test.txt", O_RDWR);
    int size = lseek(fd, 0, SEEK_END);
    // 創建內存映射區
    void* ptr = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if(ptr == MAP_FAILED)
    {
        perror("mmap");
        exit(-1);
    }

    strcpy((char*)ptr, "你是我兒子嗎===================xxxxxxxxxxxxxx?");

    // 關閉內存映射區
    munmap(ptr, size);

    return 0;
}

read文件

#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>

int main()
{
    int fd = open("test.txt", O_RDWR);
    int size = lseek(fd, 0, SEEK_END);
    // 創建內存映射區
    void* ptr = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if(ptr == MAP_FAILED)
    {
        perror("mmap");
        exit(-1);
    }

    // 讀內存
    char buf[64];
    strcpy(buf, (char*)ptr);
    printf("read data: %s\n", buf);

    // 關閉內存映射區
    munmap(ptr, size);

    return 0;
}

 

三、總結

3.1 進程間通信

  • 有血緣關係的進程通信

    - 還沒有子進程的時候。通過唯一的父進程, 先創建內存映射區,內存映射區有了之後, 創建子進程,父子進程共享創建的內存映射區

  •   無血緣關係的進程通信

    - 準備一個大小非0的磁盤文件

               - 進程1 通過磁盤文件創建內存映射區得到一個操作這塊內存的指針(讀或者寫)
               - 進程2 通過磁盤文件創建內存映射區得到一個操作這塊內存的指針(讀或者寫)
  - 使用內存映射區通信, 不阻塞

 

3.2 面試問題

1. 如果對mmap的返回值(ptr)做++操作(ptr++), munmap是否能夠成功?
      void* ptr = mmap();
      ptr++;
      munmap(ptr, len); // 錯誤
     映射區的起始地址必須要保留下來, 用於映射區的釋放


  2. 如果open時O_RDONLY, mmap時prot參數指定PROT_READ | PROT_WRITE會怎樣?
      mmap返回 MAP_FAILED
      prot參數指定PROT_READ | PROT_WRITE
      int fd = open("xxx", O_RDWR);


  3. 如果文件偏移量爲1000會怎樣?
      mmap調用失敗, 返回: MAP_FAILED


  4. mmap什麼情況下會調用失敗?
      - 第二個參數: length == 0
      - 第三個參數: prot
          - 指定了寫權限
          - prot:PROT_READ | PROT_WRITE, 
            第5個參數fd通open打開文件的時候指定了 O_RDONY/O_WRONLY


  5. 可以open的時候O_CREAT一個新文件來創建映射區嗎?
      - 創建的新文件大小肯定爲0, 這是不行的
      - 可以對新得到的文件進行拓展

- lseek(fd, 拓展的長度, SEEK_END);然後對文件進行一次寫操作: write(fd, " ", 1);
- int truncate(const char *path, off_t length);
- int ftruncate(int fd, off_t length);


  6. mmap後關閉文件描述符,對mmap映射有沒有影響?
      int fd = open("xxx");
      mmap(,,,,fd,0);
      close(fd);
      映射區還存在, 創建映射區使用的fd被關閉了


  7. 對ptr越界操作會怎樣?
      void* ptr = mmap(NULL, 100,,,,);
      映射區的最新單位是4k
      越界 == 操作非法內存 -> 段錯誤

 

 

 

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