getdents64系统调用及示例

getdents64 函数详解

1. 函数介绍

getdents64 是 Linux 系统中用于读取目录内容的底层系统调用。可以把这个函数想象成一个”目录内容扫描仪”——它能够高效地扫描目录中的所有文件和子目录,就像超市的扫描枪快速读取商品条码一样。

与高级的目录操作函数(如 readdir)不同,getdents64 是最底层的接口,直接与内核交互,提供了最大的灵活性和性能。它返回的是原始的目录项数据,包含文件名、inode 号、文件类型等信息。

2. 函数原型

#include <dirent.h>     /* 或者 <unistd.h> */
#include <sys/syscall.h>

int getdents64(unsigned int fd, struct linux_dirent64 *dirp, unsigned int count);

3. 功能

getdents64 函数用于从已打开的目录文件描述符中读取目录项(directory entries)。它一次可以读取多个目录项,比逐个读取效率更高。

4. 参数

  • fd: 已打开的目录文件描述符(通过 open() 或 opendir() 获得)
  • dirp: 指向缓冲区的指针,用于存储读取的目录项数据
  • count: 缓冲区的大小(以字节为单位)

5. struct linux_dirent64 结构体

struct linux_dirent64 {
    ino64_t        d_ino;    /* 64位 inode 号 */
    off64_t        d_off;    /* 到下一个目录项的偏移 */
    unsigned short d_reclen; /* 此目录项的长度 */
    unsigned char  d_type;   /* 文件类型 */
    char           d_name[]; /* 文件名(以 null 结尾) */
};

6. 文件类型(d_type 字段)

类型值宏定义说明
0DT_UNKNOWN未知类型
1DT_FIFO命名管道
2DT_CHR字符设备
4DT_DIR目录
6DT_BLK块设备
8DT_REG普通文件
10DT_LNK符号链接
12DT_SOCK套接字

7. 返回值

  • 成功: 返回实际读取的字节数(0 表示到达目录末尾)
  • 失败: 返回 -1,并设置相应的 errno 错误码

常见错误码:

  • EBADF: fd 不是有效的目录文件描述符
  • EFAULT: dirp 指针无效
  • EINVAL: 参数无效
  • ENOENT: 目录不存在

8. 相似函数或关联函数

  • getdents: 旧版本的目录读取函数(32位 inode)
  • readdir: POSIX 标准的目录读取函数(更高级的接口)
  • opendir/fdopendir: 打开目录
  • closedir: 关闭目录
  • scandir: 扫描目录并排序
  • ls: 命令行目录列表工具

9. 示例代码

示例1:基础用法 – 读取目录内容

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/syscall.h>
#include <dirent.h>
#include <string.h>

// 目录项结构体(64位版本)
struct linux_dirent64 {
    ino64_t        d_ino;    /* Inode number */
    off64_t        d_off;    /* Offset to next structure */
    unsigned short d_reclen; /* Size of this structure */
    unsigned char  d_type;   /* File type */
    char           d_name[]; /* Filename (null-terminated) */
};

// 获取文件类型字符串
const char* get_file_type_string(unsigned char d_type) {
    switch (d_type) {
        case DT_REG:  return "普通文件";
        case DT_DIR:  return "目录";
        case DT_LNK:  return "符号链接";
        case DT_CHR:  return "字符设备";
        case DT_BLK:  return "块设备";
        case DT_FIFO: return "命名管道";
        case DT_SOCK: return "套接字";
        case DT_UNKNOWN: 
        default:      return "未知";
    }
}

// 获取文件类型字符
char get_file_type_char(unsigned char d_type) {
    switch (d_type) {
        case DT_REG:  return 'f';
        case DT_DIR:  return 'd';
        case DT_LNK:  return 'l';
        case DT_CHR:  return 'c';
        case DT_BLK:  return 'b';
        case DT_FIFO: return 'p';
        case DT_SOCK: return 's';
        case DT_UNKNOWN: 
        default:      return '?';
    }
}

