版本比较

标识

  • 该行被添加。
  • 该行被删除。
  • 格式已经改变。

hook系统底层和socket相关的API,socket io相关的API,以及sleep系列的API。hook的开启控制是线程粒度的。可以自由选择。通过hook模块,可以使一些不具异步功能的API,展现出异步的性能。如(mysql)

hook实际就是把系统提供的api再进行一层封装,以便于在执行真正的系统调用之前进行一些操作。hook的目的是把socket io相关的api都转成异步,以便于提高性能。hook和io调度是密切相关的,如果不使用IO协程调度器,那hook没有任何意义。

考虑IOManager要调度以下协程:协程1:sleep(2) 睡眠两秒后返回协程2:在scoket fd1上send 100k数据协程3:在socket fd2上recv直到数据接收成功在未hook的情况下,IOManager要调度上面的协程,流程是下面这样的:
    1. 调度协程1,协程阻塞在sleep上,等2秒后返回,这两秒内调度器是被协程1占用的,其他协程无法被调度。
    2. 调度协徎2,协程阻塞send 100k数据上,这个操作一般问题不大,因为send数据无论如何都要占用时间,但如果fd迟迟不可写,那send会阻塞直到套接字可写,同样,在阻塞期间,调度器也无法调度其他协程。
    3. 调度协程3,协程阻塞在recv上,这个操作要直到recv超时或是有数据时才返回,期间调度器也无法调度其他协程

上面的调度流程最终总结起来就是,协程只能按顺序调度,一旦有一个协程阻塞住了,那整个调度器也就阻塞住了,其他的协程都无法执行。像这种一条路走到黑的方式其实并不是完全不可避免,以sleep为例,调度器完全可以在检测到协程sleep后,将协程yield以让出执行权,同时设置一个定时器,2秒后再将协程重新resume,这样,调度器就可以在这2秒期间调度其他的任务,同时还可以顺利的实现sleep 2秒后再执行的效果。send/recv与此类似,在完全实现hook后,IOManager的执行流程将变成下面的方式:
    1. 调度协程1,检测到协程sleep,那么先添加一个2秒的定时器,回调函数是在调度器上继续调度本协程,接着协程yield,等定时器超时。
    2. 因为上一步协程1已经yield了,所以协徎2并不需要等2秒后才可以执行,而是立刻可以执行。同样,调度器检测到协程send,由于不知道fd是不是马上可写,所以先在IOManager上给fd注册一个写事件,回调函数是让当前协程resume并执行实际的send操作,然后当前协程yield,等可写事件发生。
    3. 上一步协徎2也yield了,可以马上调度协程3。协程3与协程2类似,也是给fd注册一个读事件,回调函数是让当前协程resume并继续recv,然后本协程yield,等事件发生。
    4. 等2秒超时后,执行定时器回调函数,将协程1 resume以便继续执行。
    5. 等协程2的fd可写,一旦可写,调用写事件回调函数将协程2 resume以便继续执行send。
    6. 等协程3的fd可读,一旦可读,调用回调函数将协程3 resume以便继续执行recv。

上面的4、5、6步都是异常的,系统并不会阻塞,IOManager仍然可以调度其他的任务,只在相关的事件发生后,再继续执行对应的任务即可。并且,由于hook的函数对调用方是不可知的,调用方也不需要知道hook的细节,所以对调用方也很方便,只需要以同步的方式编写代码,实现的效果却是异常执行的,效率很高。hook系统调用就是让一条线程能够尽可能地减少阻塞时间,以尽可能多地运行协程hook的重点是在替换api的底层实现的同时完全模拟其原本的行为,因为调用方是不知道hook的细节的,在调用被hook的api时,如果其行为与原本的行为不一致,就会给调用方造成困惑。比如,所有的socket fd在进行io调度时都会被设置成NONBLOCK模式,如果用户未显式地对fd设置NONBLOCK,那就要处理好fcntl,不要对用户暴露fd已经是NONBLOCK的事实,这点也说明,除了io相关的函数要进行hook外,对fcntl, setsockopt之类的功能函数也要进行hook,才能保证api的一致性。sylar对以下函数进行了hook,并且只对socket fd进行了hook,如果操作的不是socket fd,那会直接调用系统原本的api,而不是hook之后的api:sleepusleepnanosleepsocketconnectacceptreadreadvrecvrecvfromrecvmsgwritewritevsendsendtosendmsgclosefcntlioctlgetsockoptsetsockopt动态链接的hook:外挂式hook,也称为非侵入式hook,通过优先加载动态库来实现对后加载的动态库进行hookPRE_LOAD=“a.so” ./mainreadelf -d a.out侵入式hook,需要改造代码内核式hook,插入一个内核模块,在模块中对系统调用进行hook常用命令,readelf, nm, ldd静态链接的hook:在编译链接阶段就得完成,运行时没法通过替换库的方式做到链接阶段存在符号冲突问题,同名的强符号不能共存用-Wl,--whold-archive方式加载全部的符号,实现hook,参考libgoman 8 ld.so 查找LD_PRELOAD/lib/x86_64-linux-gnu/libc.so.6 64位C运行库,软链接,指向真正的C运行库,也就是同目录下的libc-2.27.so/lib/i386-linux-gnu/libc.so.6 32位C运行库/usr/lib/x86_64-linux-gnu/libc.so 链接脚本,指向64位C运行库libc.so.5也存在过,但它不是指glibc,而是指Linux libc,这个版本的C运行库相当于glibc的一个试图替代者,但现在不用了,参考man libchttps://stackoverflow.com/questions/426230/what-is-the-ld-preload-tricklibc的链接顺序确实是在所有用户指定的链接库后面,也就是说用户可以先链自己的库,然后hook掉libc里的接口。

关于宏的骚操作
#define HOOK_FUN(XX)
    XX(sleep) \
    XX(usleep) \

#define XX(name) 
    HOOK_FUN(XX)
#undef XX

#define XX(name)
    HOOK_FUN(XX)
#undef XX

hook的这种行为和在子类中重载父类方法有些类似,如果子类重载父类方法时,通常会先调用父类的同名方法执行父类的操作,再实现自己的操作,比如:

class Child: public Base {
public:
    void print(){
        Base::print(); //调用父类的同名方法,完成基类该有的操作
        print("This is Child\n");
    }
}