Redis源码解析:IO多路复用,select poll epoll有哪些区别?

Redis源码解析:IO多路复用,select poll epoll有哪些区别?_第1张图片

基本编程模型

listenSocket = socket(); //调用socket系统调用创建一个主动套接字
bind(listenSocket);  //绑定地址和端口
listen(listenSocket); //将默认的主动套接字转换为服务器使用的被动套接字,也就是监听套接字
while (1) { //循环监听是否有客户端连接请求到来
   connSocket = accept(listenSocket); //接受客户端连接
   recv(connsocket); //从客户端读取数据,只能同时处理一个客户端
   send(connsocket); //给客户端返回数据,只能同时处理一个客户端
}

每次只能处理一个客户端连接,我们可以利用多线程来提高服务端的处理能力


listenSocket = socket(); //调用socket系统调用创建一个主动套接字
bind(listenSocket);  //绑定地址和端口
listen(listenSocket); //将默认的主动套接字转换为服务器使用的被动套接字,即监听套接字
while (1) { //循环监听是否有客户端连接到来
   connSocket = accept(listenSocket); //接受客户端连接,返回已连接套接字
   pthread_create(processData, connSocket); //创建新线程对已连接套接字进行处理
   
}

//处理已连接套接字上的读写请求
processData(connSocket){
   recv(connsocket); //从客户端读取数据,只能同时处理一个客户端
   send(connsocket); //给客户端返回数据,只能同时处理一个客户端
}

但是Redis的主流程是在一个线程中执行的,无法使用多线程的方式来提升并发处理能力

那么还有什么其他方法能提高Redis的处理能力。这就可以使用IO多复用机制了

IO多路复用的实现主要有如下3种,分别是select,poll,epoll,来看一下有什么区别!

select

本文总结自《Redis 源码剖析与实战-09 | Redis事件驱动框架(上):何时使用select、poll、epoll?》

// 
// *__readfds 监听的读数据事件集合
// *__writefds 监听的写数据事件集合
// *__exceptfds 监听的异常事件集合
// 
int select (int __nfds, fd_set *__readfds, fd_set *__writefds, fd_set *__exceptfds, struct timeval *__timeout)
typedef struct {
   …
   __fd_mask  __fds_bits[__FD_SETSIZE / __NFDBITS];} fd_set

__fd_mask类型是long int类型的别名,__FD_SETSIZE 和 __NFDBITS 这两个宏定义的大小默认为1024和32

所以fd_set结构体的定义就是一个long int类型的数组,该数组一共有32个元素(1024/32=32),每个元素是32位(long int类型的大小),每一位可以表示一个文件描述符的状态

我们现在知道了select函数对每一个描述符集合,都可以监听1024个描述符

select调用过程如下

  1. 创建描述符集合,创建监听套接字,将套接字描述符加到创建好的描述符集合中
  2. 调用select函数,并传入描述符集合。然后select函数会阻塞,当select函数检测到有描述符就绪后,就会结束阻塞,并返回就绪的文件描述符个数
  3. 遍历描述符,找出就绪的描述符进行读写或者异常处理

select函数有如下2个缺陷

  1. select函数对单个进程能监听的文件描述符数量是有限制的,能监听的文件描述符是1024
  2. 当select函数返回后,我们需要遍历描述符集合,才能找到具体是哪些描述符就绪了。这个遍历过程会产生一定开销,从而降低程序的性能

使用demo

int sock_fd,conn_fd; //监听套接字和已连接套接字的变量
sock_fd = socket() //创建套接字
bind(sock_fd)   //绑定套接字
listen(sock_fd) //在套接字上进行监听,将套接字转为监听套接字

fd_set rset;  //被监听的描述符集合,关注描述符上的读事件
 
int max_fd = sock_fd

//初始化rset数组,使用FD_ZERO宏设置每个元素为0 
FD_ZERO(&rset);
//使用FD_SET宏设置rset数组中位置为sock_fd的文件描述符为1,表示需要监听该文件描述符
FD_SET(sock_fd,&rset);

//设置超时时间 
struct timeval timeout;
timeout.tv_sec = 3;
timeout.tv_usec = 0;
 
while(1) {
   //调用select函数,检测rset数组保存的文件描述符是否已有读事件就绪,返回就绪的文件描述符个数
   n = select(max_fd+1, &rset, NULL, NULL, &timeout);
 
   //调用FD_ISSET宏,在rset数组中检测sock_fd对应的文件描述符是否就绪
   if (FD_ISSET(sock_fd, &rset)) {
       //如果sock_fd已经就绪,表明已有客户端连接;调用accept函数建立连接
       conn_fd = accept();
       //设置rset数组中位置为conn_fd的文件描述符为1,表示需要监听该文件描述符
       FD_SET(conn_fd, &rset);
   }

   //依次检查已连接套接字的文件描述符
   for (i = 0; i < maxfd; i++) {
        //调用FD_ISSET宏,在rset数组中检测文件描述符是否就绪
       if (FD_ISSET(i, &rset)) {
         //有数据可读,进行读数据处理
       }
   }
}

poll

// pollfd 结构体数组
// nfds_t __fds数组的元素个数
// __timeout poll函数阻塞的超时时间
int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);