int main(int argc, char *argv[]) {
    int fd;
    char buf[4096];
    int nread;
    char *dir_path;
    
    // 获取目录路径参数
    if (argc != 2) {
        printf("用法: %s <目录路径>\n", argv[0]);
        dir_path = ".";  // 默认当前目录
        printf("使用当前目录: %s\n\n", dir_path);
    } else {
        dir_path = argv[1];
    }
    
    // 打开目录
    fd = open(dir_path, O_RDONLY | O_DIRECTORY);
    if (fd == -1) {
        perror("open");
        return 1;
    }
    
    printf("=== 目录 '%s' 的内容 ===\n", dir_path);
    printf("%-12s %-10s %-8s %s\n", "INODE", "类型", "大小", "名称");
    printf("%-12s %-10s %-8s %s\n", "----", "----", "----", "----");
    
    // 循环读取目录项
    while (1) {
        nread = syscall(SYS_getdents64, fd, buf, sizeof(buf));
        if (nread == -1) {
            perror("getdents64");
            close(fd);
            return 1;
        }
        
        if (nread == 0) {
            break;  // 到达目录末尾
        }
        
        // 解析目录项
        for (int bpos = 0; bpos < nread;) {
            struct linux_dirent64 *d;
            d = (struct linux_dirent64 *)(buf + bpos);
            
            // 显示目录项信息
            printf("%-12llu %-10s %-8d %s\n",
                   (unsigned long long)d->d_ino,
                   get_file_type_string(d->d_type),
                   d->d_reclen,
                   d->d_name);
            
            bpos += d->d_reclen;
        }
    }
    
    close(fd);
    return 0;
}

示例2:带过滤和统计的目录扫描器

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/syscall.h>
#include <dirent.h>
#include <string.h>
#include <sys/stat.h>

struct linux_dirent64 {
    ino64_t        d_ino;
    off64_t        d_off;
    unsigned short d_reclen;
    unsigned char  d_type;
    char           d_name[];
};

// 统计信息结构体
struct dir_stats {
    int total_files;
    int directories;
    int regular_files;
    int symlinks;
    int devices;
    int fifos;
    int sockets;
    int unknown;
    long long total_size;
};

// 更新统计信息
void update_stats(struct dir_stats *stats, unsigned char d_type) {
    stats->total_files++;
    
    switch (d_type) {
        case DT_DIR:
            stats->directories++;
            break;
        case DT_REG:
            stats->regular_files++;
            break;
        case DT_LNK:
            stats->symlinks++;
            break;
        case DT_CHR:
        case DT_BLK:
            stats->devices++;
            break;
        case DT_FIFO:
            stats->fifos++;
            break;
        case DT_SOCK:
            stats->sockets++;
            break;
        case DT_UNKNOWN:
        default:
            stats->unknown++;
            break;
    }
}

// 显示统计信息
void print_stats(const struct dir_stats *stats) {
    printf("\n=== 统计信息 ===\n");
    printf("总计文件数: %d\n", stats->total_files);
    printf("目录数: %d\n", stats->directories);
    printf("普通文件: %d\n", stats->regular_files);
    printf("符号链接: %d\n", stats->symlinks);
    printf("设备文件: %d\n", stats->devices);
    printf("命名管道: %d\n", stats->fifos);
    printf("套接字: %d\n", stats->sockets);
    printf("未知类型: %d\n", stats->unknown);
}

// 过滤函数:只显示特定类型的文件
int filter_by_type(unsigned char d_type, unsigned char filter_type) {
    if (filter_type == 0) return 1;  // 不过滤
    return d_type == filter_type;
}

