第5章 文件IO:更多详情

  在本章中,我们接着讨论文件I/O。
  继续open()系统调用的讨论,我们会解释 原子(atomicity) 的概念——系统调用以单个不间断的步骤执行的行为。这是许多系统调用正确执行的必要步骤。
我们介绍另一个文件相关的系统调用——多用途的 fcntl() ,并且展示它的一个用途:获取和设置打开文件的状态标志(status flags)
  接下来,我们看一下内核中用于表示文件描述符和打开文件的数据结构。理解这些结构之间的关系可以有助于我们理解后续章节中文件I/O的细节。然后说明如何复制文件描述符。
  然后我们会考虑一些继承了read和write功能的一些系统调用。这些系统调用允许我们在文件的特定位置执行I/O操作而不改变文件偏移量。并且在程序中将数据在多个缓冲(buffer)中传递。
  我们会简要介绍 非阻塞(nonblocking) I/O的概念。以及描述一些支持 非常大文件 I/O的扩展函数(系统调用)。
  因为很多系统程序中用到 临时文件(temporary files),所以我们还会介绍一些用于创建临时文件的函数,这些临时文件的名称是随机生成的,并具有唯一性。

5.1 Atomicity and Race Conditions

  在我们讨论系统调用的操作时经常会遇到 原子性(Atomicity) 的概念。所有的系统调用都是以原子的方式执行的。内核确保系统调用中的所有步骤以单个操作完成,不会被其他进程或线程打断。
  原子性对于一些操作的成功完成是很重要的。尤其是,它允许我们可以避免 竞态条件(race conditions, race hazards) 的发生。竞态条件是一种情形:两个进程(或线程)因为获取CPU(s)时序的不同,导致对相同共享资源的操作产生的结果不可预料 (百度百科:竞态条件,从多进程间通信的角度来讲,是指两个或多个进程对共享的数据进行读或写的操作时,最终的结果取决于这些进程的执行顺序)
接下来,我们看两种会发生竞态条件的文件I/O情形,并且通过对open() flags参数的使用,保证相关文件操作的原子性,从而消除竞态条件。当我们讲到章节22.9中的sigsuspend()和章节24.4的fork()时,会重新回顾竞态条件这个主题。

Creating a file exclusively

  在章节4.3.1中,我们看到同时在open()中指定O_EXCL和O_CREAT时,如果想要打开的文件已经存在,就会返回一个错误。这就为进程提供了一种方式:可以保证文件的创建者是该进程。以原子性的方式执行文件存在性的检查和文件的创建。想要知道这种方式的重要性,可以考虑Listing 5-1的代码,我们会测试O_EXCL flag缺省的情况(在代码中,我们会打印出由getpid()系统调用返回的进程ID,用于区分两个不同程序运行的输出结果)。
Listing 5-1: Incorrect code to exclusively open a file

//fileio/bad_exclusive_open.c
fd = open(argv[1], O_WRONLY); /* Open 1: check if file exists */
if (fd != -1) { /* Open succeeded */
	printf("[PID %ld] File \"%s\" already exists\n",(long) getpid(), argv[1]);
	close(fd);
} else {
	if (errno != ENOENT) {  /* Failed for unexpected reason */
		errExit("open");
	} else {
		fd = open(argv[1], O_WRNOLY | O_CREAT, S_IRUSR | S_IWUSR);
		if (fd == -1) 
			errExit("open");
		printf("[PID %ld] Created file \"%s\" exclusively\n", (long) getpid(), argv[1]); 	/*MAY NOT BE TRUE! */
	}
}

  Listing 5-1中的代码除了冗长地使用了两次open()调用外,还存在一个bug。假定当一个进程首先调用了open(),发现文件不存在。但是当要调用第二次open()时,其他的进程已经创建了该文件。正如Figure 5-1所示,如果当进程的时间切片过期,内核调度器(kernel shcduler)决定将控制权给另一个进程时,或者,如果两个进程同时运行在多处理器系统中,就可能发生这种情况。Figure 5-1 描绘了两个进程同时执行Listing 5.1中的代码时可能发生的情况。在这种情况下,进程A在执行第二个open()前,其实不能确定文件是否已经创建。不管该文件是否存在,第二个open()都能顺利执行。虽然,这种情况发生的概率相对较小,但是代码的不可靠性,导致可能会发生这种情况。事实上,这些结果的产生取决于两个进程的调度顺序,也就是说,这就是一种竞态条件。
在这里插入图片描述
使用单个open()调用,同时指定O_CREAT和O_EXCL flags可以保证检查和创建的步骤以单个原子的操作执行,从而防止竞态条件的发生。

Appending data to a file

  需要用到 原子性 的第二个例子是多个进程将数据追加到同一个文件中(例如,一个全局日志文件)。为此,在每个writer中我们考虑使用如下的代码块:

if (lseek(fd, 0, SEEK_END) == -1) /*将文件偏移量定位到文件最后。表示对于打开的文件,从文件最后开始写入数据*/
	errExit("lseek");
if (write(fd, buf, len) != len)
	fatal("Partial/failed write");

  但是,这块代码与之前的例子具有相同的缺陷。如果第一个进程在执行lseek()和write()调用之间被另一个执行这块代码的进程中断,那么两个进程在写入之前都会将文件偏移量设置成同一个位置。当第一个进程被重新调度时,它会覆盖第二个进程刚才写入的数据。这也是一种竞态条件,因为最后的结果取决于两个进程的调度顺序。为避免这个问题,需要这两个步骤以原子操作完成:1.找到文件末尾下个字节的位置(偏移量),2.执行写入操作。打开文件时指定O_APPEND flag就能确保这两个步骤以一个原子操作完成。

有些文件系统(例如NFS)不支持O_APPEND。在这种情况下,上述的调用顺序是非原子性的,就可能发生上面描述的文件损坏问题。

5.2 File Control Operations: fcntl()

  fcntl() 系统调用可以在打开的文件描述符上执行一系列操作。

#include <fcntl.h>
//成功时返回的值依赖于cmd,发生错误时返回-1
int fcntl(int fd, int cmd, ...);

  cmd 参数可以指定一系列操作。其中某些操作我们会在下一节中讲,另外的一些会在后续章节涉及到。
  正如省略符号(…)所示,fcntl()的第三个参数可以是不同的类型,或者可以省略。内核通过cmd参数的值可以确定该参数中的数据类型。

5.3 Open File Status Flags

  fcntl()的一个用处是获取或修改 访问模式标志(access mode flag)打开文件状态标志(open file status flags) (访问模式标志和打开文件状态标志在章节4.3.1中有说明)。将 F_GETFL 传入cmd这个参数,就可以获取这些设置(flags):

int flags, accessMode;
flags = fcntl(fd, F_GETFL);
if (flags == -1)
	errExit("fcntl");

  接着上面的代码块,我们可以测试文件是否以同步写入(synchronized write)的方式(O_SYNC是一种打开文件状态标志)打开的:

if (flags & O_SYNC) 
	printf("writes are synchronized\n");

  检查文件的访问模式稍微有点复杂,因为O_RDONLY(0)、O_WRONLY(1)、O_RDWR(2)常量不对应于打开文件标志中的一个位。因此,想要进行检查,我们使用常量O_ACCMODE屏蔽(mask)flags的值,然后测试其中一个常量的相等性:

accessMode = flags & O_ACCMODE;
if (accessMode == O_WRONLY || accessMode == 0_RDWR)
	printf("file is writable");

  我们可以使用fcntl()的 F_SETFL 来修改某些打开文件的状态标志(open file status flags)。可以被修改的flags是O_APPEND、O_NONBLOCK、O_NOATIME、O_ASYNC和O_DIRECT。尝试修改其他flags会被忽略。(一些其他的UNIX系统允许fcntl()修改其他的flags,例如O_SYNC)
  使用fcntl()修改打开文件状态标志(open file status flags)在下列情形中特别有用:

  • 文件不是被调用的程序打开的,所以程序对open()调用中用到的flags没有控制权 (例如,文件可能是三种标准描述符之一,它在程序启动前就已经打开了)
  • 文件描述符不是通过open()获得,而是通过其他系统调用获得。这样的系统调用的例子有pipe(),该系统调用创建一个pipe,返回两个文件描述符,分别指向pipe的两端。还有socket(),它创建了一个socket,并返回指向这个socket的文件描述符。

   想要修改打开文件状态标志(open file status flags),我们通过使用fcntl()获取已存在flags的一份拷贝,然后就可以修改我们希望修改的位(bit),最后再次调用fcntl()来更新这个flags。因此,想要在原来flags的基础上添加O_APPEND状态标志,可以这么做:

int flags;
flags = fcntl(fd, F_GETFL);
if (flags == -1)
   errExit("fcntl");
