目录
一、进程间通信(简写为IPC)
即interprocess communication
1.进程间通信的定义和目的
1.1定义
进程间通信指的是不同进程间进行信息交流的行为。
1.2目的
一般来说,进程间通信的目的有:
数据传输:一个进程需要将数据传输给另一个进程。
资源共享:多个进程之间共享相同的资源。
通知事件:一个进程需要向另一个或多个进程发送消息,通知它们发生了某种事件。例如,子进程终止时需要通知父进程。
进程控制:有些进程希望完全控制其他进程的执行,此时控制进程能够拦截另一个进程的所有状态,并能及时知道进程的状态变化。
进程间时如何通信的呢?
由于进程的相互独立性可知,进程都有自己独立的PCB和进程地址空间,不能访问其他进程的地址空间,也无法做到通信的功能。但是,内核空间是所有进程所共享的,故,进程间的通信必须依靠内核。
(进程地址空间(虚拟的) = 内核空间+用户空间
进程地址空间通过页表和物理内存进行一一映射)
2.进程间通信的本质
因为进程是相互独立的,进程间通信必须让不同进程看到同一块资源,通过资源来达到进程通信的目的。其中,资源指的是文件(linux下一切皆文件)。
3.进程通信的方式
3.1 管道
3.1.1匿名管道
3.1.2命名管道
3.2 System V IPC
3.2.1 System V 消息队列
3.2.2 System V 共享内存
3.2.3 System V 信号量
二、匿名管道
1.管道的定义
管道对我们来说并不陌生,在之前的shell命令中就见过了。不过,我们并不真正认识管道。
管道是linux中进行进程间通信的重要通信方式,管道的本质是内存级文件(区别于磁盘文件),我们之前接触到的管道都是匿名管道。
2.具体如何建立进程间通信
即如何实现匿名管道。
2.1创建匿名管道
首先,我们需要认识一个函数——pipe()
函数原型:
#include<unistd.h>
int pipe(int pipefd[2]);
//1.pipefd是输出型参数,pipefd[0]是管道的读端fd;pipefd[1]是管道的写端fd。记忆起来容易混淆,可以形象记忆为:0像眼睛:读;1像笔:写。
//2.函数的返回值:创建管道成功则返回0;创建失败返回负数。
//3.如果创建失败,可以通过error和strerror来查看创建失败的错误原因。
2.2创建子进程
之前已经学习过的函数——fork(),fork()底层调用clone()系统调用来实现资源的拷贝。
因为子进程的进程PCB和代码数据都是父进程的拷贝,从而,父进程打开文件的fd也会被子进程继承,在这里,达到了子进程和父进程都可以访问管道文件的效果。
得到一个结论:管道通信,通常用来进行具有"血缘关系"的进程之间的进程间通信,常用父进程中的pipe创建管道。注意,用pipe()函数创建管道时,并不知道管道文件的文件名,直接通过文件fd来进行文件的访问。pipe()创建的管道是匿名管道。
2.3关闭进程无关fd
创建管道时,管道的读写端都被打开。管道进行进程间通信是靠一读一写来完成的,所以必须关闭无关的fd。例如:如果想要父进程写,子进程读,则父进程需要close(pipefd[0],即关闭读;子进程关闭pipfd[1],即关闭写。
2.4开始通信
通过一读一写来完成进程间通信。
一共有四种场景:
1.写得慢,读得快
如果读取完毕所有管道数据,如果写端还不写入内容,则读端不会继续读,需要等待写段写入。
2.写得快,读得慢
如果写端将管道写满了,则不能再继续写,必须读完才能继续写(读一次读不完,必须等读端全部读完后写端才能写)。为什么?如果写满之后读端不读,写端继续写,则原先写入管道的数据会被覆盖,原先的写操作就会失去意义。
3.关闭写端,读异常(不能再读)
如果关闭写端,读端读取完毕后,read()函数会返回0,表示读到文件结尾。
4.关闭读端,写异常(不能再写)
写端一直写,读端关闭,会发生什么?写段写入内容的目的是为了实现进程间通信,即写是为了被看到,如过读端关闭,则写端写入数据就失去意义,OS不会允许低效率或无意义的事情发生,写端进程退出。
3.管道的特点
1.单向通信(读端和写端单向信息传递(写端-->读端)) ——是半双工的一种特殊情况(读写不同时进行)
2.管道的本质是文件,因为fd的声明周期随进程,管道的生命周期也随进程。
3.管道通信通常用于具有“血缘关系”的进程,进行进程间通信,常用于父子进程——pipe()函数创建管道,不知道管道文件的名字——匿名管道。
4.在管道通信中,写入数据的次数和读取数据的次数不是严格匹配的,读写次数的多少没有强相关——表现——字节流。
5.具有一定的协同能力,让reader和writer能够按照一定的步骤进行通信——自带同步机制。
4.管道的读写规则
1.当没有数据可读时 O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。 O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
2.当管道满的时候 O_NONBLOCK disable: write调用阻塞,直到有进程读走数据 O_NONBLOCK enable:调用返回-1,errno值为EAGAIN 如果所有管道写端对应的文件描述符被关闭,则read返回0
3.如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程 退出
4.当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
5.管道的应用举例——进程控制(父进程控制子进程)
父进程可以建立一批管道,跟创建的多个子进程进行进程间通信。
(写端:父进程,读端:子进程)
父进程可以通过写入特定的内容,达到唤醒子进程的目的;可以通过关闭管道的写端,从而达到关闭子进程的目的;等等。(父进程可以让子进程完成特定的任务)
三、命名管道
1.匿名管道和命名管道的区别:
1.匿名管道由pipe函数创建并打开。 命名管道由mkfifo函数创建,打开用open
2. FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。 (在该部分不会堆命名管道的语义进行说明,同上匿名管道)
3.管道的本质是内核中的缓冲区,命名管道文件是缓冲区的标识。
2.命名管道的打开规则
利用函数mkfifo()创建命名管道:
1.pathname是命名管道文件的路径;mode是文件的权限
2.命名管道文件创建成功,函数返回0;创建失败,函数返回-1
总结:
1.echo "hello bit" > fifo 输入命名文件fifo以"hello bit"的内容,标准输入重定向到管道文件的写端,如果读端不读取,可继续向管道文件写入;如果用cat fifo查看管道文件的内容,此时写端结束。
cat命令进行管道文件的读取,标准输出
2.管道文件的大小始终为0,因为命名管道实际也是内存级文件,操作系统不会对它刷盘,即文件内容始终在内存,不会被实时更新到磁盘(没有意义),时刻记住,操作系统不会做低效率或无意义的事情。
3.命名管道的原理
利用文件名,不同进程可以访问同一份文件资源。
1.创建管道文件
1.1在命令行上创建命名管道文件 mkfifo filename
1.2用mkfifo()函数创建命名管道文件。
创建成功后,可以查看管道文件的名字和属性。
例如:
其中,p表示该文件为管道文件。
2.让读写进程分别打开文件
直接在不同的进程中通过open()以读或写的方式打开文件。
3.进行通信
向管道文件中进行读写,实现进程间的通信。
利用命名管道实现的通信也是单向的。
4.删除文件
可以用函数unlink()删除命名管道文件,也可以用shell命令进行文件的删除(方法与删除普通文件无异)。
4.总结:
1.匿名管道只能用于具有亲缘关系的进程间通信,命名管道可用于同一主机上的任意进程间通信。
2.管道的生命周期随进程,本质是内核中的缓冲区,命名管道文件的文件名只是标识,用于让多个进程找到同一块缓冲区,删除后,之前已经打开管道的进程依然可以通信。
3.管道是半双工通信,是可以选择方向的单向通信。
4.命名管道打开特性为,若以只读方式打开文件,则会阻塞,直到管道被以写的方式打开,反之亦然。
四、System V 共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到 内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
1.共享内存的原理
让不同的进程,先看到同一份资源——内存块——共享内存。
区别于管道,管道是文件;共享内存共享的是内存块。
System V独立于文件系统,是专门为进程通信设计的。
2.编写代码——原理介绍
2.1创建共享内存
2.1.1 通过ftok()让两个进程获取同一个key
两个进程,调用系统调用接口ftok(),传参parhname和proj_id的值相同,则得到的key相同,达到找到同一内存空间的效果。
key本质是在内核中使用的。
#define PATHNAME "."
#define PROJID 0X6666
//让两个进程获得同一个key
key_t GetKey()
{
key_t k = ftok(PATHNAME,PROJID);
if(k == -1)
{
cerr << errno << ": " << strerror(errno) << endl;
exit(1);
}
return k;
}
2.1.2利用系统调用接口shmget()创建共享内存
1.shmflg的传参通常用的有两种:
IPC_CREAT 创建一个共享内存,如果共享内存不存在,则创建;如果已经存在,则获取已有共享内存标识符的并返回。
IPC_EXCL 不能单独使用,必须搭配IPC_CREAT;搭配使用时,创建一个共享内存,如果内存不存在,则创建,如果已经存在,则报错(IPC_CREAT | IPC_EXCL 创建新的一份共享内存。
2.size的传参:申请共享内存的内存大小,单位字节。
3.key
4.返回值:创建成功:共享内存标识符shmid;创建失败:-1。
系统中可以用shm来实现进程间通信
--->可以有多对进程通过shm来进行来通信
--->在任何时刻,都可能有多个shm被用来进行进程间通信
--->系统中可能有多个shm同时存在
--->操作系统需要管理shm
--->先描述,再组织
--->创建shm不只是开辟内存空间,操作系统为了管理shm,还需要定义shm结构体,用来封装shm的全部属性
--->共享内存 == 共享内存的内核数据结构(伪代码:struct shm) + 真正开辟的内存空间
一个创建,一个获取现成的:
static int CreateShmHelper(key_t k,int size,int flag)
{
int shmid = shmget(k,size,flag);
if(shmid == -1)
{
cerr << errno << ": " << strerror(errno) <<endl;
exit(2);
}
return shmid;
}
//创建共享内存,创建新的共享内存
int CreateShm(key_t k,int size)
{
umask(0);
return CreateShmHelper(k,size,IPC_CREAT | IPC_EXCL | 0666);//0666修改权限
}
//获取共享内存,获取现成的
int GetShm(key_t k,int size)
{
return CreateShmHelper(k,size,IPC_CREAT);
}
2.2关联进程
关联进程就是让两个进程都能找到并访问同一块共享内存,可以通过调用shmat()实现。
函数功能:关联进程和shmid对应的共享内存。返回共享内存的首地址。
//关联共享内存
char* AttachShm(int shmid)
{
char* start = (char*)shmat(shmid,nullptr,0);
return start;
}
start接收指向共享内存的指针,并将指针类型强转为char*类型。
2.3通信
一个进程写,一个进程读,从而达到通信的目的。
//client.cc
//通信
//用共享内存进行进程间通信时,没有调用任何接口?一旦共享内存映射到进程地址空间,该共享内存就直接被所有进程看到了
//因为共享内存的这种特性,可以让进程在通信的时候,减少拷贝次数,所以共享内存是所有进程间通信,速度最快的
//共享内存没有任何保护机制(同步互斥机制)(即便没有写内容,也可以被读取)
//——为什么?管道通过系统接口进行通信,共享内存直接通信
char c = 'A';
while(c <= 'Z')
{
start[c - 'A'] = c;
c++;
start[c - 'A'] = 0;
sleep(1);
}
//server.cc
//通信
int n = 0;
while(n <= 50)
{
cout << "client --> server#" << start << endl;
sleep(1);
n++;
}
start是共享内存的起始地址。
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到 内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
2.4取消关联
通信完成后,取消进程和共享内存间的关联,可以通过shmdt()实现。
//去关联
void DelAttachShm(char* start)
{
int n = shmdt(start);
assert(n != -1);
(void)n;
}
2.5利用shmctl()释放共享内存
可以通过调用shmctl()来删除共享内存(谁创建的谁释放):
一个进程创建,另一个进程获取。
注意:进程退出后,创建的共享内存还在。
//删除共享内存
void DelShm(int shmid)
{
int n = shmctl(shmid,IPC_RMID,nullptr);
assert(n != -1);
(void)n;
}
在命令行通过指令ipcs查看进程间通信的三种System V的信息:
ipcs -m查看System V 共享内存的信息:
其中,perms表示共享内存的权限
如何删除共享内存???通过shmid还是key???--------shmid
在用户层,共享内存进行通信都是利用shmid值;key是系统级,是操作系统用来区别不同共享内存的,只有在创建共享内存时需要使用(通过系统调用接口ftok()获取)。
共享内存的生命周期不随进程。进程结束后,进程创建的共享内存不会被释放。
可以通过命令行命令 ipcrm -m 对应的shmid 来删除shmid对应的共享内存。
3.通信测试
4.共享内存的实现原理理解(伪代码)
总结:System V 共享内存的通信过程需要调用的函数:
ftok() --> shmget() --> shmat() --> 通信(不同进程访问共享内存空间) --> shmdt() --> shmctl()
五、System V 消息队列
消息队列是进程间通信的一种方法,是共享资源。队列,具有先进先出的特征,谁写入信息,就将结点连接到队列的后面,读取就从前端读取,不过,结点会有一个标记,标记是哪个进程写入的信息,读取时只会读取把别的进程发送的信息。
1.创建消息队列
系统接口msgget()
key值:获取方式同共享内存ftok()
选项msgflg:IPC_CREAT、IPC_EXCL(含义使用同共享内存)
2.发送和接收信息
msgsnd()和msgrcv() (麻烦,不考虑)
3.删除消息队列
系统接口msgctl()
cmd = IPCRMID。
六、System V 信号量
1.互斥等4个概念
所有进程都能看到的资源:共享资源
a. 互斥:任何时刻,都只允许一个执行流访问共享资源 ——加锁
b. 任何时刻都只允许一个执行流访问的共享资源 ——临界资源
c. 临界资源都是通过代码访问的,凡是访问临界资源的代码,——临界区
d. 原子性 ——要么不做,要么做完(只有这两种确定的状态)