进程间通信
一般简称为IPC----InterProcess Communication。是指在不同进程之间传播或交换信息。
进程间通信有这么几个方式:管道(命名管道/匿名管道),共享队列,消息队列,信号量
进程间为什么要通信呢?
因为进程的独立性,因此想要通信必须能够共同访问一个相同的媒介。
进程间通信的目的:数据传输,数据共享,进程间的访问控制。
也正因为通信的目的不同,使用场景不同,因此操作系统提供了多种进程间通信方式:
管道----传输数据
共享内存----共享数据
消息队列----传输数据
信号量----进程间的访问控制
管道
管道是半双工通信,双向选择的单向通信(即数据只能在一个方向上流动),具有固定的读端和写端
它是进程间的数据资料传输。在通信过程中,先将数据放到buf中,在将数据拷贝到自己的buf中在进行操作
管道生命周期随进程,如果进程消亡了,那么通信也就结束了
匿名管道
int pipe(int pipefd[2]);
//pipefd输出型参数
//数组pipefd用于返回引用结尾的两个文件描述符。
pipefd[0] 从管道读数据
pipefd[1] 从管道写数据
如果成功,则返回 0;不成功,则返回-1
只能用于具有亲缘关系的进程间通信
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.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){
close(pipefd[1]);
}else{
sleep(1);
close(pipefd[1]);
char buf[1024] = {0};
int ret = read(pipefd[0], buf, 1023);
printf("read buf:[%d - %s]\n",ret, buf);
}
return 0;
}
先建立管道,之后再创建子进程。这个时候就要考虑到管道的读写特性了
若管道中没有数据,则read会阻塞,直到读到数据返回
若管道中数据写满了,则write会阻塞,直到数据被读取,管道中有空闲位置,写入数据后返回
若管道中所有的读端都被关闭,则write会触发异常----SIGPIPE(信号标志)----导致进程退出
若管道中的所有写有写端都被关闭,则read返回0----通知用户没人写了
父子进程两端都要进行关闭
所以代码中将管道中写端关闭了,所以读端返回的是0.
虽然管道提供了双向选择,但是如果我们没有用到某一端,就把这一端关闭掉
管道同步与互斥特性
当读写数据的大小<管道pipe_buf ,是保证操作原子性-----这时操作不可被打断
互斥:保证对一个临界资源(公共资源,比如全局变量)同一时间的唯一访问性(我操作的时候你不能操作)
同步:保证对一个临界资源访问的时序可控性(我操作完了你才能操作)
|
管道符就是匿名访问
int main(){
int pipefd[2];
int ret = pipe(pipefd);
if(ret < 0){
perror("pipe error");
return -1;
}
int pid1 = fork();
if(pid1 == 0){
close(pipefd[0]);//关闭从管道读数据
dup2(pipefd[1],1);//将文件描述符表中 向管道写数据 替换 标准输出
execlp("ps","ps","-ef",NULL);//程序替换
}
int pid2 = fork();
if(pid2 == 0){
close(pipefd[1]);
dup2(pipefd[0],0);
execlp("grep","grep","ssh",NULL);
}
close(pipefd[0]);//不用的时候将读端和写端都关闭
close(pipefd[1]);
waitpid(pid1,NULL,0);
waitpid(pid2,NULL,0);
return 0;
}
grep读数据时不知道自己需要多少数据,过滤之后再次读取。
代码用图示来解答一下
命名管道(FIFO)
FIFO是一种文件类型,可以用于任意进程间通信。
可见于文件系统,因为创建命名管道会随之在文件系统中创建一个命名管道文件
类似于在进程中使用文件来传输数据,只不过FIFO类型文件同时具有管道的特性
因为所有的进程都能够通过打开管道文件,进而获取管道的操作句柄,因此命名管道可以用于同一主机上任意进程间通信
int mkfifo(const char *pathname, mode_t mode);
pathname:管道文件名
mode:创建权限 0664
fifo_read.c
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
int main(){
char *file = "./test.fifo";
umask(0);
int ret = mkfifo(file, 0664);
if(ret < 0){
if(errno != EEXIST){
perror("mkfifo errno");
return -1;
}
}
printf("open file\n");
int fd = open(file, O_RDWR);
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~~~\n");
}else{
perror("read error");
}
printf("---------\n");
}
close(fd);
return 0;
}
fifo_write.c
int main(){
char *file = "./test.fifo";
umask(0);
int ret = mkfifo(file,0664);
if(ret < 0){
if(ret != EEXIST){
perror("mkfifo error");
return -1;
}
}
printf("open file\n");
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));
}
return 0;
}
这有点类似于服务器–客户端之间建立连接
fifo_write类似于客服端发送一个请求 fifo_read类似于服务器返回一个请求
命名管道利用了文件系统创建文件进行通信。当write写入了数据,将数据从自己的buf写到test.fifo文件中,之后read在将数据从test.fifo中读取到自己的buf中,最后输出。
命名管道的读写特性
若管道没有被以写的方式打开,这时如果只读打开则会阻塞,直到文件被以写的方式打开
若管道没有被以读的方式打开,这时如果只写打开则会阻塞,直到文件被以读的方式打开
若管道以读写的方式打开,则不会阻塞
匿名管道和命名管道的区别
匿名管道:速度慢,容量有限,只有父进程进程能通讯
命名管道:任何进程间都能通讯,但速度慢
共享内存
共享内存的定义:在物理上开辟一块空间,内存直接映射到虚拟内存中,如果一块内存被多个进程映射,那么多个进程访问同一块内存,则可以实现通信。 是最快的进程间通信。因为相较于其他进程间通信方式(将数据从用户态拷贝到内核态,用的时候,从内核态拷贝到用户态),共享内存直接将一块内存映射到用户空间,用户可以直接通过地址对内存进行操作,并反馈到其他进程,少了两步数据拷贝的过程。
共享内存使用流程
1、创建/打开共享内存
int shmget(key_t key, size_t size, int shmflg);
key: 共享内存标识符
size: 共享内存大小
shmflg:打开方式/创建权限
IPC_CREAT 共享内存不存在则创建
IPC_EXCL 与IPC_CREAT同用,若存在则报错,不存在则创建
返回值:操作句柄shmid 失败:-1
key_t ftok(const char *pathname, int proj_id);
pathname: 文件名
proj_id: 数字
通过文件的 inode节点号 和 proj_id 共同得出一个key值
2、将共享内存映射到虚拟地址空间(建立映射关系)
void shmat(int shmid, const void *shmaddr, int shmflg);
shmid: 创建共享内存返回的操作句柄
shmaddr:用于指定映射在虚拟空间的首地址 通常置NULL
shmflg:0----可读可写
返回值:映射首地址(通过这个地址对共享内存进行操作) 失败:(void*)-1
3、对共享内存进行基本的内存操作,memcpy
4、解除映射关系 shmdt
int shmdt(const void *shmaddr)
shamddr: 映射返回的首地址
5、删除共享内存 shmctl
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid: 操作句柄
cmd: IPC_RMID 删除共享内存
buf: 设置或者获取共享内存信息,用不着置NULL
共享内存并不是立即删除的,只是拒绝后续映射连接,当共享内存
映射连接数为0时,则删除共享内存
shm_read.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/shm.h>
#define IPC_KEY 0x12345678
#define PROJ_ID 12345
#define SHM_SIZE 4096
int main(){
int shmid;
//1、创建共享内存
shmid = shmget(IPC_KEY, SHM_SIZE, IPC_CREAT|0666);
if(shmid < 0){
perror("shmget error");
return -1;
}
//2、将共享内存映射到虚拟地址空间
char *shm_start = (char*)shmat(shmid, NULL, 0);
if(shm_start == (void*)-1){
perror("shmat error");
return -1;
}
while(1){
printf("%s\n", shm_start);
sleep(1);
}
//4、解除映射
shmdt(shm_start);
//5、删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
shm_write.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/shm.h>
#define IPC_KEY 0x12345678
#define PROJ_ID 12345
#define SHM_SIZE 4096
int main(){
int shmid;
//1、创建共享内存
shmid = shmget(IPC_KEY, SHM_SIZE, IPC_CREAT|0666);
if(shmid < 0){
perror("shmget error");
return -1;
}
//2、将共享内存映射到虚拟地址空间
char *shm_start = (char*)shmat(shmid, NULL, 0);
if(shm_start == (void*)-1){
perror("shmat error");
return -1;
}
int i = 0;
while(1){
sprintf(shm_start, "明天又是可以学习的一天!!!+%d\n",i++);
sleep(1);
}
//4、解除映射
shmdt(shm_start);
//5、删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
共享内存双方都可以修改
共享内存没有同步与互斥
删除一块共享内存,并不会立即删除,而是判断映射连接数,若为0则删除,不为0则拒绝后续连接,直到为0删除
消息队列
消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
消息队列的建立过程为
创建消息队列---->添加数据节点---->获取数据节点---->删除
msgget---->msgsnd---->msgrcv(接收数据)---->msgctl
消息队列传输的是有类型的数据块,用户可以根据自己的需要选择性的获取某些数据类型
信号量
内核中的一个计数器----具有等待队列(PCB等待队列),具有等待和唤醒功能
用于资源计数,若计数小于等于0,表示没有资源,则需要等待
若计数大于0,表示有资源,则可以获取资源,然后计数-1
如果放置了资源,则计数+1,并且唤醒等待的进程
实现进程间的同步和互斥(资源计数为0或1的时候才具有互斥)
小结
消息队列和信号量现在的使用不是特别多,了解一下就可以。重点还是共享内存和管道的学习。
在代码过程中,我们应该可以感觉到,这种通信方式有点类似于服务器和客户端之间的处理过程。但是具体的实现是不同的。
匿名管道是通过对读端和写端的关闭和开启,在buf缓存区对数据进行拷贝和使用。
而命名管道是通过文件系统的打开和关闭,将数据进行读写。读写必须同时打开,否则另一端会被阻塞。
而共享内存是在同一个地址映射的一块虚拟地址被多个进程访问,这时也就是多个进程同时访问同一个内存。此时共享内存直接映射一块内存到用户空间,用户直接通过地址对内存进行操作,并反馈到其他进程。