比ls快8倍?百万级文件遍历的奇技淫巧 之 Linux海量文件数量统计(不能统计子目录),文章尾部链接可统计子目录

1.问题背景
在Linux下当我们操作一个文件数较少的目录时,例如执行ls列出当前目录下所有的文件,这个命令可能会瞬间执行完毕,但是当一个目录下有上百万个文件时,执行ls命令会发生什么呢,带着疑问,我们做了如下实验(实验中使用的存储设备为NVMe接口的SSD):

[root@localhost /data1/test_ls]# for i in {1..1000000}; do echo 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' > $i.txt ; done
[root@localhost /data1/test_ls]# time ls -l | wc -l
1000001

real    0m5.802s
user    0m2.544s
sys      0m3.328s

可以看到,统计一个包含1000000个小文件的目录下的文件个数花费了将近6秒的时间,那么文件个数多造成ls缓慢的原因是什么呢,且听我们详细分析。

2.原理分析
众所周知,strace是分析系统调用的利器,所以我们用strace来分析在大目录下执行ls命令的结果,其中这样的输出引起了我们的注意:

...
getdents(3, /* 1024 entries */, 32768)  = 32768
getdents(3, /* 1024 entries */, 32768)  = 32768
getdents(3, /* 1024 entries */, 32768)  = 32768
getdents(3, /* 1024 entries */, 32768)  = 32768
brk(0)                                  = 0x12e8000
brk(0x1309000)                          = 0x1309000
getdents(3, /* 1024 entries */, 32768)  = 32768
mremap(0x7f93b6246000, 2461696, 4919296, MREMAP_MAYMOVE) = 0x7f93b5d95000
getdents(3, /* 1024 entries */, 32768)  = 32768
getdents(3, /* 1024 entries */, 32768)  = 32768
getdents(3, /* 1024 entries */, 32768)  = 32768
brk(0)                                  = 0x1309000
brk(0x132a000)                          = 0x132a000
...

