IPC方法-管道/fifo/mmap
进程间通信方法:
- 管道(只能血缘关系)/fifo(非血缘关系), 队列只能读一次
- 信号(开销最小)
- 共享内存/共享映射区(非血缘关系, 可反复读取)
- 本地套接字(最复杂最稳定)
不同进程的内核地址空间指向内存的同一块地方, 我们可以用文件描述符表使两个进程修改同一个文件来通信(开销大已废弃), 也可以在内核区内使用一个管道进行通信(有血缘关系间单向流动).
管道
管道是内核缓冲区, 是不占用磁盘空间的伪文件, 读写两端分别对应两个fd, 数据写端流入读端流出. 常用的|(pipe())称作匿名管道, fifo为有名管道. 操作管道的进程被销毁之后, 管道自动被释放. 管道默认是阻塞的(读写操作均是, 所以不需要sleep()), 其数据结构是环形队列(缺点是只能读取一次), 大小默认为4k, 会适当调整.ulimit -a
可以查看管道缓冲区大小.
int pipe(int fd[2])
创建匿名管道, 传出参数为读写两端的文件描述符数组. fd[0]读, fd[1]写. 注意先pipe()后fork(), 使子进程继承父进程的PCB.
示例: 使用pipe函数实现ps aux | grep bash
, 思路是用dup2()把标准输出重定向到写端(ps函数), 然后把标准输入重定向到读端(grep 函数). 由于队列只能读一次且是单向的, 所以读的那一方要关闭写端, 写的那一方要关闭读端(close(fd[0])). 如果需要双向通信, 需要两根管道.
/* 父子进程pipe单向通信 */
int fd[2];
int ret = pipe(fd); // 先pipe后fork, 所以父子进程的fd编号是相同的
if (ret == -1)
{
perror("pipe error");
exit(1);
}
pid_t pid = fork();
if (pid == -1)
{
perror("fork error");
exit(1);
}
if (pid > 0)
{ // 父进程
close(fd[0]); // 写管道, 关闭读端
dup2(fd[1], STDOUT_FILENO); // 标准输出重定向到写端
execlp("ps", "ps", "aux", NULL);
perror("execlp"); // exec函数族正确时候无返回值, 错误才返回, 所以不用判断
exit(1);
}
else if (pid == 0)
{
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("grep", "grep", "--color=auto", "bash", NULL);
perror("execlp");
exit(1);
}
close(fd[0]);
close(fd[1]);
return 0;
如果是兄弟进程间通信, 父进程的读写两端都要关闭, 只负责回收PCB.
示例: 兄弟进程使用pipe函数实现ps aux | grep bash
, 先循环创建子进程, 通过i而不是pid来判断父子进程, 子进程1执行ps, 子进程2执行grep, 内部代码不变, 父进程把fd[0]和fd[1]都关闭并回收子进程.
/* 兄弟进程pipe单向通信, 并由父进程回收 */
int i;
for (i=0; i<2; i++)
{
pid_t pid = fork();
if (pid == -1)
{
perror("fork error");
exit(1);
}
else if (pid == 0)
break;
}
if (i == 0) { 子进程1执行ps, 同上 }
else if (i == 1) { 子进程2执行grep, 同上 }
else if (i == 2) {
// 父进程关闭读写端, 回收子进程
close(fd[0]);
close(fd[1]);
// 轮询非阻塞回收子进程, wpid==0说明子进程正在运行,
// 如果wpid==-1说明子进程已被回收完毕.
pid_t = wpid;
while((wpid = waitpid(-1, NULL, WNOHANG)) != -1)
{
if (wpid == 0) continue;
printf("child died, pid = %d\n", wpid);
}
}
管道读写行为
读管道: 1. 管道中有数据, read返回读到的字节数. 2. 管道中无数据时, 若写端全关闭, read返回0; 若仍有写端打开, 阻塞等待.
写管道: 1. 读端全关闭, 进程异常终止(SIGPIPE信号). 2. 有读端打开时, 若管道未满, 继续写数据返回字节数; 若管道已满, 阻塞(少见).
设置读端为非阻塞
默认两端都是阻塞的, 这里以读端示例改为非阻塞, 使用fcntl()函数
//获取原来的flags
int flags = fcntl(fd[0], F_GETFL);
//给flags添加新的属性, |=类似于+=, 或操作后再赋值
flags |= O_NONBLOCK
//设置新的flags
fcntl(fd[0], F_SETFL, flags);
fifo
fifo也叫有名管道, 适用于无血缘关系的进程间通信. fifo是一个伪文件(属性为p, 即第一列d/-位置的字符), 其大小永远为0, 并在内核中映射为一个对应的缓冲区. 多个进程使用的fifo必须在同一个目录下, fifo可以有多个读端和多个写端.
创建方式: 1. 命令mkfifo 管道名
, 2. 函数mkfifo(pathname, mode)
, 真实权限为(mode & ~umask).
操作时需要先open(因为是文件), 可以执行read/write操作, 但是不能执行lseek操作(因为是伪文件).
// 非血缘关系进程通信
//先创建fifo文件myfifo
mkfifo("myfifo", 0664);
//然后A进程a.c里执行读操作
int fd = open("myfifo", O_RDONLY);
read(fd, buf, sizeof(buf));
close(fd);
//之后B进程b.c里执行写操作
int fd1 = open("myfifo", O_WRONLY);
write(fd1, "hello,world", 11);
close(fd1);
mmap()
void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset)
- addr映射区首地址由内核MMU指定, 直接填NULL.
- length是映射区大小, 不可以为0, 所以新创建的文件必须通过lseek或ftruncate函数来拓展. 如果是已创建的文件, 映射区也要小于该文件的大小.
- prot即映射区权限, 有三种PROT_READ, PROT_WRITE, PROT_READ|PROT_WRITE. 当flags为MAP_SHARED时, 映射区权限必须小于等于文件open时的权限, 且open必须包含读权限(因为mmap创建时会隐含着对文件读的操作). 当flags为MAP_PRIVATE时, 映射区权限可以大于文件权限, 但文件仍需要有读权限.
- flags设置映射区所做修改是否会反映到物理设备上(磁盘文件), MAP_SHARED会, MAP_PRIVATE不会.
- fd即用来建立映射区的文件.
- offset是映射文件的偏移, 必须为4k的整数倍(一页的大小), 表示截取文件一部分进行映射, 0是不偏移.
mmap()函数成功返回映射区首地址, 失败返回MAP_FAILED. 有了首地址就可以通过指针操作映射区(如映射区声明为char*时可以用strcpy赋值).
使用munmap(p, len)
释放映射区, p必须为首地址(不能p++), 失败返回-1, 成功返回0. 文件描述符可以先于mmap映射区关闭, 映射区一旦创建成功就可以操作指针来访问, 不再需要文件的句柄.
进程间通信用来创立映射区的一般是临时文件, 对于临时文件, 可以使用unlink("temp")
删除临时文件目录项, 使文件可以在进程不占用后被释放. 目录项(dentry)包含文件名和inode编号, 通过inode编号可以找到inode文件, inode文件中stat结构体包含了存储指针地址, 再通过该指针找到文件在磁盘上的位置. 目录项的本质就是硬链接.
如果不想使用临时文件, 可以使用匿名映射区. flags参数改为MAP_SHARED | MAP_ANONYMOUS, fd参数改为-1. 该宏为linux独有, 在类Unix系统(mac)上, 可以使用/dev/zero文件作为fd传入, 可以提供任意大小的空间. 与之对应的是/dev/null, 可以往里写无限的内容(无底洞).
父子进程只能共享: 1. 打开的文件, 2. mmap建立的映射区(但必须使用MAP_SHARED). 子进程改变映射区的首地址后, 父进程是同步改变的, 所以二者可以共同操作映射区, 其他的全局变量等并不共享.
对于非血缘关系进程通信, 写进程先执行, 拥有读写权限; 读进程后执行, 只有读的权限. 写进程可以使用如memcpy(p, &student, sizeof(student))
等操作内存的函数进行写入.