【动手写协程库 5】常用IO函数的HOOK功能
【动手写协程库】系列笔记是学习sylar的协程库时的记录,参考了从零开始重写sylar C++高性能分布式服务器框架和代码随想录中的文档。文章并不是对所有代码的详细解释,而是为了自己理解一些片段所做的笔记。
hook函数的具体定义实现可以在这里查看:Github: src/hook.cpp
该协程库框架的目标并不是做成类似goroutine那样,而是希望能够通过协程来提高IO处理的效率。因此,对于每个文件描述符fd,我们都希望它有一个读写IO的超时时间。
hook的目的是在不重新编写代码的情况下,把老代码中的socket IO相关的API都转成异步,以提高性能。
需要Hook的几类函数
在sylar的设计中,只针对socket fd进行hook(因为我们更关心的是网络IO),也就是如果我们操作的不是socket fd,那么就会使用原来的API。
sylar对如下三类函数进行了hook:
- sleep延时系列接口:包括sleep/usleep/nanosleep。对于这些接口的hook,只需要给IO协程调度器注册一个定时事件,在定时事件触发后再继续执行当前协程即可。当前协程在注册完定时事件后即可yield让出执行权
- socket IO系列接口:包括read/write/recv/send…等,connect及accept也可以归到这类接口中。这类接口的hook首先需要判断操作的fd是否是socket fd,以及用户是否显式地对该fd设置过非阻塞模式,如果不是socket fd或是用户显式设置过非阻塞模式,那么就不需要hook了,直接调用操作系统的IO接口即可。如果需要hook,那么首先在IO协程调度器上注册对应的读写事件,等事件发生后再继续执行当前协程。当前协程在注册完IO事件即可yield让出执行权。
- socket/fcntl/ioctl/close等接口:这类接口主要处理的是边缘情况,比如分配fd上下文,处理超时及用户显式设置非阻塞问题。
Hook的实现
我们hook的所有函数,都要与原来的API的行为保持一致(使用这些hook api的时候就好像使用的原来的api)。例如原来的API的返回值通常用0表示成功,-1表示失败
在 Sylar 中,Hook 的实现通常涉及以下几个关键方面:
一、函数指针替换
- 保存原始函数指针:首先,需要保存被 Hook 函数的原始实现的函数指针。这可以通过在程序启动时或者在首次需要 Hook 的时候,获取原始函数的地址并存储起来。例如,可以定义一个与被 Hook 函数具有相同签名的函数指针变量,并将其初始化为指向原始函数的地址。
- 替换函数指针:然后,将被 Hook 函数的入口地址替换为自定义的 Hook 函数的地址。这样,当程序调用被 Hook 函数时,实际上会执行 Hook 函数。
二、参数传递和返回值处理
- 参数传递:在 Hook 函数中,需要接收与被 Hook 函数相同的参数。这可以通过将参数直接传递给 Hook 函数,或者使用一些技术(如函数调用栈的分析)来获取参数的值。如果被 Hook 函数是
int func(int a, char* b)
,那么 Hook 函数也应该具有相同的参数列表int hook_func(int a, char* b)
。 - 返回值处理:Hook 函数需要根据需要处理被 Hook 函数的返回值。可以选择直接返回被 Hook 函数的原始返回值,或者根据特定的逻辑修改返回值后再返回。
三、条件判断和控制
- Hook 启用 / 禁用:通常会提供一种机制来启用或禁用 Hook 功能。这可以通过一个全局变量、配置文件或者运行时参数来控制。我们可以在代码中定义一个布尔变量,如
bool hook_enable
,当它为真时启用 Hook 功能,为假时直接调用原始函数而不执行 Hook 函数。 - 特定条件下的 Hook:可以根据特定的条件来决定是否执行 Hook 函数。例如,可以检查参数的值、函数的调用者、当前的运行环境等条件,只有在满足特定条件时才执行 Hook 函数。
FdManager
我们会通过FdContext
类(注意与IOManager中的FdContext进行区分)来保存fd的一些状态,例如fd是否关闭了,是否设置为非阻塞,其读写事件超时时间是多少等。
sleep API
对sleep
,usleep
,nanosleep
三个函数进行hook操作,其逻辑一致:sleep类函数会阻塞当前线程,那么我们的改造方法就是用一个定时器来代替sleep的休眠阻塞,获取当前运行的协程,然后通过IOManager添加一个定时器,规定时间后再将这个协程加入调度,之后yield这个协程。
socket API
- socket:当使用socket创建套接字fd时,我们需要将它加入到FdManager中。
- connect:对于原始的
connect
,它是一个阻塞调用,直到连接成功或发生错误,如果网络延迟较高或目标主机不可达,可能会导致程序长时间挂起。我们需要将其改造为与异步或非阻塞操作结合。对应的实现方法就是通过设置一个超时时间,到时间后取消文件描述符的写事件。为socket fd添加写事件后,如果添加成功,则yield当前协程,并取消定时器 - setsockopt:对于
optname
为SO_RCVTIMEO
和SO_RCVTIMEO
的情况,我们需要设置sockfd对应的超时时间。
socket IO API
accept、read、readv、recv、recvfrom、recvmsg、write、writev、send、sendto、sendmsg这些函数所要作的hook操作都很类似,不同的地方无非就是读写事件的不同,其处理逻辑和connect相似,所以利用了模板来减少冗余代码。
other API
还有类似close、ioctl、fcntl的函数,由于我们在之前hook api时处理了文件描述符,因此在这些函数中我们需要对文件描述符进行清理或其他操作。