深刻理解redis高性能之IO多路复用
目录
1.文件描述符(File Descriptor)简称(fd):
1.1文件描述符的作用
2.*select()函数、poll()函数、epoll()函数*
2.1 select函数:
2.2 poll函数
2.3 epoll函数
1.文件描述符(File Descriptor)简称(fd):
文件描述符是操作系统为每个打开的文件/网络连接分配的一个整数编号,来告诉系统这是一个什么操作。
*你可以把它理解成一个身份证号*
每个打开的文件、socket连接、管道都有一个唯一的编号。
常见的文件描述符
1.1文件描述符的作用
1.文件操作:
int fd = open("file.txt", O_RDWR);
write(fd, "hello", 5);
close(fd);
这里 fd 就是 file.txt 的文件描述符,你可以通过 fd 告诉操作系统哪个文件做什么操作,这里就是告诉操作系统去写一个hello字符串。
2.网络编程:
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
int client_fd = accept(server_fd, ...);
server_fd 和 client_fd 也是文件描述符!网络 socket 连接和文件一样,都是 FD 资源。
总结:这个文件操作符号就是一个标识,告诉操作系统的一个标识,操作系统只能识别0,1,2,3。这些数字,我们总不能写一段话告诉操作系统你给我这么做吧,不现实。
下面就是IO多路复用的核心
2.*select()函数、poll()函数、epoll()函数*
知识:select、poll 和 epoll 都是 I/O 多路复用(I/O Multiplexing)技术,主要用于管理多个文件描述符(如网络连接、文件、标准输入输出等),避免阻塞式 I/O 导致的性能瓶颈。它们的主要作用是在多个 I/O 事件(如可读、可写、异常)中进行监听,并在有事件发生时通知程序处理。
2.1 select函数:
int select (int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, conststruct timeval *timeout);
2.1.1 功能:
1.select这个函数负责监听多个文件描述符,看哪些文件发生了(可读、可写、或者异常)
2.select使用了固定大小的fd_set数组(一般最大支持1024个文件描述符)。
3.调用select时候,需要遍历所有的文件描述符,检查哪个就绪(每次都遍历一遍数组效率低)。
2.1.2 工作流程:
假设你的应用是一个 Web 服务器,用户请求一个数据查询接口,整个过程如下:
1. 用户发送请求,Web 服务器通过一个 socket 接收到该请求,这时会有一个对应的文件描述符。
2. 服务器把文件描述符添加到 select() 的监视集合(fd_set数组中)中。
3. select() 开始阻塞,等待某个文件描述符的事件发生。
4. 一旦服务器查询到数据库并准备好返回数据,文件描述符变成可读状态。
5. select() 检测到该文件描述符变为可读,返回并通知应用程序。
6. 应用程序从该文件描述符读取数据,并将查询结果返回给用户。
总结:
结合功能和例子可以发现select的缺点是我们在每次发生事件调用select的时候,都需要整体遍历一遍数组,这个效率低,而且数组的长度最大是1024.这是缺点。
2.2 poll函数
poll函数实现了动态数组,解决了数组长度的限制,但是还是没有解决每次遍历的问题。
pollfd 结构体:
struct pollfd {
int fd; // 需要监听的文件描述符
short events; // 监听的事件(如可读、可写等)
short revents; // 返回的事件(当某个事件发生时,表示该文件描述符上的事件)
};
动态数组中存放的是pollfd类型的结构体,每个结构体里有对应的文件描述符。
动态数组扩容:
我想大家都想知道动态数组那么数组的扩容怎么办,这个扩容是我们要自己写的扩容方式。
使用系统提供的realloc函数
假如超过初始数组大小:
if (nfds >= max_clients) {
max_clients *= 2; // 扩展数组的大小
fds = realloc(fds, max_clients * sizeof(struct pollfd));
}
总结:他只是解决了数组大小还是没有解决遍历的问题。
2.3 epoll函数
这个函数就是集合了上面两个的缺点,然后出现的它,一般我们在redis中默认都是使用的它。
首先epoll的结构和前面的那两个不一样。
工作流程:
1.创建epoll实例
int epfd = epoll_create(1); // 创建 epoll 实例
2.使用epoll_ctl注册文件描述符
struct epoll_event event;
event.events = EPOLLIN; // 监听可读事件
event.data.fd = socket_fd; // 添加文件描述符 socket_fd
epoll_ctl(epfd, EPOLL_CTL_ADD, socket_fd, &event); // 将文件描述符注册到 epoll 实例中
3.使用 epoll_wait 来获取发生事件的文件描述符:
struct epoll_event events[10]; // 用来存储就绪事件的数组
int num_events = epoll_wait(epfd, events, 10, -1); // 阻塞直到事件发生
for (int i = 0; i < num_events; i++) {
if (events[i].events & EPOLLIN) {
// 处理可读事件
}
}
总结:记住这几个名词 ***epoll_ctl(添加/删除/修改/fd) , epoll_wait(和select函数类似,用来返回已经发生事件的数据),epoll(这个实例内部会维护一个事件表)***
下面是他的结构信息:
结构:
1.事件表:记录epoll监听的所有的fd。
2.红黑树:事件表的内部数据结构,用来存储所有注册的fd,高效的增,删,改
3.就绪队列:记录已经发生的事件fd,epoll_wait() 直接返回。
示例分析:
假设你的应用是一个 Web 服务器,用户请求一个数据查询接口,整个过程如下:
1. 用户发送请求,Web 服务器通过一个 socket 接收到该请求,这时会有一个对应的文件描述符(fd)。
2. 服务器创建了一个epoll实例内部维护了一个事件表,它的数据结构是红黑树。
3. 使用epoll_ctl向这个树添加fd。
4.这个内部的事件表等待某个fd的事件发生(这里是查询事件)。
5. 一旦服务器查询到数据库并准备好返回数据,fd变成可读状态。
6.当fd变为可读的同时,把这个fd添加到就绪队列中。
5. epoll_wait遍历就绪队列,返回并通知应用程序。
6. 应用程序从该fd读取数据,并将查询结果返回给用户。
总结:
1.红黑树存储所有的fd是因为有高效的增,删,改。(解决了存储长度问题)
2.Linux 内核使用 I/O 事件驱动机制(Interrupt + 回调机制),当 fd 发生可读/可写事件时,内核会自动把它放到就绪队列里,不需要遍历所有 fd。(解决了遍历问题)
3.就绪队列存储的只有已经发生的事件。