进程间通信(IPC)
原因:由于进程之间具有独立性,只能访问自己的虚拟地址空间,不能主动通信,因此需要操作系统提供公共的媒介。
进程间通信目的(应用场景)
- 数据传输
- 资源共享
- 进程控制
- 通知事件
进程间通信分类
管道
- 匿名管道
- 命名管道
System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC(线程)
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
管道(半双工通信)
我们把从一个进程连接到另一个进程的数据流叫做“管道”。
本质:内核中的一块缓冲区;多个进程通过访问同一块缓冲区实现通信。
匿名管道(缓冲区没有标识符)
一个进程通过系统调用在内核中创建一个管道(缓冲区),并调用返回管道的操作句柄。但是内核中的这块缓冲区没有其他标识符,只能通过操作句柄访问。
管道的操作句柄:两个描述符,一个从管道读取数据,一个向管道写入数据。
创建匿名管道
#include<unistd.h>
int pipe(int fd[2]);
//int fd[2]定义有两个int型的数组
//将数组fd[2]的首地址传给pipe;
参数:
fd:文件描述符,其中的fd[0]表示读端,fd[1]表示写端
返回值:成功返回0;失败返回-1.
用fork来共享管道
匿名管道只能用于具有亲缘关系的进程间通信。因为只能通过子进程复制父进程的方式获取到同一个管道的操作句柄,进而访问同一块缓冲区。所以管道创建于创建子进程之前。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
int main()
{
int pid;
int fd[2];
if(pipe(fd)<0){
perror("pipe error");
return -1;
}
pid=fork();
if(pid==0){
//子进程向管道读取数据
char buf[1024];
if(read(fd[0],buf,1023)<0){
perror("read error\n");
return -1;
}
printf("child get data:[%s]\n",buf);
}
else{
//父进程向管道写入数据
char *ptr="hello world";
if(write(fd[1],ptr,strlen(ptr))<0){
perror("write error");
return -1;
}
}
}
管道读写规则
- 管道中没有数据,则read读取数据会堵塞,直到有数据写入
- 管道中数据写满,则write写入数据时会阻塞,直到有数据被读出去
- 若管道中**所有写端被关闭,则read读完数据后不会阻塞,而是返回0(**read返回0表示管道不再写入)
- 若管道中所有读端被关闭,则write写入数据时会触发异常,进程退出
- 当要写入的数据量不大于PIPE_BUF=4096大小时,linux将保证操作的原子性
操作原子性:这个操作不会被打断。(实现互斥)
命名管道
内核中的这块缓冲区有一个标识符,这个标识符是一个可见于文件系统的管道文件。
特性:可用于同一台主机上的任意进程间的通信
创建命名管道
- 在命令行创建
$ mkfifo filename
- 在程序里创建
int mkfifo (const char *filename,mode_t mode);
int main(int argc,char *argv[])
{
mkfifo("p2",0664);
return 0;
}
用的时候才会创建缓冲区
命名管道的打开规则
- 若命名管道以只读方式打开会阻塞,直到管道文件被其他进程以写的方式打开。
- 若命名管道以只写方式打开会阻塞,直到管道文件被其他进程以写的方式打开。
匿名管道与命名管道的区别
- 匿名管道用pipe函数创建并打开,可用于具有亲缘关系进程间的通信
- 命名管道用mkfifo函数创建,用open打开,用于同一主机下的任意进程间的通信
//写端
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
#include<errno.h>
int main()
{
char *file="./tp";
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");
}
printf("open success\n");
while(1){
char buf[1024]={0};
scanf("%s",buf);
ret=write(fd,buf,strlen(buf));//向fd所引用的文件中写入buf的内容
if(ret<0){
perror("write error");
return -1;
}else if(ret==0){
printf("没人读就关闭");
return 0;
}
}
return 0;
}
//读端
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
#include<errno.h>
int main()
{
char *file="./tp";
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");
}
printf("open success\n");
while(1){
char buf[1024]={0};
ret=read(fd,buf,1023);//从fd中读取数据放到buf中
if(ret<0){
perror("read error");
return -1;
}else if(ret==0){
printf("没人写就关闭\n");
return 0;
}
printf("buf:[%s]\n",buf);
}
return 0;
}
管道特点
-
匿名管道用于有亲缘关系的进程之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,之后父子进程就可以应用该管道。
-
命名管道可用于同一台主机上的任意进程间的通信。
-
管道提供字节流服务。(可能会发生数据粘连)
-
进程退出,管道释放,所以管道的生命周期随进程。
-
内核会对管道的操作进行同步与互斥。
-
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立两个管道。
-
同步:通过一些条件的判断,来实现对临界资源访问的时序合理性
-
互斥:同一时间只有一个执行流能够操作临界资源,实现对数据的安全操作
system V IPC
system V共享内存***
共享内存:用于进程间数据共享。
实现原理与步骤:
- 原理:共享内存相较与其他通信方式,在通信过程中少了两次用户态与内核态间的数据拷贝,以此通信最快。
- 在物理内存中开辟一块空间——在内核中具有标识(可以被其他内存找到)
- 将这块物理内存通过页表映射到自己的虚拟地址空间。
- 通过虚拟地址进行内存操作。
- 接触映射关系。
- 删除共享内存。
1.shmget函数
功能:用来创建共享内存
原型:int shmget(key_t key ,size_t size ,int shmflg);
参数:
- key:这个共享内存段的标识(其他进程通过相同标识打开同一内存)。
- size:共享内存的大小。
- shmflg:权限标志位,用法与创建文件时使用的mode模式标志是一样的(IPC_CREAT | IPC_EXCL)。
返回值:成功返回一个非负整数(即该共享内存段的操作句柄,并不是key);失败返回-1
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/shm.h>
#include<sys/ipc.h>
#include<unistd.h>
#define key 0x12345665
#define size 4096
int main()
{ //创建共享内存
int shmid=shmget(key,size,IPC_CREAT|0664);
if(shmid<0){
perror("shmget error");
return -1;
}
return 0;
}
2.shmat函数
功能:将共享内存段连接到进程地址空间(建立映射关系)
原型:void* shamt(int shmid ,const void *shmaddr ,int shmflg);
参数:
- shmid:共享内存标识 。
- shmaddr:指定连接的地址(通常置空NULL,自动选择一个地址)。
- shmflg:指具体对这块共享内存操作。它的两个可能取值是SHM_RND和SHM_RDONLY(只读),置为0表示可读可写。
返回值:成功返回一个指针(映射首地址);指向共享内存第一个节;失败返回-1。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/shm.h>
#include<sys/ipc.h>
#include<unistd.h>
#define key 0x12345665
#define size 4096
int main()
{ //创建共享内存
int shmid=shmget(key,size,IPC_CREAT|0664);
if(shmid<0){
perror("shmget error");
return -1;
}
return 0;
//映射到虚拟地址
void *shm_start=shmat(shmid,NULL,0);
if(shm_start==(void*)-1){
perror("shmat error");
return -1;
}
return 0;
}
3.shmdt函数
功能:将共享内存段与当前进程脱离
原型:int shmdt ( const void *shmaddr );
参数:
- shmaddr:由shmat返回的指针。
4.shmctl函数
功能:用于控制共享内存
原型:int shmctl ( int shmid ,int cmd ,struct shmid_ds *buf );
参数:
- shmid:由shmget返回的操作句柄。
- cmd:将要采取的动作,有三个可取值。
命令 | 说明 |
---|---|
IPC_STAR | 把shmid_ds结构中的数据设置为共享内存的当前关联值 |
IPC_SET | 在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的的值 |
IPC_RMID | 删除共享内存段(并不会立即删除,拒绝后续的映射操作,等映射连接数为0才会删除) |
- buf:指向一个保存着共享内存模式状态和访问权限的数据结构(通常置空)。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/shm.h>
#include<sys/ipc.h>
#include<unistd.h>
#define key 0x12345665
#define size 4096
int main()
{ //创建共享内存
int shmid=shmget(key,size,IPC_CREAT|0664);
if(shmid<0){
perror("shmget error");
return -1;
}
return 0;
//映射到虚拟地址
void *shm_start=shmat(shmid,NULL,0);
if(shm_start==(void*)-1){
perror("shmat error");
return -1;
}
int i=0;
while(1){
sprintf(shm_start,"%s-%d\n","you are beautiful!",i++);//sprintf格式化一个字符串放到一个buf里
sleep(1);
}
//解除映射关系
shmdt(shm_start);//shmat返回的指针
//控制共享内存
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
特性:
- 是最快的IPC(进程间通信)形式。
- 共享内存中进程间数据传递不会涉及到内核
- 共享内存中并不保证数据的同步与互斥
- 生命周期随内核
system V消息队列
消息队列:用于实现进程间的数据块传输。
本质:内核中的一个优先级队列,多个进程通过向同一个队列放置队列节点或获取节点实现通信。
实现:
- 在内核中创建消息队列(megget)
- 向消息队列中添加节点(msgsnd)
- 从消息队列中获取节点(msgrev)
- 删除消息队列(msgctl)
特性:
- 消息队列自带同步与互斥
- 传输有类型的数据块
- 数据不会粘连
- 生命周期随内核
system V信号量
信号量:用于实现进程间同步与互斥。
- 同步:通过一些条件判断,实现对临界资源有序访问——数据访问的合理性。
- 互斥:通过同一时间只有一个进程能够访问临界资源,实现数据的安全操作——数据访问安全性。
本质:内核中原子操作的计数器 + 等待队列
等待队列:当没有资源可以访问的时候,将想要获取资源的PCB的状态置为可中断休眠,并加入到等待队列,当有资源可以访问时,就从等待队列中唤醒PCB去获取资源
实现
- 互斥:通过一个只有0/1的计数器实现
通过一个状态标记临界资源当前的访问状态,对临界资源进行访问之前先判断一下这个标记,若状态为可访问,则将这个状态修改为不可访问,然后再访问数据,访问完毕后再将状态修改为可访问状态。 - 同步:通过一个计数的判断以及等待与唤醒功能实现
通过一个计数器对资源数量进行计数,当一个进程想要获取资源的时候,则先判断计数;若有资源可被访问时,则计数-1,再进行资源的获取:若没有资源可被访问时(计数<=0),则再等待队列上进行等待,等到其他进程生产数据后计数+1,就唤醒等待队列上的进程。
信号量的pv原语:
- p 操作: 对计数进行判断,然后-1,若没有资源则等待。
- v 操作:对技术进行+1,唤醒等待队列中的挂起的进程。