flags |= O_APPEND;
if (fcntl(fd, F_SETFL, flags) == -1)
   errExit("fcntl");

5.4 Relationship Between File Descriptors and Open Files

  直到现在,我们可能会认为文件描述符(file descriptor)和打开文件(open file)之间是一一对应的关系。但事实并非如此。多个描述符指向同一个打开文件是可能的,并且是有用的。这些文件描述符可能在同个进程或者不同进程打开。为了了解这些,我们需要知道内核中维护的三种数据结构:

  • 每个进程的 文件描述符(descriptor) 列表
  • 系统级别(system-wide)的 打开文件描述(open file descriptions) 列表
  • 文件系统 i-node 列表

  对于每个进程,内核维护了一个打开文件描述符(open file descriptors) 列表。该表中的每个条目都记录了一个文件描述符的信息,包括:

  • 一组用于控制文件描述符的操作的flags (只有一个这样的flag:close-on-exec flag,会在章节27.4中讲到)
  • 指向 打开文件描述(open file descriptions) 的一个引用

  内核维护了一个系统级别的所有打开文件描述(open file descriptions)的列表 (这个表有时被称作 open file table ,它的条目有时被称为 open file handles)。打开文件描述列表存储了打开文件的所有信息,包括:

  • 当前的文件偏移量 (由read()和write()更新,或者由lseek()显示更新)
  • 打开文件时的指定的状态标志(status flags)(也就是open()的flags参数)
  • 文件访问模式(open()中指定的O_RDONLY、O_WRONLY和O_RDWR)
  • 与信号驱动(signal-driven) I/O有关的配置
  • 为该文件指向i-node对象的引用

  每个文件系统都有文件系统中所有文件的 i-node 列表。i-node的结构以及文件系统会在第14章详细讨论。现在,我们只需知道每个文件的i-node包含以下信息:

  • 文件类型 (例如普通文件、socket或者FIFO) 和权限;
  • 指向文件持有的锁的列表的指针;
  • 文件的各种属性,包括文件大小、与各种文件操作类型相关的时间戳。

这里,我们简单看下i-node在磁盘中和内存中的不同表现。磁盘上的i-node记录了一个文件的持久化属性,例如文件类型、权限和时间戳。当某个文件被访问时,在内存中会创建一份i-node的拷贝,这份i-node记录了引用了该i-node的文件描述符的数量(count),以及拷贝了这份i-node的设备的major ID和minor ID。内存中的i-node还记录了文件打开时的各种临时属性,例如文件锁。

  Figure 5-2 展示了文件描述符、打开文件描述和i-nodes之间的关系。下图中,有两个进程,每个进程都有一些打开文件的描述符。
在这里插入图片描述
  在进程A中,描述符1和20都指向同一个打开文件描述(23)。调用dup()、dup2()或者fcntl()都可能发生这种情形 (章节5.5)。
  进程A的描述符2和进程B的描述符2指向一个打开的文件描述(73)。在调用fork()时或者进程使用UNIX domain socket将打开的描述符传入另一个进程时(章节61.13.1),都会发生这种情形。
  最后,我们看到进程A的描述符0和进程B的描述符3指向不同的打开文件描述,但是这两个描述指向相同的i-node列表条目(1976),也就是说指向了同个文件。这种情况发生在当两个进程独立地调用open()来打开相同的文件。如果单个进程打开相同的文件两次,也会发生类似的情形。
从上述讨论中,我们可以得出以下结论:

  • 两个不同的文件描述符指向同一个打开文件描述会共享文件偏移量的值。因此,通过文件描述符改变文件的偏移量后(调用read()、write()或lseek()),对于另一个文件描述符可见。不管这两个文件描述符属于用一个进程还是属于不同进程,都起作用。
  • 当使用fcntl()的F_GETFL和F_SETFL操作获取和改变 打开文件状态标志 (open file status flags) (如O_APPEND、O_NONBLOCK和O_ASYNC)时,同样适用上述规则。
  • 相反,文件描述符标志(file descriptor flags) (也就是chose-on-exec flag) 对于进程和描述符来说是私有的。修改这些flags不会影响 (不管同个进程还是不同进程的) 其他的文件描述符。

