Hook模块
可以使用装饰器设计模式来理解hook
设计。在tiger
中hook
实际上就是对系统调用API
进行一次封装,将其封装成一个与原始系统调用API
同名的接口。当业务在调用这个接口时,会先执行封装中的操作,再执行原始的系统调用API
这样做有很多好处。
- 可以使应用程序在执行调用调用之前进行一些隐藏操作。比如:可以对系统提供的
malloc
和free
进行hook
,在真正进行内存分配和释放之前,统计内存的引用计数,辅助我们排查内存泄漏等问题 - 可以结合协程将
socket
相关API
都转成异步,从而提升程序的整体吞吐量。并且hook
之后的API
与原始API
相同,因此对于开发同学来说也不需要重新学习新的接口
github
https://github.com/huxiaohei/tiger.git
实现
tiger
主要hook
了系统底层socket
和sleep
相关API
,是否开始hook
的控制是线程粒度的,可以自由选择。socket
相关操作都是针对fd
,因此我们使用FDEntity
来记录fd
的相关信息,使用FDManager
保存所有的FDEntity
FDEntity
FDEntity
设计目的是为了记录fd
上下文。FDEntity
类在用户态记录了fd
的读写超时和非阻塞信息,其中非阻塞包括用户显式设置的非阻塞和hook
内部设置的非阻塞,区分这两种非阻塞可以有效应对用户对fd
设置/获取NONBLOCK
模式的情形
class FDEntity {
private:
bool m_is_init = false;
bool m_is_sys_nonblock = false;
bool m_is_user_nonblock = false;
bool m_is_socket = false;
bool m_is_closed = false;
int m_fd = 0;
uint64_t m_connect_timeout = 0;
uint64_t m_recv_timeout = 0;
uint64_t m_send_timeout = 0;
public:
typedef std::shared_ptr<FDEntity> ptr;
FDEntity(int fd);
~FDEntity(){};
public:
bool init();
};
FDEntity::FDEntity(int fd)
: m_fd(fd) {
init();
}
bool FDEntity::init() {
if (m_is_init) return false;
m_connect_timeout = 0;
m_recv_timeout = 0;
m_send_timeout = 0;
struct stat fd_stat;
if (-1 == fstat(m_fd, &fd_stat)) {
m_is_init = false;
m_is_socket = false;
} else {
m_is_init = true;
m_is_socket = S_ISSOCK(fd_stat.st_mode);
}
if (m_is_socket) {
int flags = fcntl_f(m_fd, F_GETFL, 0);
if (!(flags & O_NONBLOCK)) {
fcntl_f(m_fd, F_SETFL, flags | O_NONBLOCK);
}
m_is_sys_nonblock = true;
} else {
m_is_sys_nonblock = false;
}
m_is_user_nonblock = false;
m_is_closed = false;
return m_is_init;
}
FDManager
FDManager
采用单例设计模式,管理所有FDEntity
实例。因此创建、删除、获取FDEntity
实例都应该使用FDManager
所提供的接口
void del_fd(int fd);
FDEntity::ptr add_fd(int fd);
FDEntity::ptr get_fd(int fd, bool auto_create = false);
注意:FDManager
中对FDEntity
寻址方式与IO
协程调度器中对Context
的选址方式类似
Hook
tiger
中hook
是在IO
协程调度器的基础上实现的,如果不使用IO
协程调度器,那么hook
就没有任何意义。
首先考虑IOManager
要在一个线程上调度以下协程
- 协程一
sleep
两秒后返回 - 协程二 通过
socket
接口send
发送100K
数据 - 协程三 通过
socket
接口recv
接收数据
在未hook
的情况下,IOManager
要调度上面的协程,流程是下面这样的:
- 协程一阻塞在
sleep
上,等两秒后返回。这两秒内调度线程是被协程一占用的,其他协程无法在当前线程上调度 - 协程二阻塞
send
上,这个操作一般问题不大,因为send
数据无论如何都要占用时间,但如果fd
迟迟不可写,那send
会阻塞直到套接字可写,同样,在阻塞期间,其他协程也无法在当前线程上调度 - 协程三阻塞在
recv
上,这个操作要直到recv
超时或是有数据时才返回,期间调度器也无法调度其他协程
从调度流程上看,协程只能按照顺序调度。在协程中一旦执行了阻塞操作,那么整个线程就会被阻塞,导致调度器也无法执行其他协程,最终降低调度器的吞吐量。当然,像这种执行方式其实是有可以避免的。比如sleep
,当调度器检测到协程sleep
后,应该将协程挂起Yield
,同时注册一个定时器事件,然后调度器在去执行其它协程,等定时器事件回调时我们在恢复被挂起的协程resume
。这样调度器就可以在这个协程sleep
期间去执行其他协程,从而提高调度器的吞吐量。socket
先关API
的hook
方法与sleep
类似
因此,在实现hook
之后,上面的协程执行应该如下
- 协程一,检测到协程
sleep
,那么先添加一个定时器,定时器回调函数是恢复resume
本协程,接着协程yield
,等定时器超时 - 因为协程一已经
yield
了,所以协徎二并不需要继续等待,而是立刻执行。同样,调度器检测到协程send
,由于不知道fd
是不是马上可写,所以先在IOManager
上给fd
注册一个写事件,回调函数是让当前协程resume
并执行实际的send
操作,然后当前协程yield
,等可写事件发生 - 协徎二也
yield
了,可以马上调度协程三。协程三与协程二类似,也是给fd
注册一个读事件,回调函数是让当前协程resume
并继续recv
,然后本协程yield
,等事件发生 - 等定时器超时后,执行定时器回调函数,将协程一
resume
以便继续执行 - 等协程二的
fd
可写,一旦可写,调用写事件回调函数将协程二resume
以便继续执行send
- 等协程三的
fd
可读,一旦可读,调用读事件回调函数将协程三resume
以便继续执行recv
sleep和socket相关API的hook
实现原理已经在上面提到,这里就不过多解释。列举部分代码,如果有兴趣可以直接阅读源码
unsigned int sleep(unsigned int seconds) {
if (!tiger::__enable_hook()) return sleep_f(seconds);
pid_t t = tiger::Thread::CurThreadId();
auto iom = tiger::IOManager::GetThreadIOM();
auto co = tiger::Coroutine::GetRunningCo();
iom->add_timer(
seconds * 1000, [iom, co, t]() {
iom->schedule(co, t);
},
false);
tiger::Coroutine::Yield();
return 0;
}
template <typename OrgFunc, typename... Args>
static ssize_t do_socket_io(int fd, OrgFunc func, const char *hook_func_name,
tiger::IOManager::EventStatus status, Args &&...args) {
if (!tiger::__enable_hook()) return func(fd, std::forward<Args>(args)...);
auto fd_entity = tiger::SingletonFDManager::Instance()->get_fd(fd);
if (!fd_entity) return func(fd, std::forward<Args>(args)...);
if (fd_entity->is_closed()) {
errno = EBADF;
return -1;
}
if (!fd_entity->is_socket() || fd_entity->is_user_nonblock()) {
return func(fd, std::forward<Args>(args)...);
}
int timeout = -1;
if (status & tiger::IOManager::EventStatus::READ) {
timeout = fd_entity->recv_timeout();
} else if (status & tiger::IOManager::EventStatus::WRITE) {
timeout = fd_entity->send_timeout();
} else {
TIGER_LOG_E(tiger::SYSTEM_LOG) << "[iomanager event status not found"
<< " status:" << status
<< " func:" << hook_func_name << "]";
}
auto state = std::make_shared<SocketIoState>();
ssize_t n = -1;
do {
n = func(fd, std::forward<Args>(args)...);
if (n == -1 && errno == EAGAIN) {
auto iom = tiger::IOManager::GetThreadIOM();
tiger::TimerManager::Timer::ptr timer;
std::weak_ptr<SocketIoState> week_state(state);
if (timeout >= 0) {
timer = iom->add_cond_timer(
timeout, [week_state, fd, iom, status]() {
auto _week_state = week_state.lock();
if (!_week_state || _week_state->canceled) {
return;
}
_week_state->canceled = true;
iom->cancel_event(fd, status);
},
week_state, false);
}
if (iom->add_event(fd, status)) {
tiger::Coroutine::Yield();
iom->cancel_timer(timer);
if (state->canceled) {
errno = ETIMEDOUT;
return -1;
}
state->canceled = false;
continue;
} else {
iom->cancel_timer(timer);
TIGER_LOG_E(tiger::SYSTEM_LOG) << "[iomanager add event error"
<< " status:" << status
<< " hookName:" << hook_func_name << "]";
return -1;
}
}
} while (n == -1 && (errno == EINTR || errno == EAGAIN));
return n;
}
ssize_t read(int fildes, void *buf, size_t nbyte) {
return do_socket_io(fildes, read_f, "read",
tiger::IOManager::EventStatus::READ, buf, nbyte);
}
注意:在tiger
中非调度线程不支持启用hook