第5章 深入探究文件I/O

        本章将延续上一张的讨论,进一步探究文件I/O。

        在后续的关于open()系统调用的探讨中,将引入原子(atomicity)操作的概念---将某一系统调用所要完成的各个动作作为不可中断的操作,一次性加以执行。原子操作是多系统调用得以正确执行的必要条件。

        本章还将介绍另一个与文件操作相关的系统调用:多用途的fcntl(),并展示其应用之一读取和设置打开文件的状态标志。

        随后,本章将审视用于表示文件描述符和已打开文件的内核数据结构。后续各章将探讨文件I/O的某些微妙之处,理解这些数据结构之间的关系对此将有所助益。基于这一模型,本章还将解释如何赋值文件描述符。

        之后,本章将讨论一些支持扩展读写功能的系统调用。此类调用可以子啊不该百年文件当前偏移量的情况下,在文件特定的位置处进行读写操作,以及对程序中多个缓冲区进行数据双向传输。

        最后,将简要介绍非阻塞I/O的概念,并述及一些用于读写大文件的扩展接口。

        此外,因为临时文件在许多系统程序中有管饭的应用,所以本章也会介绍一些相关库函数:在被整随机生成唯一文件名称的同时,用于创建和操作临时文件。

5.1 原子操作和竞争条件

        在探究系统调用时会反复涉及原子操作的概念。所有系统调用都是以原子操作方式执行的。之所以这么说,是指内核保证了某系统调用中的所有步骤会作为独立操作而一次性加以执行,其间不会为其他进程或线程所中断。

        原子性时某些操作得以圆满成功的关键所在。特别是它规避了竞争状态(有时也成为竞争冒险)。竞争状态是这样一种情形:操作共享资源的两个进程或线程,其结果取决于一个无法预期的顺序,即这些进程获得CPU使用权的先后相对顺序。

        接下来,将讨论涉及文件I/O的两种竞争状态你,并展示了如何使用open()的标志位,来保证文件操作的原子性,从而消除这些竞争状态。

以独占方式创建一个文件

        4.3.1节曾提及:当同时指定O_EXCL与O_CREAT作为open()的标志位时,如果要打开的文件已经存在,则open()将返回一个错误。这提供了一种机制,保证进程是打开文件的创建者。对文件是否存在的检查和创建文件属于同一原子操作。要理解这一点的重要性,请思考程序清单5-1所示代码,该段代码中并未使用O_EXCL标志。(在此,为了对执行该程序的不同进程加以区分,在输出信息中打印有通过调用getpid()所返回的进程号。)

程序清单5-1:试图以独占方式打开文件的错误代码