5.5 Duplicating File Descriptors

  使用(Bourne shell的) I/O重定向语法 2>&1 告知shell: 我们希望将标准错误(文件描述符2)发送的地方重定向到标准输出(文件描述符1)发送的地方。因此以下的语法会将标准输出和标准错误都发送到result.log这个文件中。

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

  shell通过对文件描述符2进行复制,将复制的那份指向与文件描述符1相同的打开文件描述(正如Figure 5-2中,进程A的描述符1和描述符20指向同个文件描述),从而实现标准错误的重定向。通过使用dup()和dup2()系统调用,就能实现这种效果。
  注意,shell打开results.log文件两次:一次返回描述符1,另一次返回描述符2,这样是不可行的。其中一个原因是这两个文件描述符不会共享文件偏移量指针,因此会导致覆盖彼此的输出。另一个原因是这个文件可能不是一个磁盘文件。

  dup() 系统调用传入一个打开文件描述符,传入oldfd,返回一个新的描述符,指向相同的打开文件描述。新的文件描述符必定是最小的未使用的文件描述符。

#include <unistd.h>
//成功时返回新的描述符。发生错误时返回-1
int dup(int oldfd);

  假设我们使用了以下调用:

newfd = dup(1);

  假定正常情况下shell为程序打开了文件描述符0、1和2,而没有使用其他描述符。那么,dup()会使用描述符3创建描述符1的复制。
  如果我们希望描述符2成为描述符1的复制,可以使用以下技术:

close(2); /*释放文件描述符2*/
newfd = dup(1); /*会重用文件描述符2*/

  上面的代码和结果只有当描述符0打开的时候才成立。为了简化上面的代码,我们可以使用 dup2() 确保可以获取我们想要的文件描述符。

# include <unistd.h>
//成功时返回新的文件描述符(newfd),发生错误时返回-1
int dup2(int oldfd, int newfd);

  dup2()系统调用对文件描述符oldfd进行复制,复制后返回的描述符是newfd。如果newfd这个文件描述符已经打开,那么dup2()会先关闭它 (在close的过程中,如果发生任何错误,都会被默默得忽略。更安全的编程实践是在调用dup2()之前先明确地对newfd进行close())
  如果oldfd不是一个有效的文件描述符,那么dup2()就会失败,返回的错误是 EBADF,并且newfd不会被关闭。如果oldfd是一个有效的文件描述符,但是oldfd和newfd的值相同,那么dup2()不会做任何事情。
fcntl()的 F_DUPFD 操作为文件描述符的复制提供了更加灵活的接口。

newfd = fcntl(oldfd, F_DUPFD, startfd);

  该系统调用对oldfd进行了复制,返回的新的描述符(newfd)是大于等于startfd的未使用过的文件描述符。当我们希望新的描述符的值(newfd)落入某个范围内时,这个接口就很有用。调用dup()和dup2()经常可以改写成调用close()和fcntl(),尽管前者的调用更加简洁 (值得注意的是dup2()和fcntl()返回的错误代码errno是不同的)
  从Figure 5-2,我们可以看到复制出的文件描述符会和原来的描述符共享 打开文件描述 (open file description) 中的文件偏移量(file offset)和状态标志(status flags)。但是,新的文件描述符具有它自己的一组文件描述符标志(file descriptor flags),它的close-on-exec flag (FD_CLOEXEC)是经常关闭的。我们接下来介绍的接口可用于显式地控制新文件描述符的close-on-exec标志。
  dup3() 系统调用跟dup2()执行的任务相同,但是增加了额外的参数:flags,它是一个位屏蔽(bit mask),用于修改系统调用的行为。

#define _GNU_SOURCE
#include <unistd.h>
//如果成功返回新的描述符(newfd),发生错误时返回-1
int dup3(int oldfd, int newfd, int flags);

  目前dup3支持一个flag:O_CLOEXEC, 它可以使内核为新文件描述符开启close-on-exec flag (FD_CLOEXEC)。这个flag有用的原因与章节4.3.1中描述的open()的O_CLOEXEC flag一样。
dup3()系统调用是Linux 2.6.27中新加的,是Linux特有的。
  从Linux 2.6.24开始,Linux还支持一个新的fcntl()操作,用于复制文件描述符:F_DUPFD_CLOEXEC。该flag的作用与F_DUPFD相同,但是额外为新的文件描述符设置了close-on-exec flag(FD_CLOEXEC)。再说一遍,该操作有用的原因与open()的O_CLOEXEC flag一样。F_DUPFD_CLOEXEC是在SUSv4中指定的。

