关于eventfd,epoll,线程间通信小记

先介绍eventfd

1 #include<sys/eventfd.h>
2 int eventfd(unsigned int initval, int flags);

使用这个函数来创建一个事件对象,linux线程间通信为了提高效率,大多使用异步通信,采用事件监听和回调函数的方式来实现高效的任务处理方式(虽然会将逻辑变得复杂)。

linux内核会为这个事件对象维护一个64位的计数器(uint64_t).并在初始化时用传进去的initval来初始化这个计数器,然后返回一个文件描述符来代表这个事件对象。

第二个参数是描述这个事件对象的属性,可以设置为EFD_NONBLOCK , EFD_CLOEXEC;前面的是设置对象为非阻塞状态,如果没有设置为非阻塞状态,read系统调用来读这个计数器,且计数器的值为0时,就会一直阻塞在read系统调用上,反之如果设置了该标志位,就会返回EAGAIN错误。后面的EFD_CLOEXEC功能是在程序调用exec()函数族加载其他程序时自动关闭当前已有的文件描述符(具体为什么暂不解释)。

通过此函数得到的对象既然是一个计数器,我们就可以对它进行读和写:

使用write将缓冲区写入的8字节整形值加到内核计数器上。

使用read将内核计数的8字节值读取到缓冲区中,并把计数器重设为0,如果buffer的长度小于8字节则read会失败,错误码设为EINVAl。

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

再介绍epoll,忍不住的可以直接向下翻

epoll是对select,poll这种IO多路转接方式的改进

接口:  int epoll_create(int intsize);

          int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);

          int epoll_wait(int epfd, struct epoll_event* events,int maxevents, int timeout);

工作模式:

  水平触发:缺省的工作方式,并且同时支持block和no-blocksocket,在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表

  边缘触发:高速工作方式,只支持no-blocksocket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了

用途:使用epoll_wait对某个文件描述符进行事件监听,监听到事件后会返回相关的结构体,得到其中有事件到来的fd,使用对应的回调函数(手动实现fd到回调函数的映射)来处理该fd上的事件:读数据或者写数据之类的。

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

下面进入使用场景:

原始做法:(会有bug,下面分析)

初始化:先生成一个eventfd,初始化计数器为0,此eventfd可以通过一些方法在下面两个线程间共享

线程A:处理一些来自外部的请求,每处理完一个请求后会向eventfd的计数器中写入处理的结果,是一个整型值,然后接着处理下一个请求。

线程B:对eventfd进行Epoll监听,回调函数的功能是对eventfd的计数器读数据出来并将结果进行分发。

用例1:外部单个客户端每隔1秒向线程A发送一个请求。

用例1结果:线程A正确处理请求,并将结果写入eventfd中,线程B及时从eventfd中读取出请求处理结果,并正确分发给其他线程。

用例2:外部单个客户端连续向线程A发送多个请求。

用例2结果:线程A正确处理请求,并正确地将结果写入eventfd中,但在一定概率的情况下,线程B从eventfd中读到的结果不是线程A一次写入的结果,而是多次写入的结果。因此不能正确的分发请求。线程B中epoll捕捉到的事件次数小于线程A写入产生的事件数量。

用例3:外部多个客户端同时向线程A发送一个请求

用例3结果:线程A正确处理请求,并正确的将结果写入eventfd中,在很大的概率情况下,线程B中eventfd中读到的结果不是线程A一次写入的结果,而是多次写入的结果。因此,也不能正确的分发请求。线程B中epoll捕捉到的事件次数小于线程A写入产生的事件数量。

BUG分析:在这个场景中,线程A和线程B分别相当于生产者和消费者,只从原始生产者消费者模型上看并没有问题,满足数据为空时读不到数据,数据满时写不进数据(read,write的功能),但是在当前场景中,加了一个特别的要求:每次写入的数据应该可以被独立识别而不是累加,每次写入的事件也应该被epoll独立的捕捉到。因此,需要对事件和数据各自进行序列化上的拆分。

