操作系统之IO模型
一、相关概念
1.1 同步与异步、阻塞与非阻塞
- 同步和异步:用户程序通过recv()进行系统调用后,如果最后还需要调用read(),则是同步的;否则就是异步的。
- 阻塞和非阻塞:用户程序通过recv()进行系统调用后,如果还可以做其它事情,则是非阻塞的,反之则是阻塞的。
假设小明需要在网上下载一个软件:
- 同步阻塞:如果小明点击下载按钮之后,就一直干瞪着进度条不做其他任何事情直到软件下载完成;
- 异步阻塞(比较罕见):如果小明点击下载按钮之后,不用瞪着进度条但也不做其他任何事情直到软件下载完成,软件下载完成是会「叮」的一声通知的。
- 同步非阻塞:如果小明点击下载按钮之后,就去做其他事情了,不过他总需要时不时瞄一眼屏幕看软件是不是下载完成了
- 异步非阻塞:如果小明点击下载按钮之后,就去做其他事情了,软件下载完之后「叮」的一声通知小明,小明再回来继续处理下载完的软件
1.2.1 用户态和内核态的基本概念
- 用户态:就是提供应用程序运行的空间,为了使应用程序访问到内核管理的资源例如CPU,内存,I/O。
- 内核态:控制计算机的硬件资源,例如协调CPU资源,分配内存资源,并且提供稳定的环境供应用程序运行。
文章图片
用户空间和内核空间 1.2.2 用户态切换到内核态的方式
- 中断:当外设完成用户的请求时,会向CPU发送中断信号。
- 异常:如果当前进程运行在用户态,如果这个时候发生了异常事件,就会触发切换。例如:缺页异常。
- 系统调用:本质上也是中断,属于软中断的一种。
内核(kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。
二、IO模型 2.1 同步阻塞IO
- 应用进程向内核发起recfrom读取数据。
- 准备数据报(应用进程阻塞)。
- 将数据从内核负责到应用空间。
- 复制完成后,返回成功提示。
文章图片
同步阻塞IO 2.2 同步非阻塞IO
- 应用进程向内核发起recvfrom读取数据。
- 没有数据报准备好,即刻返回EWOULDBLOCK错误码。
- 应用进程向内核发起recvfrom读取数据。
- 已有数据包准备好就进行一下 步骤,否则还是返回错误码。
- 将数据从内核拷贝到用户空间。
- 完成后,返回成功提示。
文章图片
同步非阻塞IO 2.3 IO多路复用
定义:单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力,linux有三种实现方式,分别为select,poll和epoll。
文章图片
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要多。
进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
这个模型工作机制是:告诉内核启动某个操作,并让内核在整个操作(包括第二阶段,即将数据从内核拷贝到进程缓冲区中)完成后通知我们。
文章图片
异步IO模型
推荐阅读
- PMSJ寻平面设计师之现代(Hyundai)
- 太平之莲
- 闲杂“细雨”
- 七年之痒之后
- 深入理解Go之generate
- 由浅入深理解AOP
- 期刊|期刊 | 国内核心期刊之(北大核心)
- 生活随笔|好天气下的意外之喜
- 感恩之旅第75天
- python学习之|python学习之 实现QQ自动发送消息