第六章 进程间通讯
基本介绍
为什么需要进程间通讯
由于进程的独立性当我们想要一个进程把数据传输给另一个进程就需要进程间通讯的方式。但是想要传输数据就需要共同的媒介,以此达到数据传输,数据共享进程间访问控制等目的。
方式
由于通讯目的不同,场景不同,因此操作系统提供了多种进程间通信的方式。
1、管道(命名/匿名):传输数据。
2、共享内存:共享数据
3、消息队列:传输数据
4、信号量:进程间访问控制。
管道
什么是管道
管道是一种传输数据的媒介,所谓管道是操作系统在内核创建的一块缓冲区域,进程只要能够访问到这块缓冲区就能通过缓冲区相互通讯。并且只有在进程确认要发起进程间通讯时管道才会被创建,同时为了方便进程操作管道系统会给进程一个句柄(遥控器)方便进程操作,在LInux下这不过是给进程了两个文件描述符。为什么是两个文件描述符呢?系统为了操作更加方便易于管理功能更加全面,因此两个文件描述符一个描述符负责读管道,另一个负责写数据。
匿名管道
匿名管道是只能用于具有亲缘关系的进程间通信(共同祖先)。用pipe()
创建管道。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
/**
* int pipe(int pipefd[2]);
* pipefd[0] 用于从管道读数据
* pipefd[1] 用于从管道写数据
* 返回值:0 失败:-1
*/
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)
{
//child read
char buf[1024] = {0};
read(pipefd[0], buf, 1023);
printf("read buf:%s\n", buf);
}
else
{
//parent write
char* ptr = "Misaki!!!";
write(pipefd[1], ptr, strlen(ptr));
}
}
[misaki@localhost 第六章-进程间通讯]$ ./ipc
read buf:Misaki!!!
读写特性:若管道中没有数据,则read
会阻塞,直到读到数据返回。若管道中数据满了,则write
会阻塞,直到数据被读取,管道中有空闲位置,写入数据后返回。如果管道所有读端都被关闭,则write()
会触发异常,发起SIGPIPE
信号导致进程退出。如果管道所有写端都被关闭则read()
读完管道所有数据后返回0。如果设置非阻塞,如果管道中暂时没有数据则返回-1,errno
置为EAGAIN
。
同步与互斥特性:当读写大小小于PIPE_BUF
时保证操作原子性,即操作不可被打断,保证读写一次性完成。互斥:对临界资源(即公共资源)同一时间的唯一访问性。同步:保证对临界资源访问的时许可控性。同时管道是一个半双工通信(可选择方向的单行通信)。以上的特性保证当有多个进程访问管道时向管道中读写数据不会出现顺序出错内容出错的问题。
管道提供流式服务:面向字节流数据传输,传输十分灵活,但会造成数据粘连。
命名管道
可见于文件系统,会在创建管道时创建一个命名管道文件,所有进程都可以通过打开管道文件进而获取管道的操作句柄,因此命名管道到可以用于同一主机上的任意进程间通信。但其本质依然是系统的内核,只是通过文件向所有的进程提供了访问的方法。我们在shell中用mkfifo filename
创建命名管道。我们在进程中用mkfifo()
创建命名管道。
命名管道打开特性:
若管道没有被以写的方式打开,这时如果只读打开则会阻塞,直到文件被以写的方式打开。
若管道没有被以读的方式打开,这时如果只写打开则会阻塞,直到文件被以读的方式打开。
这里需要两个进程一个负责写入一个负责读取才能让命名管道生效,如果同时以读写方式打开进程也可以让其生效。
fifo_read.c:
/* 命名管道的基本使用
* int mkfifo(const char *pathname, mode_t mode);
* pathname: 管道文件名
* mode: 创建权限 0664
* 返回值:0 失败:-1
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.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 error");
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:
#include <string.h>
#include <fcntl.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 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));
}
close(fd);
return 0;
}
结果:
对于命名管道来说,匿名管道的读写特性,依然使用,并且依旧自带同步与互斥,采用字节流方式的数据传输。
命名管道的生命周期与进程同步,及管道创建是在内核的一块缓冲区上,其实也是一块内存,与磁盘无关,而创建的管道文件不管是用于标记这块内核中的缓冲区用的。当进程使用到命名管道时内核中才会创建这块缓冲区供进程使用,当所有进程全部关闭管道时,这块缓冲区释放。
共享内存
共享内存是在物理内存上开辟一块空间,将这块空间映射到进程的虚拟地址空间中,进程则可以用通过虚拟地址进行访问操作;如果一块内存被多个进程映射,那么多个进程访问同一块内存,则可以实现通讯。
特点
共享内存是将两个进程共用同一块物理内存,是最快的进程间通信方式,因为相较于其他进程间通信方式(将数据从用户态拷贝到内核态,用的时候,再从内核态拷贝到用户态)其直接操作内存,少了两步拷贝操作。
共享内存在创建时会在开辟一块物理内存空间,多块进程要共享同一块物理内存只需要将这块物理内存通过页表映射到多个进程的虚拟内存空间上即可使用。
共享内存的生命周期随内核,操作系统重启,共享内存删除。共享内存不具备同步和互斥,因此其是不安全的。
共享内存的操作
shell内操作
1、ipcs -m
查看共享内存。
2、ipcrm -m shmid
删除共享内存。
程序内操作
程序内使用共享内存基本分为以下4步:
1、创建共享内存。shmget()
不过在创建共享内存时我们会用到一个叫做key
的参数,这个参数成为ipc键值,每一个键值对应一个唯一的ipc对象,我们可以通过ftok()
生成一个key
,也可以自己宏定义一个key
,但是后者需要保证自己定义的key
与系统中已经存在的不冲突,而前者则有系统自己保证。
2、将共享内存映射到虚拟地址空间。shmat
3、对共享内存进行基本的内存操作。memcpy()...
4、解除映射关系。shmdt()
5、删除共享内存。shmctl()
在删除共享内存时会判断它的映射连接数,如果为0的话则直接删除,不为0则拒绝后续连接,直到为0删除。
shm_read.c
/*共享内存使用流程:
*1. 创建/打开共享内存 shmget
key_t ftok(const char *pathname, int proj_id);
pathname: 文件名
proj_id: 数字
通过文件的inode节点号和proj_id共同得出一个key值
int shmget(key_t key, size_t size, int shmflg);
key: 共享内存标识符
size: 共享内存大小
shmflg:打开方式/创建权限
IPC_CREAT 共享内存不存在则创建,存在则打开
IPC_EXCL 与IPC_CREAT同用,若存在则报错,不存在则创建
mode_flags 权限
返回值:操作句柄shmid 失败:-1
*2. 将共享内存映射到虚拟地址空间 shmat
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);
shmaddr: 映射返回的首地址
*5. 删除共享内存 shmctl IPC_RMID 删除共享内存
buf: 设置或者获取共享内存信息,用不着则置NULL
共享内存并不是立即删除的,只是拒绝后续映射连接,当共享内存
映射连接数为0时,则删除共享内存
*/
#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. 创建共享内存
//key_t key = ftok(".", PROJ_ID);
//PROJ_ID只占用后八位,即1-255
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)
{
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. 创建共享内存
//key_t key = ftok(".", PROJ_ID);
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, "Misaki:%d\n", i++);
sleep(1);
}
//4. 解除映射
shmdt(shm_start);
//5. 删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
[misaki@localhost ipc]$ ./shm_write
^C
[misaki@localhost ipc]$ ./shm_read
Misaki:0
Misaki:1
Misaki:2
Misaki:3
Misaki:4
Misaki:5
Misaki:6
Misaki:7
Misaki:8
Misaki:9
Misaki:10
Misaki:11
^C
删除共享内存时,共享内存并不会立刻被删除,因为这样可能会影响正在使用共享内存的进程崩溃,而是将共享内存的key
置为0,表示不再接收后续进程的映射,等所有进程都解除与它的映射后,共享内存才会完全被删除。
消息队列
在使用消息队列时我们会在内核中创建一个消息队列结构,在进程中进程要想操作消息队列要自行创建一个消息队列结构体,将数据放进去构成队列,然后再将队列传输到内核。由于内核中存储数据的队列只有一个,为了方便区分不同进程的数据,在消息队列结构体中会有一个type
变量以便区分这个数据属于哪个进程。在某个进程获取消息队列中的数据时可以指定type
,得到自己希望得到的进程的数据。
消息队列自带同步与互斥,生命周期随内核,数据传输自带优先级。
使用接口
msgget 创建
msgsnd 添加节点
msgrcv 获取结点
msgctl 操作-删除消息队列
信号量
信号量时内核中的一个计数器,并且具有等待队列,具有等待与唤醒的功能。
信号量用于资源计数,若计数<=0则表示没有资源则读取需要等待资源,直到如果放置了资源,则计数+1,然后唤醒等待的进程;若资源>0则可以获取资源,之后计数-1。
信号量实现进程间的同步与互斥。
信号量中有两种重要操作分别是P
操作和V
操作,P
操作表示获取资源,计数器-1,V
操作表示归还资源,计数器+1。关于信号量在线程安全篇会细致讲解。