强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

POSIX 标准详解教程 / 第二章:文件系统

第二章:文件系统

深入理解 POSIX 文件系统模型:文件类型、路径解析、权限体系、inode 机制与链接。


2.1 POSIX 文件系统概述

在 POSIX 世界中,一切皆文件(Everything is a file)。这不仅包括普通文件和目录,还包括设备、管道、套接字等。POSIX 定义了统一的文件操作接口:

open() → read()/write()/lseek() → close()

2.1.1 文件类型一览

POSIX 定义了 7 种文件类型,可通过 stat() 系统调用或 ls -l 命令查看:

文件类型宏名称ls -l 标识典型文件说明
普通文件S_ISREG()-/etc/passwd存储数据的文件
目录S_ISDIR()d/home/包含其他文件的容器
字符设备S_ISCHR()c/dev/tty以字符为单位的设备
块设备S_ISBLK()b/dev/sda以块为单位的设备
FIFO(管道)S_ISFIFO()p/tmp/myfifo命名管道
符号链接S_ISLNK()l/usr/bin/python指向另一个路径
套接字S_ISSOCK()s/var/run/docker.sock进程间通信端点

2.1.2 检测文件类型

/*
 * filetype.c - 检测文件类型
 * 编译: gcc -Wall -o filetype filetype.c
 * 用法: ./filetype /etc/passwd /dev/tty /tmp
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <sys/stat.h>

const char *get_file_type(mode_t mode)
{
    if (S_ISREG(mode))  return "普通文件 (Regular File)";
    if (S_ISDIR(mode))  return "目录 (Directory)";
    if (S_ISCHR(mode))  return "字符设备 (Character Device)";
    if (S_ISBLK(mode))  return "块设备 (Block Device)";
    if (S_ISFIFO(mode)) return "FIFO/管道 (Named Pipe)";
    if (S_ISLNK(mode))  return "符号链接 (Symbolic Link)";
    if (S_ISSOCK(mode)) return "套接字 (Socket)";
    return "未知类型";
}

int main(int argc, char *argv[])
{
    if (argc < 2) {
        fprintf(stderr, "用法: %s <文件路径> ...\n", argv[0]);
        return 1;
    }

    for (int i = 1; i < argc; i++) {
        struct stat st;
        /* lstat 不跟随符号链接,stat 会跟随 */
        if (lstat(argv[i], &st) == -1) {
            perror(argv[i]);
            continue;
        }
        printf("%-20s -> %s\n", argv[i], get_file_type(st.st_mode));
    }
    return 0;
}
$ ./filetype /etc/passwd /dev/tty /tmp /var/run/docker.sock
/etc/passwd          -> 普通文件 (Regular File)
/dev/tty             -> 字符设备 (Character Device)
/tmp                 -> 目录 (Directory)
/var/run/docker.sock -> 套接字 (Socket)

2.2 路径解析

2.2.1 绝对路径与相对路径

路径类型/ 开头起始点示例
绝对路径根目录/home/user/file.txt
相对路径当前工作目录./file.txt../parent/

2.2.2 路径解析规则

POSIX 对路径名的解析有严格规则:

  1. 路径中连续的 / 视为单个 //home///user/home/user
  2. . 代表当前目录
  3. .. 代表父目录
  4. 路径名最大长度为 PATH_MAX(通常为 4096 字节)
  5. 单个文件名最大长度为 NAME_MAX(通常为 255 字节)

2.2.3 获取和切换工作目录

/*
 * chdir_demo.c - 获取和切换工作目录
 * 编译: gcc -Wall -o chdir_demo chdir_demo.c
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <unistd.h>
#include <limits.h>
#include <stdlib.h>

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

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

    /* 切换到 /tmp */
    if (chdir("/tmp") == -1) {
        perror("chdir");
        return EXIT_FAILURE;
    }

    if (getcwd(cwd, sizeof(cwd)) == NULL) {
        perror("getcwd");
        return EXIT_FAILURE;
    }
    printf("切换后: %s\n", cwd);

    return EXIT_SUCCESS;
}

2.2.4 路径名分解:dirname() 与 basename()

/*
 * path_split.c - 分解路径为目录部分和文件名部分
 * 编译: gcc -Wall -o path_split path_split.c
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <libgen.h>
#include <string.h>

int main(void)
{
    /* 注意: dirname/basename 可能修改传入的字符串,所以使用副本 */
    char path1[] = "/home/user/documents/report.pdf";
    char path2[] = "/usr/bin/";
    char path3[] = "file.txt";

    printf("路径: %-40s dirname: %-20s basename: %s\n",
           path1, dirname(path1), basename(path1));
    printf("路径: %-40s dirname: %-20s basename: %s\n",
           path2, dirname(path2), basename(path2));
    printf("路径: %-40s dirname: %-20s basename: %s\n",
           path3, dirname(path3), basename(path3));

    return 0;
}
$ ./path_split
路径: /home/user/documents/report.pdf       dirname: /home/user/documents   basename: report.pdf
路径: /usr/bin/                             dirname: /usr                   basename: bin
路径: file.txt                              dirname: .                      basename: file.txt

2.3 文件权限模型

2.3.1 权限位说明

POSIX 文件权限由 9 个权限位加上 3 个特殊位组成:

         ┌─ SUID (Set User ID)
         │┌─ SGID (Set Group ID)
         ││┌─ Sticky Bit
         │││
      -rwsrwxrwt
        ┬┬┬ ┬┬┬ ┬┬┬
        │││ │││ │││
        │││ ││└─┤│└─ 其他用户 (Other): r/w/x
        ││└─┤└──┤└── 所属组 (Group):  r/w/x
        └┴──┴───┴─── 所有者 (Owner):  r/w/x
权限对文件的含义对目录的含义八进制值
r (读)查看内容列出目录内容(ls)4
w (写)修改内容创建/删除其中的文件2
x (执行)执行程序进入目录(cd)1
s (SUID)以文件所有者身份运行4000 (u+s)
s (SGID)以文件所属组身份运行新建文件继承目录的组2000 (g+s)
t (Sticky)仅文件所有者可删除文件1000

2.3.2 权限检查算法

POSIX 规定内核按以下顺序检查权限:

1. 进程的有效用户 ID (euid) == 文件所有者 (uid)?
   → 使用所有者权限位 (owner bits)
2. 进程的有效组 ID (egid) == 文件所属组 (gid)?
   → 使用组权限位 (group bits)
3. 使用其他用户权限位 (other bits)

注意:root 用户(euid = 0)在 POSIX 中有特殊处理,通常跳过权限检查(但 Linux 内核做了一些限制)。

2.3.3 使用 chmod() 修改权限

/*
 * chmod_demo.c - 修改文件权限
 * 编译: gcc -Wall -o chmod_demo chmod_demo.c
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

static void print_permissions(const char *path)
{
    struct stat st;
    if (stat(path, &st) == -1) {
        perror(path);
        return;
    }
    printf("  %s: %04o (%c%c%c%c%c%c%c%c%c%c)\n", path,
           st.st_mode & 07777,
           S_ISDIR(st.st_mode) ? 'd' : '-',
           (st.st_mode & S_IRUSR) ? 'r' : '-',
           (st.st_mode & S_IWUSR) ? 'w' : '-',
           (st.st_mode & S_ISUID) ? 's' : ((st.st_mode & S_IXUSR) ? 'x' : '-'),
           (st.st_mode & S_IRGRP) ? 'r' : '-',
           (st.st_mode & S_IWGRP) ? 'w' : '-',
           (st.st_mode & S_ISGID) ? 's' : ((st.st_mode & S_IXGRP) ? 'x' : '-'),
           (st.st_mode & S_IROTH) ? 'r' : '-',
           (st.st_mode & S_IWOTH) ? 'w' : '-',
           (st.st_mode & S_ISVTX) ? 't' : ((st.st_mode & S_IXOTH) ? 'x' : '-'));
}

int main(void)
{
    const char *path = "/tmp/posix_perm_test";

    /* 创建文件 */
    int fd = open(path, O_CREAT | O_WRONLY, 0644);
    if (fd == -1) { perror("open"); return 1; }
    close(fd);

    print_permissions(path);

    /* 设置为 0755 (rwxr-xr-x) */
    if (chmod(path, 0755) == -1) { perror("chmod"); return 1; }
    print_permissions(path);

    /* 添加组写权限 (0775 → rwxrwxr-x) */
    if (chmod(path, 0775) == -1) { perror("chmod"); return 1; }
    print_permissions(path);

    /* 清理 */
    unlink(path);
    return 0;
}
$ ./chmod_demo
  /tmp/posix_perm_test: 0644 (-rw-r--r--)
  /tmp/posix_perm_test: 0755 (-rwxr-xr-x)
  /tmp/posix_perm_test: 0775 (-rwxrwxr-x)

2.4 inode:文件的底层标识

2.4.1 inode 结构

每个文件在文件系统中由一个 inode(index node)表示,包含文件的元数据(但不包含文件名):

inode 字段stat 结构体成员说明
inode 编号st_ino文件的唯一标识
文件类型st_mode普通文件/目录/设备等
权限st_mode9 位权限 + 3 位特殊位
所有者 UIDst_uid用户 ID
所属组 GIDst_gid组 ID
硬链接数st_nlink指向此 inode 的目录项数
文件大小st_size字节数
最后访问时间st_atime读取文件时更新
最后修改时间st_mtime修改文件内容时更新
状态改变时间st_ctime修改元数据时更新
设备号st_dev文件所在的设备

关键理解:文件名不是 inode 的一部分,而是目录中的一个条目(directory entry),将文件名映射到 inode 编号。

2.4.2 使用 stat() 获取 inode 信息

/*
 * inode_info.c - 查看文件的 inode 信息
 * 编译: gcc -Wall -o inode_info inode_info.c
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <sys/stat.h>
#include <time.h>

int main(int argc, char *argv[])
{
    if (argc < 2) {
        fprintf(stderr, "用法: %s <文件路径>\n", argv[0]);
        return 1;
    }

    struct stat st;
    if (stat(argv[1], &st) == -1) {
        perror(argv[1]);
        return 1;
    }

    printf("文件: %s\n", argv[1]);
    printf("  inode 号:    %lu\n", (unsigned long)st.st_ino);
    printf("  设备号:      %lu\n", (unsigned long)st.st_dev);
    printf("  硬链接数:    %lu\n", (unsigned long)st.st_nlink);
    printf("  所有者 UID:  %u\n", st.st_uid);
    printf("  所属组 GID:  %u\n", st.st_gid);
    printf("  文件大小:    %ld 字节\n", (long)st.st_size);
    printf("  权限:        %04o\n", st.st_mode & 07777);
    printf("  最后访问:    %s", ctime(&st.st_atime));
    printf("  最后修改:    %s", ctime(&st.st_mtime));
    printf("  状态改变:    %s", ctime(&st.st_ctime));

    return 0;
}

2.5 硬链接与符号链接

2.5.1 概念对比

特性硬链接 (Hard Link)符号链接 (Symbolic Link)
跨文件系统❌ 不可以✅ 可以
链接目录❌ 通常不允许✅ 可以
原文件删除后仍可访问(数据仍在)❌ 变成悬空链接
inode 编号与原文件相同独立的 inode
文件大小与原文件相同目标路径的长度
创建接口link()symlink()
读取链接stat()readlink()

2.5.2 创建硬链接

/*
 * hardlink_demo.c - 创建硬链接,验证 inode 共享
 * 编译: gcc -Wall -o hardlink_demo hardlink_demo.c
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

static void show_inode(const char *label, const char *path)
{
    struct stat st;
    if (stat(path, &st) == -1) {
        perror(path);
        return;
    }
    printf("  %s: inode=%lu, nlink=%lu, size=%ld\n",
           label, (unsigned long)st.st_ino,
           (unsigned long)st.st_nlink, (long)st.st_size);
}

int main(void)
{
    const char *original = "/tmp/posix_original.txt";
    const char *hardlink = "/tmp/posix_hardlink.txt";

    /* 创建原始文件并写入内容 */
    int fd = open(original, O_CREAT | O_WRONLY | O_TRUNC, 0644);
    if (fd == -1) { perror("open"); return 1; }
    const char *data = "Hello, POSIX hard link!\n";
    write(fd, data, 24);
    close(fd);

    /* 创建硬链接 */
    if (link(original, hardlink) == -1) {
        perror("link");
        return 1;
    }

    printf("创建硬链接后:\n");
    show_inode("原始文件", original);
    show_inode("硬链接 ", hardlink);

    /* 删除原始文件 */
    unlink(original);
    printf("\n删除原始文件后:\n");
    show_inode("硬链接(仍可访问)", hardlink);

    /* 清理 */
    unlink(hardlink);
    return 0;
}
$ ./hardlink_demo
创建硬链接后:
  原始文件: inode=1234567, nlink=2, size=24
  硬链接 : inode=1234567, nlink=2, size=24

删除原始文件后:
  硬链接(仍可访问): inode=1234567, nlink=1, size=24

2.5.3 创建符号链接

