UNIX环境高级编程 学习笔记 第三章 文件I/O

UNIX系统大多数文件IO只用open、read、write、lseek、close函数。

不带缓冲的IO指每个read和write都调用内核中的一个系统调用,不带缓冲的IO不是ISO C的组成部分,但它是POSIX.1和SUS(是POSIX.1标准的超集)的组成部分。

对内核而言,所有打开的文件都通过文件描述符引用,当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。open、creat函数都能打开文件并返回该打开文件的描述符标识,可将文件描述符作为参数传递给read和write函数。

UNIX的shell通常把文件描述符0与进程标准输入关联,文件描述符1与标准输出关联,文件描述符2与标准错误关联,与UNIX内核无关。如不遵守此惯例,很多UNIX系统应用程序就不能正常工作。

符合POSIX.1的应用程序中,幻数0、1、2虽然已经标准化,但在头文件unistd.h中,它们被替换成符号常量(#define定义的或const的全局常量)STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO以提高可读性。

文件描述符范围为0~OPEN_MAX-1,早期UNIX上限值为19,现在很多系统将上限增加到了63。而对FreeBSD 8.0、Linux 3.2.0、Mac OS X 10.6.8以及Solaris 10中,文件描述符的变化范围几乎无限。

可调用open或openat函数打开或创建一个文件:
在这里插入图片描述
以上函数的最后一个参数为…,ISO C用这种方法表示余下的参数的数量和类型是可变的。对于open函数,只有在创建新文件时才使用最后这个参数。

path参数是要打开或创建文件的名字,oflag参数用下列一个或多个常量进行或运算构成oflag参数,这些常量定义在头文件fcntl.h中:

O_RDONLY    // 只读打开,通常定义为0
O_WRONLY    // 只写打开,通常定义为1
O_RDWR    // 读写打开,通常定义为2
O_EXEC    // 只执行打开
O_SEARCH    // 只搜索打开(仅适用于目录),目的在于目录打开时验证它的搜索权限,之后对目录的文件描述符的操作就不用再次检查对该目录的搜索权限,很多系统没有实现它

以上定义是为了与早期的程序兼容,且以上五个常量中必须指定一个且只能指定一个,以下常量可选:

O_APPEND    // 每次写追加到文件尾
O_CLOEXEC    // 把FD_CLOEXEC设置为文件描述符标志,此文件描述符标志作用为,进程执行exec后,自动关闭该打开文件
             // 此标志位还可以防止多线程条件下,在用open函数打开文件和fcntl函数给该打开文件添加此标志的间隙,其它线程执行了exec函数导致新进程中此文件描述符还未关闭
O_CREAT    // 若文件不存在就创建它,使用此选项时,必须指定第三个mode参数,以描述文件的访问权限位
O_DIRECTORY    // 如path不是目录,则出错
O_EXCL    // 如还指定了O_CREAT且path参数指定的文件已存在,则出错,可用此测试一个文件是否存在,如不存在,则创建此文件,这使得测试文件是否存在和创建文件两者成为一个原子操作
O_NOCTTY    // 如path是终端设备,则不将该设备作为此进程的控制终端
O_NOFOLLOW    // 如path引用的是符号链接,则出错
O_NONBLOCK    // 如path引用的是一个FIFO、块设备、字符特殊文件时,此选项为文件的本次打开操作和后续的IO操作设置非阻塞方式
O_NDELAY    // 较早的System V引入的,它与前一常量类似,但如果不能从管道、FIFO、或设备读到数据,则返回0,这与表示读到文件尾端返回0冲突,因此不应使用它
O_SYNC    // 每次write等待物理IO操作完成,包括由该write引起的文件属性更新所需的IO
O_TRUNC    // 如果文件存在,且为只写或读写打开(即可以写时),将其长度截断为0
O_TTY_INIT    // 如打开一个还未打开的终端设备,设置非标准termios参数值,使其符合SUS

以上的O_CLOEXEC用法之一是,当我们fork后使用exec时,fork出来的子进程获得父进程的数据空间、堆栈副本,此时子进程和父进程的相同文件描述符指向同一文件表项,而当子进程exec时会使用新程序替换子进程的正文、数据、堆栈,使得子进程之前的文件描述符无法关闭,而子进程不知道打开了多少文件描述符,如果能在fork前找到需要关闭的文件,并将O_CLOEXEC添加到文件的打开模式中即可实现在子进程exec时自动关闭该文件描述符。

以下的标志是SUS及POSIX.1中同步输入输出选项的一部分:

O_DSYNC    // 每次write都等待物理IO完成,但不更新文件属性等元数据
           // 而O_SYNC数据和文件属性总是同步更新
           // 如文件用O_DSYNC打开,重写其现有的部分内容时,文件的时间属性不会同步更新,而用O_SYNC打开时,每次对文件的write都在write返回前更新文件时间
O_RSYNC    // 让文件描述符上每个读操作等待(read操作被阻塞),直到对该文件同一部分的写操作都完成
           // 此标志只影响读取操作,必须与O_SYNC或O_DSYNC结合使用

FreeBSD和Mac OS X设置了另一个标志O_FSYNC,与O_SYNC等效

函数open和openat返回的文件描述符是最小的未用描述符的数值,这样可以在标准输入、标准输出、标准错误上打开文件,如一个程序可以先关闭标准输出,再打开另一文件,这样这个文件就会在文件描述符1上打开。

函数openat比函数open多了一个fd参数,对于openat函数:
1.若path指定的是绝对路径名,此时,fd参数被忽略,openat相当于open。
2.若path指定的是相对路径名,fd参数是相对路径名的基准目录的文件描述符,fd参数通过打开基准目录来获取。
3.若path指定了相对路径名,并且fd参数为AT_FDCWD,此时,基准目录是当前工作目录,openat函数行为相当于open函数。

openat函数是POSIX.1最新版本新增的一类函数之一,希望解决两个问题:
1.让线程能使用相对路径名(相对于fd参数表示的目录)打开目录中的文件,而不是只能相对于当前工作目录。同一进程中所有线程共享相同的当前工作目录。
2.避免TOCTTOU( time-of-check-to-time-of-use)错误,该问题为:两个基于文件的函数,若第二个函数依赖于第一个调用的结果,那么这个程序是脆弱的,因为两个调用并不是原子操作,可能第一个函数调用后文件会被改变。

若NAME_MAX值是14,而我们在当前目录创建了一个文件名长度为15个字符的新文件,在System V中,会将文件名截断为14个字符而不报错,在BSD类系统中,会返回出错状态并将errno设置为ENAMETOOLONG。截断而不报错可能会导致出错,如果NAME_MAX被设置为14,而存在一个文件名恰好就是14个字符的文件,那么以其路径名作为其参数的任一函数(open、stat等)都无法确定文件的原始名是什么,因为这些函数无法判断该文件名是否被截断过。

可以输出limits.h中的MAX_NAME值,我输出的是255,之后使用creat函数创建一个名字大小为256字节的文件,会报错并将errno值设为ENAMETOOLONG,而可以创建名字大小为255及以下的文件,在命令行创建文件时同上,只能创建名字长度为255及以下的文件:
在这里插入图片描述

POSIX.1中的常量_POSIX_NO_TRUNC决定了长文件名和长路径名被截断还是返回一个错误,这个值取决于不同的文件系统,我们可以使用fpathconf或pathconf函数来获取它的行为。

若_POSIX_NO_TRUNC有效,则路径名长度超过PATH_MAX或路径名中任一文件名长度超过NAME_MAX时,返回出错,并将errno设为ENAMETOOLONG。

creat函数可创建一个新文件:
在这里插入图片描述
creat函数等价于:

open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);

因为早期UNIX中,open函数的第二个参数只能是0、1、2,因此无法打开一个不存在的文件,需要先用creat函数创建该文件,而现在open函数提供了O_CREAT和O_TRUNC选项,功能上可以代替creat函数了。

如果函数creat要创建的文件原来就存在,会将这个文件的长度截短为0。

creat以只写打开创建的文件,如果想创建文件之后,先写入再读取文件,则必须先调用creat、close,然后再调用读取模式的open,但现在可用以下方式实现:

open(path, O_RDWR | O_CREAT | O_TRUNC, mode);

close函数关闭一个文件:
在这里插入图片描述
关闭一个文件还会释放该进程加在该文件上的所有记录锁。一个进程终止时,内核自动关闭它所有打开的文件。

每个打开的文件都有一个与其关联的当前文件偏移量,它通常是一个非负整数,用以度量从文件开始处计算的字节数,通常,读写操作都从当前文件偏移处开始,并使偏移量增加所读写的字节数。默认,当打开文件时不指定O_APPEND选项时,偏移量设置为0。用lseek函数可显式为一个打开的文件设置偏移量:
在这里插入图片描述
参数offset的解释与whence参数的值有关:
1.whence是SEEK_SET,则文件偏移量设置为距文件开始处offset个字节。
2.whence是SEEK_CUR,则文件偏移量设置为其当前值加offset,offset可正可负。
3.whence是SEEK_END,则文件的偏移量设置为文件长度加offset,offset可正可负。

