一、背景
redis是内存数据库,并且是单线程,为什么单线程也能够这么快?
因为,所有线上请求的set、get操作都是在内存中,涉及到磁盘和网络的部分都是由后台线程执行,尽量减少了主线程的开销。单线程只是说对字典空间set、get时是单线程的,不需要同步机制,而将数据在用户空间和页缓存之间的拷贝是由IOThreads做的,其中主线程也算是其中一个IOThread。
二、epoll使用案例
为了降低redis理解时的复杂度,先给出一个epoll使用时的case,有助于后面的理解。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/socket.h>
#include<errno.h>
#include<fcntl.h>
#define _BACKLOG_ 5
#define _BUF_SIZE_ 10240
#define _MAX_ 64
typedef struct _data_buf
{
int fd;
char buf[_BUF_SIZE_];
}data_buf_t,*data_buf_p;
static void usage(const char* proc)
{
printf("usage:%s[ip][port]\n",proc);
}
static int start(int port,char *ip)
{
assert(ip);
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
perror("socket");
exit(1);
}
struct sockaddr_in local;
local.sin_port=htons(port);
local.sin_family=AF_INET;
local.sin_addr.s_addr=inet_addr(ip);
int opt=1; //设置为接口复用
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
{
perror("bind");
exit(2);
}
if(listen(sock,_BACKLOG_)<0)
{
perror("listen");
exit(3);
}
return sock;
}
static int epoll_server(int listen_sock)
{
int epoll_fd=epoll_create(256);//生成一个专用的epoll文件描述符
if(epoll_fd<0)
{
perror("epoll_create");
exit(1);
}
struct epoll_event ev;//用于注册事件
struct epoll_event ret_ev[_MAX_];//数组用于回传要处理的事件
int ret_num=_MAX_;
int read_num=-1;
ev.events=EPOLLIN;
ev.data.fd=listen_sock;
//注册监听套接字
if(epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_sock,&ev)<0)//用于控制某个文件描述符上的事件(注册,修改,删除)
{
perror("epoll_ctl");
return -2;
}
int done=0;
int i=0;
int timeout=5000;
struct sockaddr_in client;
socklen_t len=sizeof(client);
while(!done)
{
//ret_ev存放的是触发的事件
switch(read_num=epoll_wait(epoll_fd,ret_ev,ret_num,timeout))//用于轮寻I/O事件的发生
{
case 0:
printf("time out\n");
break;
case -1:
perror("epoll");
exit(2);
default:
{
for(i=0;i<read_num;++i)
{
//连接建立事件
if(ret_ev[i].data.fd==listen_sock&&(ret_ev[i].events&EPOLLIN))
{
int fd=ret_ev[i].data.fd;
int new_sock=accept(fd,(struct sockaddr*)&client,&len);
if(new_sock<0)
{
perror("accept");
continue;
}
ev.events=EPOLLIN;
ev.data.fd=new_sock;
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,new_sock,&ev);
printf("get a new client...\n");
}
else //normal sock
{
//读事件
if(ret_ev[i].events&EPOLLIN)
{
int fd=ret_ev[i].data.fd;
data_buf_p mem=(data_buf_p)malloc(sizeof(data_buf_t));
if(!mem){
perror("malloc failed...");
continue;
}
mem->fd=fd;
memset(mem->buf,'\0',sizeof(mem->buf));
ssize_t _s=read(mem->fd,mem->buf,sizeof(mem -> buf)-1);
if(_s>0)
{
mem->buf[_s-1]='\0';
printf("client: %s\n",mem->buf);
ev.events=EPOLLOUT;
ev.data.ptr=mem;
epoll_ctl(epoll_fd,EPOLL_CTL_MOD,fd,&ev);
}
else if(_s==0)
{
printf("client close...\n");
epoll_ctl(epoll_fd,EPOLL_CTL_DEL,fd,NULL);
close(fd);
free(mem);
}
}
else if(ret_ev[i].events&EPOLLOUT) //写事件准备就绪
{
data_buf_p mem=(data_buf_p)ret_ev[i].data.ptr;
char* msg="http/1.0 200 ok\r\n\r\nhello bit\r\n";
int fd=mem->fd;
write(fd,msg,strlen(msg));
close(fd);
epoll_ctl(epoll_fd,EPOLL_CTL_DEL,fd,&ev); //写完服务端直接退出
free(mem);
}
}
}
}
}
}
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
usage(argv[0]);
return 1;
}
int port=atoi(argv[2]);
char *ip=argv[1];
//创建监听套接字
int listen_sock=start(port,ip);
printf("listening... ip: %s, port: %d\n",ip,port);
//注册监听套接字事件,accept新链接并注册进入epoll
epoll_server(listen_sock);
close(listen_sock);
return 0;
}
主要流程如下:
- 创建socket,并转化成为listen socket。
- 将listen socket fd注册到epoll中,epollfd是用来标识epoll这个对象的,处处皆文件。
- 如果触发事件的fd是listen socket fd,那表示来了新连接,
- 先要epoll_wait,将数据读入到用户空间;
- 调用accept创建新连接newfd;
- 将newfd注册读事件到epoll中。
- 如果触发的是写事件,则调用write,并删除这个事件。
- 如果触发的是读事件,则调用read获取请求,并投递到业务线程池,执行完后会注册一个写事件到达epoll中。服务端一般采用ET模式,没读完下次也不会返回这个事件,读时一般会用while循环一次性读完。
三、redis事件注册
redis初始化时会创建所有的listen socket并注册到eventloop中监听,绑定accept Handler,当连接建立时就会accept新连接,创建client并向eventloop注册读事件。
下面主要会从以下几方面叙述:
- 事件的注册逻辑,包括listen socket fd以及client fd。
- 读事件分发处理逻辑。
- 过期key逻辑。
- 写事件分发处理逻辑。
- 监听器暂时不讲。
事件注册包括创建listen socket,注册事件,设置handler等。
事件分发流程如下所示,后面会慢慢介绍。
1 数据结构
//只涉及后面用到的字段
struct redisServer {
redisDb *db;
aeEventLoop *el; //epoll
socketFds cfd; /* listening socket */
list *clients; /* List of active clients */
list *clients_to_close; /* Clients to close asynchronously */
list *clients_pending_write; /* There is to write or install handler. */
list *clients_pending_read; /* Client has pending read socket buffers. */
client *current_client;
}
//io多路复用
/* State of an event based program */
typedef struct aeEventLoop {
int maxfd; /* highest file descriptor currently registered */
int setsize; /* max number of file descriptors tracked */
long long timeEventNextId;
aeFileEvent *events; /* 当前所有注册的事件,包括已经触发和没有触发的*/
aeFiredEvent *fired; /* 本次触发的事件*/
aeTimeEvent *timeEventHead;
int stop;
void *apidata; /* aeApiState */
aeBeforeSleepProc *beforesleep; //预处理,很重要,请求分发逻辑
aeBeforeSleepProc *aftersleep; //尾处理
int flags;
} aeEventLoop;
/* File event structure */
typedef struct aeFileEvent {
int mask; /* one of AE_(READABLE|WRITABLE|BARRIER) */
aeFileProc *rfileProc;
aeFileProc *wfileProc;
void *clientData;
} aeFileEvent;
/* A fired event */
typedef struct aeFiredEvent {
int fd;
int mask;
} aeFiredEvent;
2 创建listen socket
这一阶段完成了socket、bind、listen三步。
int listenToPort(int port, socketFds *sfd) {
int j;
char **bindaddr = server.bindaddr;
/* If we have no bind address, we don't listen on a TCP socket */
if (server.bindaddr_count == 0)