Linux信号驱动I/O 学习记录

来源:互联网 发布:neo4j rest api java 编辑:程序博客网 时间:2024/05/17 22:24

Q:什么是信号驱动I/O?
A:对于给定的I/O口(一般就是对于文件描述符)设定为信号驱动I/O,则当I/O口准备好之后(读:有数据可读;写:有空间可写),向注册它的进程发送事先约定好的信号,进程收到信号后触发signal handler进行I/O处理。

Q:Linux下信号驱动I/O的注册方法?
A:系统设定两种信号为专用信号:SIGIO和SIGURG,这两种都是非实时、不可靠信号,不能参加排队。前者是通用信号,后者则专门用于网络传输中带外数据(紧急数据)的I/O,本文不涉及。具体步骤如下:
1. 对文件描述符调用fcntl函数,设置O_NONBLOCK和O_ASYNC(只能对终端和网络的文件描述符使用)属性;
fcntl(fd, F_SETFL, old | O_NONBLOCK | O_ASYNC)
2. 对文件描述符设置SIGIO、SIGURG信号所通知的进程,一般是调用进程本身;
fcntl(fd, F_SETOWN, getpid())
3. 编写signal handler函数,并使用signal或sigaction函数注册。

这就是APUE书上所说的BSD异步I/O方法。严格的说信号驱动I/O不是异步的I/O,因为它I/O的操作本身仍然是同步的(需要进程主动去读取或写入),只是使用了signal这种异步机制通知进程什么时候该读写了,属于伪异步。

实验一:将标准输入注册为信号驱动I/O

    #include <stdio.h>    #include <stdlib.h>    #include <unistd.h>    #include <errno.h>    #include <fcntl.h>    #include <signal.h>    void sigio_handler(int sig)    {        static int cnt = 0;        printf("receive SIGIO signal %d\n", ++cnt);        fflush(stdin);        fflush(stdout);    }    int main(int argc, char **argv)    {        struct sigaction sig_act;        int fl = 0;        //signal driven I/O setting        fflush(stdin);        fflush(stdout);        fl = fcntl(STDIN_FILENO, F_GETFL, 0);        fcntl(STDIN_FILENO, F_SETFL, fl | O_NONBLOCK | O_ASYNC );        fcntl(STDIN_FILENO, F_SETOWN, getpid());        sigemptyset(&sig_act.sa_mask);        sig_act.sa_flags = 0;        sig_act.sa_handler = sigio_handler;        sigaction(SIGIO, &sig_act, NULL);        printf("test start\n");        fflush(stdin);        fflush(stdout);        while (1)        {            sleep(1);        }        return 0;    }

实验结果:循环打印signal handler中的语句,如下:

test startreceive SIGIO signal 1receive SIGIO signal 2receive SIGIO signal 3receive SIGIO signal 4receive SIGIO signal 5receive SIGIO signal 6receive SIGIO signal 7receive SIGIO signal 8receive SIGIO signal 9receive SIGIO signal 10receive SIGIO signal 11receive SIGIO signal 12receive SIGIO signal 13receive SIGIO signal 14receive SIGIO signal 15receive SIGIO signal 16receive SIGIO signal 17receive SIGIO signal 18...

为什么会这样?在stdin没有键入任何字符,在注册为信号驱动I/O之前有过冲洗stdin/stdout,所以stdin不应该在准备好的状态,不会触发SIGIO信号。推测是因为在stdout输出了内容而触发了SIGIO信号,进而调用signal handler,而其中又有对stdout的输出,这样形成了死循环。所以,进行以下代码测试。

实验二

在循环部分中,每次向stdout输出一个’t’,循环十次;而signal handler部分将打印输出在文件中,以避免对stdout的干扰,查看文件打印内容来确定SIGIO信号触发的次数。

#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <errno.h>#include <fcntl.h>#include <signal.h>#include <string.h>#define BSZ 64FILE *ftp = NULL;void sigio_handler(int sig){    static int cnt = 0;    char buf[BSZ];    memset(buf, 0, BSZ);    fprintf(ftp, "receive SIGIO signal %d\n", ++cnt);    fflush(stdin);    fflush(stdout);}int main(int argc, char **argv){    struct sigaction sig_act;    char buf[BSZ];    int fl = 0;    int i = 0;    if ((ftp = fopen("file4.tmp", "a")) < 0)    {        printf("fopen error\n");        exit(1);    }    memset(buf, 0, BSZ);    //signal driven I/O setting    fflush(stdin);    fflush(stdout);    fl = fcntl(STDIN_FILENO, F_GETFL, 0);    fcntl(STDIN_FILENO, F_SETFL, fl | O_NONBLOCK | O_ASYNC );    fcntl(STDIN_FILENO, F_SETOWN, getpid());    sigemptyset(&sig_act.sa_mask);    sig_act.sa_flags = 0;    sig_act.sa_handler = sigio_handler;    sigaction(SIGIO, &sig_act, NULL);    while (i < 10)    {        sleep(1);        putchar('t');            i++;    }    fclose(ftp);    return 0;}

