linux系统的io复用方式,Linux下多路复用I/O接口

多路复用

1.函数说明

前面的fcntl()函数解决了文件的共享问题,接下来该处理I/O复用的情况了。

总的来说,I/O处理的模型有5种。

● 阻塞I/O模型:在这种模型下,若所调用的I/O函数没有完成相关的功能,则会使进程挂起,直到相关数据到达才会返回。如常见对管道设备、终端设备和网络设备进行读写时经常会出现这种情况。

● 非阻塞I/O模型:在这种模型下,当请求的I/O操作不能完成时,则不让进程睡眠,而且立即返回。非阻塞I/O使用户可以调用不会阻塞的I/O操作,如open()、write()和read()。如果该操作不能完成,则会立即返回出错(如打不开文件)或者返回0(如在缓冲区中没有数据可以读取或者没空间可以写入数据)。

● I/O多路转接模型:在这种模型下,如果请求的I/O操作阻塞,且它不是真正阻塞I/O,而是让其中的一个函数等待,在此期间,I/O还能进行其他操作。如本小节要介绍的select()和poll()函数,就是属于这种模型。

● 信号驱动I/O模型:在这种模型下,进程要定义一个信号处理程序,系统可以自动捕获特定信号的到来,从而启动I/O。这是由内核通知用户何时可以启动一个I/O操作决定的。

它是非阻塞的。当有就绪的数据时,内核就向该进程发送SIGIO信号。 无论我们如何处理SIGIO信号,这种模型的好处是当等待数据到达时,可以不阻塞。主程序继续执行,只有收到SIGIO信号时才去处理数据即可。

● 异步I/O模型:在这种模型下,进程先让内核启动I/O操作,并在整个操作完成后通知该进程。这种模型与信号驱动模型的主要区别在于:信号驱动I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知进程I/O操作何时完成的。现在,并不是所有的系统都支持这种模型。

可以看到,select()和poll()的I/O多路转接模型是处理I/O复用的一个高效的方法。它可以具体设置程序中每一个所关心的文件描述符的条件、希望等待的时间等,从select()和poll()函数返回时,内核会通知用户已准备好的文件描述符的数量、已准备好的条件(或事件)等。通过使用select()和poll()函数的返回结果(可能是检测到某个文件描述符的注册事件或是超时,或是调用出错),就可以调用相应的I/O处理函数了。

2.函数格式

select()函数的语法要点如表2.8所示。

表2.8 select()函数语法要点

所需头文件

#include

#include

#include

函数原型

int select(int numfds, fd_set *readfds, fd_set *writefds,

fd_set *exeptfds, struct timeval *timeout)

函数传入值

numfds:该参数值为需要监视的文件描述符的大值加1

readfds:由select()监视的读文件描述符集合

writefds:由select()监视的写文件描述符集合

exeptfds:由select()监视的异常处理文件描述符集合

timeout

NULL:永远等待,直到捕捉到信号或文件描述符已准备好为止

具体值:struct timeval类型的指针,若等待了timeout时间还没有检测到任何文件描符准备好,就立即返回

0:从不等待,测试所有指定的描述符并立即返回

函数返回值

成功:准备好的文件描述符

0:超时; 1:出错

可以看到,select()函数根据希望进行的文件操作对文件描述符进行了分类处理,这里对文件描述符的处理主要涉及4个宏函数,如表2.9所示。

表2.9 select()文件描述符处理函数

FD_ZERO(fd_set *set)

清除一个文件描述符集

FD_SET(int fd, fd_set *set)

将一个文件描述符加入文件描述符集中

FD_CLR(int fd, fd_set *set)

将一个文件描述符从文件描述符集中清除

FD_ISSET(int fd, fd_set *set)

如果文件描述符fd为fd_set集中的一个元素,则返回非零值,可以用于调用select()后测试文件描述符集中的哪个文件描述符是否有变化

一般来说,在每次使用select()函数之前,首先使用FD_ZERO()和FD_SET()来初始化文件描述符集(在需要重复调用select()函数时,先把一次初始化好的文件描述符集备份下来,每次读取它即可)。在select()函数返回后,可循环使用FD_ISSET()来测试描述符集,在执行完对相关文件描述符的操作后,使用FD_CLR()来清除描述符集。

另外,select()函数中的timeout是一个struct timeval类型的指针,该结构体如下所示:

struct timeval

{

long tv_sec; /* 秒 */

long tv_unsec; /* 微秒 */

}

可以看到,这个时间结构体的精确度可以设置到微秒级,这对于大多数的应用而言已经足够了。

poll()函数的语法要点如表2.10所示。

表2.10 poll()函数语法要点

所需头文件

#include

#include

函数原型

int poll(struct pollfd *fds, int numfds, int timeout)

函数传入值

fds:struct pollfd结构的指针,用于描述需要对哪些文件的哪种类型的操作进行监控

struct pollfd

{

int fd; /* 需要监听的文件描述符 */

short events; /* 需要监听的事件 */

short revents; /* 已发生的事件 */

}

events成员描述需要监听哪些类型的事件,可以用以下几种标志来描述。

POLLIN:文件中有数据可读,下面实例中使用到了这个标志

POLLPRI::文件中有紧急数据可读

POLLOUT:可以向文件写入数据

POLLERR:文件中出现错误,只限于输出

POLLHUP:与文件的连接被断开,只限于输出

POLLNVAL:文件描述符是不合法的,即它并没有指向一个成功打开的文件

