高级I/O

阻塞I/O

一般的I/O操作为阻塞I/O,比如read和write。阻塞的意思,就是在得不到数据或资源时一直等着,直到拿到了数据或资源。应用的函数如果不能被完成,长时间处于等待结果的状态,我们就称为阻塞I/O。这样会造成CPU空闲。

非阻塞I/O

非阻塞I / O是指,如果这种I/O操作不能完成,则立即出错返回,表示该操作如果继续执行将会阻塞。在出错返回后,进程或线程可以接着处理下一函数,但需要定期轮询,以完成I/O操作。

 

 

对于一个给定的描述符有两种方法对其指定非阻塞I / O:

(1) 如果是调用o p e n以获得该描述符,则可指定O _ N O N B L O C K标志

(2) 对于已经打开的一个描述符,则可调用f c n t l打开O _ N O N B L O C K文件状态标志

/**********************使能非阻塞I/O********************
*int flags;
*if(flags = fcntl(fd, F_GETFL, 0) < 0)
*{
*    perror("fcntl");
*    return -1;
*}
*flags |= O_NONBLOCK;
*if(fcntl(fd, F_SETFL, flags) < 0)
*{
*    perror("fcntl");
*    return -1;
*}
*******************************************************/

/**********************关闭非阻塞I/O******************
flags &= ~O_NONBLOCK;
if(fcntl(fd, F_SETFL, flags) < 0)
{
    perror("fcntl");
    return -1;
}
*******************************************************/

 

I/O多路复用

I/O多路复用是指,使用一个线程来检查多个文件描述符的就绪状态(主要是select和poll、epoll )。此时阻塞发生在select/poll/epoll的系统调用上,而不是阻塞在实际的I/O系统调用上。

IO多路复用的高级之处在于:它能同时等待多个文件描述符,而这些文件描述符其中的任意一个进入读就绪状态,select等函数就可以返回。

 

这个图和阻塞 IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而阻塞 IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个连接。

所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用多线程 + 阻塞 IO的web server性能更好,可能延迟还更大。

select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

在IO多路复用中,实际上,对于每一个socket,一般都设置成为非阻塞的,但是,如上图所示,整个用户的进程其实是一直被阻塞在select/poll/epoll处的。

Select

#include <sys/types.h>/* fd_set data type */

#include <sys/time.h> /* struct timeval */

#include <unistd.h> /* function prototype might be here */

int select (int maxfd+1,  fd_set * readfds,  fd_set * writefds,  fd_set * exceptfds,

struct timeval * tvptr) ;

s e l e c t有三个可能的返回值。

(1) 返回值-1表示出错。这是可能发生的,例如在所指定的描述符都没有准备好时捕捉到

一个信号。

(2) 返回值0表示没有描述符准备好。若指定的描述符都没有准备好,而且指定的时间已经

超过,则发生这种情况。

(3) 返回一个正值说明了已经准备好的描述符数,在这种情况下,三个描述符集中仍旧打

开的位是对应于已准备好的描述符位。

 

先说明最后一个参数,它指定愿意等待的时间。

struct timeval{

long tv_sec; /* seconds */

long tv_usec; /* and microseconds */

};

有三种情况:

• t v p t r= =NULL : 永远等待;

• t v p t r- >t v _ s e c= =0 && t v p t r- >t v _ u s e c= =0 : 完全不等待;

• t v p t r- >t v _ s e c ! =0 | | t v p t r- >t v _ u s e c! =0 : 等待指定的秒数和微秒数。

 

中间三个参数re a d f d s、w r i t e f d s和e x c e p t f d s是指向描述符集的指针。这三个描述符集说明了我们关心的可读、可写或处于优先条件的各个描述符。每个描述符集存放在一个 f d _ s e t数据类型中。

s e l e c t第一个参数m a x f d p1的意思是“最大f d加1(max fd plus 1)”。内核就只需在此范围内寻找打开的位,而不必在数百位的大范围内搜索。

Poll

#include <stropts.h>

#include <poll.h>

int poll(struct pollfd fdarray[ ], unsigned long nfds, int timeout) ;

返回:准备就绪的描述符数,若超时则为 0,若出错则为- 1

与s e l e c t不同,p o l l不是为每个条件构造一个描述符集,而是构造一个 p o l l f d结构数组,每个数组元素指定一个描述符编号以及对其所关心的条件。

 

struct pollfd {

int fd ; /* file descriptor to check, or < 0 to ignore */

short events ; /* events of interest on fd */

short revents ; /* events that occurred on fd */

} ;

fdarray数组中的元素数由nfds说明。

 

应将e v e n t s成员设置为下表所示值的一个或几个。通过这些值告诉内核我们对该描述

