Linux/Unix--IO模型

传统模型:阻塞式IO模型

  单个进程每次只在一个文件描述符上进行I/O操作,每次系统I/都会阻塞知道完成数据传输
  比如,当从一个管道中读取数据时,如果管道中恰好没有数据,那么通常read()会阻塞。而如果管道中没有足够的空间保存写入的数据时,write()也会被阻塞


  由于阻塞式IO模型的缺陷,传统有两种方式解决,一种是多线程,另一种是非阻塞IO
  多线程:如果对文件描述符进行IO操作时阻塞了,创建一个子线程继续进行IO,此时父线程可以去处理其他的任务。缺陷:开销昂贵而且复杂
  非阻塞IO:周期性地检查某个文件描述符上是否可以执行IO操作。缺陷:这样轮询对CPU是一种浪费


由于非阻塞IO和多线程都有其各自的局限性,下面的备选方案往往是更加可取的

1.I/O多路复用
2.信号驱动I/O
3.epoll API

实际上三种方案,其实都是为了实现同一个目标:同时检查多个文件描述符,看他们是否准备好了执行IO操作


水平触发和边缘触发

  在深入讨论IO模型之前,需要先了解两种文件描述符准备就绪的通知方式
  水平触发:如果文件描述符上可以非阻塞地进行IO系统调用,此时认为它已经准备就绪
  边缘触发:如果文件描述符自上次状态检查以来有了新的IO系统调用,此时触发通知

IO模式水平触发边缘触发
select,poll()
信号驱动IO
epoll

  对于通知模式的选择是如何影响我们设计程序方式的讨论:
  1. 水平触发:我们可以在任意时刻检查文件描述符的就绪状态,而只要确定他是就绪态的时候,就可以执行一些IO操作,然后再检查当前的或者别的文件描述符的就绪状态,因此我们没必要每次在文件描述符就绪后就尽可能多地执行IO操作


  2. 边缘触发:我们只有在IO事件发生时才收到通知,之后在下一个IO事件之前都不会收到通知了,因此我们应该尽可能多地执行IO,如果程序采用循环来对文件描述符执行尽可能多地IO,那么文件描述符就一定要是非阻塞模式,出现错误码才结束,不然一旦IO操作空了,就将一直阻塞。
  (带来的问题:如果仅对一个文件描述符执行大量的IO操作,那么很可能让其他的文件描述符处于饥饿状态)


1. IO多路复用

  IO多路复用允许我们同时检查多个文件描述符,看其中任意一个是否可以执行IO操作。我们可以采用两个功能几乎相同的系统调用来实现IO多路复用操作:1.select(),2.poll()
  这两个系统调用都会一直阻塞,直到有一个或多个文件描述符成为就绪态(准确地说poll是这样,而select是等到一个或者多个文件描述符集合成为就绪态)

select()

nfds:监听套接字的最大值+1
下面三个都是指向文件描述符集合的指针,三个指针指向的结构体先初始化来包含我们感兴趣的文件描述符,之后的select调用会修改这些结构体,使之最后包含的是已处于就绪态的文件描述符集合
readfds:检测输入是否就绪的文件描述符集合
writefds:检测输出是否就绪的文件描述符集合
exceptfds:检测异常情况是否发生的文件描述符集合
timeout:控制select()的阻塞行为,当指定为null时,select()将会一直阻塞

select()的返回值
  1. 返回-1,这表示有错误发生,EBADF表示三个文件描述符集合中有文件描述符是非法的,EINTR表示改用被信号处理例程中断了
  2. 返回0,表示在任何文件描述符在成为就绪态之前,select()已经超时了
  3. 返回一个正整数表示有1个或者多个文件描述符已经到达就绪态,返回的值就是个数。(这里和pol()有很大区别,在于如果一个文件描述符在多个集合中时,将会被重复统计)

poll()

