IPC之十:使用共享文件进行进程间通信的实例

IPC 是 Linux 编程中一个重要的概念,IPC 有多种方式,常用的 IPC 方式有管道、消息队列、共享内存等,但其实使用广大程序员都熟悉的文件也是可以完成 IPC 的,本文介绍如何使用共享文件实现进程间通信,本文给出了具体的实例,并附有完整的源代码;本文实例在 Ubuntu 20.04 上编译测试通过,gcc版本号为:9.4.0;本文的实例中涉及多进程编程、文件锁等概念,所以对 Linux 编程的初学者有一些难度,但对于了解 Linux 下共享文件,特别是文件锁的应用,将是非常难得的。

1 使用共享文件实现IPC的基本概念

  • 文件操作是一个程序员的必备技能,相比较 IPC 的各种方法(比如:管道、消息队列、共享内存等),程序员显然更熟悉文件的操作;

  • 那么,能不能使用文件实现进程间通信呢?答案时肯定的,多个进程共享一个文件同样可以完成进程间通信;

  • 首先描述一个场景:

    • Server/Client 模式,一个服务端进程,三个客户端进程;
    • 进程间通信时,以每个进程的 PID 作为通信地址的唯一标识
    • 客户端只与服务端进程进行通信,客户端进程之间不进行通信;
  • 使用共享文件实现 IPC,其实就是发送方将消息写入文件,接收方再从相同的文件中读出,看起来十分简单,但在多进程环境中,并不像看起来的那么简单;

  • 使用共享文件进行 IPC 时,有两个比较麻烦的地方,一个是文件指针,另一个是文件锁机制;

  • 先说文件指针问题:

    • 当一个文件被打开时,其文件指针的偏移为 0,当读出 10 个字节时,其文件指针偏移将增加 10;
    • 写入文件时,会从当前文件指针处写入文件,当写入 10 个字节后,其文件指针偏移将增加 10;
    • 一般读出需要从文件头顺序读取,但是写入需要向文件的尾部写入,所以如果一个进程中对同一个共享文件既有读操作又有写操作时,文件指针将比较混乱;
    • 这种混乱还表现在可能还有其它进程对共享文件进行写操作,导致你期望的文件指针与实际有所不同;
    • 为了避免这种文件指针的混乱,通常在一个进程中对同一个共享文件仅做读操作或者仅做写操作;
    • 对于我们上面描述的 IPC 场景,服务端需要接收客户端的消息并做出回应,通常我们要使用两个共享文件,一个文件服务端仅做读操作,客户端仅做写操作,用于客户端向服务端传递消息,另一个文件服务端仅做写操作,客户端仅做读操作,用于服务端向客户端传递消息;
  • 再说文件锁机制:

    • 当多个进程同时对一个文件进行写操作时,很明显是会有冲突的,假定进程 1 要写入 100 个字节,进程 2 要写入 50 个字节,可能进程 1 写入完 30 个字节时,产生了进程调度,使进程 2 开始向文件写入数据,从而导致写入数据的混乱;
    • 当一个进程对文件进行写入操作时,如果有另一个进程正在读数据,也是有冲突的,假定写进程要写入 100 个字节,写入 30 个字节时,产生进程调度,读进程开始读文件,读出了刚刚写入的 30 个字节,而这 30 个字节是要写入的 100 个字节中的一部分,是不完整的数据;
    • 所以,当一个进程对一个共享文件进行写操作时,需要独占该文件,也就是同时不能有其它进程对该文件进行读写操作;
    • 当一个进程对一个文件进行读操作时,当然不能允许有其它进程进行写操作,但可以允许其它进程进行读操作;
    • 这种对文件的占有机制又叫做文件锁机制,我们在下一节会做专门的介绍;
  • 使用共享文件进行 IPC 并不是一种常用的方式,在编程实践中很少这样去做,其实际运行时是有真实的文件 I/O 发生的,也就是其通信过程会真实的写入到文件系统中,如果通信频繁、信息量大且持续时间长,有可能在磁盘上产生一个很大的物理文件;

  • 很显然,使用共享文件进行 IPC 的运行效率也是不高的,但仍然不失为一种 IPC 方法,而且相关的编程实践对理解 Linux 的共享文件及文件锁机制将会非常有帮助。

