目录
一.进程间通信介绍
1.进程间通信的目的
2.进程间通信的本质
3.进程间通信分类
二.什么是管道
三.匿名管道
1. 匿名管道只能用于具有亲缘关系的进程之间进行通信,常用于父子。
2.pipe函数
3. 匿名管道的使用
4.管道的读写规则
5.管道的特点,
6.管道的4中特殊情况
7.验证管道的大小
四.命名管道
1. 基本概念
2.创建命名管道
3.命名管道的打开规则
4.命名管道的四个使用示例
5. 命名管道和匿名管道的区别
6.命令行中的管道理解
五.system V进程间通信
六.system V共享内存
1.共享内存示意图
2.共享内存数据结构
3.共享内存的主体使用逻辑
4.共享内存的创建
5.共享内存的释放
6.共享内存进行关联
7.共享内存去关联
8.client 和 serve进行共享内存通信
七.共享内存和管道的比较
1.通信速度比较
2.管道的数据拷贝过程
3.共享内存的数据拷贝过程
4.为什么共享内存是速度最快的IPC方法?
5.为什么共享内存的拷贝次数少?
八.System V消息队列
九.System V信号量
1.理解信号量的相关概念
2.同步和互斥
进程间通信的本质是让 不同的进程看到同一份资源(内存 , 文件,内核缓冲等)
资源由谁(OS的哪些模块)提供 , 就有了不同的进程间通信方式!
这里的模块可以是: (文件–管道) , (OS内核IPC提供- SystemV IPC) , (网络–套接字)
a.进程运行的时候是具有独立性的!(数据层面) , 因此进程之间要实现通信是非常困难的。
b.进程间通信,一般一定要借助第三方(OS)资源。
c.通信的本质就是”数据的拷贝“,
管道
System V IPC
POSIX IPC
who命令用于查看当前云服务器的登录用户(一行显示一个用户),wc -l用于统计当前的行数
②解释
进程间通信的本质就是,让不同的进程看到同一份资源,使用匿名管道实现父子进程间通信的原理就是,让两个父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行读写草在,进而实现父子进程间通信
功能 : 创建一个无名管道原型 : int pipe(int fd[2]);参数 : fd:文件描述符数组 , 其中 fd[0] 表示读端 , fd[1] 表示写端 (0->嘴:读 , 1->笔:写)返回值 : 成功返回 0 ,失败返回错误代码
①代码+结果
1 #include
2 #include
3 #include
4 #include
5 #include
6 #include
7
8
9 //child -> write ; father -> read
10 int main()
11 {
12 int fd[2] = { 0 };
13 if (pipe(fd) < 0){ //使用pipe创建匿名管道
14 perror("pipe");
15 return 1;
16 }
17
18 pid_t id = fork();
19 if (id == 0){ //child
20
21 close(fd[0]); //子进程关闭读端
22
23 //子进程向管道写入数据
24 const char* buf = "hello father, I am child...";
25 int count = 5;
26 while (count--){
27 write(fd[1], buf, strlen(buf));
28 sleep(1); //每隔一秒写一条数据
29 }
30 close(fd[1]); //子进程写入完毕,关闭文件
31 exit(0);
32 }
33
34 //father
35 close(fd[1]); //父进程关闭写端
36
37 //父进程从管道读取数据
38 char buff[64];
39 while (1){
40 ssize_t s = read(fd[0], buff, sizeof(buff));
41 if (s > 0){
42 buff[s] = '\0'; //C语言读写规则
43 printf("child send to father:%s\n", buff);
44 }
45 else if (s == 0){
46 printf("read file end\n");
47 break;
48 }
49 else{
50 printf("read error\n");
51 break;
52 }
53 }
54
55 close(fd[0]); //父进程读取完毕,关闭文件
56 waitpid(id, NULL, 0);
57 return 0;
58 }
②创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用,具体步骤如下:
1)父进程调用pipe函数
2)fork创建子进程
3)子进程关闭读端,父进程关闭写端
③站在文件描述符角度深入理解管道
1)父进程调用pipe函数
2)fork创建子进程
3)子进程关闭读端,父进程关闭写端
pipe2函数与pipe函数类似,用于创建匿名管道,其函数原型如下:
int pipe2(int pipefd[2], int flags);
pipe2函数的第二个参数用于设置选项。
1、当没有数据可读时:
2、当管道满的时候:
3、如果所有管道写端对应的文件描述符被关闭,则read返回0。
4、如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出。
5、当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。
6、当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。
①管道内部自带同步与互斥机制。
②管道的生命周期随进程。
管道本质上是通过文件进行通信的,也就是说管道依赖于文件系统,那么当所有打开该文件的进程都退出后,该文件也就会被释放掉,所以说管道的生命周期随进程。
③管道提供的是流式服务。
我们一般所谓的流式概念就是,给你提供一个通信的信道,你的写端就直接写,读端直接读,但是具体写多少,读多少完全有上层决定。底层就只是提供一个数据通信的信道就完了,它不关心数据本身的一些细节格式,这叫做面向字节流。
流式服务: 数据没有明确的分割,一次拿多少数据都行。
数据报服务: 数据有明确的分割,拿数据按报文段拿。
④管道是半双工通信的。
在数据通信中,数据在线路上的传送方式可以分为以下三种:
1)前面的①②两种情况就能够很好的说明,管道是自带同步与互斥机制的,读端进程和写端进程是有一个步调协调的过程的,不会说当管道没有数据了读端还在读取,而当管道已经满了写端还在写入。读端进程读取数据的条件是管道里面有数据,写端进程写入数据的条件是管道当中还有空间,若是条件不满足,则相应的进程就会被挂起,直到条件满足后才会被再次唤醒。
2) 如何理解阻塞挂起 ?唤醒?
3) ③理解,读端进程已经将管道当中的所有数据都读取出来了(读端就会read返回值0,代表文件结束),而且此后也不会有写端再进行写入了,那么此时读端进程也就可以执行该进程的其他逻辑了,而不会被挂起。
4) ④理解,既然管道当中的数据已经没有进程会读取了,那么写端进程的写入将没有意义,因此操作系统直接将写端进程杀掉。而此时子进程代码都还没跑完就被终止了,属于异常退出,那么子进程必然收到了某种信号。
5)管道是单向通信,如果读端不读数据且把文件描述符关闭,那么写端做的就没有意义了。
写端相当于废弃的动作,浪费资源,所以OS直接将子进程干掉。为什么?OS不做不做任何浪费空间或者低效的事情,只要发现OS一定要把这个事情修正了。
6)验证OS发送信号杀掉进程
#include
#include
#include
#include
#include
#include
int main()
{
int fd[2] = { 0 };
if (pipe(fd) < 0){ //使用pipe创建匿名管道
perror("pipe");
return 1;
}
pid_t id = fork(); //使用fork创建子进程
if (id == 0){ //child
close(fd[0]); //子进程关闭读端
//子进程向管道写入数据
const char* buf = "I am child...";
int count = 5;
while (count--){
write(fd[1], buf, strlen(buf));
sleep(1);
}
close(fd[1]); //子进程写入完毕,关闭文件
exit(0);
}
//father
close(fd[1]); //关闭写端
close(fd[0]); //关闭读端
int status = 0;
waitpid(id, &status, 0);
printf("child get signal:%d\n", status & 0x7F); //打印子进程收到的信号
return 0;
}
7)使用命令查看信号 kill - l
管道的容量是有限的,如果管道已满,那么写端将阻塞或失败,需要了解一下管道的大小
①方法一:使用man手册
根据man手册,在2.6.11之前的Linux版本中,管道的最大容量与系统页面大小相同,从Linux 2.6.11往后,管道的最大容量是65536字节。
查看Linux系统版本
这里使用的是Linux 2.6.11之后的版本,因此管道的最大容量是65536字节。
②方法二:使用ulimit命令
可以使用ulimit -a 命令,查看当前资源限制的设定, 管道的最大容量是 512 × 8 = 4096 字节
③写代码验证管道容量
#include
#include
#include
#include
int main()
{
int fd[2] = { 0 };
if (pipe(fd) < 0){ //使用pipe创建匿名管道
perror("pipe");
return 1;
}
pid_t id = fork(); //使用fork创建子进程
if (id == 0){ //child
close(fd[0]); //子进程关闭读端
char c = '.';
int count = 0;
while (1){
write(fd[1], &c, 1);
count++;
printf("%d\n", count); //打印当前写入的字节数
}
close(fd[1]);
exit(0);
}
//father
close(fd[1]); //父进程关闭写端
//父进程不读取数据
waitpid(id, NULL, 0);
close(fd[0]);
return 0;
}
(1)使用命令创建
[gsx@VM-0-2-centos 220621]$ mkfifo pipe
(2)通过命令使用命名管道
①一个进程通过shell脚本重定向到管道 ,不断地往 pipe管道里面写数据 ;另一个进程使用cat命令 不断地读数据。这两个毫不相关的进程可以通过命名管道进行数据传输,完成了通信。
②使用cat命令的读进程主动退出,另一个写进程就被杀掉了; 因为写端执行的循环脚本是由命令行解释器bash执行的,所以此时bash就会被操作系统杀掉,我们的云服务器也就退出了。(当管道的读端进程退出后,写端进程再向管道写入数据就没有意义了,此时写端进程会被操作系统杀掉)
(3)使用函数创建命名管道
函数 : int mkfifo(const char *pathname, mode_t mode);
参数 : mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件。
- 若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。
- 若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。(注意当前路径的含义)
mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限。
返回值: 命名管道创建成功,返回0 ; 命名管道创建失败,返回-1。
①如果当前打开操作是为读而打开FIFO时。
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO。
O_NONBLOCK enable:立刻返回成功。
②如果当前打开操作是为写而打开FIFO时。
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO。
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO。
(1)通过命名管道 ,client & server进行通信
①comm.h中包含一些头文件供client 和 server使用
//comm.h
#pragma once
#include
#include
#include
#include
#include
#include
#define FILE_NAME "myfifo" //让客户端和服务端使用同一个命名管道
②server.c 代码
实现服务端(server)和客户端(client)之间的通信之前,我们需要先让服务端运行起来,我们需要让服务端运行后创建一个命名管道文件,然后再以读的方式打开该命名管道文件,之后服务端就可以从该命名管道当中读取客户端发来的通信信息了。
#include "comm.h"
int main()
{
if(mkfifo(FILE_NAME , 0644) < 0){ //创建管道文件
perror("mkfifo");
}
int fd = open(FILE_NAME , O_RDONLY); //只读形式打开
if(fd < 0){
perror("open error!\n");
return 2;
}
char buf[128];
while(1){
ssize_t s = read(fd ,buf, sizeof(buf)-1); //开始读数据
if(s > 0){
buf[s] = 0;
printf("client# %s\n" , buf);
}
else if(s == 0){
printf("client quit!\n");
break;
}
else{
printf("read error!\n");
break;
}
}
close(fd);
return 0;
}
③client.c 代码
而对于客户端来说,因为服务端运行起来后命名管道文件就已经被创建了,所以客户端只需以写的方式打开该命名管道文件,之后客户端就可以将通信信息写入到命名管道文件当中,进而实现和服务端的通信。
#include "comm.h"
int main()
{
int fd = open(FILE_NAME , O_WRONLY); //以写的方式打开文件
if(fd < 0){
perror("open error!\n");
return 1;
}
char buf[128];
while(1){
printf("Please Enter# "); //提示语句
fflush(stdout);
ssize_t s = read(0 , buf , sizeof(buf)); //从键盘中读取数据
if(s > 0){
buf[s-1] = 0; //输入时多了一个\n
write(fd , buf ,strlen(buf)); //把读取到的数据写到管道中
}
}
return 0;
}
④运行结果
客户端写入的信息进入命名管道当中,服务端再从命名管道当中将信息读取出来打印在服务端的显示器上,该现象说明服务端是能够通过命名管道获取到客户端发来的信息的,换句话说,此时这两个进程之间是能够通信的。
通过ps命令查看这两个进程的信息,可以发现这两个进程确实是两个毫不相关的进程,因为它们的PID和PPID都不相同
⑤client 和 server 谁先退出问题
1) 客户端先退出,服务端将管道当中的数据读完后就再也读不到数据了,那么此时服务端也就会去执行它的其他代码了(在当前代码中是直接退出了)。
2)服务端先退出,客户端写入管道的数据就不会被读取了,也就没有意义了,那么当客户端下一次再向管道写入数据时,就会收到操作系统发来的13号信号(SIGPIPE),此时客户端就被操作系统强制杀掉了。
⑥通信是在内存当中进行的
client端代码不变 ,server端只是以读的方式打开,但是不读取数据 :
运行程序前后两次查看myfifo管道文件的大小始终为0,说明了双方进程之间的通信依旧是在内存当中进行的,和匿名管道通信是一样的。
(2)通过命名管道,派发计算任务
①两个进程之间的通信,并不是简单的发送字符串而已,服务端是会对客户端发送过来的信息进行某些处理的 ; 这里我们的client端发送计算任务,server端将数据计算出来并打印到显示器上。
②client端的代码没有发生变化,只是server端读取数据时对数据进行了一些处理:
#include "comm.h"
int main()
{
if(mkfifo(FILE_NAME , 0644) < 0){ //创建管道文件
perror("mkfifo");
}
int fd = open(FILE_NAME , O_RDONLY); //只读形式打开
if(fd < 0){
perror("open error!\n");
return 2;
}
char buf[128];
while(1){
ssize_t s = read(fd ,buf, sizeof(buf)-1); //开始读数据
if(s > 0){
buf[s] = 0;
//简单计算
char *p = buf;
const char *lable="+-*/%";
int flag = 0; //记录计算的符号,利用下标
while(*p){
switch(*p){
case '+':
flag = 0;
break;
case '-':
flag = 1;
break;
case '*':
flag = 2;
break;
case '/':
flag = 3;
break;
case '%':
flag = 4;
break;
}
p++;
}
char *data1 = strtok(buf, "+-*/%"); //通过算数符号将左右两个数字分开
char *data2 = strtok(NULL, "+-*/%");
int x = atoi(data1); //将字符转整形计算
int y = atoi(data2);
int z = 0;
switch(flag){
case 0:
z = x + y;
break;
case 1:
z = x - y;
break;
case 2:
z = x * y;
break;
case 3:
z = x / y;
break;
case 4:
z = x % y;
break;
}
printf("%d %c %d = %d\n", x,lable[flag], y, z);
}
else if(s == 0){
printf("client quit!\n");
break;
}
else{
printf("read error!\n");
break;
}
}
close(fd);
return 0;
}
③结果
(3)通过命名管道,进行命令操作
①我们可以通过一个进程来控制另一个进程的行为,比如我们从客户端输入命令到管道当中,再让服务端将管道当中的命令读取出来并执行。简单实现了让服务端执行不带选项的命令,若是想让服务端执行带选项的命令,可以对管道当中获取的命令进行进一步的解析处理。
②client端的代码不变,server端对于输入的数据进行解析,如果有命令则执行:
#include "comm.h"
int main()
{
if(mkfifo(FILE_NAME , 0644) < 0){ //创建管道文件
perror("mkfifo");
}
int fd = open(FILE_NAME , O_RDONLY); //只读形式打开
if(fd < 0){
perror("open error!\n");
return 2;
}
char buf[128];
while(1){
ssize_t s = read(fd ,buf, sizeof(buf)-1); //开始读数据
if(s > 0){
buf[s] = 0;
printf("client# %s\n" , buf);
//执行命令
if(fork() == 0){ //child
execlp(buf , buf ,NULL); //进程替换
exit(1);
}
waitpid(-1 , NULL ,0); //进程等待
}
else if(s == 0){
printf("client quit!\n");
break;
}
else{
printf("read error!\n");
break;
}
}
close(fd);
return 0;
}
③结果
(4)通过命名管道,进行文件拷贝
①大致思路是,client端将log.txt 文件通过管道发送给server端,server端读取管道中的数据创建一个本地文件,将数据拷贝到本地文件中,以此来实现文件的拷贝。(本实验是在同一个机器上,且在同一个目录下,所以发送文件的文件名不能和接受文件的文件名重复)
②client端代码 : 以读的方式打开log.txt文件 , 以写的方式打开管道文件 ,将log.txt文件中的数据写到管道中.
#include "comm.h"
int main()
{
int fd = open(FILE_NAME, O_WRONLY); //以写的方式打开命名管道文件
if (fd < 0){
perror("open");
return 1;
}
int fdin = open("log.txt", O_RDONLY); //以读的方式打开log.txt文件
if (fdin < 0){
perror("open");
return 2;
}
char buf[128];
while (1){
//从log.txt文件当中读取数据
ssize_t s = read(fdin, buf, sizeof(buf));
if (s > 0){
write(fd, buf, s); //将读取到的数据写入到命名管道当中
}
else if (s == 0){
printf("read end of file!\n");
break;
}
else{
printf("read error!\n");
break;
}
}
close(fd); //通信完毕,关闭命名管道文件
close(fdin); //数据读取完毕,关闭log.txt文件
return 0;
}
③server端代码 : 以读的方式打开管道文件(没有管道文件创建) , 以写的方式在本地创建一个log-bat.txt文件,并将管道中的数据写到log-bat.txt文件中。
#include "comm.h"
int main()
{
if(mkfifo(FILE_NAME , 0644) < 0){ //创建管道文件
perror("mkfifo");
}
int fd = open(FILE_NAME , O_RDONLY); //只读形式打开
if(fd < 0){
perror("open error!\n");
return 2;
}
int fdout = open("log-bat.txt" , O_WRONLY|O_CREAT , 0644);
if(fdout < 0){
perror("open error!\n");
return 3;
}
char buf[128];
while(1){
ssize_t s = read(fd ,buf, sizeof(buf)-1); //开始读数据
if(s > 0){
write(fdout , buf , s); //将数据从管道写入文件
}
else if(s == 0){
printf("client quit!\n");
break;
}
else{
printf("read error!\n");
break;
}
}
close(fd); //通信关闭,关闭管道文件描述符
close(fdout); //数据写入完毕
return 0;
}
④结果
⑤进一步理解文件拷贝
使用管道在本地进行的文件拷贝,所以看似没什么意义,但我们若是将这里的管道想象成“网络”,将客户端想象成“Windows Xshell”,再将服务端想象成“centos服务器”, 那我们此时实现的就是文件上传的功能,若是将方向反过来,那么实现的就是文件下载的功能。
(1)使用cat 和 grep 命令 , 利用 管道 “ | ” 对信息进行过滤。
(2)管道“ | ” 是匿名管道还是命名管道 ?
①由于匿名管道只能用于有亲缘关系的进程之间的通信,而命名管道可以用于两个毫不相关的进程之间的通信,因此我们可以先看看命令行当中用管道(“|”)连接起来的各个进程之间是否具有亲缘关系。
②通过管道连接了三个进程,通过ps命令查看这三个进程可以发现,这三个进程的PPID是相同的,也就是说它们是由同一个父进程创建的子进程。
③三个sleep进程的父进程是bash ,三个sleep进程互为兄弟
④结论
若是两个进程之间采用的是命名管道,那么在磁盘上必须有一个对应的命名管道文件名,而实际上我们在使用命令的时候并不存在类似的命名管道文件名,因此命令行上的管道实际上是匿名管道。
1.管道通信本质是基于文件的,也就是说操作系统并没有为此做过多的设计工作,而system V IPC是操作系统特地设计的一种通信方式。但是不管怎么样,它们的本质都是一样的,都是在想尽办法让不同的进程看到同一份资源。
2. system V IPC提供的通信方式有以下三种:
3.其中,system V共享内存和system V消息队列是以传送数据为目的的,而system V信号量是为了保证进程间的同步与互斥而设计的,虽然system V信号量和通信好像没有直接关系,但属于通信范畴。
共享内存让不同进程看到同一份资源的方式 : 在物理内存当中申请一块内存空间,然后将这块内存空间分别与需要进行进程间通信的进程的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫做共享内存。
物理内存映射到地址空间中:
①如何实现: 本质:就是修改页表,虚拟地址空间中开辟空间
②是谁做的: 开辟物理空间开辟虚拟地址、建立映射等操作都是调用系统接口完成的,也就是说这些动作都由操作系统来完成。
(1)在系统当中可能会有大量的进程在进行通信,因此系统当中就可能存在大量的共享内存,那么操作系统必然要对其进行管理,所以共享内存除了在内存当中真正开辟空间之外,系统一定还要为共享内存维护相关的内核数据结构。
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 */
};
(2)当我们申请了一块共享内存后,为了让要实现通信的进程能够看到同一个共享内存,因此每一个共享内存被申请时都有一个key值,这个key值用于标识系统中共享内存的唯一性(让两个进程看到的是同一个共享内存)。
上面共享内存数据结构的第一个成员是shm_perm,shm_perm是一个ipc_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;
};
(3)在结构体的定义在Linux的具体目录下面
共享内存的建立大致包括以下两个过程:
共享内存的释放大致包括以下两个过程:
函数原型: int shmget(key_t key, size_t size, int shmflg);
参数说明:
- 第一个参数key,表示待创建共享内存在系统当中的唯一标识。
- 第二个参数size,表示待创建共享内存的大小。
- 第三个参数shmflg,表示创建共享内存的方式。
返回值说明:
- shmget调用成功,返回一个有效的共享内存标识符(用户层标识符)。
- shmget调用失败,返回-1。
注意: 我们把具有标定某种资源能力的东西叫做句柄,而这里shmget函数的返回值实际上就是共享内存的句柄,这个句柄可以在用户层标识共享内存,当共享内存被创建后,我们在后续使用共享内存的相关接口时,都是需要通过这个句柄对指定共享内存进行各种操作。
(1) 共享内存唯一标识符key需要通过函数获取
key_t ftok(const char *pathname, int proj_id);
(2)传入shmget函数的第三个参数shmflg,常用的组合方式:
使用结果:
(3)使用ftok 和 shmget 函数测试
#include
#include
#include
#include
#include
#define PATHNAME "/home/gsx/daily/220625/share_test/server.c" //路径名
#define PROJ_ID 0x1234 //整数标识符
#define SIZE 4096 //共享内存的大小
int main()
{
key_t k = ftok(PATHNAME, PROJ_ID); //生成唯一key
if(k < 0){
perror("ftok error!\n");
return 1;
}
printf("key: %x\n" , k);
int shmid = shmget(k ,SIZE , IPC_CREAT|IPC_EXCL|0644); //创建共享内存
if(shmid < 0){
perror("shmget");
return 2;
}
printf("shmid: %d\n" , shmid);
return 0;
}
结果
(4)查看共享内存信息
①使用ipcs命令查看
单独使用ipcs命令时,会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看它们之间某一个的相关信息,可以选择携带以下选项:
②使用ipcs -m查看共享内存
标题 | 含义 |
---|---|
key | 系统区别共享内存的唯一标识 |
shmid | 共享内存的用户层id(句柄) |
owner | 共享内存的拥有者 |
perms | 共享内存的权限 |
bytes | 共享内存的大小 |
nattch | 关联共享内存的进程数 |
status | 共享内存的状态 |
注意: key是在内核层面上保证共享内存唯一性的方式,而shmid是在用户层面上保证共享内存的唯一性,key和shmid之间的关系类似于fd和FILE*之间的的关系。
(5)关于第二个参数SIZE
如果size设置成4097 ,在OS底层给你分配了2页(按页对齐),但是你要4097字节那么我就只让你看到4097个字节的空间,绝对不少给你但也不多给你,少给了可能会出问题,多给了也可能出问题,用户要我怎么办我就怎么办,严格按照用户来 ; 所以最好设置4096的整数倍。
(1)使用命令删除共享内存
使用ipcrm -m shmid命令释放指定id的共享内存资源
[gsx@VM-0-2-centos share_test]$ ipcrm -m 2
(2)使用函数释放共享内存
函数 : int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
- 第一个参数shmid,表示所控制共享内存的用户级标识符。
- 第二个参数cmd,表示具体的控制动作。
- 第三个参数buf,用于获取或设置所控制共享内存的数据结构。
返回值:
- shmctl调用成功,返回0。
- shmctl调用失败,返回-1。
①shmctl函数的第二个参数传入的常用的选项:
②代码测试
#include
#include
#include
#include
#include
#define PATHNAME "/home/gsx/daily/220625/share_test/server.c" //路径名
#define PROJ_ID 0x1234 //整数标识符
#define SIZE 4096 //共享内存的大小
int main()
{
key_t k = ftok(PATHNAME, PROJ_ID); //生成唯一key
if(k < 0){
perror("ftok error!\n");
return 1;
}
printf("key: %x\n" , k);
int shmid = shmget(k ,SIZE , IPC_CREAT|IPC_EXCL|0644); //创建共享内存
if(shmid < 0){
perror("shmget");
return 2;
}
printf("shmid: %d\n" , shmid);
sleep(2);
shmctl(shmid , IPC_RMID , NULL); //释放共享内存
return 0;
}
运行server , 2s后释放共享内存 ,使用shell脚本监视:
[gsx@VM-0-2-centos share_test]$ while :; do ipcs -m; echo "############" ; sleep 1 ;done
函数 : void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
- 第一个参数shmid,表示待关联共享内存的用户级标识符。
- 第二个参数shmaddr,指定共享内存映射到进程地址空间的某一地址,通常设置为NULL,表示让内核自己决定一个合适的地址位置。
- 第三个参数shmflg,表示关联共享内存时设置的某些属性。
返回值:(和malloc很像)
- shmat调用成功,返回共享内存映射到进程地址空间中的起始地址。
- shmat调用失败,返回(void*)-1。
第三个参数shmflg传入的常用的选项:
- SHM_RDONLY 关联共享内存后只进行读取操作
- SHM_RND 若shmaddr不为NULL,则关联地址自动向下调整为SHMLBA的整数倍。公式:shmaddr-(shmaddr%SHMLBA)
- 0 默认为读写权限
代码测试
#include
#include
#include
#include
#include
#define PATHNAME "/home/gsx/daily/220625/share_test/server.c" //路径名
#define PROJ_ID 0x1234 //整数标识符
#define SIZE 4096 //共享内存的大小
int main()
{
key_t k = ftok(PATHNAME, PROJ_ID); //生成唯一key
if(k < 0){
perror("ftok error!\n");
return 1;
}
printf("key: %x\n" , k);
int shmid = shmget(k ,SIZE , IPC_CREAT|IPC_EXCL|0644); //创建共享内存
if(shmid < 0){
perror("shmget");
return 2;
}
printf("shmid: %d\n" , shmid);
printf("attach begin!\n");
char* mem = shmat(shmid , NULL , 0); //关联共享内存
if(mem == (void*)-1){
perror("shmat");
return 3;
}
printf("attach end!\n");
sleep(5);
shmctl(shmid , IPC_RMID , NULL); //释放共享内存
return 0;
}
结果:
注意事项:
函数 : int shmdt(const void *shmaddr);
参数:
- 待去关联共享内存的起始地址,即调用shmat函数时得到的起始地址。
返回值:
- shmdt调用成功,返回0。
- shmdt调用失败,返回-1。
将共享内存段与当前进程脱离不等于删除共享内存,只是取消了当前进程与该共享内存之间的联系。
(1)comm.h共同包含的头文件
为了让client和server在使用ftok函数获取key值时,能够得到同一种key值,那么服务端和客户端传入ftok函数的路径名和和整数标识符必须相同,这样才能生成同一种key值,进而找到同一个共享内存进行挂接。
#pragma once
#include
#include
#include
#include
#include
#define PATHNAME "/home/gsx/daily/220625/share"
#define PROJ_ID 0x123
#define SIZE 4096
(2)server.c 建立共享内存,并建立关联,从共享内存中接收数据
#include "comm.h"
int main()
{
key_t k = ftok(PATHNAME , PROJ_ID); //生成唯一key
if(k < 0){
perror("ftok error!\n");
return 1;
}
printf("key: %x\n" , k);
int shmid = shmget(k ,SIZE , IPC_CREAT|IPC_EXCL|0644); //创建共享内存
if(shmid < 0){
perror("shmget");
return 2;
}
printf("shmid: %d\n" , shmid);
char* mem =(char*)shmat(shmid , NULL , 0);//和共享内存进行关联
printf("%p\n" , mem);
//TO DO
int count = 10;
while(count--){
printf("client mesg: %s \n" , mem);
sleep(1);
}
shmdt(mem);//取消共享内存的关联
shmctl(shmid , IPC_RMID , NULL); //删除共享内存
return 0;
}
(3)client.c 通过k找到共享内存建立关联,向共享内存中发送数据
#include "comm.h"
int main()
{
key_t k = ftok(PATHNAME , PROJ_ID);
if(k < 0){
perror("ftok");
return 1;
}
printf("key:%x\n" , k);
int shmid = shmget(k , SIZE , IPC_CREAT);//创建共享内存
if(shmid < 0){
perror("shmget");
return 2;
}
printf("shmid: %d\n" , shmid);
char* mem = (char*)shmat(shmid , NULL ,0);//建立关联
//TO DO
int i = 0;
int count = 10;
while(count--){
mem[i] = 'A'+ i;
sleep(1);
i++;
mem[i] = '\0';
}
shmdt(mem);//去关联
return 0;
}
(4)结果
当共享内存创建好后就不再需要调用系统接口进行通信了(直接对地址空间进行操作),而管道创建好后仍需要read、write等系统接口进行通信。实际上,共享内存是所有进程间通信方式中最快的一种通信方式
read是把数据从内核缓冲区复制到进程缓冲区 , write是把进程缓冲区复制到内核缓冲区
①所以共享内存是所有进程间通信方式中最快的一种通信方式,因为该通信方式需要进行的拷贝次数最少。
②但是共享内存也是有缺点的,我们知道管道是自带同步与互斥机制的,但是共享内存并没有提供任何的保护机制,包括同步与互斥。
① 共享内存的拷贝次数少
② 在使用共享内存时不涉及系统调用接口(也就是不会有内核态到用户态之间的转化,因为都是在用户层进行操作的)
③ 不提供任何保护机制(没有同步与互斥)
共享内存的使用方法就和使用堆空间类似,直接向共享内存中写入,另一个进程直接就能看到。它与管道不同,管道还需要拷贝数据到管道,另一个进程再从管道中拷贝数据到自己当中。
消息队列实际上就是在系统当中创建了一个队列,队列当中的每个成员都是一个数据块,这些数据块都由类型和信息两部分构成,两个互相通信的进程通过某种方式看到同一个消息队列,这两个进程向对方发数据时,都在消息队列的队尾添加数据块,这两个进程获取数据块时,都在消息队列的队头取数据块。其中消息队列当中的某一个数据块是由谁发送给谁的,取决于数据块的类型
总结一下:
(1)进程间通信通过共享资源来实现,这虽然解决了通信的问题,但是也引入了新的问题,那就是通信进程间共用的临界资源,若是不对临界资源进行保护,就可能产生各个进程从临界资源获取的数据不一致等问题。
(2)保护临界资源的本质是保护临界区,我们把进程代码中访问临界资源的代码称之为临界区,信号量就是用来保护临界区的,信号量分为二元信号量和多元信号量。
(3)例如现在有一份500字节的资源,平均分成5份,每份100字节,每一份用一个信号量进行标识,总共有5个信号量。
(4)信号量本质是一个计数器,在二元信号量中,信号量的个数为1(相当于将临界资源看成一整块),二元信号量本质解决了临界资源的互斥问题 , 通过伪代码进行理解:
大致意思 : 当进程A申请访问共享内存资源时,如果此时sem为1(sem代表当前信号量个数),则进程A申请资源成功,此时这个资源就被A进程占有,此时需要将sem- -,然后进程A就可以对共享内存进行一系列操作,如果在进程A在访问共享内存时,进程B想要申请访问该共享内存资源,此时sem就为0了,那么这时进程B会被挂起,直到进程A访问共享内存结束后将sem加加,此时才会将进程B唤起,然后进程B再对该共享内存进行访问操作。
在这种情况下,无论什么时候都只会有一个进程在对同一份共享内存进行访问操作,也就解决了临界资源的互斥问题。
(5) PV操作,P操作就是申请信号量,而V操作就是释放信号量。