linux之进程间通信(IPC)

1. 进程间通信方式

  • 管道(使用最简单)
  • 信号(开销最小)
  • 共享内存(无血缘关系进程通信)
  • 本地套接字(最稳定)

2. 管道之pipe

2.1 管道的原理

  • 管道的本质是一个伪文件(实际上是内核缓冲区)
  • 它有两个文件描述符引用,一个表示读端,一个表示写端。规定数据从管道的写段流入管道,从读端流出。
  • 管道实际上是内核使用环形队列机制(先进先出),借助内核缓冲区(默认4k,可以通过命令ulimit -a可查看)实现

补充:什么是伪文件

  • - 文件
  • d 目录
  • l 符号连接
  • s 套接字
  • b 块设备
  • c 字符设备
  • p 管道

前三种占用磁盘存储,后四种是伪文件,伪文件不占用磁盘存储。

2.2 管道的局限性

  • 管道的两端连接着两个进程,所以一个进程不能自己读管道还自己写管道
  • 数据一旦被读走,便不在管道中存在,不可反复读取
  • 由于管道采用半双工通信方式。因此数据只能在一个方向上流动
  • 只能在有公共祖先的进程间使用管道

补充:通信方式:

  • 单工通信:单向通信
  • 半双工通信:双向通信,但是一方发送的时候另一方不能也同时发送
  • 全双工通信:双向通信,接收发送可以同时进行

2.3 创建管道

int pipe(int pipefd[2]);
  • 返回值:int类型,成功返回0,失败返回-1
  • pipefd[2]: 传出参数,管道的双端的文件描述符。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

int main(void)
{
    int fd[2];
    pid_t  pid;
    int i;

    int ret = pipe(fd);
    if (ret == -1) {
        perror("pipe error:");
        exit(1);
    }

    for (i = 0; i < 2; i++){
        pid = fork();
        if (pid == -1) {
            perror("pipe error:");  //ls | wc -l
            exit(1);
        }
        if (pid == 0)
            break;
    }

    if (i == 0) {  //兄  ls 
        close(fd[0]);
        dup2(fd[1], STDOUT_FILENO);
        execlp("ls", "ls", NULL);
    } else if (i == 1) { // 弟 wc -l 
        close(fd[1]);
        dup2(fd[0], STDIN_FILENO);
        execlp("wc", "wc", "-l", NULL);
    } else if (i == 2) {  //父 
        close(fd[0]);
        close(fd[1]);
        for(i = 0; i < 2; i++)
            wait(NULL);
    }

    return 0;
}

2.4 读管道与写管道

  • 读管道
    • 管道中有数据,read返回实际读到的字节数
    • 管道中无数据:
      • 如果管道写端全部关闭,read返回0(如同读到文件结尾)
      • 如果管道写端没有全部关闭,read阻塞等待
  • 写管道
    • 管道读端全部关闭,进程会异常终止
    • 管道读端没有全部关闭:
      • 管道已满,write阻塞
      • 管道未满,write将数据写入,并返回实际写入的字节数

2.5 管道的优劣

  • 优点
    • 简单,相比于信号,套接字实现进程间通信,简单很多
  • 缺点
    • 只能单向通信,双向通信需要建立两个管道
    • 只能用于父子进程,兄弟进程之间通信

3. 管道之FIFO

3.1 原理

  • FIFO是linux基础文件类型的一种。但FIFO文件在磁盘上没有数据块,仅仅用来表示内存中一条通道。各进程可以打开这个文件进程read/write
  • 实际上是在读写内核通道,实现了进程间通信

3.2 创建方式

  • 方式一:通过命令 mkfifo 管道名
  • 方式二:库函数
int mkfifo(const char *pathname,mode_t mode)
  • 一旦创建了使用mkfifo创建了一个有名管道,就可以使用open打开它。
  • 常见的文件I/O函数都可用于fifo。如close、read、write、unlink等
  • fifo的操作跟对文件操作是一样的。

4. 共享存储映射(共享内存)

