目录
前言
什么是进程间通信
进程间通信是指在不同进程之间传播或交换信息;每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信
进程间通信方式
管道
本质:在内核中开辟一块缓冲区;若多个进程拿到同一个管道(缓冲区)的操作句柄,就可以访问同一个缓冲区,就可以进行通信
相对速度:涉及到两次用户态与内核态之间的数据拷贝----将数据写入管道与从管道读取数据
分类:匿名管道 和 命名管道
匿名管道(PIPE)
匿名管道:在内核中的缓冲区是没有具体的标识符的,匿名管道只能用于具有亲缘关系的进程间通信
在父子进程中,子进程几乎复制了父进程的所有信息,当父进程开辟一个缓冲区时,会有一个操作句柄,而子进程也复制了该操作句柄,通过操作句柄,就能访问到该同一个缓冲区了。
如何创建匿名管道
接口int pipe(int pipefd[2])
pipefd[2]是具有两个int类型的节点的数组的首地址,用于接收创建管道而返回的操作句柄
pipefd[0]
用于从管道中读取数据
pipefd[1]
用于向管道中写入数据
管道是一个单向的资源传输,自身并不会限制资源的传输方向;管道是一个半双工通信(可以选择方向的单向传输);所以当要使用读/写数据时,将另一方向写/读关闭即可,读写不能同时进行
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main()
{
int pipefd[2] = {-1};
int ret = pipe(pipefd);
if (ret < 0)
{
perror("pipe error");
return -1;
}
pid_t pid = fork();
if (pid == 0)
{
//子进程从管道读取数据
char buf[1024] = {0};
read(pipefd[0], buf, 1023);
printf("child:%s", buf);
}
else if (pid > 0)
{
//父进程向管道写入数据
char *ptr = "Hello WhiteShirtI\n";
write(pipefd[1], ptr, strlen(ptr));
}
return 0;
}
管道的读写特性
- 若管道中没有数据,则调用read读取数据会阻塞
- 若管道中数据满了(约64k),则调用write写入数据会阻塞
- 若管道的所有读端pipefd[0]被关闭,则继续调用write会产生异常导致进程退出 ----父子进程都调用
close(pipefd[0])
- 若管道的所有写端pipefd[1]被关闭,则继续调用read,read读完管道中的数据后不再阻塞,而返回0 ----父子进程都调用
close(pipefd[1])
命令行管道符的实现
以ps -ef | grep chg
来举例 功能:在ps -ef查找到的数据中过滤出含有chg
字符的所在行数据
//pipe2.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
int pipefd[2] = {-1};
if (pipe(pipefd) < 0)
{
perror("pipe error");
return -1;
}
//ps程序
pid_t ps_pid = fork();
if (ps_pid == 0)
{
//将标准输出重定向到管道的写端,将要打印的数据写到管道中
dup2(pipefd[1], 1);
//将该子进程替换成ps进程
execlp("ps", "ps", "-ef", NULL);
exit(0);
}
//grep程序
pid_t grep_pid = fork();
if (grep_pid == 0)
{
//关闭写端,防止读完数据后发生阻塞
close(pipefd[1]);
//将标准输入重定向到管道的读端,从0-标准输入读取的数据相当于从管道读取数据
dup2(pipefd[0], 0);
//将该子进程替换成grep进程
execlp("grep", "grep", "ssh", NULL);
exit(0);
}
//关闭父进程的读取端
close(pipefd[0]);
close(pipefd[1]);
waitpid(ps_pid, NULL, 0);
waitpid(grep_pid, NULL, 0);
return 0;
}
命名管道(FIFO)
命名管道:命名管道也是内核中的一块缓冲区,并且这个缓冲区具有标识符;这个标识符是一个可见于文件系统的管道文件,能够被其他进程找到并打开管道文件,则可以获取管道的操作句柄,所以该命名管道可用于同一主机上的任意进程间通信。
多个进程要通过命名管道来通信,最终是要通过打开命名管道文件访问同一块内核中的缓冲区来实现通信的
命令行中可通过mkfifo name.fifo
创建管道文件
在代码中:int mkfifo(const char *pathname, mode_t mode)
参数内容(pathname:管道文件名称;mode:管道文件权限)
返回值:成功返回0;失败返回-1
io接口中open打开命名管道的特性:
- 若文件以只读的方式打开,则会阻塞,直到文件被以写的方式打开
- 若文件以只写的方式打开,则会阻塞,直到文件被以读的方式打开
接下来演示两个进程间通过命名管道来实现通信
fifo_read.c
//fifo_read.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
int main()
{
umask(0);
//创建管道文件
int ret = mkfifo("./test.fifo",0664 );
//管道文件不是因为存在而创建失败
if (ret < 0 && errno != EEXIST)
{
perror("mkfifo error");
return -1;
}
//以只读方式获取管道文件的操作句柄
int fd = open("./test.fifo", O_RDONLY);
if (fd < 0)
{
perror("open fifo error");
return -1;
}
printf("open fifo success\n");
int i = 0;
//一直读数据
while(1)
{
char buf[1024] = {0};
//将从管道读取的文件写到buf中
int ret = read(fd, buf, 1023);
++i;
if (ret < 0)
{
perror("read error");
return -1;
}
//所有写端都关闭
else if (ret == 0)
{
perror("all write closed");
return -1;
}
//打印所读到的数据
printf("read buf:[%s] %d\n",buf, i);
}
close(fd);
return 0;
}
fifo_write.c
//fifo_write.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
int main()
{
umask(0);
int ret = mkfifo("./test.fifo", 0664);
if (ret < 0 && errno != EEXIST)
{
perror("mkfifo error");
return -1;
}
//以只写的方式打开管道文件
int fd = open("./test.fifo", O_WRONLY);
if (fd < 0)
{
perror("open fifo error");
return -1;
}
printf("open fifo success\n");
int i = 0;
//一直写
while(1)
{
char buf[1024] = {0};
//将字符串内容写到buf中,并记录写的次数
sprintf(buf, "hello WhiteShirtI", i++);
//将buf中的内容写到管道中
write(fd, buf, strlen(buf));
printf("write data success\n");
sleep(1);
}
close(fd);
return 0;
}
先运行只读的进程
再运行只写的进程
再看只读的进程
这样就验证了以open打开命名管道的特性
在关闭时,如果先将只读进程关闭,那么只写进程也会一起退出,如果先将只写进程关闭,那么只读进程中的read接口会返回0,通过该值控制进程退出
管道特性
- 管道是半双工通信
- 管道的读写特性
2.1 若管道中没有数据,则调用read读取数据会阻塞
2.2 若管道中数据满了(约64k),则调用write写入数据会阻塞
2.3. 若管道的所有读端pipefd[0]被关闭,则继续调用write会产生异常导致进程退出
2.4. 若管道的所有写端pipefd[1]被关闭,则继续调用read,read读完管道中的数据后不再阻塞,而返回0 - 管道的生命周期与进程一样(当打开的管道的所有进程都退出,管道就会被释放)
- 管道提供字节流传输服务(可靠的、有序的、基于连接的一种灵活性比较高的传输服务)
- 对于命名管道的独有特性:
5.1 若文件以只读的方式打开,则会阻塞,直到文件被以写的方式打开
5.2 若文件以只写的方式打开,则会阻塞,直到文件被以读的方式打开 - 管道自带同步与互斥
6.1 管道中没有数据则read会阻塞/管道中数据满了则write会阻塞
6.2 管道的读写操作在PIPE_BUF(4096byte)大小以内保证操作的原子性
补充知识:
同步:通过条件判断实现对临界资源操作的合理性
互斥:通过同一时间的唯一访问实现临界资源操作的安全性
临界资源:所有进程都能访问到的资源
原子性(原子操作):不能被打断的操作,指的是一个操作要么一次完成,要么就不操作
如果管道不具备第6点特性,当要进行写读写时由于中断或者时间片到的原因,会停止一个进程的操作,下一个进程的操作开始,会产生数据的二义性
共享内存
本质:在物理内存上开辟一块内存空间,多个进程可以将同一块物理内存空间映射到自己的虚拟地址空间,通过自己的虚拟地址直接访问这块空间,通过这种方式实现数据共享
特性:
- 共享内存是最快的进程间通信方式
- 生命周期不随进程,随内核(没有人为情况下)
- 共享内存没有自带同步与互斥,多个进程进行访问的时候存在安全问题
相对速度:直接通过虚拟地址访问物理内存实现共享内存中的数据操作,速度最快
共享内存的操作流程
- 创建共享内存----在物理内存上开辟空间
- 进程将共享内存映射到自己的虚拟地址空间
- 基本的内存操作都可以对这块空间进行操作
- 如何一进程要退出,就要解除虚拟地址空间与共享内存的映射关系
- 释放共享内存空间
如何创建共享内存
接口int shmget(ket_t key, size_t size, int shmflg)
参数内容:(key
内核中共享内存的标识符,多个进程通过相同的标识符才能打开同一个共享内存; size
共享内存的大小,以内存页(4096byte)为单位进行分配; shmflg
IPC_CREAT存在打开,不存在创建 | IPC_EXCL与IPC_CREAT同时使用,存在保存,不存在创建 | mode权限)
返回值:返回一个非负整数,也就是共享内存的操作句柄,失败返回-1
如何将共享内存映射到自己的虚拟地址空间
接口void* shmat(int shmid, const void* shmaddr, int shmflg)
参数内容(shmid
返回的共享内存的操作句柄;shmaddr
共享内存映射到虚拟地址空间的首地址----通常置NULL;shmflg
映射成功之后对共享内存可以进行的操作 SHM_RDONLY 用于只读(前提是有读的权限),默认为0,可读可写)
返回值:返回共享内存映射在虚拟地址空间的首地址,通过这个首地址进行后序的内存操作,失败返回-1
对共享内存的读写操作
如果内存中没有数据,读取共享内存数据不会发生阻塞
共享内存数据的写入,是一种针对地址指向空间的覆盖式写入
如何解除映射关系
接口int shmdt(const void* shmaddr)
参数内容(shmaddr
映射在虚拟地址空间的首地址)
返回值:成功返回0,失败返回-1
如何删除共享内存
接口shmctl(int shmid, int cmd, struct shmid_ds *buf)
参数内容(shmid
共享内存的操作句柄;cmd
共享内存想要进行的操作 IPC_RMID删除共享内存 buf
用于获取/设置共享内存信息的结构,不使用置NULL)
返回值:成功返回0,失败返回-1
共享内存删除的时候,并不会立即被删除,只是将其状态置为被销毁状态,移除标识符key,为了不让这个共享内存继续被其他进程映射连接,然后等到当前共享内存映射连接数为0的时候,才会真正删除这块共享内存
操作系统中进程间通信资源的命令
ipcs
查看进程间通信资源
-m
查看共享内存
-q
查看消息队列
-s
查看信号量
ipcrm -m/q/s shmid
删除指定的通信资源
代码实现
shm_read.c
//shm_read.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/shm.h>
//自定义标识符,最终是以16进制来显示
#define IPC_KEY 0x12345678
int main()
{
//1.创建共享内存
int shm_id = shmget(IPC_KEY, 32, IPC_CREAT|0664);
if (shm_id < 0)
{
perror("shmget error");
return -1;
}
//2.建立映射关系
void* shm_start = shmat(shm_id, NULL, 0);
if (shm_start == (void*)-1)
{
perror("shmat error");
return -1;
}
//3.进行内存操作
while(1)
{
//打印内存中的数据
printf("[%s]\n", shm_start);
sleep(1);
}
//4.解除映射关系
shmdt(shm_start);
//5.删除共享内存
shmctl(shm_id, IPC_RMID, NULL);
return 0;
}
shm_write.c
//shm_write.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/shm.h>
#define IPC_KEY 0x12345678
int main()
{
//1.创建共享内存
int shm_id = shmget(IPC_KEY, 32, IPC_CREAT|0664);
if (shm_id < 0)
{
perror("shmget error");
return -1;
}
//2.建立映射关系
void* shm_start = shmat(shm_id, NULL, 0);
if (shm_start == (void*)-1)
{
perror("shmat error");
return -1;
}
//3.进行内存操作
while(1)
{
//将数据按格式写入shm_start中
sprintf(shm_start,"%s+%d", "WhiteShirtI", i++ );
sleep(1);
}
//4.解除映射关系
shmdt(shm_start);
//5.删除共享内存
shmctl(shm_id, IPC_RMID, NULL);
return 0;
}
先运行shm_read
再运行shm_write后查看shm_read的打印情况
当结束shm_read后果5秒再运行,可以证明共享内存写入是覆盖式写入
当结束shm_write进程后,重新执行shm_read,会发现读取的还是原来最后写入的数据,证明共享内存周期并不会跟随进程的周期结束而销毁
消息队列
内核中的一个优先级队列,多个进程通过访问同一个队列,进行添加结点或者获取结点实现通信
操作流程
- 创建消息队列,也就是在内核中创建一个优先级队列
- 进程可以向队列中添加或获取节点
- 删除消息队列
如何创建消息队列
接口int msgget(key_t key, int msgflg)
参数内容(key
标识符;msgflg
创建和权限)
如何获取消息队列
接口int msgsnd(int msqid, const void* msgp, size_t msgsz, int msgflg)
和 ssize_t msgcv(int msqid, void* msgp, size_t msgsz, long msgtyp, int msgflg)
参数内容(mspid
操作句柄;msgp
向队列中添加的结点;msgsz
数据大小;msgflg
;msgtyp
指定获取什么类型的数据,与msgp中结点的类型对应;msgflg
创建和权限)
msgp结点是一个结构体,需要用户自己定义
strcut msgbuf
{
long mtype;
char mtext[1];
}
如何删除消息队列
int msgctl(int msqid, int cmd, struct msqid_ds *buf)
参数内容(msqid
标识符;cmd
共享内存想要进行的操作 IPC_RMID删除共享内存 buf
用于获取/设置共享内存信息的结构,不使用置NULL)
特性
- 自带同步与互斥
- 生命周期随内核
信号量
信号量是用于实现进程间的同步与互斥(如共享内存不提供同步与互斥,存在安全隐患,可以使用信号量搭配共享内存使用)
组成:一个内核中的计数器+pcb等待队列+使进程等待/唤醒的接口
本质:信号量通过自身的计数器实现对资源进行计数并在访问时进行条件判断
同步流程:获取资源前进行P操作,合理则获取资源,不合理就阻塞,访问完资源后进行v操作;计数器>0表示可以访问,获取一个资源,计数器-1(P操作);计数器<=0表示不能访问,计数器-1,然后将pcb状态置为可中断休眠状态,加入等待队列中;当有进程释放资源后(V操作),从等待队列中唤醒一个pcb去获取资源
互斥流程:保证信号量的资源计数器不大于1----假设资源只有一份,只有一个进程能够访问,用完后释放该资源,唤醒下一个进程的访问
面试常问:你了解IPC吗
1、进程间通信方式是操作系统为用户提供的进程之间实现数据通信的方式,因为进程之间具有独立性,无法直接通信。各进程都使用的是自己的虚拟地址空间,无法通过同一地址访问同一块内存
2、操作系统根据不同的场景提供了不同的方式,管道,共享内存,消息队列和信号量。其中管道是内核中的一块缓冲区,分为匿名管道和命名管道。匿名管道只能用于具有亲缘关系的进程间;而命名管道可用于同一主机上任意进程间通信。共享内存的本质是一块物理内存,多个进程将同一块物理内存映射到自己的虚拟地址空间中,再通过页表映射到物理地址达到进程间通信,它是最快的进程间通信方式,相较其他通信方式少了两步数据拷贝操作。消息队列是内核中的一个优先级队列,多个进程通过访问同一个队列,在队列当中添加或者获取节点来实现进程间通信。信号量的本质是内核中的一个计数器,主要实现进程间的同步与互斥,对资源进行计数,有两种操作,分别是在访问资源之前进行的p操作,还有产生资源之后的v操作。