IPC方法(Inter-Process Communication)
Linux 环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核。在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,Inter-Process Communication)
常用的进程间通信方式:
- 管道(最简单,但需要血缘关系,也叫匿名管道)
- FIFO(管道的一种,无需血缘关系,也叫命名管道)
- 信号(开销最小)
- 共享映射区mmap(无血缘关系)
- 本地套接字socket(最稳定)
管道
管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。
- 原理:管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现
- 特性:
- 属于伪文件,本质是内核缓冲区
- 由两个文件描述符引用,一个表示读端,一个表示写端
- 数据从管道的写端流入管道,从读端流出
- 局限性:
- 进程不能自己写数据,再自己读
- 管道中数据不能反复读取,一旦读走,数据就没有了
- 半双工通信方式,在同一时间数据只能单方向流动
- 只能在有血缘关系的进程间可用(父子、兄弟)
ps:使用命令ulimit -a
可以获取管道缓冲区大小,默认为4KB
pipe函数
int pipe(int fd[2]);
创建并打开管道
- 传出参数:
- fd[0]:读端
- fd[1]:写端
- 返回值:
- 成功:0
- 失败:-1,errno
管道的读写行为
读管道:
- 管道有数据:read返回实际读到的字节数
- 管道无数据:
- 无写端:read返回0(类似于读到文件尾)
- 有写端:read阻塞等待(等着写端的数据流过来)
写管道:
- 无读端:异常终止(SIGPIPE信号导致)
- 有读端:
- 管道已满:阻塞等待
- 管道未满:返回写出的字节个数
编程练习:
使用管道实现父子进程间通信,完成:ls | wc -l
,假定父进程实现ls
,子进程实现wc
提示:ls
命令正常会将结果写到stdout,但现在要写入管道的写端;wc -l
正常应该从stdin读取数据,但现在要从管道的读端读
完整代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<pthread.h>
//使用管道实现父子进程间通信,完成:ls | wc -l
//假定父进程实现ls,子进程实现wc
int main(int argc, char *argv[]) {
int ret;
int fd[2];
ret = pipe(fd); //先创建管道,再fork进程,这样父子进程都拥有了这个管道
if(ret == -1) {
perror("pipe error:");
exit(1);
}
pid_t pid;
pid = fork(); //现在父子进程都掌管着同一个管道
//因此父进程要关闭读端,子进程关闭写端
if(pid > 0) {
close(fd[0]);
dup2(fd[1], STDOUT_FILENO); //重定向
execlp("ls", "ls", NULL);
} else if(pid == 0) {
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("wc", "wc", "-l", NULL);
} else {
perror("fork error:");
}
return 0;
}
FIFO
FIFO是一种特殊的管道,常被称为命名管道,可以用于无血缘关系的进程间通信
创建命名管道:mkfifo
(命令或函数都可以)
实现非血缘关系进程间通信:把fifo当成文件操作即可
- 读端:
open fifo O_RDONLY
- 写端:
open fifo O_WRONLY
共享存储映射(mmap)
文件实现进程间通信
打开的文件是内核中的一块缓冲区,多个无血缘关系的进程可以同时访问该文件
存储映射I/O
存储映射I/O使一个磁盘文件与存储空间的一个缓冲区相映射(目的:当文件在磁盘上时,只能使用write
、read
等系统调用函数,而映射到存储空间上,操作文件(指针)可用的函数就很多了,比如strcpy
、memcmp
函数等等)
创建/释放共享内存映射区
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
创建共享内存映射
- 参数:
- addr:指定映射区的首地址(通常传NULL,表示让系统自动分配)
- length:表示共享内存映射区的大小(<= 文件的实际大小)
- prot:共享内存映射区的读写属性
- PROT_READ
- PROT_WRITE
- PROT_READ|PROT_WRITE
- flags:标注共享内存的共享属性
- MAP_SHARED
- MAP_PRIVATE
- MAP_ANONYMOUS
- fd:用于创建共享内存映射区的那个文件的文件描述符
- offset:偏移位置,必须是 4k 的整数倍(默认为0,表示映射文件全部)
- 返回值:
- 成功:映射区的首地址
- 失败:MAP_FAILED (void*(-1)), errno
int munmap(void *addr, size_t length);
释放共享内存映射区
- 参数:
- addr:
mmap
函数的返回值 - length:共享内存映射区的大小
- addr:
注意事项:
- 用于创建映射区的文件大小为0,实际创建非0大小的映射区,报错“总线错误”
- 用于创建映射区的文件大小为0,实际创建0大小的映射区,报错“无效参数”
- 用于创建映射区的文件读写属性为只读,映射区属性为读、写,报错“无效参数”
- 创建映射区需要那个文件有读权限,只有写权限是不行的。当访问权限指定为MAP_SHARED时, mmap的读写权限应该 <=文件的open权限
- 文件描述符fd在mmap创建映射区完成后即可关闭,后续访问文件是用地址访问的
- offset偏移量必须是 4096的整数倍(因为MMU映射的最小单位是4k)
- 对申请的映射区内存,不能越界访问
- munmap用于释放的地址,必须是mmap申请返回的地址
- 映射区访问权限为MAP_PRIVATE,对内存所做的所有修改只在内存上生效,不会映射到磁盘上
- 映射区访问权限为MAP_PRIVATE,只需要open文件时有读权限即可,用于创建映射区
- mmap创建映射区出错率很高,因此一定要检查函数返回值,确保映射区建立成功再进行后续操作
mmap函数的保险调用方式:
fd = open("文件名", O_RDWR);
mmap(NULL, 有效文件大小, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
编程练习:创建共享内存映射区进行文件读写
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/stat.h>
#include <dirent.h>
#include <errno.h>
#include <sys/mman.h>
void sys_err(const char *str) {
perror(str);
exit(1);
}
int main(int argc, char *argv[]) {
char *p = NULL;
int fd;
fd = open("testmap", O_RDWR | O_CREAT | O_TRUNC, 0644);
if(fd == -1) {
sys_err("open error");
}
ftruncate(fd, 20); //将文件大小截断为20字节,需要写权限
int len = lseek(fd, 0, SEEK_END); //获取文件当前大小
p = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(p == MAP_FAILED) {
sys_err("mmap error");
}
close(fd);
//这样就可以使用p对文件进行读写操作
strcpy(p, "hello mmap"); //写
printf("---%s\n", p); //读
int ret = munmap(p, len);
if(ret == -1) {
sys_err("munmap error");
}
return 0;
}
mmap父子进程通信
父进程先创建映射区:open(…, O_RDWR); mmap(…, MAP_SHARED, …);
fork()再创建子进程
一个进程读, 另外一个进程写
※mmap无血缘关系进程间通信【必须会写】
两个进程打开同一个文件,创建映射区,指定参数flags为MAP_SHARED
一个进程写入,另外一个进程读出
mmap_w.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <dirent.h>
#include <errno.h>
struct student {
int id;
char name[256];
int age;
};
void sys_err(const char *str) {
perror(str);
exit(1);
}
int main(int argc, char *argv[]) {
struct student stu = {1, "guapi", 18};
struct student *p;
int fd = 0;
fd = open("test_brommp", O_RDWR | O_CREAT | O_TRUNC, 0664);
if (fd == -1) {
sys_err("open error");
}
ftruncate(fd, sizeof(stu));
p = mmap(NULL, sizeof(stu), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) {
sys_err("mmap error");
}
close(fd);
while (1) {
memcpy(p, &stu, sizeof(stu));
stu.id++;
sleep(1);
}
munmap(p, sizeof(stu));
return 0;
}
mmap_r.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <dirent.h>
#include <errno.h>
struct student {
int id;
char name[256];
int age;
};
void sys_err(const char *str) {
perror(str);
exit(1);
}
int main(int argc, char *argv[]) {
struct student stu;
struct student *p;
int fd = 0;
fd = open("test_brommp", O_RDONLY);
if(fd == -1) {
sys_err("open error");
}
p = mmap(NULL, sizeof(stu), PROT_READ, MAP_SHARED, fd, 0);
if(p == MAP_FAILED) {
sys_err("mmap error");
}
close(fd);
while(1) {
printf("%d %s %d\n", p->id, p->name, p->age);
sleep(1);
}
munmap(p, sizeof(stu));
return 0;
}
ps:无血缘关系进程间通信,用mmap方法数据可以重复读取,而用fifo的话数据只能读取一次
匿名映射
不再依附于文件建立共享映射区,但只能用于有血缘关系进程间通信
p = (int *)mmap(NULL, 40, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);