目录
- 一、进程间通信的介绍
- 二、管道
- 三、匿名管道
- 四、命名管道
- 五、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查看 )。
共享内存的生命周期是随内核的,而管道是随进程的。
共享内存不提供任何的同步和互斥机制,需要程序员自行保证数据安全。
共享内存在各种进程间通信方式中具有最高的效率。访问共享内存区域和访问进程独有的内存区域一样快,并不需要通过系统调用或者其它需要切入内核的过程来完成。同时它也避免了对数据的各种不必要的复制。
其他通信方式将会陆续补充进来