Linux 系统编程 · 第 4 章:文件属性与元数据

Linux 系统编程 · 第 4 章:文件属性与元数据

本章深入讲解 Linux 文件的元数据体系:stat 系列函数、文件权限模型、inode 结构,以及时间戳、链接、目录操作等核心主题。


目录

  1. [stat 系列函数](#stat 系列函数)

  2. [struct stat 字段详解](#struct stat 字段详解)

  3. 文件类型判断

  4. 文件权限模型

  5. [inode 深度解析](#inode 深度解析)

  6. 文件时间戳

  7. 文件所有权

  8. 目录操作

  9. 综合实践


1. stat 系列函数

1.1 函数原型与区别

复制代码
#include <sys/stat.h>
#include <fcntl.h>      /* AT_* 常量 */
​
/* stat:通过路径获取文件元数据,跟随符号链接 */
int stat(const char *pathname, struct stat *statbuf);
​
/* lstat:通过路径获取文件元数据,不跟随符号链接
 * 对符号链接本身调用,返回链接自身的信息
 */
int lstat(const char *pathname, struct stat *statbuf);
​
/* fstat:通过已打开的文件描述符获取元数据
 * 不受路径变化影响,更安全(TOCTOU 安全)
 */
int fstat(int fd, struct stat *statbuf);
​
/* fstatat:相对于目录 fd 的路径版本(POSIX.1-2008)
 * flags 可为 AT_SYMLINK_NOFOLLOW(等价于 lstat)
 */
int fstatat(int dirfd, const char *pathname,
            struct stat *statbuf, int flags);
​
/* 返回值:0 成功,-1 失败(设置 errno)*/
复制代码
四个函数的区别:
─────────────────────────────────────────────────────────────
  stat()    路径 + 跟随符号链接  → 获取链接目标的信息
  lstat()   路径 + 不跟随链接   → 获取链接自身的信息
  fstat()   fd(已打开)        → 最安全,无 TOCTOU 问题
  fstatat() 目录fd + 相对路径   → 灵活,支持 AT_FDCWD
​
  TOCTOU(Time-Of-Check-Time-Of-Use)竞争:
  stat(path) → 检查权限 → open(path)
               ↑ 这两步之间文件可能被替换!
  解决:open() 后用 fstat(fd) 检查,而非 stat(path)
─────────────────────────────────────────────────────────────

1.2 基本使用示例

复制代码
/* 文件名:stat_basic.c
 * 演示 stat/lstat/fstat 的基本用法与区别
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <time.h>
​
/* 格式化输出 struct stat 的关键字段 */
void print_stat(const struct stat *st, const char *label) {
    char timebuf[32];
    struct tm *tm_info = localtime(&st->st_mtime);
    strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", tm_info);
​
    printf("【%s】\n", label);
    printf("  inode 号:    %lu\n",  (unsigned long)st->st_ino);
    printf("  文件大小:    %ld 字节\n", (long)st->st_size);
    printf("  硬链接数:    %lu\n",  (unsigned long)st->st_nlink);
    printf("  所有者 UID:  %u\n",   st->st_uid);
    printf("  所属组 GID:  %u\n",   st->st_gid);
    printf("  权限位:      %04o\n", (unsigned)(st->st_mode & 07777));
    printf("  修改时间:    %s\n",   timebuf);
    printf("  块大小:      %ld 字节\n", (long)st->st_blksize);
    printf("  占用块数:    %ld(×512字节)\n\n", (long)st->st_blocks);
}
​
int main(void) {
    struct stat st;
    const char *path = "/etc/hostname";
​
    /* ── stat:跟随符号链接 ── */
    if (stat(path, &st) == -1) {
        perror("stat");
        return 1;
    }
    print_stat(&st, "stat(\"/etc/hostname\")");
​
    /* ── 创建符号链接演示 lstat 的区别 ── */
    const char *link_path = "/tmp/hostname_link";
    unlink(link_path);   /* 清理旧链接 */
    symlink(path, link_path);
​
    struct stat st_link, st_target;
​
    /* stat 跟随链接,得到目标文件信息 */
    stat(link_path, &st_target);
    /* lstat 不跟随,得到链接自身信息 */
    lstat(link_path, &st_link);
​
    printf("符号链接 %s:\n", link_path);
    printf("  stat()  → inode=%lu,大小=%ld(目标文件)\n",
           (unsigned long)st_target.st_ino, (long)st_target.st_size);
    printf("  lstat() → inode=%lu,大小=%ld(链接自身,存储路径长度)\n",
           (unsigned long)st_link.st_ino, (long)st_link.st_size);
    printf("  inode 不同: %s\n\n",
           st_target.st_ino != st_link.st_ino ? "✓ 是" : "✗ 否");
​
    /* ── fstat:通过 fd 获取(TOCTOU 安全)── */
    int fd = open(path, O_RDONLY);
    if (fd == -1) { perror("open"); return 1; }
​
    struct stat st_fd;
    fstat(fd, &st_fd);
    printf("fstat(fd) inode=%lu(与 stat 一致: %s)\n",
           (unsigned long)st_fd.st_ino,
           st_fd.st_ino == st.st_ino ? "✓" : "✗");
    close(fd);
​
    /* ── fstatat:相对路径版本 ── */
    int dir_fd = open("/etc", O_RDONLY | O_DIRECTORY);
    struct stat st_at;
    /* AT_FDCWD 表示相对于当前工作目录 */
    fstatat(dir_fd, "hostname", &st_at, 0);
    printf("fstatat(dir_fd, \"hostname\") inode=%lu(一致: %s)\n",
           (unsigned long)st_at.st_ino,
           st_at.st_ino == st.st_ino ? "✓" : "✗");
    close(dir_fd);
​
    unlink(link_path);
    return 0;
}
复制代码
gcc -o stat_basic stat_basic.c
./stat_basic
# 输出示例:
# 【stat("/etc/hostname")】
#   inode 号:    131073
#   文件大小:    8 字节
#   硬链接数:    1
#   所有者 UID:  0
#   所属组 GID:  0
#   权限位:      0644
#   修改时间:    2024-01-15 10:30:00
#   块大小:      4096 字节
#   占用块数:    8(×512字节)
#
# 符号链接 /tmp/hostname_link:
#   stat()  → inode=131073,大小=8(目标文件)
#   lstat() → inode=987654,大小=13(链接自身,存储路径长度)
#   inode 不同: ✓ 是

2. struct stat 字段详解

2.1 完整结构体说明

复制代码
/* struct stat 的完整定义(x86_64 Linux)*/
struct stat {
    dev_t     st_dev;     /* 文件所在设备的设备号(主设备号+次设备号)*/
    ino_t     st_ino;     /* inode 号(文件在文件系统中的唯一标识)*/
    mode_t    st_mode;    /* 文件类型 + 权限位(16位)*/
    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; /* 文件系统 I/O 的最优块大小(通常 4096)*/
    blkcnt_t  st_blocks;  /* 实际分配的 512 字节块数(稀疏文件可能远小于 st_size/512)*/
    time_t    st_atime;   /* 最后访问时间(access time)*/
    time_t    st_mtime;   /* 最后修改时间(modification time,内容变化)*/
    time_t    st_ctime;   /* 最后状态变更时间(change time,元数据变化)*/
    /* 注意:Linux 没有"创建时间"(birth time),
     * 部分文件系统通过 statx() 提供 stx_btime */
};
​
/*
 * st_mode 的位布局(16位):
 *
 * 15 14 13 12 | 11 10  9 | 8  7  6 | 5  4  3 | 2  1  0
 * ─────────── | ──────── | ─────── | ─────── | ───────
 *  文件类型    |  特殊位  |  owner  |  group  |  other
 *  (4位)      | suid sgid sticky | r  w  x | r  w  x | r  w  x
 */

2.2 st_mode 位操作

复制代码
/* 文件名:stat_mode.c
 * 深入解析 st_mode 字段的每一位
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <pwd.h>
#include <grp.h>
​
/* 将 st_mode 转为完整的 ls -l 风格字符串(如 -rwxr-xr-x)*/
void mode_to_ls_string(mode_t mode, char *buf) {
    /* 文件类型字符 */
    switch (mode & S_IFMT) {
        case S_IFREG:  buf[0] = '-'; break;  /* 普通文件 */
        case S_IFDIR:  buf[0] = 'd'; break;  /* 目录 */
        case S_IFLNK:  buf[0] = 'l'; break;  /* 符号链接 */
        case S_IFCHR:  buf[0] = 'c'; break;  /* 字符设备 */
        case S_IFBLK:  buf[0] = 'b'; break;  /* 块设备 */
        case S_IFIFO:  buf[0] = 'p'; break;  /* 命名管道 */
        case S_IFSOCK: buf[0] = 's'; break;  /* 套接字 */
        default:       buf[0] = '?';
    }
​
    /* owner 权限(含 SUID)*/
    buf[1] = (mode & S_IRUSR) ? 'r' : '-';
    buf[2] = (mode & S_IWUSR) ? 'w' : '-';
    /* SUID:执行时以文件所有者身份运行(如 /usr/bin/passwd)*/
    if (mode & S_ISUID)
        buf[3] = (mode & S_IXUSR) ? 's' : 'S';  /* s=有执行权,S=无执行权 */
    else
        buf[3] = (mode & S_IXUSR) ? 'x' : '-';
​
    /* group 权限(含 SGID)*/
    buf[4] = (mode & S_IRGRP) ? 'r' : '-';
    buf[5] = (mode & S_IWGRP) ? 'w' : '-';
    /* SGID:执行时以文件所属组身份运行 */
    if (mode & S_ISGID)
        buf[6] = (mode & S_IXGRP) ? 's' : 'S';
    else
        buf[6] = (mode & S_IXGRP) ? 'x' : '-';
​
    /* other 权限(含 Sticky)*/
    buf[7] = (mode & S_IROTH) ? 'r' : '-';
    buf[8] = (mode & S_IWOTH) ? 'w' : '-';
    /* Sticky bit:目录中只有文件所有者才能删除自己的文件(如 /tmp)*/
    if (mode & S_ISVTX)
        buf[9] = (mode & S_IXOTH) ? 't' : 'T';
    else
        buf[9] = (mode & S_IXOTH) ? 'x' : '-';
​
    buf[10] = '\0';
}
​
/* 打印文件的完整 stat 信息(ls -l 风格)*/
void print_full_stat(const char *path) {
    struct stat st;
    if (lstat(path, &st) == -1) {
        perror(path);
        return;
    }
​
    char mode_str[11];
    mode_to_ls_string(st.st_mode, mode_str);
​
    /* 获取用户名和组名 */
    struct passwd *pw = getpwuid(st.st_uid);
    struct group  *gr = getgrgid(st.st_gid);
​
    /* 格式化时间 */
    char timebuf[32];
    struct tm *tm_info = localtime(&st.st_mtime);
    strftime(timebuf, sizeof(timebuf), "%b %d %H:%M", tm_info);
​
    /* 输出格式:权限 链接数 用户 组 大小 时间 文件名 */
    printf("%s %3lu %-8s %-8s %8ld %s %s",
           mode_str,
           (unsigned long)st.st_nlink,
           pw ? pw->pw_name : "unknown",
           gr ? gr->gr_name : "unknown",
           (long)st.st_size,
           timebuf,
           path);
​
    /* 符号链接显示目标 */
    if (S_ISLNK(st.st_mode)) {
        char link_target[256] = {0};
        ssize_t n = readlink(path, link_target, sizeof(link_target) - 1);
        if (n > 0) printf(" -> %s", link_target);
    }
    printf("\n");
}
​
int main(void) {
    printf("=== st_mode 位解析 ===\n\n");
​
    /* 演示各种特殊权限位 */
    const char *files[] = {
        "/etc/passwd",          /* 普通文件 rw-r--r-- */
        "/usr/bin/passwd",      /* SUID 文件 */
        "/tmp",                 /* Sticky bit 目录 */
        "/usr/bin/wall",        /* SGID 文件(如果存在)*/
        "/bin",                 /* 符号链接 */
        "/dev/null",            /* 字符设备 */
        NULL
    };
​
    printf("%-11s %3s %-8s %-8s %8s %s %s\n",
           "权限", "链接", "用户", "组", "大小", "时间", "文件名");
    printf("%s\n", "─────────────────────────────────────────────────────────");
​
    for (int i = 0; files[i]; i++) {
        print_full_stat(files[i]);
    }
​
    /* 解析 st_mode 的每一位 */
    printf("\n=== st_mode 位详细解析 ===\n");
    struct stat st;
    stat("/usr/bin/passwd", &st);
​
    printf("/usr/bin/passwd 的 st_mode = 0%04o (0x%04x)\n\n",
           (unsigned)(st.st_mode & 07777),
           (unsigned)st.st_mode);
​
    printf("文件类型位 (st_mode & S_IFMT):\n");
    printf("  S_IFREG (0100000): %s\n", S_ISREG(st.st_mode) ? "✓" : "✗");
​
    printf("\n特殊权限位:\n");
    printf("  SUID (04000): %s  ← 执行时以所有者身份运行\n",
           (st.st_mode & S_ISUID) ? "✓ 已设置" : "✗ 未设置");
    printf("  SGID (02000): %s\n",
           (st.st_mode & S_ISGID) ? "✓ 已设置" : "✗ 未设置");
    printf("  Sticky(01000): %s\n",
           (st.st_mode & S_ISVTX) ? "✓ 已设置" : "✗ 未设置");
​
    printf("\n普通权限位:\n");
    printf("  owner: %c%c%c\n",
           (st.st_mode & S_IRUSR) ? 'r' : '-',
           (st.st_mode & S_IWUSR) ? 'w' : '-',
           (st.st_mode & S_IXUSR) ? 'x' : '-');
    printf("  group: %c%c%c\n",
           (st.st_mode & S_IRGRP) ? 'r' : '-',
           (st.st_mode & S_IWGRP) ? 'w' : '-',
           (st.st_mode & S_IXGRP) ? 'x' : '-');
    printf("  other: %c%c%c\n",
           (st.st_mode & S_IROTH) ? 'r' : '-',
           (st.st_mode & S_IWOTH) ? 'w' : '-',
           (st.st_mode & S_IXOTH) ? 'x' : '-');
​
    return 0;
}
复制代码
gcc -o stat_mode stat_mode.c
./stat_mode
# 输出示例:
# 权限        链接 用户     组       大小 时间         文件名
# ─────────────────────────────────────────────────────────
# -rw-r--r--   1 root     root     2847 Jan 15 10:30 /etc/passwd
# -rwsr-xr-x   1 root     root    59976 Mar 23 08:00 /usr/bin/passwd
# drwxrwxrwt  18 root     root     4096 Jun 12 09:00 /tmp
# ...
#
# /usr/bin/passwd 的 st_mode = 04755 (0x81ed)
# 特殊权限位:
#   SUID (04000): ✓ 已设置  ← 执行时以所有者身份运行

3. 文件类型判断

3.1 七种文件类型

复制代码
/* 文件名:file_type.c
 * 演示 Linux 七种文件类型的判断方法
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
​
/* 文件类型判断宏(定义在 <sys/stat.h>):
 *   S_ISREG(m)   普通文件(regular file)
 *   S_ISDIR(m)   目录(directory)
 *   S_ISLNK(m)   符号链接(symbolic link)
 *   S_ISCHR(m)   字符设备(character device)
 *   S_ISBLK(m)   块设备(block device)
 *   S_ISFIFO(m)  命名管道/FIFO(named pipe)
 *   S_ISSOCK(m)  套接字(socket)
 */
​
typedef struct {
    const char *path;
    const char *desc;
} FileEntry;
​
/* 返回文件类型的中文描述 */
const char *get_file_type(mode_t mode) {
    if (S_ISREG(mode))  return "普通文件  (-)";
    if (S_ISDIR(mode))  return "目录      (d)";
    if (S_ISLNK(mode))  return "符号链接  (l)";
    if (S_ISCHR(mode))  return "字符设备  (c)";
    if (S_ISBLK(mode))  return "块设备    (b)";
    if (S_ISFIFO(mode)) return "命名管道  (p)";
    if (S_ISSOCK(mode)) return "套接字    (s)";
    return "未知类型";
}
​
int main(void) {
    printf("=== Linux 七种文件类型 ===\n\n");
​
    /* 创建各种类型的文件用于演示 */
    const char *fifo_path   = "/tmp/demo_fifo";
    const char *socket_path = "/tmp/demo_socket";
​
    /* 创建命名管道 */
    unlink(fifo_path);
    mkfifo(fifo_path, 0644);
​
    /* 创建 Unix 域套接字 */
    unlink(socket_path);
    int sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    struct sockaddr_un addr = { .sun_family = AF_UNIX };
    strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1);
    bind(sock_fd, (struct sockaddr *)&addr, sizeof(addr));
​
    /* 创建符号链接 */
    const char *link_path = "/tmp/demo_link";
    unlink(link_path);
    symlink("/etc/hostname", link_path);
​
    /* 测试文件列表 */
    FileEntry entries[] = {
        { "/etc/passwd",    "普通文本文件" },
        { "/etc",           "系统目录" },
        { link_path,        "符号链接" },
        { "/dev/tty",       "字符设备(终端)" },
        { "/dev/sda",       "块设备(磁盘,可能不存在)" },
        { fifo_path,        "命名管道(FIFO)" },
        { socket_path,      "Unix 域套接字" },
        { NULL, NULL }
    };
​
    printf("%-30s %-20s %s\n", "路径", "描述", "文件类型");
    printf("%s\n", "─────────────────────────────────────────────────────────");
​
    for (int i = 0; entries[i].path; i++) {
        struct stat st;
        /* 使用 lstat 避免跟随符号链接 */
        if (lstat(entries[i].path, &st) == -1) {
            printf("%-30s %-20s [不存在或无权限]\n",
                   entries[i].path, entries[i].desc);
            continue;
        }
        printf("%-30s %-20s %s\n",
               entries[i].path,
               entries[i].desc,
               get_file_type(st.st_mode));
    }
​
    /* 演示 S_IFMT 掩码的直接使用 */
    printf("\n=== S_IFMT 掩码直接比较 ===\n");
    struct stat st;
    lstat("/etc/passwd", &st);
    printf("/etc/passwd: st_mode & S_IFMT = 0x%04x\n",
           (unsigned)(st.st_mode & S_IFMT));
    printf("  S_IFREG = 0x%04x,匹配: %s\n",
           (unsigned)S_IFREG,
           (st.st_mode & S_IFMT) == S_IFREG ? "✓" : "✗");
​
    /* 清理 */
    close(sock_fd);
    unlink(fifo_path);
    unlink(socket_path);
    unlink(link_path);
    return 0;
}
复制代码
gcc -o file_type file_type.c
./file_type
# 输出示例:
# 路径                           描述                 文件类型
# ─────────────────────────────────────────────────────────
# /etc/passwd                    普通文本文件          普通文件  (-)
# /etc                           系统目录              目录      (d)
# /tmp/demo_link                 符号链接              符号链接  (l)
# /dev/tty                       字符设备(终端)       字符设备  (c)
# /tmp/demo_fifo                 命名管道(FIFO)       命名管道  (p)
# /tmp/demo_socket               Unix 域套接字         套接字    (s)

4. 文件权限模型

4.1 权限检查机制

复制代码
Linux 权限检查流程(内核执行顺序):
─────────────────────────────────────────────────────────────
  进程访问文件时,内核按以下顺序检查:

  1. 进程是否是 root(UID=0)?
     → 是:绕过所有权限检查(除执行权限外)
     → 否:继续

  2. 进程的 EUID 是否等于文件的 UID(所有者)?
     → 是:使用 owner 权限位(rwx 的高3位)
     → 否:继续

  3. 进程的 EGID 或任一附加组是否等于文件的 GID?
     → 是:使用 group 权限位(rwx 的中3位)
     → 否:使用 other 权限位(rwx 的低3位)

  注意:检查是互斥的,匹配第一个就停止!
  例如:文件权限 0640,进程是文件所有者但不在文件组中
        → 使用 owner 权限(rw-),不会再检查 group 权限
─────────────────────────────────────────────────────────────

4.2 chmod / fchmod --- 修改权限

复制代码
/* 文件名:chmod_demo.c
 * 演示权限修改、access 检查、特殊权限位
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

/* 打印文件权限(八进制 + rwx 格式)*/
void print_perms(const char *path) {
    struct stat st;
    if (stat(path, &st) == -1) { perror(path); return; }

    char perm[11];
    mode_t m = st.st_mode;
    perm[0]  = S_ISDIR(m) ? 'd' : '-';
    perm[1]  = (m & S_IRUSR) ? 'r' : '-';
    perm[2]  = (m & S_IWUSR) ? 'w' : '-';
    perm[3]  = (m & S_ISUID) ? ((m & S_IXUSR) ? 's' : 'S')
                              : ((m & S_IXUSR) ? 'x' : '-');
    perm[4]  = (m & S_IRGRP) ? 'r' : '-';
    perm[5]  = (m & S_IWGRP) ? 'w' : '-';
    perm[6]  = (m & S_ISGID) ? ((m & S_IXGRP) ? 's' : 'S')
                              : ((m & S_IXGRP) ? 'x' : '-');
    perm[7]  = (m & S_IROTH) ? 'r' : '-';
    perm[8]  = (m & S_IWOTH) ? 'w' : '-';
    perm[9]  = (m & S_ISVTX) ? ((m & S_IXOTH) ? 't' : 'T')
                              : ((m & S_IXOTH) ? 'x' : '-');
    perm[10] = '\0';
    printf("  %s  %04o  %s\n", perm, (unsigned)(m & 07777), path);
}

int main(void) {
    const char *path = "/tmp/chmod_test.txt";

    /* 创建测试文件 */
    int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    close(fd);

    printf("=== chmod 权限修改演示 ===\n");
    printf("%-12s %-6s %s\n", "权限字符串", "八进制", "路径");
    printf("%s\n", "─────────────────────────────────────");

    /* 测试各种权限值 */
    mode_t modes[] = { 0644, 0755, 0600, 0400, 0777, 0000 };
    for (int i = 0; i < 6; i++) {
        chmod(path, modes[i]);
        print_perms(path);
    }

    /* ── access():检查进程是否有权限访问文件 ── */
    printf("\n=== access() 权限检查 ===\n");
    chmod(path, 0644);

    /* access 使用真实 UID/GID(而非有效 UID/GID)检查 */
    struct {
        int mode;
        const char *desc;
    } checks[] = {
        { F_OK, "文件存在(F_OK)" },
        { R_OK, "可读(R_OK)" },
        { W_OK, "可写(W_OK)" },
        { X_OK, "可执行(X_OK)" },
        { R_OK | W_OK, "可读写(R_OK|W_OK)" },
        { 0, NULL }
    };

    for (int i = 0; checks[i].desc; i++) {
        int ret = access(path, checks[i].mode);
        printf("  access(\"%s\", %s): %s\n",
               path, checks[i].desc,
               ret == 0 ? "✓ 允许" : "✗ 拒绝");
    }

    /* ── 特殊权限位演示 ── */
    printf("\n=== 特殊权限位 ===\n");

    /* SUID(Set-UID):执行时以文件所有者身份运行 */
    chmod(path, 04755);   /* 0755 + SUID */
    printf("设置 SUID (04755):\n");
    print_perms(path);
    printf("  → 执行时进程 EUID = 文件所有者 UID\n");
    printf("  → 典型例子: /usr/bin/passwd(需要修改 /etc/shadow)\n");

    /* SGID(Set-GID):执行时以文件所属组身份运行 */
    chmod(path, 02755);   /* 0755 + SGID */
    printf("设置 SGID (02755):\n");
    print_perms(path);
    printf("  → 执行时进程 EGID = 文件所属组 GID\n");
    printf("  → 目录上的 SGID:新建文件继承目录的组\n");

    /* Sticky bit:目录中只有文件所有者才能删除自己的文件 */
    chmod(path, 01777);   /* 0777 + Sticky */
    printf("设置 Sticky (01777):\n");
    print_perms(path);
    printf("  → 典型例子: /tmp(任何人可写,但只能删自己的文件)\n");

    /* ── fchmod:通过 fd 修改权限(更安全)── */
    printf("\n=== fchmod(通过 fd 修改)===\n");
    fd = open(path, O_RDWR);
    fchmod(fd, 0644);
    printf("fchmod(fd, 0644) 后:\n");
    print_perms(path);
    close(fd);

    unlink(path);
    return 0;
}
复制代码
gcc -o chmod_demo chmod_demo.c
./chmod_demo
# 输出示例:
# === chmod 权限修改演示 ===
# 权限字符串   八进制  路径
# ─────────────────────────────────────
#   -rw-r--r--  0644  /tmp/chmod_test.txt
#   -rwxr-xr-x  0755  /tmp/chmod_test.txt
#   -rw-------  0600  /tmp/chmod_test.txt
#   -r--------  0400  /tmp/chmod_test.txt
#   -rwxrwxrwx  0777  /tmp/chmod_test.txt
#   ----------  0000  /tmp/chmod_test.txt
#
# === access() 权限检查 ===
#   access(..., 文件存在(F_OK)):    ✓ 允许
#   access(..., 可读(R_OK)):        ✓ 允许
#   access(..., 可写(W_OK)):        ✓ 允许
#   access(..., 可执行(X_OK)):      ✗ 拒绝

4.3 umask --- 权限掩码

复制代码
/* 文件名:umask_demo.c
 * 深入演示 umask 的工作原理
 */
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

int main(void) {
    printf("=== umask 工作原理 ===\n\n");

    /*
     * umask 是进程级别的权限掩码
     * 新建文件的实际权限 = 请求权限 & ~umask
     *
     * 常见 umask 值:
     *   022  → 屏蔽组写(w)和其他写(w)  → 文件:644,目录:755
     *   027  → 屏蔽组写和其他所有权限  → 文件:640,目录:750
     *   077  → 只有所有者有权限        → 文件:600,目录:700
     */

    struct {
        mode_t umask_val;
        mode_t req_mode;
        const char *desc;
    } tests[] = {
        { 0022, 0666, "默认 umask=022,创建文件(请求0666)" },
        { 0022, 0777, "默认 umask=022,创建目录(请求0777)" },
        { 0027, 0666, "安全 umask=027,创建文件" },
        { 0077, 0666, "严格 umask=077,创建文件" },
        { 0000, 0666, "无掩码 umask=000,创建文件" },
        { 0, 0, NULL }
    };

    printf("%-40s  请求   umask  实际权限\n", "场景");
    printf("%s\n", "─────────────────────────────────────────────────────────");

    for (int i = 0; tests[i].desc; i++) {
        mode_t old = umask(tests[i].umask_val);
        mode_t actual = tests[i].req_mode & ~tests[i].umask_val;
        printf("%-40s  %04o   %04o   %04o\n",
               tests[i].desc,
               tests[i].req_mode,
               tests[i].umask_val,
               actual);
        umask(old);   /* 恢复 */
    }

    /* 实际创建文件验证 */
    printf("\n=== 实际创建文件验证 ===\n");
    mode_t old_umask = umask(0022);

    const char *path = "/tmp/umask_verify.txt";
    int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    close(fd);

    struct stat st;
    stat(path, &st);
    printf("umask=022,请求0666,实际权限: %04o(期望0644): %s\n",
           (unsigned)(st.st_mode & 0777),
           (st.st_mode & 0777) == 0644 ? "✓" : "✗");

    unlink(path);
    umask(old_umask);

    /* umask 对 chmod 无效! */
    printf("\n注意:umask 只影响文件创建,不影响 chmod!\n");
    fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    close(fd);
    chmod(path, 0666);   /* chmod 直接设置,不受 umask 影响 */
    stat(path, &st);
    printf("chmod(0666) 后实际权限: %04o(umask 无效)\n",
           (unsigned)(st.st_mode & 0777));
    unlink(path);

    return 0;
}
复制代码
gcc -o umask_demo umask_demo.c
./umask_demo
# 输出示例:
# 场景                                      请求   umask  实际权限
# ─────────────────────────────────────────────────────────
# 默认 umask=022,创建文件(请求0666)       0666   0022   0644
# 默认 umask=022,创建目录(请求0777)       0777   0022   0755
# 安全 umask=027,创建文件                   0666   0027   0640
# 严格 umask=077,创建文件                   0666   0077   0600
# 无掩码 umask=000,创建文件                 0666   0000   0666

5. inode 深度解析

5.1 inode 的本质

复制代码
inode(Index Node)是文件系统中存储文件元数据的数据结构:

磁盘布局(ext4 文件系统):
─────────────────────────────────────────────────────────────
  超级块(Superblock)
  ├── 块组描述符表
  ├── inode 位图(哪些 inode 已使用)
  ├── 块位图(哪些数据块已使用)
  ├── inode 表(存储所有 inode 结构体)
  │   ├── inode 1(根目录 /)
  │   ├── inode 2(/etc)
  │   ├── inode 3(/etc/passwd)
  │   └── ...
  └── 数据块(存储文件实际内容)

inode 存储的信息:
  ✓ 文件类型和权限(st_mode)
  ✓ 所有者 UID/GID
  ✓ 文件大小
  ✓ 时间戳(atime/mtime/ctime)
  ✓ 硬链接计数
  ✓ 数据块指针(直接/间接/双重间接)

inode 不存储的信息:
  ✗ 文件名(文件名存储在目录项中!)
  ✗ 文件路径
─────────────────────────────────────────────────────────────

5.2 硬链接与软链接

复制代码
/* 文件名:link_demo.c
 * 深入演示硬链接与软链接的区别
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

void print_inode_info(const char *path) {
    struct stat st;
    if (lstat(path, &st) == -1) {
        printf("  %-30s [不存在]\n", path);
        return;
    }
    printf("  %-30s inode=%-8lu 大小=%-6ld 硬链接数=%lu %s\n",
           path,
           (unsigned long)st.st_ino,
           (long)st.st_size,
           (unsigned long)st.st_nlink,
           S_ISLNK(st.st_mode) ? "(符号链接)" : "");
}

int main(void) {
    const char *original  = "/tmp/link_original.txt";
    const char *hard_link = "/tmp/link_hard.txt";
    const char *soft_link = "/tmp/link_soft.txt";

    /* 创建原始文件 */
    int fd = open(original, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    write(fd, "原始文件内容\n", 14);
    close(fd);

    printf("=== 创建链接前 ===\n");
    print_inode_info(original);

    /* ── 硬链接(link)── */
    link(original, hard_link);
    printf("\n=== 创建硬链接后 ===\n");
    print_inode_info(original);
    print_inode_info(hard_link);
    printf("  → 两者 inode 相同,硬链接数变为 2\n");

    /* ── 软链接(symlink)── */
    symlink(original, soft_link);
    printf("\n=== 创建软链接后 ===\n");
    print_inode_info(original);
    print_inode_info(hard_link);
    print_inode_info(soft_link);
    printf("  → 软链接有独立 inode,大小=路径字符串长度\n");

    /* ── 修改内容,观察链接行为 ── */
    printf("\n=== 通过硬链接修改内容 ===\n");
    fd = open(hard_link, O_WRONLY | O_APPEND);
    write(fd, "通过硬链接追加\n", 16);
    close(fd);

    /* 原始文件也能看到修改(共享同一 inode 和数据块)*/
    printf("原始文件内容(通过硬链接修改后):\n");
    system("cat /tmp/link_original.txt");

    /* ── 删除原始文件,硬链接仍然有效 ── */
    printf("\n=== 删除原始文件 ===\n");
    unlink(original);
    printf("删除 original 后:\n");
    print_inode_info(original);    /* 不存在 */
    print_inode_info(hard_link);   /* 仍然有效,硬链接数变为 1 */
    printf("硬链接仍可读取:\n");
    system("cat /tmp/link_hard.txt");

    /* 软链接变成悬空链接(dangling symlink)*/
    printf("\n软链接变为悬空链接:\n");
    char target[256] = {0};
    readlink(soft_link, target, sizeof(target) - 1);
    printf("  软链接目标: %s(已不存在)\n", target);
    fd = open(soft_link, O_RDONLY);
    printf("  通过软链接打开: %s\n",
           fd == -1 ? "失败(悬空链接)" : "成功");
    if (fd != -1) close(fd);

    /* ── 硬链接的限制 ── */
    printf("\n=== 硬链接的限制 ===\n");
    /* 1. 不能跨文件系统 */
    int ret = link("/tmp/link_hard.txt", "/proc/test_hard_link");
    printf("跨文件系统硬链接: %s(errno=%d)\n",
           ret == -1 ? "失败(预期)" : "成功", errno);

    /* 2. 不能对目录创建硬链接(防止循环)*/
    ret = link("/tmp", "/tmp/link_to_dir");
    printf("目录硬链接: %s(errno=%d)\n",
           ret == -1 ? "失败(预期)" : "成功", errno);

    /* 清理 */
    unlink(hard_link);
    unlink(soft_link);
    return 0;
}
复制代码
gcc -o link_demo link_demo.c
./link_demo
# 输出示例:
# === 创建链接前 ===
#   /tmp/link_original.txt         inode=1234567  大小=14     硬链接数=1
#
# === 创建硬链接后 ===
#   /tmp/link_original.txt         inode=1234567  大小=14     硬链接数=2
#   /tmp/link_hard.txt             inode=1234567  大小=14     硬链接数=2
#   → 两者 inode 相同,硬链接数变为 2
#
# === 创建软链接后 ===
#   /tmp/link_original.txt         inode=1234567  大小=14     硬链接数=2
#   /tmp/link_hard.txt             inode=1234567  大小=14     硬链接数=2
#   /tmp/link_soft.txt             inode=9876543  大小=22     硬链接数=1 (符号链接)

5.3 inode 耗尽问题

复制代码
# 查看文件系统 inode 使用情况
df -i
# 输出示例:
# Filesystem      Inodes  IUsed   IFree IUse% Mounted on
# /dev/sda1      6553600 123456 6430144    2% /
# tmpfs           512000    456  511544    1% /tmp

# 查看某目录的 inode 号
ls -i /etc/passwd
# 输出:131073 /etc/passwd

# 查看文件系统详细信息(ext4)
# tune2fs -l /dev/sda1 | grep -i inode

# inode 耗尽的典型场景:大量小文件(如邮件队列、临时文件)
# 即使磁盘空间充足,inode 耗尽也无法创建新文件!

6. 文件时间戳

6.1 三种时间戳

复制代码
Linux 文件的三种时间戳:
─────────────────────────────────────────────────────────────
  atime(access time):最后访问时间
    → 读取文件内容时更新(read)
    → 挂载选项 noatime/relatime 可禁用(提升性能)

  mtime(modification time):最后修改时间
    → 文件内容变化时更新(write、truncate)
    → ls -l 默认显示的时间

  ctime(change time):最后状态变更时间
    → 元数据变化时更新(chmod、chown、rename、link)
    → 内容变化也会更新 ctime(因为 mtime 变了)
    → 注意:ctime 不是"创建时间"!

  注意:Linux 传统上没有"创建时间"(birth time)
    → ext4/btrfs 等文件系统支持,通过 statx() 获取
─────────────────────────────────────────────────────────────
复制代码
/* 文件名:timestamp_demo.c
 * 演示文件时间戳的变化规律
 */
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <time.h>
#include <utime.h>    /* utime */
#include <sys/time.h> /* utimes */

/* 格式化时间戳(纳秒精度)*/
void print_timestamps(const char *path) {
    struct stat st;
    if (stat(path, &st) == -1) { perror(path); return; }

    char buf[32];
    struct tm *tm_info;

    printf("  文件: %s\n", path);

#define PRINT_TIME(label, ts) \
    tm_info = localtime(&(ts)); \
    strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tm_info); \
    printf("  %-8s %s\n", label, buf);

    PRINT_TIME("atime:", st.st_atime);
    PRINT_TIME("mtime:", st.st_mtime);
    PRINT_TIME("ctime:", st.st_ctime);
    printf("\n");
}

int main(void) {
    const char *path = "/tmp/timestamp_test.txt";

    /* 创建文件 */
    int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644);
    write(fd, "initial content\n", 16);
    close(fd);

    printf("=== 初始时间戳 ===\n");
    print_timestamps(path);
    sleep(1);   /* 等待1秒,使时间戳变化可见 */

    /* ── 读取文件 → 更新 atime ── */
    printf("=== 读取文件后(atime 更新)===\n");
    fd = open(path, O_RDONLY);
    char buf[32];
    read(fd, buf, sizeof(buf));
    close(fd);
    print_timestamps(path);
    sleep(1);

    /* ── 写入文件 → 更新 mtime 和 ctime ── */
    printf("=== 写入文件后(mtime + ctime 更新)===\n");
    fd = open(path, O_WRONLY | O_APPEND);
    write(fd, "appended\n", 9);
    close(fd);
    print_timestamps(path);
    sleep(1);

    /* ── chmod → 只更新 ctime ── */
    printf("=== chmod 后(只更新 ctime)===\n");
    chmod(path, 0755);
    print_timestamps(path);
    sleep(1);

    /* ── utime:手动设置时间戳 ── */
    printf("=== utime 手动设置时间戳 ===\n");
    struct utimbuf times = {
        .actime  = 1000000000,   /* 2001-09-09 01:46:40 UTC */
        .modtime = 1000000000,
    };
    utime(path, &times);
    print_timestamps(path);

    /* ── utimensat:纳秒精度设置(推荐)── */
    printf("=== utimensat 纳秒精度设置 ===\n");
    struct timespec ts[2] = {
        { .tv_sec = 1700000000, .tv_nsec = 123456789 },  /* atime */
        { .tv_sec = 1700000000, .tv_nsec = 987654321 },  /* mtime */
    };
    utimensat(AT_FDCWD, path, ts, 0);
    print_timestamps(path);

    /* UTIME_NOW:设置为当前时间 */
    struct timespec ts_now[2] = {
        { .tv_nsec = UTIME_NOW },    /* atime = 当前时间 */
        { .tv_nsec = UTIME_OMIT },   /* mtime = 不修改 */
    };
    utimensat(AT_FDCWD, path, ts_now, 0);
    printf("=== utimensat UTIME_NOW(只更新 atime)===\n");
    print_timestamps(path);

    unlink(path);
    return 0;
}
复制代码
gcc -o timestamp_demo timestamp_demo.c
./timestamp_demo
# 输出示例:
# === 初始时间戳 ===
#   文件: /tmp/timestamp_test.txt
#   atime:   2026-06-12 10:00:00
#   mtime:   2026-06-12 10:00:00
#   ctime:   2026-06-12 10:00:00
#
# === 写入文件后(mtime + ctime 更新)===
#   atime:   2026-06-12 10:00:00
#   mtime:   2026-06-12 10:00:02  ← 更新
#   ctime:   2026-06-12 10:00:02  ← 更新

