Linux(七)进程间通信(管道、共享内存)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

进程间是如何进行通信的

        通过前面的学习之后我们知道进程间是具有独立性的在操作系统的层面来看进程就是一块pcb是对运行中的程序动态运行过程的描述在Linux角度下进程就是一个task_struct结构体pcb里面的信息存放在虚拟地址空间并通过页表来映射到一块物理内存上每一个进程都对应着其自己的pcb所以说进程间是相互独立的那么如何使这一些相互独立的pcb之间能够形成通信呢

        Linux中这这么一些通信方式、管道、消息队列、共享内存、信号量……他们建立进程间通信都是通过同样的一种关联关系能够共同访问的同一块内存

一、管道

1、介绍

管道的灵感来源于生活管道在生活中随处可见排水输水管道、自来水管……

Linux中也引入了这一想法并根据通信传输方向分为

单工通信单向通信比如有两端只能从A->B

双工通信双向通信比如有两端既可以从A->B,也可以从B->A

半双工通信一种可以选择方向的单向通信比如有A和B两端要么从A->B,要么从B->A

                     同一时间不能同时既发送又接收

管道的本质操作系统为进程间通信提供了一个空间交叉点进程间都能访问这个交叉点

                     在程序中管道就是内核的一块缓冲区缓冲区本质就是内核中的一块内存

2、匿名管道

定义没有文件描述符不能被其他进程找到所以只能用于具有亲缘关系进程间通信

实现原理概述

        一个进程创建一个匿名管道就是在内核空间中创建一块缓冲区也就是一块内存或者说一个文件然后创建之后给该进程返回一个文件描述符这时候我们创建一个或多个子进程那么子进程就会复制父进程大部分的描述信息其中就有该管道文件的描述符那么子进程就可以与该父进程对同一块文件进程进行读写实现通信。

 如果这时候再创建一个子进程那么这个子进程和父进程可以进行通信当然也可以与之前创建的子进程进行通信该子进程再创建一个子进程父进程的孙进程当然它们之间也能进行通信

接口

        int pipeint pipefd[2]

                功能创建一个管道并通过参数返回管道的俩个操作句柄

                参数pipefd - 具有2个整形元素的数组内部创建管道会将描述符存储在数组中

                        pipefd[0] -- 用于从管道中读数据

                        pipefd[1] -- 用于向管道中写数据

                返回值成功返回0失败返回-1

                注意创建匿名管道前一定要在创建子进程之前

进行一段代码通信在第一个子进程中向管道写入数据然后第二个子进程进行读取数据

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

int main()
{
    int pipefd[2];
    // [0] 为读
    // [1] 为写
    int ret = pipe(pipefd);
    if(ret < 0){
      perror("pipe error");
      return -1;
    }

    pid_t pid1 = fork();
    if(pid1==0){
      // 兄 子进程
      const char *str = "我是哥哥新年好啊\n";
      write(pipefd[1], str, strlen(str));   // 向管道中写入数据
      exit(0);
    }
    pid_t pid2 = fork();
    if(pid2 == 0){
      // 弟 子进程
      char buf[1024] = {0};
      read(pipefd[0], buf, 1023);    // 从管道中读取数据
      printf("%s",buf);
      exit(0);
    }
    wait(NULL);
    wait(NULL);
    return 0;
}

管道特性

1、当管道中没有数据时读端就会阻塞直到有数据被写入管道才会进行读取

2、当管道中数据满了那么写端就会阻塞等待管道中数据被读取有空间可以写入了才会进行写入数据。

3、当管道的所有读端被关闭再次写入数据就会导致程序崩溃

        因为没有进程来进行读取了那么在进行写入都是没有意义的所以系统就进行处理

4、当管道的所有写端被关闭那么读端读取完管道中的所有数据后将不在阻塞等待而是返回0

        所以没有意义的事情大佬就会替我们考虑好读端关闭写入就会报错写端关闭读取完剩余就会退出返回0

2、命名管道

具有标识符的管道其他进程可以找到。

实现概述

进程通过mkfifo创建一个管道文件这个管道文件的标识符可以被其他进程访问到当A进程通过这个管道文件描述符访问这个缓冲区进行写入数据那么B进程打开通过这个管道文件访问管道可以进行数据读取

