第4章文件 I/O:通用的I/O模型

        本章所关注的是磁盘文件的I/O操作。然而,监狱可以采用相同的系统调用对诸如管道、终端等所有类型的文件施以输入输出操作,故而本章的大部分内容会与后续章节有关。

        第5章会在本章的而基础上对文件I/O做深入探讨。缓冲是文件I/O的另一要点,其复杂程度足以专辟一张讲述。13章就涵盖了内核和stdio库中的I/O缓冲。

4.1概述

        所有执行I/O操作的系统调用都以文件描述符,一个非负整数(通常是小整数),来指代打开的文件。文件描述符用以表示所有类型的已打开文件,包括管道(pipe)、FIFO、socket、终端、设备和普通文件。针对每个进程,文件描述符都自成一套。

        按照惯例,大多数程序员都期望能够使用3种标准的文件描述符,见表4-1.在程序开始运行之前,shell代表程序打开这三各文件描述符。更确切地说,程序继承了shell文件描述符地副本----在shell地日常操作种,这三个文件描述符始终是打开地。(在交互式shell种,这3个文件描述通常指向shell所运行地终端。)如果命令行指定对输入、输出进行重定向操作,那么shell会对文件描述符做适当修改,然后再启动程序。

 在程序中时代这些文件描述符时,可以使用数字(0、1、2)表示,或者采用<unistd.h>所定义地POSIX标准名称--此方法更为可取。

        虽然stdin、stdout和stderr变量在程序初始化时用于指代进程地标准输入、标准输出和标准错误,但是调用freopen()可函数可以使这些变量指代其他任何文件对象。作为其操作地一部分,freopen()可以在将流(stream)重新打开之际一并更换隐匿其中地文件描述符。换言之,针对stdout调用freopen()函数之后,无法保证stdout变量值仍为1.

        下面介绍执行文件I/O操作地4个主要系统调用。

  • fd=open(pathname,flags,mode)函数打开pathname所标识地文件,并返回文件描述符,用以在后续函数调用中指代打开地文件。如果文件不存在,open()函数可以创建之,这取决于对位掩码参数flags的设置。flags参数还可以指定文件的打开方式:只读、只写、或是读写方式。mode参数则制定了由open()调用创建文件的访问权限,如果open()函数并未创建文件,那么可以忽略或省略mode参数。
  • numread = read(fd,buffer,count)调用从fd所指代的打开文件中读取之多count字节的数据,并存储到buffer中。read调用的返回值为实际读取到的字节数。如果再无字节刻度,则返回值0;
  • numwritten = write(fd,buffer,count)调用从buffer中读取多达count字节的数据写入由fd所指代的已打开文件中。write()调用的返回值为实际写入文件中的字节数;且有可能小于count。
  • status = close(fd)在左右输入/输出操作完成后,调用close(),释放文件描述符fd以及与之相关的内核资源。

        在详细说明这些系统调用之前,程序清单4-1简要展示了他们的使用方法。该程序实现了一个简版的cp(1)命令,将原文件复制到新文件中。在命令行中,程序的第一个参数代表已存在的源文件,第二个代表文件。程序清单4-1如下所示:

程序清单4-1:使用I/O系统调用  --copy.c

#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

#define BUF_SIZE 1024

int main(int argc,char *argv[])
{
    int inputFd, outputFd, openFlags;
    mode_t filePerms;
    ssize_t numRead;
    char buf[BUF_SIZE];

    if(argc !=3 || strcmp(argv[1],"--help") == 0)
    {
        printf("%s old-file new-filename\n",argv[0]);
        return -1;
    }
    /**open input file and output file*/
    inputFd = open(argv[1],O_RDONLY);
    if(inputFd == -1)
    {
        perror("open:");
        return -1;
    }
    openFlags = O_CREAT | O_WRONLY | O_TRUNC;
    filePerms = S_IRUSR | S_IWUSR | S_IRGRP |S_IWGRP |
                S_IROTH | S_IWOTH;
    
    outputFd = open(argv[2],openFlags,filePerms);
    if(outputFd == -1)
    {
        printf("opening file %s:\n",argv[2]);
        return -1;
    }
    /*Transfer data until we encounter end of input or an error*/

    while((numRead = read(inputFd,buf,BUF_SIZE)) >0)
    {
        if(write(outputFd,buf,numRead) != numRead)
            printf("couldn't write whole buffer");
    }
    if(numRead == -1)
    {
        perror("read:");
        return -1;
    }
    if(close(inputFd) == -1)
    {
        perror("close input:");
        return -1;
    }
    if(close(outputFd) == -1)
    {
        perror("close output:");
        return -1;
    }

    exit(EXIT_SUCCESS);
}

