热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

LinuxI/O与select、poll、epoll

一、LinuxI/O输入/输出(I/O)是指在主存和外部设备

一、Linux I/O

输入/输出(I/O)是指在主存和外部设备之间复制数据的过程。输入是设备到主存,输出是主存到设备。在Linux系统中,所有的I/O设备都被模型化为文件,所有的输入输出则是对应文件的读写操作。应用程序要求内核打开一个文件,即访问一个I/O设备,而内核则返回一个非负整数,成为文件描述符,用于标识该文件。Linux系统中,文件分为三种,普通文件、目录、套接字。

  1. 非缓存I/O与标准I/O
    (1)非缓存I/O
    Linux提供read和write系统调用,它们在用户空间中没有缓冲区,但在内核中有缓冲区。当执行一个write,数据写入内核缓冲区,缓冲区满后再写入到文件。
    (2)标准I/O
    也就是再用户层面存在缓冲区,进行写操作时,数据先写入标准I/O库的流缓冲区,写满后,调用write,将数据复制到内核缓冲区,再写入文件。流缓冲区的大小和分配空间由标准I/O库执行。
    标准I/O的流缓冲区的目的是减少read和write的系统调用次数。假设内核缓冲区长100字节,每次写入10个字节,每次写入都要调用一次write。采用标准I/O,假设流缓冲区大小为50字节,每次写满后再调用write将数据写入。
    实际上标准I/O为每个I/O流提供了缓存管理,共有3种类型的缓存:
    a、 全缓存。当流缓冲区写满后执行I/O操作
    b、 行缓存。输入输出遇到换行符或者缓冲区写满时执行I/O操作。
    c、 无缓存。相当于read和write。

  2. I/O模式
    考虑read或write系统调用时,数据实际上经历了两个过程。以read为例,首先等待数据准备完成;然后将准备好的数据拷贝到进程。根据这两个阶段,Linux系统中存在以下5种I/O模型。

(1) 阻塞式I/O
Linux中,所有套接字默认情况下都是阻塞的。如下图所示,当进行recvfrom系统调用时,内核进行数据准备,并将数据从内核复制到用户空间,然后recvfrom返回。整个过程中进程都是阻塞的,直到recvfrom返回。在这里插入图片描述

(2) 非阻塞式I/O
Linux支持将socket设置成非阻塞的,即告诉内核,如果当前请求的I/O操作必须将进程休眠,那么不要将进程休眠,而是返回一个错误。如下图,前三次recvfrom调用时,没有数据准备好,recvfrom直接返回一个EWOULDBLOCK错误;第四次调用时,数据已准备好,内核进行数据复制,recvfrom成功返回。在这个过程中,进程需要循环调用recvfrom,以访问内核是否有数据准备好,也称为轮询。不过这样会浪费大量CPU时间。

在这里插入图片描述

(3)I/O复用
I/O复用,即一个进程能够处理多个I/O,也就是select、poll、epoll这三个系统调用的功能了。在没有I/O调用时,进程阻塞在select而非真正的系统调用上;当有socket的数据准备好了,select就会返回,通知进程调用read。

在这里插入图片描述

(4)信号驱动式I/O
首先需要开启socket的信号驱动I/O功能,并通过sigaction系统调用,安装一个信号处理函数,这个系统调用会直接返回,不阻塞进程。当数据准备好时,内核产生一个SIGIO信号,信号处理函数捕捉到这个信号,并在其中调用recvfrom。
在这里插入图片描述

(5)异步I/O
异步I/O的机制是,进程告知内核进行某个操作,令内核在操作完成后,通过递交信号通知进程操作已完成。如图所示,进程调用aio_read(POSIX),然后立即返回,并不阻塞;内核进行I/O操作,并在完成时递交一个信号。异步I/O与信号驱动I/O的区别是,异步I/O在操作完成时递交信号通知,而信号驱动I/O由内核通知何时开始一个操作。
在这里插入图片描述

二、I/O复用

1.select

函数原型:

#include
#include
int select(int maxfdg1, fd_set * readset, fd_set *writeset , fd_set *exceptset, const struct timeval *timeout);

-maxfdp1为制定的待测试的描述符个数,它的值是待测试的最大描述符-1。
-readset、writeset、exceptset三个参数是指定让内核测试读、写和异常条件的描述符。支持的异常条件有:某个套接字的带外数据到达;某个已置为分组模式的伪终端存在可从其主端读取的控制状态信息。Select使用描述符集,通常是一个整数数组,其中每个整数的每一位对应一个描述符。当描述集中有描述符就绪时,select返回就绪描述符。
-timeout参数告知内核等待所指定描述符中的任何一个就绪可花多长时间,结构体形式为:

struct timeval{
long tv_sec; /*seconds*/
long tv_usec; /*microseconds*/
}

2.poll

函数原型:

#include
int poll(struct pollfd *fdarray,unsigned long nfds, int timeout);

-*fdarray参数是一个指向一个结构数组第一个元素的指针,每个元素都是一个pollfd结构,用于指定测试某个给定描述符fd的条件。要测试的条件由events成员指定,并在revents中返回描述符的状态。

struct pollfd{
int fd;
short events;
short revents;
}

-nfds指定了结构数组中元素的个数。
-timeout参数指定了poll函数返回前等待多长时间。(ms)

下表中是一些events、revents标志的常值和含义。

EventRevents说明
POLLIN普通或优先数据可读
POLLRDNORM普通数据可读
POLLRDBAND优先数据可读
POLLPRI高优先级数据可读
POLLOUT普通数据可写
POLLWRNORM普通数据可写
POLLWRBAND优先数据可写
POLLERR发生错误
POLLHUP发生挂起
POLLNVAL描述符不是一个打开的文件
POLLRDHUPTCP连接被对方关闭,或者对方关闭了写操作(是Linux的非协议扩展)

注:关于POLLHUP与POLLRDHUP的区别,见下文:
Q:
According to the poll man page, the poll function can return POLLHUP and POLLRDHUP events. From what I understand, only POLLHUP is POSIX compliant, and POLLRDHUP is a Linux non-standard extension. Howerver, both seem to signal that the write end of a connection is closed, so I don’t understand the added value of POLLRDHUP over POLLHUP. Would someone please explain the difference between the two?
A:
No, when poll()ing a socket, POLLHUP will signal that the connection was closed in both directions.
POLLRDHUP will be set when the other end has called shutdown(SHUT_WR) or when this end has called shutdown(SHUT_RD), but the connection may still be alive in the other direction.
You can have a look at net/ipv4/tcp.c the kernel source:

if (sk->sk_shutdown == SHUTDOWN_MASK || state == TCP_CLOSE)
mask |= EPOLLHUP;
if (sk->sk_shutdown & RCV_SHUTDOWN)
mask |= EPOLLIN | EPOLLRDNORM | EPOLLRDHUP;

SHUTDOWN_MASK is RCV_SHUTDOWN|SEND_SHUTDOWN. RCV_SHUTDOWN is set when a FIN packet is received, and SEND_SHUTDOWN is set when a FIN packet is acknowledged by the other end, and the socket moves to the FIN-WAIT2 state.

[except for the TCP_CLOSE part, that snippet is replicated by all protocols; and the whole thing works similarly for unix sockets, etc]

There are other important differences – POLLRDHUP (unlike POLLHUP) has to be set explicitly in .events in order to be returned in .revents.

And POLLRDHUP only works on sockets, not on fifos/pipes or ttys

3.epoll

epoll是Linux特有的I/O复用函数,与select和poll不同的是,epoll采用一组函数来实现功能,并且epoll将调用者关心的描述符事件维护在内核的一个事件表中,因此不需要传入描述符集;但需要传入一个标识时间表的描述符。

1、epoll_create

#include
int epoll_create(int size)

-size告诉内核需要的事件表的大小
-返回值为文件描述符,作为其他epoll系统调用的第一个参数,

2、epoll_ctl

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

-epfd内核事件表的描述符
-op为指定的操作类型,包含3种:
EPOLL_CTL_ADD: 向注册表中注册fd上的事件
EPOLL_CTL_MOD: 修改fd上的注册事件
EPOLL_CTL_DEL: 删除fd上的注册事件
-fd 为要操作的文件描述符
-*event为指定的事件,是epoll_event结构体指针

