@bit::Shadow
✧(≖ ◡ ≖✿
目录
[1.0select poll回顾](#1.0select poll回顾)
[1.2.0struct epoll_event { :设置于内核中的关心事件体(结构)](#1.2.0struct epoll_event { :设置于内核中的关心事件体(结构))
[3.1struct eventpoll {](#3.1struct eventpoll {)
[3.2struct epintem {](#3.2struct epintem {)
[1.如何正确认识epoll_ctl 中的第三参数fd与第四参数中data的fd?](#1.如何正确认识epoll_ctl 中的第三参数fd与第四参数中data的fd?)
[2.为什么就绪后,不进行判断_revts0.data.fd == _listenfd?而是_revtsi == _listenfd](#2.为什么就绪后,不进行判断_revts[0].data.fd == _listenfd?而是_revts[i] == _listenfd)
前言:epoll是Linux特有的I/O多路复用机制,从Linux2.5.44(2002年)引入,2.6正式可用。它解决select、poll在高并发下的性能瓶颈而设计的高度解耦的I/O机制。
1.认识epoll
epoll定位:基于对多个fd等待的就绪通知机制。
1.0select poll回顾
cpp
int select(int nfds,
fd_set* rfds,
fd_set* wfds,
fd_set* exefds,
struct timeval* timeout
);
int poll(struct pollfd* fds, nfd_t nfds, int timeout);
对比出epoll的优势
select/poll 是"我告诉你我关心哪些 fd,你每次帮我挨个问一遍";epoll 是"我注册一次,你有事直接通知我"。
1.1epoll_create
创建一个epoll模型(文件描述符)
cpp
int epoll_create(int size);
现在参数size已经被忽略,但传入要求传入大于 0 的值。(256 128)
成功返回fd (epoll模型),失败返回-1;错误码被设置
1.2epoll_ctl
++用户告诉内核++ 译作"掌控"更好理解
cpp
int epoll_ctl(int epollfd, int op, int fd, struct epoll_event* event);
用户告诉内核,向epoll实例里添加、修改、删除要监控的文件描述符。
epollfd:epoll实例对应的文件描述符。
op:操作选项。EPOLL_CTL_ADD(增) EPOLL_CTL_MOD(改) EPOLL_CTL_DEL(删)
fd:要操作的目标fd。(像服务端对应的_listenfd)
1.2.0struct epoll_event {:设置于内核中的关心事件体(结构)
cpp
//glibc提供的用户态:(真正使用)
struct epoll_event
{
uint32_t events; // 事件掩码
epoll_data_t data; // 用户数据(union)
};
/*linu内核态*/
struct epoll_event
{
__u32 events; //epoll events EPOLLIN EPOLLOUT
__u64 data; //用户数据,内核原样带回。可当指针、fd 或整数用
} EPOLL_PACKED;
①__u32 events :内部成员是"关心事件宏"(只有1个比特位为1)。通过设置特定宏来关心fd的事件
cpp
EPOLLIN = 0x001
EPOLLPRI = 0x002
EPOLLOUT = 0x004
EPOLLRDNORM = 0x040
EPOLLRDBAND = 0x080
②__u64 data:本质是一个枚举体结构(选择一个传入的参数 "内核不修改" )
cpp
//glibc提供的用户态:(真正的使用层)
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
1.3epoll_wait
内核告诉用户!
cpp
int epoll_wait(int epfd,
struct epoll_event* events,
int maxevents,
int timeout
);
epoll_wait是epoll的"等待接口"------阻塞/超时等待epoll实例里被监控的fd发生事件,返回时只把就绪者的fd交给用户。
epfd:epoll_create创建的epoll实例fd
events:【输出型参数】内核将就绪的事件添加进来
maxevents:告诉内核数组容量。(最多一次返回多少个事件)
timeout:限定时间长度
- -1:无限阻塞,有就绪就返回。
- 0:立即返回,不阻塞。
- > 0:最多阻塞那么多毫秒超时返回0。
返回值 n :
> 0:就绪的fd个数。(仅events0~eventsn-1有效)
= 0:超时。
-1 :出错。
实例
cpp
public:
//TCP版本
EpollServer(const char* port)
:_ptr(std::make_unique<TCPSocket>(port))
{
//1.TCP服务器
_ptr->TCPServerBuild ();/*Socket Bind Listen*/
_listenfd = _ptr->sockfdGet ();
// 2.epoll IO
_epfd = epoll_create (512);//参数大于零即可
if(_epfd < 0)
RETFATAL ("epoll_create");
// LOG (LogLevel::DEBUG) << "epfd: " << _listenfd << " " << _epfd << " epollServer 启动成功";//成功!!
//3.IO监视
// //临时性栈空间变量
//1.读事件
struct epoll_event epEvt;
epEvt.events = EPOLLIN;//暂时仅关心读事件
//// //2.就绪响应fd设置
epEvt.data.fd = _listenfd;
//理解第三参数与第四参数内fd的区别
int ret = epoll_ctl (_epfd, EPOLL_CTL_ADD, _listenfd, &epEvt);//设置到内核
if(ret < 0)
RETFATAL ("epoll_ctl");
}
~EpollServer()
{
//文件描述符关闭完全?
}
void Start()
{
// 请求链接 读 写
while(true)
{
/*wait for an IO event on an epollfd*/
int n = epoll_wait (_epfd, _epevnt, epoll_event_size,
epoll_wait_val); // 内核---用户 输出型
if(n > 0)
{
/*n个fd就绪*/
//事件分发
Dispatch ();
}
else if(n == 0)
{
/*超时*/
LOG (LogLevel::DEBUG) << "time out ...";//epoll_wait_val设置为1000每1stime out...
continue;
}
else{
//epoll_wait出错
LOG(LogLevel::DEBUG) << "epoll_wait err";
perror("epoll_wait");
exit(1);
}
}/*while*/
}/*void Start()*/
读取细节及IO处理链接🔗(必看)(标注" / / "为理解重点,还有问号注释、一般注释都要仔细理解)注意:客户端退出时一定是先epoll_ctl -- DEL 后 close(fd)
也非常容易理解因为ctl是用户告诉内核的
2.epoll内核原理
结论:
1.epoll_create 主要创建了两种数据结构并携带了fd的一种机制,红黑树、就绪队列及一种关于fd的"回调机制"。

2.epoll_ctl 将红黑树中**【k,v】(fd,events)**对,做增、删、改。
☆检测fd就绪,回调"激活红黑树节点"到就绪队列是谁做的?
答:是由OS自主检测抉择的。(理解这点非常重要)
3.epoll_wait
- 关心是否有fd就绪,仅检测就绪队列 中是否为空即可。O(1)
- 获取就绪时间,从就绪队列中获取。O(N)
3.内核代码中epoll的有关结构
3.1struct eventpoll {
epoll虽然称extend-poll但在内核中以 struct eventpoll { 组织
该结构的核心是红黑树、就绪队列,它们解决的主要问题就是"如何在系统层面尽量地解决select poll的遍历难题"
cpp
/*epoll_create创建*/
struct eventpoll {
/* Protect the this structure access */
rwlock_t lock;//保证线程安全的锁
/*
* This semaphore is used to ensure that files are not removed
* while epoll is using them. This is read-held during the event
* collection loop and it is write-held during the file cleanup
* path, the epoll file exit code and the ctl operations.
*/
struct rw_semaphore sem;//保证线程安全的信号量
/* Wait queue used by sys_epoll_wait() */
wait_queue_head_t wq;
/* Wait queue used by file->poll() */
wait_queue_head_t poll_wait;//等待队列
/* List of ready file descriptors */
struct list_head rdllist;//就绪队列
/* RB-Tree root used to store monitored fd structs */
struct rb_root rbr;//红黑树
};
【了解】
- rwlock_t lock 保证线程安全的锁
- struct rw_semaphore sem 保证线程安全的信号量
- . . .
- wait_queue_head_t poll_wait 等待队列
- struct list_head rdllist 就绪队列
- struct rb_root rbr; 红黑树
信号量(Semaphore)回顾
一、是什么
信号量是一个整型计数器 ,用来协调多个执行单元(进程/线程)对共享资源的访问。它由 Dijkstra 提出,核心只有两个原子操作:
操作 别名 语义 P wait()/down()/sem_wait()申请资源:计数减 1,若 < 0 则阻塞 V signal()/up()/sem_post()释放资源:计数加 1,唤醒等待者 关键 :P 和 V 都是原子操作,不可被打断。
二、核心语义
c
sem_wait(sem); // P:sem->value--; 若 value < 0,阻塞 sem_post(sem); // V:sem->value++; 若有等待者,唤醒一个计数器的值
value有两种解读:
value > 0 :还有
value个资源可用,不用等value == 0:资源刚好用完
value < 0 :有
|value|个执行单元正在等待(这是 Linux 内核的实现视角)
解释红黑树节点与就绪队列中的节点与公共struct epitem { 的关系:
答:红黑树节点与就绪队列节点拥有公共的结构存储即epitem
3.2struct epitem {
重要定位:红黑树、就绪队列的公有结构
各字段

3.3fd回调机制
epoll_ctl ( 的系统调用为sys_epoll_ctl(
调用 epoll_ctl 后通过系统调用信号触发,创建了 struct eppoll { 结构;其内部的void* base指针指向红黑树节点,若该节点就绪,就会放置到就绪队列中。 wait_queue_t func 就是fd回调的函数指针
所以 epoll_ctl 真正做的就是将红黑树节点插入到就绪队列内。(上文说过这是OS自动检测调度的、红黑树结构其实是【fd,events】------在此处照应了"通过系统调用信号触发"、fd红黑树索引机制)
注:eppoll_entry是双链表结构
epoll的优点(和 select 的缺点对应)
- 接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
- 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
- 事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响.
- 没有数量限制: 文件描述符数目无上限.
注意!! 网上有些博客说, epoll中使用了内存映射机制
- 内存映射机制: 内核直接将就绪队列通过mmap的方式映射到用户态. 避免了拷贝内存这样的额外性能开销. 这种说法是不准确的. 我们定义的struct epoll_event是我们在用户空间中分配好的内存. 势必还是需要将内核的数据拷贝到这个用户空间的内存中的.
*Select与poll关于进程阻塞的内核原理
当创建socket进行通信时,socket对象内拥有一个"wait"字段,这就是当我们read / recv时的阻塞队列管理者。(进程pendding)

当select poll阻塞时其实是将当前进程链入到了(该套接字的)tesk_list因此在套接字均有一个wait等待队列,当进程select poll时,若无任何一个就绪那么"当前进程的PCB"就会被链入到wait。
当有100个sockfd那么就要添加100次,之后若某就绪了就又要更改100个的全部wait情况。拷贝、系统调用耗费了极大CPU资源。
但是select poll并未对此做到了像epoll般(红黑树 就绪队列)关键性的优化,因此导致了放大效应慢上加慢
问题
1.如何正确认识epoll_ctl 中的第三参数fd与第四参数中data的fd?
①第三参数fd:要进行op(增删改)的对象。
②第四参数data.fd:在次fd上关心的特定事件(EPOLLIN)就绪时携带fd(什么)回来。
如图在server端进行事件派发时必须判断是否是"新链接到来",这时候我们必须通过已经纳入POLLIN关心条件的server的_listenfd是否就绪(按位与)来进行首先判断☝️。(理解这点非常重要它涉及事件关心机制)

2.为什么就绪后,不进行判断_revts0.data.fd == _listenfd?而是_revtsi == _listenfd
结合上图理解
你将poll与epoll混淆了,所谓 0 或者 i 其验证对象是无序的(是由OS检测决断才纳入的)------一旦data.fd == _listenfd指fd无序化请求建立链接这样_listenfd才纳入了就绪队列--->通过epoll+wait输出到数组中。(这个问题非常好)
监视进程工具
bash
ss -tlnp | grep 8080 #检测进程情况 LISTEN状态
ss -tan state time-wait #查询TIME_WAIT信号进程 "文件描述符未关闭"
例如

感谢支持,长期连载
欢迎关注
