redis之IO多路复用
2023-03-25 15:34:50
本文总阅读量

问题

  1. 为什么需要IO多路复用?

前置知识

select

过程

  1. 设置需要监控的文件描述符
    • 这里使用fd_set结构体,使用一堆的宏来定义的,最终的结果为:
      1
      2
      3
      typedef struct{
      long int fds_bits[32];
      } fd_set;
      sizeof (long int)为4or8,如果为4的话,每一个数能表示32位,32*32=1024,所以很多地方写的1024就是这么来的。 初始化监控的文件描述符就是将对应位置的位改为1,linux提供了很多的api来修改fd_set的值。
  2. 调用select函数
    • 函数声明为:
      1
      2
      3
      4
      5
      6
      7
      int select(
      int maxfd, // 一般为最大的fd+1
      fd_set *readset, // 监听read系统调用的fd
      fd_set *writeset, // 监听write系统调用的fd
      fd_set *exceptset,
      const struct timecval *timeout // 超时时间,0不等待,-1一直等待,其他值
      );
      以readset为例,关于这里的监听read系统调用的fd的意思,我的理解是:以服务端为例:
      • 对于socket()返回的用于监听的fd,他需要监听是否有连接的请求,也是read系统调用。
      • 当建立完连接后(这里又创建了一个新的socket用于数据传输)需要等待客户端发来的信息,在客户端还没有写数据前这个新的socket会一直阻塞住,为了不阻塞,我们需要一直监听read系统调用的fd是否就绪。
  3. select函数调用完成后,对应位为1的fd表示已经准备好,因此在循环调用select时,需要每一次初始化好需要监控的fd。

代码 《TCP/IP网络编程》 P203

  1. 28行:最一开始select中监听的fd只有这个serv_socket
  2. 58行:假设一共有5和客户端与该server连接,那么这里就会执行5次,最终监听的fd就有五个,一个是用于监听客户端连接的fd,另外五个是监听客户端什么时候写数据,在服务端就是read系统调用
  3. 49行:在调用select,内部判断fd是否就绪时,需要从用户态切换到内核态:
    • 将fd_set拷贝到内核空间
    • 内核遍历fd_set查看哪些fd已经准备就绪,如果已经就绪,在该位置就会打上一个标记(设为1),遍历完后返回已经就绪的个数,用户态发现准备就绪的fd的个数大于1,就从头遍历一遍找到就绪的fd,执行相应的操作(在这里就是read系统调用)。有一种情况就是在内核态中遍历一遍后发现没有一个fd是准备就绪的,那么这里又有两种策略:
      • 一直循环检查,会一直占用CPU
      • 遍历一遍之后将该进程阻塞住,当客户端给服务端发送数据时,数据会通过网络传输到达网卡,网卡会通过DMA的方式将这个数据写到指定的内存当中,写入完成后会通过中断信号告诉CPU有了新的数据包到达,CPU收到中断信号后会进行响应中断,并调用中断的处理程序进行处理,根据这个数据包的ip和端口号找到这个socket,将数据写到这个socket的buffer中,再检查这个socket的等待队列当中是否有进程进行阻塞等待,如果有的话就唤醒该进程,接下来就是和之前一样的操作了。

poll

1
2
3
4
5
6
7
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};

poll相比于select有了两个优化:

  1. 不需要每次重置需要监听的fd,因为用了新数据结构,fd包含了需要监听的fd
  2. 将fds拷贝到内核态时,底层使用的是链表,所以没有了1024的限制

问题

  1. 每次还是需要用户态到内核态的切换,并将所有的fd拷贝到内核态中
  2. 并不知道哪些fd是准备就绪的,所以需要遍历所有的fd

epoll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int epoll_create(int size);  // 返回值为创建的epoll的文件描述符

typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;

struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};

int epoll_ctl( // 0成功,1失败
int epfd, // epoll_create的返回值
int op, // 操作类型:1新增,2删除,3更新
int fd, // 本次操作的fd
struct epoll_event *event // 需要监听的事件
);


int epoll_wait( // -1出错,0超时
int epfd, // epoll_create的返回值
struct epoll_event *events, // 用于回传就绪的事件
int maxevents,
int timeout // 等待IO的超时时间,-1阻塞,0非阻塞
);

ref

  1. https://blog.csdn.net/Zorro721/article/details/107565000
  2. https://www.bilibili.com/video/BV1r54y1f7bU/
  3. https://man7.org/