2 文件锁及其操作

  • fcntl() 函数可以对文件进行加锁操作;

  • fcntl() 可以对一个文件描述符做很多操作,在此,我们仅介绍其符合 POSIX 标准部分,与文件“锁”相关的调用方法;

  • 下面是 fcntl() 的调用方法:

    #include <unistd.h>
    #include <fcntl.h>
    
    int fcntl(int fd, int cmd, ... /* arg */ );
    
  • fcntl() 是一个不定参数的调用函数,但对于 POSIX 的文件锁而言,它只有三个参数:

    int fcntl(int fd, int cmd, (struct flock *)lock);
    
  • 在这个调用中,fd 是一个已经打开的文件描述符,cmd 是要执行的命令;

  • POSIX 与文件锁相关的命令有三个:

    • F_SETLK:获取文件锁或者释放文件锁,如果文件锁已被其它进程占有会立即返回错误;
    • F_SETLKW:执行与 F_SETLK 相同的指令,但当文件锁被其它进程占有时,会产生阻塞,直到获得该文件锁;
    • F_GETLK:获取当前文件锁状态;
  • 其中,struct flock 的定义如下:

    struct flock {
        short l_type;   /* Type of lock: F_RDLCK, F_WRLCK, F_UNLCK */
        short l_whence; /* How to interpret l_start: SEEK_SET, SEEK_CUR, SEEK_END */
        off_t l_start;  /* Starting offset for lock */
        off_t l_len;    /* Number of bytes to lock */
        pid_t l_pid;    /* Process holding the lock. */
    };
    
    • POSIX 文件锁可以分为读文件锁和写文件锁两种;
    • POSIX 规定文件锁可以仅锁定文件中的一部分,而不是锁定整个文件,struct flock 结构不仅定义了锁的类型,同时,l_startl_len 两个字段还定义了文件中那一部分被这个文件锁锁定;
    • l_type:锁类型,F_RDLCK - 读文件锁,F_WRLCK - 写文件锁,F_UNLCK - 释放文件锁;
    • l_startl_len:该文件锁仅锁定从偏移量 l_start 开始,长度为 l_len 字节的区域,l_len 为 0 表示从 l_start 开始到文件结束;
    • l_whencel_start 偏移量计算的起始位置,可以有三个选项:
      • SEEK_SET:从文件的开始计算 l_start 的偏移量,此时 l_start 必须是一个正整数;
      • SEEK_CUR:从当前文件指针处计算 l_start 的偏移量,此时,l_start 可以为负整数,但不能跑到文件起始位置之前;
      • SEEK_END:从文件尾部计算 l_start 的偏移量,此时,l_start 为负整数或者 0;
    • l_pid:在调用 F_GETLK 获取当前文件锁状态时,如果文件锁被其它进程占用,该字段将返回占用文件锁的进程号;
  • 在大多数的应用中,无需仅锁定文件的一部分,锁定整个文件即可,也就是 l_wence=SEEK_SET; l_start=0; l_len=0

  • 下面代码片段在文件 fd 上获取写文件锁:

    ......
    struct flock lock;
    
    lock.l_tyepe = F_WRLCK;
    lock.l_wence = SEEK_SET;
    lock.l_start = 0;
    lock.l_len = 0;
    
    fcntl(fd, F_SETLKW, &lock);
    ......
    
  • 下面代码片段释放了一个文件锁:

    ......
    struct flock lock;
    
    lock.l_tyepe = F_UNLCK;
    lock.l_wence = SEEK_SET;
    lock.l_start = 0;
    lock.l_len = 0;
    
    fcntl(fd, F_SETLKW, &lock);
    ......
    
  • 命令 F_SETLKF_SETLKW 的唯一区别是一个不阻塞直接返回,另一个阻塞直到获得所请求的文件锁;

  • man fcntl 可以查看该函数的在线手册;

