epoll在多线程中的应用-EPOLLEXCLUSIVE和REUSEPORT(一)

2023-11-17

以下均为对epoll多线程中的使用的一些笔记,如果有不对的地方,烦请指出

主要对于我所遇到的问题进行讨论,不会讨论代码如何改写,探讨如何解决这个问题

一.引言

这些问题均是我在编写我的Web服务器遇到的,我在编写多线程Web服务器的时候,思考如何利用多核的优势来编写Web服务器.在学习了muduo网络库之后,我的先前一个版本的Web服务器采用这种方式,一个master线程+多个工人线程,但是我觉得在高并发的情况下只有一个线程可以accept这无疑限制了accept吞吐量,并不算利用多核优势.进而引发了我对于在多线程中如何高效合理的使用epoll有了探索.

二.epoll file descriptor 和 kernel file description 生命周期的问题

`谈及这个问题,我们需要了解
  • (1) 进程维护file descriptor表 ,每个fd包含
  • fd 标志
  • 指向内核的file descriptor表象的指针
  • (2)内核维护所有打开文件的file description表,每一个表都包含文件的状态标志
  • 当前文件的offest
  • 文件状态标志(读,写,阻塞,非阻塞)
  • 指向该文件v节点

每个进程的task_struct 包含了用于完全应该工作的成员

struct task_struct {
	//文件系统信息
	int link_count , total_link_count;
	---
	struct files_struct * files; //打开的文件信息
	---
}

struct files_struct {

	-----
	atomic_t count; //引用技术
}

图片来源
上述图片来源

  • 在用户态使用close() 减少一次count , 当count 为0,才会从内核的file description 删除

  • 实际上,epoll( ) 主要是混淆了用户态的file descriptor和内核态中真正用于实现的 file description . 当进程调用close 关闭fd ,就会出现问题,也就是说我们在调用的时候,传入的是用户态的 file descriptor ,也就是平时我们所用的fd那个数字,但是在内核中,引用的是file description ,就是那个内核对象

  • epoll_ctl(EPOLL_CTL_ADD) 实现上并不是注册一个file descriptor(fd) , 而是将fd 和一个指向内核file description 的指针一块注册给了epoll ,也就是说epoll中管理的fd 生命周期,是内核中的相对应的file descriptor

我们可以查看最权威的man手册

Q6 Will closing a file descriptor cause it to be removed from all epoll sets automatically?
A6 Yes, but be aware of the following point. A file descriptor is a reference to an open file
description (see open(2)). Whenever a descriptor is duplicated via dup(2), dup2(2), fcntl(2)
F_DUPFD, or fork(2), a new file descriptor referring to the same open file description is created.
An open file description continues to exist until all file descriptors referring to it have been
closed. A file descriptor is removed from an epoll set only after all the file descriptors refer-
ring to the underlying open file description have been closed (or before if the descriptor is
explicitly removed using epoll_ctl() EPOLL_CTL_DEL). This means that even after a file descriptor
that is part of an epoll set has been closed, events may be reported for that file descriptor if
other file descriptors referring to the same underlying file description remain open.

同时也就是说当我们使用close去关闭掉一个fd 的时候,如果这个fd 是内核中file description的唯一引用
,内核中的 file description 同时也会跟着一起删除,这样没有什么问题,但是如果有其他引用,close并不会删除这个file descrption. 这样epoll会继续上报这个已经close掉的fd 上的事件,并且此时上次的应用程序看到的现象是关闭了fd ,但是 epoll_wait 会不断返回关闭的fd的事件信息,更为头疼的是fd已经被关闭了,我们无法通过epoll_ctl去从集合中删除掉这个fd

从Marek的博客中的代码中得到

rfd, wfd = pipe()
write(wfd, "a")             # Make the "rfd" readable
epfd = epoll_create()
epoll_ctl(efpd, EPOLL_CTL_ADD, rfd, (EPOLLIN, rfd))

rfd2 = dup(rfd)
close(rfd)

r = epoll_wait(epfd, -1ms)  # still recv event!!!

所以我们应该在close之前首先调用epoll_ct在epoll中删除掉相应的fd

三.特定的TCP listen fd 的accept 的问题

这个问题是我思索很久的问题,如果我的web服务器在某种场景上,需要应对大量的短连接,那么我会想如何把accpet()分发到不同的CPU 上,来利用多核的能力.

1)首先我想到的方案是所有线程共用一个isten fd每一个线程创建epollfd ,把listen fd加入到所有线程的epoll 中.

  • 一开始发现这种情况会引发惊群问题,如果有新的连接,会唤醒所有的线程,但是只有一个线程可以accpet成功,其他线程会进行失败
  • 但是我发现这种形式效率并不低,按道理来说惊群应该会降低效率,但是为什么会增加效率?
  • 经过查询资料和自己的测试,可以这麽理解,如果说线程是一堆小鸡的话,在饲料不多的情况下,是需要一些小鸡进行休息的,但是在饲料很多的情况下,如何让饲料更快的消耗?当然是让所有小鸡同时进行饲料的竞争

2)这样说的话,将所有的线程共用一个epollfd也会引起惊群问题

  • 在尝试这个方法的同时,我想到了边缘触发会不会更好处理这个问题?
  • 但是并没有,我们可以来看看下面这个例子
  1. 内核:收到第一个连接请求。线程 A 和 线程 B 两个线程都在 epoll_wait() 上等待。由于采用边缘触发模式,所以只有一个线程会收到通知。这里假定线程 A 收到通知
  2. 线程A:epoll_wait() 返回
  3. 线程A:调用 accpet() 并且成功
  4. 内核:此时 accept queue 为空,所以将边缘触发的 socket 的状态从可读置成不可读
  5. 内核:收到第二个建连请求
  6. 内核:此时,由于线程 A 还在执行 accept() 处理,只剩下线程 B 在等待 epoll_wait(),于是唤醒线程 B
  7. 线程A:继续执行 accept() 直到返回 EAGAIN
    .8. 线程B:执行 accept(),并返回 EAGAIN,此时线程 B 可能有点困惑(“明明通知我有事件,结果却返回 EAGAIN”)
    .9. 线程A:再次执行 accept(),这次终于返回 EAGAIN
  • 当然也有可能会造成线程饥饿的问题,我们来看看下面这个例子
  1. 内核:接收到两个建连请求。线程 A 和 线程 B 两个线程都在等在 epoll_wait()。由于采用边缘触发模式,只有一个线程会被唤醒,我们这里假定线程 A 先被唤醒
  2. 线程A:epoll_wait() 返回
  3. 线程A:调用 accpet() 并且成功
  4. 内核:收到第三个建连请求。由于线程 A 还没有处理完(没有返回 EAGAIN),当前 socket 还处于可读的状态,由于是边缘触发模式,所有不会产生新的事件
  5. 线程A:继续执行 accept() 希望返回 EAGAIN 再进入 epoll_wait() 等待,然而它又 accept() 成功并处理了一个新连接
  6. 内核:又收到了第四个建连请求
  7. 线程A:又继续执行 accept(),结果又返回成功

线程A很忙但是线程B没有活干

3)说这多,那什么是正确的做法?

从epoll的角度来考虑的话,有两种做法

  • 最好也是唯一支持的是从内核4.5+开始新增的水平触发模式的EPOLLEXCLUSIVE标志,这个标志会保证一个事件只有一个epoll_wait()会被唤醒,避免了"惊群效应",并且可以完美的在多个CPU之间进行扩展
  • 那么EPOLLEXCLUSIVE为什么可以做到这一点? 有这个标志位的fd,会唤醒线程中的空闲队列的头一个
  • 当然内核不够4.5怎么办?我们可以通过ET下的EPOLLONESHOT 来模拟 LT+ EPOLLEXCLUSIVE的效果
    这样是有一定的代价的,需要在每次事件处理完成之后额外多调用一次 epoll_ctl(EPOLL_CTL_MOD) 重置这个 fd。这样做可以将负载均分到不同的 CPU 上,但是同一时刻,只能有一个 worker 调用 accept(2)。显然,这样又限制了处理 accept(2) 的吞吐。

4)当然我们也有其他方案(SO_REUSEPORT)

  • 我们可以使用SO_REUSEPORT这个socket option ,创建多个listen socket 共用一个端口号
  • SO_REUSEPORT在TCP连接中是通过内核来选取套接字来进行接受连接
  • SO_REUSEPORT有两种模式,热备份和负载均衡模式,当然在3.9之后,全部是负载均衡模式
  • SO_REUSEPORT当有连接到来时,用数据包的源IP/源端口作为一个HASH函数的输入,将结果对reuseport套接字数量取模,得到一个索引,该索引指示的数组位置对应的套接字便是工作套接字。
  • 但是也有弊端,就是一个listen socket  fd 被关,被分到这个listen socket fd 的accept队列上的东西会被丢弃掉
  • SO_REUSEPORT的好处:解决了epoll惊群问题,程序有了更好的扩展性,只有在同一个用户下相同的服务器进程才能监听同一ip:port (安全性考虑)
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

epoll在多线程中的应用-EPOLLEXCLUSIVE和REUSEPORT(一) 的相关文章

  • 多线程编程技巧

    java中 多线程类需要继承Thread或实现Runnable接口 在Run函数中执行多线程代码 但是需要用Start 函数开始执行 多线程并行执行 执行的顺序取决于本地操作系统给谁分配系统资源 Runnable共享资源的方法 a 如果每个
  • 线程-Linux下的轻量级进程

    首先我们知道 每个进程都是在各自独立的地址空间上运行 如果要同时完成好几个任务 比如你一边在下载软件 另一边在进行着其他的操作 那么试想一下 可不可以在一个进程里面把这几个事件同时进行呢 这里就要提到线程的概念了 但其实Linux中 并没有
  • 多线程、反射

    目录 线程 实现线程 死锁 反射 注解 多线程 在Java思想中 将并发完成的每一件事情称为线程 java语言提供了并发机制 程序猿可以在程序中执行多个线程 每一个线程完成一个功能 并与其他线程并发执行 这种机制称为多线程 一个简单的线程代
  • 开启hyper-v的嵌套虚拟化

    有的时候会用到windows自带的hyper v来测试一些系统 如果安装的虚拟机需要启用虚拟化原来是一件很麻烦的事情 现在有个不错的powershell命令执行一下就好 Set VMProcessor VMName NestedVM Exp
  • C#学习笔记 异步操作

    同步操作 默认情况下我们的代码都是同步操作 这种情况下 所有的操作都在同一个线程中 如果遇到需要长时间执行的操作或者是一个IO操作 那么代码可能会阻塞比较长的时间 在阻塞的这段时间里 无法进行其他工作 这是很不好的 这里是一个同步操作的例子
  • QT多线程(QThread)小结

    QThread只有run函数是在新线程里的 其他所有函数都在QThread生成的线程里 如果QThread是在ui所在的线程里生成 那么QThread的其他非run函数都是和ui线程一样的 所以 QThread的继承类的其他函数尽量别要有太
  • 利用ScheduledThreadPoolExecutor定时执行任务

    最近时间好忙 终于抽出时间来写一篇博客了 想了想 把之前遇到的一个小bug分享一下吧 之前在做时钟插件时候 我用到了一个定时器 即大家常用的TimerTask 但它总是意外的停止 就是我开启了一个定时器 每一秒都会走run方法 有时候定时器
  • Java中的wait()与notify()/notifyAll()

    1 wait 与sleep yield 的不同 调用sleep 和yield 时 线程掌握的对象上的锁不会被释放 而调用wait 时 线程掌握的对象上的锁会被释放掉 这一点是需要注意的 也是有意义的 因为调用wait 会释放锁 所以在一个s
  • Chromium OS初体验 就是一款Linux

    好奇 弄了一个Chromium OS for VMWare 玩玩 发现Chromium OS并非像我之前想象的一样 并非完全是一个自主研发的独立操作系统 启动 Chromium OS 时 vmware 被设置成图形模式 但一片漆黑什么都看不
  • Linux的进程管理

    目录 1 概述 2 进程描述符 2 1 进程描述符的分配 2 2 进程描述符的存放 2 3 进程状态 2 4 进程上下文 2 5 进程家族树 3 进程的创建 4 进程的终结 5 线程的实现 1 概述 进程是执行期的代码 但是进程不止包括这样
  • 深入理解计算机系统(原书第三版)系列 第十一章 网络编程

    第十一章 网络编程
  • 《实现VM机与本机互ping》

    实现VM机与本机互ping 1 安装VM机及对应的OS gt 到本机检测VMware Network Adapter VMnet8是否已经生成 网上邻居 网络连接 2 查看 VMware Network Adapter VMnet8 网络I
  • jemalloc原理分析

    jemalloc原理分析 转载自http club alibabatech org article detail htm articleId 36 首先介绍一下jemalloc中的几个核心概念 1 arena jemalloc的核心分配管理
  • 多线程的一些小问题集锦

    1 线程死亡之后不能再次启动 测试线程是否已经死亡 可以调用isAlive 方法 当线程处于就绪 运行 阻塞三种状态时 返回true 当线程处于死亡 新建状态时返回false 实例代码如下 package com thread public
  • 进程,线程,协程总结

    进程 三种状态 就绪态 运行的条件都已经慢去 正在等在cpu执行 执行态 cpu正在执行其功能 等待态 等待某些 条件满足 例如一个程序sleep了 此时就处于等待态 生命周期 用户编写代码 代码本身是以进程运行的 启动程序 进入进程 就绪
  • 计算机操作系统-进程篇

    一 进程 进程 progress 是指计算机中已运行的程序 每个进程都有自己的地址空间 内存 寄存器和堆栈等资源 它们与其他进程相互隔离 互不干扰 进程是操作系统中最基本的资源分配单位 也是操作系统中最重要的概念之一 在操作系统中 进程是由
  • VMWare 6.5.3 绿色精简版汉化 +VMware Workstation 6.5.3 Build 185404 汉化绿色精简版

    绿色精简版 参考网上6 5 X几个绿色精简版更新制作 bat不加密 不加入个人信息 喜欢研究的随便看 精简版一般使用够用了 高手估计会觉得缺少某些功能了 那就只能装完整版了 bridge 桥接 usb服务 host only都可以使用 VM
  • C#学习笔记 线程操作

    完整代码在这里 https github com techstay csharp learning note 创建并使用线程 使用线程执行任务 要创建一个线程很简单 实例化一个System Threading Thread对象并向其构造函数
  • 关于tomcat繁忙线程数获取

    在某些情况下 我们需要对tomcat的繁忙线程数进行监控以满足我们队应用服务器状态信息的把控 那么我们该如何通过我们自定义的接口来获得tomcat的繁忙线程数 首先 我们应该想到tomcat本身是否为我们提供了类似的方法 博主在实际开发中拜
  • QT在子线程发送信号给主线程,主线程信号槽函数没有反应的解决办法

    在编写线程时遇到了一个问题 即子线程发送信号给主线程 主线程信号槽函数没有反应 这个问题卡了半天 最终找到解决办法 自己记录一下 问题 在子线程的run函数发送了一个信号 在主函数中定义了一个信号槽函数用来响应这个信号 但是槽函数不执行 修

随机推荐