Linuxでgolangを使用してfind(1)の代替を作成する

ある内部ユーティリティについては、findユーティリティと同様にディレクトリの変更のスキャンを書く必要があり、予期しないことに出くわしました:標準のOpen()+ Readdir()+ Close()は、Linuxの動作に比べて非常に遅いです検索ユーティリティ。 写真では、このユーティリティの痕跡を見ることができます。 彼女は非常に奇妙なシステムコールを行うことがわかります。これらのシステムコールでは、Linuxでのみ動作しますが、同等の速度で実行されるアナログ検索を見つけて記述しようとします。





素朴な実装



まず、findの「単純な」実装を作成します。これは、単純にするために、ディレクトリ内のファイルを再帰的にリストします。



package main import (...) func readdir(dir string) { dh, err := os.Open(dir) defer dh.Close() for { fis, err := dh.Readdir(10) if err == io.EOF { break } for _, fi := range fis { fmt.Printf("%s/%s\n", dir, fi.Name()) if fi.IsDir() { readdir(dir + "/" + fi.Name()) } } } } func main() { readdir(os.Args[1]) }
      
      





完全なコード、インポートおよびエラー処理付き



このコードを実行すると、確かに機能しますが、壁時間とルッセージは検索の約3倍になります(1つのファイルの違いは、検索とは異なり、要求されたディレクトリ自体を印刷しないためです) :



 $ time find /path/to/dir | wc -l 169561 real 0m0.333s user 0m0.104s sys 0m0.272s $ time GOGC=300 ./find-simple /path/to/dir | wc -l 169560 real 0m1.160s user 0m0.540s sys 0m0.944s
      
      







システム時間が既に非常に長いことに注意してください、そして出力バッファリングを追加してください:



 @@ -7,5 +7,7 @@ import ( +var bufout = bufio.NewWriter(os.Stdout) func readdir(dir string) { @@ -28,3 +30,7 @@ func readdir(dir string) { for _, fi := range fis { - fmt.Printf("%s/%s\n", dir, fi.Name()) + bufout.WriteString(dir) + bufout.WriteByte('/') + bufout.WriteString(fi.Name()) + bufout.WriteByte('\n') if fi.IsDir() { @@ -44,2 +50,3 @@ func main() { readdir(dir) + bufout.Flush() }
      
      





フルバージョン



結果はすでにはるかに優れていますが、それでも理想からはほど遠いです。



 $ time GOGC=300 ./find-simple-bufio /path/to/dir | wc -l 169560 real 0m0.796s user 0m0.352s sys 0m0.616s
      
      







たとえば、同じPHPプログラムの結果は次とおりです。



 $ php ~/find-ob.php /path/to/dir | wc -l 169560 real 0m0.777s user 0m0.276s sys 0m0.500s
      
      







PHPプログラムは少し高速であり、消費するリソースがかなり少ないです! これは明らかに、コンパイルされた言語から得たいものではありません...



Linuxシステムコールの学習



findからstraceをよく見ると、getdents64を作成し、statをほとんど作成していないことに気付くでしょう。 しかし、同時に、ユーティリティはどの名前がディレクトリに対応するかを何らかの方法で知っています。 getdents64の呼び出しがどのようなものかを見てみましょう。



 SYNOPSIS int getdents(unsigned int fd, struct linux_dirent *dirp, unsigned int count); DESCRIPTION This is not the function you are interested in. Look at readdir(3) for the POSIX conforming C library interface. This page documents the bare kernel system call interface. The system call getdents() reads several linux_dirent structures from the directory referred to by the open file descriptor fd into the buffer pointed to by dirp. The argument count is specifies the size of that buffer. The linux_dirent structure is declared as follows: struct linux_dirent { ... char d_name[]; char pad; // Zero padding byte */ char d_type; // File type (only since Linux 2.6.4 }
      
      







これはまさに私たちが必要とするものです! 興味深いことに、BSDシステムでディレクトリを読み取る場合、一部のファイルシステムではファイルタイプ(d_type)のフィールドも使用できます。 このシステムコールを使用することは強くお勧めしませんが、findユーティリティは少し気にしません。



結局のところ、「内部」で標準のgoライブラリはgetdents(2)も呼び出すので、これを行うコードを「切り取り」、不要なものをすべてクリアする必要があります。



 package main import (...) var bufout = bufio.NewWriter(os.Stdout) func readdir(dir string, file string, dirfd int, pathbuf []byte) { origbuf := make([]byte, 4096) // ,    getdents dh, err := syscall.Openat(dirfd, file, syscall.O_RDONLY, 0777) //       origpathbuf := pathbuf[0:0] //       ,   for { n, errno := syscall.ReadDirent(dh, origbuf) //  getdents if n <= 0 { break // EOF } buf := origbuf[0:n] //     ,    :) for len(buf) > 0 { //      ParseDirent: dirent := (*syscall.Dirent)(unsafe.Pointer(&buf[0])) buf = buf[dirent.Reclen:] if dirent.Ino == 0 { // File absent in directory. continue } bytes := (*[10000]byte)(unsafe.Pointer(&dirent.Name[0])) name := bytes[0:clen(bytes[:])] //  clen()     if len(name) == 1 && name[0] == '.' || len(name) == 2 && name[0] == '.' && name[1] == '.' { continue } //         , //        pathbuf = append(pathbuf, dir...) pathbuf = append(pathbuf, '/') pathbuf = append(pathbuf, file...) dirlen := len(pathbuf) pathbuf = append(pathbuf, '/') pathbuf = append(pathbuf, name...) pathbuf = append(pathbuf, '\n') bufout.Write(pathbuf) //   ,   ,  string if dirent.Type == syscall.DT_DIR { readdir(string(pathbuf[0:dirlen]), string(name), dh, origpathbuf) } pathbuf = origpathbuf[0:0] } buf = origbuf[0:0] } syscall.Close(dh) } func main() { dir := os.Args[1] parentDir := filepath.Dir(dir) dh, err := os.Open(parentDir) pathbuf := make([]byte, 16 * 1024) readdir(parentDir, filepath.Base(dir), int(dh.Fd()), pathbuf) bufout.Flush() }
      
      





エラー処理とclen関数を備えたプログラムの完全なソースコード



結果



すべての最適化とシステムコールを使用して、getdentsはリソースの消費を、見つけるよりも速く動作する程度にまで削減しました。



 $ time GOGC=300 ./find-simple /path/to/dir | wc -l 169560 real 0m1.160s user 0m0.540s sys 0m0.944s $ time GOGC=300 ./find-optimized /path/to/dir | wc -l 169560 real 0m0.200s user 0m0.060s sys 0m0.160s $ time find /path/to/dir | wc -l 169561 real 0m0.333s user 0m0.104s sys 0m0.272s
      
      







リソース消費と総実行時間は、findの1.5倍です。 これの主な理由は、findがファイルのソートされたリストを返し、リストをフィルタリングする条件も受け入れるためです。



明らかに、ファイルのリストを取得して画面に表示するだけではあまり価値がありませんが、この方法を使用すると、多くのgoプログラムを高速化できます。たとえば、ディレクトリからファイルのリストを読み取るオーバーヘッドを減らすことで、 grepの代替手段を提供します



参照:




All Articles