IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> Linux I/O与select、poll、epoll -> 正文阅读

[系统运维]Linux I/O与select、poll、epoll

一、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 <sys/select.h>
#include <sys/time.h>

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 <poll.h>
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 <sys/epoll.h>
int epoll_create(int size)

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

2、epoll_ctl

#include <sys/epoll.h>
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 <sys/epoll.h>
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高效模式。

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-01-25 10:59:37  更:2022-01-25 11:00:36 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/16 7:54:40-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码