4.2 通用I/O

        UNIX I/O模型的显著特点之一是其输入、输出的通用性概念。这意味着使用4个同样的系统调用open()、read()、write()和close()可以对所有类型的文件执行I/O操作。包括终端之类的设备。因此仅使用这些系统调用编写的程序,对任何类型的文件有效。例如,针对陈晓古清单4-1中的程序,如下操作都是有效的:

         要实现通用I/O,就必须确保每一文件系统和设备驱动程序都实现了相同的I/O系统调用集。由于文件系统或设备所持有的操作细节在内核中处理,在编程时通常可以忽略设备专用的因素。一旦应用程序需要访问文件系统或设备的专有功能时,可以选择瑞士军刀般的ioctl()系统调用(4.8节),改调用为通用I/O模型之外的专有特性提供了访问接口。

4.3 打开一个文件:open()

        open()调用既能打开一个也已存在的文件,也能创建并打开一个新文件。

#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname,int flags,.../*mode_t mode*/);

        Returns file descriptor on success, or -1 on error.

        要打开的文件由参数pathname来标识。如果pathname是一个符号链接,会对其进行解引用。如果调用成功,open()将返回一文件描述符,用于在后续函数调用中指代该文件,若发生错误,则返回-1,并将errno置为相应的错误标识。

        参数flags为位掩码,用于指定文件的访问模式,可选择表4-2所示的常量之一。

        当调用open()创建新文件时,位掩码参数mode制定了文件的访问权限。(SUSv3 规定,mode 的数据类型mode_t属于整数类型。)如果open()并未指定O_CREAT标志,则可以忽略mode参数。

15.4节将详细描述文件权限。之后,读者会了解到新建文件的访问权限不仅仅依赖于参数mode,而且受到进程的umask值(15.4.6节)和(可能存在的)父目录的默认访问控制列表(17.6节)影响。与此同时,需要注意mode参数可以指定为数字(通常为八进制数),更为可取的做法是对0个或多个表15-4(15.4.1节)中所列位掩码常量进行逻辑或操作。

        程序清单4-2展示了open()调用的几个使用实例,其中有些调用用到了其他标志位,后续将会加以介绍。

        程序清单4-2:open函数使用的例子 

/*Open existing file for reading*/

fd = open("startup",O_RDONLY);
if(fd == -1)
{
    perror("open:");
    return -1;
}

/*Open new or existing file for reading and writing,truncating to zero
bytes; file permissions read+write for owner,nothing for all others*/

fd = open("myfile",O_RDWR|O_CREAT|O_TRUNC,S_IRUSR|S_IWUSR);
if(fd == -1)
{
    perror("open:");
    return -1;
}

/*Open new or existing for writing; writes should always
append to end of file*/
fd = open("w.log",O_WRONLY|O_CREAT|O_TRUNC|O_APPEND,
                    S_IRUSR|S_IWUSR);
if(fd == -1)
{
    perror("open:");
    return -1;
}

 open()调用所返回的文件描述符数值

        SUSv3规定,如果open调用成功,必须保证其返回值为进程未用文件描述符中数值最小者。可以利用该特性以特定文件描述符打开某一文件。例如如下代码序列就会确保使用标准输入(文件描述符0)打开以文件。

if(close(STDIN_FILENO) == -1)//Close file descriptor 0
{
    perror("close");
    return -1;
}

fd=open(pathname,O_RDONLY);
if(fd == -1)
{
    perror("open:");
    return -1;
}

        由于文件描述符0未用,所以open()调用势必使用此描述符打开文件。5.5节中所论及的dup2()和fcntl()也可实现类似功能,但对于文件描述符的控制更加灵活。

