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
      越界 == 操作非法内存 -> 段错误

 

 

 

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