Linux系统编程(三):文件和目录(下)

参考引用

  • UNIX 环境高级编程 (第3版)
  • 嵌入式Linux C应用编程-正点原子

1. Linux 系统中的文件类型

  • 在 Windows 系统下,操作系统识别文件类型一般是通过文件名后缀来判断,在 Windows 操作系统下打开文件,首先会识别文件名后缀得到该文件的类型,然后再调用相应的程序去打开它,如 .zip 文件,会使用解压软件去打开
  • Linux 下一切皆文件,并不会通过文件后缀名来识别一个文件的类型,Linux 系统下一共分为 7 种文件类型

1.1 普通文件

  • 普通文件(regular file)也就是一般意义上的文件

    • 普通文件中的数据存在系统磁盘中,可以访问文件中的内容
    • 文件中的内容以字节为单位进行存储与访问
  • 普通文件可以分为两大类:文本文件和二进制文件

    • 文本文件
      • 文件中的内容是由文本构成的,所谓文本指的是 ASCII 码字符
      • 文件中的内容其本质上都是数字(因为计算机本身只有 0 和 1,存储在磁盘上的文件内容也都是由 0 和 1 所构成),而文本文件中的数字应该被理解为这个数字所对应的 ASCII 字符码
      • 如常见的 .c、.h、.sh、.txt 等这些都是文本文件,文本文件的好处就是方便人阅读、浏览以及编写
    • 二进制文件
      • 二进制文件中存储的本质上也是数字,只不过对于二进制文件来说,这些数字并不是文本字符编码,而是真正的数字
      • 如 Linux 系统下的可执行文件、C 代码编译之后得到的 .o 文件、.bin 文件等都是二进制文件
  • 在 Linux 系统下,可以通过 stat 命令或者 ls -l 命令来查看文件类型

    $ stat test.c    # 会直观把文件类型显示出来
    $ ls -l test.c     
    # 第一个字符('-')就用于表示文件的类型
        # '-':普通文件
        # 'd':目录文件
        # 'c':字符设备文件
        # 'b':块设备文件
        # 'l':符号链接文件
        # 's':套接字文件
        # 'p':管道文件
    

1.2 目录文件

  • 目录(directory)就是文件夹,文件夹在 Linux 系统中是一种特殊文件,可使用 vi 编辑器打开文件夹
    • 文件夹中记录了该文件夹缺省的路径以及该文件夹下所存放的文件
    • 文件夹作为一种特殊文件,并不适合使用文件 I/O 的方式来读写,在 Linux 系统下,会有一些专门的系统调用用于读写文件夹
    $ vi tcp_socket
    " ============================================================================
    " Netrw Directory Listing                                        (netrw v156)
    "   /home/yxd/tcp_socket
    "   Sorted by      name
    "   Sort sequence: [\/]$,\,\.h$,\.c$,\.cpp$,\~\=\*$,*,\.o$,\
    "   Quick Help: :help  -:go up dir  D:delete  R:rename  s:sort-by  x:special
    " ==============================================================================
    ../                                                                             
    ./
    client.c
    server.c
    client*
    server*
    

1.3 字符设备文件和块设备文件

  • 设备文件(字符设备文件、块设备文件)对应的是硬件设备,在 Linux 系统中,硬件设备会对应到一个设备文件,应用程序通过对设备文件的读写来操控、使用硬件设备,如:LCD 显示屏、串口、音频、按键等
  • 设备文件并不对应磁盘上的一个文件,也就是说设备文件并不存在于磁盘中,而是由文件系统虚拟出来的,一般是由内存来维护,当系统关机时,设备文件都会消失
  • 字符设备文件一般存放在 Linux 系统 /dev/ 目录下,所以 /dev 也称为虚拟文件系统 devfs

1.4 符号链接文件

  • 符号链接文件(link)类似于 Windows 系统中的快捷方式文件,是一种特殊文件,它的内容指向的是另一个文件路径,当对符号链接文件进行操作时,系统根据情况会对这个操作转移到它指向的文件上去,而不是对它本身进行操作
    • 如:读取一个符号链接文件内容时,实际上读到的是它指向的文件的内容
    • 符号链接文件的后面箭头所指向的文件路径便是符号链接文件所指向的文件

1.5 管道文件

  • 管道文件(pipe)主要用于进程间通信

1.6 套接字文件

  • 套接字文件(socket)也是一种进程间通信的方式,与管道文件不同的是,它们可以在不同主机上的进程间通信,实际上就是网络通信

2. stat 函数

  • Linux 下可以使用 stat 命令查看文件的属性,其实这个命令内部就是通过调用 stat() 函数来获取文件属性的,stat 函数是 Linux 中的系统调用,用于获取文件相关的信息
    #include 
    #include 
    #include 
    
    // pathname:用于指定一个需要查看属性的文件路径。
    // buf:调用 stat 函数的时候需要传入一个 struct stat 变量的指针,获取到的文件属性信息就记录在 struct stat 结构体中
    // 返回值:成功返回 0;失败返回 -1,并设置 error
    int stat(const char *pathname, struct stat *buf);
    

2.1 struct stat 结构体

  • struct stat 是内核定义的一个结构体,在 头文件中申明,所以可以在应用层使用,这个结构体中的所有元素加起来构成了文件的属性信息
    struct stat {
        dev_t st_dev;             /* 文件所在设备的 ID,用于描述此文件所在的设备,不常用 */
        ino_t st_ino;             /* 文件对应 inode 节点编号 */
        mode_t st_mode;           /* 文件对应的模式,如文件类型、文件权限都记录在该变量中 */
        nlink_t st_nlink;         /* 文件的链接数 */
        uid_t st_uid;             /* 文件所有者的用户 ID */
        gid_t st_gid;             /* 文件所有者的组 ID */
        dev_t st_rdev;            /* 设备号(只针对设备文件) */
        off_t st_size;            /* 文件大小(以字节为单位) */
        blksize_t st_blksize;     /* 文件内容存储的块大小 */
        blkcnt_t st_blocks;       /* 文件内容所占块数 */
        struct timespec st_atim;  /* 文件最后被访问的时间 */
        struct timespec st_mtim;  /* 文件内容最后被修改的时间 */
        struct timespec st_ctim;  /* 文件状态最后被改变的时间 */
    };
    

2.2 st_mode 变量

  • st_mode 是 struct stat 结构体中的一个成员变量,是一个 32 位无符号整型数据,该变量记录了文件的类型、文件的权限这些信息,其表示方法如下所示

    • O 对应的 3 个 bit 位用于描述其它用户的权限
    • G 对应的 3 个 bit 位用于描述同组用户的权限
    • U 对应的 3 个 bit 位用于描述文件所有者的权限
    • S 对应的 3 个 bit 位用于描述文件的特殊权限
      Linux系统编程(三):文件和目录(下)_第1张图片
  • 表示权限的宏定义

    S_IRWXU     00700     owner has read, write, and execute permission
    S_IRUSR     00400     owner has read permission
    S_IWUSR     00200     owner has write permission
    S_IXUSR     00100     owner has execute permission
    S_IRWXG     00070     group has read, write, and execute permission
    S_IRGRP     00040     group has read permission
    S_IWGRP     00020     group has write permission
    S_IXGRP     00010     group has execute permission
    S_IRWXO     00007     others (not in group) have read, write, and execute permission
    S_IROTH     00004     others have read permission
    S_IWOTH     00002     others have write permission
    S_IXOTH     00001     others have execute permission
    
  • “文件类型” 这 4 个 bit 位用于描述该文件的类型,如:该文件是普通文件、还是链接文件、亦或者是一个目录等

    • 注意下面这些数字使用的是八进制方式来表示的,C 语言中八进制方式表示一个数字需要在数字前面添加一个 0
    S_IFSOCK     0140000     socket(套接字文件)
    S_IFLNK      0120000     symbolic link(链接文件)
    S_IFREG      0100000     regular file(普通文件)
    S_IFBLK      0060000     block device(块设备文件)
    S_IFDIR      0040000     directory(目录)
    S_IFCHR      0020000     character device(字符设备文件)
    S_IFIFO      0010000     FIFO(管道文件)
    
    // S_IFMT 宏是文件类型字段位掩码
    S_IFMT       0170000
    
  • 通过 st_mode 变量判断文件类型(假设 st 是 struct stat 类型变量)

    /* 判断是不是普通文件 */
    if ((st.st_mode & S_IFMT) == S_IFREG) {
        /* 是 */
    }
    
    /* 判断是不是链接文件 */
    if ((st.st_mode & S_IFMT) == S_IFLNK) {
        /* 是 */
    }
    
  • 还可以使用 Linux 系统封装好的宏来判断文件类型(m 是 st_mode 变量)

    S_ISREG(m)     # 判断是不是普通文件,如果是返回 true,否则返回 false
    S_ISDIR(m)     # 判断是不是目录,如果是返回 true,否则返回 false
    S_ISCHR(m)     # 判断是不是字符设备文件,如果是返回 true,否则返回 false
    S_ISBLK(m)     # 判断是不是块设备文件,如果是返回 true,否则返回 false
    S_ISFIFO(m)    # 判断是不是管道文件,如果是返回 true,否则返回 false
    S_ISLNK(m)     # 判断是不是链接文件,如果是返回 true,否则返回 false
    S_ISSOCK(m)    # 判断是不是套接字文件,如果是返回 true,否则返回 false
    
    /* 判断是不是普通文件 */
    if (S_ISREG(st.st_mode)) {
        /* 是 */
    }
    
    /* 判断是不是目录 */
    if (S_ISDIR(st.st_mode)) {
        /* 是 */
    }
    

2.3 struct timespec 结构体

  • 该结构体定义在 头文件中,是 Linux 系统中时间相关的结构体
    • time_t 时间指的是一个时间段,从某一个时间点到某一个时间点所经过的秒数,称为日历时间
    • struct stat 结构体中包含了三个文件相关的时间属性:st_atim、st_mtim、st_ctim,此三个字段分别用于记录文件最后被访问的时间、文件内容最后被修改的时间以及文件状态最后被改变的时间,都是 struct timespec 类型变量
    struct timespec {
        // time_t 其实是指 long int 类型
        time_t tv_sec;            /* 秒 */
        syscall_slong_t tv_nsec;  /* 纳秒 */
    };
    

2.4 练习

  • 1、获取文件的 inode 节点编号以及文件大小,并将它们打印出来

    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main(void) {
        struct stat file_stat;
        int ret;
    
        /* 获取文件属性 */
        ret = stat("./test_file", &file_stat);
        if (ret == -1) {
            perror("stat error");
            exit(-1);
        }
    
        /* 打印文件大小和 inode 编号 */
        printf("file size: %ld bytes\ninode number: %ld\n", file_stat.st_size, file_stat.st_ino);
    
        exit(0);
    }
    
    $ ls -li test_file 
    2883653 -rw-rw-r-- 1 yue yue 13 12月 20 21:37 test_file
    
    $ gcc demo01.c -o demo01
    $ ./demo01 
    file size: 13 bytes
    inode number: 2883653
    
  • 2、获取文件的类型,判断此文件对于其它用户(Other)是否具有可读可写权限

    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main(void) {
        struct stat file_stat;
        int ret;
    
        /* 获取文件属性 */
        ret = stat("./test_file", &file_stat);
        if (ret == -1) {
            perror("stat error");
            exit(-1);
        }
    
        /* 判读文件类型 */
        switch (file_stat.st_mode & S_IFMT) {
            case S_IFSOCK: printf("socket"); break;
            case S_IFLNK: printf("symbolic link"); break;
            case S_IFREG: printf("regular file"); break;
            case S_IFBLK: printf("block device"); break;
            case S_IFDIR: printf("directory"); break;
            case S_IFCHR: printf("character device"); break;
            case S_IFIFO: printf("FIFO"); break;
        }
        printf("\n");
        /* 判断该文件对其它用户是否具有读权限 */
        if (file_stat.st_mode & S_IROTH)
            printf("Read: Yes\n");
        else
            printf("Read: No\n");
        /* 判断该文件对其它用户是否具有写权限 */
        if (file_stat.st_mode & S_IWOTH)
            printf("Write: Yes\n");
        else
            printf("Write: No\n");
    
        exit(0);
    }
    
    $ gcc demo02.c -o demo02
    $ ./demo02
    regular file
    Read: Yes
    Write: No
    
  • 3、获取文件的时间属性,包括文件最后被访问的时间、文件内容最后被修改的时间以及文件状态最后被改变的时间,并使用字符串形式将其打印出来,包括时间和日期、表示形式自定

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main(void) {
        struct stat file_stat;
        struct tm file_tm;
        char time_str[100];
        int ret;
    
        /* 获取文件属性 */
        ret = stat("./test_file", &file_stat);
        if (-1 == ret) {
            perror("stat error");
            exit(-1);
        }
    
        /* 打印文件最后被访问的时间 */
        localtime_r(&file_stat.st_atim.tv_sec, &file_tm);
        strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", &file_tm);
        printf("time of last access: %s\n", time_str);
    
        /* 打印文件内容最后被修改的时间 */
        localtime_r(&file_stat.st_mtim.tv_sec, &file_tm);
        strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", &file_tm);
        printf("time of last modification: %s\n", time_str);
    
        /* 打印文件状态最后改变的时间 */
        localtime_r(&file_stat.st_ctim.tv_sec, &file_tm);
        strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", &file_tm);
        printf("time of last status change: %s\n", time_str);
        exit(0);
    }
    
    $ gcc demo03.c -o demo03
    $ ./demo03
    time of last access: 2023-12-20 21:37:14
    time of last modification: 2023-12-20 21:37:14
    time of last status change: 2023-12-20 21:37:14
    

3. fstat 和 lstat 函数

3.1 fstat 函数

  • fstat 与 stat 区别在于,stat 是从文件名出发得到文件属性信息,不需要先打开文件,而 fstat 函数则是从文件描述符出发得到文件属性信息,所以使用 fstat 函数之前需要先打开文件得到文件描述符
    • 并不想通过打开文件来得到文件属性信息,那么就使用 stat,如果文件已经打开了,那么就使用 fstat
    #include 
    #include 
    #include 
    
    int fstat(int fd, struct stat *buf);
    
  • 示例
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main(void) {
        struct stat file_stat;
        int fd;
        int ret;
    
        /* 打开文件 */
        fd = open("./test_file", O_RDONLY);
        if (-1 == fd) {
            perror("open error");
            exit(-1);
        }
    
        /* 获取文件属性 */
        ret = fstat(fd, &file_stat);
        if (-1 == ret)
            perror("fstat error");    
            close(fd);
    
        exit(ret);
    }
    

3.2 lstat 函数

  • lstat 与 stat、fstat 的区别在于,对于符号链接文件,stat、fstat 查阅的是符号链接文件所指向的文件对应的文件属性信息,而 lstat 查阅的是符号链接文件本身的属性信息
    #include 
    #include 
    #include 
    
    int lstat(const char *pathname, struct stat *buf);
    

4. 文件属主

  • 文件所有者表示该文件属于“谁”,也就是属于哪个用户
    • 一般来说文件在创建时,其所有者就是创建该文件的那个用户
    • 如:当前登录用户为 dt,使用 touch 命令创建了一个文件,那么这个文件的所有者就是 dt
    • 同理,在程序中调用 open 函数创建新文件时也是如此,执行该程序的用户是谁,其文件所有者便是谁
  • 文件所属组则表示该文件属于哪一个用户组
    • 在 Linux 中,系统并不是通过用户名或用户组名来识别不同的用户和用户组,而是通过 ID
    • ID 就是一个编号,Linux 系统会为每一个用户或用户组分配一个 ID,将用户名或用户组名与对应的 ID 关联起来,所以系统通过用户 ID(UID)或组 ID(GID)就可识别出不同的用户和用户组
  • 文件的用户 ID 和组 ID 分别由 struct stat 结构体中的 st_uid 和 st_gid 所指定。既然 Linux 下的每一个文件都有与之相关联的用户 ID 和组 ID,那么对于一个进程来说亦是如此,与一个进程相关联的 ID 有 5 个或更多

Linux系统编程(三):文件和目录(下)_第2张图片

4.1 有效用户 ID 和有效组 ID

  • 对于有效用户 ID 和有效组 ID 来说,这是进程所持有的概念,对于文件来说,并无此属性,有效用户 ID 和有效组 ID 是用于给操作系统判断当前执行该进程的用户在当前环境下对某个文件是否拥有相应的权限
  • 在 Linux 系统中,当进程对文件进行读写操作时,系统首先会判断该进程是否具有对该文件的读写权限,通过该文件的权限位来判断(struct stat 结构体中的 st_mode 字段)
    • 当进行权限检查时,并不是通过进程的实际用户和实际组来参与权限检查的,而是通过有效用户和有效组来参与文件权限检查
    • 通常,绝大部分情况下,进程的有效用户等于实际用户(有效用户 ID 等于实际用户 ID),有效组等于实际组(有效组 ID 等于实际组 ID)

4.2 chown 函数

  • chown 是一个系统调用,该系统调用可用于改变文件的所有者(用户 ID)和所属组(组 ID)

    #include 
    
    // pathname:用于指定一个需要修改所有者和所属组的文件路径
    // owner:将文件的所有者修改为该参数指定的用户(以用户 ID 的形式描述)
    // group:将文件的所属组修改为该参数指定的用户组(以用户组 ID 的形式描述)
    int chown(const char *pathname, uid_t owner, gid_t group);
    
  • 该函数有以下两个限制条件

    • 只有超级用户进程能更改文件的用户 ID
    • 普通用户进程可以将文件的组 ID 修改为其所从属的任意附属组 ID,前提条件是该进程的有效用户 ID 等于文件的用户 ID,而超级用户进程可以将文件的组 ID 修改为任意值
  • 示例

    #include 
    #include 
    #include 
    
    int main(void) {
        // 将 test_file 文件的用户 ID 和用户组 ID 修改为 0、0。0 指的就是 root 用户和 root 用户组
        if (chown("./test_file", 0, 0) == -1) {
            perror("chown error");
            exit(-1);
        }
    
        exit(0);
    }
    
    $ gcc demo
    $ sudo ./a.out  # 必须使用 sudo 超级用户权限才可执行
    File: test_file
      Size: 13        	Blocks: 8          IO Block: 4096   regular file
    Device: 801h/2049d	Inode: 2246591     Links: 1
    Access: (0664/-rw-rw-r--)  Uid: (    0/    root)   Gid: (    0/    root)
    Access: 2023-12-21 20:16:57.033979115 +0800
    Modify: 2023-12-21 20:16:21.528235441 +0800
    Change: 2023-12-21 20:16:57.033979115 +0800
     Birth: -
    
  • 在 Linux 系统下,可以使用 getuid 和 getgid 两个系统调用分别用于获取当前进程的用户 ID 和用户组 ID,这里说的进程的用户 ID 和用户组 ID 指的就是进程的实际用户 ID 和实际组 ID

    #include 
    #include 
    
    uid_t getuid(void);
    gid_t getgid(void);
    
    // 使用示例
    printf("uid: %d\n", getuid());
    

4.3 fchown 和 lchown 函数

  • 这两个同样也是系统调用,作用与 chown 函数相同,只是参数、细节方面有些许不同。fchown()、lchown() 这两个函数与 chown() 的区别就像是 fstat()、lstat() 与 stat 的区别

5. 文件访问权限

  • 所有文件类型(目录、设备文件)都有访问权限,并非只有普通文件才有访问权限

5.1 普通权限和特殊权限

  • 文件的权限可以分为两个大类,分别是普通权限和特殊权限(也可称为附加权限)
    • 普通权限包括对文件的读、写以及执行
    • 特殊权限则包括一些对文件的附加权限
5.1.1 普通权限
  • 每个文件都有 9 个普通的访问权限位,可将它们分为 3 类
    Linux系统编程(三):文件和目录(下)_第3张图片

  • 下述 ‘-rwxrwxr-x’ 就描述了该文件的 9 个访问权限以及文件类型

    • 首个字符 “-” 表示该文件是一个普通文件
    • r 表示具有读权限,w 表示具有写权限,x 表示具有执行权限,- 表示无此权限
    $ ls -l
    total 20
    -rwxrwxr-x 1 yxd  yxd  8384 12月 21 20:16 a.out
    -rw-rw-r-- 1 yxd  yxd   287 12月 21 20:15 demo.c
    -rw-rw-r-- 1 root root   13 12月 21 20:16 test_file
    

Linux系统编程(三):文件和目录(下)_第4张图片

  • 对于进程来说,参与文件权限检查的是进程的有效用户、有效用户组以及附属组用户
    • 文件所有者:有效用户 ID = 文件所有者 ID
    • 文件所属组成员:有效用户组 ID 或 附属组 ID = 文件组 ID
    • 其它用户:以上两点均不满足
    • root 用户:有效用户 ID = 0
5.1.2 特殊权限
  • st_mode 字段中的 S 字段权限位,S 字段三个 bit 位中,从高位到低位依次表示文件的 set-user-ID 位权限、set-group-ID 位权限以及 sticky 位权限
    • 当进程对文件进行操作的时候将进行权限检查,如果文件的 set-user-ID 位权限被设置,内核会将进程的有效 ID 设置为该文件的用户 ID(文件所有者 ID),意味着该进程直接获取了文件所有者的权限并以文件所有者的身份操作该文件
    • 当进程对文件进行操作的时候将进行权限检查,如果文件的 set-group-ID 位权限被设置,内核会将进程的有效用户组 ID 设置为该文件的用户组 ID(文件所属组 ID),意味着该进程直接获取了文件所属组成员的权限并以文件所属组成员的身份操作该文件
    // 以下数字使用的是八进制方式表示
    // 对应的 bit 位数字为 1,则表示设置了该权限、为 0 则表示并未设置该权限
    S_ISUID     04000     set-user-ID bit
    S_ISGID     02000     set-group-ID bit
    S_ISVTX     01000     sticky bit
    

Linux 系统下绝大部分的文件都没有设置 set-user-ID 位权限和 set-group-ID 位权限,通常情况下,进程的有效用户 = 实际用户(有效用户 ID = 实际用户 ID),有效组 = 实际组(有效组 ID = 实际组 ID)

5.2 目录权限

  • 删除文件、创建文件这些操作也是需要相应权限的,那这些权限又是从哪里获取的呢?
    • 目录。目录(文件夹)在 Linux 系统下也是一种文件,拥有与普通文件相同的权限方案(S/U/G/O)
      • 目录的读权限:可列出(如:通过 ls 命令)目录之下的内容(即目录下有哪些文件)
      • 目录的写权限:可以在目录下创建文件、删除文件
      • 目录的执行权限:可访问目录下的文件,如:对目录下的文件进行读、写、执行等操作
  • 只拥有读权限,无法访问目录下的文件,要想访问目录下的文件,如:查看文件的 inode 节点、大小、权限等信息,还需要对目录拥有执行权限
  • 若拥有对目录的执行权限、而无读权限,只要知道目录内文件的名称,仍可对其进行访问,但不能列出目录下的内容(即目录下包含的其它文件的名称)
  • 要想在目录下创建文件或删除原有文件,需要同时拥有对该目录的执行和写权限
  • 如果需要对文件进行读、写或执行等操作,不光是需要拥有该文件本身的读、写或执行权限,还需要拥有文件所在目录的执行权限

5.3 检查文件权限 access

  • 程序当中对文件进行相关操作之前,可以使用 access 系统调用检查执行进程的用户是否对该文件拥有相应权限
    #include 
    
    // pathname:需要进行权限检查的文件路径
    // mode:该参数可以取以下值(除了单独使用外,还可通过按位或运算符 "|" 组合)
        // F_OK:检查文件是否存在
        // R_OK:检查是否拥有读权限
        // W_OK:检查是否拥有写权限
        // X_OK:检查是否拥有执行权限
    int access(const char *pathname, int mode);
    

5.4 修改文件权限 chmod

  • 在 Linux 系统下,可以使用 chmod 命令修改文件权限,该命令内部实现方法其实是调用了 chmod 函数,chmod 函数是一个系统调用

    • 要想更改文件权限,要么是超级用户(root)进程、要么进程有效用户 ID 与文件的用户 ID(文件所有者)相匹配
    #include 
    
    // pathname:需要进行权限修改的文件路径,若该参数所指为符号链接,实际改变权限的文件是符号链接所指向的文件,而不是符号链接文件本身
    // mode :该参数用于描述文件权限,可以直接使用八进制数据来描述,也可以使用相应的权限宏
    int chmod(const char *pathname, mode_t mode);
    
  • fchmod 函数

    • fchmod() 与 chmod() 的区别在于使用了文件描述符来代替文件路径,就像是 fstat 与 stat 的区别
    #include 
    
    int fchmod(int fd, mode_t mode);
    

5.5 umask 函数

  • umask 命令用于查看/设置权限掩码,权限掩码主要用于对新建文件的权限进行屏蔽,权限掩码的表示方式与文件权限的表示方式相同,但是需要去除特殊权限位,umask 不能对特殊权限位进行屏蔽

    $ umask
    0002
    
  • 当新建文件时,文件实际的权限并不等于所设置的权限,如:调用 open 函数新建文件时,文件实际的权限并不等于 mode 参数所描述的权限,而是通过如下关系得到实际权限

    mode & ~umask
    
    • 如调用 open 函数新建文件时,mode 参数指定为 0777,假设 umask 为 0002,那么实际权限为
    0777 & (~0002) = 0775
    
  • umask 权限掩码是进程的一种属性,用于指明该进程新建文件或目录时,应屏蔽哪些权限位。Linux 系统提供了 umask 函数用于设置进程的权限掩码,该函数是一个系统调用

    • umask 是进程自身的一种属性、A 进程的 umask 与 B 进程的 umask 无关
    #include 
    #include 
    
    mode_t umask(mode_t mask);
    

6. 文件的时间属性

  • 3 个文件的时间属性:文件最后被访问的时间、文件内容最后被修改的时间以及文件状态最后被改变的时间,分别记录在 struct stat 结构体的 st_atim、st_mtim 以及 st_ctim 变量中
  • 不同函数对文件时间属性的影响
    Linux系统编程(三):文件和目录(下)_第5张图片

6.1 utime()、utimes() 修改时间属性

文件的时间属性虽然会在对文件进行相关操作(如:读、写)时发生改变,但这些改变都是隐式、被动的改变

  • 使用 Linux 系统调用 utime() 和 utimes() 函数来显式修改文件的时间属性
    • 只能显式修改文件的最后一次访问时间和文件内容最后被修改的时间,不能显式修改文件状态最后被改变的时间
6.1.1 utime()
#include 
#include 

// filename:需要修改时间属性的文件路径
// times:将时间属性修改为该参数所指定的时间值,times 是一个 struct utimbuf 结构体类型的指针,如果将 times 设置为 NULL
// 则会将文件的访问时间和修改时间设置为系统当前时间
int utime(const char *filename, const struct utimbuf *times);
  • struct utimbuf 结构体

    struct utimbuf {
        // time_t 其实就是 long int 类型,所以这两个时间是以秒为单位的
        time_t actime;   /* 访问时间 */
        time_t modtime;  /* 内容修改时间 */
    }
    
  • 文件时间属性的修改也不是任何用户都可以随便修改的,只有以下几种进程可对其进行修改

    • 超级用户进程(以 root 身份运行的进程)
    • 有效用户 ID 与该文件用户 ID(文件所有者)相匹配的进程
    • 在参数 times 等于 NULL 的情况下,对文件拥有写权限的进程
  • 示例

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #define MY_FILE "./test_file"
    
    int main(void) {
        struct utimbuf utm_buf;
        time_t cur_sec;
        int ret;
    
        /* 检查文件是否存在 */
        ret = access(MY_FILE, F_OK);
        if (ret == -1) {
            printf("Error: %s file does not exist!\n", MY_FILE);
            exit(-1);
        }
    
        /* 获取当前时间 */
        time(&cur_sec);
        utm_buf.actime = cur_sec;
        utm_buf.modtime = cur_sec;
    
        /* 修改文件时间戳 */
        ret = utime(MY_FILE, &utm_buf);
        if (ret == -1) {
            perror("utime error");
            exit(-1);
        }
        exit(0);
    }
    
    $ gcc demo03.c 
    $ sudo ./a.out 
    $ stat test_file 
      File: test_file
      Size: 13        	Blocks: 8          IO Block: 4096   regular file
    Device: 801h/2049d	Inode: 2246591     Links: 1
    Access: (0664/-rw-rw-r--)  Uid: (    0/    root)   Gid: (    0/    root)
    # test_file 文件的访问时间和内容修改时间均被更改为当前时间
    Access: 2023-12-22 12:36:27.458464548 +0800
    Modify: 2023-12-22 12:36:27.000000000 +0800
    Change: 2023-12-22 12:36:27.458464548 +0800
     Birth: -
    
6.1.2 utimes()
  • utimes() 也是系统调用,功能与 utime()函数一致,只是参数、细节上有些许不同,utimes() 与 utime() 最大的区别在于前者可以以微秒级精度来指定时间值

    #include 
    
    // filename:需要修改时间属性的文件路径
    // times:将时间属性修改为该参数所指定的时间值,times 是一个 struct timeval 结构体类型的数组
        // 数组共有两个元素,第一个元素用于指定访问时间,第二个元素用于指定内容修改时间
        // 如果 times 参数为 NULL,则会将文件的访问时间和修改时间设置为当前时间
    int utimes(const char *filename, const struct timeval times[2]);
    
  • struct timeval 结构体

    struct timeval {
        long tv_sec;   /* 秒 */
        long tv_usec;  /* 微秒 */
    };
    

6.2 futimens()、utimensat() 修改时间属性

  • 这两个系统调用相对于 utime 和 utimes 函数有以下三个优点
    • 可按纳秒级精度设置时间戳
    • 可单独设置某一时间戳
      • 如:只设置访问时间,而修改时间保持不变,如果要使用 utime() 或 utimes() 来实现此功能,则需要首先使用 stat() 获取另一个时间戳的值,然后再将获取值与打算变更的时间戳一同指定
    • 可独立将任一时间戳设置为当前时间
      • 使用 utime() 或 utimes() 函数虽然也可以通过将 times 参数设置为 NULL 来达到将时间戳设置为当前时间的效果,但是不能单独指定某一个时间戳,必须全部设置为当前时间
6.2.1 futimens()函数
#include 
#include 

// fd:文件描述符。
// times:将时间属性修改为该参数所指定的时间值,times 指向拥有 2 个 struct timespec 结构体类型变量的数组
    // 数组共有两个元素,第一个元素用于指定访问时间,第二个元素用于指定内容修改时间
int futimens(int fd, const struct timespec times[2]);
6.2.2 utimensat() 函数
  • utimensat() 与 futimens() 在参数以及细节上存在一些差异,使用 futimens() 函数,需要先将文件打开,通过文件描述符进行操作,utimensat() 可以直接使用文件路径方式进行操作
    #include 
    #include 
    
    // dirfd:该参数可以是一个目录的文件描述符,也可以是特殊值 AT_FDCWD;如果 pathname 参数指定的是文件的绝对路径,则此参数会被忽略
    // pathname:指定文件路径。如果 pathname 参数指定的是一个相对路径、并且 dirfd 参数不等于特殊值 AT_FDCWD,则实际操作的文件路径是相对于文件描述符 dirfd 指向的目录进行解析
        // 如果 pathname 参数指定的是一个相对路径、并且 dirfd 参数等于特殊值 AT_FDCWD,则实际操作的文件路径是相对于调用进程的当前工作目录进行解析
    // times:与 futimens() 的 times 参数含义相同
    // flags:此参数可以为 0, 也可以设置为 AT_SYMLINK_NOFOLLOW, 如果设置为 AT_SYMLINK_NOFOLLOW
        // 当 pathname 参数指定的文件是符号链接,则修改的是该符号链接的时间戳,而不是它所指向的文件
    int utimensat(int dirfd, const char *pathname, const struct timespec times[2], int flags);
    

7. 符号链接(软链接)与硬链接

  • 在 Linux 系统中有两种链接文件,分为软链接(也叫符号链接)文件和硬链接文件
    • 软链接文件是 Linux 系统下的七种文件类型之一,其作用类似于 Windows 下的快捷方式
    • 从使用角度来讲,两者没有任何区别,都与正常的文件访问方式一样,支持读、写以及执行
    • 使用 ln 命令可以为一个文件创建软链接文件或硬链接文件
      • 硬链接:ln 源文件 链接文件
      • 软链接:ln -s 源文件 链接文件

7.1 硬链接

  • 硬链接文件与源文件拥有相同的 inode 号,既然 inode 相同,也就意味着它们指向了物理硬盘的同一个区块,仅仅只是文件名字不同而已,创建出来的硬链接文件与源文件对文件系统来说是完全平等的关系
    • inode 数据结结构中会记录文件的链接数,这个链接数指的就是硬链接数
    • 当为文件每创建一个硬链接,inode 节点上的链接数就会加一,每删除一个硬链接,inode 节点上的链接数就会减一,直到为 0,inode 节点和对应的数据块才会被文件系统所回收,也就意味着文件已经从文件系统中删除
    • 源文件本身就是一个硬链接文件
7.1.1 创建硬链接 link()
#include 

// oldpath:用于指定被链接的源文件路径,应避免 oldpath 参数指定为软链接文件,为软链接文件创建硬链接没有意义,虽然并不会报错
// newpath:用于指定硬链接文件路径,如果 newpath 指定的文件路径已存在,则会产生错误
int link(const char *oldpath, const char *newpath);
  • 测试
    #include 
    #include 
    #include 
    
    int main(void) {
        int ret;
        ret = link("./test_file", "./hard");
        if (ret == -1) {
            perror("link error");
            exit(-1);
        }
        exit(0);
    }
    

7.2 软链接

  • 软链接文件与源文件有不同 inode 号,所以也就意味着它们之间有不同的数据块,但软链接文件的数据块中存储的是源文件的路径名,链接文件可以通过这个路径找到被链接的源文件
    • 它们之间类似于一种 “主从” 关系,当源文件被删除之后,软链接文件依然存在,但此时它指向的是一个无效的文件路径,这种链接文件被称为悬空链接
    • inode 节点中记录的链接数并未将软链接计算在内
7.2.1 创建软链接 symlink()
  • 创建软链接并不要求 target 参数指定的文件路径已经存在,如果不存在,那么创建的软链接将成为 “悬空链接”
    #include 
    
    // target:用于指定被链接的源文件路径,target 参数指定的也可以是一个软链接文件
    // linkpath:用于指定硬链接文件路径,如果 newpath 指定的文件路径已存在,则会产生错误
    int symlink(const char *target, const char *linkpath);
    
7.2.2 读取软链接文件
  • 软链接文件数据块中存储的是被链接文件的路径信息,使用系统调用 readlink 读取出软链接文件中存储的路径信息

    • 不可以使用 read 函数,因为需要先 open 打开该文件得到文件描述符,但是调用 open 打开一个链接文件本身是不会成功的,因为打开的并不是链接文件本身,而是其指向的文件,所以不能使用 read 来读取
    #include 
    
    // pathname:需要读取的软链接文件路径。只能是软链接文件路径,不能是其它类型文件,否则调用函数将报错
    // buf:用于存放路径信息的缓冲区
    // bufsiz:读取大小,一般读取的大小需要大于链接文件数据块中存储的文件路径信息字节大小
    // 返回值:失败将返回-1,并会设置 errno;成功将返回读取到的字节数
    ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);
    
  • 示例

    #include 
    #include 
    #include 
    #include 
    
    int main(void) {
        char buf[50];
        int ret;
    
        memset(buf, 0x0, sizeof(buf));
        ret = readlink("./soft", buf, sizeof(buf));
        if (ret == -1) {
            perror("readlink error");
            exit(-1);
        }
    
        printf("%s\n", buf);
        exit(0);
    }
    

7.3 对比

  • 硬链接来说,存在一些限制情况
    • 不能对目录创建硬链接(超级用户可以创建,但必须在底层文件系统支持的情况下)
    • 硬链接通常要求链接文件和源文件位于同一文件系统中
  • 软链接文件的使用并没有上述限制条件,优点如下
    • 可以对目录创建软链接
    • 可以跨越不同文件系统
    • 可以对不存在的文件创建软链接

8. 目录

  • 目录(文件夹)在 Linux 系统也是一种文件,是一种特殊文件
    • 目录作为一种特殊文件,并不适合使用常规文件 I/O 方式进行读写等操作
    • 在 Linux 系统下,会有一些专门的系统调用或 C 库函数用于对文件夹进行操作,如:打开、创建文件夹、删除文件夹、读取文件夹以及遍历文件夹中的文件等

8.1 目录存储形式

  • 目录在文件系统中的存储方式与常规文件类似,常规文件包括了 inode 节点以及文件内容数据存储块,但对于目录来说,其存储形式则是由 inode 节点和目录块所构成,目录块当中记录了有哪些文件组织在这个目录下,记录它们的文件名以及对应的 inode 编号
    • 普通文件由 inode 节点和数据块构成
    • 目录由 inode 节点和目录块构成
      Linux系统编程(三):文件和目录(下)_第6张图片

8.2 创建和删除目录

  • Linux 系统提供了专门用于创建目录 mkdir 以及删除目录 rmdir 相关的系统调用
8.2.1 mkdir 函数
#include 
#include 

// pathname:需要创建的目录路径,该路径名可以是相对路径或绝对路径,若指定的路径名已经存在,则调用将会失败
// mode:新建目录的权限设置,设置方式与 open 函数的 mode 参数一样,最终权限为(mode & ~umask)
int mkdir(const char *pathname, mode_t mode);
  • 示例
    #include 
    #include 
    #include 
    #include 
    
    int main(void) {
        int ret;
        ret = mkdir("./new_dir", S_IRWXU |
        S_IRGRP | S_IXGRP |
        S_IROTH | S_IXOTH);
    
        if (ret == -1) {
            perror("mkdir error");
            exit(-1);
        }
        exit(0);
    }
    
8.2.2 rmdir 函数
#include 

// pathname:需要删除的目录对应的路径名,并且该目录必须是一个空目录,也就是该目录下只有 . 和 .. 这两个目录项
    // pathname 指定的路径名不能是软链接文件,即使该链接文件指向了一个空目录
int rmdir(const char *pathname);
  • 示例
    #include 
    #include 
    #include 
    
    int main(void) {
        int ret;
        ret = rmdir("./new_dir");
        if (ret == -1) {
            perror("rmdir error");
            exit(-1);
        }
        exit(0);
    }
    

8.3 打开、读取及关闭目录

  • Linux 系统使用 opendir()、readdir()和 closedir() 三个库函数来打开、读取以及关闭目录
8.3.1 打开文件 opendir
  • opendir()函数用于打开一个目录,并返回指向该目录的句柄,供后续操作使用。Opendir 是一个 C 库函数
    #include 
    #include 
    
    // name:指定需要打开的目录路径名,可以是绝对路径,也可以是相对路径。
    // 返回值:成功将返回指向该目录的句柄,一个 DIR 指针(实质是一个结构体指针),作用类似 open 函数返回的文件描述符 fd
    // 后续对该目录的操作需要使用该 DIR 指针变量;若调用失败,则返回 NULL
    DIR *opendir(const char *name);
    
8.3.2 读取目录 readdir
  • readdir() 用于读取目录,获取目录下所有文件的名称以及对应 inode 号,这里介绍的 readdir() 是一个 C 库函数
    #include 
    
    // dirp:目录句柄 DIR 指针
    // 返回值:返回一个指向 struct dirent 结构体的指针,该结构体表示 dirp 指向的目录流中的下一个目录条目
    // 在到达目录流的末尾或发生错误时,它返回 NULL
    struct dirent *readdir(DIR *dirp);
    
  • struct dirent 结构体
    // 只需要关注 d_ino 和 d_name 两个字段即可,分别记录了文件的 inode 编号和文件名,其余字段并不是所有系统都支持
    struct dirent {
        ino_t d_ino; /* inode 编号 */
        off_t d_off; /* not an offset; see NOTES */
        unsigned short d_reclen; /* length of this record */
        unsigned char d_type; /* type of file; not supported by all filesystem types */
        char d_name[256]; /* 文件名 */
    };
    
8.3.3 rewinddir 函数
  • rewinddir() 是 C 库函数,可将目录流重置为目录起点,以便对 readdir() 的下一次调用将从目录列表中的第一个文件开始
    #include 
    #include 
    
    // dirp:目录句柄
    void rewinddir(DIR *dirp);
    
8.3.4 closedir 函数
  • closedir() 函数用于关闭处于打开状态的目录,同时释放它所使用的资源
    #include 
    #include 
    
    int closedir(DIR *dirp);
    
8.3.5 示例
  • 打开一个目录、并将目录下的所有文件的名称以及其对应 inode 编号打印出来
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main(void) {
        struct dirent *dir;
        DIR *dirp;
        int ret = 0;
    
        /* 打开目录 */
        dirp = opendir("./test");
        if (dirp == NULL) {
            perror("opendir error");
            exit(-1);
        }   
    
        /* 循环读取目录流中的所有目录条目 */
        errno = 0;
    
        while ((dir = readdir(dirp)) != NULL) {
            printf("%s %ld\n", dir->d_name, dir->d_ino);
        }   
        if (0 != errno) {
            perror("readdir error");
            ret = -1; 
            goto err;
        } else
            printf("End of directory!\n");
    
    err:
        closedir(dirp);
        exit(ret);
    }
    
    $ gcc dir.c 
    $ ./a.out 
    . 2246427
    demo02.c 2246598
    rm.c 2246603
    test_file 2246591
    mkdir.c 2246599
    .. 2228289
    demo03.c 2246592
    demo.c 2246475
    a.out 2246385
    End of directory!
    

8.4 进程的当前工作目录

  • Linux 下的每一个进程都有自己的当前工作目录,当前工作目录是该进程解析、搜索相对路径名的起点(不是以 “/” 斜杆开头的绝对路径)
    • 如:调用 open 函数打开文件时,传入的文件路径使用相对路径方式进行表示,那么该进程解析这个相对路径名时,会以进程的当前工作目录作为参考目录
8.4.1 获取进程当前工作目录
  • 可通过系统调用 getcwd 函数来获取进程的当前工作目录

    #include 
    
    // buf:getcwd() 将内含当前工作目录绝对路径的字符串存放在 buf 缓冲区中
    // size:缓冲区的大小,分配的缓冲区大小必须要大于字符串长度,否则调用将会失败
    // 返回值:如果调用成功将返回指向 buf 的指针,失败将返回 NULL,并设置 errno
    char *getcwd(char *buf, size_t size);
    
  • 示例:读取进程的当前工作目录

    #include 
    #include 
    #include 
    #include 
    
    int main(void) {
        char buf[100];
        char *ptr;
    
        memset(buf, 0x0, sizeof(buf));
        ptr = getcwd(buf, sizeof(buf));
        if (NULL == ptr) {
            perror("getcwd error");
            exit(-1);
        }
        printf("Current working directory: %s\n", buf);
    
        exit(0);
    }
    
    $ gcc dir2.c 
    $ ./a.out 
    Current working directory: /home/yxd/Desktop/test
    
8.4.2 改变进程当前工作目录
  • 系统调用 chdir() 和 fchdir() 可以用于更改进程的当前工作目录
    • chdir() 是以路径的方式进行指定
    • fchdir() 则是通过文件描述符,文件描述符可调用 open() 打开相应的目录时获得
    #include 
    
    // path:将进程的当前工作目录更改为 path 参数指定的目录,可以是绝对路径或相对路径,指定的目录必须要存在,否则会报错
    // fd:将进程的当前工作目录更改为 fd 文件描述符所指定的目录
    int chdir(const char *path);
    int fchdir(int fd);
    
  • chdir 函数使用示例
    #include 
    #include 
    #include 
    #include 
    
    int main(void) {
        char buf[100];
        char *ptr;
        int ret;
    
        /* 获取更改前的工作目录 */
        memset(buf, 0x0, sizeof(buf));
        ptr = getcwd(buf, sizeof(buf));
        if (NULL == ptr) {
            perror("getcwd error");
            exit(-1);
        }
        printf("Before the change: %s\n", buf);
    
        /* 更改进程的当前工作目录 */
        ret = chdir("./new_dir");
        if (-1 == ret) {
            perror("chdir error");
            exit(-1);
        }
    
        /* 获取更改后的工作目录 */
        memset(buf, 0x0, sizeof(buf));
        ptr = getcwd(buf, sizeof(buf));
        if (NULL == ptr) {
            perror("getcwd error");
            exit(-1);
        }
        printf("After the change: %s\n", buf);
    
        exit(0);
    }
    

9. 删除文件

  • 通过系统调用 unlink() 或使用 C 库函数 remove() 删除一个普通文件

9.1 unlink()

  • unlink() 用于删除一个文件(不包括目录)
    • link 函数,用于创建一个硬链接文件,创建硬链接时,inode 节点上的链接数就会增加;unlink() 的作用与 link() 相反,unlink() 系统调用用于移除/删除一个硬链接(从其父级目录下删除该目录条目)
    • unlink() 系统调用实质上是移除 pathname 参数指定的文件路径对应的目录项(从其父级目录中移除该目录项),并将文件的 inode 链接计数减 1,如果该文件还有其它硬链接,则仍可通过其它链接访问该文件的数据;只有当链接计数变为 0 时,该文件的内容才可被删除;只要有进程打开了该文件,其内容也不能被删除
    • unlink() 系统调用并不会对软链接进行解引用操作,若 pathname 指定的文件为软链接文件,则删除软链接文件本身,而非软链接所指定的文件
    #include 
    
    int unlink(const char *pathname);
    

9.2 remove()

  • remove() 是一个 C 库函数,用于移除一个文件或空目录
    • 如果 pathname 参数指定的是一个非目录文件,则 remove() 调用 unlink()
    • 如果 pathname 参数指定的是一个目录,那么 remove() 调用 rmdir()
    • remove() 同样不对软链接进行解引用操作,若 pathname 参数指定的是一个软链接文件,则 remove()会删除链接文件本身、而非所指向的文件
    #include 
    
    int remove(const char *pathname);
    

10. 文件重命名

  • 借助于 rename() 既可以对文件进行重命名,又可以将文件移至同一文件系统中的另一个目录下
    • rename() 调用仅操作目录条目,而不移动文件数据(不改变文件 inode 编号、不移动文件数据块中存储的内容
    • 重命名既不影响指向该文件的其它硬链接,也不影响已经打开该文件的进程
    #include 
    
    // oldpath:原文件路径
    // newpath:新文件路径
    // 返回值:成功返回 0;失败将返回-1,并设置 errno
    int rename(const char *oldpath, const char *newpath);
    
  • 根据 oldpath、newpath 的不同,有以下不同的情况需要进行说明
    • 若 newpath 参数指定的文件或目录已经存在,则将其覆盖
    • 若 newpath 和 oldpath 指向同一个文件,则不发生变化(且调用成功)
    • rename() 系统调用对其两个参数中的软链接均不进行解引用。如果 oldpath 是一个软链接,那么将重命名该软链接;如果 newpath 是一个软链接,则会将其移除、被覆盖
    • 如果 oldpath 指代文件,而非目录,那么就不能将 newpath 指定为一个目录的路径名。要想重命名一个文件到某一个目录下,newpath 必须包含新的文件名
    • 如果 oldpath 指代为一个目录,在这种情况下,newpath 要么不存在,要么必须指定为一个空目录
    • oldpath 和 newpath 所指代的文件必须位于同一文件系统
    • 不能对 .(当前目录)和 …(上一级目录)进行重命名

你可能感兴趣的:(Linux系统编程,linux,c语言,嵌入式,文件和目录)