I/O相关概念记录

I/O模式

对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:

等待数据准备 (Waiting for the data to be ready)
将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
正式因为这两个阶段,linux系统产生了下面五种网络模式的方案。

  • 阻塞 I/O(blocking IO)
  • 非阻塞 I/O(nonblocking IO)
  • I/O 多路复用( IO multiplexing)
  • 信号驱动 I/O( signal driven IO)
  • 异步 I/O(asynchronous IO)

阻塞I/O

在linux中,所有的socket默认都是blocking的

I/O相关概念记录_第1张图片

当用户进程调用了recvfrom这个系统调用,kernel就开始了I/O的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来

  • blocking I/O的特点是I/O执行的两个阶段全部都被block了

非阻塞I/O(nonBlocking I/O)

在linux下,可以通过设置socket使其变成non-blocking。当对一个non-blocking socket执行读操作时,流程如下:

I/O相关概念记录_第2张图片

当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回

  • non-blocking的特点是用户进程需要不断地主动询问kernel数据准备好了没有

I/O多路复用(I/O multiplexing)

IO多路复用指内核一旦发现进程指定的一个或者多个IO条件准备读取,他就通知该进程,有时候也被称为事件驱动I/O(event driven I/O)

IO多路复用适用于以下场景:

  • 当客户处理多个描述符(一般是交互式输入和网络套接口),必须使用IO多路复用
  • 当一个客户同时处理多个套接口时
  • 当一个TCP服务器既要处理监听套接口,又要处理已连接套接口
  • 当一个服务器既要处理TCP,又要处理UDP
  • 当一个服务器要处理多个服务或多个协议

与多进程和多线程相比,IO多路复用的优势是系统开销小,系统不必创建线程或者进程,也不必维护这些线程或进程

IO多路复用就是通过一种机制,一个进程可以监控多个描述符,一旦某个描述符就绪(读/写就绪),能够通知程序进行相应的读写操作

目前支持I/O多路复用的系统调用有select,poll,epoll,pselect,本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的

select

select函数监视的文件描述符有3类,分别是writefds,readfds,exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据读、写、except)或者超时,函数返回。当select函数返回后,可以遍历fdset,来找到就绪的描述符。

  • 优点:支持跨平台
  • 缺点:单个进程能监视的描述符数量存在最大限制,Linux上一般为1024。

select本质上是通过设置或检查描述符标志位的数据结构进行下一步处理,这样带来几个缺点:

  • 单个进程监控的描述符最大数量有限制,32位机1024,64位机2048
  • 多socket进行扫描是线性的,采用轮训,效率低下

    当socket比较多时,每次select都要遍历FD_SETSIZE个socket,不管哪个是活跃的,都要遍历一遍,会浪费很多时间(epoll和kqueue对此有改进

  • 需要维护一个存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
I/O相关概念记录_第3张图片
select调度过程
poll

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

  • poll没有最大连接数,因为它是基于链表来存储的,其余的和select没有多大区别
epoll

epoll有3个系统调用:

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event event);
int epoll_wait(int* epfd, struct epoll_event events, int* maxevents, int timeout)
  • epoll_create建立一个epoll对象,参数size是内核保证能正确处理的最大句柄数,多于这个数内核不保证效果)
  • epoll_ctl可以操作上面建立的epoll对象。 将socket放入epoll让其监控,或者把监控的某个socket句柄移除,不再监控(将I/O流放到内核)
  • epoll_wait在调用时,在给定的timeout时间内,当在监控的句柄中有事件发生时,就返回用户态的进程(在内核层面捕获可读写的I/O事件)

epoll高效的地方还在于:epoll里面有个内核高速cache,被监控的socket在cache里面以红黑树的结构存储,同时,epoll还有个list链表,存储准备就绪的事件

在调用epoll_create时,内核会建立红黑树存储socket,建立链表存储准备就绪的事件;在调用epoll_wait时,仅仅观察这个链表有没有数据,没有数据就sleep,有数据就返回,等到timeout没数据也返回;在执行epoll_ctl时,如果增加socket,则检查红黑树是否存在,存在就立即返回,不存在就添加到红黑树,然后向内核注册回调函数,用于当中断事件来临时向准备就绪的链表中插入数据。

epoll水平触发和边缘触发的实现
epoll_wait返回用户态时是否清空链表,清空了就是边缘触发,未清空就是水平触发

select/poll和epoll的区别:

  • select/poll 监控的socket句柄列表在用户态,每次调用都需要从用户态将句柄列表拷贝到内核态;但是epoll监控的句柄就是建立在内核态,减少了内核和用户态的拷贝
  • select/poll 在用户态和内核之间的拷贝每次都是全部列表,同时遍历效率低;epoll则只需要将准备就绪的句柄返回即可,数量较少

异步I/O(asynchronous I/O)

I/O相关概念记录_第4张图片

用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

最后

就好比去买一件商品
  1)阻塞I/O:你自己跑去商店下单(只能一个一个来),等有了物品还要自己去拿回来
  2)非阻塞I/O:你可以网上下单了,而且网上看有没有货,有的话自己去拿回来
  3)I/O多路复用:和第一种情况差不多,但是这个商店下单窗口多,可以同时多个人跑来下单,谁的货到了,自己来取
  4)异步I/O:你只要网上下个单,其余就不管了。商家不仅发快递,快递小哥还把商品直接送到你家里

你可能感兴趣的:(I/O相关概念记录)