epoll三大接口到内核+正确认识应用层与内核的联系(6000字)

@bit::Shadow
✧(≖ ◡ ≖✿

目录

1.认识epoll

[1.0select poll回顾](#1.0select poll回顾)

1.1epoll_create

1.2epoll_ctl

[1.2.0struct epoll_event { :设置于内核中的关心事件体(结构)](#1.2.0struct epoll_event { :设置于内核中的关心事件体(结构))

1.3epoll_wait

2.epoll内核原理

3.内核代码中epoll的有关结构

[3.1struct eventpoll {](#3.1struct eventpoll {)

信号量(Semaphore)回顾

一、是什么

二、核心语义

[3.2struct epintem {](#3.2struct epintem {)

各字段

3.3fd回调机制

*Select与poll关于进程阻塞的内核原理

问题

[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

  1. 关心是否有fd就绪,仅检测就绪队列 中是否为空即可。O(1)
  2. 获取就绪时间,从就绪队列中获取。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信号进程 "文件描述符未关闭"

例如

感谢支持,长期连载

欢迎关注

相关推荐
皓月盈江1 小时前
Linux系统使用VSCode搭建GO开发环境
linux·vscode·ubuntu·golang·debian
captain3761 小时前
网络原理(4)-TCP
java·服务器·网络·网络协议·tcp/ip·java-ee
pt10431 小时前
Cisco Splunk for AI Operations:AIOps机器学习
运维·网络·人工智能·机器学习
CHHH_HHH1 小时前
【Linux系统篇】进程间通信揭秘:从管道到共享内存
linux·服务器·c语言·开发语言·后端·ubuntu
.冰块.1 小时前
华为云计算服务学习笔记|ECS + 镜像 IMS + 弹性伸缩 AS 完整踩坑实录
linux·华为云·ims·as·ecs·弹性云服务器·计算类云服务
AlanBruce3 小时前
摩尔信使MThings IEC 60870-5-104数据配置使用指南
网络·网络协议·tcp/ip
我爱吃土豆110 小时前
AI 编程工具远程开发指南:Codex 桌面版与 WorkBuddy 通过 SSH 连接云服务器实践
服务器·人工智能·ssh
一技安身10 小时前
【信创】银河麒麟V10、统信UOS解压7Z文件
linux·运维·服务器
奔跑的架构师11 小时前
[A-50]ARMv9/v8-DVFS系统架构
linux·arm开发·架构·系统架构·arm