7. 文件所有权

7.1 chown / fchown --- 修改所有者

复制代码
/* 文件名:chown_demo.c
 * 演示文件所有权的查询与修改
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <pwd.h>
#include <grp.h>

/* 通过 UID 获取用户名 */
const char *uid_to_name(uid_t uid) {
    struct passwd *pw = getpwuid(uid);
    return pw ? pw->pw_name : "unknown";
}

/* 通过 GID 获取组名 */
const char *gid_to_name(gid_t gid) {
    struct group *gr = getgrgid(gid);
    return gr ? gr->gr_name : "unknown";
}

/* 通过用户名获取 UID */
uid_t name_to_uid(const char *name) {
    struct passwd *pw = getpwnam(name);
    return pw ? pw->pw_uid : (uid_t)-1;
}

int main(void) {
    const char *path = "/tmp/chown_test.txt";

    /* 创建测试文件 */
    int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    close(fd);

    /* 查看当前所有权 */
    struct stat st;
    stat(path, &st);
    printf("=== 当前文件所有权 ===\n");
    printf("  UID=%u (%s),GID=%u (%s)\n",
           st.st_uid, uid_to_name(st.st_uid),
           st.st_gid, gid_to_name(st.st_gid));

    /* 查看当前进程的身份 */
    printf("\n=== 当前进程身份 ===\n");
    printf("  真实 UID (RUID): %u (%s)\n",
           getuid(), uid_to_name(getuid()));
    printf("  有效 UID (EUID): %u (%s)\n",
           geteuid(), uid_to_name(geteuid()));
    printf("  真实 GID (RGID): %u (%s)\n",
           getgid(), gid_to_name(getgid()));
    printf("  有效 GID (EGID): %u (%s)\n",
           getegid(), gid_to_name(getegid()));

    /* chown:修改所有者(通常需要 root 权限)
     * 传入 -1 表示不修改该字段
     */
    printf("\n=== chown 演示 ===\n");

    /* 只修改 GID(不修改 UID)*/
    if (chown(path, (uid_t)-1, getgid()) == 0) {
        stat(path, &st);
        printf("chown(-1, gid) 成功: UID=%u, GID=%u\n",
               st.st_uid, st.st_gid);
    } else {
        printf("chown 失败(可能需要 root): %s\n", strerror(errno));
    }

    /* fchown:通过 fd 修改(更安全,避免 TOCTOU)*/
    fd = open(path, O_RDWR);
    if (fchown(fd, (uid_t)-1, getgid()) == 0) {
        printf("fchown(fd, -1, gid) 成功\n");
    }
    close(fd);

    /* ── 演示 SUID/SGID 被 chown 清除 ── */
    printf("\n=== chown 清除 SUID/SGID ===\n");
    chmod(path, 04755);   /* 设置 SUID */
    stat(path, &st);
    printf("设置 SUID 后权限: %04o\n", (unsigned)(st.st_mode & 07777));

    /* 非 root 用户 chown 后,SUID/SGID 会被自动清除(安全机制)*/
    chown(path, getuid(), getgid());
    stat(path, &st);
    printf("chown 后权限: %04o(SUID 被清除)\n",
           (unsigned)(st.st_mode & 07777));

    unlink(path);
    return 0;
}
复制代码
gcc -o chown_demo chown_demo.c
./chown_demo
# 输出示例:
# === 当前文件所有权 ===
#   UID=1000 (alice),GID=1000 (alice)
#
# === 当前进程身份 ===
#   真实 UID (RUID): 1000 (alice)
#   有效 UID (EUID): 1000 (alice)
#   真实 GID (RGID): 1000 (alice)
#   有效 GID (EGID): 1000 (alice)
#
# === chown 清除 SUID/SGID ===
# 设置 SUID 后权限: 4755
# chown 后权限: 0755(SUID 被清除)

8. 目录操作

8.1 mkdir / rmdir / rename

复制代码
/* 文件名:dir_ops.c
 * 演示目录的创建、删除、重命名及遍历
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <sys/stat.h>

/* 递归创建目录(类似 mkdir -p)*/
int mkdir_p(const char *path, mode_t mode) {
    char tmp[256];
    char *p;
    size_t len;

    snprintf(tmp, sizeof(tmp), "%s", path);
    len = strlen(tmp);
    if (tmp[len - 1] == '/') tmp[len - 1] = '\0';

    for (p = tmp + 1; *p; p++) {
        if (*p == '/') {
            *p = '\0';
            if (mkdir(tmp, mode) == -1 && errno != EEXIST)
                return -1;
            *p = '/';
        }
    }
    return mkdir(tmp, mode) == -1 && errno != EEXIST ? -1 : 0;
}

/* 遍历目录(opendir/readdir)*/
void list_directory(const char *path, int show_hidden) {
    DIR *dir = opendir(path);
    if (!dir) { perror(path); return; }

    struct dirent *entry;
    struct stat st;
    char full_path[512];
    int count = 0;

    printf("目录 %s 的内容:\n", path);
    printf("%-20s %-12s %-10s %s\n", "名称", "类型", "大小", "权限");
    printf("%s\n", "─────────────────────────────────────────────");

    while ((entry = readdir(dir)) != NULL) {
        /* 跳过隐藏文件(除非 show_hidden)*/
        if (!show_hidden && entry->d_name[0] == '.') continue;

        snprintf(full_path, sizeof(full_path),
                 "%s/%s", path, entry->d_name);
        if (lstat(full_path, &st) == -1) continue;

        /* 文件类型 */
        const char *type;
        switch (entry->d_type) {
            case DT_REG:  type = "普通文件"; break;
            case DT_DIR:  type = "目录";     break;
            case DT_LNK:  type = "符号链接"; break;
            case DT_CHR:  type = "字符设备"; break;
            case DT_BLK:  type = "块设备";   break;
            case DT_FIFO: type = "管道";     break;
            case DT_SOCK: type = "套接字";   break;
            default:      type = "未知";
        }

        printf("%-20s %-12s %-10ld %04o\n",
               entry->d_name, type,
               (long)st.st_size,
               (unsigned)(st.st_mode & 0777));
        count++;
    }
    printf("共 %d 个条目\n\n", count);
    closedir(dir);
}

int main(void) {
    const char *base = "/tmp/dir_ops_demo";

    /* ── mkdir:创建目录 ── */
    printf("=== mkdir 创建目录 ===\n");
    if (mkdir(base, 0755) == 0) {
        printf("mkdir(%s, 0755) 成功\n", base);
    } else if (errno == EEXIST) {
        printf("目录已存在\n");
    } else {
        perror("mkdir");
        return 1;
    }

    /* ── mkdir_p:递归创建 ── */
    char deep_path[256];
    snprintf(deep_path, sizeof(deep_path), "%s/a/b/c", base);
    if (mkdir_p(deep_path, 0755) == 0) {
        printf("mkdir_p(%s) 成功\n", deep_path);
    }

    /* 在目录中创建一些文件 */
    char fpath[256];
    const char *fnames[] = { "file1.txt", "file2.log", ".hidden", NULL };
    for (int i = 0; fnames[i]; i++) {
        snprintf(fpath, sizeof(fpath), "%s/%s", base, fnames[i]);
        int fd = open(fpath, O_WRONLY | O_CREAT | O_TRUNC, 0644);
        write(fd, fnames[i], strlen(fnames[i]));
        close(fd);
    }

    /* ── opendir/readdir:遍历目录 ── */
    printf("\n=== opendir/readdir 遍历目录 ===\n");
    list_directory(base, 0);   /* 不显示隐藏文件 */

    /* ── rename:重命名/移动 ── */
    printf("=== rename 重命名 ===\n");
    char old_path[256], new_path[256];
    snprintf(old_path, sizeof(old_path), "%s/file1.txt", base);
    snprintf(new_path, sizeof(new_path), "%s/file1_renamed.txt", base);

    if (rename(old_path, new_path) == 0) {
        printf("rename 成功: file1.txt → file1_renamed.txt\n");
    }

    /* rename 也可以移动文件(同一文件系统内是原子操作)*/
    snprintf(old_path, sizeof(old_path), "%s/file2.log", base);
    snprintf(new_path, sizeof(new_path), "%s/a/file2_moved.log", base);
    if (rename(old_path, new_path) == 0) {
        printf("rename 移动: file2.log → a/file2_moved.log\n");
    }

    /* ── rmdir:删除空目录 ── */
    printf("\n=== rmdir 删除目录 ===\n");
    snprintf(fpath, sizeof(fpath), "%s/a/b/c", base);
    if (rmdir(fpath) == 0) {
        printf("rmdir(%s) 成功\n", fpath);
    }

    /* 非空目录无法删除 */
    if (rmdir(base) == -1) {
        printf("rmdir(%s) 失败(非空): %s\n", base, strerror(errno));
    }

    /* 清理 */
    system("rm -rf /tmp/dir_ops_demo");
    return 0;
}
复制代码
gcc -o dir_ops dir_ops.c
./dir_ops
# 输出示例:
# === mkdir 创建目录 ===
# mkdir(/tmp/dir_ops_demo, 0755) 成功
# mkdir_p(/tmp/dir_ops_demo/a/b/c) 成功
#
# === opendir/readdir 遍历目录 ===
# 目录 /tmp/dir_ops_demo 的内容:
# 名称                 类型         大小       权限
# ─────────────────────────────────────────────
# a                    目录         4096       755
# file1.txt            普通文件     9          644
# file2.log            普通文件     9          644
# 共 3 个条目

8.2 getcwd / chdir --- 工作目录

复制代码
/* 文件名:cwd_demo.c
 * 演示工作目录的获取与切换
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>

int main(void) {
    char cwd[1024];

    /* ── getcwd:获取当前工作目录 ── */
    if (getcwd(cwd, sizeof(cwd)) == NULL) {
        perror("getcwd");
        return 1;
    }
    printf("当前工作目录: %s\n", cwd);

    /* ── chdir:切换工作目录 ── */
    if (chdir("/tmp") == 0) {
        getcwd(cwd, sizeof(cwd));
        printf("chdir(\"/tmp\") 后: %s\n", cwd);
    }

    /* 相对路径操作(基于当前工作目录)*/
    mkdir("cwd_test_dir", 0755);
    chdir("cwd_test_dir");
    getcwd(cwd, sizeof(cwd));
    printf("进入子目录后: %s\n", cwd);

    /* 返回上级目录 */
    chdir("..");
    getcwd(cwd, sizeof(cwd));
    printf("返回上级后: %s\n", cwd);

    /* ── fchdir:通过 fd 切换目录(更安全)── */
    printf("\n=== fchdir 演示 ===\n");
    int saved_dir = open(".", O_RDONLY | O_DIRECTORY);   /* 保存当前目录 */
    printf("保存当前目录 fd=%d\n", saved_dir);

    chdir("/etc");
    getcwd(cwd, sizeof(cwd));
    printf("切换到 /etc: %s\n", cwd);

    /* 通过 fd 返回之前的目录 */
    fchdir(saved_dir);
    close(saved_dir);
    getcwd(cwd, sizeof(cwd));
    printf("通过 fchdir 返回: %s\n", cwd);

    /* 清理 */
    rmdir("/tmp/cwd_test_dir");
    return 0;
}
复制代码
gcc -o cwd_demo cwd_demo.c
./cwd_demo
# 输出示例:
# 当前工作目录: /workspace
# chdir("/tmp") 后: /tmp
# 进入子目录后: /tmp/cwd_test_dir
# 返回上级后: /tmp
#
# === fchdir 演示 ===
# 保存当前目录 fd=3
# 切换到 /etc: /etc
# 通过 fchdir 返回: /tmp

9. 综合实践

9.1 实现 ls -l 命令

复制代码
/* 文件名:my_ls.c
 * 实现类似 ls -l 的目录列表工具
 * 支持:文件类型、权限、链接数、用户、组、大小、时间、文件名
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <dirent.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <pwd.h>
#include <grp.h>
#include <time.h>

/* 将 mode_t 转为 ls -l 风格字符串 */
static void mode_str(mode_t m, char *buf) {
    buf[0]  = S_ISDIR(m)?'d':S_ISLNK(m)?'l':S_ISCHR(m)?'c':
              S_ISBLK(m)?'b':S_ISFIFO(m)?'p':S_ISSOCK(m)?'s':'-';
    buf[1]  = (m & S_IRUSR) ? 'r' : '-';
    buf[2]  = (m & S_IWUSR) ? 'w' : '-';
    buf[3]  = (m & S_ISUID) ? ((m & S_IXUSR) ? 's':'S')
                             : ((m & S_IXUSR) ? 'x':'-');
    buf[4]  = (m & S_IRGRP) ? 'r' : '-';
    buf[5]  = (m & S_IWGRP) ? 'w' : '-';
    buf[6]  = (m & S_ISGID) ? ((m & S_IXGRP) ? 's':'S')
                             : ((m & S_IXGRP) ? 'x':'-');
    buf[7]  = (m & S_IROTH) ? 'r' : '-';
    buf[8]  = (m & S_IWOTH) ? 'w' : '-';
    buf[9]  = (m & S_ISVTX) ? ((m & S_IXOTH) ? 't':'T')
                             : ((m & S_IXOTH) ? 'x':'-');
    buf[10] = '\0';
}

/* 格式化文件大小(人类可读)*/
static void human_size(off_t size, char *buf) {
    if      (size >= 1073741824LL) snprintf(buf, 8, "%.1fG", size/1073741824.0);
    else if (size >= 1048576LL)    snprintf(buf, 8, "%.1fM", size/1048576.0);
    else if (size >= 1024LL)       snprintf(buf, 8, "%.1fK", size/1024.0);
    else                           snprintf(buf, 8, "%ldB",  (long)size);
}

/* 打印单个文件的 ls -l 行 */
static void print_entry(const char *dir, const char *name) {
    char full[512];
    snprintf(full, sizeof(full), "%s/%s", dir, name);

    struct stat st;
    if (lstat(full, &st) == -1) return;

    char mstr[11], timebuf[16], szstr[8];
    mode_str(st.st_mode, mstr);
    human_size(st.st_size, szstr);

    /* 时间格式:6个月内显示时间,否则显示年份 */
    time_t now = time(NULL);
    struct tm *tm_info = localtime(&st.st_mtime);
    if (now - st.st_mtime < 6 * 30 * 86400)
        strftime(timebuf, sizeof(timebuf), "%b %d %H:%M", tm_info);
    else
        strftime(timebuf, sizeof(timebuf), "%b %d  %Y", tm_info);

    /* 用户名和组名 */
    struct passwd *pw = getpwuid(st.st_uid);
    struct group  *gr = getgrgid(st.st_gid);
    char uid_str[16], gid_str[16];
    snprintf(uid_str, sizeof(uid_str), "%s",
             pw ? pw->pw_name : "unknown");
    snprintf(gid_str, sizeof(gid_str), "%s",
             gr ? gr->gr_name : "unknown");

    printf("%s %3lu %-8s %-8s %6s %s %s",
           mstr, (unsigned long)st.st_nlink,
           uid_str, gid_str, szstr, timebuf, name);

    /* 符号链接显示目标 */
    if (S_ISLNK(st.st_mode)) {
        char target[256] = {0};
        readlink(full, target, sizeof(target) - 1);
        printf(" -> %s", target);
    }
    printf("\n");
}

/* 比较函数(按文件名排序)*/
static int cmp_name(const void *a, const void *b) {
    return strcmp(*(const char **)a, *(const char **)b);
}

int main(int argc, char *argv[]) {
    const char *dir_path = (argc > 1) ? argv[1] : ".";

    DIR *dir = opendir(dir_path);
    if (!dir) {
        fprintf(stderr, "my_ls: 无法打开目录 '%s': %s\n",
                dir_path, strerror(errno));
        return 1;
    }

    /* 收集所有文件名 */
    char *names[4096];
    int count = 0;
    struct dirent *entry;

    while ((entry = readdir(dir)) != NULL && count < 4096) {
        /* 跳过 . 和 .. */
        if (strcmp(entry->d_name, ".") == 0 ||
            strcmp(entry->d_name, "..") == 0) continue;
        /* 跳过隐藏文件 */
        if (entry->d_name[0] == '.') continue;

        names[count] = strdup(entry->d_name);
        count++;
    }
    closedir(dir);

    /* 排序 */
    qsort(names, (size_t)count, sizeof(char *), cmp_name);

    /* 计算总块数 */
    long total_blocks = 0;
    for (int i = 0; i < count; i++) {
        char full[512];
        struct stat st;
        snprintf(full, sizeof(full), "%s/%s", dir_path, names[i]);
        if (lstat(full, &st) == 0)
            total_blocks += st.st_blocks;
    }
    printf("总计 %ld\n", total_blocks / 2);   /* 转换为 1KB 块 */

    /* 打印每个文件 */
    for (int i = 0; i < count; i++) {
        print_entry(dir_path, names[i]);
        free(names[i]);
    }

    return 0;
}
复制代码
gcc -O2 -o my_ls my_ls.c
./my_ls /etc
# 输出示例(类似 ls -l /etc):
# 总计 1024
# -rw-r--r--   1 root     root      3.0K Jan 15 10:30 adduser.conf
# drwxr-xr-x   3 root     root      4.0K Jan 15 10:30 alternatives
# -rw-r--r--   1 root     root      401B Jan 15 10:30 bash.bashrc
# ...

# 对比系统 ls -l
ls -l /etc | head -5

9.2 文件元数据汇总工具

复制代码
#!/bin/bash
# 文件名:file_meta.sh
# 功能:显示文件的完整元数据信息

set -euo pipefail

if [ $# -eq 0 ]; then
    echo "用法: $0 <文件路径> [...]"
    exit 1
fi

for FILE in "$@"; do
    if [ ! -e "$FILE" ] && [ ! -L "$FILE" ]; then
        echo "文件不存在: $FILE"
        continue
    fi

    echo "═══════════════════════════════════════════"
    echo "  文件: $FILE"
    echo "═══════════════════════════════════════════"

    # 使用 stat 命令获取元数据
    stat "$FILE" 2>/dev/null || lstat_info="无法获取"

    echo ""
    echo "【文件类型】"
    file "$FILE"

    echo ""
    echo "【inode 信息】"
    ls -li "$FILE" 2>/dev/null

    echo ""
    echo "【详细权限】"
    stat -c "  权限(数字): %a\n  权限(字符): %A\n  所有者: %U (%u)\n  所属组: %G (%g)" "$FILE"

    echo ""
    echo "【时间戳】"
    stat -c "  访问时间(atime): %x\n  修改时间(mtime): %y\n  变更时间(ctime): %z" "$FILE"

    echo ""
    echo "【存储信息】"
    stat -c "  文件大小: %s 字节\n  占用块数: %b(×512字节)\n  最优块大小: %o 字节\n  inode号: %i\n  设备号: %d" "$FILE"

    # 符号链接显示目标
    if [ -L "$FILE" ]; then
        echo ""
        echo "【符号链接目标】"
        echo "  -> $(readlink -f "$FILE")"
    fi

    echo ""
done
复制代码
chmod +x file_meta.sh
./file_meta.sh /etc/passwd /bin/ls /tmp

知识点总结

复制代码
第 4 章 核心知识图谱
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

┌──────────────────────────────────────────────────────────┐
│               文件属性与元数据                            │
└────────┬──────────┬──────────┬──────────┬────────────────┘
         │          │          │          │
   ┌─────▼────┐ ┌───▼────┐ ┌──▼─────┐ ┌──▼──────┐
   │ stat系列  │ │文件类型│ │权限模型│ │  inode  │
   └─────┬────┘ └───┬────┘ └──┬─────┘ └──┬──────┘
         │          │         │           │
   stat/lstat    7种类型    chmod/umask  硬链接
   fstat         S_IS*宏    SUID/SGID   软链接
   fstatat       S_IFMT     Sticky bit  inode号
   TOCTOU安全    access()   权限检查顺序 df -i

struct stat 关键字段:
  st_mode   → 文件类型(4位) + 特殊位(3位) + 权限(9位)
  st_ino    → inode 号(文件系统内唯一)
  st_nlink  → 硬链接计数(降为0时删除文件)
  st_size   → 逻辑大小(稀疏文件可能远大于实际占用)
  st_blocks → 实际占用的 512 字节块数
  st_[amc]time → 访问/修改/变更时间

时间戳规律:
  read()   → atime
  write()  → mtime + ctime
  chmod()  → ctime only
  link()   → ctime only

黄金法则:
  ① 用 lstat 检查符号链接自身,用 stat 检查目标
  ② 用 fstat(fd) 代替 stat(path) 避免 TOCTOU 竞争
  ③ 权限检查顺序:root → owner → group → other(互斥)
  ④ umask 只影响文件创建,不影响 chmod
  ⑤ 硬链接不能跨文件系统,不能链接目录
  ⑥ chown 会清除 SUID/SGID(安全机制)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

📚 参考资料

  • man 2 stat / man 2 lstat / man 2 fstat / man 2 fstatat

  • man 2 chmod / man 2 chown / man 2 utime / man 2 utimensat

  • man 2 link / man 2 symlink / man 2 readlink / man 2 unlink

  • man 2 mkdir / man 2 rmdir / man 2 rename / man 3 opendir

  • man 7 inode / man 7 symlink / man 1 stat

  • 《Linux/UNIX 系统编程手册》第 15、18 章 --- Michael Kerrisk

  • 《UNIX 环境高级编程(APUE)》第 4 章 --- W. Richard Stevens

相关推荐
kernelcraft2 小时前
Boto3:Python 操作 AWS 的官方 SDK
开发语言·python·其他·aws
C语言小火车2 小时前
什么时候用智能指针?什么时候用裸指针?
c语言·c++·学习·指针
小生不才yz2 小时前
Shell脚本精读 · S02-03 | 词拆分、通配符与未加引号的变量
linux
D3bugRealm2 小时前
cryptography:Python 开发者的加密标准库
开发语言·python·其他
2601_961845422 小时前
法考真题及答案解析|历年真题|资料已整理
linux·windows·ubuntu·macos·centos·gnu
A_humble_scholar2 小时前
Linux(七)调度器:从硬件矛盾到进程切换的底层逻辑
linux·服务器·网络
Rain5092 小时前
2.1 Nest.js 项目初始化与模块化架构
开发语言·前端·javascript·后端·架构·数据分析·node.js
小熊美家熊猫系统3 小时前
电子合同技术实现与合规实践
java·开发语言·分布式
ytttr8733 小时前
C# 定时数据库备份工具
开发语言·数据库·c#