进程间通信

进程间通信📮

1. 进程间通信相关概念💩

1.1 什么是进程间通信

Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能 相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,interProcess Communication)

1.2 进程间通信的方式

文件、管道、信号、共享内存、消息队列、套接字、命名管道等

常用的进程通信方式:

  • 管道(使用最简单)
  • 信号(开销最小)
  • 共享映射区(无血缘关系)
  • 本地套接字(最稳定)

2. 管道pipe🔌

2.1 管道的概念

管道是一种最基本的IPC机制,也称匿名管道,应用于有血缘关系的进程之间,完成数据传递。调用pipe函数即可创建一个管道

  • 管道的本质是一块内核缓冲区
  • 由两个文件描述符引用,一个表示读端,一个表示写端
  • 规定数据从管道的写端流入管道,从读端流出
  • 当两个进程都终结的时候,管道也自动消失
  • 管道的读端和写端默认都是阻塞的

2.2 管道的原理

  • 管道的实质是内核缓冲区,内部使用环形队列实现
  • 默认缓冲区大小为4K,可以使用 ulimit -a 命令获取大小
  • 实际操作过程中缓冲区会根据数据压力做适当调整

2.3管道的局限性

  • 数据一旦被读走,便不在管道中存在,不可反复读取
  • 数据只能在一个方向流动,若要实现双向流动,必须使用两个管道
  • 只能在有血缘关系的进程间使用管道

2.4 创建管道pipe函数

#include <unistd.h>
  • 函数作用:创建一个管道
  • 函数原型:
    int pipe(int fildes[2]);
  • 函数参数:
    fildes[0] 存放管道的读端,fildes[1] 存放管道的写端
  • 返回值:
    • 成功返回 0
    • 失败返回 -1,并设置 errno

2.5 父子进程使用管道通信

  1. 父进程创建管道
  2. 父进程 fork 子进程
  3. 父进程关闭 fildes[0/1],子进程关闭 fildes[1/0]
  1 //pipe函数测试
  2 #include<stdio.h>
  3 #include<stdlib.h>
  4 #include<string.h>
  5 #include<sys/types.h>
  6 #include<unistd.h>
  7 #include<sys/wait.h>
  8 
  9 int main()
 10 {   
 11     //创建管道
 12     int fd[2];
 13     int ret = pipe(fd);
 14     if (ret<0)
 15     {   
 16         perror("pipe error");
 17         return -1;
 18     }
 19     //创建子进程
 20     pid_t pid = fork();
 21     if (pid<0)
 22     {   
 23         perror("fork error");
 24         return -1;
 25     }
 26     else if (pid>0)     //父进程
 27     {   
 28         //关闭读端
 29         close(fd[0]);
 30         sleep(5);
 31         write(fd[1], "hello world", strlen("hello world"));
 32         wait(NULL);
 33     }
 34     else if (pid==0)        //子进程
 35     {   
 36         //关闭写端
 37         close(fd[1]);
 38         char buf[64];
 39         memset(buf, 0x00, sizeof(buf));
 40         int n = read(fd[0], buf, sizeof(buf));
 41         printf("read over,n==[%d],buf==[%s]\n",n,buf);
 42     }
 43     return 0;
 44 }

 >>>>执行结果
 read over,n==[11],buf==[hello world]

2.6 管道练习

【💊】父子进程间通信实现 ps aus | grep bash

  1. 创建管道pipe
  2. 创建子进程fork
  3. 在父进程中关闭读端 fd[0]
  4. 在子进程中关闭写端 fd[1]
  5. 在父进程中将标准输出重新定向到管道的写端
  6. 在子进程中将标准输入重新定向到管道的读端
  7. 在父进程调用execl函数执行 ps aux 命令
  8. 在子进程调用execl函数执行 grep bash 命令
  9. 在父进程中回收子进程wait函数
  1 //父子进程管道模拟ps aux | grep bash
  2 #include<stdio.h>
  3 #include<stdlib.h>
  4 #include<string.h>
  5 #include<sys/types.h>
  6 #include<sys/wait.h>
  7 #include<unistd.h>
  8 
  9 int main()
 10 {
 11     //创建管道
 12     int fd[2];
 13     int ret = pipe(fd);
 14     if (ret<0)
 15     {
 16         perror("pipe error");
 17         return -1;
 18     }
 19     //创建子进程
 20     pid_t pid = fork();
 21     if (pid<0)
 22     {
 23         perror("fork error");
 24         return -1;
 25     }
 26     else if (pid>0)         //父进程
 27     {
 28         close(fd[0]);
 29         dup2(fd[1], STDOUT_FILENO);
 30         execl("/usr/bin/ps", "ps", "aux", NULL);
 31         perror("execl error");
 32         wait(NULL);
 33     }
 34     else if (pid==0)        //子进程
 35     {
 36         close(fd[1]);
 37         dup2(fd[0], STDIN_FILENO);
 38         execl("/usr/bin/grep", "grep", "--color==auto", "bash", NULL);
 39         perror("execl error");
 40     }
 41     return 0;
 42 }