符关心的是什么。返回时,内核设置r e v e n t s成员,以说明对该描述符发生了什么事件。(p o l l没有更改e v e n t s成员,这与s e l e c t不同,s e l e c t修改其参数以指示哪一个描述符已准备好了。)

Epoll

epollLinux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。(水平触发:在一个状态时触发,在关心状态时有用;边沿触发:状态改变时触发,在关心事件时有用)

 

EPOLL的使用 :

文件描述符的创建

#include <sys/epoll.h>

int epoll_create ( int size );

     

注册监控事件

#include <sys/epoll.h>

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

函数说明:

     fd:要操作的文件描述符

     op:指定操作类型

操作类型:

     EPOLL_CTL_ADD:往事件表中注册fd上的事件

     EPOLL_CTL_MOD:修改fd上的注册事件

     EPOLL_CTL_DEL:删除fd上的注册事件

     event:指定事件,它是epoll_event结构指针类型

     epoll_event定义:

结构体说明:

     events:描述事件类型,和poll支持的事件类型基本相同(两个额外的事件:EPOLLET和EPOLLONESHOT,高效运作的关键)

     data成员:存储用户数据

 

epoll_wait函数

#include <sys/epoll.h>

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

函数说明:

     返回:成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno

     timeout:指定epoll的超时时间,单位是毫秒。当timeout为-1是,epoll_wait调用将永远阻塞,直到某个时间发生。当timeout为0时,epoll_wait调用将立即返回。

     maxevents:指定最多监听多少个事件

     events:检测到事件,将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组中。

 

select、poll和epoll三种I/O复用模式的比较

系统调用

select

poll

epoll

 

事件集合

用户通过3个参数分别传入感兴趣的可读,可写及异常等事件

内核通过对这些参数的在线修改来反馈其中的就绪事件

这使得用户每次调用select都要重置这3个参数

统一处理所有事件类型,因此只需要一个事件集参数。

用户通过pollfd.events传入感兴趣的事件,内核通过

修改pollfd.revents反馈其中就绪的事件

内核通过一个事件表直接管理用户感兴趣的所有事件。

因此每次调用epoll_wait时,无需反复传入用户感兴趣

的事件。epoll_wait系统调用的参数events仅用来反馈就绪的事件

应用程序索引就绪文件

描述符的时间复杂度

O(n)

O(n)

O(1)

最大支持文件描述符数

一般有最大值限制

65535

65535

工作模式

LT(水平触发)

LT

支持ET高效模式(边沿触发)

内核实现和工作效率

采用轮询方式检测就绪事件,时间复杂度:O(n)

采用轮询方式检测就绪事件,时间复杂度:O(n)

采用回调方式检测就绪事件,时间复杂度:O(1)

 

异步I/O

异步I/O是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核拷贝到我们自己的缓冲区)完成后通知我们。

 

信号驱动I/O

当数据准备完毕的时候,信号通知程序数据准备完毕,第2阶段阻塞;

我们首先开启套接口的信号驱动I/O功能,并通过sigaction系统调用安装一个信号处理函数。该系统调用立即返回,我们的进程继续工作,也就是说它没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个SIGIO信号。我们随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已准备好待处理,也可以立即通知主循环,让它读取数据报。

 

无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间,进程不被阻塞。主循环可以继续执行,只要不时等待来自信号处理函数的通知:既可以是数据已准备好被处理,

也可以是数据报已准备好被读取。

同步、异步、阻塞、非阻塞

同步 & 异步

同步与异步是针对多个事件(线程/进程)来说的。

  • 如果事件A需要等待事件B的完成才能完成,这种串行执行机制可以说是同步的,这是一种可靠的任务序列,要么都成功,要么都失败。
  • 如果事件B的执行不需要依赖事件A的完成结果,这种并行的执行机制可以说是异步的。事件B不确定事件A是否真正完成,所以是不可靠的任务序列。

同步异步可以理解为多个事件的执行方式和执行时机如何,是串行等待还是并行执行。同步中依赖事件等待被依赖事件的完成,然后触发自身开始执行,异步
中依赖事件不需要等待被依赖事件,可以和被依赖事件并行执行,被依赖事件执行完成后,可以通过回调、通知等方式告知依赖事件。

阻塞 & 非阻塞

阻塞与非阻塞是针对单一事件(线程/进程)来说的。

  • 对于阻塞,如果一个事件在发起一个调用之后,在调用结果返回之前,该事件会被一直挂起,处于等待状态。
  • 对于非阻塞,如果一个事件在发起调用以后,无论该调用当前是否得到结果,都会立刻返回,不会阻塞当前事件。