3 实例

  • 正如第 1 节中描述的场景一样,该实例建立一个服务端进程,三个客户端进程,模拟一个 client/server 架构的服务过程;

  • 正如第 1 节介绍的一样,需要使用两个共享文件实现客户端进程与服务端进程之间的通信,从服务端进程看,一个文件用于服务端读取客户端的消息,另一个文件用于服务端向客户端发送消息;

  • 两个共享文件由服务端进程建立,服务端进程要最先开始运行,否则客户端进程无法打开共享文件;

  • 整个通信过程以每个进程的进程号作为唯一地址标识,当目的进程号为 0 时表示是一条广播消息,所有进程都要接收并处理;

  • 客户端进程启动时,需要知道服务端进程的 PID 才可以与服务端进行通信,此时要发出一条广播消息,服务端进程收到后回应一条消息从而建立通信通道;

  • 客户端在空闲时循环向服务端发送一个字符串,服务端在收到后回应一个确认消息,模拟一个服务端为客户端提供服务的过程;

  • 服务端向多个客户端进程发送消息时使用同一个共享文件,所以每个客户端进程要具备过滤地址的功能,即:只保留发给自己的消息,丢弃发给其它客户端进程的消息;

  • 因为多个客户端进程都要向同一个共享文件中写入数据(即向服务端发送消息),每次写入时应该写在文件的尾部,但对每个进程而言,当前的文件指针不一定是在文件的尾部,所以在获取了文件写入锁以后,需要将文件指针移动的文件的尾部才能写入数据;

  • 为了通信方便,在传送信息时,所有进程使用下面的统一结构:

    struct ipc_msg {
        int len;            // total length including itself
        int src_pid;        // source PID
        int dest_pid;       // destination PID
        uint seq_num;       // sequence number of the current message
        ushort cmd;         // command code
        char msg[1];        // the auxiliary information
    };
    
  • len 为整个信息的总长度,包括 len 字段自身,接收端首先接收该字段,然后确定该信息后面还需要读取的字节数,再一次性地读取完整个结构;

  • src_pid 为发送该信息的进程 PID;

  • dest-pid 为接收该信息的进程 PID,当该字段为 0 时,表示该信息为广播消息,所以,一个进程应该接收该字段为自身 PID 或者该字段为 0 的消息,并丢弃其它消息;

  • cmd 表示该信息的含义,目前有五个可选值:

    1. CMD_SERVER_ONLINE - 表示服务端在线,客户端在启动后并不知道服务端进程的 PID,所以应该周期性地广播 CMD_SERVER_STATUS 消息,服务端进程收到该广播消息后,向相应的客户端进程发送 CMD_SERVER_ONLINE 消息,客户端收到该消息便可获知服务端进程的 PID,从而建立通信通道;
    2. CMD_SERVER_OFFLINE - 表示服务端离线,当服务端准备退出时,广播该信息,客户端在收到该消息时,应主动退出;
    3. CMD_SERVER_STATUS - 客户端进程启动后广播该信息,服务端进程收到该信息应回复 CMD_SERVER_ONLINE,从而使客户端获得服务端进程的 PID;
    4. CMD_STRING - 客户端在空闲时定期向服务端进程发送一个字符串,以模拟客户端进程向服务端进程请求服务的过程,发送此消息时,字符串应放在 msg 字段中,所以这个消息的长度是不定长的,在实际的应用中,这个字符串可以是一个 json 数据,可以实现复杂的服务请求;
    5. CMD_STRING_OK - 服务端在收到客户端进程发送的 CMD_STRING 消息后,回应一个 CMD_STRING_OK 消息,模拟对客户端请求服务的响应;
  • 各个进程在向共享文件写入数据时,均要求以 struct ipc_msg 格式写入,分下面几个步骤完成:

    1. struct ipc_msg 分配内存,如果有 ipc_msg.msg 字段,则分配的内存要包含 ipc_msg.msg 字符串的长度;
    2. 计算整个消息的长度,长度应包括 ipc_msg.msg 最后的 \0 字符,将消息长度填写到 ipc_msg.len 字段中;
    3. 将当前进程的 PID 写入到 ipc_msg.src_pid 字段;
    4. 将接收进程的 PID 写入到 ipc_msg.dest_pid 字段,如果是广播消息,该字段填 BROADCAST_PROCESS_ID
    5. 将消息序列号写入到 ipc_msg.seq_num 字段,
    6. 根据情况填写 ipc_msg.cmd 字段;
    7. 如果有 ipc_msg.msg,将字符串写入 ipc_msg.msg 中;
    8. struct ipc_msg 写入共享文件;
    9. 释放为 struct ipc_msg 分配的内存;
  • 各进程在读入数据时,要遵循下面步骤:

    1. 首先读取一个 int,此为 struct ipc_msg 中的 len 字段,然后根据 len 字段的值读取剩余的数据;
    2. 检查 dest_pid 字段是否为自身的 PID 或者 BROADCAST_PROCESS_ID,否则丢弃该消息,转到步骤 1 读取下一个消息;
    3. 根据消息内容做出回应;
  • 源程序:ipc-files.c(点击文件名下载源程序)演示了如何使用共享文件实现进程间通信;

  • 编译:gcc -Wall -g ipc-files.c -o ipc-files

  • 运行:./ipc-files

  • 运行动图:

    screenshot of running ipc-files

