stat系统调用及示例

好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍一组用于获取文件状态信息的函数:statfstat, 和 lstat。它们让你能够查询文件的各种属性,如大小、权限、所有者、时间戳等,而无需打开文件(statlstat)或只需已打开的文件描述符(fstat)。


1. 函数介绍

statfstat, 和 lstat 是三个密切相关的 Linux 系统调用,它们都用于获取文件的状态信息。这些信息被填充到一个 struct stat 类型的结构体中。

  • stat: 通过文件路径名 (pathname) 获取文件状态。如果路径名指向一个符号链接 (symbolic link),它会跟随链接并返回链接目标文件的状态。
  • fstat: 通过已打开的文件描述符 (file descriptor) 获取文件状态。因为文件已经打开,所以它直接操作文件描述符,不涉及路径解析。
  • lstat: 也通过文件路径名 (pathname) 获取文件状态。但它不会跟随符号链接,而是返回符号链接本身的信息。

理解这三个函数的关键在于区分符号链接和其目标文件,以及何时需要获取哪个的信息。


2. 函数原型

#include <sys/types.h>  // 通常需要
#include <sys/stat.h>   // 必需,包含 struct stat 和函数声明
#include <unistd.h>     // 通常需要

// 通过路径名获取文件状态
int stat(const char *pathname, struct stat *statbuf);

// 通过文件描述符获取文件状态
int fstat(int fd, struct stat *statbuf);

// 通过路径名获取文件状态,但不跟随符号链接
int lstat(const char *pathname, struct stat *statbuf);

3. 功能

这三个函数的功能都是将指定文件的详细状态信息填充到调用者提供的 struct stat 结构体指针 statbuf 所指向的内存中。

  • stat(pathname, buf): 查找 pathname,如果它是一个符号链接,则查找链接指向的目标文件,并将该目标文件的状态信息存入 buf
  • fstat(fd, buf): 获取与文件描述符 fd 关联的文件的状态信息并存入 buf
  • lstat(pathname, buf): 查找 pathname,即使它是一个符号链接,也将该符号链接本身的状态信息存入 buf

4. 参数

  • const char *pathname (statlstat): 指向一个以空字符结尾的字符串,该字符串包含要查询状态的文件的路径名。
  • int fd (fstat): 一个有效的、已打开的文件描述符。
  • struct stat *statbuf: 指向一个 struct stat 类型的结构体的指针。函数调用成功后,该结构体将被填入文件的状态信息。

5. struct stat 结构体

struct stat 结构体包含了大量的文件属性信息。虽然具体实现可能因系统而异,但以下字段是标准且常用的:

struct stat {
    dev_t     st_dev;         // 文件所在设备的 ID
    ino_t     st_ino;         // inode 节点号 (唯一标识文件)
    mode_t    st_mode;        // 文件类型和权限
    nlink_t   st_nlink;       // 硬链接数
    uid_t     st_uid;         // 所有者用户 ID
    gid_t     st_gid;         // 所有者组 ID
    dev_t     st_rdev;        // (如果是设备文件) 设备 ID
    off_t     st_size;        // 文件大小 (以字节为单位)
    blksize_t st_blksize;     // 文件系统 I/O 块大小
    blkcnt_t  st_blocks;      // 分配的 512B 块数
    time_t    st_atime;       // 上次访问时间 (自 Unix 纪元以来的秒数)
    time_t    st_mtime;       // 上次修改时间
    time_t    st_ctime;       // 上次状态改变时间 (如权限、所有者)
    // ... 可能还有其他字段,取决于具体实现 ...
};

常用字段解释:

  • st_mode: 这个字段包含了文件类型和权限信息。可以通过一系列宏来测试:
    • 文件类型:
      • S_ISREG(m): 是否为普通文件。
      • S_ISDIR(m): 是否为目录。
      • S_ISCHR(m): 是否为字符设备文件。
      • S_ISBLK(m): 是否为块设备文件。
      • S_ISFIFO(m): 是否为命名管道 (FIFO)。
      • S_ISLNK(m): 是否为符号链接 (对 lstat 结果有效)。
      • S_ISSOCK(m): 是否为套接字。
    • 权限位 (按位与 & 操作检查):
      • S_IRUSRS_IWUSRS_IXUSR: 文件所有者的读、写、执行权限。
      • S_IRGRPS_IWGRPS_IXGRP: 文件所属组的读、写、执行权限。
      • S_IROTHS_IWOTHS_IXOTH: 其他用户的读、写、执行权限。
      • S_IRWXUS_IRWXGS_IRWXO: 分别代表所有者、组、其他用户的读/写/执行权限组合。
  • st_size: 文件的大小,以字节为单位。对于目录或设备文件,此值可能没有意义。
  • st_mtime: 文件内容上次被修改的时间。常用于判断文件是否已更新。

