目次
一、进程间通信概念
二、进程间通信的发展
三、进程间通信的分类
四、管道
4.1 什么是管道
4.2 匿名管道
4.2 基于匿名管道设计进程池
4.3 定名管道
4.4 用定名管道实现server&client通信
五、system V共享内存
5.1 system V共享内存的引入
5.2 共享内存的原理
5.3 共享内存函数
5.4 使用共享内存的步骤
5.5 基于共享内存的进程间通信示例
5.6 共享内存的特点
5.7 共享内存数据结构
六、简述system V消息队列和system V信号量
6.1 system V消息队列
6.2 system V信号量
七、回首共享内存数据结构
一、进程间通信概念
进程固然具有独立性,但是进程和进程之间是大概进行协作的。协作的前提是进程之间可以传递信息,即需要进程间通信。
Linux中进程间通信(Inter-Process Communication,IPC)是指为了协调进程之间的行为,差别进程之间进行信息交换和资源共享的机制。
进程间通信的目的包括:
- 数据传输:答应一个进程将数据发送给另一个进程。
- 资源共享:答应多个进程访问相同的资源,如文件、内存地区等。
- 关照变乱:一个进程可以向另一个或一组进程发送消息,关照它(它们)某个变乱的发生,如进程停止时关照父进程。
- 进程控制:答应一个进程完全控制另一个进程的执行。比方调试进程需要拦截另一个进程的全部陷入和异常,并能够实时知道它的状态改变。
(“陷入”通常指的是程序的执行被操纵系统或其他进程逼迫停息,以便处置惩罚系统调用或硬件中断。)
进程间通信怎样实现呢?之前讲到进程具有独立性,那么A进程的数据要交给B进程,不能直接把A进程的数据直接给B进程,因为A进程访问B进程的内存地区把数据拷贝进去,大概B进程访问A进程的内存地区把数据拷贝出来,这两种都不行,会粉碎进程的独立性。所以就需要进程通信时的中心媒介。这样既能保持进程的独立性也能实现进程间通信。因此进程间通信的本质就是让差别的进程看到OS中的同一份资源,从而实现数据的传递和共享。(该资源不能由A/B进程提供,但是能由A/B进程申请)
二、进程间通信的发展
进程间通信的发展经历了以下几个阶段:
- 管道:包括匿名管道(pipe)和定名管道(FIFO)。匿名管道只能用于具有亲缘关系的进程间通信,而定名管道可以用于不具有亲缘关系的进程间通信。
- System V进程间通信:包括System V消息队列、System V共享内存、System V信号量等。这些机制提供了更为复杂的IPC功能,支持多种情势的通信和同步。
- POSIX进程间通信:包括POSIX消息队列、POSIX共享内存、POSIX信号量、互斥量、条件变量、读写锁等。POSIX IPC提供了与System V IPC雷同的功能,但具有更好的可移植性。
三、进程间通信的分类
Linux中的进程间通信可以分为以下几类:
- 管道:
匿名管道:用于具有亲缘关系的进程间通信。
定名管道:用于不具有亲缘关系的进程间通信。
- System V IPC:
消息队列:用于进程间传递消息。
共享内存:用于进程间共享内存地区。
信号量:用于进程间同步和互斥。
- POSIX IPC:
消息队列:与System V消息队列雷同。
共享内存:与System V共享内存雷同。
信号量:与System V信号量雷同。
互斥量:用于进程间同步。
条件变量:用于进程间同步。
读写锁:用于进程间同步。
四、管道
4.1 什么是管道
管道(Pipe)是Unix系统中用于进程间通信的一种机制,它答应一个进程的输出直接作为另一个进程的输入。管道是一种单向的通信通道,数据只能从管道的一端流向另一端。
回首文件系统:
【Linux】文件形貌符和重定向-CSDN博客 【Linux】文件系统和软硬链接-CSDN博客
怎样做到让差别的进程看到了同一个管道文件?
进程是具有独立性的,一个进程的数据,另一个数据是无法直接拿到的。就连父子进程也会因为修改数据而触发写时拷贝。所以不能通过数据传递(这里指定名的变量),而是使用其他方式。
可执行程序加载到内存时,要创建task_struct,其中包含指向files_struct结构体的指针,在该结构体中有一个fd_array指针数组。当加载一个文件到内存时,会创建struct file ,结构体中会包含文件的inode、方法集、文件缓冲区。并将自己链入到fd_array中。
在上层用户使用某个方法向磁盘写入数据时,会打开文件、得到fd、找到struct inode、文件缓冲区、通过方法集的方法将数据刷新到磁盘。
创建子进程,父进程的task_struct 、flies_struct 都要给子进程拷贝一份(flies_struct属于进程部分的数据),flies_struct是浅拷贝,直接拷贝内里的指针。因此父子进程的fd_array[]指向相同的file。struct file不需要重新拷贝,此时差别的进程看到OS中的同一份资源,父进程只需要向自己的文件缓冲区中写入数据,子进程就可以通过它的文件形貌符得到该数据。
打开平凡文件就要有路径,最终数据刷新到磁盘上。父进程想要给子进程发消息,假如通过这种把数据写到缓冲区里,再写到磁盘中的方式,服从就太低下了(一般文件缓冲区的数据都要刷新到磁盘)。如今就需要这个文件是一个纯内存级的文件,不需要在磁盘中存在,乃至不需要名字,只要保证父子进程能访问到它即可。这种文件就叫做管道文件,所以管道文件也是纯内存级的文件,不需要向磁盘刷新。不需要名字也不需要路径,所以也叫匿名管道。
管道文件有一个特点:实现了资源共享之后,只答应单向通信。
这种单向传递的通信特性很像一样寻常生活中的管道,所以起名叫做管道。比方家里自来水永久都是自来水公司到家里。
在Unix系统中,管道通常通过下令行中的管道符号('|')来创建。比方在下令行中输入以下下令时:
下令'command1'的输出会被重定向到管道中,而下令'command2'的输入会从管道中读取。这样,'command1'的输出就会成为'command2'的输入,实现了两个进程之间的数据传递。
除了下令行中的管道,Unix系统还提供了两种类型的管道:匿名管道和定名管道。
4.2 匿名管道
匿名管道(pipe)是在下令行中自动创建的,用于具有亲缘关系的进程间通信,如父进程和子进程。它有一个管道文件形貌符,分别对应读端和写端。
匿名管道在创建后不能被其他进程打开。
- #include <unistd.h>
- 功能:创建一匿名管道
- 原型
- int pipe(int fd[2]);
- 参数
- fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
- 返回值:成功返回0,失败返回错误代码
复制代码 注:fd是输出型参数,返回读写端对应的fd,用来关掉读/写端。
匿名管道的原理:(这里实现父写子读)
- 把一个文件按读方式和写方式打开。
- 创建子进程时,子进程直接拷贝父进程的文件形貌符表。
- 父进程关闭fd[0],留下写端,子进程关闭fd[1],留下读端。
- 就形成了单向通信的管道通路。
1. 为什么最开始时把一个文件按读方式和写方式打开?
因为只保留读端或写端,创建子进程时不能保留单向信道。保留读端和写端,子进程也有读端和写端,再进行得当的关闭。就可以实现单向信道,父进程读子进程写,大概父进程写子进程读。
2. 同一个进程把文件分别进行读打开和写打开,在内存里,文件的内容和属性会存在几份?
只用存在一份。这是因为文件的内容和属性(如权限、全部者、巨细、创建和修改时间等)都存储在文件的 inode 结构体中,而 inode 结构体在文件系统中被唯一标识。
3. 同一个进程把文件分别进行读打开和写打开,需要几个struct file结构体?
需要两个struct file结构体。struct file内部有一个字段f_pos ,表示当前的操纵位置,相称于文件内部的偏移量。文件读和写打开时,它读位置和写位置不一样。同一个进程把文件分别进行读打开和写打开,需要创建两个struct file结构体,一个用来读取、一个用来写入。只不过这两个struct file结构体会指向同样的一个inode、同一个方法集、同一个缓冲区。
4. 进程结束时,文件会被直接关闭吗?
不会。创建子进程时,由于files_struct是浅拷贝,所以指向相同的struct file结构体。形成管道时父子进程关闭各自的读/写端。struct file 中有一个引用计数的字段f_count,用于跟踪有多少个进程正在使用这个文件。当进程打开该文件时,f_count 会增加;当进程关闭文件时,f_count 会减少。所以进程关闭读/写端的实质是把文件形貌符表内部指向struct file 的指针清空,然后依次将引用计数f_count--,此时进程就认为把文件关了,但末了文件是否关闭是由操纵系统决定的,要判定f_count是否减到0。最终,是否关闭文件由操纵系统决定,它会在全部引用计数减到0时开释与文件相关的资源。
5. 引用计数f_count和硬链接数的差别
硬链接是在磁盘中用来统计有多少文件名和我的文件inode产生映射关系的;但是上面的引用计数f_count是用来记述内核数据结构struct file被多少进程文件形貌符表指向的。两者固然都是引用计数,但引用的场景差别
现让父进程创建一个管道文件,进行父读子写,即父进程关闭写端,子进程关闭读端
- #include <iostream>
- #include <unistd.h>
- #include <cassert>
- #include <cstring>
- #include <sys/types.h>
- #include <sys/wait.h>
- #define MAX_SIZE 1024
- int main()
- {
- int pipefd[2] = {0};
- int ret = pipe(pipefd);
- assert(ret == 0); //防止编译器告警,意料之中的错误用assert,意料之外的错误用if
- (void)ret;
- pid_t id = fork();
- if(id < 0)
- {
- perror("fork");
- return 1;
- }
- if(id == 0)
- {
- //子进程写
- close(pipefd[0]);//关闭读端
- int n = 5;
- while(n--)
- {
- char buffer[MAX_SIZE];
- snprintf(buffer, sizeof(buffer),"child progress,pid: %d, n: %d\n",getpid(),n);
- write(pipefd[1], buffer,strlen(buffer));
- sleep(1);
- }
- exit(0);
- }
- else
- {
- //父进程读
- close(pipefd[1]);//关闭写端
- char buffer2[MAX_SIZE];
- while(true)
- {
- ssize_t n = read(pipefd[0],buffer2,sizeof(buffer2)-1);
- if(n > 0)
- {
- buffer2[n] = 0;
- std::cout << getpid() << ", child words: "<<buffer2 << std::endl;
- }
- else
- {
- break;
- }
- }
- }
- int status = 0;
- pid_t rid = waitpid(id, &status, 0);
- if(rid == id)
- {
- std::cout << "wait success" << std::endl;
- }
- return 0;
- }
复制代码
注:
- 系统调用的接口是C语言的,为了更好地顺应某些极端场景,可以使用C语言的接口,比方示例中使用了snprintf接口。
- sizeof()-1是为了传递字符串时预留一个\0,固然大部分场景也会预留\0,乃至字符串截断也会预留\0,但在某些场景还是要sizeof()-1。比方read,它不知道传进来的是二进制还是字符串还是其它类型。\0结尾是字符串的尺度,读写文件没有义务在数据后面预留\0,所以需要我们自己预留维护。
a. 管道的4种情况
- 正常情况,假如管道没有数据了,读端必须等待,直到有数据为止(写端写入数据)。
- 正常情况,假如管道被写满了,写端必须等待,直到有空间为止(读端读走数据)。
- 写端关闭,读端继承读取,它将读到管道中的全部数据,直到read返回值为0, 表示读到文件结尾。
- 读端关闭,写端写入时,OS会直接杀掉写端进程,通过向目的进程发送SIGPIPE(13)信号,停止写端进程。
b. 管道的5种特性
- 匿名管道,可以答应具有血缘关系的进程之间进行进程间通信。(父子、爷孙...)
- 匿名管道,默认给读写端要提供同步机制 --- 相识现象:读端和写端是顺序进行的,它们之间不会同时进行。
- 面向字节省 --- 现象:不关心数据的格式,只关心数据的巨细温顺序,按字节一次性将数据获取。管道可以传输任何类型的数据
- 管道的生命周期是随进程的。当创建管道的进程结束时,管道也随之消失。
- 管道是单向通信的,半双工通信的一种特别情况
补充:假如 read 成功读取数据,它会返回实际读取的字节数。假如 read 调用失败,它将返回 -1 并设置 errno 以指示错误。假如到达文件末尾,read 将返回 0。
比方下令: sleep 1000 | sleep 2000 | sleep 3000
操纵系统创建了3个进程,两个管道。
使用管道之后,原本向尺度输出输出的内容将重定向到管道文件中。原本从尺度输入获得的内容将重定向到从管道文件中获取。
4.2 基于匿名管道设计进程池
进程池的概念:
一个进程可以创建许多进程,通过管道与每个进程相连。正常情况,假如管道没有数据了,读端必须等待,直到有数据为止。这样就可以通过对特定的管道传输数据实现唤醒特定的进程。
创建进程会消耗时间和空间资源,假如要处置惩罚一个任务要等到任务来到时再处置惩罚,进行创建进程、分配资源,这样就有些延长时间,假如提前把进程创建好,等任务来到时让已经创建好的进程完成任务,这样就可以节省创建进程的时间。这些提前创建好的进程就叫做进程池。
补充内存池的概念:
调用系统调用是有成本的。调用自己的函数也有成本,所以才有了宏函数、内联函数。调用系统调用时操纵系统会做许多事情,好比申请内存,假如内存不足,操纵系统就要执行内存管理算法协调内存,开释、调解、置换挂起等等。一次性申请100MB内存比申请十次10MB内存服从更高。在C++尺度模板库(STL)中,有一个参数为内存设置器,它是一个模板类,用于指定用于存储容器元素的内存管理战略。它界说了怎样分配内存、怎样构造新元素、怎样开释内存以及怎样管理内存池等。在申请内存时它会额外多申请一部分,这样在需要扩容时就可以减少系统调用,这种多申请内存的方法就叫做内存池。
模仿实现进程池
Task.hpp如下:
- #pragma once
- #include <iostream>
- #include <vector>
- #include <unistd.h>
- #include <functional>
- #include <ctime>
- typedef std::function<void()> task_t;
- void Download()
- {
- std::cout << "我是一个下载任务"
- << " 处理者: " << getpid() << std::endl;
- }
- void PrintLog()
- {
- std::cout << "我是一个打印日志的任务"
- << " 处理者: " << getpid() << std::endl;
- }
- void PushVideoStream()
- {
- std::cout << "这是一个推送视频流的任务"
- << " 处理者: " << getpid() << std::endl;
- }
- class Init
- {
- public:
- // 任务码
- const static int g_download_code = 0;
- const static int g_printlog_code = 1;
- const static int g_push_videostream_code = 2;
- // 任务集合
- std::vector<task_t> tasks;
- public:
- Init()
- {
- tasks.push_back(Download);
- tasks.push_back(PrintLog);
- tasks.push_back(PushVideoStream);
- srand(time(nullptr) ^ getpid());
- }
- // 检查任务码
- bool CheckCode(int code)
- {
- if (code >= 0 && code < tasks.size())
- return true;
- else
- return false;
- }
- // 运行任务
- void RunTask(int code)
- {
- return tasks[code]();
- }
- // 随机选择任务
- int SelectTask()
- {
- return rand() % tasks.size();
- }
-
- // 描述任务码对应的任务名称
- std::string ToDesc(int code)
- {
- switch (code)
- {
- case g_download_code:
- return "Download";
- case g_printlog_code:
- return "PrintLog";
- case g_push_videostream_code:
- return "PushVideoStream";
- default:
- return "Unknow";
- }
- }
- };
- Init init;
复制代码 ProcessPool.cc如下:
- #include <iostream>
- #include <unistd.h>
- #include <string>
- #include <cassert>
- #include <vector>
- #include "Task.hpp"
- #include <sys/types.h>
- #include <sys/wait.h>
- static int number = 0; // 管道的编号
- const int count = 5; // 子进程和管道个数
- // 用来确定有哪些任务
- class Channel
- {
- public:
- Channel(int fd, pid_t workerid)
- : _fd(fd), _workerid(workerid)
- {
- _name = "channel: " + std::to_string(number++);
- }
- public:
- // 管道fd 子进程pid 管道名
- int _fd;
- pid_t _workerid;
- std::string _name;
- };
- void Work()
- {
- while (true)
- {
- int code = 0; // 用来规定buffer,读取必须是4个字节,得到任务码
- ssize_t n = read(0, &code, sizeof(code)); // 已经完成输入重定向
- // read读到数据长度n必须等于sizeof(code)
- if (n == sizeof(code)) // 读到正确的code
- {
- if (!init.CheckCode(code)) // 不合法直接continue
- continue;
- init.RunTask(code); // 合法,执行任务,相当于init.tasks[code]()
- }
- else if (n == 0) // 写端关闭,读端继续读取,它将读到管道中的所有数据,直到read返回值为0
- {
- break;
- }
- else
- {
- }
- }
- std::cout << "child quit" << std::endl;
- }
- void PrintFd(const std::vector<int> &fds)
- {
- std::cout << getpid() << " close fds: ";
- for (auto fd : fds)
- {
- std::cout << fd << " ";
- }
- std::cout << std::endl;
- }
- // 传参形式:
- // 1. 输入参数:const &
- // 2. 输出参数:*
- // 3. 输入输出参数:&
- void CreatChannel(std::vector<Channel> *c)
- {
- // bug
- // 父进程在不断创建管道时,创建第一个进程,父进程的信道写端已经在文件描述符里,
- // 再创建第二个管道和进程时,除了建立正常的通信信道以外,上一个信道在父进程的写端也会被下一个进程继承,
- // 再创建第三个管道和进程时,这个子进程的文件描述符表将包含指向三个信道。
- // 一直创建管道和进程,只有最后一个创建的管道只有一个写端指向,其它的管道都有多个写端指向。
- // 所以回收时要关闭全部信道写端再wait,如果close和wait同时进行,关闭信道写端从上往下关,关闭后还有无数个进程指向该信道,引用计数不为0,管道不释放,read读不到0,也就阻塞了
- std::vector<int> old;
- for (int i = 0; i < count; i++)
- {
- // 1. 定义并创建管道
- int pipefd[2];
- int n = pipe(pipefd);
- assert(n == 0);
- (void)n;
- // 2. 创建进程
- pid_t id = fork();
- assert(id != -1);
- // 3. 构建单向信道
- if (id == 0) // 子进程
- {
- if (!old.empty())
- {
- for (auto fd : old)
- {
- close(fd); // 把不属于自己的管道的写端关闭
- }
- PrintFd(old);
- }
- close(pipefd[1]);
- dup2(pipefd[0], 0); // 使用dup2后就不用给Work传参了,只用从标准输入拿数据即可
- Work();
- exit(0); // 会自动关闭自己打开的所有的fd
- }
- // 父进程
- close(pipefd[0]);
- c->push_back(Channel(pipefd[1], id)); // 之后对信道的增删查改就变成了对该vector的增删查改
- old.push_back(pipefd[1]); // 记录父进程的管道写端
- }
- }
- void SendCommand(const std::vector<Channel> &c, bool flag, int num = -1)
- {
- int pos = 0;
- while (true)
- {
- // 1. 选择任务,得到任务码,4字节
- int taskcode = init.SelectTask();
- // 2. 选择信道(进程),轮询或随机,较为平均地将任务给进程,要考虑子进程完成任务的负载均衡
- const auto &channel = c[pos++];
- pos %= c.size();
- // debug 查看任务发送给谁了
- std::cout << "send taskcode " << init.ToDesc(taskcode) << "[" << taskcode << "]"
- << " in "
- << channel._name << " worker is : " << channel._workerid << std::endl;
- // 3. 发送任务
- write(channel._fd, &taskcode, sizeof(taskcode));
- // 4. 判断是否退出
- if (!flag)
- {
- num--;
- if (num <= 0)
- break;
- }
- sleep(1);
- }
- std::cout << "SendCommand done..." << std::endl;
- }
- void ReleaseChannel(const std::vector<Channel> &c)
- {
- // 父进程退出了,与信道写端对应的文件描述符自动关闭
- // 写端关闭,读端继续读取,它将读到管道中的所有数据,直到read返回值为0
- for (const auto &channel : c)
- {
- close(channel._fd);
- waitpid(channel._workerid, nullptr, 0);
- }
- // for (const auto &channel : c)
- // {
- // pid_t rid = waitpid(channel._workerid, nullptr, 0);
- // if (rid == channel._workerid)
- // {
- // std::cout << "wait child: " << channel._workerid << " success" << std::endl;
- // }
- // }
- // 还有一种方法,不用使用old关闭不属于自己的写端:倒状回收
- // int pos = c.size();
- // for (; pos >= 0; pos--)
- // {
- // close(c[pos]._fd);
- // waitpid(c[pos]._workerid, nullptr, 0);
- // }
- }
- int main()
- {
- std::vector<Channel> channels;
- // 创建信道、创建进程
- CreatChannel(&channels);
- // 向不同的管道发送不同任务
- const bool g_always_loop = true;
- // SendCommand(channels,g_always_loop);
- SendCommand(channels, !g_always_loop, 10);
- // 回收资源,子进程退出、释放管道
- ReleaseChannel(channels);
- return 0;
- }
复制代码 4.3 定名管道
定名管道(也称为FIFO)在Linux中是一种特别的文件类型,它答应差别进程之间通过一个定名的管道进行通信。定名管道在文件系统中有一个可见的名称,可以像平凡文件一样访问,但它们的操纵方式与匿名管道差别。
定名管道是通过系统调用'mkfifo'创建的,可以用于不具有亲缘关系/绝不相关的进程进行进程间通信。它是一个文件,通常具有特定的扩展名(如'.fifo' 点表示匿名文件),但它实际上并不是文件系统中的平凡文件,而是一个特别的文件。
创建定名管道
- 定名管道可以从下令行上创建:
$ mkfifo filename
- 定名管道也可以从程序里创建:
- #include <sys/types.h>
- #include <sys/stat.h>
- int mkfifo(const char *pathname, mode_t mode);
- pathname是命名管道的路径名。
- mode是设置命名管道的权限模式,与open函数的mode参数类似。注意与umask的运算
- 成功返回0,失败返回-1。
复制代码 定名管道文件是创建出来的磁盘级的符号,实际在进行数据通信时,由于该文件是管道文件,被打开时数据也不会向磁盘刷新。定名管道文件有路径和文件名,因为路径是具有唯一性的,所以,我们可以使用路径+文件名,来唯一的让差别进程看到同一份资源!
创建名为filename的定名管道,使用ll下令,发现定名管道文件类型为p,即管道文件。
匿名管道与定名管道的区别
- 匿名管道由pipe函数创建并打开。
- 定名管道由mkfifo函数创建,打开用open
- FIFO(定名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式差别,一旦这些工作完成之后,它们具有相同的语义。(原理和特性一样)
4.4 用定名管道实现server&client通信
文件:comm.h client.cc server.cc Makefile
Makefile如下:
- .PHONY:all
- all:clientPipe serverPipe
- clientPipe:client.cc
- g++ -o $@ $^ -std=c++11
- serverPipe:server.cc
- g++ -o $@ $^ -std=c++11
- .PHONY:clean
- clean:
- rm -f clientPipe serverPipe
复制代码 comm.h如下:
- #pragma once
- #define FILENAME "fifo"
复制代码 client.cc如下:
- #include <iostream>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include "comm.h"
- #include <fcntl.h>
- #include <cstring>
- #include <unistd.h>
- #include <string>
- int main()
- {
- // 打开命名管道
- int fifo_wfd = open(FILENAME, O_WRONLY);
- if (fifo_wfd < 0)
- {
- std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
- exit(0);
- }
- std::cout << "open fifo success-------write" << std::endl;
- // 向管道写入数据
- std::string message;
- while (true)
- {
- std::cout << "Please Enter# ";
- std::getline(std::cin, message);
- ssize_t num = write(fifo_wfd, message.c_str(), message.size());
- if (num < 0)
- {
- std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
- break;
- }
- }
- close(fifo_wfd);
- std::cout << "close fifo success..." << std::endl;
- return 0;
- }
复制代码 server.cc 如下:
- #include <iostream>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include "comm.h"
- #include <fcntl.h>
- #include <cstring>
- #include <unistd.h>
- // 创建命名管道
- bool MakeFifo()
- {
- int n = mkfifo(FILENAME, 0666);
- if (n < 0)
- {
- std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
- return false;
- }
- std::cout << "mkfifo success-------read" << std::endl;
- return true;
- }
- int main()
- {
- Start:
- // 不管有没有管道,直接打开命名管道,有管道就会返回fifo_rfd
- int fifo_rfd = open(FILENAME, O_RDONLY);
- if (fifo_rfd < 0)//没有管道就创建,然后再次打开
- {
- std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
- if(MakeFifo()) goto Start;
- else return 1;
- }
- std::cout << "open fifo success-------read" << std::endl;
- // version 1 命名管道创建后再运行serverPipe会提示管道文件已存在
- // // 创建命名管道
- // int n = mkfifo(FILENAME, 0666);
- // if (n < 0)
- // {
- // std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
- // exit(0);
- // }
- // std::cout << "mkfifo success-------read" << std::endl;
- // // 打开命名管道
- // int fifo_rfd = open(FILENAME, O_RDONLY);
- // if (fifo_rfd < 0)
- // {
- // std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
- // exit(0);
- // }
- // std::cout << "open fifo success-------read" << std::endl;
- // 从管道读数据
- char buffer[1024];
- while (true)
- {
- ssize_t num = read(fifo_rfd, buffer, sizeof(buffer) - 1);
- if (num > 0)
- {
- buffer[num] = 0; // 或等于'\0'
- std::cout << "Client say: " << buffer << std::endl;
- }
- else if (num == 0)
- {
- std::cout << "client quit, server quit too!" << std::endl;
- break;
- }
- }
- close(fifo_rfd);
- std::cout << "close fifo success..." << std::endl;
- return 0;
- }
复制代码 五、system V共享内存
5.1 system V共享内存的引入
管道不是为了通信而专门设置的一套方案,而是为了通信复用了之前的代码。而实际上OS在通信时场景许多,只有一种通信方式是不够的,因此,操纵系统提供了多种IPC机制,包括但不限于:
- 管道(Pipe)和定名管道(FIFO):用于单向数据流通信。
- 消息队列(Message Queue):答应一个或多个进程向队列中写入消息,其他进程则可以读取队列中的消息。
- 信号量(Semaphore):用于同步进程间的访问共享资源。
- 共享内存(Shared Memory):答应多个进程共享一段内存地区,是最快的IPC方式,因为它不需要数据复制。
- 套接字(Socket):提供了在网络上的差别主机间进行通信的能力,也可以用于同一主机上的差别进程间通信。
System V共享内存是操纵系统中提供的一种IPC机制,它答应差别的进程访问同一块内存地区,从而实现数据共享。
共享内存区是最快的IPC情势。一旦这样的内存映射到共享它的进程的地点空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递相互的数据
5.2 共享内存的原理
- 共享内存答应多个进程共享一段内存地区,而且共享内存段是物理内存中的一部分。
- 在物理内存新增共享内存段时,要对共享内存段先形貌再构造,即使用struct shmid_ds形貌了共享内存段的属性,如巨细、访问权限、创建者信息等。通过链表进行对共享内存段的管理。
- 共享内存的创建是进程发起的。每个进程在Linux内核中都有一个task_struct结构来表示,这个结构包含了进程的全部信息,其中包括它的地点空间。地点空间被分为多个部分,包括代码段、数据段、堆、栈、共享区等。
- 每个进程都有自己的页表,通过页表可以将虚拟地点翻译成物理地点。在新增共享内存时,要在页表中进行映射,共享内存被映射到进程地点空间的共享区中,并向上层返回地点共享区的起始地点,使得进程可以通过地点空间,像访问自己的内存一样访问共享内存。
- 在使用System V共享内存时,每个共享内存段都有一个唯一的键(key),用于在进程间标识和访问共享内存段。内核使用这个键来查找或创建对应的struct shmid_ds。
OS中会存在许多进程,这些进行都有大概申请和使用共享内存,OS一定会答应系统中同时存在多个共享内存。共享内存,也要被操纵系统管理,管理的方法就是先形貌再构造,即上面讲到的struct shmid_ds结构体。但是上面的步骤只是一个进程创建共享内存,那么怎样保证第二个之后的参与通信的进程,看到的就是同一个共享内存呢?
注意,进程不能直接给另一个进程直接传值,因为假如这样就说明已经能通信了,就不需要共享内存来传递消息了。所以进程不能将key传给另一个进程。方法:提前进行约定,让使用同一块共享内存的进程使用相同的key,这个key可以用户自己界说,也可以使用库方法,只要保证key唯一即可。
5.3 共享内存函数
shmget函数:既能创建也能获取
shmget函数用于创建一个新的共享内存段大概获取一个已经存在的共享内存段的标识符。
原型:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数:
- key:一个键值,用于唯一标识共享内存段。在创建共享内存时就将key加载到其中。
- size:共享内存的巨细。
- shmflg:一个标志位,用于控制共享内存的创建和访问权限。(使用方法雷同open的flags标志位)
返回值:
成功时,返回共享内存段的标识符shmid;失败时,返回-1。
补充:
1. shmflg参数中常用的标志位
- IPC_CREAT:假如这个标志位被设置,而且共享内存段不存在,那么shmget函数会创建一个新的共享内存段。假如共享内存段已经存在,shmget会返回已存在的共享内存段的标识符。
- IPC_EXCL:这个标志位必须与IPC_CREAT标志位一起使用。假如IPC_CREAT和IPC_EXCL都被设置,而且共享内存段不存在,shmget函数会创建一个新的共享内存段。假如共享内存段已经存在,shmget函数会失败,并返回-1。用来保证共享内存段是新创建的。
- mode:这个值通常作为shmflg参数的低位部分,它表示共享内存段的权限模式。比方0666。
- 示例:int shmid = shmget(11223344, 4096, IPC_CREAT | 0666);
2. ftok函数来生成一个键值
- #include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
- 用户可以通过ftok函数来生成一个键值,这个键值通常基于一个路径名和一个项目ID。ftok函数返回一个整数,这个整数就是用于shmget函数的key参数。
- 示例:
key_t key;
key = ftok("path/to/file", 1); // "path/to/file"是文件路径,1是项目ID
int shmid = shmget(key, 4096, IPC_CREAT | 0666);
- 因为用户界说的key不容易保证唯一性,所以使用ftok函数获取key。(相同的参数相同的算法,最终得到相同的值)
注意:
- key和shmid的区别
key是操纵系统用来区分共享内存段的,shmid是用户用来进行对共享内存段的操纵的。下面的shmat、shmctl都是使用shmid来对指定的共享内存段操纵。包括下令行指令也是通过shmid进行操纵。
- 共享内存(IPC资源)的生命周期是随内核的!共享内存需要用户主动开释,除非重启OS
ipcs -m shmid 下令,检察有多少共享内存
ipcrm -m 下令,删除指定的共享内存
shmat函数:at->attach建立关联
shmat函数用于将共享内存段连接到进程的地点空间。
原型:
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
- shmid:共享内存段的标识符。
- shmaddr:指定连接的地点,假如为NULL,内核将自动选择地点。
- shmflg:连接标志,可以指定读写权限等。
返回值:
成功时,返回指向共享内存的指针,即映射到地点空间的起始虚拟地点;失败时,返回-1。
说明:
- shmaddr为NULL,焦点自动选择一个地点
- shmaddr不为NULL且shmflg无SHM_RND标志,则以shmaddr为连接地点。
- shmaddr不为NULL且shmflg设置了SHM_RND标志,则连接的地点会自动向下调解为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
- shmflg=SHM_RDONLY,表示连接操纵用来只读共享内存
shmdt函数:dt->detach去关联
shmdt函数用于将共享内存段与当前进程的地点空间脱离,即解除映射。
原型:
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
参数:
返回值:
成功时,返回0;失败时,返回-1。
注意:将共享内存段与当前进程脱离不等于删除共享内存段。只是将页表中与共享内存段的映射清空。
什么时间删除共享内存?
struct shmid_ds中有shm_nattch字段,它是一个引用计数器,表示有多少个进程正在使用这个共享内存段。当一个进程使用shmat函数将共享内存段映射到自己的地点空间时,shm_nattch的值会增加;当进程使用shmdt函数将共享内存段从自己的地点空间脱离时,shm_nattch的值会减少。
当shm_nattch的值降至0时,意味着没有进程在使用这个共享内存段。在这种情况下,内核会考虑删除共享内存段,但还需要满足其他条件,好比共享内存段没有被其他进程以只读方式映射。只有当全部使用该共享内存段的进程都调用了shmdt函数后,操纵系统才会删除共享内存段。
shmctl函数:ctl->control
shmctl函数用于控制共享内存,如删除共享内存段、改变共享内存的权限等。
原型:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
- shmid:共享内存段的标识符。
- cmd:将要采取的动作,如删除(IPC_RMID)、改变权限(IPC_SET)等。
IPC_STAT:获取共享内存段的当前状态,并将其存储在buf指向的struct shmid_ds结构体中。
IPC_SET:设置共享内存段的当前状态,并从buf指向的struct shmid_ds结构体中读取信息。
IPC_RMID:删除共享内存段,开释系统资源。(remove id 或remove immediately)
- buf:指向一个'struct shmid_ds'结构体,该结构体包含共享内存的属性信息。
返回值:
成功时,返回0;失败时,返回-1。
5.4 使用共享内存的步骤
- 生成key:通过ftok函数来生成一个键值,基于一个路径名和一个项目ID。
- 创建共享内存段:使用shmget函数,指定key和共享内存的巨细及其他属性来创建一个新的共享内存段大概获取一个已经存在的共享内存段的标识符。内核会创建一个struct shmid_ds来形貌这个共享内存段,并在文件系统中创建一个对应的特别文件。
- 映射共享内存段:使用shmat函数,将共享内存段映射到进程的地点空间中。内核会更新进程的页表,将共享内存的虚拟地点映射到物理内存的页面。
- 访问共享内存:进程可以使用指针操纵来读取和写入共享内存中的数据。当进程访问共享内存时,它的页表会将虚拟地点翻译成物理地点,从而访问共享内存的物理页面。
- 解除映射:当进程完成共享内存的使用后,应该使用shmdt函数来解除映射。内核会更新进程的页表,取消共享内存的虚拟地点到物理地点的映射。
- 删除共享内存段:假如共享内存不再需要,可以使用shmctl函数来标志删除。内核会删除对应的struct shmid_ds,并在文件系统中删除对应的特别文件。
5.5 基于共享内存的进程间通信示例
文件:comm.hpp client.cc server.cc Makefile
Makefile如下:
- .PHONY:all
- all:clientPipe serverPipe
- clientPipe:client.cc
- g++ -o $@ $^ -std=c++11
- serverPipe:server.cc
- g++ -o $@ $^ -std=c++11
- .PHONY:clean
- clean:
- rm -f clientPipe serverPipe
复制代码 comm.hpp如下:
- #pragma once
- #include <iostream>
- #include <sys/ipc.h>
- #include <sys/shm.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <cstring>
- #include <unistd.h>
- const char *pathname = "/home/zzx/2024/0604/shm";
- const int projectID = 1111; // 项目ID
- const int Size = 4096; // 文件大小
- const char *filename = "fifo"; // 命名管道
- key_t GetKey()
- {
- return ftok(pathname, projectID);
- }
- // int CreateShm(key_t key)
- // {
- // int shmid = shmget(key, Size, IPC_CREAT | 0666);
- // if(shmid < 0)
- // {
- // std::cerr << "errno" << errno << ",errnostring: " << strerror(errno) << endl;
- // exit(2);
- // }
- // return shmid;
- // }
- int __CreateOrGetShm(key_t key, int flag)
- {
- int shmid = shmget(key, Size, flag);
- if (shmid < 0)
- {
- std::cerr << "errno" << errno << ",errnostring: " << strerror(errno) << std::endl;
- exit(2);
- }
- return shmid;
- }
- int CreateShm(key_t key)
- {
- return __CreateOrGetShm(key, IPC_CREAT | IPC_EXCL | 0666);
- }
- int GetShm(key_t key)
- {
- return __CreateOrGetShm(key, IPC_CREAT /*0也可以*/);
- }
- bool MakeFifo()
- {
- int n = mkfifo(filename, 0666);
- if (n < 0)
- {
- std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
- return false;
- }
- std::cout << "mkfifo success... read" << std::endl;
- return true;
- }
复制代码 client.cc如下:
- #include "comm.hpp"
- int main()
- {
- // 使用共享内存
- key_t key = GetKey();
- int shmid = GetShm(key);
- std::cout << "GetShm success --- client" << std::endl;
- char* shmaddr = (char*)shmat(shmid, nullptr,0);
- std::cout << "attach success --- client" << std::endl;
- int fd = open(filename,O_WRONLY);
- char c = 'a';
- while (c < 'z')
- {
- shmaddr[c-'a'] = c;
- std::cout << "write: " << shmaddr << std::endl;
- sleep(1);
- int code = 1;//只是通知作用,用来同步
- write(fd,&code,sizeof(code));
- c++;
- }
- shmdt(shmaddr);
- close(fd);
- return 0;
- }
复制代码 server.cc如下:
- #include "comm.hpp"
- class Init
- {
- public:
- Init()
- {
- // 创建管道文件,复用同步机制
- bool r = MakeFifo();
- if (!r)
- return;
- // 创建共享内存
- key_t key = GetKey();
- shmid = CreateShm(key); // 封装了底层接口,其它函数也可以这样实现,在此不作实现
- std::cout << "CreateShm success --- server" << std::endl;
- // 与进程地址空间进行关联
- shmaddr = (char *)shmat(shmid, nullptr, 0);
- std::cout << "shmat success --- server" << std::endl;
- fd = open(filename, O_RDONLY);
- }
- ~Init()
- {
- // 与进程地址空间去关联
- shmdt(shmaddr);
- std::cout << "shmdt success --- server" << std::endl;
- // 删除共享内存
- shmctl(shmid, IPC_RMID, nullptr);
- std::cout << "shmctl success --- server" << std::endl;
- }
- public:
- int fd;
- int shmid;
- char *shmaddr;
- };
- int main()
- {
- Init init;
- while (true)
- {
- int code = 0;
- ssize_t n = read(init.fd, &code, sizeof(code));
- if (n > 0)
- {
- std::cout << "共享内存的内容:" << init.shmaddr << std::endl;
- }
- else if (n == 0)
- {
- break;
- }
- }
- return 0;
- }
复制代码 5.6 共享内存的特点
- 共享内存的通信方式,不会提供同步机制,共享内存是直接裸露给全部的使用者的,一定要注意共享内存的使用安全题目。
- 共享内存是全部进程间通信,速度最快的。
- 共享内存可以提供较大的空间
共享内存通信速度快是因为它减少了数据拷贝次数。在使用管道传递数据时要先创建管道,然后差别端向管道写入或读取数据,调用write或read等系统调用。在计算机中,凡是数据迁移,都是对数据的拷贝。用户通过进程A将数据写到管道,进程B从管道读出数据写入体现器,用户把数据传给进程A,进程B把数据打印到体现器文件也都用到了拷贝,拷贝也有代价。
使用共享内存,用户把数据传给进程A,就直接传到了共享内存中,数据一旦进入共享内存,进程B立即就能知道(因为没有同步机制),进程B直接共享区数据传给体现器,中心就至少减少两次系统调用(write, read)。
简而言之,在传统的IPC机制中,如管道,数据需要颠末以下步骤:
- 用户空间到内核空间:用户通过系统调用(如write)将数据从用户空间拷贝到内核空间。
- 内核空间到内核空间:数据在内核空间之间传递,大概需要通过网络堆栈、文件系统等。
- 内核空间到用户空间:数据从内核空间拷贝到用户空间,通过系统调用(如read)被进程读取
这个过程涉及了多次数据拷贝,而且每次拷贝都会带来一定的开销。
相比之下,共享内存通信的过程是这样的:
- 用户空间到共享内存:用户进程将数据写入共享内存。
- 共享内存到用户空间:另一个进程从共享内存中读取数据。
在这个过程中,只有两次数据拷贝
5.7 共享内存数据结构
上面讲到的shmid_ds结构体,包括buf参数也使用一个指向shmid_ds结构的指针,shmid_ds结构体在<sys/shm.h>中界说如下:
- struct shmid_ds {
- struct ipc_perm shm_perm; /* Ownership and permissions */
- size_t shm_segsz; /* Size of segment (bytes) */
- time_t shm_atime; /* Last attach time */
- time_t shm_dtime; /* Last detach time */
- time_t shm_ctime; /* Last change time */
- pid_t shm_cpid; /* PID of creator */
- pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
- shmatt_t shm_nattch; /* No. of current attaches */
- ...
- };
复制代码 ipc_perm结构界说如下(突出体现的字段可以使用IPC_SET设置):
- struct ipc_perm {
- key_t __key; /* Key supplied to shmget(2) */
- uid_t uid; /* Effective UID of owner */
- gid_t gid; /* Effective GID of owner */
- uid_t cuid; /* Effective UID of creator */
- gid_t cgid; /* Effective GID of creator */
- unsigned short mode; /* Permissions + SHM_DEST and
- SHM_LOCKED flags */
- unsigned short __seq; /* Sequence number */
- };
复制代码 从中可以看到shmid_ds结构体的首元素是一个结构体ipc_perm,它包含创建共享内存段时提供的键值。
要想相识shmid_ds和ipc_perm就要介绍一下system V消息队列和system V信号量
六、简述system V消息队列和system V信号量
6.1 system V消息队列
消息队列的特性:
- 消息队列提供了一个从一个进程向另外一个进程发送一个数据块的方法。这个数据块也叫消息。
- 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有差别的类型值。接收者进程可以指定它只接收特定类型的消息。这答应差别的消息可以同时存在于队列中,而不需要接收者知道队列中有哪些类型的消息。比方进程A要求进程B能看到,类型就设置为B。
- 每个消息队列都有一个唯一的标识符msqid,用于在系统中标识和访问该队列。
- 与共享内存段雷同,消息队列也可以通过键来标识,用于在系统中唯一标识消息队列。
- 与System V的其他IPC资源一样,消息队列需要显式地删除,否则不会自动清除,除非重启,所以system V 消息队列资源的生命周期随内核。
- 系统中可以同时存在多个消息队列,消息队列在内核中管理,也要先形貌,再构造,因此消息队列=队列+队列的属性。
System V消息队列函数:
msgget:创建或获取一个消息队列标识符。
原型:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
参数:
- key:用于标识消息队列的键值,可以是一个已存在的键值大概通过ftok函数生成的键值。
- msgflg:标志位,用于控制消息队列的创建和访问权限。
返回值:成功时返回消息队列标识符,失败时返回-1。
注意:msgflg参数可以设置权限标志,如IPC_CREAT(创建消息队列)、IPC_EXCL(创建时检查消息队列是否存在)等。用法与System V共享内存shmget函数的shmflg参数相同。
msgctl:控制消息队列。
原型:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数:
- msqid:消息队列标识符。
- cmd:操纵下令,如IPC_STAT(获取消息队列状态)、IPC_SET(设置消息队列属性)、IPC_RMID(删除消息队列)等。
- buf:指向struct msqid_ds的指针,用于存储消息队列的状态信息。
返回值:成功时返回0,失败时返回-1。
msgsnd:向消息队列发送消息。
原型:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数:
- msqid:消息队列标识符。
- msgp:指向消息的指针。
- msgsz:消息的巨细。
- msgflg:标志位,用于控制发送操纵的行为。
返回值:成功时返回0或消息巨细,失败时返回-1。
msgrcv:从消息队列接收消息。
原型:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数:
- msqid:消息队列标识符。
- msgp:指向接收消息缓冲区的指针。
- msgsz:接收缓冲区的巨细。
- msgtyp:接收消息的类型值。
- msgflg:标志位,用于控制接收操纵的行为。
返回值:成功时返回接收到的消息巨细,失败时返回-1。
msgsnd和msgrcv函数的msgflg参数可以设置壅闭标志,如MSG_EXCEPT(接收除指定类型外的消息)、MSG_NOERROR(假如接收消息失败,返回-1而不是设置错误码)等。
msqid_ds数据结构界说如下:
- struct msqid_ds {
- struct ipc_perm msg_perm; /* Ownership and permissions */
- time_t msg_stime; /* Time of last msgsnd(2) */
- time_t msg_rtime; /* Time of last msgrcv(2) */
- time_t msg_ctime; /* Time of last change */
- unsigned long __msg_cbytes; /* Current number of bytes in
- queue (nonstandard) */
- msgqnum_t msg_qnum; /* Current number of messages
- in queue */
- msglen_t msg_qbytes; /* Maximum number of bytes
- allowed in queue */
- pid_t msg_lspid; /* PID of last msgsnd(2) */
- pid_t msg_lrpid; /* PID of last msgrcv(2) */
- };
复制代码 ipc_perm结构界说如下:
- struct ipc_perm {
- key_t __key; /* Key supplied to msgget(2) */
- uid_t uid; /* Effective UID of owner */
- gid_t gid; /* Effective GID of owner */
- uid_t cuid; /* Effective UID of creator */
- gid_t cgid; /* Effective GID of creator */
- unsigned short mode; /* Permissions */
- unsigned short __seq; /* Sequence number */
- };
复制代码 6.2 system V信号量
System V信号量函数也有semget、semctl、semop函数,在此不讲述。它们用法也和共享内存、消息队列雷同,因为都是system V系列的。
semid_ds结构体界说如下:
- struct semid_ds {
- struct ipc_perm sem_perm; /* Ownership and permissions */
- time_t sem_otime; /* Last semop time */
- time_t sem_ctime; /* Last change time */
- unsigned long sem_nsems; /* No. of semaphores in set */
- };
复制代码 ipc_perm界说如下:
- struct ipc_perm {
- key_t __key; /* Key supplied to semget(2) */
- uid_t uid; /* Effective UID of owner */
- gid_t gid; /* Effective GID of owner */
- uid_t cuid; /* Effective UID of creator */
- gid_t cgid; /* Effective GID of creator */
- unsigned short mode; /* Permissions */
- unsigned short __seq; /* Sequence number */
- };
复制代码 信号量的本质是一组计数器。信号量主要用于同步和互斥。
为了让进程间能够通信,就要让多个进程看到同一份资源,这份资源称为公共资源,使用公共资源就大概导致并发访问、数据不一致题目,比方读的时间另一个进程读、读的时间另一个进行写、写的时间。。。所以就需要在一个进程使用资源的时间,将这份资源保护起来,全部进程按顺序使用,这就是互斥和同步。
互斥:任何一个时间只答应一个执行流(进程)访问公共资源,(加锁实现的)
同步:多个执行流执行时,按照一定的顺序执行。
临界资源:被保护起来的公共资源。(不是临界资源的就是非临界资源)
临界区:访问该临界区的代码。(维护临界资源就是维护临界区)
原子性:只有两态,要么没做,要么做完。
好比在电影院买票,电影院和内部座位就是多人共享的资源 --- 公共资源(大概被拆分为多份资源)。我们买票的本质:是对资源的预订机制。可以看成,电影院有一个计数器用来表示公共资源的个数。别人买票时要先看计数器内另有没有剩余的座位,有的话就分配,计数器--,没有就让那人等着。
假如公共资源没有被拆分只有一份,用二元信号量int sem =1表示互斥锁来完成互斥功能,在临界区前面和后面加上维护代码,检测sem是否有剩余,假如有剩余就答应继承临界区的代码、sem--,没有剩余就继承等待,直至有一个临界区完成并sem++。其实这个信号量也可以看作一个结构体,内里有一个计数器和一个等待队列,没有剩余就将进程放入等待队列中,知道有一个sem++,就执行等待队列的下一个进程。
信号量:表示资源数量的计数器,每一个执行流想访问公共资源内的某一份资源,不应该让执行流直接访问,而是先申请信号量资源,其实就是先对信号量计数器进行--操纵。本质上,只要--成功,完成了对资源的预订机制,假如申请不成功,执行流被挂起壅闭。
七、回首共享内存数据结构
在看到共享内存、消息队列和信号量的数据结构后,发现它们都使用了ipc_perm结构体,而且都是位于对应数据结构的第一个,这是因为在底层中,在系统层面有一个类型为kern_ipc_perm *p[0]的柔性指针数组,通过该数组管理全部的IPC资源。比方创建一个共享内存的数据结构shmid_ds,在柔性指针数组中加上对应的ipc_perm结构体的地点,将来对shmid_ds进行管理时,由于ipc_perm结构体是shmid_ds第一个元素,所以只需要对它进行类型转换,就可以酿成shmid_ds的地点,就可以对它的数据成员进行操纵。比方(shmid_ds *)p[1] 、 (msqid_ds*)p[2]。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |