操作系统之IO模型

一、相关概念 1.1 同步与异步、阻塞与非阻塞

  • 同步和异步:用户程序通过recv()进行系统调用后,如果最后还需要调用read(),则是同步的;否则就是异步的。
  • 阻塞和非阻塞:用户程序通过recv()进行系统调用后,如果还可以做其它事情,则是非阻塞的,反之则是阻塞的。
概念有些抽象,可以通过以下的例子来理解一下这组概念:
假设小明需要在网上下载一个软件:
  • 同步阻塞:如果小明点击下载按钮之后,就一直干瞪着进度条不做其他任何事情直到软件下载完成;
  • 异步阻塞(比较罕见):如果小明点击下载按钮之后,不用瞪着进度条但也不做其他任何事情直到软件下载完成,软件下载完成是会「叮」的一声通知的。
  • 同步非阻塞:如果小明点击下载按钮之后,就去做其他事情了,不过他总需要时不时瞄一眼屏幕看软件是不是下载完成了
  • 异步非阻塞:如果小明点击下载按钮之后,就去做其他事情了,软件下载完之后「叮」的一声通知小明,小明再回来继续处理下载完的软件
1.2 用户态和内核态
1.2.1 用户态和内核态的基本概念
  • 用户态:就是提供应用程序运行的空间,为了使应用程序访问到内核管理的资源例如CPU,内存,I/O。
  • 内核态:控制计算机的硬件资源,例如协调CPU资源,分配内存资源,并且提供稳定的环境供应用程序运行。
操作系统之IO模型
文章图片
用户空间和内核空间 1.2.2 用户态切换到内核态的方式
  • 中断:当外设完成用户的请求时,会向CPU发送中断信号。
  • 异常:如果当前进程运行在用户态,如果这个时候发生了异常事件,就会触发切换。例如:缺页异常。
  • 系统调用:本质上也是中断,属于软中断的一种。
1.3 文件描述符
内核(kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。
二、IO模型 2.1 同步阻塞IO
  • 应用进程向内核发起recfrom读取数据。
  • 准备数据报(应用进程阻塞)。
  • 将数据从内核负责到应用空间。
  • 复制完成后,返回成功提示。
操作系统之IO模型
文章图片
同步阻塞IO 2.2 同步非阻塞IO
  • 应用进程向内核发起recvfrom读取数据。
  • 没有数据报准备好,即刻返回EWOULDBLOCK错误码。
  • 应用进程向内核发起recvfrom读取数据。
  • 已有数据包准备好就进行一下 步骤,否则还是返回错误码。
  • 将数据从内核拷贝到用户空间。
  • 完成后,返回成功提示。
操作系统之IO模型
文章图片
同步非阻塞IO 2.3 IO多路复用
定义:单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力,linux有三种实现方式,分别为select,poll和epoll。
操作系统之IO模型
文章图片
IO多路复用 2.3.1 select
  • 源码
/* According to POSIX.1-2001, POSIX.1-2008 */ #include /* According to earlier standards */ #include #include #include int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask); void FD_CLR(int fd, fd_set *set); intFD_ISSET(int fd, fd_set *set); void FD_SET(int fd, fd_set *set); void FD_ZERO(fd_set *set);

select的调用会阻塞到有文件描述符可以进行IO操作或被信号打断或者超时才会返回。
【操作系统之IO模型】select将监听的文件描述符分为三组,每一组监听不同的需要进行的IO操作。readfds是需要进行读操作的文件描述符,writefds是需要进行写操作的文件描述符,exceptfds是需要进行异常事件处理的文件描述符。这三个参数可以用NULL来表示对应的事件不需要监听。
当select返回时,每组文件描述符会被select过滤,只留下可以进行对应IO操作的文件描述符。
FD_xx系列的函数是用来操作文件描述符组和文件描述符的关系。
FD_ZERO用来清空文件描述符组。每次调用select前都需要清空一次。
select可同时监听的文件描述符数量是通过FS_SETSIZE来限制的,在Linux系统中,该值为1024。
2.3.2 poll
  • 源码
#include int poll(struct pollfd *fds, nfds_t nfds, int timeout); #include #include int ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *tmo_p, const sigset_t *sigmask); struct pollfd { int fd; /* file descriptor */ short events; /* requested events to watch */ short revents; /* returned events witnessed */ };

poll和select用三组文件描述符不同的是,poll只有一个pollfd数组,数组中的每个元素都表示一个需要监听IO操作事件的文件描述符。events参数是我们需要关心的事件,revents是所有内核监测到的事件。
poll在linux下没有监听的文件描述符数量限制,但是如果过多的话,速度也会变慢。
2.3.3 epoll
  • 源码
#include int epoll_create(int size); int epoll_create1(int flags); 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); int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask);

epoll_create用于创建一个epoll实例,而epoll_ctl用于往epoll实例中增删改要监测的文件描述符,epoll_wait则用于阻塞的等待可以执行IO操作的文件描述符直到超时。
epoll优于select&poll在下面几点:
  • 在需要同时监听的文件描述符数量增加时,select&poll是O(N)的复杂度,epoll是O(1),在N很小的情况下,差距不会特别大,但如果N很大的前提下,一次O(N)的循环可要比O(1)慢很多,所以高性能的网络服务器都会选择epoll进行IO多路复用。
  • epoll内部用一个文件描述符挂载需要监听的文件描述符,这个epoll的文件描述符可以在多个线程/进程共享,所以epoll的使用场景要比select&poll要多。
2.4 异步IO
进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
这个模型工作机制是:告诉内核启动某个操作,并让内核在整个操作(包括第二阶段,即将数据从内核拷贝到进程缓冲区中)完成后通知我们。
操作系统之IO模型
文章图片
异步IO模型

    推荐阅读