现象:进程休眠10s之后输出10个’t’,然后结束;检查file4.tmp文件没有输出,说明没有触发SIGIO信号。由于stdout是行缓冲机制,只有在缓冲区满、遇到换行符或者显式调用fflush函数才会输出到设备上。本例中,每次循环都将一个’t’放入stdout缓冲区,但并没有输出到设备;等到进程结束阶段,自动冲洗所有标准流,才将结果输出在显示屏上。

那么,是不是要进行冲洗才会触发SIGIO信号呢?

实验三

进行了如下三组实验:
1. 每次循环仅仅是对stdou进行fflush,而不在stdout输出任何字符;
2. 每次循环在stdout输出’t’,并同时用fflush进行冲洗;
3. 每次循环在stdout输出”ttttt”,并同时用fflush进行冲洗;

case 1:单纯进行冲洗while (i < 10){    sleep(1);    fflush(stdout);    i++;}case 2:每输出一个字符就进行冲洗while (i < 10){    sleep(1);    putchar('t');    fflush(stdout);    i++;}case 3:每输出一串字符就进行冲洗while (i < 10){    sleep(1);    putchar('ttttt');    fflush(stdout);    i++;}

case 1现象与实验二相同;case 2、case 3进程都只休眠了1s,然后输出相应个数的’t’,接着退出,而两者文件中的输出相同:

receive SIGIO signal 1receive SIGIO signal 2receive SIGIO signal 3receive SIGIO signal 4receive SIGIO signal 5receive SIGIO signal 6receive SIGIO signal 7receive SIGIO signal 8receive SIGIO signal 9

这组实验说明:
1. 缓冲区中没数据时,对stdout的冲洗不产生SIGIO信号;
2. 缓冲区有数据时,对stdout进行冲洗的确能够触发SIGIO信号;
3. 缓冲区中数据量不会影响SIGIO触发的次数,仍然是一次冲洗触发一次SIGIO信号。

休眠时间解释:产生的SIGIO信号会中断sleep函数的调用,所以case 2和case 3中进程只是在第一次调用sleep的时候休眠了1s,之后的调用都被信号给中断了。但是为什么只触发了9次,还有一次呢?推测是因为,第十次还没来得及触发或者触发了还没来得及调用处理函数,进程就结束了。如果在while循环之后再加上一条sleep调用,就能看到第十条的打印了,如下:

while (i < 10){    sleep(1);    putchar('t');    fflush(stdout);    i++;}sleep(1);

从以上两个实验可以得出结论:对于stdin绑定了信号驱动I/O的进程,在stdout上的输出能触发SIGIO信号需要满足:stdout缓冲区中有数据,stdout缓冲区必须被冲洗;而与缓冲区中数据大小无关。

实验四:

那么从键盘键入字符时,产生SIGIO信号的行为又是怎样的呢?实验代码如下:

#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <errno.h>#include <fcntl.h>#include <signal.h>#include <string.h>#define BSZ 64void sigio_handler(int sig){    static int cnt = 0;    FILE *ftp = NULL;    char buf[BSZ];    memset(buf, 0, BSZ);    if ((ftp = fopen("file1.tmp", "a")) < 0)    {        printf("fopen error\n");        exit(1);    }    fprintf(ftp, "receive SIGIO signal %d\n", ++cnt);    read(STDIN_FILENO, buf, BSZ);    fputs(buf, ftp);    fflush(stdin);    fflush(stdout);    fclose(ftp);}int main(int argc, char **argv){    struct sigaction sig_act;    int fl = 0;    //signal driven I/O setting    fflush(stdin);    fflush(stdout);    fl = fcntl(STDIN_FILENO, F_GETFL, 0);    fcntl(STDIN_FILENO, F_SETFL, fl | O_NONBLOCK | O_ASYNC );    fcntl(STDIN_FILENO, F_SETOWN, getpid());    sigemptyset(&sig_act.sa_mask);    sig_act.sa_flags = 0;    sig_act.sa_handler = sigio_handler;    sigaction(SIGIO, &sig_act, NULL);    while (1)    {        sleep(1);    }    return 0;}

进程运行后,键入test并回车,在Linux 2.6和Linux 3.13上得到file1.tmp中的结果不一致:

Linux 2.6.8:receive SIGIO signal 1receive SIGIO signal 2receive SIGIO signal 3receive SIGIO signal 4receive SIGIO signal 5testLinux 3.13:receive SIGIO signal 1test

可见,在Linux 2.6上,每键入一个字符就会触发一次SIGIO信号,包括回车键;而在Linux 3.13上,只有敲入了回车键之后才会触发一次SIGIO信号

总结

对于stdin绑定了信号驱动I/O的进程,在stdout上的输出能触发SIGIO信号需要满足:stdout缓冲区中有数据,stdout缓冲区必须被冲洗;而与缓冲区中数据大小无关。在Linux 2.6上,每键入一个字符就会触发一次SIGIO信号,包括回车键;而在Linux 3.13上,只有敲入了回车键之后才会触发一次SIGIO信号。

但是,是什么原因产生这样的结果?还是说这些结论是跟平台相关的?这些问题我现在还不明白,还需要继续学习。

0 0