IPC 是 Linux 编程中一个重要的概念,IPC 有多种方式,常用的 IPC 方式有管道、消息队列、共享内存等,但其实使用广大程序员都熟悉的文件也是可以完成 IPC 的,本文介绍如何使用共享文件实现进程间通信,本文给出了具体的实例,并附有完整的源代码;本文实例在 Ubuntu 20.04 上编译测试通过,gcc版本号为:9.4.0;本文的实例中涉及多进程编程、文件锁等概念,所以对 Linux 编程的初学者有一些难度,但对于了解 Linux 下共享文件,特别是文件锁的应用,将是非常难得的。
文件操作是一个程序员的必备技能,相比较 IPC 的各种方法(比如:管道、消息队列、共享内存等),程序员显然更熟悉文件的操作;
那么,能不能使用文件实现进程间通信呢?答案时肯定的,多个进程共享一个文件同样可以完成进程间通信;
首先描述一个场景:
Server/Client
模式,一个服务端进程,三个客户端进程;使用共享文件实现 IPC,其实就是发送方将消息写入文件,接收方再从相同的文件中读出,看起来十分简单,但在多进程环境中,并不像看起来的那么简单;
使用共享文件进行 IPC 时,有两个比较麻烦的地方,一个是文件指针,另一个是文件锁机制;
先说文件指针问题:
再说文件锁机制:
使用共享文件进行 IPC 并不是一种常用的方式,在编程实践中很少这样去做,其实际运行时是有真实的文件 I/O 发生的,也就是其通信过程会真实的写入到文件系统中,如果通信频繁、信息量大且持续时间长,有可能在磁盘上产生一个很大的物理文件;
很显然,使用共享文件进行 IPC 的运行效率也是不高的,但仍然不失为一种 IPC 方法,而且相关的编程实践对理解 Linux 的共享文件及文件锁机制将会非常有帮助。
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
相同的指令,但当文件锁被其它进程占有时,会产生阻塞,直到获得该文件锁;其中,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. */
};
struct flock
结构不仅定义了锁的类型,同时,l_start
和 l_len
两个字段还定义了文件中那一部分被这个文件锁锁定;l_start
开始,长度为 l_len
字节的区域,l_len
为 0 表示从 l_start
开始到文件结束;l_start
偏移量计算的起始位置,可以有三个选项:
l_start
的偏移量,此时 l_start
必须是一个正整数;l_start
的偏移量,此时,l_start
可以为负整数,但不能跑到文件起始位置之前;l_start
的偏移量,此时,l_start
为负整数或者 0;在大多数的应用中,无需仅锁定文件的一部分,锁定整个文件即可,也就是 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_SETLK
和 F_SETLKW
的唯一区别是一个不阻塞直接返回,另一个阻塞直到获得所请求的文件锁;
man fcntl
可以查看该函数的在线手册;
正如第 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 表示该信息的含义,目前有五个可选值:
各个进程在向共享文件写入数据时,均要求以 struct ipc_msg
格式写入,分下面几个步骤完成:
struct ipc_msg
分配内存,如果有 ipc_msg.msg
字段,则分配的内存要包含 ipc_msg.msg
字符串的长度;ipc_msg.msg
最后的 \0
字符,将消息长度填写到 ipc_msg.len
字段中;ipc_msg.src_pid
字段;ipc_msg.dest_pid
字段,如果是广播消息,该字段填 BROADCAST_PROCESS_ID
;ipc_msg.seq_num
字段,ipc_msg.cmd
字段;ipc_msg.msg
,将字符串写入 ipc_msg.msg
中;struct ipc_msg
写入共享文件;struct ipc_msg
分配的内存;各进程在读入数据时,要遵循下面步骤:
struct ipc_msg
中的 len 字段,然后根据 len 字段的值读取剩余的数据;dest_pid
字段是否为自身的 PID 或者 BROADCAST_PROCESS_ID
,否则丢弃该消息,转到步骤 1 读取下一个消息;源程序:ipc-files.c(点击文件名下载源程序)演示了如何使用共享文件实现进程间通信;
编译:gcc -Wall -g ipc-files.c -o ipc-files
运行:./ipc-files
运行动图: