Unix之文件IO(三)
本文主体内容来自《UNIX环境高级编程第三版》。
本文先说明可用的文件I/O函数——打开文件、读文件、写文件等。UNIX系统中的大多数文件I/O只需用到5个函数:open、read、write、lseek以及close。然后说明不同缓冲长度对read和write函数的影响。
本文描述的函数经常被称为不带缓冲的I/O(unbuffered I/O)。术语不带缓冲指的是每个read和write都调用内核中的一个系统调用。这些不带缓冲的I/O函数不是ISO C的组成部分,但是,它们是POSIX.1和Single UNIX Specification的组成部分。
只要涉及在多个进程间共享资源,原子操作的概念就变得非常重要。将通过文件I/O和open函数的参数来讨论此概念。然后,进一步讨论在多个进程间如何共享文件,以及所涉及的内核有关数据结构。在描述了这些特征后,将说明dup、fcntl、sync、fsync和ioctl函数。
文件描述符
对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。当读、写一个文件时,使用open或creat返回的文件描述符标识该文件,将其作为参数传送给read或write。
按照惯例,UNIX系统shell把文件描述符0与进程的标准输入关联,文件描述符1与标准输出关联,文件描述符2与标准错误关联。这是各种 shell以及很多应用程序使用的惯例,与UNIX内核无关。尽管如此,如果不遵循这种惯例,很多UNIX系统应用程序就不能正常工作。
在符合POSIX.1的应用程序中,幻数0、1、2虽然已被标准化,但应当把它们替换成符号常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO以提高可读性。这些常量都在头文件<unistd.h>
中定义。
函数open和openat
调用open或openat函数可以打开或创建一个文件。
1 |
|
将最后一个参数写为…,ISO C用这种方法表明余下的参数的数量及其类型是可变的。对于open函数而言,仅当创建新文件时才使用最后这个参数。
path参数是要打开或创建文件的名字。oflag参数可用来说明此函数的多个选项。用下列一个或多个常量进行“或”运算构成oflag参数(这些常量在头文件<fcntl.h>
中定义)。
O_RDONLY
只读打开。O_WRONLY
只写打开。O_RDWR
读、写打开。
大多数实现将O_RDONLY定义为0,O_WRONLY定义为1,O_RDWR定义为2,以与早期的程序兼容。
O_EXEC
只执行打开。O_SEARCH
只搜索打开(应用于目录)。
在这5个常量中必须指定一个且只能指定一个。下列常量则是可选的。
O_APPEND
每次写时都追加到文件的尾端。O_CLOEXEC
把FD_CLOEXEC
常量设置为文件描述符标志。O_CREAT
若此文件不存在则创建它。使用此选项时,open函数需同时说明第3个参数mode(openat函数需说明第4个参数mode),用mode指定该新文件的访问权限位。O_DIRECTORY
如果path引用的不是目录,则出错。O_EXCL
如果同时指定了O_CREAT
,而文件已经存在,则出错。用此可以测试一个文件是否存在,如果不存在,则创建此文件,这使测试和创建两者成为一个原子操作。O_NOCTTY
如果path引用的是终端设备,则不该将设备分配作为此进程的控制终端。O_NOFOLLOW
如果path引用的是一个符号链接,则出错。O_NONBLOCK
如果path引用的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选项为文件的本次打开操作和后续的I/O操作设置非阻塞方式。O_SYNC
使每次write等待物理I/O操作完成,包括由该write操作引起的文件属性更新所需的I/O。O_TRUNC
如果此文件存在,而且为只写或读-写成功打开,则将其长度截断为0。
由open和openat函数返回的文件描述符一定是最小的未用描述符数值。例如,一个应用程序可以先关闭标准输出(通常是文件描述符1),然后打开另一个文件,执行打开操作前就能了解到该文件一定会在文件描述符1上打开。
fd参数把open和openat函数区分开,共有3种可能性。
(1)path参数指定的是绝对路径名,在这种情况下,fd参数被忽略,openat函数就相当于open函数。
(2)path参数指定的是相对路径名,fd参数指出了相对路径名在文件系统中的开始地址。fd参数是通过打开相对路径名所在的目录来获取。
(3)path参数指定了相对路径名,fd参数具有特殊值AT_FDCWD。在这种情况下,路径名在当前工作目录中获取,openat函数在操作上与open函数类似。
openat函数是POSIX.1最新版本中新增的一类函数之一,希望解决两个问题。第一,让线程可以使用相对路径名打开目录中的文件,而不再只能打开当前工作目录。第二,可以避免time-of-check-to-time-of-use(TOCTTOU)错误。
TOCTTOU错误的基本思想是:如果有两个基于文件的函数调用,其中第二个调用依赖于第一个调用的结果,那么程序是脆弱的。因为两个调用并不是原子操作,在两个函数调用之间文件可能改变了,这样也就造成了第一个调用的结果就不再有效,使得程序最终的结果是错误的。文件系统命名空间中的TOCTTOU错误通常处理的就是那些颠覆文件系统权限的小把戏,这些小把戏通过骗取特权程序降低特权文件的权限控制或者让特权文件打开一个安全漏洞等方式进行。
函数close
可调用close函数关闭一个打开文件。
1 |
|
关闭一个文件时还会释放该进程加在该文件上的所有记录锁。当一个进程终止时,内核自动关闭它所有的打开文件。很多程序都利用了这一功能而不显式地用close关闭打开文件。
函数lseek
每个打开文件都有一个与其相关联的“当前文件偏移量”(current file offset)。它通常是一个非负整数,用以度量从文件开始处计算的字节数(本节稍后将对“非负”这一修饰词的某些例外进行说明)。通常,读、写操作都从当前文件偏移量处开始,并使偏移量增加所读写的字节数。按系统默认的情况,当打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置为0。
可以调用lseek显式地为一个打开文件设置偏移量。
1 |
|
对参数offset的解释与参数whence的值有关。
- 若whence是SEEK_SET,则将该文件的偏移量设置为距文件开始处offset个字节。
- 若whence是SEEK_CUR,则将该文件的偏移量设置为其当前值加offset,offset可为正或负。
- 若whence是SEEK_END,则将该文件的偏移量设置为文件长度加offset,offset可正可负。
若lseek成功执行,则返回新的文件偏移量,为此可以用下列方式确定打开文件的当前偏移量:
1 | off_t currpos; |
这种方法也可用来确定所涉及的文件是否可以设置偏移量。如果文件描述符指向的是一个管道、FIFO或网络套接字,则lseek返回−1,并将errno设置为ESPIPE
。
通常,文件的当前偏移量应当是一个非负整数,但是,某些设备也可能允许负的偏移量。但对于普通文件,其偏移量必须是非负值。因为偏移量可能是负值,所以在比较 lseek 的返回值时应当谨慎,不要测试它是否小于0,而要测试它是否等于−1。
因为偏移量(off_t
)是带符号数据类型,所以文件的最大长度会减少一半。例如,若off_t
是32位整型,则文件最大长度是\(2^{31}-1\)个字节。
lseek仅将当前的文件偏移量记录在内核中,它并不引起任何I/O操作。然后,该偏移量用于下一个读或写操作。
文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被读为0。
文件中的空洞并不要求在磁盘上占用存储区。具体处理方式与文件系统的实现有关,当定位到超出文件尾端之后写时,对于新写的数据需要分配磁盘块,但是对于原文件尾端和新开始写位置之间的部分则不需要分配磁盘块。
函数read
调用read函数从打开文件中读数据。
1 |
|
有多种情况可使实际读到的字节数少于要求读的字节数:
- 读普通文件时,在读到要求字节数之前已到达了文件尾端。例如,若在到达文件尾端之前有30个字节,而要求读100个字节,则read返回30。下一次再调用read时,它将返回0(文件尾端)。
- 当从终端设备读时,通常一次最多读一行。
- 当从网络读时,网络中的缓冲机制可能造成返回值小于所要求读的字节数。
- 当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么read将只返回实际可用的字节数。
- 当从某些面向记录的设备(如磁带)读时,一次最多返回一个记录。
- 当一信号造成中断,而已经读了部分数据量时。
函数write
调用write函数向打开文件写数据。
1 |
|
其返回值通常与参数nbytes的值相同,否则表示出错。write出错的一个常见原因是磁盘已写满,或者超过了一个给定进程的文件长度限制。
对于普通文件,写操作从文件的当前偏移量处开始。如果在打开该文件时,指定了O_APPEND选项,则在每次写操作之前,将文件偏移量设置在文件的当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数。
I/O的效率
图3-5程序只使用read和write函数复制一个文件。
图3-5 将标准输入复制到标准输出
还没有回答的一个问题是如何选取BUFFSIZE值。在回答此问题之前,先用各种不同的BUFFSIZE值来运行此程序。图3-6显示了用20种不同的缓冲区长度,读516 581 760字节的文件所得到的结果。
用图 3-5 的程序读文件,其标准输出被重新定向到/dev/null 上。此测试所用的文件系统是Linux ext4文件系统,其磁盘块长度为4 096字节(磁盘块长度由st_blksize表示)。这也证明了图 3-6 中系统 CPU 时间的几个最小值差不多出现在BUFFSIZE为4 096及以后的位置,继续增加缓冲区长度对此时间几乎没有影响。
图3-6 Linux上用不同缓冲长度进行读操作的时间结果
大多数文件系统为改善性能都采用某种预读(read ahead)技术。当检测到正进行顺序读取时,系统就试图读入比应用所要求的更多数据,并假想应用很快就会读这些数据。预读的效果可以从图3-6中看出,缓冲区长度小至32字节时的时钟时间与拥有较大缓冲区长度时的时钟时间几乎一样。
文件共享
UNIX系统支持在不同进程间共享打开文件。在介绍dup函数之前,先要说明这种共享。为此先介绍内核用于所有I/O的数据结构。
内核使用3种数据结构表示打开文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
(1)每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
a.文件描述符标志(close_on_exec)
b.指向一个文件表项的指针。
(2)内核为所有打开文件维持一张文件表。每个文件表项包含:
a.文件状态标志(读、写、添写、同步和非阻塞等,关于这些标志的更多信息参见3.14节);
b.当前文件偏移量;
c.指向该文件v节点表项的指针。
(3)每个打开文件(或设备)都有一个 v 节点(v-node)结构。v 节点包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件,v节点还包含了该文件的i节点(i-node,索引节点)。这些信息是在打开文件时从磁盘上读入内存的,所以,文件的所有相关信息都是随时可用的。例如,i 节点包含了文件的所有者、文件长度、指向文件实际数据块在磁盘上所在位置的指针等。
Linux没有使用v节点,而是使用了通用i节点结构。虽然两种实现有所不同,但在概念上, v节点与i节点是一样的。两者都指向文件系统特有的i节点结构。
图3-7显示了一个进程对应的3张表之间的关系。该进程有两个不同的打开文件:一个文件从标准输入打开(文件描述符0),另一个从标准输出打开(文件描述符为1)。
图3-7 打开文件的内核数据结构
如果两个独立进程各自打开了同一文件,则有图3-8中所示的关系。
图3-8 两个独立进程各自打开同一个文件
我们假定第一个进程在文件描述符3上打开该文件,而另一个进程在文件描述符4上打开该文件。打开该文件的每个进程都获得各自的一个文件表项,但对一个给定的文件只有一个v节点表项。之所以每个进程都获得自己的文件表项,是因为这可以使每个进程都有它自己的对该文件的当前偏移量。
给出了这些数据结构后,现在对前面所述的操作进一步说明。
在完成每个write后,在文件表项中的当前文件偏移量即增加所写入的字节数。如果这导致当前文件偏移量超出了当前文件长度,则将i节点表项中的当前文件长度设置为当前文件偏移量(也就是该文件加长了)。
如果用O_APPEND标志打开一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有追加写标志的文件执行写操作时,文件表项中的当前文件偏移量首先会被设置为i节点表项中的文件长度。这就使得每次写入的数据都追加到文件的当前尾端处。
若一个文件用lseek定位到文件当前的尾端,则文件表项中的当前文件偏移量被设置为i节点表项中的当前文件长度(注意,这与用O_APPEND标志打开文件是不同的)。
lseek函数只修改文件表项中的当前文件偏移量,不进行任何I/O操作。
注意,文件描述符标志和文件状态标志在作用范围方面的区别,前者只用于一个进程的一个描述符,而后者则应用于指向该给定文件表项的任何进程中的所有描述符。
原子操作
函数pread和pwrite
Single UNIX Specification包括了XSI扩展,该扩展允许原子性地定位并执行I/O。pread和pwrite就是这种扩展。
1 |
|
调用pread相当于调用lseek后调用read,但是pread又与这种顺序调用有下列重要区别。
调用pread时,无法中断其定位和读操作。
不更新当前文件偏移量。
调用pwrite相当于调用lseek后调用write,但也与它们有类似的区别。
一般而言,原子操作(atomic operation)指的是由多步组成的一个操作。如果该操作原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。
函数dup和dup2
下面两个函数都可用来复制一个现有的文件描述符。
1 |
|
由dup返回的新文件描述符一定是当前可用文件描述符中的最小数值。对于 dup2,可以用fd2参数指定新描述符的值。如果fd2已经打开,则先将其关闭。如若fd等于fd2,则dup2返回fd2,而不关闭它。否则,fd2的FD_CLOEXEC文件描述符标志就被清除,这样fd2在进程调用exec时是打开状态。
这些函数返回的新文件描述符与参数fd共享同一个文件表项,如图3-9所示。
图3-9 dup(1)后的内核数据结构
在此图中,假定进程启动时执行了:newfd = dup(1);
当此函数开始执行时,假定下一个可用的描述符是3(这是非常可能的,因为0,1和2都由shell打开)。因为两个描述符指向同一文件表项,所以它们共享同一文件状态标志(读、写、追加等)以及同一当前文件偏移量。
复制一个描述符的另一种方法是使用 fcntl
函数。实际上,调用dup(fd);
等效于fcntl (fd, F_DUPFD, 0);
而调用dup2(fd, fd2);
等效于
close(fd2); fcntl(fd, F_DUPFD, fd2);
函数sync、fsync和fdatasync
传统的UNIX系统实现在内核中设有缓冲区高速缓存或页高速缓存,大多数磁盘I/O都通过缓冲区进行。当我们向文件写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘。这种方式被称为延迟写(delayed write)。
通常,当内核需要重用缓冲区来存放其他磁盘块数据时,它会把所有延迟写数据块写入磁盘。为了保证磁盘上实际文件系统与缓冲区中内容的一致性,UNIX 系统提供了 sync、fsync 和fdatasync三个函数。
1 |
|
sync只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。
通常,称为update的系统守护进程周期性地调用(一般每隔30秒)sync函数。这就保证了定期冲洗(flush)内核的块缓冲区。
fsync函数只对由文件描述符fd指定的一个文件起作用,并且等待写磁盘操作结束才返回。fsync可用于数据库这样的应用程序,这种应用程序需要确保修改过的块立即写到磁盘上。
fdatasync函数类似于fsync,但它只影响文件的数据部分。而除数据外,fsync还会同步更新文件的属性。
函数fcntl
fcntl函数可以改变已经打开文件的属性。
1 |
|
在本节的各实例中,第3个参数总是一个整数,与上面所示函数原型中的注释部分对应。但是在说明记录锁时,第3个参数则是指向一个结构的指针。
fcntl函数有以下5种功能。
(1)复制一个已有的描述符(cmd=F_DUPFD或F_DUPFD_CLOEXEC)。
(2)获取/设置文件描述符标志(cmd=F_GETFD或F_SETFD)。
(3)获取/设置文件状态标志(cmd=F_GETFL或F_SETFL)。
(4)获取/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN)。
(5)获取/设置记录锁(cmd=F_GETLK、F_SETLK或F_SETLKW)。
我们先说明这11种cmd中的前8种(14.3节说明后3种,它们都与记录锁有关)。我们将讨论与进程表项中各文件描述符相关联的文件描述符标志以及每个文件表项中的文件状态标志。
F_DUPFD
复制文件描述符fd。新文件描述符作为函数值返回。它是尚未打开的各描述符中大于或等于第3个参数值(取为整型值)中各值的最小值。新描述符与 fd共享同一文件表项。但是,新描述符有它自己的一套文件描述符标志,其FD_CLOEXEC
文件描述符标志被清除(这表示该描述符在exec时仍保持有效,我们将在第8章对此进行讨论)。F_DUPFD_CLOEXEC
复制文件描述符,设置与新描述符关联的FD_CLOEXEC
文件描述符标志的值,返回新文件描述符。F_GETFD
对应于fd的文件描述符标志作为函数值返回。当前只定义了一个文件描述符标志FD_CLOEXEC
。F_SETFD
对于fd设置文件描述符标志。新标志值按第3个参数(整型值)设置。
要知道,很多现有的与文件描述符标志有关的程序并不使用常量FD_CLOEXEC
,而是将此标志设置为0(系统默认,在exec时不关闭)或1(在exec时关闭)。
F_GETFL
对应于fd的文件状态标志作为函数值返回。我们在说明open函数时,已描述了文件状态标志。它们列在图3-10中。
图3-10 对于fcntl的文件状态标志
F_SETFL
将文件状态标志设置为第3个参数的值(取为整型值)。可以更改的几个标志是:O_APPEND
、O_NONBLOCK
、O_SYNC
、O_DSYNC
、O_RSYNC
、O_FSYNC
和O_ASYNC
。F_GETOWN
获取当前接收SIGIO
和SIGURG
信号的进程ID或进程组ID。F_SETOWN
设置接收SIGIO
和SIGURG
信号的进程ID或进程组ID。正的arg指定一个进程ID,负的arg表示等于arg绝对值的一个进程组ID。
fcntl的返回值与命令有关。如果出错,所有命令都返回-1,如果成功则返回某个其他值。下列4个命令有特定返回值:F_DUPFD
、F_GETFD
、F_GETFL
以及F_GETOWN
。第1个命令返回新的文件描述符,第2个和第3个命令返回相应的标志,最后一个命令返回一个正的进程ID或负的进程组ID。
函数ioctl
ioctl函数一直是I/O操作的杂物箱。不能用本章中其他函数表示的I/O操作通常都能用ioctl表示。
1 |
|
对于ISO C原型,它用省略号表示其余参数。但是,通常只有另外一个参数,它常常是指向一个变量或结构的指针。
在此原型中,我们表示的只是ioctl函数本身所要求的头文件。通常,还要求另外的设备专用头文件。例如,除POSIX.1所说明的基本操作之外,终端I/O的ioctl命令都需要头文件<termios.h>
。
每个设备驱动程序可以定义它自己专用的一组 ioctl 命令,系统则为不同种类的设备提供通用的ioctl命令。图3-15中总结了FreeBSD支持的通用ioctl命令的一些类别。
图3-15 FreeBSD中通用的ioctl操作
磁带操作使我们可以在磁带上写一个文件结束标志、倒带、越过指定个数的文件或记录等,用本章中的其他函数(read、write、lseek 等)都难于表示这些操作,所以,对这些设备进行操作最容易的方法就是使用ioctl。
/dev/fd
较新的系统都提供名为/dev/fd
的目录,其目录项是名为
0、1、2
等的文件。打开文件/dev/fd/n等效于复制描述符n(假定描述符n是打开的)。
在下列函数调用中:
fd = open("/dev/fd/0", mode);
大多数系统忽略它所指定的 mode,而另外一些系统则要求 mode 必须是所引用的文件(在这里是标准输入)初始打开时所使用的打开模式的一个子集。因为上面的打开等效于
fd = dup(0);
所以描述符0和fd共享同一文件表项(见图3-9)。例如,若描述符0先前被打开为只读,那么我们也只能对fd进行读操作。即使系统忽略打开模式,而且下列调用是成功的:
fd = open("/dev/fd/0", O_RDWR);
仍然不能对fd进行写操作。
Linux实现中的/dev/fd
是个例外。它把文件描述符映射成指向底层物理文件的符号链接。例如,当打开/dev/fd/0
时,事实上正在打开与标准输入关联的文件,因此返回的新文件描述符的模式与/dev/fd
文件描述符的模式其实并不相关。
我们也可以用/dev/fd
作为路径名参数调用creat,这与调用open时用O_CREAT
作为第2个参数作用相同。例如,若一个程序调用creat,并且路径名参数是/dev/fd/1
,那么该程序仍能工作。
注意,在Linux上这么做必须非常小心。因为Linux实现使用指向实际文件的符号链接,在/dev/fd
文件上使用creat会导致底层文件被截断。
某些系统提供路径名/dev/stdin
、/dev/stdout
和/dev/stderr
,这些等效于/dev/fd/0
、/dev/fd/1
和/dev/fd/2
。
/dev/fd
文件主要由shell使用,它允许使用路径名作为调用参数的程序,能用处理其他路径名的相同方式处理标准输入和输出。例如,cat
命令对其命令行参数采取了一种特殊处理,它将单独的一个字符“-”解释为标准输入。例如:
filter file2 | cat file1 - file3 | lpr
首先cat读file1,接着读其标准输入(也就是filter file2命令的输出),然后读file3,如果支持/dev/fd,则可以删除cat对“-”的特殊处理,于是就可键入下列命令行:
filter file2 | cat file1 /dev/fd/0 file3 | lpr
作为命令行参数的“-”特指标准输入或标准输出,这已由很多程序采用。但是这会带来一些问题,例如,如果用“-”指定第一个文件,那么看来就像指定了命令行的一个选项。/dev/fd则提高了文件名参数的一致性,也更加清晰。
习题
3.1 当读/写磁盘文件时,本章中描述的函数确实是不带缓冲机制的吗?请说明原因。
所有磁盘I/O都要经过内核的块缓存区(也称为内核的缓冲区高速缓存)。既然read或write的数据都要被内核缓冲,那么术语“不带缓冲的I/O”指的是在用户的进程中对这两个函数不会自动缓冲,每次read或write就要进行一次系统调用。
3.3 假设一个进程执行下面3个函数调用:
fd1 = open(path, oflags);
fd2 = dup(fd1);
fd3 = open(path, oflags);
画出类似于图3-9的结果图。对fcntl作用于fd1来说,F_SETFD命令会影响哪一个文件描述符?F_SETFL呢?
每次调用open函数就分配一个新的文件表项。但是因为两次打开的是同一个文件,则两个文件表项指向相同的v节点。调用dup引用已存在的文件表项(此处指fd1的文件表项),见图C-2。当F_SETFD作用于fd1时,只影响fd1的文件描述符标志;F_SETFL作用于fd1时,则影响fd1及fd2指向的文件表项。
图C-2 open和dup的结果
3.4 许多程序中都包含下面一段代码:
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
if (fd > 2)
close(fd);
为了说明if语句的必要性,假设fd是1,画出每次调用dup2时3个描述符项及相应的文件表项的变化情况。然后再画出fd为3的情况。
如果fd是1,执行dup2(fd, 1)后返回1,但没有关闭文件描述符1(见3.12节)。调用3次dup2后,3个描述符指向相同的文件表项,所以不需要关闭描述符。
如果fd为3,调用3次dup2后,有4个描述符指向相同的文件表项,这种情况下就需要关闭描述符3。
3.5 在Bourne shell、Bourne-again shell和Korn shell中,digit1>&digit2表示要将描述符digit1重定向至描述符digit2的同一文件。请说明下面两条命令的区别。
./a.out > outfile 2>&1
./a.out 2>&1 > outfile
(提示:shell从左到右处理命令行。)
因为shell从左到右处理命令行,所以
./a.out > outfile 2>&1首先设置标准输出到outfile,然后执行dup将标准输出复制到描述符2(标准错误)上,其结果是将标准输出和标准错误设置为同一个的文件,即描述符 1 和 2 指向同一个文件表项。而对于命令行
./a.out 2>&1 > outfile
由于首先执行dup,所以描述符2成为终端(假设命令是交互执行的),标准输出重定向到outfile。结果是描述符1指向outfile的文件表项,描述符2指向终端的文件表项。
3.6 如果使用追加标志打开一个文件以便读、写,能否仍用lseek在任一位置开始读?能否用lseek更新文件中任一部分的数据?请编写一段程序验证。
这种情况下,仍然可以用lseek和read函数读文件中任意一个位置的内容。但是write函数在写数据之前会自动将文件偏移量设置为文件尾,所以写文件时只能从文件尾端开始。