注意点

        当创建管道文件时并不是直接就将数据传输管道内存中的缓冲区创建好了而是只有在有进程打开这个管道进行数据访问时才会真正的创建这个管道。联想写时拷贝技术。

命名管道的独有特性没读端只写就会阻塞  ||  没写端只读就会阻塞

        只以只写的方式打开管道写端就会被阻塞直到管道有以可读的进程访问

        当只以只读方式打开管道读端就会阻塞直到管道有以可写的进程访问

原理因为当一个管道没有构成读写都满足的条件时这个管道是没有意义的也就没有必要创建这个缓冲区。也正好对应上方只有真正满足数据传递的时候才会真正创建这个缓冲区

 接口

        int  mkfifochar* pathname, mode_t mode 头文件#include<sys/stat.h>

                pathname 创建的管道文件路径  如果当前路径下可以 ./pipe.fifo

                mode 创建文件的访问权限

        返回值成功返回0失败返回-1

读端进程

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

int main()
{
    umask(0);
    int ret = mkfifo("./named_pipe.fifo", 0664);
    if(ret < 0 && errno!=EEXIST){ // 因为创建管道的文件如果存在那么访问就会出错加入errno判断可以去掉该报错
      perror("mkfifo error");
      return -1;
    }
    int fd = open("named_pipe.fifo", O_WRONLY);
    if(fd < 0){
      perror("open error");
      return -1;
    }
    while(1)
    {
        printf("A进程说:");
        fflush(stdout);
        char buf[1024] = {0};
        scanf("%s",buf);
        int ret = write(fd, buf, strlen(buf));
        if(ret < 0){
          perror("write error");
          close(fd);
          return -1;
        }
    }
    close(fd);
    return 0;
}

写端进程

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

int main()
{
    umask(0);
    int ret = mkfifo("./named_pipe.fifo", 0664);
    if(ret < 0 && errno!=EEXIST){ // 因为创建管道的文件如果存在那么访问就会出错加入errno判断可以去掉该报错
      perror("mkfifo error");
      return -1;
    }
    int fd = open("named_pipe.fifo", O_RDONLY);
    if(fd < 0){
      perror("open error");
      return -1;
    }
    while(1)
    {
        char buf[1024] = {0};
        int ret = read(fd, buf, 1023);
        if(ret < 0){
          perror("read error");
          close(fd);
          return -1;
        }else if(ret == 0)
        {
          printf("所有的写端被关闭读端读取完数据返回0\n");
          close(fd);
          return -1;
        }
        printf("%s\n", buf);
    }
    close(fd);
    return 0;
}

3、管道学习总结

本质

        内存中的一块缓冲区多个进程通过访问同一块缓冲区来实现通信

分类

匿名管道只能用于具有亲缘关系的进程间通信只能通过子进程复制父进程访问获取操作句柄

命名管道可用于同一主机的任意进程间通信通过打开同一个管道文件来访问同一块缓冲区

特性

        1半双工通信

        2管道声明周期随进程不需要人为关闭当所有打开管道的进程退出后管道释放

        3提供字节流传输服务数据先进进出、按序到达、不会丢失数据、面向连接

                所有读端被关闭继续写就会异常所有写端被关闭读端读完剩余不在阻塞返回0

        4自带同步与互斥

                互斥通过同一时间对共享资源的唯一访问保证原子性

                      原子性具有不可分割特性        原子操作一个操作不会被打断

                同步通过进程对资源的访问限制让进程对资源访问更加合理

                      饥饿问题不会出现A进程一直在写另外B进程一直等着。

                      数据满了 write进程阻塞没有数据read阻塞。

二、共享内存

1、简介

作用实现多个进程之间的数据共享

特性最快的数据传输方式

           生命周期随内核删除并非直接删除而是拒绝后续映射当映射连接数为0时表示没有进程访问了操作系统才会对其回收资源

原理开辟出一块物理内存然后多个进程都将这块内存映射到自己的虚拟地址上再通过虚拟地址来访问一块空间中的数据。

 通过访问同一块物理内存将物理地址映射到各自的虚拟地址中进行数据访问。

管道通信通过在内核中开辟的缓冲区共同访问这一块缓冲区数据传输是从发送端拷贝到管道中接收端再从管道中拷贝取出数据而共享内存就不需要这俩次拷贝处理所以它是最快的数据传输方式。

2、流程

1、创建/打开指定共享内存