以上三个符号常量是System V中引入的,在此之前,whence被指定为0(绝对偏移量)、1(相对于当前位置偏移量)、2(相对于文件尾端的偏移量),很多软件仍把这些数字直接写在代码中。

若lseek函数成功执行,则返回新文件偏移量,我们可以这样获取打开文件的当前偏移量:

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

这种方法也能用来确定所涉及的文件是否可以设置偏移量,如文件描述符指向的是一个管道、FIFO、网络套接字,则lseek返回-1,并将errno设置为ESPIPE。

lseek函数名字中的l表示长整型,在引入off_t类型之前,它的返回类型是长整型的。

测试标准输入能否设置偏移量:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>

int main() {
    if (lseek(STDIN_FILENO, 0, SEEK_CUR) == -1) {    // lseek、STDIN_FILENO、SEEK_CUR都在头文件unistd.h中
        if (errno == ESPIPE) {
            printf("cannot seek\n");
        }
    } else {
        printf("seek OK\n");
    }
    
    exit(0);    // exit在头文件stdlib.h中
}

执行结果:
在这里插入图片描述
将文件作为标准输入时:
在这里插入图片描述
文件当前偏移量应是一个非负整数,但某些设备可能允许负偏移量,但普通文件的偏移量必须是一个非负值,因此,比较lseek函数的返回值时不应测试它是否小于0,而应该测试它是否等于-1。

偏移量off_t是带符号数据类型,所以文件最大长度会减少一半,如off_t是32bit整型,则文件最大长度为2^31 - 1。

lseek函数仅将当前文件偏移量记录在内核中,不引起任何IO操作,然后该偏移量用于下次的读或写。

文件偏移量可以大于文件当前长度,此时,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,空洞中的字节被读为0。

文件中的空洞并不要求在磁盘上占用存储区,具体处理方式与文件系统的实现有关。定位到文件尾端之后写时,新写的数据需要分配磁盘块,但对于原文件尾端和新开始写位置之间不需分配磁盘块。

创建具有空洞的文件:

#include <stdio.h>
#include <stdlib.h>

char buf1[] = "abcdefghij";
char buf2[] = "ABCDEFGHIJ";

int main() {
    int fd;
    
    if ((fd = creat("file.hole", 0644)) < 0) {    // creat创建的文件返回的fd是只写打开
        printf("creat error");
        exit(0);
    }

    if (write(fd, buf1, 10) != 10) {
        printf("buf1 write error");
        exit(0);
    }
    /* offset now = 10 */

    if (lseek(fd, 16384, SEEK_SET) == -1) {
        printf("lseek error");
        exit(0);
    }
    /* offset now = 16384 */

    if (write(fd, buf2, 10) != 10) {
        printf("buf2 write error");
        exit(0);
    }
    /* offset now = 16394 */

    exit(0);
}

执行它:
在这里插入图片描述
可以使用od命令观察文件实际内容,-c选项是以字符方式打印文件内容:
在这里插入图片描述
如上,每一行开始的7位数是以八进制形式表示的字节偏移量。

创建一个没有空洞且长度相同的文件比较:
在这里插入图片描述
虽然文件长度相同,但无空洞的文件占了20个磁盘块。

lseek函数使用了off_t类型表示偏移量,具体实现上各平台自行选择合适的数据类型。大多数平台提供两种接口处理文件偏移量,一组使用32位文件偏移量,另一组使用64位文件偏移量。

SUS向应用提供了一种方法,可通过sysconf函数确定支持何种环境:
在这里插入图片描述

应用程序可加上宏定义_FILE_OFFSET_BITS,并将该值设置为64,以支持64位偏移量,这样就将off_t的定义改为64位带符号整型了,但这种方法不可移植。尽管可以实现64位文件偏移量,但能否创建一个大于2GB(2^31 - 1字节)的文件依赖于底层文件系统的类型。

read函数从打开的文件中读数据:
在这里插入图片描述
如read成功,返回读到的字节数,如已到达文件尾,返回0。

读到的字节数少于要求读的字节数发生的情况:
1.读普通文件时,在读到要求字节数前到达了文件尾,此时返回已读到的字符数。
2.从终端设备读时,通常一次最多读一行。
3.从网络读时,网络中缓冲机制可能造成返回值小于要求读的字符数。
4.从管道或FIFO读时,若管道中包含的字节少于要求的数量,read只返回实际读的字节数。
5.从面向记录的设备读时(如磁带),一次最多返回一个记录。
6.当信号造成中断,而已经读了部分数据时。

