项目代码仓库
hook理论部分
简单来说,HOOK是对原API的同名封装在调用这个接口的时候首先执行的是我们封装好的同名API,其目的是比如在系统提供的malloc()和free()进行一些隐藏操作,在真正进行内存分配和释放之前,统计内存的引用计数,以排查内存泄漏的问题。
本项目使用hook有什么作用?
首先为什么说hook和io协程调度器密切相关,如果在未使用hook的情况下IO协程调度器的流程:
先执行协程1,这样因为sleep的阻塞调用会暂停2s,此时协程1被阻塞,等到sleep结束后协程1让出控制权,执行协程2,这里会因为send等不到要发送的写资源一样陷入阻塞,等到写资源来了,才会让出执行权给协程3,但是此时又阻塞在recv的读上,这些过程协程就变成了一个阻塞并且同步的框架,完全实现不了我们的非阻塞协程的切换。这是为什么?
总而言之,在IO协程调度中对相关的系统调用进行hook,可以让调度线程尽可能得把时间片花在有意义的操作上,而不是浪费在阻塞等待中。
补充注意:hook的重点是在替换API的底层实现的同时完全模拟器原本的行为,因为调用方是不知道hook的细节的,在调用被hook的API时,如果其行为与原本的行为不一致,就会给调用方造成困惑。比如,所有socket fd在进行IO调度时都会被设置成NOBLOCK模式,如果用户未显式地对fd设置NONBLOCK,那就要处理好fcntl,不要对用户暴露fd已经是NONBLOCK的事实,这点也说明了,除了IO相关的函数要进行hook外,对fcntl、setsockopt之类的功能函数也要进行hook,才能保证API的一致性。
HOOK的两种方式:
侵入式:
侵入式很简单,就是直接改造代码,将目标函数的入口点替换为自定义的代码,从而在函数执行之前或之后注入自定义的逻辑。
外挂式:
外挂式hook是优先加自定义动态库来实现对后加载动态库进行hook,比如我们自己实现了write函数:
#include <unistd.h>
#include <sys/syscall.h>
#include <string.h>
// 用户定义的这个函数名和标准库函数write()完全相同,从而覆盖了标准库的实现(函数劫持/函数拦截)。
ssize_t write(int fd, const void *buf, size_t count) {
syscall(SYS_write, STDOUT_FILENO, "12345\n", strlen("12345\n"));
}
将其编译成libhook.so动态库,通过设置LD_PRELOAD环境变量,将libhook.so设置成优先加载。
LD_PRELOAD="./libhook.so" ./a.out
LD_PRELOAD环境变量,它指明了在运行a.out之前,系统会优先把libhook.so加载到程序的进程空间,使得a.out运行之前,其全局符号表就已经有一个write符号,这样在后续加载libc共享库时,由于全局符号介入机制,libc中的write符号不会再被加入全局符号表,所以全局符号表中的write就变成了我们自己的实现。
为hook使用的补充类 Fd_manager类
定义了两个主要的类:FdCtx 和 FdManager,用于管理文件描述符(fd)的上下文和其相关的操作。
Fd_manager.h
Fdctx类: 主要用于管理与文件描述符相关的状态和操作。
FdCtx类在用户态记录了fd的读写超时和非阻塞信息,其中非阻塞包括用户显式设置的非阻塞和hook内部设置的非阻塞,区分这两种非阻塞可以有效应对用户对fd设置/获取NONBLOCK模式的情形。
// fd info
class FdCtx: public std::enable_shared_from_this<FdCtx> {
private:
//标记文件描述符是否已初始化。
bool m_isInit = false;
//标记文件描述符是否是一个套接字。
bool m_isSocket = false;
//标记文件描述符是否设置为系统非阻塞模式
bool m_sysNonblock = false;
//标记文件描述符是否设置为用户非阻塞模式
bool m_userNonblock = false;
//标记文件描述符是否已关闭。
bool m_isClosed = false;
//文件描述符的整数值
int m_fd;
// read event timeout
//读事件的超时时间,默认为 -1 表示没有超时限制。
uint64_t m_recvTimeout = (uint64_t)-1;
// write event timeout
//写事件的超时时间,默认为 -1 表示没有超时限制。
uint64_t m_sendTimeout = (uint64_t)-1;
public:
FdCtx(int fd);
~FdCtx();
//初始化 FdCtx 对象。
bool init();
bool isInit() const {
return m_isInit;
}
bool isSocket() const {
return m_isSocket;
}
bool isClosed() const {
return m_isClosed;
}
//设置和获取用户层面的非阻塞状态。
void setUserNonblock(bool v) {
m_userNonblock = v;
}
bool getUserNonblock() const {
return m_userNonblock;
}
//设置和获取系统层面的非阻塞状态。
void setSysNonblock(bool v) {
m_sysNonblock = v;
}
bool getSysNonblock() const {
return m_sysNonblock;
}
//设置和获取超时时间,type 用于区分读事件和写事件的超时设置,v表示时间毫秒。
void setTimeout(int type, uint64_t v);
uint64_t getTimeout(int type);
};
FdManager类: 用于管理 FdCtx 对象的集合。它提供了对文件描述符上下文的访问和管理功能。
class FdManager {
public:
//构造函数
//获取指定文件描述符的 FdCtx 对象。如果 auto_create 为 true,在不存在时自动创建新的 FdCtx 对象。
FdManager();
std::shared_ptr<FdCtx> get(int fd, bool auto_create = false);
//删除指定文件描述符的 FdCtx 对象
void del(int fd);
private:
//用于保护对 m_datas 的访问,支持共享读锁和独占写锁。
std::shared_mutex m_mutex;
//存储所有 FdCtx 对象的共享指针。
std::vector<std::shared_ptr<FdCtx>> m_datas;
};
Singleton: 实现了单例模式,确保一个类只有一个实例,并提供全局访问点。
单例模式的学习:深入浅出设计模式——创建型模式之单例模式 Singleton
【C++】C++ 单例模式总结(5种单例实现方法)
如下是懒汉模式+互斥锁维持线程安全:
template<typename T>
class Singleton {
private:
//对外提供的实例
static T* instance;
//互斥锁
static std::mutex mutex;
protected:
Singleton() {}
public:
// Delete copy constructor and assignment operation
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static T* GetInstance() {
if (instance == nullptr) {
std::lock_guard<std::mutex> lock(mutex); // 线程安全
if (instance == nullptr) {
instance = new T();
}
}
//提高对外的访问点,在系统生命周期中
//一般一个类只有一个全局实例。
return instance;
}
static void DestroyInstance() {
std::lock_guard<std::mutex> lock(mutex);
delete instance;
//防止野指针
instance = nullptr;
}
};
//重定义将Singleton<FdManager> 变成FdMgr的缩写。
typedef Singleton<FdManager> FdMgr;
}
Fd_manager.cpp
Singleton模板类:
// instantiate
//FdManager 类有一个全局唯一的单例实例。
// Static variables need to be defined outside the class
//这些行代码定义了 Singleton 类模板的静态成员变量 instance 和 mutex。静态成员变量需要在类外部定义和初始化。
// 编译器马上为Singleton<FdManager>生成代码(如构造函数、GetInstance()方法、静态成员定义等)。
// 后续其他文件再次使用该模板时,不再生成新代码,而直接引用已经生成好的版本。
// 编译链接过程更快速、更高效,二进制文件体积更小
// 这里的class关键词不是定义类,而是在模板实例化语法中固定的关键词,告诉编译器现在要实例化的是一个模板类。这样,程序在其他.cpp文件中再使用时,不会重新生成这些代码,而只引用已经生成的实例。
/*
template
告诉编译器:接下来是一个模板相关的定义或实例化。
class
这里的关键字class并非在定义新类,而是明确告诉编译器:“接下来实例化的是一个模板类”。
Singleton<FdManager>
要实例化的模板类名(模板类Singleton,实例化类型为FdManager)。
通俗地讲,这行代码的意思是:
编译器,请立即生成模板类Singleton以FdManager为模板参数的全部代码,不要等我用到时才生成。
默认情况下,模板类只有被用到时才生成具体代码。
当你显式写出这句,编译器就提前为你准备好了Singleton<FdManager>所有的成员函数、静态变量、构造函数、析构函数等具体代码。
*/
template class Singleton<FdManager>;
/*
不显式实例化:
多个.cpp文件中使用Singleton<FdManager>
↓↓
编译阶段各自生成Singleton<FdManager>代码
↓↓
链接时合并多个重复的Singleton<FdManager>代码
↓↓
程序体积增大,链接时间增长
显式实例化:
模板类定义Singleton<T>
↓↓(立即实例化)
template class Singleton<FdManager>;
↓↓
只生成一次Singleton<FdManager>代码
↓↓
后续使用直接引用此代码,无需再次生成
*/
// Static variables need to be defined outside the class
// 静态成员变量必须有一个单独的、类外的定义,否则链接时会报错。
template<typename T>
T* Singleton<T>::instance = nullptr;
template<typename T>
std::mutex Singleton<T>::mutex;
FdCtc类实现:
FdCtx::FdCtx(int fd):m_fd(fd) {
init();
}
FdCtx::~FdCtx() {
// Destructor implementation needed
}
bool FdCtx::init() {
// 如果已经初始化过了就直接返回 true
if(m_isInit) {
return true;
}
struct stat statbuf;
// fd is in valid
// fstat 函数用于获取与文件描述符 m_fd 关联的文件状态信息存放到 statbuf 中。如果 fstat() 返回 -1,表示文件描述符无效或出现错误。
/*
int fstat(int fd, struct stat *buf);
fd:你想检查的文件描述符。
buf:一个stat结构的指针,调用成功后fd的信息会填充进去。
成功时返回0;
出错时返回-1(说明fd无效、已关闭或者其它错误)。
S_ISSOCK()宏:
作用:判断给定的文件是否为套接字类型。
用法:S_ISSOCK(statbuf.st_mode)
返回值为非0值表示是socket类型,否则不是。
*/
if(-1 == fstat(m_fd, &statbuf)) {
m_isInit = false;
m_isSocket = false;
} else {
// S_ISSOCK(statbuf.st_mode) 用于判断文件类型是否为套接字
m_isInit = true;
m_isSocket = S_ISSOCK(statbuf.st_mode);
}
// if it is a socket -> set to nonblock
// 如果fd是socket,强制设置为非阻塞模式
// 通常网络应用程序(如高性能服务器)都使用非阻塞socket进行高效的IO处理,避免IO操作阻塞程序执行。
if(m_isSocket) {
// 表示 m_fd 关联的文件是一个套接字:
// fcntl_f() -> the original fcntl() -> get the socket info
// 获取文件描述符的状态
/*
fcntl()用于修改文件描述符(fd)的属性或获取属性。
此处使用两种cmd值:
F_GETFL:获取文件描述符的标志(当前状态)。
F_SETFL:设置文件描述符的新状态标志。
当cmd为F_GETFL或F_GETFD时,该参数可忽略(通常传0)。
O_NONBLOCK标志表示非阻塞模式。
*/
int flags = fcntl(m_fd, F_GETFL, 0);
// 检查当前标志中是否已经设置了非阻塞标志。如果没有设置:
if(!(flags & O_NONBLOCK)) {
// if not -> set to nonblock
// 如果没有非阻塞模式,则强制设置非阻塞模式
fcntl_f(m_fd, F_SETFL, flags | O_NONBLOCK);
}
// hook 非阻塞设置成功
m_sysNonblock = true;
} else {
// 如果不是一个 socket 那就没必要设置非阻塞了。
m_sysNonblock = false;
}
// 即初始化是否成功
return m_isInit;
}
问题:S_ISSOCK函数是做什么的?
定义:SISSOCK是一个宏,用于检查’st_mode’中的位,以确定文件是否是一个套接字(socket)。该宏定义在<sys/stat.h>头文件中。
FdCtx::setTimeout和getTimeout
设置和获取与文件描述符相关的超时时间
/*
接收超时 (SO_RCVTIMEO)
发送超时 (SO_SNDTIMEO)
*/
// setTimeout 函数用于设置套接字(socket)相关的超时时间(timeout),可设置两种超时类型
//type指定超时类型的标志。可能的值包括 SO_RCVTIMEO 和 SO_SNDTIMEO,分别用于接收超时和发送超时。v代表设置的超时时间,单位是毫秒或者其他。
void FdCtx::setTimeout(int type, uint64_t v) {
//如果type类型的读事件,则超时事件设置到recvtimeout上,否则就设置到sendtimeout上。
if(type == SO_RCVTIMEO) {
m_recvTimeout = v;
} else {
m_sendTimeout = v;
}
}
//同理根据type类型返回对应读或写的超时时间。
uint64_t FdCtx::getTimeout(int type) {
if(type == SO_RCVTIMEO) {
return m_recvTimeout;
} else {
return m_sendTimeout;
}
}
FdManager的构造函数:
作用是给std::vector<std::shared_ptr>m_datas;分配空间大小。
FdManager::FdManager() {
m_datas.resize(64);
}
FDManager::get
用于获取m_datas数组中FdCtx的对象,如果FdCtx对象不存在并且m_datas.size()<=fd的话会先扩展数组大小,然后通过Fd_Ctx的构造函数创建其对象存放到m_datas数组中。
std::shared_ptr<FdCtx> FdManager::get(int fd, bool auto_create) {
if(fd == -1) {
//文件描述符无效则直接返回。
return nullptr;
}
std::shared_lock<std::shared_mutex> read_lock(m_mutex);
//如果 fd 超出了 m_datas 的范围,并且 auto_create 为 false,则返回 nullptr,表示没有创建新对象的需求。
if(m_datas.size() <= fd) {
/*
bool auto_create:
是否在找不到对应的 FdCtx 对象时自动创建。
为 true 时找不到就创建。
为 false 时找不到就返回空指针。
*/
if(auto_create == false) {
return nullptr;
}
} else {
if(m_datas[fd] || !auto_create) {
return m_datas[fd];
}
}
//当fd的大小超出m_data.size的值也就是m_datas[fd]数组中没找到对应的fd并且auto_create为true时候会走到这里。
// 由于接下来要修改数据,因此必须释放读锁,避免死锁。
read_lock.unlock();
// 加写锁,修改数据:
std::unique_lock<std::shared_mutex> write_lock(m_mutex);
if(m_datas.size() <= fd) {
m_datas.resize(fd * 1.5);
}
m_datas[fd] = std::make_shared<FdCtx>(fd);
return m_datas[fd];
}
FdManager::del
删除指定文件描述的FdCtx对象。
reset简单补充:reset()用于释放std::shared_ptr所管理的对象,并将智能指针重新置为nullptr(即空指针)。调用reset()会使智能指针对象的引用计数减1;当引用计数减少到0时,所管理的对象会被自动销毁,释放其占用的资源。因此,reset()既可以主动释放对象,也可以通过减少引用计数触发对象的自动销毁机制。
void FdManager::del(int fd) {
std::unique_lock<std::shared_mutex> write_lock(m_mutex);
if(m_datas.size() <= fd) {
return;
}
m_datas[fd].reset();
}
hook.h
这个代码片段是一个用于网络钩子(hook)的头文件,旨在拦截并重定向某些系统调用,如与网络相关的socket、connect、read、write等函数。这种技术通常用于网络库、异步I/O操作或协程库中,以便对底层系统调用进行自定义处理,从而实现非阻塞I/O、超时控制或其他功能。
可能出现的疑问就是extern "C"的typedef extern。
// 保证在C++编译器编译时,这个代码块中的函数使用C语言风格的函数名修饰方式,便于动态链接与系统调用兼容。
extern "C" {
// track the original version
// 定义一个函数指针类型sleep_fun,这个类型的函数指针指向的是参数为一个unsigned int,并返回unsigned int类型的函数
//首先typedef 定义了一个返回值是unsigned int 类型参数是unsigned int seconds的函数指针
//这样下次直接使用sleep_fun代表的就是就是返回值是unsigned int类型 参数是(unsigned int seconds)的函数
typedef unsigned int (*sleep_fun)(unsigned int seconds);
// 加了extern说明这个变量是在其他源文件中定义的,此处仅声明
// 声明一个外部变量sleep_f,其类型为函数指针sleep_fun,用于指向原始的sleep函数的真实地址。
//声明了一个外部变量名sleep_f,类型是sleep_fun(简单理解为int p这样)。
extern sleep_fun sleep_f;
例子:
#include <unistd.h> // sleep 函数
sleep_fun sleep_f = sleep; // 将 sleep_f 指向 sleep 函数
//可以直接通过sleep_调用该函数
unsigned int result = sleep_f(5); // 等价于调用 sleep(5)
完整hook.h
#ifndef _HOOK_H_
#define _HOOK_H_
//这些头文件提供了与系统调用和文件描述符操作相关的基本函数和数据结构。
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <sys/ioctl.h>
#include <fcntl.h>
namespace sylar {
//用于判断钩子功能是否启用。
bool is_hook_enable();
//用于设置钩子功能的启用或禁用状态
void set_hook_enable(bool flag);
}
// 保证在C++编译器编译时,这个代码块中的函数使用C语言风格的函数名修饰方式,便于动态链接与系统调用兼容。
//确保正确调用c库中的系统调用,c++编译器不会对这些函数名进行修饰。
extern "C" {
//C函数原型声明与重定向
// track the original version
// 定义一个函数指针类型sleep_fun,这个类型的函数指针指向的是参数为一个unsigned int,并返回unsigned int类型的函数
//首先typedef 定义了一个返回值是unsigned int 类型参数是unsigned int seconds的函数指针
//这样下次直接使用sleep_fun代表的就是就是返回值是unsigned int类型 参数是(unsigned int seconds)的函数
typedef unsigned int (*sleep_fun)(unsigned int seconds);
// 加了extern说明这个变量是在其他源文件中定义的,此处仅声明
// 声明一个外部变量sleep_f,其类型为函数指针sleep_fun,用于指向原始的sleep函数的真实地址。
//声明了一个外部变量名sleep_f,类型是sleep_fun(简单理解为int p这样)。
extern sleep_fun sleep_f;
typedef int (*usleep_fun) (useconds_t usec);
extern usleep_fun usleep_f;
typedef int (*nanosleep_fun) (const struct timespec* req, struct timespec* rem);
extern nanosleep_fun nanosleep_f;
typedef int (*socket_fun) (int domain, int type, int protocol);
extern socket_fun socket_f;
typedef int (*connect_fun) (int sockfd, const struct sockaddr *addr, socklen_t addrlen);
extern connect_fun connect_f;
typedef int (*accept_fun) (int sockfd, struct sockaddr *addr, socklen_t *addrlen);
extern accept_fun accept_f;
typedef ssize_t (*read_fun) (int fd, void *buf, size_t count);
extern read_fun read_f;
typedef ssize_t (*readv_fun)(int fd, const struct iovec *iov, int iovcnt);
extern readv_fun readv_f;
typedef ssize_t (*recv_fun) (int sockfd, void *buf, size_t len, int flags);
extern recv_fun recv_f;
typedef ssize_t (*recvfrom_fun) (int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
extern recvfrom_fun recvfrom_f;
typedef ssize_t (*recvmsg_fun) (int sockfd, struct msghdr *msg, int flags);
extern recvmsg_fun recvmsg_f;
typedef ssize_t (*write_fun) (int fd, const void *buf, size_t count);
extern write_fun write_f;
typedef ssize_t (*writev_fun) (int fd, const struct iovec *iov, int iovcnt);
extern writev_fun writev_f;
typedef ssize_t (*send_fun) (int sockfd, const void *buf, size_t len, int flags);
extern send_fun send_f;
typedef ssize_t (*sendto_fun) (int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
extern sendto_fun sendto_f;
typedef ssize_t (*sendmsg_fun) (int sockfd, const struct msghdr *msg, int flags);
extern sendmsg_fun sendmsg_f;
typedef int (*close_fun) (int fd);
extern close_fun close_f;
typedef int (*fcntl_fun) (int fd, int cmd, ... /* arg */ );
extern fcntl_fun fcntl_f;
typedef int (*ioctl_fun) (int fd, unsigned long request, ...);
extern ioctl_fun ioctl_f;
typedef int (*getsockopt_fun) (int sockfd, int level, int optname, void *optval, socklen_t *optlen);
extern getsockopt_fun getsockopt_f;
typedef int (*setsockopt_fun) (int sockfd, int level, int optname, const void *optval, socklen_t optlen);
extern setsockopt_fun setsockopt_f;
// function prototype -> 对应.h中已经存在 可以省略
// sleep function
//函数重定义
//可以通过判断是否启用了钩子来决定是调用原始的系统函数,还是执行自定义的逻辑。
unsigned int sleep(unsigned int seconds);
int usleep(useconds_t usce);
int nanosleep(const struct timespec* req, struct timespec* rem);
// socket funciton
int socket(int domain, int type, int protocol);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// read
ssize_t read(int fd, void *buf, size_t count);
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
// write
ssize_t write(int fd, const void *buf, size_t count);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
// fd
int close(int fd);
// socket control
int fcntl(int fd, int cmd, ... /* arg */ );
int ioctl(int fd, unsigned long request, ...);
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
}
#endif
hook.cpp
宏定义的使用 这个宏HOOK_FUN(XX)是一个宏展开机制,通过将XX依次应用于宏定义中的每一个函数名称来生成一系列代码。这种方式可以有效减少重复代码,提高代码的可读性和维护性。
宏定义一个函数列表
// apply XX to all functions
#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)
#define XX(name) name ## _f = (name ## _fun)dlsym(RTLD_NEXT, #name);
HOOK_FUN(XX)
#undef XX //提前取消了xx的作用域
具体的宏展开
1、首先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)
2、XX(name)的宏定义将进一步的展开为:
sleep_f=(sleep_fun)dlsym(RTLD_NEXT,"sleep");
usleep_f=(usleep_fun)dlsym(RTLD_NEXT,"usleep");
...
补充:
dlsym()使用的补充:
动态库加载函数dlsym 在C/C++编程中的使用
总结一下:因为我们使用了hook外挂式改变了本应该加载的libc共享库,全局符号表中以及有我们的sleep了,此时如果想在代码中继续获取原始的系统调用就需要借助dlsym(从动态库中获取符号地址函数)和RTLD_NEXT,二者结合的就说RTLD_NEXT告诉dlsym查找原始符号从当前库或者程序之后继续搜索,也就是搜索没被加载进来的libc共享库的sleep原始系统调用。 这里补充宏定义简单的学习:
详解宏定义(#define)
详细解释#define XX(name) name##_f = (name##_fun)dlsym(RTLD_NEXT, #name);
#define XX(name) name##_f = (name##_fun)dlsym(RTLD_NEXT, #name);
HOOK_FUN(XX)
#undef XX
关键函数:
namespace sylar: 首先这里现在匿名空间sylar中实现了钩子函数的初始化,并提供了一种方式来控制钩子函数是否启用。
namespace sylar {
// if this thread is using hooked function
//使用线程局部变量,每个线程都会判断一下是否启用了钩子
//表示当前线程是否启用了钩子功能。初始值为 false,即钩子功能默认关闭。
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机制,主要用来获取系统原始函数的地址并保存到对应的函数指针中。
void hook_init() {
//通过一个静态变量来确保 hook_init() 只初始化一次,防止重复初始化。
static bool is_inited = false;
// 防止重复初始化
if(is_inited) {
return;
}
// test
is_inited = true;
// assignment -> sleep_f = (sleep_fun)dlsym(RTLD_NEXT, "sleep"); -> dlsym -> fetch the original symbols/function
// 批量获取原始函数地址
// dlsym是Linux动态链接库的函数,作用是根据函数名字符串查找动态库中原始函数的地址。
// RTLD_NEXT的含义是查找当前链接库后面的(即系统库或下一个库)原始函数地址。
// 例如:sleep_f = (sleep_fun)dlsym(RTLD_NEXT, "sleep");
// 获取系统原始的sleep函数的真实地址,并赋值给函数指针sleep_f。
// 使用宏定义 HOOK_FUN(XX),批量完成上述任务,而无需逐个手写。
#define XX(name) name##_f = (name##_fun)dlsym(RTLD_NEXT, #name);
HOOK_FUN(XX)
#undef XX
}
// static variable initialisation will run before the main function
// 这个机制可以确保:
// 在程序正式执行主函数之前,自动完成Hook系统的初始化。
struct HookIniter {
//钩子函数
HookIniter() {
//初始化hook,让原始调用绑定到宏展开的函数指针中
hook_init();
}
};
//定义了一个静态的 HookIniter 实例。由于静态变量的初始化发生在 main() 函数之前,所以 hook_init() 会在程序开始时被调用,从而初始化钩子函数。
static HookIniter s_hook_initer;
} // end namespace sylar
timer_info结构体:
用于跟踪定时器的状态。具体来说,它有一个cancelled成员变量,通常用于表示定时器是否已经被取消。
struct timer_info {
int cancelled = 0;
};
do_io的通用模板:
可以发现项目代码中的:自定义的系统调用最后都将其参数放入do_io模板来做一个统一的规范化
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);
}
补充:std::forward完美转发:C++ 新特性 | C++ 11 | std::forward、万能引用与完美转发
// universal template for read and write function
// OriginFun:原始系统调用函数指针类型(如read_fun, write_fun)。
// Args&&... args:变参模板参数,原样转发给系统调用
/*
fd: 文件描述符
fun: 原始系统调用函数
hook_fun_name: 用于调试输出的函数名,便于定位当前Hook的具体函数(如"read", "write")
event: IO事件类型(如读事件或写事件)一般定义在IO管理器(如sylar::IOManager::READ, sylar::IOManager::WRITE)
timeout_so: 超时的类型(发送或接收超时)一般指定为SO_RCVTIMEO(接收超时)或SO_SNDTIMEO(发送超时)。
args: 其他所有的参数,转发给fun函数
提供了一个通用的Hook机制,封装所有可能会阻塞的IO调用,将其转换为非阻塞协程版调用,并处理事件循环、超时和重试逻辑。
Args&&... args 表示函数可以接受任意个数的参数,并且能以完美转发(perfect forwarding) 的方式传递给其他函数,既保持参数的原始类型与引用特性(左值/右值不变)。
*/
template<typename OriginFun, typename... Args>
// 表示函数为内部链接,仅在定义它的编译单元(cpp文件)中有效。
static ssize_t do_io(int fd, OriginFun fun, const char* hook_fun_name, uint32_t event, int timeout_so, Args&&... args) {
if(!sylar::t_hook_enable) {
// 如果没有开启hook,则直接调用原始函数,结束。
// 这里所有参数的类型、引用性质、左值右值,都原封不动地传递给系统调用函数
return fun(fd, std::forward<Args>(args)...);
}
// 获取文件描述符上下文 (FdCtx)
// typedef Singleton<FdManager> FdMgr;
std::shared_ptr<sylar::FdCtx> ctx = sylar::FdMgr::GetInstance()->get(fd);
if(!ctx) {
// 如果找不到ctx,则不执行hook,直接调用原始函数
return fun(fd, std::forward<Args>(args)...);
}
// 如果fd已经被关闭
if(ctx->isClosed()) {
// 设置错误码为EBADF(坏的文件描述符)
errno = EBADF;
return -1;
}
// 如果当前fd不是socket类型,或用户自己将fd设置成了非阻塞模式,就不会进行hook处理,直接调用原始的系统调用返回。
// 目的是只对socket类型的阻塞调用进行封装,避免干扰其他IO操作
if(!ctx->isSocket() || ctx->getUserNonblock()) {
return fun(fd, std::forward<Args>(args)...);
}
// get the timeout
// 根据当前fd的上下文(FdCtx)获取超时时间(发送或接收超时)。
uint64_t timeout = ctx->getTimeout(timeout_so);
// timer condition
// timer_info结构用于后续记录超时事件状态(是否超时)。
std::shared_ptr<timer_info> tinfo(new timer_info);
/*
调用原始IO函数
│
├─成功 → 返回
└─失败(EAGAIN)→ 注册事件 & 定时器,挂起协程
│
├─IO事件发生 → 唤醒 → 重新尝试
└─超时事件发生 → 唤醒 → 返回超时错误
*/
// 标签 retry 和 IO 调用逻辑
retry:
// run the function
// 实际调用系统的IO函数。
ssize_t n = fun(fd, std::forward<Args>(args)...);
// EINTR ->Operation interrupted by system ->retry
// 如果调用过程中因为信号中断(EINTR)而失败,则自动重试,确保调用得到明确结果(成功或其他错误)。
while(n == -1 && errno == EINTR) {
n = fun(fd, std::forward<Args>(args)...);
}
// 0 resource was temporarily unavailable -> retry until ready
// 非阻塞IO重试处理逻辑(EAGAIN)
// 在非阻塞模式下,资源暂时不可用时,IO调用返回EAGAIN。
// 这里处理这种情况,通过异步事件机制等待资源可用
if(n == -1 && errno == EAGAIN) {
sylar::IOManager* iom = sylar::IOManager::GetThis();
// timer
std::shared_ptr<sylar::Timer> timer;
std::weak_ptr<timer_info> winfo(tinfo);
// 1 timeout has been set -> add a conditional timer for canceling this operation
// 如果设置了超时时间,注册一个定时器
if(timeout != (uint64_t)-1) {
/*
参数1 (timeout):定时器的超时时间(毫秒)。
参数2(lambda):定时器触发后执行的回调函数。
参数3 (winfo):条件对象的弱引用(weak_ptr),用于在定时器执行时判断目标对象是否有效,避免对象已经销毁后访问。
*/
timer = iom->addConditionTimer(timeout, [winfo, fd, iom, event]() {
// 将弱引用(weak_ptr)提升为强引用(shared_ptr)
auto t = winfo.lock();
// 若提升失败(!t),说明原对象(timer_info)已被销毁,直接返回,不执行后续逻辑。
if(!t || t->cancelled) {
return;
}
// 置位标志cancelled,表明此次操作已超时。
// 这里使用标准错误码ETIMEDOUT(连接或操作超时)
t->cancelled = ETIMEDOUT;
// cancel this event and trigger once to return to this fiber
// 调用IOManager的cancelEvent方法,取消之前注册的fd上对应事件。
iom->cancelEvent(fd, (sylar::IOManager::Event)(event));
}, winfo);
}
// 2 add event -> callback is this fiber
// 注册当前协程到IOManager,表示当前协程需要等待
int rt = iom->addEvent(fd, (sylar::IOManager::Event)(event));
if(rt) {
// 注册事件失败的处理
std::cout << hook_fun_name << " addEvent("<< fd << ", " << event << ")";
// 若定时器已注册,则主动调用timer->cancel()取消定时器,避免不必要的超时回调。
if(timer) {
timer->cancel();
}
return -1;
} else {
// 协程挂起等待事件发生或超时
sylar::Fiber::GetThis()->yield();
// 3 resume either by addEvent or cancelEvent
// 协程恢复后,无论是否超时,都应主动取消定时器。
// 防止定时器未触发,但后续已经无需超时控制的情况,避免无效定时器回调
// 假设你在煮饭,设置了个闹钟(定时器)提醒你30分钟后必须关火(超时)。
// 如果饭熟了(IO事件发生),你主动关了火。
// 此时闹钟已经没用了,必须要手动关掉闹钟,不然过了一会闹钟响了,又提醒你去关火一次,是不是很奇怪?
// 同理,协程恢复后也要“关掉闹钟”,防止后续无用的提醒。
if(timer) {
timer->cancel();
}
// by cancelEvent
// 判断是否超时
// 若为ETIMEDOUT,代表协程被超时机制唤醒。
// 设置系统错误码errno为ETIMEDOUT。
// 返回-1表明操作超时。
if(tinfo->cancelled == ETIMEDOUT) {
errno = tinfo->cancelled;
return -1;
}
// 重新尝试IO操作 (goto retry)
goto retry;
}
}
return n;
}
下面是具体函数调用do_io通用模板或内部修改
Sleep函数:
实现了一个协程版本的sleep,通过钩子机制拦截sleep的调用,并将其改为使用协程来实现非阻塞的休眠。
补充:需要注意的是sleep*1000是转换微秒。
// only use at task fiber
unsigned int sleep(unsigned int seconds) {
// 如果全局hook功能未开启,则调用原生系统函数sleep_f(阻塞式)。
// 此时表现与原生sleep一致。
if(!sylar::t_hook_enable) {
return sleep_f(seconds);
}
// 获取当前执行的协程对象
std::shared_ptr<sylar::Fiber> fiber = sylar::Fiber::GetThis();
// 获取当前协程调度器(IOManager)
sylar::IOManager* iom = sylar::IOManager::GetThis();
// add a timer to reschedule this fiber
// seconds * 1000:睡眠时间,单位是毫秒
// lambda的作用是唤醒协程:
// scheduleLock用于把之前挂起(睡眠)的协程重新放入执行队列中,准备恢复执行。
iom->addTimer(seconds * 1000, [fiber, iom]() {
iom->scheduleLock(fiber, -1);
});
// wait for the next resume
// 协程主动挂起,进入等待状态。
// 此时线程不会阻塞,线程可以继续执行其他任务或协程。
// 协程直到定时器到期,才会恢复执行
fiber->yield();
return 0;
}
Usleep函数:
系统调用usleep的封装,利用协程和定时器来实现延迟时操作。
补充:关于useconds_t的类型: useconds_t一个无符号整数类型,通常用于表示微秒数。在这个函数中,usec表示延时的微秒数,将其转换为毫秒数(usec/1000)后用于定时器。
int usleep(useconds_t usec) {
if(!sylar::t_hook_enable) {
return usleep_f(usec);
}
//这里的步骤和sleep函数类似。
std::shared_ptr<sylar::Fiber> fiber = sylar::Fiber::GetThis();
sylar::IOManager* iom = sylar::IOManager::GetThis();
// add a timer to reschedule this fiber
iom->addTimer(usec / 1000, [fiber, iom]() {
iom->scheduleLock(fiber);
});
// wait for the next resume
fiber->yield();
return 0;
}
nanosleep
处理协程中的休眠操作,表示的是纳秒级别的睡眠。
// req: 请求的睡眠时长 (timespec结构包含秒tv_sec和纳秒tv_nsec)。
// rem: 当sleep被信号中断时,剩余的未睡眠时长会被写入rem(本实现简化,未考虑中断情况)。
int nanosleep(const struct timespec* req, struct timespec* rem) {
if(!sylar::t_hook_enable) {
return nanosleep_f(req, rem);
}
//timeout_ms 将 tv_sec 转换为毫秒,并将 tv_nsec 转换为毫秒,然后两者相加得到总的超时毫秒数。所以从这里看出实现的也是一个毫秒级的操作。
// 将用户输入的睡眠时长转换为毫秒(ms):
// 秒(tv_sec)转为毫秒: tv_sec * 1000
// 纳秒(tv_nsec)转为毫秒: tv_nsec / 1000000
// req = { tv_sec = 1, tv_nsec = 500000000 }
// 计算结果 timeout_ms = 1000 + 500 = 1500ms
int timeout_ms = req->tv_sec * 1000 + req->tv_nsec / 1000 / 1000;
// 获取当前协程和调度器
std::shared_ptr<sylar::Fiber> fiber = sylar::Fiber::GetThis();
sylar::IOManager* iom = sylar::IOManager::GetThis();
// add a timer to reschedule this fiber
iom->addTimer(timeout_ms, [fiber, iom](){iom->scheduleLock(fiber, -1);});
// wait for the next resume
fiber->yield();
return 0;
}
总的来说:具体的三个sleep的函数实现的过程都是类似,实现的目标都是为了将时间转换成毫秒,添加到超时时间堆中。然后,暂停协程,方便其他任务执行。
socket钩子函数
对系统调用socket的封装,同时添加了一些额外的逻辑,用于处理自定义的钩子和文件描述符管理。
/*
这完全遵循了原生socket函数的定义和接口参数:
domain:套接字所属的协议族(如AF_INET表示IPv4)。
type:套接字类型(如SOCK_STREAM表示TCP,SOCK_DGRAM表示UDP)。
protocol:指定具体协议(通常填0自动推断)。
*/
int socket(int domain, int type, int protocol) {
if(!sylar::t_hook_enable) {
return socket_f(domain, type, protocol);
}
//如果钩子启用了,则通过调用原始的 socket 函数创建套接字,并将返回的文件描述符存储在 fd 变量中。
int fd = socket_f(domain, type, protocol);
//fd是无效的情况
if(fd == -1) {
std::cerr << "socket() failed:" << strerror(errno) << std::endl;
return fd;
}
//如果socket创建成功会利用Fdmanager的文件描述符管理类来进行管理,判断是否在其管理的文件描述符中,如果不在扩展存储文件描述数组大小,并且利用FDctx进行初始化判断是是不是套接字,是不是系统非阻塞模式。
sylar::FdMgr::GetInstance()->get(fd, true);
return fd;
}
connect_with_timeout函数
用于在连接超时情况下处理非阻塞套接字连接的实现。 它首先尝试使用钩子功能来捕获并管理连接请求的行为,然后使用IOManager和TImer来管理超时机制,可以发现具体的逻辑实现上和do_io类似。
注意:如果没有启用hook或者不是一个套接字、用户启用了非阻塞。都去调用connect系统调用,因为connect_with_timeout本身就在connect系统调用基础上调用的。
/*
这个函数的主要功能是支持超时控制的非阻塞connect,通过协程机制实现:
原生connect系统调用默认阻塞线程,无法指定超时时间。
通过hook后的connect_with_timeout函数,允许设定自定义超时时间,并通过协程机制挂起协程,而不阻塞线程。
fd: 套接字文件描述符。
addr: 指向要连接的目标地址结构。
addrlen: 地址结构的长度。
timeout_ms: 超时时间(单位为毫秒,ms)。
*/
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 的上下文信息 FdCtx
// 通过FdMgr单例获取对应fd的上下文信息(FdCtx)
std::shared_ptr<sylar::FdCtx> ctx = sylar::FdMgr::GetInstance()->get(fd);
//检查文件描述符上下文是否存在或是否已关闭。
// 若fd未注册或已关闭,则返回错误,设置errno为EBADF(坏的文件描述符)
if(!ctx || ctx->isClosed()) {
//EBAD表示一个无效的文件描述符
errno = EBADF;
return -1;
}
//如果不是一个套接字调用原始的
if(!ctx->isSocket()) {
return connect_f(fd, addr, addrlen);
}
//检查用户是否设置了非阻塞模式。如果是非阻塞模式,
if(ctx->getUserNonblock()) {
return connect_f(fd, addr, addrlen);
}
// attempt to connect
//尝试进行 connect 操作,返回值存储在 n 中。
int n = connect_f(fd, addr, addrlen);
// 若connect直接返回成功,直接返回0,表示连接成功。
if(n == 0) {
return 0;
} else if(n != -1 || errno != EINPROGRESS) {
//说明连接请求未处于等待状态,直接返回结果。
return n;
}
// 若返回-1,且errno==EINPROGRESS,表示连接正在进行中(非阻塞模式特有返回值),需等待socket可写事件。
// wait for write event is ready -> connect succeeds
//获取当前线程的 IOManager 实例。
sylar::IOManager* iom = sylar::IOManager::GetThis();
//声明一个定时器对象。
std::shared_ptr<sylar::Timer> timer;
//创建追踪定时器是否取消的对象
std::shared_ptr<timer_info> tinfo(new timer_info);
//判断追踪定时器对象是否存在
std::weak_ptr<timer_info> winfo(tinfo);
//检查是否设置了超时时间。如果 timeout_ms 不等于 -1,则创建一个定时器
if(timeout_ms != (uint64_t)-1) {
//添加一个定时器,当超时时间到达时,取消事件监听并设置 cancelled 状态。
timer = iom->addConditionTimer(timeout_ms, [winfo, fd, iom]() {
auto t = winfo.lock();
//判断追踪定时器对象是否存在或者追踪定时器的成员变量是否大于0.大于0就意味着取消了
if(!t || t->cancelled) {
return;
}
//如果超时了但时间仍然未处理
// 定时器回调触发 → 设置cancelled=ETIMEDOUT
t->cancelled = ETIMEDOUT;
//将指定的fd的事件触发将事件处理。
// 取消监听事件并强制触发事件
// 我们在等待fd变为可写(连接成功时fd自动变可写)
iom->cancelEvent(fd, sylar::IOManager::WRITE);
}, winfo);
}
//为文件描述符 fd 添加一个写事件监听器。这样的目的是为了上面的回调函数处理指定文件描述符
// 协程想要等待连接完成,所以必须监听fd变为可写状态
// 这一步是开启事件监听,告诉IOManager:
// “我要等待这个fd的WRITE事件(连接成功时fd变可写),事件发生时请唤醒我。”
int rt = iom->addEvent(fd, sylar::IOManager::WRITE);
//返回0表示注册监听成功
if(rt == 0) {
// 如果addEvent注册成功(返回0),协程立即调用yield()主动挂起:
// 协程进入休眠状态,线程释放出来去执行其他协程。
// 此时协程不会占用CPU和线程,达到高效并发的目的。
sylar::Fiber::GetThis()->yield();
// resume either by addEvent or cancelEvent
//如果有定时器,取消定时器。
if(timer) {
timer->cancel();
}
//发生超时错误或者用户取消
if(tinfo->cancelled) {
//赋值给errno通过其查看具体错误原因。
errno = tinfo->cancelled;
return -1;
}
} else {
// 若最初的addEvent本身失败(返回非零),事件监听根本未注册成功
//添加事件失败
if(timer) {
timer->cancel();
}
std::cerr << "connect addEvent(" << fd << ", WRITE) error";
}
// check out if the connection socket established
// 检查连接套接字是否真正建立成功(真正确认连接结果)
int error = 0;
// 明确告诉编译器这是socket长度参数
socklen_t len = sizeof(int);
//通过getsocketopt检查套接字实际错误状态
//来判断是否成功或失败。
// 为了明确知道连接是否真的成功,必须调用getsockopt来查询真实连接状态
/*
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
sockfd:套接字文件描述符。
level:表示你要获取的选项所在的层次。常用值:SOL_SOCKET:表示套接字层次的通用选项。
IPPROTO_TCP:表示TCP层次的特定选项(如TCP_KEEPALIVE)
optname:具体的选项名称,常见的SOL_SOCKET选项包括:
SO_ERROR:获取并清除套接字上最近一次的错误状态。
SO_REUSEADDR:地址复用选项。
这里使用SO_ERROR表示查询连接的实际错误状态。
optval:指针,用于保存查询到的错误码(连接状态)。
optlen:传入时表示optval缓冲区大小,传出时保存实际返回的数据长度。
返回值为0:函数调用本身成功(查询到了连接状态)。
返回值为-1:函数调用失败(例如套接字无效),此时应立即返回错误
*/
if(-1 == getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &len)) {
return -1;
}
//如果没有错误,返回 0 表示连接成功。
if(!error) {
return 0;
} else {
//如果有错误,设置 errno 并返回错误。
errno = error;
return -1;
}
}
connect函数:
connect_with_timeout函数实际上是在原始connect系统调用基础上,增加了超时控制的逻辑。
在超时时间为-1时,表示不启用超时功能,也就是不会调用addconditiontimer函数放入到超时时间堆中,等待超时唤醒tickle触发IOManager::idle函数中epoll,而是就只是监听这个事件,这个事件没到就一直阻塞直到成功或失败。
// 网络库或框架中:对connect函数做统一的封装,以便管理连接超时时间
//s_connect_timeout 是一个 static 变量,表示默认的连接超时时间,类型为 uint64_t,可以存储 64 位无符号整数。
//-1 通常用于表示一个无效或未设置的值。由于它是无符号整数,-1 实际上会被解释为 UINT64_MAX,表示没有超时限制
// uint64_t为64位无符号整数类型,通常用于表示毫秒或微秒级的超时时间。
// 初始赋值为-1,由于s_connect_timeout是无符号类型,因此实际存储的值为UINT64_MAX,通常代表“不设超时”或“无限超时”。
static uint64_t s_connect_timeout = -1;
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen) {
//调用hook启用后的connect_with_timeout函数
return connect_with_timeout(sockfd, addr, addrlen, s_connect_timeout);
}
accept
用于处理套接字接收连接的操作,同时支持超时连接控制,和recv等函数一样使用了do_io的模板,实现了非阻塞accpet的操作,并且如果成功接收了一个新的连接,则将新的文件描述符fd添加到文件描述符管理器(FdManager)中进行跟踪管理。
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen) {
int fd = do_io(sockfd, accept_f, "accept", sylar::IOManager::READ, SO_RCVTIMEO, addr, addrlen);
if(fd >= 0) {
//添加到文件描述符管理器FdManager中
sylar::FdMgr::GetInstance()->get(fd, true);
}
return fd;
}
close函数:
很简单就说将所有文件描述符的事件处理了调用IOManager的callAll函数将fd上的读写事件全部处理,最后从FdManger文件描述符管理中移除该fd。
int close(int fd) {
if(!sylar::t_hook_enable) {
return close_f(fd);
}
// 获取文件描述符上下文(FdCtx)
std::shared_ptr<sylar::FdCtx> ctx = sylar::FdMgr::GetInstance()->get(fd);
// 检查并取消所有事件
if(ctx) {
auto iom = sylar::IOManager::GetThis();
if(iom) {
// 通过IOManager取消所有与该文件描述符相关的异步事件。
iom->cancelAll(fd);
}
// del fdctx
// 删除文件描述符上下文(FdCtx)
sylar::FdMgr::GetInstance()->del(fd);
}
//处理完后调用原始系统调用
return close_f(fd);
}
ioctl函数:
实际处理了文件描述符(fd)上的ioctl系统调用,并在特定条件下对FIONBIO(用于设置非阻塞模式)进行了特殊处理。
int ioctl(int fd, unsigned long request, ...) {
//va持有处理可变参数的状态信息
// 定义一个变量va用于保存可变参数的处理状态
va_list va;
//给va初始化让它指向可变参数的第一个参数位置。
// 初始化va变量,指向可变参数列表中的第一个参数。
// 第二个参数request为最后一个已知参数
va_start(va, request);
//将va的指向参数的以void*类型取出存放到arg中
// 从参数列表中取出一个参数,存储在arg中。
// 这里假设传入的第一个可变参数是一个指针类型(一般 ioctl 接口都是如此)
void* arg = va_arg(va, void*);
//用于结束对 va_list 变量的操作。清理va占用的资源
// 表示可变参数处理结束,释放资源,避免资源泄漏
va_end(va);
//用于设置非阻塞模式的命令
// 检测当前的请求(request)是否为FIONBIO。
// FIONBIO是标准命令,用于在套接字或文件描述符上设置或清除非阻塞模式
if(FIONBIO == request) {
//当前 ioctl 调用是为了设置或清除非阻塞模式。
// 先将arg(指针)转换为整型指针 (int*),然后取出值*(int*)arg。
// 再用双重逻辑非!!操作符,将其转换为明确的布尔值:
// 若原值为0,则user_nonblock为false(阻塞模式)。
// 若原值非0,则user_nonblock为true(非阻塞模式)
bool user_nonblock = !!*(int*)arg;
// 通过sylar::FdMgr(文件描述符管理器)获取对应于文件描述符fd的上下文对象(FdCtx)
std::shared_ptr<sylar::FdCtx> ctx = sylar::FdMgr::GetInstance()->get(fd);
//检查获取的上下文对象是否有效(即 ctx 是否为空)。如果上下文对象无效、文件描述符已关闭或不是一个套接字,则直接调用原始的 ioctl 函数,返回处理结果。
if(!ctx || ctx->isClosed() || !ctx->isSocket()) {
return ioctl_f(fd, request, arg);
}
//如果上下文对象有效,调用其 setUserNonblock 方法,将非阻塞模式设置为 user_nonblock 指定的值。这将更新文件描述符的非阻塞状态。
// 更新上下文中的非阻塞模式状态
ctx->setUserNonblock(user_nonblock);
}
return ioctl_f(fd, request, arg);
}
va_list 处理可变参数的用法
va_list处理可变参数的用法:VA_LIST可变参数列表的使用方法与原理
!!的作用
补充:!!是为了保证明确的将其转为成一个bool类型,比如一开始结果是true,经过一次!转换成了false,然后!再一次转换成了true;
!! 是 C 和 C++ 中的一种惯用表达式,用于将任意类型的值转换成明确的布尔值(true 或 false)。
fnctl函数:
fcntl是一个用于操作文件描述符的系统调用,可以执行多种操作,比如设置文件描述符状态,锁定文件等。 这个封装的fcntl函数对某些操作进行自定义处理,比如处理非阻塞模式表示,同时保留了对原始fcntl的调用。
补充:对于ifdef和endif的补充,其目的是为了代码在不同系统环境下更具灵活性,例如,F_SETPIPE_SZ是一个可能并非在所有系统中都可以用的宏,特别是不同操作系统或内核版本可能支持不同的控制命令,所有使用宏先预处理如果没有定义就不会进入到ifdef和endif内了
int fcntl(int fd, int cmd, ...) {
// to access a list of mutable parameters
va_list va;
// 初始化参数列表,让va指向第一个可变参数
//使其指向第一个可变参数(在 cmd 之后的参数)
va_start(va, cmd);
switch(cmd) {
// 设置文件状态标志
//用于设置文件描述符的状态标志(例如,设置非阻塞模式)
case F_SETFL:
{
// Access the next int argument
// 从参数列表取出一个整型参数arg,即文件状态标志
int arg = va_arg(va, int);
va_end(va);
// 使用FdMgr获取对应fd的上下文对象FdCtx
std::shared_ptr<sylar::FdCtx> ctx = sylar::FdMgr::GetInstance()->get(fd);
//如果ctx无效,或者文件描述符关闭不是一个套接字就调用原始调用
if(!ctx || ctx->isClosed() || !ctx->isSocket()) {
return fcntl_f(fd, cmd, arg);
}
// 用户是否设定了非阻塞
// arg & O_NONBLOCK用于提取用户是否设置了非阻塞模式:
// 若设置非阻塞,则存储true;
// 若未设置,则存储false。
ctx->setUserNonblock(arg & O_NONBLOCK);
// 最后是否阻塞根据系统设置决定
if(ctx->getSysNonblock()) {
arg |= O_NONBLOCK;
} else {
// 实际设置为阻塞
arg &= ~O_NONBLOCK;
}
return fcntl_f(fd, cmd, arg);
}
break;
// 获取文件状态标志
case F_GETFL:
{
va_end(va);
//调用原始的 fcntl 函数获取文件描述符的当前状态标志。
int arg = fcntl_f(fd, cmd);
std::shared_ptr<sylar::FdCtx> ctx = sylar::FdMgr::GetInstance()->get(fd);
//如果上下文无效、文件描述符已关闭或不是套接字,则直接返回状态标志
if(!ctx || ctx->isClosed() || !ctx->isSocket()) {
return arg;
}
// 这里是呈现给用户 显示的为用户设定的值
// 但是底层还是根据系统设置决定的
if(ctx->getUserNonblock()) {
return arg | O_NONBLOCK;
} else {
return arg & ~O_NONBLOCK;
}
}
break;
// 这些命令(F_DUPFD、F_DUPFD_CLOEXEC、F_SETFD 等)都会共享执行同一段代码
case F_DUPFD:
case F_DUPFD_CLOEXEC:
case F_SETFD:
case F_SETOWN:
case F_SETSIG:
case F_SETLEASE:
case F_NOTIFY:
// F_SETPIPE_SZ 用于设置管道(pipe)的缓冲区大小。
// 此命令是Linux内核特定扩展,不同的平台可能不支持,所以用条件编译#ifdef进行判断。
#ifdef F_SETPIPE_SZ
case F_SETPIPE_SZ:
#endif
{
//从va获取标志位
int arg = va_arg(va, int);
//清理va
va_end(va);
//调用原始调用
return fcntl_f(fd, cmd, arg);
}
break;
// 不需要额外参数的命令处理
// 获取文件描述符的标志(如close-on-exec)
case F_GETFD:
// 获取当前文件描述符关联的进程或进程组ID
case F_GETOWN:
case F_GETSIG:
case F_GETLEASE:
#ifdef F_GETPIPE_SZ
case F_GETPIPE_SZ:
#endif
{
va_end(va);
return fcntl_f(fd, cmd);
}
break;
//设置文件锁,如果不能立即获得锁,则返回失败。
// 非阻塞地尝试给文件加锁,若文件已被锁定则立即返回错误
// 文件锁允许进程独占或共享访问文件特定区域,防止多个进程同时读写文件导致数据混乱。
case F_SETLK:
//设置文件锁,且如果不能立即获得锁,则阻塞等待。
// 阻塞地尝试给文件加锁,若文件已被锁定则等待直到锁可用
case F_SETLKW:
// 获取当前文件的锁状态,返回锁的信息,但不实际锁定文件
//获取文件锁的状态。如果文件描述符 fd 关联的文件已经被锁定,那么该命令会填充 flock 结构体,指示锁的状态。
case F_GETLK:
{
//从可变参数列表中获取 struct flock* 类型的指针,这个指针指向一个 flock 结构体,包含锁定操作相关的信息(如锁的类型、偏移量、锁的长度等)
struct flock* arg = va_arg(va, struct flock*);
va_end(va);
return fcntl_f(fd, cmd, arg);
}
break;
/*
struct f_owner_exlock {
int type; // 指定所有者类型
// F_OWNER_TID (线程)
// F_OWNER_PID (进程)
// F_OWNER_PGRP (进程组)
pid_t pid; // 对应的进程ID或线程ID
};
*/
// 一般用于异步 I/O 或信号驱动 I/O 中指定谁会接收特定的 I/O 信号或通知
//获取文件描述符 fd 所属的所有者信息。这通常用于与信号处理相关的操作,尤其是在异步 I/O 操作中。
case F_GETOWN_EX:
//设置文件描述符 fd 的所有者信息
case F_SETOWN_EX:
{
//从可变参数中提取相应类型的结构体指针
struct f_owner_exlock* arg = va_arg(va, struct f_owner_exlock*);
va_end(va);
return fcntl_f(fd, cmd, arg);
}
break;
default:
va_end(va);
return fcntl_f(fd, cmd);
}
}
fnctl的学习:
【Linux C | 文件I/O】fcntl函数详解 | 设置描述符非阻塞、文件(记录)锁
getsockopt函数:
一个用于获取套接字选项值的函数。它允许你检查指定套接字的某些选项的当前设置。
int getsockopt(int sockfd, int level, int optname, void* optval, socklen_t* optlen) {
return getsockopt_f(sockfd, level, optname, optval, optlen);
}
setsockopt函数:
用于设置套接字的选项。它允许你对套接字的行为进行配置,如设置超时时间、缓冲区大小、地址重用等。
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);
}
//如果 level 是 SOL_SOCKET 且 optname 是 SO_RCVTIMEO(接收超时)或 SO_SNDTIMEO(发送超时),代码会获取与该文件描述符关联的 FdCtx 上下文对象:
// 判断是否为套接字级别(SOL_SOCKET)的超时选项
if(level == SOL_SOCKET) {
if(optname == SO_RCVTIMEO || optname == SO_SNDTIMEO) {
// 获取文件描述符的上下文对象(FdCtx)
std::shared_ptr<sylar::FdCtx> ctx = sylar::FdMgr::GetInstance()->get(sockfd);
//那么代码会读取传入的 timeval 结构体,将其转化为毫秒数,并调用 ctx->setTimeout 方法,记录超时设置:
// 如果上下文对象有效,则记录超时时间到上下文
/*
struct timeval {
time_t tv_sec; // 秒数
suseconds_t tv_usec; // 微秒数
};
*/
if(ctx) {
const timeval* v = (const timeval*)optval;
// 毫秒数 = 秒数 * 1000 + 微秒数 / 1000;
ctx->setTimeout(optname, v->tv_sec * 1000 + v->tv_usec / 1000);
}
}
}
//无论是否执行了超时处理,最后都会调用原始的 setsockopt_f 函数来设置实际的套接字选项。
return setsockopt_f(sockfd, level, optname, optval, optlen);
}
之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!