h5是什么网站上面做的,适合个人做外贸的网站,湛江做网站咨询电话,中端网站建设公司目录
一、进程间通信的介绍二、管道三、匿名管道四、命名管道五、system V进程间通信
一、进程间通信的介绍
1.进程间通信的概念
进程通信#xff08;Interprocess communication#xff09;#xff0c;简称#xff1a;IPC#xff1b;
本来进程之间是相互独立的。但是…目录
一、进程间通信的介绍二、管道三、匿名管道四、命名管道五、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.hint pipe(int pipefd[2]); 函数的参数是两个文件的描述符是输出型参数 pipefd[0]读管道 --- 对应的文件描述符是3 pipefd[1]写管道 --- 对应的文件描述符是4 返回值成功返回0失败返回-1 #include stdio.h
#include unistd.h
#include stdlib.h
#include string.hint 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.hint 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 disableread调用阻塞即进程暂停执行一直等到有数据来到为止。
O_NONBLOCK enableread调用返回-1errno值为EAGAIN。
当管道满的时候
O_NONBLOCK disable write调用阻塞直到有进程读走数据
O_NONBLOCK enable调用返回-1errno值为EAGAIN
如果所有管道写端对应的文件描述符被关闭则read返回0
如果所有管道读端对应的文件描述符被关闭则write操作会产生信号SIGPIPE,进而可能导致write进程退出
当要写入的数据量不大于PIPE_BUF时linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时linux将不再保证写入的原子性。 四、命名管道
匿名管道应用的一个限制就是只能在具有共同祖先具有亲缘关系的进程间通信。
如果我们想在不相关的进程之间交换数据可以使用FIFO文件来做这项工作它经常被称为命名管道。 命名管道是一种特殊类型的文件
1.命名管道的创建
1.命名行创建
命名管道可以从命令行上创建命令行方法是使用下面这个命令
[mlgVM-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阻塞直到有相应进程为写而打开该FIFOO_NONBLOCK enable立刻返回成功
如果当前打开操作是为写而打开FIFO时
O_NONBLOCK disable阻塞直到有相应进程为读而打开该FIFOO_NONBLOCK enable立刻返回失败错误码为ENXIO
3.用命名管道实现serverclient通信
实现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.hint 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_SIZE4096字节)这样新创建的共享内存的大小实际上是从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.使用命令释放
[mlgVM-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调用成功返回0shmctl调用失败返回-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调用成功返回0shmdt调用失败返回-1 代码同上运行结果如下 8.用共享内存实现serveclient通信 刚刚我们是一个进程和共享内存关联的接下来我们让两个进程通过共享内存进行通信在线之前我们先测试一下这两个进程能否成功挂接到同一个共享内存上 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查看 。
共享内存的生命周期是随内核的而管道是随进程的。
共享内存不提供任何的同步和互斥机制需要程序员自行保证数据安全。
共享内存在各种进程间通信方式中具有最高的效率。访问共享内存区域和访问进程独有的内存区域一样快并不需要通过系统调用或者其它需要切入内核的过程来完成。同时它也避免了对数据的各种不必要的复制。 其他通信方式将会陆续补充进来