可以使用装饰器设计模式来理解hook
设计。在tiger
中hook
实际上就是对系统调用API
进行一次封装,将其封装成一个与原始系统调用API
同名的接口。当业务在调用这个接口时,会先执行封装中的操作,再执行原始的系统调用API
这样做有很多好处。
malloc
和free
进行hook
,在真正进行内存分配和释放之前,统计内存的引用计数,辅助我们排查内存泄漏等问题socket
相关API
都转成异步,从而提升程序的整体吞吐量。并且hook
之后的API
与原始API
相同,因此对于开发同学来说也不需要重新学习新的接口https://github.com/huxiaohei/tiger.git
tiger
主要hook
了系统底层socket
和sleep
相关API
,是否开始hook
的控制是线程粒度的,可以自由选择。socket
相关操作都是针对fd
,因此我们使用FDEntity
来记录fd
的相关信息,使用FDManager
保存所有的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
采用单例设计模式,管理所有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
的选址方式类似
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
实现原理已经在上面提到,这里就不过多解释。列举部分代码,如果有兴趣可以直接阅读源码
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