在select()中,我们提供三个集合,每个集合中标明我们感兴趣的文件描述符
而在poll()中,我们不是以事件来"分表",而是以文件描述符"分表",我们直接提供一系列的文件描述符,在每个文件描述符上标明我们感兴趣的事件
timeout:-1时,无限阻塞,0时,完全不等待,如果大于0,那就是阻塞timeout毫秒
pollfd:结构体数组

struct pollfd {
	int fd; 		// file descriptor
	short events;	// requested events bit mask
	short revents;	// returned events bit mask
}
//event中的标志位指定需要做检查的事件,如果感兴趣则为1,不感兴趣则0,和IP掩码的原理是类似的
//revents中的标志位表示该文件描述符实际的事件

文件描述符何时就绪?

正确使用select()和poll()需要理解:在什么情况下文件描述符会表示为就绪态?

  1. 普通文件
      代表普通文件的文件描述符总是被select()表示为可读和可写的,对于poll()而言,则是在revents字段中返回POLLIN和POLLOUT标志
  2. 终端和伪终端
    有输入有输出的情况是正常的,但是伪终端调用close()后,select()视为rw,poll()取决于具体实现
  3. 管道和FIFO(有名管道)
  4. 套接字

select()和poll()糟糕的性能延展性源自于这些局限性:
通常,程序重复调用所检查的文件描述符集合都是相同的,但是内核并不会在每次调用成果后就记录下它们。


2. 信号驱动IO

在信号驱动IO中,当文件描述符上可执行IO操作时,进程请求内核为自己发送一个信号。之后进程就可以执行任何其他的任务,然后等到内核发送信号给进程,进程收到通知信号,尽可能多地循环执行I/O调用直到失败为止

使用信号驱动I/O的步骤

  1. 为内核发送的通知信号安装一个信号处理例程
  2. 设定文件描述符的属主,即收到通知信号的进程或者进程组。通常情况下,我们都会让调用进程成为属主(这就是我通知我自己吧,XD)
  3. 改变2个标志位,使能非阻塞IO和信号驱动IO
  4. 调用进程请求内核,此时调用进程执行其他任务,内核发送一个信号,调用信号处理例程
  5. 属主尽可能多地循环之星IO系统调用知道失败为止

3. epoll API

Linux特有,Unix没有
epoll API的主要优点:

  1. 当检查大量的文件描述符时,epoll的性能延展性比select()和poll()高很多
  2. epoll API既支持水平触发也支持边缘触发
  3. 在性能上与信号驱动IO相似,但是epoll比信号驱动IO好的点有:
    1. 可以避免复杂的信号处理流程
    2. 灵活性高,可以指定希望检查的时间类型

epoll API的核心数据结构-epoll实例

它和一个打开的文件描述符相关联,不是用来做IO操作的,而是内核数据结构的句柄,这些内核数据结构实现了两个目的

  1. 记录了在进程中声明过的感兴趣的文件描述符列表–interest list(兴趣列表)
  2. 维护了处于IO就绪态的文件描述符列表–ready list(就绪列表)

epoll API由三个系统调用组成

  1. epoll_create:创建一个epoll实例,返回代表该实例的文件描述符
  2. epoll_ctk():操作同epoll实例相关联的兴趣列表,增删文件描述符和修改代表文件描述符上事件类型的位掩码
  3. epoll_wait:返回与epoll实例相关联的就绪列表中的成员
    epoll运行机制:
    每当执行IO操作使得文件描述符成为就绪态时,内核就在epoll描述符的就绪列表中添加一个元素,之后的epoll_wait()调用从就绪列表里简单地取出这些元素就可以

epoll的水平触发和边缘触发的区别

  1. 套接字有输入到来
  2. 调用一次epoll_wait(),无论采用的是哪种触发,该调用都会通知我们套接字已经准备就绪了
  3. 再次调用epoll_wait(),如果是水平触发,则该调用会告诉我们套接字会处于就绪态;而如果是边缘触发,则该调用将会阻塞,因为没有新的IO到来

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值