第 15 章 文件属性 (File Attributes)

      +

      核心结论

      • stat() 系列stat(path, &sb) 查 pathname 文件;lstat(path, &sb) 不跟随符号链接;fstat(fd, &sb) 查已打开的 fd;返回 struct stat 包含文件全部元数据。

      • struct stat 字段:st_dev(设备)、st_ino(i-node 号)、st_mode(类型+权限)、st_nlink(硬链接数)、st_uid/st_gid、st_size、st_atime/mtime/ctime、st_blksize、st_blocks。

      • 文件类型:从 st_mode 提取——S_ISREG/S_ISDIR/S_ISCHR/S_ISBLK/S_ISLNK/S_ISFIFO/S_ISSOCK 宏;S_IFMT 掩码后比较。

      • 文件权限:9 位 (rwxrwxrwx) + 3 特殊位 (setuid/setgid/sticky);用 S_IRUSR 等宏比较;chmod/fchmod 改变。

      • 文件时间戳:atime(访问)、mtime(数据修改)、ctime(i-node 状态变更);utime/utimes 修改;utimensat 纳秒精度。

      • i-node 标志 (chattr):ext2 扩展属性——append-only (a)、immutable (i)、no-dump (d)、sync (s) 等;lsattr 查看。

      • 设备 ID 提取:major() 和 minor() 宏提取设备 major/minor(需 _BSD_SOURCE)。

      本章主旨

      本章是文件元数据查询的核心。核心:(1) stat/lstat/fstat 三个系统调用;(2) struct stat 的所有字段含义;(3) 文件类型与权限的提取;(4) 时间戳的修改与影响。本章不展开 ACL(详见第 17 章)、硬链接/符号链接(详见第 18 章)、扩展属性(详见第 16 章)。

      一、核心概念

      本章围绕 6 个核心概念展开:从「stat 系统调用」到「struct stat 字段」到「权限与时间戳」。

      概念 定义 + 重要性 实现提示

      stat/lstat/fstat

      stat 查 pathname;lstat 不跟随符号链接;fstat 查 fd;返回 struct stat。

      §15.1;stat 跟随符号链接;lstat 不跟随。

      struct stat 字段

      st_dev、st_ino、st_mode、st_nlink、st_uid/st_gid、st_size、st_atime/mtime/ctime、st_blksize、st_blocks。

      §15.1;st_dev + st_ino 唯一标识文件。

      文件类型提取

      st_mode & S_IFMT 提取类型位;用 S_ISREG/S_ISDIR/S_ISLNK 等宏判断。

      §15.1;类型有 7 种:reg/dir/lnk/chr/blk/fifo/sock。

      权限位与特殊位

      9 位权限(owner/group/other × rwx)+ 3 特殊位(setuid/setgid/sticky);用 S_IRUSR 等宏比较。

      §15.4;特殊位有 ls 时的 s/S 标记。

      文件时间戳

      atime 访问、mtime 数据修改、ctime i-node 变更;utime/utimes 修改 atime+mtime。

      §15.2;O_NOATIME flag 可禁止 atime 更新。

      i-node 标志

      ext2 扩展属性:append-only (a)、immutable (i)、no-dump (d) 等;chattr 设置、lsattr 查看。

      §15.5;仅 root 可设;a 让文件只能追加不能覆盖。

      二、详细笔记

      15.1 stat() 系统调用

      Whatstat/lstat/fstat 是查询文件元数据的核心系统调用;返回 struct stat,包含几乎所有文件属性。

      Why:理解 stat 是理解所有文件元数据相关操作(ls、find、chmod)的基础;写程序需要「文件是什么类型」「多大」「什么时候修改的」时用 stat。

      How

      // 摘自《The Linux Programming Interface》第 15 章
      #include <sys/stat.h>
      
      int stat(const char *pathname, struct stat *statbuf);
      // 查 pathname;跟随符号链接
      
      int lstat(const char *pathname, struct stat *statbuf);
      // 查 pathname;不跟随符号链接(用于查看链接本身)
      
      int fstat(int fd, struct stat *statbuf);
      // 查已打开的 fd
      
      // 全部成功返回 0;失败返回 -1

      权限要求(§15.1):

      • stat/lstat:需要父目录的 execute (search) 权限;不需要文件本身的权限。

      • fstat:只要 fd 有效。

      When

      • 想看符号链接指向的文件——stat

      • 想看符号链接本身——lstat

      • 已经打开文件——fstat(避免路径名解析)。

      Example

      // 摘自《The Linux Programming Interface》第 15 章 files/t_stat.c
      struct stat sb;
      if (stat(argv[1], &sb) == -1) errExit("stat");
      printf("File type: ");
      if (S_ISREG(sb.st_mode))      printf("regular\n");
      else if (S_ISDIR(sb.st_mode)) printf("directory\n");
      else if (S_ISLNK(sb.st_mode)) printf("symlink\n");
      // ...
      printf("Size: %lld bytes\n", (long long) sb.st_size);
      printf("Inode: %ld\n", (long) sb.st_ino);

      15.2 struct stat 字段详解

      Whatstruct stat 是 stat 系统调用的返回结构;包含文件所有元数据。

      Why:理解每个字段的含义是正确使用 stat 的前提。

      How:字段定义(§15.1):

      struct stat {
          dev_t     st_dev;         // 文件所在设备
          ino_t     st_ino;         // i-node 号
          mode_t    st_mode;        // 文件类型 + 权限
          nlink_t   st_nlink;       // 硬链接数
          uid_t     st_uid;         // owner UID
          gid_t     st_gid;         // owner GID
          dev_t     st_rdev;        // 设备文件专用:设备 major/minor
          off_t     st_size;        // 文件大小(字节)
          blksize_t st_blksize;     // 最优 I/O 块大小(字节)
          blkcnt_t  st_blocks;      // 分配的 512 字节块数
          time_t    st_atime;       // 最后访问时间
          time_t    st_mtime;       // 最后数据修改时间
          time_t    st_ctime;       // 最后 i-node 状态变更时间
      };
      字段 关键点

      st_dev + st_ino

      跨 FS 唯一标识文件

      st_mode

      编码文件类型 + 权限 + 特殊位

      st_nlink

      硬链接数;为 0 时数据被释放

      st_uid/st_gid

      owner;可被 chown 改变

      st_size

      字节数;普通文件为文件大小;符号链接为目标路径长度

      st_blksize

      推荐 I/O 块大小(不是 FS 块大小)

      st_blocks

      实际分配块数(512 字节单位);空洞文件 < size/512

      st_atime

      访问时间;O_NOATIME 不更新

      st_mtime

      数据修改时间

      st_ctime

      i-node 状态变更时间(不可直接修改)

      提取 major/minor(§15.1):

      #define _BSD_SOURCE
      #include <sys/types.h>
      #include <sys/sysmacros.h>
      
      unsigned int major(dev_t dev);
      unsigned int minor(dev_t dev);
      // 返回值类型随实现变化;打印时强转 unsigned int

      When

      • 唯一标识文件——(st_dev, st_ino)

      • 看磁盘使用——st_blocks * 512(或 du -k)。

      • 检测空洞文件——st_blocks * 512 < st_size

      • 看设备——major/minor(st_rdev)(设备文件)。

      Example

      // 摘自《The Linux Programming Interface》第 15 章
      struct stat sb;
      stat(path, &sb);
      printf("Device: %d:%d\n", major(sb.st_dev), minor(sb.st_dev));
      printf("Inode: %ld\n", (long) sb.st_ino);
      printf("Size: %lld, Allocated: %lld*512\n",
             (long long) sb.st_size, (long long) sb.st_blocks);

      15.3 文件类型提取

      Whatst_mode 字段编码了文件类型;通过 S_IFMT 掩码后与常量比较,或用 S_ISXXX 宏判断。

      Why:程序需要根据文件类型决定操作(如「是目录就递归」「是设备文件就用 read/write」)——需要正确提取类型。

      How

      // 摘自《The Linux Programming Interface》第 15 章
      // 类型掩码和常量
      if ((sb.st_mode & S_IFMT) == S_IFREG)   /* 普通文件 */
      if (S_ISREG(sb.st_mode))                 /* 同上,宏更易读 */

      7 种文件类型(§15.1 表 15-1):

      类型 S_IFXXX 宏

      S_ISXXX 宏

      普通文件

      S_IFREG

      S_ISREG()

      目录

      S_IFDIR

      S_ISDIR()

      字符设备

      S_IFCHR

      S_ISCHR()

      块设备

      S_IFBLK

      S_ISBLK()

      FIFO(命名管道)

      S_IFIFO

      S_ISFIFO()

      套接字

      S_IFSOCK

      S_ISSOCK()

      符号链接

      S_IFLNK

      S_ISLNK()(仅 lstat)

      When

      • ls -l 实现——用 S_ISXXX 宏判断类型并显示对应字符。

      • 写文件浏览器——根据类型显示图标或展开目录。

      • 写文件处理工具——只处理特定类型。

      Example

      // 摘自《The Linux Programming Interface》第 15 章
      struct stat sb;
      if (lstat(path, &sb) == -1) errExit("lstat");
      
      switch (sb.st_mode & S_IFMT) {
          case S_IFREG:  printf("regular file\n"); break;
          case S_IFDIR:  printf("directory\n"); break;
          case S_IFLNK:  printf("symbolic link\n"); break;
          case S_IFCHR:  printf("character device\n"); break;
          case S_IFBLK:  printf("block device\n"); break;
          case S_IFIFO:  printf("FIFO\n"); break;
          case S_IFSOCK: printf("socket\n"); break;
          default:       printf("unknown?\n");
      }

      15.4 文件权限

      Whatst_mode 的低 12 位编码文件权限:9 位 rwx + 3 位特殊(setuid/setgid/sticky)。

      Why:理解权限是写安全程序、理解 set-UID 程序、设计文件共享方案的前提。

      How:权限位(§15.4):

      含义

      对目录

      S_IRUSR (0400)

      owner 读

      可列出文件

      S_IWUSR (0200)

      owner 写

      可添加/删除/重命名

      S_IXUSR (0100)

      owner 执行

      可搜索(cd/访问内文件)

      S_IRGRP (0040)

      group 读

      同上

      S_IXGRP (0010)

      group 执行

      同上

      S_IROTH (0004)

      other 读

      同上

      S_IXOTH (0001)

      other 执行

      同上

      S_ISUID (04000)

      set-user-ID

      不适用

      S_ISGID (02000)

      set-group-ID

      目录内文件继承组

      S_ISVTX (01000)

      sticky

      权限检查算法:

      1. 如果进程的 effective UID == 文件 st_uid → 用 owner 权限。

      2. 否则如果进程的 effective GID 或任一 supplementary GID == 文件 st_gid → 用 group 权限。

      3. 否则用 other 权限。

      chmod/fchmod(§15.4):

      #include <sys/stat.h>
      int chmod(const char *pathname, mode_t mode);
      int fchmod(int fd, mode_t mode);
      // 成功返回 0;进程需是 file owner 或有 CAP_FOWNER

      When

      • ls -l 显示权限。

      • 写文件前检查——access(path, R_OK) 或 open 失败。

      • 协作组共享——chmod g+rw,chown :group。

      • /tmp 防止互删——chmod +t。

      Example

      // 摘自《The Linux Programming Interface》第 15 章
      if (chmod("file", S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) == -1)
          errExit("chmod");
      // 等价于 chmod 644 file
      
      // 打开 set-UID
      if (chmod("prog", S_ISUID | S_IXUSR) == -1)
          errExit("chmod");
      // 等价于 chmod u+s prog

      15.5 文件时间戳

      What:每个文件有 3 个时间戳:atime(访问)、mtime(数据修改)、ctime(i-node 状态变更)。

      Why:时间戳是 make(1)、日志轮转、备份策略、文件同步的基础。

      How

      字段 何时更新

      谁能直接改

      st_atime

      read 成功时

      utime/utimes/utimensat

      st_mtime

      write 成功时

      同上

      st_ctime

      任何 i-node 状态变更(权限、owner、链接数等)

      内核自动;不可直接改

      修改时间戳(§15.2):

      #include <utime.h>
      int utime(const char *filename, const struct utimbuf *times);
      // 精度 1 秒;times == NULL → 设为当前时间
      
      #include <sys/time.h>
      int utimes(const char *filename, const struct timeval times[2]);
      // 精度微秒;times[0] = atime, times[1] = mtime
      
      // Linux 特有
      #include <fcntl.h>
      int utimensat(int fd, const char *path, const struct timespec times[2], int flags);
      // 精度纳秒;可与 AT_SYMLINK_NOFOLLOW 一起用
      // times[0] = atime, times[1] = mtime

      O_NOATIME flag:

      // 摘自《The Linux Programming Interface》第 4 章
      int fd = open("file", O_RDONLY | O_NOATIME);
      // read 不更新 atime;减少磁盘写
      // 需要 file owner 或 CAP_FOWNER

      When

      • make 判断文件新鲜度——mtime vs 依赖文件 mtime。

      • 日志轮转——按 mtime 删旧日志。

      • cp -p 保留时间戳——cp --preserve=timestamps

      • 高性能读取——O_NOATIME 减少元数据写。

      Example

      // 摘自《The Linux Programming Interface》第 15 章
      // 设置 atime 和 mtime 为特定时间(纳秒精度)
      struct timespec times[2];
      times[0].tv_sec = 1234567890; times[0].tv_nsec = 0;  // atime
      times[1].tv_sec = 1234567891; times[1].tv_nsec = 0;  // mtime
      
      if (utimensat(AT_FDCWD, "file", times, 0) == -1)
          errExit("utimensat");

      15.6 i-node 标志 (chattr)

      What:i-node 标志(也称 ext2 扩展属性)是文件级标志,控制内核对文件的特殊处理;仅 ext2/3/4 支持。

      Why:append-only 标志让日志文件只能追加;immutable 标志让文件不可修改——安全和数据完整性保护。

      How

      标志 含义

      a (append-only)

      只能追加;不能覆盖/截断

      i (immutable)

      不可修改(任何方式);只能 root 解除

      d (no-dump)

      dump(8) 跳过

      s (synchronous)

      修改同步写(不经过 page cache)

      A (no-atime-update)

      不更新 atime

      c (compress)

      透明压缩(内核支持时)

      u (undelete)

      允许恢复删除的文件

      chattr 命令设置;lsattr 查看;需 root 权限:

      # 摘自《The Linux Programming Interface》第 15 章
      # 设置 append-only
      $ chattr +a /var/log/important.log
      
      # 设置 immutable(root 自己也改不了)
      $ chattr +i /etc/passwd
      
      # 查看
      $ lsattr /etc/passwd
      ----i-------e- /etc/passwd

      When

      • 日志文件——+a 防止误覆盖/篡改。

      • 关键配置——+i 防止误删。

      • 高完整性需求——+s 强制同步写。

      三、关键图表

      非可视化条目(stat API 速查)
      API 用途

      stat(path, &sb)

      查 pathname(跟随符号链接)

      lstat(path, &sb)

      查 pathname(不跟随符号链接)

      fstat(fd, &sb)

      查已打开 fd

      S_ISREG/S_ISDIR/…​

      判断文件类型

      major/minor

      提取设备 ID

      chmod/fchmod

      修改权限

      chown/fchown/lchown

      修改 owner

      utime/utimes/utimensat

      修改 atime+mtime

      O_NOATIME

      open 时禁止 atime 更新

      chattr/lsattr

      i-node 标志

      四、思维导图

      mindmap
        root((第 15 章 文件属性))
          stat 系列
            stat 跟随链接
            lstat 不跟随
            fstat 已打开
          struct stat
            st_dev st_ino
            st_mode
            st_uid st_gid
            st_size st_blocks
            atime mtime ctime
          文件类型
            7 种类型
            S_IFMT 掩码
            S_ISXXX 宏
          权限
            9 位 rwx
            3 特殊位
            setuid setgid sticky
            chmod fchmod
          时间戳
            atime 访问
            mtime 数据
            ctime 状态
            utime utimes
            utimensat 纳秒
            O_NOATIME
          i node 标志
            chattr
            a append
            i immutable
            s sync
            仅 root

      五、重点与易错点

      1. st_dev + st_ino 唯一标识文件:跨文件系统也唯一(除非设备复用 i-node 号)。

      2. 符号链接的 st_size 是路径长度:不是目标大小。

      3. lstat vs stat:lstat 看链接本身;stat 看链接指向——写 find、ls 必须正确选择。

      4. st_blocks 单位是 512 字节:即使 FS 块大小是 4096。

      5. chmod 需要 owner 或 root:普通用户不能 chmod 别人的文件。

      6. ctime 不能直接修改:ctime 由内核在 i-node 状态变更时自动更新。

      7. O_NOATIME 需要 owner 权限:否则 EPERM;防止读取他人文件隐藏访问时间。

      8. st_blksize ≠ FS 块大小:是「最优 I/O 块大小」,可能因 FS 而异。

      9. chattr +i 后即使 root 也改不了:需要先 chattr -i(但 root 不能被锁在外面)。

      10. stat 比 ls 解析更可靠:ls 输出格式可能变;stat 字段稳定。

      11. 跨章衔接:第 14 章文件系统;本章文件属性;第 16 章扩展属性;第 17 章 ACL;第 18 章链接与目录。