欢迎订阅 『进程间通信专栏』


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/270772.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

在线文稿演示应用PPTist

PPTist 是一个基于 Vue3.x TypeScript 的在线演示文稿&#xff08;幻灯片&#xff09;应用&#xff0c;还原了大部分 Office PowerPoint 常用功能&#xff0c;支持文字、图片、形状、线条、图表、表格、视频、音频、公式等几种最常用的元素类型&#xff0c;每一种元素都拥有高…

【bug】uniapp一键登录,自定义协议条款是否支持内部路由?

uniapp一键登录的自定义协议条款&#xff0c;不支持内部路由跳转 在uniapp文档上搜一键登录 加二维码之后可以提问

助力打造清洁环境,基于轻量级YOLOv8开发构建公共场景下垃圾堆放垃圾桶溢出检测识别系统

公共社区环境生活垃圾基本上是我们每个人每天几乎都无法避免的一个问题&#xff0c;公共环境下垃圾投放点都会有固定的值班时间&#xff0c;但是考虑到实际扔垃圾的无规律性&#xff0c;往往会出现在无人值守的时段内垃圾堆放垃圾桶溢出等问题&#xff0c;有些容易扩散的垃圾比…

Flink电商实时数仓(六)

交易域支付成功事务事实表 从topic_db业务数据中筛选支付成功的数据从dwd_trade_order_detail主题中读取订单事实数据、LookUp字典表关联三张表形成支付成功宽表写入 Kafka 支付成功主题 执行步骤 设置ttl&#xff0c;通过Interval join实现左右流的状态管理获取下单明细数据…

支付宝、学习强国小程序input、textarea数据双向绑定

前言 和 vue 的绑定有些区别&#xff0c;需要注意。直接 value"{{inputValue}}" 是无法双向绑定的。 正确思路 文档说的比较详细&#xff0c;不过没有组合使用的案例&#xff0c;需要自行理解。这里正确的方法是先用 value 绑定数据&#xff0c;再使用 onInput 事件…

(GCC) 库的操作

文章目录 预备静态库生成链接环境区别 动态库生成链接环境区别 END参考ar指令 预备 准备两个文件&#xff0c;以最简单的形式进行展示。 add.c int add(int x, int y) {return x y; }main.c 为了方便直接在头文件中声明函数 #include <stdio.h>extern int add(int,…

【C++初阶】九、STL容器中的string类(上)

相关代码gitee自取&#xff1a; C语言学习日记: 加油努力 (gitee.com) 接上期&#xff1a; 【C初阶】八、初识模板&#xff08;泛型编程、函数模板、类模板&#xff09;-CSDN博客 目录 一 . STL简介 什么是STL STL的版本 HP 原始版本&#xff1a; P.J. 版本&#xff1a; R…

HBase基础知识(四):HBase API

HBase还提供了API&#xff0c;我们可以通过编程的方式来进行对HBase的操作。 1. 环境准备 新建项目后在 pom.xml 中添加依赖&#xff1a; <dependencies><dependency><groupId>org.apache.hbase</groupId><artifactId>hbase-server</artifa…

2023年教程汇总 | 《小杜的生信笔记》

2023年总结 2023年即将结束&#xff0c;我们即将迎来2024年。2023年&#xff0c;我们做了什么呢&#xff1f;&#xff1f;这个是个值得深思的问题…? 12月份是个快乐且痛苦时间节点。前一段时间&#xff0c;单位需要提交2023年工作总结&#xff0c;真的是憋了好久才可以下笔…

重塑资产管理:三叠云助力企业高效运营

资产管理是企业管理中不可或缺的一环&#xff0c;它对企业的经营管理和决策起着至关重要的作用。资产管理涉及到企业的固定资产、无形资产、流动资产等各类资产&#xff0c;它们的管理情况影响着企业的经济效益和运营效率。因此&#xff0c;企业需要建立一套完善的资产管理体系…

