IPC进程间通信详解
匿名管道
匿名管道的本质就是内存里一块缓冲区
pipe(int fd[2] );
创建一个struct pipe_inode_info,创建两个struct file,struct file的void* private_data指向struct pipe_inode_info,第一个file绑定读端,第二个file绑定写端,最后将两个file的文件描述符按照读写的顺序返回给int fd[2]数组
struct file {
struct path f_path; // 文件路径(含dentry和挂载点)
void *private_data; // 实际指向 pipe_inode_info(管道专属数据)
};
struct path {
struct dentry *dentry; // 目录项(指向管道文件的dentry)
};
struct dentry {
struct inode *d_inode; // 关联的inode(指向管道inode)
};
struct inode {
struct pipe_inode_info *i_pipe; // 指向管道数据结构
const struct file_operations *i_fop; // 操作函数集(指向fifo_fops)
};
struct pipe_inode_info {
struct mutex mutex; // 互斥锁(保护管道操作)
unsigned int head; // 写入位置
unsigned int tail; // 读取位置
struct pipe_buffer *bufs; // 环形缓冲区数组(存储数据页)
};
1.struct file里的file_operations也是用的inode里面的file_operations,这样就可以实现不同文件类型就有不同的回调函数系统调用,
- 管道:
pipefifo_fops
(实现pipe_read()
/pipe_write()
)。 - 普通文件:
ext4_file_operations
(实现磁盘读写)。 - Socket:
socket_file_ops
(实现网络通信)。
这是一切皆文件的设计思路
2.匿名管道需要进程fork创建子进程,因为匿名管道没有路径无法被其他进程看到,只能通过继承的方式使子进程继承父进程的文件描述符表
3.匿名管道半双工的,只能支持一个方向的通信,双方进程要商量谁保留读,谁保留写。如果要用管道实现全双工,则需要两个管道。为什么要设计半双工呢?因为管道通信的阻塞和唤醒都是由内核来做的,半双工是为了简化设计
4.管道的读写是通过系统调用的,所以数据会被先拷贝进内核缓冲区,再拷贝进管道缓冲区
命名管道
命令行:mkfifo filename
系统调用:int mkfifo(const char* filename, mode_t mode)
mkfifo的行为
struct dentry {
struct inode *d_inode; // 关联的inode(指向管道inode)
struct dentry *d_parent; // 父目录的 dentry
struct qstr d_name; // 文件名(含哈希值)
};
struct inode {
struct pipe_inode_info *i_pipe; // 指向管道数据结构
const struct file_operations *i_fop; // 操作函数集(指向fifo_fops)
};
struct pipe_inode_info {
struct mutex mutex; // 互斥锁(保护管道操作)
unsigned int head; // 写入位置
unsigned int tail; // 读取位置
struct pipe_buffer *bufs; // 环形缓冲区数组(存储数据页)
};
创建对应dentry结构,并创建struct inode和pipe_inode_info,pipe_inode_info里有pipe_buffer缓冲区。有了dentry结构后加入dentry树,然后给父目录里加文件名inode映射,这样命名管道就有了路径
open命名管道的行为
open会根据路径去dentry树里查找fifo,找到之后创建struct file,其void* private_data指向dentry里struct inode里面的pipe_inode_info,struct file里的file_operations也指向struct inode里的file_operations,然后返回文件描述符
1.匿名管道同样有dentry结构,但没有加入dentry树里面,所以无法走路径那一套来创建文件描述符,只能靠继承来传递文件描述符,然后再利用系统调用来实现通信。而命名管道将dengtry结构加入dentry树可以走路径这一套来创建struct file,然后用系统调用来通信,并且路径这一套使通信不再局限在血缘关系进程之间而是所有进程之间
2.命名管道也是半双工的,只能支持一个方向的通信,open方式表明该struct file绑定的读端还是写端。如果要用命名管道实现全双工,则也需要两个管道。为什么要设计半双工呢?因为管道通信的阻塞和唤醒都是由内核来做的,半双工是为了简化设计
3.命名管道的读写也是通过系统调用的,所以数据会被先拷贝进内核缓冲区,再拷贝进管道缓冲区
system V共享内存
共享内存是最快的进程间通信方式,和管道一样,共享内存也是内存里的一块缓冲区,那为什么共享内存比管道快呢?
1.共享内存直接靠页表将物理内存映射到虚拟空间,用虚拟地址来直接操作物理内存,而管道是通过系统调用来操作物理内存,这其中还有一个先将数据拷贝到内核缓冲区的过程
2.管道通信是通过内核系统调用实现的,因此有阻塞唤醒这些进程同步机制,而共享内存不加保护,所以会更快
System V 共享内存的使用流程
(1) 创建/获取共享内存段
#include <sys/shm.h>
key_t key = ftok("/some/file", 'A'); // 根据路径和项目ID生成唯一 key
int shmid = shmget(key, size, IPC_CREAT | 0666); // 创建/获取共享内存
-
shmget
参数:key
:唯一标识符(ftok
生成或手动指定)。size
:共享内存大小(字节)。flags
:IPC_CREAT
(不存在则创建) + 权限(如0666
)。
(2) 映射到进程地址空间
void *shmaddr = shmat(shmid, NULL, 0); // 映射到当前进程
-
shmat
参数:shmid
:shmget
返回的 ID。addr
:通常设为NULL
(由内核选择映射地址)。shmflg
:0
(可读写),或SHM_RDONLY
(只读)。
(3) 使用共享内存
// 进程 A 写入数据
sprintf((char *)shmaddr, "Hello, Shared Memory!");
// 进程 B 读取数据
printf("Data: %s\n", (char *)shmaddr);
- 直接通过指针操作,无需
read/write
。
(4) 解除映射
shmdt(shmaddr); // 解除映射(不删除共享内存)
(5) 删除共享内存(
shmctl(shmid, IPC_RMID, NULL); // 标记为删除(最后一个进程 detach 后真正释放)