UNP Part-2: Chapter 6. I/O Multiplexing: The select and poll Functions 的读书笔记。

这篇博客 的最后,我们对文章中的服务器-客户端模型保留了这么一个问题:客户端同时存在 socket 和 stdin 两种 I/O ,但是它处理发方式仅仅是「运行到哪就读取哪」,即所谓的「阻塞型 I/O」,不能及时处理另外一个 I/O 所输入的信息。

解决这一问题的方法是 I/O 复用 (I/O Multiplex)。

I/O 复用适用于以下场合:

  • 需要同时处理多个有关 I/O 的描述符(即上述的场景)
  • 需要同时处理多个套接字
  • 一个 TCP 服务器既要处理 listen 套接字,又要处理已连接的套接字
  • 一个服务器既要处理 UDP,又要处理 TCP

参考资料:

I/O模型

Unix 环境下的 I/O 模型:

  • 阻塞型 I/O (blocking I/O)
  • 非阻塞型 I/O (non-blocking I/O)
  • I/O 复用 (I/O Multiplexing): select, poll 函数
  • 信号驱动 I/O (Signal Driven I/O): SIGIO 信号
  • 异步 I/O (Asynchronous I/O): POSIX aio_xxx 系列函数

网络通信中,数据的传输一般经历 3 个层次:

+---------------+
|应用进程(用户态)|
+---------------+
|操作系统(内核态)|
+---------------+
| 网络硬件接口 |
+---------------+

一个输入操作一般分为 2 个过程:

  1. 等待数据准备好
  2. 从内核向进程复制数据

对于 socket 套接字上的输入操作,第一步是等待数据从网络中传达,当所等待的报文分组到达时,它会复制到内核中的某个缓冲区。第二步是把内核缓冲区的数据复制到应用进程。

PS: 有个叫「零拷贝」的知识点,可以在上述过程减少拷贝次数,具体措施是:将用户进程的部分地址空间与内核缓冲区建立内存映射(与进程通信中的共享内存类似)。

可以参考:Link1, Link2 .

阻塞型 I/O

常见的 stdin 都是阻塞型 I/O 。阻塞型 I/O 具体过程如下图所示。

如果通过阻塞 I/O 的方式调用 recvfrom ,该系统调用直到数据报文到达且复制到用户进程中,或者发生错误(例如被信号处理函数中断)才返回。

非阻塞 I/O

非阻塞 I/O 的具体行为是:相对于阻塞型 I/O 而言,当所请求的 I/O 操作需要阻塞时,非阻塞 I/O 模型不阻塞进程,而是返回一个错误,其过程如下图所示。

如果通过非阻塞 I/O 的方式调用 recvfrom,那么就要通过上图轮询 (Polling) 的方式,但显然这种方式是十分耗费 CPU 时间片。

while (recvfrom(sockfd, buf, len, flags, src_addr, addrlen))
{
if (errno == EWOULDBLOCK) continue;
else
{
// do something according to buf
}
}

I/O 复用

与 I/O 复用相关的 API 是 selectpoll ,这里通过讲解 select 函数和 poll 函数的具体行为来解释上图的过程。

select

函数原型:

/* According to POSIX.1-2001, POSIX.1-2008 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
// Returns: positive count of ready descriptors, 0 on timeout, –1 on error void FD_CLR(int fd, fd_set *set); // turn off the bit for fd in fdset
void FD_SET(int fd, fd_set *set); // turn on the bit for fd in fdset
void FD_ZERO(fd_set *set); // clear all bits in fdset
int FD_ISSET(int fd, fd_set *set); // is the bit for fd in fdset ?

timeval 的结构如下:

struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};

调用 select 会使进程阻塞,等待事件的到来,当且仅当下列 2 个条件发生时,进程唤醒:

  • 阻塞超过指定的时间 timeout
  • 描述符集合 fdset 中的任意一个或多个事件发生。

下面举例说明,所谓的「事件」是什么?

  • {1,4,5} 中的任意描述符准备好读;
  • {1,4,5} 中的任意描述符准备好写;
  • {1,4,5} 中的任意描述符有异常事件等待处理;

下面对每个参数进行解析。

timeval 有 3 种可能:

  • 空指针:永远等待下去,仅在有描述符准备好 I/O 时才返回;
  • 零值:不等待,两个字段均为 0 ,这种情况不会阻塞进程,select 检查描述符后返回,也是所谓的轮询方式。
  • 非零值:等待一段固定的时间,在有描述符准备好 I/O 时返回,但最多等待 timeval

fd_set 是一个结构体,成员是一串比特位(每个比特位代表描述符 fd 是否在这个集合当中),可以通过上面的 FD_XXX 系列函数来操作这些比特位。

readfds, writefds, exceptfds 三个参数指定让内核测试读、写和是否有异常的描述符集合,目前支持的异常事件有 2 个:

  • The arrival of out-of-band data for a socket. It will be described in more detail in Chapter 24. (Out-of-band Data 是一类特殊的数据,可参考 IBM Out-of-band Data
  • The presence of control status information to be read from the master side of a pseudo-terminal that has been put into packet mode. We do not talk about pseudo-terminals in this book. (与伪终端相关,UNP 一书不讨论这个,所以可忽略)

select 函数执行时会修改这三个 fd_set 参数,当它返回时,这 3 个 fd_set 里存放的就是已经就绪的描述符,因此可以通过 FD_ISSET 去检查哪些描述符已经就绪(如下面的代码所示)。

nfds 是待测试的描述符的个数,它的值是待测试的最大描述符 + 1,这样 [1, 2, ..., nfds-1] 都会被测试。

select 的主要作用是:在所有指定要求测试的描述符,只要有一个描述符有事件发生,那么阻塞在 select 上的进程就会被唤醒(即 select 函数返回),当 select 函数返回后,可以通过遍历 fdset,来找到就绪的描述符。

PS:「测试」一词的意思是让内核检测描述符是否有事件发生。

这是 I/O 复用模型的一个典型例子,通过 select 也就能够解决本文开头提出的问题。

例子

fd_set fd_in, fd_out;
struct timeval tv; // Reset the sets
FD_ZERO( &fd_in );
FD_ZERO( &fd_out ); // Monitor sock1 for input events
FD_SET( sock1, &fd_in ); // Monitor sock2 for output events
FD_SET( sock2, &fd_out ); // Find out which socket has the largest numeric value as select requires it
int largest_sock = sock1 > sock2 ? sock1 : sock2; // Wait up to 10 seconds
tv.tv_sec = 10;
tv.tv_usec = 0; // Call the select
int ret = select( largest_sock + 1, &fd_in, &fd_out, NULL, &tv ); // Check if select actually succeed
if ( ret == -1 )
// report error and abort
else if ( ret == 0 )
// timeout; no event detected
else
{
if ( FD_ISSET( sock1, &fd_in ) )
// input event on sock1 if ( FD_ISSET( sock2, &fd_out ) )
// output event on sock2
}
#

pselect

函数原型:

#include <sys/select.h>
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timespec *timeout,
const sigset_t *sigmask);
// Returns: count of ready descriptors, 0 on timeout, –1 on error

pselect 是有 POSIX 规范制定的,与 select 的区别如下:

  1. 时间的结构体不同(就是时间单位变了,需要跟 POSIX 的 API 对接上)
struct timespec {
long tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};

注意到 select 的时间参数是没有 const 修饰的,原因是 select 执行后可能会改变 timeval 的值,改为还有多少时间剩余,而 pselect 不会修改 timeout 。

  1. 增加参数 sigmask

如果 sigmask 为空,那么 pselectselect 的行为一致。

sigmask 是一个信号集合(其实就是一个比特位表示一个信号),用于指定进程的屏蔽信号,完成后恢复。它其实相当于 select 的下列操作:

sigset_t originmask;
sigset_t sigmask;
sigprocmask(SIG_SETMASK, &sigmask, &originmask); // save the original signal mask
int ret = select(nfds, readfs, writefds, exceptfds, timeout);
sigprocmask(SIG_SETMASK, &originmask, NULL); // revert the signal mask of current process

poll

函数原型:

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

pollfd 结构体:

struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};

poll 的功能与 select 类似,也是等待一组描述符中的一个成为就绪状态,但工作方式不一样。

此外, poll 可以通过 events 指定某个 fd 的事件,而 revents 是作为返回值使用的。

事件也是通过比特位来表示的,具体如下图所示。

例子:

// The structure for two events
struct pollfd fds[2]; // Monitor sock1 for input
fds[0].fd = sock1;
fds[0].events = POLLIN; // Monitor sock2 for output
fds[1].fd = sock2;
fds[1].events = POLLOUT; // Wait 10 seconds
int ret = poll( &fds, 2, 10000 );
// Check if poll actually succeed
if ( ret == -1 )
// report error and abort
else if ( ret == 0 )
// timeout; no event detected
else
{
// If we detect the event, zero it out so we can reuse the structure
if ( fds[0].revents & POLLIN )
fds[0].revents = 0;
// input event on sock1 if ( fds[1].revents & POLLOUT )
fds[1].revents = 0;
// output event on sock2
}

小结

selectepoll 的区别如下:

  1. select 能处理的最大连接,默认是 1024 个,可以通过修改配置来改变,但终究是有限个;而 poll 理论上可以支持无限个;
  2. selectpoll 在管理海量的连接时,会频繁的从用户态拷贝到内核态,比较消耗资源。
  3. 如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select

其实,epoll 也是 I/O 复用的重要知识点,但 UNP 这一章节没提到,后面有时间再整理这一部分内容。

信号驱动 I/O

这里需要信号相关知识,可以参考 APUE 一书的第 10 章。

信号驱动 I/O :内核在描述符数据就绪时发送 SIGIO 信号通知进程。

首先自定义 I/O 处理函数 sigio_handler ,通过 signal(SIGIO, sigio_handler) 指定当 SIGIO 信号发生时,执行该信号处理程序。

我们知道,信号这一机制本身就是异步的,那么信号驱动 I/O 其实也能算是一种特殊的「异步 I/O」,但与 POSIX 的异步 I/O 有所不同,下面会提到这一点。

异步 I/O

异步 I/O 是由 POSIX 规范来定义的,与之相关的 API 是 aio_xxx 系列函数。

与信号驱动 I/O 的区别是:信号驱动 I/O 是内核通知进程何时可以启动 I/O 操作;而 POSIX 的异步 I/O 是内核通知进程 I/O 操作何时完成,如下图所示。

在信号驱动 I/O 中,需要我们主动去调用 recvfrom ,但在这里的异步 I/O 中,这一操作也是通过 aio_read 来完成的。

aio_read

函数原型:

#include <aio.h>
int aio_read(struct aiocb *aiocbp);

调用该函数会立即返回,进程不会阻塞。

可通过参数向 aio_read 传递描述符、缓冲区指针、缓冲区大小(与 read 函数类似)和文件偏移量(与 lssek 类似),并告诉内核 I/O 操作完成时如何通知进程。

在 GUN Lib 下,aiocb 结构定义为:

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. */
#endif
char __glibc_reserved[32];
};

