syalr-7-hook
对sylar的hook模块进行说明
hook模块是任务调度模块的最后一个模块,完成本节,下图红色部分,包括日志模块,配置模块,任务调度模块就已全部完成,后面将会使用这些基础模块开发更高级的应用。但是在此之前,我们还需要介绍Hook模块,它原理很简单,但确是sylar实现高性能的关键。

基础知识
什么是hook?
hook实际上是对系统调用api进行的一次封装,将其封装成⼀个与原始的系统调⽤API同名的接口,应用在调用这个接口时,会先执行封装中的操作,再执行原始的系统调用API。比如,我们想统计write系统调用的次数,我们就可以自己写一个write的函数,一个全局变量记录write调用次数,每次调用时次数加1,然后再调用系统的write函数。
为什么需要hook?
系统中很多操作是同步操作,会阻塞协程,比如sleep,connect等,这使得系统的性能下降,通过添加hook,当系统在执行同步操作时,我们添加一个定时器事件后即返回,此时线程可以去执行其他工作,当事件发生或已经超时时,再切换回来处理。将同步的socket操作替换成异步操作,可大大增强系统性能。
举一个例子,如果一个协程需要睡眠2s,另一个协程需要睡眠3s,在不添加hook时,线程是按顺序执行,先执行第一个协程,线程会睡眠2s,然后再执行第二个协程,线程又睡眠3s,最终整个系统需要5s才结束。在对sleep添加hook后,线程执行第一个协程,添加一个2s的定时器事件后返回,然后执行第二个协程,添加一个3s的定时器后返回,过了2s,第一个定时器事件触发,再过1s,第二个定时器触发,最终整个系统只需3s就结束。
使用
是否hook可以线程粒度进行控制,但目前采用更简洁的做法,所有线程要么全都hook,要么全都不hook。IOManager构造函数有个hook_enable标志控制是否开启hook。
IOManager(size_t threads = 1, bool use_caller = true, const std::string& name = "", bool hook_enable = true);
hook对代码业务代码是无侵入的,所以添不添加hook对代码来说都是一样的,以下的测试只是为了检验添加hook后各个函数仍然正常工作。
sylar::Logger::ptr g_logger = SYLAR_LOG_ROOT();
// 为什么需要hook小节介绍的例子
void test_sleep() {
// thread number,use caller?,name,enable_hook?
sylar::IOManager iom(1, true, "main", true);
iom.schedule([](){
sleep(2);
SYLAR_LOG_INFO(g_logger) << "sleep 2";
});
iom.schedule([](){
sleep(3);
SYLAR_LOG_INFO(g_logger) << "sleep 3";
});
SYLAR_LOG_INFO(g_logger) << "test_sleep";
}
// 测试sock同步io操作->异步io操作后功能是否正常
void test_sock() {
int sock = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(80);
inet_pton(AF_INET, "112.80.248.75", &addr.sin_addr.s_addr);
SYLAR_LOG_INFO(g_logger) << "begin connect";
int rt = connect(sock, (const sockaddr*)&addr, sizeof(addr));
SYLAR_LOG_INFO(g_logger) << "connect rt=" << rt << " errno=" << errno;
if(rt) {
return;
}
const char data[] = "GET / HTTP/1.0\r\n\r\n";
rt = send(sock, data, sizeof(data), 0);
SYLAR_LOG_INFO(g_logger) << "send rt=" << rt << " errno=" << errno;
if(rt <= 0) {
return;
}
std::string buff;
buff.resize(4096);
rt = recv(sock, &buff[0], buff.size(), 0);
SYLAR_LOG_INFO(g_logger) << "recv rt=" << rt << " errno=" << errno;
if(rt <= 0) {
return;
}
buff.resize(rt);
SYLAR_LOG_INFO(g_logger) << buff;
}
int main(int argc, char** argv) {
// test_sleep();
// return 0;
sylar::IOManager iom;
iom.schedule(test_sock);
return 0;
}
实现
概述
syalr主要对socket描述符实现hook,所以,在我们进行hook操作时,我们必须得知道这个文件描述符的类型,对socket类型我们默认采用NONBLOCK非阻塞的方式,我们需要向上层用户屏蔽此信息,那么我们同时得hook用户获取,设置socket描述符的相关函数,如getsockopt,setsockopt, fcntl, ioctl。另外,对socket的读写我们需要hook的有,connect,accept,以及read,write相关函数。除了socket外,sylar还实现了对某些通用同步操作的hook,如sleep相关的函数。总而言之,我们需要时实现hook的函数类别有:
- sleep相关:sleep, usleep, nanosleep
- socket读写相关:accept, connect, read/readv/recv/recvfrom/recvmsg,write/writev,send/sendto/sendmsg
- socket状态相关:socket, close, fcntl, ioctl, getsockopt, setsockopt
为了方便获取某个文件描述符的信息,syalr创建了一个FdCtx类来表示一个文件描述符,并用FdManager来管理所有的FdCtx对象。
sylar实现Hook,利用了dlsym从动态库中获取函数的功能,比如对于sleep函数,sylar重写了原本的sleep方法,而原本的sleep通过dlsym动态获取,并用sleep_f表示,新的sleep可以通过sleep_f调用旧sleep,也可以完全重写。
hook原理
namespace sylar {
// 线程是否hook,是通过线程局部变量t_hook_enable来控制的
static thread_local bool t_hook_enable = false;
bool is_hook_enable() {
return t_hook_enable;
}
void set_hook_enable(bool flag) {
t_hook_enable = flag;
}
/*
HOOK_FUN宏和hook_init函数配合,最终生成:
read_f = (read_func)dlsym(RILD_NEXT, read);
wirte_f = (write_func)dlsym(RILD_NEXT, write);
...
为各种hook原本的函数赋值,以便hook函数能利用
(他们的定义在下面)
*/
#define HOOK_FUN(XX) \
XX(sleep) \
XX(usleep) \
XX(nanosleep) \
XX(socket) \
...
// 设置标准库的函数read_f ...
void hook_init() {
static bool is_inited = false;
if(is_inited) {
return;
}
#define XX(name) name ## _f = (name ## _fun)dlsym(RTLD_NEXT, #name);
HOOK_FUN(XX);
#undef XX
}
// 通过生成该struct的静态全局变量,保证在main函数运行之前初始化完毕
static uint64_t s_connect_timeout = -1;
struct _HookIniter {
_HookIniter() {
hook_init();
s_connect_timeout = g_tcp_connect_timeout->getValue();
g_tcp_connect_timeout->addListener([](const int& old_value, const int& new_value){
SYLAR_LOG_INFO(g_logger) << "tcp connect timeout changed from "
<< old_value << " to " << new_value;
s_connect_timeout = new_value;
});
}
};
static _HookIniter s_hook_initer;
} // end namespace
// 定义各类hook原本函数
extern "C" {
/*
XX(sleep) ->
sleep_func sleep_f = nullptr;
*/
#define XX(name) name ## _fun name ## _f = nullptr;
HOOK_FUN(XX);
#undef XX
....
}
FdManager
FdCtx类,封装了文件描述符的信息
class FdCtx : public std::enable_shared_from_this<FdCtx> {
public:
/**
* @brief 通过文件句柄构造FdCtx
*/
FdCtx(int fd);
...
private:
/**
* @brief 初始化
*/
bool init();
private:
/// 是否初始化
bool m_isInit: 1;
/// 是否socket
bool m_isSocket: 1;
/// 是否hook非阻塞
bool m_sysNonblock: 1;
/// 是否用户主动设置非阻塞
bool m_userNonblock: 1;
/// 是否关闭
bool m_isClosed: 1;
/// 文件句柄
int m_fd;
/// 读超时时间毫秒
uint64_t m_recvTimeout;
/// 写超时时间毫秒
uint64_t m_sendTimeout;
};
FdCtx::FdCtx(int fd)
:m_isInit(false)
,m_isSocket(false)
,m_sysNonblock(false)
,m_userNonblock(false)
,m_isClosed(false)
,m_fd(fd)
,m_recvTimeout(-1)
,m_sendTimeout(-1) {
init();
}
// 主要工作是通过fstat获取fd的信息,然后设置其成员变量
bool FdCtx::init() {
if(m_isInit) {
return true;
}
m_recvTimeout = -1;
m_sendTimeout = -1;
struct stat fd_stat;
if(-1 == fstat(m_fd, &fd_stat)) {
m_isInit = false;
m_isSocket = false;
} else {
m_isInit = true;
m_isSocket = S_ISSOCK(fd_stat.st_mode);
}
// 自动为socket添加NONBLOCK特性
if(m_isSocket) {
int flags = fcntl_f(m_fd, F_GETFL, 0);
if(!(flags & O_NONBLOCK)) {
fcntl_f(m_fd, F_SETFL, flags | O_NONBLOCK);
}
m_sysNonblock = true;
} else {
m_sysNonblock = false;
}
m_userNonblock = false;
m_isClosed = false;
return m_isInit;
}
FdManager类,对FdCtx进行管理
class FdManager {
public:
...
// 获取文件句柄
FdCtx::ptr get(int fd, bool auto_create = false);
// 删除文件句柄
void del(int fd);
private:
/// 读写锁
RWMutexType m_mutex;
/// 文件句柄集合
std::vector<FdCtx::ptr> m_datas;
};
FdCtx::ptr FdManager::get(int fd, bool auto_create) {
if(fd == -1) {
return nullptr;
}
// fd代表了文件句柄在m_datas中的下标
RWMutexType::ReadLock lock(m_mutex);
if((int)m_datas.size() <= fd) {
if(auto_create == false) {
return nullptr;
}
} else {
if(m_datas[fd] || !auto_create) { // 找到 or 没找到但不需要创建
return m_datas[fd];
}
}
lock.unlock();
RWMutexType::WriteLock lock2(m_mutex);
FdCtx::ptr ctx(new FdCtx(fd));
if(fd >= (int)m_datas.size()) {
m_datas.resize(fd * 1.5);
}
m_datas[fd] = ctx;
return ctx;
}
void FdManager::del(int fd) {
RWMutexType::WriteLock lock(m_mutex);
if((int)m_datas.size() <= fd) {
return;
}
m_datas[fd].reset();
}
hook函数实现
sleep相关
unsigned int sleep(unsigned int seconds) {
// 若没有开启hook
if(!sylar::t_hook_enable) {
return sleep_f(seconds);
}
// 添加一个seconds秒的定时器,超时之后才返回,从而模拟sleep。注意这是非阻塞的
sylar::Fiber::ptr fiber = sylar::Fiber::GetThis();
sylar::IOManager* iom = sylar::IOManager::GetThis();
iom->addTimer(seconds * 1000, std::bind((void(sylar::Scheduler::*)
(sylar::Fiber::ptr, int thread))&sylar::IOManager::schedule
,iom, fiber, -1));
sylar::Fiber::YieldToHold();
return 0;
}
socket读写相关
struct timer_info {
int cancelled = 0;
};
// socket读写相关的接口基本都有相似的操作,因此封装成一个do_io方法
template<typename OriginFun, typename... Args>
static ssize_t do_io(int fd, OriginFun fun, const char* hook_fun_name,
uint32_t event, int timeout_so, Args&&... args) {
// 若没有开启hook
if(!sylar::t_hook_enable) {
return fun(fd, std::forward<Args>(args)...);
}
// 若该文件描述符没被FdManager管理,则其肯定不是socket,调用原方法
sylar::FdCtx::ptr ctx = sylar::FdMgr::GetInstance()->get(fd);
if(!ctx) {
return fun(fd, std::forward<Args>(args)...);
}
// 若socket已经被关闭
if(ctx->isClose()) {
errno = EBADF;
return -1;
}
// 不是socket或者用户主动设置非阻塞,则不需要加hook
if(!ctx->isSocket() || ctx->getUserNonblock()) {
return fun(fd, std::forward<Args>(args)...);
}
// ------- hooks实现,同步->异步 ---------
// 获取超时时间
uint64_t to = ctx->getTimeout(timeout_so);
std::shared_ptr<timer_info> tinfo(new timer_info);
retry:
// 非阻塞调用
ssize_t n = fun(fd, std::forward<Args>(args)...);
while(n == -1 && errno == EINTR) {
n = fun(fd, std::forward<Args>(args)...);
}
// 数据未就绪
if(n == -1 && errno == EAGAIN) {
sylar::IOManager* iom = sylar::IOManager::GetThis();
sylar::Timer::ptr timer;
std::weak_ptr<timer_info> winfo(tinfo);
// 若设置了超时时间
if(to != (uint64_t)-1) {
/*
添加条件计时器,to时间消息还没来,就取消事件
*/
timer = iom->addConditionTimer(to, [winfo, fd, iom, event]() {
auto t = winfo.lock();
if(!t || t->cancelled) {
return;
}
t->cancelled = ETIMEDOUT;
iom->cancelEvent(fd, (sylar::IOManager::Event)(event));
}, winfo);
}
int rt = iom->addEvent(fd, (sylar::IOManager::Event)(event));
// 添加失败
if(SYLAR_UNLIKELY(rt)) {
SYLAR_LOG_ERROR(g_logger) << hook_fun_name << " addEvent("
<< fd << ", " << event << ")";
if(timer) {
timer->cancel();
}
return -1;
} else {
/* addEvent成功,把执行时间让出来
* 只有两种情况会从这回来:
* 1) 超时了, timer cancelEvent triggerEvent会唤醒回来
* 2) addEvent数据回来了会唤醒回来 */
sylar::Fiber::YieldToHold();
if(timer) {
timer->cancel();
}
if(tinfo->cancelled) {
errno = tinfo->cancelled;
return -1;
}
goto retry;
}
}
return n;
}
// 以read为例,演示如何调用do_io函数
ssize_t read(int fd, void *buf, size_t count) {
// 可以看到,read只是对do_io的一层简单封装,其他io操作也是如此,只有connect是例外
return do_io(fd, read_f, "read", sylar::IOManager::READ, SO_RCVTIMEO, buf, count);
}
// connect的hook接口和do_io非常类似,除了他的超时时间是通过配置g_tcp_connect_timeout获取,以及与connect相关的一些细微差别,读者感兴趣可阅读源码。
socket状态相关
// 创建socket的函数,在原socket的基础上,添加了获取socket信息并加入FdManager管理的代码
int socket(int domain, int type, int protocol) {
if(!sylar::t_hook_enable) {
return socket_f(domain, type, protocol);
}
int fd = socket_f(domain, type, protocol);
if(fd == -1) {
return fd;
}
sylar::FdMgr::GetInstance()->get(fd, true);
return fd;
}
// 在原close的基础上,对于socket描述符,添加了从IOManager中删除,并取消其所有事件代码
int close(int fd) {
if(!sylar::t_hook_enable) {
return close_f(fd);
}
sylar::FdCtx::ptr ctx = sylar::FdMgr::GetInstance()->get(fd);
if(ctx) {
auto iom = sylar::IOManager::GetThis();
if(iom) {
iom->cancelAll(fd);
}
sylar::FdMgr::GetInstance()->del(fd);
}
return close_f(fd);
}
int fcntl(int fd, int cmd, ... /* arg */ ) {
va_list va;
va_start(va, cmd);
switch(cmd) {
// 设置fd状态
case F_SETFL:
{
int arg = va_arg(va, int);
va_end(va);
sylar::FdCtx::ptr ctx = sylar::FdMgr::GetInstance()->get(fd);
if(!ctx || ctx->isClose() || !ctx->isSocket()) {
return fcntl_f(fd, cmd, arg);
}
ctx->setUserNonblock(arg & O_NONBLOCK);
// sysNonblock是系统行为,而UserNonblock是用户指定的行为,系统行为是真正的行为,但是对用户就好像用户指定的行为一样
if(ctx->getSysNonblock()) {
arg |= O_NONBLOCK;
} else {
arg &= ~O_NONBLOCK;
}
return fcntl_f(fd, cmd, arg);
}
break;
// 获取fd状态
case F_GETFL:
{
va_end(va);
int arg = fcntl_f(fd, cmd);
sylar::FdCtx::ptr ctx = sylar::FdMgr::GetInstance()->get(fd);
if(!ctx || ctx->isClose() || !ctx->isSocket()) {
return arg;
}
// 设置用户实际的行为,并返回
if(ctx->getUserNonblock()) {
return arg | O_NONBLOCK;
} else {
return arg & ~O_NONBLOCK;
}
}
break;
...
}
}
int ioctl(int d, unsigned long int request, ...) {
va_list va;
va_start(va, request);
void* arg = va_arg(va, void*);
va_end(va);
// FIONBIO用于设置文件描述符的非阻塞模式
if(FIONBIO == request) {
bool user_nonblock = !!*(int*)arg;
sylar::FdCtx::ptr ctx = sylar::FdMgr::GetInstance()->get(d);
if(!ctx || ctx->isClose() || !ctx->isSocket()) {
return ioctl_f(d, request, arg);
}
// 是socket && socket没关闭
ctx->setUserNonblock(user_nonblock);
}
return ioctl_f(d, request, arg);
}
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen) {
return getsockopt_f(sockfd, level, optname, optval, optlen);
}
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen) {
if(!sylar::t_hook_enable) {
return setsockopt_f(sockfd, level, optname, optval, optlen);
}
// 如果设置socket通用选项
if(level == SOL_SOCKET) {
// 如果设置超时选项
if(optname == SO_RCVTIMEO || optname == SO_SNDTIMEO) {
sylar::FdCtx::ptr ctx = sylar::FdMgr::GetInstance()->get(sockfd);
// 转为毫秒保存
if(ctx) {
const timeval* v = (const timeval*)optval;
ctx->setTimeout(optname, v->tv_sec * 1000 + v->tv_usec / 1000);
}
}
}
return setsockopt_f(sockfd, level, optname, optval, optlen);
}