使用lstat对所有文件进行golang os *File.Readdir操作。能否优化?

7
我正在编写一个程序,使用os.File.Readdir查找所有子目录,这些子目录包含大量文件的父目录。但是运行strace以查看系统调用计数时,发现Go版本在父目录中的所有文件/目录上使用了lstat()。(我现在正在使用/usr/bin目录进行测试)
Go代码:
package main
import (
        "fmt"
    "os"
)
func main() {
    x, err := os.Open("/usr/bin")
    if err != nil {
        panic(err)
    }
    y, err := x.Readdir(0)
    if err != nil {
        panic(err)
    }
    for _, i := range y {
    fmt.Println(i)
    }

}

程序上的Strace(不跟随线程):
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 93.62    0.004110           2      2466           write
  3.46    0.000152           7        22           getdents64
  2.92    0.000128           0      2466           lstat // this increases with increase in no. of files.
  0.00    0.000000           0        11           mmap
  0.00    0.000000           0         1           munmap
  0.00    0.000000           0       114           rt_sigaction
  0.00    0.000000           0         8           rt_sigprocmask
  0.00    0.000000           0         1           sched_yield
  0.00    0.000000           0         3           clone
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         2           sigaltstack
  0.00    0.000000           0         1           arch_prctl
  0.00    0.000000           0         1           gettid
  0.00    0.000000           0        57           futex
  0.00    0.000000           0         1           sched_getaffinity
  0.00    0.000000           0         1           openat
------ ----------- ----------- --------- --------- ----------------
100.00    0.004390                  5156           total

我使用了C语言的readdir()进行了相同的测试,但没有看到这种行为。
C代码:
#include <stdio.h>
#include <dirent.h>

int main (void) {
    DIR* dir_p;
    struct dirent* dir_ent;

    dir_p = opendir ("/usr/bin");

    if (dir_p != NULL) {
        // The readdir() function returns a pointer to a dirent structure representing the next
        // directory entry in the directory stream pointed to by dirp.
        // It returns NULL on reaching the end of the directory stream or if an error occurred.
        while ((dir_ent = readdir (dir_p)) != NULL) {
            // printf("%s", dir_ent->d_name);
            // printf("%d", dir_ent->d_type);
            if (dir_ent->d_type == DT_DIR) {
                printf("%s is a directory", dir_ent->d_name);
            } else {
                printf("%s is not a directory", dir_ent->d_name);
            }

            printf("\n");
        }
            (void) closedir(dir_p);

    }
    else
        perror ("Couldn't open the directory");

    return 0;
}

程序上的Strace:
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00    0.000128           0      2468           write
  0.00    0.000000           0         1           read
  0.00    0.000000           0         3           open
  0.00    0.000000           0         3           close
  0.00    0.000000           0         4           fstat
  0.00    0.000000           0         8           mmap
  0.00    0.000000           0         3           mprotect
  0.00    0.000000           0         1           munmap
  0.00    0.000000           0         3           brk
  0.00    0.000000           0         3         3 access
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         4           getdents
  0.00    0.000000           0         1           arch_prctl
------ ----------- ----------- --------- --------- ----------------
100.00    0.000128                  2503         3 total

我知道在POSIX.1中强制规定的dirent结构中仅有d_name和d_ino两个字段,但我正在为特定的文件系统编写此代码。
尝试使用*File.Readdirnames()函数,该函数不使用lstat并提供所有文件和目录的列表,但是要查看返回字符串是文件还是目录最终又需要再次使用lstat。
我想知道是否有可能以一种避免不必要地对所有文件进行lstat()的方式重新编写go程序。我发现C程序正在使用以下系统调用open(“/usr/bin”,O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC)= 3 fstat(3,{st_mode = S_IFDIR | 0755,st_size = 69632,...})= 0 brk(NULL)= 0x1098000 brk(0x10c1000)= 0x10c1000 getdents(3,/ * 986条目 * /,32768)= 32752
这是否像是过早优化的问题,不必担心?我提出这个问题是因为被监视的目录中的文件数量将具有大量小归档文件,并且系统调用之间的差异几乎是C版本和GO版本之间的两倍,这将影响磁盘。

1
这个包看起来应该提供所需的行为。 - user142162
谢谢@TimCooper。如果你能把它作为答案,我会接受它。 - nohup
2个回答

7

这个包dirent似乎可以完成你想要的功能。下面是用Go语言编写的你的C示例:

package main

import (
    "bytes"
    "fmt"
    "io"

    "github.com/EricLagergren/go-gnulib/dirent"
    "golang.org/x/sys/unix"
)

func int8ToString(s []int8) string {
    var buff bytes.Buffer
    for _, chr := range s {
        if chr == 0x00 {
            break
        }
        buff.WriteByte(byte(chr))
    }
    return buff.String()
}

func main() {
    stream, err := dirent.Open("/usr/bin")
    if err != nil {
        panic(err)
    }
    defer stream.Close()
    for {
        entry, err := stream.Read()
        if err != nil {
            if err == io.EOF {
                break
            }
            panic(err)
        }

        name := int8ToString(entry.Name[:])
        if entry.Type == unix.DT_DIR {
            fmt.Printf("%s is a directory\n", name)
        } else {
            fmt.Printf("%s is not a directory\n", name)
        }
    }
}

通过你的示例代码,我只能获取到大约 28,000,000 个文件夹中的前 63 个文件。你有什么想法为什么会这样? - donatJ

2
从Go 1.16(2021年2月)开始,一个好的选择是使用os.ReadDir
package main
import "os"

func main() {
   files, e := os.ReadDir(".")
   if e != nil {
      panic(e)
   }
   for _, file := range files {
      println(file.Name())
   }
}

os.ReadDir返回fs.DirEntry而不是fs.FileInfo,这意味着省略了SizeModTime方法,使得过程更加高效。

https://golang.org/pkg/os#ReadDir


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接