目录
一、认识进程间通信
1.1 概念
1.2 通信目的
1.3 通信本质
1.4 通信分类
二、管道
2.1 管道概念
2.2 匿名管道
2.2.1 匿名管道原理
2.2.2 pipe、pipe2函数
2.2.3 匿名管道使用理解
2.3 命名管道
2.3.1 命名管道的原理
2.3.2 创建命名管道
2.3.3 命名管道的打开规则
2.3.4 利用命名管道实现serve&&client通信
2.4 管道特点
2.5 管道读写规则
2.6 管道大小
三、System V IPC
3.1 共享内存
3.1.1 共享内存原理
3.1.2 "描述"共享内存
3.1.3 查看共享内存信息
3.1.4 共享内存的创建
3.1.5 共享内存的释放
3.1.6 共享内存的关联
3.1.7 共享内存的去关联
3.1.8 利用共享内存实现serve&&client通信
3.1.9 共享内存与管道对比
3.2 消息队列
3.3 信号量
进程间通信简称IPC(Interprocess communication),进程间通信就是在不同进程之间传播或交换信息
进程间通信的本质: 让不同的进程看到同一份资源
由于进程具有独立性,所以各个进程若想进进行通信一定要借助第三方资源。若可以对第三方资源进行写入或读取数据,就可以实现进程间通信。
因此,进程间通信的本质就是,让不同的进程看到同一份资源(内存,文件内核缓冲区等)。 由于这份资源可以由操作系统中的不同模块提供,因此出现了不同的进程间通信方式。
管道
System V IPC
POSIX IPC(这部分会在后面的文章进行讲解)
管道是Unix中最古老的进程间通信形式,从一个进程连接到另一个进程的数据流被称为一个"管道"
who 和 wc命令运行起来是两个进程,who进程将数据(运行结果)写入"管道”中,wc进程再从"管道"中读取数据,至此便完成了数据的传输,进而可以完成数据的进一步加工处理。
注意:who命令用于查看当前服务器的登录用户(一行显示一个用户),wc -l用于统计行数
在使用命令的时候并不会生成命名管道文件,且who和wc进程都是通过bash进程的子进程程序替换得到的,因此命令行上的管道实际上是匿名管道。
匿名管道用于进程间通信,且仅限于本地父子进程之间的通信。其原理就是:让父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信。
父子进程看到的同一份文件资源由操作系统进行维护,所以当父子进程对该文件进行写入操作时,该文件缓冲区当中的数据并不会进行写时拷贝。
管道虽然用的是文件的方案,但操作系统不会把进程进行通信的数据刷新到磁盘当中,因为这样做有磁盘IO参与会降低效率。这种文件是一种内存级文件,并不会在磁盘中存在。
int pipe(int pipefd[2]);
pipe函数参数是一个输出型参数,数组pipefd由两个分别指向管道读端和写端的文件描述符组成
返回值:调用成功时返回0,调用失败时返回-1
int pipe2(int pipefd[2], int flags);
pipe2函数的第二个参数用于设置选项。
1、当没有数据可读时:
2、当管道满的时候:
若想实现父子进程间通信,需将pipe()和fork()搭配使用
操作案例:
#include
#include
#include
#include
#include
#include
int main()
{
int fd[2] = { 0 };
if (pipe(fd) < 0) {
perror("pipe");
return 1;
}
pid_t id = fork();
if (id == 0){
close(fd[0]);
const char* msg = "hello father, I am child...";
int count = 10;
while (count--){
write(fd[1], msg, strlen(msg));
sleep(1);
}
close(fd[1]);
exit(0);
}
else if(id > 0) {
close(fd[1]);
char buff[64] = {'\0'};
while (1){
ssize_t s = read(fd[0], buff, sizeof(buff));
if (s > 0){
buff[s] = '\0';
printf("child send to father:%s\n", buff);
}
else if (s == 0){
printf("read file end\n");
break;
}
else{
printf("read error\n");
break;
}
}
close(fd[0]);
waitpid(id, NULL, 0);
}
return 0;
}
匿名管道只能用于具有亲缘关系的进程之间的通信,若要实现两个毫不相关进程之间的通信,可以使用命名管道。命名管道就是一种特殊类型的文件,两个进程通过文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而可以进行通信。
注意:
使用命令创建命名管道
可以使用 mkfifo 命令创建一个命名管道
[bjy@VM-8-2-centos fifo_test]$ mkfifo fifo
在程序中使用mkfifo函数创建命名管道
int mkfifo(const char *pathname, mode_t mode);
pathname:
mode:
表示创建命名管道文件的默认权限,受umask(0002)影响。实际创建出来文件的权限为:mode&(~umask)
返回值:
创建案例:
#include
#include
#include
#define FILE_NAME "myfifo"
int main()
{
umask(0); //将文件默认掩码设置为0
if (mkfifo(FILE_NAME, 0666) < 0) {
perror("mkfifo");
return 1;
}
//... ...
return 0;
}
1、若当前打开操作是为读而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO。
O_NONBLOCK enable:立刻返回成功
2、若当前打开操作是为写而打开FIFO时。
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO。
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
实现通信前先使得服务端运行起来,创建命名管道并打开,然后服务端会发生阻塞直到客户端打开命名管道。
//my_server.cc
#include "com.h"
int main()
{
umask(0);
if (mkfifo(FILE_NAME, 0666) < 0) {
perror("mkfifo");
return 1;
}
int fd = open(FILE_NAME, O_RDONLY); //以读的方式打开命名管道文件
if (fd < 0) {
perror("open");
return 1;
}
char msg[128] = {0};
while (1){
memset(msg,'\0',128);
ssize_t s = read(fd, msg, sizeof(msg)-1);
if (s > 0) {
printf("client: %s", msg);
}
else if (s == 0) {
printf("client quit!\n");
break;
}
else {
printf("read error!\n");
break;
}
}
close(fd);
unlink(FILE_NAME);
return 0;
}
客户端运行起来后先打开命名管道,一直向管道中写入数据即可
//my_client.cc
#include "com.h"
int main()
{
int fd = open(FILE_NAME, O_WRONLY); //以写的方式打开命名管道文件
if (fd < 0) {
perror("open");
return 1;
}
char msg[128];
while (1){
memset(msg,'\0',sizeof(msg));
printf("Please Enter :>");
fflush(stdout);//printf字符串中没有'\n'
ssize_t s = read(0, msg, sizeof(msg)-1);//从标准输出流文件中读取
if (s > 0) {
write(fd, msg, strlen(msg));
}
}
close(fd);
return 0;
}
客户端和服务端包含同一个头文件,该头文件当中提供命名管道文件的文件名,客户端和服务端即可通过这个文件名,打开同一个命名管道文件进行通信
#pragma once
#include
#include
#include
#include
#include
#include
#define FILE_NAME "myfifo" //让客户端和服务端使用同一个命名管道
1、管道内部自带同步与互斥机制
同一时刻只允许一个进程使用的资源被称为临界资源。管道在同一时刻只允许一个进程对其进行写入或是读取操作,因此管道也就是一种临界资源。
临界资源是需要被保护的,若不对管道这种临界资源进行任何保护机制,那么就可能出现同一时刻有多个进程对同一管道进行操作的情况,进而导致同时读写、交叉读写以及读取到的数据不一致等问题。
为了避免这些问题,内核会对管道操作进行同步与互斥:
实际上,同步是一种更为复杂的互斥。对于管道的场景来说,互斥就是两个进程不可以同时对管道进行操作,它们会相互排斥,必须等一个进程操作完毕,另一个才能操作,而同步也是指两个进程不能同时对管道进行操作,但这两个进程必须要按照某种次序来对管道进行操作。即互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,而同步的任务之间则有明确的顺序关系。
2、管道的生命周期随进程
管道本质上是通过文件进行通信,那么当所有打开该文件的进程都退出后,该文件也就会被释放掉,所以说管道的生命周期随进程。
3、管道提供流式服务
对于进程A写入管道当中的数据,进程B每次从管道读取的数据的多少是任意的,这种被称为流式服务,与之相对应的是数据报服务:
4、管道是半双工通信的
管道是半双工的,数据只能向一个方向流动。需要双方通信时,通常会建立起两个管道
管道自带同步与互斥机制,读端进程和写端进程存在一个步调协调的过程,不会发生管道中没有数据但读端还在读取,或管道已经满了但写端还在写入的情况。读端进程读取数据的条件是管道里面有数据,写端进程写入数据的条件是管道当中还有空间,若是条件不满足,则相应的进程就会被挂起,直到条件满足后才会被再次唤醒。
读端进程已经将管道当中的所有数据都读取出来了,而且此后也不会有写端再进行写入了,那么此时读端进程也就可以执行该进程的其他逻辑了,而不会被挂起。
管道中的数据已经没有进程读取了,那么写端进程的写入将没有意义,因此操作系统直接将写端进程杀掉。进程代码都还没跑完就被终止,属于异常退出,那么进程必然是收到了某种信号。
#include
#include
#include
#include
#include
#include
int main()
{
int fd[2] = {0};
if (pipe(fd) < 0) {
perror("pipe");
return 1;
}
pid_t id = fork();
if (id == 0) { // child
close(fd[0]);
const char *msg = "hello father, I am child...";
for (int i = 0; i < 10; ++i) {
write(fd[1], msg, strlen(msg));
sleep(1);
}
close(fd[1]);
exit(0);
}
else if (id > 0) { // father
close(fd[1]);
close(fd[0]); //父进程直接关闭读端(导致子进程被操作系统杀死)
int status = 0;
waitpid(id, &status, 0);
printf("child get signal:%d\n", WTERMSIG(status));
}
return 0;
}
通过下面的代码来测试本机器上的管道大小
#include
#include
#include
#include
#include
int main()
{
int fd[2] = { 0 };
if (pipe2(fd , O_NONBLOCK) < 0) {
perror("pipe");
return 1;
}
pid_t id = fork();
if (id == 0) { //child
close(fd[0]); //子进程关闭读端
char c = 'a';
int count = 0;
while (true) {
int num = write(fd[1], &c, 1);
if(num == -1) break;
count++;
}
printf("%d\n", count); //打印当前写入的字节数
close(fd[1]);
exit(0);
}
else if(id > 0) {//father
close(fd[1]);
waitpid(id, NULL, 0);
close(fd[0]);
}
else {
perror("fork");
exit(0);
}
return 0;
}
使用ulimit -a命令也可以查看管道大小,但与实际情况有所不同
共享内存让不同进程看到同一份资源的方式是:在物理内存中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表之间进行联系,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起映射关系,至此进程便看到同一份物理内存
在系统当中可能会有大量的进程在进行通信,因此系统当中就可能存在大量的共享内存,那么操作系统必然要对其进行管理,所以共享内存除了在内存当中真正开辟空间之外,为了维护管理共享内存,系统一定要"描述"共享内存
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
当申请一块共享内存后,为了要让实现通信的进程能够看到同一个共享内存,因此每个共享内存被申请时都有一个key值,这个key值用于标识系统中共享内存的唯一性。
可以看到上面"描述"共享内存的结构体的第一个成员是shm_perm,shm_perm是一个ipc_perm类型的结构体变量,每个共享内存的key值存储在shm_perm这个结构体变量当中。
struct ipc_perm{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
注意:shmid_ds
和ipc_perm
结构体分别在/usr/include/linux/shm.h和/usr/include/linux/ipc.h中定义
使用ipcs命令可以查看共享内存的信息。但是该命令会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看某一个的相关信息,可以选择携带选项
ipcs命令列出的每列信息的含义如下:
注意: key是内核层面上保证共享内存唯一性的方式;shmid是在用户层上保证共享内存的唯一性
使用shmget()函数进行共享内存的创建
int shmget(key_t key, size_t size, int shmflg);
返回值:
参数:
注意: 具有标定某种资源能力的东西被称作句柄(譬如FILE即文件句柄),而shmget函数的返回值实际上就是共享内存的句柄,其可以在用户层标识共享内存。当共享内存被创建后,在后续使用共享内存的相关接口时,都需要通过这个句柄对指定共享内存进行操作
使用ftok函数获取参数key
key_t ftok(const char *pathname, int proj_id);
ftok函数的作用就是,将一个已存在的路径名pathname和一个整数标识符proj_id转换成一个key值,称为IPC键值,在使用shmget函数获取共享内存时,这个key值会被填充进维护共享内存的数据结构当中
注意:
参数shmflg
选项(组合) | 作用 |
IPC_CREAT | 若不存在键值与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;若存在这样的共享内存,则直接返回该共享内存的句柄 |
IPC_CREAT | IPC_EXCL | 若内核中不存在键值与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;若存在这样的共享内存,则出错返回 |
IPC_EXCL | 单独使用无任何作用 |
当进程运行完毕后申请的共享内存依旧存在,并没有被操作系统释放,因为共享内存的生命周期是随内核的。若进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启(system V IPC都是如此)。
用命令释放共享内存资源
使用ipcrm -m shmid命令
使用程序接口释放共享内存资源
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数cmd:
返回值:
建立共享内存与进程地址空间的映射关系时需要使用shmat函数
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数shmflg:
返回值:
取消共享内存与进程地址空间之间的关联需使用shmdt()函数
int shmdt(const void *shmaddr);
shmaddr参数:
返回值:
服务端创建一个新的共享内存并建立连接
//my_server.cxx
#include "com.h"
int main()
{
key_t key = ftok(PATHNAME, PROJ_ID); //获取key值
if (key < 0) {
perror("ftok");
return 1;
}
int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建新的共享内存
if (shm < 0) {
perror("shmget");
return 2;
}
char* mem = (char*)shmat(shm, NULL, 0); //关联共享内存
//服务端不断读取共享内存当中的数据并输出
while (1) {
printf("client# %s\n", mem);
sleep(1);
}
shmdt(mem); //共享内存去关联
shmctl(shm, IPC_RMID, NULL); //释放共享内存
return 0;
}
客户端连接的是已创建好的共享内存,并往其中不断写入数据
//my_client.cxx
#include "com.h"
int main()
{
key_t key = ftok(PATHNAME, PROJ_ID); //获取与server进程相同的key值
if (key < 0) {
perror("ftok");
return 1;
}
int shm = shmget(key, SIZE, IPC_CREAT); //获取server进程创建的共享内存的用户层id
if (shm < 0) {
perror("shmget");
return 2;
}
char* mem = (char*)shmat(shm, NULL, 0); //关联共享内存
//客户端向共享内存写入数据
int i = 0;
while (1) {
mem[i++] = 'A' + i;
mem[i] = '\0';
sleep(1);
}
shmdt(mem); //共享内存去关联
return 0;
}
服务端和客户端代码中都包含该头文件,确保生成的key、路径、连接的共享内存相同
#include
#include
#include
#include
#include
#include
#define PATHNAME "/home/bjy/BaoLinux/code/signal_com/shm/shm_2" //路径名
#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小
共享内存创建好后就不需要调用系统接口进行通信了,而管道创建好后仍需要read、write等系统接口进行通信。共享内存是所有进程间通信方式中最快的一种通信方式
使用管道通信的方式,将文件中的数据从一个进程传输到另一个进程需要进行四次拷贝操作:
使用共享内存进行通信,将文件中的数据从一个进程传输到另一个进程只需要进行两次拷贝操作:
共享内存同样也存在问题,其并没有提供任何的保护机制,类似于同步与互斥
该技术已面临淘汰,等博主有时间会进行更新
后续博主有时间会进行更新