ipc(进程间通信)的概念
interprocess communication:进程间通信——Linux通过内核给你提供了一块缓冲区,使得进程实现通信。
IPC通信的方式
- pipe 管道——最简单
- fifo 有名管道
- mmap 文件映射共享IO——速度最快(原理:在内存开辟一片缓冲区,把文件映射到内存上,你直接去操作内存就可以了)
- 本地socket 最稳定
- 信号 携带信息量最小的
- 共享内存 开辟一块内存区域,大家都能访问,一个进程退出之后,这块内存还会给你保留下来,后来者还可以继续使用
- 消息队列
管道——半双工通信
(带fd的参数一般都是指文件描述符)
一端读,一端写
一个进程写,一个进程读,他俩只能是父子进程。
常见的通信方式
单工(广播)、半双工(对讲机)、全双工(打电话)
管道通信使用举例
管道函数:
int pipe(int pipefd[2]);
- pipefd 读写文件描述符 0 代表 读, 1 代表 写
- 返回值 成功返回 0, 失败返回 -1.
调用完 就相当于在内核开辟了一块缓冲区。
父子进程间通信
#include<stdio.h>
#include<unistd.h>
int main() {
//创建文件描述符
int fd[2];
//创建管道
pipe(fd);
pid_t pid = fork();
if (pid == 0) {
//son
sleep(3);
write(fd[1], "hello", 5);
}
else if (pid > 0) {
//father
//现有缓冲区,然后才能读
char buf[12] = { 0 };
//阻塞等待,哪怕子进程sleep了,也要等
int ret = read(fd[0], buf, sizeof(buf));
//说明读到了
if (ret > 0) {
write(STDOUT_FILEND, buf, ret);
}
}
return 0;
}
运行结果
父子进程实现pipe通信,实现ps aux | grep bash 功能
由于父子进程都掌握着管道(pipe)的读写两端,因此有如下结构示意图
出现问题的测试代码:(我们希望子进程写入,父进程读取)
#include<stdio.h>
#include<unistd.h>
int main() {
//创建文件描述符
int fd[2];
//创建管道
pipe(fd);
pid_t pid = fork();
if (pid == 0) {
//son
//son执行ps命令
//1、先重定向
dup2(fd[1], STDOUT_FILENO);//标准输出重定向
//2、execlp NULL是哨兵,告诉命令,后面不会再有参数输入了
execlp("ps"."ps"."aux", NULL);
}
else if (pid > 0) {
//father
//1、先重定向
dup2(fd[0], STDIN_FILENO);//标准输出重定向
//2、execlp NULL是哨兵,告诉命令,后面不会再有参数输入了
execlp("grep"."grep"."bash", NULL);
}
return 0;
}
出现的问题:
子进程变成僵尸进程,父进程执行grep之后一直在等待输入。
分析原因:
grep命令的特性:等待标准输入,如果你输入的正确,则给你一个反馈。阻塞等待,因为grep一直认为还有输入,除非是输入端的进程放弃了写入的机会。grep阻塞等待示例,如图所示
上面代码出现问题,是因为虽然子进程死去了,但是父进程还掌握着管道写入端的句柄,所以grep一直认为还会有输入,虽然此时读写两端都是父进程掌握。所以我们需要在代码中关闭子进程的读端,以及父进程对于管道的写端。
//关闭 读端
close(fd[0]);
//关闭 写端
close(fd[1]);
在使用管道时,应该把读写两端都规划好。这样数据的流向才稳定。
修正之后的代码:
#include<stdio.h>
#include<unistd.h>
int main() {
//创建文件描述符
int fd[2];
//创建管道
pipe(fd);
pid_t pid = fork();
if (pid == 0) {
//son
//son执行ps命令
//关闭 读端
close(fd[0]);
//1、先重定向
dup2(fd[1], STDOUT_FILENO);//标准输出重定向
//2、execlp NULL是哨兵,告诉命令,后面不会再有参数输入了
execlp("ps"."ps"."aux", NULL);
}
else if (pid > 0) {
//father
//关闭 写端
close(fd[1]);
//1、先重定向
dup2(fd[0], STDIN_FILENO);//标准输出重定向
//2、execlp NULL是哨兵,告诉命令,后面不会再有参数输入了
execlp("grep"."grep"."bash", NULL);
}
return 0;
}
运行结果
管道的读写行为分析
读管道
- 写端全部关闭——read读到0,相当于读到文件末尾
- 写端没有全部关闭
- 有数据——read读到数据
- 没有数据——read阻塞 fcntl 函数可以更改阻塞为非阻塞
写管道
- 读端全部关闭——产生一个信号 SIGPIPE ,程序异常终止
- 读端未全部关闭
- 管道已满——write阻塞
- 管道未满——write正常写入
测试代码
1、写端全部关闭——read读到0,相当于读到文件末尾
子进程关闭读端——子进程写入数据——子进程关闭写端——父进程关闭写端——父进程读取数据——读到0则说明读取完毕,输出“read over”
#include<stdio.h>
#include<unistd.h>
int main() {
//创建文件描述符
int fd[2];
//创建管道
pipe(fd);
pid_t pid = fork();
if (pid == 0) {
//son
sleep(3);
//关闭 读端
close(fd[0]);
//写入数据
write(fd[1], "hello", 5);
//关闭写端
close(fd[1]);
//关闭读写之后,先别去死
while (1) {
sleep(1);
}
}
else if (pid > 0) {
//father
char buf[12] = { 0 };
//关闭写端
close(fd[1]);
while (1) {
//阻塞等待,读取动作
int ret = read(fd[0], buf, sizeof(buf));
//读取完毕
if (ret == 0) {
printf("read over\n");
}
}
}
return 0;
}
运行结果(正常退出):
2、读端全部关闭——产生一个信号 SIGPIPE ,程序异常终止
父进程关闭读写——子进程关闭读端——写入数据——子进程异常终止,产生信号SIGPIPE——父进程获取子进程的死亡信息
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
int main() {
//创建文件描述符
int fd[2];
//创建管道
pipe(fd);
pid_t pid = fork();
if (pid == 0) {
//son
sleep(3);
//关闭 读端
close(fd[0]);
//写入数据
write(fd[1], "hello", 5);
//关闭写端
close(fd[1]);
//关闭读写之后,先别去死
while (1) {
sleep(1);
}
}
else if (pid > 0) {
//father
//关闭写端
close(fd[1]);
//关闭 读端
close(fd[0]);
//获取子进程死亡信息
int status;
wait(&status);
if (WIFSIGNALED(status)) {
printf("killed by %d\n", WTERMSIG(status));
}
while (1) {
sleep(1);
}
}
return 0;
}
运行结果:
实现兄弟间进程通信, ps aux | grep bash
管道的大小和优劣
使用命令查看当前系统中创建管道文件所对应的内核缓冲区大小
ulimit -a
函数fpathconf
优点
- 简单
缺点
- 只能有血缘关系之间的进程通信
- 父子进程单方向通信,如果需要双向通信,需要创建多个管道。
FIFO通信
有名管道,实现无血缘关系进程的通信
- 创建一个管道的伪文件
- mkfifo myfifo 命令创建
- 也可以用函数int mkfifo(const char *pathname, mode_t mode);
- 内核会为fifo文件开辟一块缓冲区,操作fifo文件,可以操作缓冲区,实现进程间通信——实际上就是文件读写。
- open函数的注意事项:打开fifo文件的时候,read端会阻塞等待write端open,write端同理,也会阻塞等待另外一端打开。
被mkfifo myfifo 命令创建出来的管道伪文件截图
写端示例
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
//如果运行时没有输入文件名,则会报错
printf("./a.out fifoname\n");
return -1;
}
//当前目录中存在myfifo文件
//打开fifo文件
int fd = open(argv[1], O_WRONLY);
//写
char buf[256];
int num = 1;
while (1) {
memset(buf, 0x00, sizeof(buf));
sprintf(buf, "xiaoming%04d", num++);
write(fd, buf, strlen(buf));
sleep(1);
}
//关闭描述符
close(fd);
return 0;
}
读端示例
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
//如果运行时没有输入文件名,则会报错
printf("./a.out fifoname\n");
return -1;
}
int fd = open(argv[1], O_RDONLY);
char buf[256];
int ret;
while (1) {
//循环读
ret = read(fd, buf, sizeof(buf));
if (ret > 0) {
printf("read:%s\n", buf);
}
}
//关闭描述符
close(fd);
return 0;
}
读端和写端中的open函数会阻塞,之后对端的open函数也打开了,他才会停止阻塞行为
mmap共享映射区
把文件中的某一段映射到内存上
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
- addr 传地址,一般写NULL
- length 映射区长度
- prot
- PROT_READ 可读
- PROT_WRITE 可写
- flags
- MAP_SHARED 共享的,对内存的修改会影响到源文件
- MAP_PRIVATE 私有的
- fd 文件描述符
- offset 偏移量
- 返回值
- 成功 返回 可用内存的首地址
- 失败 返回 MAP_FAILED
释放内存区
#include <sys/mman.h>
int munmap(void *addr, size_t length);
- addr 传mmap返回值
- length mmap创建的长度
- 返回值
实例(MAP_SHARED的作用是,你修改了内存,会影响文件)
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/mman.h>
#include<string.h>
int main(){
int fd=open("men.txt", O_RDWR);
//创建映射区
char *mem=mmap(NULL, 8, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(mem==MAP_FAILED){
perror("mmap err");
return -1;
}
//拷贝数据
strcpy(mem, "hello");
//释放mmap
munmap(mem, 8);
close(fd);
return 0;
}
运行结果
因此可以用mmap来修改文件内容。
mmap九问:
问题一的测试:(因此不能改,否则会释放失败)
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/mman.h>
#include<string.h>
int main(){
int fd=open("men.txt", O_RDWR);
//创建映射区
char *mem=mmap(NULL, 8, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(mem==MAP_FAILED){
perror("mmap err");
return -1;
}
//拷贝数据
strcpy(mem, "hello");
mem++;
//如果释放失败
if (munmmap(mem, 8) < 0) {
perror("munmmap err");
}
close(fd);
return 0;
}
运行结果
问题二的测试:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/mman.h>
#include<string.h>
int main(){
int fd=open("men.txt", O_RDWR);
//创建映射区
char *mem=mmap(NULL, 8, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(mem==MAP_FAILED){
perror("mmap err");
return -1;
}
//拷贝数据
strcpy(mem, "hellollllllllllll");
//释放内存
munmmap(mem, 8);
close(fd);
return 0;
}
文件的大小对映射区操作有影响,尽量避免。
char *mem=mmap(NULL, 8, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 1000000000);
offset必须是4k的整数倍。
没有。因为mmap之后,通道就打通了,文件就没用了。
int fd=open("men.txt", O_RDWR|O_TRUNC, 0664);//创建并截断文件
bus error//总线错误
可以,但是文件大小不能为0.
不可以
不可以。SHARED的时候,映射区的权限要小于等于open文件的权限。
mmap实现父子进程通信
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/wait.h>
int main()
{
// 先创建映射区
int fd = open("mem.txt",O_RDWR);
int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
//int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_PRIVATE,fd,0);
if(mem == MAP_FAILED){
perror("mmap err");
return -1;
}
// fork子进程
pid_t pid = fork();
// 父进程和子进程交替修改数据
if(pid == 0 ){
//son
*mem = 100;
printf("child,*mem = %d\n",*mem);
sleep(3);
printf("child,*mem = %d\n",*mem);
}
else if(pid > 0){
//parent
sleep(1);
printf("parent,*mem=%d\n",*mem);
*mem = 1001;
printf("parent,*mem=%d\n",*mem);
//回收子进程
wait(NULL);
}
//释放内存
munmap(mem,4);
close(fd);
return 0;
}
运行结果
将上面的注释去掉,改成
//int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_PRIVATE,fd,0);
运行结果
父进程并没有读到子进程的数据,子进程也没有读到父进程改的数据。
因此,
匿名映射——避免打开文件的操作(上面的例子都有调用open函数)
int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0);
使用示例
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/wait.h>
int main()
{
int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0);
if(mem == MAP_FAILED){
perror("mmap err");
return -1;
}
pid_t pid = fork();
if(pid == 0 ){
//son
*mem = 101;
printf("child,*mem=%d\n",*mem);
sleep(3);
printf("child,*mem=%d\n",*mem);
}else if(pid > 0){
//parent
sleep(1);
printf("parent,*mem=%d\n",*mem);
*mem = 10001;
printf("parent,*mem=%d\n",*mem);
wait(NULL);
}
munmap(mem,4);
return 0;
}
运行结果
注意:有的Unix系统中没有MAP_ANON,ANONYMOUS这两个宏。此时该怎么办?
此时用这个
/dev/zero 是一个聚宝盆,无限大,用它做匿名映射,你想取多大都可以。
小技巧——快速把一个文件头几行的数据重定向到另一个文件中
mmap实现无血缘进程通信
写端
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/wait.h>
typedef struct _Student{
int sid;
char sname[20];
}Student;
int main(int argc,char *argv[])
{
if(argc != 2){
printf("./a.out filename\n");
return -1;
}
// 1. open file
int fd = open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0666);
//结构体的大小
int length = sizeof(Student);
//将文件大小改变为参数length指定的大小,
//如果原来的文件大小比参数length大,则超过的部分会被删除,
//如果原来的文件大小比参数length小,则文件将被扩展
ftruncate(fd,length);
// 2. mmap
Student * stu = mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
//如果不判断是否出错,会死的很难看
if(stu == MAP_FAILED){
perror("mmap err");
return -1;
}
int num = 1;
// 3. 修改内存数据
while(1){
stu->sid = num;
sprintf(stu->sname,"xiaoming-%03d",num++);
sleep(1);//相当于每隔1s修改一次映射区的内容
}
// 4. 释放映射区和关闭文件描述符
munmap(stu,length);
close(fd);
return 0;
}
读端
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/wait.h>
typedef struct _Student{
int sid;
char sname[20];
}Student;
int main(int argc,char *argv[])
{
//open file
int fd = open(argv[1],O_RDWR);
//mmap
int length = sizeof(Student);
Student *stu = mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(stu == MAP_FAILED){
perror("mmap err");
return -1;
}
//read data
while(1){
printf("sid=%d,sname=%s\n",stu->sid,stu->sname);
sleep(1);
}
//close and munmap
munmap(stu,length);
close(fd);
return 0;
}
运行结果:
再开一个读的