Linux 系统编程 03:目录与文件属性

前言:

承接上一篇文件高级操作内容,本篇深入 Linux 文件系统底层,从文件属性获取、inode 本质、目录遍历到权限管理逐层拆解。理解这些内容,才能真正搞懂 Linux "一切皆文件" 的设计底层,掌握软硬链接、目录扫描、权限控制等工程高频操作,同时也是笔试面试中文件系统原理题的核心考点。


一、文件属性获取:stat 函数族

1. 三个核心 stat 函数

Linux 提供了三个函数用于获取文件属性,核心区别在于打开方式和对软链接的处理:

复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *pathname, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *pathname, struct stat *buf);
函数 入参形式 软链接处理
stat 文件路径 跟随软链接,返回链接指向的目标文件属性
fstat 文件描述符 已打开的文件,直接获取属性
lstat 文件路径 不跟随软链接,返回软链接自身的属性

面试高频考点:区分 stat 和 lstat 的核心差异。判断一个文件是不是软链接,必须用 lstat,用 stat 永远只能得到目标文件的类型。

2. stat 结构体核心成员

struct stat是存储文件属性的核心结构体,最常用的成员如下:

复制代码
struct stat {
    ino_t     st_ino;     // inode编号
    mode_t    st_mode;    // 文件类型与权限
    nlink_t   st_nlink;   // 硬链接数
    uid_t     st_uid;     // 所有者用户ID
    gid_t     st_gid;     // 所属组ID
    off_t     st_size;    // 文件大小(字节)
    time_t    st_atime;   // 最后访问时间
    time_t    st_mtime;   // 最后修改时间
    time_t    st_ctime;   // 最后属性修改时间
    blksize_t st_blksize; // IO块大小
    blkcnt_t  st_blocks;  // 占用的磁盘块数
};

3. 文件类型与权限判断

st_mode是一个整型,高位存储文件类型,低位存储权限。标准提供了专门的宏来判断,不需要手动位运算。

文件类型判断宏

  • S_ISREG(m):普通文件
  • S_ISDIR(m):目录文件
  • S_ISLNK(m):软链接文件
  • S_ISCHR(m):字符设备文件
  • S_ISBLK(m):块设备文件
  • S_ISFIFO(m):管道文件
  • S_ISSOCK(m):套接字文件

权限位掩码

  • S_IRUSR:所有者读权限(0400)
  • S_IWUSR:所有者写权限(0200)
  • S_IXUSR:所有者执行权限(0100)
  • S_IRGRPS_IWGRPS_IXGRP:所属组权限
  • S_IROTHS_IWOTHS_IXOTH:其他用户权限

4. 实战:获取并打印文件属性

复制代码
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.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 (lstat(argv[1], &st) == -1) {
        perror("lstat failed");
        return 1;
    }

    printf("inode编号:%ld\n", (long)st.st_ino);
    printf("文件大小:%ld 字节\n", (long)st.st_size);
    printf("硬链接数:%ld\n", (long)st.st_nlink);

    // 判断文件类型
    printf("文件类型:");
    if (S_ISREG(st.st_mode)) printf("普通文件\n");
    else if (S_ISDIR(st.st_mode)) printf("目录\n");
    else if (S_ISLNK(st.st_mode)) printf("软链接\n");
    else printf("其他类型\n");

    printf("最后修改时间:%s", ctime(&st.st_mtime));
    return 0;
}

二、inode:文件系统的核心本质

1. 什么是 inode

inode(索引节点)是 Linux 文件系统的核心数据结构,每个文件对应唯一的 inode,用于存储文件的元数据信息:

  • 文件的权限、所有者、大小、时间戳等属性(也就是 stat 结构体里的大部分内容)
  • 文件数据块的存储位置指针
  • 硬链接计数

核心结论:文件名不是文件的本质,inode 才是文件的唯一标识。文件名只是方便人类记忆的别名,目录本质上就是一张 "文件名→inode 号" 的映射表。

