Sylar服务器框架——Hook

1、Hook概述

  • hook实际上就是对系统调用API进行一次封装,将其封装成一个与原始的系统调用API同名的接口,应用在调用这个接口时,会先执行封装中的操作,再执行原始的系统调用API。这些添加进去的操作为了完整想要的需求,而系统中提供的API又没有这些需求。比如可以对系统提供malloc()和free()进行hook,在真正进行内存分配和释放之前,统计内存的引用计数,以排查内存泄露问题。sylar框架中的Hook定义了线程局部变量t_hook_enable,用于表示当前线程是否启用hook,使用线程局部变量表示hook模块是线程粒度的,各个线程可单独启用或关闭hook
    2、框架中Hook的设计目的
  • 主要目的是对一些阻塞的操作进行hook,在用户看来这些操作还是阻塞的,但是系统中,这些阻塞的操作时间可以拿出来做有意义的事情,而不是让cpu阻塞在那,浪费资源。实现这些功能,就用到了之前实现的定时器和协程相关的功能。
    3、如何使Hook函数生效
    Sylar的代码中,hook.h头文件并没有函数声明,为什么可以在其他cc文件中,调用一系列的api呢?这是因为在CMakeList中编译可执行程序时,将sylar的动态库加载到了程序的进程空间,使得在执行可执行程序之前,其全局符号表中就有了函数对应的符号,这样在后续加载libc共享库时,由于全局符号介入机制,libc中的函数符号不会再被加入全局符号表,所以全局符号表中的函数就变成了我们自己的实现。通过ldd 可执行程序可以看到可执行程序依赖于sylar动态库:在这里插入图片描述
    其实还可以通过另一种方式使hook生效,就是在声明一遍函数签名,这样全局符号表中的符号自然就是自己写的函数。
    上面的过程涉及到了查找后加载的动态库里被覆盖的符号地址问题。首先,这个操作本身就具有合理性,因为程序运行时,依赖的动态库无论是先加载还是后加载,最终都会被加载到程序的进程空间中,也就是说,那些因为加载顺序靠后而被覆盖的符号,它们只是被“雪藏”了而已,实际还是存在于程序的进程空间中的,通过一定的办法,可以把它们再找回来。在Linux中,这个方法就是dslym,它的函数原型如下:
// 使用dlsym找回被覆盖的符号时,第一个参数固定为 RTLD_NEXT,第二个参数为符号的名称
void *dlsym(void *handle, const char *symbol);

4、代码实现

  • Socket Fd相关
/**
 * @brief 文件句柄上下文类
 * @details 管理文件句柄类型(是否socket)
 *          是否阻塞,是否关闭,读/写超时时间
 */
 //FdCtx类在用户态记录了fd的读写超时和非阻塞信息,其中非阻塞包括用户显式设置的非阻塞和hook内部设置的非阻塞,区分这两种非阻塞可以有效应对用户对fd设置/获取NONBLOCK模式的情形。
class FdCtx : public std::enable_shared_from_this<FdCtx> {
public:
    typedef std::shared_ptr<FdCtx> ptr;
    /**
     * @brief 通过文件句柄构造FdCtx
     */
    FdCtx(int fd);
    /**
     * @brief 析构函数
     */
    ~FdCtx();
    ....
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;
};
 
/**
 * @brief 文件句柄管理类
 */
class FdManager {
public:
    typedef RWMutex RWMutexType;
    /**
     * @brief 无参构造函数
     */
    FdManager();
 
    /**
     * @brief 获取/创建文件句柄类FdCtx
     * @param[in] fd 文件句柄
     * @param[in] auto_create 是否自动创建
     * @return 返回对应文件句柄类FdCtx::ptr
     */
    FdCtx::ptr get(int fd, bool auto_create = false);
 
    /**
     * @brief 删除文件句柄类
     * @param[in] fd 文件句柄
     */
    void del(int fd);
private:
    /// 读写锁
    RWMutexType m_mutex;
    /// 文件句柄集合
    std::vector<FdCtx::ptr> m_datas;
};
 
/// 文件句柄单例
typedef Singleton<FdManager> FdMgr;
  • 查找原始函数地址