int main(int argc, char *argv[]) {
    int fd;
    char buf[8192];
    int nread;
    char *dir_path;
    unsigned char filter_type = 0;  // 0 表示不过滤
    struct dir_stats stats = {0};
    
    // 解析命令行参数
    if (argc < 2) {
        printf("用法: %s <目录路径> [过滤类型]\n", argv[0]);
        printf("过滤类型: d(目录), f(文件), l(链接), 其他类型字符\n");
        dir_path = ".";
    } else {
        dir_path = argv[1];
    }
    
    // 设置过滤类型
    if (argc > 2) {
        switch (argv[2][0]) {
            case 'd': filter_type = DT_DIR; break;
            case 'f': filter_type = DT_REG; break;
            case 'l': filter_type = DT_LNK; break;
            case 'c': filter_type = DT_CHR; break;
            case 'b': filter_type = DT_BLK; break;
            case 'p': filter_type = DT_FIFO; break;
            case 's': filter_type = DT_SOCK; break;
        }
        if (filter_type != 0) {
            printf("过滤类型: %c\n", argv[2][0]);
        }
    }
    
    // 打开目录
    fd = open(dir_path, O_RDONLY | O_DIRECTORY);
    if (fd == -1) {
        perror("open");
        return 1;
    }
    
    printf("=== 目录 '%s' 的内容 ===\n", dir_path);
    if (filter_type != 0) {
        printf("(已过滤)\n");
    }
    printf("%c %-20s %12s %10s\n", 
           'T', "名称", "INODE", "大小(字节)");
    printf("%c %-20s %12s %10s\n", 
           '-', "----", "-----", "----------");
    
    // 读取目录项
    while (1) {
        nread = syscall(SYS_getdents64, fd, buf, sizeof(buf));
        if (nread == -1) {
            perror("getdents64");
            close(fd);
            return 1;
        }
        
        if (nread == 0) {
            break;  // 到达目录末尾
        }
        
        // 解析目录项
        for (int bpos = 0; bpos < nread;) {
            struct linux_dirent64 *d;
            d = (struct linux_dirent64 *)(buf + bpos);
            
            // 应用过滤器
            if (filter_by_type(d->d_type, filter_type)) {
                // 获取文件大小(对于普通文件)
                long long file_size = 0;
                if (d->d_type == DT_REG) {
                    char full_path[1024];
                    struct stat st;
                    snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, d->d_name);
                    if (stat(full_path, &st) == 0) {
                        file_size = st.st_size;
                        stats.total_size += file_size;
                    }
                }
                
                printf("%c %-20s %12llu %10lld\n",
                       get_file_type_char(d->d_type),
                       d->d_name,
                       (unsigned long long)d->d_ino,
                       file_size);
            }
            
            // 更新统计信息
            update_stats(&stats, d->d_type);
            
            bpos += d->d_reclen;
        }
    }
    
    close(fd);
    
    // 显示统计信息
    print_stats(&stats);
    
    return 0;
}

示例3:递归目录遍历工具

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/syscall.h>
#include <dirent.h>
#include <string.h>
#include <sys/stat.h>

struct linux_dirent64 {
    ino64_t        d_ino;
    off64_t        d_off;
    unsigned short d_reclen;
    unsigned char  d_type;
    char           d_name[];
};

// 全局统计变量
int total_files = 0;
int total_dirs = 0;
long long total_size = 0;
int max_depth = 0;

// 递归遍历目录
void traverse_directory(const char *path, int current_depth, int max_show_depth) {
    int fd;
    char buf[8192];
    int nread;
    
    // 更新最大深度
    if (current_depth > max_depth) {
        max_depth = current_depth;
    }
    
    // 打开目录
    fd = open(path, O_RDONLY | O_DIRECTORY);
    if (fd == -1) {
        printf("无法打开目录: %s\n", path);
        return;
    }
    
    // 显示当前目录(如果在显示范围内)
    if (current_depth <= max_show_depth) {
        for (int i = 0; i < current_depth; i++) {
            printf("  ");
        }
        printf("[%s]\n", path);
    }
    
    // 读取目录项
    while (1) {
        nread = syscall(SYS_getdents64, fd, buf, sizeof(buf));
        if (nread == -1) {
            printf("读取目录失败: %s\n", path);
            close(fd);
            return;
        }
        
        if (nread == 0) {
            break;  // 到达目录末尾
        }
        
        // 解析目录项
        for (int bpos = 0; bpos < nread;) {
            struct linux_dirent64 *d;
            d = (struct linux_dirent64 *)(buf + bpos);
            
            // 跳过 . 和 ..
            if (strcmp(d->d_name, ".") == 0 || strcmp(d->d_name, "..") == 0) {
                bpos += d->d_reclen;
                continue;
            }
            
            // 构造完整路径
            char full_path[1024];
            snprintf(full_path, sizeof(full_path), "%s/%s", path, d->d_name);
            
            if (d->d_type == DT_DIR) {
                total_dirs++;
                // 显示目录(如果在显示范围内)
                if (current_depth < max_show_depth) {
                    for (int i = 0; i <= current_depth; i++) {
                        printf("  ");
                    }
                    printf("├── %s/\n", d->d_name);
                } else if (current_depth == max_show_depth) {
                    for (int i = 0; i <= current_depth; i++) {
                        printf("  ");
                    }
                    printf("├── %s/ (子目录未展开)\n", d->d_name);
                }
                
                // 递归遍历子目录
                traverse_directory(full_path, current_depth + 1, max_show_depth);
                
            } else if (d->d_type == DT_REG) {
                total_files++;
                // 获取文件大小
                struct stat st;
                if (stat(full_path, &st) == 0) {
                    total_size += st.st_size;
                }
                
                // 显示文件(如果在显示范围内)
                if (current_depth < max_show_depth) {
                    for (int i = 0; i <= current_depth; i++) {
                        printf("  ");
                    }
                    printf("├── %s (%lld bytes)\n", d->d_name, 
                           (long long)(stat(full_path, &st) == 0 ? st.st_size : 0));
                }
            } else {
                total_files++;
                // 显示其他类型的文件
                if (current_depth < max_show_depth) {
                    for (int i = 0; i <= current_depth; i++) {
                        printf("  ");
                    }
                    printf("├── %s [%c]\n", d->d_name, get_file_type_char(d->d_type));
                }
            }
            
            bpos += d->d_reclen;
        }
    }
    
    close(fd);
}

