Unix/Linux编程:信号驱动IO

信号驱动IO

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

  1. 为内核发送的通知信号安装一个信号处理例程。默认情况下,这个通知信号为SIGIO
  2. 设定文件符的属主,也就是当文件描述符山可执行IO时会接收通知信号的进程或进程组。通常我们让调用进程成为属主。设定属主可通过 fcntl()的 F_SETOWN 操作来完成
fcntl(fd, F_SETOWN , pid);
  1. 通过设定O_NONBLOCK标识使能非阻塞IO
  2. 通过打开O_ASYNC标志使能信号驱动IO。这可以和上一步合并为一个操作,因为它们都需要用到 fcntl()的 F_SETFL 操作
flags = fcntl(fd, F_GETFL);  // get current flags
fcntl(fd, F_SETFL, flags | O_ASYNC | O_NONBLOCK);
  1. 调用进程线程可以执行其他任务了。当IO操作就绪时,内核为进程发送一个信号,然后调用在第1步中安装好的信号处理例程
  2. 信号驱动IO提供的是边缘触发通知。这表示一旦进程被通知IO就绪,它就应该尽可能的能多地执行 I/O(例如尽可能多地读取字节)。假设文件描述符时非阻塞的,这表示需要在循环中执行IO系统调用直到失败位置,此时的错误码为EAGAIN(再来一次)或者EWOULDBLOCK(期望阻塞)

在 Linux 2.4 版及更早的时候,信号驱动 I/O 能应用于套接字、终端、伪终端以及其他特定类型的设备上。Linux 2.6 版上信号驱动 I/O 还可以应用到管道和 FIFO 上。自 Linux 2.6.25版以来,inotify 文件描述符上也可以使用信号驱动 I/O 了。

  • 历史上,信号驱动 I/O 有时也被称为异步 I/O,这一点从相关的打开文件标志(O_ASYNC)中就能看出来。但是,如今术语异步 I/O 是用来表示由 POSIX AIO 规范所提供的功能。使用POSIX AIO 时,进程请求内核执行一次 I/O 操作,内核启动该操作之后立刻将控制权还给调用进程,稍后当 I/O 操作完成或有错误发生时,该进程会得到通知。
  • 其他一些 UNIX 实现,尤其是比较老的实现中并没有在 fcntl()中定义 O_ASYNC 常量。相反,这个常量被命名为 FASYNC,而 glibc 将这个名字定义为 O_ASYNC 的别名。