2. 硬链接的本质

硬链接就是多个文件名指向同一个 inode,共享同一份文件数据。

  • 创建硬链接后,inode 的硬链接计数 + 1
  • 删除一个硬链接,计数 - 1,只有计数减到 0 时,文件才真正被删除
  • 硬链接不能跨文件系统,不能给目录创建硬链接

3. 文件删除的底层逻辑

调用unlink删除一个文件时,内核做两件事:

  1. 删除目录中对应的文件名映射
  2. inode 的硬链接计数减 1 只有当硬链接计数为 0,且没有进程打开该文件时,文件的数据块才会被真正释放。

这也是为什么:程序打开的文件,即使被 rm 删除,磁盘空间也不会立刻释放,直到进程关闭文件。


三、目录操作:目录遍历实现

1. 核心目录操作函数

Linux 通过目录流的方式操作目录,核心函数有三个:

复制代码
#include <dirent.h>

DIR *opendir(const char *name);
struct dirent *readdir(DIR *dirp);
int closedir(DIR *dirp);
  • opendir:打开目录,返回目录流指针,失败返回 NULL
  • readdir:读取目录中的下一个条目,读到末尾返回 NULL,失败也返回 NULL(需通过 errno 区分)
  • closedir:关闭目录流,释放资源

2. dirent 结构体

readdir返回的目录条目结构体,核心成员:

复制代码
struct dirent {
    ino_t d_ino;       // 文件的inode号
    char  d_name[256]; // 文件名
};

3. 实战:遍历目录打印所有文件

复制代码
#include <stdio.h>
#include <dirent.h>
#include <sys/stat.h>
#include <string.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "用法:%s 目录路径\n", argv[0]);
        return 1;
    }

    DIR *dir = opendir(argv[1]);
    if (dir == NULL) {
        perror("opendir failed");
        return 1;
    }

    struct dirent *entry;
    while ((entry = readdir(dir)) != NULL) {
        // 跳过 . 和 .. 两个特殊目录
        if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
            continue;
        }
        printf("%s\n", entry->d_name);
    }

    closedir(dir);
    return 0;
}

工程注意:readdir 返回的指针指向内部静态缓冲区,不保证线程安全;多线程场景应使用readdir_r版本。


四、文件权限与属性修改

1. 修改文件权限:chmod/fchmod

复制代码
#include <sys/stat.h>

int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
  • chmod:通过路径修改文件权限
  • fchmod:通过已打开的文件描述符修改权限
  • mode参数:可以直接写八进制数(如0644),也可以用权限宏按位或组合

2. umask 与默认权限

umask是进程的文件权限掩码,用于控制新建文件的默认权限。

  • 新建文件的默认权限 = 0666 & ~umask
  • 新建目录的默认权限 = 0777 & ~umask

系统默认 umask 通常是0022,因此:

  • 新建文件默认权限:0666 & ~0022 = 0644

  • 新建目录默认权限:0777 & ~0022 = 0755

    #include <sys/stat.h>
    mode_t umask(mode_t mask);

umask 是进程级属性,修改只影响当前进程及其子进程,不影响系统全局。

3. 修改所有者与所属组

复制代码
#include <unistd.h>

int chown(const char *pathname, uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group);
int lchown(const char *pathname, uid_t owner, gid_t group);
  • chown:修改文件的所有者和所属组
  • lchown:不跟随软链接,修改软链接自身的所有者
  • 注意:只有 root 用户可以修改文件的所有者;普通用户只能修改自己拥有的文件的所属组,且只能改成自己所在的组

五、软硬链接深度对比

1. 创建链接的函数

复制代码
#include <unistd.h>

int link(const char *oldpath, const char *newpath);   // 创建硬链接
int symlink(const char *target, const char *linkpath); // 创建软链接
int unlink(const char *pathname);                      // 删除链接/文件
  • unlink:删除一个硬链接,也就是删除文件名映射。对于普通文件,删除最后一个硬链接就等于删除文件;对于软链接,删除的是链接本身,不影响目标文件。

2. 软硬链接核心对比

对比维度 硬链接 软链接(符号链接)
本质 多个文件名指向同一个 inode 独立文件,存储目标文件的路径
inode 号 和源文件相同 拥有独立的 inode
跨文件系统 不可以,只能在同一文件系统内 可以,支持跨分区跨文件系统
目录支持 不允许给目录创建硬链接 支持给目录创建软链接
文件大小 和源文件完全一致 很小,只存储目标路径字符串
源文件删除 不影响,文件依然存在 软链接失效,成为悬空链接
相对路径 无影响 相对路径是相对于链接文件所在位置

六、面试高频考点与易错坑点

1. 经典面试问答

Q1:stat 和 lstat 有什么核心区别?

答: stat 会跟随软链接,返回的是软链接指向的目标文件的属性;lstat 不会跟随软链接,返回的是软链接自身的属性。 因此判断一个文件是不是软链接,必须使用 lstat,用 stat 永远只能得到目标文件的类型,无法判断链接本身。

Q2:什么是 inode?文件名和 inode 是什么关系?

答: inode 是文件系统中存储文件元数据的结构体,每个文件对应唯一的 inode 号,inode 里记录了文件的权限、大小、时间、数据块位置等信息。 文件名不是文件的本质,只是存放在目录里的别名,目录本质上是一张 "文件名→inode 号" 的映射表。系统访问文件时,先通过文件名找到 inode 号,再通过 inode 找到文件数据。

Q3:硬链接和软链接有什么本质区别?

答:

  1. 本质不同:硬链接是多个文件名指向同一个 inode,共享同一份数据;软链接是独立文件,存储目标文件的路径,有自己的 inode。
  2. 跨分区:硬链接不能跨文件系统,软链接可以。
  3. 目录:硬链接不能给目录创建,软链接可以。
  4. 源文件删除:删除源文件硬链接不受影响,文件依然存在;删除源文件后软链接失效,成为悬空链接。

Q4:rm 删除文件后,磁盘空间为什么有时候没有立刻释放?

答: 因为文件真正被删除需要满足两个条件:一是硬链接计数为 0,二是没有进程打开该文件。 如果有进程正在打开并持有文件描述符,rm 只是删除了文件名映射,硬链接计数减一,但进程还在引用文件,数据块不会释放。直到进程关闭文件,空间才会真正被回收。

Q5:umask 的作用是什么?新建文件的默认权限怎么计算?

答: umask 是进程的权限掩码,用于屏蔽新建文件的部分权限,控制默认权限。 新建普通文件基础权限是 0666,新建目录是 0777,最终默认权限等于基础权限按位取反 umask 后的值。 比如 umask 为 0022 时,文件默认权限是 0644,目录默认权限是 0755。

2. 常见易错坑点

  1. 用 stat 判断软链接,永远得到目标文件类型,误以为不是链接,必须用 lstat
  2. 遍历目录时忘记跳过...,递归遍历会导致无限循环
  3. readdir 返回 NULL 就认为遍历结束,忽略错误情况,需要判断 errno 是否为 0
  4. 误以为 rm 会立刻释放磁盘空间,忽略进程打开文件的引用计数
  5. 软链接使用相对路径时,误以为是相对于当前工作目录,实际是相对于链接文件所在目录
  6. 普通用户调用 chown 修改文件所有者,以为会成功,实际只有 root 权限才能修改所有者
  7. 认为硬链接可以跨文件系统,实际 inode 号只在单个文件系统内唯一,跨分区无法共享 inode

以上就是 Linux 目录与文件属性的核心内容。理解 inode 与文件系统底层逻辑,才能真正看懂 Linux 文件操作的各种行为,排查磁盘空间不释放、权限异常等工程问题。下一篇我们正式进入进程模块,讲解进程本质与 fork 创建机制。


制作不易,如果对你有用,希望能点赞收藏支持一下。