golang os *File.Readdir 在所有文件上使用 lstat。可以优化吗?

golang os *File.Readdir using lstat on all files. Can it be optimised?

我正在编写一个程序,该程序使用 os.File.Readdir 从包含大量文件的父目录中查找所有子目录,但是 运行 和 strace 来查看计数系统调用显示 go 版本在父目录中存在的所有 files/directories 上使用 lstat()。 (我目前正在使用 /usr/bin 目录进行测试)

转到代码:

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

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)
        }
    }
}

从 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 returns fs.DirEntry 而不是 fs.FileInfo,这意味着 SizeModTime 方法被省略,使过程更有效率。

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