下面程序在标准输入上使能信号驱动 I/O,之后将终端置为 cbreak 模式,这样每次输入只会有一个字符。之后程序进入无限循环,所做的工作就是递增变量 cnt,同时等待输入就绪。当有输入存在时,SIGIO 信号处理例程就设定一个标志 gotSigio,该标志由主程序监控。当主程序看到该标志被设定后,就读取所有存在的输入字符并将它们连同变量cnt 的当前值一起打印出来。如果输入中读取到了井字符(#),程序就退出

看个例子

#include <signal.h>
#include <ctype.h>
#include <fcntl.h>
#include <termios.h>

static volatile sig_atomic_t gotSigio = 0;

static void sigioHandler(int sig){
    gotSigio = 1;
}
int ttySetCbreak(int fd, struct termios *prevTermios)
{
    struct termios t;

    if (tcgetattr(fd, &t) == -1)
        return -1;

    if (prevTermios != NULL)
        *prevTermios = t;

    t.c_lflag &= ~(ICANON | ECHO);
    t.c_lflag |= ISIG;

    t.c_iflag &= ~ICRNL;

    t.c_cc[VMIN] = 1;                   /* Character-at-a-time input */
    t.c_cc[VTIME] = 0;                  /* with blocking */

    if (tcsetattr(fd, TCSAFLUSH, &t) == -1)
        return -1;

    return 0;
}
int main(int argc, char *argv[]){
    int flags, j, cnt;
    struct termios origTermios;
    char ch;
    struct sigaction sa;
    bool done;

    /* 为  "I/O possible" 信号建立处理程序 */
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    sa.sa_handler = sigioHandler;
    if (sigaction(SIGIO, &sa, NULL) == -1){
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    /* 设置接收"I/O possible" 信号的所有者进程  */
    if (fcntl(STDIN_FILENO, F_SETOWN, getpid()) == -1){
        perror("fcntl  F_SETOWN");
        exit(EXIT_FAILURE);
    }

    /* 启用“I/Opossible”信号并使文件描述符的I/O非阻塞 */
    flags = fcntl(STDIN_FILENO, F_GETFL);
    if (fcntl(STDIN_FILENO, F_SETFL, flags | O_ASYNC | O_NONBLOCK) == -1)
    {
        perror("fcntl  F_SETFL");
        exit(EXIT_FAILURE);
    }

    if (ttySetCbreak(STDIN_FILENO, &origTermios) == -1)
    {
        perror("ttySetCbreak");
        exit(EXIT_FAILURE);
    }

    for (done = false, cnt = 0; !done ; cnt++) {
        for (j = 0; j < 100000000; j++)
            continue;                   /* Slow main loop down a little */

        if (gotSigio) {                 /* Is input available? */
            gotSigio = 0;

            /* Read all available input until error (probably EAGAIN)
               or EOF (not actually possible in cbreak mode) or a
               hash (#) character is read */

            while (read(STDIN_FILENO, &ch, 1) > 0 && !done) {
                printf("cnt=%d; read %c\n", cnt, ch);
                done = ch == '#';
            }
        }
    }

    /* Restore original terminal settings */

    if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &origTermios) == -1)
    {
        perror("tcsetattr TCSAFLUSH");
        exit(EXIT_FAILURE);
    }
    exit(EXIT_SUCCESS);
}