阻塞与非阻塞可以理解为单个事件在发起其他调用以后,自身的状态如何,是苦苦等待还是继续干自己的事情。非阻塞虽然能提高CPU利用率,但是也带来了系统线程切换的成本,需要在CPU执行时间和系统切换成本之间好好估量一下。

同步阻塞

应用程序执行系统调用,应用程序会一直阻塞,直到系统调用完成。应用程序处于不再消费CPU而只是简单等待响应的状态。当响应返回时,数据被移动到用户空间的缓冲区,应用程序解除阻塞。

 

同步非阻塞

设备以非阻塞形式打开,I/O操作不会立即完成,read操作可能会返回一个错误代码。应用程序可以执行其他操作,但需要请求多次I/O操作,直到数据可用。

 

同步非阻塞形式实际上是效率低下的,因为:

  • 应用程序需要在不同的任务之间切换。异步非阻塞是你只需要执行当前任务,系统调用会主动通知你,不用频繁切换。
  • 数据在内核中变为可用到调用read返回数据之间存在时间间隔,会造成整体数据吞吐量降低

异步非阻塞

应用程序的其他处理任务与I/O任务重叠进行。读请求会立即返回,说明请求已经成功发起,应用程序不被阻塞,继续执行其它处理操作。当read响应到达,将数据拷贝到用户空间,产生信号或者执行一个基于线程回调函数完成I/O处理。应用程序不用在多个任务之间切换。

 

 

各种I/O模型的比较

非阻塞I/O和异步I/O区别在于,在非阻塞I/O中,虽然进程大部分时间不会被block,但是需要不停的去主动check,并且当数据准备完成以后,也需要应用程序主动调用recvfrom将数据拷贝到用户空间;异步I/O则不同,就像是应用程序将整个I/O操作交给了内核完成,然后由内核发信号通知。期间应用程序不需要主动去检查I/O操作状态,也不需要主动从内核空间拷贝数据到用户空间。

非阻塞I/O看起来是non-blocking的,但是只是在内核数据没准备好时,当数据准备完成,recvfrom需要从内核空间拷贝到用户空间,这个时候其实是被block住的。而异步I/O是当进程发起I/O操作后,再不用主动去请求,知道内核数据准备好并发出信号通知,整个过程完全没有block。

 

散布读和聚集写、多次读和多次写

r e a d v和w r i t e v函数用于在一个函数调用中读、写多个非连续缓存。有时也将这两个函数称为散布读(scatter re a d)和聚集写(gather write)。

#include <sys/types.h>

#include <sys/uio.h>

ssize_t readv(int filedes, const struct iovec iov[ ], int iovcnt) ;

ssize_t writev(int filedes, const struct iovec iov[ ], int iovcnt) ;

两个函数返回:已读、写的字节数,若出错则为 - 1

这两个函数的第二个参数是指向i o v e c结构数组的一个指针:

struct iovec {

void *iov_base; /* starting address of buffer */

size_t iov_len; /* size of buffer */

} ;

i o v数组中的元素数由i o v c n t说明。

 

 

r e a d n和w r i t e n的功能是读、写指定的N字节数据,并处理返回值小于

要求值的情况。这两个函数只是按需多次调用r e a d和w r i t e直至读、写了N字节数据。

#include "o u r h d r . h"

ssize_t readn(int filedes, void *buff, size_t nbytes) ;

ssize_t writen(int filedes, void *buff, size_t nbytes) ;

两个函数返回:已读、写字节数,若出错则为 - 1

存储映射I/O

存储映射I / O使一个磁盘文件与存储空间中的一个缓存相映射。于是当从缓存中取数据,

就相当于读文件中的相应字节。与其类似,将数据存入缓存,则相应字节就自动地写入文件。

这样,就可以在不使用r e a dw r i t e的情况下执行I / O

为了使用这种功能,应首先告诉内核将一个给定的文件映射到一个存储区域中。这是由

m m a p函数实现的。

#include <sys/types.h>

#include <sys/mman.h>

caddr_t mmap(caddr_t  addr, size_t len, int prot, int flag, int filedes, off_t off) ;

返回:若成功则为映射区的起始地址,若出错则为 – 1

数据类型c a d d r _ t通常定义为char *a d d r参数用于指定映射存储区的起始地址。通常将其设置为0,这表示由系统选择该映射区的起始地址。此函数的返回地址是:该映射区的起始地址。

f i l e d e s指定要被映射文件的描述符。在映射该文件到一个地址空间之前,先要打开该文件。

l e n是映射的字节数。o f f是要映射字节在文件中的起始位移量。p ro c参数说明映射存储区的保护要求。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值