6. 返回值

这三个函数的返回值规则相同:

  • 成功时: 返回 0。同时,statbuf 指向的结构体被成功填充。
  • 失败时: 返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 ENOENT 文件不存在、EACCES 权限不足、EBADF fd 无效 (仅 fstat) 等)。

7. 示例代码

示例 1:使用 stat 获取普通文件信息

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pwd.h>  // getpwuid
#include <grp.h>  // getgrgid
#include <time.h> // ctime

void print_file_info(const char *pathname) {
    struct stat file_stat;
    struct passwd *pwd;
    struct group *grp;
    char date_str[100];

    // 调用 stat 获取文件状态
    if (stat(pathname, &file_stat) == -1) {
        perror("stat");
        return; // 或 exit(EXIT_FAILURE);
    }

    // 打印基本信息
    printf("File: %s\n", pathname);
    printf("  Size: %ld bytes\n", (long)file_stat.st_size);
    printf("  Inode: %ld\n", (long)file_stat.st_ino);
    printf("  Device ID: %ld\n", (long)file_stat.st_dev);

    // 打印文件类型
    printf("  Type: ");
    if (S_ISREG(file_stat.st_mode)) {
        printf("Regular File\n");
    } else if (S_ISDIR(file_stat.st_mode)) {
        printf("Directory\n");
    } else if (S_ISLNK(file_stat.st_mode)) {
        printf("Symbolic Link\n");
    } else if (S_ISCHR(file_stat.st_mode)) {
        printf("Character Device\n");
    } else if (S_ISBLK(file_stat.st_mode)) {
        printf("Block Device\n");
    } else if (S_ISFIFO(file_stat.st_mode)) {
        printf("FIFO (Named Pipe)\n");
    } else if (S_ISSOCK(file_stat.st_mode)) {
        printf("Socket\n");
    } else {
        printf("Unknown Type\n");
    }

    // 打印权限 (简化版)
    printf("  Permissions: ");
    printf( (S_ISDIR(file_stat.st_mode)) ? "d" : "-");
    printf( (file_stat.st_mode & S_IRUSR) ? "r" : "-");
    printf( (file_stat.st_mode & S_IWUSR) ? "w" : "-");
    printf( (file_stat.st_mode & S_IXUSR) ? "x" : "-");
    printf( (file_stat.st_mode & S_IRGRP) ? "r" : "-");
    printf( (file_stat.st_mode & S_IWGRP) ? "w" : "-");
    printf( (file_stat.st_mode & S_IXGRP) ? "x" : "-");
    printf( (file_stat.st_mode & S_IROTH) ? "r" : "-");
    printf( (file_stat.st_mode & S_IWOTH) ? "w" : "-");
    printf( (file_stat.st_mode & S_IXOTH) ? "x" : "-");
    printf("\n");

    // 打印所有者和组
    pwd = getpwuid(file_stat.st_uid);
    grp = getgrgid(file_stat.st_gid);
    printf("  Owner: %s (UID: %d)\n", pwd ? pwd->pw_name : "Unknown", (int)file_stat.st_uid);
    printf("  Group: %s (GID: %d)\n", grp ? grp->gr_name : "Unknown", (int)file_stat.st_gid);

    // 打印时间戳 (使用 ctime 格式化)
    // 注意: ctime 返回的字符串末尾自带 \n
    printf("  Last Access: %s", ctime(&file_stat.st_atime)); // ctime 返回的字符串已包含 \n
    printf("  Last Modify: %s", ctime(&file_stat.st_mtime));
    printf("  Last Status Change: %s", ctime(&file_stat.st_ctime));

}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    print_file_info(argv[1]);

    return 0;
}

代码解释:

  1. 定义了一个 print_file_info 函数来封装 stat 调用和信息打印。
  2. 调用 stat(argv[1], &file_stat) 获取文件状态。
  3. 检查返回值,失败则打印错误。
  4. 使用 S_ISREGS_ISDIR 等宏判断文件类型。
  5. 通过按位与操作检查 st_mode 来构建权限字符串。
  6. 使用 getpwuid 和 getgrgid 将 UID/GID 转换为用户名/组名。
  7. 使用 ctime 函数将 time_t 时间戳格式化为人类可读的字符串。

编译并运行:gcc -o stat_example stat_example.c && ./stat_example /etc/passwd

示例 2:比较 statlstatfstat 对符号链接的行为

