Redis epoll
linux5种IO
-
IO个人理解的Socket的读取方式,将socket中的数据拷贝到内核态,再从内核态拷贝到用户态(程序中)。
-
同步阻塞IO
-
linux下使用man bio命令,可以得到如下描述
A BIO is an I/O abstraction, it hides many of the underlying I/O details from an application. 表示是一种IO的使用方法
-
应用程序和内核态的系统调用一直等待数据或者满足某些特定条件(阻塞)。
-
每个socket都有一个线程进行操作
-
-
同步非阻塞IO
- 访问socket,如果有数据就将数据从内核态拷贝到用户态,没有数据返回-1
- 需要循环遍历所有的socket,并且每次socket查询操作需要进行一次系统调用,用户态到内核态的切换
-
多路复用:select,poll,epoll
- 将所有的socket信息一次性拷贝到内核态,由内核态访问所有socket,如果有数据,通知用户态进程进行相关操作
- select:
- set是一个字节数组 一个bit可以表示一个文件描述符,例如初始化之后为0000,0000 如果有个fd为5那么set就编程0001,0000,如再加入fd1,2则变成0001,0011
- 每次循环都需要将set清空,然后遍历文件描述符array,将文件描述符里面的值赋值到set中。并且set的大小为linux系统固定。
//linux里对select的描述 int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); void FD_CLR(int fd, fd_set *set); int FD_ISSET(int fd, fd_set *set); void FD_SET(int fd, fd_set *set); void FD_ZERO(fd_set *set);
参数 意义 nfds 文件描述符集合中最大值加一 readfds 监视文件描述符中是否有数据可读,当select返回时只留下可读的文件描述符,可以被recv和read writefds 监视文件描述符是否有写数据,当select返回时,只留下可写的文件描述符,可以被send和write exceptfds 监视文件中错误异常,可用于监视带外数据 timeout 监视文件描述符的事件,有三种情况:null代表阻塞读,可被信号量打断,0代表轮询描述符,返回可操作的个数。具体值代表规定时间内如满足条件函数返回 - poll:
- 返回的是数字,大于零代表有事件,会遍历fds找到所有revents不为零的pollfd,然后做相应处理,等于零代表超时或者无事件发生,小于零代表错误。
- poll对于select的改进第一是,fds没有限制数量,同时fds可以复用。每次修改的都是revents。
int poll(struct pollfd *fds, nfds_t nfds, int timeout); struct pollfd { int fd; /* file descriptor */ short events; /* requested events */ short revents; /* returned events */ };
参数 意义 pollfd *fds 文件描述符结构体集合的指针 nfds 描述符的个数,即结构体的个数 timeout 同select 结构体参数 意义 fd 具体的文件描述符,为-1表示忽略 events 表示需要关注的事件,可以设置为POLLIN|POLLOUT表示同时关注可读和可写 reevents 函数返回时实际发生的事件 -
epoll
-
对比poll的改进是,epoll是在内核态开辟了一个空间存放文件描述符集合(自定义结构体)(不需要每次将监听的端口拷贝到内核态),epoll总是访问需要操作的文件描述符
-
epdf空间主要分为两块
- 一个是红黑树存储的所有文件描述符,由poll_ctl控制,每当一个新的文件描述符加入,修改,删除,就会在红黑树中寻找到对应的文件描述符进行操作。执行插入操作的同时将这个文件描述符注册到回调函数中。
struct epitem { struct rb_node rbn; //用于主结构管理的红黑树 struct list_head rdllink; //事件就绪队列 struct epitem *next; //用于主结构体中的链表 struct epoll_filefd ffd; //每个fd生成的一个结构 int nwait; struct list_head pwqlist; //poll等待队列 struct eventpoll *ep; //该项属于哪个主结构体 struct list_head fllink; //链接fd对应的file链表 struct epoll_event event; //注册的感兴趣的事件,也就是用户空间的epoll_event } struct eventpoll { spin_lock_t lock; //对本数据结构的访问 struct mutex mtx; //防止使用时被删除 wait_queue_head_t wq; //sys_epoll_wait() 使用的等待队列 wait_queue_head_t poll_wait;//file->poll()使用的等待队列 struct list_head rdllist; //事件满足条件的链表 struct rb_root rbr; //用于管理所有fd的红黑树 struct epitem *ovflist; //将事件到达的fd进行链接起来发送至用户空间 }
-
一个是文件描述符就绪列表,主要是利用回调函数触发事件的回调
//这部分函数在epoll_ctl中执行 //初始化poll回调函数,并注册 init_poll_funcptr(&epq.pt,ep_ptable_queue_proc); //当poll被中断唤醒时调用回调函数 ep_ptable_queue_proc(struct file *file,wait_queue_head_t *whead,poll_table *pt) //真正的回调函数ep_poll_callback init_waitqueue_func_entry(&pwq-> wait, ep_poll_callback) //ep_poll_callback回调函数调用将fd添加到监听列表 list_add_tail(&epi->rdllink,&ep->rdllist); //wait阶段的主要函数调用 //ep_wait调用ep_poll(ep,events,maxevents,timeout) ep_poll(struct eventpoll *ep,struct epoll_event __user *events,int maxevents,long timeout) //ep_poll里面调用send,向用户空间发送就绪事件 ep_send_events(struct eventpoll *ep,struct epoll_event __user *events,int maxevents)
-
#include <sys/epoll.h> int epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//成功返回0,失败返回-1和错误信息 int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
ctl参数 | 含义 |
---|---|
epdf | create返回值,未使用的最小文件描述符,也是开辟的空间文件描述符 |
op | 要执行的操作, EPOLL_CTL_ADD:向多路复用实例加入一个连接socket的文件描述符 EPOLL_CTL_MOD:改变多路复用实例中的一个socket的文件描述符的触发事件 EPOLL_CTL_DEL:移除多路复用实例中的一个socket的文件描述符 |
fd | 需要操作的文件描述符 |
event | EPOLLIN:文件描述有可以读取的内容 EPOLLOUT:文件描述符可以写入 EPOLLRDHUP:套接字流关闭,或写到一半的时候连接断开(没搞懂英文解释是啥意思) EPOLLPRI:发生异常情况,比如所tcp连接中收到了带外消息 EPOLLET:设置多路复用实例的文件描述符的事件触发机制为边沿触发,默认为水平触发 EPOLLONESHOT:epoll_wait只会对该文件描述符第一个到达的事件有反应,之后的其他事件都不向调用者抛出。 |
wait参数 | 含义 |
---|---|
epfd | 同上 |
events | 同上 |
maxevents | 最多监听的文件描述符数量 |
timeout | 超时事件 |
//官方使用案例
#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;
/* Code to set up listening socket, 'listen_sock',
(socket(), bind(), listen()) omitted */
epollfd = epoll_create1(0);//这个函数同epoll_create(size),这个暂时还没弄太懂,不过推荐使用create1
if (epollfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
for (;;) {
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}
for (n = 0; n < nfds; ++n) {
if (events[n].data.fd == listen_sock) {
conn_sock = accept(listen_sock,(struct sockaddr *) &addr, &addrlen);
if (conn_sock == -1) {
perror("accept");
exit(EXIT_FAILURE);
}
setnonblocking(conn_sock);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
&ev) == -1) {
perror("epoll_ctl: conn_sock");
exit(EXIT_FAILURE);
}
} else {
do_use_fd(events[n].data.fd);
}
}
}
-
信号驱动IO
- 信号处理机制,例如网卡中断,由DMA将内核态的读取缓冲区直接拷贝到套接字相关的缓冲区(如果硬件和驱动程序支持可以直接将内核缓冲区的数据文件描述符和和描述信息直接拷贝到套接字缓冲区,然后读取时根据文件描述符读取到内存缓冲区的数据,避免了cpu将数据完整复制到套接字缓冲区)
-
异步IO
- 应用程序进行IO操作时,不需要等待数据的返回,只需要分配出一部分的空间存储将来要到来的数据,直接进行之后的操作
redis epoll调用流程
strace -ff -o out ./redis-server
使用strace命令获取redis启动调用的进程函数,本机启动的pid为1950
查看1950的输出文件
openat(AT_FDCWD, "/etc/localtime", O_RDONLY|O_CLOEXEC) = 3
epoll_create(1024) = 5
socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP) = 6
bind(6, {sa_family=AF_INET6, sin6_port=htons(6379), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, 28) = 0
listen(6, 511) = 0
fcntl(6, F_GETFL) = 0x2 (flags O_RDWR)
fcntl(6, F_SETFL, O_RDWR|O_NONBLOCK) = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 7
bind(7, {sa_family=AF_INET, sin_port=htons(6379), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(7, 511)
epoll_ctl(5, EPOLL_CTL_ADD, 6, {EPOLLIN, {u32=6, u64=6}}) = 0
epoll_ctl(5, EPOLL_CTL_ADD, 7, {EPOLLIN, {u32=7, u64=7}}) = 0
epoll_ctl(5, EPOLL_CTL_ADD, 3, {EPOLLIN, {u32=3, u64=3}}) = 0
epoll_wait(5, [], 10128, 100) = 0
getpid() = 1950
openat(AT_FDCWD, "/proc/1950/stat", O_RDONLY) = 8
read(8, "1950 (redis-server) R 1948 1948 "..., 4096) = 327
close(8) = 0
epoll_wait(5, [], 10128, 100) = 0
可以看见初始化之后有了4个文件描述符,分别是时间描述符3,ep_create创建的文件描述符5和创建的ipv6和ipv4的socket文件描述符6,7,然后对6,7分别绑定端口6379,监听,设置非阻塞。然后就开始不断循环调用epoll_wait(),一直返回0。
在启动一个客户端之后
accept(7, {sa_family=AF_INET, sin_port=htons(55950), sin_addr=inet_addr("127.0.0.1")}, [128->16]) = 8
fcntl(8, F_GETFL) = 0x2 (flags O_RDWR)
fcntl(8, F_SETFL, O_RDWR|O_NONBLOCK) = 0
setsockopt(8, SOL_TCP, TCP_NODELAY, [1], 4) = 0
setsockopt(8, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0
setsockopt(8, SOL_TCP, TCP_KEEPIDLE, [300], 4) = 0
setsockopt(8, SOL_TCP, TCP_KEEPINTVL, [100], 4) = 0
setsockopt(8, SOL_TCP, TCP_KEEPCNT, [3], 4) = 0
epoll_ctl(5, EPOLL_CTL_ADD, 8, {EPOLLIN, {u32=8, u64=8}}) = 0
getpeername(8, {sa_family=AF_INET, sin_port=htons(55950), sin_addr=inet_addr("127.0.0.1")}, [128->16]) = 0
accept(7, 0x7ffc98dbcfc0, [128]) = -1 EAGAIN (Resource temporarily unavailable)
epoll_wait(5, [{EPOLLIN, {u32=8, u64=8}}], 10128, 43) = 1
启动一个客户端之后,调用accept得到文件描述符8,然后ctl函数将文件描述符添加到epfd的文件描述符中,成功返回0。
然后循环调用的wait函数就能得到相应的值。