// 获取文件类型字符
char get_file_type_char(unsigned char d_type) {
    switch (d_type) {
        case DT_REG:  return 'f';
        case DT_DIR:  return 'd';
        case DT_LNK:  return 'l';
        case DT_CHR:  return 'c';
        case DT_BLK:  return 'b';
        case DT_FIFO: return 'p';
        case DT_SOCK: return 's';
        default:      return '?';
    }
}

int main(int argc, char *argv[]) {
    char *start_path;
    int show_depth = 3;  // 默认显示3层
    
    // 解析命令行参数
    if (argc < 2) {
        printf("用法: %s <起始路径> [显示深度]\n", argv[0]);
        start_path = ".";
    } else {
        start_path = argv[1];
    }
    
    if (argc > 2) {
        show_depth = atoi(argv[2]);
        if (show_depth < 1) show_depth = 1;
    }
    
    printf("=== 递归目录遍历工具 ===\n");
    printf("起始路径: %s\n", start_path);
    printf("显示深度: %d\n\n", show_depth);
    
    // 开始遍历
    traverse_directory(start_path, 0, show_depth);
    
    // 显示统计信息
    printf("\n=== 遍历统计 ===\n");
    printf("总目录数: %d\n", total_dirs);
    printf("总文件数: %d\n", total_files);
    printf("总大小: %.2f MB (%lld bytes)\n", 
           total_size / (1024.0 * 1024.0), total_size);
    printf("最大深度: %d\n", max_depth);
    
    return 0;
}

编译和运行说明

# 编译示例程序
gcc -o getdents64_example1 example1.c
gcc -o getdents64_example2 example2.c
gcc -o getdents64_example3 example3.c

# 运行示例
./getdents64_example1
./getdents64_example1 /etc
./getdents64_example2 /home
./getdents64_example2 /usr/bin f  # 只显示普通文件
./getdents64_example2 /dev d      # 只显示目录
./getdents64_example3 /usr 2      # 显示2层目录结构

重要注意事项

  1. 缓冲区大小: 建议使用较大的缓冲区(如 4KB-8KB)以提高效率
  2. 目录文件描述符: 必须使用 O_DIRECTORY 标志打开目录
  3. 错误处理: 始终检查返回值和 errno
  4. 字符串处理: 目录项名称是 null 结尾的,但结构体大小可能包含填充
  5. 性能getdents64 比 readdir 更高效,因为它一次返回多个目录项
  6. 移植性: 这是 Linux 特有的系统调用

与 readdir 的比较

特性getdents64readdir
级别系统调用标准库函数
效率高(批量读取)较低(逐个读取)
使用复杂度复杂简单
可移植性Linux 特有跨平台
功能原始数据解析后的结构体

实际应用场景

  1. 文件管理器: 快速显示目录内容
  2. 备份工具: 扫描需要备份的文件
  3. 搜索工具: 快速遍历文件系统
  4. 系统监控: 监控目录变化
  5. 磁盘分析: 统计磁盘使用情况
  6. 安全扫描: 检查可疑文件

这些示例展示了 getdents64 函数的各种使用方法,从基本的目录读取到复杂的递归遍历,帮助你掌握这个高效的目录操作接口。

getdents64 是 Linux 系统底层目录扫描函数,用于高效读取目录内容。它直接与内核交互,返回包含文件名、inode号、文件类型等信息的原始目录项数据。相比高级接口如 readdirgetdents64 提供了更高的灵活性和性能。使用时需提供目录文件描述符、缓冲区和大小参数,返回实际读取字节数或错误码。该函数常用于需要精细控制目录扫描的场景,如文件系统工具开发。示例代码展示了如何用 getdents64 实现目录内容扫描和文件类型统计功能。

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

发表回复

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