syalr-7-hook

对sylar的hook模块进行说明

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

image-20240407163934842

基础知识

什么是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);
}