fd = open(argv[1],O_WRONLY);//OPEN 1 check if file exists
if(fd != -1)
{
    printf("[PID %ld] File \"%s\" already exits\n",(long)getpid(),argv[1]);
    close(fd);
}else{
    if(errno != ENOENT){  
        perror("open:");
        return -1;
    }else{
        /*WINDOW FOR FAILURE*/
        fd = open(argv[1],O_WRONLY | O_CREAT,S_IRUSR | S_IWUSR );
        if(fd == -1)
        {
            perror("open22:");
            return -1;
        }
        printf("[PID %ld] File \"%s\" exclusively\n",(long)getpid(),argv[1]);
        //MAY NOT BE TRUE
    }
}

        程序清单5-1中所示的代码,除了要啰啰嗦嗦的调用open()两次外,还潜伏者一个bug。假设如下情况:当第一次调用open(0时,希望打开的文件还不存在,而当第二次调用open(0时,其他进程已经创建了该文件。如图5-1所示,若内核调度器判断出分配给A进程的时间片已经耗尽,并将CPU使用权交给B进程,就可能会发生这种问题。再比如两个进程在一个CPU系统上同时运行时,也会出现这种情况。图5-1展示了两个进程同时执行程序清单5-1中代码的清醒。在这一场景下,进程A讲得出错误的结论:目标文件是由自己创建的。因为无论目标文件存在与否,进程A对open()的第二次调用都会成功。

        虽然几次呢很难过将自己误认为文件创建者的可能性相对较小,但毕竟是存在的,这已然将此段代码置于不可靠的境地。操作的结果将依赖于两个进程的调度顺序,这一事实也就意味着出现了竞争状态。

        为了说明这段代码的确存在问题,可以使用一段代码替换程序5-1中的注释行“处理文件不存在的情况”,在检查文件是否存在与创建文件这两个动作之间人为制造一个长时间的等待。

 printf("[PID %ld] File \"%s\" doesn't exits yet \n",(long)getpid(),argv[1]);
 if(argc >2){       /*Delay between check and creat*/
    sleep(5);       /*Suspend excution for 5 seconds*/
     printf("[PID %ld] Done sleeping\n",(long)getpid());
 }

        sleep()函数将可将当前执行的进程挂起指定的秒数。         

        如果同时运行程序清单5-1中程序的两个实例,两个进程都会生成自己以独占方式创建了文件。

         从上面输出的倒数第二行可以发现,shell提示符里夹杂了第一个实例的输出信息。

        由于第一个进程在检查文件是否存在和创建文件之间发生了中断,造成两个进程都声称自己是文件的创建者。结合O_CREAT和O_EXCL标志来一次性的调用open()可以防止这种情况,因为这确保了检查文件和创建文件的步骤术语一个单一的原子操作。

向文件尾部追加数据

        用以说明原子操作必要性的第二个例子是:多个进程同时向同一个文件(例如,全局日志文件)尾部添加数据,为了达到这一目的,也许可以考虑在每个写进程中使用如下代码。

 if(lseek(fd,0,SEEK_END) == -1)
 {
    perror("lseek:");
    return -1;
 }

 if(write(fd,buf,len) !=len)
 {
    perror("write:");
    return -1;
 }

        但是这段代码存在的缺陷与前一个例子如出一辙。如果第一个进程执行到lseek()和write()之间,被执行相同代码的第二个进程所中断,那么这两个进程会在写入数据前,将文件偏移设置为相同位置,而当第一个进程再次获得调度时,会覆盖第二个进程已写入的数据。此时再次出现了竞争状态,因为执行的结果依赖于内核对两个进程的调度顺序。

        要规避这一问题,需要将文件偏移量的移动与数据写入纳入同一原子操作。在打开文件时加入O_APPEND标志就可以保证这一点。

有些文件系统(例如NFS)不支持O_APPEND标志。在这种情况下,内核会选择按如上代码所示的方式,施之以非原子操作的调用序列,从而可能导致上述的文件脏写入问题。

5.2 文件控制操作:fcntl()

        fcntl()系统调用对一个打开的文件描述符执行一系列控制操作。

#include <fcntl.h>
int fcntl(int fd,int cmd,...)
        Return on success depends on cmd,or -1 on error

        cmd参数支持的范围很广。本章随后各节会对其中的部分操作加以研讨,剩下的操作将在后续各章中进行论述。

        fcntl()的第三个参数以省略号来表示,这意味着可以将其设置为不同的类型,或者加以省略。内核会根据cmd参数(如果有的话)的值来确定该参数的数据类型。

5.3 打开文件的状态标志

        fcntl()的用途之一是针对一个打开的文件,获取或修改其访问模式和状态标志(这些值是通过open()调用的flag参数来设置的)。要获取这些设置,应将fcntl()的cmd参数设置为F_GETFL

 int flags,accessMode;
 flags = fcntl(fd,F_GEFL);  //Third argument is not required
 if(flags == -1)
 {
    perror("fcntl:");
    return -1;
 }

        在上述代码之后,可以以如下代码测试文件是否已同步方式打开:

if(flags & O_SYNC)
{
    printf("write are syncchronized\n")
}

SUSv3 规定:针对一个打开的文件,只有通过open()或后续的fcntl()的F_SETFL操作,才能对该文件的状态标志进行设置,然而在如下方面,Linux实现与标准有所偏离:如果一个程序编译时采用了5.10节所提及的打开大文件技术,那么当使用F_GETTFL命令获取文件状态标志时,标志中总是包含O_LAGEFILE标志。

        判定文件的访问模式有一点复杂,这是因为O_RDONLY(0)、O_WRONLY(1)和O_RDWR(2)这三个常量并不与打开文件状态标志中的单个比特位对应。因此言判断访问模式,需要使用掩码O_ACCMODE与flag相与,将结果与3个常量进行对比,示例代码如下:

accessMode = flags &O_ACCMODE;
if(accessMode == O_WRONLY ||accessMode == O_RDWR )
    printf("file is writable\n");

        可以使用fcntl()的F_SETFL命令来修改打开文件的某些标志。允许修改的标志有O_APPEND、O_NONBLOCK、O_NOAIME和O_DIRECT。系统将忽略对其他标志的修改操作。

        使用fcntl()修改文件状态标志,尤其适用于如下场景。

  • 文件不是有调用程序打开的,所以open()调用来控制文件的状态标志(例如,文件是3个标准输入输出描述符中的一员,这些描述符在程序启动之前就被打开)。
  • 文件描述符的获取是通过open()之外的系统调用。比如pope()调用,该调用创建一个管道,并返回两个文件描述符分别对应管道的两端,再比如socket()调用,该调用创建一个套接字并返回指向该套接字的文件描述符。

        为了修改打开文件的状态标志,可以使用fcntl()的F_SETFL命令来获取当前标志的副本,然后修改需要变更的比特位,最后再次第哦啊用fcntl()函数的F_SETFL命令来更新此状态标志。因此,为了添加O_APPEND标志,可以编写如下代码:

int flags;
flags = fcntl(fd,F_GETFL);
if(flags == -1)
{
    perror("fcntl():");
    return -1;
}

flags |= O_APPEND;
if(fcntl(fd,F_SERFL,falgs) == -1)
{
    perror("fcntl():");
    return -1;
}

5.4 文件描述符和打开文件之间的关系

        到目前为止,文件描述符和打开的文件之间似乎呈现出一一对应的关系。然而,实际并非如此。多个文件描述符指向同一打开文件,这既有可能,也属必要。这些描述符可能在相同或不同的进程中打开。

        要理解具体情况如何,需要查看由内核维护的3个数据结构。

  • 进程级的文件描述符表
  • 系统级的打开文件表
  • 文件系统的i-node表

针对每个进程,内核为其维护打开的文件描述符表。该表的每一条目都记录了单个文件描述符的相关信息,如下所示。

  • 控制文件描述符操作的一组标志。(目前,此类标志仅定义了一个,即close-on-exec标志,)
  • 对打开文件句柄的引用。

        内核对所有打开的文件都维护有一个系统级的描述表格。有时,也称之为打开文件表,并将表中各条目称为打开文件句柄。一个打开文件句柄存储了与一个打开文件相关的全部信息,如下所示。

  • 当前文件偏移量(调用read()和write()时更新,或使用lseek直接修改)。
  • 打开文件时所使用的状态标志(即open()的flags参数)。
  • 与信号驱动I/O相关的设置(63.3)
  • 对该文件i-node对象的引用。

        每个文件系统都会为其主流其上的所有文件建立一个i-node表。第14章将详细讨论i-node结构和文件系统的总体结构。这里只是列出每个文件的i-node信息,具体如下。

  • 文件类型(例如,常规文件、在套接字或FIFO)和访问权限。
  • 一个指针,指向该文件所持有的锁的列表。
  • 文件的各种属性,包括文件大小以及与不同类型操作相关的时间戳。

此处将忽略i-node在磁盘和内存中的表示差异。磁盘上的i-node计略了文件的固有属性,诸如:文件类型、访问权限和时间戳。访问一个文件时,会在内存中为i-node创建一个副本,其中记录了引用该i-node的打开文件句柄数量以及该i-node所在设备的主、从设备号,还包括一些打开文件时与文件相关的临时属性,例如:文件锁。

        图5.2展示了文件描述符、打开的文件句柄以及i-node之间的关系。在下图中,两个进程拥有诸多打开的文件描述符。

        在进程A中,文件描述符1和20都指向同一个打开的文件句柄(标号为23)。者可能是通过调用dup()、dup()、dup3()而形成的

        进程A的文件描述符2和进程 B的文件描述符2都指向同一个打开的文件句柄(标号为73).这种情形可能在调用fork()后出现(即进程A和进程B时父子关系),或当某进程通过UNIX套接字将一个打开的文件描述符传递给另一进程时,也会发生(61.13.3节)

        此外,进程A的描述符0和进程B的描述符3分别指向不同的打开文件句柄,但这些句柄均指向i-node表中的相同条目(1976),换言之,指向同一文件。发生这种秦广是因为每个进程各自对同一文件发起了open调用,同一个进程两次打开同一文件,也会发生类似情况。

        上述讨论揭示出如下要点。

  • 两个不同的文件描述符,若指向同一打开句柄,将共享同一文件偏移量。因此,如果通过其中一个文件描述符来修改文件偏移量(由read()、write()或者lseek()所致),那么从另一文件描述符中也会观察到这一变化。无论这两个文件描述符分属于不同进程还是同一进程,情况都是如此。
  • 要获取和修改打开的文件标志(例如,O_APPEND、O_NONBLOCK和O_ASYNC),可以执行fcntl()的F_GETFL和F_SETFL操作,其对作用域的约束与上一条颇为类似。
  • 相形之下,文件描述符标志(亦即,close-on-exec标志)为进程和文件描述符所私有。对这一标志的修改将不会影响到同一进程或不同进程的其他文件描述符。

 5.5 复制文件描述符

        Bourne Shell 的I/O重定向语法2>&1,意在通知shell把标准错误(文件描述符2)c重定向至标准输出(文件描述符1).因此,下列命令将把(因为shell按从左至右的顺序处理I/O重定向语句)标准输出和标准错误写入result.log文件:

$ ./myscript >result.log 2>&1

        shell 通过复制文件描述符2实现了标准错误的重定向操作,因此文件描述符2与文件描述符1指向同一个打开句柄(类似于图5-2中进程A的描述符1和20指向同一个打开文件句柄的情况)。可以通过dup()和dup2()来实现此功能。

        请注意,要满足shell的这一要求,仅仅简单地打开result.log文件两次是远远不够地(第一次是在描述符1上打开,第二次是在描述符2上打开)。首先两个文件描述符不能共享相同地文件偏移量指针,因此有可能导致相互覆盖彼此地输出。在这打开地文件不一定就是磁盘文件。在如下命令中,标准错误就将和标准输出一起送达同一管道:

$ ./mycrript 2>&1 | less

dup()调用复制一个打开地文件描述符oldfd,并返回一个新描述符,二者都指向同一打开地文件句柄。系统会保证新描述符一定是编号值最低地未用地文件描述符。

#include <unistd.h>
#int dup(int oldfd);
               Returns(new)file descriptor on success, or -1 on error

        假如发起如下调用

newfd = dup(1);

        在假定在正常地情况下,shell已经代表程序打开了文件描述符0、1、2,且没有其他描述符在用,dup()调用就会创建文件描述符1的副本,返回文件描述符编号值为3.

        如果希望返回文件描述符2,可以使用如下技术:

close(2)   //free file descriptor 2
newfd  =dup(1)  //should reuse descriptor 2

        只有当描述符0已经打开时,这段代码方可工作。如果想进一步简化上述代码,同时总是能获得期望的描述符,可以调用dup2().

#include <unistd.h>
int dup2(int oldfd,int newfd);
        Returns(new)file descriptor on success, or -1 on error

        dup2()系统调用会为oldfd参数所指定的文件描述符创建副本,其编号由newfd参数指定。如果由newfd参数所指定编号的文件描述符之前已经打开,那么dup2()首先将其关闭。(dup2()调用会默认忽略newfd关闭期间的错误。故此,编码时更为安全的做法时:在调用dup2()之前,若newfd()已经打开,则应显式调用close()将其关闭。)

        前述调用close()和dup()的代码可以简化为

dup2(1,2)

若dup2()调用成功,则返回副本的文件描述符编号(即newfd参数指定的值)。

如果oldfd并非有效的文件描述符,那么dup2()调用将失败并返回错误WBADF,且不关闭newfd.如果oldfd有效,且与newfd相等,那么dup2()将生么也不做,不关闭newfd,并将其作为调用结果返回。

        fcntl()的F_FUPFD操作时复制文件描述符的另一接口,更为灵活性。

newfd = fcntl(oldfd,F_DUPFD,startfd);

        该调用为oldfd创建一个副本,且将大于等于startfd的最小未用值作为描述符编号。该调用还能保证新描述符(newfd)编号落在特定的区间范围内。总是能将dup()和dup2()调用改写为对close()和fcntl()的调用,虽然前者更为简洁。

        由图5-2可知,文件描述符的正副文本之间共享同一打开文件句柄所含的文件偏移量和状态标志,然而,新文件描述符有其自己的一套文件描述符标志,且其close-on-exec标志(FD_CLOEXEC)总是出于关闭状态。下面将要介绍的接口,可以直接控制新文件鸟舒服的close-on-exec标志。

        dup3(0系统调用完成的工作与dup2()相同,只是新增加了一个附加参数flag,这是一个可以修改系统调用行为的位掩码。

#define _GNU_SOURCE
#define <unistd.h>

int dup3(int oldfd,int newfd,int flags);
        Returns new file descriptor on success, or -1 on error

        目前,dup3()只支持一个标志O_CLOEXEC,这将促使内阁为新描述符设置close-on-exec标志(FD_CLOEXEC)。这只该标志的缘由,类似于4.3.1节对open()调用中O_CLOEXEC标志的描述。

        dup3()系统调用始见于Linux2.6.27,为Linux所特有。

        Linux 从2.6.24开始支持fcntl()用于复制描述符的附加命令:DUPFD_CLOEXEC。该标志不仅实现了与F_DUPFD相同的功能,还为新文件描述符设置close-on-exec标志。同样,此命令之所以得以一显身手,其原因也剋四与open()调用中的O_CLOEXEC标志。SUSv3并未论及F_DUPFD_CLOEXEC标志,但是SUSv4对其做了规范。

5.6在特定文件偏移量处的I/O:pread()和pwrite()

        系统调用pread()和pwrite()完成与read()和write()相类似的工作,只是前两者会offset参数所指定的为止进行文件I/O操作,而非始于文件的当前偏移量处,且他们不会改变当前的偏移量。

#include <unistd.h>
ssize_t pread(int fd,void *buf,size_t count,off_t offset);
        Returns number of bytes read,0 on EOF,or -1 on error
ssize_t pwrite(int fd,const void *buf,size_t count,off_t offset);
        Returns number of bytes written,or -1 on error

        pread()调用等同于将如下调用纳入同一原子操作:

off_t orig;
orig = lseek(fd,0,SEEK_CUR);//Save current offset
lseek(fd,offset,SEEK_SET);
s = read(fd,buf,len);
lseek(fd,orig,SEEK_SET);//Restore original file offset

        对pread()和pwrite()而言,fd所指代的文件必须时刻定位的(即允许对文件描述符执行lseek()调用)。

        多线程应用为这些系统调用提供了用物之地。正如第29章所述,进程下辖的所有线程都将共享同一文件描述符表。这也意味着每个已打开的文件的文件偏移量为所有线程所共享。当调用pread()或pwrite()时,多个线程可同时对同一文件执行I/O操作,且不会因其他线程修改文件偏移量而受到影响。如果还试图使用lseek()和read()或write()来替代pread()或pwrite(),那么将引发竞争状态,这类似于5.1节讨论O_APPEND标志时的描述(当多个进程的文件描述符指向相同的打开文件句柄时,使用pread()和pwrite()系统调用同样能够避免进程间出现竞争状态)。

        如果反复执行lseek(),并伴之以文件I/O,那么pread()和pwrite()系统调用在某些情况下是具有性能优势的。这是因为执行单个pread()(或pwrite())系统调用的成本要低于执行lseek(0和read()或write两个系统调用。然而,较之于执行I/O实际所需的时间,系统调用的开销就有些相形见绌了(执行实际I/O的开销要远大于执行系统调用,系统调用的性能优势作用有限)。

5.7分散输入和集中输出(Scatter-Gather I/O):readv()和writev()

        readv()和writev()系统调用分别实现了分散输入和集中输出的功能。

#include <sys/uio.h>
ssize_t readv(int fd,const struct iovec *iov,int iovcnt)
            Returns number of bytes read,0 on EOF,or -1 on error
ssize_t writev(int fd,const struct iovec *iov,int iovcnt);
            Returns number of bytes written,or -1 on error

        这些系统调用并非只针对单个缓冲区进行读写操作,而是一次可传输多个缓冲区的数据。数组iov定义了一组用来传输数据的缓冲区。整型数iovcnt则指定了iov的成员个数。i中的每个成员都有如下形式的数据结构。

struct iovec{
    void *iov_base;   //start address of buffer
    ssize_t iov_len;  //number of bytes to transfer to/from buffer
}

SUSv3 标准允许系统实现对iov中成员的个数加以限制。系统实现可以通过定义<limits.h>文件中IOV_MAX来通告这一限额,程序也可以在系统运行时调用sysconf(_SC_IOV_MAX)来获取这一限额。SUSv3要求限额不得小于16.Linux将IOV_MAX的值定义为1024,这是与内核对该向量大小的限制对应的。

        然而,glibc对readv()和writev()的封装函数还悄悄做了些额外工作,若系统嗲用因iovcnt参数值过大而失败,封装函数将临时分配一块缓冲区,其大小足以容纳iov参数所有成员描述的数据缓冲区,随后再执行readv()或write()调用。

        图5-3展示的是一个关于iov、iovcnt以及iov指向的缓冲区之间关系的示例。

 分散输入

        readv()系统调用实现了分散输入的功能:从文件描述符fd所指代的文件中读取一片连续的字节,然后将其散置(“分散放置”)于iov指定的缓冲区中。这一散置动作从iov[0]开始,一次填满每个缓冲区。

        原子性是readv()的重要性。换言之,从调用进程的角度来看,当调用readv()时,内核再fd所指代的文件与用户内存之间一次性的完成了数据转移。这意味着,假设即使有另一进程(或线程)与其共享同一文件偏移量,且在调用readv()的同事企图修改文件偏移量,readv()所读取的数据仍将是连续的。

        调用readv()成功将返回读取的字节数,若文件结束将返回0。调用者必须对返回值进行检查,以验证读取的字节数是否满足要求。若数据不足以填充所有缓冲区,则只会占用部分缓冲区,其中最后一个缓冲区可能只存有部分数据。

程序清单5-2:使用readv()执行分散输------t_readv.c

        

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

#define STR_SIZE 100
int main(int argc,char *argv[])
{
    int fd;
    struct iovec iov[3];
    struct stat myStruct;  //fist buffer
    int x;                //sceond buffer
    
    char str[STR_SIZE];   //Third buffer
    ssize_t numRead,toRequired;

    if(argc != 2 || strcmp(argv[1],"help") == 0)
    {
        printf("%s file\n",argv[0]);
        return -1;
    }

    fd = open(argv[1],O_RDONLY);
    if(fd == -1)
    {
        perror("open:");
        return -1;
    }
    
    toRequired = 0;

    iov[0].iov_base = &myStruct;
    iov[0].iov_len = sizeof(struct stat);
    toRequired+=iov[0].iov_len;

    iov[1].iov_base = &x;
    iov[1].iov_len = sizeof(x);
    toRequired+=iov[1].iov_len;

    iov[2].iov_base = str;
    iov[2].iov_len = STR_SIZE;
    toRequired+=iov[2].iov_len;

    numRead = readv(fd,iov,3);
    if(numRead == -1)
    {
        perror("readv:");
        return -1;
    }

    if(numRead <toRequired)
        printf("Read fewer bytes than reauested\n");

    printf("total bytes reauested:%ld;bytes read: %ld\n",(long)toRequired,(long)numRead);

    exit(EXIT_SUCCESS);
}

集中输出

        writev()系统调用实现了集中输出:将iov所指定的所有缓冲区中的数据拼接("集中")起来,然后以连续的字节序列写入文件描述符fd指代的文件中。多缓冲区的数据的"集中"始于iov[0]所指定的缓冲区,并按照数组顺序展开。

        像readv一样,writev()调用也属于原子操作,即所有数据将一次性的从用户内存传输到fd指代的文件中。因此,在像普通文件中写入数据时,writev()调用会把所有的请求连续写入文件,而不会在其他进程(或线程)写操作的影响下分散的写入文件。

        如同write()调用,writev()调用页可能存在部分写的问题。因此必须检查writev()调用的返回值,以确定写入的字节数是否与要求相符。

        readv()调用和writev()调用的主要优势在于便捷。如下两种方案,任选其一都可替代对writev()的调用。

  • 编码时,首先分配一个大缓冲区,随即再从进程地址空间的其他为止将数据复制过来,最后调用write()输出其中的所有数据。
  • 发起一系列write()调用,逐一输出每一个缓冲区的数据

        尽管方案一在语义上等同于writev()调用,但需要在用户空间内分配缓冲区,进行数据复制,很不方便(效率也低)。

        方案二在语义上就不同于单次的writev()调用,因为发起多次write()调用将无法保证原子性。更何况,执行一次writev()调用比执行多次write()调用开销小。

在指定的文件偏移处执行分散输入/集中输出

        Linux 2.6.30版本新增了两个系统调用:preadv()、pwritev(),将分散输入/集中输出和于指定文件偏移量处的I/O二者集于一身。他们并非标准的系统调用,但是获得了现代BSD的支持。

#define _BSD_SOURCE
#include <sys/uio.h>

ssize_t preadv(int fd,cpnst struct iovec *iov,int iovcnt,off_t offset)
                        Returns number of bytes read, 0 on EOF, or -1 on error
ssize_t pwritev(int fd,cpnst struct iovec *iov,int iovcnt,off_t offset)
                                Returns number of bytes written, or -1 on error

        preadv()和peritev()系统调用所执行的任务于readv()和writev()相同,但执行I/O的为止将由offset参数指定(类似于pread()和pwrite()系统调用)。

        对于那些既想从分散-集中I/O中受益,又不愿受制于当前文件偏移量的应用程序(比如,多线程的应用程序)而言,这些系统调用恰好可以派上用场。

5.8截断文件:truncate()和ftruncate()系统调用

        truncate()和ftruncate()系统调用将文件大小设置为length参数指定的值。

#include <unistd.h>
int truncate(const char *pathname,off_t length);
int ftruncate(int fd,off_t length)
            Both return 0 on success, or -1 on error

        若文件当前长度大于参数length,调用将丢弃超出部分,若小于length,调用将在文件尾部添加一系列空字节或是一个文件空洞。

        两个系统调用之间的差别在于如何指定操作文件。truncate()以路径名字符串来指定文件,并要求可访问该文件,且对文件拥有写权限。若文件名为符号链接,那么调用将对其进行解引用。而调用ftruncate()之前,需以可写方式打开操作文件,获取其文件描述符以指代文件,该系统调用不会修改文件偏移量。

        若ftruncate()的length参数超出文件的当前大小,SUSv3允许两种行为:要么扩展该文件(如Linux),要么返回错误。而符合XSI标准的系统必须采取前一种行为。相同的情况,遂于truncate()系统调用SUSv3则要求总是能扩展文件。

 5.9非阻塞I/O

        再打开文件时指定O_NONBLOCK标志,目的有二。

  • 若open()调用未能立即打开文件,则返回错误,而非陷入阻塞。有一种情况属于例外,调用open()操作FIFO可能会陷入阻塞。
  • 调用open()成功后,后续的I/O操作也是非阻塞的。若I/O系统调用未能立即完成,则可能会只传输部分数据,或者系统调用失败,并返回EAGAIN或EWOULDBLOCK错误。具体返回何种错误将依赖于系统调用。Linux系统与许多UNIX实现一样,将两个错误常量视为同义。

        管道、FIFO、套接字、设备(比如终端、伪终端)都支持非阻塞模式。(因为无法通过open()来获取管道和套接字的文件描述符,所以要启用非阻塞模式,就必须使用fcntl()的F_SETFL命令。)

        正如13.1节所述,由于内核缓冲区保证了普通文件I/O不会陷入阻塞,故而打开普通文件时一般会忽略O_NONBLOCK标志。然而,当强制使用文件锁时,O_NONBLOCK标志对普通文件也是起作用的。

        更多非阻塞I/O请参见 44.9和63章

5.10 大文件I/O

        通常将存放文件偏移量的数据类型off_t实现为一个有符号的长整型。(之所以采用有符号数据类型是要以-1来表示错误情况。)在32位体系架构中(比如x86-32),这将文件大小至于2^31-1个字节(2GB)限制之下。

        然而磁盘的容量早已超出这一限制,因此32位UNIX实现有处理超过2GB大小文件的需求,这也在情理之中。尤文问题较为普遍,UNIX厂商联盟在大型文件峰会(Large File Summit)上就此进行了协商,并针对必须的大文件访问功能,形成了对SUSv2规范的扩展。本节将该书LFS增强的特性。

        始于内核版本2.4,32位Linux系统开始提供对LFS的支持(glibc版本必须为2.2或更高)。另一前提是,响应的文件系统也必须支持大文件操作。大所属原生Linux文件系统提供了LFS支持,但一些非原生文件系统则未提供该功能。

        应用程序可以使用如下两种方式之一以获得LFS功能。

  • 使用大文件操作的备选API.该API由LFS设计,意在作为SUS规范的过渡性扩展。因此尽管大部分系统都支持这一API,带内对于符合SUSv2或SUSv3规范的系统其实非必须。这一方法现已过时。
  • 在编译应用程序时,将宏_FILE_OFFSET_BITS的值定义为64.这一方法更为可取,因为符合SUS规范的应用程序无需修改任何源码即可获得LFS功能。

过渡性LFS API

        要使用过度型的LFS API,必须在编译程序时定义_LARGEFILE64_SOURCE 功能测试宏,该定义可以通过命令行指定,也可以定义于源文件中包含所有头文件之前的位置。该API所属函数具有处理64位文件大小和文件偏移的能力。这些函数与其32位版本命名相同,只是尾部缀以64以示区别。其中包括:fopen64()、open/64()、lseek64()、truncate64()、stat()、mmap64()和setrlimit64()。(针对这些函数的32位版本,本书已经讨论了一部分,还有一些将在后续章节中描述。)

        要访问大文件,可以使用这些函数的64位版本,例如,打开带文件的编码示例如下:

fd  = open64(name,O_CREAT | O_RDWR,mode);
if(fd == -1)
{
    perror("open64:");
    return -1;
}

调用open64().相当于在调用open()时指定O_LARGEFILE标志。若调用open()时未指定此标志,且欲打开的文件大小大于2GB,那么调用将返回错误。

        另外除了上述提及的函数之外,过度型LFS API还增加了一些新的数据类型,如下所示。

  • struct stat64:类似于stat结构(15.1节),支持大文件尺寸。
  • off64_t:64位类型,用于表示文件偏移量。

        如程序清单5-3所示,除去使用了该API中其他64位函数之外,lseek()就用到了数据类型off64_t,该程序接收两个命令行参数:欲打开的文件名称和给定的文件偏移量(整型)值。程序首先打开指定的文件,然后搜索至给定的文件偏移量处,随即写入一串字符。如下图所示的shell会话中,程序检索到一个超大的文件偏移量处(超过10GB),再写入一些字节:

程序清单5-3:访问大文件----large_file.c

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

int main(int argc, char *argv[])
{
    int fd;
    off64_t off;
    if(argc !=3 || strcmp(argv[1],"--help") ==0)
    {
        printf("%s pathname offset\n",argv[0]);
        return -1;
    }

    fd = open64(argv[1],O_RDWR | O_CREAT,S_IRUSR | S_IWUSR);
    if(fd == -1)
    {
        perror("open64:");
        return -1;
    }

    off = atoll(argv[2]);
    if(lseek64(fd,off,SEEK_SET)==-1)
    {
        perror("lseek64:");
        return -1;
    }

    if(write(fd,"test",4)==-1)
    {
        perror("write:");
        return -1;
    }
    exit(EXIT_SUCCESS);
}

_FILE_OFFSET_BITS宏

         要获取LFS功能,推荐的做法是:在编译程序时,将宏_FILE_OFFSET_BITS的值定义为64.做法之一是利用C语言编译器的命令行选项:

$ cc -D FILE_OFFSET_BITS=64 prog.c

        另一种方法,是在C语言的源文件中,在包含所有头文件之前添加如下宏定义:

#define _FILE_OFFSET_BITS 64

        所有相关的32位函数和数据类型将自动转换为64位版本。因而,例如,实际会将open()转换为open64(),数据类型off_t的长度将转而定义为64位。换言之,无需对源码进行任何修改,只要对已有程序进行重新编译,既能够实现大文件操作。

        显然,使用宏_FILE_OFFSET_BITS要比采用过度型的LFS API更为简单,但也要求应用程序的代码编写必须规范(例如声明用于防止文件偏移量的变量) 

 向ptintf()调用传递off_t值

        LFS扩展功能没有解决的问题之一是,如何向printf()调用传递off_t值。36.2节特别之处,对于预定义的系统类型(诸如pid_t,uid_t),展示其值的可移植方法是将该值强制转换为long类型,并在printf()中使用%ld.然而,一旦使用了LFS扩展功能,%ld将不足以处理off_t数据类型,因为对数据类型的处理可能会超出long类型的范围,一般位lon long类型。据此,若要显示off_t类型的值,则先要将其强制转换为long long类型,然后使用printf()函数的%lld限定符显示,如下所示:

#define _FILR_OFFSET_BITS 64
off_t offset;   //Will be 64 bits,the size of 'long long'

/*Other code assigning a value to 'offset'*/
printf("offset=%lld\n",(long long)offset);

        在处理stat结构所使用的blkcnt_t数据类型时,也应予以类似关注。

如需在独立的编译模块之间传递off_t或stat类型的参数值,则需确保在所有模块中,这些数据类型的大小相同(即在编译这些模块时,要没有将宏_FILE_OFFSET_BITS的值都定义为64,要么都不定义)。

5.11 /dev/fd目录

        对于每个进程,内核都提供一个特殊的虚拟目录/dev/fd。该目录中包含"/dev/fd/n"形式的文件名,其中n是与进程中的打开文件描述符相应的编号。因此,例如,/dev/fd/0就对英语进程的标准输入。(SUSv3对/dev//fd特性未作规定,但有些其他的UNIX实现也提供了这一特性。)

        打开/dev/fd目录中的一个文件等同于复制相应的文件描述符,所以下列两行代码是等价的:

        fd = open("/dev/fd/1",O_WRONLY);        

        fd = fup(1);    //duplicate standlard output

        在为open调用设置flag参数时,需要注意将其设置为与原描述符相同的访问模式。这一场景下,在flag标志的设置中yinruqitabiaozhi-,诸如O_CREAT,是毫无意义的(系统会将其忽略)。

        /dev/fd实际上是一个符号链接,连接到Linux所专有的/proc/self/fd目录。后者又是Linux特有的/proc/PID/fd   目录族的特例之一,此目录族中的每一目录都包含有符号链接,与一进程打开的所有文件相对应。

        程序中很少会使用/dev/fd目录中的文件。其主要用途在shell中。许多用户级shell命令将文件名作为参数,有时需要将命令输出至管道,并将某个参数替换为标准输入或标准输出。出于这一目的,有些命令(例如,diff、ed、tar和comm)提供了一个解决方法,使用“-”符号作为命令的参数之一,用以表示标准输入或输出(视情况而定)。所以,要比较ls命令输出的文件名列表与之前生成的文件名列表就可以写成:

$ ls | diff - oldfilelist

        这种方法有不少问题。首先,该方法要求每个程序都对“-”符号做专门处理,但是许多程序并未实现这样的功能,这些命令只能处理文件,不支持将标准输入或输出作为参数。其次,有些程序还将单个“-”符解释为表征命令行选项结束的分隔符。

        使用/dev/fd目录,上述问题将迎刃而解,可以吧标准输入、标准输出和标准错误作为将文件名参数传递给任何需要他们的程序。所以,可以将前一个shell命令改写为如下形式:

$ ls | diff /dev/fd/0 oldfilelist

        方便起见,系统还提供了三个符号链接:/dev/stdin、/dev/stdout和/dev/stderr,分别连接到/dev/fd/0、/dev/fd/1和/dev/fd/2。

5.12 创建临时文件

        有些程序需要创建一些临时文件,进攻其在运行期间使用,程序终止后进行删除。例如,很多编译器会在编译过程中创建临时文件。GNU C语言函数库为此而提供了一系列库函数(之所以有“一系列”的库函数,部分原因是由于这些函数分别继承自各种UNIX实现。)本节将介绍其中的两个函数:mkstemp()和tmpfile()。

        基于调用者提供的模板,mkstemp()函数生成一个唯一文件名并打开该文件,返回一个可用于I/O调用的文件描述符。

#include <stdlib.h>
int mkstemp(char *template);
    Returns file descriptor on success,or -1 on error

         模板参数采用路径名形式,其中最后六个字符必须为XXXXXX。这六个字符将被替换,以保证文件名的唯一性,且修改后的字符串将通过template参数传回。因为会对传入的template参数进行修改,所以必须将其zhidingwie-字符数组,而非字符串常量。

        文件拥有者对mkstemp()函数建立的文件拥有读写权限(其他用户则没有任何操作权限),且文件打开时使用了O_EXCL标志,以保证调用者以独占方式访问文件。

        通常,打开临时文件不久,程序就会使用unlink()系统调用(见18.3节)将其删除。故而,mkstemp()函数的示例代码如下所示:

int fd;
char template[] = "/tmp/somstringXXXXXX";
fd = mkstemp(template);
if(fd == -1)
{
    perror("mkstemp:");
    return -1;
}
printf("Generated filename was: %s\n",template);
unlink(template);
/*Name disappears immediately,but the file is removed only after close()*/

/*Use file I/O system calls-read(),write,and so on*/
if(close(fd) ==-1)
{
    perror("close:");
    return -1;
}

tempfilefile()函数会创建一个名称唯一的临时文件,并以读写方式将其打开。(打开该文件时使用了O_EXCL标志,以防一个可能性极小的冲突,即另一个进程已经创建了一个同名文件。)

#include <stdio.h>
FILE *temfile(void);
        Returns file pointer on success, or NULL on error

        tempfile()函数执行成功,将返回一个文件流供stdio库函数使用。文件流关闭后将自动删除临时文件。为达到这一目的,tmpfile()函数会在打开文件后,从内部立即调用unlink()来删除该文件名。

5.13总结

        本章介绍了原子操作的概念,这对于一些系统调用的正确操作至关重要。特别是,指定O_EXCL标志调用open(),这确保了调用者就是文件的创建者。而指定O_APPEND标志来调用open(),还确保了多个进程在对同一文件追加数据时不会覆盖彼此的输出。

        系统调用fcntl()可以执行许多文件控制操作,其中包括:修改打开文件的状态标志、复制文件描述符。使用dup()和dup2()也能实现文件描述符的复制功能。

        本章接着研究了文件描述符、打开文件句柄和文件i-node之间的关系,并特别之处这三个对象各自包含的不同信息。文件描述符及其副本指向同一个打开文件句柄,所以也将共享打开文件的状态标志和文件偏移量。

        之后描述的诸多系统调用,是对常规read()和write()系统调用的功能扩展。pread()和pwrite(0系统调用可在文件的指定位置处执行I/O功能,且不会修改文件偏移量。readv()和writev()系统调用实现了分散输入和集中输出的功能。preadv()和pwritev()系统调用则集上述两者系统调用的功能于一身。

        使用truncate()和ftruncate()系统调用,既可以丢弃多余的字节以缩小文件大小,又能使用填充为0的文件空洞来增加文件大小

        本章还简单介绍了非阻塞I/O的概念,后续章节还将继续讨论。

        LFS规范顶一个一套扩展共嗯那个,允许在32位系统上运行的进程来操作无法以32位表示的大文件。

        运行虚拟目录/dev/fd中的编号文件,进程就可以通过文件描述符标号来访问自己打开的文件,这在shell命令中尤其有用。

        mkstemp()和tmpfile()函数允许应用程序去创建临时文件。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值