Linux 系统编程 · 第 4 章:文件属性与元数据
本章深入讲解 Linux 文件的元数据体系:
stat系列函数、文件权限模型、inode 结构,以及时间戳、链接、目录操作等核心主题。
目录
-
[stat 系列函数](#stat 系列函数)
-
[struct stat 字段详解](#struct stat 字段详解)
-
[inode 深度解析](#inode 深度解析)
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, ×);
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