网络IO模型专题(一)基本概念

问题的由来

每一个socket都是一个文件描述符。可以对其进行打开、读取、写入、关闭操作。

在服务器编程时,往往需要同时处理多个socket。每个socket对应了一个客户端。
这样的话,如何读写(特别是读)这些socket?这就是本文关注问题的由来,即:
对socket的读写方式——网络IO模型
* 注意,这里不要和accept阻塞搞混了。讨论IO模型的前提是已经建立了若干个socket,如何对这些socket读写才是IO模型关注的焦点。

* IO模型是一种策略,是一种方法论,并不是具体的实现。这些策略需要(并且可以)通过调用POSIX具体函数加以实现。

* POSIX支持这些IO模型的函数包括(但不限于)如下几类,

同步读写函数(IO函数),主要有:read()/write、readv()/writev()、recv()/send()、recvfrom()/writeto()、recvmsg()/sendmsg();

异步读写函数,主要有:aio_read()/aio_write()

监视函数(用与在读写之前监视socket状况),主要有:select()、pselect()、poll()、ppoll()、epoll();

下文为了方便讨论,我们选择recvmsg()作为IO函数的代表,select()作为监视函数的代表。

Linux的网络IO模型

根据《Linix网络编程》(宋/孙版)里面的介绍,我们可以知道,Linux网络IO模型有如下5种:

· 阻塞IO
· 非阻塞IO
· IO复用
· 信号驱动IO复用

· 异步IO

阻塞模型

含义:对某个socket进行读取的时候,如果内核没有收到足够的数据供用户进程读取,用户进程就阻塞,直到内核有足够的数据到达,内核就唤醒用户进程,用户进程完成读取操作。

优点:简单,实际上这是用的最多的IO方式。

缺点:低效。假如现在有100个客户端,对应了100个socket。这100个socket什么时候有数据到达是不知道的。这时候通常会采用轮询或者并发的方式。
假设采用轮询的方式,就是对100个socket进行读取尝试,没数据就阻塞,可想而知,进程长时间都处于阻塞状态,吞吐量非常小。
假设采用并发的方式,会产生大量进程去处理用户请求。而且大多数进程处于阻塞状态,浪费系统资源。

* 由于在Linux系统层面,线程实际上轻量级进程,这里就不区分进程线程了。

做法:通过ftncl函数将socket设置为阻塞模式,然后借助recvmsg函数对socket进行读取。伪代码如下:

* socket默认是阻塞方式的(无论是连接接入如accept还是数据读取recvmsg),可以通过fcntl函数实现对socket的控制使其变为非阻塞方式或者信号驱动方式。

while(true)
{
    for(int i=0;i<sizeof(sockets)/sizeof(sockets[0]);i++){
        data = recvmsg(sockets[i]);//这一步会导致阻塞
        handle(data);
    }
}

非阻塞模型

含义:非阻塞模型就是在对某个socket进行读取的时候,如果内核没有收到足够的数据供用户进程读取,就直接返回一个错误。

优点:也比较简单

缺点:由于socket大部分时间是没有数据可读的,这就意味着进程在很长时间内在空转。

做法:通过ftncl函数将socket设置为非阻塞模式,然后借助recvmsg函数对socket进行读取。伪代码如下

for(int i=0;i<sizeof(sockets)/sizeof(sockets[0]);i++){
	ftncl(sockets[i],no_block);
}
while(true){
    for(int i=0;i<sizeof(sockets)/sizeof(sockets[0]);i++){
        data = recvmsg(sockets[i]);//如果没有数据可读,直接返回,不会阻塞
        if(data==NULL)
            continue;
        handle(data);
    }
}

IO复用模型

含义:在一定的时间内对socket集合进行监视,待超时之后筛选出具备读写条件的socket,然后交给读写函数操作。

优点:IO复用模型通过把多个IO阻塞复用(集中)到同一个监视器(select)的阻塞上,使得进程处理多连接的能力显著增强。相对于暴力的多进程方案,IO复用模型的系统开销较小。

缺点:超时机制本身就是阻塞操作,仍然比较低效

做法:先使用select等监视函数对socket集合进行监视,得到符合要求的socket子集,然后再进行读写操作。伪代码如下

while(true){
    sockets_have_data = select(timeout,sockets); //这一步会阻塞,等待timeout时间之后将会返回具备读写条件的socket集合
    if(sockets_have_data == NULL)
	    continue;
	for(int i=0;i<sizeof(sockets_have_data)/sizeof(sockets[0]);i++){
	    data = recvmsg(sockets[i]);//由于前面已经监视,所以这里必定是有数据可读的
		handle(data)
	}
}

信号驱动IO模型

用户进程开始的时候注册一个信号处理的回调函数,用户进程继续执行。当信号发生时意味着有数据到来,当前进程被打断转入消息处理函数(即前面注册的回调函数),此时用户进程和借助recvmsg函数来完成数据读取。

异步IO模型(AIO)

用户进程开始的时候注册一个信号处理的回调函数,并提供一个用户内存空间作为缓冲区,用户程序继续执行。当信号发生时意味着数据已经写到了缓冲区(即用户内存),当前进程被打断转入消息处理函数,此时用户直接处理数据即可(不用再去内核读数据了)。

Linux对AIO的支持是从2.5开始的,在2.6里面已经是一个标准特性了。最常见的函数就是aio_read。

同步IO与异步IO

根据用户进程与内核协同方式来划分,所有的这些IO模型可以分为同步/异步两大类。

同步IO:用户进程与系统调用是串行的,用户进程发起系统调用后必须等待系统调用完成并返回结果,用户进程才能继续向后执行,这种IO方式即是同步IO。不难发现,阻塞IO、非阻塞IO、IO复用都是同步IO。

异步IO:用户进程与系统调用是并行的,用户进程发起系统调用后,系统调用立即返回,用户进程继续向后执行,内核有结果之后通过信号机制打断用户进程,用户进程转而进行消息处理,即进行IO操作,这种方式就是异步IO。信号驱动IO模型和异步IO模型都是异步IO。

* 其实信号驱动IO到底是同步IO还是异步IO,这个见仁见智。如果从信号机制打断用户进程这一点来看信号驱动IO当属于异步IO。但是信号驱动IO的在信号处理时,还是用同步的方式读取数据(还是借助类似recvmsg函数的IO函数从socket里面读取数据)。即如何得知有数据到来是异步的,但是读取数据是同步的,从这点来看又可以认为是同步IO

猜你喜欢

转载自blog.csdn.net/yongyu_it/article/details/80803043
今日推荐