嵌入式软件异步编程:请求的多阶段异步处理

本文节选自《深入理解Nginx模块开发与架构解析(第2版)》第8章 Nginx基础架构 >> 8.2Nginx的架构设计 >> 8.2.3 请求的多阶段异步处理,和这段文字一样格式的内容是我追加的注解。

这里所讲的多阶段异步处理请求与事件驱动架构是密切相关的,换句话说,请求的多阶段异步处理只能基于事件驱动架构实现。什么意思呢?就是把一个请求的处理过程按照事件的触发方式划分为多个阶段,每个阶段都可以由事件收集、分发器来触发。

例如,处理一个获取静态文件的HTTP请求可以分为以下几个阶段(见表8-1)。

表8-1 处理获取静态文件的HTTP请求时切分的阶段及各阶段的触发事件:

阶段意义触发事件
建立TCP连接接收到TCP中的SYNC包
开始接收用户请求接收到TCP中的ACK包表示连接建立成功
接收到用户请求并分析已接收的请求是否完整接收到用户的数据包
接收到完整的用户请求后开始处理用户请求接收到用户的数据包
由目标静态文件中读取部分内容(避免长期阻塞事件分发者进程)并直接发送给用户接收到用户的数据包、或者接收到TCP中的ACK包表示用户已接收到上次发送的数据包,TCP滑动窗口向前滑动
对于非keep-alive请求,在发送完静态文件后主动关闭连接接收到TCP中的ACK包表示用户已接收到之前发送的所有数据包
由于用户关闭连接而结束请求接收到TCP中的FIN包

这个例子中大致分为7个阶段,这些阶段是可以重复发生的,因此一个下载静态资源请求可能会由于请求数据过大、网速不稳定等因素而被分解成上千个表8-1中所列出的阶段。

对于嵌入式软件而言,可以拿I2C驱动做个例子,I2C的操作过程可以划分为以下阶段:发送起始位阶段、发送设备地址阶段、数据传输阶段、ACK处理阶段、发送停止位阶段。其中数据传输阶段和ACK处理阶段会循环多次。

异步处理和多阶段是相辅相成的,只有把请求分为多个阶段,才有所谓的异步处理。也就是说,当一个事件被分发到事件消费者中进行处理时,事件消费者处理完这个事件只相当于处理完1个请求的某个阶段。什么时候可以处理下一个阶段呢?这只能等待内核的通知,即当下一次事件出现时,epoll等事件分发器将会获取到通知,再继续调用事件消费者处理请求。这样,每个阶段中的事件消费者都不清楚本次完整的操作究竟什么时候会完成,只能异步被动地等待下一次事件的通知。

请求的多阶段异步处理优势在哪里?这种设计配合事件驱动架构,将会极大地提高网络性能,同时使得每个进程都能全力运转,不会或者尽量少地出现进程休眠状况。因为一旦出现进程休眠,必然减少并发处理事件的数目,一定会降低网络性能,同时会增加请求时间的平均时延!这时,如果网络性能无法满足业务需求将只能增加进程数目,进程数目过多就会增加操作系统内核的额外操作:进程间切换,可是频繁地进行进程间切换仍会消耗CPU等资源,从而降低网络性能。同时,休眠的进程会使进程占用的内存得不到有效释放,这最终必然导致系统可用内存的下降,从而影响系统能够处理的最大并发连接数。

根据什么原则来划分请求的阶段呢?一般是找到请求处理流程中的阻塞方法(或者造成阻塞的代码段),在阻塞代码段上按照下面4种方式来划分阶段:

(1)将阻塞进程的方法按照相关的触发事件分解为两个阶段

一个本身可以能导致进程休眠的方法或系统调用,一般都能够分解为多个更小的方法或者系统调用,这些调用间可以通过事件触发关联起来。大部分情况下,一个阻塞进程的方法调用时可以划分为两个阶段:阻塞方法改为非阻塞方法调用,这个调用非阻塞方法并将进程归还给事件分发器的阶段是第一阶段;增加新的处理阶段(第二阶段)用于处理非阻塞方法最终返回的结果,这里的结果返回事件就是第二阶段的触发事件。

例如,在使用send调用发送数据给用户时,如果使用阻塞socket句柄,那么send调用在向操作系统内核发出数据包后就必须把当前进程休眠,直到成功发出数据才能“醒来”。这时的send调用发送数据并等待结果。我们需要把send调用分解为两个阶段:发送且不等待结果阶段、send结果返回阶段。因此,可以使用非阻塞socket句柄,这样调用send发送数据后,进程是不会进入休眠的,这就是发送且不等待结果阶段;再把socket句柄加入到事件收集器中就可以等待相应的事件触发下一个阶段,send发送的数据被对方收到后这个事件就会触发send结果返回阶段。这个send调用就是请求的划分阶段点。

