TLPI读书笔记第63章:IO多路复用4

63.3 信号驱动 I/O

在 I/O 多路复用中,进程是通过系统调用( select()或 poll())来检查文件描述符上是否可以执行 I/O 操作。而在信号驱动 I/O 中,当文件描述符上可执行 I/O 操作时,进程请求内核为自己发送一个信号。之后进程就可以执行任何其他任务直到 I/O 就绪为止,此时内核会发送信号给进程。要使用信号驱动 I/O,程序需要按照如下步骤来执行。

1.为内核发送的通知信号安装一个信号处理例程。默认情况下,这个通知信号为 SIGIO。

2.设定文件描述符的属主,也就是当文件描述符上可执行 I/O 时会接收到通知信号的进程或进程组。通常我们让调用进程成为属主。设定属主可通过 fcntl()的 F_SETOWN 操作来完成:

fcntl(fd,F_SETOWN,pid);

3.通过设定 O_NONBLOCK 标志使能非阻塞 I/O。

4.通过打开 O_ASYNC 标志使能信号驱动 I/O。这可以和上一步合并为一个操作,因为它们都需要用到 fcntl()的 F_SETFL 操作(见 5.3 节)。

flags=fcntl(fd,F_GETFL)
fcntl(fd,flags|O_NONBLOCK |O_ASYNC )

5.调用进程现在可以执行其他的任务了。当 I/O 操作就绪时,内核为进程发送一个信号,然后调用在第 1 步中安装好的信号处理例程。

6.信号驱动 I/O 提供的是边缘触发通知(见 63.1.1 节)。这表示一旦进程被通知 I/O 就绪,它就应该尽可能多地执行 I/O(例如尽可能多地读取字节)。 假设文件描述符是非阻塞式的,这表示需要在循环中执行 I/O 系统调用直到失败为止,此时错误码为 EAGAIN 或EWOULDBLOCK。 在 Linux 2.4 版及更早的时候,信号驱动 I/O 能应用于套接字、终端、伪终端以及其他特定类型的设备上。 Linux 2.6 版上信号驱动 I/O 还可以应用到管道和 FIFO 上。自 Linux 2.6.25版以来, inotify 文件描述符上也可以使用信号驱动 I/O 了。 在下面几页中,我们先给出一个使用信号驱动 I/O 的例子,然后详细解释上述这些步骤。

63.3.1 何时发送“I/O 就绪”信号

现在我们针对多种文件类型考虑何时会发送“ I/O 就绪”信号。

终端和伪终端

对于终端和伪终端,当产生新的输入时会生成一个信号,即使之前的输入还没有被读取也是如此。如果终端上出现文件结尾的情况,此时也会发送“输入就绪”的信号(但伪终端上不会)。对于终端来说没有“输出就绪”的信号。当终端断开连接时也不会发出信号。 从 2.4.19 版内核开始, Linux 对伪终端的从设备端提供了“输出就绪”的信号。当伪终端主设备侧读取了输入后就会产生这个信号。

管道和 FIFO

对于管道或 FIFO 的读端,信号会在下列情况中产生。

1.数据写入到管道中(即使已经有未读取的输入存在)。

2.管道的写端关闭。

对于管道或 FIFO 的写端,信号会在下列情况中产生。

1.对管道的读操作增加了管道中的空余空间大小,因此现在可以写入 PIPE_BUF 个字节而不被阻塞。

2.管道的读端关闭。

套接字

信号驱动 I/O 可适用于 UNIX 和 Internet 域下的数据报套接字。信号会在下列情况中产生。

1.一个输入数据报到达套接字(即使已经有未读取的数据报正等待读取)。

2.套接字上发生了异步错误。

信号驱动 I/O 可适用于 UNIX 和 Internet 域下的流式套接字。信号会在下列情况中产生。

1.监听套接字上接收到了新的连接。

2.TCP connect()请求完成,也就是 TCP 连接的主动端进入 ESTABLISHED 状态,如图61-5 所示。对于 UNIX 域套接字,类似情况下是不会发出信号的。

