简介
我们以文件输入输出操作函数为开始,对UNIX系统进行讲解。文件输入输出函数包括:打开文件、读取文件、写文件等等。大多数UNIX系统上面的文件输入输出操作,都可以使用5个函数来完成: open
, read
, write
, lseek
,以及 close
。我们之后也会看到,对读写文件时候,设置不同的缓存大小会有什么效果。
本章所述的函数一般都被称作无I/O缓冲的函数,和我们第5章所讲述的标准I/O函数相对。非缓冲的意思就是说,每次 read
或者 write
会产生内核中的一次系统调用。这些非缓冲的I/O函数并不是 ISO C
的一个部分,但是却是 POSIX.1
和 the Single UNIX Specification
的一个部分。
当我们讲到多进程之间的资源共享的时候,原子操作会变得非常重要。我们在讨论文件I/O以及 open
函数的参数的时候会对此进行讲述。这会就引出了多个进程之间如何共享文件,以及内核包含什么样的数据结构,这样的话题。讲述这些之后,我们会继续讲述 dup
, fcntl
, sync
, fsync
和 ioctl
函数。
译者注
原文参考
1、文件描述符号
内核用文件描述符来引用所有打开文件。文件描述符是一个非负整数。当打开已有文件或创建新文件时,内核就向进程返回一个文件描述符。当读、写一个文件时,用 open
或 creat
返回的文件描述符标识该文件,将其作为参数传送给 read
或 write
。
按照惯例,UNIX shell使用文件描述符0表示进程的标准输入,文件描述符1表示标准输出,文件描述符2表示标准错误输出。按照这个惯例,在 POSIX.1
应用程序中,幻数0、1、2应被代换成符号常数 STDIN_FILENO
, STDOUT_FILENO
和 STDERR_FILENO
,这样能够提高程序的可移植特性。这些常数都定义在头文件 <unistd.h>
中。另外,文件描述符的范围是 0~OPEN_MAX
(表示一个进程最多可以打开的文件数目),具体取值依据系统有所不同。
后面将对基本文件操作函数做简单介绍,具体可以运行 man 2 <functionname>
参见我们自己系统上的用户手册。
译者注
原文参考
2、 open
函数
调用 open
函数可以打开或创建一个文件。其具体声明如下:
#include <fcntl.h>
int open(const char *pathname, int oflag, ... /* mode_t mode */ );
返回:如果成功返回文件描述符号,如果错误返回1。
(注意,这里出错的时候返回值为1,经过网上搜索发现,一般错误的时候,返回-1,也就是说,前面说的“如果错误返回1”中的“1”其实是布尔值,其实际值一般为-1,并且产生错误的时候,还会将错误编码记录到 errno
全局变量中。本书好多地方都这样说返回值,虽然简洁,但是可能会带来一些困扰所以读者应当注意,具体需要细究的时候,还需亲自查找手册上对错误情况返回值的说明)
这里,第三个参数写为 ...
,这是 ANSI C
表示余下参数的数目和类型可以变化的方法。 pathname
是要打开或创建的文件的名字。 oflag
参数可用来说明此函数的多个选择项。
oflag
必须指定如下三个值之一:
-
O_RDONLY
只读打开。 -
O_WRONLY
只写打开。 -
O_RDWR
读、写打开。
下列常数则是可选择的:
-
O_APPEND
每次写时都加到文件的尾端。 -
O_CREAT
若此文件不存在则创建它。使用此选择项时,需同时指明第三个参数mode
,以说明该新文件的存取许可权位。 -
O_EXCL
如果同时指定了O_CREAT
,而文件已经存在,那么就出错。这样可以测试文件是否存在,不存在则创建,并且这个操作是原子的。 -
O_TRUNC
如果此文件存在,而且为只读或只写方式打开,则将其长度截短为0。 -
O_NOCTTY
如果pathname
指的是终端设备,则不将此设备分配作为此进程的控制终端。 -
O_NONBLOCK
如果pathname
指的是一个FIFO
、一个块特殊文件或一个字符特殊文件,则本次打开操作和后续的I/O
操作设置非阻塞方式。 -
O_SYNC
使每次write
都等到物理I/O
操作完成。
由 open
返回的文件描述符一定是最小的没有使用的描述符数字。如果一个应用程序先关闭标准输出(通常是文件描述符1),然后打开另一个文件,那么就能知道该文件一定会在文件描述符1上打开,这个方法也是一个很常用的方法。后面讲到 dup2
函数时,会了解到有更好的方法来保证在一个给定的描述符上打开一个文件。
当用 append
标记打开文件的时候,还是可以用 lseek
定位到任何地方来读取文件内容的,但是如果写的话,文件的 offset
会自动地定位到文件的结尾并且写。也就是说,用 append
的话,写操作不会写到除了结尾之外的地方(即使是用 lseek
定位也不会,这里的内容最好也参考原书3章13节,以及练习3.6)。
译者注
原文参考
3、 create
函数
也可用 creat
函数创建一个新文件。声明如下:
#include <fcntl.h>
int creat(const char *pathname, mode_t mode);
返回:如果成功返回一个只写的文件描述符浩,如果错误返回1。
此函数等效于:
open(pathname,O_WRONLY|O_CREAT|O_TRUNC, mode)。
译者注
原文参考
4、 close
函数
close
函数关闭一个打开文件。声明如下:
#include <unistd.h>
int close(int filedes);
返回:如果成功返回0,如果错误返回1。
进程结束的时候内核会自动关闭它打开的所有文件;关闭文件的时候,会自动释放当前进程持有的所有在那个被关闭文件上面的锁。很多程序都使用这一功能而不显式地用 close
关闭打开的文件。
译者注
原文参考
5、 lseek
函数
每个被打开的文件都有一个与其相关联的“当前文件偏移量” 。其值为从文件开始到当前位置的字节数。通常,读、写操作都从当前文件偏移量开始,并每次读写之后,会相应地对移量进行增加。系统默认,打开一个文件时,偏移量为0(除非指定 O_APPEND
选项)。
我们可以通过函数 lseek
来设置文件偏移量,这个函数声明如下:
#include <unistd.h>
off_t lseek(int filedes, off_t offset, int whence);
返回:如果成功返回新的文件偏移,如果错误返回1。
对参数 offset
的含义根据参数 whence
的值有不同的解释:
- 若
whence
是SEEK_SET
,则offset
是相对文件开始计算的偏移字节数。 - 若
whence
是SEEK_CUR
,则offset
是相对文件当前偏移值计算的偏移字节数(可正可负)。 - 若
whence
是SEEK_END
,则offset
是相对文件末尾开始计算的偏移字节数(可正可负)。
若 lseek
成功执行,则返回新的文件偏移量。如下方法可以确定一个打开文件的当前偏移量:
off_t currpos;
currpos = lseek(fd, 0, SEEK_CUR);
这种方法也可用来确定所涉及的文件是否可以设置位移量。如果文件描述符引用的是一个管道或 FIFO
,则 lseek
返回-1,并将 errno
设置为 EPIPE
。
pipe
, = FIFO= , = socket= 等这样不能 lseek
的文件当被 lseek
的时候,会返回1并且置 errno
为 ESPIPE.lseek
返回值对于设备文件等特殊的文件可能为负数,对于正规文件非负,所以检测时候要小心。检测方法大致如下:
if (lseek(STDIN_FILENO, 0, SEEK_CUR) == -1)
printf("cannot seek\n");
else
printf("seek OK\n");
另外,原文在第4章12节中提到,当 lseek
的位置大于文件的大小,然后在写入数据,会扩展文件大小,并且在原来的结尾和 lseek
处创建文件“空洞”。文件空洞并不会消耗磁盘空间,查看文件空洞可以用 od
。具体还是参见上面的网址。后面会继续说道文件“空洞”。大致是 ls -l
的 size
和 du
的 size
不是一样的,似乎前者包含了 hole
的,而 du
是不包含空洞的。 cat
就会把 hole
用空字符打印出来,所以一个包含了 hole
的文件 core
,用:
#cat core>core.copy
之后,再:
#du -s core.copy
根据显示的大小会发现比原来的大小(用 du -s core
看)大了。
译者注
原文参考
6、 read
函数
read
函数用来从打开的文件当中读取数据。声明如下:
#include <unistd.h>
ssize_t read(int filedes, void *buf, size_t nbytes);
返回:如果成功则返回读取的字节数目,如果遇到文件结尾则返回0,如果错误返回1。
read
读取,是从当前的文件偏移开始读取的。(对于返回1的时候如何确定是正确读取还是错误,经过网上搜索发现,一般错误的时候, read
返回 -1
,具体情况前面讲述 open
函数的时候说明了对于返回值是如何处理的)
译者注
原文参考
7、 write
函数
write
函数用来想打开的文件写入数据。声明如下:
#include <unistd.h>
ssize_t write(int filedes, const void *buf, size_t nbytes);
返回:如果成功返回写入字节数目,如果错误返回1。(对于返回1如何确定是正确写入还是错误,经过网上搜索发现,一般错误的时候, write
返回 -1
,具体情况前面讲述 open
函数的时候说明了对于返回值是如何处理的)
write
写入,是从当前的文件偏移开始写入的。
译者注
原文参考
8、 I/O
的效率
对于如下代码片断:
#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);
}
这个代码的解释详细参见参考资料。对于这里的 read
和 write
调用,其 BUFFSIZE
表示每次调用的时候,尝试读、写的字节数目。这个 BUFFSIZE
取不同的值,会导致 read
和 write
调用次数的不同,一般来说, BUFFSIZE
取值越大,则调用次数越少,调用次数越少,则消耗的系统时间越小。但是当 BUFFSIZE
大到一定程度的时候,就不会对 I/O
效率有更多的改善了。下面给出的一个表格,对比了各种 BUFFSIZE
对 I/O
效率的影响。
通过不同的缓存大小进行 read
所消耗的时间
+-------------------------------------------------------------------------------------------+
| BUFFSIZE | User CPU (seconds) | System CPU (seconds) | Clock time (seconds) | #loops |
|----------+--------------------+----------------------+----------------------+-------------|
| 1 | 124.89 | 161.65 | 288.64 | 103,316,352 |
|----------+--------------------+----------------------+----------------------+-------------|
| 2 | 63.10 | 80.96 | 145.81 | 51,658,#176 |
|----------+--------------------+----------------------+----------------------+-------------|
| 4 | 31.84 | 40.00 | 72.75 | 25,829,088 |
|----------+--------------------+----------------------+----------------------+-------------|
| 8 | 15.17 | 21.01 | 36.85 | 12,914,544 |
|----------+--------------------+----------------------+----------------------+-------------|
| 16 | 7.86 | 10.27 | 18.76 | 6,457,272 |
|----------+--------------------+----------------------+----------------------+-------------|
| 32 | 4.13 | 5.01 | 9.76 | 3,228,636 |
|----------+--------------------+----------------------+----------------------+-------------|
| 64 | 2.11 | 2.48 | 6.76 | 1,614,318 |
|----------+--------------------+----------------------+----------------------+-------------|
| 128 | 1.01 | 1.27 | 6.82 | 807,159 |
|----------+--------------------+----------------------+----------------------+-------------|
| 256 | 0.56 | 0.62 | 6.80 | 403,579 |
|----------+--------------------+----------------------+----------------------+-------------|
| 512 | 0.27 | 0.41 | 7.03 | 201,789 |
|----------+--------------------+----------------------+----------------------+-------------|
| 1,024 | 0.17 | 0.23 | 7.84 | 100,894 |
|----------+--------------------+----------------------+----------------------+-------------|
| 2,048 | 0.05 | 0.19 | 6.82 | 50,447 |
|----------+--------------------+----------------------+----------------------+-------------|
| 4,096 | 0.03 | 0.16 | 6.86 | 25,223 |
|----------+--------------------+----------------------+----------------------+-------------|
| 8,192 | 0.01 | 0.18 | 6.67 | 12,611 |
|----------+--------------------+----------------------+----------------------+-------------|
| 16,384 | 0.02 | 0.18 | 6.87 | 6,305 |
|----------+--------------------+----------------------+----------------------+-------------|
| 32,768 | 0.00 | 0.16 | 6.70 | 3,152 |
|----------+--------------------+----------------------+----------------------+-------------|
| 65,536 | 0.02 | 0.19 | 6.92 | 1,576 |
|----------+--------------------+----------------------+----------------------+-------------|
| 131,072 | 0.00 | 0.16 | 6.84 | 788 |
|----------+--------------------+----------------------+----------------------+-------------|
| 262,144 | 0.01 | 0.25 | 7.30 | 394 |
|----------+--------------------+----------------------+----------------------+-------------|
| 524,288 | 0.00 | 0.22 | 7.35 | 198 |
+-------------------------------------------------------------------------------------------+
(我们可以这样理解,使用 read
或者 write
等系统调用直接对文件操作,如果我们自己知道那个临界的 BUFFERSIZE
,那么就能够找到最优效率的参数来调用它;而不使用系统调用且使用标准库函数进行读写的话,标准库函数中自动设置了一个比较通用的 BUFFERSIZE
,不用要求我们自己设置大小了,这样可以得到较优的读写效率,这也是库函数和系统调用的一个不同)。
总之,直接使用系统调用的 read
和 write
进行文件输入输出操作,没有自动指定缓存大小(需要手动设置每次读取的大小);而使用库函数的话就有缓存了,缓存的大小设置为和磁盘块大小一样最省时间。也就是说,一次 read
的数据如果是在磁盘块大小之内的话,时间是差不多的,所以最好把缓存设置为和磁盘块一样大小。
另外,一般读写文件的时候,操作系统会自动尝试把文件缓存到内核中,这样下次操作同样文件的时候会比较快一些,所以测试文件操作时间的时候使用不同的文件会比较准确。这也是 coredump
的来源。使用 sync
可以将缓存的数据刷新到磁盘上面。有许多类型的 sync
,有的只刷新文件数据,有的连文件属性也刷新了。
网友评论