事件驱动IO模式(图解+秒懂+史上最全)

Posted 架构师-尼恩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了事件驱动IO模式(图解+秒懂+史上最全)相关的知识,希望对你有一定的参考价值。

文章很长,建议收藏起来,慢慢读! Java 高并发 发烧友社群:疯狂创客圈 奉上以下珍贵的学习资源:


推荐:入大厂 、做架构、大力提升Java 内功 的 精彩博文

入大厂 、做架构、大力提升Java 内功 必备的精彩博文2021 秋招涨薪1W + 必备的精彩博文
1:Redis 分布式锁 (图解-秒懂-史上最全)2:Zookeeper 分布式锁 (图解-秒懂-史上最全)
3: Redis与MySQL双写一致性如何保证? (面试必备)4: 面试必备:秒杀超卖 解决方案 (史上最全)
5:面试必备之:Reactor模式6: 10分钟看懂, Java NIO 底层原理
7:TCP/IP(图解+秒懂+史上最全)8:Feign原理 (图解)
9:DNS图解(秒懂 + 史上最全 + 高薪必备)10:CDN图解(秒懂 + 史上最全 + 高薪必备)

Java 面试题 30个专题 , 史上最全 , 面试必刷阿里、京东、美团… 随意挑、横着走!!!
1: JVM面试题(史上最强、持续更新、吐血推荐)2:Java基础面试题(史上最全、持续更新、吐血推荐
3:架构设计面试题 (史上最全、持续更新、吐血推荐)4:设计模式面试题 (史上最全、持续更新、吐血推荐)
17、分布式事务面试题 (史上最全、持续更新、吐血推荐)一致性协议 (史上最全)
29、多线程面试题(史上最全)30、HR面经,过五关斩六将后,小心阴沟翻船!
9.网络协议面试题(史上最全、持续更新、吐血推荐)更多专题, 请参见【 疯狂创客圈 高并发 总目录

SpringCloud 精彩博文
nacos 实战(史上最全) sentinel (史上最全+入门教程)
SpringCloud gateway (史上最全)更多专题, 请参见【 疯狂创客圈 高并发 总目录

特别说明:本文所属书籍已经更新啦,最新内容以书籍为准(书籍也免费送哦)

下面的内容,来自于《Java高并发核心编程卷1》一书,此书的最新电子版,已经免费赠送,大家找尼恩领取即可。

而且,《Java高并发核心编程卷1》的电子书,会不断优化和迭代。最新一轮的迭代,增加了 消息驱动IO模型的内容,这是之前没有的,使得在 Java NIO 底层原理这块,书的内容变得非常全面。
另外,如果出现内容需要更新,到处要更新的话,工作量会很大,所以后续的更新,都会统一到电子书哦。

信号驱动IO的简介

在信号驱动IO模型中,用户线程通过IO事件的回调函数注册,来避免IO时间查询的阻塞。

具体的做法是,用户进程预先在内核中设置一个回调函数,当某个事件发生时,内核使用信号(SIGIO)通知进程运行回调函数。 然后用户线程会继续执行,在信号回调函数中调用IO读写操作来进行实际的IO请求操作。

信号驱动IO的基本流程

信号驱动IO的基本流程是:

用户进程通过系统调用,向内核注册SIGIO信号的owner进程和以及进程内的回调函数。内核IO事件发生后(比如内核缓冲区数据就位)后,通知用户程序,用户进程通过read系统调用,将数据复制到用户空间,然后执行业务逻辑。

信号驱动IO模型,每当套接字发生IO事件时,系统内核都会向用户进程发送SIGIO事件,所以,一般用于UDP传输,在TCP套接字的开发过程中很少使用,原因是SIGIO信号产生得过于频繁,并且内核发送的SIGIO信号,并没有告诉用户进程发生了什么IO事件。

但是在UDP套接字上,通过SIGIO信号进行下面两个事件的类型判断即可:

1 数据报到达套接字

2 套接字上发上一部错误

因此,在SIGIO出现的时候,用户进程很容易进行判断和做出对应的处理:如果不是发生错误,那么就是有数据报到达了。

事件注册的步骤

举个例子。发起一个异步IO的read读操作的系统调用,流程如下:

(1)设置SIGIO信号的信号处理回调函数。

(2)设置该套接口的属主进程,使得套接字的IO事件发生时,系统能够将SIGIO信号传递给属主进程,也就是当前进程。

(3)开启该套接口的信号驱动I/O机制,通常通过使用fcntl方法的F_SETFL操作命令,使能(enable)套接字的 O_NONBLOCK非阻塞标志和O_ASYNC异步标志完成。

完成以上三步,用户进程就完成了事件回调处理函数的设置。当文件描述符上有事件发生时,SIGIO 的信号处理函数将被触发,然后便可对目标文件描述符执行 I/O 操作。

关于以上三步的详细介绍,具体如下:

第一步:

设置SIGIO信号的信号处理回调函数。Linux中通过 sigaction() 来完成。参考的代码如下:


   // 注册SIGIO事件的回调函数

   sigaction(SIGIO, &act, NULL); 

sigaction函数的功能是检查或修改与指定信号相关联的处理动作(可同时两种操作),函数的原型如下:


int sigaction(int signum, const struct sigaction *act,

                     struct sigaction *oldact);

对其中的参数说明如下:

1 signum参数指出要捕获的信号类型

2 act参数指定新的信号处理方式

3 oldact参数输出先前信号的处理方式(如果不为NULL的话)。

该函数是Linux系统的一个基础函数,不是为信号驱动IO特供的。在信号驱动IO的使用场景中,signum的值为常量 SIGIO。

第二步:

设置该套接口的属主进程,使得套接字的IO事件发生时,系统能够将SIGIO信号传递给属主进程,也就是当前进程。属主进程是当文件描述符上可执行 I/O 时,会接收到通知信号的进程或进程组。

为文件描述符的设置IO事件的属主进程,通过 fcntl() 的 F_SETOWN 操作来完成,参考的代码如下:


fcntl(fd,F_SETOWN,pid)

当参数pid 为正整数时,代表了进程 ID 号。当参数pid 为负整数时,它的绝对值就代表了进程组 ID 号。

第三步:

开启该套接口的信号驱动IO机制,通常通过使用fcntl方法的F_SETFL操作命令,使能(enable)套接字的 O_NONBLOCK非阻塞标志和O_ASYNC异步标志完成。参考的代码如下:

int flags = fcntl(socket_fd, F_GETFL, 0);

    flags |= O_NONBLOCK;  //设置非阻塞

    flags |= O_ASYNC;    //设置为异步

    fcntl(socket_fd, F_SETFL, flags );

这一步通过 fcntl() 的 F_SETFL 操作来完成,O_NONBLOCK为非阻塞标志,O_ASYNC为信号驱动 I/O的标志。

使用事件驱动IO进行UDP通信应用的开发,参考的代码如下(C代码):


int socket_fd = 0;

 

//事件的处理函数

void do_sometime(int signal) {

    struct sockaddr_in cli_addr;

    int clilen = sizeof(cli_addr);

    int clifd = 0;

 

    char buffer[256] = {0};

    int len = recvfrom(socket_fd, buffer, 256, 0, (struct sockaddr *)&cli_addr,

                       (socklen_t)&clilen);

    printf("Mes:%s", buffer);

    

    //回写

    sendto(socket_fd, buffer, len, 0, (struct sockaddr *)&cli_addr, clilen);

}

 

int main(int argc, char const *argv[]) {

    socket_fd = socket(AF_INET, SOCK_DGRAM, 0);

    struct sigaction act;
    act.sa_flags = 0;

    act.sa_handler = do_sometime;

    // 注册SIGIO事件的回调函数
    sigaction(SIGIO, &act, NULL); 

    struct sockaddr_in servaddr;

    memset(&servaddr, 0, sizeof(servaddr));

    servaddr.sin_family = AF_INET;

    servaddr.sin_port = htons(8888);

    servaddr.sin_addr.s_addr = INADDR_ANY;

 

    //第二步为文件描述符的设置 属主

    //设置将要在socket_fd上接收SIGIO的进程

    fcntl(socket_fd, F_SETOWN, getpid());

 

    //第三步:使能套接字的信号驱动IO

    int flags = fcntl(socket_fd, F_GETFL, 0);

    flags |= O_NONBLOCK;  //设置非阻塞

    flags |= O_ASYNC;    //设置为异步

    fcntl(socket_fd, F_SETFL, flags );

 

    bind(socket_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    while (1) sleep(1); //死循环

    close(socket_fd);

    return 0;

}

当套件字的IO事件发生时,回调函数被执行,在回调函数中,用户进行执行数据复制即可。

信号驱动IO优势:

用户进程在等待数据时,不会被阻塞,能够用户进程的效率。具体来说:在信号驱动式I/O模型中,应用程序使用套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。

信号驱动IO缺点:

1 在大量IO事件发生时,可能会由于处理不过来,而导致信号队列溢出。

2 对于处理UDP套接字来讲,对于信号驱动I/O是有用的。可是,对于TCP而言,由于致使SIGIO信号通知的条件为数众多,进行IO信号进一步区分的成本太高,信号驱动的I/O方式近乎无用。

3 信号驱动IO可以看成是一种异步IO,可以简单理解为系统进行用户函数的回调。但是,信号驱动IO的异步特性,又做的不彻底。信号驱动IO仅仅在IO事件的通知阶段,是异步的,但是,在将数据从内核缓冲区复制到用户缓冲区这个过程,用户进程是阻塞的、同步的。

以上是关于事件驱动IO模式(图解+秒懂+史上最全)的主要内容,如果未能解决你的问题,请参考以下文章

Nginx 事件驱动模型 (秒懂+史上最全)

红黑树(图解+秒懂+史上最全)

MappedByteBuffer 详解(图解+秒懂+史上最全)

DNS图解(秒懂 + 史上最全)

rocket高可用 (图解+秒懂+史上最全)

Docker原理(图解+秒懂+史上最全)