4.3.1 open()调用中的flags参数

        在程序清单4-2展示的一些open调用例子中,flags参数除了使用文件访问标志外,还使用了其他操作标志(O_CREAT、O_TRUNC和O_APPEND)。现在将详细介绍flags参数。表4-3总结了可参与flags参数逐位或运算(|)的以整套常量,最后一类显式常量标准化与SUSv3还是SUSv4.

         表4-3常量分为如下几组。

  • 文件访问模式标志:先前描述的O_RDONLY、O_WRONLY和O_RDWR标志均在此列,调用open时,上述三者在flags参数中不能同时使用,只能指定其中一种。调用functl()的F_GETFL操作能够检索文件的访问模式(5.3节)。
  • 文件创建标志:这些标志在表4-3中位于第二部分,其控制范围不拘于open()调用行为的方方面面,还涉及后续I/O操作的各个选项。这些标志不能检索,也无法修改。
  • 已打开文件的状态标志:这些标志时表4-3中的剩余部分,使用fcntl()的F_GETFL和F_SETFL操作可以分别检索和修改此类标志。有时干脆将其称之为文件状态标志。

        如下是flags常量的详细描述。

O_APPEND

        总是在文件尾部追加数据,5.1节将讨论此标志的意义。

O_ASYNC

        当对于open()调用所返回的文件描述符可以实施I/O操作时,系统会产生一个信号通知进程。这一特性,也被称为信号驱动I/O,仅对特定类型的文件有效,诸如终端、FIFOS以及socket。(在SUSv3中并未规定O_ASYNC标志,但大多数UNIX实现都支持此标志或者老版本中与其等效的FASYNC标志。)在linux中,调用open()时指定O_ASYNC标志没有任何实质效果。要启用信号驱动I/O特性,必须调用fcntl()的F_SETFL操作来设置O_ASYNC标志(5.3节)。(其他一些UNIX系统的实现有类似行为。)关于O_AASYNC标志的更多内容可参考63.3节。

O_CLOEXEC(自Linux2.6.23版本开始支持)

        为新(创建)的文件描述符启用close-on-flag标志(FD_CLOEXEC)。27.4节将描述FD_CLOEXEC标志。使用O_CLOEXEC标志(打开文件),可以免去程序执行fcntl()的F_GETFD和F_SETFD操作来设置close-on-exec标志的额外工作。在多线程程序中执行fcntl()的F_GETFD和F_SETFD操作有可能导致竞争状态,而是用O_CLOEXEC标志则能够避开这一点,可能引发竞争的场景是:线程甲打开以文件描述符,尝试为描述符标记close-on-exec标志,与此同时。线程乙执行fork()调用,然后调用exec()执行任意一个程序。(假设在甲打开文件描述符和调用fcntl()设置close-on-exec标志之间,乙成功执行了fork()和exec()操作。)此类竞争可能会在无意间将打开的文件描述符泄露给不安全的程序。

O_CREAT

        如果文件不存在,将创建一个新的空文件。即使文件以只读方式打开,此标志依然有效。如果如果在open()调用中指定O_CREAT标志,那么还需要提供mode参数,否则,会将新文件的权限设置为栈中的某个随机值。

O_DIRECT

        无系统缓冲的文件I/O操作。该特性将在13.6中详述。为使O_FIRECT标志的常量定义在<fcntl.h>中有效,必须定义_GNU_SOURCE功能测试宏。

O_DIRECTORY

        如果pathname参数并非目录,将返回错误(错误号errno为ENOTDIR)。这一标志是专为实现opendir()函数(18.8节)而设计的扩展标志。为使O_DIRECTORY标志的常量定义在<fcntl.h>中有效,必须定义_GNU_SOURCE功能测试宏。

O_DSYNC(自Linux 2.6.33版本开始支持)

        根据同步I/O数据完整性的完成要求来执行文件写操作。参见13.3节中关于内核I/O缓冲的讨论。

O_EXCL

        此标志与O_CREAT标志结合使用表明如果文件一ing存在,则不会打开文件,且open()调用失败,并返回错误,错误号errno为EEXIST.换言之,此标志确保了调用者(open()的调用进程)就是创建文件的进程。检查文件存在与否和创建文件这两部属于同一原子操作。5.1节将讨论原子操作的概念。如果在flags参数中同时指定了O_CREAT和O_EXCL标志,且pathname参数是符号链接,则open()函数调用失败(错误号errno为EEXIST)。SUSv3之所以如此规定,是要求有特权的应用程序在已知目录下创建文件,从而消除了如下 安全隐患,使用符号链接打开文件会导致在另一位置创建文件(例如,系统目录)。