可以看到,在大目录下执行ls命令会频繁调用getdents这一系统调用,实际上我们通过查看coreutils的ls.c源码可以发现:

 static void
 print_dir (const char *name, const char *realname)
 {
   register DIR *dirp;
   register struct dirent *next;
   register uintmax_t total_blocks = 0;
   static int first = 1;

   errno = 0;
   dirp = opendir (name);
   ...
   while (1)
     {    
       /* Set errno to zero so we can distinguish between a readdir failure
      and when readdir simply finds that there are no more entries.  */
       errno = 0; 
       if ((next = readdir (dirp)) == NULL)
     {    
       if (errno)
         {    
           /* Save/restore errno across closedir call.  */
           int e = errno;
           closedir (dirp);
           errno = e; 

           /* Arrange to give a diagnostic after exiting this loop.  */
           dirp = NULL;
         }    
       break;
     }    

ls会首先调用opendir打开一个目录,然后循环调用readdir这个glibc中的函数直到遇到目录流的结尾,也即读完所有的目录项(dentry)为止。我们首先看一下man page里面对于readdir的定义:

struct dirent *readdir(DIR *dirp);

readdir返回一个指向dirent结构体的指针,指向目录流dirp中的下一个目录项,所以在print_dir的循环中,每次从目录流中取出一个目录项并赋值给next变量。既然说到目录流(directory stream),我们顺便看一下glibc中对它的定义:

#define __dirstream DIR

struct __dirstream
  {
    int fd;            /* File descriptor.  */

    __libc_lock_define (, lock) /* Mutex lock for this structure.  */

    size_t allocation;        /* Space allocated for the block.  */
    size_t size;        /* Total valid data in the block.  */
    size_t offset;        /* Current offset into the block.  */

    off_t filepos;        /* Position of next entry to read.  */

    /* Directory block.  */
    char data[0] __attribute__ ((aligned (__alignof__ (void*))));
  };

从上面的定义中可以看到,目录流实则维护一个buffer,这个buffer的大小由allocation来确定,那么问题来了,allocation值什么时候确定,其实是在opendir过程中确定下来的。opendir的调用路径如下所示:

__opendir-->__opendirat-->__alloc_dir

在__alloc_dir中,

DIR *
internal_function
__alloc_dir (int fd, bool close_fd, int flags, const struct stat64 *statp)
{
    ...
    const size_t default_allocation = (4 * BUFSIZ < sizeof (struct dirent64)
                     ? sizeof (struct dirent64) : 4 * BUFSIZ);
    size_t allocation = default_allocation;
    ...
    DIR *dirp = (DIR *) malloc (sizeof (DIR) + allocation);
    ...

    dirp->fd = fd;
    ...
    dirp->allocation = allocation;
    dirp->size = 0;
    dirp->offset = 0;
    dirp->filepos = 0;

    return dirp;
}

会分配sizeof(DIR) + allocation大小的内存空间,最后将allocation赋值给目录流dirp的allocation变量。allocation的默认值通过比较4*BUFSIZ的大小和dirent64结构体的大小(<32768)来确定,BUFSIZ的大小在以下几个头文件中定义:

stdio.h:        #define BUFSIZ _IO_BUFSIZ
libio.h:        #define _IO_BUFSIZ _G_BUFSIZ
_G_config.h:    #define _G_BUFSIZ 8192

回看一下strace中的输出,getdents第三个参数以及返回值32768就是这么来的。
讲完目录流的buffer大小是怎么确定的之后,让我们回到readdir的glibc实现。

DIRENT_TYPE *
__READDIR (DIR *dirp)
{
    DIRENT_TYPE *dp;
    ...
    do
    {
        size_t reclen;
        if (dirp->offset >= dirp->size)
        {
            /* We've emptied out our buffer.  Refill it.  */
            size_t maxread;
            ssize_t bytes;
#ifndef _DIRENT_HAVE_D_RECLEN
            /* Fixed-size struct; must read one at a time (see below).  */
            maxread = sizeof *dp;
#else
            maxread = dirp->allocation;
#endif
            bytes = __GETDENTS (dirp->fd, dirp->data, maxread);
            ...
            dirp->size = (size_t) bytes;

            /* Reset the offset into the buffer.  */
            dirp->offset = 0;
        }

        dp = (DIRENT_TYPE *) &dirp->data[dirp->offset];

#ifdef _DIRENT_HAVE_D_RECLEN
        reclen = dp->d_reclen;
#else
        assert (sizeof dp->d_name > 1);
        reclen = sizeof *dp;
        dp->d_name[sizeof dp->d_name] = '\0';
#endif
        dirp->offset += reclen;

#ifdef _DIRENT_HAVE_D_OFF
        dirp->filepos = dp->d_off;
#else
        dirp->filepos += reclen;
#endif

      /* Skip deleted files.  */
    } while (dp->d_ino == 0);
    ...
    return dp;
}

这段代码的逻辑还是比较清晰的,首先判断目录流的偏移量有没有超过buffer的大小,如果超过,则说明已经读完缓冲区中的所有内容,需要重新调用getdents读取,getdents一次最多读取32768个字节(有_DIRENT_HAVE_D_RECLEN定义时为dirp->allocation),并将读取到的buffer返回给dirp->data,读取到的字节数返回给dirp->size,然后重置偏移量为0。如果没有超过buffer大小,则从dirp->offset开始读,然后将偏移量增加reclen个字节作为下次读取的起点,reclen记录在目录项结构体dirent的d_reclen变量中,表示当前目录项的长度,dirent(DIRENT_TYPE)这个结构体的定义如下所示:
总结一下以上整个过程就是,ls命令每次调用readdir都会从目录流中读取一个目录项,如果目录流的buffer读完,就会重新调用getdents填充这一buffer,下次从新buffer的开头开始读,buffer的默认大小为32K,这也就意味着如果一个目录下有大量的目录项(目录项的总大小可以通过ls -dl查看),则执行ls命令时将会频繁地调用getdents,导致目录下的文件数越多时ls的执行时间越长。

3.解决方法
既然glibc中readdir的buffer大小我们没法控制,何不绕过readdir直接调用getdents,在这个系统调用中我们可以直接控制buffer的大小,以下就是一个简单的例子listdir.c:

#define _GNU_SOURCE
#include <dirent.h>     /* Defines DT_* constants */
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/syscall.h>

#define handle_error(msg) \
        do { perror(msg); exit(EXIT_FAILURE); } while (0)

struct linux_dirent {
    long           d_ino;
    off_t          d_off;
    unsigned short d_reclen;
    char           d_name[];
};

#define BUF_SIZE 1024*1024*5

int
main(int argc, char *argv[])
{
    int fd, nread;
    char buf[BUF_SIZE];
    struct linux_dirent *d;
    int bpos;
    char d_type;

    fd = open(argc > 1 ? argv[1] : ".", O_RDONLY | O_DIRECTORY);
    if (fd == -1)
        handle_error("open");

    for ( ; ; ) {
        nread = syscall(SYS_getdents, fd, buf, BUF_SIZE);
        if (nread == -1)
            handle_error("getdents");

        if (nread == 0)
            break;

        printf("--------------- nread=%d ---------------\n", nread);
        printf("inode#    file type  d_reclen  d_off   d_name\n");
        for (bpos = 0; bpos < nread;) {
            d = (struct linux_dirent *) (buf + bpos);
            printf("%8ld  ", d->d_ino);
            d_type = *(buf + bpos + d->d_reclen - 1);
            printf("%-10s ", (d_type == DT_REG) ?  "regular" :
                             (d_type == DT_DIR) ?  "directory" :
                             (d_type == DT_FIFO) ? "FIFO" :
                             (d_type == DT_SOCK) ? "socket" :
                             (d_type == DT_LNK) ?  "symlink" :
                             (d_type == DT_BLK) ?  "block dev" :
                             (d_type == DT_CHR) ?  "char dev" : "???");
            printf("%4d %10lld  %s\n", d->d_reclen,
                    (long long) d->d_off, d->d_name);
            bpos += d->d_reclen;
        }
    }

    exit(EXIT_SUCCESS);
}

在这段代码中,我们将getdents的buffer大小设置为5M,编译执行这段代码,我们得到如下结果:

[root@localhost /data1]# time ./listdir test_rm | wc -l
1000016

real    0m0.755s
user    0m0.432s
sys     0m0.320s

统计目录中的文件数由默认的5.802s缩短为0.755s,可以看到提升还是较为明显的。
4. 总结
其实不止是ls命令,其他一些命令如rm -r等的实现中都会用到glibc中的readdir函数,所以如果遇到操作百万级文件的大目录这种场景(当然实践中不提倡一个目录下放这么多文件),不妨直接调用getdents并加上自己的一些逻辑,这样就可以在实现标准命令功能的基础上,还能获得其不具备的性能提升。

本文分享自微信公众号 - 腾讯数据库技术(gh_83eebc796d5d),作者:jaredzhao
原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。
原始发表时间:2017-12-27

参考链接(可以统计子目录及文件):
https://blog.csdn.net/guotianqing/article/details/89054843

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: MATLAB可以通过调用Excel COM对象来读取Excel数据,并使用MATLAB的绘图函数来绘制图形。 以下是一个简单的示例代码,用于读取Excel文件中的数据并绘制折线图: ```matlab % 读取Excel文件 excel = actxserver('Excel.Application'); workbook = excel.Workbooks.Open('data.xlsx'); sheet = workbook.Sheets.Item(1); range = sheet.UsedRange; data = range.Value; % 关闭Excel workbook.Close(false); excel.Quit(); % 提取数据并绘图 x = data(:,1); y = data(:,2); plot(x, y); xlabel('X'); ylabel('Y'); title('数据图'); ``` 在这个例子中,我们首先使用`actxserver`函数创建一个Excel COM对象,然后打开Excel文件并选择要读取的工作表。使用`UsedRange`属性可以获取工作表中使用的单元格范围,然后使用`Value`属性将数据读取到MATLAB中。 读取数据后,我们可以使用MATLAB的绘图函数(例如`plot`)来绘制图形。在这个例子中,我们使用第一列作为X轴数据,第二列作为Y轴数据,并添加一些标签和标题。 最后,我们需要关闭Excel COM对象,以释放资源并避免内存泄漏。这可以通过调用`Close`和`Quit`方法来完成。 ### 回答2: MATLAB是一款广泛应用于科学计算和工程设计等领域的软件,它可以快速读取Excel文件并进行数据分析和图形绘制。在本文中,我们将介绍如何使用MATLAB读取Excel数据并绘图。 1. 读取Excel文件 MATLAB可以通过使用readtable函数轻松地读取Excel文件中的数据。readtable函数可以读取Excel文件中的所有数据或指定工作表中的数据。 创建Excel文件: ![excel文件示例1](https://img-blog.csdn.net/20180425173105957?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbGltaXRfZmFjdHVyZXI=) 代码: ```matlab table = readtable('data.xlsx'); %读取整个excel xls文件 % table = readtable('data.xlsx', 'sheet', 'Sheet1'); %读取data.xlsx文件的sheet1数据 data = table2cell(table); % 将 table 类型 转换为 cell 类型 ``` 2. 数据处理 在MATLAB中,我们可以使用不同的数据处理方法,例如加,减,乘和除等运算。可以使用MATLAB的内置函数对这些数据进行各种类型的统计分析。如果我们要画图,通常需要做的数据处理有数据清洗、数据转换和数据缩放等。 例如,如果我们想要绘制Excel中两个数字列的对比,那么就需要将这两列分别读取然后进行处理,生成新的比较数据列,再将新的列绘制在一个图表上。这些数据处理方法在MATLAB中也非常容易实现。 3. 绘制图表 MATLAB支持各种类型的图表绘制,例如散点图,直方图,折线图,饼图和二维/三维图等。在本例中,我们将展示如何绘制两列数据的对比图。 代码如下: ```matlab % 读取Excel文件 table = readtable('data.xlsx'); data = table2cell(table); % 提取需要比较的两列 col1 = cell2mat(data(:,1)); col2 = cell2mat(data(:,2)); % 绘制散点图 scatter(col1,col2); % 添加标题和标签 title('Comparison of two columns from Excel'); xlabel('X axis label'); ylabel('Y axis label'); % 设定X和Y坐标轴的范围 xlim([min(col1) max(col1)]); ylim([min(col2) max(col2)]); ``` 以上代码通过使用scatter函数生成散点图,为图表添加了标题和标签,并设定了X和Y轴的范围。 通过这篇文章,我们介绍了如何使用MATLAB读取Excel数据并绘制图表。MATLAB可以轻松将Excel文件中的数据读取到MATLAB命令窗口,并使用MATLAB的各种数据处理和图表绘制工具可视化数据。如果您经常需要处理和绘制Excel文件中的数据,使用MATLAB非常方便。 ### 回答3: MATLAB是一种非常简单、快速的数值计算与数据可视化软件。它可以轻松读取Excel文件中的数据,并且可以将这些数据拟合成人们所需要的更具有生动性和可视化的图像。 读取Excel数据及转换 MATLAB软件自带内置函数load(),可以直接读取Excel文件中的数据。首先,需要打开Excel文件,然后选择“另存为”类型为“CSV(逗号分隔)(*.csv)”,在保存的过程中Excel表中的“逗号”被视为分隔符号被存储为CSV文件(即数据以逗号分隔的形式存储在文件中),进而可以读取和加载。 代码示例: filename = 'data.csv'; %文件名为data.csv delimiter = ','; %指明分隔符为"," startRow = 2; %数据从excel表格的第2行开始 formatSpec = '%f%f%f%f%f%f%f%f%[^\n\r]'; %读取出每列数据格式 fileID = fopen(filename,'r'); %以只读方式打开data.csv dataArray = textscan(fileID, formatSpec, 'Delimiter', delimiter,'HeaderLines', startRow-1, 'ReturnOnError', false); fclose(fileID); %关闭文件 Data = [dataArray{1:end-1}]; %读取表格中数值型数据 textData = dataArray{end}; %读取表格中字符数据 clearvars filename delimiter startRow formatSpec fileID dataArray ans; 实现数据可视化 在读取数据之后,可以对数据做一些运算或者改变展现形式(如某些奇技淫巧),从而可以通过MATLAB进行更直观的图像展示。 代码示例: 1.绘制折线图 plot(Data(:,1),Data(:,5));%绘制第1列与第5列之间的折线 2.绘制散点图 scatter(Data(:,2),Data(:,5));%绘制第2列与第5列之间的散点图 3.绘制柱状图 bar(Data(:,3));%绘制第3列的柱状图 4.绘制饼状图 pie(Data(:,7));%绘制第7列的饼图 5.绘制3D图 mesh(Data(:,6),Data(:,4),Data(:,8)); %绘制第4、6和8列的三维坐标系 总结与展望 MATLAB读取Excel数据并绘图是一个相对简单但又十分实用的技能。在实际应用中,数据可视化有助于我们更好的了解数据,从中更快速和高效地获取我们所需要的信息,更精确地进行决策。 值得注意的是,MATLAB读取Excel并绘图也存在一些限制性,如对数据量大小、数据类型以及对于不符合默认条件的Excel文件格式等方面进行的处理等。因此,在实际使用过程中,需了解Excel数据的具体格式,并合理使用MATLAB函数进行分析与展示。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值