6.3 底层文件I/O操作
本节主要介绍文件I/O操作的系统调用,主要用到5个函数:open()、read()、write()、lseek()和close()。这些函数的特点是不带缓存,直接对文件(包括设备)进行读写操作。这些函数虽然不是ANSI C的组成部分,但是是POSIX的组成部分。
6.3.1 基本文件操作
(1)函数说明。
open()函数用于打开或创建文件,在打开或创建文件时可以指定文件的属性及用户的权限等各种参数。
close()函数用于关闭一个被打开的文件。当一个进程终止时,所有被它打开的文件都由内核自动关闭,很多程序都使用这一功能而不显示地关闭一个文件。
read()函数用于将从指定的文件描述符中读出的数据放到缓存区中,并返回实际读入的字节数。若返回0,则表示没有数据可读,即已达到文件尾。读操作从文件的当前指针位置开始。当从终端设备文件中读出数据时,通常一次最多读一行。
write()函数用于向打开的文件写数据,写操作从文件的当前指针位置开始。对磁盘文件进行写操作,若磁盘已满或超出该文件的长度,则write()函数返回失败。
lseek()函数用于在指定的文件描述符中将文件指针定位到相应的位置。它只能用在可定位(可随机访问)文件操作中。管道、套接字和大部分字符设备文件是不可定位的,所以在这些文件的操作中无法使用lseek()调用。
(2)函数格式。
open()函数的语法格式如表6.1所示。
表6.1 open()函数语法要点
所需头文件 |
#include <sys/types.h> /* 提供类型pid_t的定义 */
#include <sys/stat.h> |
||
函数原型 |
int open(const char *pathname, int flags, int perms) |
||
函数传入值 |
pathname |
被打开的文件名(可包括路径名) |
|
flag:文件打开的方式 |
O_RDONLY:以只读方式打开文件 |
||
O_WRONLY:以只写方式打开文件 |
|||
O_RDWR:以读写方式打开文件 |
|||
O_CREAT:如果该文件不存在,就创建一个新的文件,并用第三个参数为其设置权限 |
|||
O_EXCL:如果使用O_CREAT时文件存在,则可返回错误消息。这一参数可测试文件是否存在。此时open是原子操作,防止多个进程同时创建同一个文件 |
|||
O_NOCTTY:使用本参数时,若文件为终端,那么该终端不会成为调用open()的那个进程的控制终端 |
|||
O_TRUNC:若文件已经存在,那么会删除文件中的全部原有数据,并且设置文件大小为0。 |
|||
O_APPEND:以添加方式打开文件,在打开文件的同时,文件指针指向文件的末尾,即将写入的数据添加到文件的末尾 |
|||
perms |
被打开文件的存取权限 可以用一组宏定义:S_I(R/W/X)(USR/GRP/OTH) 其中R/W/X分别表示读/写/执行权限 USR/GRP/OTH分别表示文件所有者/文件所属组/其他用户 例如,S_IRUSR | S_IWUSR表示设置文件所有者的可读可写属性。八进制表示法中600也表示同样的权限 |
||
函数返回值 |
成功:返回文件描述符 |
在open()函数中,flag参数可通过“|”组合构成,但前3个标志常量(O_RDONLY、O_WRONLY以及O_RDWR)不能相互组合。perms是文件的存取权限,既可以用宏定义表示法,也可以用八进制表示法。
close()函数的语法格式表6.2所示。
表6.2 close()函数语法要点
所需头文件 |
#include <unistd.h> |
函数原型 |
int close(int fd) |
函数输入值 |
fd:文件描述符 |
函数返回值 |
0:成功 |
read()函数的语法格式如表6.3所示。
表6.3 read()函数语法要点
所需头文件 |
#include <unistd.h> |
函数原型 |
ssize_t read(int fd, void *buf, size_t count) |
函数传入值 |
fd:文件描述符 |
buf:指定存储器读出数据的缓冲区 |
|
count:指定读出的字节数 |
|
函数返回值 |
成功:读到的字节数 |
在读普通文件时,若读到要求的字节数之前已到达文件的尾部,则返回的字节数会小于希望读出的字节数。
write()函数的语法格式如表6.4所示。
表6.4 write()函数语法要点
所需头文件 |
#include <unistd.h> |
函数原型 |
ssize_t write(int fd, void *buf, size_t count) |
函数传入值 |
fd:文件描述符 |
buf:指定存储器写入数据的缓冲区 |
|
count:指定读出的字节数 |
|
函数返回值 |
成功:已写的字节数 |
在写普通文件时,写操作从文件的当前指针位置开始。
lseek()函数的语法格式如表6.5所示。
表6.5 lseek()函数语法要点
所需头文件 |
#include <unistd.h> #include <sys/types.h> |
||
函数原型 |
off_t lseek(int fd, off_t offset, int whence) |
||
函数传入值 |
fd:文件描述符 |
||
offset:偏移量,每一读写操作所需要移动的距离,单位是字节,可正可负(向前移,向后移) |
|||
whence: 当前位置的基点 |
SEEK_SET:当前位置为文件的开头,新位置为偏移量的大小 |
||
SEEK_CUR:当前位置为文件指针的位置,新位置为当前位置加上偏移量 |
|||
SEEK_END:当前位置为文件的结尾,新位置为文件的大小加上偏移量的大小 |
|||
函数返回值 |
成功:文件的当前位移 |
(3)函数使用实例。
下面实例中的open()函数带有3个flag参数:O_CREAT、O_TRUNC和O_WRONLY,这样就可以对不同的情况指定相应的处理方法。另外,这里对该文件的权限设置为0600。其源码如下所示:
下面列出文件基本操作的实例,基本功能是从一个文件(源文件)中读取最后10KB数据并到另一个文件(目标文件)。在实例中源文件是以只读方式打开,目标文件是以只写方式打开(可以是读写方式)。若目标文件不存在,可以创建并设置权限的初始值为644,即文件所有者可读可写,文件所属组和其他用户只能读。
读者需要留意的地方是改变每次读写的缓存大小(实例中为1KB)会怎样影响运行效率。
/* copy_file.c */
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#define BUFFER_SIZE 1024 /* 每次读写缓存大小,影响运行效率*/
#define SRC_FILE_NAME "src_file" /* 源文件名 */
#define DEST_FILE_NAME "dest_file" /* 目标文件名文件名 */
#define OFFSE 10240 /* 复制的数据大小 */
int main()
{
int src_file, dest_file;
unsigned char buff[BUFFER_SIZE];
int real_read_len;
/* 以只读方式打开源文件 */
src_file = open(SRC_FILE_NAME, O_RDONLY);
/* 以只写方式打开目标文件,若此文件不存在则创建该文件, 访问权限值为644 */
dest_file = open(DEST_FILE_NAME,
O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
if (src_file < 0 || dest_file < 0)
{
printf("Open file error\n");
exit(1);
}
/* 将源文件的读写指针移到最后10KB的起始位置*/
lseek(src_file, -OFFSET, SEEK_END);
/* 读取源文件的最后10KB数据并写到目标文件中,每次读写1KB */
while ((real_read_len = read(src_file, buff, sizeof(buff))) > 0)
{
write(dest_file, buff, real_read_len);
}
close(dest_file);
close(src_file);
return 0;
}
$ ./copy_file
$ ls -lh dest_file
-rw-r--r-- 1 david root 10K 14:06 dest_file
注意 |
open()函数返回的文件描述符一定是最小的未用文件描述符。由于一个进程在启动时自动打开了0、1、2三个文件描述符,因此,该文件运行结果中返回的文件描述符为3。读者可以尝试在调用open()函数之前,加一句close(0),则此后在调用open()函数时返回的文件描述符为0(若关闭文件描述符1,则在程序执行时会由于没有标准输出文件而无法输出)。 |
6.3.2 文件锁
(1)fcntl()函数说明。
前面的这5个基本函数实现了文件的打开、读写等基本操作,本小节将讨论的是,在文件已经共享的情况下如何操作,也就是当多个用户共同使用、操作一个文件的情况,这时,Linux通常采用的方法是给文件上锁,来避免共享的资源产生竞争的状态。
文件锁包括建议性锁和强制性锁。建议性锁要求每个上锁文件的进程都要检查是否有锁存在,并且尊重已有的锁。在一般情况下,内核和系统都不使用建议性锁。强制性锁是由内核执行的锁,当一个文件被上锁进行写入操作的时候,内核将阻止其他任何文件对其进行读写操作。采用强制性锁对性能的影响很大,每次读写操作都必须检查是否有锁存在。
在Linux中,实现文件上锁的函数有lockf()和fcntl(),其中lockf()用于对文件施加建议性锁,而fcntl()不仅可以施加建议性锁,还可以施加强制锁。同时,fcntl()还能对文件的某一记录上锁,也就是记录锁。
记录锁又可分为读取锁和写入锁,其中读取锁又称为共享锁,它能够使多个进程都能在文件的同一部分建立读取锁。而写入锁又称为排斥锁,在任何时刻只能有一个进程在文件的某个部分上建立写入锁。当然,在文件的同一部分不能同时建立读取锁和写入锁。
注意 |
fcntl()是一个非常通用的函数,它可以对已打开的文件描述符进行各种操作,不仅包括管理文件锁,还包括获得和设置文件描述符和文件描述符标志、文件描述符的复制等很多功能。在本节中,主要介绍建立记录锁的方法。 |
(2)fcntl()函数格式。
用于建立记录锁的fcntl()函数格式如表6.6所示。
表6.6 fcntl()函数语法要点
所需头文件 |
#include <sys/types.h> #include <unistd.h> #include <fcntl.h> |
|
函数原型 |
int fcnt1(int fd, int cmd, struct flock *lock) |
|
函数传入值 |
fd:文件描述符 |
|
cmd |
F_DUPFD:复制文件描述符 |
|
F_GETFD:获得fd的close-on-exec标志,若标志未设置,则文件经过exec()函数之后仍保持打开状态 |
||
F_SETFD:设置close-on-exec标志,该标志由参数arg的FD_CLOEXEC位决定 |
||
F_GETFL:得到open设置的标志 |
||
F_SETFL:改变open设置的标志 |
||
F_GETLK:根据lock参数值,决定是否上文件锁 |
||
F_SETLK:设置lock参数值的文件锁 |
||
F_SETLKW:这是F_SETLK的阻塞版本(命令名中的W表示等待(wait))。 在无法获取锁时,会进入睡眠状态;如果可以获取锁或者捕捉到信号则会返回 |
||
lock:结构为flock,设置记录锁的具体状态 |
||
函数返回值 |
0:成功 |
这里,lock的结构如下所示:
struct flock
{
short l_type;
off_t l_start;
short l_whence;
off_t l_len;
pid_t l_pid;
}
lock结构中每个变量的取值含义如表6.7所示。
表6.7 lock结构变量取值
l_type |
F_RDLCK:读取锁(共享锁) |
||
F_WRLCK:写入锁(排斥锁) |
|||
F_UNLCK:解锁 |
|||
l_stat |
相对位移量(字节) |
||
l_whence:相对位移量的起点(同lseek的whence) |
SEEK_SET:当前位置为文件的开头,新位置为偏移量的大小 |
||
SEEK_CUR:当前位置为文件指针的位置,新位置为当前位置加上偏移量 |
|||
SEEK_END:当前位置为文件的结尾,新位置为文件的大小加上偏移量的大小 |
|||
l_len |
加锁区域的长度 |
||
小技巧 |
为加锁整个文件,通常的方法是将l_start设置为0,l_whence设置为SEEK_SET,l_len设置为0。 |
(3)fcntl()使用实例
下面首先给出了使用fcntl()函数的文件记录锁功能的代码实现。在该代码中,首先给flock结构体的对应位赋予相应的值。接着使用两次fcntl()函数,分别用于判断文件是否可以上锁和给相关文件上锁,这里用到的cmd值分别为F_GETLK和F_SETLK(或F_SETLKW)。
用F_GETLK命令判断是否可以进行flock结构所描述的锁操作:若可以进行,则flock结构的l_type会被设置为F_UNLCK,其他域不变;若不可行,则l_pid被设置为拥有文件锁的进程号,其他域不变。
用F_SETLK和F_SETLKW命令设置flock结构所描述的锁操作,后者是前者的阻塞版。
文件记录锁功能的源代码如下所示:
/* lock_set.c */
int lock_set(int fd, int type)
{
struct flock old_lock, lock;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
lock.l_type = type;
lock.l_pid = -1;
/* 判断文件是否可以上锁 */
fcntl(fd, F_GETLK, &lock);
if (lock.l_type != F_UNLCK)
{
/* 判断文件不能上锁的原因 */
if (lock.l_type == F_RDLCK) /* 该文件已有读取锁 */
{
printf("Read lock already set by %d\n", lock.l_pid);
}
else if (lock.l_type == F_WRLCK) /* 该文件已有写入锁 */
{
printf("Write lock already set by %d\n", lock.l_pid);
}
}
/* l_type 可能已被F_GETLK修改过 */
lock.l_type = type;
/* 根据不同的type值进行阻塞式上锁或解锁 */
if ((fcntl(fd, F_SETLKW, &lock)) < 0)
{
printf("Lock failed:type = %d\n", lock.l_type);
return 1;
}
switch(lock.l_type)
{
case F_RDLCK:
{
printf("Read lock set by %d\n", getpid());
}
break;
case F_WRLCK:
{
printf("Write lock set by %d\n", getpid());
}
break;
case F_UNLCK:
{
printf("Release lock by %d\n", getpid());
return 1;
}
break;
default:
break;
}/* end of switch */
return 0;
}
下面的实例是文件写入锁的测试用例,这里首先创建了一个hello文件,之后对其上写入锁,最后释放写入锁,代码如下所示:
/* write_lock.c */
#include <unistd.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include "lock_set.c"
int main(void)
{
int fd;
/* 首先打开文件 */
fd = open("hello",O_RDWR | O_CREAT, 0644);
if(fd < 0)
{
printf("Open file error\n");
exit(1);
}
/* 给文件上写入锁 */
lock_set(fd, F_WRLCK);
getchar();
/* 给文件解锁 */
lock_set(fd, F_UNLCK);
getchar();
close(fd);
exit(0);
}
为了能够使用多个终端,更好地显示写入锁的作用,本实例主要在PC机上测试,读者可将其交叉编译,下载到目标板上运行。下面是在PC机上的运行结果。为了使程序有较大的灵活性,笔者采用文件上锁后由用户键入一任意键使程序继续运行。建议读者开启两个终端,并且在两个终端上同时运行该程序,以达到多个进程操作一个文件的效果。在这里,笔者首先运行终端一,请读者注意终端二中的第一句。
终端一:
$ ./write_lock
write lock set by 4994
release lock by 4994
终端二:
$ ./write_lock
write lock already set by 4994
write lock set by 4997
release lock by 4997
由此可见,写入锁为互斥锁,同一时刻只能有一个写入锁存在。
接下来的程序是文件读取锁的测试用例,原理和上面的程序一样。
/* fcntl_read.c */
#include <unistd.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include "lock_set.c"
int main(void)
{
int fd;
fd = open("hello",O_RDWR | O_CREAT, 0644);
if(fd < 0)
{
printf("Open file error\n");
exit(1);
}
/* 给文件上读取锁 */
lock_set(fd, F_RDLCK);
getchar();
/* 给文件解锁 */
lock_set(fd, F_UNLCK);
getchar();
close(fd);
exit(0);
}
同样开启两个终端,并首先启动终端一上的程序,其运行结果如下所示:
终端一:
$ ./read_lock
read lock set by 5009
release lock by 5009
终端二:
$ ./read_lock
read lock set by 5010
release lock by 5010
读者可以将此结果与写入锁的运行结果相比较,可以看出,读取锁为共享锁,当进程5009已设置读取锁后,进程5010仍然可以设置读取锁。
思考 |
如果在一个终端上运行设置读取锁的程序,则在另一个终端上运行设置写入锁的程序,会有什么结果呢? |
6.3.3 多路复用
(1)函数说明。
前面的fcntl()函数解决了文件的共享问题,接下来该处理I/O复用的情况了。
总的来说,I/O处理的模型有5种。
n 阻塞I/O模型:在这种模型下,若所调用的I/O函数没有完成相关的功能,则会使进程挂起,直到相关数据到达才会返回。对管道设备、终端设备和网络设备进行读写时经常会出现这种情况。
n 非阻塞模型:在这种模型下,当请求的I/O操作不能完成时,则不让进程睡眠,而且立即返回。非阻塞I/O使用户可以调用不会阻塞的I/O操作,如open()、write()和read()。如果该操作不能完成,则会立即返回出错(例如:打不开文件)或者返回0(例如:在缓冲区中没有数据可以读取或者没有空间可以写入数据)。
n I/O多路转接模型:在这种模型下,如果请求的I/O操作阻塞,且它不是真正阻塞I/O,而是让其中的一个函数等待,在这期间,I/O还能进行其他操作。本节要介绍的select()和poll函数()就是属于这种模型。
n 信号驱动I/O模型:在这种模型下,通过安装一个信号处理程序,系统可以自动捕获特定信号的到来,从而启动I/O。这是由内核通知用户何时可以启动一个I/O操作决定的。
n 异步I/O模型:在这种模型下,当一个描述符已准备好,可以启动I/O时,进程会通知内核。现在,并不是所有的系统都支持这种模型。
可以看到,select()和poll()的I/O多路转接模型是处理I/O复用的一个高效的方法。它可以具体设置程序中每一个所关心的文件描述符的条件、希望等待的时间等,从select()和poll()函数返回时,内核会通知用户已准备好的文件描述符的数量、已准备好的条件等。通过使用select()和poll()函数的返回结果,就可以调用相应的I/O处理函数。
(2)函数格式。
select()函数的语法格式如表6.8所示。
表6.8 select()函数语法要点
所需头文件 |
#include <sys/types.h> #include <sys/time.h> #include <unistd.h> |
|
函数原型 |
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:成功,返回准备好的文件描述符的数目 |
思考 |
请读者考虑一下如何确定被监视的文件描述符的最大值? |
可以看到,select()函数根据希望进行的文件操作对文件描述符进行了分类处理,这里,对文件描述符的处理主要涉及4个宏函数,如表6.9所示。
表6.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()函数时,可循环使用FD_ISSET()来测试描述符集,在执行完对相关文件描述符的操作之后,使用FD_CLR()来清除描述符集。
另外,select()函数中的timeout是一个struct timeval类型的指针,该结构体如下所示:
struct timeval
{
long tv_sec; /* 秒 */
long tv_unsec; /* 微秒 */
}
可以看到,这个时间结构体的精确度可以设置到微秒级,这对于大多数的应用而言已经足够了。
poll()函数的语法格式如表6.10所示。
表6.10 poll()函数语法要点
所需头文件 |
#include <sys/types.h> #include <poll.h> |
函数原型 |
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)使用实例。
由于多路复用通常用于I/O操作可能会被阻塞的情况,而对于可能会有阻塞I/O的管道、网络编程,本书到现在为止还没有涉及。这里通过手动创建(用mknod命令)两个管道文件,重点说明如何使用两个多路复用函数。
在本实例中,分别用select()函数和poll()函数实现同一个功能,以下功能说明是以select()函数为例描述的。
本实例中主要实现通过调用select()函数来监听3个终端的输入(分别重定向到两个管道文件的虚拟终端以及主程序所运行的虚拟终端),并分别进行相应的处理。在这里我们建立了一个select()函数监视的读文件描述符集,其中包含3个文件描述符,分别为一个标准输入文件描述符和两个管道文件描述符。通过监视主程序的虚拟终端标准输入来实现程序的控制(例如:程序结束);以两个管道作为数据输入,主程序将从两个管道读取的输入字符串写入到标准输出文件(屏幕)。
为了充分表现select()调用的功能,在运行主程序的时候,需要打开3个虚拟终端:首先用mknod命令创建两个管道in1和in2。接下来,在两个虚拟终端上分别运行cat>in1和cat>in2。同时在第三个虚拟终端上运行主程序。在程序运行之后,如果在两个管道终端上输入字符串,则可以观察到同样的内容将在主程序的虚拟终端上逐行显示。如果想结束主程序,只要在主程序的虚拟终端下输入以‘q’或‘Q’字符开头的字符串即可。如果三个文件一直在无输入状态中,则主程序一直处于阻塞状态。为了防止无限期的阻塞,在select程序中设置超时值(本实例中设置为60s),当无输入状态持续到超时值时,主程序主动结束运行并退出。而poll程序中依然无限等待,当然poll()函数也可以设置超时参数。
该程序的流程图如图6.2所示。
图6.2 select实例流程图
使用select()函数实现的代码如下所示:
/* multiplex_select */
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <errno.h>
#define MAX_BUFFER_SIZE 1024 /* 缓冲区大小*/
#define IN_FILES 3 /* 多路复用输入文件数目*/
#define TIME_DELAY 60 /* 超时值秒数 */
#define MAX(a, b) ((a > b)?(a):(b))
int main(void)
{
int fds[IN_FILES];
char buf[MAX_BUFFER_SIZE];
int i, res, real_read, maxfd;
struct timeval tv;
fd_set inset,tmp_inset;
/*首先以只读非阻塞方式打开两个管道文件*/
fds[0] = 0;
if((fds[1] = open ("in1", O_RDONLY|O_NONBLOCK)) < 0)
{
printf("Open in1 error\n");
return 1;
}
if((fds[2] = open ("in2", O_RDONLY|O_NONBLOCK)) < 0)
{
printf("Open in2 error\n");
return 1;
}
/*取出两个文件描述符中的较大者*/
maxfd = MAX(MAX(fds[0], fds[1]), fds[2]);
/*初始化读集合inset,并在读集合中加入相应的描述集*/
FD_ZERO(&inset);
for (i = 0; i < IN_FILES; i++)
{
FD_SET(fds[i], &inset);
}
FD_SET(0, &inset);
tv.tv_sec = TIME_DELAY;
tv.tv_usec = 0;
/*循环测试该文件描述符是否准备就绪,并调用select函数对相关文件描述符做对应操作*/
while(FD_ISSET(fds[0],&inset)
|| FD_ISSET(fds[1],&inset) || FD_ISSET(fds[2], &inset))
{
/* 文件描述符集合的备份, 这样可以避免每次进行初始化 */
tmp_inset = inset;
res = select(maxfd + 1, &tmp_inset, NULL, NULL, &tv);
switch(res)
{
case -1:
{
printf("Select error\n");
return 1;
}
break;
case 0: /* Timeout */
{
printf("Time out\n");
return 1;
}
break;
default:
{
for (i = 0; i < IN_FILES; i++)
{
f (FD_ISSET(fds[i], &tmp_inset))
{
memset(buf, 0, MAX_BUFFER_SIZE);
real_read = read(fds[i], buf, MAX_BUFFER_SIZE);
if (real_read < 0)
{
if (errno != EAGAIN)
{
return 1;
}
}
else if (!real_read)
{
close(fds[i]);
FD_CLR(fds[i], &inset);
}
else
{
if (i == 0)
{/* 主程序终端控制 */
if ((buf[0] == 'q') || (buf[0] == 'Q'))
{
return 1;
}
}
else
{/* 显示管道输入字符串 */
buf[real_read] = '\0';
printf("%s", buf);
}
}
} /* end of if */
} /* end of for */
}
break;
} /* end of switch */
} /*end of while */
return 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
select call
TEST PROGRAMME
test programme
END
end
q /* 在终端上输入‘q’或‘Q’则立刻结束程序运行 */
程序的超时结束结果如下:
$ ./multiplex_select
……
Time out
可以看到,使用select()可以很好地实现I/O多路复用。
但是当使用select()函数时,存在一系列的问题,例如:内核必须检查多余的文件描述符,每次调用select()之后必须重置被监听的文件描述符集,而且可监听的文件个数受限制(使用FD_SETSIZE宏来表示fd_set结构能够容纳的文件描述符的最大数目)等。实际上,poll机制与select机制相比效率更高,使用范围更广,下面给出用poll()函数实现同样功能的代码。
/* multiplex_poll.c */
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <poll.h>
#define MAX_BUFFER_SIZE 1024 /* 缓冲区大小*/
#define IN_FILES 3 /* 多路复用输入文件数目*/
#define TIME_DELAY 60 /* 超时时间秒数 */
#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;
}
/*循环测试该文件描述符是否准备就绪,并调用select函数对相关文件描述符做对应操作*/
while(fds[0].events || fds[1].events || fds[2].events)
{
if (poll(fds, IN_FILES, 0) < 0)
{
printf("Poll error\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;
}
}
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);
}
运行结果与select程序类似。请读者比较使用select()和poll()函数实现的代码的运行效率(提示:使用获得时间的函数计算程序运行时间,可以证明poll()函数的效率更高)。