O_LARGEFILE

        支持一大文件方式打开文件。在32位操作系统中使用此标志,以支持大文件操作。监管在SUSv3中没有规定这一标志,但其他一些UNIX实现都支持这一特性。

O_NOATIME(Linux 2.6.8版本开始)

        在读文件是,不更新文件的最近访问时间(15.1节中所描述的st_atimes属性)要使用该标志,妖媚调用进程的有效用户ID必须与文件的拥有者相匹配,要么进程需要拥有特权(CAP_FOWNER)。否则,open()调用失败,并返回错误,错误号errno为WPERM。(事实上,如9.5所述,对于非特权进程,当以O_NOATIME标志打开文件时,与文件用户ID必须匹配的是进程的文件系统ID,而非进程的有效用户ID。)此标志是Linux特有的非标准扩展。要从<fcntl.h>中启用此标志,必须定义_GNU_SOURCE功能测试宏。O_NOATIME标志的设计旨在为索引和备份程序服务。该标志的使用能够显著减少磁盘的活动量,省却了既要读取文件内容,又要更新i-node结构中最近访问时间的繁琐,进而节省了磁头在磁盘上的反复寻道时间(14.4节)。mount()函数中MS_NOATIME标志(14.8.1节)和FS_NOATIME_FL标志(15.5节)与O_NOATIME标志功能相似。

O_NOCTTY

        如果正在打开的文件属于终端设备,O_NOCTTY标志防止其称为控制终端。34.4节将讨论控制终端。如果正在打开为文件不是设备终端,则此标志无效

O_NOFOLLOW

        通常,如果pathname参数是符号链接,open()函数将对pathname参数进行解引用。一旦在open()函数中指定了O_NOFOLLOW标志,且path参数属于符号链接,则open()函数将返回失败(错误号errno为ELOOP)。此标志在特权程序中极为有用,能够确保open()函数不对符号链接进行解引用,为使O_NOFOLLOW标志在<fcntl.h>中有效,必须定义_GNU_SOURCE功能测试宏。

O_NONBLOCK

        以非阻塞方式打开文件 5.9节

O_SYNC

        以同步I/O方式打开文件,参见13.3节针对内核I/O缓冲的讨论。

O_TRUNC

        如果文件已经存在且为普通文件,那么将清空文件内容,将其长度置0.在Linux使用此标志,无论以读、写方式打开文件,都可清空文件内容(在这两种情况下,都必须拥有对文件的写权限)。SUSv3对O_RDONLY与O_TRUNC标志组合并未做规定,但多数其他UNIX实现与Linux的处理方式相同。

4.3.2 open()函数的错误

        若打开文件时发生错误,open()将返回-1,错误号errno标识错误原因。以下是一些可能发生的错误

EACCES

        文件权限不允许调用进程以flags参数指定的方式打开文件。无法访问文件,其可能的原因有目录权限的限制、文件不存在并且也无法创建该文件。

EISDIR

        所指定的文件属于目录,而调用者企图打开该文件进行写操作。不允许这种用法。(另一方面,在某些场合中,打开目录进行读操作是必要的。18.11将举例说明。)

EMFILE

进程已打开的文件描述符数量达到了进程资源限制所设定的上限。

ENFILE

        文件打开数量已达到系统允许的上限

EOENT

        要么文件不存在且未指定O_CREAT标志,要么指定了O_CREAT标志,但pathname参数指定路径之一不存在,或者pathname参数为符号链接,而该链接指向的文件不存在(空连接)。

EROFS

        所指定的文件隶属于只读文件系统,而调用者企图以写让是打开文件。