改进做法:

初始化:先生成一个eventfd,初始化计数器为1,再生成一个空队列Q和互斥锁,此eventfd,队列Q和互斥锁可以通过一些方法在下面两个线程间共享,

线程A:处理一些来自外部的请求,每处理完一个请求后会从eventfd的计数器read数据,加1之后再write,将处理结果写入到队列末尾,然后接着处理下一个请求。

线程B:对eventfd进行Epoll监听,回调函数的功能是对eventfd的计数器read数据出来然后判断,如果大于1就自减1然后从队列头部取出数据,并将结果进行分发
,最后再写入新的计数器数据。如果等于1那么就直接返回,代表没有新的数据到来。

用例1,2,3在此环境下均可正常跑通。

回过头来分析原始做法的fatal error在哪:

作为生产者的线程A没有向线程B解释自己向eventfd中写入了多少个数据,产生了多少次事件。

作为消费者的线程B一次read就把eventfd中所有的数据当做一个数据读了出来,却没有相关依据来对读出来的数据做拆分。

作为通信工具的eventfd只能将数据进行累加,起到计数器的作用而不能存储实际数据。

作为消息监听的epoll在水平触发模式下只能通知是否有事件而不能通知有多少事件,在边缘触发下不能保留每次事件的产生都能及时被消费者捕获到。

因此,改进做法是将事件的多少通过计数器来表达,将实际传输的数据通过FIFO队列来传达。

Happy Ending Every Day.

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Samego开发资源

RAID磁盘阵列-Redundant Arrays of Independent Disks

1243
来自专栏刘明的小酒馆

Linux内核调度分析(进程调度)

本文是《Linux内核设计与实现》第四章的阅读笔记,代码则是摘自最新的4.6版本linux源码(github),转载请注明出处。

8549
来自专栏zhisheng

【死磕Java并发】—–Java内存模型之总结

经过四篇博客阐述,我相信各位对Java内存模型有了最基本认识了,下面LZ就做一个比较简单的总结。

833
来自专栏开发与安全

常见多线程与并发服务器设计方案举例

一、3点基础知识 1、一个主机的端口号为所有进程所共享,但普通用户进程绑定bind不了一些特殊端口号如20、80等。      多个进程不能同时监听listen...

21810
来自专栏java系列博客

深入理解Java内存模型(三)——顺序一致性

1212
来自专栏Vamei实验室

协议森林11 涅槃 (TCP重新发送)

TCP协议是一个可靠的协议。它通过重新发送(retransmission)来实现TCP片段传输的可靠性。简单的说,TCP会不断重复发送TCP片段,直到片段被正确...

1716
来自专栏JAVA高级架构

大型网站系统与 Java 中间件实践

第一章 分布式系统介绍 分布式系统的定义:组件分布在网络计算机上,组件间仅仅通过消息传递来通信并协调行动。 分布式系统的意义: 升级单机处理能力的性价比越来越...

3357
来自专栏Java技术

并发编程JMM系列之基础!

Java程序员在进行多线程开发时,并不需要关心线程间是如何通信的,这些对程序员本来来说完全是透明的,但是内存可见性问题很容易让我们困惑,今天我们就讲讲Java内...

602
来自专栏java一日一条

就是要你懂 Java 中 volatile 关键字实现原理

我们知道volatile关键字的作用是保证变量在多线程之间的可见性,它是java.util.concurrent包的核心,没有volatile就没有这么多的并发...

562
来自专栏架构师之旅

【Java SE】Java NIO系列教程(十二)Java NIO与IO

当学习了Java NIO和IO的API后,一个问题马上涌入脑海: 我应该何时使用IO,何时使用NIO呢?在本文中,我会尽量清晰地解析Java NIO和IO的差异...

1825

扫描关注云+社区