使用中断驱动模式时就不自觉地使用了这种划分,设置外设的代码是一个阶段(发起请求的阶段),中断服务函数是另一个阶段(处理结果的阶段),而且中断服务函数确实是事件(中断是事件的一种特殊形式,硬件产生的事件通常称为中断)触发的。

(2)将阻塞方法调用按照时间分解为多个阶段的方法调用

注意,系统中的事件收集、分发者并非可以处理任何事件。如果按照前一种方式试图划分某个方法时,那么可能会发现找出的触发事件不能够被事件收集、分发器所处理,这时只能按照执行时间来拆分这个方法了。

例如读取文件的调用(非异步I/O),如果我们读取10MB的文件,这些文件在磁盘中的块未必是连续的,这意味着当这10MB文件内容不在操作系统的缓存中时,可能需要多次驱动硬盘寻址。在寻址过程中,进程多半会休眠或者等待。我们可能会希望像上文所说的那样把读取文件调用分解成两个阶段:发送读取命令且不等待结果阶段、读取结果返回阶段。这样当然很好,可惜的是,如果我们的事件收集、分发者不支持这么做,该怎么办?例如,在Linux上Nginx的事件模块在没打开异步I/O时就不支持这种方法,像ngx_epoll_module模块只要是针对网络事件的,而主机的磁盘事件目前还不支持(必须通过内核异步I/O)。这时,我们可以这样来分解读取文件调用:把10MB均分成1000份,每次只读取10KB。这样,读取10KB的时间就是可控的,意味着这个事件接受器占用进程的时间不会太久,整个系统可以及时地处理其他请求。

那么,在读取0KB~10KB的阶段完成后,怎样进入10KB~20KB阶段呢?这有很多种方式,如读取完10KB文件后,可能需要使用网络来发送它们,这时可以由网络事件来触发。或者,如果没有网络事件,也可以设置一个简单的定时器,在某个时间点后再次调用下一个阶段。

在单线程编程模型中,将长时任务分割成多个短时任务,这可以避免执行长时任务时饿死其他任务的现象。这个方法在《The Definitive Guide to ARM® Cortex®-M0 and Cortex-M0+ Processors》3.2.5 Introduction to Embedded Software Program Flows >> Handling Concurrent Processes 小节中也有论述。

(3)在“无所事事”且必须等待系统的响应,从而导致进程空转时,使用定时器划分阶段

有时阻塞的代码段可能是这样的:进行某个无阻塞的系统调用后,必须通过持续的检查标志位来确定是否继续向下执行,当标志位没有获得满足时就循环地检查下去。这样的代码段本身没有阻塞方法调用,可实际上是阻塞进程的。这时,应该使用定时器来代替循环检查标志,这样定时器事件发生时就会先检查标志,如果标志位不满足,就立刻归还进程控制权,同时继续加入期望的下一次定时器事件。

在嵌入式软件中,这种情况可以对应繁忙等待轮询,繁忙等待轮询的过程就是“无所事事”的过程,可以使用定时轮询替代繁忙等待轮询。我碰到的一个只能轮询的情况是:写数据到SD卡。数据传输完毕后要不断地读取SD卡状态寄存器看是否写入完成。对于没有中断线的外设,例如SD卡、USB设备等,都是只支持轮询的。

(4)如果阻塞方法完全无法继续划分,则必须使用独立的进程执行这个阻塞方法

如果某个方法调用时可能导致进程休眠,或者占用进程时间过长,可是又无法将该方法分解为不阻塞的方法,那么这种情况是与事件驱动架构相违背的。通常是由于这个方法的实现者没有开发非阻塞接口所导致,这时必须通过产生新的进程或者指定某个非事件分发者进程来执行阻塞方法,并在阻塞方法执行完毕时向事件收集、分发器进程发送事件通知继续执行。因此,至少要拆分为两个阶段:阻塞方法执行前阶段、阻塞方法执行后阶段,而阻塞方法的执行要使用单独的进程去调用,并在方法返回后发送事件通知。一旦出现上面这种设计,我们必须审视这样的事件消费者是否足够合理,有没有必要用这种违反事件驱动架构的方式来解决阻塞问题。

当然也可以由新创建的线程来执行阻塞的操作,甚至可以创建专门的线程池来处理阻塞操作。

请求的多阶段异步处理将会提高网络性能、降低请求的时延,在与事件驱动架构配合工作后,可以使得Web服务器同时处理十万甚至百万级别的并发连接,我们在开发Nginx模块时必须遵循这一原则。

对于嵌入式软件而言,多阶段异步处理机制的引入,使用单线程就可以完成原来需要多线程才能完成的工作,减少线程就可以减少线程堆栈空间的使用,进而减少RAM的使用,甚至可以完全摒弃RTOS。另一方面,异步处理机制并发处理多个任务,使得CPU有很好的执行效率,可以在更短的时间内完成任务,这可以节省电力,在低功耗应用场合,尽快完成任务是非常关键的。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值