序
linux系统下一切皆文件,我们几乎无时无刻不在跟文件打交道。内核对文件I/O做了很好的封装,使得开发人员便捷地操作文件,但也因此隐藏了很多细节。如果对其不求甚解,在实际开发中可能会碰到一些意想不到的问题。这次,让我们手拿放大镜,一起窥探文件I/O的全貌。
1. 文件件描述符
内核会为每个进程维护一个打开文件的列表,该列表称为文件表。文件表是由一些非负整数进行索引,这些非负整数称为文件描述符(file descriptor 简称fd)。列表的每一项是一个打开的文件的信息,包括指向该文件索引节点(inode)内存拷贝的指针以及关联的元数据(如文件位置指针和访问模式)[1]。图1给出了文件描述符和文件的对应关系。
默认情况下,当通过fork创建子进程时,子进程会维护一份父进程的文件表副本。在该副本中,打开文件列表及其访问模式、当前文件位置以及其他元数据,都和父进程的文件表相同。只在一个地方有区别,即当子进程关闭一个文件时,不会影响父进程的文件表。
图1 文件描述符和文件的对应关系
从图1可以得到以下几点信息。
- 内核为每个进程都维护了一个文件表,文件表在底层是一个数组,索引从0开始,索引即为文件描述符,几乎所有对文件的操作均以文件描述符作为基本参数。在linux系统下,每个进程可打开的文件数是有上限的,默认上限值是1024。
2. 每个文件描述符都对应一个文件指针,该指针指向系统中已经打开的文件信息,该文件信息也是一个数组,打开的文件信息包括文件偏移量、状态标志以及inode指针。
3. inode指针指向系统中的inode表,inode表中的每个数据项都对应一个具体的磁盘文件。
4. 同一个进程内可以有两个文件指针指向同一个打开的文件信息(如进程A中fd=11和fd=12,这种情况表示,同一个进程下开启了两个线程,而这两个线程打开了同一个文件)。
5. 不同进程下可以有两个文件指针指向同一个打开的文件信息(如进程A中fd=18和进程B中的fd=0),有三种可能造成这种结果
- 进程B是进程A的子进程,子进程会维护父进程文件表副本
- 进程B和进程A打开了同一个文件,且分配的文件描述符相同
- 进程A通过socket将一个打开的文件描述符传递给进程B
6. 打开的文件信息包括文件偏移量和状态标志,文件偏移量跟读写文件有关,下文会有详细介绍,状态标志标识当前文件的状态,图中给出了只读、只写、读写三种模式。
另外值得一提的是,每个进程都至少包含三个文件描述符:0、1和2,分别表示标准输入(sdtin)、标准输出(sdtout)和标准错误(sdterr)。
2. 文件的读写
上一章介绍了文件描述符的概念,从这一章开始,讨论文件最基本的两个操作:文件的读写,也即文件I/O。
2.1. 打开/关闭文件
linux系统下一切皆文件,而对文件的所有操作都需要打开文件,所有操作结束之后都需要关闭文件,否则会出现预期之外的错误。
2.1.1. open
打开文件的系统函数是open。
代码语言:javascript复制int open (const char* name, int flags);
int open (const char* name, int flags, mode_t mode);
这里重点介绍第二个重载的open函数。open函数成功调用会返回打开的文件描述符。参数name是待打开的文件名,flags表示文件的访问模式,当创建文件时,mode提供了新建文件的权限,比如0644(文件所有者可以读写,其他人只能读)。open函数中最复杂的参数是flags,这里列举一些linux系统下常用的取值(O_REONLY、O_WRONLY、O_RDWR上文已介绍且较为简单,这里不再列举)。
取值 | 含义 |
---|---|
O_APPEND | 文件以追加模式打开,每次写操作之前,将更新文件位置指针,指向文件末尾。 |
O_ASYNC | 当指定的文件可读可写时,会产生一个信号。该标志位只适用于FIFO、管道、socket和终端,不适用普通文件(因为普通文件大多是可读可写的) |
O_CREAT | 当参数name指定的文件不存在时,内核自动创建 |
O_SYNC | 打开文件用于同步I/O。在数据写到磁盘此前,写操作都不会完成(下文会对写文件进行详细说明) |
O_NONBLOCK | 文件以非阻塞模式打开。不管是open调用还是其他调用,都不会导致进程在I/O中阻塞。 |
2.1.2. close
相比于open函数,close就简单直接得多。
代码语言:javascript复制int close (int fd);
close函数会取消当前进程文件描述符fd与其关联的文件之间的映射。值得一提的是,关闭文件并不意味着文件的数据已经写到磁盘,如果希望这么做,在打开文件时可指定O_SYNC模式。
2.1.3. creat
当指定的文件不存在时,open函数的flags可以指定为O_CREAT进行创建,linux有个系统调用支持这个功能
代码语言:javascript复制int creat (const char* name, mode_t mode);
creat函数中mode参数含义与open相同,成功时,返回新建的文件对应的文件描述符。
2.2. 通过read读文件
上文介绍了文件I/O的准备工作,从这一节开始,就要正式进入文件I/O的主题。
文件操作涉及到的最基础、最常见的系统调用是read和write。在此之外,也有高级读写函数,将在2.6节介绍。
代码语言:javascript复制ssize_t read (int fd, void *buf, size_t len);
每次调用read函数,会从fd指向的文件当前偏移开始读取len字节到buf所指向的内存中。调用成功后,fd的偏移量会移动,移动的长度由读取到的字节数决定。
调用read函数可能出现的结果如下:
1. 返回值等于len。表示读取的所有len个字节被存储到buf中,是正常的情况。
2. 返回值小于len,大于0。出现这种情况有2种原因。
- 读取到文件末尾,不够len个字节。
- 读取过程中信号中断或者读取中出错,可读数据大于0但小于len。
3. 返回值为0。表示刚好读取到文件末尾。
4. 阻塞。由于当前没有数据可读,调用阻塞。这种情况一般是读取socket文件。
5. 返回值为-1。表示出现错误,而错误原因非常多,有些重试可以解决,有些是致命的错误,即使重试也不会成功。
2.3. 通过write写文件
代码语言:javascript复制ssize_t write(int fd, const void *buf, size_t count);
write调用会从文件描述符fd指向的文件当前偏移开始,将buf中至多count个字节写入到文件中。调用成功后,fd的偏移量会移动,移动的长度由写入的字节数决定。
相比于read函数,调用write函数的结果就简单一些。
1. 返回值-1,表示出错。
2. 返回值0,没有什么特殊含义,只是表示写入了零个字节。
3. 返回值大于0(记为n),表示写入了n个字节的数据,同时偏移量往后移动n个字节。
2.4. 文件偏移量
上文反复强调文件的偏移量,它表示对当前文件进行操作的位置,相当于window系统打开文件光标所在处。一般情况下,文件的读写不需要手动修改文件偏移量(即I/O是线性的),如果某些应用需要跳跃读取文件内容(即随机I/O),那么就需要更新文件偏移量。linux系统提供lseek系统调用支持该功能。
代码语言:javascript复制off_t lseek(int fd, off_t pos, int origin);
lseek最常见的用法是将指针定位到文件开始、末尾或确定文件描述符的当前文件位置。
代码语言:javascript复制lseek(fd, (off_t)1250, SEEK_SET); // 将偏移量设置为1250
lseek(fd, 0, SEEK_SET); // 将偏移量设置为文件开头
lseek(fd, 0, SEEK_END); // 将偏移量设置为文件末尾
lseek(fd, 0, SEEK_CUR); // 确定当前文件位置
2.5. 同步I/O与页回写
数据只有在被写入磁盘中才算写入成功,但是进程又无法直接读写磁盘,只能借由内核提供的系统调用函数进行操作,上文所述的write就是linux内核提供给用户空间写磁盘的系统函数,那么,write函数执行成功后,数据真的写到磁盘了吗。答案是,未必。
2.5.1. 文件写流程
图2 文件写流程
图2给出了文件写的流程。
① 进程发起写请求,调用write系统函数;
② write函数更新内核空间中的页缓存,此次写请求需要更新两个页,分别是页缓存1和页缓存2(此时,页缓存中的数据和磁盘中的数据不一致,称这样的页为“脏页”);
③ 内核空间页缓存更新完毕,向进程返回成功。注意此时并未将页缓存中的数据刷新到磁盘文件;
④ 进程收到write返回成功,继续其他操作处理。一段时间后,发起读请求,调用read系统函数;
⑤ 内核空间收到read调用请求,发现需要读取的数据部分落在页缓存2和页缓存3中(称为读缓存命中),读取其内容;
⑥ read请求发现数据不完全在页缓存2和页缓存3中,于是读取磁盘文件1,并将文件中需要读取的数据放到页缓存3中;
⑦ 系统调用read将读取到的数据组装好返回给进程;
⑧ 满足一定条件时,内核空间将页缓存1和页缓存2中的数据刷新到磁盘。
从上图中可以看到,linux系统在进程发起write系统调用时,只是将数据写入内核缓冲区中的页缓存即返回,将内核空间页缓存中的数据刷新到磁盘(步骤⑧ )是异步的。这一点,与我们平常的认知是有些出入的,这一操作称为页回写。
2.5.2. 页回写
上文中提到,write函数调用并不会同步刷磁盘,而是等到一定时机后再执行,这个过程叫页回写,这里,着重讨论三个问题。
2.5.2.1. linux为什么要使用页回写机制
引入页回写机制一定是因为该机制对提升性能有较大的帮助。从图1中的流程可以看到,相比于同步刷盘,引入页缓存/页回写机制,能显著提高write/read的性能。众所周知,相比于内存,磁盘的IO是非常慢的,通过页缓存,write系统调用几乎不需要写入磁盘,read系统调用在缓存命中时也不需要读取磁盘,I/O的性能得到了质的提升,而且还不会修改读写语义(写完之后能立刻读到写的内容)。
2.5.2.2. 什么情况下会执行页回写刷盘
相信通过前文的介绍,读者这里也大约能够猜到页回写的时机,一个是时间维度,一个是空间维度。
- 时间维度 当内核缓冲区中某一个“脏页”存在的时长超过设定的阈值时,该“脏页”回写磁盘。该参数是/proc/sys/vm/dirty_expire_centisecs,单位是厘秒,默认值是3000,也就是30s,可以看到,“脏页”在内核缓冲区的最大缓存时效还是比较长的。
- 空间维度 当内核缓冲区中“脏页”的数量达到一定比例,或者说空闲缓冲页比率小于设定的阈值时,“脏页”回写磁盘,回收缓冲页。该参数是/proc/sys/vm/dirty_background_ratio,单位是百分比,默认值是10%,即空闲的页缓存比率小于10%时,触发页回写。
2.5.2.3. 页回写机制有什么问题
了解了页缓存/页回写机制,现在来看看页回写机制会引发什么问题。最大的问题是如果页回写失败了,会丢失写操作的数据。因为数据还只在内核空间的页缓存中,并没有持久化到磁盘,当操作系统重启后,内核空间清空,数据丢失。而且,数据丢失之后,还无法通知到用户进程。因为内核并不知道某一页的数据是由哪些用户线程写入,即使知道了,也无法通知它们,因为刷盘是异步的,此时可能用户线程已经销毁。
进而,什么情况下会造成页回写失败呢?系统调用write函数的具体行为是由操作系统控制的,正常情况下write函数都会成功,除非磁盘损坏或者操作系统出现严重错误宕机。
然而这两种情况下,数据是否写入磁盘都已经无关紧要了,磁盘损坏的情况下,已经无法读取,所以即使write成功了也没有意义;操作系统宕机后,服务都无法启动,就更不用谈文件读取了。
2.5.3. 同步I/O相关系统调用函数
上一小节详细探讨了页回写机制的几个问题,提到了刷盘的问题,这里介绍3个同步磁盘的系统调用函数
2.5.3.1. sync
代码语言:javascript复制void sync (void);
sync系统调用会一次将内核缓冲区中的数据全部写入磁盘,linux系统下该函数是同步函数,即所有“脏页”回写磁盘后才会返回。在执行之前,内核缓冲区中可能有大量的“脏页”,因此sync可能需要一段时间才能执行完,一般不建议使用,只有在需要重启linux操作系统时,在重启之前才应该执行sync函数。
2.5.3.2. fsync
相比于sync,fsync就轻量的多。
代码语言:javascript复制int fsync (int fd);
fsync需要传入一个文件描述符fd,该fd必须以写的方式打开。fsync是对该文件描述符对应的内核缓冲区中的”脏页“刷盘,该调用会回写数据和元数据。数据即文件的内容,元数据包括文件修改的时间戳以及索引节点中的其他属性。fsync是使用最频繁的同步I/O系统调用函数之一。
2.5.3.3. fdatasync
代码语言:javascript复制int fdatasync (int fd);
fdatasync和fsync使用方法相同,功能也类似,区别在于,fdatasync只会回写文件的数据,文件的元数据不会被更新。
就性能而言,fdatasync > fsync >>(注意这里是远远大于) sync
2.5.4. 同步I/O的应用
上一小节提到了同步I/O的相关系统调用函数,工程实践中fsync系统调用函数也有较为广泛的应用,这里给出mysql和redis的两个使用场景。
1. mysql的redo日志写磁盘
mysql在提交事务时需要将事务执行过程中执行的操作记录到redo日志中,innodb_flush_log_at_trx_commit变量控制redo刷盘的时机,取值情况如下
- 当该系统变量值为0时,表示在事务提交时不立即向磁盘中同步 redo 日志,这个任务是交给后台线程做的。这样很明显会加快请求处理速度,但是如果事务提交后服务器挂了,后台线程没有及时将 redo 日志刷新到磁盘,那么该事务对页面的修改会丢失。
- 当该系统变量值为1时,表示在事务提交时需要将 redo 日志同步到磁盘,可以保证事务的持久性 。1也是 innodb_flush_log_at_trx_commit的默认值。(也即调用了write fsync)
- 当该系统变量值为2时,表示在事务提交时需要将 redo 日志写到操作系统的缓冲区中,但并不需要保证将日志真正的刷新到磁盘。这种情况下如果数据库挂了,操作系统没挂的话事务的持久性还是可以保证的,但是操作系统也挂了的话,那就不能保证持久性了。(即只调用了write)
2. redis AOF持久化写磁盘
redis AOF持久化中,appendfsync用于控制何时将缓冲区中的文件刷新到磁盘。注意,下表的aof_buf缓冲区是进程内的缓冲区(下一节即会介绍),属于图1中的用户空间,不是内核空间的页缓存,千万不要混淆。
appendfsync选项的取值 | flushApppendOnlyFile函数的行为 |
---|---|
always | 将 aof_buf 缓冲区中的所有内容写入并同步到 AOF 文件(相当于执行write fsync)。 |
everysec | 将 aof_buf 缓冲区中的所有内容写入到 AOF 文件, 如果上次同步 AOF 文件的时间距离现在超过一秒钟, 那么再次对 AOF 文件进行同步, 并且这个同步操作是由一个线程专门负责执行的(相当于write 定时fsync)。 |
no | 将 aof_buf 缓冲区中的所有内容写入到 AOF 文件, 但并不对 AOF 文件进行同步, 何时同步由操作系统来决定(只执行write)。 |
在了解了linux文件写流程之后,相信对于这两个工程实践中参数的取值会有更深刻的理解。
2.6. 用户缓冲I/O
上一小节介绍了访问文件最基本的方式:系统调用。linux引入内核缓冲区,将需要写的数据更新到内核缓冲区后即返回,大大提升了write系统调用的性能。内核缓冲是由linux引入的,对于上层应用来说是“透明”的,那么,应用进程能否通过某种手段,来加速文件的读写呢?答案就是使用用户缓冲。
用户缓冲I/O是在用户空间而不是在内核中完成的,用户缓冲的作用是减少系统I/O调用的次数。
2.6.1. 带有用户缓冲I/O相关的函数
代码语言:javascript复制#include <stdio.h>
FILE * fopen (const char *path, const char *mode); // 根据mode参数,按照指定模式打开path指向的文件,并给他关联新的流。mode取值有:"r","r ","w","w "...
FILE * fdopen (int fd, const char *mode);
int fclose (FILE *stream); // 关闭流
int fgetc (FILE *stream); // 从流中读取单个字符
int fputc (int c, FILE *stream); // 向流中写入单个字符
这些带有用户缓冲的I/O函数称为”标准I/O“,而上文介绍的只有内核缓冲区的那些I/O函数称为系统调用。
2.6.2. 用户缓冲I/O示例程序
代码语言:javascript复制#include <stdio.h>
#define BUFFER_SIZE 100
int main(void) {
FILE *in, *out;
char data[]="that is spencer";
char show_data [BUFFER_SIZE];
char filename[] = "spencer.txt";
out = fopen(filename,"w");
for(char x:data){
fputc(x, out);
}
fclose(out);
in = fopen(filename, "r");
fgets(show_data,BUFFER_SIZE, in);
printf("%sn",show_data);
fclose(in);
}
表面上看,写文件时每次只写一个字符,效率应该很低,实际上,只写一个字符与写一个字符串性能上并没有太大的差别,主要原因就是使用了用户缓冲,如图3。
图3 带有用户缓冲的I/O
图3中,每次fputc函数只会写1个字母,但是由于存在用户缓存,原先需要调用4次write函数(that每个字母各调用一次),汇总为"that"之后调用一次write,减少了系统调用的次数。图3中隐藏了内核与磁盘的交互,与图2一致,这里的重点是用户空间和内核空间的交互。
2.6.3 用户缓冲的类型
标准I/O实现了三种类型的用户缓冲,并为开发者提供了接口,可以控制缓冲区类型和大小。不同类型的用户缓冲提供不同功能,并适用于不同的场景 [7]。
- 无缓冲
不执行用户缓冲,数据直接提交给内核。通常很少使用,只有标准错误采用这种模式。
- 行缓冲
缓冲以行为单位执行,每遇到换行符,缓冲区会被提交到内核。行缓冲对把流输出到屏幕时很有用,因此,标准输出使用行缓冲模式。
- 块缓冲
缓冲以块为单位执行,它适用于处理文件,默认情况下,和文件相关的所有流都是块缓冲模式。
2.7. 对象存储映射mmap
上文介绍了文件I/O的基本函数,包括系统调用和标准I/O,除了这些函数,还有其他的I/O方式,对象存储映射即为其中一种。
2.7.1. 函数原型
mmap调用请求内核将文件描述符fd所指向的对象len个字节数据映射到内存中,起始位置从offset开始,函数原型为
代码语言:javascript复制#include <sys/mman.h>
void * mmap (void *addr, size_t len, int prot, int flags, int fd, off_t offset);
- addr参数指定内核映射文件的最佳位置,一般情况下传0;
- prot参数指定了访问权限,取值有PROT_READ(页可读)/PROT_WRITE(页可写)/PROT_EXEC(页可执行),访问权限不可与fd打开的访问模式冲突;
- flags指定了其他操作行为,常用取值有MAP_PRIVATE(表示映射区不共享。文件映射采用了“写时复制“,进程对内存的任何改变不影响真正文件或其他进程的映射)/MAP_SHARED(表示和所有其他映射该文件的进程共享映射内存,对内存的写操作等效于写文件)。
图4 对象存储映射
2.7.2. 写时复制
mmap中flags参数取值为MAP_PRIVATE时表示文件映射采用”写时复制“(Copy on Write COP),写时复制的全称是“读时共享,写时复制”。
当通过 fork() 来创建一个子进程时,操作系统需要将父进程虚拟内存空间中的大部分内容全部复制到子进程中(主要是数据段、堆、栈、代码段)。这个操作不仅非常耗时,而且会浪费大量物理内存[2]。如果父子进程只有极少数的内容是不同的,那么这种浪费和性能损耗会变的尤其严重。因此,linux提供了写时复制技术。当fork子进程时,子进程保存指向父进程相同资源的指针,每当有进程修改资源时,就会复制该资源,并把副本提供给该进程,复制过程对其他进程是“透明”的。
2.7.3. 写时复制的实践应用
redis的rdb持久化中,使用fork子进程的方式。当redis进程收到bgsave命令时,会创建一个子进程,让子进程执行快照落盘的操作,父进程继续接受客户端请求[3]。
图5 fork子进程时使用COP VS 不使用COP
图5中上半部分为普通父子进程复制方式,子进程会完全拷贝父进程中的数据,复制完成后,子进程拥有独立的完整的数据,但耗时较长。下半部分为采用COP的复制方式,子进程拥有父进程的数据的指针,当父进程写数据时,发生缺页中断,并拷贝一份,子进程拥有副本,父进程在该页上修改数据不影响子进程。
2.7.4. mmap实例
代码语言:javascript复制#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
int mmapDemo(char* filename){
struct stat sb;
off_t len;
char*p;
int fd = open(filename, O_RDWR);
// 获取fd的元信息并将其存入sb结构中
if(fstat(fd, &sb) == -1){
perror("fstat");
return 1;
}
// 建立存储映射
p = (void *)mmap(0, sb.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(p == MAP_FAILED){
perror("mmap");
return 1;
}
if(close(fd) == -1){
perror("close");
return 1;
}
// 读文件内容
for(len=0;len<sb.st_size;len ){
putchar(p[len]);
}
// 写部分文件内容
for(len=3;len<sb.st_size;len ){
p[len] = 's';
}
p[len-1] = 'n';
// 取消存储映射
if(munmap(p, sb.st_size) == -1){
perror("munmap");
return 1;
}
return 0;
}
int main(){
char filename[] = "spencer.txt";
return mmapDemo(filename);
}
图6 mmap执行的结果
上文程序清单中使用mmap映射了文件spencer.txt,并读取了该文件的内容,同时修改了部分内容。从中也可以看到,即使已经关闭文件(调用了close函数),依旧可以写入文件,因为存储映射还在。写文件内容时,从第三个字节开始,将文件后续的内容全部刷为字母’s‘。
程序中使用了munmap函数取消存储映射,一旦取消了存储映射,再对p进行操作就不会反应到文件中。
2.7.5. mmap评价
2.7.5.1. mmap的优点
相对于系统调用read&write而言,使用mmap有很多优点,主要体现在:
1. 使用read和write系统调用时,需要从用户缓冲区进行数据的读写,一次读写需要经历4次拷贝过程(磁盘---->内核缓冲区,内核缓冲区---->用户缓冲区;用户缓冲区---->内核缓冲区,内核缓冲区---->磁盘),而使用mmap只需要2次拷贝(磁盘---->内核缓冲区,内核映射区--→磁盘);
2. 读写映射文件不会带来系统上下文切换的开销,而使用read&write,CPU需要从用户态切换到内核态,再从内核态切换到用户态。
图7 基本文件读写与mmap的对比
2.7.5.2. mmap的限制
相比于read&write,mmap在性能上有明显的提升,但是mmap使用却比前者更受限。从mmap的api也可以看到,mmap必须将文件的len字节信息映射到进程空间的映射区,需要开发人员在一开始就确定len的大小,不太适用于随机读写的场景,read&write则更加通用一些。另外,由于需要进行映射,会占用进程映射区的空间,因此映射的大小有限制,一般在1.5G~2G之间。
2.7.6. mmap的实践应用
kafka之所以吞吐量高,其中的一个重要原因就是使用了mmap的方式[4]。producer将数据写到broker中,comsumer从broker读取数据时使用了mmap。因为此时broker的log文件对于consumer是只读的,而且,kafka会存储consumer读取的偏移量,也就刚好对应mmap中的offset。
3. I/O多路复用
上文详细介绍了磁盘文件的I/O,现在,将目光聚集到网络文件I/O,看看网络文件的I/O与磁盘文件I/O有何区别。
磁盘上的文件在正常情况下总是可读且有数据,但是,网络文件却并非如此,即使socket文件处于可读状态,数据也不一定就绪。如果进程需要监听多个网络文件,每个文件就需要开启一个线程处理,可以想象当网络文件数量达到一定程度,进程处理就显得捉襟见肘,I/O多路复用就是解决此类问题。
I/O多路复用支持应用在多个文件描述符上阻塞,并在其中某个带监听的事件发生时收到通知。linux提供了三种I/O多路复用方案,分别为select、poll、epoll,下面对其进行详细介绍。
3.1. select
最基础的I/O多路复用机制是select,与select相关的系统调用函数如下。
代码语言:javascript复制int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
FD_CLR (int fd, fd_set *set); // 向指定集合中删除一个文件描述符
FD_ISSET (int fd, fd_set *set); // 检查一个文件描述符是否在给定集合中
FD_SET (int fd, fd_set *set); // 向指定集合中添加一个文件描述符
FD_ZERO (fd_set *set); // 清空一个文件描述符集合
select函数可以监听3中类别的文件描述符,分别是读文件描述符集合,写文件描述符集合和异常文件描述符集合。假定readfds集合中一开始有两个文件描述符7和9,当调用返回时,如果描述符7还在readfds集合中,说明fd=7的文件处于可读状态,而fd=9的文件在读取时很可能发生阻塞。
3.1.1. select示例
下面通过一个具体的实例,了解select的用法及原理,并从中找出select的局限性。
下面代码想要达到的效果是,监听两个已打开的文件(分别是标准输入和标准错误,实际使用时,可以替换成任意已打开的文件的文件描述符)读事件,当这些文件中任意一个有数据可读时,拿到这些可读的文件描述符,随后进行读取。
代码语言:javascript复制#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#define TIMEOUT 5 // 设置select的超时时间
#define BUF_LEN 1024 // 读缓冲区大小
int readFile(int fd){
char buf[BUF_LEN 1];
int len = read(fd, buf, BUF_LEN);
if (len == -1) {
perror("read");
return 1;
}
if (len) {
buf[len] = '