4.1 原理

  • 使一个磁盘文件与内存空间中的一个缓冲区相映射。于是当从缓冲区中取数据,将相当于读文件中的相应字节。
  • 与此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可以在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。
  • 使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。

4.2 mmap函数

void *mmap(void *adrr, size_t length, int prot, int flags, int fd, off_t offset); 
  • addr:建立映射区的首地址,由linux内核指定。使用时,直接传递null
  • length:欲创建映射区的大小(其实是虚拟地址大小
  • prot:映射区权限PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
  • flags:标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区)
    • MAP_SHARED:会将映射区所做的操作反映到物理设备上
    • MAP_PRIVATE:映射区所做的修改不会反映到物理设备。
  • fd:用来建立映射区的文件描述符
  • offset:映射文件的偏移(必须设置为4k的整数倍)
  • 返回值
    • 成功:返回创建映射区的首地址
    • 失败:MAP_FAILED宏

4.3 munmap函数

  • 同malloc函数申请内存空间类似,mmap建立的映射区在使用结束后也应调用类似free的函数来释放内存
int munmap(void *addr,size_t length);
  • 成功:返回0; 失败:返回-1;

4.4 使用mmap要注意的事项

  • 创建映射区的过程,隐含了一次对映射文件的读操作
  • 当flags为MAP_SHARED时,要求:映射区的权限<=文件打开的权限(防止对文件的写入会报错)。
  • 当flags为MAP_PRIVATE时则无所谓,因为mmap中的权限是对内存的限制。
  • 当映射文件的大小为0时,不能创建映射区。所以用于映射的文件必须要有实际大小。
  • munmap传入的地址一定要是mmap的返回地址。
  • 文件偏移量必须为4k的整数倍(因为映射区是内核帮我们创建,有mmu完成地址转换,mmu映射的单位就是4k)
  • mmap创建映射区出错概率非常高,一定要检查返回值。确保映射区建立成功再进行后续操作
  • mmap映射成功后,即使关闭文件描述符,也不会对mmap映射有影响。因为内存与磁盘的映射已经建立。

4.5 当利用mmap进行父子进程通信时flags参数意义

  • 当父子等有血缘关系的进程利用mmap建立映射区来完成数据通信时,flags标志位的意义就发生了变化
    • MAP_PRIVATE:私有映射,父子进程各自独占映射区
    • MAP_SHARED:共享映射,父子进程共享映射区。
  • 如果希望父子进程利用mmap实现数据通信,可以将flags标志位设置为MAP_SHARED

4.6 匿名映射

  • 前面利用mmap创建共享映射的过程中还是需要依赖一个文件,需要很多的文件操作,但实际使用的时候又用不到这个文件。所以如何解决这个问题?可以通过匿名映射来解决这个问题
  • 匿名映射利用flags参数来实现的
int *p=mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0)
  • 在flags参数中
    • MAP_ANONYMOUS:表示匿名映射
  • 文件描述符填-1,因为匿名映射,不需要文件描述符了。

补充:类Unix系统如何实现匿名映

fd=open("/dev/zero",O_RDWR);
p=mmap(NULL,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);

4.7 mmap实现无血缘关系进程间通信

  • 将flags参数设置为MAP_SHARED。
  • 在共享模式下,操作系统会将这个映射缓冲区的虚拟地址映射在3~4G的内核空间。
  • 因为多个进程虽然每个都有自己的虚拟地址空间,但是3~4G为内核空间,所有线程共享。

4. 信号量

4.1 kill函数:向指定进程发送信号

int kill(pid_t pid, int sig);
  • 需要有相应的权限

4.2 sigqueue函数:向指定进程发送信号,同时可以携带数据

int sigqueue(pid_t pid, int sig, const union sigval value);
  • value:携带的数据
union sigval {
      // 可以携带一个整型数据
      int   sival_int;
      // 如果发送给自己,地址有用,如果发送给其他进程,虚拟地址空间不共享,地址没用
      void *sival_ptr;
};

5. 本地套接字

  •  用于本机之内做通信。所以通信双方进程只需要直到对方的端口号就行了。

  • 本地通信不需要网络通信协议。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值