简介
我们以文件输入输出操作函数为开始,对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 ,有的只刷新文件数据,有的连文件属性也刷新了。



京公网安备 11010502036488号