还是先思考几个问题:
1.select、poll、epoll是什么?作用是什么?为什么用它?
2.select、poll、epoll原理。
3.对比优缺点。
4.epoll如何实现高并发服务器。
在解释这几个问题之前,我们来先看一下,如果没有这几个复用管理,我们之前对tcp的套接字socket fd如何管理的呢?
如果有100条tcp连接,那么我们要高效的监控并读写这100条连接:
单线程:
阻塞模式下单线程几乎是不可能完成监控+读写的。
非阻塞模式下,单线程不停的遍历所有的socket fd来判断现在是否可以读写,这里处理的效率是非常低的,即使没有读写,线程也要不停的遍历,耗cpu。
多线程:
阻塞模式下多线程处理,那么tcp连接非常多的时候,你的线程也会非常多,线程上下文切换也会非常频繁,效率低下。
非阻塞模式也是一样,比阻塞模式效率更低,因为即使没有读写,也会空跑。
那么为了解决管理socket fd,内核为我们提供了管理,(select 、poll、epoll),好了在这种背景下我们引出了这几个多路复用。
第一个问题:
1.select、poll、epoll是什么?作用是什么?为什么用它?
其实在我们上面的分析中不难找出答案,如果你读懂了上面的分析的话。
是什么?
多路复用。
作用是什么?为什么用它?
内核帮应用程序管理大量socket fd状态。应用程序只需要调用系统提供的api即可,状态维护在内核中,降低开发难度。(当然你了解原理后,觉得自己实现的会更好,效率和安全性更高,你可以选择自己实现)
第二个问题:
2.select、poll、epoll原理。
select原理:
支持阻塞操作的设备驱动通常会实现一组自身的等待队列如读/写等待队列用于支持上层(用户层)所需的BLOCK或NONBLOCK操作。当应用程序通过设备驱动访问该设备时(默认为BLOCK操作),若该设备当前没有数据可读或写,则将该用户进程插入到该设备驱动对应的读/写等待队列让其睡眠一段时间,等到有数据可读/写时再将该进程唤醒。
select就是巧妙的利用等待队列机制让用户进程适当在没有资源可读/写时睡眠,有资源可读/写时唤醒。
api接口:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
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);
poll原理:(跟select和epoll相比较其实是个比较鸡肋的东西,了解一下就好,主要是epoll和select要深入下)
poll只是在select基础上对支持的最大fd数量做了改进,内核存储的是fd相应结构链表。从来解决最大数量问题,但是比较积累。不改变每次都遍历的方式,其实只改支持数量,也不会改善太多。
api接口:
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
epoll原理:
内核中红黑树+双向链表
红黑树存所有关心事件,双向链表存放准备就绪事件。每次获取可读写事件查询双向链表即可。
api接口:
int epoll_create(int size);
int epoll_create1(int flags);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数介绍:
epfd:file descriptor epfd (epoll_create返回值)
op:EPOLL_CTL_ADD\EPOLL_CTL_MOD\EPOLL_CTL_DEL
fd:需要监控的文件描述符
epoll_event:
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_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
int close(int fd);
第三个问题
3.对比优缺点:
select缺点:
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);// 阻塞函数,主要是使用该函数
1)select的实现,在内核中它是被定义成一个数组的结构,数组肯定有大小,数组的大小被默认定义为1024,默认情况下只能支持1024个socket的管理。
当然,可以修改内核中的这个个数宏,增大支持个数。
2)select返回后,内核管理的这个fd_set会清空,下次调用select依然需要用户态拷贝数据(fd_set) 到内核态。
3)select返回后,依然需要遍历fd_set来判断期望的socket fd是否发生我们关心的事件。
poll改善:
poll内核用链表替换数组来解决支持fd上限1024问题。
但高并发情况下select和poll支持并发有限,eg:c10k(1w左右并发)。
为了解决并发问题,内核重新设计了机制,也就是epoll来解决高并发问题。
epoll通过红黑树+双向链表就解决上面的三个问题
红黑树存放fd,fd上有事件发生时,通过回调把事件添加到双向链表中,epoll_wait获取双向链表中的事件。
第四个问题:
4.epoll如何实现高并发服务器。
这个我改日再剖析,代码会更新在我的github上。