/*
 * symlink_demo.c - 创建符号链接并读取链接目标
 * 编译: gcc -Wall -o symlink_demo symlink_demo.c
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <unistd.h>
#include <limits.h>
#include <fcntl.h>

int main(void)
{
    const char *target = "/tmp/posix_sym_target.txt";
    const char *linkpath = "/tmp/posix_symlink";

    /* 创建目标文件 */
    int fd = open(target, O_CREAT | O_WRONLY, 0644);
    if (fd == -1) { perror("open"); return 1; }
    close(fd);

    /* 创建符号链接 */
    if (symlink(target, linkpath) == -1) {
        perror("symlink");
        return 1;
    }

    /* 读取符号链接指向的目标(不跟随链接) */
    char buf[PATH_MAX];
    ssize_t len = readlink(linkpath, buf, sizeof(buf) - 1);
    if (len == -1) {
        perror("readlink");
        return 1;
    }
    buf[len] = '\0';
    printf("符号链接 %s -> %s\n", linkpath, buf);

    /* 清理 */
    unlink(linkpath);
    unlink(target);
    return 0;
}

2.6 文件创建与 I/O 基础

2.6.1 open() 标志位

标志说明
O_RDONLY只读打开
O_WRONLY只写打开
O_RDWR读写打开
O_CREAT文件不存在时创建
O_EXCLO_CREAT 配合,文件已存在则失败(原子操作)
O_TRUNC打开时截断为 0
O_APPEND每次写操作追加到文件末尾
O_NONBLOCK非阻塞模式
O_DIRECTORY必须是目录(否则失败)
O_NOFOLLOW不跟随符号链接

2.6.2 创建临时文件

POSIX 提供了安全创建临时文件的标准方式:

/*
 * tempfile.c - 使用 mkstemp() 安全创建临时文件
 * 编译: gcc -Wall -o tempfile tempfile.c
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main(void)
{
    char template[] = "/tmp/posix_tmpfile_XXXXXX";

    /* mkstemp 创建并打开临时文件,返回文件描述符 */
    int fd = mkstemp(template);
    if (fd == -1) {
        perror("mkstemp");
        return EXIT_FAILURE;
    }

    printf("临时文件路径: %s\n", template);
    printf("文件描述符: %d\n", fd);

    /* 写入数据 */
    const char *msg = "This is a POSIX temporary file.\n";
    write(fd, msg, strlen(msg));

    /* 关闭并删除 */
    close(fd);
    unlink(template);
    printf("临时文件已清理\n");

    return EXIT_SUCCESS;
}

2.6.3 目录操作

/*
 * dir_ops.c - 创建、读取和删除目录
 * 编译: gcc -Wall -o dir_ops dir_ops.c
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <sys/stat.h>
#include <dirent.h>
#include <unistd.h>
#include <string.h>

int main(void)
{
    const char *dir = "/tmp/posix_test_dir";

    /* 创建目录,权限 0755 */
    if (mkdir(dir, 0755) == -1) {
        perror("mkdir");
        return 1;
    }
    printf("创建目录: %s\n", dir);

    /* 在目录中创建几个文件 */
    for (int i = 0; i < 3; i++) {
        char path[256];
        snprintf(path, sizeof(path), "%s/file_%d.txt", dir, i);
        FILE *f = fopen(path, "w");
        if (f) {
            fprintf(f, "File %d\n", i);
            fclose(f);
        }
    }

    /* 读取目录内容 */
    printf("目录内容:\n");
    DIR *dp = opendir(dir);
    if (dp == NULL) {
        perror("opendir");
        return 1;
    }

    struct dirent *entry;
    while ((entry = readdir(dp)) != NULL) {
        /* 跳过 . 和 .. */
        if (strcmp(entry->d_name, ".") == 0 ||
            strcmp(entry->d_name, "..") == 0)
            continue;
        printf("  %s (inode: %lu)\n",
               entry->d_name, (unsigned long)entry->d_ino);
    }
    closedir(dp);

    /* 清理:删除文件和目录 */
    for (int i = 0; i < 3; i++) {
        char path[256];
        snprintf(path, sizeof(path), "%s/file_%d.txt", dir, i);
        unlink(path);
    }
    rmdir(dir);
    printf("\n已清理目录\n");

    return 0;
}

2.7 文件偏移与截断

2.7.1 lseek() 用法

/*
 * lseek_demo.c - 文件偏移操作
 * 编译: gcc -Wall -o lseek_demo lseek_demo.c
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(void)
{
    const char *path = "/tmp/posix_lseek_test";
    int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) { perror("open"); return 1; }

    /* 写入 "ABCDE" */
    write(fd, "ABCDE", 5);
    printf("写入 'ABCDE' 后,偏移: %ld\n", (long)lseek(fd, 0, SEEK_CUR));

    /* 回到文件开头 */
    lseek(fd, 0, SEEK_SET);
    printf("SEEK_SET(0) 后,偏移: %ld\n", (long)lseek(fd, 0, SEEK_CUR));

    /* 向前移动 2 字节 */
    lseek(fd, 2, SEEK_SET);
    write(fd, "XX", 2);  /* 将 "CDE" 的前两个字符替换为 "XX" */

    /* 回到开头读取 */
    lseek(fd, 0, SEEK_SET);
    char buf[6] = {0};
    read(fd, buf, 5);
    printf("文件内容: %s\n", buf);  /* ABXXE */

    /* 获取文件大小:SEEK_END 到偏移 0 */
    off_t size = lseek(fd, 0, SEEK_END);
    printf("文件大小: %ld 字节\n", (long)size);

    close(fd);
    unlink(path);
    return 0;
}

2.8 业务场景:日志文件管理

在服务器程序中,常见的需求是实现一个安全的、支持并发的日志写入模块:

/*
 * posix_logger.c - POSIX 日志模块示例
 * 演示: O_APPEND 原子追加、文件锁、权限控制
 * 编译: gcc -Wall -o posix_logger posix_logger.c
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <stdarg.h>

static int log_fd = -1;

int log_open(const char *path)
{
    /* O_APPEND 保证多进程/线程原子追加 */
    log_fd = open(path, O_WRONLY | O_CREAT | O_APPEND, 0644);
    return log_fd;
}

void log_write(const char *fmt, ...)
{
    if (log_fd == -1) return;

    /* 获取时间戳 */
    time_t now = time(NULL);
    struct tm tm;
    localtime_r(&now, &tm);

    char timestamp[64];
    strftime(timestamp, sizeof(timestamp),
             "[%Y-%m-%d %H:%M:%S] ", &tm);

    /* 格式化消息 */
    char msg[1024];
    va_list args;
    va_start(args, fmt);
    vsnprintf(msg, sizeof(msg), fmt, args);
    va_end(args);

    /* 原子写入(O_APPEND 保证) */
    char line[1100];
    int len = snprintf(line, sizeof(line), "%s%s\n", timestamp, msg);
    write(log_fd, line, len);
}

void log_close(void)
{
    if (log_fd != -1) {
        close(log_fd);
        log_fd = -1;
    }
}

int main(void)
{
    log_open("/tmp/posix_demo.log");
    log_write("服务启动, PID=%d", getpid());
    log_write("处理请求: user_id=%d, action=%s", 42, "login");
    log_write("服务关闭");
    log_close();

    /* 查看日志 */
    printf("日志内容:\n");
    FILE *f = fopen("/tmp/posix_demo.log", "r");
    if (f) {
        char line[256];
        while (fgets(line, sizeof(line), f))
            printf("  %s", line);
        fclose(f);
    }
    unlink("/tmp/posix_demo.log");
    return 0;
}

2.9 注意事项

⚠️ 竞态条件 (TOCTOU):使用 access() 检查权限后再 open() 存在安全漏洞——两次操作之间文件状态可能改变。应直接 open() 并检查返回值。

⚠️ 符号链接风险:在不可信路径上操作时,使用 O_NOFOLLOW 标志或 lstat() 避免符号链接攻击。

⚠️ 文件名编码:POSIX 文件名是字节序列,不含 NUL/。推荐使用 UTF-8 编码,但不强制。处理非 ASCII 文件名时要注意 LC_ALL 环境变量。

⚠️ PATH_MAX 不可靠PATH_MAX 在某些文件系统上可能不够用。POSIX 允许动态分配,使用 pathconf() 查询限制。


2.10 扩展阅读

  1. POSIX 文件系统 APIhttps://pubs.opengroup.org/onlinepubs/9699919799/functions/chap04.html
  2. Linux inode 详解man 7 inode
  3. 《The Linux Programming Interface》 第 4-6 章:文件 I/O、文件属性
  4. ext4 文件系统设计https://ext4.wiki.kernel.org/
  5. POSIX ACL (Access Control Lists)man 5 acl

2.11 本章小结

要点说明
7 种文件类型普通文件、目录、字符设备、块设备、FIFO、符号链接、套接字
权限模型所有者/组/其他 × 读/写/执行,加上 SUID/SGID/Sticky
inode文件元数据的容器,文件名仅是目录中的映射
硬链接共享 inode,不能跨文件系统,不能链接目录
符号链接独立 inode,存储目标路径,可跨文件系统
路径操作dirname()basename()realpath()