这个例子创建一个符号链接,并比较三个函数获取的信息。

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    struct stat stat_info, lstat_info, fstat_info;
    int fd;
    const char *target_file = "target_file.txt";
    const char *symlink_name = "my_symlink";

    // 创建一个目标文件 (如果不存在)
    FILE *f = fopen(target_file, "w");
    if (f) {
        fprintf(f, "This is the target file.\n");
        fclose(f);
        printf("Created target file: %s\n", target_file);
    } else {
        perror("Failed to create target file");
        // 即使创建失败,也可能文件已存在,继续尝试后续步骤
    }

    // 创建符号链接 (如果不存在)
    if (symlink(target_file, symlink_name) == -1) {
        if (errno != EEXIST) { // EEXIST 表示链接已存在,可以接受
            perror("Failed to create symbolic link");
            // 清理可能已创建的目标文件
            unlink(target_file);
            exit(EXIT_FAILURE);
        } else {
            printf("Symbolic link '%s' already exists.\n", symlink_name);
        }
    } else {
        printf("Created symbolic link '%s' -> '%s'\n", symlink_name, target_file);
    }

    // --- 使用 stat ---
    printf("\n--- Using stat('%s', ...) ---\n", symlink_name);
    if (stat(symlink_name, &stat_info) == -1) {
        perror("stat failed");
    } else {
        printf("  Inode: %ld (This is the INODE of the TARGET file)\n", (long)stat_info.st_ino);
        printf("  Size: %ld bytes\n", (long)stat_info.st_size);
        if (S_ISLNK(stat_info.st_mode)) {
            printf("  Type: Symbolic Link (This should NOT happen with stat)\n");
        } else {
            printf("  Type: Not a Symbolic Link (stat followed the link)\n");
        }
    }

    // --- 使用 lstat ---
    printf("\n--- Using lstat('%s', ...) ---\n", symlink_name);
    if (lstat(symlink_name, &lstat_info) == -1) {
        perror("lstat failed");
    } else {
        printf("  Inode: %ld (This is the INODE of the SYMBOLIC LINK itself)\n", (long)lstat_info.st_ino);
        printf("  Size: %ld bytes (Size of the link's PATHNAME string)\n", (long)lstat_info.st_size);
        if (S_ISLNK(lstat_info.st_mode)) {
            printf("  Type: Symbolic Link (lstat did NOT follow the link)\n");
        } else {
            printf("  Type: Not a Symbolic Link (Unexpected)\n");
        }
    }

    // --- 使用 fstat ---
    printf("\n--- Using fstat(fd_of_symlink, ...) ---\n");
    // 打开符号链接本身 (需要 O_PATH 或 O_RDONLY, 且不跟随链接的行为取决于系统和标志,但 fstat 总是作用于已打开的 fd)
    // 更准确的做法是打开目标文件来演示 fstat
    fd = open(target_file, O_RDONLY); // 打开目标文件
    if (fd == -1) {
        perror("Failed to open target file for fstat");
        // 清理
        unlink(symlink_name);
        unlink(target_file);
        exit(EXIT_FAILURE);
    }

    if (fstat(fd, &fstat_info) == -1) {
        perror("fstat failed");
    } else {
        printf("  Inode: %ld (Inode of the file associated with fd %d)\n", (long)fstat_info.st_ino, fd);
        printf("  Size: %ld bytes\n", (long)fstat_info.st_size);
        if (S_ISLNK(fstat_info.st_mode)) {
            printf("  Type: Symbolic Link (Associated file is a symlink)\n");
        } else {
            printf("  Type: Not a Symbolic Link (Associated file is regular/dir/etc)\n");
        }
    }
    close(fd); // 关闭 fstat 使用的文件描述符


    // 清理 (可选)
    // printf("\nCleaning up...\n");
    // unlink(symlink_name);
    // unlink(target_file);

    return 0;
}

代码解释:

  1. 首先创建一个目标文件 target_file.txt 和一个指向它的符号链接 my_symlink
  2. 调用 stat(symlink_name, ...): 它返回的是目标文件 (target_file.txt) 的信息,包括目标文件的 inode 和大小。
  3. 调用 lstat(symlink_name, ...): 它返回的是符号链接本身 (my_symlink) 的信息,包括符号链接文件的 inode 和大小(大小通常是其指向路径名字符串的长度)。
  4. 调用 fstat(fd, ...): 它需要一个文件描述符。这里打开的是目标文件 target_file.txt,所以返回的是目标文件的信息。如果打开的是符号链接(且系统支持),fstat 仍然会作用于链接最终指向的文件(除非用特殊标志打开链接本身,但这比较复杂且不常用)。

这个例子清晰地展示了 stat 跟随链接,lstat 不跟随链接,而 fstat 作用于已打开的文件描述符所关联的文件。

理解 statfstatlstat 对于编写需要根据文件属性进行不同处理的程序至关重要。

此条目发表在linux文章分类目录,贴了标签。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注