Linux —— 进程间通信
目录
- 一、进程间通信的介绍
- 二、管道
- 三、匿名管道
- 四、命名管道
- 五、system V进程间通信
一、进程间通信的介绍
1.进程间通信的概念
进程通信(Interprocess communication),简称:IPC;
本来进程之间是相互独立的。但是由于不同的进程之间可能要共享某些信息,所以就必须要有通讯来实现进程间的互斥和同步。比如说共享同一块内存、管道、消息队列、信号量等等就是实现这一过程的手段,相当于移动公司在打电话的作用。
2.进程间通信的目的
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
3.进程间通信的前提
进程间通信的前提本质:由操作系统参与,提供一份所有通信进行都能看到的公共资源;两个或多个进程相互通信,必须先看到一份公共的资源,这里的所谓的资源是属于操作系统的,就是一段内存(可能以文件的方式提供、可能以队列的方式提供,也有可能提供的就是原始内存块),这也就是通信方式有很多种的原因;
4.进程间通信的分类
管道
- 匿名管道pipe
- 命名管道
System V IPC
- System V 消息队列
- System V 共享内存(重点介绍)
- System V 信号量
POSIX IPC(本次不做介绍)
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
二、管道
管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
通过管道我们查看test.c文件写了多少行代码。其中cat和wc是两个命令,运行起来也就是进程,cat test.c 进程将查看内容通过管道交给了下一个进程wc -l 来计算代码行数;
三、匿名管道
1.基本原理
匿名管道用于进程间通信,且仅限于父子进程之间的通信。
我们知道进程的PCB中包含了一个指针数组 struct file_struct,它是用来描述并组织文件的。父进程和子进程均有这个指针数组,因为子进程是父进程的模板,其代码和数据是一样的;
打开一个文件时,其实是将文件加载到内核中,内核将会以结构体(struct file)的形式将文件的相关属性、文件操作的指针集合(即对应的底层IO设备的调用方法)等;
当父进程进行数据写入时(例如:写入“hello Linux”),数据是先被写入到用户级缓冲区,经由系统调用函数,又写入到了内核缓冲区,在进程结束或其他的操作下才被写到了对应的设备中;
如果数据在写入设备之前,“hello Linux”是在内核缓冲区的,因为子进程和父进程是同时指向这个文件的,所以子进程是能够看到这个数据的,并且可以对其操作;
简单来说,父进程向文件写入数据时,不直接写入对应的设备中,而是将数据暂存在内核缓冲区中,交给子进程来处理;
所以这种基于文件的方式就叫做管道;
2.管道的创建步骤
在创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用,具体步骤如下:
匿名管道属于单向通信,意味着父子进程只有一个端是打开的,实现父子通信的时候就需要根据自己的想要实现的情况,关闭对应的文件描述符;
1.pipe函数
#include <unistd.h>int pipe(int pipefd[2]);
函数的参数是两个文件的描述符,是输出型参数:
pipefd[0]:读管道 --- 对应的文件描述符是3
pipefd[1]:写管道 --- 对应的文件描述符是4
返回值:成功返回0,失败返回-1;
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>int main()
{int pipefd[2] = {0};if(pipe(pipefd) != 0){perror("pipe error!");return 1;}//pipefd[0]:读取段 pipefd[1]:写入端printf("pipefd[0]:%d\n",pipefd[0]);//3printf("pipefd[1]:%d\n",pipefd[1]);//4return 0;
}
2.代码实战
接下来我们来实现子进程写入数据,父进程读取数据;那么我们就需要针对父子进程关闭对应的文件描述符fd,子进程关闭读端,父进程关闭写端;
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>//让子进程sleep
int main()
{int pipefd[2] = {0};if(pipe(pipefd) != 0){ //创建匿名管道perror("pipe error!");return 1;}//pipefd[0]:读取端 pipefd[1]:写入端printf("pipefd[0]:%d\n",pipefd[0]);//3printf("pipefd[1]:%d\n",pipefd[1]);//4if(fork() == 0){//子进程---写入close(pipefd[0]); //关闭子进程的读取端const char* msg = "hello-linux!";while(1){write(pipefd[1], msg, strlen(msg)); //子进程不断的写数据sleep(1);}exit(0);}//父进程---读取close(pipefd[1]); //关闭父进程的写入端char buffer[64] = {0};while(1){//如果read返回值是0,就意味着子进程关闭文件描述符了ssize_t s = read(pipefd[0], buffer, sizeof(buffer)); //父进程不断的读数据if(s == 0){break;}else if(s > 0){buffer[s] = 0;printf("child say to father:%s\n",buffer);}else{break;}}
return 0;
}
3.管道的五个特点和四种情况
五个特点:
- 管道是一个只能单向通信的通信信道,仅限于父子间通信
- 管道提供流式服务
- 管道操作自带同步与互斥机制
- 进程退出,管道释放,所以管道的生命周期随进程
- 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
四种情况:
- 读端不读或者读的慢,写端要等待读端;
- 读端关闭,写端收到SIGPIPE信号直接终止;
- 写端不写或者写的慢,读端要等待写端;
- 写端关闭,读端读完pipe内部的数据然后再读,会读到0为止,表明读到文件结尾;
接下来我们通过下面的程序进行验证 :管道是单向通信和面向字节流
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>int main()
{int pipefd[2] = {0};if(pipe(pipefd) != 0){ //创建匿名管道perror("pipe error!");return 1;}//pipefd[0]:读取端 pipefd[1]:写入端printf("pipefd[0]:%d\n",pipefd[0]);//3printf("pipefd[1]:%d\n",pipefd[1]);//4if(fork() == 0){//子进程---写入close(pipefd[0]); //关闭子进程的读取端const char* msg = "hello-linux!";while(1){write(pipefd[1], msg, strlen(msg)); //子进程写数据sleep(1);}exit(0);}//父进程---读取close(pipefd[1]); //关闭父进程的写入端char buffer[64] = {0};while(1){sleep(1);ssize_t s = read(pipefd[0], buffer, sizeof(buffer)); //父进程读数据if(s == 0){break;}else if(s > 0){buffer[s] = 0;printf("child say to father:%s\n",buffer);}else{break;}}
return 0;
}
上述代码中,在父子进程中都有sleep函数:(我们切换使用)
1.当子进程sleep时,父进程没有sleep,运行结果如下:
我们可以发现,子进程在写入数据后经由管道交给父进程处理,这就验证了管道是单向通信的信道;
2.当父进程sleep时,子进程没有sleep,运行结果如下:
我们发现打印出来的数据并不想像刚才那样一条一条的打印,这是因为子进程在写入数据时,只要pipe内部有缓冲区,就不断的写入;当父进程在读取的时候,只要管道内有数据就会一直读;这就是所谓的字节流;即管道是面向字节流的(提供流式服务)
通过下面的程序来验证:同步机制
int main()
{int pipefd[2] = {0};if(pipe(pipefd) != 0){perror("pipe error!");return 1; }//pipefd[0]:读取端 pipefd[1]:写入端printf("pipefd[0]:%d\n",pipefd[0]);//3printf("pipefd[1]:%d\n",pipefd[1]);//4if(fork() == 0){//子进程---写入close(pipefd[0]);int count = 0;while(1){write(pipefd[1], "a", 1);count++;printf("count: %d\n",count);}exit(0);}//父进程---读取close(pipefd[1]);while(1){sleep(1);}return 0;
}
上面的代码中,子进程在不断的写入数据,而父进程一直不读取数据,运行结果如下:
我们运行起来后,就会一直刷屏,直到count为65536的时候停下来。这里为什么子进程不继续写了呢?这首先说明管道是有大小的,在我的云服务器下Linux的管道容量是65536(64Kb),其次子进程不继续写了,表明写端写满后要等待读端读取,才可以继续写入;
我们对上面的代码进行修改,让父进程一次读取一个字符,检验一下子进程会不会继续写入。
//这里简写了,其他内容和上面的代码一样
//父进程---读取
close(pipefd[1]);
while(1){sleep(10);char c = 0;read(pipefd[0], &c, 1);printf("father taken:%c\n", c);
}
我们发现父进程每过10秒读取一个字符,但是子进程并没有写入,我们试着将读取字符大小调整到4096个字节时,会发现读端读走数据后,写端就进行写入了;这表明管道自带同步机制(当然管道肯定也是有互斥机制的,这里不做讲解)。
通过下面的程序验证:写端不写或者写的慢,读端会等待写端;(读端不写同理)
int main()
{int pipefd[2] = {0}; if(pipe(pipefd) != 0){perror("pipe error!");return 1;}//pipefd[0]:读取端 pipefd[1]:写入端printf("pipefd[0]:%d\n",pipefd[0]);//3 printf("pipefd[1]:%d\n",pipefd[1]);//4//子进程写的慢if(fork() == 0){//子进程---写入close(pipefd[0]);const char* msg = "hello-linux!";while(1){write(pipefd[1], msg, strlen(msg));sleep(10); }exit(0);}//父进程---读取close(pipefd[1]);while(1){sleep(10);char c[64] = {0};ssize_t s = read(pipefd[0], &c, sizeof(c)-1);c[s] = 0;printf("father taken:%s\n", c);}return 0;
}
运行结果如下:
从运行结果可以看出,读端是在等待写端的,这也就是所谓的同步机制,当我们对写端不在进行写入时,读端也会一直在的等待写端的数据写入
通过下面的程序验证:写端关闭,读端读完pipe内部的数据然后再读,会读到0为止,表明读到文件结尾
int main()
{int pipefd[2] = {0}; if(pipe(pipefd) != 0){perror("pipe error!");return 1;}//pipefd[0]:读取端 pipefd[1]:写入端printf("pipefd[0]:%d\n",pipefd[0]);//3 printf("pipefd[1]:%d\n",pipefd[1]);//4//子进程写的慢if(fork() == 0){//子进程---写入close(pipefd[0]);const char* msg = "hello-linux!";while(1){write(pipefd[1], msg, strlen(msg));sleep(10); break; }close(pipefd[1]);exit(0);}//父进程---读取close(pipefd[1]);while(1){sleep(10);char c[64] = {0};ssize_t s = read(pipefd[0], &c, sizeof(c)-1);if(s > 0){c[s] = 0;printf("father taken:%s\n", c);}else if(s ==0){printf("write quit...\n");break;}else{break;}}return 0;
}
在上面的程序中,我们让写端写入一条数据后,10秒直接退出,然后关闭读端,运行结果如下:
当写端写入数据后关闭了写端,读端会从管道内读取到文件的末尾,接收到写端关闭后,就自行退出了。
通过下面的程序验证: 读端关闭,写端收到SIGPIPE信号直接终止
int main()
{int pipefd[2] = {0}; if(pipe(pipefd) != 0){perror("pipe error!");return 1;}//pipefd[0]:读取端 pipefd[1]:写入端printf("pipefd[0]:%d\n",pipefd[0]);//3 printf("pipefd[1]:%d\n",pipefd[1]);//4//子进程写的慢if(fork() == 0){//子进程---写入close(pipefd[0]);const char* msg = "hello-linux!";while(1){write(pipefd[1], msg, strlen(msg)); }exit(0);}//父进程---读取close(pipefd[1]);while(1){sleep(10);char c[64] = {0};ssize_t s = read(pipefd[0], &c, sizeof(c)-1);if(s > 0){c[s] = 0;printf("father taken:%s\n", c);}else if(s ==0){printf("write quit...\n");break;}else{break;}break;}close(pipefd[0]);return 0;
}
首先我们对程序进行分析,子进程处于一直写的状态,父进程读取一次数据后就break了,然后将读端关闭了(文件描述符0);
当我们的读端关闭,写端还在写入,在操作系统的层面上,严重不合理;这本质上就是在浪费操作系统的资源,所以操作系统在遇到这样的情况下,会将子进程杀掉(发送13号信号---SIGPIPE);
close(pipefd[0]);
//在源程序的基础上加上,用来获取子进程退出信号
int status = 0;
waitpid(-1, &status, 0);
printf("exit code: %d\n",(status >> 8)& 0xFF);
printf("exit signal: %d\n",status& 0x7F);
4.管道的读写规则
int pipe(int pipefd[2]);
int pipe2(int pipefd[2], int flags);
当没有数据可读时
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
当管道满的时候
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
如果所有管道写端对应的文件描述符被关闭,则read返回0
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
四、命名管道
匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。 命名管道是一种特殊类型的文件;
1.命名管道的创建
1.命名行创建
命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
[mlg@VM-20-8-centos lesson6-进程间通信]$ mkfifo myfifo
我们创建好命令管道后,就可以实现两个进程间的通信了;(左图的进程进行循环的数据写入,右图进程进行读取)当我们关闭读端的时候,写端也会被操作系统关闭,当我们关闭写端时,读端会一直在等写端;
当然也可以让读端不断的读取数据,写端只要写就行了()
2..程序创建(mkfifo函数)
在程序中创建命名管道使用mkfifo函数,mkfifo函数的函数原型如下:
int mkfifo(const char *pathname, mode_t mode);
pathname:表示你要创建的命名管道文件
- 如果pathname是以文件的方式给出,默认在当前的路径下创建;
- 如果pathname是以某个路径的方式给出,将会在这个路径下创建;
mode:表示给创建的命名管道设置权限
我们在设置权限时,例如0666权限,它会受到系统的umask(文件默认掩码)的影响,实际创建出来是(mode & ~umask)0664;
所以想要正确的得到自己设置的权限(0666),我们需要将文件默认掩码设置为0;
返回值:命名管道创建成功返回0,失败返回-1
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>#define MY_FIFO "myfifo" //默认是在当前路径下创建
//#define MY_FIFO "../xxx/myfifo" //指定在上级目录下的xxx目录下创建int main()
{umask(0); if(mkfifo(MY_FIFO, 0666) < 0){perror("mkfifo");return 1;}return 0;
}
2.命名管道的打开规则
如果当前打开操作是为读而打开FIFO时
- O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
- O_NONBLOCK enable:立刻返回成功
如果当前打开操作是为写而打开FIFO时
- O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
- O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
3.用命名管道实现server&client通信
实现server(服务端)和client(客户端)之间的通信,我们让server创建命名管道,用来读取命名管道内的数据;client获取管道,用来向命名管道内写数据;server(服务端)和client(客户端)想要使用同一个管道,这里我们可以让客户端和服务端包含同一个头文件comm.h,该头文件当中提供这个共用的命名管道文件的文件名,这样客户端和服务端就可以通过这个文件名,打开同一个命名管道文件,进而进行通信了。
comm.h:
#pragma once
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define MY_FIFO "./fifo"
server.c:
#include "comm.h"
int main()
{umask(0); //将文件掩码设置为0,确保得到我们设置的权限if(mkfifo(MY_FIFO, 0666) < 0){ //服务端用来创建命名管道文件perror("mkfifo");return 1;}int fd = open(MY_FIFO, O_RDONLY); //以只读的方式打开命名管道文件if(fd < 0){perror("open");return 2;}while(1){char buffer[64] = {0};ssize_t s = read(fd, buffer, sizeof(buffer) - 1); //从fd(命名管道)中读数据到buffer中if(s > 0){ buffer[s] = 0;printf("client: %s\n", buffer); //打印客户端发来的数据}else if(s == 0){printf("client qiut...\n");break;}else{perror("open");break;}}close(fd); //通信结束,关闭命名管道文件return 0;}
client.c:
#include "comm.h"int main()
{//这里不需要创建fifo,只需要获取就行int fd = open(MY_FIFO, O_WRONLY); //以写的方式打开命名管道文件if(fd < 0){ perror("open");return 1;}//业务逻辑while(1){printf("请输入:");fflush(stdout);char buffer[64] = {0};//先把数据从标准输入拿到我们的client进程内部ssize_t s = read(0, buffer, sizeof(buffer) - 1);if(s > 0){buffer[s-1] = 0;printf("%s\n",buffer);//拿到了数据,将数据写入命名管道write(fd, buffer, strlen(buffer));}}close(fd); //通信完毕,关闭命名管道文件return 0;
}
编写Makefile:
接下来使用Makefile进行编译,然后我们需要先将服务端运行起来,再运行客户端,因为服务端是用来创建命名管道文件的,先运行客户端的话,是不可以打开一个不存在的文件的;
4.用命名管道实现client控制server执行某种任务
两个进程间的通信,不是只能发送一些字符串,还可以实现一个进程控制另一个进程去完成某种任务;比如:client(客户端)向让server(服务端)执行“显示当前目录下的所有文件信息”的任务和执行“小火车命令sl”
#include "comm.h"
int main()
{umask(0); //将文件掩码设置为0,确保得到我们设置的权限if (mkfifo(MY_FIFO, 0666) < 0) { //服务端用来创建命名管道文件perror("mkfifo");return 1;}int fd = open(MY_FIFO, O_RDONLY); //以只读的方式打开命名管道文件if (fd < 0) {perror("open");return 2;}while (1) {char buffer[64] = { 0 };ssize_t s = read(fd, buffer, sizeof(buffer) - 1); //从fd(命名管道)中读数据到buffer中if (s > 0) {buffer[s] = 0;//client控制server完成某种动作/任务if (strcmp(buffer, "show") == 0) {if (fork() == 0) {execl("/usr/bin/ls", "ls", "-l", NULL);exit(1);}waitpid(-1, NULL, 0);}else if (strcmp(buffer, "run") == 0) {if (fork() == 0) {execl("/usr/bin/sl", "sl", NULL);}}else {printf("client: %s\n", buffer);}}else if (s == 0) {printf("client qiut...\n");break;}else {perror("open");break;}}close(fd); //通信结束,关闭命名管道文件return 0;
}
客户端输入show之后,服务端就显示数当前目录下的所有文件
客户端输入run之后,服务端就让小火车跑起来了
5.管道的总结
管道:
管道分为匿名管道和命名管道;
管道通信方式的中间介质是文件,通常称这种文件为管道文件;
匿名管道:管道是半双工的,数据只能单向通信;需要双方通信时,需要建立起两个管道;只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。
命名管道:不同于匿名管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信
利用系统调用pipe()创建一个无名管道文件,通常称为无名管道或PIPE;利用系统调用mkfifo()创建一个命名管道文件,通常称为有名管道或FIFO。
PIPE是一种非永久性的管道通信机构,当它访问的进程全部终止时,它也将随之被撤消。
FIFO是一种永久的管道通信机构,它可以弥补PIPE的不足。管道文件被创建后,使用open()将文件进行打开,然后便可对它进行读写操作,通过系统调用write()和read()来实现。通信完毕后,可使用close()将管道文件关闭。
匿名管道的文件是内存中的特殊文件,而且是不可见的,命名管道的文件是硬盘上的设备文件,是可见的。
五、system V进程间通信
它是操作系统层面上专门为进程间通信设计的一个方案,其通信方式包括如下三种:
- system V共享内存
- system V消息队列
- system V信号量
其中共享内存和消息队列是以传输数据为目的的,信号量是为了保证进程间的同步与互斥而设计的;本篇主要针对共享内容进行介绍
1.system V共享内存
1.共享内存的基本原理(示意图)
不同的进程想要看到同一份资源,在操作系统内部,一定是通过某种调用,在物理内存当中申请一块内存空间,然后通过某种调用,让参与通信进程“挂接”到这份新开辟的内存空间上;其本质:将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些参与通信进程便可以看到了同一份物理内存,这块物理内存就叫做共享内存。
2.共享内存的数据结构
我们知道在操作系统中是存在大量的进程的,如果两两进程进程进行通信,就需要多个共享内存。既然共享内存在系统中存在多份,就一定要将这些不同的共享内存管理起来,即先描述,再组织;为了保证两个或多个进程能够看到它们的同一份共享内存,那么共享内存一定要有能够唯一标识性的ID,方便让不同的进程识别它们的同一份共享内存;这个所谓的ID一定是在共享内存的数据结构中;
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 */
};
/*shm_perm 成员储存了共享内存对象的存取权限及其它一些信息。shm_segsz 成员定义了共享的内存大小(以字节为单位) 。shm_atime 成员保存了最近一次进程连接共享内存的时间。shm_dtime 成员保存了最近一次进程断开与共享内存的连接的时间。shm_ctime 成员保存了最近一次 shmid_ds 结构内容改变的时间。shm_cpid 成员保存了创建共享内存的进程的 pid 。shm_lpid 成员保存了最近一次连接共享内存的进程的 pid。shm_nattch 成员保存了与共享内存连接的进程数目
*/
对于每个IPC对象,系统共用一个struct ipc_perm的数据结构来存放权限信息,以确定一个ipc操作是否可以访问该IPC对象。
3.共享内存相关函数总览
4.共享内存的创建
创建共享内存我们需要用shmget函数,shmget函数的函数原型如下:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
函数说明:
得到一个共享内存标识符或创建一个共享内存对象并返回共享内存标识符
参数说明:
参数key:表示标识共享内存的键值
- 需要ftok函数获取
参数size:表示待创建共享内存的大小
- size是要建立共享内存的长度。所有的内存分配操作都是以页为单位的。所以如果一段进程只申请一块只有一个字节的内存,内存也会分配整整一页(在32位下一页的缺省大小PACE_SIZE=4096字节);这样,新创建的共享内存的大小实际上是从size这个参数调整而来的页面大小。即如果 size为1至4096,则实际申请到的共享内存大小为4K(一页);4097到8192,则实际申请到的共享内存大小为8K(两页),依此类推。
参数shmflg:表示创建共享内存的方式
shmflg主要和一些标志有关。
其中有效的包括IPC_CREAT和IPC_EXCL,它们的功能与open()的O_CREAT和O_EXCL相当。 IPC_CREAT 如果共享内存不存在,则创建一个共享内存,否则打开操作。 IPC_EXCL 只有在共享内存不存在的时候,新的共享内存才建立,否则就产生错误。如果单独使用IPC_CREAT:
shmget()函数要么返回一个已经存在的共享内存的标识符 ,要么返回一个新建的共享内存的标识符。如果将 IPC_CREAT和IPC_EXCL标志一起使用:
shmget()将返回一个新建的共享内存的标识符;如果该共享内存已存在,或者返回-1。
IPC_EXEL标志本身并没有太大的意义,但是和IPC_CREAT标志一起使用可以用来保证所得的对象是新建的,而不是打开已有的对象。
返回值:
- 调用成功,返回一个有效的共享内存标识符。
- 调用失败,返回-1,错误原因存于errno中。
传入shmget函数的第一个参数key,需要我们使用ftok函数进行获取
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
//把从pathname导出的信息与proj_id的低序8位组合成一个整数IPC键,传给shmget函数的key
ftok函数的作用就是,将一个已存在的路径名pathname(此文件必须存在且可存取)和一个整数标识符proj_id转换成一个key值。在使用shmget函数创建共享内存时,首先要调用ftok函数获取这个key值,这个key值会被填充进维护共享内存的数据结构当中,作为共享内存的唯一标识。
结合上面的知识,我们就可以来创建共享内存了,代码如下:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>#define PATH_NAME "./" //路径名
#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小int main()
{key_t key = ftok(PATH_NAME, PROJ_ID);//获取key值if(key < 0){perror("ftok");return 1;}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命令查看有关进程间通信设施的信息
这里的key和上面打印出来的key是一样的,我们是以 无符号数10进制打印的;
单独使用ipcs命令时,会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看它们之间某一个的相关信息,可以选择携带以下选项:
-q:列出消息队列相关信息。
-m:列出共享内存相关信息。
-s:列出信号量相关信息。
其中:
- key:共享内存的唯一键值
- shmid:共享内存的编号
- owner:创建的用户
- perms:共享内存的权限
- bytes:共享内存的大小
- nattach:连接到共享内存的进程数
- status:共享内存的状态
key vs shmid
key:只是用来在系统层面上进行标识唯一性的,不能用来管理共享内存;
shmid:是操作系统给用户返回的id,用来在用户层上进行管理共享内存;
key和shmid之间的关系类似于 fd 和 FILE* 之间的的关系。
5.共享内存的释放
刚刚我们已经创建好了共享内存,当我们的进程运行完毕后,申请的共享内存依旧存在,并没有被操作系统释放。实际上,管道是生命周期是随进程的,而共享内存的生命周期是随内核的,也就是说进程虽然已经退出,但是曾经创建的共享内存不会随着进程的退出而释放。
这说明,如果进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启(system V IPC都是如此),同时也说明了IPC资源是由内核提供并维护的。
此时我们若是要将创建的共享内存释放,有两个方法,一就是使用命令释放共享内存,二就是在进程通信完毕后调用释放共享内存的函数进行释放。
1.使用命令释放
[mlg@VM-20-8-centos shared_memory]$ ipcrm -m 5
//指定删除时使用的是共享内存的用户层id,即列表当中的shmid
2.使用函数释放
控制共享内存我们需要用shmctl函数,shmctl函数的函数原型如下:
#include <sys/types.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
函数说明:完成对共享内存的控制
参数说明:
shmctl函数的参数说明:
- shmid:共享内存标识符
- cmd:表示具体的控制动作
- buf:共享内存管理结构体(参考上文的共享内存的数据结构)
返回值:
- shmctl调用成功,返回0
- shmctl调用失败,返回-1
其中,第二个参数传入的常用的选项有以下三个:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>#define PATH_NAME "./" //路径名
#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小int main()
{key_t key = ftok(PATH_NAME, PROJ_ID);//获取key值if(key < 0){perror("ftok");return 1;}int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL);//创建共享内存if(shmid < 0){perror("shmget");return 2;} printf("key: %u shmid: %d\n", key, shmid);sleep(10);shmctl(shmid, IPC_RMID, NULL);//释放共享内存sleep(10);printf("key: 0x%x, shmid: %d -> shm delete success\n", key, shmid);return 0;
}
通过shell脚本查看共享内存的状态:
while :; do ipcs -m;echo "##############################";sleep 1;done
通过监控脚本可以确定共享内存确实创建并且成功释放了。
上文我们提到ipcs是查看进程间通信设施的信息的,这里的perms是共享内存的权限,此时为0,表示没有任何权限,所以我们在创建共享内存的时候,想要获得权限可以如下操作:
int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建权限为0666的共享内存
6.共享内存的关联(挂接)
将共享内存连接到进程地址空间需要用shmat函数,shmat函数的函数原型如下:
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
函数说明:
连接共享内存标识符为shmid的共享内存,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问;
参数说明:
返回值:
- shmat调用成功,返回共享内存映射到进程地址空间中的起始地址
- shmat调用失败,返回(void*) -1
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>#define PATH_NAME "./" //路径名
#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小int main()
{ key_t key = ftok(PATH_NAME, PROJ_ID); //获取key 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); //休眠10s后,关联共享内存 printf("attaches shm success\n"); sleep(5); shmdt(mem); //5秒后,共享内存去关联printf("detaches shm success\n"); sleep(5); shmctl(shmid, IPC_RMID, NULL); //释放共享内存printf("key: 0x%x, shmid: %d -> shm delete success\n", key, shmid); sleep(10); return 0;
}
7.共享内存的去关联
取消共享内存与进程地址空间之间的关联需要用shmdt函数,shmdt函数的函数原型如下:
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
函数说明:
与shmat函数相反,是用来断开与共享内存附加点的地址,禁止本进程访问此片共享内存;(并不是释放共享内存)
参数说明:
shmaddr:连接的共享内存的起始地址
返回值:
- shmdt调用成功,返回0
- shmdt调用失败,返回-1
代码同上,运行结果如下:
8.用共享内存实现serve&client通信
刚刚我们是一个进程和共享内存关联的,接下来我们让两个进程通过共享内存进行通信;在线之前我们先测试一下这两个进程能否成功挂接到同一个共享内存上;
comm.h
#pragma once
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h> #define PATH_NAME "./"
#define PROJ_ID 0x6666
#define SIZE 4097
server.c
#include "comm.h" int main()
{ key_t key = ftok(PATH_NAME, PROJ_ID); //获取key 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(5); char* mem = (char*)shmat(shmid, NULL, 0); //休眠10s后,关联共享内存 printf("attaches shm success\n"); sleep(5); /*通信内容(暂时不写):先测试两个进行能不能同时挂接到同一个共享内存上*/shmdt(mem); //5秒后,共享内存去关联printf("detaches shm success\n"); sleep(5); shmctl(shmid, IPC_RMID, NULL); //释放共享内存printf("key: 0x%x, shmid: %d -> shm delete success\n", key, shmid); sleep(5); return 0;
}
client.c
#include "comm.h" int main()
{ key_t key = ftok(PATH_NAME, PROJ_ID); if(key < 0){ perror("ftok"); return 1; } //client只需要获取即可,不需要创建 int shmid = shmget(key, SIZE, IPC_CREAT);//单独使用IPC_CREAT,共享内存存在就获取,反之创建 if(shmid < 0){ perror("shmid"); return 1; } printf("key: %u , shmid: %d\n", key, shmid);sleep(5); char* mem = (char*)shmat(shmid, NULL, 0); sleep(5); printf("client process attaches success\n"); /*通信内容(暂时不写):先测试两个进行能不能同时挂接到同一个共享内存上*/ shmdt(mem); sleep(5); printf("client process detaches success\n"); return 0;
}
从运行结果来看,两个进程确实都挂接到了共享内存;
接下来我们来实现通信内容:
//server.c
while(1){sleep(1); printf("%s\n", mem);
}
服务端不断的从共享内存中读数据;
//client.c
char c = 'A';
while(c < 'Z'){ mem[c - 'A'] = c;c++;mem[c - 'A'] = 0;sleep(2);
}
客户端不断的向共享内存写数据;
此时先运行服务端创建共享内存,当我们运行客户端时服务端就开始不断输出数据,说明服务端和客户端是能够正常通信的。
9.共享内存的总结
共享内存:
要使用一块共享内存,进程必须首先分配它。随后需要访问这个共享内存块的每一个进程都必须将这个共享内存绑定到自己的地址空间中。
在 Linux 系统中,每个进程的虚拟内存是被分为许多页面的。这些内存页面中包含了实际的数据。每个进程都会维护一个从内存地址到虚拟内存页面之间的映射关系。尽管每个进程都有自己的内存地址,不同的进程可以同时将同一个内存页面映射到自己的地址空间中,从而达到共享内存的目的。
分配一个新的共享内存块会创建新的内存页面。因为所有进程都希望共享对同一块内存的访问,只应由一个进程创建一块新的共享内存。再次分配一块已经存在的内存块不会创建新的页面,而只是会返回一个标识该内存块的标识符。
一个进程如需使用这个共享内存块,则首先需要将它绑定到自己的地址空间中。这样会创建一个从进程本身虚拟地址到共享页面的映射关系。当对共享内存的使用结束之后,这个映射关系将被删除。当再也没有进程需要使用这个共享内存块的时候,必须有一个(且只能是一个)进程负责释放这个被共享的内存页面。
所有共享内存块的大小都必须是系统页面大小的整数倍。系统页面大小指的是系统中单个内存页面包含的字节数。在 Linux 系统中,内存页面大小是4KB,不过您仍然应该通过调用 getpagesize 获取这个值(通过man 2 getpagesize查看 )。
共享内存的生命周期是随内核的,而管道是随进程的。
共享内存不提供任何的同步和互斥机制,需要程序员自行保证数据安全。
共享内存在各种进程间通信方式中具有最高的效率。访问共享内存区域和访问进程独有的内存区域一样快,并不需要通过系统调用或者其它需要切入内核的过程来完成。同时它也避免了对数据的各种不必要的复制。
其他通信方式将会陆续补充进来
相关文章:

Linux —— 进程间通信
目录 一、进程间通信的介绍二、管道三、匿名管道四、命名管道五、system V进程间通信 一、进程间通信的介绍 1.进程间通信的概念 进程通信(Interprocess communication),简称:IPC; 本来进程之间是相互独立的。但是…...

ASP.NET信息安全研究所设备管理系统的设计与实现
摘 要 以研究所的设备管理系统为背景,以研究所设备管理模式为研究对象,开发了设备管理系统。设备管理系统是设备管理与计算机技术相结合的产物,根据系统的功能需求分析与定义的数据模式,分析了应用程序的主要功能和系统实现的主…...
<网络安全>《81 微课堂<安全产品微简介(1)>》
1 简单的了解复杂的安全产品 产品简要防火墙网络区域边界上部署,主要作用是隔离阻断。安全审计一般包括网络日志的分析、网络流量的监控和用户行为的跟踪等。发现网络中的潜在问题和漏洞。入侵检测IDS实时监控和检测网络中的异常活动和入侵行为。入侵防御IPS防病毒…...

【6D位姿估计】FoundationPose 跑通demo 训练记录
前言 本文记录在FoundationPose中,跑通基于CAD模型为输入的demo,输出位姿信息,可视化结果。 然后分享NeRF物体重建部分的训练,以及RGBD图为输入的demo。 1、搭建环境 方案1:基于docker镜像(推荐…...

Python 中 “yield“ 的不同行为
在我们使用Python编译过程中,yield 关键字用于定义生成器函数,它的作用是将函数变成一个生成器,可以迭代产生值。yield 的行为在不同的情况下会有不同的效果和用途。 1、问题背景 在 Python 中,“yield” 是一种生成器࿰…...

迅睿CMS中实现关键词搜索高亮
在迅睿CMS系统中实现关键词搜索高亮是提升用户体验和搜索效果的重要手段。当用户搜索某个关键词时,将搜索结果中的关键词高亮显示,可以帮助用户更快速地定位到所需信息。 关键词高亮的实现 在迅睿CMS中,你可以使用内置的dr_keyword_highlig…...

晶振的精度与稳定性有什么关系?
晶振的精度和稳定性是电子设备中非常重要的参数,它们受到多种因素的影响,主要包括: 精度的影响因素: 温度变化:晶体的温度系数会使得频率随温度变化而变化,通常在0C到55C的工业标准温度范围内,…...
【C】137 只出现一次的数字
给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。 你必须设计并实现线性时间复杂度的算法且使用常数级空间来解决此问题。 解法一 #include <stdio.h>int singleNumber(i…...

51单片机入门:DS1302时钟
51单片机内部含有晶振,可以实现定时/计数功能。但是其缺点有:精度往往不高、不能掉电使用等。 我们可以通过DS1302时钟芯片来解决以上的缺点。 DS1302时钟芯片 功能:DS1302是一种低功耗实时时钟芯片,内部有自动的计时功能&#x…...
Redis-5 分布式锁
一.为什么要使用分布式锁? 传统的互斥锁synchronized只能作用于同一台虚拟机上的线程,在使用服务器集群部署的情况下,互斥锁就会失效,因此要采用分布式锁来处理不同服务器上的线程访问同一资源的情况。 二.redis的分布式锁是如何…...

音转文工具,9.8k star! 【送源码】
我们经常会遇到将音频转为文字的情况,比如在开会时录音的会议纪要、上课时录下的老师讲课内容。虽然网上也有一些在线的工具可以将音频转为文字,但是考虑到数据安全和费用问题,使用起来也不是很方便。 今天了不起给大家介绍一款开源工具——…...

【首次发布】华为 OD 机试 C卷抽中题库清单(真题库),目前华为OD机考以C卷为主,特殊情况会发送D卷
本篇博客为大家整理华为 OD 考友反馈 C 卷抽中题,经过 1 个的考友复盘,目前已经收录 100 题目,预计在有 2 周可以收集完整。 所有题目,都有考友截图反馈,同时欢迎大家机考过后,提供橡皮擦真题,获…...

【进程等待】waitpid的参数pid | status的位图位操作WIFEXITEDWEXITSTATUS宏
目录 waitpid pid status status位图 status按位操作 输入型参数和输入型参数 宏WIFEXITED&WEXITSTATUS options&非阻塞等待 上篇进程等待我们介绍到怎样去进程等待。我们介绍了wait函数&阻塞等待。本篇我们将介绍waitpid函数的参数pid和status。 waitp…...
unity---常用API
1. Vector3:结构体由x、y、z这3个数值组成,表示一个向量 magnitude变量返回该向量的长度normalized变量返回 magnitude 为 1 时的该向量zero静态变量Vector3(0, 0, 0)one静态变量Vector3(1, 1, 1)forward静态变量Vector3(0, 0, 1)back静态变量Vector3(0…...

设计模式: 模板模式
目录 一,模板模式 二,特点 三,组成部分 四,实现步骤 五,案例 一,模板模式 模板模式(Template Pattern)是一种行为型设计模式,它在超类中定义了一个算法的骨架&#…...

[虚拟机+单机]梦幻契约H5修复版_附GM工具
本教程仅限学习使用,禁止商用,一切后果与本人无关,此声明具有法律效应!!!! 教程是本人亲自搭建成功的,绝对是完整可运行的,踩过的坑都给你们填上了 视频演示 [虚拟机单…...
头文件相互包含 前向声明
当两个类需要相互访问对方的成员或方法时,通常意味着它们之间存在某种依赖关系。这种依赖关系可能源于类的设计或功能需求。以下是一个简单的例子,展示了当两个类需要相互访问对方成员或方法时,如何使用包含对方头文件的方式来解决循环包含的…...

七款好用的上网行为管理软件推荐 |有没有好用的上网行为管理系统
七款好用的上网行为管理软件推荐 |有没有好用的上网行为管理系统 员工上班刷视频! 员工上班炒股! 员工上班干副业! 碰见这种情况怎么办?当然是用电脑监控软件来监视员工的一举一动了,那么这样的软件有哪些呢&#…...
centos7-bcc 安装
检查 内核信息 确保安装的kernel-dev 和内核版本一致!!! rpm -qa|grep kernel kernel-tools-3.10.0-1160.92.1.el7.x86_64 kernel-devel-3.10.0-1160.92.1.el7.x86_64 kernel-headers-3.10.0-1160.92.1.el7.x86_64 kernel-3.10.0-1160.92.1.el7.x86_64 kernel-tools-libs-3.1…...

5.06号模拟前端面试8问
5.06号模拟前端面试8问 1.promise如何实现then处理 在JavaScript中,Promise 是一个代表异步操作最终完成或失败的对象。它有三种状态:pending(等待),fulfilled(完成),rejected&…...

基于FPGA的PID算法学习———实现PID比例控制算法
基于FPGA的PID算法学习 前言一、PID算法分析二、PID仿真分析1. PID代码2.PI代码3.P代码4.顶层5.测试文件6.仿真波形 总结 前言 学习内容:参考网站: PID算法控制 PID即:Proportional(比例)、Integral(积分&…...

【入坑系列】TiDB 强制索引在不同库下不生效问题
文章目录 背景SQL 优化情况线上SQL运行情况分析怀疑1:执行计划绑定问题?尝试:SHOW WARNINGS 查看警告探索 TiDB 的 USE_INDEX 写法Hint 不生效问题排查解决参考背景 项目中使用 TiDB 数据库,并对 SQL 进行优化了,添加了强制索引。 UAT 环境已经生效,但 PROD 环境强制索…...
WEB3全栈开发——面试专业技能点P2智能合约开发(Solidity)
一、Solidity合约开发 下面是 Solidity 合约开发 的概念、代码示例及讲解,适合用作学习或写简历项目背景说明。 🧠 一、概念简介:Solidity 合约开发 Solidity 是一种专门为 以太坊(Ethereum)平台编写智能合约的高级编…...
React---day11
14.4 react-redux第三方库 提供connect、thunk之类的函数 以获取一个banner数据为例子 store: 我们在使用异步的时候理应是要使用中间件的,但是configureStore 已经自动集成了 redux-thunk,注意action里面要返回函数 import { configureS…...

视频行为标注工具BehaviLabel(源码+使用介绍+Windows.Exe版本)
前言: 最近在做行为检测相关的模型,用的是时空图卷积网络(STGCN),但原有kinetic-400数据集数据质量较低,需要进行细粒度的标注,同时粗略搜了下已有开源工具基本都集中于图像分割这块,…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...

uniapp手机号一键登录保姆级教程(包含前端和后端)
目录 前置条件创建uniapp项目并关联uniClound云空间开启一键登录模块并开通一键登录服务编写云函数并上传部署获取手机号流程(第一种) 前端直接调用云函数获取手机号(第三种)后台调用云函数获取手机号 错误码常见问题 前置条件 手机安装有sim卡手机开启…...
C#学习第29天:表达式树(Expression Trees)
目录 什么是表达式树? 核心概念 1.表达式树的构建 2. 表达式树与Lambda表达式 3.解析和访问表达式树 4.动态条件查询 表达式树的优势 1.动态构建查询 2.LINQ 提供程序支持: 3.性能优化 4.元数据处理 5.代码转换和重写 适用场景 代码复杂性…...
【LeetCode】3309. 连接二进制表示可形成的最大数值(递归|回溯|位运算)
LeetCode 3309. 连接二进制表示可形成的最大数值(中等) 题目描述解题思路Java代码 题目描述 题目链接:LeetCode 3309. 连接二进制表示可形成的最大数值(中等) 给你一个长度为 3 的整数数组 nums。 现以某种顺序 连接…...
API网关Kong的鉴权与限流:高并发场景下的核心实践
🔥「炎码工坊」技术弹药已装填! 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 引言 在微服务架构中,API网关承担着流量调度、安全防护和协议转换的核心职责。作为云原生时代的代表性网关,Kong凭借其插件化架构…...