【💊】兄弟进程间通信实现 ps aus | grep bash

  1 //兄弟进程管道模拟ps aux | grep bash
  2 #include<stdio.h>
  3 #include<stdlib.h>
  4 #include<string.h>
  5 #include<sys/types.h>
  6 #include<sys/wait.h>
  7 #include<unistd.h>
  8 
  9 int main()
 10 {
 11     //创建管道
 12     int fd[2];
 13     int ret = pipe(fd);
 14     if (ret<0)
 15     {
 16         perror("pipe error");
 17         return -1;
 18     }
 19     //创建子进程
 20     int i = 0;
 21     for(i=0;i<2;i++)
 22     {
 23         pid_t pid = fork();
 24         if (pid<0)
 25         {
 26             perror("fork error");
 27             return -1;
 28         }
 29         else if (pid==0)        //子进程
 30         {
 31             break;
 32         }
 33     }
 34     if (i==0)
 35     {
 36         close(fd[0]);
 37         dup2(fd[1], STDOUT_FILENO);
 38         execl("/usr/bin/ps", "ps", "aux", NULL);
 39         perror("execl error");
 40     }
 41     if (i==1)
 42     {
 43         close(fd[1]);
 44         dup2(fd[0], STDIN_FILENO);
 45         execl("/usr/bin/grep", "grep", "--color=auto", "bash", NULL);
 46         perror("execl error");
 47     }
 48     if (i==2)
 49     {
 50         close(fd[0]);
 51         close(fd[1]);
 52         while(1)
 53         {
 54             pid_t wpid = waitpid(-1, NULL, WNOHANG);
 55             if (wpid==-1)
 56             {
 57                 break;
 58             }
 59         }
 60     }
 61     return 0;
 62 }

2.7 管道的读写行为

  • 读操作
    • 有数据
      read 正常读,返回读出的字节数
    • 无数据
      • 写端全部关闭
        read 解除阻塞,返回 0 ,相当于读文件读到了尾部
      • 没有全部关闭
        read 阻塞
  • 写操作
    • 读端全部关闭
      管道破裂,进程终止,内核给当前进程发 SIGPIPE 信号
    • 读端没有全部关闭
      • 缓冲区写满了
        write 阻塞
      • 缓冲区没有满
        继续 write

2.8 设置管道为非阻塞

默认管道两端阻塞,设置读端非阻塞方法:

  1. int flags = fcntl(fd[0],F_GETFL,0);
  2. flag |= O_NONBLOCK;
  3. fcntl(fd[0],F_SETFL,flags);

若是读端设置为非阻塞:

  • 写端没有关闭,管道中没有数据可读,则 read 返回 -1
  • 写端没有关闭,管道中有数据可读,则 read 返回实际读到的字节数
  • 写端已经关闭,管道中有数据可读,则 read 返回实际读到的字节数
  • 写端已经关闭,管道中没有数据可读,则 read 返回 0

2.9 查看管道缓冲区大小

  • 命令 ulimit -a
  • 函数
    long fpathconf(int fd,int name);
    printf("pipe size==[%d]\n", fpathconf(fd[0], _PC_PIPE_BUF));
    printf("pipe size==[%d]\n", fpathconf(fd[1], _PC_PIPE_BUF));

3. FIFO💉

3.1 FIFO介绍

FIFO 常被称为命名管道,以区分管道(pipe)。pipe只能用于有血缘关系的进程间通信。而FIFO可在不相关的进程间交换数据。

FIFO是LInux基础文件类型中的一种(文件类型为 p)。但FIFO文件在磁盘上没有数据块,文件大小为 0,仅仅用来标识内核中一条通道。进程可以打开着个文件进行read/write,实际是在读写内核缓冲区,这样就实现了进程间通信。

3.2 创建管道

  • 方式1️⃣ 使用命令
    mkfifo 管道名
  • 方式2️⃣ 使用函数
    int mkfifo(const char *pathname,mode_t mode);

【📢】 FIFO 文件IO函数,均适用(除 lseek() 等文件定位操作)
严格遵循先进先出,对FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾

[xfk@centos FIFODIR]$ mkfifo fifo
[xfk@centos FIFODIR]$ ls -l
总用量 0
prw-rw-r--. 1 xfk xfk 0 1月  19 15:39 fifo

