目录
1.基本介绍
-
1.1 进程间通信引入
因为进程的独立性,使得它们之间通信较为麻烦,因此需要OS提供一些方法 使得进程之间能够共同访问一个相同的媒介。
-
1.2 进程间通信目的
- 数据传输
- 数据共享
- 进程间的访问控制
- 事件通知
-
1.3 进程间通信方式
- 通信目的不同,使用场景不同
- -> OS提供多种进程间通信方式
- 管道(命名管道/匿名管道) -> 传输数据
- 共享内存 -> 共享数据
- 消息队列 -> 传输数据
- 信号量 -> 进程间的访问控制
- -> OS提供多种进程间通信方式
- 通信目的不同,使用场景不同
2. 管道
内核中的一块缓冲区,主要用于实现进程间数据资源传输
- 半双工通信
- 双向选择的单向通信
- 管道生命周期随进程 -> 进程结束,管道消失。
- 原理
- OS为了进程,在内核创建了一块缓冲区,只要进程能够访问到缓冲区,它们就能通过管道进行通信。
- 管道提供流式服务
- 面向字节流数据传输 -> 传输灵活,但是会造成数据粘连。
-
2.1 匿名管道:pipe
- 只能用于具有亲缘关系的进程间通信
- 缓冲区没有具体标识符,只能通过子进程复制父进程的方式获取到管道的操作句柄
- 两个文件描述符 fd[0] -> 读 fd[1] -> 写
- 缓冲区没有具体标识符,只能通过子进程复制父进程的方式获取到管道的操作句柄
- 原理
- 内核的缓冲区,需要父进程创建子进程,通过复制的方式,让子进程能够访问到这个缓冲区。
- 匿名管道的创建接口:int pipe(int pipefd[2]);
- pipefd:输出型参数
- pipefd[0]:向管道读数据
- pipefd[1]:向管道写数据
- 返回值
- 成功 -> 0
- 失败 -> -1
- pipefd:输出型参数
- 读写特性
- 虽然管道提供了双向选择,但是若没有用到某一端,就把这一端关闭。
- 若管道中没有数据,read会堵塞,直到读到数据返回
- 过管道中数据满了,write会阻塞,直到数据被读取,管道中有空闲位置,写入数据后返回。
- 若管道所有的读端都被关闭,则write会触发异常,发送SIGPIPE信号,导致进程退出(通知用户没人读了)。
- 若管道所有的写端都被关闭,则read读完管道数据后,返回0(通知用户没人写了)。
- 虽然管道提供了双向选择,但是若没有用到某一端,就把这一端关闭。
- 管道自带同步与互斥特性
- 当读写大小不超过PIPE_BUF时,读写操作受保护,保证操作原子性(操作不可被打断)。
- 同步:保证对临界资源访问的时序可控性
- 控制购买商品的人不能一直购买,完成一次购买后,排队等待下一次购买
- 互斥:保证对临界资源同一时间的唯一访问性
- 一名顾客在买东西的时候,其他的顾客只能等待
- 只能用于具有亲缘关系的进程间通信
/*
* 匿名管道的基本使用
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main(){
//创建管道
int pipefd[2];
int ret=pipe(pipefd);
if(ret <0){
perror("pipe error");
return -1;
}
int pid=fork();
if(pid<0){
perror("fork error");
return -1;
}else if(pid==0){
//子进程 write
sleep{3};
char *ptr="bonsoir~~~";
write(pipefd[1],ptr,strlen(ptr));
}else{
//父进程 read
char buf[1024]={0};
read(pipefd[0],buf,1023);
printf("read buf:[%s]\n",buf);
}
return 0;
}
- 匿名管道实现命令连接,将一个命令的输出结果作为下一个命令的输入
/*
* 匿名管道实现命令连接
* ps -ef|grep ssh
* ps -ef:
* 输出:所有的进程信息写入标准输出
* grep ssh:
* 从标准输入读取数据,进行过滤处理
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(){
int pipefd[2];
int ret=pipe(pipefd);
//管道创建失败
if(ret<0){
perror("pipe error");
return -1;
}
//创建子进程1执行前部分命令
int pid1=fork();
if(pid1==0){
//child1 -> ps -ef
//关闭读端
close(pipefd[0]);
//将输出放入管道
dup2(pipefd[1],1);
execlp("ps","ps","-ef",NULL);
}
//创建子进程2执行后部分命令
int pid2=fork();
if(pid2==0){
//child2 -> grep ssh
//关闭写端
close(pipefd[1]);
//从管道中读取数据
dup2(pipefd[0],0);
execlp("grep","grep","ssh",NULL);
}
//parent
//关闭父进程管道
close(pipefd[0]);
close(pipefd[1]);
waitpid(pid1,NULL,0);
waitpid(pid2,NULL,0);
return 0;
}
-
2.2 命名管道
- 可见于文件系统,因为创建命名管道会随之在文件系统中创建一个命名管道文件(管道的名称)。
- 所有的进程都能够通过打开管道文件,进而获取管道的操作句柄
- 因此命名管道可以用于同一主机上任意进程间通信
- 原理
- 依然还是内核的缓冲区,只是通过文件向所有进程都提供了能够访问管道的机会而已
- 命名管道的创建接口:int mkfifo(const char *pathname, mode_t mode);
- pathname:管道文件名
- mode:创建权限
- 返回值:
- 成功 -> 0
- 失败 -> -1
- 打开特性
- 以只读方式打开时,若管道文件没有其他进程以写的方式打开,则阻塞,直到文件被以写的方式打开
- 以只写方式打开时,若管道文件没有其他进程以读的方式打开,则阻塞,直到文件被以读的方式打开
- 以读写方式打开时,则不会阻塞。
- 读写特性与匿名管道相同
- 可见于文件系统,因为创建命名管道会随之在文件系统中创建一个命名管道文件(管道的名称)。
/*命名管道的基本使用 读*/
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main(){
char *file="./test.fifo";
umask(0);
int ret=mkfifo(file,0664);
if(ret<0){
if(errno!=EEXIST){
perror("mkfifo error");
return -1;
}
}
int fd=open(file,O_RDONLY);
if(fd<0){
perror("open error");
return -1;
}
printf("open success!!\n");
while(1){
char buf[1024]={0};
//读取数据
int ret =read(fd,buf,1023);
//读到数据
if(ret>0){
printf("read buf:[%s]\n",buf);
//所有写端关闭
}else if(ret==0){
printf("write closed");
}else{
perror("read error");
}
}
close(fd);
return 0;
}
/*命名管道的基本使用 写*/
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main(){
char *file="./test.fifo";
umask(0);
int ret=mkfifo(file,0664);
if(ret<0){
if(errno!=EEXIST){
perror("mkfifo error");
return -1;
}
}
int fd=open(file,O_WRONLY);
if(fd<0){
perror("open error");
return -1;
}
printf("open success!!\n");
while(1){
//写入数据
char buf[1024]={0};
scanf("%s",buf);
write(fd,buf,strlen(buf));
}
close(fd);
return 0;
}
3. 共享内存
在物理内存中开辟出一块空间,映射到虚拟地址空间。多个进程通过将同一块内存映射到自己的虚拟地址空间,达到数据共享的一个目的。
- 是最快的进程间通信方式
- 其他进程间通信方式 -> 将数据从用户态拷贝到内核态,使用时从内核态拷贝到用户态;需要跟内核缓冲区进行数据交互
- 共享内存 -> 直接将一块内存映射到虚拟地址空间,用户可以直接通过地址对内存进行操作,并反馈到其他进程
- 相较于其他进程间通信方式,少了两步数据拷贝的过程。
-
3.1 使用流程
- 1. 创建 / 打开共享内存 shmget
- key_t ftok(const char *pathname, int proj_id);
- pathname:文件名
- proj_id:数字
- 通过文件的inode节点号和proj_id共同得出一个key值
- int shmget(key_t key, size_t size, int shmflg);
- key:共享内存标识符
- size:共享内存大小
- shmflg:打开方式 / 创建权限
- IPC_CREAT 共享内存不存在则创建,存在则打开
- IPC_EXCL 与IPC_CREAT同用,若存在 -> 报错;不存在 -> 创建
- mode_flags 权限
- 返回值
- 成功 -> 操作句柄shmid
- 失败 -> -1
- key_t ftok(const char *pathname, int proj_id);
- 2. 将共享内存映射到虚拟地址空间 shmat
- void *shmat(int shmid, const void *shmaddr, int shmflg);
- shmid:创建恭喜囊诶村返回的操作句柄
- shmaddr:用于指定映射在虚拟地址空间的首地址,通常置NULL
- shmflg:0 -> 可读可写
- 返回值
- 成功 -> 映射首地址,通过这个地址对共享内存进行操作
- 失败 -> (void*)-1
- void *shmat(int shmid, const void *shmaddr, int shmflg);
- 3. 对共享内存进行基本的内存操作 memcpy...
- 4. 解除映射关系 shmdt
- int shmdt(const void *shmaddr);
- shmaddr:映射返回的首地址
- int shmdt(const void *shmaddr);
- 5. 删除共享内存 shmctl
- int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- shmid:操作句柄
- cmd
- IPC_RMID:删除共享内存
- buf:设置 / 获取共享内存信息,不使用则置NULL
- int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 共享内存的查看
- ipcs命令 ipcs -m
- 共享内存的删除
- ipcrm命令 ipcrm -m shmid
- 1. 创建 / 打开共享内存 shmget
- 共享内存并不是立即删除的,只是拒绝后续映射连接。当共享内存映射连接数为0时,则删除共享内存。
4. 消息队列
- 传输的是有类型的数据块,用户可以根据自己的需要选择性的获取某些类型的数据。
-
4.1 使用流程
- 1. 创建消息队列 msgget
- int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
- msgp:数据节点
- int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
- 2. 添加数据节点 msgsnd
- int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
- 3. 获取数据节点 msgrcv
- ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
- 4. 删除 msgctl
- int msgctl(int msqid, int cmd, struct msqid_ds *buf);
- 1. 创建消息队列 msgget
5. 信号量
- 内核中的一个计数器(具有等待、唤醒功能的等待队列),用于资源计数
- 若计数<=0 ,没有资源 -> 需要等待
- 若计数>0 ,有资源 -> 可以获取资源,计数-1
- 若放入资源 -> 计数+1,并且唤醒等待的进程
- 实现进程间的同步与互斥
- 互斥:资源为0或1时,才具有互斥效果;但是可以实现互斥。