5.6 File I/O at a Specified Offset: pread() and pwrite()

  pread()pwrite() 系统调用的作用与read()和write()类似,除了文件I/O从offset所指定的位置开始执行,而不是当前文件偏移量开始执行。这些调用不会使文件偏移量发生变动。

# include<unistd.h>
//返回读取的字节数,遇到EOF时返回0,发生错误时返回-1
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
//返回写入的字节数,发生错误时返回-1
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);

  调用pread()相当于 原子性 地执行以下调用:

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

  pread()和pwrite()中传入的fd所指向的文件必须是可以进行seek的(seekable) (也就是文件描述符所指的文件必须有权调用lseek()的)
  这些系统调用在多线程应用中尤其有用。我们会在第29章看到,一个进程中的所有线程共享同个文件描述符列表。也就是说每个打开文件的文件偏移量对所有线程来说是全局的。使用pread()和pwrite(),多个线程可以在同个文件描述符上同时执行I/O,而不影响文件偏移量。如果我们尝试使用lseek()加上read()或write()来替代,那么就会发生类似章节5.1中的竞态条件。(当多个进程的文件描述符指向同个打开文件描述时,pread()和pwrite()可以避免竞态条件的发生)

如果我们重复执行lseek(),随后进行文件I/O,那么在某些情况下,pread()和pwrite()系统调用可以提供更优的性能。这是因为单个pread()或pwrite()的开销小于两次系统调用:lseek()和read()。

5.7 Scatter-Gather I/O: readv() and writev()

  readv()和writev()系统调用用于执行scatter-gather I/O。

#include <sys/uio.h> 
//返回读取的字节数、遇到EOF返回0,遇到错误返回-1
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
//返回写入的字节数,遇到错误时返回-1
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

  这些函数在单个系统调用中从多个缓冲(buffers)读取或者写入数据。这些缓冲在数组iov中定义。整型iovcnt指定了iov中元素的个数。每个iov元素都是以下形式的结构体:

struct iovec {
	void *iov_base; /*Start address of buffer*/
	size_t iov_len; /*Number of bytes to transfer to/form */
}

Figure 5-3 描述了iov和iovcnt参数之间的关系,以及它们指向的buffers。
在这里插入图片描述

Scatter input

  readv() 系统调用用于执行 scatter input (散点输入):从文件描述符fd所指向的文件中读取一个连续的字节序列,将这些字节 放入(scatter) iov所指定的buffers中。首先会将字节放入到iov[0]这个buffer中,放满后,接着放到下一个buffer中(iov[1])…
  readv()的一个重要性质是以 原子性 完成。也就是说,从调用进程的角度看,内核从fd指向的文件到用户内存之间的数据转移只有一次!这就意味着:例如,当从一个文件读取时,我们可以确信该范围内的字节读取是连续的,即使另外一个进程(或线程)共享了同个文件偏移量,并企图同时使用readv()系统调用操作offset。
  在成功完成时,readv()返回读取的字节数,或者遇到end-of-file时返回0。调用者必须通过检查count来验证是否所有的字节都已被读取。如果没有足够多的可读数据,那么只有一部分buffers才会被填满,最后的一些buffers可能只有部分被放入了数据。
Listing 5-2 展示了readv()的用法

/* Listing 5-2: Performing scatter input with readv() */
#include <sys/stat.h>
#include <sys/uio.h>
#include <fcntl.h>
#include "tlpi_hdr.h"
int main(int argc, char *argv[])
{
    int fd;
    struct iovec iov[3];
    struct stat myStruct;    /* First buffer */
    int x;  /* Second buffer */
    #define STR_SIZE 100
    char str[STR_SIZE];  /* Third buffer */
    ssize_t numRead, totRequired;

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

    fd = open(argv[1], O_RDONLY);
    if (fd == -1)
        errExit("open");

    totRequired = 0;

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

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

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

    numRead = readv(fd, iov, 3);
    if (numRead == -1)
        errExit("readv");

    if (numRead < totRequired)
        printf("Read fewer bytes than requested\n");

    printf("total bytes requested: %ld; bytes read: %ld\n",
            (long) totRequired, (long) numRead);
    exit(EXIT_SUCCESS);
}

