UNIX系统的大部分文件I/O操作都可以通过以下五个方法实现:open、read、write、lseek和close。这些方法被称之为Unbuffered的原因是它们直接执行系统调用。
文件描述符
文件描述符代表被打开的文件。进程的标准输出文件、标准输入文件和标准错误文件分别对应文件描述符0、1和2。
常用函数
I/O效率
使用read/write进行文件读写操作。
#include "apue.h"
#define BUFFSIZE 4096
int
main(void)
{
int n;
char buf[BUFFSIZE];
while ((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0)
if (write(STDOUT_FILENO, buf, n) != n)
err_sys("write error");
if (n < 0)
err_sys("read error");
exit(0);
}
下图为使用不同的BUFFSIZE运行程序的结果
- 运行程序,标准输入重定向到文件,标准输出重定向到/dev/null。
- 系统环境:Linux,ext4文件系统,block大小为4096字节。
- 从结果看,文件系统支持预先读取,所以BUFFSIZE为32时,性能也不太差。
- 测试过程中,需要注意内核缓存,如果重复读取同一个文件,操作系统会缓存(incore)。
BUFFSIZE | User CPU (seconds) | System CPU (seconds) | Clock CPU (seconds) | Number Of loops |
1 | 20.03 | 117.50 | 138.73 | 516,581,760 |
2 | 9.69 | 58.76 | 68.60 | 258,290,880 |
4 | 4.60 | 36.47 | 41.27 | 129,145,440 |
8 | 2.47 | 15.44 | 18.38 | 64,572,720 |
16 | 1.07 | 7.93 | 9.38 | 32,286,360 |
32 | 0.56 | 4.51 | 8.82 | 16,143,180 |
64 | 0.34 | 2.72 | 8.66 | 8,071,590 |
128 | 0.34 | 1.84 | 8.69 | 4,035,795 |
256 | 0.15 | 1.30 | 8.69 | 2,017,898 |
512 | 0.09 | 0.95 | 8.63 | 1,008,949 |
1,024 | 0.02 | 0.78 | 8.58 | 504,475 |
2,048 | 0.04 | 0.66 | 8.68 | 252,238 |
4,096 | 0.03 | 0.58 | 8.62 | 126,119 |
8,192 | 0.00 | 0.54 | 8.52 | 63,060 |
16,384 | 0.01 | 0.56 | 8.69 | 31,530 |
32,768 | 0.00 | 0.56 | 8.51 | 15,765 |
65,536 | 0.01 | 0.56 | 9.12 | 7,883 |
131,072 | 0.00 | 0.58 | 9.08 | 3,942 |
262,144 | 0.00 | 0.60 | 8.70 | 1,971 |
524,288 | 0.01 | 0.58 | 8.58 | 986 |
进程间文件共享
- process table entry:由每个进程管理,包含打开的文件描述符、文件描述符标记(如close-on-exec)和指向file table entry的指针。
- file table entry:由内核维护,包含文件打开状态标记(对应之前open函数中的oflag)、当前位置偏移量和指向v-node table entry的指针
- v-node table entry:文件元数据和指向文件数据的指针。(其中Linux只有i-node,下一章中讲解具体结构)
正常情况,两个进程分别打开同一个文件,它们拥有不同的process table entry和file table entry,共享同一个v-node table entry,所以能够支持不同的打开状态标记,不同的位置偏移量等。(之前提到的lseek操作只改变file table entry中的偏移量,不进行实际I/O操作。)
如果使用dup方法,针对这个文件,不同的进程共享相同的file table entry,拥有不同的process table entry。这常见于fork父子进程场景(后续介绍)。
原子操作
Appending
如果使用lseek+write操作,可以实现在文件末尾处写入数据,潜在问题是这是两个系统调用,不是原子性的。正确方式是使用O_APPEND标记打开文件,再进行write操作,内核会保证每次write操作都是在文件末尾处写入数据。XSI还提供了两个方法,实现原子性的lseek和读写操作,其中offset参数代表位置偏移量。
#include<unistd.h>
ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
Returns: number of bytes read, 0 if end offile, -1 on error
ssize_t pwrite(int fd, const void *buf, size_tnbytes, off_t offset);
Returns: number of bytes written if OK, -1 on error
创建文件
同样,如果先检查文件是否存在,再创建文件,这两个操作也不是原子性的。正确方式是使用O_CREATE和O_EXCL组合标记open文件。
其他函数
dup函数
#include<unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);
Both return: new file descriptor if OK, -1 on error
使用dup(fd1)函数返回fd3后的文件共享结构图如下,fd3和fd1具有相同的文件状态标记和当前位置。dup2可以直接指定新的文件描述符为fd2,常用于实现输入输出重定向操作。如dup2(fd1,STDOUT_FILENO),先关闭当前标准输出,再将标准输出重定向到fd1。
sync、fsync和fdatasync函数
为了提高效率,写入操作不会立刻将数据写入到磁盘,只是写入到内核维护的cache中,不定期同步到硬盘,这称之为延缓写入(delayed write)。系统也会定期(一般每隔30s)调用sync函数。为了保持一致性,可以手动调用sync系列方法。注:Sync只是将内核缓冲中的数据送给磁盘写队列,不会等待写入操作完成。
fcntl函数
获取、更新已经打开文件的状态(包括process table entry和file table entry中的状态),比如文件描述符状态标记FD_CLOEXEC(调用exe后是否关闭文件),文件打开状态标记等。