进程间通信(匿名管道 | 命名管道 | 共享内存)教程
进程通信的目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止
时要通知父进程)。 - 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程所有的陷入和异常,并能够及时知道它状态的改变。
进程是具有独立性的(进程的代码和数据与另外一个进程的代码和数据是没有关系的)。可是,要通信,就意味着两个进程要进行数据交互,那么数据便有可能相关了。
- -
要让两个进程实现通信,前提条件是得先让两个进程看到同一份资源。
资源:内存空间,它由谁提供,以什么样的方式提供,这就表现了不同的通信方式。
一、管道
什么是管道?
- 把从一个进程连接到另一个进程的一个数据流称为一个“管道”
匿名管道
#include <unistd.h>
int pipe(int fd[2]);
功能:创建一个匿名管道。
fd:文件描述符数组,默认会以读写方式打开,把读写方式打开的文件描述符,分别保存到fd[0]和fd[1]中。
返回值:成功返回0,失败返回错误代码。
下面的代码演示了创建管道,同时创建子进程,并实现让子进程写,父进程读的过程。
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
int pipefd[2] = {0};
pipe(pipefd);
pid_t id = fork();
if(id == 0)
{
// child, write
close(pipefd[0]);// 关掉读
const char* msg = "i am child......\n";
while(1)
{
write(pipefd[1], msg, strlen(msg));
sleep(1);
}
}
else{
// father, read
close(pipefd[1]);// 关掉写
char buff[64];
ssize_t s = read(pipefd[0], buff, sizeof(buff));
if(s > 0)
{
buff[s] = 0;
}
printf("father get message : %s\n", buff);
}
return 0;
}
管道的四种情况(在代码层面体现的四个特征):
1、如果写端不关闭文件描述符,且不写入,此时读出条件不满足(管道为空),那么读端可能会进行长时间阻塞(状态由R变S,由运行队列放置等待队列)。
2、当我们实际在进行写入时,如果写入条件不满足(管道满了),写端会进行阻塞。
3、如果写端关闭文件描述符,读端在读取完数据后,会读取到文件结尾。
4、如果读端关闭,写端进程可能在后续被直接杀掉。因为操作系统不做任何浪费空间和低效的事情,只要发现,它就会进行修正。没有读端,写这个动作就浪费了系统资源和内存。会通过13号信号杀掉。
管道属性特征:
1、只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道。
2、管道提供流式服务
3、当进程退出时,管道释放,所以管道的生命周期随进程。
4、内核会对管道操作进行同步与互斥。
5、管道是半双工的,数据只能向一个方向流动;需要双向通信时,需要建立两个管道。
命名管道
作用:两个毫不相关的进程,实现进程通信。
使用FIFO文件通信,它经常被称为命名管道。
创建:
可以使用命令行mkfifo filename
也可以使用函数:
#include<sys/types.h>
#include<sys/stat.h>
int mkfifo(const char *filename,mode_t mode);
用命令行创建举例:
可以发现,命名管道是一种特殊类型的文件,文件类型为p。
可以把fifo简单理为一种符号或标志,代表的是两个进程通过它通信。
操作系统在内存中创造管道文件,在内存通信,这样数据不会也不需要刷新到磁盘。如果真的是一个进程往文件里写,另一个进程从文件中读,那么就是IO,效率太低。
应用实例:客户端发送,服务端接收
- server.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#define FIFO_FILE "./fifo"
int main()
{
if(-1 == mkfifo("FIFO_FILE",0644))
{
perror("mkfifo");
return 1;
}
//打开文件
int fd = open(FIFO_FILE,O_RDONLY);
if(fd >= 0)
{
char buff[64];
while(1)
{
ssize_t s = read(fd, buff, sizeof(buff) - 1);
if(s > 0)
{
buff[s] = 0;
printf("client %s\n", buff);
}
else if(s == 0)
{
printf("client quit, my too.\n");
break;
}
else{
perror("read");
break;
}
}
}
return 0;
}
- client.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#define FIFO_FILE "./fifo"
int main()
{
//打开文件
int fd = open(FIFO_FILE,O_WRONLY);
if(fd >= 0)
{
char buff[64];
while(1)
{
printf("please enter message ");
fflush(stdout);
ssize_t s = read(0, buff, sizeof(buff) - 1);
if(s > 0)
{
buff[s] = 0;
write(fd, buff, s);
}
}
}
return 0;
}
匿名与命名的区别:
匿名管道由pipe函数创建并打开。
命名管道由mkfifo函数创建,打开用open。
匿名管道只能让具有亲缘关系的进程之间进行通信,常用于父子;命名管道可以两个毫不相关的进程之间进行通信。
二、共享内存
有没有绕过文件系统的通信方式?
共享内存几乎是进程通信中速度最快的方式,因为它拷贝的次数少,不需要用户到内核,内核到用户。
管道是基于文件实现通信,而共享内存是操作系统给我们提供的通信相关的数据结构,来实现通信。
管道一般是两个进程通信,而共享内存可以多个。
我们来认识一些使用共享内存的函数。
- shmget函数
功能:用来创建共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
- shmat函数
功能:将共享内存段连接到进程地址空间
#include <sys/ipc.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid:共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM\_RND和SHM\_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
- shmdt函数
功能:将共享内存段与当前进程脱离
#include <sys/ipc.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
shmaddr:由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
- shmctl
功能:删除共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
在使用这些函数之前,我们要先使用ftok
函数来生成一个唯一值key。
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok( const char * fname, int id )
ftok这两个参数可以随便填写,但要保证让在同一块共享内存通信的进程所填写的参数相同才行。也就是说,ftok的实际上是通过算法来生成一个唯一值。
举例实现共享内存的创建、关联、取消关联、删除
- comm.h
#pragma once
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/ipc.h>
#define PATHNAME "/tmp"
#define PROJ_ID 0x6688
#define SIZE 4096
- server.c
#include"comm.h"
int main()
{
//创建了key值,算法生成,保证key唯一性
key_t k = ftok(PATHNAME, PROJ_ID);
int shmid = shmget(k, SIZE, IPC_CREAT| IPC_EXCL| 0664);
if(shmid < 0){
perror("shmget");
return 1;
}
printf("shmid:%d\n", shmid);
//关联
char* str = (char*)shmat(shmid, NULL, 0);
while(1)
{
sleep(1);
printf("%s\n",str);
}
//取消关联
shmdt(str);
//删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
- client.c
#include"comm.h"
int main()
{
//也得调ftok,传同样的参数
key_t k = ftok(PATHNAME, PROJ_ID);
int shmid = shmget(k, SIZE, 0);
if(shmid < 0){
perror("shmget");
return 1;
}
printf("shmid:%d\n", shmid);
//关联
char* str = (char*)shmat(shmid, NULL, 0);
char c = 'a';
for(; c<= 'z'; c++)
{
str[c - 'a'] = c;
}
//取消关联
shmdt(str);
//删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
查看共享内存:ipcs -m
ipc资源的生命周期随内核。
共享内存底层不提供任何的同步与互斥机制,而管道提供。所以管道里为空或为满时,对应的读端或写端会被阻塞。
- -
总结
进程间通信的本质就是看到同一份资源。
在共享内存中,这里的资源是由操作系统提供的。
而管道是由文件系统提供的,文件系统也属于操作系统。
所以进程间所有的共享资源都是由操作系统提供的,只不过是通过文件部分提供还是进程管理部分提供。
操作系统也要对所有通信的进程和资源进行管理。