本文主要介绍了LInux操作系统中进程间通信的常用方式:管道、共享内存、消息队列等,详细介绍其定义与使用方式。
目录
进程间通信
每一个进程的数据都是存储在物理内存当中,进程通过各自的进程虚拟地址空间进行访问,访问的时候,通过各自的页表的映射关系,访问到物理内存
从进程的角度看,每个进程都认为自己拥有4G的空间,只有物理内存当中属于如何存储,页表如何映射,进程不清楚,这也就造就了进程的独立性
进程独立性:
好处:让每个进程运行的时候都是独立进行,数据不会乱窜
坏处:如果两个进程之间需要数据交换,那么由于进程独立性,就没有那么方便
进程间通信是为了让进程和进程之间交换数据
进程间通信方式:管道、共享内存、消息队列、网络
管道
(1)匿名管道
管道的符号:
pa aux | grep xxx
pa aux:列举当前Linux操作系统当中的进程信息
|:管道符号
grep xxx:过滤存在xxx字符串的项
总结:ps aux的返回结果通过管道传输给grep进程,grep在ps aux的结果中过滤存在 “xxx” 的字符串
管道的本质:
管道在内核当中是一块缓冲区,供进程进行读写,交换数据
管道的接口:
程序员可以通过该函数创建一个匿名管道
int pipe(int pipefd[2]); pipefd[0]:管道的读端 pipefd[1]:管道的写端
参数:参数为输出型参数,也就是pipefd的值是pipe函数进行填充的,调用者进行使用
pipefd[2]:数组元素保存的内容是文件描述符
返回值:
创建成功:0
创建失败:-1
代码
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4
5 int main(){
6 int fd[2];
7 int ret = pipe(fd);
8 if(ret<0){
9 perror("pipe");
10 return 0;
11 }
12
13 ret = fork();
14 if(ret <0){
15 perror("fork");
16 return 0;
17 }else if(ret == 0){
18 //child
19 char buf[1024]={0};
20 read(fd[0],buf,sizeof(buf)-1);
21 printf("child read:%s\n",buf);
22 }else{
23 //fathe
24 const char* lp = "write father";
25 write(fd[1],lp,strlen(lp));
26 }
27 return 0;
28 }
问题:
父进程和子进程是抢占式执行,为什么不论父子进程谁先执行,输出结果总是一样的呢?
回答:
当管道中没有数据时,子进程调用read函数从管道中读取时会阻塞,直到管道当中有内容,read函数读回来之后,read才返回
验证:
将父进程程序设置为sleep(1000),同时使用ps aux | grep 程序名称来查看子进程状态,结果发现子进程处于【-read-nocancel】,即可证明上述内容
管道的特性
- 半双工:数据只能从写端流向读端,单项通信
- 匿名管道没有标识符,只能具有亲缘性关系的进程进行进程间通信
- 管道的生命周期跟随进程,进程退出,管道在内核当中销毁
- 管道大小(pipe_buffer)为64k,当管道写满之后再调用write往管道当中写的时候,write就会阻塞
- 管道提供字节流服务:
- ·先后两次写入管道的数据没有间隔
- 数据是被读走(剪切),而不是拷贝走(复制)
- pipe_size:4096字节,当写入/读取的字节数量小于pipe_size,则管道保证读写的原子性
- 修改大小方式:ulimit -p [数字]
- 原子性:读/写操作再同一个时刻只有一个进程在操作,保证进程在操作的时候,读写操作不会被其他进程打扰(一次操作单位为4096个字节,在这个单位内的字节被操作时不会被其他操作打断)
阻塞属性
当读写两端的文件描述符初始的属性为阻塞属性
当write一致调用写,read不去读,则write写满之后write会阻塞
当read一直进行读,当管道的内部被读完之后,则read会阻塞
非阻塞属性
针对于管道的读写两端的文件描述符而言的(程序员调用read/write的时候是非阻塞调用的)
- 写不关闭,一直读,读端调用read函数后,返回值为-1,errno设置为EAGAIN,此时进程课正常操作fd[1],可以正常操作写端
read提示:资源不可用
返回值:-1
- 写关闭,一直读,读端调用read函数后,返回0,表示什么都没读到
read提示:success
返回值:0
- 读不关闭,一直写,管道写满后,再调用write,就会返回-1
write提示:资源不可用
返回值:-1
- 读关闭,写端运行,当写端调用write进行写的时候,就会发生奔溃(原因为:写端的进程收到SIGPIPE信号,导致写端进程奔溃)
程序崩溃(收到SIGPIPE信号)
设置非阻塞属性
int fcntl(int fd,int cmd,.../*arg*/);
参数:
fd:待要操作的文件描述符
cmd:告知fcntl函数要做什么操作
F_GETFL:获取文件描述符的属性信息
int flag = fcntl(fd[0],F_GETFL)
F_SETFL:设置文件描述符的属性信息,设置新的属性放到可变参数列表当中
fcntl(fd[1],F_SETFL,属性) 将第三个参数的属性“替换”给第一个参数 fcntl(fd[1],F_SETFL,属性1 |属性2) 将第三个参数的多个属性“替换”给第一个参数
多属性为什么使用按位或(|)来链接?
返回值:
F_GETFL:返回文件描述符的属性
F_SETFL:
0:设置成功
-1:设置失败
扩展知识
系统接口当中,文件打开方式的宏,再内核当中的使用方式为位图
int一个32比特位的二进制数字,它的每一个数值都对应一个属性,使用按位或(|)操作的原因是将对应属性位的数值更改为1
操作系统内核当中大量使用位图的原因
1. 位操作速度快
2. 节省内存空间
(2)命名管道
创建命名管道
方法一:指令方法
mkfifo [文件名]; 生成一个对应的管道文件,文件属性为P(管道文件)
1、数据还是存储在内核的缓冲区当中
2、 管道文件的作用是为了让不同的进程可以找到这块缓冲区
方法二:程序方法
int mkfifo(const char*pathname,mode_t mode);
参数:
pathname:待要创建的命名管道文件
mode_t:指定管道文件的权限(0664)
返回值:
成功:0
失败:-1
命名管道特性
1、支持不同进程进行进程间通信,不依赖亲缘性
2、因为不同的进程可以通过命名管道文件,找到命名管道(操作系统内核的缓冲区)
共享内存
Linux中最快的进程间通信方式(追求效率的首选,如守护进程和被守护进程)
(1)共享内存的原理
- 在物理内存当中开辟一段空间
- 不同进程通过页表将物理内存空间映射到自己的进程虚拟地址空间当中
- 不用的进程通过操作自己进程寻地址空间当中的虚拟地址来操作共享内存
(2)共享内存的接口
1、创建或获取共享内存接口
int shmget(key_t key,size_t size,int shmflg);
参数:
key:共享内存标识符(共享内存的名字)
size:共享内存大小
shmflg:获取/创建共享内存时,传递的属性信息
IPC_CREAT:如果获取的共享内存不存在,则创建
IPC_EXCT | IPC_CREAT:如果获取的共享内存存在,则函数报错
如果获取的共享内存不存在,则创建
本质上:该组合时要要获取时重新创建的共享内存
按位或上权限,权限也是8进制
返回值:
成功:返回共享内存操作句柄
失败:-1
2、将共享内存附加到进程的虚拟地址空间
void *shmat(int shmid, const void *shmaddr,int shmflg);
参数:
shmid:共享内存操作句柄
shmaddr:将共享内存附加到共享区当中的第一个地址上(一般让操作系统自己分配,传递NULL)
shmflg:以什么权限将共享内存附加到进程当中
SHM_RDONLY:只读
0:可读可写
返回值:
成功:返回附加的虚拟地址
失败:-1
3、分离
int shmdt(const void*shmaddr);
参数:
shmaddr:shmat的返回
返回值:
成功:0
失败:-1
4、操作共享内存接口
int shmctl(int shmid,int cmd,struct shmid_ds *buf);
参数:
shmid:共享内存的操作句柄
cmd:告诉shmctl函数需要完成什么功能
IPC_SET:设置共享内存属性信息
IPC_STAT:获取共享内存属性信息
IPC_RMID:删除共享内存,第三个参数传递NULL
buf:共享内存数据结构
(3)共享内存的特性
1、生命周期跟随操作系统
2、共享内存时覆盖写的方式,读的时候,是访问地址
3、共享内存的删除特性(ipcs命令&ipcrm命令)
ipcrm -m [shmid] 删除共享内存
- 一旦共享内存被删除后,共享内存的物理内存当中的空间被销毁了
- 如果删除的共享内存的时候,共享内存附加的进程数量为0,则内核当中描述该共享内存的结构体也被释放了
- 如果删除的共享内存的时候,共享内存附加的进程数量不为0,则会将该共享内存的key,变成0x00000000.表示当前共享内存不能被其他进程所附加,共享内存的状态会被设置为destory,附加的进行一旦全部退出之后,该共享内存的结构体会被操作系统释放掉。
消息队列
(1)消息队列的原理
1、msgqueue采用链表来实现消息队列,该链表是由系统内核维护
2、系统中可能由很多的msgqueue,每个MQ用消息队列描述符(消息队列ID:qid)来区分,qid是唯一的,用来区分不同的MQ
3、再进行进程间通信时,一个进程将消息加到MQ尾端,另一个进程从消息队列中取消息(不一定以先进先出(队列特性)来取消息,也可以按照消息类型字段取消),这样就实现了进程间的通信。
补充:
1、对垒的特性是先进先出,只要满足先进先出特性的数据结构都可以称为队列
2、消息队列:就是操作系统内核用连败哦实现的队列
3、进程间通信取消息时,可以按照消息类型先进先出,也可以按照原本的顺序先进先出
(2)消息队列接口
创建消息队列接口
int msgget(key_t key,int msgflg);
参数:
key:消息队列的标识符
msgflg:创建的标志,例如IPC_CREAT
返回值:
成功:返回队列ID
失败:返回-1,并设置erron
(3)发送消息
int msgsnd(int msqid,const void*msgp,size_t msgsz,int msgflg);
参数:
msgid:消息队列ID
msgp:指向msgbuf的指针,用来指定发送的消息
msgsz:要发送消息的长度(结构体中mtext变量的大小)
msgflg:创建标记,如果指定IPC_NOWAIT,失败会立刻返回
返回值:
成功:0
失败:-1,并设置erron
(4)接收消息
ssize_t msgrv(int msqid,void *msgp,size_t msgsz,long msgtyp,int msgflg);
参数:
nsgid:消息队列ID
msgp:指向msgbuf的指针,用来接收消息(struct msgbuf{.....}:出参)
msgsz:要接收消息的长度
注意:参数msgsz指定由msgp参数指向的结构的成员mtext的最大大小(以字节为单位),msgtyp也有3种方式
msgtyp:接收消息的方式
1、msgtyp = 0:读取队列种的第一条消息(不区分类型,按插入顺序先进先出)
2、msgtyp > 0:读取队列种类型为msgtyp的第一条消息(顺序读取选中类型的队列内容)。除非在msgflg中指定了MSG_EXCEPT,否则将读取类型不等于msgtyp的第一条消息(顺序读取除选择类型外的队列内容)
3、msgtyp < 0:读取队列中类型小于或等于msgtyp绝对值的第一条消息
msgflg:创建标记,如果指定IPC_NOWAIT,获取失败立即返回
返回值:
成功:返回实际读取消息的字节数
失败:返回-1,并设置erron
(5)操作消息队列的接口
int msgctl(int msqid,int cmd,struct msqid_ds *buf);
参数:
msqid:消息队列ID
cmd:控制命令
例如:IPC_RMID,删除命令
IPC_STAT,获取状态
buf:存储消息队列的相关信息的buf
返回值:
成功:根据不同的cmd有不同的返回值
失败:返回-1,并设置erron
持续制作更新中, 创作不易求点赞,欢迎大家交流讨论!!!