ETXTBSY

        所指定的文件为可执行文件(程序),且正在运行。系统不允许修改正在运行的程序(比如以写方式打开文件)。(必须首先终止程序运行,然后方可修改可执行文件。)

        后续在描述其他系统调用或库函数时,一般不会再以上述方式展现可能发生的一系列错误。(每个系统调用或库函数的错误列表可从相关手册中查询获得。)采用上述方式原因有二,一是因为open()是本书详细描述的首个系统调用,而上述列表表明任一原因都有可能导致系统调用或库函数的调用失败。二是open()调用失败的具体原因列表v恩深就颇为指的玩味,他展示了影响文件访问的若干因素,以及访问文件系统所执行的一系列检查。(上述错误列表并不完整,更多open()调用失败的错误原因请查看open(2)的操作手册。)

4.3.3 creat()系统调用

        在早期的UNIX实现中,open()只有两个参数,无法创建新文件,而是使用creat()系统调用来创建并打开一个新文件。

#include<fcntl.h>

int creat(const char *pathname,mode_t mode);
        Returns file descriptor, or -1 on error

        creat()系统调用根据pathname参数创建并打开一个文件,若文件已存在,则打开文件,并清空文件内容,将其长度清0.creat()返回一文件描述符,供后续系统调用使用。creat()系统调用等价于如下open()调用:

fd = open(pathname,O_WRONLY | O_CREAT | O_TRUNC,mode)

        尽管crea()在一些老旧程序中还存在,但由于open()的flags能对文件打开提供更多控制。对creat()的使用现在已不多见。

4.4 读取文件内容:read()

        read()系统调用从文件描述符fd所指代的打开文件中读取数据。

#include <unistd.h>
ssize_t read(int fd,void *buffer,size_t count);
        Returns number of bytes read,0 on EOF,or -1 on error

        count 参数指定最多能读取的字节数。(size_t 数据类型属于无符号整数类型。)buffer参数提供用来存放输入数据的内存缓冲地址。缓冲区至少应有count个字节。

        系统调用不会分配内存缓冲区以返回信息给调用者。所以,必须预先分配合适的缓冲区并肩缓冲区指针传递给系统调用。与此相反,有些库函数却会分配内存缓冲区用以返回信息给调用者。

        如果read()调用成功,将返回实际读取的字节数,如果遇到文件结束(EOF)则返回0,如果出现错误则返回-1.ssize_t数据类型属于有符号的整数类型,用来存放(读取的)字节数或-1(表示错误)。

        一次read()调用所读取的字节数可以小于请求的字节数。对于普通文件而言,这有可能是因为当前读取位置靠近文件尾部。

        当read()应用于其它文件类型时,比如管道、FIFO、socket或终端,在不同环境下也会出现read()调用读取的字节数小于请求字节数的情况。例如,默认情况下从终端读取字符,一遇到换行符(\n),read()调用就会结束。

        使用read()从终端读取一连串字符,我们也许期望下面的代码会起作用:

#define MAX_READ  20
char buffer[MAX_READ +1];
ssize_t numRead;

numRead = read(STDIN_FILENO,buffer,MAX_READ);
if(numRead == -1)
{
    perror("read:");
    return -1;
}

printf("The input data was: %s\n",buffer);

        这段代码的输出可能会很奇怪,因为输出结果除了实际输入的字符串外还会包括其他字符。这是因为read()没有在printf()函数打印的字符串尾部添加一个表示终止的空字符。思索片刻就会意识到这肯定是症结所在,因为read()能够从文件中读取任意序列的字节。有些情况下,输入信息可能是文本数据,但在其他情况下,有可能是二进制整数或者二进制形式的C语言数据结构。read()无从区分这些数据,故而也无法遵从C语言对字符串处理的约定,在字符串尾部追加标识字符串结束的空字符。如果输入缓冲区的结尾处需要一个标识终止的空字符,必须显式追加

#define MAX_READ  20
char buffer[MAX_READ +1];
ssize_t numRead;

numRead = read(STDIN_FILENO,buffer,MAX_READ);
if(numRead == -1)
{
    perror("read:");
    return -1;
}

buffer[numRead] = '\0';
//如果读取的是字符串,组要在已读字符数后手动添加null符号
printf("The input data was: %s\n",buffer);

        由于表示字符串终止的空字符需要一个字节的内存,所以缓冲区的大小至少要比预计读取的最大字符串长度多出一个字节。

4.5 数据写入文件:write()

        write()系统调用将数据写入一个已打开的文件中。

#include <unistd.h>
ssize_t write(int fd,void *biffer,size_t count);
        Returns number of bytes writen,or -1 on error

        write()调用的参数含义与read()调用类似。BUffer参数为要写入数据的内存地址,count参数为欲从buffer写入文件的数据字节数,fd参数为一文件描述符,指代数据要写入的文件。

        如果write()调用成功,将返回实际写入文件的字节数,该返回值可能小于count参数值。这被称为部分写。对磁盘文件来说,造成部分写的原因可能是由于磁盘已满,或是因为进程资源对文件大小的限制。

        对磁盘文件执行I/O操作时,write()调用成功并不能保证数据已经写入磁盘。因为为了减少磁盘活动量和加快write()系统调用,内核会缓存磁盘的I/O操作

4.6 关闭文件:close()

        close()系统调用关闭一个打开的文件描述符、,并将其释放回调用进程,供该进程继续使用。当一进程终止时,将自动关闭已打开的所有的文件描述符。

#include <unistd.h>

int close(int fd);
        Returns 0 on success, or -1 on error

        显式关闭不再需要的文件描述符往往时良好的编程习惯,会使代码在后续修改时更具可读性,也更可靠。进而言之,文件描述符属于优先资源,因此文件描述符关闭失败可能会导致一个进程将文件描述符资源消耗殆尽。在编写需要长期运行并处理大量文件的程序时,比如shell或者网络服务器软件,需要特别加以关注。

        像其他所有系统调用一样,应对close进行错误检查,如下所示:

if(close(fd) == -1)
{
    perror("close:");
    return -1;
}

        上述代码能够捕获的错误有:企图关闭一个未打开的文件描述符,或者两次关闭同一文件描述符,也能捕获特定文件系统再关闭操作中诊断出的错误条件。

        针对特定文件系统的错误,NFS(网络文件系统)就是一例。如果NFS出现提交失败,这意味着数据没有抵达远程磁盘,随之将这一错误作为lose调用失败的原因递给应用系统。

4.7 改变文件偏移量

        对于每个打开的文件,系统内核会记录器文件偏移量,有时也将文件偏移量称为读写偏移量或指针。文件偏移量是指执行下一个read()或write()操作的文件起始位置,会议相对于文件头部起点的文件当前位置来标识。文件第一个字节的偏移量为0。

        文件打开时,会将文件偏移量设置为指向文件开始,以后每次read()或write()调用将自动对其调整,以指向已读或已写数据后的下一字节。因此,连续的read()和write()调用将按照顺序递进,对文件进行操作。

        针对文件描述符fd参数所指代的已打开文件,lseek()系统调用依照offset和whence参数调整该文件的偏移量。

#include <unistd.h>
off_t lseek(int fd,off_t offset,int whence);
        Returns new file offset if successful, or -1 on error

        offset参数指定了一个以字节为单位的数值。(SUSv3规定offset_t数据类型为有符号整型数。)whence擦拭农户则表明应参照哪个基点来解释offset参数,应为下列其中之一:

SEEK_SET

        将文件偏移量设置为从文件头部起始点开始的offset个字节。

SEEK_CUR

        相对于当前文件偏移量,将文件偏移量调整offset个字节。

SEEK_END

        将文件偏移量设置为起始于文件尾部的offset个字节。也就是说,offset参数应该从文件最后一个字节之后的下一个字节算起。

        在早期的UNIX实现中,whence参数用整数0、1、2来表示,而非正文中显示的SEEK_*常量。

        如果whence参数值为SEEK_CUR或SEEK_END,offset参数可以为正数,也可以为负数。如果wgen参数值为SEEK_SET,offset参数值必须为非负数。

        lseek()调用成功会返回新的文件偏移量。下面的调用只是获取文件偏移量的当前位置,并没有修改它。

curr  = lseek(fd,0,SEEK_CUR);

 这里给出了lseek()调用的其他一些例子,在注释中说明了将文件偏移到的具体位置。