2、将内存映射到自己的虚拟地址空间

3、内存操作……

4、解除映射关系

5、删除共享内存

1创建/打开指定共享内存

头文件 #include<sys/shm.h>

int  shmget key_t key size_t size int shmflg

        key共享内存的标识符名字

        size要创建的共享内存大小最好是PAGE_SIZE的整数倍

        shmfgIPC_CREAT | IPC_EXCL | 0664

                IPC_CUEAT如果共享内存不存在则创建打开存在则直接打开

                IPC_EXCL:   与IPC_CREAT搭配使用共享内存不存在则创建打开存在则报错返回

                mode_flags 共享内存的访问权限 0664

        返回值成功返回一个操作句柄非负整数、失败返回-1


2将共享内存映射到当前进程的虚拟地址空间

获取到首地址后就可以通过首地址访问内存中的数据以及可以修改内存中的数据

 头文件 #include<sys/shm.h>

void *shmatint shmidconst void *shmaddr int shmflg

        shmid之前shmget打开共享内存返回的操作句柄

        addr映射首地址通常置为NULL让操作系统进行分配

        shmflag默认为0表示可读可写SHM_RDONLY 表示只读

                        前提该共享内存创建的权限允许

        返回值成功返回映射首地址失败返回void*-1

3解除映射关系

int  shmdtconst void *shmaddr

        shmaddr映射首地址也就是shmat的返回值

4数据交互

对内存内容进行修改或者读取

5删除共享内存

int shmctlint shmid int cmdstruct shmid_ds *buf

        shmid创建共享内存的返回值

        cmd对共享内存指向的操作命令

                使用IPC_RMID标记一个共享内存段需要被删除

        buf当cmd命令为接收获取共享内存信息时的容器删除命令不需要用

真的会实际删除这个共享内存嘛

        不会的正如上面命令所说的只会进行一次标记标记的共享内存将不会接受新的映射而是等当前的映射连接计数为0时再实际删除。

        真正的删除是由系统执行的进程所做的删除操作只是进程标记一下。

3、代码模拟通信

给共享内存中写入数据

#include<stdio.h>
#include<sys/shm.h>
#include<unistd.h>

#define SHM_KEY 0x12345678
int main()
{
    // 创建/打开共享内存
    int shmid = shmget(SHM_KEY, 4096, IPC_CREAT | 0664);
    if(shmid < 0){
      perror("shget error");
      return -1;
    }

    // 进行组织映射
    void *start = shmat(shmid, NULL, 0);
    if(start == (void*)-1){
      perror("shmat error");
      return -1;
    }

    // 进行数据交互
    int id = 0;
    while(1)
   {
      sprintf(start, "已经传输了%d内容到共享内存中\n", id++);
      sleep(1);
    }

    // 解除映射
    shmdt(start);
    shmctl(shmid, IPC_RMID, NULL);
    return 0;
}

使用共享内存读取数据

#include<stdio.h>
#include<sys/shm.h>
#include<unistd.h>

#define SHM_KEY 0x12345678
int main()
{
    // 创建/打开共享内存
    int shmid = shmget(SHM_KEY, 4096, IPC_CREAT | 0664);
    if(shmid < 0){
      perror("shget error");
      return -1;
    }

    // 进行组织映射
    void *start = shmat(shmid, NULL, 0);
    if(start == (void*)-1){
      perror("shmat error");
      return -1;
    }

    // 进行数据交互
    while(1)
    {
      printf("%s\n", start);// 之前进行映射后返回的就是共享内存的地址
      sleep(1);
    }

    // 解除映射
    shmdt(start);
    shmctl(shmid, IPC_RMID, NULL);
    return 0;
}

三、进程间通信所学命令小结

  ipcs 查看进程间通信资源/ipcrm 删除进程间通信资源

  -m 针对共享内存的操作

  -q 针对消息队列的操作

  -s 针对信号量的操作

  -a 针对所有资源的操作

 使用ipcs -m命令获取共享内存通信方式信息

ipcsrm -m shmid  删除shmid对应的共享内存。

注意删除的时候如果没有其他进程访问会直接删除共享内存如果有其他进程访问则会将共享内存键值设置为一个无法访问的量。

ipcs 获取当前进程间通信所有方式信息

底行模式下使用  :12s/shmread/shmwrite/g

将1-2行中的shmread修改为shmwrite。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: linux