高性能分布式网络服务器--Hook模块

Hook模块

可以使用装饰器设计模式来理解hook设计。在tigerhook实际上就是对系统调用API进行一次封装,将其封装成一个与原始系统调用API同名的接口。当业务在调用这个接口时,会先执行封装中的操作,再执行原始的系统调用API

这样做有很多好处。

  • 可以使应用程序在执行调用调用之前进行一些隐藏操作。比如:可以对系统提供的mallocfree进行hook,在真正进行内存分配和释放之前,统计内存的引用计数,辅助我们排查内存泄漏等问题
  • 可以结合协程将socket相关API都转成异步,从而提升程序的整体吞吐量。并且hook之后的API与原始API相同,因此对于开发同学来说也不需要重新学习新的接口

github

https://github.com/huxiaohei/tiger.git

实现

tiger主要hook了系统底层socketsleep相关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

tigerhook是在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先关APIhook方法与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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

虎小黑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值