目录
- 1. 进程间通信目的
- 2. 管道
- 2.1 管道特性(匿名管道)
- 2.1.1 单向通信
- 2.1.2 面向字节流
- 2.2 管道的大小
- 2.3 命名管道
- 3. system V进程间通信
- 3.1 shmget函数
- 3.1.1 key VS shmid
- 3.2 shmctl函数
- 3.3 shmat函数 VS shmdt函数:
- 3.4 测试
- 4. 感性认识
- 4.1 什么是临界资源
- 4.2 什么是临界区
- 4.3 什么是原子性
- 4.4 什么是互斥
- 4.5 什么是同步
- 4.6 什么是信号量
1. 进程间通信目的
进程间通信的本质是让不同进程看到同一份数据。
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
2. 管道
管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。
管道的本质就是OS中的管道的本质是内核中的缓冲区,通过内核缓冲区实现通信。
- 管道创建函数:
匿名管道只能父子进程间通信
创建无名管道:
参数fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端;用来读到打开的两个fd
返回值:成功返回0,失败返回错误代码
2.1 管道特性(匿名管道)
2.1.1 单向通信
管道是一个只能单向通信的通信信道
管道存在的原因:由于进程是独立的,那么想要实现进程间的通信成本就会比较大,所以首要解决的问题就是如何使两个进程看到同一份资源。
通过管道就可以实现:利用子进程继承父进程资源的特性,把管道继承下来,达到让不同的进程看到同一份资源的目的。
首先创建无名管道:
int pipefd[2];
if(pipe(pipefd) != 0){
perror("pipe failed!\n");
exit(1);
}
printf("pipefd[0] = %d\n",pipefd[0]);//0
printf("pipefd[1] = %d\n",pipefd[1]);//1
其中0是读端,1是写端;若要实现子进程写入,父进程读出,首先要关闭子进程的读出端,也就是pipefd[0]:
if(fork() == 0){
//child
//0是读端
close(pipefd[0]);
const char* msg = "aaaaaa\n";
while(1){
sleep(1);
write(pipefd[1],msg,strlen(msg));
}
exit(0);
}
再关闭父进程的写端pipefd[1]:
//parent
close(pipefd[1]);
while(1){
//sleep(1);
char buffer[64] = {0};
ssize_t s = read(pipefd[0],buffer,sizeof(buffer)-1);
if(s <= 0){
break;
}
else{
buffer[s] = 0;
printf("child said to father# %s",buffer);
}
}
上述代码形成的结果:
2.1.2 面向字节流
管道的传输是通过字节方式
上述代码运行结果:
可以看见,每隔一秒子进程写入,随后父进程读出,打印在屏幕上,也就是每隔一秒钟会打印一次。
但是如果不让子进程休眠,而让父进程每隔一秒读一次:
可以看见每隔一秒读出来的数据是很多行,这是因为一秒内子进程往缓冲区写入了这么多的数据,而没有识别到分隔符的话,能打印多少取决于子进程在这个过程中能打印多少字节,这便是面向字节流。
管道的读写有四种情况:
- 读端不读或者读的慢,写端要等读端;
- 读端关闭,写端收到SIGPIPE信号直接终止;
- 写段不写或者写的慢,读端要等写端;
- 写端关闭,读端读完pipe内部的数据然后在读,会读到0,表示读到文件结尾。
2.2 管道的大小
修改上述代码,每次子进程写入字符串a,父进程依旧死循环,但是不读:
int count = 0;
if(fork() == 0){
close(pipefd[0]);
const char* msg = "aaaaaa\n";
while(1){
write(pipefd[1],"a",1);
count++;
printf("count = %d\n",count);
}
exit(0);
}
//parent
close(pipefd[1]);
while(1){}
return 0;
}
可以看到输出结果是count = 65536后,不再增加:
- 结论
65536正好是64KB,这说明管道的大小正是664KB。
2.3 命名管道
创建命名管道函数mkfifo:
参数:第一个参数代表需要创建命名管道的文件的路径,第二个代表管道文件的权限。
返回值:返回值等于零创建成功,-1则创建失败。
命名管道可以实现两个进程之间的通信。
如果有一个进程创建了管道,那么另一个进程可以直接使用该管道来进行通信。
举例:实现进程间通信
- 客户端
- 服务端
- 头文件
可以看见在make以后,不仅产生了两个可执行文件,还产生了fifo文件,这个文件就是管道文件,由服务端的mkfifo函数调用生成,权限是自己设置的:
运行结果就是在客户端可以发送信息给服务端接收,本质就是通过管道完成的:
- 提示:
命名管道之所以叫命名管道,是因为进程间通信的方式是通过管道名,也就是说这个管道一定要有名字;而对于匿名管道,可以没有名字的原因是:它是通过父子间进程继承的方式看到同一份资源,并不需要通过管道文件名。
命名管道的文件名只是标识符,并非其真实的通信介质,只是用来让不同进程找到同一块缓冲区。所以磁盘空间大小并不决定通信内容的大小,而由缓冲区决定。
3. system V进程间通信
进程间通信有三个内容:共享内存、消息队列、信号量。这里解释共享内存。
上述方式都是基于文件的进程间通信,而system V进程间通信是基于OS层面专门给进程间通信设计的一种方案。可以说,同一主机间的进程间通信方案,就是system V方案。
其中有一个部分叫做共享内存;
共享内存区是最快的IPC形式。 一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
3.1 shmget函数
- 创建共享内存函数shmget
- 参数
- 第一个参数是标识符,作为“共同”的一个共享内存,需要有独特的、公有的标志来表示:
我们也可以自己设置,但是一般是采用ftok函数来获得:两个参数分别代表自定义路径名和自定义项目id,若设置失败则返回-1:
生成key值:
#include "commend.h"
#define PATH_NAME "./"
#define PROJ_ID 0x6666
int main()
{
key_t key = ftok(PATH_NAME,PROJ_ID);
if(key < 0){
perror("ftok");
return 1;
}
printf("%u\n",key);
return 0;
}
输出结果:
此时如果另一个进程想要与此进程进行通信,必须执行与之相同的代码生成相同的key值。
-
第二个参数是申请共享内存的大小,建议是4KB的整数倍
-
第三个参数是标志位,如果单独使用IPC_CREAT,或者flag为0:不存在共享内存就会创建一个,如果创建的共享内存已存在,就会直接返回当前已存在的共享内存。
对于IPC_EXCL,单独使用没有意义,但是一起使用的话,上述的规则就变成:不存在共享内存则创建之;如果已经有了,则返回出错。
其意义是:如果调用成功,那么这一定是个全新的、没人使用过的共享内存。
在刚才的代码基础上,申请共享内存:
#include "commend.h"
#include <sys/ipc.h>
#include <sys/shm.h>
#define PATH_NAME "./"
#define PROJ_ID 0x6666
#define SIZE 4066
int main()
{
//代码省略
//创建全新的id,如已存在则报错
int shmid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL);
if(shmid < 0){
perror("shmget");
return 2;
}
printf("key:%u,shmid:%d\n",key,shmid);
return 0;
}
输出结果:
ipcs指令是查看system资源的指令,默认查看三个内容:消息队列、共享内存、信号量:
执行ipcs -m以后,单独查看共享内存:
可以看见,执行完可执行文件以后(进程退出),此时的系统仍然存在共享内存,并没有被释放。
这说明system V的IPC资源,生命周期是随内核的,只能通过程序员的指令或者是OS重启来进行释放。 (删除指令ipcrm -m + shmid,不加shmid默认删除第一个)
3.1.1 key VS shmid
key:只是用来在系统层面进行标识唯一性的,不能用来管理共享内存;
shmid:是OS给用户提供的id,用来在用户层进行共享内存管理。
对于这两个概念,key可以类比为struct file,也就是fd的地址,具有唯一性;而shmid类比成fd,用来管理文件。
通过上述表达,知道了想要保证不同进程看到的是同一个共享内存,需要我们形成的key的算法和原始数据是一样的,就能形成同一个ID,达到目的。
这里的key同时也会被设置进入 内核中的关于共享内存的结构数据中。
3.2 shmctl函数
- 控制共享内存函数shmctl
对于参数cmd,表示将要采取的动作,有三个可取值:
-
IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值;
-
IPC_SET :在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值;
-
IPC_RMID:删除共享内存段。
对于第三个参数,代表的就是控制共享内存的数据结构,里面包含了key,每个进程能找到相同的key,就能找到对应的共享内存:
3.3 shmat函数 VS shmdt函数:
挂接函数(shmat)与去挂接(shmdt)函数
对于挂接函数来说:
参数:shmaddr表示要挂接的共享内存的起始地址,shmflg代表对应标志位;(shmaddr为NULL,核心自动选择一个地址)
返回值:成功返回一个指针变量,存放共享内存起始地址,失败返回-1.
对于去挂接函数来说:
参数:由shmat所返回的指针返回值:成功返回0,失败返回-1;(可以类比malloc函数的返回值)
去挂接作用是让进程和共享内存去挂接,而不是清除共享内存。
//获取key值
key_t key = ftok(PATH_NAME,PROJ_ID);
if(key < 0){
perror("ftok");
return 1;
}
//获取共享内存
int shmid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);
if(shmid < 0){
perror("shmget");
return 2;
}
//printf("key:%u,shmid:%d\n",key,shmid);
//sleep(10)
//挂接
char* mem = (char*)shmat(shmid,NULL,0);
//去挂接
shmdt(mem);
//控制共享内存(删除)
shmctl(shmid,IPC_RMID,NULL);
3.4 测试
在服务端申请并且挂接内存,然后往打印:
//如果client端不进行写入
//server端进行读取
//获取key值
key_t key = ftok(PATH_NAME,PROJ_ID);
if(key < 0){
perror("ftok");
return 1;
}
//获取共享内存
int shmid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);
if(shmid < 0){
perror("shmget");
return 2;
}
//挂接
char* mem = (char*)shmat(shmid,NULL,0);
while(1){
printf("%s\n",mem);
sleep(2);
}
//去挂接
shmdt(mem);
//控制共享内存(删除)
shmctl(shmid,IPC_RMID,NULL);
运行结果:
可以看出,当client端没有写入的时候,server端依旧在读取,并不会等待client写入,只不过读取的是空白字符。
但是当client端进行写入:
key_t key = ftok(PATH_NAME,PROJ_ID);
int shmid = shmget(key,SIZE,0);
//挂接
char* mem = (char*)shmat(shmid,NULL,0);
while(1){
sleep(2);
strcpy(mem,"i am process A\n");
}
//去挂接
shmdt(mem);
shmctl(shmid,IPC_RMID,NULL);
可以看见server端可以接收到client端写来的消息。
虽然是进行通信,但是不使用read等系统接口,是如何做到将进程A的数据给到进程B并且进程B将其打印出来的?
本质原因:在这个过程中并没有像管道通信那样调用系统接口read或者write(这两个接口的本质是将数据从内核拷贝到用户,或者从用户拷贝到内核),所以,共享内存一旦建立好并映射进自己进程的地址空间,该进程就可以直接看到共享内存,就如malloc空间一般,不需要任何系统调用接口。
这里虽然使用了字符串拷贝函数,但是也可以直接通过下标操作修改地址mem对应的值。
由此,共享内存是所有进程空间通信中速度最快的。
4. 感性认识
4.1 什么是临界资源
凡是需要进程间通信,就会引入被多个进程看到的资源(通信资源),同时,也引入了一个新的问题,临界资源的问题。
**凡是被多个执行流能够同时访问的资源就是临界资源。**同时向显示器打印,进程间通信的时候,管道、共享内存、消息队列等都是临界资源。
4.2 什么是临界区
进程的代码是有很多的,其中,用来访问临界资源的代码,就叫临界区。
4.3 什么是原子性
一件事要么不做,要做就做完,没有中间态,这就是原子性。
4.4 什么是互斥
在任意一个时候,只能允许一个执行流进入临界资源,执行它自己的临界区。
4.5 什么是同步
4.6 什么是信号量
命名管道和匿名管道、共享内存、消息队列,都是以传输数据为目的;而信号量不是以传输数据为目的,是通过共享“资源”的方式,来达到多个进程的同步和互斥的目的。
信号量的本质,是一个计数器,类似int count;是用来衡量临界资源中资源数目的。
可以这么理解:一个电影院相当于一个临界资源,而买票系统的计数系统会有上限,这个技术系统就是信号量。