IO问题的本质以及针对不同场景的处置策略
作者:互联网
epoll.h中的接口
// /usr/include/x86_64-linux-gnu/sys/epoll.h
extern int epoll_create (int __size) __THROW;
extern int epoll_create1 (int __flags) __THROW;
extern int epoll_ctl (int __epfd, int __op, int __fd,
extern int epoll_wait (int __epfd, struct epoll_event *__events,
extern int epoll_pwait (int __epfd, struct epoll_event *__events,
需求是什么?系统内核为什么会抽象出EPOLL这样一个接口?它适合什么样的场景?
对于编程经历不同的同学,需要明白哪些事情才能真正地了解EPOLL的设计哲学?
EPOLL设计上的实现细节有哪些值得研究的?
我平常的编程,主要是算法题上的编程,主要跟算法和数据结构相关,而算法和数据结构都是内存操作,基本不涉及文件操作。
哪怕在学C语言的时候,遇到了文件操作,也都是往文件中写数据,从文件中读数据,也就是所操作的文件都是就绪的,时刻准备好的,不需要我去编程判断文件是否可用。
当然了,算法题编程中,会遇到判断文件是否可用的情况,即scanf和cin,但是一般我不会关心这些处理细节。我只关心我程序接收到的数据正确与否。
但是在网络编程中,判断文件是否可用,就成了重头戏了。网络的目的是提供一系列服务使得两台地理上分隔(其实不太准确,宿主机和虚拟机也通过网络进行通信,不过这就不是地理上隔离的计算机之间的通信了)的计算机能够通信。通信即交换数据。我们知道通信的两端是 分别位于网络两端主机上 的 两个进程。
两端的进程均位于计算机网络的应用层,也就是进程需要使用运输层提供的接口,进行网络通信。
运输层接口的设计逻辑是,提供一个文件描述符(file descriptor)。当文件描述符可读的时候,就意味着网络上有数据来了;向文件描述符中写,就代表向网络中的发送数据。
当一个进程需要同时维护与网络中多个进程的连接的时候,问题就来了。
IO模型
我们上文书说到,当一个进程需要同时维护与网络中多个进程的连接的时候,会出现问题。我们接下来介绍常见的IO模型。参考博客
服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种:
- 同步阻塞IO(Blocking IO):即传统的IO模型
- 同步非阻塞IO(Non-blocking IO):默认创建的socket都是阻塞的,非阻塞IO要求socket被设置成Non-blocking
- IO多路复用(IO Multiplexing):即经典的Reactor设计模式,有时也成为异步阻塞IO,Java中的Selector和Linux中的epoll都是这种模型
- 异步IO(Asynchronous IO):即经典的Proactor设计模式,也称为异步非阻塞IO
同步和异步的概念描述的是用户线程与内核的交互方式:
- 同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;
- 异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。
阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:
- 阻塞是指IO操作需要彻底完成后才返回到用户空间;
- 非阻塞是指IO操作被调用后立即返回给用户一个状态值,无须等到IO操作彻底完成。
另外,Richard Stevens在《Unix网络编程》卷1中提到的基于信号驱动的IO(Signal Driven IO)模型,由于该模型并不常用,本文不作涉及。接下来,我重点介绍四种常见的IO模型的特点,关于他们的实现原理的详细分析,请参考博客。为了方便描述,我们统一使用IO的读操作作为示例。
同步阻塞IO
同步阻塞IO是最简单的IO模型,用户线程在内核进行IO操作时被阻塞。
同步非阻塞IO
同步非阻塞IO是在同步阻塞IO的基础上,将socket设置为Non-blocking。这样做用户线程可以在发起IO请求后立即返回。
用户线程使用同步非阻塞IO模型的伪代码描述为:
{
while (read(socket, buffer) != SUCCESS) {
;
}
process(buffer);
}
IO多路复用
IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。
用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。
从流程上看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可以达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
用户线程使用select函数的伪代码为:
{
select(socket);
while (1) {
sockets = select();
for (socket in sockets) {
if (can_read(socket)) {
read(socket, buffer);
process(buffer);
}
}
}
}
异步IO
“真正”的异步IO需要操作系统更强的支持。在IO多路复用模型中,事件循环将文件句柄的状态事件通知给用户线程,由用户线程自行读取数据、处理数据。而在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。
举个例子
以上厕所为例。小明心里想上厕所。
同步阻塞模型的流程是这样:
小明告诉厕所管理员想上厕所,厕所管理员会检查厕所是否可用,可用的话就让小明上厕所,如果不可用,那就等到厕所可用了再让小明上厕所,这个过程中也不告诉小明厕所是不是可用,反正一旦小明告诉管理员后,小明必须等到厕所可用并且上完厕所才能离开,等到一半憋得难受不想等了,想溜,那是不可能的事情。小明此时内心OS,这得等到啥时候,也不让我走,这厕所不上了也不行。
同步非阻塞模型是这样:
小明告诉管理员想上厕所,厕所管理员会检查厕所是否可用,并告知小明厕所是否可用的情况。小明根据厕所管理员的回答,自行决定是否等待。如果小明想上厕所,他就必须不停地向厕所管理员进行询问,知道厕所可用。
IO多路复用是这样的:
小明除了想上厕所,还想使用打印机,还想去银行取钱,但是现在厕所有人,打印机有人用,银行人更多,都得等。那怎么办?IO多路复用是这样,小明把上厕所、用打印机、去银行取钱这三个请求都告诉一个超级牛批的管理员,这个管理员可以查看厕所能不能用,打印机能不能用,银行现在人是不是走光了可以取钱了。小明可以选择管理员的通知频率,比如让管理员每隔5分钟报告一次,是否可以办事,这个时候如果有可以办事的,管理员要告知,如果没有可以办事的,管理员依然要如实告知。然后小明就等着这个管理员的通知。这样小明相当于一下子在三个地方排了队,很是高级。
异步IO模型是这样的:
小明告诉管理员想上厕所,厕所管理员会登记一下小明的信息,等到厕所可用了就通知小明,小明就过来上厕所。这期间,小明可以忙其他的事情,比如,可以去吃饭。
IO问题的本质
IO问题的本质是,资源不能保证立即可用,等待资源是无法避免的事情。
IO问题的优化目标是,减少等待时间。
IO模型(另一角度)
参考博客
Linux下有一下几个经典的服务器模型:
PPC模型和TPC模型
PPC(process per connection)模型和TPC(Thread Per Connection)模型的设计思想类似,就是给每一个到来的连接都分配一个独立的进程或者线程来服务。对于这两种模型,都需要耗费较大的时间和空间资源
内核态用户态
所谓用户态到内核态的切换,实质是CPU的控制权从用户进程转移到内核进程。
尽管普通函数调用和内核函数调用,在代码中都是call。但是,普通函数调用不改变用户进程对CPU的控制权,内核函数调用从用户进程占有CPU变成内核进程占有CPU。
为什么要分内核态和用户态?
我想,支持内核态和用户态的操作系统,在操作系统运行的时候,都有一个内核进程在默默运行,这个内核进程负责用户进程的创建和销毁等工作。
既然有些操作可以让用户自己来操作,有些操作必须让内核来操作,那就得为不同的操作分配优先级了。这就是Ring0,1,2,3
。
标签:处置,小明,场景,用户,线程,内核,IO,厕所 来源: https://blog.csdn.net/qq_29421241/article/details/112028999