事件驱动IO模式(图解+秒懂+史上最全)
文章很长,而且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 :
免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备
免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《尼恩Java面试宝典 最新版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取
特别说明:本文所属书籍已经更新啦,最新内容以书籍为准(书籍也免费送哦)
下面的内容,来自于《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事件的通知阶段,是异步的,但是,在将数据从内核缓冲区复制到用户缓冲区这个过程,用户进程是阻塞的、同步的。