3.3 FIFO进程通信

  • 进程A:
    1. 创建FIFO文件,代码中使用函数,终端使用shell命令
    2. open FIFO文件,获得一个文件描述符fd
    3. 写FIFO文件 write(fd,"xxx",...)
    4. 关闭FIFO文件 close(fd);
  • 进程B:
    1. open FIFO文件,获得一个文件描述符fd
    2. 读FIFO文件 read(fd,buf,sizeof(buf))
    3. 关闭FIFO文件 close(fd);
  1 //fifo进程通信测试 进程A
  2 #include<stdio.h>
  3 #include<stdlib.h>
  4 #include<string.h>
  5 #include<sys/types.h>
  6 #include<unistd.h>
  7 #include<sys/stat.h>
  8 #include<fcntl.h>
  9 
 10 int main()
 11 {
 12     //创建fifo文件
 13     int ret = access("./myfifo", F_OK);		//判断文件是否存在
 14     if (ret!=0)
 15     {
 16         int ret = mkfifo("./myfifo", 0777);
 17         if (ret<0)
 18         {
 19             perror("mkfifo error");
 20             return -1;
 21         }
 22     }
 23     //打开文件
 24     int fd = open("./myfifo", O_RDWR);
 25     if (fd<0)
 26     {
 27         perror("open error");
 28         return -1;
 29     }
 30     //写fifo文件
 31     int n = 0;
 32     char buf[64];
 33     while(1)
 34     {
 35         memset(buf, 0x00, sizeof(buf));
 36         sprintf(buf, "%d:%s", n, "hello world");
 37         write(fd, buf, strlen(buf));
 38         sleep(1);
 39         n++;
 40     }
 41     //关闭文件
 42     close(fd);
 43     //getchar();
 44     return 0;
 45 }

  1 //fifo进程通信测试 进程B
  2 #include<stdio.h>
  3 #include<stdlib.h>
  4 #include<string.h>
  5 #include<sys/types.h>
  6 #include<unistd.h>
  7 #include<sys/stat.h>
  8 #include<fcntl.h>
  9 
 10 int main()
 11 {
 12     //打开文件
 13     int fd = open("./myfifo", O_RDWR);
 14     if (fd<0)
 15     {
 16         perror("open error");
 17         return -1;
 18     }
 19     //读fifo文件
 20     int n;
 21     char buf[64];
 22     while(1)
 23     {
 24         memset(buf, 0x00, sizeof(buf));
 25         n = read(fd, buf, sizeof(buf));
 26         printf("n==[%d],buf==[%s]\n", n, buf);
 27     }
 28     //关闭文件
 29     close(fd);
 30     //getchar();
 31     return 0;
 32 }

 >>>>执行结果 进程B
 n==[13],buf==[0:hello world]
 n==[13],buf==[1:hello world]
 n==[13],buf==[2:hello world]
 n==[13],buf==[3:hello world]
 n==[13],buf==[4:hello world]
 n==[13],buf==[5:hello world]
 n==[13],buf==[6:hello world]
 n==[13],buf==[7:hello world]
 n==[13],buf==[8:hello world]
 n==[13],buf==[9:hello world]
 n==[14],buf==[10:hello world]
 n==[14],buf==[11:hello world]
 ...

【📢】先运行read进程,阻塞;再运行write进程

4. 内存映射区🍁

4.1 存储映射区介绍

存储映射I/O(Memory-mapped I/O)使一个磁盘文件与储存空间中的一个缓冲区相映射。从缓冲区中读取数据,就相当于读文件中的相应字节;将数据写入缓存区,则会将数据写入文件。这样,就可以在不使用read和write函数的情况下,使用地址(指针)完成I/O操作

【🎫】使用存储映射,首先应该通知内核,将一个指定文件映射到存储区域中。
映射工作可由 mmap 函数实现

4.2 mmap函数

#include<sys/mman.h>
  • 函数作用:建立存储映射区
  • 函数原型:
    void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
  • 函数参数:
    • addr 指定映射的起始地址,通常设为NULL,由系统指定
    • length 映射到内存的文件长度
      lseekstat 函数
    • prot 映射区的保护方式
      • 读:PROT_READ
      • 写:PROT_WRITE
      • 读写:PROT_READ | PROT_WRITE
    • flags 映射区的特性
      • MAP_SHARED 写入映射区的数据会写回文件,且允许其他映射该文件的进程共享
      • MAP_PRIVATE 对映射区的写入操作会产生一个映射区的复制(copy-on-write),对此区域所做的修改不会写回原文件
    • fd 由open反悔的文件描述符,代表要映射的文件
    • offset 以文件开始处的偏移量,必须是4k的整数倍,通常为0,表示从文件头开始映射
  • 返回值:
    • 成功:返回创建的映射区首地址
    • 失败:MAP_FAILED

