目录
进程的运行具有独立性,有独立的页表,pcb,等等父子进程之间,数据不相干扰
这就让我们进程想要通信的难度比较大。
因为操作系统在设计的时候,它本身就是独立的。
进程间通信的本质:
先让不同的进程看到同一份资源(内存空间)
为什么要进行进程间通信?
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。(交换数据、空值、通知等目标)
进程间通信的必要性
单进程的,那么也就无法使用并发能力,更加无法实现多进程协同
进程间通信不是目的,而是手段,为了实现多进程协同。
进程通信的技术背景
1.进程是具有独立性的。虚拟地址空间+页表 保证进程运行的独立性(进程内核数据接口+进程的代码和数据)
2.通信的成本会比较高
进程间通信的本质理解
1.进程间通信的前提,首先需要让不同的进程看到同一块“内存”(特定的结构组织)
2.所以所谓的进程看到同一块“内存”,属于哪一个进程呢?不能隶属于任何一个进程,而应该更强调共享。
进程间通信分类:
进程间通信的方式也有一些标准
1.Linux原生提供--管道
匿名管道pipe
命名管道
2.System V IPC(侧重于本地通信(单机通信))--多进程
System V 消息队列
System V 共享内存
System V 信号量
3.POSIX IPC(侧重于网络通信)--多线程
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
标准在我们使用者看来,都是接口上具有一定的规律
什么是管道?
(天然气管道、石油管道、自来水管道……)
(这里我们简化一下,有一个入口和一个出口的管道)
1.只能单向通信。
(一般都是天然气公司把天然气输送到你家,不是你家把天然气输送到天然气公司)
2.管道内传输的都是资源
计算机通信领域的设计者,设计了一种单向通信的方式--管道
计算机中的管道:
传输资源->数据!
管道通信背后是进程之间通过管道进行通信
下面的|就是将第一个命令的执行结果作为参数传给后面那个命令
因为中间的数据资源不属于任何一个进程。
上面是我们的再基础IO中所说过的文件系统。
那么当前进程如果创建了子进程,会发生什么呢?
struct file*fd_array[]文件描述符表要不要拷贝给子进程呢?
首先这个表表示这个进程和文件的描述符表之间的关系,可就是当前的进程可以看到哪些被打开的文件。这个必须拷贝给子进程!
拷贝只是第一次拷贝,之后父子进程持有的就是独立的表结构。
那么这个表指向的一堆文件要不要拷贝给子进程呢?
不 ,我们不需要。与进程相关的都会被拷贝,与文件相关的并不会被拷贝!
父子进程的struct file_struct是一样的,所以里面的文件指针都是一样的,所以我们打开的文件也是一样的。
(比防说我们父子进程在打印到屏幕上的时候,都是打印到1号文件中,都是打印到同一个显示器上!)
所以我们这里的struct file是能被父进程访问,也能被子进程访问!
所以我们的父子进程是不是看到了一份公共文件,这个也就是我们的管道!
(管道的临时文件不需要刷新到磁盘)
双方进程各自关闭不需要的文件描述符
让父进程进行写入,子进程进行读取,父进程写,就需要关闭读,保留写,子进程关闭写,保留读的功能。每个进程内部都有各自的文件描述符。
也就是让不同的进程看到同一份资源。
Linux下,一切皆文件,管道就是我们上面的共享文件。
这里的文件是属于内核的,所有的进程间通信都是内核级别的。
那两进程进行通信,需不需要将这个管道文件保存到磁盘?
不需要!
进程间通信的管道必须在内存中,是一个纯内存的通信方式,如果还要写磁盘的话,我们的通信效率就太低了。
并且进程间通信的数据往往属于临时数据,不需要将数据持久化保存
#include
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
像下面的|就是一个简单的管道
ps axj |head -1 && ps axj|grep sleep
管道使用完毕之后,记得要关闭文件描述符。
如何做到让不同的进程,看到同一份资源的呢?
fork让子进程继承的--能够让具有血缘关系的进程进行进程间通信--常用于父子进程。
pipefd[0]:读端
pipefd[1]:写端
int pipe(int pipefd[2]);输出型参数,希望通过它来验证我们的管道成功搭建
- #include
- #include
- #include
- using namespace std;
- int main()
- {
- //1.创建管道
- int pipefd[2]={0};
- int n=pipe(pipefd);
- //在debug模式下assert是有效的,但是release版本下是会无效的
- assert(n!=-1);
- //所以我们这里需要写下面的代码,证明n被使用过
- (void)n;
-
- cout<<"pipefd[0]"<
0]< - cout<<"pipefd[1]"<
1]< - return 0;
- }
ok,我们这里的文件描述符已经成功打开了,接下来我们在进一步搭建我们的管道
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- using namespace std;
- int main()
- {
- //1.创建管道
- int pipefd[2]={0};//pipefd[0]:读端,pipefd[1]:写端
- int n=pipe(pipefd);
- //在debug模式下assert是有效的,但是release版本下是会无效的
- assert(n!=-1);
- //所以我们这里需要写下面的代码,证明n被使用过
- (void)n;
-
- //如果是DEBUG模式下就不打印了,相当于就是注释掉了
- #ifdef DEBUG
- cout<<"pipefd[0]"<
0]< - cout<<"pipefd[1]"<
1]< - #endif
-
- //2.创建子进程
- pid_t id=fork();
- assert(id!=-1);
- if(id==0)
- {
- //子进程
- //3.构建单向通信的信道
- //父进程写入,子进程读取,让父子关闭不需要的文件描述符
- //3.1关闭子进程不需要的fd
- //子进程进行读取,将写端给关闭
- close(pipefd[1]);
- char buffer[1024];
- while (true)
- {
- //从0号文件描述符中读取,读取到缓冲区buffer中
- size_t s=read(pipefd[0],buffer,sizeof(buffer)-1);
- if(s>0)
- {
- //添加\0
- buffer[s]=0;
- cout<<"child get a message["<<getpid()<<"] Father#"<
- }
- }
- // close(pipefd[0]);
- exit(0);
- }
- //父进程
- //3.构建单向通信的信道
- //父进程进行写入,将读取端进行关闭
- close(pipefd[0]);
- string message="我是父进程,我正在给你发消息";
- int count=0;
- char send_buffer[1024];
- while(true)
- {
- //3.2构建一个变化的字符串
- //将printf的内容格式化到字符串中
- snprintf(send_buffer,sizeof(send_buffer),"%s[%d]:%d",message.c_str(),getpid(),count++);
- //3.3写入
- //这里strlen不需要+1,\0写入也没有意义。
- write(pipefd[1],send_buffer,strlen(send_buffer));
-
- //3.4故意sleep
- sleep(1);
- }
- pid_t ret=waitpid(id,nullptr,0);
- assert(ret<0);
- (void)ret;
- close(pipefd[1]);
- //子进程中的pipefd[0]关闭可写可不写,因为进程退出了,进程中的文件描述符也会被关掉
- return 0;
- }
为什么我们不写一个全局的缓冲区(buffer)来进行通信呢?
因为有写时拷贝的存在,无法更改通信。
2.总结管道的特点,理解以前的管道
1.管道是一种进程间通信的方式,管道是用来进行具有血缘关系的进程进行进程间通信,常用于父子进程。
2.我们上面的代码中,我们的父进程是1秒钟发送一条消息,但是我们的子进程并没有设置读取信息的时间间隔,但是我们的子进程依旧是跟随父进程的节奏在打印。
那么我们父进程在sleep的期间,子进程在干什么呢?
子进程在等待父进程的写入。
管道是一个文件,显示器也是一个文件,父子同时往显示器写入的时候,有没有说一个会等另一个的情况呢?
没有!
之前我们往显示器上打印的时候,都是交错着疯狂往显示器上打印的。
这种情况,我们将其称为缺乏访问控制!
那我们上面的子进程等待父进程的写入,就是具有访问控制。
也就是说,管道具有通过让进程间协同,提供了访问控制!
管道里满的时候,写的一方要等待读的一方将数据读取
管道空的时候,读取的一方要等待写的一方写入。
下面我们验证一下,下面我们是将父进程循环写入,但是子进程需要等待20秒才写入
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- using namespace std;
- int main()
- {
- //1.创建管道
- int pipefd[2]={0};//pipefd[0]:读端,pipefd[1]:写端
- int n=pipe(pipefd);
- //在debug模式下assert是有效的,但是release版本下是会无效的
- assert(n!=-1);
- //所以我们这里需要写下面的代码,证明n被使用过
- (void)n;
-
- //如果是DEBUG模式下就不打印了,相当于就是注释掉了
- #ifdef DEBUG
- cout<<"pipefd[0]"<
0]< - cout<<"pipefd[1]"<
1]< - #endif
-
- //2.创建子进程
- pid_t id=fork();
- assert(id!=-1);
- if(id==0)
- {
- //子进程
- //3.构建单向通信的信道
- //父进程写入,子进程读取,让父子关闭不需要的文件描述符
- //3.1关闭子进程不需要的fd
- //子进程进行读取,将写端给关闭
- close(pipefd[1]);
- char buffer[1024*8];
- while (true)
- {
- //从0号文件描述符中读取,读取到缓冲区buffer中
- size_t s=read(pipefd[0],buffer,sizeof(buffer)-1);
- if(s>0)
- {
- sleep(20);
- //添加\0
- buffer[s]=0;
- cout<<"child get a message["<<getpid()<<"] Father#"<
- }
-
-
- }
- // close(pipefd[0]);
- exit(0);
- }
- //父进程
- //3.构建单向通信的信道
- //父进程进行写入,将读取端进行关闭
- close(pipefd[0]);
- string message="我是父进程,我正在给你发消息";
- int count=0;
- char send_buffer[1024*8];
- while(true)
- {
- //3.2构建一个变化的字符串
- //将printf的内容格式化到字符串中
- snprintf(send_buffer,sizeof(send_buffer),"%s[%d]:%d",message.c_str(),getpid(),count++);
- //3.3写入
- //这里strlen不需要+1,\0写入也没有意义。
- write(pipefd[1],send_buffer,strlen(send_buffer));
-
- //3.4故意sleep
- cout<
- // sleep(10);
- }
- pid_t ret=waitpid(id,nullptr,0);
- assert(ret<0);
- (void)ret;
- close(pipefd[1]);
- //子进程中的pipefd[0]关闭可写可不写,因为进程退出了,进程中的文件描述符也会被关掉
- return 0;
- }
如果缓冲区满了,就不能写入了,就只能等待子进程读取。所以这里子进程读取一次的数据可能是父进程写了好几次的结果
3.管道提供的是面向流式的通信服务--面向字节流(需要对应的协议)
你写了十次,但是我可能一次就全部都读取完了。
4.管道是基于文件的,文件的生命周期是随进程的,管道的生命周期是随进程的。
写入的一方,fd没有关闭,如果有数据,就读,没有数据,就等
写入的一方,fd关闭,读取的一方,read会返回0,表示读到了文件的结尾(将缓冲区中的内容读取完毕之后,就可以退出了!)
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- using namespace std;
- int main()
- {
- //1.创建管道
- int pipefd[2]={0};//pipefd[0]:读端,pipefd[1]:写端
- int n=pipe(pipefd);
- //在debug模式下assert是有效的,但是release版本下是会无效的
- assert(n!=-1);
- //所以我们这里需要写下面的代码,证明n被使用过
- (void)n;
-
- //如果是DEBUG模式下就不打印了,相当于就是注释掉了
- #ifdef DEBUG
- cout<<"pipefd[0]"<
0]< - cout<<"pipefd[1]"<
1]< - #endif
-
- //2.创建子进程
- pid_t id=fork();
- assert(id!=-1);
- if(id==0)
- {
- //子进程
- //3.构建单向通信的信道
- //父进程写入,子进程读取,让父子关闭不需要的文件描述符
- //3.1关闭子进程不需要的fd
- //子进程进行读取,将写端给关闭
- close(pipefd[1]);
- char buffer[1024*8];
- while (true)
- {
- //从0号文件描述符中读取,读取到缓冲区buffer中
- size_t s=read(pipefd[0],buffer,sizeof(buffer)-1);
- if(s>0)
- {
- // sleep(20);
- //写入的一方,fd没有关闭,如果有数据,就读,没有数据,就等
- //写入的一方,fd关闭,读取的一方,read会返回0,表示读到了文件的结尾
- //添加\0
- buffer[s]=0;
- cout<<"child get a message["<<getpid()<<"] Father#"<
- }
- else if(s==0)
- {
- cout<<"writer quit(father),me quit"<
- break;
- }
- }
- // close(pipefd[0]);
- exit(0);
- }
- //父进程
- //3.构建单向通信的信道
- //父进程进行写入,将读取端进行关闭
- close(pipefd[0]);
- string message="我是父进程,我正在给你发消息";
- int count=0;
- char send_buffer[1024*8];
- while(true)
- {
- //3.2构建一个变化的字符串
- //将printf的内容格式化到字符串中
- snprintf(send_buffer,sizeof(send_buffer),"%s[%d]:%d",message.c_str(),getpid(),count++);
- //3.3写入
- //这里strlen不需要+1,\0写入也没有意义。
- write(pipefd[1],send_buffer,strlen(send_buffer));
-
- //3.4故意sleep
- sleep(1);
- cout<
- if(count==5)
- {
- cout<<"writer quit(father)"<
- break;
- }
-
- }
- close(pipefd[1]);
- pid_t ret=waitpid(id,nullptr,0);
- cout<<"id:"<
"ret:"< - assert(ret>0);
- (void)ret;
- //子进程中的pipefd[0]关闭可写可不写,因为进程退出了,进程中的文件描述符也会被关掉
- return 0;
- }
只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
管道提供流式服务
一般而言,进程退出,管道释放,所以管道的生命周期随进程
一般而言,内核会对管道操作进行同步与互斥
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。
5.管道是单向通信的,就会半双工通信的一种特殊情况
作为通信的一方,要么我在发送,要么我在接收,我不能同时接收和发送,这种就称为半双工通信。
有时候呢,我们既可以收,又可以发,这就称为全双工通信。
比防说我们在听老师上课,老师在将,我们在听,这就是半双工通信。
但是如果两个人在吵架,你在吵的时候我也在吵,同时我也在听你说什么,你也在听我说什么,这就是全双工通信。
a.写快,读慢,写满的时候就不能再写了
b.写慢,读快,管道没有数据的时候,读的这一方就必须等待
c.写关,读0,表示读到了文件结尾
d.读关,写继续写,OS终止写进程
3.扩展
如何写一个进程池?
我们给父进程和每一个子进程建立一个管道,并以固定大小的方式command_code(4kb),给我们的子进程发送指令。
创建Makefile文件
- ProcessPool:ProcessPool.cc
- g++ -o $@ $^ -std=c++11 -DEBUG
- .PHONY:clean
- clean:
- rm -f ProcessPool
创建我们的任务头文件Task.cpp
- #pragma once
- #include
- #include
- #include
- #include
- #include
- #include
- //返回void,参数为()
- typedef std::function<void()> func;
- std::vector
callbacks; - std::unordered_map<int,std::string> desc;
- void readMySQL()
- {
- std::cout<<"sub process["<<getpid()<<"]执行访问数据库的任务\n"<
- }
-
- void execuleUrl()
- {
- std::cout<<"sub process["<<getpid()<<"]执行url解析\n"<
- }
-
- void cal()
- {
- std::cout<<"sub process["<<getpid()<<"]执行加密任务\n"<
- }
-
- void save()
- {
- std::cout<<"sub process["<<getpid()<<"]执行数据持久化任务\n"<
- }
-
- void load()
- {
- desc.insert({callbacks.size(),"readmySQL:读取数据库"});
- callbacks.push_back(readMySQL);
- desc.insert({callbacks.size(),"execuleUrl:进行URL解析"});
- callbacks.push_back(execuleUrl);
- desc.insert({callbacks.size(),"cal:进行加密计算"});
- callbacks.push_back(cal);
- desc.insert({callbacks.size(),"save:进行数据的文件保存"});
- callbacks.push_back(save);
- }
-
- void showHandler()
- {
- for(const auto& iter:desc)
- {
- std::cout<
"\t"< - }
- }
- //返回有多少个任务
- int handlerSize()
- {
- return callbacks.size();
- }
创建我们的主程序文件
自动派发任务的版本
- #include
- #include
- #include
- #include
- #include
- #include
- #include"Task.hpp"
- #include
- #include
- //默认创建的进程个数
- #define PROCESS_NUM 5
-
- using namespace std;
-
-
- //等待命令
- int waitCommand(int waitfd,bool&quit)
- {
- uint32_t command=0;
- ssize_t s=read(waitfd,&command,sizeof(command));
- //如果读取到对应的0,那么就是文件描述符关掉了,就直接退出
- if(s==0)
- {
- quit=true;
- return -1;
- }
- //看看有没有读取成功
- assert(s==sizeof(uint32_t));
- return command;
- }
-
- //拖过文件描述符像进程发送命令
- void SendAndWakeup(pid_t who,int fd,uint32_t command)
- {
- write(fd,&command,sizeof(command));
- cout<<"main process: call process"<
"execute"<"through"< - }
-
- int main()
- {
- //将任务装载进来
- load();
- //子进程的pid_t,信道pipfd的键值对
- //表示一个一个进程相关的信息
- vector
pid_t,int>> slots; - //先创建多个进程
- for(int i=0;i
- {
- //创建管道
- int pipedf[2]={0};
- int n=pipe(pipedf);
- assert(n==0);
- (void)n;
-
- //创建子进程
- pid_t id=fork();
- assert(id!=-1);
- //子进程我们让他进行读取
- if(id==0)
- {
- //child,进行读取,关闭写入端
- close(pipedf[1]);
- while(true)
- {
- //等待命令
- bool quit=false;
- int command=waitCommand(pipedf[0],quit);//如果对方不发,我们就阻塞
- if(quit)
- break;
- //执行对应的命令
- if(command>=0&&command<handlerSize())
- {
- callbacks[command]();
- }
- else
- {
- cout<<"非法command"<
- }
- }
- exit(1);
- }
- //father,进行写入,关闭读取端
- close(pipedf[0]);
- slots.push_back(pair<pid_t,int>(id,pipedf[1]));
- }
- //父进程派发任务
- //将任务均衡地拍付给每一个任务称为单机版的负载均衡
- srand((unsigned long)time(nullptr) ^ getpid()^2332313L);//让我们的数据源更随机
- while(true)
- {
- int command=rand()%handlerSize();
- //采用随机数的方式,选择子进程来完成任务,这是一种随机数的方式来实现负载均衡。
- int choice=rand()%slots.size();
- //布置任务
- //把任务给指定的进程
- SendAndWakeup(slots[choice].first,slots[choice].second,command);
- sleep(1);
- }
- //关闭fd,结束所有的进程
- //关闭所有的写的文件描述符
- //所有的子进程在读取完之后都会退出
- for(const auto &slot:slots)
- {
- close(slot.second);
- }
-
- //回收所有的子进程。
- for(const auto &slot:slots)
- {
- //等待全部的子进程
- waitpid(slot.first,nullptr,0);
- }
- }
手动派发任务的版本
- #include
- #include
- #include
- #include
- #include
- #include
- #include"Task.hpp"
- #include
- #include
- //默认创建的进程个数
- #define PROCESS_NUM 5
-
- using namespace std;
-
-
- //等待命令
- int waitCommand(int waitfd,bool&quit)
- {
- uint32_t command=0;
- ssize_t s=read(waitfd,&command,sizeof(command));
- //如果读取到对应的0,那么就是文件描述符关掉了,就直接退出
- if(s==0)
- {
- quit=true;
- return -1;
- }
- //看看有没有读取成功
- assert(s==sizeof(uint32_t));
- return command;
- }
-
- //拖过文件描述符像进程发送命令
- void SendAndWakeup(pid_t who,int fd,uint32_t command)
- {
- write(fd,&command,sizeof(command));
- cout<<"main process: call process"<
"execute"<"through"< - }
-
- int main()
- {
- //将任务装载进来
- load();
- //子进程的pid_t,信道pipfd的键值对
- //表示一个一个进程相关的信息
- vector
pid_t,int>> slots; - //先创建多个进程
- for(int i=0;i
- {
- //创建管道
- int pipedf[2]={0};
- int n=pipe(pipedf);
- assert(n==0);
- (void)n;
-
- //创建子进程
- pid_t id=fork();
- assert(id!=-1);
- //子进程我们让他进行读取
- if(id==0)
- {
- //child,进行读取,关闭写入端
- close(pipedf[1]);
- while(true)
- {
- //等待命令
- bool quit=false;
- int command=waitCommand(pipedf[0],quit);//如果对方不发,我们就阻塞
- if(quit)
- break;
- //执行对应的命令
- if(command>=0&&command<handlerSize())
- {
- callbacks[command]();
- }
- else
- {
- cout<<"非法command:"<
- }
- }
- exit(1);
- }
- //father,进行写入,关闭读取端
- close(pipedf[0]);
- slots.push_back(pair<pid_t,int>(id,pipedf[1]));
- }
- //父进程派发任务
- //将任务均衡地拍付给每一个任务称为单机版的负载均衡
- srand((unsigned long)time(nullptr) ^ getpid()^2332313L);//让我们的数据源更随机
- while(true)
- {
- int select;
- int command;
- cout<<"##########################################"<
- cout<<"# 1. show functions 2.send command #"<
- cout<<"##########################################"<
- cout<<"Please Select>"<
- cin>>select;
- if(select==1)
- {
- showHandler();
- }
- else if(select=2)
- {
- cout<<"Enter Your Command>";
- //选择任务
- cin>>command;
- //发送命令,并且唤醒子进程
- //选择进程
-
- //选择一个任务,如果这个任务是从网络中来的?
- // int command=rand()%handlerSize();
- //采用随机数的方式,选择进程来完成任务,这是一种随机数的方式来实现负载均衡。
- int choice=rand()%slots.size();
- // //布置任务
- // //把任务给指定的进程
- SendAndWakeup(slots[choice].first,slots[choice].second,command);
- // sleep(1);
- }else
- {
- cout<<"该指令不再可以选择的范围内"<
- continue;
- }
- }
- //关闭fd,结束所有的进程
- //关闭所有的写的文件描述符
- //所有的子进程在读取完之后都会退出
- for(const auto &slot:slots)
- {
- close(slot.second);
- }
-
- //回收所有的子进程。
- for(const auto &slot:slots)
- {
- //等待全部的子进程
- waitpid(slot.first,nullptr,0);
- }
- }
管道读写规则
当没有数据可读时
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将不再保证写入的原子性。
三、命名管道
要两个不同的进程看到同一份资源,才能建立起管道。
磁盘上可以创建一个管道文件。
管道文件可以被打开,但是不会将内存数据进行刷新到磁盘。
该文件一定在系统路径中,路径具有唯一性。
双方进程就可以通过管道文件的路径,看到同一份资源!
与匿名文件的差别是匿名文件仅仅是在内存中创建了一个文件,由父子进程进行访问
而命名管道的管道文件也是内存中的文件,但是在磁盘上有一个映射,有文件目录。
mkfifo
mkfifo name_pipe
p开头的,我们将其称为管道文件
echo "hello world" >name_pipe
我写了,但是对方还没有打开,此时这个文件就处于阻塞状态
我们将数据从管道中独取出来
cat
现在我们尝试循环输入hello world
while :; do echo "hello world"; sleep 1; done >name_pipe
在另外一个终端进行接收
cat
删除管道文件
unlink name_pipe
制作管道实验
man 3 mkfifo
1.日志头文件Log.hpp
- #ifndef _LOG_H_
- #define _LOG_H_
-
- #include
- #include
-
-
- #define Debug 0
- #define Notice 1
- #define Waring 2
- #define Error 3
-
-
- //定义日志的几种状态
- const std::string msg[]={
- "Debug",
- "Notice",
- "Warning",
- "Error"
- };
- std::ostream &Log(std::string message,int level)
- {
- //时间,日志信息
- std::cout<<"|"<<(unsigned)time(nullptr)<<"|"<
"|"< - return std::cout;
- }
-
- #endif
2.公共头文件comm.hpp
- #ifndef _COMM_H_
- #define _COMM_H_
-
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include "Log.hpp"
- using namespace std;
- //设置管道文件的权限
- #define MODE 0666
- //缓冲区的大小
- #define SIZE 128
- //管道文件的路径
- string ipcPath="./fifo.ipc";
-
-
- #endif
3.客户端文件client.cc
- #include"comm.hpp"
-
- int main()
- {
- //1.获取管道文件,以写的方式打开
- int fd=open(ipcPath.c_str(),O_WRONLY);
- if(fd<0)
- {
- perror("open");
- exit(1);
- }
- //2.ipc过程
- //创建要发送的字符串
- string buffer;
- while(true)
- {
- cout<<"Please Enter Messaage Line :> ";
- //将字符串放入buffer中
- std::getline(std::cin,buffer);
- //将buffer写入管道文件中
- write(fd,buffer.c_str(),buffer.size());
- }
- //3.关闭文件
- close(fd);
- return 0;
- }
4.服务端文件server.cc
- #include"comm.hpp"
- #include
- static void getMessage(int fd)
- {
- char buffer[SIZE];
- while(true)
- {
- memset(buffer,'\0',sizeof(buffer));
- ssize_t s=read(fd,buffer,sizeof(buffer)-1);
- if(s>0){
- cout<<"["<<getpid()<<"]"<<"client say>"<
- }else if(s=0){
- //end of file
- cerr<<"["<<getpid()<<"]"<<"read end of file,clien quit,server quit too!"<
- break;
- }else{
- //read error
- perror("read");
- break;
- }
- }
- }
- int main()
- {
- //1、创建管道文件
- if(mkfifo(ipcPath.c_str(),MODE)<0)
- {
- perror("mkfifo");
- exit(1);
-
- }
- //写日志
- Log("创建管道文件成功",Debug)<<"step 1"<
- //2.正常的文件操作
- int fd=open(ipcPath.c_str(),O_RDONLY);
- if(fd<0)
- {
- perror("open");
- exit(2);
- }
- Log("打开管道文件成功",Debug)<<"step 2"<
-
- //这里我们创建了三个进程来读取
- int nums=3;
- for(int i=0;i
- {
- pid_t id=fork();
- if(id==0)
- {
- //通信
- getMessage(fd);
- exit(2);
- }
- }
- //回收子进程
- for(int i=0;i
- {
- waitpid(-1,nullptr,0);
- }
- //3.编写正常的通信代码
- //创建缓冲区
- char buffer[SIZE];
- while(true)
- {
- //将缓冲区清空
- memset(buffer,'\0',sizeof(buffer));
- //将管道中的内容读取到缓冲区中
- ssize_t s=read(fd,buffer,sizeof(buffer)-1);
- //如果读取成功的话
- if(s>0){
- cout<<"["<<getpid()<<"]"<<"client say>"<
- }else if(s=0){
- //如果读取到了结尾的位置
- //end of file
- cerr<<"["<<getpid()<<"]"<<"read end of file,clien quit,server quit too!"<
- break;
- //如果读取失败了
- }else{
- //read error
- perror("read");
- break;
- }
- }
- //4.关闭文件
- close(fd);
- Log("关闭管道文件成功",Debug)<<"step 3"<
- unlink(ipcPath.c_str());//通信完毕,删除文件
- Log("删除管道文件成功",Debug)<<"step 4"<
- return 0;
- }
使用makefile创建我们的工程
- .PHONY:all
- all:client mutiServer
-
- client:client.cc
- g++ -o $@ $^ -std=c++11
- mutiServer:server.cc
- g++ -o $@ $^ -std=c++11
- .PHONY:clean
- clean:
- rm -f client mutiServer
我们观察到这里是我们创建的三个管道争抢着接收我们的客户端发送的信息的,谁抢到了谁就将信息发送出来。
四、system v共享内存
1.原理:
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
共享内存的建立
这一块共享内存既不属于左边的进程也不属于右边的进程,它属于操作系统。共享内存的提供者是操作系统。
共享内存是操作系统单独设立的内核模块,专门负责进程间通信。
如果每一对进程通信都需要用共享内存,那么操作系统要不要对其进行管理呢?
(管道的管理和文件系统的管理差不多,所以操作系统不用单独地去管理)
先描述,再组织!
重新理解共享内存
共享内存=共享内存块+对应的共享内存的内核数据结构
shmget
创建并获取共享内存,size为大小,shmflg为IPC_CREAT或者IPC_EXCL,其中IPC_CREAT就是0
IPC_CREAT:创建共享内存的时候,如果系统底层已经存在,那么直接获取之,并且返回,如果不存在,就创建之,并返回。
IPC_EXCL:单独使用IPC_EXCL是没有意义的
IPC_CREAT和IPC_EXCL合起来使用,如果底层不存在,创建之,并返回,如果底层存在,出错返回。
返回成功,一定是一个全新的shm。
返回值共享内存的用户层标识符,类似曾经的fd
这里的key是什么呢
要通信的对方进程,怎么保证对方能看到,并且看到的就是我创建饿共享内存呢?
通过key,key的数据是多少不重要,只要能够在系统中唯一即可。server&&client使用同一个key,只要key值相同,就是看到了同一个共享内存。
使用同样的算法规则,形成唯一的key值就可以了。
只有创建的时候用key,大部分情况用户访问共享内存,都用的是shmid
ftok
需要传入一个路径(pathname)和项目ID,
ftok算法就是将路径和项目id合并起来,形成一个唯一值。但是由于这里创建出来的键值底层可能也会有,所以我们这里的ftok不一定会创建成功
让我们客户端和服务器端的key值相同
客户端
- #include"comm.hpp"
- int main()
- {
- key_t k =ftok(PATH_NAME,PROJ_ID);
- Log("create key done",Debug)<<"Client say :"<
- return 0;
- }
服务器端
- #include"comm.hpp"
- int main()
- {
- //1.创建公共的key值
- key_t k =ftok(PATH_NAME,PROJ_ID);
- Log("create key done",Debug)<<"Server say:"<
-
- return 0;
- }
comm.hpp
- #pragma once
-
- #include
- #include
- #include
- #include
- #include
- #include "Log.hpp"
- using namespace std;
-
- #define PATH_NAME "/home/zhuyuan"
- #define PROJ_ID 0x66
makefile
- .PHONY:all
- all:client server
-
- client:shmClient.cc
- g++ -o $@ $^ -std=c++11
- server:shmServer.cc
- g++ -o $@ $^ -std=c++11
- .PHONY:clean
- clean:
- rm -f client server
ipcs -m查看系统当中的共享内存
我运行完了,但是共享内存还在。
ipcrm -m关闭共享内存
system V IPC资源的生命周期随内核!除非重启,否则一直存在
1.手动删除
2.代码删除
我们可以使用下面的shmctl来关闭共享内存
shmctl
监控脚本
while :; do ipcs -m ;sleep 1; done
attach关联 detach不关联,
n表示个数,attach表示关联,表示有多少个进程和我们的共享内存是关联的
SHMAT
接下来我们就要创建共享内存
shmat的参数中,shmaddr共享内存的虚拟地址,我们写0,让操作系统帮我们填写,shmflg,选择挂载方式,我们写0,让操作系统进行填写
SHMDT
然后将我们创建的共享内存和我们的程序关联起来,shmaddr就是我们共享内存的虚拟地址
1.创建makefile文件
- .PHONY:all
- all:client server
-
- client:shmClient.cc
- g++ -o $@ $^ -std=c++11
- server:shmServer.cc
- g++ -o $@ $^ -std=c++11
- .PHONY:clean
- clean:
- rm -f client server
2.日志头文件Log.hpp
- #ifndef _LOG_H_
- #define _LOG_H_
-
- #include
- #include
-
-
- #define Debug 0
- #define Notice 1
- #define Waring 2
- #define Error 3
-
- //不同的状态
- const std::string msg[]={
- "Debug",
- "Notice",
- "Warning",
- "Error"
- };
- //将时间,状态和信息介入日志中,并且打印出来
- std::ostream &Log(std::string message,int level)
- {
- //时间,日志信息
- std::cout<<"|"<<(unsigned)time(nullptr)<<"|"<
"|"< - return std::cout;
- }
-
- #endif
3.共享的头文件comm.hpp
- #pragma once
-
- #include
- #include
- #include
- #include
- #include
- #include "Log.hpp"
- #include
- #include
- using namespace std;
-
- //定义项目的路径,为了我们的key值的生成
- #define PATH_NAME "/home/zhuyuan"
- //定义项目的id,这个值可以自己取
- #define PROJ_ID 0x66
- #define SHM_SIZE 4096//共享内存的大小,最好是页(PAGE:4096)的整数倍
4.客户端文件shmClient.cc
- #include"comm.hpp"
- int main()
- {
- //创建我们上述的key值,用于我们的两个进程都找到这一块共享内存
- key_t k =ftok(PATH_NAME,PROJ_ID);
- //如果key值创建失败了,也就是我们的底层已经有这一个key值了,它会返回-1
- if(k<0)
- {
- Log("create key failed",Error)<<"Client say :"<
- exit(1);
- }
- //如果创建成功,就将其写入日志当中
- Log("create key done",Debug)<<"Client say :"<
-
- //获取共享内存
- //shmid(key值,需要开辟的共享内存的大小,也就是定义在comm.hpp中的4096,以及创建共享内存的模式0就是我们上述的IPC_CREATE)
- int shmid=shmget(k,SHM_SIZE,0);
- //如果我们的共享内存开辟失败了,就返回失败信息,并且退出
- if(shmid<0)
- {
- Log("create key failed",Error)<<"Client say :"<
- exit(2);
- }
- Log("create key success",Debug)<<"Client say :"<
- //睡眠10秒,便于我们下面的测试实验
- sleep(10);
-
- //shmat(打开的共享内存的地址,链接地址shmaddr设置为nullptr,系统会帮我们填写,shmflg填写为0,也就是让系统帮我们填写权限)
- char *shmaddr=(char*)shmat(shmid,nullptr,0);
- //将共享内存段链接进地址空间
- //如果链接失败了,就直接打印日志并返回
- if(shmaddr==nullptr)
- {
- Log("attach shm failed",Error)<<"Client say :"<
- exit(3);
- }
- Log("create shm success",Debug)<<"Client say :"<
- sleep(10);
-
- //去关联
- //将共享内存段与当前进程脱离
- int n=shmdt(shmaddr);
- //如果脱离失败了,就打印日志
- assert(n!= -1);
- Log("detach shm success",Debug)<<"Client say :"<
- sleep(10);
-
- //client要不要chmctl删除呢?不需用server会删的。
- return 0;
- }
5.服务端头文件shmServer.cc
- #include"comm.hpp"
-
- //将k转换成十六进制进行输出
- string TransToHex(key_t k)
- {
- char buffer[32];
- snprintf(buffer,sizeof buffer,"0x%x",k);
- return buffer;
- }
- int main()
- {
- //1.创建公共的key值
- //这里的创建key值的参数和我们的客户端是一样的,所以我们能够得到一个和客户端相同的key值
- key_t k =ftok(PATH_NAME,PROJ_ID);
- //如果创建失败了
- assert(k!=-1);
- Log("create key done",Debug)<<"Server say:"<<TransToHex(k)<
-
- //2.创建共享内存,建议要创建一个全新的共享内存--通信的发起者
- //0666表明我们的操作权限
- //这里的标志位我们是通过或的方式拼装的
- int shmid =shmget(k,SHM_SIZE,IPC_CREAT|IPC_EXCL|0666);
- //如果创建失败了就写日志,并且退出
- if(shmid==-1)
- {
- perror("shmget");
- exit(1);
- }
- Log("create shm done",Debug)<<" shmid :"<
-
- sleep(10);
- //3.将指定的共享内存,挂载到自己的地址空间
- char *shmaddr=(char*)shmat(shmid,nullptr,0);
- Log("attach shm done",Debug)<<" shmid :"<
-
- sleep(10);
-
- //这里就是通信的逻辑了
-
-
- //4.将指定的共享内存,从自己的地址中空间中去关联
- int n=shmdt(shmaddr);
- assert(n!=-1);
- (void)n;
- Log("detach shm done",Debug)<<" shmid :"<
- sleep(10);
-
-
- //last.删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
- //shmid是我们共享内存的地址
- n=shmctl(shmid,IPC_RMID,nullptr);
- assert(n!=-1);
- (void)n;
- Log("del shm done",Debug)<<" shmid :"<
-
- return 0;
- }
这里我们先启动server,也就是服务器的程序,然后启动客户端的程序,然后在监控脚本中查看我们的共享内存的连接状况
监控脚本的结果截图
这里我们的服务器端程序先被启动,创建了共享内存,在将共享内存挂载到自己的地中空间之后休眠了10秒,然后再去除与共享内存的关联,然后再等待10秒过后,我们的客户端将共享内存的空间给释放
这里我们的客户端程序后被启动,在开辟了共享内存之后休眠了10秒,在链接了共享内存之后由休眠了10秒,然后再等待10秒过后
所以我们观察到我们的共享内存的链技术先是从0变成了1,也就是我们的服务器端连接了,然后再变成了2,也就是我们的客户端也连接了,然后又变成了1,也就是我们的服务器端退出了,然后变成0,也就是我们的客户端也退出了
堆栈之间的共享区域是属于内核的还是用户的?
这一部分区域是属于用户空间的。
也就是不用经过系统调用,可以直接访问
双方进程如果想要通信,直接进行内存级的读和写即可。
为什么pipe,fifo都要通过read,write来进行通信,为什么?
像read和write都是系统调用接口。
这样的接口调用都是属于系统调用。
因为其调用的管道其实都是属于文件,而文件是内核当中的特定数据结构,是由操作系统进行维护的。
而我们的共享内存是在堆栈之间的,是属于用户的空间
我们上面的所有的工作 属于什么工作呢?
让不同的进程看到同一份资源
6.初步实现进程间通信
shmClient
- #include"comm.hpp"
- int main()
- {
- Log("child pid is:",Debug)<<getpid()<
- //创建我们上述的key值,用于我们的两个进程都找到这一块共享内存
- key_t k =ftok(PATH_NAME,PROJ_ID);
- //如果key值创建失败了,也就是我们的底层已经有这一个key值了,它会返回-1
- if(k<0)
- {
- Log("create key failed",Error)<<"Client say :"<
- exit(1);
- }
- //如果创建成功,就将其写入日志当中
- Log("create key done",Debug)<<"Client say :"<
-
- //获取共享内存
- //shmid(key值,需要开辟的共享内存的大小,也就是定义在comm.hpp中的4096,以及创建共享内存的模式0就是我们上述的IPC_CREATE)
- int shmid=shmget(k,SHM_SIZE,0);
- //如果我们的共享内存开辟失败了,就返回失败信息,并且退出
- if(shmid<0)
- {
- Log("create key failed",Error)<<"Client say :"<
- exit(2);
- }
- Log("create key success",Debug)<<"Client say :"<
- //睡眠10秒,便于我们下面的测试实验
- // sleep(10);
-
- //shmat(打开的共享内存的地址,链接地址shmaddr设置为nullptr,系统会帮我们填写,shmflg填写为0,也就是让系统帮我们填写权限)
- char *shmaddr=(char*)shmat(shmid,nullptr,0);
- //将共享内存段链接进地址空间
- //如果链接失败了,就直接打印日志并返回
- if(shmaddr==nullptr)
- {
- Log("attach shm failed",Error)<<"Client say :"<
- exit(3);
- }
- Log("create shm success",Debug)<<"Client say :"<
- // sleep(10);
-
- //使用
- //client将共享内存看做一个char类型的buffer
- char a='a';
- for(;a<='c';a++)
- {
- //我们是每一次都向shmaddr[共享内存的起始地址]写入
- snprintf(shmaddr,SHM_SIZE-1,"hello serve,我是其它进程,我的pid,%d,inc:%c\n",getpid(),a);
- sleep(5);
- }
- //想共享内存中拷贝一个quit
- strcpy(shmaddr,"quit");
- //去关联
- //将共享内存段与当前进程脱离
- int n=shmdt(shmaddr);
- //如果脱离失败了,就打印日志
- assert(n!= -1);
- Log("detach shm success",Debug)<<"Client say :"<
- // sleep(10);
-
- //client要不要chmctl删除呢?不需用server会删的。
- return 0;
- }
shmServer
- #include"comm.hpp"
-
- //将k转换成十六进制进行输出
- string TransToHex(key_t k)
- {
- char buffer[32];
- snprintf(buffer,sizeof buffer,"0x%x",k);
- return buffer;
- }
- int main()
- {
- //1.创建公共的key值
- //这里的创建key值的参数和我们的客户端是一样的,所以我们能够得到一个和客户端相同的key值
- key_t k =ftok(PATH_NAME,PROJ_ID);
- //如果创建失败了
- assert(k!=-1);
- Log("create key done",Debug)<<"Server say:"<<TransToHex(k)<
-
- //2.创建共享内存,建议要创建一个全新的共享内存--通信的发起者
- //0666表明我们的操作权限
- //这里的标志位我们是通过或的方式拼装的
- int shmid =shmget(k,SHM_SIZE,IPC_CREAT|IPC_EXCL|0666);
- //如果创建失败了就写日志,并且退出
- if(shmid==-1)
- {
- perror("shmget");
- exit(1);
- }
- Log("create shm done",Debug)<<" shmid :"<
-
- // sleep(10);
- //3.将指定的共享内存,挂载到自己的地址空间
- char *shmaddr=(char*)shmat(shmid,nullptr,0);
- Log("attach shm done",Debug)<<" shmid :"<
-
- // sleep(10);
-
- //这里就是通信的逻辑了
- //将共享内存当成一个大字符串
- char bvuffeer[SHM_SIZE];
- for(;;)
- {
- //共享内存上读取到了什么就打印什么
- printf("%s\n",shmaddr);
- if(strcmp(shmaddr,"quit")==0)
- {
- break;
- }
- sleep(1);
- }
-
- strcmp(shmaddr,"quit");
-
- //4.将指定的共享内存,从自己的地址中空间中去关联
- int n=shmdt(shmaddr);
- assert(n!=-1);
- (void)n;
- Log("detach shm done",Debug)<<" shmid :"<
- // sleep(10);
-
-
- //last.删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
- //shmid是我们共享内存的地址
- n=shmctl(shmid,IPC_RMID,nullptr);
- assert(n!=-1);
- (void)n;
- Log("del shm done",Debug)<<" shmid :"<
-
- return 0;
- }
结论1:只要是通信双方使用共享内存,一方直接向共享内存中写入数据,另一方马上就可以看见这些数据
所以共享内存是所有的进程间通信(IPC)速度最快的!
为什么呢?
因为共享内存不需要过多的拷贝。
不需要将操作数据交给操作系统
管道
1.从键盘到我们的自己定义的缓冲区是我们的第一次拷贝
2.从我们自己定义的缓冲区到管道文件是第二次拷贝
3.从我们的管道文件拷贝到我们的用户层缓冲区是我们的第三次拷贝
4.从我们的自己定义的缓冲区到打印到我们的屏幕上是我们的第四次拷贝
共享内存
进程间通信(客户端我们自己输入,服务器端读取数据)
shmClient
- #include"comm.hpp"
- int main()
- {
- Log("child pid is:",Debug)<<getpid()<
- //创建我们上述的key值,用于我们的两个进程都找到这一块共享内存
- key_t k =ftok(PATH_NAME,PROJ_ID);
- //如果key值创建失败了,也就是我们的底层已经有这一个key值了,它会返回-1
- if(k<0)
- {
- Log("create key failed",Error)<<"Client say :"<
- exit(1);
- }
- //如果创建成功,就将其写入日志当中
- Log("create key done",Debug)<<"Client say :"<
-
- //获取共享内存
- //shmid(key值,需要开辟的共享内存的大小,也就是定义在comm.hpp中的4096,以及创建共享内存的模式0就是我们上述的IPC_CREATE)
- int shmid=shmget(k,SHM_SIZE,0);
- //如果我们的共享内存开辟失败了,就返回失败信息,并且退出
- if(shmid<0)
- {
- Log("create key failed",Error)<<"Client say :"<
- exit(2);
- }
- Log("create key success",Debug)<<"Client say :"<
- //睡眠10秒,便于我们下面的测试实验
- // sleep(10);
-
- //shmat(打开的共享内存的地址,链接地址shmaddr设置为nullptr,系统会帮我们填写,shmflg填写为0,也就是让系统帮我们填写权限)
- char *shmaddr=(char*)shmat(shmid,nullptr,0);
- //将共享内存段链接进地址空间
- //如果链接失败了,就直接打印日志并返回
- if(shmaddr==nullptr)
- {
- Log("attach shm failed",Error)<<"Client say :"<
- exit(3);
- }
- Log("create shm success",Debug)<<"Client say :"<
- // sleep(10);
-
- while(true)
- {
- //从0号文件中读取数据,也就是从我们的标准输入读取数据,直接放入我们的缓冲区中,大小为SHM_SIZE
- ssize_t s=read(0,shmaddr,SHM_SIZE-1);
- //如果我们的数据成功读取了
- if(s>0)
- { //我们从标准输入读取到的字符串假设是abcd回车,也就是abcd\n
- //但是由于我们是想要和沃尔玛的呢字符串quit进行比较,所以我们需要将\n修改成是\0
- shmaddr[s-1]=0;
- if(strcmp(shmaddr,"quit")==0)
- {
- break;
- }
- }
- }
-
- //去关联
- //将共享内存段与当前进程脱离
- int n=shmdt(shmaddr);
- //如果脱离失败了,就打印日志
- assert(n!= -1);
- Log("detach shm success",Debug)<<"Client say :"<
- // sleep(10);
-
- //client要不要chmctl删除呢?不需用server会删的。
- return 0;
- }
shmServer
- #include"comm.hpp"
-
- //将k转换成十六进制进行输出
- string TransToHex(key_t k)
- {
- char buffer[32];
- snprintf(buffer,sizeof buffer,"0x%x",k);
- return buffer;
- }
- int main()
- {
- //1.创建公共的key值
- //这里的创建key值的参数和我们的客户端是一样的,所以我们能够得到一个和客户端相同的key值
- key_t k =ftok(PATH_NAME,PROJ_ID);
- //如果创建失败了
- assert(k!=-1);
- Log("create key done",Debug)<<"Server say:"<<TransToHex(k)<
-
- //2.创建共享内存,建议要创建一个全新的共享内存--通信的发起者
- //0666表明我们的操作权限
- //这里的标志位我们是通过或的方式拼装的
- int shmid =shmget(k,SHM_SIZE,IPC_CREAT|IPC_EXCL|0666);
- //如果创建失败了就写日志,并且退出
- if(shmid==-1)
- {
- perror("shmget");
- exit(1);
- }
- Log("create shm done",Debug)<<" shmid :"<
-
- // sleep(10);
- //3.将指定的共享内存,挂载到自己的地址空间
- char *shmaddr=(char*)shmat(shmid,nullptr,0);
- Log("attach shm done",Debug)<<" shmid :"<
-
- // sleep(10);
-
- //这里就是通信的逻辑了
- //将共享内存当成一个大字符串
- char bvuffeer[SHM_SIZE];
- for(;;)
- {
- //共享内存上读取到了什么就打印什么
- printf("%s\n",shmaddr);
- if(strcmp(shmaddr,"quit")==0)
- {
- break;
- }
- sleep(1);
- }
-
- strcmp(shmaddr,"quit");
-
- //4.将指定的共享内存,从自己的地址中空间中去关联
- int n=shmdt(shmaddr);
- assert(n!=-1);
- (void)n;
- Log("detach shm done",Debug)<<" shmid :"<
- // sleep(10);
-
-
- //last.删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
- //shmid是我们共享内存的地址
- n=shmctl(shmid,IPC_RMID,nullptr);
- assert(n!=-1);
- (void)n;
- Log("del shm done",Debug)<<" shmid :"<
-
- return 0;
- }
如果管道里面没有数据了,我们就没有办法读取了,如果我们的管道已经被写满了,我们的写入端就没有办法写入了。这就是管道的阻塞,也就是管道的访问控制,可以协调管道两端的访问控制。
结论二:共享内存缺乏访问控制。
共享内存天生就是为了给我们提供一种快速地访问内存的操作机制,所以我们的共享内存没有任何关于访问控制。
无论共享内存中有没有数据,我们的server都会不停地读取,甚至读取和写入方根本就不知道对方的存在!不会因为没有内容就没有办法读取
这会导致并发问题
假设我们的客户端想要完整地发送hello world
如果没有控制的话,我们就可能在客户端只输入了hello的时候,我们的服务器端就将数据读取走了。
我们就将其称为数据不一致问题
上面的相关问题称为临界区和临界区资源问题
用管道控制共享内存的访问控制
如果我想实现共享内存的访问控制呢?
能实现,可以通过管道来实现。
可以将管道的同步共享能力迁移到我们的共享内存中
也就是我们的数据写完了,让server读取的时候,server才能读取,没有让server读的时候,server就不能读取
log.hpp
- #ifndef _LOG_H_
- #define _LOG_H_
-
- #include
- #include
-
-
- #define Debug 0
- #define Notice 1
- #define Waring 2
- #define Error 3
-
- //不同的状态
- const std::string msg[]={
- "Debug",
- "Notice",
- "Warning",
- "Error"
- };
- //将时间,状态和信息介入日志中,并且打印出来
- std::ostream &Log(std::string message,int level)
- {
- //时间,日志信息
- std::cout<<"|"<<(unsigned)time(nullptr)<<"|"<
"|"< - return std::cout;
- }
-
- #endif
comm.hpp
- #pragma once
-
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include "Log.hpp"
- using namespace std;
-
- #define PATH_NAME "/home/zhuyuan"
- #define PROJ_ID 0x66
- #define SHM_SIZE 4096//共享内存的大小,最好是页(PAGE:4096)的整数倍
- #include
-
- //创建一个管道文件名的宏定义
- #define FIFO_NAME "./fifo"
-
- //创建一个类,用于创建管道
- class Init
- {
- public:
- Init()
- {
- //将权限掩码设置为0
- umask(0);
- //创建我们的管道,分别传入管道文件的地址,还有我们的读写的权限
- int n = mkfifo(FIFO_NAME, 0666);
-
- //判断我们的管道是否创建成功
- assert(n == 0);
- //让其不要有告警
- (void)n;
- Log("create fifo success",Notice) << "\n";
- }
- ~Init()
- {
- //将我们的管道删除
- unlink(FIFO_NAME);
- Log("remove fifo success",Notice) << "\n";
- }
- };
-
- //定义我们的读取和写入模式
- #define READ O_RDONLY
- #define WRITE O_WRONLY
-
- //封装接口,打开我们的文件
- int OpenFIFO(std::string pathname, int flags)
- {
- //要打开的文件的路径还有打开文件的模式
- int fd = open(pathname.c_str(), flags);
- //判断是否打开成功
- assert(fd >= 0);
- return fd;
- }
-
- //让进程进行等待
- void Wait(int fd)
- {
- Log("等待中....", Notice) << "\n";
- //将我们的temp写入我们的管道中。
-
- uint32_t temp = 0;
- //将数据从fd管道中读取到我们的tmp中,读取4个字节的大小
- ssize_t s = read(fd, &temp, sizeof(uint32_t));
- //返回的s是否是我们读取到的字节的个数
- assert(s == sizeof(uint32_t));
- //置于从管道中读取到了是什么不重要,只是为了让它在这里进行等待
- (void)s;
- }
-
- //唤醒另外一个进程
- void Signal(int fd)
- {
- uint32_t temp = 1;
- //将我们的1写入我们的管道中
- ssize_t s = write(fd, &temp, sizeof(uint32_t));
- assert(s == sizeof(uint32_t));
- (void)s;
- Log("唤醒中....", Notice) << "\n";
- }
- //关闭我们的管道文件
- void CloseFifo(int fd)
- {
- close(fd);
- }
shmClint.cc
- #include"comm.hpp"
- int main()
- {
- Log("child pid is:",Debug)<<getpid()<
- //创建我们上述的key值,用于我们的两个进程都找到这一块共享内存
- key_t k =ftok(PATH_NAME,PROJ_ID);
- //如果key值创建失败了,也就是我们的底层已经有这一个key值了,它会返回-1
- if(k<0)
- {
- Log("create key failed",Error)<<"Client say :"<
- exit(1);
- }
- //如果创建成功,就将其写入日志当中
- Log("create key done",Debug)<<"Client say :"<
-
- //获取共享内存
- //shmid(key值,需要开辟的共享内存的大小,也就是定义在comm.hpp中的4096,以及创建共享内存的模式0就是我们上述的IPC_CREATE)
- int shmid=shmget(k,SHM_SIZE,0);
- //如果我们的共享内存开辟失败了,就返回失败信息,并且退出
- if(shmid<0)
- {
- Log("create key failed",Error)<<"Client say :"<
- exit(2);
- }
- Log("create key success",Debug)<<"Client say :"<
- //睡眠10秒,便于我们下面的测试实验
- // sleep(10);
-
- //shmat(打开的共享内存的地址,链接地址shmaddr设置为nullptr,系统会帮我们填写,shmflg填写为0,也就是让系统帮我们填写权限)
- char *shmaddr=(char*)shmat(shmid,nullptr,0);
- //将共享内存段链接进地址空间
- //如果链接失败了,就直接打印日志并返回
- if(shmaddr==nullptr)
- {
- Log("attach shm failed",Error)<<"Client say :"<
- exit(3);
- }
- Log("create shm success",Debug)<<"Client say :"<