struct epoll_event{
__uint32_t events; /*epoll事件*/
epoll_data_t data; /*用户数据*/
}
typedef union epoll_data{
void* ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t

3、epoll_wait
该系统调用在一段超时时间内等待一组文件描述符上的事件。

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

-epfd 内核事件表的描述符
-*events 所有就绪的事件将从内核事件表复制到该指针指向的数组中。
-maxevents 指定最多监听多少事件
-timeout参数指定了epoll_wait函数返回前等待多长时间。(ms)

/*使用poll返回就绪描述符*/
int ret = poll(fds, MAX_EVENT_NUMBER, -1);
for(int i = 0; I < MAX_EVENT_NUMBER;i++){
if(fds[i].revents & POLLIN){
int sockfd=fds[i].fd;
/*处理socket*/
}
}
/*使用epoll返回的就绪描述符*/
int ret =epoll_wait(epollfd, events, MAX_EVENT_NUMBER,-1);
for(int i = 0; I < ret ;i++){
if(fds[i].revents & POLLIN){
int sockfd=events[i].data.fd;
/*处理socket*/
}
}

Epoll对文件描述符的操作有两种模式,Level Trigger和Edge Trigger。LT为默认的工作模式,相当于效率较高的poll,当事件发生时,应用程序可以不立即处理事件,当下次调用epoll_wait时,会再次通告该事件;ET模式下,应用程序需要立即处理epoll_wait检测到的事件,因为后续epoll_wait不会再次通知重复事件。

Epoll相较于poll,新增了EPOLLET和EPOLLONESHOT两个事件。当epoll为一个文件描述符注册EPOLLET事件时,epoll会以ET模式操作该文件描述符。对于注册了EPOLLONESHOT的文件描述符,操作系统最多触发其上注册的一个可读、可写或异常事件,且只触发一次。这样一个线程对一个socket操作时,不会有其他线程同时操作该socket。不过,当第一个线程对该socket完成操作时,应立即重置EPOLLONESHOT,以保证该socket在下次可读时能够正常触发EPOLLIN事件。

/*将fd上的EPOLLIN和EPOLLET事件注册到事件表中,EPOLLONESHOT 注册与否则取决于oneshot的值*/
void addfd(int epollfd, int fd ,bool oneshot){
epoll_event event;
event_data.fd = fd;
event.events = EPOLLIN;
if(oneshot){
event.events |= EPOLLONESHOT;
}
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
setnonblocking(fd);
}
/*重置fd上的事件*/
void reset_oneshot(int epollfd, int fd){
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
epoll_ctl(epollfd,EPOLL_CTL_MOD, FD, &event);
}
三、select、poll、epoll对比

select、poll、epoll这三个I/O复用系统调用,都能够实现同时监听多个文件描述符的功能,在timeout时间范围内,等待一个或多个文件描述符上的事件,当事件发生时返回。但是从多个方面看,这三个系统调用存在着差异。

1、事件集合
select:通过维护可读、可写、异常三个事件的文件描述符集合,因此select不能处理更多类型的事件;由于内核对描述符集合进行在线修改,应用程序进行下次select调用前,需要重置这三个描述符集合;

poll:将每个描述符与事件绑定,定义一个pollfd结构,内核进行修改的是revents变量,events变量不会被修改,也就是说下次调用poll时,进程无需对pollfd的参数进行重置。

epoll:在内核中维护一个事件表,通过一个独立的系统调用epoll_ctl来对事件表进行添加修改删除。这样epoll_wait每次都从事件表中读取注册的事件,而非反复从用户空间读入事件。

由于select和poll每次调用均需要返回整个注册事件表的集合,因此进程索引就绪文件描述符的时间复杂度为O(n);而epoll_wait的events参数仅用来返回就绪的事件,因此进程索引就绪文件描述符的时间复杂度为O(1)。

2、描述符数量
poll和epoll_wait分别通过nfds和maxevents参数来指定最多监听多少个文件描述符和事件,这两个数值都能达到系统允许打开的最大文件描述符数目,即65535。

而select允许监听的最大文件描述符数量通常有限制。在最初设计select时,操作系统通常对每个进程可用的最大描述符进行了限制;但在现在的Unix版本中,允许每个进程使用事实上无限数目的描述符,具体数量的限制来自于内存总量和管理性限制。
目前许多实现中有如下声明:

#ifndef FD_SETSIZE
#define FD_SETSIZE 256
#endif

但是,仅仅修改宏定义值,并不能够改变select描述符集的大小,因为还需要重新编译内核。有些厂家允许FD_SETSIZE值修改为更大的值,但这样的改动,可能会导致可移植性问题。

3、实现原理与工作模式

实现原理上讲,select与poll都采用的时轮询的方式,每次调用都要扫描整个注册描述符集合,因此它们的时间复杂度是O(n)。epoll_wait采用的是回调的方式,内核检测到描述符就绪则触发回调函数,回调函数将该文件描述符上对应的事件插入内核就绪事件队列,内核在适当的时机将就绪队列内容复制到用户空间。所以epoll_wait的时间复杂度为O(n)。但当活动连接较多时,回调函数会频繁触发,所以epoll_wait效率未必高于select和poll。
select和poll工作模式为LT,epoll则支持ET高效模式。


推荐阅读
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
  • 本文介绍了使用哈夫曼树实现文件压缩和解压的方法。首先对数据结构课程设计中的代码进行了分析,包括使用时间调用、常量定义和统计文件中各个字符时相关的结构体。然后讨论了哈夫曼树的实现原理和算法。最后介绍了文件压缩和解压的具体步骤,包括字符统计、构建哈夫曼树、生成编码表、编码和解码过程。通过实例演示了文件压缩和解压的效果。本文的内容对于理解哈夫曼树的实现原理和应用具有一定的参考价值。 ... [详细]
  • GetWindowLong函数
    今天在看一个代码里头写了GetWindowLong(hwnd,0),我当时就有点费解,靠,上网搜索函数原型说明,死活找不到第 ... [详细]
  • Linux服务器密码过期策略、登录次数限制、私钥登录等配置方法
    本文介绍了在Linux服务器上进行密码过期策略、登录次数限制、私钥登录等配置的方法。通过修改配置文件中的参数,可以设置密码的有效期、最小间隔时间、最小长度,并在密码过期前进行提示。同时还介绍了如何进行公钥登录和修改默认账户用户名的操作。详细步骤和注意事项可参考本文内容。 ... [详细]
  • 本文讨论了在Windows 8上安装gvim中插件时出现的错误加载问题。作者将EasyMotion插件放在了正确的位置,但加载时却出现了错误。作者提供了下载链接和之前放置插件的位置,并列出了出现的错误信息。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 闭包一直是Java社区中争论不断的话题,很多语言都支持闭包这个语言特性,闭包定义了一个依赖于外部环境的自由变量的函数,这个函数能够访问外部环境的变量。本文以JavaScript的一个闭包为例,介绍了闭包的定义和特性。 ... [详细]
  • 本文介绍了Linux Shell中括号和整数扩展的使用方法,包括命令组、命令替换、初始化数组以及算术表达式和逻辑判断的相关内容。括号中的命令将会在新开的子shell中顺序执行,括号中的变量不能被脚本余下的部分使用。命令替换可以用于将命令的标准输出作为另一个命令的输入。括号中的运算符和表达式符合C语言运算规则,可以用在整数扩展中进行算术计算和逻辑判断。 ... [详细]
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
  • 本文讨论了Kotlin中扩展函数的一些惯用用法以及其合理性。作者认为在某些情况下,定义扩展函数没有意义,但官方的编码约定支持这种方式。文章还介绍了在类之外定义扩展函数的具体用法,并讨论了避免使用扩展函数的边缘情况。作者提出了对于扩展函数的合理性的质疑,并给出了自己的反驳。最后,文章强调了在编写Kotlin代码时可以自由地使用扩展函数的重要性。 ... [详细]
  • 浏览器中的异常检测算法及其在深度学习中的应用
    本文介绍了在浏览器中进行异常检测的算法,包括统计学方法和机器学习方法,并探讨了异常检测在深度学习中的应用。异常检测在金融领域的信用卡欺诈、企业安全领域的非法入侵、IT运维中的设备维护时间点预测等方面具有广泛的应用。通过使用TensorFlow.js进行异常检测,可以实现对单变量和多变量异常的检测。统计学方法通过估计数据的分布概率来计算数据点的异常概率,而机器学习方法则通过训练数据来建立异常检测模型。 ... [详细]
  • FineReport平台数据分析图表显示部分系列接口的应用场景和实现思路
    本文介绍了FineReport平台数据分析图表显示部分系列接口的应用场景和实现思路。当图表系列较多时,用户希望可以自己设置哪些系列显示,哪些系列不显示。通过调用FR.Chart.WebUtils.getChart("chartID").getChartWithIndex(chartIndex).setSeriesVisible()接口,可以获取需要显示的系列图表对象,并在表单中显示这些系列。本文以决策报表为例,详细介绍了实现方法,并给出了示例。 ... [详细]
author-avatar
手机用户2602906791
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有