文件访问

一.文件的创建

       创建文件一般使用open/openat,这两个函数比crate更加灵活,因为这两个函数可以指定打开标志,而close以只写方式打开创建的文件。在众多打开标志中选取4个标志进行说明,O_CLOEXEC选项用于将FD_CLOEXEC置为文件描述符标志,即当执行exec函数启动某程序后是否关闭该描述符;O_SYNC 选项启用时,在该文件描述符上调用write函数会等待物理I/O完成(即将数据写入磁盘)并且更新完文件属性(比如文件大小,文件最后修改时间等);O_DSYNC选项启用时同样需要等待物理I/O完成,但不一定要等待属性更新完毕,所谓不一定是指对于那些会影响读取刚写入的数据的属性需要等待(如文件大小),而对于那些不会影响读取数据的属性则无需等待(如文件最后更新时间);O_RSYNC选项启用后,当在该文件描述符上调用read则会等待对文件同一部分挂起的写操作全部完成时才开始读取数据。根据选择最小可用描述符的原理,每次调用open之类的函数(还有socket)都会返回一个最小可用的描述符。

1.openat的优点

      该函数的第一个优点是可以更灵活的使用相对路径,(APUE中说更易于让同一进程中的不同线程在同一时间工作在不同的目录中(原因不太明白)。第二个优点是可以防止TOCTTOU攻击(该概念不太明了,书中只是提及)。

二.进程间的文件共享

1.文件结构概要

       我们要在此处说明的文件共享是当多个进程(不共享文件对象)访问同一个文件时会出现的情况。要了解多个进程同时访问文件会出现什么情况,那么我们首先需要了解LIinux下文件系统的模型。APUE中是以UNIX下的文件系统进行说明的,此处不再赘述,此文说明的是Linux下的文件系统,两者大同小异。LInux下用于描述每个进程的进程描述符(struct task_struct{})中有一个文件打开表,该表中含有一个指向文件对象的指针数组,而我们平时所说的文件描述符便是这个数组的下标,此外进程描述符中还维护了一张在执行exec()时要关闭的文件描述符的位图数组(fd_set* close_onexec)。文件对象描述了进程应该怎样于一个打开的文件进行交互,其中包含了:文件对象的引用计数(有多少个文件描述符引用该文件对象),打开文件时所指定的标志(如O_APPEND),文件指针(即文件偏移),指向文件操作表的指针(含有指向进行文件操作的函数,根据文件类型的不同会指向不同的函数)等等。文件对象中有包含了一个目录项指针(此处先不予说明),目录项对象用于描述目录文件,目录文件是由若干子目录和文件组成,目录项对象包含用于将自身链入引用相同索引节点目录链表的结构体(struct list_head d_alias),指向父目录对象的指针,子目录链表,以及一个指向索引节点(struct inode{})的指针,等的。索引节点包含了处理一个文件所需的所有信息,如:硬链接数目(当位0时删除该索引节点指向的文件),文件类型与访问权限(st_mode),文件的字节数,上次写文件的时间等等。其结构图如下:

       

2.文件共享

       当我们了解了这些之后便可以讨论进程间共享文件时会发生什么了。先考虑一个简单的例子,若我们先调用lseek函数设置偏移量,那么会改变文件对象中文件指针的位置,这也就是说每个进程可以拥有不同的文件偏移量,前提是不共享一个文件对象。之后在完成每个write函数后会将文件对象中的偏移量设置到索引节点中。接着让我们再考虑一个例子,当我们在打开文件时设置了O_APPEND选项,那么会将文件对象中的相应标志置位,并在每次写之前将索引节点中的文件大小设置到文件对象的文件指针中,之后完成写入,且设置文件指针与写入(包括设置索引节点中的文件长度)是原子的(二者不会被拆为两步)。当有多个进程以追加方式打开同一个文件时,那么来自每个进程的数据都可以正确地写道文件中。

      为了防止用户在调用lseek与write两个函数时,在二者调用之间被打断,比如一个进程lseek到末尾1000字节处,此时另一个进程已将600字节的数据追加到末尾,那么这600字节的数据有一部分或全部会被覆盖。因此提供了两个原子的偏移写和偏移读函数pread和pwrite。

3.文件描述符的复制

      我们可以使用dup与dup2函数复制一个文件描述符,新的描述符将与被复制的描述符指向同一文件对象,但新描述符的执行时关闭标志将总会被清除,如3.1中所述,进程描述符中维护了一张执行时关闭描述符位图,因为只是改变这张位图,而非改变文件对象(该标志应该不记录在文件对象中),所以不会影响其它文件描述符。对于dup2有一个特殊点就是,若指定的新描述符已经被打开,则会先关闭。

     还可以使用fcntl函数来复制一个文件描述符,dup(fd) == fcntl(fd, FD_DUPFD, 0),但是dup2(fd,fd2) != close(fd2) + fcntl(放到, FD_DUPFD, fd2),因为两条语句并非原子操作,而dup2是一个原子操作。

【注】:用两个open打开的是两个文件对象。

4.文件锁

    参考博文《高级I/O》

三.延迟写

       延迟写是当我们向文件写入数据时,内核通常先将这些数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘。当内核需要重用缓冲区来存放其它磁盘数据或者当内核周期性的调用sync进行刷新时便会把数据写入磁盘(放入磁盘的写队列后便返回)。fsync(fd)会等待写磁盘结束并更新完文件属性后才返回。而fdatasync与fsync类似,只是无需等待文件属性的更新。

四.文件访问权限

1.所有者(组)ID与实际用户(组)ID

      文件的所有者ID与所有组ID即文件的所有者与所有组,其决定了用户访问该文件的权限,这两个ID是存储在索引节点中的,分别为uid_t i_uid和 gid_t i_gid,可以通过fstat之类的函数获取,会通过struct stat中的st_uid和st_gid返回。进程的有效用户ID与有效组ID一般为为执行用户的用户ID和组ID,但有时会被提升为执行文件的所有者(组)ID(在后面进行说明),这两个ID被存储在文件对象中。当要访问文件时会将文件对象中存储的有效用户(组)ID与所有者(组)ID进行比较,若不符合要求则无访问权限。

2.文件模式字st_mode(文件类型与访问权限)

1)文件类型位

       st_mode中保存了文件类型以及文件的访问权限(9个访问权限位),以及两个用于执行文件的标志位。st_mode有几位用于标识文件类型,可以用相应的宏进行读取,具体操作方法可以参考APUE p76。