read函数的返回值类型ssize_t是带符号整型。第三个参数是不带符号size_t类型。

write函数向打开的文件写数据:
在这里插入图片描述
write函数的返回值通常与参数nbytes相同,否则表示出错,出错原因之一是磁盘已写满或超过了一个给定进程的文件长度限制。

对于普通文件,写操作从文件的当前偏移量开始,指定了O_APPEND选项后,每次写之前,文件偏移量设置在文件当前结尾处。每次写成功后,文件偏移量增加实际写的字数。

使用read和write复制一个文件:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

#define BUFFSIZE 4096

int main() {
    int n;
    char buf[BUFFSIZE];

    while ((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0) {
         if (write(STDOUT_FILENO, buf, n) != n) {    // 写入的字符数要等于读取的字符数
             printf("write error\n");
         }
    }

    if (n < 0) {    // n小于0时说明读取失败
        printf("read error\n");
    }
 
   exit(0);
} 

执行结果:
在这里插入图片描述
上述程序从标准输入读,向标准输出写,这要求标准输入、输出已经安排好,实际上,UNIX的shell都提供一种方法,在标准输入上打开一个文件用于读,在标准输出上创建(或重写)一个文件,这使得程序不用亲自打开输入和输出文件,并允许用户利用shell的IO重定向功能。

UNIX系统内核会在程序结束时关闭其打开的所有文件描述符,因此程序结束前不用显式关闭文件。

对UNIX内核而言,文本文件和二进制代码文件并无区别,以上程序对这两种文件都有效。

对于BUFFSIZE的选取,进行以下测试,测试的磁盘块长度为4096字节:
在这里插入图片描述
上图中,系统CPU时间的几个最小值出现在BUFFSIZE为4096及之后的位置,继续增加缓冲区长度对此时间几乎没有影响。大多数文件系统为改善性能都采用某种预读技术,当检测到正进行顺序读取时,系统会试图读入比应用所要求的更多的数据,并假设应用会用到这些数据,上图中预读效果可以从缓冲区长度32字节及以上的时钟时间几乎一样看出。

进行文件读、写性能度量时,操作系统会用高速缓存技术将相关文件放在主存中,这使得第一次之后的测试很可能好于第一次,以上测试每次都使用了不同的副本,并且文件足够大,不会全部保留在高速缓存中。

UNIX系统支持在不同进程间共享打开的文件。先介绍内核用于所有IO的数据结构,与特定实现可能匹配,也可能不匹配。内核使用三种数据结构表示打开文件,它们之间的关系决定了文件共享时一个进程对另一个进程可能产生的影响:
1.每个进程在进程表中都有一个进程表项,进程表项中包含一张打开文件描述符表,打开文件描述符表中包含每个打开文件的描述符和指向一个文件表项的指针。
2.内核为所有打开文件维持一张文件表,每个文件表项包含:
(1)文件状态标志(读、写、添加、同步和非阻塞等)。
(2)当前文件偏移量。
(3)指向该文件v节点表项的指针。
3.每个打开文件(或设备)都有一个v节点结构,其中包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件,v节点还包含了该文件的i节点(索引节点)。这些信息是打开文件时从磁盘读入内存的。(i节点包含了文件的所有者、文件长度、指向文件实际数据块在磁盘上的位置的指针等)

在这里插入图片描述
上图显示一个进程打开了两个文件,一个从标准输入打开(文件描述符0),一个从标准输出打开。

创建v节点的目的是对在一个系统上的多文件系统类型提供支持,Sun把这种文件系统称为虚拟文件系统,把与文件系统无关的i节点部分称为v节点,当各个制造商的实现增加了对Sun的网络文件系统(NFS)的支持时,它们都广泛采用了v节点结构。BSD系列首先提供v节点的是增加了NFS的4.3BSD Reno。在SVR4中,v节点替代了SVR3中与文件系统无关的i节点结构,Solaris是从SVR4发展而来的,因此它也用v节点。

Linux没有使用v节点,而是使用了通用i节点结构(一个与文件系统相关的i节点和一个与文件系统无关的i节点)。

如果两个进程各自打开了同一个文件:
在这里插入图片描述
打开该文件的每个进程都获得了各自的一个文件表项(因为每个进程都有它自己的对该文件的当前偏移量),但对于一个给定的文件只有一个v节点表项。对于每个操作:
1.完成每个write后,文件表项中的当前文件偏移量即增加所写入的字节数,如导致当前文件偏移量超出了当前文件长度,则将i节点表项中的当前文件长度加长为当前文件偏移量。
2.如用O_APPEND打开一个文件,相应标志也被设置到文件表项的文件状态标志中,每次对这种文件执行写操作时,文件表项中当前文件偏移量首先会被设置为i节点表项中的文件长度,使得每次写入的数据都被追加到文件的当前尾端。
3.文件用lseek函数定位到文件当前尾端时,文件表项中的当前文件偏移量被设置为i节点表项中当前文件长度。
4.lseek函数只修改文件表项中的当前文件偏移量,不进行IO操作。

可能会有多个文件描述符指向同一文件表项,使用dup或fork(父进程和子进程每一个相同的打开文件描述符共享同一个文件表项)函数后会发生这种情况。

如果一个进程要将数据追加到文件尾,早期的UNIX的open函数没有O_APPEND选项,程序会被写成以下形式:

if (lseek(fd, OL, 2) < 0) {    // 移动到2(即文件尾)的0偏移量处,L表示0是long类型数据
    err_sys("lseek error");
}
if (write(fd, buf, 100) != 100) {
    err_sys("write error");
}

对于单个进程来说,上述程序可正常工作,但若有多个进程同时用以上方法写数据,会产生问题,如两个进程同时写同一文件,这个文件只有一个v节点,假设一个进程使用了lseek函数将文件偏移量设置为1500(文件尾),然后内核切换到另一进程,另一进程也使用lseek函数将文件偏移量设置为1500,同时write使得该文件偏移量增加到1600,然后内核切换回原进程,原进程再调用write时就不是从文件尾写了,而是从1500处写,这就覆盖了另一进程写的内容。

以上问题的解决方法是使定位到文件尾和写数据对其他进程而言成为一个原子操作。UNIX的O_APPEND标志实现了这一操作,这样每次写时就不用调用lseek了。

SUS包括了XSI扩展,该扩展允许原子地定位并执行IO,pread和pwrite函数就是这样:
在这里插入图片描述
调用pread相当于调用lseek后调用read,但也有以下区别:
1.调用pread时,无法中断其定位和读的操作。(原子性)
2.不更新当前文件偏移量。

open函数的O_CREAT和O_EXCL标志的组合就是原子操作的例子,同时指定这两个选项时,如该文件已经存在,open将失败(不会创建文件),检查文件是否存在和创建文件这两个操作是作为一个原子操作进行的,如果没有这个原子操作,那么以下程序会出错:

if ((fd = open(pathname, O_WRONLY)) < 0) {    // 如果打开失败
    if (errno == ENOENT) {    // ENOENT表示因文件不存在而打开失败
        if ((fd = creat(path, mode)) < 0) {    // 创建它,如创建失败
            err_sys("creat error");
        }
    } else {    // 如果是因为其他原因打开失败
        err_sys("open error");
    }
}           

如上,如果在调用open和creat之间,另一个进程创建了该文件,并且写入了一些数据,然后这个进程再调用creat,会导致另一个进程写入的数据被抹去,但原子操作可以解决这个问题。

以下函数可以复制一个现有文件描述符:
在这里插入图片描述
函数dup返回的新文件描述符一定是当前可用文件描述符中的最小值(这点和open、openat函数相同)。函数dup2可以用fd2参数指定新的描述符的值,如果fd2已经打开,则会先将其关闭,如fd等于fd2,则返回fd2,而不会关闭它。fd2的FD_CLOEXEC文件描述符flag将被清除,因此如果进程调用exec,fd2将保持打开状态。

函数dup和dup2返回的新文件描述符与参数fd共享同一个文件表项:
在这里插入图片描述
上图,我们执行了:

newfd = dup(1);

并且可用的最小描述符是3。因为这两个文件描述符指向同一文件表项,所以它们共享同一文件状态标志(读、写、追加等)和同一当前文件偏移量。

每个文件描述符都有它自己的一套文件描述符标志,以上dup、dup2函数返回的新描述符的执行时关闭标志总是会被清除。

fcntl函数也能复制一个描述符,以下调用:

dup(fd);
dup2(fd, fd2);

等价于:

// 等价于dup函数
fcntl(fd, F_DUPFD, 0);    
// 等价于dup2函数
close(fd2);
fcntl(fd, F_DUPFD, fd2);

后一种情况中,dup2函数不完全等同于调用close加fcntl,区别如下:
1.函数dup2是原子操作,而close和fcntl的调用之间可能调用了信号捕获函数,其中可能修改了文件描述符;或调用之间不同线程改变了文件描述符。
2.函数dup2和fcntl有一些不同的errno。

传统UNIX内核中都设有缓冲区高速缓存和页高速缓存,大多数磁盘IO都通过缓冲区进行,向文件写数据时,内核先将数据复制到缓冲区,然后再排入队列,晚些时候再写入磁盘,以上写入过程称为延迟写。当内核需要重用缓冲区存放其他磁盘块数据时,会把所有延迟写数据块写入磁盘,以下函数保证缓冲区内容和实际磁盘上文件系统内容的一致性:
在这里插入图片描述
函数sync只是将所有修改过的块缓冲排入写队列,然后返回,实际并不等待写磁盘操作结束。

名为update的系统守护进程周期性调用(一般30秒)sync函数,定期flush内核的块缓冲区。命令sync会调用sync函数。

函数fsync只对一个文件起作用,并等待写磁盘操作结束才返回,fsync函数可用于数据库这样的程序,这种程序需要确保修改过的块立即写到磁盘上。

函数fdatasync类似于函数fsync,但它只影响文件的数据部分,fsync函数还会同步更新文件的属性。

fcntl函数可以改变已经打开的文件的属性:
在这里插入图片描述
fcntl函数功能:
1.复制一个已有的描述符(cmd=F_DUPFD或F_DUPFD_CLOEXEC)。
2.获取/设置文件描述符标志(cmd=F_GETFD或F_SETFD,当前只有一个文件描述符标志FD_CLOEXEC)。
3.获取/设置文件状态标志(cmd=F_GETFL或F_SETFL)。
4.获取/设置异步IO所有权(cmd=F_GETOWN或F_SETOWN)。
5.获取/设置记录锁(cmd=F_GETLK、F_SETLK、F_SETLKW)。

各种cmd作用:
1.F_DUPFD:复制文件描述符fd。新文件描述符作为函数值返回,它是尚未打开的描述符中大于等于第三个参数值(取整型值)的最小值。新描述符与fd共享同一文件表项,但新描述符有它自己的一套文件描述符标志,新描述符的FD_CLOEXEC文件描述符标志被清除(即新描述符在exec后仍有效)。
2.F_DUPFD_CLOEXEC:复制文件描述符,并且设置新描述符的FD_CLOEXEC文件描述符标志位,返回新文件描述符值。
3.F_GETFD:对应于fd的文件描述符标志作为函数值返回,当前只定义了一个文件描述符标志值FD_CLOEXEC。
4.F_SETFD:按第三个参数值设置fd的文件描述符标志。(很多程序不使用FD_CLOEXEC,而是设为0或1,0表示在exec时打开)
5.F_GETFL:返回fd的文件状态标志(以int返回),返回值如下:
在这里插入图片描述
但前五个访问方式标志并不各占一位,这五个值互斥,因此首先必须用常量O_ACCMODE与fcntl函数返回的文件状态标志相与,之后查看相与的结果是五个值中的哪一个。
6.F_SETFL:将文件状态标志设置为第三个参数的值,可以更改的标志为:O_APPEN、O_NONBLOCK、O_SYNC、O_DSYNC、O_RSYNC、O_FSYNC、O_ASYNC。
7.F_GETOWN:获取当前接收SIGIO和SIGURG信号的进程或进程组ID,返回一个正的进程ID或负的进程组ID。
8.F_SETOWN:设置接收SIGIO和SIGURG信号的进程或进程组ID,正的arg参数表示一个进程ID,负的arg参数表示一个等于其绝对值的进程组ID。

打印文件标志说明:

#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
    int val;
    
    if (argc != 2) {
        printf("usage: a.out <descriptor#>");
        exit(1);
    }

    if ((val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0) {    // atoi函数所在头文件为stdlib.h
        printf("fcntl error for fd %d", atoi(argv[1]));
    }

    switch (val & O_ACCMODE) {    // 这几个标志位互斥
        case O_RDONLY:    // 文件访问标志位在头文件fcntl.h中
            printf("read only");
            break;
        case O_WRONLY:
            printf("write only");
            break;
        case O_RDWR:
            printf("read write");
            break;
        default:
            printf("unknow access mode");
            exit(1);
    }

    if (val & O_APPEND) {
        printf(", append");
    }
    if (val & O_NONBLOCK) {
        printf(", nonblocking");
    }
    if (val & O_SYNC) {
        printf(", synchronous writes");
    }

#if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC) && (O_FSYNC != O_SYNC)    // _POSIX_C_SOURCE表示编译时希望只与POSIX的定义有关,而忽略具体实现的定义
    if (val & O_FSYNC) {
        printf(", synchronous writes");
    }
#endif

    putchar('\n');    // putchar函数所在头文件为stdio.h
    
    exit(0);
}

执行以上程序:
在这里插入图片描述
上图中5<>temp.foo表示以读写方式在文件描述符5上打开文件temp.foo。

修改文件描述符标志或文件状态标志时要先获取现在的标志,修改它后再设置新值,不能简单地只是执行F_SETFD或F_SETFL,这样会关闭其他标志位:

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

void set_fl(int fd, int flags) {
    int val;
    
    if ((val = fcntl(fd, F_GETFL, 0)) < 0) {
        printf("fcntl F_GETFL error");
        exit(1);
    }
     
    val |= flags;    // 打开flags
     
    if (fcntl(fd, F_SETFL, val) < 0) {
        printf("fcntl F_SETFL error");
        exit(1);
    }
}

如果想关闭某些flag:

val &= ~flags;

使用以上程序:

set_fl(STDOUT_FILENO, O_SYNC);    // 开启标准输出的同步写标志

上述代码开启同步写标志后,每次write都要等待,直至数据已写到磁盘上再返回。UNIX中,一般write只是将数据排入队列,而实际写操作可能在以后某个时间进行。而数据库系统需要使用O_SYNC,使得数据真正写到磁盘上再返回,以免在系统异常时丢失数据。

设置O_SYNC标志会增加程序运行的系统时间和时钟时间。我们运行本章写过的将一个文件复制到另一个文件中的程序,将磁盘文件中一个492.6MB大小的文件复制到另一个文件:
在这里插入图片描述
以上测试都在BUFFSIZE=4096字节时测量的。第二行是从磁盘读一个文件,之后再写到磁盘;而第一行是从磁盘读入一个文件,然后写入/dev/null,因此没有磁盘输出,这是第一行和第二行有差异的原因,在往磁盘写数据时,系统CPU时间增加了,因为内核需要从进程中复制数据,并将数据排入队列以便磁盘驱动器写到磁盘上。

支持同步写时(第三行),系统时间和时钟时间会显著增加,但第三行(同步写)所用的系统时间并不比第二行(延迟写)增加很多,这意味着要么Linux对延迟写和同步写的工作量相同(不太可能);要么O_SYNC标志没起到期望作用,这说明Linux不允许我们通过fcntl函数设置O_SYNC标志,fcntl函数会失败而不返回错误(但它应该接受这个flag,如果这个文件是打开的)。

时钟时间(Clock Time)在最后三行里的增加反映了将数据写在磁盘上所等待的额外时间。而最后一行中,我们已经同步写(设置了O_SYNC)到磁盘,之后再调用fsync,耗费的时间与第五行消耗时间大致相同,进一步表明O_SYNC标志失效。

在另一个系统上的测试:
在这里插入图片描述
上表表明此MAC OS上的O_SYNC标志是有用的。同步写后再调用fsync(第五行)与同步写(第三行)的时钟时间消耗差不多。在延迟写后再调用fsync(第四行)时间比普通的同步写(第三行)时间要短,可能原因是,向某个文件写入新数据时,操作系统已经将以前写入的数据都冲洗到了磁盘上,所以调用函数fsync时只需做很少的工作。

函数fsync和fdatasync都能更新文件内容,就像用了O_SYNC标志每次写入文件都更新文件内容一样,这两个函数的性能依赖很多因素,包括底层操作系统实现、磁盘驱动器的速度、文件系统的类型。

fcntl函数是必要的,程序输入一个文件描述符时,会在一个描述符上进行操作,shell不知道由shell打开的相应文件的文件名,因为这是由shell打开的,所以就不能在打开时设置O_SYNC标志,使用fcntl函数只需知道打开文件的描述符,就能修改描述符属性。管道也会用到fcntl函数,因为对于管道,我们只知道其描述符。

ioctl函数是IO操作的杂物箱:
在这里插入图片描述
函数ioctl是SUS的一个扩展部分,以便处理STREAMS设备,但在SUSv4中被弃用。UNIX用它进行很多杂项设备操作。有些实现将它扩展到普通文件。

以上函数原型对应POSIX.1,在FreeBSD 8.0和Mac OS X 10.6.8中第二个参数被声明为unsigned long,因为第二个参数总是头文件中一个#define的名字,所以这种细节没什么影响。

ISO C原型用省略号表示其他参数,但通常只有另外一个参数,指向一个变量或结构的指针。

上图仅表示ioctl函数本身所要求的头文件,通常还要求另外的设备专用头文件,如POSIX.1终端IO的ioctl命令还需头文件termios.h。

每个设备驱动程序可定义它自己专用的一组ioctl命令,系统会为不同种类设备提供通用的ioctl命令。FreeBSD支持的通用ioctl命令:
在这里插入图片描述
磁带操作可以进行倒带、写一个文件结束符等操作,其他函数如read、write、lseek等难以表示这些操作。

ioctl函数还能获取和设置终端窗口大小、访问伪终端。

较新系统都提供/dev/fd目录,目录项是名为0、1、2等的文件,打开/dev/fd/n等效于复制描述符n(假定描述符n是打开的)。它不是POSIX.1的组成部分。

以下调用:

fd = open("/dev/fd/0", mode);    // 大多系统忽略它指定的mode,一些系统要求mode必须是所引用的文件(此处是标准输入)初始打开时模式的子集

等价于:

fd = dup(0);

文件描述符fd和0共享同一文件表项,如0之前被打开为只读,那么我们只能对fd读,即使mode参数是O_RDWR且执行成功。但Linux系统中,/dev/fd文件夹中的描述符被映射为指向底层物理文件的符号链接,此时打开/dev/fd/0时实际打开的是与标准输入关联的文件,因此返回的新文件描述符模式与/dev/fd/文件描述符的模式不相关。

我们可以使用/dev/fd作为路径名参数调用creat,与调用open函数时用O_CREAT作第二个参数作用相同,这使得以例如/dev/fd/1为path参数调用creat时,程序能正常运行。但在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,之后lpr打印这些内容。也可以改为:

filter file2 | cat file1 /dev/fd/0 file3 | lpr

很多程序采用"-"作为标准输入或标准输出。但如果用-指定第一个文件,看起来像指定了命令行的一个选项,用/dev/fd提高了文件名参数的一致性,也更清晰。

自己实现dup2函数,不使用函数fcntl,可以重复调用dup,直到返回的文件描述符等于指定大小:

#include <unistd.h>
#include <fcntl.h>
#include <iostream>
using namespace std;

const int MAX_TRY = 1000;

int myDup2(int fd, int t_fd) {
    if (fd == t_fd) {
        return t_fd;
    }
    
    int currFd = dup(fd), startFd = currFd;
    int count = 1;
    while (currFd < t_fd && (count++ < MAX_TRY)) {
        currFd = dup(fd);
        
        cout << "current fd:" << currFd << endl;
    }
    
    if (currFd == t_fd) {
        for (int i = startFd; i < t_fd; ++i) {
            close(i);
        }

        return t_fd;
    } else {
        return -1;
    }
}

int main() {
    int fd = open("test.file", O_RDWR | O_TRUNC | O_CREAT);
    int res = myDup2(fd, 200);

    if (res == 200) {
        cout << "success" << endl;
    } else {
        cout << "fail" << endl;
    }
} 

经过以下调用:

fd1 = open(path, oflags);
fd2 = dup(fd1);
fd3 = open(path, oflags);

实际内存:
在这里插入图片描述
此时若调用fcntl修改fd1的文件描述符标志位和文件状态标志位,会影响到fd1和fd2。

dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);