lseek(fd,0,SEEK_SET);       /*Start of file*/
lseek(fd,0,SEEK_END);       /*Next byte after the end of the file*/
lseek(fd,-1,SEEK_END);      /*Last byte of file*/
lseek(fd,-10,SEEK_CUR);     /*Ten bYTES prior to current location*/
lseek(fd,1000,SEEK_END);    /*1001 bytes past last byte of file*/

        lseek()调用只是调整内核中与文件描述符相关的文件偏移记录,并没有引起对任何物理设备的访问。

        5.4节将进一步描述文件偏移量、文件描述符、已打开文件三者的关系。

        lseek()并不适用于所有类型的文件。不允许将lseek()应用于管道、FIFOsocket或者终端。一旦如此,调用将会失败,并将errno置为ESPIPE。另一方面,只要heqingheli-,也可以将lseek()应用于设备。例如,在磁盘或者磁带上查找一处具体位置。

文件空洞

        如果程序的文件偏移量依然跨越了文件结尾,然后再执行I/O操作,将会发生什么情况?read()调用将返回0,表示文件结尾。有点令人惊讶的是,write()函数可以在文件结尾后的任意位置写入数据。

        从文件结尾后道心写入数据见的这段空间被称为文件空洞。从变成角度看,文件空洞中是存在字节的,读取空洞将返回以0(空字节)填充的缓冲区 。

        然而,文件空洞不占用任何磁盘空间。知道后续某个时间点,在文件空洞中写入了数据,文件系统曹辉位置分配磁盘块。文件空洞的主要优势在于,与为实际需要的空字节分配磁盘块相比,系数填充的文件会占用较少的磁盘空间。核心咋混储文件(core dump 22.1节)是包含空洞文件的常见例子。

        对文件空洞不占用磁盘空间的说法需要稍微限定一下。在大多数文件系统中,文件空间的分配是以块为单位的(14.3节)。块的大小取决于文件系统,通常是2014字节、2048字节、4096字节。如果空洞的边界落在块内,而非恰好落在边界上,则会分配一个完整的块来存储数据,块中与空洞相关的部分则以空字节填充。

        空洞的存在意味着一个文件名义上的大小可能要比其占用的磁盘存储量要大(有时会大出许多)。向文件空洞中写入字节,内核需要为其分配存储单元,即使文件大小不变,系统的可用磁盘空间也将减少。这种情况并不常见,但也需要了解。

        SUSv3的函数posix_fallocate(fd,offset,len)规定,针对文件描述符fd所知带的文件,能确保按照由offse参数和len参数所确定的字节范围为其在磁盘山分配存储空间。这样,应用程序对文件的后续write()调用不会因磁盘空间耗尽而失败(否则,当文件中一个空洞被填满后,或者因其他应用程序消耗了磁盘空间时,都可能因磁盘空间耗尽而引发此类错误)。在过去,glibc库在实现posix_fallocate()函数时,通过向指定范围内的每个块写入一个值为0的字节以达到预期结果。自内核版本2.6.23开始,Linux系统提供了fallocate()系统掉哦那个,能更为高效的确保所需存储空间的分配。当fallocate调用可用时,glibc库会利用其来实现posix_fallocate()函数的功能。

        14.4节将描述空洞在文件中的表示方式。25.1节将描述stat()系统调用,该调用能够提供文件当前大小和实际分配给文件的块数量等信息。

示例程序

       程序清单4-3 演示了lseek()与read()、write()的协作使用。该程序的第一个命令行参数为要打开的文件名称,剩余的参数则指定了在文件上执行的输入/输出操作。每个表示操作的参数都以一个字母开头,紧跟以相关值(中间无空格分隔)。

  • softset:从文件开始检索到offset字节位置。
  • rlength:在当前文件便宜量处,从文件中读取legnth字节数据,并以文本形式显示。
  • Rlength:在当前文件偏移量处,从文件中读取length字节数据,并以十六进制形式显示。
  • wstr:在当前文件偏移量处,向文件写入由str指定的字符串。

程序清单4-3:read()、write()和lseek()使用示范  --seek_io.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <ctype.h>
#include <errno.h>