2)文件执行时具有的权限位

      用于执行文件的两个标志位用于判断是否要在执行此文件时,将进程的有效用户ID设置为文件的所有者ID(进程的有效用户ID原先为实际用户ID,即执行该进程的用户的用户ID),或是将有效组ID设置为文件所有组ID(进程的有效组ID原先为实际组ID,即执行该进程的用户的组ID),我们称之为设置用户ID和设置组ID。当文件所有者是超级用户,并且设置了该文件的模式字为设置用户ID,那么当出现文件由一个出现执行时,该进程具有超级用户权限,而无需在意执行此文件的实际用户ID是什么。即,进程的有效用户(组)ID不一定是实际用户(组)ID,也可能被设置为某文件的所有用户(组)ID。

     下面举一个例子,我们先以root权限创建了一个文本文件root.txt(其中存有文本"root"),之后我们又编写了一个以读写方式打开文件的应用TestFileUserID(该文件的所有者为非超级用户),该应用文件的代码如下:

#include <fcntl.h>
#include <iostream>
#include <unistd.h>

int main(int argc, char *argv[])
{
    int fileFd = open(argv[1],O_RDWR);
    if(fileFd < 0) {
        std::cout<<"open err:"<<errno<<std::endl;
        exit(1);
    }
    char buf[1000];
    if(read(fileFd, buf, sizeof(buf)) < 0) {
        std::cout<<"read err:"<<errno<<std::endl;
        exit(1);
    }
    std::cout<<buf;

    return 0;
}

   接着我们先以普通用户的身份,用该应用文件代开root.txt,此时会返回"open err:13"EACCES,即权限不够。接着我们使用chown命令将该应用文件的所有者更改为超级用户(组),并使用chmod命令将设置用户(组)ID位打开,之后我切换回普通用户,并再次使用该应用代开root.txt文件,将回显示打开成功。因为会将执行进程的有效ID设置为文件的所有者ID,此时该进程便拥有了超级用户权限,即使其实际ID的权限不够。操作步骤如下:

                            

