> 文章列表 > Linux搭建Web服务器(一)——阻塞与非阻塞、同步与异步、Linux五种IO模型

Linux搭建Web服务器(一)——阻塞与非阻塞、同步与异步、Linux五种IO模型

Linux搭建Web服务器(一)——阻塞与非阻塞、同步与异步、Linux五种IO模型

目录

0x01 阻塞与非阻塞、同步与异步

阻塞与非阻塞

同步与异步

总结

0x02 Unix、Linux上的五种IO模型

阻塞(blocking)

非阻塞(non-blocking——NIO)

IO复用(IO multiplexing)

信号驱动(signal-driven)

异步(asynchronous)


0x01 阻塞与非阻塞、同步与异步

为了理清楚这几个概念,我们可以从数据就绪以及数据读写层面来看待这个问题。

阻塞与非阻塞

在数据就绪层面,需要考虑阻塞与非阻塞的问题。比如我们需要在操作系统中调用一个网络层的recv函数,这个函数可以设置为阻塞以及非阻塞,阻塞顾名思义是我们需要等待这个消息的传入到TCP数据缓冲区,否则我们将一直把这个线程挂起。如果设置为非阻塞,那么我们不管数据是否已经读取到,我们都会不断的去读取这个TCP缓冲区是否有数据到达,这个时候程序一直处于不断地去判断这个函数的返回值的状态中,这个时候一直在消耗着CPU的资源

ssize_t recv(int sockfd,void *buf,size_t len,int flags);
int size = recv(sockfd,buf,1024,0);

对于recv函数,这两种状态不一样在于其返回值的判断:如果这个时候出现了出错的状态,也就是变量size出现-1值得时候,这个时候我们需要去判断出现这个-1的情况所对应的类型,通过数据手册说明可以得到这个状态有如下:EINTR、EAGAIN、EWOULDBLOCK,对于非阻塞的情况,它会是处于一个EAGAIN的状况,对于阻塞的情况,则是EINTTR,一个会让线程继续接收消息,一个则会让线程直接退出状态。(这个处理会在后面的讲解中细说)

那么我们现在就可以很清楚的总结出阻塞与非阻塞的区别:

  • 阻塞和非阻塞这两个概念与程序(线程)等待消息通知(无所谓同步或者异步)时的状态有关。也就是说阻塞与非阻塞主要是程序(线程)等待消息通知时的状态角度来说的。

  • 阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。

  • 非阻塞调用指在不能立刻得到结果之前,不会改变线程的状态,可以通过返回值判断当前是否有数据可以进行读写。

同步与异步

  • 所谓同步,就是发出一个功能调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作。

  • 异步与同步相对,当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。

举个例子,在我们应用程序中,我们现在需要读取TCP缓冲区中的数据,这个时候我们需要自己调用一些接口去自行读取数据(具体实现取决于底层),当这些数据还未读取完时,我们不可以去进行下一步操作,这个过程我们称之为同步,这个时候数据的处理一直是应用程序在处理的。那么对于异步,这个时候我们设置一个异步接口,传入了我们需要读取的文件描述符、数据存储地址、通知方式(如signal信号等),之后我们就可以继续执行我们的程序了,直到有通知出现,我们才去取数据,这种无需我们自行等待数据的方式我们可以称之为异步,在数据处理的时候消耗的并非应用程序的时间,在异步的处理中,应用程序可以自己处理自己的事情。异步IO诸如:aio_read()/aio_write()(Linux异步IO接口)。

总结

在处理IO的时候,阻塞和非阻塞都是同步IO,只有使用了特殊的API才是异步IO。注意:IO多路复用技术都是同步。

一个典型的网络IO接口调用,分为两个阶段,分别是“数据就绪”和“数据读写”,数据就绪分为阻塞和非阻塞,表现的到的结果就是阻塞当前线程或直接返回。

同步表示A向B请求调用一个网络的IO接口时(或者调用某个业务逻辑API接口时),数据的读写都是由请求方A自己来完成的(不管是阻塞还是非阻塞);异步表示A向B请求调用一个网络接口时(或者调用某个业务逻辑API接口时),向B传入请求事件以及事件发生时通知的方式,A就可以处理其他逻辑了,当B监听到事件处理完成后,会用事先约定好的通知方式,通知A处理结果。

0x02 Unix、Linux上的五种IO模型

阻塞(blocking)

调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的去检查这个函数有没有返回,必须等这个函数返回才能进行下一步动作。

比如函数read、recv,这与文件描述符有关,直接可以去文件描述符中设置其阻塞或者非阻塞。这也是个同步IO的接口。以上这种方式只能一次检测一个事件,相当于是单线程。

非阻塞(non-blocking——NIO)

非阻塞等待,每隔一段时间就去检测IO事件是否就绪。没有就绪就可以做其他事。非阻塞I/O执行系统调 用总是立即返回,不管事件是否已经发生,若事件没有发生,则返回-1,此时可以根据 errno 区分这两种情况,对于accept,recv 和 send,事件未发生时,errno 通常被设置成 EAGAIN/EWOULDBLOCK

 

这也是一个同步的IO,数据处理等待还是在用户态。以上这种方式只能一次检测一个事件,相当于是单线程。

IO复用(IO multiplexing)

Linux 用 select/poll/epoll 函数实现 IO 复用模型,这些函数也会使进程阻塞,但是和阻塞IO所不同的是这些函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检测。直到有数据可读或可写时,才真正调用IO操作函数。

 这种类似于多进程多线程的处理。但是也是同步的机制。

信号驱动(signal-driven)

Linux 用套接口进行信号驱动 IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO事件就绪,进程收到SIGIO 信号,然后处理 IO 事件。

 内核在第一个阶段是异步,在第二个阶段是同步,因为这个时候的数据拷贝还是在用户空间进行的;与非阻塞IO的区别在于它提供了消息通知机制,不需要用户进程不断的轮询检查,减少了系统API的调用次数,提高了效率。

异步(asynchronous)

Linux中,可以调用 aio_read 函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。

 内核自动把数据传到用户空间所规定的存储空间中,无需用户自行拷贝数据。使用如下结构体进行设置:

/* Asynchronous I/O control block. */
struct aiocb
{int aio_fildes; /* File desriptor. */int aio_lio_opcode; /* Operation to be performed. */int aio_reqprio; /* Request priority offset. */volatile void *aio_buf; /* Location of buffer. */size_t aio_nbytes; /* Length of transfer. */struct sigevent aio_sigevent; /* Signal number and value. *//* Internal members. */struct aiocb *__next_prio;int __abs_prio;int __policy;int __error_code;__ssize_t __return_value;
#ifndef __USE_FILE_OFFSET64__off_t aio_offset; /* File offset. */char __pad[sizeof (__off64_t) - sizeof (__off_t)];
#else__off64_t aio_offset; /* File offset. */
#endifchar __glibc_reserved[32];
}