【🎫】匿名映射
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
MAP_SHARED|MAP_ANONYMOUS 配合使用,fd 指定为 -1
只能用于有血缘关系的进程间通信

【💊】父子进程共享映射区通信

  1 //mmap函数测试父子进程
  2 #include<stdio.h>
  3 #include<stdlib.h>
  4 #include<string.h>
  5 #include<sys/types.h>
  6 #include<unistd.h>
  7 #include<sys/stat.h>
  8 #include<fcntl.h>
  9 #include<sys/wait.h>
 10 #include<sys/mman.h>
 11 
 12 int main()
 13 {
 14     //打开文件
 15     int fd = open("./test.log", O_RDWR);
 16     if (fd<0)
 17     {
 18         perror("open error");
 19         return -1;
 20     }
 21     int len = lseek(fd, 0, SEEK_END);
 22     //使用mmap函数建立共享映射区
 23     void *addr = mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
 24     if (addr==MAP_FAILED)
 25     {
 26         perror("mmap error");
 27         return -1;
 28     }
 29     //创建子进程
 30     pid_t pid = fork();
 31     if (pid<0)
 32     {
 33         perror("fork error");
 34         return -1;
 35     }
 36     else if (pid>0)     //父进程
 37     {
 38         memcpy(addr, "hello world", strlen("hello world"));
 39         wait(NULL);
 40     }
 41     else if (pid==0)    //子进程
 42     {
 43         sleep(1);
 44         char *p = (char *)addr;
 45         printf("[%s]", p);
 46     }
 47     return 0;
 48 }

【💊】AB进程(不相关)共享映射区通信

  1 //mmap函数测试 进程A:write
  2 #include<stdio.h>
  3 #include<stdlib.h>
  4 #include<string.h>
  5 #include<sys/types.h>
  6 #include<unistd.h>
  7 #include<sys/stat.h>
  8 #include<fcntl.h>
  9 #include<sys/wait.h>
 10 #include<sys/mman.h>
 11 
 12 int main()
 13 {
 14     //打开文件
 15     int fd = open("./test.log", O_RDWR);
 16     if (fd<0)
 17     {
 18         perror("open error");
 19         return -1;
 20     }
 21     int len = lseek(fd, 0, SEEK_END);
 22     //使用mmap函数建立共享映射区
 23     void *addr = mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
 24     if (addr==MAP_FAILED)
 25     {
 26         perror("mmap error");
 27         return -1;
 28     }
 29     memcpy(addr, "0123456789", 10);
 30     return 0;
 31 }
 
  1 //mmap函数测试 进程B:read
  2 #include<stdio.h>
  3 #include<stdlib.h>
  4 #include<string.h>
  5 #include<sys/types.h>
  6 #include<unistd.h>
  7 #include<sys/stat.h>
  8 #include<fcntl.h>
  9 #include<sys/wait.h>
 10 #include<sys/mman.h>
 11 
 12 int main()
 13 {
 14     //打开文件
 15     int fd = open("./test.log", O_RDWR);
 16     if (fd<0)
 17     {
 18         perror("open error");
 19         return -1;
 20     }
 21     int len = lseek(fd, 0, SEEK_END);
 22     //使用mmap函数建立共享映射区
 23     void *addr = mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
 24     if (addr==MAP_FAILED)
 25     {
 26         perror("mmap error");
 27         return -1;
 28     }
 29     char buf[64];
 30     memset(buf, 0x00, sizeof(buf));
 31     memcpy(buf, addr, 10);
 32     printf("buf==[%s]\n",buf);
 33     return 0;
 34 }

【📢】注意事项:

  1. 创建映射区的过程中,隐含着一次对映射文件的读操作,将文件内容读取到映射区
  2. 映射区的释放与文件关闭无关,只要 映射建立成功,文件可以立即关闭
  3. 当映射文件大小为0, 不能创建映射区
  4. 文件的偏移量必须为 0 或 4K 的整数倍
  5. mmap创建映射区出错概率非常高,一定要检查返回值,确保成功再进行后续操作

4.3 munmap函数

#include<sys/mman.h>
  • 函数作用:释放由munmap函数建立的存储映射区
  • 函数原型:
    int munmap(void *addr, size_t length);
  • 函数参数:
    • addr 调用mmap函数成功返回的映射区首地址
    • length 映射区大小(mmap函数的第二个参数)

【📢】munmap传入的地址一定是mmap的返回地址


✍️ 小方块xfk

📅 写于 2023年1月

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值