美文网首页Linux学习之路
APUE读书笔记-03文件输入输出(1)

APUE读书笔记-03文件输入输出(1)

作者: QuietHeart | 来源:发表于2020-04-04 09:03 被阅读0次

    简介

    我们以文件输入输出操作函数为开始,对UNIX系统进行讲解。文件输入输出函数包括:打开文件、读取文件、写文件等等。大多数UNIX系统上面的文件输入输出操作,都可以使用5个函数来完成: openreadwritelseek ,以及 close 。我们之后也会看到,对读写文件时候,设置不同的缓存大小会有什么效果。

    本章所述的函数一般都被称作无I/O缓冲的函数,和我们第5章所讲述的标准I/O函数相对。非缓冲的意思就是说,每次 read 或者 write 会产生内核中的一次系统调用。这些非缓冲的I/O函数并不是 ISO C 的一个部分,但是却是 POSIX.1the Single UNIX Specification 的一个部分。

    当我们讲到多进程之间的资源共享的时候,原子操作会变得非常重要。我们在讨论文件I/O以及 open 函数的参数的时候会对此进行讲述。这会就引出了多个进程之间如何共享文件,以及内核包含什么样的数据结构,这样的话题。讲述这些之后,我们会继续讲述 dupfcntlsyncfsyncioctl 函数。

    译者注

    原文参考

    参考: APUE2/ch03lev1sec1.html

    1、文件描述符号

    内核用文件描述符来引用所有打开文件。文件描述符是一个非负整数。当打开已有文件或创建新文件时,内核就向进程返回一个文件描述符。当读、写一个文件时,用 opencreat 返回的文件描述符标识该文件,将其作为参数传送给 readwrite

    按照惯例,UNIX shell使用文件描述符0表示进程的标准输入,文件描述符1表示标准输出,文件描述符2表示标准错误输出。按照这个惯例,在 POSIX.1 应用程序中,幻数0、1、2应被代换成符号常数 STDIN_FILENOSTDOUT_FILENOSTDERR_FILENO ,这样能够提高程序的可移植特性。这些常数都定义在头文件 <unistd.h> 中。另外,文件描述符的范围是 0~OPEN_MAX (表示一个进程最多可以打开的文件数目),具体取值依据系统有所不同。

    后面将对基本文件操作函数做简单介绍,具体可以运行 man 2 <functionname> 参见我们自己系统上的用户手册。

    译者注

    原文参考

    参考: APUE2/ch03lev1sec2.html

    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)。

    译者注

    原文参考

    参考: APUE2/ch03lev1sec3.html

    3、 create 函数

    也可用 creat 函数创建一个新文件。声明如下:

    #include <fcntl.h>
    int creat(const char *pathname, mode_t mode);
    

    返回:如果成功返回一个只写的文件描述符浩,如果错误返回1。

    此函数等效于:

    open(pathname,O_WRONLY|O_CREAT|O_TRUNC, mode)。
    

    译者注

    原文参考

    参考: APUE2/ch03lev1sec4.html

    4、 close 函数

    close 函数关闭一个打开文件。声明如下:

    #include <unistd.h>
    int close(int filedes);
    

    返回:如果成功返回0,如果错误返回1。

    进程结束的时候内核会自动关闭它打开的所有文件;关闭文件的时候,会自动释放当前进程持有的所有在那个被关闭文件上面的锁。很多程序都使用这一功能而不显式地用 close 关闭打开的文件。

    译者注

    原文参考

    参考: APUE2/ch03lev1sec5.html

    5、 lseek 函数

    每个被打开的文件都有一个与其相关联的“当前文件偏移量” 。其值为从文件开始到当前位置的字节数。通常,读、写操作都从当前文件偏移量开始,并每次读写之后,会相应地对移量进行增加。系统默认,打开一个文件时,偏移量为0(除非指定 O_APPEND 选项)。

    我们可以通过函数 lseek 来设置文件偏移量,这个函数声明如下:

    #include <unistd.h>
    off_t lseek(int filedes, off_t offset, int whence);
    

    返回:如果成功返回新的文件偏移,如果错误返回1。

    对参数 offset 的含义根据参数 whence 的值有不同的解释:

    • whenceSEEK_SET ,则 offset 是相对文件开始计算的偏移字节数。
    • whenceSEEK_CUR ,则 offset 是相对文件当前偏移值计算的偏移字节数(可正可负)。
    • whenceSEEK_END ,则 offset 是相对文件末尾开始计算的偏移字节数(可正可负)。

    lseek 成功执行,则返回新的文件偏移量。如下方法可以确定一个打开文件的当前偏移量:

    off_t    currpos;
    currpos = lseek(fd, 0, SEEK_CUR);
    

    这种方法也可用来确定所涉及的文件是否可以设置位移量。如果文件描述符引用的是一个管道或 FIFO ,则 lseek 返回-1,并将 errno 设置为 EPIPE

    pipe , = FIFO= , = socket= 等这样不能 lseek 的文件当被 lseek 的时候,会返回1并且置 errnoESPIPE.lseek 返回值对于设备文件等特殊的文件可能为负数,对于正规文件非负,所以检测时候要小心。检测方法大致如下:

    if (lseek(STDIN_FILENO, 0, SEEK_CUR) == -1)
            printf("cannot seek\n");
    else
            printf("seek OK\n");
    

    另外,原文在第4章12节中提到,当 lseek 的位置大于文件的大小,然后在写入数据,会扩展文件大小,并且在原来的结尾和 lseek 处创建文件“空洞”。文件空洞并不会消耗磁盘空间,查看文件空洞可以用 od 。具体还是参见上面的网址。后面会继续说道文件“空洞”。大致是 ls -lsizedusize 不是一样的,似乎前者包含了 hole 的,而 du 是不包含空洞的。 cat 就会把 hole 用空字符打印出来,所以一个包含了 hole 的文件 core ,用:

    #cat core>core.copy
    

    之后,再:

    #du -s core.copy
    

    根据显示的大小会发现比原来的大小(用 du -s core 看)大了。

    译者注

    原文参考

    参考: APUE2/ch03lev1sec6.html

    6、 read 函数

    read 函数用来从打开的文件当中读取数据。声明如下:

    #include <unistd.h>
    ssize_t read(int filedes, void *buf, size_t nbytes);
    

    返回:如果成功则返回读取的字节数目,如果遇到文件结尾则返回0,如果错误返回1。

    read 读取,是从当前的文件偏移开始读取的。(对于返回1的时候如何确定是正确读取还是错误,经过网上搜索发现,一般错误的时候, read 返回 -1 ,具体情况前面讲述 open 函数的时候说明了对于返回值是如何处理的)

    译者注

    原文参考

    参考: APUE2/ch03lev1sec7.html

    7、 write 函数

    write 函数用来想打开的文件写入数据。声明如下:

    #include <unistd.h>
    ssize_t write(int filedes, const void *buf, size_t nbytes);
    

    返回:如果成功返回写入字节数目,如果错误返回1。(对于返回1如何确定是正确写入还是错误,经过网上搜索发现,一般错误的时候, write 返回 -1 ,具体情况前面讲述 open 函数的时候说明了对于返回值是如何处理的)

    write 写入,是从当前的文件偏移开始写入的。

    译者注

    原文参考

    参考: APUE2/ch03lev1sec8.html

    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);
    }
    

    这个代码的解释详细参见参考资料。对于这里的 readwrite 调用,其 BUFFSIZE 表示每次调用的时候,尝试读、写的字节数目。这个 BUFFSIZE 取不同的值,会导致 readwrite 调用次数的不同,一般来说, BUFFSIZE 取值越大,则调用次数越少,调用次数越少,则消耗的系统时间越小。但是当 BUFFSIZE 大到一定程度的时候,就不会对 I/O 效率有更多的改善了。下面给出的一个表格,对比了各种 BUFFSIZEI/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 ,不用要求我们自己设置大小了,这样可以得到较优的读写效率,这也是库函数和系统调用的一个不同)。

    总之,直接使用系统调用的 readwrite 进行文件输入输出操作,没有自动指定缓存大小(需要手动设置每次读取的大小);而使用库函数的话就有缓存了,缓存的大小设置为和磁盘块大小一样最省时间。也就是说,一次 read 的数据如果是在磁盘块大小之内的话,时间是差不多的,所以最好把缓存设置为和磁盘块一样大小。

    另外,一般读写文件的时候,操作系统会自动尝试把文件缓存到内核中,这样下次操作同样文件的时候会比较快一些,所以测试文件操作时间的时候使用不同的文件会比较准确。这也是 coredump 的来源。使用 sync 可以将缓存的数据刷新到磁盘上面。有许多类型的 sync ,有的只刷新文件数据,有的连文件属性也刷新了。

    译者注

    原文参考

    参考: APUE2/ch03lev1sec9.html

    相关文章

      网友评论

        本文标题:APUE读书笔记-03文件输入输出(1)

        本文链接:https://www.haomeiwen.com/subject/qinvuhtx.html