Gather output

  writev() 系统调用执行gather output 。它将从iov所指定的所有buffers中的数据连结(“gathers”)到一块,并以一系列有序字节写入到文件描述符fd指向的文件中。buffers是按数组顺序gather到一块,起始的buffer是iov[0]所指向的buffer。
  像readv()那样,writev()也是以原子方式完成的,所有数据从用户内存传递到fd所指向的文件只有单次操作。因此,当写入一个普通文件时,我们可以肯定所有请求写入的数据是 连续地 写入到文件中的,而不会被其他进程(或线程)的写入所中断。
  readv()和writev()的主要优势是 方便(convenience)速度(speed) 。例如,我们可以使用下面任意方式来替代writev():

  • 分配一个 很大 的buffer,将进程的地址空间的数据写入到buffer中,然后调用write()将buffer中的数据进行输出。
  • 调用一系列write(),每个write()都各自从buffer中将数据进行输出。

  第一种方式,虽然语义上与使用writev()等价,但是分配buffer和复制用户空间的数据是 低效的和不便的。
  第二种方式,语义上与调用writev()不等价。因为一系列write()调用不是原子执行的。此外,执行一次writev()系统调用比执行多次write()系统调用开销更小。

Performing scatter-gather I/O at a specified offset

  Linux 2.6.30增加了两个新的系统调用:preadv()pwritev(),可以在指定offset上执行scatter-gather I/O。这两个系统调用是非标准的(nonstandard),但是在现代BSDs系统中也有这两个系统调用。

#define _BSD_SOURCE
#include <sys/uio.h>
//返回读取的字节数,读到EOF返回0,发生错误返回-1
ssize_t preadv(int fd, const struct iovec *iov, int iovcnt, off_t offset);
//返回写入的字节数,发生错误时返回-1
ssize_t pwritev(int fd, const struct iovec *iov, int iovcnt, off_t offset);

  preadv()和pwrite()系统调用执行的任务与readv()和writev()相同,只不过可以像pread()和pwrite()那样指定文件的offset,从这个位置开始执行I/O 。

5.8 Truncating a File: truncate() and ftruncate()

  truncate()ftruncate() 系统调用通过指定的length的值设置文件的size。

#include <unistd.h>
//成功时返回0,错误时返回-1
int truncate(const char *pathname, off_t length);
int ftruncate(int fd, off_t length);

  如果文件的长度(size)比length长,那么超出的数据会丢失。如果当前文件的长度比length短,会使用一系列null字节或者hole将文件长度扩展到length。
  这两个系统调用的不同之处在于文件是如何指定的。truncate()中,文件必须是可访问的和可写的,由pathname指定了文件路径。如果pathname是一个符号链接,它会间接引用目标文件。ftruncate()系统调用传入一个已经打开的可写入的文件描述符。它不会改变文件的文件偏移量(offset)。
  如果 ftruncate() 的length参数超过了当前文件的长度(size),SUSv3允许两种行为:文件被扩展(Linux中)或者系统调用返回一个错误。XSI-conformant系统必须采用前者行为。对于 truncate() ,如果length比当前文件长度(size)长,SUSv3要求truncate()扩展文件。

truncate()系统调用是唯一一个不需要通过open()获取文件描述符就可以对文件内容进行修改的系统调用。

5.9 Nonblocking I/O

  打开文件时指定O_NONBLOCK flag主要有两个目的:

  • 如果文件不能立刻被打开(open()),那么open会返回错误而不是阻塞。open()会阻塞的一个例子是在FIFOs中(章节44.7)。
  • 在成功地执行open()后,随后的I/O操作也是非阻塞的。如果一个I/O系统调用不能立刻完成,那么说明只传递了部分数据或者发生了EAGAIN或EWOLDBLOCK这两个错误之一,返回哪个错误取决于系统调用。在Linux和其他很多UNIX系统中,这两个错误常量是同义的。

  非阻塞模式可用于设备 (例如终端和伪终端)、pipes、FIFOs和sockets。(因为pipes和sockets的文件描述符不是通过open()获得的,我们必须使用fcntl()的F_SETFL操作来开启O_NONBLOCK)
  对于普通文件,一般会忽视O_NONBLOCK。因为内核的缓冲缓存(buffer cache)确保在普通文件上的I/O不会阻塞 (章节13.1)。但是当使用了 强制文件锁(mandatory file locking) (章节55.4) 时,O_NONBLOCK就会对普通文件产生影响。我们会在章节44.9和第63章会非阻塞I/O进行更具体的探讨。

