Unix/Linux 编程:网络编程之 IO模型
Posted sesiria
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unix/Linux 编程:网络编程之 IO模型相关的知识,希望对你有一定的参考价值。
一、阻塞IO(blocking)
主线程阻塞等待用户连接请求(accept阻塞)。等请求达到以后再调用(recv)系统调用接收数据,这时候又阻塞等待。
等内核将数据接受好并从内核空间拷贝到用户空间后,系统调用返回,表明数据读取完毕。
这个操作在等待IO的时候会将主线程挂起而不能执行其他操作,浪费了大量服务器性能。
二、多线程(blocking+multithread)
由于accept系统调用会对每个客户端的连接都返回一个fd,因此可以利用多线程的方法。主线程只负责accept处理连接请求。
在收到具体的请求以后 ,将客户请求的fd作为参数传给工作线程,工作线程会调用相应的线程函数来处理。
理解值观,在处理并发量不太高的时候效果可以。但是如果并发量特别高的情况下,线程的切换和调度会带来大量cpu资源的浪费(可能有大量的线程被阻塞再IO等待)并且创建线程的开销也不低。
三、非阻塞IO(no-blocking)
非阻塞IO在数据未准备好的情况下,recv等系统调用也能立即返回。而不会将整个主线程阻塞,主线程可以去处理其他操作。
然而需要主线程自行去检查每个IO队应的fd是否准备就绪(轮询select/poll或者等待系统通知epoll等)。
select/epoll的优势不是在于对于单个连接能处理得更块,而是在于能够处理更多的连接!
而传统的阻塞式的web模型,再处理连接数不是那么高的情况下,性能还是可以的。当连接数上去以后就会遇到性能瓶颈!因为大量的线程切换也是需要开销的。并且线程之间的数据同步,需要用到大量的锁等资源,还容易造成死锁等问题。
四,异步IO
asynchronous I/O Linux下的异步IO通常用于磁盘操作,不可用于网络。
真正的非阻塞,:异步IO在 调用read后是立即返回的,后续靠kernel的signal来进行相关的操作
而上面的非阻塞IO(No-blocking)仅仅是在数据准备阶段(没准备好)的阶段可以返回,在实际读取数据的时候比如调用read,整个线程还是会被阻塞的(其本质是synchronous IO)
五,信号驱动IO(signal driven IO)
给套接口(socket)设置一个信号驱动IO,并安装一个信号处理函数。进程继续运行并不阻塞。
当数据准备好以后,进程会接到一个信号,可以在信号处理函数中调用I/O操作函数来处理数据。
优势:等待数据到达(第一阶段)期间,进程可以继续执行,不会被阻塞。免去了select的阻塞于轮询。
noblocking模型于asynchronous 模型的主要区别:非阻塞模型虽然大部分时候都不会被阻塞(IO没准备好的时候)。但是需要靠轮询去check IO是否准备就绪(由主线程or工作线程去check)
而asynchronous模型则在注册了异步IO回调以后立即返回(不会阻塞当前线程),后续IO准备就绪也是由Kernel去调用实现注册回调函数。
六、服务器模型Reactor与Proactor
并于高并发编程,网络连接上的消息处理,可以分为两个阶段:等待消息准备好、消息处理。
当使用默认的阻塞套接字(blocking IO),一个主线程捆包多个工作线程。这导致高并发下线程会频繁的睡眠
高并发编程的方法是把两个阶段分开处理。即,等待消息准备好的代码与处理消息的代码是分离的。这也要求套接字是非阻塞的。
否则处理消息的代码段很容易导致条件不满足,又进入了睡眠的等待阶段。
解决方法:1 .线程主动查询。 2,设定一个工作线程去专门等待。(这就是IO多路复用)
I/O事件, 定时事件及信号,两种高效的事件处理模型:Reactor和Proactor.
1. Reactor模型
传统的调用机制是,线程调用某函数(阻塞)等待函数执行返回后讲控制器交给当前线程,后续继续执行。
Reactor逆置了事件的处理流程,应用程序需要提供相应的接口并注册到Reactor上,如果相应的事件发生,Reactor将主动调用应用程序注册的接口,这又称为“回调函数”
Reactor模式将处理IO事件注册到一个中心IO多复复用器上,同时主线程/进程阻塞在多路复用器上。
一旦有IO事件到来或是准备就绪(文件描述或sock可读,可写)。多路复用器返回并将实现注册的相应的IO事件分发到队应的处理器中。
Reactor模型一般有三个重要组件:
1). 多路复用器:一般是select,poll或者epoll
2).事件分发器: 将多路复用器中返回的就绪事件分到队应的处理函数中
3). 事件处理器:负责处理特定事件的处理函数。
优缺点:
*响应快,不必为单个同步事件所阻塞,虽然reactor本身依然是同步的;
*编程相对简单,避免复杂的多线程及同步问题,避免多线程切换的开销
*可扩展性,可以方便的通过增加Reactor实例的个数来充分利用CPU资源
*可复用,reactor框架本身与具体事件处理逻辑无关,具有高度复用性。
具体流程如下:
a. 注册读就绪事件(也可以是其他事件)和相应的事件处理器;
b. 事件分离器等待事件;
c. 事件到来,激活分离器,分离器调用事件队应的处理器;
d.
几种Reactor模型的应用:
Reference: http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf
1)单线程模式
这是最简单的单Reactor单线程模型。Reactor线程是个多面手,负责多路分离套接字,Accept新连接,并分派请求到处理器链中。该模型适用于处理器链中业务处理组件能快速完成的场景。不过这种单线程模型不能充分利用多核资源,所以实际使用的不多。
2)多线程模式
该模型在事件处理器(Handler)链部分采用了多线程(线程池),也是后端程序常用的模型。
3)多Reactor模式
比起第二种模型,它是将Reactor分成两部分,mainReactor负责监听并accept新连接,然后将建立的socket通过多路复用器(Acceptor)分派给subReactor。subReactor负责多路分离已连接的socket,读写网络数据;业务处理功能,其交给worker线程池完成。通常,subReactor个数上可与CPU个数等同。
2. Proactor模型
具体流程如下:
1. 处理器发起异步操作,并关注IO完成事件
2. 事件分离器等待操作完成事件
3.分离器等待的过程中,内核并行执行实际的IO操作,并将结果存入用户自定义的缓冲区,最后通知事件分离器读操作完成
4. IO完成后,通过事件分离器呼唤处理器
5. 事件处理器处理用户自定义缓冲区中的数据
Proactor的最大特点是异步IO。异步是由系统内核完成的。
Proactor可以在系统态将读写优化,利用IO并行能力,提供一个高性能的单线程模型。
3. 使用Reactor来模拟Proactor
1. 注册读事件(同时再提供一段缓冲区)
2. 事件分离器等待可读事件
3.事件到来,激活分离器,分离器(立即读数据,写缓冲区)调用事件处理器
4. 事件处理器处理数据,删除事件(需要再用异步接口注册)
比如boost.asio在windows上采用IOCP的Proactor,在linux上采用epoll实现的reactor来模拟proactor.
reactor来模拟proactor主要是通过开启一个工作线程(模拟系统异步IO)来完成的
1. 主线程往epoll内核事件表中注册socket上的读就绪事件。
2. 主线程调用epoll_wait等待socket上由数据可读。
3. 当socket上由数据可读时,epoll_wait通知主线程。主线程从socket循环读取数据,知道没有更多的数据可读,然后将读取到的数据封装成一个请求对象并插入请求队列。
4. 睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往epoll内核时间表中注册socket上的写就绪事件。
5. 主线程调用epoll_wait等待socket可写。
6. 当socket可写时,epoll_wait通知主线程。主线程往socket上写入服务器处理客户请求的结果。
Reactor与Proactor的关系
相同点:两个模式都是堆某个IO事件的事件通知(即告诉某个模块,这个IO操作可以进行或以及完成)。在结构上两者也有相同点:demultiplexor负责提交IO操作(异步),
查询设备是否可操作(同步), 当然条件满足时,就回调注册的处理函数。
不同点:异步情况下(Proactor),当回调注册处理函数时,表示IO已经处理完成;同步情况下(Reactor),回调注册的处理函数时,表示IO设备可以进行某个操作,交给注册的处理函数进行。
以上是关于Unix/Linux 编程:网络编程之 IO模型的主要内容,如果未能解决你的问题,请参考以下文章