做软装搭配的网站,重庆门户网站排名,做网站需要写代码,wordpress多板块目录
1、进程间通信介绍 进程间通信的概念 进程间通信的本质 进程间通信的分类
2、管道 2.1、什么是管道 2.2、匿名管道 匿名管道的原理 pipe函数 匿名管道使用步骤 2.3、管道的读写规则 2.4、管道的特点 2.5、命名管道 命名管道的原理 使用命令创建命名管道 mkfifo创建命名管…目录
1、进程间通信介绍 进程间通信的概念 进程间通信的本质 进程间通信的分类
2、管道 2.1、什么是管道 2.2、匿名管道 匿名管道的原理 pipe函数 匿名管道使用步骤 2.3、管道的读写规则 2.4、管道的特点 2.5、命名管道 命名管道的原理 使用命令创建命名管道 mkfifo创建命名管道 用命名管道实现server client间的通信 2.6、匿名管道和命名管道的区别
3、system V共享内存 3.1、共享内存的原理 3.2、共享内存的数据结构 3.3、共享内存函数 shmget创建共享内存 shmctl释放共享内存 shmat关联共享内存 shmdt去关联共享内存 3.4、用共享内存实现serveclient通信 3.5、共享内存与管道进行对比
4、system V IPC联系 临界资源、临界区、原子性、互斥 信号量 IPC资源 1、进程间通信介绍
进程间通信的概念 进程间通信IPCInterprocess communication是一组编程接口让程序员能够协调不同的进程使之能在一个操作系统里同时运行并相互传递、交换信息。 这使得一个程序能够在同一时间里处理许多用户的要求。 因为即使只有一个用户发出要求也可能导致一个操作系统中多个进程的运行进程之间必须互相通话。 进程间通信的目的 数据传输一个进程需要将它的数据发送给另一个进程资源共享多个进程之间共享同样的资源。通知事件一个进程需要向另一个或一组进程发送消息通知它它们发生了某种事件如进程终止时要通知父进程。进程控制有些进程希望完全控制另一个进程的执行如Debug进程此时控制进程希望能够拦截另一个进程的所有陷入和异常并能够及时知道它的状态改变。进程间通信的本质 进程间通信的本质让不同的进程看到同一份资源内存文件内核缓冲等 由于各个运行进程之间具有独立性这个独立性主要体现在数据层面而代码逻辑层面可以私有也可以公有例如父子进程因此各个进程之间要实现通信是非常困难的。各个进程之间若想实现通信一定要借助第三方资源这些进程就可以通过向这个第三方资源写入或是读取数据进而实现进程之间的通信这个第三方资源实际上就是操作系统提供的一段内存区域。 因此进程间通信的本质就是让不同的进程看到同一份资源内存文件内核缓冲等。 由于这份资源可以由操作系统中的不同模块提供因此出现了不同的进程间通信方式。 进程间通信的分类 管道 匿名管道pipe命名管道System V IPC System V 消息队列System V 共享内存System V 信号量POSIX IPC 消息队列共享内存信号量互斥量条件变量读写锁2、管道 通信之前要让不同的进程看到同一份资源文件内存块。我下面谈的进程间通信不是告诉我们如何通信而是如何让这两个进程先看到同一份资源。因为资源的不同会决定不同种类的通信方式而管道是提供共享资源的一种手段。 如下假设内存中有进程1和进程2这俩进程想要通信磁盘上有一个文件当这俩进程都把这个文件打开的时候此时就建立了进程1和进程2的一份公共文件示例 上述就是一个简单的进程间通信的小测试但是我们尽量不要把通信的方式放到外设上效率低而是内存级的通信。 2.1、什么是管道 管道是Unix中最古老的进程间通信的形式我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。 例如我们统计当前使用云服务器上的登录用户个数 其中who命令和wc命令都是两个程序当它们运行起来后就变成了两个进程who进程通过标准输出将数据打到“管道”当中wc进程再通过标准输入从“管道”当中读取数据至此便完成了数据的传输进而完成数据的进一步加工处理。 注意who命令用于查看当前云服务器的登录用户一行显示一个用户wc -l用于统计当前的行数。现实中的管道是单向的并且是传输资源的。进程间通信中的管道也是单向的并且是传输数据的。当要写入的数据量不大于PIPE_BUF一般是4096字节时linux将保证写入的原子性。当要写入的数据量大于PIPE_BUF时linux将不再保证写入的原子性。 2.2、匿名管道
匿名管道的原理 进程间通信的本质就是让不同的进程看到同一份资源使用匿名管道实现父子进程间通信的原理就是让两个父子进程先看到同一份被打开的文件资源然后父子进程就可以对该文件进行写入或是读取操作进而实现父子进程间通信。 解释上图 我们都清楚一个进程会维护一个文件指针数组默认打开012。磁盘要把信息加载到内存上struct file这个结构体里这里面包含了文件的所有属性有操作方法和file自己的内部缓冲区当打开一个文件的时候进程给它分配文件描述符为3并指向此struct file。此时fork创建子进程struct file文件不会被拷贝一份因为创建进程和文件没有关系而files_struct会被拷贝因为它属于进程并拷贝了文件描述符的内容曾经父进程所打开的文件子进程也拷贝下来了此映射关系此时父子进程指向同一份文件这个文件就是这俩进程的同一份资源所以我们设计的时候是普通文件就往磁盘上写如果是管道文件就直接往缓冲区写不要往磁盘上刷新了。pipe函数 pipe函数用于创建匿名管道pipe的函数原型如下 int pipe(int pipefd[2]); pipe函数的参数是一个输出型参数数组pipefd用于返回两个指向管道读端和写端的文件描述符 数组元素含义pipefd[0]管道读端的文件描述符pipefd[1]管道写端的文件描述符pipe函数调用成功时返回0调用失败返回-1。 匿名管道使用步骤 在创建匿名管道实现父子进程间通信的过程中需要pipe函数和fork函数搭配使用具体步骤如下 1、父进程调用pipe函数创建管道 2、父进程fork创建子进程 3、父进程关闭写端子进程关闭读端 注意 管道只能够进行单向通信因此当父进程创建完子进程后需要确认父子进程谁读谁写然后关闭相应的读写端。从管道写端写入的数据会被内核缓冲直到从管道的读端被读取。现在站在文件描述符的角度来看看这三个步骤 1、父进程调用pipe函数创建管道 2、父进程创建子进程 3、父进程关闭写端子进程关闭读端 问1为什么父进程要用两个文件描述符分别打开读端和写端 为了让子进程继承让子进程不用再打开读和写了。问2为什么父子要关闭对应的读写端 管道必须是单向通信的。问3谁来决定父子进程关闭什么读写 不是由管道本身决定的而是由我们自己的需求决定的。先来测试一下 #includeiostream
#includecstdio
#includeunistd.h
using namespace std;
int main()
{int pipefd[2] { 0 };if (pipe(pipefd) ! 0){cerr pipe error endl;return 1;}cout fd[0]: pipefd[0] endl;cout fd[1]: pipefd[1] endl;return 0;
} 现在我们来实现一个管道 #includeiostream
#includecstdio
#includeunistd.h
#includecstring
#includestring
#includectime
#includesys/wait.h
#includesys/types.h
using namespace std;
int main()
{//1、创建管道int pipefd[2] { 0 };if (pipe(pipefd) ! 0){cerr pipe error endl;return 1;}//2、创建子进程pid_t id fork();if (id 0){cerr fork error endl;return 2;}else if (id 0){//child//子进程进行读取子进程就应该关闭写端close(pipefd[1]);#define NUM 1024char buffer[NUM];while (true){cout 时间戳 (uint64_t)time(nullptr) endl;memset(buffer, 0, sizeof(buffer));ssize_t s read(pipefd[0], buffer, sizeof(buffer) - 1);if (s 0){//读取成功buffer[s] \0;cout 子进程收到消息内容是 buffer endl;}else if (s 0){cout 父进程写完了子进程我也退出啦 endl;break;}else{//Do Nothing}}close(pipefd[0]);exit(0);}else {//parent//父进程进行写入父进程就应该关闭读端close(pipefd[0]);const char* msg 你好子进程我是父进程这次发送的信息编号是: ;int cnt 0;while (cnt 5){char sendBuffer[1024];sprintf(sendBuffer, %s : %d, msg, cnt);write(pipefd[1], sendBuffer, strlen(sendBuffer));sleep(2); //这里是为了一会儿看现象明显cnt;}close(pipefd[1]);cout 父进程写完了 endl;}pid_t res waitpid(id, nullptr, 0);if (res 0){cout 等待子进程成功 endl;}return 0;
} 问1当父进程关闭管道的写端子进程是怎么知道父进程关了的并且后面还把数据读完就关了呢 父进程创建了子进程之后在文件对应的属性中有引用计数表示有多少个指针指向改进程因此如果引用计数为1说明该进程只要一个人在用因此只要这个人读完就代表文件结束了。问2父进程是每隔2秒sleep一次那为什么子进程没有进行sleep但读取节奏却和父进程一样呢 因为当父进程没有写入数据的时候子进程在等待。所以当父进程写入之后子进程才能read会返回到数据子进程打印读取数据要以父进程的节奏为主。因此父进程和子进程在读写的时候是有一定的顺序性的。 管道内部没有数据的时候reader就必须阻塞等待将当前进程的take_struct放入等待队列中等待管道有数据如果数据被写满writer就必须阻塞等待等待管道中有空间。不过呢在父子进程各自printf的时候向显示器写入【显示器也是文件】并没有什么顺序因为缺乏访问控制。而管道内部是有顺序的因为它自带访问控制机制同步和互斥机制。再来实现一个进程控制让父进程通过管道控制子进程让子进程去做事情 #include iostream
#include cstdio
#include unistd.h
#include cstring
#include string
#include vector
#include unordered_map
#include ctime
#include cstdlib
#include sys/wait.h
#include sys/types.h
#include cassert
using namespace std;typedef void (*functor)();
vectorfunctor functors; // 方法集合// for debug
unordered_mapuint32_t, string info;
void f1()
{cout 这是一个处理日志的任务执行的进程 ID [ getpid() ] 执行时间是[ time(nullptr) ] endl;
}
void f2()
{cout 这是一个备份数据的任务执行的进程 ID [ getpid() ] 执行时间是[ time(nullptr) ] endl;
}
void f3()
{cout 这是一个处理网络连接的任务执行的进程 ID [ getpid() ] 执行时间是[ time(nullptr) ] endl;
}void loadFunctor()
{info.insert({functors.size(), 处理日志的任务});functors.push_back(f1);info.insert({functors.size(), 备份数据的任务});functors.push_back(f2);info.insert({functors.size(), 处理网络连接的任务});functors.push_back(f3);
}int main()
{// 0、加载任务列表loadFunctor();// 1、创建管道int pipefd[2] {0};if (pipe(pipefd) ! 0){cerr pipe error endl;return 1;}// 2、创建子进程pid_t id fork();if (id 0){cerr fork error endl;return 2;}else if (id 0){// 3、关闭不需要的文件fd// child —— readclose(pipefd[1]);// 4、业务处理while (true){uint32_t operatorType 0;//如果有数据就读取如果没有数据就阻塞等待等待任务的到来ssize_t s read(pipefd[0], operatorType, sizeof(uint32_t));if (s 0){cout 我要退出啦我是给人打工的老板都走了. . . endl;break;}assert(s sizeof(uint32_t));//assert断言编译有效 debug模式//release 模式断言就没有了//一旦断言没有了s变量就是只被定义了没有被使用。release模式中可能会有warning(void)s;if (operatorType functors.size()){functors[operatorType]();}else{cerr bug? operatorType operatorType endl;;}}close(pipefd[0]);exit(0);}else{srand((long long)time(nullptr));// 3、关闭不需要的文件fd// parent —— writeclose(pipefd[0]);// 4、指派任务int num functors.size();int cnt 10;while (cnt--){// 5、形成任务码uint32_t commandCode rand() % num;cout 父进程指派任务完成任务是 info[commandCode] 任务的编号是 cnt endl;// 向指定的进程下达任务的操作write(pipefd[1], commandCode, sizeof(uint32_t));sleep(1);}close(pipefd[1]);pid_t res waitpid(id, nullptr, 0);if (res)cout wait success endl;}return 0;
} 上述操作实现了让父进程去控制一个进程那么如何让父进程控制一批进程呢 假设现在想让父进程控制3个进程那么我可以创建三个管道让每一个进程都和父进程建立管道父进程向指派进程1那就往进程1的管道去写向指派进程2就往进程2的管道去写……。此时我的主进程就相当于分配业务的进程这些子进程就相当于是执行业务的进程。我们基于上述的策略其实就是利用的进程池。 代码如下 #include iostream
#include cstdio
#include unistd.h
#include cstring
#include string
#include vector
#include unordered_map
#include ctime
#include cstdlib
#include sys/wait.h
#include sys/types.h
#include cassert
using namespace std;typedef void (*functor)();
vectorfunctor functors; // 方法集合// for debug
unordered_mapuint32_t, string info;
void f1()
{cout 这是一个处理日志的任务执行的进程 ID [ getpid() ] 执行时间是[ time(nullptr) ]\n endl;
}
void f2()
{cout 这是一个备份数据的任务执行的进程 ID [ getpid() ] 执行时间是[ time(nullptr) ]\n endl;
}
void f3()
{cout 这是一个处理网络连接的任务执行的进程 ID [ getpid() ] 执行时间是[ time(nullptr) ]\n endl;
}void loadFunctor()
{info.insert({functors.size(), 处理日志的任务});functors.push_back(f1);info.insert({functors.size(), 备份数据的任务});functors.push_back(f2);info.insert({functors.size(), 处理网络连接的任务});functors.push_back(f3);
}//int32_t: 进程pidint32_t: 该进程对应的管道写端fd
typedef pairint32_t, int32_t elem;
int processNum 5;//设置子进程的个数为5void work(int blockFd)
{cout 进程[ getpid() ] 开始工作 endl;//子进程核心工作的代码while (true){//a.阻塞等待 b.获取任务信息uint32_t operatorCode 0;ssize_t s read(blockFd, operatorCode, sizeof(uint32_t));if (s 0) break;assert(s sizeof(uint32_t));(void)s;//c.处理任务if (operatorCode functors.size()){functors[operatorCode]();}}cout 进程[ getpid() ] 结束工作 endl;
}//pair的结构[子进程的pid子进程的管道fd]
void blanceSendTask(const vectorelem processFds)
{srand((long long)time(nullptr));while (true){sleep(1);//选择一个进程选择进程是随机的这里没有压着一个进程给任务//较为均匀的将任务给所有的子进程 —— 负载均衡uint32_t pick rand() % processFds.size();//选择一个任务uint32_t task rand() % functors.size();//把任务给一个指定的进程write(processFds[pick].second, task, sizeof(task));//打印对应的提示信息cout 父进程指派任务- info[task] 给进程: processFds[pick].first 编号: pick endl;}
}
int main()
{ loadFunctor();vectorelem assignMap;//创建processNum个进程for (int i 0; i processNum; i){//定义保存管道fd的对象int pipefd[2] { 0 };//创建管道pipe(pipefd);//创建子进程pid_t id fork();if (id 0){//子进程读取, r - pipefd[0]close(pipefd[1]);//子进程执行work(pipefd[0]);close(pipefd[0]);exit(0);}//父进程做的事情, w - pipefd[1]close(pipefd[0]);elem e(id, pipefd[1]);assignMap.push_back(e);}cout create all process success! endl;//父进程派发任务blanceSendTask(assignMap);//回收资源for (int i 0; i processNum; i){if (waitpid(assignMap[i].first, nullptr, 0) 0) {cout wait for: pid assignMap[i].first wait success! nummber: i endl;}close(assignMap[i].second);}return 0;
} 我们写一个如下的监控脚本来辅助观察现象 [xzyecs-333953 pipe]$ while :; do ps ajx | head -1 ps ajx | grep mypipe |grep -v grep ; sleep 1; done 问我曾经在命令行中写的 | 管道是什么意思呢 看如下的测试 如上我通过 | 链接形成了10000和2000这俩进程并写了一段监控脚本。通过观察可以看到这俩进程的PID各不相同但是PPID是相同的所以这俩进程是兄弟关系。此时这俩进程指向的读端和写端是相同的因此可以让父进程不再进行操作就从父子通信转换成了兄弟通信就相当于两个子进程共享了一个管道。综上命令行中的 | 就是一种匿名管道。2.3、管道的读写规则 1、当没有数据可读时 O_NONBLOCK disableread调用阻塞即进程暂停执行一直等到有数据来到为止。O_NONBLOCK enableread调用返回-1errno值为EAGAIN。2、当管道满的时候 O_NONBLOCK disable write调用阻塞直到有进程读走数据O_NONBLOCK enable调用返回-1errno值为EAGAIN3、如果所有管道写端对应的文件描述符被关闭则read返回0 4、如果所有管道读端对应的文件描述符被关闭则write操作会产生信号SIGPIPE,进而可能导致write进程退出 5、当要写入的数据量不大于PIPE_BUF时linux将保证写入的原子性。 6、当要写入的数据量大于PIPE_BUF时linux将不再保证写入的原子性。 2.4、管道的特点 1、管道只能用于具有共同祖先的进程具有亲缘关系的进程之间进行通信常用于父子间通信一个管道由一个进程创建然后该进程调用fork此后父、子进程之间就可应用该管道。 2、管道只能单向通信内核的实现所决定是半双工的一种特殊情况 在数据通信中数据在线路上的传送方式可以分为以下三种 单工通信(Simplex Communication)单工模式的数据传输是单向的。通信双方中一方固定为发送端另一方固定为接收端。半双工通信(Half Duplex)半双工数据传输指数据可以在一个信号载体的两个方向上传输但是不能同时传输。全双工通信(Full Duplex)全双工通信允许数据在两个方向上同时传输它的能力相当于两个单工通信方式的结合。全双工可以同时(瞬时)进行信号的双向传输。管道是半双工的数据只能向一个方向流动需要双方通信时要建立起两个管道 3、管道自带同步互斥机制pipe满writer等pipe空reader等 —— 自带访问控制 我们将一次只允许一个进程使用的资源称为临界资源。管道在同一时刻只允许一个进程对其进行写入或是读取操作因此管道也就是一种临界资源。临界资源是需要被保护的若是我们不对管道这种临界资源进行任何保护机制那么就可能出现同一时刻有多个进程对同一管道进行操作的情况进而导致同时读写、交叉读写以及读取到的数据不一致等问题。为了避免这些问题内核会对管道操作进行同步与互斥 同步 两个或两个以上的进程在运行过程中协同步调按预定的先后次序运行。比如A任务的运行依赖于B任务产生的数据。互斥 一个公共资源同一时刻只能被一个进程使用多个进程不能同时使用公共资源。实际上同步是一种更为复杂的互斥而互斥是一种特殊的同步。对于管道的场景来说互斥就是两个进程不可以同时对管道进行操作它们会相互排斥必须等一个进程操作完毕另一个才能操作而同步也是指这两个不能同时对管道进行操作但这两个进程必须要按照某种次序来对管道进行操作。也就是说互斥具有唯一性和排它性但互斥并不限制任务的运行顺序而同步的任务之间则有明确的顺序关系。 4、管道的生命周期随进程 管道本质上是通过文件进行通信的也就是说管道依赖于文件系统那么当所有打开该文件的进程都退出后该文件也就会被释放掉所以说管道的生命周期随进程。5、管道是面向字节流的 —— 先写的字符一定是先被读取的没有格式边界需要用户来定义区分内容的边界 2.5、命名管道
命名管道的原理 匿名管道只能用于具有共同祖先的进程具有亲缘关系的进程之间的通信通常一个管道由一个进程创建然后该进程调用fork此后父子进程之间就可应用该管道。如果要实现两个毫不相关进程之间的通信可以使用命名管道来做到。命名管道就是一种特殊类型的文件两个进程通过命名管道的文件名打开同一个管道文件此时这两个进程也就看到了同一份资源进而就可以进行通信了。 注意 普通文件是很难做到通信的即便做到通信也无法解决一些安全问题。命名管道和匿名管道一样都是内存文件只不过命名管道在磁盘有一个简单的映像但这个映像的大小永远为0因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中。使用命令创建命名管道 我们可以使用mkfifo命令创建一个命名管道 使用如下 如上可以看到创建出来的文件类型是p代表该文件是命名管道文件。 使用这个命名管道文件就能实现两个进程之间的通信了。我们在一个进程进程A中用shell脚本每秒向命名管道写入一个字符串在另一个进程进程B当中用cat命令从命名管道当中进行读取。现象就是当进程A启动后进程B会每秒从命名管道中读取一个字符串打印到显示器上。这就证明了这两个毫不相关的进程可以通过命名管道进行数据传输即通信。 之前我们说过当管道的读端进程退出后写端进程再向管道写入数据就没有意义了此时写端进程会被操作系统杀掉在这里就可以很好的得到验证当我们终止掉读端进程后因为写端执行的循环脚本是由命令行解释器bash执行的所以此时bash就会被操作系统杀掉我们的云服务器也就退出了。 mkfifo创建命名管道 在程序中创建命名管道使用mkfifo函数mkfifo函数的函数原型如下 int mkfifo(const char *pathname, mode_t mode); mkfifo的第一个参数pathname表示要创建的命名管道文件。 若pathname以路径的方式给出则将命名管道文件创建在pathname路径下。若pathname以文件名的方式给出则将命名管道文件默认创建在当前路径下。注意当前路径的含义mkfifo的第二个参数mode表示创建命名管道文件的默认权限。 mkfifo函数的返回值 命名管道创建成功返回0。命名管道创建失败返回-1。#includeiostream
using namespace std;
#includesys/types.h
#includesys/stat.h
#define FILE_NAME myfifo
int main()
{if (mkfifo(FILE_NAME, 0666) 0){cerr mkfifo error endl;return 1;}//create successcout hello world endl;
} 例如上述将mode设置为0666但命名管道创建出来的权限如下 实际上创建出来文件的权限值还会受到umask文件默认掩码的影响实际创建出来文件的权限为mode(~umask)。umask的默认值一般为0002当我们设置mode值为0666时实际创建出来文件的权限为0664。若想创建出来命名管道文件的权限值不受umask的影响则需要在创建文件前使用umask函数将文件默认掩码设置为0。 umask(0); //将文件默认掩码设置为0 用命名管道实现server client间的通信 实现服务端(server)和客户端(client)之间的通信之前我们需要先让服务端运行起来让服务端运行后创建一个命名管道文件然后再以读的方式打开该命名管道文件之后服务端就可以从该命名管道当中读取客户端发来的通信信息了。 注意如果一个进程已经把文件创建好了那么另一个进程不需要创建这个文件了直接用就可以了server服务端代码如下 //读取
#includecomm.h
using namespace std;
int main()
{umask(0);//将文件默认掩码设置为0if (mkfifo(IPC_PATH, 0600) ! 0){cerr mkfifo error endl;return 1;}int pipefd open(IPC_PATH, O_RDONLY);//以读的方式打开命名管道文件if (pipefd 0){cerr open fifo error endl;return 2;}//正常的通信过程#define NUM 1024char buffer[NUM];while (true){ssize_t s read(pipefd, buffer, sizeof(buffer) - 1);if (s 0){buffer[s] \0;//手动设置\0便于输出cout 客户端-服务器# buffer endl;//输出客户端发来的信息}else if (s 0){cout 客户退出啦我也退出啦 endl;break;}else{//do nothingcout read: strerror(errno) endl;break;}}close(pipefd);cout 服务端退出啦 endl;unlink(IPC_PATH);//通信完毕后自动帮我们删除管道文件return 0;
} 接着再将客户端也运行起来此时我们从客户端写入的信息被客户端写入到命名管道当中服务端再从命名管道当中将信息读取出来打印在服务端的显示器上该现象说明服务端是能够通过命名管道获取到客户端发来的信息的换句话说此时这两个进程之间是能够通信的。client客户端代码如下 //写入
#includecomm.h
using namespace std;
int main()
{int pipefd open(IPC_PATH, O_WRONLY);//以写的方式打开命名管道文件if (pipefd 0){cerr open: strerror(errno) endl;return 1;}#define NUM 1024char line[NUM];while (true){printf(请输入你的消息# );fflush(stdout);memset(line, 0, sizeof(line));//每次读取之前将line清空//fgets -》C语言接口 -》line结尾自动添加\0if (fgets(line, sizeof(line), stdin) ! nullptr){//abcd\n\0line[strlen(line) - 1] \0;//除去回车后多余的\0write(pipefd, line, strlen(line));}else {break;}}close(pipefd);//通信完毕关闭命名管道文件cout 客户端退出啦 endl;
} 对于如何让客户端和服务端使用同一个命名管道文件这里我们可以让客户端和服务端包含同一个头文件该头文件当中提供这个共用的命名管道文件的文件名这样客户端和服务端就可以通过这个文件名打开同一个命名管道文件进而进行通信了。comm.h头文件代码如下 #pragma once
#includeiostream
#includestring
#includesys/types.h
#includesys/stat.h
#includefcntl.h
#includeunistd.h
#includecstring
#includecerrno
#includecstdio#define IPC_PATH ./.fifo makefile文件代码如下 .PHONY:all
all: clientFifo serverFifo
clientFifo:clientFifo.cppg -Wall -o $ $^ -stdc11
serverFifo:serverFifo.cppg -Wall -o $ $^ -stdc11
.PHONY:clean
clean:rm -f clientFifo serverFifo .fifo 此时我运行创建好的serverFifo可执行程序可以看到创建好的.pipe管道文件 此时再运行clientFifo可执行程序并观察俩进程的pid和ppid的关系 此时发现进程serverFifo和进程clientFifo的pid和ppid完全没有关系现在对这俩毫无关系的进程之间进行通信 这里我们可以看到服务器端多了个空行这是因为我们在输入完后会按回车键就相当于多传输了一个\n因此我们可以这样处理 现在就显示正常了 2.6、匿名管道和命名管道的区别 匿名管道子进程继承父进程。由pipe函数创建并打开。命名管道通过一个fifo文件有路径具有唯一性通过路径找到同一个资源。由mkfifo函数创建由open函数打开。FIFO命名管道与pipe匿名管道之间唯一的区别在于它们创建与打开的方式不同一旦这些工作完成之后它们具有相同的语义。3、system V共享内存
3.1、共享内存的原理 共享内存让不同进程看到同一份资源的方式就是通过一种接口在物理内存当中申请一块内存空间然后通过此接口将这块内存空间分别与各个进程各自的页表之间建立映射再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置使得虚拟地址和物理地址之间建立起对应关系至此这些进程便看到了同一份物理内存这块物理内存就叫做共享内存。 注意 这里所说的开辟物理空间、建立映射等操作都是调用系统接口完成的也就是说这些动作都由操作系统来完成。所以操作系统需要提供具有如下功能的接口 创建共享内存 —— 删除共享内存OS内部帮我们做关联共享内存 —— 去关联共享内存进程做实际也是OS做3.2、共享内存的数据结构 在系统当中可能会有大量的进程在进行通信因此系统当中就可能存在大量的共享内存那么操作系统必然要对其进行管理所以共享内存除了在内存当中真正开辟空间之外系统一定还要为共享内存维护相关的内核数据结构。共享内存的数据结构如下 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 */
}; 3.3、共享内存函数
shmget创建共享内存 创建共享内存我们需要使用shmget函数shmget函数的原型如下 #include sys/ipc.h
#include sys/shm.h
int shmget(key_t key, size_t size, int shmflg); shmget函数的参数说明 key表示待创建共享内存在系统中的唯一标识size表示带创建共享内存的大小建议设置为页[4KB]的整数倍shmflg表示创建共享内存的方式shmget函数的返回值说明 shmget调用成功返回一个有效的共享内存标识符用户层标识符shmget调用失败返回-1着重强调size 上面说到size要设置为页4KB的整数倍我们假设有4GB的空间约等于2^20次方个页对于这么多页操作系统需要把共享内存上的这么多页管理起来依旧是先描述再组织OS内部用数组的方式将页保存了起来struct page mem[2^20]。着重强调shmflg 当创建共享内存的时候OS需要在物理内存中申请如上的物理页struct page……。在申请的时候可能会面临如下的问题此共享内存该由谁创建呢如果你创建好了那我该怎么办如果底层存在我该怎么办针对上述问题就由shmget函数的第三个参数shmflg来解决shmflg有两个常见的选项IPC_CREAT和IPC_EXCL 组合方式作用IPC_CREAT 如果内核中不存在键值与key相等的共享内存则新建一个共享内存并返回该共享内存的句柄 如果存在这样的共享内存则直接返回该共享内存的句柄。 IPC_CREAT | IPC_EXCL IPC_CREAT不能单独使用必须和IPC_CREAT配合如果不存在键值与key相等的共享内存则新建一个共享内存并返回该共享内存的句柄 如果存在这样的共享内存则出错返回。 换句话说 使用组合IPC_CREAT一定会获得一个共享内存的句柄但无法确认该共享内存是否是新建的共享内存。使用组合IPC_CREAT | IPC_EXCL只有shmget函数调用成功时才会获得共享内存的句柄并且该共享内存一定是新建的共享内存。着重强调key 共享内存是存在内核中的内核会给我们维护共享内存的结构。共享内存需要被管理依旧是先描述再组织。于是乎就和我们上文谈到的共享内存的数据结构串联起来了此结构就是struct shmid_ds[ ]里面维护了各种属性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_permshm_perm是一个ipc_perm类型的结构体变量每个共享内存的key值存储在shm_perm这个结构体变量当中其中ipc_perm结构体的定义如下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 */
}; ipc_perm结构体的第一个成员key就是标识共享内存唯一性的方法这个key一般由用户提供。综上我们是否知道共享内存存在与否就取决于这个key方法。问为什么此key值得由用户提供呢 假设通信的进程为client和server进程如果key值由用户提供那么server就可以提供一个key值让操作系统帮他创建一个进程并约定好让client也使用同样的key值访问此共享内存。进程间通信的前提是让不同的进程看到同一份资源。综上共享内存在内核中想让不同的进程看到同一份共享内存做法就是让他们拥有同一个key即可。那么我们如何能拥有与之前不同的key值呢这就需要用到 ftok 函数来获取key值。ftok函数的原型如下 #include sys/types.h
#include sys/ipc.h
key_t ftok(const char* pathname, int proj_id); ftok函数的作用就是将一个已存在的路径名pathname和一个整数标识符proj_id转换成一个key值称为IPC键值在使用shmget函数获取共享内存时这个key值会被填充进维护共享内存的数据结构当中。需要注意的是pathname所指定的文件必须存在且可存取。 注意 使用ftok函数生成key值可能会产生冲突此时可以对传入ftok函数的参数进行修改。需要进行通信的各个进程在使用ftok函数获取key值时都需要采用同样的路径名和和整数标识符进而生成同一种key值然后才能找到同一个共享资源。至此我们就可以使用ftok和shmget函数创建一块共享内存了创建后我们可以将共享内存的key值和句柄进行打印以便观察代码如下 #include iostream
#include sys/types.h
#include sys/ipc.h
#include sys/shm.h
#include unistd.h
using namespace std;#define PATH_NAME /home/xzy/dir/date27 //路径名
#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 shm shmget(key, SIZE, IPC_CREAT | IPC_EXCL); //创建新的共享内存if (shm 0) {perror(shmget);return 2;}printf(key: %x\n, key); //打印key值printf(shm: %d\n, shm); //打印句柄return 0;
} 来看如下的一个测试代码 头文件Comm.hpp #pragma once
#includeiostream
#includesys/types.h
#includesys/ipc.h
#includesys/shm.h
#includecstdlib
#includecstring
#includecerrno
using namespace std;#define PATH_NAME /home/xzy/dir/date26 //路径名
#define PROJ_ID 0x14 //整数标识符
#define MEM_SIZE 4096//共享内存的大小//创建key值
key_t CreateKey()
{key_t key ftok(PATH_NAME, PROJ_ID);if (key 0){cerr ftok: strerror(errno) endl;exit(1);}return key;
} Log.hpp #pragma once
#includeiostream
#includectimestd::ostream Log()
{std::cout Fot Debug | timestamp: (uint64_t)time(nullptr) | ;return std::cout;
} 创建共享内存IpcShmSer.cc #include Comm.hpp
#include Log.hpp
// 充当创建共享内存的角色//我想创建全新的共享内存
const int flags IPC_CREAT | IPC_EXCL;
int main()
{key_t key CreateKey();Log() key: key endl;int shmid shmget(key, MEM_SIZE, flags);if (shmid 0){Log() shmget: strerror(errno) endl;return 2;}Log() create shm success, shmid: shmid endl;return 0;
} 使用共享内存IpcShmCli.cc #includeComm.hpp
#includeLog.hpp
//充当使用共享内存的角色
int main()
{key_t key CreateKey();Log() key: key endl;return 0;
} 结果如下 当我们运行完毕创建全新的共享内存的代码后进程退出了但是在第二n次的时候该代码无法运行告诉我们file存在共享内存是存在的。因此system V下的共享内存其生命周期是随内核的管道的生命周期是随进程的。共享内存如果不显示删除只能通过OS重启来解决。具体删除的方法见下文。 shmctl释放共享内存 此时我们若是要将创建的共享内存释放有两个方法一就是使用命令释放共享内存二就是在进程通信完毕后调用释放共享内存的函数进行释放。 法一使用命令释放共享内存 在Linux中我们可以使用 ipcs 命令查看有关进程间通信设施的信息。 单独使用 ipcs 命令时会默认列出消息队列共享内存以及信号量相关的信息若指向查看他们之间某一个的相关信息可以选择携带以下选项 -q列出消息队列相关信息。-m列出共享内存相关信息。-s列出信号量相关信息。例如携带-m选项查看共享内存相关信息 此时根据ipcs命令的查看结果和我们的输出结果可以确认共享内存已经创建成功了。ipcs命令输出的每列信息的含义如下 标题含义key系统区别各个共享内存的唯一标识shmid共享内存的用户层id句柄owner共享内存的拥有者perms共享内存的权限bytes共享内存的大小nattch关联共享内存的进程数status共享内存的状态在共享内存列表中的perms代表的是该共享内存的权限这个是可以修改的 int shmid shmget(key, MEM_SIZE, flags | 0666); 再创建时| 0666就更改权限为666了 注意 key是在内核层面上保证共享内存唯一性的方式而shmid是在用户层面上保证共享内存的唯一性key和shmid之间的关系类似于fd和FILE*之间的的关系。如果我们想要显示的删除就使用ipcrm -m shmid [xzyecs-333953 date26]$ ipcrm -m 6 如上已经将原来的共享内存删除了并且创建了一个新的共享内存。 法二使用系统接口shmctl删除共享内存 控制共享内存我们需要使用shmctl函数shmctl函数的函数原型如下 #include sys/ipc.h
#include sys/shm.h
int shmctl(int shmid, int cmd, struct shmid_ds *buf); shmctl函数的参数说明 shmid表示所控制共享内存的用户级标识符cmd表示具体的控制动作buf用于获取或设置所控制共享内存的数据结构shmctl函数的返回值说明 shmctl调用成功返回0shmctl调用失败返回-1其中作为shmctl函数的第二个参数cmd传入的常用的选项有以下三个 选项作用IPC_STAT获取共享内存的当前关联值此时参数buf作为输出型参数 IPC_SET 在进程有足够权限的前提下将共享内存的当前关联值设置为buf所指的数据结构中的值IPC_RMID删除共享内存段这里修改IpcShmSer.cc文件的代码使其在创建共享内存之后自动删除 #include Comm.hpp
#include Log.hpp
// 充当创建共享内存的角色//我想创建全新的共享内存
const int flags IPC_CREAT | IPC_EXCL;
int main()
{key_t key CreateKey();Log() key: key endl;Log() create share memory begin endl;sleep(5);int shmid shmget(key, MEM_SIZE, flags | 0666);if (shmid 0){Log() shmget: strerror(errno) endl;return 2;}Log() create shm success, shmid: shmid endl;//用它sleep(5);//删它shmctl(shmid, IPC_RMID, nullptr);Log() delete shm : shmid success endl;sleep(5);return 0;
} 我们也可以使用如下的监控脚本时刻观察共享内存的资源分配情况这里就不做演示了。 [xzyecs-333953 date26]$ while :; do ipcs -m; sleep 1; echo ——————————————————————————————————————————————————————————————;done 实现了共享内存的创建和删除下面就来演示如何使用共享内存见下文的shmat和shmdt。 shmat关联共享内存 将共享内存连接到进程地址空间我们需要用shmat函数shmat函数的原型如下 #include sys/types.h
#include sys/shm.h
void *shmat(int shmid, const void *shmaddr, int shmflg); shmat函数的参数说明 shmid表示待关联共享内存的用户级标识符shmaddr指定共享内存映射到进程地址空间的某一地址通常设置为NULL表示让内核自己决定一个合适的地址位置shmflg表示关联共享内存时设置的某些属性shmat函数的返回值说明 shmat调用成功返回共享内存映射到进程地址空间中的起始地址shmat调用失败返回(void*)-1其中作为shmat函数的第三个参数shmflg传入的常用的选项有以下三个 选项作用SHM_RDONLY关联共享内存后只进行读取操作SHM_RND若shmaddr不为NULL则关联地址自动向下调整为SHMLBA的整数倍。公式shmaddr-(shmaddr%SHMLBA)0默认为读写权限这时我们可以尝试使用shmat函数对共享内存进行关联IpcShmSer.cc文件 #include Comm.hpp
#include Log.hpp
// 充当创建共享内存的角色//我想创建全新的共享内存
const int flags IPC_CREAT | IPC_EXCL;
int main()
{key_t key CreateKey();Log() key: key endl;Log() create share memory begin endl;int shmid shmget(key, MEM_SIZE, flags | 0666);if (shmid 0){Log() shmget: strerror(errno) endl;return 2;}Log() create shm success, shmid: shmid endl;sleep(2);//用它//1、将共享内存和自己的进程产生关联attchchar* str (char*)shmat(shmid, nullptr, 0);Log() attach shm : shmid success endl;sleep(2);//删它shmctl(shmid, IPC_RMID, nullptr);Log() delete shm : shmid success endl;sleep(2);return 0;
}注意如果使用shmget函数创建共享内存时并没有对创建的共享内存设置权限创建出来的共享内存的默认权限为0即什么权限都没有那么server进程没有权限关联该共享内存。我们应该在使用shmget函数创建共享内存时在其第三个参数处设置共享内存创建后的权限权限的设置规则与设置文件权限的规则相同。这个上文已经讲过 int shmid shmget(key, MEM_SIZE, flags | 0666); 运行程序后即可发现关联该共享内存的进程数由0变成了1即关联成功。如果想要去关联则需要用到下文讲到的shmdt函数。 shmdt去关联共享内存 取消共享内存与进程地址空间之间的关联我们需要用shmdt函数shmdt函数的函数原型如下 #include sys/types.h
#include sys/shm.h
int shmdt(const void *shmaddr); shmdt函数的参数说明 shmaddr待去关联共享内存的起始地址即调用shmat函数时得到的起始地址shmat函数的返回值shmdt函数的返回值说明 shmdt调用成功返回0shmdt调用失败返回-1现在我们就能够取消共享内存与进程之间的关联了 #include Comm.hpp
#include Log.hpp
// 充当创建共享内存的角色//我想创建全新的共享内存
const int flags IPC_CREAT | IPC_EXCL;
int main()
{key_t key CreateKey();Log() key: key endl;Log() create share memory begin endl;int shmid shmget(key, MEM_SIZE, flags | 0666);if (shmid 0){Log() shmget: strerror(errno) endl;return 2;}Log() create shm success, shmid: shmid endl;sleep(2);//1、将共享内存和自己的进程产生关联attchchar* str (char*)shmat(shmid, nullptr, 0);Log() attach shm : shmid success endl;sleep(2);//用它//2、去关联shmdt(str);Log() detach shm : shmid success endl;sleep(2);//删它shmctl(shmid, IPC_RMID, nullptr);Log() delete shm : shmid success endl;sleep(2);return 0;
}我们使用如下的监控脚本时刻观察共享内存的资源分配情况 [xzyecs-333953 date26]$ while :; do ipcs -m; sleep 1; echo ——————————————————————————————————————————————————————————————;done 运行程序通过监控即可发现该共享内存的关联数由1变为0的过程即取消了共享内存与该进程之间的关联。 3.4、用共享内存实现serveclient通信 知道了共享内存的创建、关联、去关联以及释放后实际上前面都是对server做的调整准备工作做好后下面只需要对client进行处理使用共享内存即可。总体代码如下 Comm.hpp #pragma once
#includeiostream
#includesys/types.h
#includesys/ipc.h
#includesys/shm.h
#includeunistd.h
#includecstdlib
#includecstring
#includecerrno
using namespace std;#define PATH_NAME /home/xzy/dir/date26 //路径名
#define PROJ_ID 0x14 //整数标识符
#define MEM_SIZE 4096//共享内存的大小//创建key值
key_t CreateKey()
{key_t key ftok(PATH_NAME, PROJ_ID);if (key 0){cerr ftok: strerror(errno) endl;exit(1);}return key;
} Log.hpp #pragma once
#includeiostream
#includectimestd::ostream Log()
{std::cout Fot Debug | timestamp: (uint64_t)time(nullptr) | ;return std::cout;
} IpcServer.cc #include Comm.hpp
#include Log.hpp
// 充当创建共享内存的角色//我想创建全新的共享内存
const int flags IPC_CREAT | IPC_EXCL;
int main()
{key_t key CreateKey();Log() key: key endl;Log() create share memory begin endl;int shmid shmget(key, MEM_SIZE, flags | 0666);if (shmid 0){Log() shmget: strerror(errno) endl;return 2;}Log() create shm success, shmid: shmid endl;sleep(2);//1、将共享内存和自己的进程产生关联attchchar* str (char*)shmat(shmid, nullptr, 0);Log() attach shm : shmid success endl;sleep(2);//用它//2、去关联shmdt(str);Log() detach shm : shmid success endl;sleep(2);//删它shmctl(shmid, IPC_RMID, nullptr);Log() delete shm : shmid success endl;sleep(2);return 0;
}IpcShmCli.cc #includeComm.hpp
#includeLog.hpp
//充当使用共享内存的角色
int main()
{//创建相同的key值key_t key CreateKey();Log() key: key endl;//获取共享内存int shmid shmget(key, MEM_SIZE, IPC_CREAT);if (shmid 0){Log() shmget: strerror(errno) endl;return 2;}//挂接char* str (char*)shmat(shmid, nullptr, 0);//使用sleep(2);//去关联shmdt(str);//不需要做删除操作return 0;
} 测试结果如下 结果如上挂接数先因为Ser挂接由0变成1再因为Cli挂接由1变成2然后再依次去关联最后变成0。现在我们来真正让两个进程进行通信 IpcShmCli.cc#includeComm.hpp
#includeLog.hpp
//充当使用共享内存的角色
int main()
{//创建相同的key值key_t key CreateKey();Log() key: key endl;//获取共享内存int shmid shmget(key, MEM_SIZE, IPC_CREAT);if (shmid 0){Log() shmget: strerror(errno) endl;return 2;}//挂接char* str (char*)shmat(shmid, nullptr, 0);//使用// sleep(2);//让client进程给server进程发消息int cnt 0;while (cnt 26){str[cnt] A cnt;cnt;str[cnt] \0;sleep(1);}//去关联shmdt(str);//不需要做删除操作return 0;
} IpcShmSer.cc#include Comm.hpp
#include Log.hpp
// 充当创建共享内存的角色//我想创建全新的共享内存
const int flags IPC_CREAT | IPC_EXCL;
int main()
{key_t key CreateKey();Log() key: key endl;Log() create share memory begin endl;int shmid shmget(key, MEM_SIZE, flags | 0666);if (shmid 0){Log() shmget: strerror(errno) endl;return 2;}Log() create shm success, shmid: shmid endl;// sleep(2);//1、将共享内存和自己的进程产生关联attchchar* str (char*)shmat(shmid, nullptr, 0);Log() attach shm : shmid success endl;// sleep(2);//用它while (true){printf(%s\n, str);sleep(1);}//2、去关联shmdt(str);Log() detach shm : shmid success endl;// sleep(2);//删它shmctl(shmid, IPC_RMID, nullptr);Log() delete shm : shmid success endl;// sleep(2);return 0;
}如上我实现了client进程给server进程但是我client进程中并没有使用系统调用向共享内存中写入像之前学习的管道创建好后想要用它必须要用writeread等系统接口而使用共享内存竟然没有使用任何的系统接口。解释如下 我们把共享内存实际上是映射到了我们进程地址空间的用户空间了堆-栈之间。堆栈都是属于用户的因此对每一个进程而言挂接到自己的上下文中的共享内存是属于自己的空间类似于堆空间或者栈空间是可以被用户直接使用的不需要调用系统接口。测试结果如下 当一开始只运行服务端IpcShmSer可执行程序时会发现无论IpcShmCli是否像挂接的地址空间中写入了消息IpcShmSer是一直在刷新的只不过共享内存是空的一开始随后我们运行客户端IpcShmCli可执行程序此时客户端往共享内存中写入数据我服务端就能立马看到了。 因此共享内存因为它自身的特性它没有任何访问控制共享内存被双方直接看到属于双方的用户空间可以直接通信但是不安全。也因此共享内存是所有进程间通信中速度最快的现在对IpcShmCli.cc文件再次进行修改 #includeComm.hpp
#includeLog.hpp
#includecstdio
//充当使用共享内存的角色
int main()
{//创建相同的key值key_t key CreateKey();Log() key: key endl;//获取共享内存int shmid shmget(key, MEM_SIZE, IPC_CREAT);if (shmid 0){Log() shmget: strerror(errno) endl;return 2;}//挂接char* str (char*)shmat(shmid, nullptr, 0);//使用//让client进程给server进程发消息while (true){printf(Please Enter# );fflush(stdout);ssize_t s read(0, str, MEM_SIZE);if (s 0){str[s] \0;}}//去关联shmdt(str);//不需要做删除操作return 0;
} 这里就可以我们自己再IpcShmCli中输入在IpcShmSer中得到通信的结果。 下面我们再用命名管道对共享内存做访问控制 Comm.hpp #pragma once
#include iostream
#include sys/types.h
#include sys/stat.h
#include sys/ipc.h
#include sys/shm.h
#include unistd.h
#include fcntl.h
#include cstdlib
#include cstring
#include cerrno
#include cassert
#include Log.hpp
using namespace std;#define PATH_NAME /home/xzy/dir/date26 // 路径名
#define PROJ_ID 0x14 // 整数标识符
#define MEM_SIZE 4096 // 共享内存的大小#define FIFO_FILE .fifo// 创建key值
key_t CreateKey()
{key_t key ftok(PATH_NAME, PROJ_ID);if (key 0){cerr ftok: strerror(errno) endl;exit(1);}return key;
}// 创建命名管道
void CreateFifo()
{umask(0);if (mkfifo(FIFO_FILE, 0666) 0){// 创建失败Log() strerror(errno) endl;exit(2);}
}#define READER O_RDONLY
#define WRITER O_WRONLY
// 读 写
int Open(const string filename, int flags)
{return open(filename.c_str(), flags);
}int Wait(int fd)
{uint32_t values 0;ssize_t s read(fd, values, sizeof(values));return s;
}int Signal(int fd)
{uint32_t cmd 1;write(fd, cmd, sizeof(cmd));
}int Close(int fd, const string filename)
{close(fd);unlink(filename.c_str());
}IpcShmSer.cc #include Comm.hpp
#include Log.hpp
// 充当创建共享内存的角色//我想创建全新的共享内存
const int flags IPC_CREAT | IPC_EXCL;
int main()
{CreateFifo();int fd Open(FIFO_FILE, READER);assert(fd 0);key_t key CreateKey();Log() key: key endl;Log() create share memory begin endl;int shmid shmget(key, MEM_SIZE, flags | 0666);if (shmid 0){Log() shmget: strerror(errno) endl;return 2;}Log() create shm success, shmid: shmid endl;// sleep(2);//1、将共享内存和自己的进程产生关联attchchar* str (char*)shmat(shmid, nullptr, 0);Log() attach shm : shmid success endl;// sleep(2);//用它while (true){//让读端进行等待if (Wait(fd) 0) break;printf(%s\n, str);sleep(1);}//2、去关联shmdt(str);Log() detach shm : shmid success endl;// sleep(2);//删它shmctl(shmid, IPC_RMID, nullptr);Log() delete shm : shmid success endl;Close(fd, FIFO_FILE);// sleep(2);return 0;
} IpcShmCli.cc #includeComm.hpp
#includeLog.hpp
#includecstdio
//充当使用共享内存的角色
int main()
{int fd Open(FIFO_FILE, WRITER);//创建相同的key值key_t key CreateKey();Log() key: key endl;//获取共享内存int shmid shmget(key, MEM_SIZE, IPC_CREAT);if (shmid 0){Log() shmget: strerror(errno) endl;return 2;}//挂接char* str (char*)shmat(shmid, nullptr, 0);//使用// sleep(2);//让client进程给server进程发消息while (true){printf(Please Enter# );fflush(stdout);ssize_t s read(0, str, MEM_SIZE);if (s 0){str[s] \0;}Signal(fd);}//去关联shmdt(str);//不需要做删除操作return 0;
} 把上述代码再复盘一下 上述我给共享内存加上了命名管道当共享内存没有数据时Server服务端就一直在阻塞等待直至有数据时再读而不是像之前那样一直读。这就是基于共享内存 管道的一个访问控制的效果。3.5、共享内存与管道进行对比 当共享内存创建好后就不再需要调用系统接口进行通信了而管道创建好后仍需要read、write等系统接口进行通信。实际上共享内存是所有进程间通信方式中最快的一种通信方式。 我们先来看看管道通信 从这张图可以看出使用管道通信的方式将一个文件从一个进程传输到另一个进程需要进行四次拷贝操作 服务端将信息从输入文件复制到服务端的临时缓冲区中将服务端临时缓冲区的信息复制到管道中客户端将信息从管道复制到客户端的缓冲区中将客户端临时缓冲区的信息复制到输出文件中再来看看共享内存通信 从这张图可以看出使用共享内存进行通信将一个文件从一个进程传输到另一个进程只需要进行两次拷贝操作 从输入文件到共享内存。从共享内存到输出文件。所以共享内存是所有进程间通信方式中最快的一种通信方式因为该通信方式需要进行的拷贝次数最少。但是共享内存也是有缺点的我们知道管道是自带同步与互斥机制的但是共享内存并没有提供任何的保护机制包括同步与互斥。 4、system V IPC联系
临界资源、临界区、原子性、互斥 临界资源 被多个进程能够同时看到的资源叫做临界资源管道共享内存中的资源都属于临界资源如果没有对临界资源进行任何保护对于临界资源的访问双方进程在进行访问的时候就会都是乱序的可能会因为读写交叉而导致各种乱码、废弃数据、访问控制方面的问题。临界区 对多个进程而言访问临界资源的代码叫做临界区。在之前写的进程代码中只有一部分代码会访问临界资源这部分代码就叫做临界区原子性 我们把一件事情要么没做要么做完了称之为原子性互斥 任何时刻只允许一个进程访问临界资源我们称之为互斥信号量 一、信号量相关函数 信号量集的创建semget int semget(key_t key, int nsems, int semflg); 说明一下 创建信号量集也需要使用ftok函数生成一个key值这个key值作为semget函数的第一个参数。semget函数的第二个参数nsems表示创建信号量的个数。semget函数的第三个参数与创建共享内存时使用的shmget函数的第三个参数相同。信号量集创建成功时semget函数返回的一个有效的信号量集标识符用户层标识符。信号量集的删除semctl int semctl(int semid, int semnum, int cmd, ...); 信号量集的操作semop int semop(int semid, struct sembuf *sops, unsigned nsops); 二、信号量的概念 先看如下的场景 假设一个电影院只有100个位置那么它只能卖100张电影票。电影院是可以被多个人进出的所以它即临界资源。通常情况下我只要买到了票那么电影院中的一个位置必然是我的。买票其实就是对放映厅中特定的座位的一种预定机制。为了防止电影票多卖出去导致座位不够我们是定义一个cnt变量初始化100当有人买票的时候那么就--cnt并返回一个编号票号直至cnt 0。上述放映厅就是一个临界资源当此放映厅只允许一个人看电影那么就只有一张电影票这里体现的就是互斥特性。 当此放映厅有100个座位时每个人都是一个进程只要我们坐在不同的位置且都可以访问此临界资源此情况下我们只需要保证不能有多余的人进来以及保证大家访问的是不同的资源由上层业务决定即可。我们同样可以定义一个cnt变量初始化100当有人买票且cnt 0时就wait等待cnt 0时就cnt--当有人离场时cnt。上述的cnt就类似于信号量信号量本质就是一个计数器。当信号量为1的时候表现的就是互斥特性我们称之为二元信号量。我们把常规的信号量称之为多元信号量。临界资源每被占一块信号量就--信号量的大小就等于临界资源的大小。 而多个人看电影首先需要买票本质就是对信号量int cnt 100进行抢占申请。类似的任何进程想要访问临界资源必须先申请信号量。如果申请成功就一定能访问临界资源中的一部分资源。每一个人必须先申请cnt --》 每一个人必须先看到这个计数器。类似的每一个进程要先申请信号量 --》 每一个进程都必须先看到这个信号量。在这种情况下计数器和信号量本身也是一个临界资源。注意不考虑其它因素cnt--是会存在很多的中间状态“单纯的计数器它不是原子性的”见下文 问如何理解cnt-- 在计算机里头只有cpu才有计算能力我们定义的整型变量cnt是在内存中的要计算的时候cnt--首先要把cnt变量读到cpu里头然后cpu内部做--计算后再把结果写回我的内存中。图示如下 但是cnt--是会存在多个中间状态的因为当执行完图示的第①②步后正准备写回时cnt99进程切换了上下文的数据被保存起来了此时假设另一个进程来了同样是在cpu加载数据我们假设这里计算时有个while循环一直--最终计算后的结果是50并将结果返回到内存中当此进程退出后原进程被切回来了此时把之前村的数据cnt99要写回内存了那么原先计算的数据50又被覆盖成99了此时就出现了因为多执行流交叉的问题导致变量的值出现不一致的问题。为了保证不会出现此问题必须保证计数器是原子性的。总结为了保证计数器执行前后不会被中断不会因为执行流切换而导致变量的值出现不一致的问题信号量必须是具有原子性的。信号量是一个计数器这个计数器对应的操作是原子的。 信号量对应的操作是PV操作 semop-1申请资源Psemop1释放资源V共享内存不做访问控制可以通过信号量进行对资源保护。 IPC资源 其实我们所有的IPC资源在内核中是通过数组来管理的名为ipc_perm来看如下共享内存、信号量、消息队列的结构 如上三个对应的结构体的第一个字段都是ipc_perm它们三个对于此的资源都是一样的所以每一种IPC资源描述结构体第一个都是一样的都是struct ipc_perm为下列结构 struct ipc_perm
{//key//权限……
}; 操作系统需要把这些字段管理起来其内部有一个ipc_ids这样的结构里面有一个*entries这样的柔性数组其指向ipc_id_ary这样的结构它是一个指针数组struct ipc_perm* ipc_id_ar[n]它的所有元素最终都指向这个ipc_perm结构而ipc_perm就是每种资源的第一个字段。所以当我想申请一块共享内存的时候OS帮我创建此shmid_ds结构对象并用前面的指针数组ipc_id_ary的0号指针指向此shmid_ds结构对象的第一个元素struct ipc_perm以此类推…… 上述使用ipc_id_ary指针数组的好处是便于访问资源当下标0指向共享内存的第一个字段时为了访问该共享内存结构体的剩下元素我们可以使用强转struct id_ds*ipc_id_ar[0] - 访问其它元素上述能这么做的原因是结构体的第一个元素的地址在数字上和结构体整体的地址大小是一样的。上述做法我们就可以用统一的规则在内核中管理不同种类的IPC资源啦有点像C的多态技术的切片。