进程间通信(IPC)
命令 :ipcs 查看系统的进程间通信方式
ipcs:-m 查看共享内存
-s 查看
-q 消息队列
ipcrm -m 0
操作系统为用户提供的几种进程间通信方式的学习
操作系统为什么要为用户提供进程间通信方式:因为进程的独立性(都操作的是自己虚拟地址空间中的虚拟地址,无法访问别人的地址)因此无法直接通信
因为进程将通信的场景不同因此提供了不同的几种进程间通信方式:
system V:管道,共享内存,消息队列,信号量
posix:信号量
管道
本质:内核中的一块缓冲区
实现进程间通信的原理:让多个进程通过访问到相同的缓冲区来实现通信,管道实现通信使用的是系统调用的IO接口(遵循一切皆文件的思想)
匿名管道
一个进程创建匿名管道,操作系统在内核重建一块缓冲区,并返回两个文件描述符作为管道的操作句柄(一个用于读,一个用于写,方向选择权交给用户),但是这个缓冲区在内核中没有标识。
操作接口:
int pipe(int pipefd[2]);
pipefd:至少具有两个int型元素的数组
创建一个管道,通过pipefd获取系统返回的管道操作句柄,其中:
pipefd[0]:用于从管道读取数据
pipefd[1]:用于从管道写入数据
返回:0 失败:-1
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>
int main()
{
//管道必须创建于创建子进程之前,这样子进程才能复制到管道的操作句柄
int pipefd[2];
int ret = pipe(pipefd);
if(ret < 0)
{
perror("pipe error");
return -1;
}
int pid = fork();
if(pid == 0)
{
//子进程
//close(pipefd[1]);
char buf[1024] = {0};
int ret = read(pipefd[0],buf,1023);
printf("child read:%d--%s\n",ret,buf);
exit(0);
}
else if(pid > 0)
{
//close(pipefd[0]);
write(pipefd[1],"hello world",11);
}
return 0;
}
运行结果:
管道的读写特性:
-
若管道中没有数据,则read会阻塞;直到数据被写入(缓冲区中有数据)
-
若管道中数据满了,则write会阻塞;直到数据被读取(缓冲区有空闲空间)
-
若管道的所有读端被关闭,则write会触发异常,进程退出
-
若管道所有写端被关闭,则read会返回0
管道的read返回0,不仅仅指的是没有读到数据 ----所有写端被关闭
匿名管道的简单实现:
ls | grep make ls:浏览目录,并且将结果写入到标准输出 grpe make:从标准输入循环读取数据,对读到的数据进行过滤匹配
匿名管道的实现就是创建两个进程,一个运行ls,一个运行grep make
让ls这个进程标准输出,重定向写入到管道写入端
让grep这个进程标准输入,重定向到管道的读取端
实现
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<fcntl.h>
#include<sys/wait.h>
int main()
{
int pipefd[2];
int ret = pipe(pipefd);
if(ret < 0)
{
perror("pipe error");
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
close(pipefd[1]);
dup2(pipefd[0],0);
execlp("grep","grep","make",NULL);
exit(0);
}
close(pipefd[1]);
close(pipefd[0]);
waitpid(pid1,NULL,0);
waitpid(pid2,NULL,0);
return 0;
}
命名管道
命名管道可以用户同一主机上的任意进程间通信
命名管道在内核中这块缓冲区是有标识的,意味着所有的进程都可以通过这个标识找到这块缓冲区实现通信
命名管道的标识实际上是一个文件,可见于文件系统,意味着所有进程都可以通过打开文件进而访问到内核中的缓冲区
命名管道的打开特性:
若文件当前没有已经被已读的方式打开,则以O_WRONLY打开时会阻塞
若文件当前没有已经被已写的方式打开,则以O_RDONLY打开时会阻塞
命名管道实现
写
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<errno.h>
#include<sys/stat.h>
int main()
{
char *fifo = "./test.fifo";
umask(0);
//创建命名管道
int ret = mkfifo(fifo,0664);
if(ret < 0)
{
if(errno != EEXIST)
{
perror("mkfifo error!");
return -1;
}
}
printf("---start open---\n");
//以只读打开命名管道
int fd = open(fifo,O_WRONLY);
printf("end open------\n");
if(fd < 0)
{
perror("open error");
return -1;
}
printf("fifo:%s open success\n",fifo);
while(1)
{
char buf[1024] = {0};
printf("i say: ");
fflush(stdout);
scanf("%s",buf);
write(fd,buf,strlen(buf));
}
close(fd);
return 0;
}
读
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<errno.h>
#include<sys/stat.h>
int main()
{
char *fifo = "./test.fifo";
umask(0);
//创建命名管道
int ret = mkfifo(fifo,0664);
if(ret < 0)
{
if(errno != EEXIST)
{
perror("mkfifo error!");
return -1;
}
}
printf("---start open---\n");
//以只读打开命名管道
int fd = open(fifo,O_WRONLY);
printf("end open------\n");
if(fd < 0)
{
perror("open error");
return -1;
}
printf("fifo:%s open success\n",fifo);
while(1)
{
sleep(3);
char buf[1024] = {0};
read(fd,buf,1023);
printf("peer say: %s\n",buf);
}
close(fd);
return 0;
}
管道总结
管道有两种:匿名管道,命名管道
匿名管道只能用于具有亲缘关系的进程间通信
命名管道可以用于同一主机上的任意进程间通信
管道特性:
-
管道是半双工的通信
-
管道的读写特性+(命名管道的打开特性)
-
管道的生命周期随进程(所有管道的操作句柄被关闭)
-
管道自带同步与互斥(管道的读写数据大小在不超过PIPE_BUF(4096)时保证操作的原子性)
-
管道提供字节流服务:传输灵活,但是存在粘包问题—本质原因:没有明显边界
同步:保证对临界资源操作的时序合理性(没有数据则读会阻塞,写满了则写会阻塞)
互斥:保证对临界资源操作在同一时间的唯一性(我操作时别人不能操作)
共享内存
最快的进程间通信方式
管道通信:通信的本质是通过内核的缓冲区来实现通信;
进程1将数据从用户态缓冲区拷贝到内核态缓冲区
进程2将数据从拷贝内核态缓冲区到用户缓冲区
涉及两次用户态与内核态之间的数据拷贝
[外链图片转存失败(img-gdzjssGX-1566019765847)(D:\Desktop\笔记\笔记\笔记\Linux笔记\Linux笔记\Linux笔记\图片\进程间通信\2.png)]
共享内存原理:
1.在物理内存中开辟一块空间
2.将这块内存空间通过页表映射到进程的虚拟地址空间
3.进程可以直接通过进程虚拟地址访问到这块物理空间,进行操作(若多个进程映射同一块物理内存,就可以实现相互通信–直接通过虚拟地址改变内存中的数据,其他进程也会随之改变,相较于其他通信方式,少了两步内核态与用户态之间的数据拷贝过程,因此速度最快)
4.解除映射
5.删除共享内存
1.int shmget(key_t key,size_t size,int shmflg)
key:共享内存在操作系统中的标识符
size:共享内存的大小
shmflg:
IPC_CREAT 共享内存若存在则打开,否则创建
IPC_EXCL 与IPC_CREAT同时使用时,若共享内存存在则报错返回
mode 共享内存的的操作权限
返回:正整数--共享内存的操作句柄
2.void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid:创建共享内存的操作句柄
shmaddr:共享内存在虚拟地址空间中的首地址---通常置NULL
shmflg:SHM_RDONLY---映射之后,共享内存只读;通常置零--可读可写
返回值:映射首地址 失败:(void*) -1
3.共享内存的操作
memcpy/strcpy
4.解除映射关系
int shmdt(const void*shmaddr)
shmaddr:shmat建立映射时返回的映射首地址
5.删除共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:共享内存操作句柄
cmd:即将进行的操作
IPC_RMID:删除共享内存
buf:用于获取/设置共享信息
共享内存的删除流程:共享内存再删除的时候,首相会判断当前映射链接数是否为0,若为0则直接删除;否则表示现在还有其他进程正在使用,则共享内存不能被立即删除,但是会拒绝后续进程的映射链接,等待映射链接数为0时删除这片共享内存
写
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>
#define IPC_KEY 0x1234567//给一个共享内存在操作系统中的标识符
int main()
{
//创建共享内存,返回值为操作句柄
int shmid = shmget(IPC_KEY,32,IPC_CREAT|0664);
if(shmid < 0)
{
perror("shmget error");
return -1;
}
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","good afternoon",i++);
sleep(1);
}
shmdt(shm_start);//解除映射关系
shmctl(shmid,IPC_RMID,NULL);//删除共享内存
return 0;
}
读
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>
#define IPC_KEY 0x1234567//给一个共享内存在操作系统中的标识符
int main()
{
//创建共享内存,返回值为操作句柄
int shmid = shmget(IPC_KEY,32,IPC_CREAT|0664);
if(shmid < 0)
{
perror("shmget error");
return -1;
}
void* shm_start = shmat(shmid,NULL,0); //建立映射,返回映射首地址
if(shm_start==(void*)-1)
{
perror("shmat error");
return -1;
}
int i = 0;
while(1)
{
printf("%s\n",shm_start);
sleep(1);
}
shmdt(shm_start);//解除映射关系
shmctl(shmid,IPC_RMID,NULL);//删除共享内存
return 0;
}
消息队列
操作系统在内核中为用户创建的一个队列;其他进程可以通过访问相同的队列进行通信
消息队列传输的是有类型的数据块
消息队列生命周期随内核
信号量
用于实现进程间同步与互斥
内核中的一个计数器
同步:时序合理----------功能:等待与唤醒
互斥:唯一访问
在对资源进行访问操作之前,先判断信号量计数(是否有资源能够操作);
若计数<=0;则等待(等待别人创建资源)计数-1;等待在等待队列上
若计数》0;则直接返回(按照程序流程就可以直接操作资源了)计数-1
其他进程创建了资源,计数+1;并且唤醒等待队列上的那些进程