if (fd > 2) {
    close(fd);
}

以上代码是为了将标准输入、标准输出、标准错误重定向到一个文件,之后描述符fd就可以关闭了。

vi在命令模式下输入:set number(nu)可以显示行号,输入:set nonumber(nonu)可以取消显示。

Bourne shell、Bourne-again shell、Korn shell中,digit1>&digit2表示将描述符digit1重定向至描述符digits2的同一文件:

./a.out > outfile 2>&1 
./a.out 2>&1 > outfile

上述代码第一句表示先将标准输出重定向到outfile中,再将标准错误重定向到标准输出(即outfile),此时两者都指向outfile的文件描述符,结果就是标准输出和标准错误都输出到outfile中;而第二句表示先将标准错误重定向到标准输出,之后标准输出再重定向到outfile,此时两者指向不同的文件表项,结果就是标准错误显示在标准输出上,而标准输出被写入outfile。

以O_APPEND和O_RDWR打开文件时,可以调用lseek在任意位置开始读,但不能在任意位置更新数据:

// 验证可以在任意位置读数据
#include <unistd.h>
#include <fcntl.h>
#include <string>
#include <iostream>
using namespace std;

int main() {
    int fd = open("test.file", O_RDWR | O_APPEND);
    lseek(fd, 3, SEEK_SET);
    char buf[11];
    if (read(fd, buf, 10) < 0) {
        cout << "read error" << endl;
        return -1;
    }
    buf[10] = '\0';

    cout << string(buf) << endl;
}

如果test.file内容如下:
在这里插入图片描述
则输出如下:
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值