Linux note2
文件和目录stat()、fstat()、lstat()、fstatat()#include sys/stat.h int stat(const char *pathname, struct stat *buf); int fstat(int fd, struct stat *buf); int lstat(const char *pathname, struct stat *buf); int fstatat(int dirfd, const char *pathname, struct stat *buf, int flags);stat根据文件路径获取文件属性会跟随符号链接返回链接指向的目标文件信息。pathname文件路径跟随符号链接获取链接指向文件信息fstat通过已打开的文件描述符 fd 获取文件属性无需文件路径。fd已打开文件描述符无需路径不受进程目录、链接影响lstat和 stat 路径参数一致不跟随符号链接如果路径是软链接返回软链接本身的 inode 信息遍历目录树时必须使用 lstat。参数同 stat不跟随软链接直接获取链接本身属性fstatat相对于指定目录 dirfd 进行路径查询的扩展函数dirfd基准目录 fdAT_FDCWD 当前目录pathname 为绝对路径时 fd 参数被忽略。pathname相对 / 绝对路径flagsAT_SYMLINK_NOFOLLOW不追踪软链接等价 lstat无该标志追踪软链接等价 statstruct 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; // 文件系统最优IO块大小 blkcnt_t st_blocks; // 文件占用的磁盘块数量 }; struct timespec { time_t tv_sec; // 秒 long tv_nsec; // 纳秒实现高精度时间戳 };设置用户id和设置组id进程关联的 7 个 IDID 名称作用说明实际用户 ID (RUID)标识进程真实是谁登录会话一般保持不变超级用户可修改实际组 ID (RGID)进程真实所属用户组有效用户 ID (EUID)用于文件访问权限校验有效组 ID (EGID)用于文件访问权限校验附属组 ID额外附属用户组权限校验一并参与保存的设置用户 ID (SUID)exec 执行程序时保存 EUID 副本权限切换场景使用保存的设置组 ID (SGID)exec 执行程序时保存 EGID 副本默认状态EUIDRUIDEGIDRGID。文件模式字st_mode中存在两个特殊标志位S_ISUID设置用户 ID 位setuid 位 若文件开启该位执行此程序时进程有效用户 ID (EUID) 被设置为【文件所有者 st_uid】而不是进程原本的实际用户 ID。S_ISGID设置组 ID 位setgid 位 若文件开启该位执行此程序时进程有效组 ID (EGID) 被设置为【文件所属组 st_gid】getuid()返回实际用户 IDreal uid, ruidgeteuid()返回有效用户 IDeffective uid, euidsetuid(uid_t u)特权进程 (root)同时修改 ruid、euid、saved uid 三个 UID永久切换身份普通进程仅当 u ruid 或 u saved uid 时修改 euid切换后无法切回更高权限。seteuid(uid_t u)只修改有效 UID euid。仅能在 ruid ↔ saved uid 之间来回切换权限setuid 程序常用临时降权 / 提权#define _GNU_SOURCE #include stdio.h #include stdlib.h #include unistd.h #include sys/types.h #include fcntl.h void print_uids(const char *tag) { uid_t ruid, euid, suid; getresuid(ruid, euid, suid); printf([%s] RUID%d, EUID%d, saved-suid%d\n, tag, ruid, euid, suid); } int main() { print_uids(start); // ① 临时放弃特权将有效UID改为实际UID普通用户 if (seteuid(getuid()) ! 0) { perror(seteuid(getuid())); exit(1); } print_uids(after seteuid(ruid)); // ② 此时以普通用户权限安全地读取其个人配置文件 FILE *user_file fopen(/home/user/.myapprc, r); if (user_file) { char line[256]; if (fgets(line, sizeof(line), user_file)) printf(读取到用户配置: %s, line); fclose(user_file); } else { perror(fopen user config); } // ③ 需要执行特权操作时从保存的 set-user-ID 恢复特权 // 因为 saved-suid 还是 root(0)可以直接 seteuid(0) 恢复 if (seteuid(0) ! 0) { perror(seteuid(0)); exit(1); } print_uids(after seteuid(0)); // ④ 以 root 权限修改系统文件 FILE *sys_file fopen(/etc/myapp.conf, a); if (sys_file) { fprintf(sys_file, # 由程序更新\n); fclose(sys_file); printf(成功写入系统配置。\n); } else { perror(fopen system config); } return 0; }文件访问权限没个文件都有9个访问权限位分为三类 用户(u)、组(g)、其他(o)。宏常量含义S_IRUSR用户读S_IWUSR用户写S_IXUSR用户执行S_IRGRP组读S_IWGRP组写S_IXGRP组执行S_IROTH其他读S_IWOTH其他写S_IXOTH其他执行目录文件的读权限、执行权限和其他文件有所不同读权限 (r)可以列出目录内所有文件名ls不能进入目录、不能访问目录内文件。执行权限 (x) → 搜索位 允许穿越目录、按文件名查找内部文件想要打开目录下文件路径上每一级目录必须具备执行权限。目录 写权限 (w)允许在目录内创建、删除、重命名文件。一个目录仅有 x 无 r不能 ls 列出所有文件但是如果你提前知道文件名可以直接打开该文件。访问权限判定EUID 0超级用户 root → 直接允许访问不再后续判断EUID 文件 st_uid进程是文件所有者 → 检查【用户权限位】不再检查组、其他权限EGID / 附属组 ID 文件 st_gid → 检查【组权限位】不再检查其他权限否则 → 检查【其他用户权限位】新文件和目录文件的所有权文件用户id新建文件 / 目录的所有者 UID 进程有效用户 ID (EUID)组idLinux默认下面方案 1仅当父目录设置了S_ISGID置组 ID 位才启用方案 2 继承目录 GID方案 1新文件 GID 进程有效组 ID (EGID)方案 2新文件 GID 文件所在父目录的组 ID继承目录组目录mkdir创建目录时会自动继承父目录的 SGID 标志保证子目录也能传递组所有权。access()和faccessat()检查调用进程对文件的访问权限基于进程的实际 uid/gid#include unistd.h int access(const char *pathname, int mode); int faccessat(int dirfd, const char *pathname, int mode, int flags);mode检测权限掩码 F_OK文件是否存在 R_OK可读 W_OK可写 X_OK可执行返回值成功 0失败 - 1 并置 errno。umask()为当前进程设置文件创建屏蔽字仅对本进程内open/creat/mkdir新建文件生效不影响其他进程、shell 全局 umask只对当前打开的 shell 窗口有效。最终权限 创建时传入mode (~umask)#include sys/stat.h mode_t umask(mode_t mask);mask新的文件权限屏蔽字八进制常用如 0022返回值返回调用前旧的 umask 值。chmod()、fchmod()、fchmodat()hmod接收文件路径默认跟随符号链接修改链接指向的真实文件权限。fchmod基于已打开fd操作无法作用于符号链接打开链接时已穿透到目标文件。fchmodatfdAT_FDCWD、pathname 为绝对路径等价chmodfd指向目录pathname 为相对路径基于该目录解析#include sys/stat.h int chmod(const char *path, mode_t mode); int fchmod(int fd, mode_t mode); int fchmodat(int dirfd, const char *path, mode_t mode, int flags);chmod参数文件路径 path目标权限 mode特性跟随符号链接修改链接指向文件权限返回成功 0失败 - 1 并置 errnofchmod参数已打开文件描述符 fd目标权限 mode特性通过 fd 操作不受路径、软链接影响返回成功 0失败 - 1 并置 errnofchmodatdirfd基准目录 fdAT_FDCWD 等价 chmodpath相对 / 绝对路径mode目标权限flags仅支持 AT_SYMLINK_NOFOLLOW不追踪软链接返回成功 0失败 - 1 并置 errno粘着位保存正文位早期UNIX 系统无请求分页虚拟内存可执行程序设置该位后第一次运行结束机器指令正文段会永久保存在交换分区下次启动程序无需重新从磁盘加载提速启动。现代操作系统全部实现虚拟内存、高速文件系统普通文件该功能彻底废弃。现代用途给目录设置S_ISVTX后目录内文件的删除 / 重命名权限被强制限制。满足以下三者之一才能能删除 / 改名文件是该文件的所有者是该目录的所有者是超级用户 root。目录开放全局读写执行权限所有用户均可创建临时文件设置粘着位后普通用户只能删除自己创建的临时文件不能删除别人的临时文件保障多用户临时目录安全。chown ()、lchown()、fchown()、fchownat()修改文件的uid和gid。chown()跟随符号链接修改链接指向的文件lchown()不跟随符号链接直接修改符号链接自身所属主属组fchown()基于打开文件修改fchownat有flags等价于chown()flagsAT_SYMLINK_NOFOLLOW等价于lchown#include unistd.h int chown(const char *path, uid_t owner, gid_t group); int lchown(const char *path, uid_t owner, gid_t group); int fchown(int fd, uid_t owner, gid_t group); #include fcntl.h int fchownat(int dirfd, const char *path, uid_t owner, gid_t group, int flags);path文件路径fd已打开文件描述符owner新属主 uid传-1则不修改用户group新属组 gid传-1则不修改组dirfd基准目录 fdAT_FDCWD等价当前工作目录flags仅AT_SYMLINK_NOFOLLOW有效不跟随软链接返回值成功返回 0失败返回 - 1设置 errno。文件长度stat 结构体中成员st_size文件逻辑长度字节仅对普通文件、目录、符号链接有效普通文件逻辑字节大小可取值 0读超出已写入位置返回 EOF。目录长度一般是 16/512 字节的整数倍存放目录项。符号链接存储目标路径字符串的实际字节数不含末尾\0示例 lib - usr/libst_size7。st_blksize /st_blocks物理存储st_blksize文件系统最优 I/O 块大小标准库读写会按该块批量操作减少 IO 次数。st_blocks文件实际占用磁盘块数量不同系统单位不统一不可移植 Linux默认 512 字节 / 块设POSIXLY_CORRECT环境变量时为 1024 字节 / 块ls -l 读取 st_size逻辑长度du 读取 st_blocks实际磁盘占用。文件空洞普通文件特有产生原因调用lseek将文件偏移量移动到文件尾部之后再执行 write 写入数据中间未写入的区域形成空洞。读空洞未写入的空洞位置read读取返回字节0wc -c统计会把空洞计入总逻辑长度。磁盘占用空洞不占用实际磁盘数据块因此du显示占用远小于ls -l逻辑大小。复制空洞文件cat core core.copycat 逐字节读取空洞位置读出 0 并写入新文件新文件空洞被填满磁盘占用大幅上升。文件截断truncate ()和ftruncate()两个函数功能一致将文件逻辑长度设置为length若原文件长度 length截断尾部多余数据若原文件长度 length文件空洞扩展填充 0不修改文件读写偏移量#include unistd.h int truncate(const char *path, off_t length); int ftruncate(int fd, off_t length);truncatepath文件路径会跟随软链接length目标文件字节长度成功返回 0失败返回 - 1设置 errnoftruncatefd已打开文件描述符必须可写length目标文件字节长度成功返回 0失败返回 - 1设置 errnoftruncate常用于加锁后清空文件文件系统磁盘可切分为多个分区一个分区存放一套独立文件系统UFS。单个文件系统内部分层自举块 → 超级块 → 柱面组多组单个柱面组组成 超级块副本、配置信息、i 节点图、块位图、i 节点数组、数据块核心概念节点inode定长记录保存文件全部属性类型ino_tstat 绝大多数字段来自 inode。数据块存放普通文件内容、目录项、符号链接目标路径。目录项仅存「文件名 inode 编号」不属于 inode。硬链接定义 多个目录项指向同一个 inode称为硬链接链接计数st_nlinknlink_t记录指向该 inode 的目录项总数。关键规则 删除文件本质unlink删除一条目录项链接计数 - 1仅当计数 0 时inode 与数据块才释放。 限制不能跨文件系统不同分区 inode 编号独立无法映射ln命令不能跨分区创建硬链接。 同分区重命名mv仅新增 / 删除目录项inode 不变链接计数不变文件数据不移动。 目录硬链接计数特殊规则 空叶子目录无下级子目录链接数 2自身目录项 目录内. 有子目录的目录链接数≥3父目录项 . 每个子目录的..每多一个子目录父目录链接数 1 常量LINK_MAX单个 inode 允许的最大硬链接数量。注用户手动不能创建指向目录的硬链接符号链接软链接 Symbolic Link文件类型标识inode 中文件类型为S_IFLNK。存储软链接自身数据块存放目标文件路径字符串st_size 等于路径字符长度不含\0。无跨分区限制可指向目录、不存在文件、跨文件系统。与硬链接核心区别不增加目标文件的st_nlink计数。inode和目录项的分工inode 存储文件类型、权限、长度、数据块指针、时间戳、链接计数。目录项仅存两项文件名、inode 编号文件名不存于 inode。目录项是文件名与 inode 编号的映射记录目录本质是目录项列表。每条目录项存储三部分核心信息文件名字符串、对应文件的 inode 编号、文件类型标记普通文件 / 目录 / 软链接等link ()、linkat()、unlink ()、unlinkat()、remove()link()和linkat()为文件创建硬链接跟随软链接。unlink()和unlinkat()删除路径对应的硬链接链接计数归 0 且无进程打开文件时释放 inode。不能删除目录。遇软链接只删链接本身不操作目标文件。#include unistd.h // 创建硬链接 int link(const char *oldpath, const char *newpath); int linkat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath, int flags); // 删除硬链接 int unlink(const char *pathname); int unlinkat(int dirfd, const char *pathname, int flags); #include stdio.h // 删除文件/空目录 int remove(const char *pathname);link()oldpath 原文件newpath 新硬链接路径。linkat()olddirfd原文件所在目录 fdnewdirfd新链接所在目录 fdflagsAT_SYMLINK_FOLLOW 跟随原路径软链接unlinkat()dirfd基准目录 fdAT_FDCWD等价 unlinkflagsAT_REMOVEDIR → 可删除空目录remove()封装接口普通文件等价 unlink ()空目录等价 rmdir ()返回值全部成功返回 0失败返回 - 1 并置 errno。rename()、renameat()rename()、renameat()的本质是“改目录项的名字文件数据在磁盘上没动。特点可跨同文件系统移动文件跨设备无法使用 若 newpath 已存在且是文件直接覆盖删除 若 newpath 是空目录覆盖非空目录报错 对软链接操作操作链接本身不追踪指向文件#include stdio.h int rename(const char *oldpath, const char *newpath); #include fcntl.h int renameat2(int olddirfd, const char *oldpath, int newdirfd, const char *newpath, unsigned int flags);rename ()oldpath原文件 / 目录路径newpath新路径名renameat ()olddirfd原文件所在目录 fdAT_FDCWD 代表当前目录oldpath相对 / 绝对原路径newdirfd新目录 fdnewpath相对 / 绝对新路径flagsAT_RENAME_EXCHANGE交换两个文件、AT_RENAME_NOREPLACE不覆盖已存在文件Linux特有返回值均成功 0失败 - 1置 errno符号链接硬链接限制必须和原文件位于同一文件系统仅 root 用户可创建目录硬链接极易造成目录循环多数系统禁止普通用户创建目录硬链接。符号链接优势无文件系统限制可跨分区、跨设备任意用户均可创建指向目录的软链接本质是间接指针单独存储一段目标路径字符串拥有独立 inode。特殊特例open(O_CREAT | O_EXCL)若路径是软链接open 直接返回失败errnoEEXIST用于安全防护防止特权进程被欺骗覆盖文件。符号链接循环touch foo/a ln -s ../foo foo/testdir # testdir指向父目录foo形成循环遍历工具如ftw递归访问时无限循环内核检测后返回错误 errnoELOOP。循环消除难易对比软链接循环极易清理unlink foo/testdir 直接删除软链接本身unlink 不跟随链接目录硬链接循环极难修复会破坏文件系统fsck 无法修复因此link()默认禁止创建目录硬链接。创建和读取符号链接symlink ()、symlinkat()、readlink()、readlinkat()symlink()基于linkpath的路径创建符号链接target 无需真实存在symlinkat()基于文件描述符的相对路径创建符号链接。#include unistd.h // 创建软链接 int symlink(const char *target, const char *linkpath); int symlinkat(const char *target, int newdirfd, const char *linkpath);symlink()target 链接指向的目标字符串linkpath 软链接文件路径返回值成功 0失败 - 1symlinkat()newdirfd基准目录 fdAT_FDCWD等价 symlinklinkpath 为相对路径时以 newdirfd 对应目录为基准创建软链接返回值成功 0失败 - 1readlink()读取软链接存储的目标字符串不跟随链接readlinkat()与readlink()一致#include unistd.h // 读取软链接内容 ssize_t readlink(const char *pathname, char *buf, size_t bufsiz); ssize_t readlinkat(int dirfd, const char *pathname, char *buf, size_t bufsiz);readlink()pathname软链接路径buf输出缓冲区bufsiz缓冲区大小返回成功返回读到的字节数-1 失败注结果不带字符串结束符\0readlinkat()dirfd基准目录 fdAT_FDCWD等价 readlinkpathname 相对路径基于 dirfd 目录解析只读取软链接自身内容返回值成功返回有效字节 失败返回-1。文件的时间stat里的3个时间字段struct timespec st_atime; // 最后访问时间 struct timespec st_mtime; // 最后修改文件内容时间 struct timespec st_ctime; // 文件元属性最后修改时间权限、所有者、链接数变更st_mtim vs st_ctim 核心区分st_mtim修改时间仅当文件磁盘数据内容被改写时更新权限、属主、链接数变更不影响该字段。st_ctim状态更改时间只要inode 元数据发生变化就更新权限、UID/GID、硬链接计数、文件大小、时间戳全部会触发 ctim 更新哪怕不修改文件内容。目录与普通文件的区别读写普通文件 仅修改该文件自身的 st_atim /st_mtim/st_ctim父目录时间不受影响。增删改目录内文件touch/rm/rename 改动的是目录项列表会更新父目录的 st_mtim、st_ctim同时新建 / 被修改文件自身时间戳同步更更新修改时间戳也会触发 ctime 更新access()和stat()不会更新时间戳系统不会因为查询修改st_atime。futimen()、utimensat()、utimes()修改文件的 atime、mtime 时间戳基于进程当前工作目录解析路径。#include sys/time.h // utimes int utimes(const char *path, const struct timeval times[2]); #include fcntl.h #include sys/stat.h // futimens int futimens(int fd, const struct timespec times[2]); // utimensat int utimensat(int dirfd, const char *path, const struct timespec times[2], int flags);utimes()struct timeval秒 微秒精度微秒级futimens()struct timespec秒 纳秒精度纳秒级utimensat()相对于dirfd的路径dirfdAT_FDCWD 等价 futimens 的路径版本flagsAT_SYMLINK_NOFOLLOW 不跟随软链接直接修改链接本身时间time同futimens()返回值均 成功0 失败-1设置 errnomkdir()、mkdirat()、rmdir()mkdir()mkdirat()创建目录rmdiir()只能删除空目录目录存在文件 / 子目录则报错若 path 是软链接无法删除链接作用于链接指向目录不可删除普通文件#include sys/stat.h // 创建目录 int mkdir(const char *pathname, mode_t mode); int mkdirat(int dirfd, const char *pathname, mode_t mode);#include unistd.h // 删除空目录 int rmdir(const char *pathname);mkdir()pathname待创建目录路径mode目录权限受进程 umask 屏蔽mkdirat()dirfd基准目录 fdAT_FDCWD 等价 mkdirpathname 为相对路径时以 dirfd 对应目录为基准创建mode 规则与 mkdir 完全一致全部成功返回 0失败返回 - 1 并设置 errno读目录对目录有访问权限的任何用户都可以读目录只有内核可以写目录。函数功能返回值DIR *opendir(const char *pathname);通过路径打开目录返回 DIR 句柄成功返回 DIR*失败 NULLDIR *fdopendir(int fd);将已打开的目录 fd 转为 DIR 句柄SUSv4 XSI 扩展成功返回 DIR*失败 NULLstruct dirent *readdir(DIR *dp);读取目录下一条目录项读到条目返回 dirent*目录尾 / 出错返回 NULLvoid rewinddir(DIR *dp);将目录读取指针重置到目录开头无返回值int closedir(DIR *dp);关闭 DIR 句柄释放资源成功 0失败 - 1long telldir(DIR *dp);获取当前目录读取偏移位置XSI 扩展非 POSIX 基础当前位置偏移量void seekdir(DIR *dp, long loc);设置目录读取偏移位置XSI 扩展无返回值DIR 是内部不透明结构体类似标准 IO 的FILE保存目录读取状态fdopendir 依赖文件描述符的当前偏移读取起点由 fd 偏移决定opendir 固定从目录第一条开始目录项遍历顺序不保证字母序由文件系统实现决定。struct dirent { char d_name[]; // 以\0结尾的文件名容量至少NAME_MAX字节 ino_t d_ino; // 文件inode编号POSIX基础标准未定义 }不能通过数组长度判断文件名长度必须用strlen(d_name)目录默认包含 .当前目录、..父目录遍历时常需要过滤。chdir()、fchdir()、getcwd()内核仅保存目录 inode 指针不存储完整绝对路径因此需要getcwd逐层回溯拼接完整路径。每个进程独有当前工作目录CWD是解析相对路径的起点。改变进程属性仅作用于自身不影响父 / 子进程、shell。chdir()文件系统路径字符串相对 / 绝对路径均可fchdir() 切换当前工作目录为 fd 对应的目录getcwd() 把当前工作目录绝对路径存入 buf#include unistd.h // 修改进程当前工作目录 int chdir(const char *path); int fchdir(int fd); // 获取当前工作目录 char *getcwd(char *buf, size_t size);chadir()返回成功 0失败 -1 置errorfchdir()返回成功 0失败 -1getcwd()buf用户提供缓冲区传 NULL 时部分系统自动 malloc 缓冲区需手动 freesize缓冲区字节大小必须 ≥ 路径长度 1存终止符\0返回值成功返回 buf 指针 失败NULL置 errno常见 ERANGE缓冲区太小为什么cd 不做成外部程序而是 shell 内置命令builtinshell 执行外部程序时会创建子进程运行程序 chdir 修改子进程的 CWD子进程退出后销毁shell 父进程目录不变注chdir 会跟随符号链接 getcwd 回溯时展示真实物理路径不会显示软链接路径。设备特殊文件dev_t、主设备号、次设备号dev_t存储设备编号的系统类型由主设备号 次设备号拼接而成。主设备号major标识硬件驱动程序同一种硬件驱动共用同一个主号。次设备号minor区分同一驱动下的不同子设备分区、终端等。提取宏头文件Linuxsys/sysmacros.h Solarissys/mkdev.h BSDsys/types.hmajor(x)传入一个 dev_t 变量 x从这个整数里拆分、提取出主设备号作为整数返回。 minor(x)传入一个 dev_t 变量 x拆分提取次设备号作为整数返回。struct stat st; stat(/dev/tty0, st); dev_t x st.st_dev; // 从stat获取合法设备号 cout major(x) minor(x);st_dev定义文件所在文件系统的设备号。即该文件 inode 存放于哪一块存储设备。所有文件均拥有st_devst_rdev仅块 / 字符特殊文件有效定义硬件设备本身的设备号只有两类文件存在有效值块设备文件和字符设备文件。S_ISCHR(buf.st_mode) // 是否字符特殊文件 S_ISBLK(buf.st_mode) // 是否块特殊文件struct stat st; stat(/dev/sda, st); if (S_ISBLK(st.st_mode)) cout 块设备; if (S_ISCHR(st.st_mode)) cout 字符设备;文件访问权限权限段读写执行用户 (属主)S_IRUSRS_IWUSRS_IXUSR用户组S_IRGRPS_IWGRPS_IXGRP其他用户S_IROTHS_IWOTHS_IXOTH常量名称普通文件作用目录作用S_ISUID设置用户 ID程序执行时进程有效用户 ID 改为文件所有者 ID目录无效果S_ISGID设置组 ID可执行文件执行时有效组 ID 改为文件组 ID部分系统支持文件锁目录内新建文件自动继承该目录的组 IDS_ISVTX粘滞位早期程序正文缓存到交换区现代系统废弃目录限制仅文件所有者 /root 才能删除、重命名目录内文件典型/tmp