#define HOOK_FUN(XX) \
    XX(sleep) \
    XX(usleep) \
    XX(nanosleep) \
    XX(socket) \
    XX(connect) \
    XX(accept) \
    XX(read) \
    XX(readv) \
    XX(recv) \
    XX(recvfrom) \
    XX(recvmsg) \
    XX(write) \
    XX(writev) \
    XX(send) \
    XX(sendto) \
    XX(sendmsg) \
    XX(close) \
    XX(fcntl) \
    XX(ioctl) \
    XX(getsockopt) \
    XX(setsockopt)
 
extern "C" {
#define XX(name) name ## _fun name ## _f = nullptr;
    HOOK_FUN(XX);
#undef XX
}
 
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
}
  • sleep/usleep/nanosleep的hook实现
//以sleep为例
unsigned int sleep(unsigned int seconds) {
    if(!sylar::t_hook_enable) {
        return sleep_f(seconds);
    }
 
    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::GetThis()->yield();
    //这一段是精华,利用定时器实现sleep,当时间到的时候,会再执行当前协程,只剩return
    // 而在sleep的过程中,协程让出了cpu资源去做其它有意义的事情
    return 0;
}
  • connect_with_timeout
int connect_with_timeout(int fd, const struct sockaddr* addr, socklen_t addrlen, uint64_t timeout_ms) {
    if(!sylar::t_hook_enable) {
        return connect_f(fd, addr, addrlen);
    }
    sylar::FdCtx::ptr ctx = sylar::FdMgr::GetInstance()->get(fd);
    if(!ctx || ctx->isClose()) {
        errno = EBADF;
        return -1;
    }
 
    if(!ctx->isSocket()) {
        return connect_f(fd, addr, addrlen);
    }
 
    if(ctx->getUserNonblock()) {
        return connect_f(fd, addr, addrlen);
    }
 
    int n = connect_f(fd, addr, addrlen);
    if(n == 0) {
        return 0;
    } else if(n != -1 || errno != EINPROGRESS) {
        return n;
    }
 
    sylar::IOManager* iom = sylar::IOManager::GetThis();
    sylar::Timer::ptr timer;
    std::shared_ptr<timer_info> tinfo(new timer_info);
    std::weak_ptr<timer_info> winfo(tinfo);
 
    if(timeout_ms != (uint64_t)-1) {
        timer = iom->addConditionTimer(timeout_ms, [winfo, fd, iom]() {
                auto t = winfo.lock();
                if(!t || t->cancelled) {
                    return;
                }
                t->cancelled = ETIMEDOUT;
                iom->cancelEvent(fd, sylar::IOManager::WRITE);
        }, winfo);
    }
 
    int rt = iom->addEvent(fd, sylar::IOManager::WRITE);
    if(rt == 0) {
        sylar::Fiber::GetThis()->yield();
        // 这里的唤醒有两种方式
        // 1、定时器超时
        // 2、Event触发
        if(timer) {
            timer->cancel();
        }
        if(tinfo->cancelled) {
            errno = tinfo->cancelled;
            return -1;
        }
    } else {
        if(timer) {
            timer->cancel();
        }
        SYLAR_LOG_ERROR(g_logger) << "connect addEvent(" << fd << ", WRITE) error";
    }
 
    int error = 0;
    socklen_t len = sizeof(int);
    if(-1 == getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &len)) {
        return -1;
    }
    if(!error) {
        return 0;
    } else {
        errno = error;
        return -1;
    }
}
/*
1、判断传入的fd是否为套接字,如果不为套接字,则调用系统的connect函数并返回。
2、判断fd是否被显式设置为了非阻塞模式,如果是则调用系统的connect函数并返回。
3、调用系统的connect函数,由于套接字是非阻塞的,这里会直接返回EINPROGRESS错误。
4、如果超时参数有效,则添加一个条件定时器,在定时时间到后通过t->cancelled设置超时标志并触发一次WRITE事件。
5、添加WRITE事件并yield,等待WRITE事件触发再往下执行。
6、等待超时或套接字可写,如果先超时,则条件变量winfo仍然有效,通过winfo来设置超时标志并触发WRITE事件,协程从yield点返回,返回之后通过超时标志设置errno并返回-1;如果在未超时之前套接字就可写了,那么直接取消定时器并返回成功
*/
  • io相关的函数和上面的connect_with_timeout实现思想类似
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值