I/O 模型比较

  • 同步 I/O:将数据从内核缓冲区复制到应用进程缓冲区的阶段(第二阶段),应用进程会阻塞。
  • 异步 I/O:第二阶段应用进程不会阻塞。

上述介绍的前 4 种,阻塞型、非阻塞型、I/O 复用、信号驱动都是属于同步 I/O,因为真正调用 recvfrom 执行 I/O 操作的时候可能会阻塞进程。

[UNP] IO 复用的更多相关文章

  1. Linux中的IO复用接口简介(文件监视?)

    I/O复用是Linux中的I/O模型之一.所谓I/O复用,指的是进程预先告诉内核,使得内核一旦发现进程指定的一个或多个I/O条件就绪,就通知进程进行处理,从而不会在单个I/O上导致阻塞. 在Linux ...

  2. 第十七篇:IO复用之select实现

    前言 在看过前文:初探IO复用后,想必你已对IO复用这个概念有了初步但清晰的认识. 接下来,我要在一个具体的并发客户端中实现它(基于select函数),使得一旦服务器中的客户进程被终止的时候,客户端这 ...

  3. IO复用之select实现

    前言 在看过前文:初探IO复用后,想必你已对IO复用这个概念有了初步但清晰的认识.接下来,我要在一个具体的并发客户端中实现它( 基于select函数 ),使得一旦服务器中的客户进程被终止的时候,客户端 ...

  4. Linux网络编程-IO复用技术

    IO复用是Linux中的IO模型之一,IO复用就是进程预先告诉内核需要监视的IO条件,使得内核一旦发现进程指定的一个或多个IO条件就绪,就通过进程进程处理,从而不会在单个IO上阻塞了.Linux中,提 ...

  5. Libevent的IO复用技术和定时事件原理

    Libevent 是一个用C语言编写的.轻量级的开源高性能网络库,主要有以下几个亮点:事件驱动( event-driven),高性能;轻量级,专注于网络,不如 ACE 那么臃肿庞大:源代码相当精炼.易 ...

  6. IO复用三种方式

    简介 IO复用技术,简单来说就是同时监听多个描述符.在没有用到IO复用以前,只能是一个线程或一个 线程去监听,服务端同时有多个连接的时候,需要创建多个线程或者进程.而且,并不是所有的连 接是一直在传输 ...

  7. IO复用_select函数

    select函数: #include <sys/select.h> #include <time.h> #include <sys/types.h> #includ ...

  8. 简单聊下IO复用

    没图,不分析API Java中IO API的发展:Socket -> SocketChannel -> AsynchronousSocketChannelServerSocket -> ...

  9. IO复用(Reactor模式和Preactor模式)——用epoll来提高服务器并发能力

    上篇线程/进程并发服务器中提到,提高服务器性能在IO层需要关注两个地方,一个是文件描述符处理,一个是线程调度. IO复用是什么?IO即Input/Output,在网络编程中,文件描述符就是一种IO操作 ...