pollfd结构体包含了要监听的描述符,以及该描述符上要监听的事件类型

struct pollfd {
    int fd;         //进行监听的文件描述符
    short int events;       //要监听的事件类型
    short int revents;      //实际发生的事件类型
};

pollfd中监听的事件类型,是通过宏定义的,有可读,可写,错误事件

#define POLLRDNORM  0x040       //可读事件
#define POLLWRNORM  0x100       //可写事件
#define POLLERR     0x008       //错误事件

poll调用过程如下

  1. 创建pollfd数组和监听套接字,并进行绑定
  2. 将监听套接字加入pollfd数组,并设置监听其读事件,也就是客户端的连接请求
  3. 循环调用poll函数,检测pollfd数组中是否有就绪的文件描述符

而第三部又分为如下两种情况

  1. 如果是连接套接字就绪,表明是客户端连接,我们可以调用accept接收连接,并创建已连接套接字,将其加入pollfd数组,监听读事件
  2. 如果是已连接套接字就绪,表明客户端有读写请求,我们可以调用recv/send函数处理读写请求

和select函数相比,poll的改进之处在于,它允许监听超过1024个文件描述符,但是当调用了poll函数后,我们仍然需要遍历每个文件描述符,监测该描述符是否就绪,然后进行处理

使用demo

int sock_fd,conn_fd; //监听套接字和已连接套接字的变量
sock_fd = socket() //创建套接字
bind(sock_fd)   //绑定套接字
listen(sock_fd) //在套接字上进行监听,将套接字转为监听套接字

//poll函数可以监听的文件描述符数量,可以大于1024
#define MAX_OPEN = 2048

//pollfd结构体数组,对应文件描述符
struct pollfd client[MAX_OPEN];

//将创建的监听套接字加入pollfd数组,并监听其可读事件
client[0].fd = sock_fd;
client[0].events = POLLRDNORM; 
maxfd = 0;

//初始化client数组其他元素为-1
for (i = 1; i < MAX_OPEN; i++)
    client[i].fd = -1; 

while(1) {
   //调用poll函数,检测client数组里的文件描述符是否有就绪的,返回就绪的文件描述符个数
   n = poll(client, maxfd+1, &timeout);
   //如果监听套件字的文件描述符有可读事件,则进行处理
   if (client[0].revents & POLLRDNORM) {
       //有客户端连接;调用accept函数建立连接
       conn_fd = accept();

       //保存已建立连接套接字
       for (i = 1; i < MAX_OPEN; i++){
         if (client[i].fd < 0) {
           client[i].fd = conn_fd; //将已建立连接的文件描述符保存到client数组
           client[i].events = POLLRDNORM; //设置该文件描述符监听可读事件
           break;
          }
       }
       maxfd = i; 
   }
   
   //依次检查已连接套接字的文件描述符
   for (i = 1; i < MAX_OPEN; i++) {
       if (client[i].revents & (POLLRDNORM | POLLERR)) {
         //有数据可读或发生错误,进行读数据处理或错误处理
       }
   }
}

epoll

epoll实例内部维护了2个结构,分别记录要监听的文件描述符和已经就绪的文件描述符。这样我们在使用epoll机制时,就不用像select和poll一样,遍历查询哪些文件描述符已经就绪了。这样一来epoll的效率就比select和poll有了更高的提升

使用demo

int sock_fd,conn_fd; //监听套接字和已连接套接字的变量
sock_fd = socket() //创建套接字
bind(sock_fd)   //绑定套接字
listen(sock_fd) //在套接字上进行监听,将套接字转为监听套接字
    
epfd = epoll_create(EPOLL_SIZE); //创建epoll实例,
//创建epoll_event结构体数组,保存套接字对应文件描述符和监听事件类型    
ep_events = (epoll_event*)malloc(sizeof(epoll_event) * EPOLL_SIZE);

//创建epoll_event变量
struct epoll_event ee
//监听读事件
ee.events = EPOLLIN;
//监听的文件描述符是刚创建的监听套接字
ee.data.fd = sock_fd;

//将监听套接字加入到监听列表中    
epoll_ctl(epfd, EPOLL_CTL_ADD, sock_fd, &ee); 
    
while (1) {
   //等待返回已经就绪的描述符 
   n = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1); 
   //遍历所有就绪的描述符     
   for (int i = 0; i < n; i++) {
       //如果是监听套接字描述符就绪,表明有一个新客户端连接到来 
       if (ep_events[i].data.fd == sock_fd) { 
          conn_fd = accept(sock_fd); //调用accept()建立连接
          ee.events = EPOLLIN;  
          ee.data.fd = conn_fd;
          //添加对新创建的已连接套接字描述符的监听,监听后续在已连接套接字上的读事件      
          epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ee); 
                
       } else { //如果是已连接套接字描述符就绪,则可以读数据
           ...//读取数据并处理
       }
   }
}

总结

多路复用机制 监听的文件描述符数量 查找就绪的文件描述符
select 1024 遍历所有描述符
poll 自定义 遍历所有描述符
epoll 自定义 自动返回就绪的描述符

参考博客

[1]https://time.geekbang.org/column/article/407901

你可能感兴趣的:(Redis,redis,缓存,数据库)