numfds:需要监听的文件个数,即第一个参数所指向的数组中的元素数目

timeout:表示poll阻塞的超时时间(毫秒)。如果该值小于等于0,则表示无限等待

函数返回值

成功:返回大于0的值,表示事件发生的pollfd结构的个数

0:超时; 1:出错

3.使用实例

当使用select()函数时,存在一系列的问题,例如,内核必须检查多余的文件描述符,每次调用select()之后必须重置被监听的文件描述符集,而且可监听的文件个数受限制(使用FD_SETSIZE宏来表示fd_set结构能够容纳的文件描述符的大数目)等。实际上,poll机制与select机制相比效率更高,使用范围更广。下面以poll()函数为例实现某种功能。

本实例中主要实现通过调用poll()函数来监听三个终端的输入(分别重定向到两个管道文件的虚拟终端及主程序所运行的虚拟终端)并分别进行相应的处理。在这里我们建立了一个poll()函数监视的读文件描述符集,其中包含三个文件描述符,分别为标准输入文件描述符和两个管道文件描述符。通过监视主程序的虚拟终端标准输入来实现程序的控制(如程序结束);以两个管道作为数据输入,主程序将从两个管道读取的输入字符串写入到标准输出文件(屏幕)。

为了充分表现poll()函数的功能,在运行主程序时,需要打开3个虚拟终端:首先用mknod命令创建两个管道in1和in2。接下来,在两个虚拟终端上分别运行cat>in1和cat>in2。同时在第三个虚拟终端上运行主程序。

在程序运行后,如果在两个管道终端上输入字符串,则可以观察到同样的内容将在主程序的虚拟终端上逐行显示。

如果想结束主程序,只要在主程序的虚拟终端下输入以“q”或“Q”字符开头的字符串即可。如果三个文件一直在无输入状态中,则主程序一直处于阻塞状态。为了防止无限期的阻塞,在程序中设置超时值(本实例中设置为60s),当无输入状态持续到超时值时,主程序主动结束运行并退出。该程序的流程图如图2.3所示。

linux系统的io复用方式,Linux下多路复用I/O接口_第1张图片

图2.3 多路复用实例流程图

/* multiplex_poll.c */

#include

#include

#include

#include

#include

#include

#include

#include

#define MAX_BUFFER_SIZE1024/* 缓冲区大小 */

#define IN_FILES3/* 多路复用输入文件数目 */

#define TIME_DELAY60000/* 超时时间秒数:60s */

#define MAX(a, b)((a > b)?(a):(b))

int main(void)

{

struct pollfd fds[IN_FILES];

char buf[MAX_BUFFER_SIZE];

int i, res, real_read, maxfd;

/* 首先按一定的权限打开两个源文件 */

fds[0].fd = 0;

if((fds[1].fd = open ("in1", O_RDONLY|O_NONBLOCK)) < 0)

{

printf("Open in1 error\n");

return 1;

}

if((fds[2].fd = open ("in2", O_RDONLY|O_NONBLOCK)) < 0)

{

printf("Open in2 error\n");

return 1;

}

/* 取出两个文件描述符中的较大者 */

for (i = 0; i < IN_FILES; i++)

{

fds[i].events = POLLIN;

}

/* 循环测试是否存在正在监听的文件描述符 */

while(fds[0].events || fds[1].events || fds[2].events)

{

if (poll(fds, IN_FILES, 0) < 0)

{

printf("Poll error or Time out\n");

return 1;

}

for (i = 0; i< IN_FILES; i++)

{

if (fds[i].revents) /* 判断在哪个文件上发生了事件 */

{

memset(buf, 0, MAX_BUFFER_SIZE);

real_read = read(fds[i].fd, buf, MAX_BUFFER_SIZE);

if (real_read < 0)

{

if (errno != EAGAIN)

{

return 1; /* 系统错误,结束运行 */

}

}

else if (!real_read)

{

close(fds[i].fd);

fds[i].events = 0; /* 取消对该文件的监听 */

}

else

{

if (i == 0) /* 如果在标准输入上有数据输入时 */

{

if ((buf[0] == 'q') || (buf[0] == 'Q'))

{

return 1; /* 输入“q”或“Q”则会退出 */

}

}

else

{ /* 将读取的数据先传送到终端上 */

buf[real_read] = '\0';

printf("%s", buf);

}

} /* end of if real_read*/

} /* end of if revents */

} /* end of for */

} /*end of while */

exit(0);

}

读者可以将以上程序交叉编译,并下载到开发板上运行,以下是运行结果:

$ mknod in1 p

$ mknod in2 p

$ cat > in1            /* 在第一个虚拟终端 */

SELECT CALL

TEST PROGRAMME

END

$ cat > in2            /* 在第二个虚拟终端 */

select call

test programme

end

$ ./multiplex_select   /* 在第三个虚拟终端 */

SELECT CALL            /* 管道1的输入数据 */

select call            /* 管道2的输入数据 */

TEST PROGRAMME         /* 管道1的输入数据 */

test programme         /* 管道2的输入数据 */

END                    /* 管道1的输入数据 */

end                    /* 管道2的输入数据 */

q                      /* 在第三个终端上输入“q”或“Q”则立刻结束程序运行 */

程序的超时结束结果如下:

$ ./multiplex_select

…(在60s之内没有任何监听文件的输入)

Poll error or Time out

热点链接:

你可能感兴趣的:(linux系统的io复用方式)