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 对路径名的解析有严格规则:
- 路径中连续的
/视为单个/(/home///user→/home/user) .代表当前目录..代表父目录- 路径名最大长度为
PATH_MAX(通常为 4096 字节) - 单个文件名最大长度为
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_mode | 9 位权限 + 3 位特殊位 |
| 所有者 UID | st_uid | 用户 ID |
| 所属组 GID | st_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_EXCL | 与 O_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 扩展阅读
- POSIX 文件系统 API:https://pubs.opengroup.org/onlinepubs/9699919799/functions/chap04.html
- Linux inode 详解:
man 7 inode - 《The Linux Programming Interface》 第 4-6 章:文件 I/O、文件属性
- ext4 文件系统设计:https://ext4.wiki.kernel.org/
- POSIX ACL (Access Control Lists):
man 5 acl
2.11 本章小结
| 要点 | 说明 |
|---|---|
| 7 种文件类型 | 普通文件、目录、字符设备、块设备、FIFO、符号链接、套接字 |
| 权限模型 | 所有者/组/其他 × 读/写/执行,加上 SUID/SGID/Sticky |
| inode | 文件元数据的容器,文件名仅是目录中的映射 |
| 硬链接 | 共享 inode,不能跨文件系统,不能链接目录 |
| 符号链接 | 独立 inode,存储目标路径,可跨文件系统 |
| 路径操作 | dirname()、basename()、realpath() |