思考
- 高并发 -> socket -> epoll
- Linux 提供的系统实现 IO 多路复用器: select、poll(Linux<2.4),epoll(Linux>2.6)
学习目标
- epoll 是 Linux 提供的系统实现,核心方法只有三个
- epoll 效率高,是因为基于红黑树、双向链表、事件回调机制
- redis 的 IO 多路复用,Linux 上用 epoll 进行了实现
socket 是什么?
Socket 是在应用层和传输层之间的一个抽象层,它把 TCP/IP 层复杂的操作抽象为几个简单的接口,供应用层调用实现进程在网络中的通信。Socket 起源于 UNIX,在 UNIX 一切皆文件的思想下,进程间通信就被冠名为文件描述符(file descriptor)
,Socket 是一种“打开—读/写—关闭”模式的实现,服务器和客户端各自维护一个“文件”,在建立连接打开后,可以向文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。
select、poll
select 是操作系统的提供的内核系统调用函数,通过它可以将一组 FD 传给操作系统,操作系统对这组 FD 进行遍历,当存在 FD 处于数据就绪状态后,将其全部返回给调用方,这样应用程序就可以对已经就绪的 IO 流进行处理了。
// sets的文件描述符的最大值
// fd_set 类型,包含了需要检查是否可读的描述符,输出时表示哪些描述符可读。可为 NULL。
// fd_set 类型,包含了需要检查是否可写的描述符,输出时表示哪些描述符可写。可为 NULL。
// fd_set 类型,包含了需要检查是否出错的描述符,输出时表示哪些描述符出错。可为 NULL。
// struct timeval 类型的结构体,表示等待检查完成的最长时间。
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* errorfds, struct timeval* timeout);
- 每次调用 select,都需要把 FD 集合从用户态拷贝到内核态,当 FD 较多时资源开销相对较大
- select 并不会只返回就绪的 FD,而是需要用户进程自己一个一个进行遍历找到就绪的 FD
- select 最多只能监听 1024 个连接,默认是 1024
poll 机制实际与 select 机制区别不大,只是 poll 机制去除掉了监听连接数 1024 的限制
epoll
epoll 解决了 select 以及 poll 机制的大部分问题,主要体现在以下几个方面:
- epoll 创建的红黑树保存所有 FD,没有大小限制,且增删查的复杂度 O(logN)
- 基于 callback,利用系统内核触发感兴趣的事件
- 就绪列表为双线链表时间复杂度 O(1)
- 应用获取到的 FD 都是真实发生 IO 的 FD,与 select 和 poll 需要不断轮询判断是否可用相比,能避免无用的内存拷贝
源码分析
// 调用 epoll_create 建立一个 epoll 对象(在 epoll 文件系统中给这个句柄分配资源);
int epoll_create(int size);
// 调用 epoll_ctl,对指定描述符 FD 执行 op 的绑定操作;把 FD 写入红黑树,同时在内核注册回调函数
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 调用 epoll_wait 获取 epfd 上的 io 事件
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
struct eventpoll {
...
/*红黑树的根节点,这棵树中存储着所有添加到epoll中的事件,
也就是这个epoll监控的事件*/
struct rb_root rbr;
/*双向链表rdllist保存着将要通过epoll_wait返回给用户的、满足条件的事件*/
struct list_head rdllist;
...
};
- 我们在调用 epoll_create 时,Linux内核除了帮我们在 epoll 文件系统里建了个 file 结点(eventpoll 结构体)
- 在内核 cache 里建了个红黑树用于存储以后 epoll_ctl 传来的 socket 外,还会再建立一个 rdllist 双向链表,用于存储准备就绪的事件,当 epoll_wait 调用时,仅仅观察这个 rdllist 双向链表里有没有数据即可。有数据就返回,没有数据就 sleep,等到 timeout 时间到后即使链表没数据也返回。
两种触发模式
EPOLLLT 和 EPOLLET 两种触发模式,LT 是默认的模式,ET 是“高速”模式。
LT (水平触发)模式下,只要这个 FD 还有数据可读,每次 epoll_wait 都会返回它的事件,提醒用户程序去操作
ET(边缘触发)模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无 论 FD 中是否还有数据可读。所以在 ET 模式下,read 一个 FD 的时候一定要把它的 buffer 读光,也就是说一直读到 read 的返回值小于请求值,或者 遇到 EAGAIN 错误。
为什么要有 EPOLLET 触发模式?
如果采用 EPOLLLT 模式的话,系统中一旦有大量你不需要读写的就绪文件描述符,它们每次调用 epoll_wait() 都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率。
而采用 EPOLLET 这种边沿触发模式的话,当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知
这种模式比 LT 效率高,系统不会充斥大量你不关心的就绪文件描述符
其他
[root@iZwz9gkozl3yw55rf29gcoZ code]# cat /proc/sys/fs/file-max
78810
推荐学习
Linux C++网络编程--C/C++视频教程-后端开发-CSDN程序员研修院
C++socket网络编程--http服务器(支持php)实战教学视频--C/C++视频教程-后端开发-CSDN程序员研修院
c++高并发商业级游戏服务器干货【客户端ue4和unity3d】--C/C++视频教程-后端开发-CSDN程序员研修院
netty源码分析与架构介绍--Java视频教程-后端开发-CSDN程序员研修院
C++ 百万并发网络通信引擎架构与实现 (Socket、全栈、跨平台) Version 1.0-c++socket 发送字节,c++定时发送-C/C++视频教程-后端开发-CSDN程序员研修院