• UNIX环境高级编程-第三章


    1.文件描述符

    对于内核而言,所有打开的文件都通过文件描述符引用。 文件描述符是一个非负整数。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。当读,写一个文件时,使用open或create返回的文件描述符标识该文件,将其作为参数传送给read或write。
    按照惯例,UNIX系统shell把文件描述符0与进程的标准输入关联,文件描述符1与标准输入关联,文件描述符1与标准输出关联,文件描述符2与标准错误关联。 这是各种shell以及很多应用程序使用的惯例,与UNIX内核无关。尽管如此,如果不遵循这种惯例,很多UNIX系统应用程序就不能正常工作。
    在符合POSIX.1的应用程序中,幻数0,1,2虽然已被标准化,但应当把它们替换成符号常量STDIN_FILENO,STDOUT_FILENO和STDERR_FILENO以提高可读性。这些常量都在头文件中定义。
    文件描述符的变化范围是0-OPEN_MAX-1。早期的UNIX系统实现采用的上限值是19,但现在很多系统将其上限值增加至63.

    2.函数open和openat

    调用open或openat函数可以打开或创建一个文件

    #include
    int open(const char *path,int oflag,.../* mode_t mode */);
    int openat(int fd,const char *path,int oflag,.../* mode_t mode*/);
    
    • 1
    • 2
    • 3

    我们将最后一个参数写为…,ISO C用这种方法表明余下的参数的数量及类型是可变的。 对于open函数而言,仅当创建新文件时才使用最后这个参数。在函数原型中将此参数放置在注释中。
    path参数是要打开或创建文件的名字。 oflag参数可用来说明此函数的多个选项。用下列一个或多个常量进行"或"运算构成oflag参数(这些常量在头文件中定义)。

    O_RDONLY	只读打开
    O_WRONLY	只写打开
    O_RDWR		读,写打开。
    (大多数实现将O_RDONLY定义为0,O_WRONLY定义为1,O_RDWR定义为2,以与早期的程序兼容)。
    O_EXEC		只执行打开
    O_SEARCH	只搜索打开
    (O_SEARCH常量的目的在于目录打开时验证它的搜索权限。对目录的文件描述符的后续操作就不需要再次检查对该目录的搜索权限)
    在这5个常量中必须指定一个且只能指定一个。下列常量则是可选的。
    O_APPEND	再次写时都追加到文件的尾端。
    O_CLOEXEC	把FD_CLOEXEC常量设置为文件描述符标志。
    O_CREAT		若此文件不存在则创建它。使用此选项时,open函数须同时说明第三个参数mode,用mode指定该新文件的访问权限位。
    O_DIRECTORY	如果path引用的不是目录,则出错
    O_EXCL		如果同时指定了O_CREAT,而文件已经存在,则出错。用此可以测试一个文件是否存在,若不存在,则创建此文件,这使得测试和创建两者成为一个原子操作。
    O_NOCTTY	如果path引用的时终端设备,则不将该设备分配作为此进程的控制终端
    O_NOFOLLOW	如果path引用的是一个符号链接,则出错
    O_NONBLOCK	如果path引用的是一个FIFO,一个块特殊文件或一个字符特殊文件,则此选项为文件的本次打开操作和后续的I/O操作设置非阻塞方式。
    O_SYNC		使每次write等待物理IO操作完成,包括由该write操作引起的文件属性更新所需的IO。
    O_TRUNC		如果此文件存在,而且为只写或读-写成功打开,则将其长度截断为0.
    O_TTY_INIT	如果打开一个还未打开的终端设备,设置非标准termios参数值,使其符合Single UNIX Specification中同步输入和输出选项的一部分。
    O_DSYNC		使每次write要等待物理IO操作完成,但是如果该写操作并不影响读取刚写入的数据,则不需要等待文件属性被更新。
    O_RSYNC		使每一个以文件描述符作为参数进行的read操作等地啊,直至所有对文件同一部分挂起的写操作都完成。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    由open和openat函数返回的文件描述符一定是最小的未用描述符值。 这一点被某些应用程序用来在标准输入,标准输出或标准错误打开新的文件。例如,一个应用程序可以先关闭标准输出,然后打开另一个文件,执行打开操作前就能了解该文件一定会在文件描述符1上打开。
    fd参数把open和openat函数区分开,有三种可能性:
    (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错误通常处理的就是那些颠覆文件系统权限的小把戏,这些小把戏通过骗取特权程序降低特权文件的权限控制或者让特权文件打开一个安全漏洞等方式进行。

    文件名和路径名截断
    如果NAME_MAX是14,而我们却试图在当前目录中创建一个文件名包含15个字符的新文件,此时会发生什么呢?安装传统,早期的System V版本允许这种使用方法,但总是将文件名截断为14个字符,而且不给出任何信息,而BSD类的系统则返回出错状态,并将errno设置为ENAMETOOLONG。无声无息截断文件名会引起问题,而且它不仅仅影响到创建新文件。如果NAME_MAX是14,而存在一个文件名恰好就是14个字符的文件,那么以路径名作为其参数的任一函数都无法确定该文件的原始名是什么。其原因是这些函数无法判断改文件名是否被截断过。
    在POSIX.1中,常量_POSIX_NO_TRUNC决定是要截断过长的文件名或路径名,还是返回一个出错。正如我们在第2章中已经见到,根据文件系统的类型,此值可以变化。我们可以用fpathconf或pathconf来查询目录中具体支持何种行为。

    4.函数create

    #include
    int create(const char *path,mode_t mode);
    
    • 1
    • 2

    此函数等效于:

    open(path,O|WRONLY|O_CREAT|O_TRUNC,mode);
    
    • 1

    即只写打开,若没有该文件则创建一个,打开后,文件内容长度为0。

    5.函数close

    #include
    int close(int fd);
    
    • 1
    • 2

    关闭一个文件时还会释放该进程加在该文件上的所有记录锁。
    当一个进程终止时,内核自动关闭它所有打开的文件。很多程序都利用了这一功能而不显示地用close关闭打开的文件。

    6.函数lseek

    每个打开文件都有一个与其相关联的“当前文件偏移量”。它通常是一个非负整数,用以度量从文件开始处计算的字节数。通常,读写操作都从当前文件偏移量出开始,并使偏移量增加所读写的字节数。按系统默认的情况,当打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置为0。
    可以调用lseek显示地为一个打开文件设置偏移量

    off_t lseek(int fd,off_t offset,int where);
    
    • 1

    对参数offset的解释与参数whence的值有关。
    ①若whence是SEEK_SET,则将该文件的偏移量设置为距文件开始处offset个字节。
    ②若whence是SEEK_CUR,则将文件的偏移量设置为当前值加上offset,offset可正可负
    ③若whence是SEEK_END,则将该文件的偏移量设置为文件长度加offset,offset可正可负。

    若lseek成功执行,则返回新的文件偏移量,为此可以用下列方式确定打开文件的当前偏移量。

    off_t currpos;
    currpos=lseek(fd,0,SEEK_CUR);
    
    • 1
    • 2

    这种方法也可以用来确定所涉及的文件是否可以设置偏移量。如果文件描述符指向的是一个管道,FIFO或网络套接字,则lseek返回-1,并将errno设置为ESPIPE。
    3个符号常量SEEK_SET,SEEK_CUR,SEEK_END是在System V中引入的,在System V之前,whence被指定为0(绝对偏移量),1(相对于当前位置的偏移量),2(相对文件尾端的偏移量)。

    实例3-1:测试对其标准输入能否设置偏移量

    #include "apue.h"
    int
    main(void)
    {
    	if(lseek(STDIN_FILENO,0,SEEK_CUR)==-1)
    		printf("cannot seek\n");
    	else
    		printf("seek OK\n");
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    通常,文件的当前偏移量应当是一个非负整数,但是,某些设备也可能允许负的偏移量。但对于普通文件,其偏移量必须是非负值。因为偏移量可能是负值,所以在比较lseek的返回值时应当谨慎,不要测试它是否小于0,而要测试它是否等于-1。
    lseek仅仅将当前的文件偏移量记录在内核中,并不引起任何IO操作。
    文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加上该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被读为0。
    文件的空洞并不要求在磁盘上占用存储区。具体处理方式与文件系统的实现有关,当定位到超出文件尾端之后写时,对于新写的数据需要分配磁盘块,但是对于源文件尾端和新开始写位置之间的部分则不需要分配磁盘块。

    实例3-2:创建一个具有空洞的文件

    #include "apue.h"
    #include 
    
    char buf1[]="abcdefghij";
    char buf2[]="SBCDEFGHIJ";
    
    int
    main(void)
    {
    	int fd;
    	if((fd=creat("file.hole",FILE_MODE))<0)
    		err_sys("create error");
    	if(write(fd,buf1,10)!=10)
    		err_sys("buf1 write error");
    	if(lseek(fd,16384,SEEK_SET)==-1)
    		err_sys("lseek error");
    	if(write(fd,buf2,10)!=10)
    		err_sys("buf2 write error");
    
    	exit(0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    因为lseek使用的偏移量是用off_t类型表示的,所以允许具体实现根据各自特定的平台自行选择大小合适的数据类型。现今大多数平台提供两组接口以处理文件偏移量。一组使用32位文件偏移量,另一组则使用64位文件偏移量。
    Single UNIX Specification 向应用程序提供了一种方法,使其通过sysconf函数确定支持何种环境。图3-3总结了定义的sysconf常量。
    图3-3:
    在这里插入图片描述

    7.函数read

    调用read函数从打开文件中读数据

    #include
    ssize_t read(int fd,void *buf,size_t nbytes);
    
    • 1
    • 2

    如果read成功,则返回读到的字节数,如已到达文件的尾端,则返回0。有多种情况可使实际读到的字节数少于要求读的字节数:

    ①读普通文件时,在读到要求字节数之前已达到文件尾端。例如若在到达文件尾端之前有30个字节,而要求读100个字节,则read返回30。下一次再调用read时,它将返回0(文件尾端)。
    ②当从终端设备读时,通常一次最多读一行
    ③当从网络读时,网络中的缓冲机制可能造成返回值小于所要读的字节数
    ④当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么read将只返回实际可用的字节数。
    ⑤当从某些面向记录的设备读时,一次最多返回一个记录。
    ⑥当一信号中断,而已经读了部分数据量时。我们将在10.5进一步讨论此种情况。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    读操作从文件的当前偏移量处开始,在成功返回之前,该偏移量将增加实际读到的字节数。
    POSIX.1从几个方面对read函数的原型作了更改。原型定义是:

    int read(int fd,char *buf,unsigned nbytes);
    
    • 1

    ①首先,为了与ISO C一致,第2个参数由char *改为void *。在ISO C中,类型void *用于表示通用指针。
    ②其次,返回值必须是一个带符号整型(ssize_t),以保证能够返回正整数字节数,0(表示文件尾端),-1(出错)。
    最后,第三个参数在历史上是一个无符号整型,这允许一个16位的实现一次读或写的数据可以多大65534个字节。在1990 POSIX.1标准中,引入了新的基本系统数据类型ssize_t以提供带符号的返回值,不带符号的size_t则用于第三个参数。

    8.write函数

    调用write函数向打开文件写数据

    #include
    ssize_t write(int fd,const void *buf,size_t nbytes);
    
    • 1
    • 2

    其返回值通常与参数nbytes的值相同,否则表示出错。write出错的一个常见原因是磁盘已写满,或者超过了一个给定进程的文件长度限制。
    对于普通文件,写操作从文件的当前偏移量处开始。如果打开文件时,指定了O_APPEND选项,则在每次写操作之前,将文件偏移量设置在当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数。

    9.I/O的效率

    实例3-5:使用read和write函数复制一个文件

    #include "apue.h"
    #define BUFFSIZE 4096
    
    int
    main(void)
    {
    	int n;
    	char buf[BUFFSIZE];
    	while ((n=read(STDIN_FILENO,buf,BUFFSIZE))>0)
    		if(write(STDOUT_FILENO,buf,n)!=n)
    			err_sys("write error");
    	
    	if(n<0)
    		err_sys("read error");
    	exit(0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    关于该程序应注意:
    ①它从标准输入读,写至标准输出,所以复制文件需要重定向输入输出即可。
    ②此程序并不关闭输入和输出文件
    我们是如何选取BUFFSIZE值的呢?在回答问题之前,让我们用各种不同的BUFFSIZE值来运行此程序。图3-6显示了20中不同的缓冲区长度,读516581760字节的文件所得到的结果。
    用3-5的代码读文件,其标准输出被重定向到/dev/null上。此测试所用的文件系统是Linux ext4文件系统,其磁盘块长度为4096字节。这也证明了图3-6中系统CPU时间的几个最小值差不多出现在BUFFSIZE为4096及以后的位置, 继续增加缓冲区长度对此时间几乎没有影响。
    图3-6:
    在这里插入图片描述
    关于该程序应注意:
    ①它从标准输入读,写至标准输出,所以复制文件需要重定向输入输出即可。
    ②此程序并不关闭输入和输出文件
    我们是如何选取BUFFSIZE值的呢?在回答问题之前,让我们用各种不同的BUFFSIZE值来运行此程序。图3-6显示了20中不同的缓冲区长度,读516581760字节的文件所得到的结果。
    用3-5的代码读文件,其标准输出被重定向到/dev/null上。此测试所用的文件系统是Linux ext4文件系统,其磁盘块长度为4096字节。这也证明了图3-6中系统CPU时间的几个最小值差不多出现在BUFFSIZE为4096及以后的位置,继续增加缓冲区长度对此时间几乎没有影响。
    大多数文件系统为改善性能都采用某种预读技术。当检测到正进行顺序读取时,系统就试图读入比应用所要求更多数据,并假想应用很快就会读这些数据。预读的效果可以从图3-6中看出,缓冲区长度小至32字节时的时钟时间与拥有较大缓冲区长度时的始终时间几乎一样。

    10.文件共享

    UNIX系统支持在不同进程共享打开文件。在介绍dup函数之前,先要说明这种共享。为此先介绍内核用于所有I/O的数据结构。
    内核使用3中数据结构表示打开文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
    (1)每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
    a.文件描述符标志
    b.指向一个文件表项的指针。
    (2)内核为所有打开文件维持一张文件表。每个文件表项包含:
    a.文件状态标志
    b.当前文件偏移量
    c.指向该文件v节点表项的指针。
    (3)每个打开文件(或设备)都有一个v节点(v-node)结构。v节点包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件,v节点还包含了该文件的i节点(索引节点)。这些信息是在打开文件时从磁盘上读入内存的,所以,文件的所有相关信息都是随时可用的。例如,i节点包含了文件的所有者,文件长度,指向文件实际数据块在磁盘上所在位置的指针等。对于大多数文件,v节点还包含了该文件的i节点。这些信息是在打开文件时从磁盘上读入内存的,所以文件的所有相关信息都是随时可用的。
    我们忽略了那些不影响讨论的实现细节。例如打开文件描述符可存放在用户空间,而非进程表中。这些表也可以用多种方式实现,不必一定是数组,例如,可将它们实现为结构的链表。
    图3-7显示了一个进程对应的3张表之间的关系。
    在这里插入图片描述
    从UNIX系统的早期版本以来,这3张表之间的关系一直保持至今。这种关系对于在不同进程之间共享文件的方式非常重要,在以后的章节涉及其他文件共享方式时还会回到这张图上来。
    从两个独立进程各自打开了同一文件,则有图3-8中所示的关系。
    在这里插入图片描述
    我们假定第一个进程在文件描述符3上打开该文件,而另一个进程在文件描述符4上打开该文件。打开该文件的每个进程都获得各自的一个文件表项,但对一个给定的文件只有一个v节点表项。之所以每个进程都获得自己的文件表项,是因为这可以使得每个进程都有它自己的对该文件的当前偏移量。
    给出了这些数据结构后,现在对前面所述的操作进一步说明。
    ①在完成每个write后,在文件表项中,当前文件偏移量即增加所写入的字节数。如果这导致当前文件偏移量超出了当前文件长度,则将i节点表项中的当前文件长度设置为当前文件偏移量。
    ②如果用O_APPEND标志打开一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有追加写标志的文件执行写操作时,文件表项中的当前文件偏移量首先会被设置为i节点表项中的文件长度。这就是的每次写入的数据都追加到文件的当前尾端处。
    ③若一个文件用lseek定位到文件当前的尾端,则文件表项中的当前文件偏移量被设置为i节点表项。
    ④lseek函数只修改文件表项中的当前文件偏移量,不进行任何IO操作。

    11.原子操作

    (1)追加到一个文件
    假定有两个独立的进程A和B都对同一个文件进行追加写操作。每个进程都已打开了该文件,但未使用O_APPEND标志。此时,各数据结构之间的关系如图3-8中所示。每个进程都有它自己的文件表项,但是共享一个v节点表项。假定进程A和B都调用了lseek,它们将该文件当前偏移量设置未1500字节,当B调用write将B对该文件的偏移量增加到了1600,之后B未写完但CPU时间片到,让出CPU,A恢复运行,也调用write函数,那么此时A的操作就会覆盖B的内容是B的内容无效。
    问题出在两个分开的函数调用没有互斥,解决方法是使这两个函数调用对于其他进程而言成为一个原子操作。
    UNIX系统为这样的操作系统提供了一种原子操作方法,即在打开文件时设置O_APPEND标志。正如前一节中所述,这样做使得内核在每次写操作之前,都将进程的当前偏移量设置到该文件的尾端处,于是每次写之前就不再需要调用lseek。
    (2)函数pread和pwrite
    pread和pwrite允许原子性地定位并执行IO。

    #include
    ssize_t pread(int fd,void *buf,size_t nbytes,off_t offset);
    ssize_t pwrite(int fd,const void *buf,size_t nbytes,off_t offset);
    
    • 1
    • 2
    • 3

    调用pread相当于调用lseek后调用read,但是pread又与这种顺序调用有以下重要区别。
    ①调用pread时,无法中断其定位和读操作
    ②不更新当前文件偏移量

    调用pwrite相当于lseek后调用write。

  • 相关阅读:
    Web学习笔记-Vue3(环境配置、概念、整体布局设计)
    FreeRtos于嵌入式环境的应用
    APS排程软件帮机械加工企业解决交期承诺问题
    Moonbeam于Moonbase Alpha构建新式XCM对EVM跨链功能
    docker 容器之间通信
    开源的网络瑞士军刀「GitHub 热点速览」
    ElasticSearch学习(四):的增删改查、高亮、聚合、别名、重建索引
    wget 下载盯盘文件
    天梯赛 L2-052 吉利矩阵
    【Java8新特性】- Stream流
  • 原文地址:https://blog.csdn.net/weixin_43979090/article/details/126354450