多路复用
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所示。
图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
热点链接: