hook介绍与实现
hook模块用于100%模拟原本与网络io相关的系统调用函数,在原本的系统调用函数上面进行封装。其目的是在原本的系统调用函数之上做一层封装,在使用的时候,用户会觉得跟原本的函数的行为是一模一样的,但是实际上函数内部会为了适配协程做了一些处理,只是用户不知道。
本模块的hook采用动态链接实现。hook的实现方法就是通过动态库的全局符号,用自定义的接口来替换原本函数签名相同的系统调用函数。因为系统调用接口基本上是由C标准函数库libc提供的。所以本模块要做的事就是用自定义的动态库来覆盖掉libc中的同名函数。
hook实现的方法有两种,一种是侵入式hook,另一种是非侵入式hook
侵入式hook:
当使用gcc命令编译好程序之后,可以使用ldd命令查看可执行程序的依赖的共享库,比如
# ldd a.out
linux-vdso.so.1 (0x00007ffc96519000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fda40a61000)
/lib64/ld-linux-x86-64.so.2 (0x00007fda40c62000)
可以看到其依赖的libc共享库
下面在不重新编译代码的情况下,用自定义的动态库来替换掉可执行程序a.out中的原本的系统调用实现,新建hook.c:
#include <unistd.h>
#include <sys/syscall.h>
#include <string.h>
ssize_t write(int fd, const void *buf, size_t count) {
syscall(SYS_write, STDOUT_FILENO, "12345\n", strlen("12345\n"));
}
这里实现了一个write函数,这个函数的签名和libc提供的write函数完全一样,函数内容是用syscall的方式直接调用编号为SYS_write的系统调用,实现的效果也是往标准输出写内容,只不过这里我们将输出内容替换成了其他值。将hook.c编译成动态库:
gcc -fPIC -shared hook.c -o libhook.so
通过设置LD_PRELOAD环境变量,将libhoook.so设置成优先加载,从而覆盖掉libc中的write函数。
以上并没有重新编译可指定程序,但是write函数已经替换成自己的实现。
非侵入式hook:
这种hook实现方式是需要改造代码或者重新编译一次来指定使用的动态库。直接在程序内部写相同函数签名的代码,通过编译参数来将自定义的动态库放在libc之前进行链接。
那么现在出现了一个问题,如何找到已经被用户自定义函数覆盖掉的系统调用函数呢?linux有一个dslym系统调用函数:
#include <dlfcn.h>
void *dlsym(void *handle, const char *symbol);
dslym系统调用函数的功能是根据动态链接库操作句柄和符号,返回符号对应的地址。第一个参数传入打开动态链接库后返回的指针,第二个参数传入要求获取的函数对应的地址(全局变量也行)。在使用的时候,第一个参数直接传入RTLD_NEXT即可,它的意思是第一次出现这个函数名的函数指针。这样子就能找到原来的函数地址。
HOOK模块介绍:
本模块实现的hook功能以线程为单位,可以自由的设置当前的线程是否使用hook,默认调度器所在的线程会开启,其他线程不会开启。
extern "C":指定编译器将代码段中的代码以C语言的形式进行编译。
本模块实现hook可大致分为三类:①socket IO系列的接口②sleep延时系列接口③closs/fcntl等接口
一、socket IO系列的接口
这部分的接口因为行为比较同意,于是决定有c++中的可变形参模板来实现。这部分的接口包括read,readv,recv,recvfrom,recvmsg,write,writev,send,sendto,sendmsg。系列函数介绍如下:
read
:
- 函数原型:
ssize_t read(int fd, void *buf, size_t count);
- 说明:read 用于从文件描述符
fd
读取数据,并将数据存储到buf
中,最多读取count
个字节。- 作用:主要用于文件 I/O,通常用于读取文件内容。
- 区别:是阻塞的,会一直等待数据可用,然后读取指定数量的字节。
readv
:
- 函数原型:
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
- 说明:readv 允许同时从多个缓冲区读取数据,使用
iovec
结构数组描述不同缓冲区的位置和长度。- 作用:减少数据拷贝的需求,提高性能,特别适用于从多个缓冲区组合成单个连续块的情况。
- 区别:用于文件 I/O,是阻塞的。
recv
:
- 函数原型:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- 说明:recv 用于从套接字
sockfd
接收数据,将数据存储到buf
中,最多接收len
个字节。- 作用:主要用于网络通信,如 TCP 或 UDP 套接字,用于接收数据。
- 区别:是阻塞的,一直等待数据可用,然后读取指定数量的字节。
recvfrom
:
- 函数原型:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
- 说明:recvfrom 用于从套接字
sockfd
接收数据,并获取数据的来源地址信息,地址信息存储在src_addr
中。- 作用:主要用于 UDP 套接字,因为 UDP 是无连接的,每个数据包可能来自不同的地址。
- 区别:用于网络通信,获取数据的来源地址信息。
recvmsg
:
- 函数原型:
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
- 说明:recvmsg 用于从套接字
sockfd
接收数据,同时提供更多的信息,如辅助数据和控制信息,存储在msg
结构中。- 作用:适用于更复杂的通信需求,如 Unix 域套接字,提供更多的控制和信息。
- 区别:提供更多的控制和信息。
write
:
- 函数原型:
ssize_t write(int fd, const void *buf, size_t count);
- 说明:write 用于将数据从
buf
写入文件描述符fd
,最多写入count
个字节。- 作用:主要用于文件 I/O,通常用于写入文件内容。
- 区别:是阻塞的,会一直等待数据写入成功。
writev
:
- 函数原型:
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
- 说明:writev 允许同时将数据从多个缓冲区写入文件描述符,使用
iovec
结构数组描述不同缓冲区的位置和长度。- 作用:减少数据拷贝的需求,提高性能,特别适用于向文件描述符写入数据。
send
:
- 函数原型:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- 说明:send 用于将数据从
buf
发送到套接字sockfd
。- 作用:主要用于网络通信,如 TCP 或 UDP 套接字,用于发送数据。
- 区别:是阻塞的,一直等待数据发送成功。
sendto
:
- 函数原型:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
- 说明:sendto 用于将数据从
buf
发送到套接字sockfd
,同时指定目标地址信息,地址信息存储在dest_addr
中。- 作用:主要用于 UDP 套接字,用于向特定目标地址发送数据。
- 区别:用于网络通信,可以指定目标地址。
sendmsg
:
- 函数原型:
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
- 说明:sendmsg 用于将数据从
msg
结构中发送到套接字sockfd
,同时提供更多的信息,如辅助数据和控制信息。- 作用:适用于更复杂的通信需求,提供更多的控制和信息。
- 区别:提供更多的控制和信息。
具体代码实现:
Args&&... args
是 C++11 中的模板参数包(Template Parameter Pack)语法,通常用于函数模板,以支持可变数量的参数。这种语法允许你在函数模板中接受不定数量的参数,并在函数体中以更灵活的方式处理它们。具体来说,
Args&&... args
通常用于模板函数参数列表中,其中:
Args
是模板参数包的占位符,它表示任意类型的参数。&&
表示右值引用,它可以绑定到右值(临时对象)。...
表示参数包扩展,允许接受任意数量的参数。
转发参数包:
在C++11标准下,我们可以组合使用可变参数模板与forwad机制来编写函数,实现将其实参不变地传递给其他的函数。
std::forward<Args>(args)...
std::forward
是一个标准库函数模板,用于执行完美转发。<Args>
是模板参数包,表示参数的类型。(args)
是函数参数包,表示接受的参数。...
表示参数包展开,允许传递任意数量的参数。完美转发的主要目标是避免在传递参数时失去参数的左值或右值特性,以确保正确的参数传递。在函数模板中,你可能会遇到类似以下的情况:
template<typename OriginFun, typename... Args>
/**
* @brief 统一实现socket IO系列函数
* @param[in] fd socket句柄
* @param[in] fun 系统调用函数的函数指针
* @param[in] hook_fun_name hook的函数名字
* @param[in] event IO事件(读事件还是写事件)
* @param[in] timeout_so 设置套接字接受或发送的超时时间
* @param[in] args 原本系统调用除fd以外的其他参数
*/
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)...);
}
//若无法获取到文件句柄类,那么也是直接使用原本的系统调用函数
sylar::FdCtx::ptr ctx = sylar::FdMgr::GetInstance()->get(fd);
if(!ctx) {
return fun(fd, std::forward<Args>(args)...);
}
//fd都被关闭了,没法这个fd发送或接受数据了,直接返回-1并且设置错误代码
if(ctx->isClose()) {
errno = EBADF;
return -1;
}
//如果不是socket连接,或者是用户主动设置的非阻塞,也是直接使用原本的系统调用函数,这是因为能保证一次性的全部写入或读出,就算没读写完也不管了
if(!ctx->isSocket() || ctx->getUserNonblock()) {
return fun(fd, std::forward<Args>(args)...);
}
//获取超时时间
uint64_t to = ctx->getTimeout(timeout_so);
std::shared_ptr<timer_info> tinfo(new timer_info);
retry:
//执行socket IO系列的函数
ssize_t n = fun(fd, std::forward<Args>(args)...);
//若IO操作被信号中断的话,那么就重复执行IO函数
while(n == -1 && errno == EINTR) {
n = fun(fd, std::forward<Args>(args)...);
}
//若没有数据可读写,但是又没有读写到EOF的话,就执行下面这条分支
if(n == -1 && errno == EAGAIN) {
sylar::IOManager* iom = sylar::IOManager::GetThis();
sylar::Timer::ptr timer;
std::weak_ptr<timer_info> winfo(tinfo);
//如果设置了超时时间,就添加条件定时器,只在超时时间后执行一次,在定时时间到后触发并删除事件,并且设置t->cancelled超时标志
if(to != (uint64_t)-1) {
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);
}
// 添加事件到epoll中,并且让出执行权,epoll继续监听这个fd,并且让线程再去任务队列里选出协程任务并执行
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 {
sylar::Fiber::GetThis()->yield();
// 等待超时执行定时器的回调函数或者epoll_wait返回表示套接字可读写。
// 如果超时,且winfo条件有效,那么就通winfo设置超时标志并触发读写事件,协程从yield返回,返回之后通过超时标志设置errno并返回-1
// 如果epoll_wait返回,那么就取消定时器,并返回成功。取消定时器的时候会执行一次定时器回调。此时connect_with_timeout协程已经被执行完了,所以条件变量被释放,不会再设置标志。
if(timer) {
timer->cancel();
}
//将cancelled设置为错误代码
if(tinfo->cancelled) {
errno = tinfo->cancelled;
return -1;
}
goto retry;
}
}
return n;
}
ssize_t read(int fd, void *buf, size_t count) {
return do_io(fd, read_f, "read", sylar::IOManager::READ, SO_RCVTIMEO, buf, count);
}
ssize_t readv(int fd, const struct iovec *iov, int iovcnt) {
return do_io(fd, readv_f, "readv", sylar::IOManager::READ, SO_RCVTIMEO, iov, iovcnt);
}
ssize_t recv(int sockfd, void *buf, size_t len, int flags) {
return do_io(sockfd, recv_f, "recv", sylar::IOManager::READ, SO_RCVTIMEO, buf, len, flags);
}
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen) {
return do_io(sockfd, recvfrom_f, "recvfrom", sylar::IOManager::READ, SO_RCVTIMEO, buf, len, flags, src_addr, addrlen);
}
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags) {
return do_io(sockfd, recvmsg_f, "recvmsg", sylar::IOManager::READ, SO_RCVTIMEO, msg, flags);
}
ssize_t write(int fd, const void *buf, size_t count) {
return do_io(fd, write_f, "write", sylar::IOManager::WRITE, SO_SNDTIMEO, buf, count);
}
ssize_t writev(int fd, const struct iovec *iov, int iovcnt) {
return do_io(fd, writev_f, "writev", sylar::IOManager::WRITE, SO_SNDTIMEO, iov, iovcnt);
}
ssize_t send(int s, const void *msg, size_t len, int flags) {
return do_io(s, send_f, "send", sylar::IOManager::WRITE, SO_SNDTIMEO, msg, len, flags);
}
ssize_t sendto(int s, const void *msg, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) {
return do_io(s, sendto_f, "sendto", sylar::IOManager::WRITE, SO_SNDTIMEO, msg, len, flags, to, tolen);
}
ssize_t sendmsg(int s, const struct msghdr *msg, int flags) {
return do_io(s, sendmsg_f, "sendmsg", sylar::IOManager::WRITE, SO_SNDTIMEO, msg, flags);
}
二、sleep系列延时函数
这部分函数包括sleep,usleep,nanosleep,通过定时器来实现。这三种定时函数的区别是以秒为单位,以微秒为单位,以纳秒为单位。由于epoll_wait的限制,导致最低只能支持到毫秒,其实这三种函数的精度是一样的,以usleep为例子:
int usleep(useconds_t usec) {
// 若本线程未使用hook,那么就直接使用原本的系统调用函数即可
if(!sylar::t_hook_enable) {
return usleep_f(usec);
}
sylar::Fiber::ptr fiber = sylar::Fiber::GetThis();
sylar::IOManager* iom = sylar::IOManager::GetThis();
//添加一个定时器,usec / 1000毫秒后再执行,usec / 1000毫秒后向调度器的任务队列添加调度任务。
iom->addTimer(usec / 1000, std::bind((void(sylar::Scheduler::*)
(sylar::Fiber::ptr, int thread))&sylar::IOManager::schedule
,iom, fiber, -1));
sylar::Fiber::GetThis()->yield();
return 0;
}
三:与网络IO相关的其他函数
// 与原本系统调用函数唯一的区别在于还创建了文件句柄类
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;
}
//这个服务器框架中,还可以自定义connect的超时时间
int connect_with_timeout(int fd, const struct sockaddr* addr, socklen_t addrlen, uint64_t timeout_ms) {
// 若本线程未使用hook,那么就直接使用原本的系统调用函数即可
if(!sylar::t_hook_enable) {
return connect_f(fd, addr, addrlen);
}
// 若fd被关闭,则设置error并返回
sylar::FdCtx::ptr ctx = sylar::FdMgr::GetInstance()->get(fd);
if(!ctx || ctx->isClose()) {
errno = EBADF;
return -1;
}
// 若fd不是socket或用户自定义了非阻塞,则用原本的api
if(!ctx->isSocket()) {
return connect_f(fd, addr, addrlen);
}
if(ctx->getUserNonblock()) {
return connect_f(fd, addr, addrlen);
}
// 若成功则返回0,若发生了错误就返回错误代码
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);
//如果connect设置了超时时间,就添加条件定时器,只在超时时间后执行一次,在定时时间到后触发并删除事件,并且设置t->cancelled超时标志
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);
}
// 添加事件到epoll中,并且让出执行权,epoll继续监听这个fd,并且让线程再去任务队列里选出协程任务并执行
int rt = iom->addEvent(fd, sylar::IOManager::WRITE);
if(rt == 0) {
sylar::Fiber::GetThis()->yield();
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;
}
}
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
return connect_with_timeout(sockfd, addr, addrlen, sylar::s_connect_timeout);
}
// 与原本系统调用函数唯一的区别在于还创建了文件句柄类
int accept(int s, struct sockaddr *addr, socklen_t *addrlen) {
int fd = do_io(s, accept_f, "accept", sylar::IOManager::READ, SO_RCVTIMEO, addr, addrlen);
if(fd >= 0) {
sylar::FdMgr::GetInstance()->get(fd, true);
}
return fd;
}
//close函数还额外封装了取消并触发一次epoll监听的全部事件,以及删除文件句柄上下文类
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);
}