随机推荐

  1. 继承自List<T>的类通过NewtonJson的序列化问题

    什么问题? NewtonSoft.Json是我们最常用的Json组件库之一了.这里来讨论下使用NewtonSoft.Json序列化List<T>子类的情景.序列化使用了类JsonSeria ...

  2. office响应慢,但电脑速度没问题的解决方案

    看了非常多教程都没有用,有点崩,最后终于解决了,记录一下. 问题描述 :office启动没问题,但word打开后,选中一段文字非常慢,大概延迟鼠标移动2-3秒.点击工具栏时也有延迟(点击动画看的十分清 ...

  3. java 提供了哪些IO方式

    今天听了杨晓峰老师的java 36讲,感觉IO这块是特别欠缺的,所以讲义摘录如下: 欢迎大家去订阅: 本文章转自:https://time.geekbang.org/column/article/83 ...

  4. Python_变量作用域与修改

    引用全局变量,不需要golbal声明,修改全局变量,需要使用global声明,特别地,列表.字典等如果只是修改其中元素的值(而不是整体赋值的形式),可以直接使用全局变量,不需要global声明. 参考 ...

  5. 【非原创】codeforces 1060E Sergey and Subway 【树上任意两点距离和】

    学习博客:戳这里 本人代码: 1 #include <bits/stdc++.h> 2 using namespace std; 3 typedef long long ll; 4 con ...

  6. 宝塔面板&FLASK&centos 7.2 &腾讯云 配置网站出现的若干问题

    1.解决跨域问题&&中文显示问题 from flask import Flask, url_for, request, render_template, redirect from f ...

  7. 如何在ASP.NET Core 中使用IHttpClientFactory

    利用IHttpClientFactory可以无缝创建HttpClient实例,避免手动管理它们的生命周期. 当使用ASP.Net Core开发应用程序时,可能经常需要通过HttpClient调用Web ...

  8. Vue & Sentry & ErrorHandler

    Vue & Sentry & ErrorHandler import * as Sentry from '@sentry/browser'; import { Vue as VueIn ...

  9. CSS 弹性盒子模型

    CSS 弹性盒子模型 https://www.w3.org/TR/2016/CR-css-flexbox-1-20160526/ CSS Flexible Box Layout Module Leve ...

  10. React Hooks & Context API

    React Hooks & Context API responsive website https://reactjs.org/docs/hooks-reference.html https ...