5.10 I/O on Large Files

  off_t数据类型用于持有一个文件偏移量,它一般是使用有符号的long整型进行实现 (之所以需要使用有符号的数据类型是因为使用-1表示错误) 。在32位架构中(例如x86-32),这会限制文件的长度为231-1个字节(也就是2GB)。
  但是磁盘的空间早已超过了这个限制,因此32位UNIX系统需要解决文件比2GB大的问题。因为这是很多UNIX系统实现中存在的问题,所以组织了Large File Summit (LFS),为了让SUSv2规范中增加可以访问大文件的功能。
  Linux从kernel 2.4开始在32位系统中提供了LFS的支持。此外,相应的文件系统必须支持大文件。大部分本地Linux文件系统提供了这个支持,但是一些非本地文件系统没有提供支持 (例如,Microsoft的VFAT和NFSv2,都引入了2GB的硬限制,而不管是否采用了LFS扩展)。

因为在64位架构中,long整型是64位的,理论上文件中的最大字节数可以达到263-1。这个值已经大大超过了当前磁盘的大小。所以不会对文件的长度产生限制。

  我们可以使用两种方式来编写满足LFS功能的应用:

  • 使用一种支持大文件的API,被称为transitional(过渡的) LFS API。这种方式现在已经过时了。
  • 当我们编译程序时,将 _FILE_OFFSET_BITS 宏的值定义成 64。这是首先的方法,因为它不需要对源代码进行修改就能获得LFS功能。

The transitional LFS API

  想要使用transitional LFS API,我们必须在编译程序时定义_LARGEFILE64_SOURCE测试宏,可以在命令行中定义或者在源文件的所有头文件之前定义。该API提供的函数可以处理64位的文件长度和偏移量。这些函数的名字与32位的相同,只不过在每个函数名后面加了64这个后缀。这些函数有fopen64()、open64()、 lseek64()、 truncate64()、 stat64()、 mmap64()和setrlimit64()。
  想要访问大文件,我们只要使用函数的64位版本即可。例如想要打开大文件,可以写成如下形式:

/*
调用open64()等价于调用open()且指定了O_LARGEFILE flag。
调用open()来打开大于2GB的文件而不指定这个flag会返回一个错误。
*/
fd = open64(name, O_CREAT | O_RDWR, mode);
if (fd == -1) {
	errExit("open");
}

The _FILE_OFFSET_BITS macro

  获取LFS功能的推荐方式在编译程序时将宏_FILE_OFFSET_BITS的值定义成64。其中一种方式是通过C编译器的命令行选项:

$ cc -D_FILE_OFFSET_BITS=64 prog.c

  另一种方式是,在include任何头文件之前,在C代码中定义这个宏:

#define _FILE_OFFSET_BITS 64

  这种方式会将相应的32位函数自动转换成64位的函数。这样,例如调用open(),实际上转换成了调用open64()。off_t数据类型被定义成64位长度。换句话说,我们可以重新编译一个已经存在的程序,用于处理大文件,而不需要对源代码做任何改变。
  使用_FILE_OFFSET_BITS明显比使用transitional LSF API更见简单,但是使用这种方式时程序必须得写得正确。(举例来说,使用off_t来声明持有文件偏移量的变量,而不是使用C语言本地的整数类型)

Passing off_t values to printf()

  LFS扩展没有为我们解决的一个问题是如何将off_t的值传入到printf()调用中。在章节3.6.2中我们注意到预定义数据类型 (例如pid_t或者uid_t) 被转换成long的值,并且在printf()中使用%ld。但是如果我们使用了LFS扩展,那么仅仅使用off_t数据类型是不够的,因为它定义的类型可能比long要长,一般是long long。因此,显示一个类型为off_t的值,需要将它转换成long long,并且在printf()中使用%lld,如下:

#define _FILE_OFFSET_BITS 64
off_t offset; /*Will be 64 bits,the size of ‘long long’*/
printf("offset=%lld\n", (long long) offset);

5.11 The /dev/fd Directory

  对于每个进程,内核提供了特别的虚拟目录 /dev/fd。这个目录包含了形如 /dev/fd/n 的文件名,n是该进程的某个打开文件描述符。因此,例如,/dev/fd/0是该进程的标准输入 (/dev/fd功能不在SUSv3中指定,但是一些UNIX系统提供了这个功能) 。打开/dev/fd目录下的一个文件等价于复制(dup)相应的文件描述符。所以,下面语句是相等的:

 /*对标准输出进行复制*/
