零、前言
本章主要讲解学习Linux中本系统下的进程间通信
一、进程间通信介绍
- 概念:
进程间通信简称IPC(Inter process communication),进程间通信就是在不同进程之间传播或交换信息
- 进程间通信目的:
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
- 进程间通信本质:让不同的进程看到同一份资源
由于进程之间具有独立性,代码数据独立拥有,若想实现通信,可以通过向第三方资源(实际上就是操作系统提供的一段内存区域)写入或是读取数据,进而实现进程之间的通信
- 进程间通信发展:
管道->System V进程间通信->POSIX进程间通信
- 进程间通信分类:
- 管道
匿名管道pipe;命名管道
- System V IPC
System V 消息队列;System V 共享内存;System V 信号量
- POSIX IPC
消息队列;共享内存;信号量;互斥量;条件变量;读写锁
二、管道
- 概念:
管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
- 示图:统计当前使用云服务器上的登录用户个数
注:who命令用于查看当前云服务器的登录用户(一行显示一个用户);wc -l用于统计当前的行数
1、匿名管道
- 概念:
匿名管道用于本地具有亲戚关系的进程之间通信,常用与父子进程间通信
- pipe函数原型:
#include <unistd.h> int pipe(int fd[2]);
- 功能:
创建一无名管道
- 参数:
- fd:文件描述符数组,是一个输出型参数,拿到打开的管道文件的问文件描述符,其中fd[0]表示读端文件,fd[1]表示写端文件
- 返回值:成功返回0,失败返回错误代码
- 示图:
- 示例:父子进程匿名管道通信
#include<stdio.h> #include<unistd.h> #include<sys/types.h> #include<sys/wait.h> #include<string.h> #include<stdlib.h> int main() { int pipe_id[2]={0}; //创建管道文件资源 int ret=pipe(pipe_id); if(ret<0) { perror("pipe"); exit(1); } printf("pipe_id[0]:%d pipe_id[1]:%d\n",pipe_id[0],pipe_id[1]); //创建子进程,共享管道资源 pid_t id=fork(); if(id==0) { //child->write //关闭子进程的读端 close(pipe_id[0]); const char* msg="Hello father, I am child!\n"; int cnt=0; while(1) { cnt++; printf("child write:%d\n",cnt); write(pipe_id[1],msg,strlen(msg));//结束符不用写入,结束符是C语言的规则不是系统的规则 sleep(1); if(cnt==10) break; } close(pipe_id[1]); exit(0); } else if(id>0) { //father->read //关闭父进程的写端 close(pipe_id[1]); //进行读取管道信息 while(1) { char buffer[128]={0}; ssize_t s=read(pipe_id[0],buffer,sizeof(buffer)-1);//给结束符留一个位置 if(s>0) { buffer[s]=0;//设置结束符 printf("msg from child:%s",buffer); } else if(s==0) { printf("子进程写端关闭...\n"); break; } else break; } close(pipe_id[0]); } else { perror("fork"); exit(2); } //父进程等待 int status=0; if(waitpid(id,&status,0)>0&&WIFEXITED(status))//等待成功并退出正常 { printf("wait success! exit code:%d\n",WEXITSTATUS(status)); } else { printf("exit sign:%d\n",status&0x7F); } return 0; }
- 效果:
- 共享管道原理:
- 对于同个文件可以以读方式和以写方式打开,文件在文件系统虽然只有一份,但是在进程的PCB中的文件结构体中的文件地址数组中可以保存两份,一份指向文件的读端口,一份指向文件的写端口
- 管道通过系统接口创建管道文件资源,并构建文件与PCB的映射关系,当fork创建子进程时父子进程就见到同一份文件资源,依靠管道文件的缓冲区选择性进行单向的实时读写
注:如果是刷新到磁盘上再进行读写非常影响效率
- 单向读写:
父进程进行读,子进程进行写;父进程进行写,子进程进行读
- 示图:
- 注意:
- 只有在先fork之前读写打开文件,父子进程才能共享相同的文件指针数组,进一步灵活控制读写
- 管道只能够进行单向通信,关闭对应的读写端也是为了避免误操作
- 从管道写端写入的数据会被内核缓冲,直到从管道的读端被读取
- 以文件描述符视角理解:
- 以内核角度理解:
- 注意:
- 管道就是特殊的文件,管道的使用和文件一致
- 但是依靠管道通信的本质上依靠管道的缓冲区进行读写,其缓冲并不会真正的刷新到磁盘上
- 管道读写规则:
- 写端不写,读端无数据可读
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,进行等待写端写入数据
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN
- 写端不写,并将写端文件关闭
如果所有管道写端对应的文件描述符被关闭,则read返回0
- 读端不读,写端一直写
O_NONBLOCK disable: write调用阻塞,直到有进程读走管道缓冲区的数据
O_NONBLOCK enable: write调用返回-1,errno值为EAGAIN
- 读端不读,并将读端文件关闭
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程被终止退出
- 示图:
- 数据写入的原子性
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性
注:原子性是指 一个操作是不可中断的,要么全部执行成功要么全部执行失败,即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰
- 管道特点:
- 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常父子进程之间就可应用该管道
- 管道提供流式服务,面向字节流,读写以字节为单位进行
- 进程退出,管道释放,所以管道的生命周期随进程内核会对管道操作进行同步与互斥,即保证数据的原子性
- 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
- 示图:
2、命名管道
- 概念:
- 对于匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信
- 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道
- 命名管道创建命令:
mkfifo filename
- 示例:
- 命名管道创建函数原型:
#include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *filename, mode_t mode);
注:第一个参数即为管道的名称,第二个参数即为创建管道文件的权限,创建成功返回0,否则返回-1
- 示例:
int main() { mkfifo("fifo", 0644); return 0; }
- 匿名管道与命名管道的区别
- 匿名管道由pipe函数创建并打开,依靠父子进程的共享特性看到同一份文件资源
- 命名管道由mkfifo函数创建并主动调用函数打开,依靠文件路径的唯一性让不同进行找到并打开同一份文件资源
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义
- 命名管道的打开规则
- 如果当前打开操作是为读而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable:立刻返回成功
- 如果当前打开操作是为写而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
- 示例:用命名管道实现server&client通信
server.c: #include <stdio.h> #include <sys/stat.h> #include <sys/types.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #define FIFO "fifo" int main() { //创建命名管道 if(mkfifo(FIFO,0644)<0) { perror("mkfifo"); exit(1); } //打开管道文件 int fd=open(FIFO,O_RDONLY); if(fd<0) { perror("open"); exit(2); } //服务端进行客户端信息 while(1) { char buffer[128]={0}; //输出标识词 printf("client#"); fflush(stdout); //读取管道数据 ssize_t s=read(fd,buffer,sizeof(buffer)-1); if(s>0) { buffer[s]=0; printf("%s",buffer); } else if(s==0) { printf("write close,child quit\n"); break; } else break; } return 0; } client.c: #include <stdio.h> #include <sys/stat.h> #include <sys/types.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #define FIFO "fifo" int main() { //打开管道文件 int fd=open(FIFO,O_WRONLY); if(fd<0) { perror("open"); exit(2); } //向服务端发送消息 while(1) { char buffer[128]={0}; //输出标识词 printf("please enter#"); fflush(stdout); //读入数据 ssize_t s=read(0,buffer,sizeof(buffer)-1); if(s>0)//写入到管道 { buffer[s]=0; write(fd,buffer,strlen(buffer)); } else break; } return 0; }
- 效果: