当前位置: 首页 > Linux

基于汇编的C-C++协程-背景

时间:2023-04-06 11:48:18 Linux

近年来,出现了针对C/C++服务器中协程的解决方案。本文主要阐述汇编中实现上下文切换的协程方案,并讲解其在异步开发模式下的应用。本文地址:https://segmentfault.com/a/1190000013070736首先我们来了解一下C/C++服务器的发展史。参考资料Coroutines-Wikipedia,thefreeencyclopediaAsynchronousIO-Wikipedia,thefreeencyclopedia基于epoll设计类似libevent的异步I/O库——接口系统调用真正的效率瓶颈在哪里?什么是python协程?——这个讨论页面不仅仅是关于Python的。其实大部分都是从语言无关的角度来回答协程是什么的。传统的C/C++服务器设计框架同步I/O框架长期以来,在使用C/C++编写服务器程序时,往往采用多进程模式:一个父进程负责接受传入的连接,然后fork一个子进程处理处理;或者一个父进程创建socket后,fork多个子进程同时执行accept和process。为什么说这是同步呢?因为这种设计思路对于socket处理完全是教科书式的思路。我在介绍libev后的评论中也提到了这个想法:有专业背景的软件专业的学生,??往往会一路简单地学习socket、bind、connect、accept、read、write等API。正好,如果真的是按顺序调用,然后写一个server/client,也算是一种同步I/O的编程思想。程序执行的系统调用的每一步都会被阻塞(直接结果是造成进程切换),等待远程机器的响应,直到数据到来才会执行下一步。这是典型的同步(阻塞)I/O。如果把上述过程的每一步都简单的写下来,因为阻塞的存在,是无法支持高并发的。为了解决这个问题,加入fork,可以实现为多个client服务。同步开发模式同步I/O框架采用同步开发模式。人的思维是同步的。先做什么,再做什么,是一条线的过程。这样从头到尾一条线的开发模式,是一种同步开发模式,非常符合人们的思维习惯,易于设计和理解。同步I/O的优点简单明了——在同步I/O框架中,采用同步开发模式,设计的程序代码简洁明了。bug少——其实上面已经讲过了,程序简洁明了,容易理解,调试也很容易。高度灵活——一个服务以进程的方式存在。如果程序有bug而崩溃,不会影响其他服务。另外,如果子进程处理完连接后直接退出,几乎不需要考虑内存泄漏的问题——进程创建的所有资源都会被操作系统回收。同步I/O的缺点是效率低——fork是为了在进程间切换而设计的。这是一个需要困在内核中的操作,需要很长时间。对于高并发场景,服务器资源的利用效率很低。进程间的同步是复杂的——一个进程什么时候占用CPU,什么时候关闭是不可预测的,所以进程间同步是很有必要的。多进程同步的调试非常非常复杂。进程间通信复杂——这个没什么好说的,进程间通信够写一本书了。这一点,在需要任务间通信的场景下,增加了开发的复杂度。开个玩笑,我入职后看到的第一台服务器是基于libevent设计的,而且整个团队一直在这样设计,以至于我以前认为根本就没有用到同步I/O。..异步I/O框架先从技术层面说说“异步I/O框架”。维基百科上对“异步IO”的定义是:发起IO请求的线程不等待IO操作完成,继续执行后续代码,IO结果通知发起IO请求的程序在其他方法。也就是说,一个进程或线程需要告诉操作系统:我需要在一个文件描述符或句柄上进行读写,但是进程或线程并没有等待读或写就绪,而是等到真正有数据了当可以读取或写入数据时,执行相应的操作。其实read/write很早就在理论上提供了对此类操作的支持,那就是O_NONBLOCK标志。当为socket设置该标志时,如果资源在读/写执行过程中暂时不可用,将返回一个响应错误。此时,程序可以跳过这个句柄,转到下一个资源。但是这种方案显然是不现实的,因为当客户端数量很多的时候,需要轮询所有的资源,非常浪费CPU时间,也大大降低了服务的响应速度。因此,操作系统需要提供定义的后半部分:“通知”。实现“通知”的方式其实是一个系统调用:select。其实select的效率很低,一般的操作系统都会提供替代方案。对于Linux,它是epoll。我有很多关于异步I/O原理和编程的文章,你可以点这里查看。从技术角度来看,异步I/O框架有以下优点:高效——判断那个资源上准备好的事件,至少O(NlogN)复杂度,极其高效单线程多任务——单线程可以处理多个传入请求,实现伪并行效果,无进程/线程切换,大幅提升处理速度,优化CPU使用同线程不存在同步问题——如果同线程的多个任务需要相互通信,则有完全没有竞争和同步的问题。但是,单线程多任务其实是一个很大的劣势——多个任务在一个线程/进程中处理。如果程序有bug,整个过程就会崩溃,对服务器的发展是不利的。质量要求高。Asynchronousdevelopmentmode异步I/O框架,大多采用异步开发模式。暂且不使用这个词汇,换成大家比较熟悉的词。下面两个词其实可以解释什么叫异步开发模式:事件驱动开发模式状态机编程异步开发模式是基于事件驱动的,当任何事件到来时,调用哪个回调进行处理——或者回调决定什么事件发生了,然后调用不同的函数来处理它。这和我们传统的思维不同,所以很大程度上,我们需要画状态机来很好的说明我们的软件逻辑。异步开发模式的缺点其实异步开发的世界里充斥着各种各样的回调和回调注册。如果我们不是相应业务代码的开发者,那么当我们阅读代码,看到一个函数被执行时,我们并不知道这个函数的调用者是谁,也就无法跟踪判断下一段代码是什么是。这给调试带来了很大的困难。事实上,即使是程序的开发者,如果文档不充分,时间长了,恐怕也会忘记自己当时的业务逻辑……因此,异步开发模型对水平的要求很高开发人员和团队的编程风格。异步开发模式的优点但是异步开发模式也有很大的优点,那就是状态机编程。这其实很容易理解。对于一个逻辑不是一整条简单直线,而是有很多分叉的程序——很多外部触发条件,会导致很多不同的状态切换,异步开发模式简直就是福音。一个例子是电梯,正在运行的电梯,其执行逻辑很容易被用户在某个楼层按下按钮的动作中断。电梯需要及时响应用户的操作,以决定下一步应该采取什么行动。电梯至少有以下阶段。中断可能发生在电梯运行的任何阶段:电梯停止,门关闭电梯停止,门打开电梯停止,门正在打开电梯停止,门是关门电梯匀速运行加速电梯减速电梯运行异常。电梯维保此外,还可能存在多种类型的中断:管理员直接下达指令操作同层用户,按不同楼层用户操作。如果采用同步开发模式,这样的逻辑简直就是一场灾难。!在协程之前,我特意分别解释了同步开发模式和同步I/O,异步开发模式和异步I/O。的确,发展模式和技术手段是两个不同的东西。对于逻辑比较线性的服务(尤其是海量服务)(相对于上面“电梯”的例子),我们理想的开发方案是采用同步开发模式——最适合人脑的思维方式,以及也方便程序调试和调试使用异步I/O技术——最高效的底层实现。之前一直认为两者的结合在C/C++中是无法实现的。玩——协程简介协程作为一个服务端组件,存在于很多高级语言中。与线程和进程相比,它的切换非常快(不需要陷入内核态,不需要系统调用),非常适合在海量服务中使用。但在基于C/C++的中间语言服务器开发中,并未大规模引入。原因是C/C++离底层太近了。汇编后的目标文件直接就是汇编语言代码;而汇编语言的底层直接就是虚拟内存。唯一能影响它的就是操作下层代码。操作系统的(硬件寄存器)。但是其他高级语言,比如Java,就不一样了。Java原则上是一种解释型语言,但从开发者的角度来看,它其实和编译型语言是一样的,只是将代码编译成JVM可以识别的程序。这样,JVM可以在实际执行的程序(二进制代码)和程序代码之间提供一个中间层——以往由操作系统执行的任务调度和上下文切换可以由JVM接管,在用户中完成模式。这就是协程的实现。协程的原理协程的实现涉及两件事:协程调度上下文切换C/C++协程调度协程调度的原理,在很大程度上,其实和线程/进程的调度原理是一样的。有抢占式和非抢占式两种。对于C/C++。抢占式很难实现,也没有必要,因为实现抢占式协程调度很费功夫,反而失去了前面说的“同线程不存在同步问题”的优势。因此,对于C/C++协程来说,最好的方式就是使用非抢占式调度,这需要任务通过一定的调用主动让出CPU使用权。进一步具体到服务端编程,由于每个合法传入的连接都具有相同的优先级,所以只需要使用基于epoll的实现进行简单的调度。基于汇编实现的C/C++协程的上下文切换上下文切换是C/C++协程的一大难题,这也是为什么长期以来没有统一的C/C++协程库可用的原因。这部分文章比较长,放在下一篇。