Linux进程间通信
- 零、前言
- 一、进程间通信介绍
- 二、管道
- 1、匿名管道
- 2、命名管道
- 三、system V
- 1、共享内存概念及原理
- 2、共享内存使用接口介绍
- 1、共享内存资源的查看
- 2、共享内存的创建和释放
- 3、共享内存的链接与去连接
- 4、接口使用示例
- 3、共享内存与管道对比
- 4、消息队列/信号量
零、前言
本章主要讲解学习Linux中本系统下的进程间通信
一、进程间通信介绍
- 概念:
进程间通信简称IPC(Inter process communication),进程间通信就是在不同进程之间传播或交换信息
- 进程间通信目的:
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
- 进程间通信本质:让不同的进程看到同一份资源
由于进程之间具有独立性,代码数据独立拥有,若想实现通信,可以通过向第三方资源(实际上就是操作系统提供的一段内存区域)写入或是读取数据,进而实现进程之间的通信
- 进程间通信发展:
管道->System V进程间通信->POSIX进程间通信
- 进程间通信分类:
- 管道
匿名管道pipe;命名管道
- System V IPC
System V 消息队列;System V 共享内存;System V 信号量
- POSIX IPC
消息队列;共享内存;信号量;互斥量;条件变量;读写锁
二、管道
- 概念:
管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
- 示图:统计当前使用云服务器上的登录用户个数
注:who命令用于查看当前云服务器的登录用户(一行显示一个用户);wc -l用于统计当前的行数
1、匿名管道
- 概念:
匿名管道用于本地具有亲戚关系的进程之间通信,常用与父子进程间通信
- pipe函数原型:
#include <unistd.h>
int pipe(int fd[2]);
- 功能:
创建一无名管道
- 参数:
- fd:文件描述符数组,是一个输出型参数,拿到打开的管道文件的问文件描述符,其中fd[0]表示读端文件,fd[1]表示写端文件
- 返回值:成功返回0,失败返回错误代码
- 示图:
- 示例:父子进程匿名管道通信
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
#include<stdlib.h>
int main()
{
int pipe_id[2]={0};
//创建管道文件资源
int ret=pipe(pipe_id);
if(ret<0)
{
perror("pipe");
exit(1);
}
printf("pipe_id[0]:%d pipe_id[1]:%dn",pipe_id[0],pipe_id[1]);
//创建子进程,共享管道资源
pid_t id=fork();
if(id==0)
{
//child->write
//关闭子进程的读端
close(pipe_id[0]);
const char* msg="Hello father, I am child!n";
int cnt=0;
while(1)
{
cnt ;
printf("child write:%dn",cnt);
write(pipe_id[1],msg,strlen(msg));//结束符不用写入,结束符是C语言的规则不是系统的规则
sleep(1);
if(cnt==10)
break;
}
close(pipe_id[1]);
exit(0);
}
else if(id>0)
{
//father->read
//关闭父进程的写端
close(pipe_id[1]);
//进行读取管道信息
while(1)
{
char buffer[128]={0};
ssize_t s=read(pipe_id[0],buffer,sizeof(buffer)-1);//给结束符留一个位置
if(s>0)
{
buffer[s]=0;//设置结束符
printf("msg from child:%s",buffer);
}
else if(s==0)
{
printf("子进程写端关闭...n");
break;
}
else
break;
}
close(pipe_id[0]);
}
else
{
perror("fork");
exit(2);
}
//父进程等待
int status=0;
if(waitpid(id,&status,0)>0&&WIFEXITED(status))//等待成功并退出正常
{
printf("wait success! exit code:%dn",WEXITSTATUS(status));
}
else
{
printf("exit sign:%dn",status&0x7F);
}
return 0;
}
- 效果:
- 共享管道原理:
- 对于同个文件可以以读方式和以写方式打开,文件在文件系统虽然只有一份,但是在进程的PCB中的文件结构体中的文件地址数组中可以保存两份,一份指向文件的读端口,一份指向文件的写端口
- 管道通过系统接口创建管道文件资源,并构建文件与PCB的映射关系,当fork创建子进程时父子进程就见到同一份文件资源,依靠管道文件的缓冲区选择性进行单向的实时读写
注:如果是刷新到磁盘上再进行读写非常影响效率
- 单向读写:
父进程进行读,子进程进行写;父进程进行写,子进程进行读
- 示图:
- 注意:
- 只有在先fork之前读写打开文件,父子进程才能共享相同的文件指针数组,进一步灵活控制读写
- 管道只能够进行单向通信,关闭对应的读写端也是为了避免误操作
- 从管道写端写入的数据会被内核缓冲,直到从管道的读端被读取
- 以文件描述符视角理解:
- 以内核角度理解:
- 注意:
- 管道就是特殊的文件,管道的使用和文件一致
- 但是依靠管道通信的本质上依靠管道的缓冲区进行读写,其缓冲并不会真正的刷新到磁盘上
- 管道读写规则:
- 写端不写,读端无数据可读
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,进行等待写端写入数据 O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN
- 写端不写,并将写端文件关闭
如果所有管道写端对应的文件描述符被关闭,则read返回0
- 读端不读,写端一直写
O_NONBLOCK disable: write调用阻塞,直到有进程读走管道缓冲区的数据 O_NONBLOCK enable: write调用返回-1,errno值为EAGAIN
- 读端不读,并将读端文件关闭
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程被终止退出
- 示图:
- 数据写入的原子性
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性
注:原子性是指 一个操作是不可中断的,要么全部执行成功要么全部执行失败,即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰
- 管道特点:
- 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常父子进程之间就可应用该管道
- 管道提供流式服务,面向字节流,读写以字节为单位进行
- 进程退出,管道释放,所以管道的生命周期随进程内核会对管道操作进行同步与互斥,即保证数据的原子性
- 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
- 示图:
2、命名管道
- 概念:
- 对于匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信
- 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道
- 命名管道创建命令:
mkfifo filename
- 示例:
- 命名管道创建函数原型:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *filename, mode_t mode);
注:第一个参数即为管道的名称,第二个参数即为创建管道文件的权限,创建成功返回0,否则返回-1
- 示例:
int main()
{
mkfifo("fifo", 0644);
return 0;
}
- 匿名管道与命名管道的区别
- 匿名管道由pipe函数创建并打开,依靠父子进程的共享特性看到同一份文件资源
- 命名管道由mkfifo函数创建并主动调用函数打开,依靠文件路径的唯一性让不同进行找到并打开同一份文件资源
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义
- 命名管道的打开规则
- 如果当前打开操作是为读而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO O_NONBLOCK enable:立刻返回成功
- 如果当前打开操作是为写而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
- 示例:用命名管道实现server&client通信
server.c:
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#define FIFO "fifo"
int main()
{
//创建命名管道
if(mkfifo(FIFO,0644)<0)
{
perror("mkfifo");
exit(1);
}
//打开管道文件
int fd=open(FIFO,O_RDONLY);
if(fd<0)
{
perror("open");
exit(2);
}
//服务端进行客户端信息
while(1)
{
char buffer[128]={0};
//输出标识词
printf("client#");
fflush(stdout);
//读取管道数据
ssize_t s=read(fd,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]=0;
printf("%s",buffer);
}
else if(s==0)
{
printf("write close,child quitn");
break;
}
else
break;
}
return 0;
}
client.c:
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FIFO "fifo"
int main()
{
//打开管道文件
int fd=open(FIFO,O_WRONLY);
if(fd<0)
{
perror("open");
exit(2);
}
//向服务端发送消息
while(1)
{
char buffer[128]={0};
//输出标识词
printf("please enter#");
fflush(stdout);
//读入数据
ssize_t s=read(0,buffer,sizeof(buffer)-1);
if(s>0)//写入到管道
{
buffer[s]=0;
write(fd,buffer,strlen(buffer));
}
else
break;
}
return 0;
}
- 效果:
三、system V
1、共享内存概念及原理
- 概念:
- 管道通信本质是基于文件的,也就是说操作系统并没有为此做过多的设计工作,而system V IPC是操作系统特地设计的一种通信方式;但是不管怎么样,它们的本质都是一样的,都是在想尽办法让不同的进程看到同一份由操作系统提供的资源
- 共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
- system V IPC提供的通信方式有以下三种:
- system V共享内存
- system V消息队列
- system V信号量
注:system V共享内存和system V消息队列是以传送数据为目的的,而system V信号量是为了保证进程间的同步与互斥而设计的,虽然system V信号量和通信好像没有直接关系,但属于通信范畴
- 共享内存的基本原理:
- 用户申请共享内存:OS在物理内存当中申请一块内存空间
- 进程主动挂接共享内存:OS将这块内存空间分别与各个进程的进程地址空间建立映射关系(共享内存映射进进程地址空间的共享区)
- 各进程看到同一空间资源:OS将映射后的的共享内存的虚拟地址返回给进程
- 示图:
注:这里所说的开辟物理空间、建立映射等操作都是调用系统接口完成的,也就是说这些动作都由操作系统来完成
- 共享内存数据结构:
- 各个进程都可以进行申请共享内存,那么共享内存的需求就可能非常多,而OS也需要进行对共享内容的管理,而管理的本质就是:先描述,再组织
- 所以共享内存除了在内存当中真正开辟空间之外,系统一定还要为共享内存维护相关的内核数据结构
- shmid_ds结构定义:
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,每个共享内存的key值存储在shm_perm这个结构体变量当中
- ipc_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;
};
- 共享内存使用过程:
- 调用系统接口进行在物理内存中申请共享内存空间
- 调用接口将申请到的共享内存挂接到地址空间,建立映射关系
- 使用之后调用接口将共享内存与地址空间去关联,取消映射关系
- 调用接口释放共享内存空间,将物理内存归还给系统
2、共享内存使用接口介绍
1、共享内存资源的查看
- 如何查看共享内存资源:
使用ipcs命令查看有关进程间通信设施的信息
- 选项:
-q:列出消息队列相关信息
-m:列出共享内存相关信息
-s:列出信号量相关信息
注:单独使用ipcs命令时,会默认列出消息队列、共享内存以及信号量相关的信息
- 示图:
- ipcs输出信息含义:
标题 | 含义 |
---|---|
key | 系统区别各个共享内存的唯一标识 |
shmid | 共享内存的用户层id(句柄) |
owner | 共享内存的拥有者 |
perms | 共享内存的权限 |
bytes | 共享内存的大小 |
nattch | 关联共享内存的进程数 |
status | 共享内存的状态 |
注:key标识共享内存唯一性的方式,而shmid是用于用户指明操作对象,key和shmid之间的关系类似于inode和fd之间的的关系
2、共享内存的创建和释放
- ftok函数的函数原型:
key_t ftok(const char *pathname, int proj_id);
- 解释:
功能:将一个已存在的路径名pathname和一个整数标识符proj_id转换成一个key值,称为IPC键值,在使用shmget函数获取共享内存时,这个key值会被填充进维护共享内存的数据结构当中
- 注意:
- pathname所指定的文件必须存在且可存取;使用ftok函数生成key值存在可能会产生冲突
- 进行通信的各个进程在使用ftok函数获取key值时,需要采用同样的路径名和和整数标识符,进而生成同一种key值找到同一份共享内存
- shmget函数的函数原型:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
- 解释:
- 功能:向系统申请共享内存
- 参数:第一个参数key,表示待创建共享内存在系统当中的唯一标识;第二个参数size,表示待创建共享内存的大小;第三个参数shmflg,表示创建共享内存的方式
- 返回值:shmget调用成功,返回一个有效的共享内存标识符,用于进行操作;shmget调用失败,返回-1
注:这里shmget函数的返回值实际上就是共享内存的句柄,这个句柄可以在用户层标识共享内存,当共享内存被创建后,我们在后续使用共享内存的相关接口时,都是需要通过这个句柄对指定共享内存进行各种操作
- 第三个参数shmflg常用组合方式:
组合方式 | 作用 |
---|---|
IPC_CREAT | 如果内核中不存在与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;如果存在这样的共享内存,则直接返回该共享内存的句柄,即该共享内存可能是已有的也可能的新建的 |
IPC_CREAT | IPC_EXCL | 如果内核中不存在与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;如果存在这样的共享内存,则出错返回,即如果成功该共享内存一定是新建的共享内存 |
- 示例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "./server.c"
#define PROJ_ID 0x6666
#define SIZE 4096
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); //创建共享内存
if (shm < 0){
perror("shmget");
return 2;
}
printf("key: %xn", key);
printf("shm: %dn", shm);
return 0;
}
- 效果:
- 注意:
- 进程运行完毕后,申请的共享内存依旧存在,即共享内存的生命周期是随内核的,也就是说共享内存并不会主动随进程的退出而释放
- 如果进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启(system V IPC都是如此)
- 在命令行中我们可以使用命令ipcrm -m shmid释放共享内存 示图:
- shmctl函数的函数原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 解释:
- 功能:控制对应的共享内存资源
- 参数:第一个参数shmid表示要控制的共享内存;第二个参数cmd,表示具体的控制动作;第三个参数buf,用于获取或设置所控制共享内存的数据结构
- 返回值:shmctl调用成功,返回0;shmctl调用失败,返回-1
- shmctl函数的第二个参数常用的传入选项:
选项 | 作用 |
---|---|
IPC_STAT | 获取共享内存的当前关联值,此时参数buf作为输出型参数 |
IPC_SET | 在进程有足够权限的前提下,将共享内存的当前关联值设置为buf所指的数据结构中的值 |
IPC_RMID | 删除释放共享内存段 |
注:一般使用接口进行释放对应的共享内存资源
3、共享内存的链接与去连接
- shmat函数的函数原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
- 解释:
- 功能:将共享内存与进程建立映射关系
- 参数:第一个参数表示要关联的共享内存的对应的shmid;第二个参数shmaddr指定共享内存映射到进程地址空间的某一地址,通常设置为NULL,表示让内核自己决定一个合适的地址位置;第三个参数shmflg,表示关联共享内存时设置的某些属性,一般设置为0
- 返回值:shmat调用成功,返回共享内存映射到进程地址空间中的起始地址;shmat调用失败,返回(void*)-1
- shmat函数第三个参数的常用传入选项:
选项 | 作用 |
---|---|
SHM_RDONLY | 关联共享内存后只进行读取操作 |
SHM_RND | 若shmaddr不为NULL,则关联地址自动向下调整为SHMLBA的整数倍。公式:shmaddr-(shmaddr%SHMLBA) |
0 | 默认为读写权限 |
- shmdt函数的函数原型:
int shmdt(const void *shmaddr);
- 解释:
- 功能:取消共享内存与进程的映射关系
- 参数:待去关联共享内存的起始地址,即调用shmat函数时得到的起始地址
- 返回值:shmdt调用成功,返回0;shmdt调用失败,返回-1
4、接口使用示例
- 示例:
servershm.c:
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include"Common.h"
#include<stdlib.h>
#include<unistd.h>
int main()
{
//申请唯一key值
key_t key=ftok(PATHNAME,PROJ_ID);
if(key<0)
{
perror("ftok");
exit(1);
}
printf("key alreadly creat...n");
sleep(3);
//创建共享内存资源,保证一定是新的共享内存资源,并设置权限为0644
int shm_id=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0644);
if(shm_id<0)
{
perror("shmget");
exit(2);
}
printf("shm alreadly get...n");
sleep(3);
//以读写方式链接共享内存资源
char* start=(char*)shmat(shm_id,NULL,0);
printf("server alreadly at shm...n");
//进行读写操作
while(1)
{
printf("%sn",start);
sleep(1);
}
shmdt(start);
printf("server alreadly dt shm...n");
sleep(2);
shmctl(shm_id,IPC_RMID,NULL);
printf("delete shm...n");
return 0;
}
clientshm.c:
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include"Common.h"
#include<stdlib.h>
#include<unistd.h>
int main()
{
//申请唯一key值
key_t key=ftok(PATHNAME,PROJ_ID);
if(key<0)
{
perror("ftok");
exit(1);
}
//获取共享内存id
int shm_id=shmget(key,SIZE,IPC_CREAT);
if(shm_id<0)
{
perror("shmget");
exit(2);
}
sleep(3);
//链接共享内存资源
char* start=(char*)shmat(shm_id,NULL,0);
//进行读写操作
char ch='a';
while(ch<='z')
{
start[ch-'a']=ch;
ch ;
sleep(2);
}
sleep(5);
shmdt(start);
return 0;
}
Common.h:
#define PATHNAME "/home/zgj/lesson/lesson16/shmdir"
#define PROJ_ID 0x666
#define SIZE 4097
- 效果:
注:共享内存没有进行同步与互斥,读端并不会管写端写的原子性
3、共享内存与管道对比
- 共享内存通信方式需要进行的拷贝次数最少,由此速度最快 对于管道通信数据传输过程:将数据先写到管道缓冲区,再冲管道缓冲区中读取数据 共享内存通信数据传输过程:直接对共享内存进行读写
- 共享内存也是有缺点的,管道是自带同步与互斥机制的,但是共享内存并没有提供任何的保护机制,包括同步与互斥
4、消息队列/信号量
- 消息队列概念:
- 消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法
- 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
- 特性方面:
IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核
- 消息队列的基本原理:
- 消息队列实际上就是在系统当中创建了一个队列,队列当中的每个成员都是一个数据块,这些数据块都由类型和信息两部分构成
- 两个互相通信的进程通过某种方式看到同一个消息队列,这两个进程向对方发数据时,都在消息队列的队尾添加数据块,这两个进程获取数据块时,都在消息队列的队头取数据块
- 信号量概念:
- 信号量主要用于同步和互斥的,进程之间存在对资源的竞争性,但是资源有限,需要保证对象获取资源的个数在承受范围之内
- 就相当于每个进程在获取资源之前,需要先通过信号量获取获得资源的一个凭证,就像一个预定机制一样
- 也就是说,每个进行也需要竞争获取信号量资源,即信号量也是一个临界资源,此时就需要信号量本身就原子的,其对应的操作具有原子性
- 从本质上来说,信号量是用来描述临界资源数目的一个计数器
- 注意:
- 由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥
- 系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源,在进程中涉及到互斥资源的程序段叫临界区
- 特性方面:
IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核