目录
示例代码
头文件(comm.hpp)
log.hpp
基础版 -- 服务端
代码
运行情况
加入客户端
代码
运行情况
两端进行通信
客户端
代码
注意点
服务端
代码
两端运行情况
共享内存特点
拷贝次数少
管道的拷贝次数
共享内存的拷贝次数
没有访问控制
管道
共享内存
并发问题
添加访问控制(通过管道)
代码
头文件
服务端
客户端
运行情况
我们已经介绍了共享内存多个接口的使用,接下来就开始实际调用一下吧
示例代码
头文件(comm.hpp)
#ifndef COMM_H #define COMM_H #include <iostream> #include <cstring> #include <sys/types.h> #include <sys/stat.h> #include <string> #include <fcntl.h> #include <sys/wait.h> #include <unistd.h> #include <sys/ipc.h> #include <sys/shm.h> #include "log.hpp" #include <assert.h> using namespace std; #define PATH_NAME "/home/mufeng" #define PROJ_ID 0x1234 #define SUM_SIZE 4096 #endif
log.hpp
我们将不同的报错信息分成4种等级
#pragma once #include <iostream> #include <string> #include <time.h> using namespace std; #define debug 0 #define notice 1 #define warning 2 #define error 3 const string msg[]{ "debug", "notice", "warning", "error"}; ostream &log(string message, int level) { cout << "|" << (unsigned)time(nullptr) << "|" << message << "|" << msg[level] << "|"; return cout; }
基础版 -- 服务端
代码
注意我们使用assert来确保调用成功,并且成功后就进行日志打印
#include"comm.hpp" int main(){ key_t key = ftok(PATH_NAME, PROJ_ID); assert(key != -1); (void)key; log("key created success", debug) << ", key : " << key << endl; int shmid = shmget(key, SUM_SIZE, IPC_CREAT|IPC_EXCL|0666); assert(shmid != -1); (void)shmid; log("shm created success", debug) << ", shmid : " << shmid << endl; sleep(3); char *addres = (char *)shmat(shmid, nullptr, 0); log("process link success", debug) << endl; sleep(3); int ret = shmdt(addres); assert(ret != -1); (void)ret; log("process unlink success", debug) << endl; sleep(3); ret = shmctl(shmid, IPC_RMID, nullptr); assert(ret != -1); (void)ret; log("shm unlink success", debug) << endl; sleep(3); return 0; }
运行情况
监控共享内存的使用情况(while :;do ipcs -m ;sleep 1;done):
- 连接数从0 -> 1
- 解除连接后,连接数从1 -> 0
- 删除后,共享内存块消失
加入客户端
代码
相似的操作
但我们的客户端不需要创建新的共享内存,而是使用服务端使用的那个:
int main() { key_t key = ftok(PATH_NAME, PROJ_ID); if (key < 0) { log("key created failed", error) << ", client key : " << key << endl; } int shmid = shmget(key, SUM_SIZE, 0); if (shmid < 0) { log("shmid created failed", error) << ", client shmid : " << shmid << endl; } char *addres = (char *)shmat(shmid, nullptr, 0); if (addres == nullptr) { log("process link failed", error) << endl; } sleep(3); int ret = shmdt(addres); if (ret < 0) { log("process unlink failed", error) << endl; } sleep(3); //这里不需要删除,服务端会将这块内存释放掉 return 0; }
运行情况
连接数从0 -> 1 -> 2 -> 1 -> 0:
两端进行通信
客户端
- 客户端一般是发送内容给服务端
- 这里我们将从标准输入(也就是键盘)读入的内容,写入到addres中
- 如果读到了quit,就退出
代码
//通信 while(true){ ssize_t size=read(0,addres,SUM_SIZE-1); if(size>0){ addres[size-1]=0; if(strcmp(addres,"quit")==0){ break; } } }
注意点
- 我们在输入时,实际上会将按的回车也读入
- 但我们判断的是"quit",它不包括换行符
- 所以需要将addres从读入的那个换行符开始,设置为0
服务端
- 从addres中读取数据
- 这里我们直接将内容打印
- 如果读到了quit,就退出
代码
while (true){ if (strcmp(addres, "quit") == 0) { break; } cout << addres << endl; sleep(2); }
两端运行情况
共享内存特点
这里我们使用命名管道作为对比的例子,之后会使用管道来完善共享内存
拷贝次数少
管道的拷贝次数
- 通过管道通信 -- 也就是创建两个文件作为管道的读端和写端
- 当写入的时候,我们通过键盘输入,输入的数据先被拷贝到我们自己设定的缓冲区(也就是定义的数组)中,然后再被传输到管道文件中
- 读出也是一样,先要从管道文件到设定的缓冲区,再打印出来,而打印也就是将数据传输到显示器上
- 所以至少需要四次拷贝
共享内存的拷贝次数
- 共享内存是直接在物理内存上开辟一块空间,然后映射到需要进行通信的进程的地址空间中
- 写入的时候,输入的内容实际上是直接写入到共享内存中的,不需要经过自定义的缓冲区
- 打印也同样,直接从共享内存中读出,然后显示到显示器上
- 所以只需要两次
没有访问控制
管道
- 前面已经操作过了,管道文件只有当双方同时打开时,才会开始通信,否则会阻塞
- 写满 / 没有写,另一方会等待,而不是一直在读
共享内存
- 没有任何的控制
- 从前面的操作可以看到,其中一方的运行不需要依赖另一方
- 只要写完一句,就直接会被读走
- 即使没有写,也会一直读
- 这样就会导致并发问题
并发问题
- 可能要传递的信息是很长的,但可能中途就会被服务端读走
- 这样它就拿不到完整的数据,可能就会导致无法执行相应的操作
添加访问控制(通过管道)
因为管道是有访问控制的,所以可以借助管道,让共享内存也具有访问控制
代码
头文件
// 加入访问控制(通过管道来传递信号,接收到信号才进行读取) #define FIFO_PATH "./fifo" #define READ O_RDONLY #define WRITE O_WRONLY | O_TRUNC class Init //让管道文件具有类的特性,出作用域自动释放 { public: Init() { umask(0); int ret = mkfifo(FIFO_PATH, 0666); assert(ret == 0); (void)ret; log("fifo created success", notice) << endl; } ~Init() { unlink(FIFO_PATH); log("fifo removed success", notice) << endl; } }; void wait_signal(int fd) //读取指定文件内容作为信号 { uint32_t signal = 0; log("waiting ...", notice) << endl; ssize_t size = read(fd, &signal, sizeof signal); assert(size == sizeof(uint32_t)); (void)size; } void send_signal(int fd) //向指定文件写入signal { uint32_t signal = 1; ssize_t size = write(fd, &signal, sizeof signal); assert(size == sizeof(uint32_t)); (void)size; log("being awakened ...", notice) << endl; } int open_fifo(string path, int flags) //以指定方式打开创建好的管道文件 { int fd = open(path.c_str(), flags); assert(fd >= 0); return fd; } void close_fifo(int fd) { close(fd); }
服务端
- 创建管道文件
- 等待客户端的信号(也就是等待管道文件中出现内容时)
- 被唤醒后打印addres中的内容
//通信 // 添加访问控制 Init init; // 创建管道文件 int fd = open_fifo(FIFO_PATH, READ); while (true) { wait_signal(fd); // 等待唤醒 if (strcmp(addres, "quit") == 0) { break; } cout << addres << endl; } close_fifo(fd); // 通信结束
客户端
- 打开创建好的管道文件
- 读取键盘输入内容,存入addres中
- 成功输入时,向服务端发送信号(也就是向管道写入数据)
// 添加访问控制 int fd = open_fifo(FIFO_PATH, WRITE); while (true) { ssize_t size = read(0, addres, SUM_SIZE - 1); if (size > 0) { addres[size - 1] = 0; //处理回车符 send_signal(fd); if (strcmp(addres, "quit") == 0) { break; } } }
运行情况
只有一方时,阻塞在管道文件打开的位置:
当客户端接入后:
发送信息时,会将信号和数据都传递给对方:
退出: