《UNIX环境高级编程》读书笔记05: 文件和目录
作者: andylin02
学习章节: 第4章 文件和目录
关键词: stat/lstat/fstatat、文件类型、文件权限、umask、chmod、chown、硬链接、符号链接、目录遍历、文件时间戳
一、引言:从文件I/O到文件属性的跨越
上一章我们聚焦于文件I/O的五个核心系统调用,理解了如何读写文件数据。本章更进一步,将目光投向文件本身——不仅仅是数据,还包括文件的元数据。第3章关注“文件的内容是什么”,第4章关注“文件是什么”。前者是读写数据,后者是理解文件的身份、权限、类型和时间等属性,二者构成了Unix文件操作的两大基石。
本章的学习目标:掌握如何获取和修改文件的元数据,理解Unix文件系统的组织结构,学会遍历目录和操作文件权限。
二、stat函数族:获取文件属性
2.1 stat、fstat、lstat和fstatat
UNIX系统提供了四个stat函数,用于获取文件的属性信息:
#include <sys/stat.h> int stat(const char *restrict pathname, struct stat *restrict buf); int fstat(int fd, struct stat *buf); int lstat(const char *restrict pathname, struct stat *restrict buf); int fstatat(int fd, const char *restrict pathname, struct stat *restrict buf, int flag);| 函数 | 参数类型 | 特点 |
|---|---|---|
stat | 路径名 | 跟随符号链接,返回链接指向的文件信息 |
fstat | 文件描述符 | 获取已打开文件的属性 |
lstat | 路径名 | 不跟随符号链接,返回符号链接本身的信息 |
fstatat | 目录文件描述符+路径名 | 相对于目录打开文件,flag控制是否跟随符号链接 |
关键区别:当文件是符号链接时,
stat返回链接所指向的文件的信息,而lstat返回符号链接本身的信息。在遍历目录树时,使用lstat可以正确识别符号链接本身,而不是陷入无限循环。
2.2 stat结构体
这四个函数通过buf指针填充一个struct stat结构体。虽然具体实现可能有所不同,但其基本形式如下:
struct stat { mode_t st_mode; // 文件类型和访问权限 ino_t st_ino; // i节点号 dev_t st_dev; // 文件所在设备的设备号 dev_t st_rdev; // 特殊文件的设备号 nlink_t st_nlink; // 硬链接数 uid_t st_uid; // 文件所有者的用户ID gid_t st_gid; // 文件所有者的组ID off_t st_size; // 文件大小(字节数) struct timespec st_atime; // 最后访问时间 struct timespec st_mtime; // 最后修改时间 struct timespec st_ctime; // 最后状态更改时间 blksize_t st_blksize; // 最佳I/O块大小 blkcnt_t st_blocks; // 分配的磁盘块数 };POSIX.1与XSI扩展:POSIX.1未要求
st_rdev、st_blksize和st_blocks字段,Single UNIX Specification的XSI扩展才定义了这些字段。timespec结构按照秒和纳秒提供了更高精度的时间戳,为了保持兼容性,旧的名字可以定义为tv_sec成员。
三、文件类型
3.1 UNIX的七种文件类型
UNIX/Linux系统将文件分为7种类型,通过stat结构的st_mode成员判断:
| 文件类型 | 测试宏 | 说明 |
|---|---|---|
| 普通文件 | S_ISREG() | 最常见类型,包含某种形式的数据,UNIX内核不区分文本或二进制 |
| 目录文件 | S_ISDIR() | 包含其他文件名和指针,只有内核可以直接写目录 |
| 块特殊文件 | S_ISBLK() | 对设备提供带缓冲的访问,每次以固定长度为单位(如磁盘) |
| 字符特殊文件 | S_ISCHR() | 对设备提供不带缓冲的访问,每次长度可变(如终端) |
| FIFO | S_ISFIFO() | 命名管道,用于进程间通信 |
| 套接字 | S_ISSOCK() | 用于进程间的网络通信 |
| 符号链接 | S_ISLNK() | 指向另一个文件的间接指针 |
3.2 判断文件类型的代码示例
#include "apue.h" int main(int argc, char *argv[]) { int i; struct stat buf; char *ptr; for (i = 1; i < argc; i++) { printf("%s: ", argv[i]); if (lstat(argv[i], &buf) < 0) { err_ret("lstat error"); continue; } if (S_ISREG(buf.st_mode)) ptr = "regular"; else if (S_ISDIR(buf.st_mode)) ptr = "directory"; else if (S_ISCHR(buf.st_mode)) ptr = "character special"; else if (S_ISBLK(buf.st_mode)) ptr = "block special"; else if (S_ISFIFO(buf.st_mode)) ptr = "fifo"; else if (S_ISLNK(buf.st_mode)) ptr = "symbolic link"; else if (S_ISSOCK(buf.st_mode)) ptr = "socket"; else ptr = "** unknown mode **"; printf("%s\n", ptr); } exit(0); }为什么使用
lstat而不是stat?lstat返回符号链接本身的信息,而stat会跟随符号链接返回目标文件的信息。用lstat可以正确识别符号链接这种文件类型。
四、文件访问权限
4.1 九个基本权限位
每个文件有9个访问权限位,取自<sys/stat.h>:
| 类别 | 常量 | 含义 |
|---|---|---|
| 用户(所有者) | S_IRUSR | 用户读 |
S_IWUSR | 用户写 | |
S_IXUSR | 用户执行 | |
| 组 | S_IRGRP | 组读 |
S_IWGRP | 组写 | |
S_IXGRP | 组执行 | |
| 其他 | S_IROTH | 其他读 |
S_IWOTH | 其他写 | |
S_IXOTH | 其他执行 |
4.2 目录执行权限的含义
对于目录,执行权限位常被称为搜索位。打开任一类型的文件时,对该路径名的每一级目录都必须具有执行权限。
| 权限 | 对文件的作用 | 对目录的作用 |
|---|---|---|
| 读(r) | 读取文件内容 | 读取目录中的文件名列表 |
| 写(w) | 修改文件内容 | 在目录中创建/删除文件 |
| 执行(x) | 执行文件(对脚本/程序) | 搜索目录(进入目录的通行证) |
4.3 新文件和目录的所有权
当创建一个新文件时:
用户ID:设置为进程的有效用户ID
组ID:可以是进程的有效组ID,也可以是所在目录的组ID(若目录设置了
setgid位)
五、umask函数:文件创建掩码
umask函数为进程设置文件模式创建屏蔽字,并返回之前的值:
#include <sys/stat.h> mode_t umask(mode_t cmask);工作原理:umask的功能是创建新文件时屏蔽掉用户不希望生效的权限位。对于文件,权限的最大值是666(不允许直接赋予执行权限);目录则允许777。
示例:创建两个文件,第一个的umask为0,第二个的umask值禁止所有组和其他用户的访问权限。
#include "apue.h" #include <fcntl.h> #define RWRWRW (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) int main(void) { umask(0); if (creat("foo", RWRWRW) < 0) err_sys("creat error for foo"); umask(S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); if (creat("bar", RWRWRW) < 0) err_sys("creat error for bar"); exit(0); }运行结果:
foo:权限为-rw-rw-rw-(umask=0)bar:权限为-rw-------(umask禁止了组和其他用户的读写)
📌常用umask值:
002(阻止其他用户写)、022(阻止同组成员和其他用户写)、027(阻止同组成员写,阻止其他用户读写执行)。
六、chmod和fchmod函数
chmod和fchmod用于更改已有文件的访问权限:
#include <sys/stat.h> int chmod(const char *pathname, mode_t mode); int fchmod(int fd, mode_t mode);权限要求:要修改文件权限,进程的有效用户ID必须等于文件的所有者ID,或者进程具有超级用户权限。
示例:修改文件权限
#include "apue.h" int main(void) { struct stat statbuf; // 打开set-group-ID位,关闭组执行位 if (stat("foo", &statbuf) < 0) err_sys("stat error for foo"); if (chmod("foo", (statbuf.st_mode & ~S_IXGRP) | S_ISGID) < 0) err_sys("chmod error for foo"); // 设置绝对模式为 rw-r--r-- if (chmod("bar", S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) < 0) err_sys("chmod error for bar"); exit(0); }七、粘着位(Sticky Bit)
粘着位(S_ISVTX)在历史上用于普通文件的交换区保存,现代系统对目录设置了粘着位后,只有目录所有者、文件所有者或超级用户才能删除或重命名目录中的文件。这个特性常用于/tmp和/var/tmp等共享目录,防止用户删除其他用户的临时文件。
📌普通文件的粘着位:只有超级用户可以设置普通文件的粘着位,以防止不怀好意的用户设置粘住位。
八、硬链接与符号链接
8.1 链接函数
#include <unistd.h> int link(const char *existingpath, const char *newpath); // 创建硬链接 int unlink(const char *pathname); // 删除目录项 int symlink(const char *actualpath, const char *sympath); // 创建符号链接 ssize_t readlink(const char *pathname, char *buf, size_t bufsize); // 读符号链接 int remove(const char *pathname); // 对文件=unlink,对目录=rmdir int rename(const char *old, const char *new);8.2 硬链接与符号链接的对比
| 特性 | 硬链接 | 符号链接 |
|---|---|---|
| 本质 | 目录中另一个指向相同i节点的文件名 | 存储目标文件路径的特殊文件 |
| 跨文件系统 | ❌ 否,必须在同一文件系统内 | ✅ 是 |
| 指向目录 | ❌ 否(只有超级用户可以) | ✅ 是 |
| 链接数影响 | st_nlink增加 | st_nlink不变 |
| 删除原文件 | 数据仍存在(通过其他硬链接) | 链接变成“悬空”(dangling) |
| 文件大小 | 与原文件共享数据块 | 存储路径字符串,st_size为路径长度 |
硬链接的限制:硬链接通常要求链接和文件位于同一文件系统中,且只有超级用户才能创建指向目录的硬链接。引入符号链接正是为了避开这些限制。
8.3 目录遍历的流程
目录遍历的核心函数调用链如下:
DIR *dp = opendir("."); struct dirent *entry; while ((entry = readdir(dp)) != NULL) { printf("%s\n", entry->d_name); } closedir(dp);opendir函数返回指向DIR结构的指针,readdir循环读取每个目录项,返回dirent结构指针,d_name成员为文件名。
九、文件时间戳
9.1 三个时间值
每个文件维护三个时间字段:
| 字段 | 说明 | 修改操作 | ls选项 |
|---|---|---|---|
st_atime | 最后访问时间 | read、exec、lstat等 | -u |
st_mtime | 最后修改时间(内容) | write、truncate | 默认 |
st_ctime | 最后状态更改时间(i节点) | chmod、chown、link、unlink等 | -c |
9.2 修改时间戳的函数
#include <sys/time.h> int utimes(const char *pathname, const struct timeval times[2]); int futimens(int fd, const struct timespec times[2]); int utimensat(int fd, const char *path, const struct timespec times[2], int flag);十、目录操作函数汇总
| 函数 | 功能 |
|---|---|
mkdir | 创建新目录 |
rmdir | 删除空目录 |
opendir | 打开目录,返回DIR*指针 |
readdir | 读取目录项,返回struct dirent* |
rewinddir | 重置目录流到开头 |
closedir | 关闭目录流 |
telldir/seekdir | 获取/设置目录流位置 |
chdir/fchdir | 更改当前工作目录 |
getcwd | 获取当前工作目录绝对路径 |
十一、完整代码示例
11.1 实现简单的ls命令
#include "apue.h" #include <dirent.h> int main(int argc, char *argv[]) { DIR *dp; struct dirent *dirp; if (argc != 2) err_quit("usage: ls directory_name"); if ((dp = opendir(argv[1])) == NULL) err_sys("can't open %s", argv[1]); while ((dirp = readdir(dp)) != NULL) printf("%s\n", dirp->d_name); closedir(dp); exit(0); }11.2 递归遍历目录树(ftw风格的简化版)
#include "apue.h" #include <dirent.h> #include <limits.h> typedef int Myfunc(const char *, const struct stat *, int); static Myfunc myfunc; static int myftw(char *, Myfunc *); static int dopath(Myfunc *); static long nreg, ndir, nblk, nchr, nfifo, nslink, nsock, ntot; int main(int argc, char *argv[]) { int ret; if (argc != 2) err_quit("usage: ftw <starting-pathname>"); ret = myftw(argv[1], myfunc); ntot = nreg + ndir + nblk + nchr + nfifo + nslink + nsock; if (ntot == 0) ntot = 1; printf("regular files = %7ld, %5.2f %%\n", nreg, nreg * 100.0 / ntot); printf("directories = %7ld, %5.2f %%\n", ndir, ndir * 100.0 / ntot); printf("block special = %7ld, %5.2f %%\n", nblk, nblk * 100.0 / ntot); printf("char special = %7ld, %5.2f %%\n", nchr, nchr * 100.0 / ntot); printf("FIFOs = %7ld, %5.2f %%\n", nfifo, nfifo * 100.0 / ntot); printf("symbolic links = %7ld, %5.2f %%\n", nslink, nslink * 100.0 / ntot); printf("sockets = %7ld, %5.2f %%\n", nsock, nsock * 100.0 / ntot); exit(ret); } #define FTW_F 1 // 非目录文件 #define FTW_D 2 // 目录 #define FTW_DNR 3 // 不可读目录 #define FTW_NS 4 // 无法获取stat信息 static char *fullpath; static size_t pathlen; static int myftw(char *pathname, Myfunc *func) { fullpath = path_alloc(&pathlen); if (pathlen <= strlen(pathname)) { pathlen = strlen(pathname) * 2; if ((fullpath = realloc(fullpath, pathlen)) == NULL) err_sys("realloc failed"); } strcpy(fullpath, pathname); return dopath(func); } static int dopath(Myfunc *func) { struct stat statbuf; struct dirent *dirp; DIR *dp; int ret, n; if (lstat(fullpath, &statbuf) < 0) return func(fullpath, &statbuf, FTW_NS); if (S_ISDIR(statbuf.st_mode) == 0) return func(fullpath, &statbuf, FTW_F); // 是目录,先处理该目录本身 if ((ret = func(fullpath, &statbuf, FTW_D)) != 0) return ret; n = strlen(fullpath); if (n + NAME_MAX + 2 > pathlen) { pathlen *= 2; if ((fullpath = realloc(fullpath, pathlen)) == NULL) err_sys("realloc failed"); } fullpath[n++] = '/'; fullpath[n] = 0; if ((dp = opendir(fullpath)) == NULL) return func(fullpath, &statbuf, FTW_DNR); while ((dirp = readdir(dp)) != NULL) { if (strcmp(dirp->d_name, ".") == 0 || strcmp(dirp->d_name, "..") == 0) continue; strcpy(&fullpath[n], dirp->d_name); if ((ret = dopath(func)) != 0) break; } fullpath[n-1] = 0; if (closedir(dp) < 0) err_ret("can't close directory %s", fullpath); return ret; } static int myfunc(const char *pathname, const struct stat *statptr, int type) { switch (type) { case FTW_F: switch (statptr->st_mode & S_IFMT) { case S_IFREG: nreg++; break; case S_IFBLK: nblk++; break; case S_IFCHR: nchr++; break; case S_IFIFO: nfifo++; break; case S_IFLNK: nslink++; break; case S_IFSOCK: nsock++; break; case S_IFDIR: ndir++; break; } break; case FTW_D: ndir++; break; case FTW_DNR: err_ret("can't read directory %s", pathname); break; case FTW_NS: err_ret("stat error for %s", pathname); break; } return 0; }十二、第4章知识点速查表
| 知识点 | 核心内容 |
|---|---|
| stat系列函数 | stat/fstat/lstat/fstatat的区别 |
| 七种文件类型 | 普通文件、目录、块/字符特殊文件、FIFO、套接字、符号链接 |
| 文件权限 | 9个基本权限位 + 粘着位 |
| umask | 文件创建掩码,屏蔽不需要的权限位 |
| chmod/fchmod | 修改已有文件的权限 |
| chown/fchown | 修改文件所有者 |
| 硬链接 vs 符号链接 | i节点 vs 路径指针,跨文件系统、跨目录的区别 |
| 目录遍历 | opendir/readdir/closedir |
| 三个时间戳 | st_atime、st_mtime、st_ctime的区别 |
| 文件系统结构 | i节点、目录项、数据块的层次关系 |
十三、学习心得
第4章是Unix文件系统的“百科全书”。本章的核心在于理解i节点、目录项和路径解析的关系。重点掌握:
stat与lstat的区别:符号链接的处理方式文件权限模型:9个基本位 + 粘着位的含义
硬链接与符号链接的本质区别:一个指向i节点,一个指向路径
目录遍历的方法:
opendir/readdir是递归处理文件树的基石三个文件时间戳的含义:访问、修改、状态更改的区别
本章内容多而杂,但每条知识点都有实际用途。建议配合man手册和大量编码练习,将各种系统调用的行为理解透彻。
十四、下一篇预告
下一篇将进入第5章“标准I/O库”,内容包括:
标准I/O库与系统调用的关系
流与FILE对象
缓冲类型(全缓冲、行缓冲、无缓冲)
打开、关闭、读写流(
fopen、fclose、fread、fwrite)格式化I/O(
printf、scanf家族)临时文件(
tmpfile、tmpnam)内存流(
fmemopen、open_memstream)
敬请期待!
本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!
