前言
以<深入理解计算机系统>(以下称“本书”)内容为基础,对程序的整个过程进行梳理。本书内容对整个计算机系统做了系统性导引,每部分内容都是单独的一门课.学习深度根据自己需要来定
引入
接续上一帖理解计算机系统_并发编程(2)_基于I/O复用的并发(一):select浅解-CSDN博客,实现一个基于I/O多路复用的并发事件驱动服务器.研读代码,总结编程的一些模式.
状态机
本书P686和P687介绍了状态机的概念并配图
在事件驱动程序中,某些事件会导致流向前推进.一般思路是将逻辑流模型化为状态机.一个状态机就是一组状态,输入事件和转移,其中转移是将状态和输入事件映射到状态.---黑体字是原话
---解读:逻辑是什么?是一种因果关系.逻辑流解释为从因到果的流程.状态机和逻辑流相互对应
状态对应逻辑(因果),表述为:当条件A满足时,用事件B响应.
输入事件对应起因已满足,表述为:条件A已满足
转移对应用结果响应,表述为:事件B
/*书面叙述概念时,常用较大的篇幅来写.而读者理解时可以采用简单的方式*/.
所有的逻辑都可以转化成状态机
状态机的推导:"状态"是逻辑是想法,不需要代码;"输入事件"和"转移"需要代码表达.所以状态机可以看作是一种理想的逻辑表达方式,从思路到实现都包含了进去.
服务器I/O多路复用的状态机
本书P687第3段:服务器使用I/O多路复用,借助select函数检测输入事件的发生.当每个已连接描述符准备好可读时,服务器就为相应的状态机执行转移,在这里就是从描述符读和写回一个文本行.
---解读:select:检测输入事件; 转移:描述符读和写回一个文本行.
任何一个状态机必然有检测输入事件,这里用的是select函数. 这里的转移事件---"描述符读和写回一个文本行"非必须,演示用.
基于I/O多路复用的并发事件驱动服务器
=============================内容分割线↓===================================
读代码的说明
/*代码是最有说服力的,表现编程思想和程序模型的存在*/
学习别人的源码,除了理解别人的思路,还要一点也很重要:从源码中整理和提取写代码的模式.
注意:读代码的顺序是从主程序开始读,先理解大概意思,再去理解他调用的函数.同样道理,遇到变量,也不要一个一个挨着去读,要结合使用去理解.
=============================内容分割线↑===================================
代码整体说明
本书P687最后一段是基于I/O多路复用的并发服务器示例代码的整体说明.
/*如果自己写了一段代码,技术文档也可以借鉴这种方式,让别人容易理解*/
一个pool结构里维护着活动客户端的集合(第3~11行) ---黑体字是原话
---解读:这里有个词叫"维护",经常在其他技术文献中看到什么维护着什么,感觉很专业.从内容上看,维护代表"描述"或者"控制".
活动客户端的集合在服务器端用一个结构来描述,结构取名叫pool(池),理解为客户端连接池.
然后是初始化init_pool,进入while循环,检测输入事件,连接请求到达,调用add_client函数,送回文本.这些内容要简单了解后,边读源码边理解.
代码研读
注意:经典书的经典代码,很难得(好好看好好学)
主程序在本书P687和P688,名为echoservers.c
函数开始
1>第3~11行是"连接池"结构声明
typedef struct{int maxfd;fd_set read_set;fd_set ready_set;int nready;int maxi;int clientfd[FD_SETSIZE];rio_t clientrio[FD_SETSIZE];
}pool;
如前所述,不用一下全看完,边读代码边看连接池结构是怎样定义的.
2>进入main函数,第17~20行是变量定义,也是结合后面代码读.注意pool定义为静态变量,即表示不想被别的模块使用,如果要用到,必须提供接口.---这是C语言基础.这里加上了static,声明成main函数中的局部静态变量,为了保持更新.
注意这里的命名方式不太合理,类型名和变量名取得一样.规则写法应该把类型名写成首字母大写Pool.
3>第26行调用Open_listenfd函数
这里再梳理一遍监听描述符的建立:Open_listenfd函数是由服务器调用,表示等待客户端连接请求.一旦客户端发来连接请求,代码才执行,生成listenfd监听描述符,代表服务器已经知道你的请求了.后续将视情况调用accept接受请求,生成connfd描述符.
init_pool函数
在本书P688图12-9.从名字看来表示:初始化连接池.
第4~7行代码如下
int i;
p->maxi=-1;
for(i=0;i<FD_SETSIZE;i++)p->clentfd[i]=-1;
---解读:clientfd[FD_SETSIZE]是第一个理解的数据,他是什么意思?书上说了clientfd数组表示已连接描述符的集合.
/*已连接描述符在服务器端通常用connfd,书上用了clientfd来表示连接(客户端连接),connfd用到了其他地方*/
开始时没有连接进来,所以数组中所有整数值设置为-1.-1表示槽位(用其他值也可以,不一定是-1,但既然其值用来表示描述符,所以推荐小于-1的值和其他文件描述符做区分)
FD_SETSIZE,本书没有显式说明.从全大写的表示来看,他是#define宏定义的一个常数.字面意思代表了允许连接数.
maxi:已连接集合里的最大索引.初始时没有连接,也被设置为-1.
笔者开始时在这里也饶了半天,这个数组的来源是这样的,演示如下:
服务器每当建立一个连接,就会生成一个描述符connfd来传输数据,描述符是个整型数据(从4开始).那么当前有多少个描述符已建立呢?这些描述符该怎样控制其数量呢?设计了一个整型数组
typedef struct{...int maxi; //当前已连接描述符的个数-1,同时也是下面数组中不等于-1的索引值int clientfd[FD_SETSIZE]; //已连接描述符数组,最大连接数限制为FD_SETSIZE...
}pool;
初始化时,clientfd[FD_SETSIZE]={-1,-1.....-1},此时索引maxi设置-1.
当有一个描述符(比如4)被产生,写个算法让他进去数组中,此时clientfd[FD_SETSIZE]={4,-1.....-1},maxi加1,等于0.当遍历这个数组查询时,就知道有1个元素(索引等于0)
这样是不是把意思表达清晰了?而这个把数组索引maxi单独表达的写法,也值得一学.
第10~12行代码如下
p->maxfd=listenfd;
FD_ZERO(&p->read_set);
FD_SET(listenfd,&p->read_set);
---解读:maxfd是读集合的基数,"读集合"和"基数"意思在理解计算机系统_并发编程(2)_基于I/O复用的并发(一):select浅解-CSDN博客有,此处不多说.
FD_ZERO清空读集合,FD_SET把listenfd添加到读集合中. ---初始化配置读集合
回到主函数
第29行进入while循环,第30行设置"准备好集合",第31行调用Select函数
再看pool中对应数据的使用:
read_set:读集合;
ready_set:准备好集合.
nready:准备好集合中元素的个数
第35行调用FD_ISSET传入listenfd,表示连接判定---如果有客户端请求连接,则执行.
第36行的意思在本书前面一章(笔者也没有细读,照着用)
第37行调用Accept函数,表示服务器端同意连接,生成connfd描述符(写法也参照前面一章)
add_client()函数
添加一个新的客户端到活动客户端池中,更新pool结构中的数据.
=============================内容分割线↓===================================
注意select函数的解读,他的第一次调用和多次调用的返回值不同.
当第一次被调用,也就是maxfd+1=4的情况下,此时读集合只有一种情况响应---第一个连接请求进来,listenfd描述符被激活.这种情况下p->nready等于1.而如果select函数非第一次调用,表示他可能会有新的连接,或者已有连接准备好读,这时的p->nready不等于1.---代码要考虑满足多种情况.
=============================内容分割线↑===================================
第4行更新p->nready,添加描述符到客户端池的时候,准备好集合的元素个数-1.
第5~8行查找槽位,把生成的connfd描述符写入clientfd数组.
注意第6行的if(p->clientfd[i]<0)不是唯一写法,因为在init_pool的定义中clientfd数组中的值都等于-1.而生成的connfd的值都是≥4的,所以写成if(p->clientfd[i]==-1)也可以.
第9行初始化读缓冲区,这部分内容没有细说,可以"抄".这里有个clientrio数组,是分配给每个连接的缓冲区.
第12行把connfd添加进读集合,意思是下次调用select可以作为条件进入准备好集合.
第15,16行更新maxfd,当读集合添加了一个描述符后,其基数应该加1.这里用了一个比较
第17,18行更新maxi,"顺势"使新进入的连接作为准备好集合的一员,参加后面的代码,更新准备好集合的索引
第21行可不写,写了更明确:如果i==FD_SETSIZE,超出最大连接数,add_client函数执行无效
check_clients()函数
/*主函数中已没有其他内容,check_clients()函数紧跟add_client()函数.*/
本书P690中间有注释:check_clients服务准备好的客户端连接.遍历clientfd数组,取出文件描述符并读写,不详述.
注意:
第7行的for条件中p->nready>0,表示第一次添加进准备好集合(上面17,18行的描述)不能进入这个环节.
第12行的connfd>0笔者没看懂,以为每个connfd都要≥4(可能有些东西没完全搞懂),
第12行的FD_ISSET(connfd,&p->ready_set)判断遍历出来的connfd是否是本次select执行的结果,以此作为后面代码执行的前提.从这里可以反推出select函数执行的结果是类似于{1,3,5}这样的整型数组.
/*语法上使用if或者for,while做条件判断时,如果有多重判断,值得注意他的意思*/
I/O多路复用技术的优劣
以下黑体字是本书原话
1.基于事件驱动,听起来很好,为客户端提供他们需要的服务,对于基于进程的并发服务器来说,是很困难的.
2.编码复杂.我们的事件驱动的并发echo服务器需要的代码比基于进程的服务器多三倍,并且很不行,随着并发粒度的减小,复杂性还会上升.
3.不能充分利用多核处理器.
代码层面的小结
1.当想要对程序数据进行控制和描述时,维护一个结构.结构的数据类型设计,和读代码时一样,不用一次到位,一边写代码一边根据需要增加(笔者在前面数据类设计中提到过,即使有冗余属性问题也不大---也就是代码不够优雅).结构的好处是他是全局变量,可以动态修改,实时更新.
2.select函数的第一个参数,从前面的硬编码listenfd+1变成了maxfd+1,实时更新.
3.遗憾本书并没有把select讲得很透彻,需要找资料补充.select的使用限于"复制粘贴".比如select调用到什么情况下结束?和时间长短是否有关系(像进程一样由时间片停止)
小结
基于I/O多路复用的并发事件驱动服务器的一点理解