fd = open("/dev/fd/1", O_WRONLY);
fd = dup(1);    /**/

  open()调用中的flags参数是会被解释的(interpreted),所以我们应当注意为它指定与原来描述符中相同的访问模式。在这个例子中,指定其他的flags,如O_CREAT是无意义的(会被忽视)。

/dev/fd 事实上是一个符号链接,指向Linux特有的/proc/self/fd目录。后面的目录是Linux特有的/proc/PID/fd目录的一个特例。/dev/df中的符号链接对应该进程打开的所有文件。

  /dev/fd目录中的文件很少在程序中使用到。它们最常用的作用是在shell中。

5.12 Creating Temporary Files

  有些程序需要创建临时文件,这些临时文件只在程序运行时用到,当程序被终止时,这些文件会被删除。例如,很多编译器会在编译的过程中创建临时文件。GNU C库提供了很多库函数来实现这个功能。这里,我们介绍其中两个函数:mkstemp()tmpfile()
  mkstemp() 函数根据调用者(caller)提供的模板产生一个唯一的文件名,并且打开这个文件,返回一个文件描述符。

#include <stdlib.h>
//成功时返回文件描述符,发生错误时返回-1
int mkstemp(char *template);

  template(模板)参数以一个路径名的形式作为参数,最后6个字符必须是XXXXXX。这6个字符会被一个字符串所替换,它使得文件名唯一。并且修改后的文件名是通过template参数返回的。因为template会被修改,所以它必须是一个字符数组。而不是一个字符串常量。
  mkstemp()函数创建的文件对于文件所有者(file owner)具有读写权限,而其他用户没有权限。使用O_EXCL flag进行打开,确保调用者对文件具有独占访问权。
  一般来说,一个临时文件在打开后会很快地unlinked (deleted,被删除)。可以使用 unlink() (章节18.3)系统调用删除文件名。因此,可以像下面那样使用mkstemp():

int fd;
char template[] = "/tmp/somestringXXXXXX";
fd = mkstemp(template);
if (fd == -1)
	errExit("mkstemp");
printf("Generated filename was: %s\n", template);
unlike(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)
	errExit("close");

tmpnam()、tempnam()和mktemp()函数也可以产生唯一的文件名。但是应该避免使用这些函数,因为在应用中它们会创建security holes。想要进一步了解,请查阅这些函数的手册页。

  tmpfile() 函数创建一个具有唯一名称的临时文件,并且打开后用于读写。(这个文件会以O_EXCL flag打开,以确保没有其他进程已经创建了该文件)

#include <stdio.h>
//成功时返回文件指针,发生错误时返回NULL;
FILE *tmpfile(void);

  执行成功时,tmpfile()返回一个文件流(file stream)。当tempfile()打开了文件后,内部会调用unlink()立刻删除文件名。当临时文件关闭时会被自动删除。

5.13 Summary

  在本章中,我们介绍了原子的概念,它对一些系统调用的正确执行有着很重要的作用。特别是,open()的O_EXCL flag使得调用者可以确保某个文件的创建者是该调用者。open()的O_APPEND flag可以确保多个进程可以同时将数据追加到同个文件,而不会覆盖彼此输出的数据。
  fcntl()系统调用用于执行各种文件控制操作,包括改变打开文件状态标志(open file status flags)和复制文件描述符。复制文件描述符还可以使用dup()和dup2()。
  我们介绍了文件描述符、打开文件描述、文件i-nodes之间的关系,并且注意到这三个对象里都保存着不同的信息。复制出的文件描述符与原来的描述符共同指向同个打开文件描述,因此共享打开文件状态标志和文件偏移量。
  我们介绍了很多系统调用,它们扩展了传统的read()和write()系统调用的功能。pread()和pwrite()系统调用可以在指定文件位置执行I/O,而不会改变文件偏移量。readv()和writev()系统调用执行scatter-gather I/O。preadv()和pwritev()系统调用结合了scatter-gather I/O功能和在指定文件位置执行I/O的功能。
  truncate()和ftruncate()系统调用可用于缩短文件长度,将多出的字节抛弃,或者增加文件长度,使用null字节或者file hole补上。
  我们简要介绍了非阻塞I/O的概念,我们会在后续章节详细介绍。
  LFS规范定义了一组扩展,允许在32位系统上创建和访问很大的文件(超过2GB)。
  /dev/fd虚拟目录中数字化的文件允许进程通过文件描述符数字访问自己打开的文件。在shell命令中尤其有用。
  mkstemp()和tmpfile()函数允许应用创建临时文件。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值