如何让别人浏览我做的网站,网站怎么做下载网页代码吗,东莞定制网站开发,手机网站开发需求文档一 什么是通信 就是进程间的数据交换#xff0c;进程间由于具有独立性#xff0c;需要操作系统提供能让进程间交换信息#xff0c;也就是数据的方法。
二 如何做到 让不同进程看到同一份资源(这不是很简单的事吗)#xff0c;我在父进程定义一个变量#xff0c;子进程不就… 一 什么是通信 就是进程间的数据交换进程间由于具有独立性需要操作系统提供能让进程间交换信息也就是数据的方法。
二 如何做到 让不同进程看到同一份资源(这不是很简单的事吗)我在父进程定义一个变量子进程不就能看到吗这不就看到同一份资源了吗可是进程间具有独立性如果你后面想修改这个变量的值会发生写时拷贝子进程就看不到父进程修改了什么?你说为什么要有独立性因为免得进程间相互影响假如没有独立性父进程把数据全改了然后子进程啥也没做数据就全被改了这合理吗?所以父子进程间数据都是独立(父子进程数据独立不表示数据各自一份而是用写时拷贝和缺页中断来实现父子进程的互不影响(这个我在博客曾详细提过进程地址空间-CSDN博客)毫不相关的进程更要独立。 噢原来这么难啊所以这份资源就不应该属于进程(不过都是操作系统申请的咋有些属于系统有些属于进程我想系统是会区分的而是应该由操作系统提供一份资源然后你们往这块空间写数据和读数据就行了这个空间就是匿名管道。 既然涉及到了操作系统显然要用系统调用访问啦而系统中有多个进程都要通信系统中就会有多个匿名管道这些匿名管道一定都要被系统管理起来怎么管理呢?也就是用一个结构体先描述匿名管道再将所有结构体用数据结构组织起来这样对匿名管道的管理就变成了对一个数据结构的增删改查。(至于操作系统为什么要管理为什么管理就是用一个结构体来描述它我在从硬件结构到软件-CSDN博客曾提及文字不少在此不赘述)
三 那为什么要有通信呢 我好端端一个进程为什么要通信呢不通信不行吗你这有进程独立性也不好通信啊那不如进程间别通信别交换数据了。我查资料说大的项目中一个进程是不足以支撑功能的所以需要多个进程来协作通信这个估计得工作后才好理解。 先前博客我小小的模拟了一下shell当时的实现是父进程接收键盘输入的指令然后用fork创建子进程再用execl函数替换子进程的代码去执行例如lspwd等指令这个我一开始以为是进程间通信后来感觉有点不像是进程间通信父进程也只是一开始能给子进程的main函数传个参之后就没办法了也算不上通信。
四 进程通信方式-匿名管道 接下来就正式谈匿名管道的实现就是用文件噢文件好啊磁盘的文件父进程能打开子进程一般也能打开因为大家的权限一般是一样的可能都是user那我们就可以把通信的数据就可以放到文件中这不就实现了通信吗。好好好我悟了原来这么简单。 不过这个思想是好的但是如果真的用磁盘文件通信那我们存放和读取都会伴随着大量和外设的IO操作影响效率不过你本来读取磁盘的内容就要先加载到内存我才能读取写也是先写给内存再刷新到磁盘上那不如os在内存上划分一个区域用于通信算了。这个区域其实和文件还有点关系正是文件的系统缓冲区(注意文件系统缓冲区一词多意有时候是指用户缓冲区有时候指系统缓冲区)此时我们指的是内核缓冲区。
1 具体原理 弄一个内存级文件。如下图一个文件一般在磁盘上有自己的空间打开后内存也会为文件创建配套的缓冲区inode结构体用来存属性先前还提过一个file结构体也是存文件属性但是inode内部的属性更多因为file结构体是用来管理文件的管理文件不需要文件的所有属性。 而内存级文件也就是一个文件没有对应的磁盘空间file_operators对应的也不是硬件的读写方法而是对缓冲区的读写方法也不用刷新缓冲区这种特殊的文件是可以实现的因为它几乎和普通文件没任何区别操作系统可以像管理普通文件一样管理它只是刷新的时候做一下判断这种缓冲区就不刷新。 子进程会复制父进程的files_struct结构体而struct file结构体没必要复制只需要对引用计数即可也不能复制因为file结构体内会记录一个位置信息这个位置信息用于读写文件时记录读写位置所以如果file被复制了那file内记录的位置信息就是独立的当父进程去写的时候子进程再去写时会覆盖所以file不能复制换一种理解就是file是属于os的不属于进程不能复制。下面为了简洁所以给父子进程都画了一份file结构体。 父进程打开了一个内存级文件然后fork创建子进程父子进程就都看到了这个内存级文件说是看到其实也就是各自的files_struct中的数组存有file结构体的指针这样就巧妙地利用继承实现了不同进程看到同一份资源的功能后面总结管道特征会再说明匿名管道只适用于有血缘关系的进程AB。 父进程如果以读方式打开子进程也是只读因为files_struct中数组存的指针是一样的指向的struct file结构体是一样的那如何通信? 解决:父进程同时以读写方式打开子进程也可以对缓冲区读和写了然后父关闭读端子关闭写端我们实现的是单向通信因为我们是不建议父进程既可以读又可以写的。 为什么设计者不支持父子进程可以对一个管道既可以读又可以写呢为了简化file实现假设我们设计让父子进程可以对一个文件缓冲区同时能读能写此时父进程往缓冲区写了数据然后我们又调用父进程去读为了防止父进程读到自己刚写的数据是不是就要想想如何区分这个数据是父进程写的还是子进程写的有点复杂设计者为了简化就没有实现让父子进程可以同时读写的方法而是让我们父进程只能读或者写。
如果当前管道是父写子读我还想实现父读子写呢多设一个管道就可以了。
2 创建管道文件 用系统调用pipe函数既然是系统调用当然是在man手册中的二号手册中。 参数是个数组显然这是个输出型参数pipe函数里面要对数组元素进行修改然后我们在外界就可以直接获取到了这两个数组存的其实就是管道的文件描述符其中pripefd[0]就是以读方式打开的文件描述符而pipefd[1]则是以写方式打开的文件描述符。注意:一个文件可以被相同方式或者不相同的方式打开多次反正open一次就会创建一个file对象如下可以验证open一次创建一个file对象写了两次显示最后一句话说明有两个file对象内部的位置信息独立所以没有接着写而是直接覆盖写。突然想到了就提一下我们创建匿名管道不是直接用的open。 下图又没有覆盖写说明父子进程共用一个file对象所以内部记录的位置信息是一样的。 这个对象的指针要存到file_struct数组中所以一个文件可以有多个文件描述符。 当所有准备工作做好后我们就可以开始实现通信了除去创建管道的部分其余都是非常简单的函数使用。
int main()
{int fd[2]{0};创建管道文件此时fd内已经装了两个文件描述符pipe(fd);创建子进程int id fork();if(id 0){close(fd[0]);//子进程关闭读端Writer(fd[1]); 子进程调用writer函数向管道写数据关闭文件描述符并退出close(fd[1]);exit(0);}close(fd[1]);//父进程关闭写端Reader(fd[0]); 父进程向管道读数据关闭文件描述符并退出close(fd[0]);return 0;
}有点懵怎么会有这么多的close父进程close一遍子进程还close一遍因为进程具有独立性!下图的pcb和files_struct都是每个进程各有一份的这也必须是各自一份不然父子进程各自打开不同的文件files_struct结构体内不就同时存了父子进程打开的文件描述了?此时如果父进程退出那父进程打开的文件描述符应该都被关闭也就是把数组上的指针清空可是我怎么区分哪个是父进程的哪个是子进程的文件描述符越想越麻烦算了各自一份吧。 接下来就是看看父进程是如何读的以及子进程如何写的了。
#includestdio.h
#includeiostream
#includeunistd.h
#includeiostream
#includestring.h
#include sys/types.h
#include sys/wait.h
#include stdlib.h
using namespace std;上面是使用对应函数需要的头文件老实说自从在vim写代码感觉要包含的头文件都变多了#define NUM 1024
void Writer(int wfd)
{char buf[NUM]{0};int cnt 0;while(true){先将数据写到buf数组中snprintf(buf,sizeof(buf),I am child id : %d,getpid());系统调用write直接写向管道文件写和向普通文件是一样的write(wfd,buf,strlen(buf));sleep(1);cnt;if(cnt 5)break;}
} snprintf函数:和printf类似printf(I am child id : %d,getpid())printf是输出到显示器而snprintf则是最后输出到数组中去size应该是告诉snprintf最多读多少个字节当然读到\0也要停止\0不会被读取。 细节:Reader代码处我没写sleep但是我们会发现子进程写一次然后sleep 父进程读完后也会陷入休眠很像是父子进程在协同一样我写一句你就读一句原因管道特征中解释。
void Reader(int rfd)
{char buf[1024]{0};int num 0;while(true){int n read(rfd,buf,sizeof(buf));cout这是父进程: 读取结果: ;buf[n] \0;if(n 0){ coutbufendl;}else if(n 0){coutchild process exit father exit tooendl;break;}}
}在文件时我们就了解过write是往文件缓冲区写的那上述的通信过程中是会有多次拷贝的不考虑snprintf写到buf数组只考虑和系统缓冲区打交道的次数子进程write:buf到系统缓冲区1次父进程read系统缓冲区到buf共两次。
4 管道特征 1 只能在有血缘关系的进程间通信 2 单向的 3 会协同 4 面向字节流(也就是读端会按照指定的字节数读取不考虑分隔符全部都认为是字符) 5 管道的生命周期随进程当没有文件描述符指向它了就要删除了 特征1解释 从匿名管道使用我们就知道这个管道文件无名字我们无法通过open打开来获取文件描述符只能用pipe函数往pipefd[2]存的文件描述符而这个数据是可以被子进程继承的也就是说只有子进程是最容易拿到这个文件描述符。 为什么我不让操作系统帮忙将这个文件描述符给其它没有血缘关系的进程呢反正都在系统的管理之下拿点数据给其它进程怎么了好此时我们就是系统的设计者我们设计了一个接口能将数据传给其它进程此时我们要实现一种新的通信方式让两个不相关的进程能够通信可是我们现在不就在实现通信吗我们为了能让两个进程能用匿名管道的方式通信居然要先让他们用其它的方式通信而且我们上面每句话都说让操作系统帮我处理操作系统是追求效率的你这种麻烦低效的通信方式第一个就被枪毙了。 我最后举个例子帮助大家理解为什么说匿名管道用于有血缘关系的进程而不是父子进程例子假如父进程1调用了pipe函数然后又创建了两个子进程这两个子进程都继承了pipefd里面的文件描述符这两个子进程间可以实现通信由此我们可以发散思维不仅父子间可以用这种通信只要两个进程间具有血缘关系是亲戚就可以实现通信。 特征2解释我在写代码的时候都是只让一个进程负责读另一个进程负责写不会说一个进程可以又读又写前面具体原理部分说过了为了简化设计者就建议我们单向通信。
5 管道的四种情况 情况1 读完管道内的数据后写端未关闭读端阻塞。 这恰恰印证了管道的特征3读写端是会协同的读端要等写端没在写的时候例如先前代码中写了一句话然后让写端sleep休眠不能我在写的时候你就上来说你要读当read读完管道内的数据此时变成了管道情况1。 所以就出现了写端写一次然后sleep休眠此时读端读完明明代码里没有sleep也要进入休眠其实就是管道的设计者让读端休眠了显然并非是写端的sleep影响了读端休眠而是管道特征导致的如果读端读完sleep一下写端是不会sleep休眠但写端会一直写直到写满管道才会休眠这就是特征2了。 下图我让子进程先休眠10s再写此时读端一点反应都没有就是因为在read的时候阻塞了。 情况3 读端正常写端关闭读端的read会返回0若写端没关闭会一直在等数据就像scanf一样。
4 读端关闭写端正常此时写端进程会被kill。这就是为什么前面设计子写父读的原因就是为了方便拿到子进程收到的信号值。 子进程写一次然后父进程读完后就break退出函数了但是卡在waitpid等子进程退出然后子进程再写的时候我们就发现子进程退出了就是因为它收到了13号信号-管道破裂。 五 应用场景 进程池
先前只是简单实现了两个进程的通信接下来应用进程间通信来实现进程池。 什么是进程池?和内存池原理类似我们知道shell是bash接收任务然后创建子进程去执行如果bash是接收一个任务才创建一个子进程这样会非常影响bash处理需求的速度所以为了提高效率在无任务的时候bash就会创建出一批的子进程任务来了指派某个子进程执行即可。
执行过程如下函数接下来就一步步了解这些函数的具体实现。
int main()
{//加载任务Load(tasks);std::vectorchannel channels;//1 初始化channelsinitchannle(channels);//2 控制子进程crlprocess(channels);//清理回收QuitCtrl(channels); return 0;
}
1 加载任务
父进程既然要派发任务给子进程执行那我们首先得模拟一下有任务来的情况
typedef void(*task_t)();
void task1()
{std::cout任务1: 打印日志std::endl;
}
void task2()
{std::cout任务2: 检查版本更新std::endl;
}
void task3()
{std::cout任务3: 刷新bossstd::endl;
}
void task4()
{std::cout任务4: 发送活动通知std::endl;
}
void Load(std::vectortask_t* tasks)在main函数定义了一个vector内部存的是一个个函数指针main函数调用Load()函数来加载任务
{tasks-push_back(task1);tasks-push_back(task2);tasks-push_back(task3);tasks-push_back(task4);
}
2 创建子进程 创建的子进程和管道太多那势必要将管道和子进程管理起来那保存创建好的子进程pid是有必要的但是仅仅是保存pid是不够的因为当父进程挑选出一个子进程的pid的时候此时想用write发送消息怎么发文件fd参数都没有那好吧看来得把子进程的pid和对应管道的写端fd保存在一起。如下结构体。 循环创建子进程和管道用来初始化channels。
#define PRONUM 3class channel 描述子进程和管道
{
public:channel(int cfd, int fd, std::string name):_cfd(cfd), _fd(fd), _pname(name){;}int _cfd; 管道的写端int _fd; 子进程pidstd::string _pname; 进程名
};void initchannle(std::vectorchannel channels)
{循环创建子进程和匿名管道for (int i 0; i PRONUM; i){int fd[2] { 0 };int ret pipe(fd);先创建管道再创建子进程int id fork();if (ret -1)perror(pipe);if (id 0)//子进程读{close(fd[1]);dup2(fd[0], 0);//输入重定向执行任务slaver();这个函数实现下面会提及关闭读端并退出close(fd[0]);exit(0);}//父进程写close(fd[0]);std::string name process- to_string(i);channels.push_back(channel(fd[1], id, name));}
} 总结一下上面代码先pipe创建读写端再创建子进程1此时子进程1继承读写端随后父子分别关闭读写端此时它们的file_struct内的数组如下图。此时是父写子读。 然后我们再pipe一下然后再创建子进程2再关闭读写端最终的file_struct内的数组如下图。 此时我们发现子进程2多继承了一个文件描述符4这个4是第一次pipe的写端因为父进程一直没关闭所以下次创建子进程时之前的写端都会被继承下来这个bug在清理回收时会再提及。 而且由于父进程每次关闭了pipe[0]这样后面创建的时候我们发现读端都是3号文件描述符而写端则是4,5,6....显然文件描述符的分配是找第一个空位。
3 派发执行任务 管道读端的fd就不用保存了。 因为我们可以通过传参给slaver函数让子进程拿到fd或者用输入重定向再次说明写端要保存的因为父进程会有多个写端文件描述符子进程只需要拿到自己的读端描述符就可以接收指令了。
#define PRONUM 3
std::vectortask_t tasks;
void Menu()
{cout1: 打印日志 2: 检查更新 endl;cout3: 刷新boss 4: 发送活动通知endl;cout0: 退出endl;coutPlease select endl;
}
void slaver()
{while (true){sleep(1);int codenum 0;int n read(0, codenum, sizeof(int));if (n sizeof(codenum)){ std::cout 我是子进程 : getpid() 收到的任务为 codenum: codenum endl;tasks[codenum]();}if (n 0){std::cout 退出 endl;break;}}
}
void crlprocess(const std::vectorchannel channels)
{int cnt 0;int which 0;int select 0;while (true){打印选择菜单Menu();输入选择cinselect;if(select 0 || select tasks.size())break;sleep(1);1 选择任务int tasknum select - 1;cout父进程第which次给子进程: channels[which]._fd channels[which]._pname 发送任务码: tasknumendl;2 选择子进程并发送任务码write(channels[which]._cfd, tasknum, sizeof(tasknum));sleep(1);which;which % channels.size();采用轮询选择子进程}
}4 代码异常
异常1 此时父进程会把自己的管道读端关闭而由于先fork再创建管道此时父子进程的管道独立父进程读端关闭之后父进程要控制子进程时会往管道写入此时会发生管道破裂被系统杀死。
5 清理回收 我们选择0。 然后按照main函数逻辑此时进入QuitCtrl函数。首先父进程关闭所有的写端。
void QuitCtrl(const std::vectorchannel channels)
{for(auto e :channels)close(e._cfd);for(auto e :channels)//等待回收子进程{waitpid(e._fd,NULL,0);sleep(1);}
} 然后父进程一个一个回收子进程当然我写的代码中存子进程pid并非是必须的但是如果我是指定了多个子进程执行任务都执行完了我要选择性地回收某个子进程看执行结果此时子进程pid还是有用的。由于waitpid是阻塞等待所以此时会调用子进程那我们的子进程到哪了呢?显然会在slaver()函数中因为read返回0而退出函数。 有意思的是这个退出顺序每次都是后创建的进程先退出按道理说子进程调度顺序是未知的为什么每次都是最后创建的进程先read返回0先退出呢。 就是因为这里埋了个小雷我们先前在创建子进程章节内说了一些子进程内还存着写端的文件描述符而最后一个子进程对应的管道写端只有一个父进程关了就没有下一个子进程保存写端所以每次都是最后一个子进程先read返回0break退出函数。所以我们选择可以倒着回收或者用一个数组保存oldfd。 假如父进程push了很多次然后子进程才开始回收会不会出问题呢不会因为会发生写时拷贝。第一个子进程继承的oldfd内啥都没有父进程push写端的时候不会影响子进程的oldfd这个有点细大家可以打印看看。