读书笔记#1-2020-深入理解nginx#1之事件机制

  1. 主要是事件机制。
  2. 多路复用与select会混用,有多种事件机制,但是select打字比较容易,而且大家也比较熟悉,说epoll或者IOCP,kqueue也未尝不可。

事件机制在服务器端编程中使用得是非常多的,而且已经有了很多这样的框架,如reactor,libevent,ev还有很多象是nginx,windriver的路由协议栈框架都这样做了。基本的原理就是检查socket上的可读可写事件,然后接收数据,进行处理,一般实作上处理TCP较多,经过扩展也能够处理UDP,信号,定时器,文件IO。

第一层次是处理TCP上的可读事件,但是会自己定义帧格式,TCP本身是流式,没有分帧,但是写程序的,总是要自定义一个格式,一般来说是一个固定头,头上有个长度,用来指示消息体的长度。最简单的做法就分为两次可读事件的处理,先是读固定头,然后根据长度读消息体,而本身两次读是阻塞的,大概的伪代码就是这样的:

MyHead * p = alloc_myhead();
read_myhead(p);
body_len = get_body_len(p);
MyBody *q = alloc_mybody();
read_mybody(q);

第二层次加上可写。上面是阻塞式的做法,加上非阻塞式之后,就是在可读事件里面调用read_myhead与read_mybody。对于写回复,如果回复内容不是太大,就直接调用socket的写操作,内容都在内核缓存。对于特别大的回复消息,才需要进行可写事件处理。这就是第二层次的可写。

第三层次,可读可写完全非阻塞。如果本身消息头与消息体都很大,这时候,就需要做成完全的非阻塞式的读,基本的模式是两层,最底层,在可读时读完,在可写时写完,上面一层就是读多少内容之后,回调用户任务处理,写多少完之后,回调用户处理。为了避免可写事件频繁调用,还会设置lowat,只有达到一定程度的可写,才进行写处理。避免那种极端的情况,比如每次只能写几个字节之类的情况。

第四层次,处理定时器。socket上的读写,可以通过多路复用处理,但是定时器是不会通知多路复用器(可以理解为select系统调用),一种选择是使用内核定时器,这时候,会中断多路复用的检测(可以理解为select)调用返回,然后选择对事件进行处理(就是处理超时)。但是一般不这样做,因为如果管理的定时器太多,而且很多定时器又涉及到要取消,要重新设置时间之类的操作,通用的做法是直接在用户态实现定时器。而驱动这个定时器的一般采用两种方式:第一是最小超时值,也就是在每次select之前,检测最早超时的那个定时器,然后以那个定时器为限设置select的超时值;第二是本身最大超时值,在第一种算出来的超时值,会规定一个上限,常用的是1s或者100ms这样的值。在每次多路复用处理循环中,都进行定时器的判定与处理。

第五层次,处理文件IO。有的系统可以直接在多路复用里面加上文件异步IO,有的不可以。如果直接支持,那是再简单没有了,对于没有直接支持的,如果是因为完全没有文件异步IO,一般通过启单独的线程来模拟,如果只是多路复用上没有支持文件异步IO,那么通过定时器检测异步IO也是可以的。在《深入理解nginx》这本书里面,重点讲了linux aio与epoll的配合,如果没有epoll(事件处理),也是可以使用aio的,不就是io_setup,io_commit, io_wait这一堆操作,这个过程与多路复用的事件机制是非常类似的,只是这一次我们会阻塞在io_wait这个地方而不是epoll_wait上。但是如果我们的框架是多路事件机制了,就不能既阻塞在epoll_wait上,又阻塞在io_wait上了,怎么办呢,难不成要使用多线程,一个给epoll_wait用,一个给io_wait用?linux上的解决方案是,对于aio,你可以绑定一个通知句柄,当要进行IO通知的时候,这个句柄上就能够收到(可读)的事件,再把这个句柄交给epoll管理,我们改变了做法,不阻塞在io_wait上,而是仅仅阻塞在epoll_wait上,当文件通知句柄的钟声响起的时候,我们直接获取aio的事件。

你可能感兴趣的:(读书笔记#1-2020-深入理解nginx#1之事件机制)