美文网首页
文件和目录

文件和目录

作者: yuq329 | 来源:发表于2020-06-11 23:27 被阅读0次

文件和目录

函数stat、fstat、fstatat和lstat

  • 4个stat函数主要用于返回文件的信息结构
#include<sys/stat.h>
int stat(const char *restrict pathname, struct stat *restrict buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *restrict pathname, struct stat *restrict buf);
int fstatat(int fd, const char *restrict pathname, struct stat *restrict buf, int flag);
  • stat函数返回文件有关的信息结构
  • fstat函数获得已在描述符fd上打开文件的有关信息
  • lstat函数stat函数类似,但是在文件是一个符号链接时,返回该符号链接的有关信息,而不是由符号链接引用的文件的信息
  • fstatat函数为一个相对于当前打开目录(由fd参数指向)的路径名返回文件统计信息
    • flag参数控制是否跟随一个符号链接
      • flag设置为AT_SYMLINK_NOFOLLOW时,fstatat不会跟随符号链接,而是返回符号链接本身的信息
      • 默认情况下,,返回的是符号链接指向的实际文件的信息
      • 如果fd参数的值是AT_FDCWD,并且pathname是一个相对路径名,则会根据相对路径返回信息(cwd :current work directory
      • 如果pathname是一个绝对路径,忽略fd参数
  • macosstat定义
struct stat {
    dev_t           st_dev;         /* [XSI] ID of device containing file */
    ino_t           st_ino;         /* [XSI] File serial number */
    mode_t          st_mode;        /* [XSI] Mode of file (see below) */
    nlink_t         st_nlink;       /* [XSI] Number of hard links */
    uid_t           st_uid;         /* [XSI] User ID of the file */
    gid_t           st_gid;         /* [XSI] Group ID of the file */
    dev_t           st_rdev;        /* [XSI] Device ID */
#if !defined(_POSIX_C_SOURCE) || defined(_DARWIN_C_SOURCE)
    struct  timespec st_atimespec;  /* time of last access */
    struct  timespec st_mtimespec;  /* time of last data modification */
    struct  timespec st_ctimespec;  /* time of last status change */
#else
    time_t          st_atime;       /* [XSI] Time of last access */
    long            st_atimensec;   /* nsec of last access */
    time_t          st_mtime;       /* [XSI] Last data modification time */
    long            st_mtimensec;   /* last data modification nsec */
    time_t          st_ctime;       /* [XSI] Time of last status change */
    long            st_ctimensec;   /* nsec of last status change */
#endif
    off_t           st_size;        /* [XSI] file size, in bytes */
    blkcnt_t        st_blocks;      /* [XSI] blocks allocated for file */
    blksize_t       st_blksize;     /* [XSI] optimal blocksize for I/O */
    __uint32_t      st_flags;       /* user defined flags for file */
    __uint32_t      st_gen;         /* file generation number */
    __int32_t       st_lspare;      /* RESERVED: DO NOT USE! */
    __int64_t       st_qspare[2];   /* RESERVED: DO NOT USE! */
};

文件类型

  • 普通文件
    • 文本还是二进制数据,对于UNIX内核来说并无区别
    • 二进制可执行文件必须遵循一种标准化的格式,以便内核理解
  • 目录
    • 目录文件包含其他文件的名字以及指向与其他文件有关信息的指针
    • 任一对指定目录具有读权限的进程都可以读目录的内容,但只有内核可以写目录文件
  • 块特殊文件
    • 提供对设备(如磁盘)带缓冲的访问,每次访问以固定长度为单位进行
  • 字符特殊文件
    • 提供对设备不带缓冲的访问,每次访问长度可变
    • 系统中的设备要么是字符特殊文件,要么是块特殊文件
  • FIFO
    • 用于进程间通讯,也称为命名管道(named pipe)
  • 套接字(socket)
    • 进程间的网络通讯,也可用在一台宿主机上进程之间的非网络通信
  • 符号链接
    • 这种类型的文件指向另一个文件
#define S_ISBLK(m)      (((m) & S_IFMT) == S_IFBLK)     /* block special 块特殊文件*/
#define S_ISCHR(m)      (((m) & S_IFMT) == S_IFCHR)     /* char special 字符特殊文件*/
#define S_ISDIR(m)      (((m) & S_IFMT) == S_IFDIR)     /* directory 目录文件*/
#define S_ISFIFO(m)     (((m) & S_IFMT) == S_IFIFO)     /* fifo or socket 管道或FIFIO*/
#define S_ISREG(m)      (((m) & S_IFMT) == S_IFREG)     /* regular file 普通文件*/
#define S_ISLNK(m)      (((m) & S_IFMT) == S_IFLNK)     /* symbolic link 符号链接*/
#define S_ISSOCK(m)     (((m) & S_IFMT) == S_IFSOCK)    /* socket 套接字*/
#if !defined(_POSIX_C_SOURCE) || defined(_DARWIN_C_SOURCE)
#define S_ISWHT(m)      (((m) & S_IFMT) == S_IFWHT)     /* OBSOLETE: whiteout */
#endif
  • POSIX.1允许实现将进程间通信(IPC)对象(如消息队列和信号量等)说明为文件。这些宏与上面的不同,它们的参数不是st_mode,而是指向stat结构的指针
#define S_TYPEISMQ(buf)         (0)     /* Test for a message queue 消息队列 */
#define S_TYPEISSEM(buf)        (0)     /* Test for a semaphore 信号量*/
#define S_TYPEISSHM(buf)        (0)     /* Test for a shared memory object 共享存储对象*/
  • 示例:打印文件类型
#include <apue.h>
#include<error.h>

int main(int argc, char *argv[]) {
    int i;
    struct stat buf;
    char *ptr;
    for (i = 1; i < argc; i++) {
        printf("%s: ", argv[i]);
        if (lstat(argv[i], &buf) < 0) {
            err_ret("lstat error");
            continue;
        }
        if (S_ISREG(buf.st_mode))
            ptr = "regular";
        else if (S_ISDIR(buf.st_mode))
            ptr = "directory";
        else if (S_ISCHR(buf.st_mode))
            ptr = "charactor special";
        else if (S_ISBLK(buf.st_mode))
            ptr = "block special";
        else if (S_ISFIFO(buf.st_mode))
            ptr = "fifo";
        else if (S_ISLNK(buf.st_mode))
            ptr = "symbolic link";
        else if (S_ISSOCK(buf.st_mode))
            ptr = "socket";
        else
            ptr = "** unknown mode **";
        printf("%s\n", ptr);

    }
    exit(0);
}
  • 示例测试及输出
$ ./filedir /etc/passwd /etc /dev/tty /dev/disk4 /dev/fd
/etc/passwd: regular
/etc: symbolic link
/dev/tty: charactor special
/dev/disk4: block special
/dev/fd: directory

设置用户ID以及设置组ID

  • 与一个进程有关的ID

    关联ID 作用
    实际用户ID
    实际组ID
    我们实际上是谁
    这两个字段在登录时取自口令文件中的登录项
    有效用户ID
    有效组ID
    附属组ID
    用于文件访问权限检查
    保存的设置用户ID
    保存的设置组ID
    exec函数保存
    在执行一个程序时包含了有效用户ID有效组ID的副本
  • 每一个文件有一个所有者(st_uid)组所有者(st_gid)(在stat结构中指定)

  • st_mode中的设置用户ID位(S_ISUID)设置组ID位(S_ISGID)

    • 设置用户ID(set-user-ID):当执行此文件时,将进程的有效用户ID设置为文件所有者的用户ID
    • 设置组ID(set-group-ID):将执行此文件的进程有效组ID设置为文件的组所有者ID

文件访问权限

  • 所有类型的文件都有访问权限位

    s_mode屏蔽 含义
    S_IRUSR
    S_IWUSR
    S_IXUSR
    用户读
    用户写
    用户执行
    S_IRGRP
    S_IWGRP
    S_IXGRP
    组读
    组写
    组执行
    S_IROTH
    S_IWOTH
    S_IXOTH
    其他读
    其他写
    其他执行
  • 常见使用方式

    • 用名字打开任一类型的文件时,对该名字中包含的每一个目录,包括它可能隐含的当前工作目录,都应该具有执行权限。(目录的执行权限位常被称为搜索位
      • 例如打开/usr/include/stdio.h,需要对目录//usr/usr/include具有执行权限
      • 目录的读权限仅仅允许获得该目录中所有文件名的列表,如果不具备执行权限,则无法找到需要的文件
    • 对一个文件的读权限决定了能否打开现有文件进行读操作
    • 对一个文件的写权限决定了能否打开现有文件进行写操作
    • 为了删除一个现有文件,必须对包含该文件的目录具有写权限和执行权限,对该文件本身不需要具备读、写权限
    • exec函数执行某个文件,都必须对该文件具有执行权限,且该文件还必须是一个普通文件
  • 进程每次对文件进行操作时,内核就进行文件访问权限测试

    • 若进程的有效用户ID0(超级用户),则允许访问
    • 若进程的有效用户ID等于文件的所有者ID(进程拥有此文件),如果有适当的访问权限(例如只读权限的文件就不能写),则允许访问
    • 若进程的有效组ID进程的附属组ID之一等于文件的组ID,如果有适当访问权限,则允许访问
    • 若其他用户适当的访问权限被设置,则允许访问

新文件和目录的所有权

  • 新文件的用户ID设置位进程的有效用户ID
  • POSIX.1允许实现选择下列之一作为新文件的组ID
    • 新文件的组ID可以是进程的有效组ID
    • 新文件的组ID可以是它所在目录的组ID

函数access和faccessat

  • 内核以进程的有效用户ID有效组ID为基础进行访问权限测试,有时,进程也希望按其实际用户ID实际组ID来测试其访问能力

  • accessfaccessat函数按照实际用户ID实际组ID进行访问权限测试的

    #include<unistd.h>
    int access(const char *pathname, int mode);
    int faccessat(int fd, const char *pathname, int mode, int flag);
    
    • 如果测试文件是否已经存在,mode=F_OK,否则,mode是测试读(R_OK)、写(W_OK)、执行(X_OK)权限常量的按位或
    • pathname是绝对路径,则accessfaccessat相同
    • 如果fd=AT_FDCWD,且pathname为相对路径,则以相对路径测试访问权限
    • flag参数可以改变faccessat的行为,flag=AT_EACCESS,则访问检查用的调用进程的有效用户ID有效组ID
    #include<apue.h>
    #include<error.h>
    #include<fcntl.h>
    
    int main(int argc,char *argv[]){
        if(argc!=2)
            err_quit("usage: your_exec <pathname>");
        if (access(argv[1],R_OK)<0)
            err_ret("access error for %s",argv[1]);
        else
            printf("read access OK\n");
    
        if(open(argv[1],O_RDONLY)<0)
            err_ret("open error for %s",argv[1]);
        else
            printf("open for reading OK\n");
        exit(0);
    }
    

函数umask

  • umask函数为进程设置文件模式(mode_t)创建屏蔽字,并返回之前的值

    #include<sys/stat.h>
    mode_t umask(mode_t cmask);
    
    • cmask是若干文件访问权限位(S_IRUSRS_IWUSR等)按位“或”构成的
  • 示例

    #include<apue.h>
    #include<error.h>
    #include<fcntl.h>
    
    #define RWRWRW (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH)
    
    int main() {
        umask(0);//相当于初始化,重置之前的屏蔽字
        if (creat("foo", RWRWRW) < 0)
            err_sys("creat error for foo");
        umask(S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);//屏蔽相关权限
        if (creat("bar", RWRWRW) < 0)
            err_sys("creat error for bar");
        exit(0);
    }
    
    • 可以查看创建的文件的权限
    (base) yuqdeiMac:4_file_dir yuq$ ls -l bar foo
    -rw-------  1 yuq  staff  0 Jun 10 17:42 bar
    -rw-rw-rw-  1 yuq  staff  0 Jun 10 17:42 foo
    
  • 更改进程的文件模式创建字并不影响其父进程(通常是shell)的屏蔽字,所有shell都有内置umask命令

  • 终端中umask命令可以输出当前屏蔽字(8进制显示,umask -S以符号格式输出屏蔽字)

    $ umask
    0022
    $ umask -S
    u=rwx,g=rx,o=rx
    

函数chmod、fchmod和fchmodat

  • 3个函数主要用于更改现有文件的权限

    #include<sys/stat.h>
    int chmod(const chat *pathname, mode_t mode);
    int fchmod(int fd, mode_t mode);
    int fchmodat(int fd, const char *pathname, mode_t mode, int flag);
    
  • chmod在指定的文件上进行操作

  • fchmod对已打开的文件进行操作

  • fchmodat在两种情况下与chmod相同

    • pathname是绝对路径
    • fd取值为AT_FDCWDpathname为相对路径
    • 否则,计算相对于打开目录(fd指定)的pathname
  • flag参数改变fchmodat的行为

    • flag=AT_SYMLINK_NOFOLLOWfchmodat不会跟随符号链接
  • 为了改变一个文件的权限位

    • 进程的有效用户ID必须等于文件的所有者ID

    • 或者该进程必须拥有root权限

      mode 说明
      S_ISUID
      S_ISGID
      S_ISVTX
      执行时设置用户ID
      执行时设置组ID
      保存正文(粘着位)
      S_IRWXU
      S_IRUSR
      S_IWUSR
      S_IXUSR
      用户读、写、执行
      用户读
      用户写
      用户执行
      S_IRWXG
      S_IRGRP
      S_IWGRP
      S_IXGRP
      组读、写、执行
      组读
      组写
      组执行
      S_IRWXO
      S_IROTH
      S_IWOTH
      S_IXOTH
      其他读、写、执行
      其他读
      其他写
      其他执行
  • 示例

    #include<apue.h>
    #include<error.h>
    
    int main(){
        struct stat statbuf;
        if (stat("foo",&statbuf)<0)
            err_sys("stat error for foo");
        if (chmod("foo",(statbuf.st_mode&~S_IXGRP)|S_ISGID)<0)
            err_sys("chmod error for foo");
        if(chmod("bar",S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)<0)
            err_sys("chmod error for bar");
        exit(0);
    }
    
  • 执行后,检查文件权限

    $ ls -l foo bar
    -rw-r--r--  1 yuq  staff  0 Jun 10 17:42 bar
    -rw-rwSrw-  1 yuq  staff  0 Jun 10 17:42 foo
    
    • ls列出的时间和日期并没有改变,chmod函数更新的是i节点最近一次被更改的时间,默认情况下,ls -l列出的是最后修改文件内容的时间
  • chmod函数在下列条件下自动清除两个权限位

    • Solaris等系统对用于普通文件的粘着位(S_ISVTX)赋予了特殊含义,在这些系统上如果我们试图设置粘着位且没有root权限,那么mode中的粘着位自动被关闭。
      • 这意味着只有root用户才能设置普通文件的粘着位
    • 新创建文件的组ID可能不是调用进程所属的组。如果新文件的组ID不等于进程的有效组ID或者进程的附属组ID中的一个,而且进程没有root权限,那么设置组ID位会被自动关闭。
      • 新文件的组ID可能是父目录的组ID
      • 防止了用户创建一个设置组ID文件,而该文件是由并非该用户所属的组拥有的

粘着位

  • UNIX的早期版本中,S_ISVTX粘着位将程序正文部分(机器指令)的一个副本保存在交换区中,是的下次执行该程序时能较快的将其装载入内存
    • 原因:通常的UNIX系统中,文件的数据块很可能是随机存放的,相比较而言,交换区是被作为一个连续文件来处理的
    • 对于交换区可以同时存放的设置粘着位的文件数是有限制的,以免过多的占用交换区空间
    • 后来的UNIX版本称它为保存正文位(saved-text bit)
    • 现今较新的UNIX系统大多数都配置了虚拟存储系统以及快速文件系统,所以不再需要使用这种技术
  • Single UNIX Specification允许针对目录设置粘着位
    • 如果对一个目录设置了粘着位,则只有对该目录具有写权限的用户并且满足下列条件之一,才能删除或重命名该目录下的文件:
      • 1.拥有此文件;2.拥有此目录;3.是超级用户root

函数chown、fchown、fchownat和lchown

  • 用于更改文件的用户ID组ID

    #include<unistd.h>
    int chown(const char *pathname, uid_t owner, gid_t group);
    int fchown(int fd, uid_t owner, gid_t group);
    int fchownat(int fd, const char *pathname, uid_t owner, git_t group, int flag);
    int lchown(const char *pathname, uid_t owner, gid_t group);
    
  • 除了所引用的文件是符号链接以外,这4个函数的操作类似

  • 在符号链接情况下,lchownfchownat(设置了flag=AT_SYMLINK_NOFOLLOW标志)更改符号链接本身的所有者,而不是其指向的文件的所有者

  • fchown函数改变fd指向的打开文件的所有者,既然它在一个打开的文件上操作,那么它就不能用于改变符号链接的所有者

  • fchownat函数chown或者lchown函数在下面两种情况下是相同的

    • pathname是绝对路径
    • fd参数取值为AT_FDCWDpathname参数为相对路径
    • 在上面两种情况下,如果设置了flag=AT_SYMLINK_NOFOLLOW标志,fchownatlchown行为相同
  • _POSIX_CHOWN_RESTRICTED常量可选的定义在头文件<unistd.h>中,而且总是可以用pathconffpathconf函数进行查询。此选项还与所引用的文件有关——可在每个文件系统的基础上,使该选项起作用或不起作用

    • 1.只有超级用户才能更改一个文件的所有者
    • 2.允许任一用户更改他们所拥有的文件的所有者
  • _POSIX_CHOWN_RESTRICTED对指定的文件生效

    • 只有超级用户进程才能更改该文件的用户ID
    • 如果进程拥有该文件(有效用户ID等于该文件的用户ID),参数owner等于-1或文件的用户ID,并且参数group等于进程的有效组ID或进程的附属组ID之一,那么一个非超级用户进程可以更改该文件的组ID
  • _POSIX_CHOWN_RESTRICTED有效时,不能更改其他用户文件的用户ID。可以更改你所拥有的文件的组ID,但只能改到你所属的组

  • 如果这些函数由非超级用户进程调用,则在成功返回时,该文件的设置用户ID位设置组ID位都将被清除

文件长度

  • stat结构成员st_size表示以字节为单位的文件的长度。此字段只对普通文件、目录文件和符号链接有意义。(部分系统对管道也定义了文件长度,表示从管道中读取的字节数)
  • 对于目录,文件长度通常是一个数(如16或512)的整数倍
  • 对于符号链接,文件长度是在文件名中的实际字节数(注意,因为符号链接的文件长度总是由st_size指示,所以它并不包含通常C语言用作名字结尾的null字节
  • 大多数现代UNIX系统提供字段st_blksizest_blocks
    • st_blksize:对I/O较合适的块长度(前面曾经提到过,将st_blksize用于读操作时,读一个文件所需的时间量最少)
    • st_blocks:所分配的实际512字节(不同系统大小不一定一样)块块数
  • 文件中的空洞
    • ls -l 文件名:输出的文件长度包含了空洞
    • du -s 文件名:输出实际占用的字节块数量
    • wc -c 文件名:计算文件中的字符数(字节),正常的I/O操作读到的是整个文件的长度,包含了空洞
    • cat 文件名 > 副本文件名:复制文件,原文件中的空洞会被自动填充(填充0)

文件截断

  • 打开文件时使用O_TRUNC标志可以将一个文件长度截断为0

  • 截断文件可以调用函数truncateftruncate

    #include<unistd.h>
    int truncate(const char *pathname, off_t length);
    int ftruncate(int fd, off_t length);
    
    • 如果文件以前的长度大于length,则length以外的数据就不再能访问
    • 如果文件以前的长度小于length,则文件长度将增加,在以前的文件尾端和新的文件尾端之间的数据将读作0(也就是可能在文件中创建了一个空洞)

文件系统

  • 传统的基于BSDUNIX文件系统(UFS),以Berkeley快速文件系统为基础

  • 可以把磁盘分成一个或多个分区。每个分区可以包含一个文件系统。

  • i节点是固定长度的记录项,它包含有关文件的大部分信息

    磁盘、分区和文件系统
  • 更仔细的观察一个柱面组的i节点和数据块部分,可以看到下图所示


    较详细的柱面组的`i节点`和数据块
    • 有两个目录块指向同一个i节点。每个i节点都有一个链接计数,表明指向该i节点的目录项数。只有当链接计数为0时,才能删除该文件
      • 所以“解除对一个文件的链接”操作并不总是意味着“释放该文件占用的磁盘块”
      • 所以删除一个目录项的函数被称之为unlink而不是delete
      • stat结构中,st_nlink代表链接计数,其基本数据类型是nlink_t,这种链接类型称为硬链接
      • POSIX.1LINK_MAX常量指定一个文件链接数的最大值
    • 另外一种链接类型称为符号链接。符号链接文件的实际内容包含了该符号链接所指向的文件的名字。其i节点中的文件类型是S_IFLNK,于是系统知道这是一个符号链接
    • i节点包含了文件有关的所有信息:文件类型、文件访问权限位、文件长度和指向文件数据块的指针等。stat结构中的大部分信息取自i节点。只有两项重要数据存放在目录项中:文件名i节点编号(数据类型ino_t
    • 目录项中的i节点编号指向同一文件系统中的相应i节点,一个目录项不能指向另一个文件系统的i节点(所以硬链接ln不能跨文件系统)
    • 当在不更换文件系统的情况下,为一个文件重命名时,该文件的实际内容并未移动,只需构造一个指向现有i节点的新目录项,并删除老的目录项
  • 目录文件的链接计数


    创建了目录testdir后的文件系统实例
    • 编号2549的i节点是一个目录,链接计数为2.任何一个叶目录(不包含其他目录的目录)的链接计数总是2,一个是命名(testdir)该目录的目录项,另一个是.
    • 新建目录下自动生成的..项指向该目录的父目录,父目录的i节点链接计数加1

函数link、linkat、unlink、unlinkat和remove

  • 任何一个文件可以有多个目录项指向其i节点。创建一个指向现有文件的链接的方法是使用link函数或linkat函数

    #include<unistd.h>
    int link(const char *existingpath, const char *newpath);
    int linkat(int efd, const char *existingpath, int nfd, const char *newpath, int flag);
    
    • 创建一个新目录项newpath,引用现有文件existingpath。如果newpath已经存在,则创建失败。只创建newpath中的最后一个分量,路径中的其他部分应当已经存在(也就是前面的文件夹都存在)
    • linkat和之前类似
      • 如果任一路径existingpath或者newpath为绝对路径,则对用的文件描述符无效(efd、nfd)
      • 如果efd或nfd设置为AT_FDCWD,则根据当前目录进行计算
      • 如果路径是相对路径,根据相对于对应文件描述符进行计算
    • 当现有文件是符号链接时,有flag参数控制linkat函数是创建指向现有符号链接的链接还是创建指向现有符号链接所指向的文件的链接。
      • 如果flag=AT_SYMLINK_FOLLOW,则创建指向符号链接目标的链接
    • 创建新目录项和增加链接计数应该是一个原子操作
    • POSIX.1允许实现支持跨越文件系统的链接,但是大多数实现要求现有的和新建的两个路径名在同一个文件系统中。
    • 如果实现支持创建指向一个目录的硬链接,那么也仅限于超级用户才可以这么做。
      • 因为可能在文件系统中形成循环(想象你在test1目录下创建了test2目录,且test2目录指向test1目录,读取test1目录下的的所有文件时(包括子目录下的文件),又会通过test2读取test1,循环往复)
  • 使用unlink函数删除一个现有目录项

    #include<unistd.h>
    int unlink(const char *pathname);
    int unlinkat(int fd, const char *pathname, int flag);
    
    • 这两个函数删除目录项,并将由pathname所引用文件的链接计数减1,只有链接计数为0时,该文件的内容才可被删除,如果有进程打开了该文件,其内容也不能删除
    • 为了解除对文件的引用,必须对包含该目录项的目录具有写和执行权限
    • 如果对该目录设置了粘着位,则对该目录必须具有写权限,并且具备下面三个条件之一:
      • 1、拥有该文件;2、拥有该目录;3、具有root权限
    • 关闭一个文件时,内核首先检查打开该文件的进程个数;如果这个计数达到0,内核再去检查其链接计数;如果计数也是0,那么就删除该文件的内容
    • unlinkat
      • pathname绝对路径,忽略fd
      • pathname是相对路径,计算相对于fd代表的目录的目录名,通常fd=AT_FDCWD
      • flag=AT_REMOVEDIRunlinkat函数可以类似于rmdir一样删除目录
    #include<apue.h>
    #include<error.h>
    #include <fcntl.h>
    
    int main(){
        if(open("tempfile",O_RDWR)<0)
            err_sys("open error");
        if(unlink("tempfile")<0)
            err_sys("unlink error");
        printf("file unlinked\n");
        sleep(5);
        printf("done\n");
        exit(0);
    }
    
    • unlink的关闭特性经常被用来确保即使是在程序崩溃时,它所创建的临时文件也不会遗留下来。进程用opencreat创建一个文件,然后立即调用unlink,因为该文件仍旧是打开的,所以不会将其内容删除。只有当进程关闭该文件或终止时(此时,内核关闭进程所打开的所有文件),该文件内容才被删除(上一章pub2原子操作的实现好像可以借助unlink啊)
    • 如果pathname是符号链接,那么unlink删除该符号链接,而不是删除符号链接指向的文件;在给出符号链接的情况下,没有一个函数能删除由该符号链接所引用的文件
    • 如果文件系统支持的话,超级用户能够使用unlink删除一个目录,但是通常应该使用rmdir函数
  • remove函数解除对一个文件或目录的链接

    • 对于文件,removeunlink功能相同
    • 对于目录,removermdir功能相同

函数rename和renameat

  • 对文件或目录重命名

    #include<stdio.h>
    int rename(const char *oldname, const char *newname);
    int renameat(int oldfd, const char *oldname, int newfd, const char *newname);
    
  • 如果oldname指向的是一个文件而不是目录,则为该文件或者符号链接重命名。

    • 如果newname存在,则它不能引用一个目录
    • 如果newname存在,而且不是一个目录,则需要先删除该目录项(文件名)之后,才能正常重命名
    • 调用进程必须在两个目录项中具有合适的写权限,因为需要更改目录项
  • 如果oldname指向一个目录,则为该目录重命名。

    • 如果newname已存在,则它必须引用一个目录,且是空目录(空目录下只包含...两项)
    • 如果newname已存在,而且是空目录,则先将该目录删除,然后为oldname目录重命名
    • 重命名时,newname不能包含oldname作为其路径前缀(一个目录怎么能重命名为自己的子目录项呢?)
  • 如果oldnamenewname引用符号链接,则处理的是符号链接本身,而不是它们所指的文件

  • 不能对...重命名

  • 如果oldnamenewname引用同一文件,函数不做任何更改而成功返回

  • 如果newname已经存在,则调用进程对它需要有写权限(如删除)。调用进程需要删除oldname目录项,并可能创建newname目录项,所以要对包含这两个目录项的目录具有写和执行权限

  • 除了当newnameoldname指向相对路径时,其他情况下renameatrename函数相同

  • 如果oldname指向相对路径,则根据oldfd引用的目录来计算路径,newname类似

  • oldfd=AT_FDCWD时,根据相对于当前目录来计算相应的路径名

符号链接

  • 符号链接是对一个文件的间接指针。与硬链接不同,硬链接直接指向文件的i节点

    • 硬链接通常要求链接和文件位于同一文件系统内
    • 只有超级用户才能创建指向目录的硬链接(在底层文件系统支持的情况下)
  • 符号链接一般用于将一个文件或整个目录结构移到系统的另一个位置

  • 当使用以名字引用文件的函数时,应当注意该函数是否处理符号链接

  • 实例:使用符号链接可能在系统中引入循环

    $ mkdir foo
    $ touch foo/a
    $ ln -s ../foo foo/testdir
    $ ls -l foo
    
    • Solaris的标准函数ftw(3)以降序遍历文件结构,打印每个遇到的路径名,当处理foo目录时,将会产生ELOOP循环错误

      构成循环的符号链接testdir
    • 消除循环:unlink并不跟随符号链接,可以unlink foo/testdir,但是如果创建了一个构成循环的硬链接,则很难消除它

  • open函数打开一个符号链接时,会跟随链接到达所指定的文件。如果符号链接指向的文件不存在,则出错返回

创建和读取符号链接

  • symlinksymlinkat函数创建一个符号链接

    #include<unistd.h>
    int symlink(const char *actualpath, const char *sympath);
    int symlinkat(cosnt char *actualpath, int fd, const char *sympath);
    
    • 创建符号链接时,不要求实际文件存在,且actualpathsympath不要求在同一个文件系统内
    • symlinkatsymlink类似,只不过可以根据相对路径计算
  • 因为open函数跟随符号链接,所以需要一种方法打开该链接本身,并读该链接中的名字。readlinkreadlinkat提供了这个功能

    #include<unistd.h>
    ssize_t readlink(const char *restrict pathname, const char *restrict buf, size_t bufsize);
    ssize_t readlinkat(int fd, const char *restrict pathname, const char *restrict buf, size_t bufsize);
    

文件的时间

  • 每个文件属性所保存的实际精度依赖于文件系统的实现。

    字段 说明 例子 ls(1)选项
    st_atim 文件数据的最后访问时间 read -u
    st_mtim 文件数据的最后修改时间 write 默认
    st_ctim i节点状态的最后修改时间 chmod、chown -c

函数futimens、utimensat和utimes

  • 更改一个文件的访问和修改时间

    #include<sys/stat.h>
    int futimens(int fd, const struct timespec times[2]);
    int utimensat(int fd, const char *path, const struct timespec times[2], int flag);
    
    • 这两个函数的times数组参数的第一个元素包含访问的时间,第二个元素包含修改时间。这两个时间是日历时间,是自特定时间(1970年1月1日 00:00:00)以来所经过的秒数,不足秒的部分用纳秒表示
    • 时间戳的4种指定方式
        1. times参数是空指针,则设置为当前时间
      • times参数指向两个timespec结构的数据
          1. 任一数组元素的tv_nsec字段的值为UTIME_NOW,相应的时间戳就设置为当前时间,忽略相应的tv_sec字段
          1. 任一数组元素的tv_nsec字段的值为UTIME_OMIT,相应的时间戳就保持不变,忽略相应的tv_sec字段
          1. 任一数组元素的tv_nsec字段的值不为上面两种,相应的时间戳设置为相应的tv_sectv_nsec字段的值
    • 执行函数所要求的优先权取决于times参数的值
      • 如果times是一个空指针,或者任一tv_nsec字段的值为UTIME_NOW
        • 进程的有效用户ID必须等于文件的所有者ID
        • 进程必须对该文件具有写权限,或者进程是一个超级用户进程
      • 如果times是一个非空指针,且任一tv_nsec字段既不是UTIME_NOW,也不是UTIME_OMIT
        • 进程的有效用户ID必须等于该文件的所有者ID,或者进程必须是一个超级用户进程
        • 只对文件具有写权限是不够的
      • 如果times是一个非空指针,且两个tv_nsec字段都为UTIME_OMIT,就不执行任何权限检查(它什么都不会做,还检查什么?)
    • futimens需要打开文件来更改它的时间
    • utimensat函数提供了一种使用文件名更改文件时间的方法
    • utimensat函数的flag指定修改符号链接本身的时间还是符号链接指向的文件的链接(使用AT_SYMLINK_NOFOLLOW标志),默认跟随符号链接,并把文件的时间修改为符号链接的时间
  • utimes函数对路径名进行操作

    #include<sys/time.h>
    int utimes(const char *pathname, const struct timeval times[2]);
    
    struct timeval{
        time_t tv_sec; /* seconds */
        long tv_usec;  /* microseconds */
    }
    
    • 我们不能对状态更改时间st_ctim(i节点最近被修改的时间)指定一个值,因为调用utimes函数时,此字段会被自动更新
```c
#include <apue.h>
#include <error.h>
#include <fcntl.h>

int main(int argc, char *argv[]) {
    int i, fd;
    struct stat statbuf;
    struct timespec times[2];
    for (i = 1; i < argc; i++) {
        if (stat(argv[i], &statbuf) < 0) {
            err_ret("%s: stat error", argv[i]);
            continue;
        }
        if ((fd = open(argv[i], O_RDWR | O_TRUNC)) < 0) {
            err_ret("%s: open error", argv[i]);
            continue;
        }
        //我的系统中stat返回的time_t结构,根据需要调整
        times[0].tv_sec = statbuf.st_atime;
        times[1].tv_sec = statbuf.st_mtime;
        if (futimens(fd, times) < 0) {
            err_ret("%s: futimens error", argv[i]);
        }
        close(fd);
    }
}
```

函数mkdir、mkdirat和rmdir

  • mkdirmkdirat创建目录,用rmdir删除目录

    #include<sys/stat.h>
    int mkdir(const char *pathname, mode_t mode);
    int mkdirat(int fd, const char *pathname, mode_t mode);
    
    #include<unistd.h>
    int rmdir(const char *pathname);
    
  • 对于目录,至少要设置一个执行权限位,以允许访问该目录中的文件名

读目录

  • 对某个目录具有访问权限的任一用户都可以读该目录,但是,为了防止文件系统产生混乱,只有内核才能写目录

  • 一个目录的写权限位以及执行权限位决定了在该目录中能否创建新文件以及删除文件,但它们并不表示能否写目录本身

  • 目录的实际格式与UNIX系统实现和文件系统的设计有关

    • 早期的系统(如V7):每个目录项16字节,14字节是文件名(固定长度),2字节是i节点编号
    • 4.2BSD:允许更长的文件名,每个目录项长度可变
    • 很多实现阻止应用程序使用read函数读取目录内容
    #include<dirent.h>
    DIR *opendir(const char *pathname);
    DIR *fdopendir(int fd);
    
    struct dirent *readdir(DIR *dp);
    
    void rewinddir(DIR *dp);
    int closedir(DIR *dp);
    
    long telldir(DIR *dp);
    
    void seekdir(DIR *dp,long loc);
    
  • fdopendir函数最早出现在SUSv4中,它可以把打开文件描述符转换成目录处理函数需要的DIR结构

  • telldirseekdir函数不是基本POSIX.1标准的组成部分,是SUS中的XSI扩展,所以符合UNIX系统的实现都会提供这两个函数

  • 定义在<dirent.h>头文件中的dirent结构与实现有关。实现对此结构所做的定义至少包含下列两个成员

    ino_t d_ino; /* i-node number */
    char d_name[]; /* null-terminated filename */
    
  • 实例:遍历文件层次结构

    #include<apue.h>
    #include<error.h>
    #include<dirent.h>
    #include<limits.h>
    #include <pathalloc.h>
    
    typedef int MyFunc(const char *, const struct stat *, int);
    
    static MyFunc myfunc;
    
    static int myftw(char *, MyFunc *);
    
    static int dopath(MyFunc *);
    
    static long nreg, ndir, nblk, nchr, nfifo, nslink, nsock, ntot;
    
    int main(int argc, char *argv[]) {
        int ret;
        if (argc != 2)
            err_quit("usage: ftw <starting-pathname>");
        ret = myftw(argv[1], myfunc);
        ntot = nreg + ndir + nblk + nchr + nfifo + nslink + nsock;
        if (ntot == 0)
            ntot = 1;
        printf("regular files  = %7ld, %5.2f %%\n", nreg, nreg * 100.0 / ntot);
        printf("directories    = %7ld, %5.2f %%\n", ndir, ndir * 100.0 / ntot);
        printf("block special  = %7ld, %5.2f %%\n", nblk, nblk * 100.0 / ntot);
        printf("char special   = %7ld, %5.2f %%\n", nchr, nchr * 100.0 / ntot);
        printf("FIFOs          = %7ld, %5.2f %%\n", nfifo, nfifo * 100.0 / ntot);
        printf("symbolic links = %7ld, %5.2f %%\n", nslink, nslink * 100.0 / ntot);
        printf("sockets        = %7ld, %5.2f %%\n", nsock, nsock * 100.0 / ntot);
        exit(ret);
    }
    
    #define FTW_F 1
    #define FTW_D 2
    #define FTW_DNR 3
    #define FTW_NS 4
    
    static char *fullpath;
    static size_t pathlen;
    
    static int myftw(char *pathname, MyFunc *func) {
        fullpath = path_alloc(&pathlen);/* malloc PATH_MAX+1 bytes */
        if (pathlen <= strlen(pathname)) {
            pathlen = strlen(pathname) * 2;
            if ((fullpath = realloc(fullpath, pathlen)) == NULL)
                err_sys("realloc failed");
        }
        strcpy(fullpath, pathname);
        return dopath(func);
    }
    
    static int dopath(MyFunc *func) {
        struct stat statbuf;
        struct dirent *dirp;
        DIR *dp;
        int ret, n;
        if (lstat(fullpath, &statbuf) < 0)
            return func(fullpath, &statbuf, FTW_NS);
        if (S_ISDIR(statbuf.st_mode) == 0)
            return func(fullpath, &statbuf, FTW_F);
        if ((ret = func(fullpath, &statbuf, FTW_D)) != 0)
            return ret;
        n = strlen(fullpath);
        if (n + NAME_MAX + 2 > pathlen) {
            pathlen *= 2;
            if ((fullpath = realloc(fullpath, pathlen)) == NULL)
                err_sys("realloc failed");
        }
        fullpath[n++] = '/';
        fullpath[n] = 0;
        if ((dp = opendir(fullpath)) == NULL)
            return func(fullpath, &statbuf, FTW_DNR);
        while ((dirp = readdir(dp)) != NULL) {
            if (strcmp(dirp->d_name, ".") == 0 || strcmp(dirp->d_name, "..") == 0)
                continue;
            strcpy(&fullpath[n], dirp->d_name);
            if ((ret = dopath(func) != 0))
                break;
        }
        fullpath[n - 1] = 0;
        if (closedir(dp) < 0)
            err_ret("can't close directory %s", fullpath);
        return ret;
    }
    
    static int myfunc(const char *pathname, const struct stat *statptr, int type) {
        switch (type) {
            case FTW_F:
                switch (statptr->st_mode & S_IFMT) {
                    case S_IFREG:
                        nreg++;
                        break;
                    case S_IFBLK:
                        nblk++;
                        break;
                    case S_IFCHR:
                        nchr++;
                        break;
                    case S_IFIFO:
                        nfifo++;
                        break;
                    case S_IFSOCK:
                        nsock++;
                        break;
                    case S_IFDIR:
                        err_dump("for S_IFDIR for %s", pathname);
                }
                break;
            case FTW_D:
                ndir++;
                break;
            case FTW_DNR:
                err_ret("can't read directory %s", pathname);
                break;
            case FTW_NS:
                err_ret("stat error for %s", pathname);
                break;
            default:
                err_dump("unknown type %d for pathname %s", type, pathname);
        }
        return 0;
    }
    
    regular files  =     132, 85.16 %
    directories    =      23, 14.84 %
    block special  =       0,  0.00 %
    char special   =       0,  0.00 %
    FIFOs          =       0,  0.00 %
    symbolic links =       0,  0.00 %
    sockets        =       0,  0.00 % 
    
    • pathalloc.h:实际上应该编写cmake脚本或者makefile以动态链接库的形式使用pathalloc函数,这里为了方便直接使用头文件的方式
    • pathalloc.h代码

函数chdir、fchdir和getcwd

  • 每个进程都有一个工作目录,此路径是搜索所有相对路径名的起点

  • 当用户登录到UNIX系统时,其当前工作目录通常是口令文件(etc/passwd)中该用户登录项的第6个字段——用户的起始目录

  • 当前工作目录是进程的一个属性

  • 起始目录是登录名的一个属性

  • 进程调用chdirfchdir函数可以更改当前工作目录

    #include<unistd.h>
    int chdir(const char *pathname);
    int fchdir(int fd);
    char *getcwd(char *buf, size_t size);
    
    #include <apue.h>
    #include <error.h>
    
    int main(){
        if(chdir("/tmp")<0)
            err_sys("chdir failed");
        printf("chdir to /tmp succeeded\n");
        exit(0);
    }
    
    • chdir只能改变进程的工作目录,shell是一个单独的进程,所以为了改变shell进程自己的工作目录,shell应当直接调用chdir函数,为此,cd命令内建在shell
  • 因为内核必须维护当前工作目录的信息,所以应当能获取其当前值。但是,内核为每个进程只保存指向该目录v节点的指针等目录本身的信息,并不保存该目录的完整路径名

    • Linux内核可以确定完整路径名。完整路径名的各个组成部分分布在mount表dcache表中,然后进行重新组装,比如在读取/proc/self/cwd符号链接时
  • 我们需要一个函数,它从当前工作目录的(.)开始,用..找到其上一级目录,然后读取其目录项,直到该目录项中的i节点编号与工作目录i节点编号相同,这样就找到了其对应的文件名,按照这种方法,逐层上移,直到遇到根,这样就得到了当前工作目录完整的绝对路径名。getcwd提供了这种功能。

  • getcwd需要一个缓冲区地址buf存储绝对路径名,还有一个缓冲区大小参数,缓冲区大小必须具有足够的长度以容纳绝对路径名再加上一个终止null字节,否则返回出错

    #include <apue.h>
    #include <error.h>
    #include <pathalloc.h>
    
    int main() {
        char *ptr;
        size_t size;
    
        if (chdir("..") < 0)
            err_sys("chdir failed");
        ptr = path_alloc(&size);
        if (getcwd(ptr, size) == NULL)
            err_sys("getcwd failed");
        printf("cwd = %s\n", ptr);
        exit(0);
    }
    
    • chdir跟随符号链接
    • fchdir:先用open打开当前工作目录,保存起返回的文件描述符。当希望回到原工作空间时,只需要简单的将该文件描述符传送给fchdir

设备特殊文件

  • st_devst_rdev

    • 每个文件系统所在的存储设备都由其主、次设备号表示。
      • 设备号所用的数据类型是基本系统数据类型dev_t
      • 主设备号标识设备驱动程序,有时编码为与其通讯的外设板
      • 次设备号标识特定的子设备
    • 通常可以使用两个宏:majorminor来访问主、次设备号,大多数实现都定义这两个宏
    • 系统中与每个文件名关联的st_dev值是文件系统的设备号,该文件系统包含了这一文件名以及与其对应的i节点
    • 只有字符特殊文件块特殊文件才有st_rdev值。此值包含实际设备的设备号
    #include<apue.h>
    #include<error.h>
    #include <sys/types.h>//莫名其妙的不起作用,直接将major、 minor拷贝过来
    #define major(x)        ((int32_t)(((u_int32_t)(x) >> 24) & 0xff))
    #define minor(x)        ((int32_t)((x) & 0xffffff))
    
    int main(int argc, char *argv[]) {
        int i;
        struct stat buf;
        for (i = 1; i < argc; i++) {
            printf("%s :", argv[i]);
            if (stat(argv[i], &buf) < 0) {
                err_ret("stat error");
                continue;
            }
            printf("dev = %d/%d", major(buf.st_dev), minor(buf.st_dev));
            if (S_ISCHR(buf.st_mode) || S_ISBLK(buf.st_mode)) {
                printf(" (%s) rdev = %d/%d", (S_ISCHR(buf.st_mode)) ? "character" : "block", major(buf.st_rdev),
                       minor(buf.st_rdev));
            }
            printf("\n");
        }
        exit(0);
    }
    

文件访问权限位小结

文件访问权限小结

习题

  1. stat替换lstat函数

    1. stat总是跟随符号链接,不会显示文件类型是符号链接
    2. 访问不存在的文件会出错
  2. umask 777

    1. 屏蔽文件所有访问权限,就如chmod 777 打开文件所有权限
  3. 用户关闭自己拥有文件的用户读权限之后,将不能访问自己的文件

  4. opencreat创建已经存在的文件,将会怎么样?

    1. 已存在的文件访问权限不变
    2. 但文件被截断
  5. 目录和符号链接的长度st_size是否可以为0

    1. 普通文件的长度可以为0
    2. 目录至少包含...两项,长度不为0
    3. 符号链接的长度是其路径名包含的字符数,路径长度至少为1,长度也不为0
  6. 编写一个类似cp(1)的程序,它复制包含空洞的文件,但不将字节0写到输出文件中去。
    原文链接

    #include <stdio.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <string.h>
    #include <stdlib.h>
    #include <sys/stat.h>
    
    #define BUF_SIZE 4096
    
    int my_cp(const char *file1, const char *file2)
    {
        int fd1, fd2;
        char buffer[BUF_SIZE];
        int res, current_position = 0, byte_count =0, have_holes = 0;
        struct stat st;
    
        fd1 = open(file1, O_RDWR);
        if(-1 == fd1){
            perror("open file1 faild");
            return -1;
        }
    
        if(fstat ( fd1, &st) !=0)
            perror("fstat");
        else{
            if (S_ISREG(st.st_mode) && st.st_size > 512 * st.st_blocks) {
                have_holes = 1;
                printf("%s is a sparse-block file!\n", file1);
            } else{ 
                have_holes = 0;
                printf("%s is not a sparse-block file!\n", file1);
            }
        }
    
        fd2 = open(file2, O_RDWR | O_APPEND | O_CREAT | O_TRUNC, 0666);
        if ( -1 == fd2) {
            perror ("open file2 faild");
            return -1;
        }
    
        memset(buffer, '\0', BUF_SIZE);
        res = read(fd1, buffer, BUF_SIZE);//返回读到的字节数
        if ( -1 == res) {
            perror ("file1 read error");
            return -1;
        }
    
        if(have_holes){
            byte_count =0;
            for (current_position = 0; current_position < res; current_position ++) {
    
                if (0 != buffer[current_position] )
                {
                        buffer[byte_count] = buffer[current_position];
                        byte_count ++;
                }
            }
        }else
            byte_count = res;
    
        res = write(fd2, buffer, byte_count);
        if ( -1 == res){
            perror( " file2 write error");
            return -1;
        }
    
        close(fd1);
        close(fd2);
    }
    
    
    int main(int argc, char * argv[])
    {
        if (3 != argc){
            printf("usage error : ./a.out file1 file2\n");
            return -1;
        }
        int res = my_cp(argv[1], argv[2]);
        if ( -1 == res) {
            perror (" my_cp error");
            return -1;
        }
    
        exit(0);
    }
    
  7. 在没有更改umask的前提下,内核拷贝的文件权限与原文件权限不一致,为什么?

    1. 它们属于不同的进程,拥有的umask不是同一个
  8. df(1)命令和du(1)命令检查空闲的磁盘空间的区别

    1. du命令检查需要文件名或目录名,当unlink没有返回时,文件名没有了,但是内容还没有清除,du命令不会计算这部分内存
    2. 此时只能使用df命令查看文件系统中实际可用的空闲空间
  9. unlink函数会修改文件状态更改时间,这是怎样发生的?

    1. 如果删除的链接不是最后一个链接,此时修改文件状态更改时间
    2. 如果删除的是最后一个链接,此时文件将被物理删除,此时修改文件状态更改时间没有意义
  10. 系统对可打开文件数的限制对myftw函数有什么影响?

    1. 每次降一级就要使用另一个文件描述符,所以进程可以打开的文件数限制了我们可以遍历的文件系统树的深度。
    2. SUSXSI扩展中说明ftw允许调用者指定使用的描述符数,这隐含着可以关闭描述符并且重用它们
  11. myftw函数从不改变其目录,如果每次访问下一个遇到的目录,使用chdir修改当前工作目录到遇到的目录,这样可以使用文件名而非路径调用lstat,访问结束后在利用chdir("..")修改回工作目录,这样会更快吗?

  12. 每个进程都有一个根目录用于解析绝对路径名,可以通过chroot函数改变根目录。这个函数什么时候用?

  13. 如何只设置两个时间值中的一个来使用utimes函数

  14. 可以看一看

相关文章

  • 文件管理(一)

    目录 6.1 文件 文件概念和命名 文件类型和属性 文件存取方法 6.2 文件目录 文件控制块、文件目录与目录文件...

  • 文件和目录

    access stat fstat lstat 这些函数在python的os中也有 chmod lchmod fc...

  • 文件和目录

    文件和目录 函数stat、fstat、fstatat和lstat 4个stat函数主要用于返回文件的信息结构 st...

  • 文件和目录

    iconv -f fromEncoding -t toEncoding inputFile > outputFil...

  • 文件和目录

    Linux下的文件类型普通文件;目录文件;块特殊文件; //这种文件提供对设备带缓冲的访问 ;字符特殊文件; ...

  • Linux命令行

    文件和目录操作 复制文件目录 移动文件目录 重命名文件目录 删除文件目录 创建文件目录 查看文件内容 查看文件类型...

  • Linux的常用命令

    文件和目录 关机 重启 文件和目录 文件搜索find / -name file 从 '/' 开始搜索目标文件和目录...

  • 命令用法非完全整理

    Linux常用命令知道哪些 文件和目录rm删除文件和目录mv移动文件和目录(可用来修改文件名字)cp复制文件和目录...

  • Linux笔记2

    文件、目录操作命令 cp — 复制文件和目录mv — 移动/重命名文件和目录mkdir — 创建目录rm — 删除...

  • Linux目录和文件基本操作

    目录结构 文件和目录 从使用者的角度来介绍Linux文件系统,Linux根据文件形式将文件分为目录和普通文件,目录...

网友评论

      本文标题:文件和目录

      本文链接:https://www.haomeiwen.com/subject/imhwtktx.html