Apache Commons Math: 面向Java的数学和统计库

第1章&#xff1a;引言 大家好&#xff0c;我是小黑&#xff0c;咱们今天要聊的是Apache Commons Math这个宝藏级的Java库。为啥说它是宝藏呢&#xff1f;因为它简直就是处理数学问题的瑞士军刀&#xff0c;无论你是要搞统计分析、数值计算&#xff0c;还是解决优化问题&#…

geyser互通服基岩版进不去

Java版需要在服务器安全组开通TCP端口&#xff08;如果有宝塔&#xff0c;也需要开通&#xff09; geyser下载好的安装运行也需要开通端口&#xff0c;但是它是UDP的&#xff08;但是我同时也开启了TCP&#xff0c;可能不需要&#xff1f; Java 版玩家隧道 Java 版玩家使用 T…

VMware安装linux系统二

1、设置光驱 1.1、编辑虚拟机设置 1.2、设置虚拟机镜像 1.3、设置好后开机 2、安装Linux系统 2.1、等待安装 2.2、开始安装 2.3、选择语言&#xff0c;我选择中文 2.4、本地化不用改 2.5、软件选择一定要选&#xff0c;否则就会是默认最小安装 2.6、我这里选择的是带GUI的&am…

Windows下安装Oracle19C

官网下载oracle19c 以及客户端 官网地址&#xff1a;Software Download | Oracle 这个是要登录账号的,没有的可以注册,登录上 这个时候在点开这个官网:Database Software Downloads | Oracle 往下面滑 点了之后有个界面注意事项勾上,点下载,你就会下载: 安装oracle19c 解压安…

WebRTC概念

定义 一个实时通信标准 通话原理 媒体协商 在WebRTC中&#xff0c;参与视频通讯的双方必须先交换SDP信息&#xff0c;获得一个都支持的编码格式 网络协商 目的&#xff1a;找到一条相互通讯的链路 做法&#xff1a;获取外网IP地址映射&#xff0c;通过信令服务器交换“网…

RK3588平台开发系列讲解(AI 篇)RKNN-Toolkit2 模型的加载转换

文章目录 一、Caffe 模型加载接口二、TensorFlow 模型加载接口三、TensorFlowLite 模型加载接口四、ONNX 模型加载五、DarkNet 模型加载接口六、PyTorch 模型加载接口沉淀、分享、成长,让自己和他人都能有所收获!😄 📢 RKNN-Toolkit2 目前支持 Caffe、TensorFlow、Tensor…

【Java JVM】Java 实例对象的访问定位

Java 程序会通过栈上的 reference 数据来操作堆上的具体对象。 但是 reference 类型在《Java虚拟机规范》里面只规定了它是一个指向对象的引用, 并没有定义这个引用应该通过什么方式去定位, 访问到堆中对象的具体位置, 所以对象访问方式也是由虚拟机实现而定的&#xff0c;主流…

【MYSQL】MYSQL 的学习教程(七)之 慢 SQL 优化思

1. 慢 SQL 优化思路 慢查询日志记录慢 SQLexplain 分析 SQL 的执行计划profile 分析执行耗时Optimizer Trace 分析详情确定问题并采用相应的措施 1. 慢查询日志记录慢 SQL 如何定位慢SQL呢&#xff1f; 我们可以通过 慢查询日志 来查看慢 SQL。 ①&#xff1a;开启慢查询日志…

Django 访问前端页面一直在转异常:ReferenceError:axios is not defined

访问&#xff1a;http://127.0.0.1:8080/ my.html 一、异常&#xff1a; 二、原因 提示&#xff1a;axios找不到&#xff01;&#xff01; 查看代码<script src"https://unpkg.com/axios/dist/axios.min.js"></script>无法访问到官网 三、解决 Using j…

Opencv学习笔记(二)图像基本操作

图像基本操作 一、边界填充 二、图像融合 三、图像阈值 四、图像平滑 五、形态学预算 1、腐蚀操作 2、膨胀操作 3、开闭运算操作 4、梯度运算 5、顶帽运算 6、黑帽运算 一、边界填充 cv2.copyMakeBorder(img, top_size, bottom_size, left_size, right_size, borde…