下面是当我们运行该程序时会看到的输出,我们输入字符 x 多次,最后跟着一个井字符(#)
在这里插入图片描述

在启动信号驱动IO前安装信号处理例程

由于接收到 SIGIO 信号的默认行为是终止进程运行,因此我们应该在启动信号驱动 I/O 前先为 SIGIO 信号安装处理例程。如果我们在安装 SIGIO 信号处理例程之前先启动了信号驱动I/O,那么会存在一个时间间隙,此时如果 I/O 就绪的话内核发送过来的 SIGIO 信号就会使进程终止运行

设定文件描述符属主

我们使用 fcntl()来设定文件描述符的属主,方式如下

fcntl(fd, F_SETOWN, pid);

我们可以指定一个单独的进程或者是进程组中的所有进程在文件描述符 I/O 就绪时收到信号通知。如果参数 pid 为正整数,就解释为进程 ID 号。如果参数 pid 是负数,它的绝对值就指定了进程组 ID 号

通常会在pid中指定调用进程的进程ID号(这样信号就会发送给打开这个文件描述符的进程)。但是,也可以将其指定为另一个进程或者进程组,而信号会发送给这个目标。fcntl()的F_GETOWN操作会返回接收到信号的进程或者进程组:

id = fcntl(fd, F_GETOWN);
if(id == -1){
	exit(1);
}

进程组 ID 号小于 4096 时的 F_GETOWN 可能会遇到一些问题,可以通过在用户空间使用 F_GETOWN_EX解决。

何时发送IO就绪信号

中途挂断和伪终端

对于终端和伪终端,当产生新的输入时会生成一个信号,即使之前的输入还没有被读取。如果终端上出现文件结尾的情况,此时也会发送“输入就绪”的信号(但伪终端不会)

对于终端来说没有“输出就绪”的信号。当终端断开连接上也不会发出信号

从2.4.19版内核开始,Linux对伪终端的从设备提供了“输出就绪”的信号。当伪终端主设备侧读取了输入后就会产生这个信号。

管道和FIFO

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

  • 数据写入到管道中(即使已经有未读取的输入存在)
  • 管道的写端关闭

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

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

套接字

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

  • 一个输入数据报到达套接字(即使已经有未读取的数据报正等待读取)
  • 套接字上发生了异步错误

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

  • 监听套接字上接收到了新的连接
  • TCP connect()请求完成,也就是 TCP 连接的主动端进入 ESTABLISHED 状态。对于 UNIX 域套接字,类似情况下是不会发出信号的。
  • 套接字上接收到了新的输入(即使已经有未读取的输入存在)
  • 套接字对端使用shutdonw()关闭了连接(半关闭)或者通过close()完全关闭
  • 套接字上输出就绪(例如套接字发送缓冲区中有了空间)
  • 套接字上发生了异步错误

inotify文件描述符

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

优化信号驱动IO的使用

当需要同时检查大量文件描述符的应用程序中,同select和poll相比,信号驱动IO你那个提供限制的性能优势。信号驱动IO能到到这么高的性能是因为内核可以“记住”要检查的文件描述符,而且仅当IO事件实际发生在这些文件描述符上时才会向程序发送信号。结果就是采用信号驱动IO的程序性能可以根据发生的IO事件的性能来扩展,而与被检查的文件描述符数量无关。

要想全部利用信号驱动IO的优点,我们必须执行如下两个步骤:

  • 通过专属于Linux的fcntl() F_SETSIG操作来指定一个实时信号,当文件描述符上的IO就绪时,这个实时信号应该取代SIGIO被发送
  • 使用 sigaction()安装信号处理例程时,为前一步中使用的实时信号指定 SA_ SIGINFO标记(收到信号时处理器函数可以获取该信号的一些附加信息)

fcntl的F_SETSIG操作指定了一个可选的信号,当文件描述符上的IO就绪时会取代SIGIO信号被发送:

if(fcntl(fd, F_SETSIG, sig) == -1){
	exit(1);
}

F_GETSIG 操作取回当前为文件描述符指定的信号

sig = fcntl(fd, F_GETSIG);
if(sig == -1){
	exit(1);
}

(为了在头文件<fcntl.h>中得到 F_SETSIG 和 F_GETSIG 的定义,我们必须定义测试宏_GNU_SOURCE。

使用F_SETSIG来改变用于通知“IO就绪”的信号有两个理由,如果我们需要在多个文件描述符上检测大量的IO时间,这两个理由都是必须的:

  • 默认的“IO就绪”信号SIGIO是标准的非排队信号之一。如果有多个IO事件发送了信号,而SIGIO被阻塞了------也许是因为SIGIO信号的处理例程已经被调用了------处理第一个通知外,其他后续的通知都会丢失。如果我们通过F_SETSIG来指定一个实时信号作为“IO就绪”的通知信号,那么多个通知就能被处理
  • 如果信号处理例程是通过sigaction来安装,而且在sa。sa_flags字段中指定了SA_SIGINFO标志,那么结构体siginfo_t会作为第二个参数传递给信号处理例程。这个结构体包含的字段标识出了在哪个文件描述符上发生了事件,以及事件的类型。

注意,需要同时使用 F_SETSIG 以及 SA_SIGINFO 才能将一个合法的 siginfo_t 结构体传递到信号处理例程中去。

如果我们做 F_SETSIG 操作时将参数 sig 指定为 0,那么将导致退回到默认的行为:发送的信号仍然是 SIGIO,而且结构体 siginfo_t 将不会传递给信号处理例程。

对于“I/O 就绪”事件,传递给信号处理例程的结构体 siginfo_t 中与之相关的字段
如下。

  • si_signo:引发信号处理例程得到调用的信号值。这个值同信号处理例程的第一个参数一致
  • si_fd:发生 I/O 事件的文件描述符。
  • si_code:表示发生事件类型的代码
  • si_band:一个位掩码。其中包含的位和系统调用 poll()中返回的 revents 字段中的位相同。如下表 所示,si_code 中可出现的值同 si_band 中的位掩码有着一一对应的关系
si_codesi_band 掩码值描 述
POLL_INPOLLIN | POLLRDNORM存在输入;文件结尾情况
POLL_OUTPOLLOUT | POLLWRNORM | POLLWRBAND可输出
POLL_MSG POLLINPOLLRDNORMPOLLMSG 存在输出消息(不使用)
POLL_ERRPOLLERRI/O 错误
POLL_PRIPOLLPRI | POLLRDNORM存在高优先级输入
POLL_HUPPOLLHUP | POLLERR出现宕机

在一个纯输入驱动的应用程序中,我们可以进一步优化使用F_SETSIG。我们可以阻塞发出的“IO就绪”信号,然后通过sigwaitinfo()或者sigtimedwait()来接收排队中的信号。这些系统调用返回的siginfo_t结构体所包含的信息同传递给信号处理例程的siginfo_t结构体一样。。以这种方式接收信号,我们实际是以同步的方式在处理事件,但同 select()和 poll()相比,这种方法能够高效地获知文件描述符上发生的 I/O 事件

信号队列溢出的处理

可以排队的实时信号的数量是有限的。如果达到这个上限,内核对于“I/O 就绪”的通知将恢复为默认的 SIGIO 信号。出现这种现象表示信号队列溢出了。当出现这种情况时,我们将失去有关文件描述符上发生 I/O 事件的信息,因为 SIGIO 信号是不
会排队的。(此外,SIGIO 的信号处理例程不接受 siginfo_t 结构体参数,这意味着信号处理例程不能确定是哪一个文件描述符上产生了信号)。

我们可以通过增加可排队的实时信号数量的限制来减小信号队列溢出的可能性。但是这并不能完全消除溢出的可能。一个设计良好的采用 F_SETSIG 来建立实时信号作为“I/O 就绪”通知的程序必须也要为信号 SIGIO 安装处理例程。如果发送了 SIGIO 信号,那么应用程序可以先通过 sigwaitinfo()将队列中的实时信号全部获取,然后临时切换到 select()或 poll(),通过它们获取剩余的发生 I/O 事件的文件描述符列表

在多线程程序中使用信号驱动 I/O

从 2.6.32 版内核开始,Linux 提供了两个新的非标准的 fcntl()操作,可用于设定接收“I/O 就绪”信号的目标,它们是 F_SETOWN_EX 和 F_GETOWN_EX

F_SETOWN_EX 操作类似于 F_SETOWN,但除了允许指定进程或进程组作为接收信号的目标外,它还可以指定一个线程作为“I/O 就绪”信号的目标。对于这个操作,fcntl()的第三个参数为指向如下结构体的指针。

struct f_owner_ex{
	int type;
	pid_t pid;
};

结构体中 type 字段定义了 pid 的类型,它可以有如下几种值。

  • F_OWNER_PGRP :字段 pid 指定了作为接收“I/O 就绪”信号的进程组 ID。与 F_SETOWN 不同的是,这里进程组 ID 指定为一个正整数。
  • F_OWNER_PID :字段 pid 指定了作为接收“I/O 就绪”信号的进程 ID。
  • F_OWNER_TID :字段 pid 指定了作为接收“I/O 就绪”信号的线程 ID。这里 pid 的值为 clone()或getpid()的返回值。

F_GETOWN_EX 为 F_SETOWN_EX 的逆操作。它使用 fcntl()的第三个参数所指向的结构体 f_owner_ex 来返回之前由 F_SETOWN_EX 操作所定义的设置

总结

信号驱动 I/O 允许一个进程在文件描述符处于 I/O 就绪态时接收到一个信号。要使用信号驱动 I/O,我们必须为 SIGIO 信号安装一个信号处理例程,设定接收信号的属主进程,并在打开文件时设定 O_ASYNC 标志使得信号可以生成。相比 I/O 多路复用,当监视大量的文件描述符时信号驱动 I/O 有着显著的性能优势。Linux 允许我们修改用来通知的信号,而如果我们采用实时信号的话,那么多个信号通知就可以排队处理。信号处理例程可以使用 siginfo_t 参数来确定产生信号的文件描述符以及发生事件的类型。

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值