吃瓜群众如何理解五种IO模型

 

在讲io模型之前,我们先回顾一下进程的唤醒机制和io的过程

阻塞是进程主动发起的。用户进程在需要系统调用时,因为请求系统调用并不会立即得到进程所需要的结果或资源,所以交出cpu进入阻塞态是一种高效率的调度机制。此外,除了系统调用,在进程相互通信时,进程等待其它进程的信息时也可能会造成阻塞。

 

进程在阻塞前会注册阻塞事件及其唤醒回调。

 

对于一个network IO,它会涉及到两个系统对象:

  • application 调用这个IO的进程
  • kernel 系统内核

那他们经历的两个交互过程是:

  • 阶段1 wait for data 等待数据准备
  • 阶段2 copy data from kernel to user 将数据从内核拷贝到用户进程中

 

 

 

为了方便理解这几种io模型,我以吃瓜为例,来表示这几种模型的状态转换。

以下将我--一个12岁被宠爱过头的孩子,比作一个进程,冰箱为一个socket,瓜为数据,茶几为缓冲区,我妈为内核进程,我弟弟也是内核进程。

 

1.阻塞IO模型  

当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并唤醒进程。

代码示例为  

data = socket.read();

 

imageimage.gif

 

 

 

比喻:我是打游戏中途想吃瓜,于是我告诉我妈我要吃瓜并且放下了正在打的游戏。我决定睡觉(阻塞自己),直到我妈妈跟我说西瓜已经准备好了,我才醒来吃瓜,吃完继续打游戏。

 

 

2.非阻塞IO模型

 

当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。

  所以事实上,在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。

典型的非阻塞IO模型一般如下:

while(true){
   data = socket.read();
   if(data!= error){
       处理数据
       break;
   }
}

image.gif

 

  但是对于非阻塞IO就有一个非常严重的问题,在while循环中需要不断地去询问内核数据是否就绪,这样会导致CPU占用率非常高,因此一般情况下很少使用while循环这种方式来读取数据。

imageimage.gif

 

 

 

比喻:   我要吃瓜,我就问我妈,我想吃西瓜家里有西瓜吗?如果妈妈回答我没有西瓜,那我再问一遍。一直问到妈妈说西瓜准备好了,我就去吃瓜。

 

3.多路复用IO模型

  多路复用IO模型是目前使用得比较多的模型。

  在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。

  多路复用IO模式,通过一个线程就可以管理多个socket,只有当socket真正有读写事件发生才会占用资源来进行实际的读写操作。多路复用目标并不是对单个连接处理更迅速,而是能处理更多的连接数。连接不多时,多路复用并不一定比多线程+阻塞io快。

  不过要注意的是,多路复用IO模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用IO模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。

imageimage.gif

 

 

比喻:我的家里来了很多客人, 我想吃西瓜,表妹想吃哈密瓜、表姐想吃黄瓜、嫂子想吃甜瓜(多个IO)。我妈让我弟弟去门口的菜市场看又没有新鲜的瓜上架 (select/poll),有的话立即回来告诉妈妈,妈妈去把瓜买回来。

 

多路复用的实现机制

多路复用的关键在于监控多个文件描述符。

操作系统提供了包括 select 、poll、epoll、kqueue 等多种监控策略(都是一个函数)。

select

调用后select函数会阻塞,直到有描述符就绪,或者超时。当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符。(这意味着select 仅仅告诉你是否有文件描述符可用,但是不会告诉你具体是哪个)

select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但 是这样也会造成效率的降低。

poll

poll 可以理解为使用链表存储描述符的select。(select 使用的是数组)

和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。

虽然poll 并没有描述符数量限制,但是如果描述符过多,遍历的效率会线性下降。

epoll

epoll是在linux 2.6内核中提出的,是之前的select和poll的增强版本。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

 

我理解是这样:select和poll 在进行io时,用户进程先在自己的地址空间内注册好需要关注的描述符。调用select/poll时,涉及到用户态到内核态的转换。因为需要内核来帮忙监控描述符,所以这些用户进程创建好的描述符表会被拷贝到内核空间。之后便由内核全权关注。

 

epoll 则是一开始就注册在内核空间中。

       

 

并且没fd这个限制,所支持的FD上限是操作系统的最大文件句柄数,1G内存大概支持10万个句柄

 

epoll操作过程需要三个接口,分别如下:

 

int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大

//每新建一个连接,都通过该函数操作epoll对象,在这个对象里面修改添加删除对应的链接fd, 绑定一个callback函数
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

//轮询所有的callback集合,并完成对应的IO操作
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

 没有fd个数限制,用户态拷贝到内核态只需要一次,使用时间通知机制来触发。通过epoll_ctl注册fd,一旦fd就绪就会通过callback回调机制来激活对应fd,进行相关的io操作。

  效率提高,使用回调通知而不是轮询的方式,不会随着FD数目的增加效率下降。

  内核和用户空间mmap同一块内存实现(mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间)

 

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享

 

mmap的缺点:

1.文件如果很小,是小于4096字节的,比如10字节,由于内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位。虽然被映射的文件只有10字节,但是对应到进程虚拟地址区域的大小需要满足整页大小,因此mmap函数执行后,实际映射到虚拟内存区域的是4096个字节,11~4096的字节部分用零填充。因此如果连续mmap小文件,会浪费内存空间。

  1. 对变长文件不适合,文件无法完成拓展,因为mmap到内存的时候,你所能够操作的范围就确定了。

3.如果更新文件的操作很多,会触发大量的脏页回写及由此引发的随机IO上。所以在随机写很多的情况下,mmap方式在效率上不一定会比带缓冲区的一般写快。

 

 

 

 

 

 

 

epoll 的模式

 LT模式(默认):当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。

  ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

 

 

 不过要注意的是,多路复用IO模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用IO模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。

 

4. 信号驱动IO模型

信号驱动式I/O是指进程预先告知内核,使得当某个描述符上发生某事时,内核使用信号通知相关进程。

  

信号驱动常用于UDP,由于该信号产生的过于频繁,并且没有告诉我们发生了什么事情导致对TCP几乎无用。

 

信号驱动就类似于  你想吃瓜,并且告诉妈妈,有瓜来了就通知你。然后你又回去打游戏了,直到妈妈通知你。

5. 异步IO

异步I/O是进程执行I/O系统调用(读或写)告知内核启动某个I/O操作,内核启动I/O操作后立刻返回到进程,进程在I/O操作发生期间继续执行,当操作完成或遭遇错误时,内核以进程在I/O系统调用中指定的某种方式通知进程,

 

 

异步io类似于你和妈妈说想吃瓜,并让妈妈发现有瓜了后亲自给你切好.。然后你就回去打游戏了。

 

 

异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值