3) 文件访问权限位

        st_mode中有9位用于标识文件的9种访问权限,这9种访问权限如下图所示:

      所有类型的文件都具有访问权限(包括目录,字符特别文件(不太清除)),不同的权限提供了不同的对文件的操作能力。大致有以下几条:

  • 当我打开一个路径时,对路径种的每一个目录都必须具有执行权限,对目录而言执行权限也可以理解为搜索权限,即可以允许我们寻找特定的一个文件名。说到这应该与目录的读权限进行区别,读权限是允许我们读取该目录中所有文件名的列表,而不是搜索。对于目录文件的权限特别要注意隐含目录的权限,比如若当前目录为/usr/include/,那么为了打开文件stdio.h则必须对当前目录有执行权限。之后当操作文件时需要有对文件本身适当的权限。
  • 文件的读权限位与写权限位决定了我们能否打开文件进行读写.
  • 为了在open函数中对一个文件指定O_TRUNC标志,必须对文件具有写权限。
  • 为了删除一个文件必须对包含该文件的目录(即前一级目录)具有写权限和执行权限,但对该文件本身无需具有读写权限
  • 为了在一个目录中创建一个文件必须,必须要对该目录具有写权限和执行权限。
  • 若要调用exec执行某个文件,必须对该文件具有执行权限,且该文件必须是一个普通文件(可执行文件属于普通文件)。

4)新文件和目录的所有权及访问权限

      新文件的所有者ID即创建进程的有效用户ID,而新文件的所有组ID则根据其所在目录的st_mode中是否设置了执行时设置用户(组)ID标志,若设置了则新文件的组ID设置为所在目录的组ID,否则设置为进程的有效组ID。如果新文件的组ID不等于进程的有效组ID,且进程没有超级用户权限,那么设置组ID位会被自动关闭。且在Linux中如果没有超级用户权限的进程写一个文件,则设置用户ID位和设置组ID位会被自动清除(不太明白)。

      umask函数可以创建文件模式屏蔽字(由访问权限按或组成),当创建一个新文件或目录时,就一定会使用该文件模式屏蔽字来设置文件的访问权限。

5)chmod命令与chmod函数

       chmod命令与chmod函数都用于改变文件的访问权限,但必须是文件的拥有者或是root用户,即进程的有效用户ID等于文件的所有用户ID或者是进程的实际ID为0(root)。chmod -u [+ | -]  [ r | w | e | s] 改变的是用户权限的读写或执行权限以及设置用户ID位标志,依次类推还可以改变组权限的读写执行,其他权限的读写执行权限。chmod函数也是一样的效果。

6)测试进程的访问权限

       可以使用access函数测试进程的实际用户ID和实际组ID对文件的访问权限,faccessat函数还可以测试有效用户ID和有效组ID对文件的访问权限。

7)更改文件的所有用户ID与所有组ID

        可以通告chown命令或chown函数改变一个文件的所有者,但只有超级用户才可以改变文件的所有者。可参考2)中的操作。

【注】:所有xxxat的函数当将参数flag设置为AT_SYMLINK_NOFOLLOW时都是对符号链接本身进行操作,而非符号链接所指向的文件

8)文件空洞

      文件空洞由所设置的偏移超过了文件尾端,且写入了数据后造成的。文件空洞部分将被读位0即‘\0’.一般文件空洞不要求在磁盘上占用存储区。【思考】:是不是可以利用文件空洞来存储'\0'较多的文件

9)文件截断 ftruncate

        可以截断文件尾部,留下长度位length的文件,若是文件长度不足length,则在尾部添加空洞(0),因此也可用于设置文件长度。

        比如可用于以下情况:一个由一些记录组成的文件,用删除标记了一些不用的记录(没有真正删除),然后文件太大需要压缩整理文件,就把后面有用的记录挪到前面空的位置(标记为删除的记录位置),最后截断文件。

链接于符号链接待补叙

猜你喜欢

转载自blog.csdn.net/qq_34228327/article/details/84501110