int main(int argc,char *argv[])
{
    size_t len;
    off_t offset;
    int fd,ap,j;
    char *buf;
    ssize_t numRead,numWritten;

    if(argc <3 || strcmp(argv[1],"--help") ==0)
    {
        printf("%s file {r<length> | R<length> |W<string> | s<offset>}...\n",argv[0]);
        return -1;
    }
    fd = open(argv[1],O_RDWR | O_CREAT,S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
    if(fd == -1)
    {
        perror("open:");
        return -1;
    }

    for(ap=2;ap<argc;ap++){
        switch(argv[ap][0]){
            case 'r': /*Display bytes at current offset,as text*/
            case 'R': /*Display bytes at current offset,in hex*/
                len = atoi(&argv[ap][1]);
                buf = malloc(len);
                if(buf ==NULL)
                {
                    perror("buf:");
                    return -1;
                }
                numRead = read(fd,buf,len);
                if(numRead == -1)
                {
                    perror("read:");
                    return -1;
                }
                if(numRead == 0){
                    printf("%s : end-of-file\n",argv[ap]);
                }else{
                    printf("%s:",argv[ap]);
                    for(j=0;j<numRead;j++)
                    {
                        if(argv[ap][0] == 'r')
                            printf("%c",isprint((unsigned char)buf[j])?buf[j]:'j');
                        else
                            printf("%02x ",(unsigned int)buf[j]);
                    }
                    printf("\n");
                }

                free(buf);
                break;
            case 'w':   /*Write string at current offset*/
                numWritten = write(fd,&argv[ap][1],strlen(&argv[ap][1]));
                if(numWritten == -1)
                {
                    perror("write:");
                    return -1;
                }
                printf("%s: wrote %ld bytes \n",argv[ap],(long)numWritten);
                break;
            case 's':
                offset = atoi(&argv[ap][1]);
                if(lseek(fd,offset,SEEK_SET) == -1)
                {
                    perror("lseek:");
                    return -1;
                }
                printf("%s seek succeded\n",argv[ap]);
                break;

            default:
                printf("Argument must start with [rRws]: %s\n",argv[ap]);
        }
    }

    exit(EXIT_SUCCESS);
}

        下面的shell会话演示了程序清单4-3程序的使用,还显示了从文件空洞中读取字节的情况:

4.8 通用I/O模型以外的操作:ioctl()

        在本章上述通用I/O模型之外,ioctl()系统调用又为执行文件和设备操作提供了一种多用途机制。

#include <sys/ioctl.h>
 int ioctl(int fd,int request,.../*argp*/);
          Value returned on success depends on request,or -1 on error

        fd参数为某个设备或文件已打开的文件描述符,request参数指定了将在fd上执行的控制操作。具体设备的头文件定义了可传递给request参数的常量。

        ioctl()调用的第三个参数采用了标准C语言的省略符号(...)来表示(称之为argp),k可以是任意数据类型。ioctl()根据request的参数值来确定argp所期望的类型。通常情况下argp是指向整数或结构的指针,有些情况下不需要使用argp。

        SUSv3 为ioctl()制定的唯一规定时针对流(STREAM)设备的控制操作。(流是 System V操作系统中的特性。尽管为其开发有一些插件,主流的Linux内核并不支持该特性。)本书述及的ioctl()的其他操作都不在SUSv3的规范之列。然而,从早期版本开始,ioctl()调用就是UNIX系统的一部分在许多其他UNIX系统中都已实现,在讨论ioctl()调用的各个操作时,会点出存在的可移植性问题。

4.9 总结

        为了对普通文件执行I/O操作,首先必须调用open()以获得一个文件描述符。随之使用read()和write()执行文件的I/O操作,然后应使用close()释放文件描述符及相关资源。这些系统调用可对所有的类型的文件执行I/O操作。

        所有类型的文件和设备驱动都实现了相同的I/O接口,这保证了I/O操作的通用性,同时意味着在无需针对特定文件类型编写代码的情况下,程序通常就能够操作所有类型的文件。

        对于已打开的每个文件,内核都维护由一个文件偏移量,这决定了下一次读或写操作的起始位置。读和写操作会隐式修改文件偏移量。使用lseek()函数可以显示地将文件偏移量置为文件中或文件结尾后的任一位置。在文件原结尾处之后的某一位置写入数据将导致文件空洞。从文件空洞处读取文件将返回全0字节

        对未纳入标准I/O模型的所有设备和文件操作而言,ioctl()系统调用是个百宝箱。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值