Linux 进程间通信(管道、共享内存、消息队列、信号量)
进程通信 : 不同进程之间传播或交换信息
为什么要进程通信呢? 协同运行,项目模块化
通信原理 : 给多个进程提供一个都能访问到的缓冲区。
根据使用场景,我们能划分为以下几种通信 :
1.管道(匿名管道、命名管道)
因为是半双工通信(单向传递信息),所以叫"管道"。原理是在内核中创建一个缓冲区让通信双方传递信息。
匿名管道 :创建的缓冲区没有标识 , 只能用于具有亲缘关系的进程通信。
通信流程及代码:
/* * 匿名管道接口的基本使用 */ #include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<errno.h> #include<string.h> int main() { //创建管道 必须在子进程创建之前 //int pipe(int pipefd[2]); //pipefd : 用于获取管道中的操作描述符 //pipefd[0] : 用于从管道中读取数据 //pipefd[1] : 用于向管道中写入数据 //返回值 : 成功 0 失败 -1 int pipefd[2]; int ret = pipe(pipefd); if(ret < 0) { perror("pipe error"); return -1; } int pid = fork(); if(pid < 0) { return -1; } else if(pid == 0) { //子进程关闭写端 close(pipefd[1]); //子进程--读取管道中的数据 char buff[1024] = {0}; read(pipefd[0],buff,1023); printf("buff:[%s]\n",buff); } else { //父进程关闭读端 close(pipefd[0]); //父进程--向管道中写入数据 char *ptr = "do you like me ?"; write(pipefd[1],buff,strlen(ptr)); } return 0; }
管道的读写特性 :
如果管道中没有数据,则read会阻塞,直到读取到数据
如果管道中数据满了,则write会阻塞,直到有数据被读取出去
如果管道中所有写端都被关闭,那么读端读完管道中的数据之后,会返回0
如果管道中所有读端都被关闭,那么写端写入数据的时候会触发异常,退出进程
管道特点 :
1.半双工通信,数据只能一个方向流动
2.读写特性
3.内核会对管道进行同步与互斥操作(如果管道读写数据大小<=PIPE_BUF,读写操作将是原子性操作,是不可中断的)
4.提供字节流(不包含边界的连续流)服务(数据的传输比较灵活,但是有可能造成数据粘连(数据没有边界) )
5.生命周期随进程退出而退出
下面,用匿名管道实现ls | grep 命令:
//ls|grep的模拟实现 #include<stdio.h> #include<unistd.h> #include<errno.h> int main() { //创建匿名管道 int pipefd[2]; int ret = pipe(pipefd); if(ret<0) { perror("pipe error\n"); return -1; } int pid1=fork(); if(pid1==0) { //ls --- 写入到标准输出进行打印 //标准输出重定向到管道写端 close(pipefd[0]); dup2(pipefd[1],1); execlp("ls","ls",NULL); exit(0); } int pid2=fork(); if(pid2==0) { //grep make --- 从标准输入读取数据 //标准输入重定向到管道读端 close(pipefd[1]); dup2(pipefd[0],0); execlp("grep","grep","make",NULL); exit(0); } //wait之前关闭管道,防止影响子进程之间的管道交流 close(pipefd[0]); close(pipefd[1]); wait(NULL); wait(NULL); return 0; }
命名管道 : 是在文件系统中创建的一个管道(FIFO)文件,所有进程都可以通过打开这个文件来进行通信。
特点 : 能让同一机器任意进程都可以进行通信。
打开特性 :
管道文件如果被只读/只写方式打开,将阻塞,直到该文件被只写/只读方式打开;
被读写方式打开,不阻塞。
通信流程及代码 :
写端mkfifo创建管道文件->读端打开管道->两端可以进行单向通信了
//命名管道 读端demo #include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<string.h> #include<fcntl.h> #include<errno.h> int main() { int fd=open("./test.fifo",O_RDONLY); while(1) { char buff[1024]={0}; int ret=read(fd,buff,1023); if(ret>0) { printf("client say:%s\n",buff); } else if(ret == 0) { printf("write close!\n"); return -1; } else { perror("read error"); return -1; } } close(fd); return 0; }
//命名管道基本使用 写端demo // int mkfifo(cosnt char* pathname,mode_t mode) // mode : 权限 返回值: 成功0 失败-1 #include<stdio.h> #include<unistd.h> #include<errno.h> #include<string.h> #include<fcntl.h> #include<sys/stat.h> int main() { int ret = mkfifo("./test.fifo",0664); if(ret<0) { perror("mkfifio error"); return -1; } int fd = open("./test.fifo",O_WRONLY); if(fd<0) { perror("open error"); return -1; } printf("open fifo success!\n"); while(1) { char buff[1024] = {0}; scanf("%s",buff); write(fd,buff,strlen(buff)); printf("buff:[%s]\n",buff); } close(fd); return 0; }
运行演示 :
匿名管道和命名管道特点相同(除了亲缘关系)。
匿名管道和命名管道的区别 :
1.匿名管道用pipe函数创建打开,命名管道由mkfifo函数创建,并用open打开
2.匿名管道只能用于亲缘关系的进程,命名管道能适用于同一机器上任意进程间通信
本质上他们都是在内核中创建的一块缓冲区
2.共享内存(最快通信)
相关命令: ipcs --- 查看所有进程间通信方式
-m 查看共享内存 -s 查看信号量 -q 查看消息队列
ipcrm --- 删除进程间通信方式 -m 删除共享内存(删除时先判断链接数,链接数为0才会删除)
原理是 , 开辟一块内存,把内存映射到虚拟地址空间,然后直接通过虚拟地址空间对数据进行操作
如上图,总共发生了两次数据拷贝: 1.用户空间到物理内存 2.物理内存到用户空间
代码及流程如下:
//共享内存写入 #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<sys/shm.h> #define IPC_KEY 0x123456 #define SHM_SIZE 4096 int main() { //key_t ftok(const char *pathname,int proj_id); //pathname : 文件名 //proj_id : 自定义数字 // 通过文件名找到inode节点号与proj_id合在一起生成一个key值(相当于宏) //比如: key_t key = ftok(".",PROJ_ID); //int shmget(key_t key,size_t size,int shmflg); //key : 共享内存在操作系统中的标识符 //size : 共享内存大小 //shmflg (选项标志): // IPC_CREAT: 存在打开,否则创建 // IPC_EXCL : 存在报错,否则创建 // mode_flags : 权限 //返回值 : 进程对共享内存的操作句柄 失败返回-1 int shmid = shmget(IPC_KEY,32,IPC_CREAT | 0664); if(shmid < 0) { perror("shmget error!"); return -1; } //void *shmat(int shmid,const void *shamaddr,int shmflg); // shmat : 共享内存的操作句柄 // shmaddr : 用户指定共享内存映射在虚拟地址空间的首地址 // 置NULL,让操作系统分配 //shmflg : SHM_RDONLY -- 只读 // 0 -- 可读可写 //返回值 : 成功:映射首地址 失败:(void*)-1 void *shm_start = (char*)shmat(shmid,NULL,0); if(shm_start == (void*)-1) { perror("shmat error"); return -1; } //进行内存操作 int i=0; while(i!=10) { sprintf(shm_start,"haha~~+%d\n",i++); sleep(1); } //解除映射 //int shmdt(const void *shmaddr); //shmaddr :映射首地址 //return : success 0 failure -1 shmdt(shm_start); //int shmctl(int shmid,int cmd,struct shmid_ds *buf); //shmid : 句柄 //cmd : 对共享内存的操作 // IPC_RMID 删除共享内存 //buf : 设置/获取属性信息 不想设置 NULL //删除共享内存,并不会直接删除而是判断当前链接数 //若不为0,则拒绝连接 //直到为0,才会删除共享内存 return 0; }
//读共享内存 #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<sys/shm.h> #define IPC_KEY 0x123456 #define SHM_SIZE 4096 int main() { int shmid = shmget(IPC_KEY,SHM_SIZE,IPC_CREAT | 0664); void* shm_start = (char*)shmat(shmid,NULL,0); while(1) { printf("%s\n",shm_start); sleep(1); } shmdt(shm_start); shmctl(shmid,IPC_RMID,NULL); return 0; }
共享内存特点 :
1.共享内存是最快的ipc,一旦这样的内存区映射到共享它的进程的地址空间,这些进程间数据的传递不用在内核和用户态之间来回切换。
2.没有保证进程的同步互斥。
3.消息队列
就是一个存储消息的链表。消息有特定的格式和优先级。对消息队列有写权限的进程可以添加消息,有读权限的进程可以读出消息。
原理 : 内核中创建一个优先级队列,实现进程间数据传输
4.信号量
本质是一个计数器,用来计数资源
作用 : 保护共享(临界)资源,保证进程间的同步与互斥
工作原理 : PV操作 --- 本身具有原子性,因为要保护资源
P(申请): 如果计数>0,资源-1;如果计数=0,则挂起等待;
V(释放): 如果有进程挂起等待则唤醒;没有进程等待则计数+1;