3.套接字上接收到了新的输入(即使已经有未读取的输入存在)。

4.套接字对端使用 shutdown()关闭了写连接(半关闭),或者通过 close()完全关闭。

5.套接字上输出就绪(例如套接字发送缓冲区中有了空间)。

6.套接字上发生了异步错误。

inotify 文件描述符

当 inotify 文件描述符成为可读状态时会产生一个信号—也就是由 inotify 文件描述符监 视的其中一个文件上有事件发生时。

63.3.2 优化信号驱动 I/O 的使用

在需要同时检查大量文件描述符(比如数千个)的应用程序中—例如某种类型的网络服务端程序—同 select()和 poll()相比,信号驱动 I/O 能提供显著的性能优势。信号驱动 I/O能达到这么高的性能是因为内核可以“记住”要检查的文件描述符,且仅当 I/O 事件实际发生在这些文件描述符上时才会向程序发送信号。 结果就是采用信号驱动 I/O 的程序性能可以根据发生的 I/O 事件的数量来扩展,而与被检查的文件描述符的数量无关。 要想全部利用信号驱动 I/O 的优点,我们必须执行下面两个步骤。

1.通过专属于 Linux 的 fcntl() F_SETSIG 操作来指定一个实时信号,当文件描述符上的I/O 就绪时,这个实时信号应该取代 SIGIO 被发送。

2.使用 sigaction()安装信号处理例程时,为前一步中使用的实时信号指定 SA_ SIGINFO标记(见 21.4 节)。 fcntl()的 F_SETSIG 操作指定了一个可选的信号,当文件描述符上的 I/O 就绪时会取代SIGIO 信号被发送。 F_GETSIG 操作完成的任务同 F_SETSIG 相反,它取回当前为文件描述符指定的信号

(为了在头文件<fcntl.h>中得到 F_SETSIG 和 F_GETSIG 的定义,我们必须定义测试宏_GNU_SOURCE。 ) 使用 F_SETSIG 来改变用于通知“I/O 就绪”的信号有两个理由,如果我们需要在多个文件描述符上检查大量的 I/O 事件,这两个理由都是必须的。

1.默认的“ I/O 就绪”信号 SIGIO 是标准的非排队信号之一。如果有多个 I/O 事件发送了信号,而 SIGIO 被阻塞了—也许是因为 SIGIO 信号的处理例程已经被调用了—除了第一个通知外,其他后序的通知都会丢失。如果我们通过 F_SETSIG 来指定一个实时信号作为“ I/O 就绪”的通知信号,那么多个通知就能排队处理。

2.如果信号处理例程是通过 sigaction()来安装,且在 sa.sa_flags 字段中指定了 SA_SIGINFO 标志,那么结构体 siginfo_t 会作为第二个参数传递给信号处理例程(见21.4 节)。这个结构体包含的字段标识出了在哪个文件描述符上发生了事件,以及事件的类型。

注意,需要同时使用 F_SETSIG 以及 SA_SIGINFO 才能将一个合法的 siginfo_t 结构体传递到信号处理例程中去。 如果我们做 F_SETSIG 操作时将参数 sig 指定为 0,那么将导致退回到默认的行为:发送的 信号仍然是 SIGIO,而且结构体 siginfo_t 将不会传递给信号处理例程。 对于“ I/O 就绪”事件,传递给信号处理例程的结构体 siginfo_t 中与之相关的字段如下。

1.si_signo: 引发信号处理例程得到调用的信号值。 这个值同信号处理例程的第一个参数一致。

2.si_fd:发生 I/O 事件的文件描述符。

3.si_code: 表示发生事件类型的代码。 该字段中可出现的值以及它们的描述参见表 63-7。

4.si_band:一个位掩码。其中包含的位和系统调用 poll()中返回的 revents 字段中的位相同。 如表 63-7 所示, si_code 中可出现的值同 si_band 中的位掩码有着一一对应的关系。

上一篇:动态代理(二)


下一篇:C. Alarm Clocks Everywhere---欧几里得算法的运用--- Educational Codeforces Round 63