自己建设个人网站要花费多少,网页制作代码步骤,wordpress获取gallery,做酒店网站设计目录
写在前面的话
什么是进程间通信
为什么要进行进程间通信
进程间通信的本质理解
进程间通信的方式
管道
System V IPC
POSIX IPC
管道
什么是管道 匿名管道
什么是匿名管道
匿名管道通信的原理
pipe()的使用
匿名管道通信的特点
拓展代码 命名管道
什么是命…目录
写在前面的话
什么是进程间通信
为什么要进行进程间通信
进程间通信的本质理解
进程间通信的方式
管道
System V IPC
POSIX IPC
管道
什么是管道 匿名管道
什么是匿名管道
匿名管道通信的原理
pipe()的使用
匿名管道通信的特点
拓展代码 命名管道
什么是命名管道
命名管道通信的原理
mkfifo的使用
代码模拟命名管道通信过程 写在前面的话 本章是首次提出进程间通信的概念所以会先介绍进程间通信的相关概念以及整体的框架结构。 而本文重点是先介绍进程间通信的基本概念然后重点介绍进程间通信的第一种方式管道。
什么是进程间通信 进程间通信Inter-Process CommunicationIPC是指操作系统或计算机系统中不同进程之间进行数据交换和通信的机制或技术。由于进程是操作系统中独立运行的程序实例而进程间通信允许这些独立的进程之间相互协作、共享资源和进行数据交换。
为什么要进行进程间通信 根据我们前面讲的进程间是相互独立的进程具有独立性啊那通信不就不独立了吗 答案是正确的进程通信的确会破坏进程的完全独立性因为进程通信的目的是为了实现进程之间的数据共享、同步和协作。通过进程通信各个进程可以相互交互和共享资源这意味着它们不再完全独立而是具有一定的相互依赖性和关联性。 尽管进程通信破坏了进程的完全独立性但这种破坏是有意义且必要的。在实际的计算机系统和操作系统中进程往往需要协同工作、共享资源和交换数据才能完成复杂的任务。进程通信提供了一种机制使得不同进程之间可以进行必要的协作和交流并提供了相应的同步和保护机制来确保数据的正确性和一致性。 所以这是一种权衡和折中的方案但大部分情况下进程是相互独立的。 综上进程间通信主要是为了完成下面这些作用 数据传输一个进程需要将它的数据发送给另一个进程。资源共享多个进程之间共享同样的资源包括本地共享和远程资源共享。进程控制有些进程希望完全控制另一个进程的执行如Debug进程此时控制进程希望能够拦截另一个进程的所有陷入和异常并能够及时知道它的状态改变。通知事件一个进程需要向另一个或一组进程发送消息通知它它们发生了某种事件如进程终止时要通知父进程。 进程间通信的本质理解 1.我们知道进程具有独立性是通过虚拟地址空间 页表映射的方式来保持独立性的所以通信起来成本会比较高。 2.既然通信那么前提是一定要让不同的进程看到同一块“内存”(特定的结构组织)这块内存不能隶属于任何一个进程而更应该强调共享。
进程间通信不是目的而是手段 进程间通信的方式 大体上可以分为3种通信方式 管道 匿名管道pipe命名管道 System V IPC System V消息队列System V共享内存System V信号量
System V只能用于单机通信(本地通信). POSIX IPC 消息队列共享内存信号量互斥量条件变量读写锁
POSIX IPC可以在单机通信的基础上进行网络通信(远程资源共享)。 以上所提到的方式我会在后面的章节逐一讲解这个是进程间通信的方式.
今天我们将首要讲解进程间通信方式(一)——管道.
管道
什么是管道 管道Pipe是一种进程间通信机制用于在相关进程之间传输数据。它是一种特殊的文件描述符它可以连接一个进程的输出写入端到另一个进程的输入读取端从而使得这两个进程可以通过管道进行数据传输。 也就是说管道是单向传输的现实生活中我们所看听到的天然气管道、石油管道基本上都是单向传输的. 匿名管道
什么是匿名管道 匿名管道Anonymous Pipe是进程间通信的一种机制用于在具有亲缘关系例如父子进程或共享同一终端的兄弟进程之间传输数据。 匿名管道是一种单向的数据流通道它可以用于在进程之间传递数据。通常一个进程作为管道的写入端称为管道写入端将数据写入管道另一个进程作为管道的读取端称为管道读取端从管道中读取数据。 匿名管道的创建是通过系统调用 pipe() 来完成的。pipe的使用后面会讲。
匿名管道通信的原理 管道通信的背后是进程之间通过管道进行通信。 我们知道一个进程要运行首先要加载到内存然后创建一个task_struct结构体里面会有一个files_struct结构体然后这个结构体里又有一个fd_array[]数组每个元素指向对应的文件struct_file里面包含了文件内容等. 此时我们fork之后的子进程会重新创建一份task_struct,内容继承父进程的此时fd_array[]里的内容也被子进程继承即父进程打开的文件 子进程也继承了下来。它们指向的文件是 相同的. 假设父进程3号文件描述符是读取文件的4号文件描述符也用来写入文件的子进程继承以后fd3也是用来读取文件的fd4也是用来写入文件的 此时我们想让父进程进行写入fd4子进程进行读取fd3所以父进程就要关闭读端fd3子进程关闭写端fd4。
这样我们就做到了让不同的进程看到了同一份资源(通过fork子进程)而且通过文件描述符的方式完成了进程间的单向通信。
综上管道内部本质大体是如下流程 1.父进程分别以读写方式打开一个文件 2.fork()创建子进程 3.双方各自关闭不需要的文件描述符 整体图如下 pipe()的使用 既然我们知道了思路那我们可以用代码来使用一下管道。 首先父进程如何使用读和写方式分别打开文件呢
这里使用到了pipe函数函数用法及原型如下 参数pipefd为输出型参数我们提前在外部定义好数组然后传入结果就会保存在这个数组中分别为pipefd[0]代表的是读端pipefd[1]代表的是写端. 第二步我们利用fork创建子进程。 最后子进程用来读取文件的内容并关闭写端pipefd[1],父进程用来写入内容同时关闭读端。
匿名管道通信的特点
一个小demo如下
#include iostream
#include string
#include cstdio
#include cstring
#include string.h
#include assert.h
#include unistd.h
#include sys/types.h
#include sys/wait.h
using namespace std;int main()
{// 1.创建管道int pipefd[2] {0}; // pipefd[0] :读端 pipefd[1] :写端int n pipe(pipefd);assert(n ! -1);#ifdef DEBUG#endif// cout pipefd[0] pipefd[1] endl;// 2.创建子进程pid_t id fork();assert(id ! -1);if (id 0){// 子进程// 3.构建单向通信的信道父进程写入子进程读取// 3.1关闭子进程不需要的fdclose(pipefd[1]);char buffer[1024];while (true){ssize_t s read(pipefd[0], buffer, sizeof(buffer) - 1);if (s 0){buffer[s] 0;cout child get a message [ getpid() ] Father# buffer endl;}}exit(0);}// 父进程// 构建单向通信的信道// 3.1 关闭父进程不需要的fdclose(pipefd[0]);string message 我是父进程我正在给你发消息;int count 0;char send_buffer[1024];while (true){// 3.2 构建一个变化的字符串snprintf(send_buffer, sizeof(send_buffer), %s[%d] : %d, message.c_str(), getpid(), count);// 3.3 写入write(pipefd[1], send_buffer, strlen(send_buffer));// 3.4 sleepsleep(1);}pid_t ret waitpid(id, nullptr, 0);assert(ret 0);close(pipefd[1]);return 0;
}然后此时我们编译运行 可以看到父进程写入的内容子进程全部读到了。
这里是静态图看不出效果这些信息其实是时隔1秒打印一条的. 我们看只有子进程在读取可子进程我们并没有加任何的sleep啊理论上应该子进程一直打印才对啊。 显示器是一个文件而管道也是一个文件父子进程同时向显示器 写入时没有说一个进程会等另一个进程而是各自打印各自的消息互相干扰这是缺乏访问控制。而管道文件提供了访问控制使得子进程读取完得等待父进程写入才能读取。 这样如果我们让子进程读取先sleep 10秒期间父进程每隔1秒写入等10秒过后子进程开始读取但会把 父进程写入10次的文件的内容全部一下读出来。这说明写入次数和读取次数没有直接关系。即管道是面向字节流的具体怎么读需要定制协议后面会说 这里就针对与管道的特点做一些总结 管道是用来进行具有血缘关系的进程进行进程间通信 --- 常用于父子间通信管道具有通过让进程间协同提供了访问控制管道提供的是面向字节流式的通信服务 --- 面向字节流 --- 通过定制协议实现管道是基于文件的文件的生命周期是随进程的即管道的生命周期也随进程的管道是单向通信的就是半双工通信的一种特殊方式. 上面最后一条提到了半双工概念这里来解释一下 半双工通信的双方只能在同一时间点单向的传输数据即两个参与者不能同时发送和接收数据。在半双工通信中通信双方必须交替使用共享的通信信道。例如当一个人在对讲机上说话时另一个人必须停止接收然后才能回应。典型的半双工通信方式包括对讲机和卫星电台。 全双工全双工通信允许在同一时间点双向地传输数据。这意味着通信的两个参与者能够同时发送和接收数据而不需要交替使用通信信道。在全双工通信中通信双方可以同时进行发送和接收操作彼此之间的数据传输互不干扰。例如电话通话是一个典型的全双工通信场景双方可以同时说话和倾听对方的声音。 顺带总结一下管道的几种情况 a.写快读慢写满就不能再写了 b.写慢读快管道没有数据时读必须等待 这两种是由访问控制提供的. c.写关读继续读会标识读到了文件结尾 d.写继续写读关OS会终止写进程 拓展代码 利用匿名管道的方式创建多个子进程然后父进程分别派发随机的任务 总代码流程是父进程首先load()加载方法然后for循环创建多个进程每次创建完成后该进程都要与父进程(pipefd[1])建立关联,以方便父进程管理这些子进程。 其中每个子进程调用 waitCommand函数会阻塞在read等待着父进程的写入然后父进程开始分发任务当是对应的子进程时子进程会执行对应的任务然后继续while循环等待。 共两个文件第一个文件ProcessPool.cc文件 #include iostream
#include vector
#include ctime
#include unistd.h
#include stdlib.h
#include assert.h
#include sys/types.h
#include sys/wait.h
#include Task.hpp
using namespace std;#define PROCESS_NUM 5
int waitCommand(int waitFd, bool quit) // 如果对方不发任务就阻塞
{uint32_t command 0;ssize_t s read(waitFd, command, sizeof(command));if (s 0){quit true;return -1;}assert(s sizeof(command));return command;
}
void sendAndWakeup(pid_t who, int fd, uint32_t command)
{write(fd, command, sizeof(command));cout main process: call process who execute desc[command] through fd endl;
}int main()
{load();// pid : pipefdvectorpairpid_t, int slots;// create multiple child processfor (int i 0; i PROCESS_NUM; i){// 创建管道int pipefd[2] {0};int n pipe(pipefd);assert(n 0);pid_t id fork();assert(id ! -1);// 让子进程读取if (id 0){// 关闭写端close(pipefd[1]);// child processwhile (true){// 等命令bool quit false;int command waitCommand(pipefd[0], quit); // 如果对方不发任务就阻塞if (quit)break;// 执行对应的命令if (command 0 command handlerSize()){callbacks[command]();}else{cout 非法 command endl;}}exit(1);}// father processclose(pipefd[0]);slots.push_back(pairpid_t, int(id, pipefd[1]));}// 父进程派发任务srand((unsigned long)time(nullptr) ^ getpid() ^ 2311156L);while (true){ //选择一个任务 int command rand() % handlerSize();//选择一个进程,采用随机数的方式选择进程来完成任务随机数的方式负载均衡int choice rand() % slots.size();// 把任务给指定的进程sendAndWakeup(slots[choice].first, slots[choice].second, command);sleep(1);int select;//以下是手动派发任务// int command 0;// cout ###################################### endl;// cout 1.show functions 2.send command endl;// cout ###################################### endl;// cout Please Select ;// cin select;// if (select 1)// showHandler();// else if (select 2)// {// cout Enter your Command ;// // 选择任务// cin command;// // 选择进程// int choice rand() % slots.size();// // 把任务给指定的进程// sendAndWakeup(slots[choice].first, slots[choice].second, command);// }// else// {// }}// 关闭fd结束所有进程for (auto slot : slots){close(slot.second);}// 回收所有子进程for (auto slot : slots){waitpid(slot.first, nullptr, 0);}return 0;
} 第二个文件为Task.hpp文件主要是包含了任务的加载及任务的执行方法。 #pragma once
#includeiostream
#includevector
#includestring
#includeunordered_map
#includeunistd.h
#includefunctional
using namespace std;typedef functionvoid() func;vectorfunc callbacks;
unordered_mapint,string desc;void readMySQL()
{cout sub process[ getpid() ] 执行数据库被访问的任务\n endl;
}
void executeURL()
{cout sub process[ getpid() ] 执行url解析任务\n endl;
}
void cal()
{cout sub process[ getpid() ] 执行加密任务\n endl;
}
void save()
{cout sub process[ getpid() ] 执行数据持久化\n endl;
}void load()
{desc.insert({callbacks.size(),readMySQWL:读取数据库});callbacks.push_back(readMySQL);desc.insert({callbacks.size(),executeURL:解析URL});callbacks.push_back(executeURL);desc.insert({callbacks.size(),cal:进行加密计算});callbacks.push_back(cal);desc.insert({callbacks.size(),save:进行数据的文件保存});callbacks.push_back(save);
}
void showHandler()
{for(auto iter: desc){cout iter.first \t iter.second endl;}
}
int handlerSize()
{return callbacks.size();
} 这样每次父进程都会随机给子进程派发随机的任务 命名管道 与匿名管道不同命名管道不需要亲缘关系的进程之间也不需要共享同一终端。任意进程可以通过打开命名管道的读取端和写入端来与其进行通信。
什么是命名管道 命名管道Named Pipe是一种独立进程之间通信的机制用于在无关的进程之间进行数据传输。 命名管道通过在文件系统中创建一个特殊的文件来实现通信。这个特殊的文件被称为FIFOFirst-in, First-out或命名管道。
命名管道通信的原理 和匿名管道一样想让双方通信必须先让双方看到同一份资源它和匿名管道本质是一样的只是看到资源的方式不同。 匿名管道是通过父子进程继承来看到同一份资源的也叫做管道文件这个文件是纯内存级的所以没有名字叫做匿名管道。 而命名管道是在磁盘上有一个特殊的文件这个文件可以被打开但是打开后不会将内存中的数据刷新到磁盘。在磁盘上就有了路径而路径是唯一的所以双方就可以通过文件的路径 来看到同一份资源即管道文件。
这是命名管道的流程 创建命名管道通过调用系统调用 mkfifo() 在文件系统中创建一个特殊的文件这个文件就是命名管道。创建命名管道时需要指定管道的名称和所需的权限。 打开命名管道进程通过调用系统调用 open() 来打开命名管道得到一个文件描述符。进程可以通过打开具有相同名称的文件来打开命名管道的读取端和写入端。 进程通信一旦命名管道被打开进程就可以使用文件描述符进行通信。每个进程可以选择读取端或写入端与命名管道进行交互。 数据传输进程在读取端通过调用 read() 系统调用从命名管道中读取数据而在写入端通过调用 write() 系统调用将数据写入命名管道中。读取端和写入端可以通过文件描述符进行数据的发送和接收。 关闭命名管道进程完成通信后可以通过调用 close() 关闭命名管道的文件描述符来释放资源。当所有对命名管道的引用都被关闭时管道的文件系统条目将被删除。 mkfifo的使用 上面提到了需要使用mkfifo来创建这个特殊的文件来让独立的进程之间通过它进行通信下面来看一下它的用法。
mkfifo [选项] 文件名 非常简单的使用方法选项我们一般不用所以直接mkfifo 文件名即可 我们在当前路径下创建一个name_pipe的文件. 注意权限的最前面是p代表是管道文件。 我们此时echo一句消息到这个管道文件中 我们发现这里阻塞住了这是因为一方向管道文件里写入了但是另外一方还没有读所以此时我们新建一个窗口然后读取name_pipe里的内容 这样信息便成功的被读取出来了这就是mkfifo的简单使用。
代码模拟命名管道通信过程 其实过程和匿名管道类似只是看到同一资源的手段不一样。 上面讲的mkfifo是指令创建但是如果我想用代码该如何实现呢这里有一个mkfifo函数 第一个参数是创建的管道文件的路径第二个是权限。 当创建成功时mkfifo返回0否则返回-1.
整体的流程是是这样的
我们首先可以分成 服务端 和 客户端服务端负责 1.创建管道文件并打开 2.进行与客户端正常的通信 3.最后关闭并删除管道文件
而客户端 1.首先要打开管道文件 2.然后进行与服务端正常的通信流程即可 这里为了方便我们加入了日志可以看到每一步的动作。 所以一共四个文件分别为comm.hppclient.ccserver.ccLog.hpp. comm.hpp #pragma once
#includeiostream
#includecstdio
#includecstring
#includeunistd.h
#includevector
#includestring
#includesys/types.h
#includesys/stat.h
#includefcntl.husing namespace std;#define MODE 0666
#define SIZE 128
string ipcPath ./fifo.ipc;client.cc #include comm.hpp
int main()
{//1.获取管道文件int fd open(ipcPath.c_str(),O_WRONLY);if(fd 0){perror(open);exit(1); }//2.通信过程string buffer;while(true){cout please Enter Message Line : ;getline(cin,buffer);write(fd,buffer.c_str(),buffer.size());}//3.关闭文件return 0;
} server.cc #includecomm.hpp
#includeLog.hpp
int main()
{//1.创建管道文件if(mkfifo(ipcPath.c_str(),MODE) 0){perror(mkfifo);exit(1);}Log(创建管道文件成功,Debug) step 1 endl;//2.正常的文件操作int fd open(ipcPath.c_str(),O_RDONLY);if(fd 0){perror(open);exit(2);}Log(打开管道文件成功,Debug) step 2 endl;//3.编写正常的通信代码char buffer[SIZE]; while(true){memset(buffer,\0,sizeof(buffer));ssize_t s read(fd,buffer,sizeof(buffer)-1);if(s 0){cout client say buffer endl;}else if(s 0){//end of filecerr read emd of file, client quit, server quit too! endl;break;}else{//read errorperror(read);}}//4.关闭文件close(fd);Log(关闭管道文件成功,Debug) step 3 endl;unlink(ipcPath.c_str());//通信完毕就删除文件Log(删除管道文件成功,Debug) step 4 endl;return 0;
} Log.hpp #pragma once
#include iostream
#include ctime
#includestring
using namespace std;#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3string msg[] {Debug ,Notice,Warning,Error
};ostream Log(string message,int level)
{cout | (unsigned)time(NULL) | msg[level] | message;return cout;
} 然后我们再编译运行可以再创建一个Makefile文件直接编译好所有的文件内容如下 .PHONY:all
all:client serverclient:client.ccg -o $ $^ -stdc11
server:server.ccg -o $ $^ -stdc11.PHONY:clean
clean:rm -rf client server 此时我们直接make即可。然后会得到两个可执行文件client和server.
我们打开两个窗口首先运行server. 第一步创建管道完成然后我们在另一个窗口运行客户端. 运行起来后显示打开文件也成功了这个时候我们在客户端输入服务端都能读取到: 然后我们ctrl c 退出客户端此时服务端也会break跳出循环然后结束. 这样利用命名管道通信的代码流程也就完成了.