今回は久しぶりにカタツージさんの自作ツールである lsっていう CLIの話をしていけたらなと思うんですけど、
これは自作のlsコマンドになるんですかね。 readme見る限りパフォーマンスを機にした lsコマンドみたいな感じなのかなと思ったんですけど、
そもそもの話、なんでlsコマンドを再実装しようと思ったんですかね。 はい、それが結構いい質問で、実はlsって使えないケースあるんですよね。
使えないケース?ちょっとなんかパッと思いつかないというか、そんなケースあるんだって感じなんですけど、どういったケースですかね。
たまにあるんですけど、その一つのディレクトリーの中に何万ファイルとかって入っちゃってるみたいなディレクトリーで lsって打つと固まるんですよ、実は。
もう結果が返ってこないって感じなんですね。 結果が返ってこないですね。
で、これ理由の一つとして、lsってファイル名で相当してたりするんですよ、デフォルトだと。 なのでその相当無効にすれば少しマシになります。
でも少しなんですね。 そうですね、それでもやっぱりものすごい量あると出せないです。
固まってしまって結果が返ってこないというような結果になってしまうと。 そうですね。
で、実はこの先に言っちゃうとこの ls では最終的に3100万ファイル入ってディレクトリーのファイルのリストを全部出すことに成功してます。
3100、100万、3100万ファイル。 そうですね、その量になるともう ls だと歯が立たないです。
ちなみに今回その事例だとどういうファイルだったんですかね。 普通に画像とかユーザーが投稿したファイルですよ。
なるほど、じゃあ結構画像単体ですかそれとも雑多なものが入ってるみたいな。
まあ確か雑多だったかなあんまり記憶してないんですが、確か画像とかが中心だった気がします。
じゃあバイナリファイルとかがいっぱいあってっていう感じなんですね。 そうですね。
なんか聞く限りそのファイルのサイズには別に依存しないって感じですか。
ファイルのサイズは関係ないですね。 ファイルの数です、純粋に。
単純に量が多すぎて固まってしまうっていうのが既存の ls ではあったっていう感じなんですね。
だから ls という自作の ls コマンドを作ったっていうのがまず一番最初の背景としてある。
そうですね、この症状って昔から自分は知ってたんですよね。
ファイルがたくさん入ってる、ディレクトリーの中にファイルがたくさん入ってると ls できないっていうのは自分たまに見てたんで知ってたんですけど、
けどこれを解決する方法があるって思ってなかったんですよ。
なんか単純に ls でできない、そもそもリナクセン標準で入っているコマンドでできなかったらなんか難しいんじゃないかなって単純に思っちゃうんですけど。
そう、自分もそう思ってて ls が打てなくなるからこんなんやっちゃダメっていう知識だけあって、
じゃあ実際やっちゃった場合どうなるかっていうといやもう ls 打っても帰ってこないんだからお手上げだよねって昔の自分は思ってました。
はい、でも違った。
実が違ったんですよ。それを実装したのが ls なんでちょっと今回その ls の話をしたいなと思ってます。
はい、お願いします。
まず ls の話をする前にこのポッドキャストでも何回か話しているシステムコールってそもそもなんだっけってところからちょっといろいろ話をして少しずつ ls に近づいていければと思ってます。
Linux のシステムコールですね。
システムコールは過去何度か話している通りなんですけど、Linux のアプリケーションって基本的にユーザー空間っていうので動いていて、
ユーザー空間のプログラムだとなんか計算1たす1は2とかそういう計算とかできるんだけどファイルを読み書きするとかネットワークを通信するとかそういうのってできないんですよ。
その実ファイルに何て言うんでしょうね何かを書き込む操作だったりとかあとはデバイス系を操作する。
そうですね標準入出力とかもデバイス変更しないといけないのでできないです。
基本的にはできないよねっていうのを橋渡しするのがシステムコールって感じですか。
そうですねあのそういうその実際のハードウェアを変更するようなことってLinux kernel しかできないのでアプリケーションはLinux kernel に対してこれやってくださいってお願いするんですね。
そのお願いするのがシステムコールって呼ばれているものです。
なんか API みたいな感じですね。
雑に言うと。
それがシステムコールです。
でシステムコールはどうやって呼び出すのかっていうところでシステムコールには番号が割り当てられてるんですよ。
番号。
例えばあのここのねちょっとめんどくさいんだけど例えばなんかそうですねオープンっていうそのファイルをまず開くのにオープンというシステムコールがあるんだけど
例えばそのみんなが使っているようなそのARM例えば自分今M1Mac使っているからARMだと5番っていう番号が割り当てられていて
5番のシステムコールを呼ぶとオープンですみたいな感じになってるんですね。
なるほど。
この番号を呼び出すために実はC言語だとlibcって呼ばれているレイヤーを使ってます。
そのシステムコールを直接呼ぶんじゃなくてlibcを介して実装することが普通なんですね。
そのシステムコールに割り当てている番号がlibcの中に書かれてるっていうようなイメージですかね。
そうです。このlibcの関数呼び出すとシステムコールの5番が呼び出されますとかそういうのになってるんですけど別にそれって普通にCの例えばコードとかで5番とかって書けばいいじゃんって思うじゃないですか。
決め口で書いてしまうってことですね。
そうですね。でいいじゃんって思うと実はね何でC言語にlibcってものがあるのかって話になってくるんだけど
さっき5番って言ったんだけどこれARMだと5番であとpeki86って昔のインテルの昔のやつも5番なんですけど
例えばAMD64だと2番だったりとかあとMIPSっていう昔PSPとかで使われていたようなプロセッサーだと4005番とかあとMIPSの64ビットだと5002番とか全然違うんですよ。
同じシステムコールなのにCPUのプロセッサーによって全然違う番号が実は割り当てられてる。
でこれを自分でプロセッサーごとに実装するのってまあまあ無理じゃないですか。
全部の対応パターンを把握しないといけないのでCPUの説明書とかを全部ダウンロードしてちゃんと見ていかないといけないので結構難しいなと思いますね。
そうなのでC言語はlibcを使うのが一般的なんですね。
Libcを使うとそのLibcの関数を呼び出すことで全部どんなプロセッサーでも同じ関数が呼び出せる。
でプロセッサーごとにLibc側が吸収してくれるんですね。
そのどの番号を使うかっていうのを吸収してくれるっていう風になります。
だからC言語はLibcを使うのが一般的ですと。
でLibcは大きく今多分Linuxで使われてるんだと2つあるかなと思っていて。
一番有名なのがGNUのGlibcって言われてるやつですね。
これが多分一番使われてるやつです。
で比較的最近マッスルLibcってやつもあってアルパインLinuxってドッカーとかで多分使ったことあると思うんだけど。
軽量なLinuxですか?
そうですね。アルパインLinuxは実はGlibc使ってなくてマッスルLibcっていうのを使ってます。
別実装なんですね。
そうですね。でマッスルLibcの方が実装がかなり単純で結構読みやすいんですね。
なので自分は結構Libcの内部実装を調べたくなったらマッスルLibcを調べることが多いです。
なのでなんかそのLibcの実装ちょっと確認したいなと思ったらマッスルLibcのソースコード読むのを自分は結構お勧めしてます。
基本的には挙動は一緒だからマッスルLibc読んでおけば大丈夫?
そうですね。基本的には挙動は一緒。基本的にはGlibcでもマッスルLibcでもコンパイルできるはずなんですよ。
もちろん細かい挙動が違うんでなんか微妙にバグっぽく動くみたいなのも全然あってそういうの検索すればいろいろ出てくると思うんですけど一応動くはずってことになってます。
なるほど。すごいちょっとだけ脇道にそれたいんですけどここで気になっちゃったのでGlibcとかマッスルLibcとかいろんな実装あるわけじゃないですかCにシステムコールを呼び出す関数というような意味で
CPUのアーキテクチャごとに番号が違うわけですよね。
そうですね。
単純にオープンとか同じシステムコールだったとしても。これはCPUのアーキテクチャが新しいのを作ったら絶対にこのLibcをアップデートしないといけないってことですよね。
そうですね。
システムコールの番号を変えるんであれば仕様として。
そうですね。新しいプロセッサー追加したらそのLibcのそのプロセッサー用のLibc用意しないといけないですね。
だからGlibcにもマッスルLibcにも例えばAppleのM1のやつがリリースされる前には絶対にこうアップデートが入ってるみたいな感じになってるんですかねCPUの作り方ちょっとよくわかってないですけど。
Appleのやつでいうとアームなんですよ。アーム社から買ってきたプロセッサーの設計図で作ってるんでアームのアーキテクチャでコンパイルできるはずなんですね。
なので完全に新しい例えばその今だとリスクファイブっていうのがかなり新しいやつなんですけどそのリスクファイブを対応するためにLibcはもちろん追加されたし
今Linux kernelとかもそれでコンパイルできるように頑張って変更したりとかそういうのはやってますね。
それをやるのは基本的にはCPUを作る人たちって感じですよね。
結構みんなで分担してやってるんじゃないかなと思いますけどね。
そういう感じなんですね。
もちろんC言語のコンパイルができなかったらそのプロセッサー作る人誰もいないので基本プロセッサー作った人が中心になるのかなと思いつつ多分いろんな人で手分けしてやってると思いますよ。
そういう時にアームとかのアーキテクチャを使って流用して新しいCPUとか作ってればもうアームに適合しているLibcとかはもうすでに対応しているからそこら辺の手入れは必要ないよねっていうのが結構あるんですね。
なんでアームとかあざわざ独自のやつ使わずにアームとか使うかっていうとそういうところをですね、もうすでにLibcがあるからっていうところですね。
かなり納得感がある説明を聞けました今。なんでアームとか使うんだろうなとか若干もやるところがあったのでそういう理由もあるんだっていうのか。
分かりました。ありがとうございます。ちょっと脇道に逸れちゃったんですけど次の話行きましょう。
そうですね。多分みんなLinuxとか使ってたらマンってコマンド、マニュアル見るためにマンってコマンド使ったことあると思うんですけど、
マンってコマンドのヘルプみたいなのを出す機能がよく使われるかなと思うんですけど、システムコールとかあとLibcの関数名とかそういうのも実は調べることができます。
この辺番号でシステムコールだと2番でLibcの関数だと3番とかその番号があるんですけど、その番号で実はマニュアル出せたりするので興味あったら見てみてくださいと思っています。
例えばシステムコールの場合はマンスペースにスペースオープンとかするとオープンシステムコールのヘルプが見れますね。
システムコールの名前とコマンド名が同じやつとかもたまにあったりするんで、マンスペースでそのコマンド名とか打っちゃうとどっちが出るかわかんないので、
だから2番とか3番とかちゃんと指定しましょうって感じになります。
なるほど、既存で入っているコマンドと被ってる名前の場合ってことですね、オープンとかはあるからってことですね。
そうですね、そういう被ってるやつたまにあるので。
だからシステムコールって明示的に調べたい場合はちゃんと2番とか番号を入れておきましょうってことですね。
ここまでCの話をしてきたんですけど、5の話をここで差し始めるんですけど、5って実はLibc使ってないんですよ。
内部で使ってないんですね。
あれってことはさっき言ったLibcのレイヤーってどうやってんのって思うと思うんですけど。
これは5言語が独自実装している?
そうなんですよ、5って結構恐ろしくてLibcのレイヤーを全部自分でやってます。
大変なことしてますね。
そうすることを見てもらうと、アーキテクチャーごとに全部別ファイル、もちろんLibcも同じことやってるんですけど、5の内部でもLibcと同じことやってて、
アーキテクチャーごとに全部別のファイル名になってて、その中見ると全部定数がいろいろ定義されていて、
さっき言ったようなシステムコールの番号とか全部定数で定義されてます。
再実装してしまってるんですね。
Libcを完全に自分で再実装してます。
だから5はLibc依存はないんですけど、やっぱり5以外のほとんどの言語はCで作られてるので、
基本的にはLibc依存ってある言語の方が多いと思うんですけど、
5は本当にLibcも自前で実装してるので、Libcに依存してないっていうのが5のすごいところです。
すごいですね。例えばRubyとかの場合はCで書かれてるので、もちろんLibcとかに依存してるのかなと思うんですけど、
5の場合は違うんですね。
5は違うんですよ。
なんで相手そんなことしてるのか若干気になるんですけど、その話はせずに。
せずにというか、単純にC自体を作り直したかったんだと思うんですよね、5って。
なるほど、思想的に。
だからLibcのバグっぽい挙動とかも実は直してたりとかするんですよ、5って。
Libcってやっぱりその互換性を重視しているので、そのやっぱり挙動を変えるとCのプログラムが全部壊れちゃうから直せないんですよね、Libcって。
バグだったとしてもそのバグが一部使用化してるってことですね。
バグかどうかは人によると思うんだけど、なんかちょっと使いにくいなみたいなところがあっても直せないんですよね、Libcって。
けど5は自分で1からやっているから、そういうのも直ってたりするし、あとなんか一番有名なやつだと、
Openっていうシステムコールで、以前もちょっと話したかな、フラグってやつがあって、
そのファイルをオープンしたときに、そのファイルが存在してなかったらファイルを勝手に作るってオプション、カラーファイルを作るってオプションがあって、
これオークリエイトってオプションなんですけど、オークリエイトのクリエイトって、作るのクリエイトかと思いきや最後にEがないんですよね。
なんかちょっと変、中途半端になってて、でこれはなんでこうなってるかっていうと、なんかただのスペルミスらしいんですよ。
書き損じなんですよね。
そうらしくて、けど直せないんですよそれも。
直しちゃったら、Eを付けるのが必須にしてしまったら、
もうすでに動いてるCのプログラムが動かなくなってしまうから。
動かなくなっちゃうから、多分スペルミスなんだけど直されてない。
けどGOはOSパッケージにOS.OCREATEっていう定数があるんですけど、これはEがちゃんと付いてる。
直してますね。
直してるんですよ。
実はこれなんかどうも自分あんま詳しくないんだけど、GOを作った人たちって当時のC言語を作った人たちが結構入ってて、
実はこのスペルミスした人がGOを作った人の中に入っているらしいです。
ちょっと反省活かしてるんですね。
これを多分直したかった、多分ずっと直したかったんだと思うんですよ。
でも互換性のために直せなくて、GOを作った時に直したっていう。
すごいですね、なんか気持ちいいですね。
なので、GOはそういうリブシ依存も剥がして、リブシレイヤーも自分たちで作り直すっていうのをやった言語って感じですね。
素晴らしい。
じゃあ我々システムコール触れんの?GOの場合触れんの?っていうと、
実は標準ライブラリにSyscallっていうパッケージがあります。
なのでこれを使うとシステムコール結構直接触ることができます。
ただこのSyscallパッケージは新しいシステムコールとか機能追加って基本的にしない方針らしくて、
より広いサポートが欲しい場合は、その準標準パッケージのXSysUNIXってやつがあるので、そっちにどんどん追加していくっていう感じになるらしいです。
これは何でなんですかね?何か事情があったりするんですかね?
多分あんまり詳しくないんですけど、ちょっと標準パッケージの方はあんま大きくしたくないみたいで、こういう風になっている。
できる限り小さく保ちつつ、その準標準パッケージというか市民権を得ているパッケージの方にどんどん追加していこうという。
そうですね。GOってやっぱりすごい互換性気にする言語なんで標準パッケージに足しちゃうと絶対機能変えられないんですよ。
多分そういうのもあってあんまり公式のパッケージに機能を増やしたくないんだと思いますね。
なんか今の話だけ聞くと、Cの2の前というか、もうGOの中でも若干ミスってるところとかあるんじゃないかなって若干思えてしまうんですけど。
そうですね。その辺の話は今回はしないんですけど、
ちょっと置いといて。
その辺の話もね最近ちょっとずつで始めてるかなという気がしますね。
やっぱりそうなんですね。
まあまあまあしょうがないですね。
それはやっぱり長くやってたらどうしようもないと思っているので。
なのでそういう標準パッケージを多分小さくしたいということで、Syscallパッケージは今後あんまり機能を足さないようにするという方針みたいです。
じゃあ基本的にSystemCall5から使う場合はXSysUnixパッケージですか?
標準パッケージのSyscallパッケージで使えるんだったらそっちを使う。
そっちになかったらXSysUnix見に行けば多分入ってるんじゃないかなというところですね。
なるほど。
今回のLLSはSyscallパッケージに入ってるので、具体的に言うとGetDents64っていうシステムコールを呼び出していて、
Syscall.GetDentsってやつを使うと呼び出せるので、それを使っています。
GetDentsっていうものがあるんですね、システムコールに。
そうあるので、それ何やるかはちょっとまた後で話すかなという感じがします。
これを使えば標準のLSコマンドでできなかった大量の何千万あるファイルのリストアップができると。
できるというのをこの後また少しずつ話をしていきたいなと思ってるんですけど、まだ距離があります。
まだそこまでいかないんですね。
ここでちょっと緩和救済というか、ここまで話すと大抵以前も似たような話をしたと思うんだけど、
GoってGoで書かれてるので、この辺の話すると最初のGoってどうなったのって大体聞かれるんですよ。
なのでGoのブートストラップ問題について軽く話したいなと思っていて、
Goって実はGo1.4まではCで書かれてたんですよ。
なのでGo1.4まではGCCとかCのコンパイラーでコンパイルできたんですね。
Go1.5でGoで書かれるようになったんですよ。
セルフコンパイルができるようになった。
そう、Go1.5で。
なのでGo1.5のバイナリーを欲しい場合は、Go1.4でCのコンパイラーでコンパイルして、
Go1.4を手に入れてからGo1.5のソースコードをコンパイルするっていう風にやると、
Go1.5が手に入るって状況になったんですね。
実はGoってGo1.19までGo1.4でコンパイルする前提でやりました。
なのでかなり長い期間Go1.4がGoのコンパイラーを作る上では支配的だった。
けどGo1.19までいくとかなりいろんな機能が追加されているので、
さすがにGoのコンパイラーで新しいGoの機能を使えないのはしんどいってなったんですよ。
なのでGo1.20からブートストラップをGo1.17まで更新しました。
かなりジャンプアップしたんですね。
なのでGo1.20のGoを手に入れるにはまずGo1.4をCコンパイラーでコンパイルして、
Go1.4を手に入れた後にGo1.17をコンパイルして、
Go1.17を手に入れてそれをGo1.20のソースコードをGo1.17でコンパイルすれば手に入るっていう風になります。
最初からやりたかったらっていう話ですね。
そうですね。実際にはバイナリーが配布されているのでコンパイル済みの。
それはみんなダウンロードしてるだけだと思うんだけど、
自分でコンパイルしたいってなった場合はそうする必要がある。
まずは1.4からコンパイルする必要性があるってことですね。
そうですね。なのでGo1.4の最後のGoはCで書かれた最後のGoなので今でも特別されているバージョンだったりします。
なんでGo1.17かっていうとGo1.18でジェネリックスが入ったんですよ。
でかい変更ですね。
でかい変更が入ったんでGo1.18はちょっとバグってるかもしれないから避けたいってなってGo1.17になりました。
なるほど。安全性を気にしたんですね。
そうですね。その後Go1.22以降からブートストラップバージョンは順次勝手にどんどん上げていこうっていう風になっていて、
Go1.26は1.24.6以上を使うっていうルールになっています。
すごい。じゃあここからGo1.26を一番最初からコンパイルし直したかったらめちゃくちゃめんどくさいことになりますね。
そう。何回も何回もGoをコンパイルしてやっと手に入るっていう風になっています。
すごい。
みたいな感じですね。GoはGoで書かれているのでそういうことがあるという風になっています。
ちなみに一番最初のGo1.4Cで書かれている最後のGoとしてはコンパイラーがCで書かれているバージョンなんですけどここでさえもLibCには依存してなかったってことですかね。
いやそこはLibCに依存してますね。
依存してるんですね1.4は。
そうですね。そこはしょうがないですね。
じゃあ正確に言うとGo1.5からLibCに依存していないGoになっていったというところなんですかね。
Goのコンパイラー自体があった感じですね。Goの言語自体はLibCに依存していないです。
コンパイラーがLibCに依存しているかどうか。
コンパイラーを作るのにLibCとかCに依存していたってことですね。
理解です。なるほど。じゃあGo1.4の時点ですでにGo言語自体はLibCに依存していないってことですね。
依存していなかったですね。
理解しました。
ここからまたGoの言語仕様の話にどんどん入っていくんですけど、まずGoって配列とスライスっていう2つの概念があるんですよ。
配列とスライス。
一般的にはスライスしかみんな知らないかなと思います。
なんでかっていうと配列を使う標準ライブラリの関数あるあるんですけどそんなに多くないので基本的にみんなが使うのはスライスの方です。
リストを管理したかったらスライスを使おうねみたいな。
まあ配列ですかね。配列を使いたい時はスライスを普通Goは使います。
なんか配列って名前が2つ使われてるんでどっちがどっちのことなのかわかんなくなっちゃいますが。
そうなんで一般的に配列みたいなデータコードを使いたいと思ったらスライスを使うんだけど実はGoには配列がまた別にある。
また別概念としてあるんですね。配列というものが。
そうですね。どう違うかっていうとスライスの方は長さが決まってないというかその長さを伸ばせるんですね。
スライスはその内部でポインターとレングスとキャパシティっていうのを持っていてポインターは配列のポインターになるんですよスライスって。
配列のサイズはキャパシティっていうのに持っていてどんだけデータが入るか。
レングスのレンの方はスライスとしてどんだけのサイズで扱われているかっていうのが入ってるんですよ。
レンの方は今現在のデータの長さ。
そうですね。
キャパシティの方はどれだけメモリ上に載せられるかってことですね。
そうですね。
最大サイズみたいな感じですね。
そうですね。配列はもう長さが完全に決まっている。
固定長ってことですね。
固定長で途中で変えられないんですよ。そもそもサイズごとに全部型が別々になっちゃうんで。
型自体が違うんですね。
配列は型自体が違います。長さが違うと。
なるほど。
やっぱりC言語の感覚とここから変わってくるんですけど、C言語の配列って実は参照型で、
例えばC言語でAの配列、Aって名前の配列作ったら、Aって書くと一番先頭要素のポイントになるんですよ。
これC言語の特殊なところなんですけど。
けど、号は配列自体が値なんですよね。
配列自体がそもそもバリューというか実体なんですね。
実体になっているので、ポインターじゃないんで、ここがCと違うところなので、ここを気をつけてほしくて。
スライスは内部で配列へのポインターを持っているので、実質的に参照型になっているんですよ。
だから違いとすれば、号の配列の方は関数とかに渡すと値をコピーする。
値をコピーします。号の配列は。
スライスの方はポインターを渡しているので、同じメモリ空間を上書きするんですね。
実体をコピーしないって感じですね。
理解。理解しました。
Cの配列と微妙に挙動が違うっていうのを理解してほしくて。
さっき言っている通り、普通の号のプログラムではスライスしか出てきません。
なんで今回配列が出てくるかというと、今回システムコールを直接実行するので、
基本的にシステムコールってほとんどの場合C言語で実装するし、
あとそもそもLinuxカーネル自体がCで実装されているので、Cの世界なんですよ。システムコールって。
なのでそのシステムコールのCの世界の配列を号の世界で受け取らないといけないんですね。
そうするとそれはスライスじゃなくて号の配列と対応するんですよ。Cの配列は。
スライスで対応できないんですね。対応しようと思えば多分できるんだろうけど。
対応しようとすると、号のプログラム上でコピーするとかそういう特殊なことしないといけない。
システムコールから直接受け取れるのは配列になるんですね。
変換をわざわざ号の方でしないといけないんですね。スライスで扱うには。
そうですね。なのでシステムコールを今回直接触らないといけないので、この辺は普通の号のプログラムとは違います。
もう配列で受け取らないといけないっていう風になります。
ってことはシステムコールから返ってくる配列は、さっき言った通り固定帳で型ごとに長さが違うっていう話なので、システムコールから返ってくる型がわからないってことですかね。
システムコールから返ってくる型は決まっていて、C言語の構造体で決まってるんですよ。
なのでそのC言語の構造体と同じメモリでマッピングできる号の構造体っていうのを作って、それにマップしてもらうって形になります。
なるほど。それに合ってなければメモリエラーとかでクラッシュするとかそういう感じなんですね。
そうですね。やったことないですけど、構造体の形変えちゃうとCの構造体とマッピングできなくなるんで、エラーとかになるはずですね。
それがコンパイル時なのか事故時なのかはタイミングというか各所によるのかなという気が若干しますか。
そうですね。もしシスコールパッケージで定義されてる構造体じゃないとコンパイルできないと思うんで、多分コンパイルで落ちると思うんですけど、
自力で変な関数とか用意したら変なことできるかもしれないけど、基本的にはシスコールパッケージ使ってたらコンパイルで落ちるはずですね。
じゃあ型がはっきりしてるってことですね。
そうですね。ここまでで、割とやっと前準備なのかなと思っていて。
長い。
長いんですよ。今回問題になってるのってReadDIRっていうLibcの関数なんですよ。
ReadDIR。
これを使うとLSが基本的には実装できる。だけどこのReadDIRに実は問題があって、
さっきちょっと話したGetDense64っていうシステムコールがあって、
ReadDIRは内部でこのGetDense64を呼び出しているんですけど、呼び出すときにそのGetDense64は配列のバッファーを渡してあげると、
その中にファイル名のリストとか構造体でファイル名が入っているような構造体をガッと渡してくれるんですけど、
それをそのバッファーのサイズがちっちゃいんですよ。
ReadDIRで実装されているGetDense64の実装方法で渡しているメモリの容量が小さすぎると。
そうですね。
なのでなんでLSできないかっていうと、デフォルトのReadDIRだとそのGetDense64に渡しているバッファーのメモリーサイズがちっちゃいので、
何回も何回も呼び出し続けないといけないんですね。
足りるまでというか終わるまで。
全部出てくるまで何回も何回も呼び出し続けてるんだけど、それがいつまで経っても終わらないので途中で固まっちゃう。
なるほど。ループし続けてた結果ずっと終わらないよねってことになるってことですね。
そうです。なのでLLSがやってることって実はすごくシンプルで、
ReadDIRみたいなやつを呼ぶんじゃなくて、GetDense64のシステムコールを自分で直接呼び出しに行ってます。
その時にバッファーサイズデフォルトだと5MBにして渡しています。
この5MBだと実は今回さっき3100万ファイルって言ったんだけど、5MBだと足りないんですよ。
足りないんですよね。
足りないんですけど、けど4文で複数回呼び出すことができて、複数回呼び出すと続きを取得できるんですよ。
なのでその続きを取得して、最後まで返ってこなくなったら終わり。
GetDenseで何バイト読み込めたかっていう値が返ってくるので、
0って返ってきたら0バイト読み込めたってことだから、もう終わりだねって判断できるので、
0だったら終了。0が返ってくるまで読み続けるってプログラムになってます。
5MB、5MB、5MB、5MB、最終1MB、0MBとかそういう感じになるってことですね。
そんな感じになるはず。そういう感じでできるんですよ。
これいくつか注意点があって、まずC言語の文字列ってチャー型っていうのに入ってくるんですね。
文字列の終端がいわゆるなる文字って呼ばれているやつで、全ビット0ってやつなんですよ。
終端文字列ってやつですね。
たぶん大学とかで情報系だったらC言語の授業でそういうの習ったかなと思うんですけど。
やりましたね。基本情報とかでやった気がしますね。
それが返ってくるんですよ。ただ5の文字列と全然違うので。
5上ではただの配列で返ってくるので、
その5の関数、自分は5の関数でそのなる文字、いわば全ビット0がどこで入ってるのかっていうのを見て、
そこまでで文字列にするっていう関数を作って、それで文字列に最終的にしてます。
5の世界でそれを実装しないといけないってことですね。
そうですね。
終端文字列を判定しないといけないってことですね。
そう、終端文字列までが文字数なので、それで文字数が分かるんで、
それで5の文字列に変換して扱うようにする。
なんかやってること5書いてるのにC言語やってるみたいな感じですね。
そう、まさにそうで。やっぱシステムコールがCの世界なので、
5のコード書いてるんだけど、ここはCの構造体だからこうやらないとダメみたいなのを考えないといけないんですよ。
ちょっと嫌すぎる。
そう、なのでLLSは実は関数1個にこの辺閉じ込めてて、
この関数だけがCの世界で、それ以外は全部5の世界みたいな風になるようにしてます。
すごい分かりやすい設計をしてるんですね。
やっぱCの世界は見たくないんで、その関数を触るときだけ仕方ないから触るんだけど、
この関数を見なくていいときはもう普通の5のプログラム書けばいいっていう風な設計になっています。
分かりやすいですね。
これもいくつか気をつけてるのが、
Iノードナンバーっていうのがあって、まずはLinuxのファイルについてちょっと説明をしたいんですけど、
Linuxのファイルって、Iノードナンバーっていうのがあって、それが実体なんですよね。
ディスク上に番号を振られてて、そのIノードナンバーっていうのがファイルの実体。
このファイル名のファイルはこのIノードナンバーですよ、みたいなのが入ってるんですよ、Linuxって。
ポインターというか、インデックスされてるって感じですよね。
そうですね。
すべて列挙されてるってことですね、ファイルの一覧が。
そうなんですよね。なので、そのIノードナンバーっていうのが0のやつがあるんですよ、たまに。
0?
そう、これはファイルの実体がないけど、ディレクトリには残ってるみたいなのがあるので、こういうのってLSとかやると表示されないんで、
みんな気づかないんだけど、自分で呼び出しちゃうと0ってやつがたまに返ってくるので、その0はもうないからスキップしないといけない。
隠しファイルとかでもないってことですよね、だから。
そうですね。
Linux上、今ちょっと残っちゃってるゴミファイルだけど、0として扱っとこうみたいなことになってるわけなんですね。もう削除してもいいけどっていう。
ファイルとしてはないけど、なんか残っちゃってるみたいなやつがIノードナンバーってやつで、あとLinuxの主要で、ディレクトリの中はドットとドットドットっていうファイルが必ずあるんですね。
ありますね、前のディレクトリとか。
ドットは自分自身で、ドットドットがその1個上のディレクトリを指すファイルで、これは絶対あるんですよ。
けど別にそれは表示する必要ないんで、それは無視するっていうコードが入っていたりします。
それもIノードナンバー振られてるんですかね。
これはどうだったかな。
それ自体は。
これ振られてんのかな、ちょっとわからないですね。
でも判定することができるってことですよね。
そうですね、単純に文字列としてそのファイル名でそれがドットとかドットドットだったら無視するってコードを入れてます。
なるほど、ちょっとめんどくさいですね。
そう、すごくめんどくさいんですけど、それをやることでGoの世界でシステムコールを呼び出して、
実際にLibcよりもバッファーのサイズを大きくした上でそのシステムコールを呼び出すっていうすごく特殊なことができるようになります。
これやりたかったことってリードDIRにバッファーサイズを指定できるようになってたらよかったですよね。
そうなんですよ、それめちゃくちゃ思って。
リードDIRでバッファーサイズとか指定できればこんなことやり必要なかったんですよ、実際。
そんなゲット伝通64まで潜らなくてよかったですもんね。
そう、それがね、実はこれを作ってるときめちゃくちゃ思ったんですよ。
Libcのソースコード読みに行って、これ外から渡せないんだってちょっと絶望しました。
そういうインターフェースになってないってことですね。
そう、インターフェースになってない。
なぜなんでしょうかって感じでは。
なんでなんだろうって自分もすごい思ったんだけど、もうそうなっちゃってるんで、もう頑張るしかないと。
だから低レイヤーのシステムコールを呼ぶしかないっていう。
自分でシステムコール呼ぶしかこの問題を解決できないんだってことに気づいたんですよ。
これやったときにLibcのコードも読みに行ったんですよ。
本当にこれできないのかなと思って。できないってことが分かったんで、これを自分で実装しました。
ちなみにちょっと思ったのが、LS自体が対応してほしいですよね。
ちゃんとどんだけのファイルがあったとしても、そのディレクトリの中にどんだけファイルがあったとしても、
ちゃんとパフォーマンス上、ちゃんと列挙できるようになっててほしいなってなんとなく思うんですけど、
LS自体の更新ってされてないんですかね。
どうなんですかね。実はちょっと調べたら、G-Libcは、マッスルLibcは固定なんですけど、バッファーのサイズが。
G-Libcは自動的に1MBまで増えるみたいな仕組みが実はあるらしくて、
けど自分の経験上うまくいってないんですよ。なぜかというと、これでうまく動いてたらLLS作ってないんで。
自分の環境だとうまく動かなかったんだけど、たぶんG-Libc側はLibc側でなんとかできないのかっていうのを頑張ってるっぽいですね。
けどやっぱり実体としてはうまくいってないし、でも外から渡せるかっていうと、
それもさっきちょっと言った通り、Libcのインターフェースを壊せないから、渡す方法がないんですよね。
なんかすごく間にはまってしまっている感じですね。今までの仕様通りにしないといけないんだけど、
それを破壊しないと、そもそもメモリの確保っていうところができづらいインターフェースになってしまっているがゆえにできないところってことですね。
そうなんですよ。なので、これをやったときに本当にできないのかなと思ってLibcのコード見に行ったりとか色々したんだけど、
結局結論としてはできない。やるとしたら自分でシステムコール叩くしかないし、
Cで書いてもいいんだけど、やっぱり自分は5が好きなんで、5でもできそうだったから、5でやったらできたっていうのがこのLLSなんですよ。
なるほど。すごい長旅でしたけど、すごい納得感ありますね。
なんで再実装したのかっていうところと、やっぱりすごい昔からメンテナンスされているものの動かせないもどかしさっていうところを感じましたね、感想として。
そうですね。そもそもなんでこんなことを気づいたかっていうと、自分もさっき最初に話した通りこれやることないと思ってたんですよ。
どうしようもないと思ってたんだけど、実はこのポッドキャストでたまに出てくるハッカーニュースにちょうど自分がこの問題困ってたときに、
800万ファイルを出すことに成功したぜっていう記事を書いた人がいて、それがハッカーニュースに投稿されてちょっとだけバズってたんですよ。
で実際の日本語の記事とかでも、実はなんかハッカーニュースでバズってるよみたいな記事いくつか出てたと思うんですけど、その記事いいんだけど、この記事の問題点はそのCで直接システムコール読んだらできたぜって書いてあるけど、その実装は公開されてなくて、こうやったらできたぜって書いてあるだけなんですよ。
このシステムコールを使ったらできたぜと書いてあるが、
このシステムコールをバッファー大きくして呼び出したらできたぜって書いてあるが、じゃあこの実装使ったらできるぜとは書いてない。
までは行けてないが、ちょっと実験してやってみたらできたよっていうような報告。
しかもこれもう800万なんですよ。
そうだから、うちの3100万ファイルこれで出せるかどうかって分かんなかったんだけど、けどもうできるって言われたらやりたいじゃないですか。
てかやるしかないんですよ。
あれですよね、だからそもそも3100万のファイルリストが3100万かどうか分からなかったってことですよね。
ファイルの数も最初分かんなくて、ただ800万より多いっての分かってたんですよ。
LSで、LS確か頑張って出して1000万ぐらいで確か止まったんですよね。
あそこまではいったんですね。
確かそこまではギリギリいけて、なんで800万より多いってことは分かってたんですよ。
けどこれできるって言われたらやりたいし、でなんかソースコードはないから自分で作るしかない。
でちょっと調べたら、Goでも一応触れそうってことが分かったんで、じゃあ作るかと思って作ったんですよ。
すごい技術的にできそうっていうところと、ちょっと面白そうというところ掛け合わさって自分でやりたくなってしまったってことですね。
そうですね。そもそもやらないとやばかったっていうのが一番大きいんですけど。
普通に業務的にやらないといけなかったっていうのが。
それで言うともうこれ公開情報なんで話せるんですけど、もともとデータセンターでオンプレで動いてたんですね。
でファイルのシステム、ファイルサーバーのシステムがあって、外部のベンダーのファイルシステムで動かしてたんですよ。
そこにもうそのすっごいでかい、すごいたくさんのファイルが入ってるディレクトリがあって、それが映せない。
LSもできないしR-Syncもできないからもう他のサーバーに映せないと。
そのデータは必要なデータだったってことですね。
サービスで使われてるデータでもう映せないと。
ただそのファイルサーバーがもう容量埋まっちゃいそうだってなってて、夜埋まりそうなのにファイルのLSとかできないからもう映せないってなってたんですよ。
タイムリミットが存在している。あともうちょっとで埋まりそう、移行もできなさそうっていう状態だったと。
最終的に頑張ってその新規のファイルだけ別のところに移して、とりあえずリードオンリーにしたんですよ。
タイムリミット問題はとりあえず解決はした。
一旦タイムリミット問題は解決したが、したがって感じじゃないですか。
したがって感じですね。それずっとメンテナンスするんですか?
ファイルサーバー一生動かせないんだけどどうすんのってなって、しかもオンプレデータセンターも一生やめれないねってなったんですよ。
これちょっとまずいなと思って、どうやったらいいのかなって最初わからなかったんですけど、とりあえずファイル名のリストさえ出せればあとどうとでもなるじゃないですか。
ちょっとずつ移してくるとコピーしてくれる。
とりあえずなんでもいいからファイル名のリストだけくれって思ってたんですよ。
このハッカーニュースの記事見てこれやと思って実装してやるぞって動かしたらなんと取れたんですよ。
それで3100万ファイルだってことが後で発覚したんですよ。
素晴らしい。
これでやっぱり解決できたんですね最終的に。
技術で解決できましたね。
当時自分最初不可能だと思ったけどやる方法があって実際に解決できたっていう事例になるんですね。
ちなみにその時ファイルのリスト3100万のファイルのリストが得られたわけじゃないですか。
その後やることってファイルたちをどこかのサーバーとかに移行していく、転送していくって作業になってくるのかなと思うんですけど。
そこでなんとなくやれなかったのかなと思うのがファイル名はどうでもいいからそのディレクトリの中全部アスタリスクで全部移しちゃうみたいなことはできなかったんですかね。
それすらもできなかったんですかね。
それできないんじゃないかな。やってないというかそもそも別のストレージサービスに移したかったんでできないんじゃないか。
なるほど。だからファイル名が必要だったってことですね。
パスとしてフルパスが必要だったってことですね。
そうですね。
なるほど。
なのでそうすれざるを得なかったってことですね。
で実はこの仕組みは無事ファイル名のリストが出せたんで、新しいストレージに移した後に最終的にAWSのシステムに移していて、今はクラウドに全部移ってます。
すごい。もう全部S3に保存されてる。
あのAWSで動いているって状況で、これをやるにはやっぱり新しいストレージに移せなかったらどうしようもなかったので、それができるようになったのはこのLLSのおかげなんですよ。
素晴らしいですね。全体把握ができるようになったっていうのはそれが必要だったから本当にLLSのおかげですよね。
そうですね。本当にここはもうどうしようもないって最初思ってたんで、これがいけたんで他にもいろいろ問題あったんだけど、一番不可能だと思われたやつが突破できたんで何とかなったって感じですね。
ここまでいろいろ話してきて、Cと5の比較をしたいなと思っていて、
じゃあなんでCでよかったならCでもいいんじゃないっていう人いると思うんだけど、
Cだと一番めんどくさいのはエラーが起こった時だと思っていて、
5だとディファーってやつがあってその関数が終了したらこのディファー呼ぶとかディファーの中の処理呼ぶとかできるんだけど、
Cってそういう仕組みないから頑張ってエラー起こったらGo to errorとか書いてそのエラーのところ呼び出すとか、
なんか頑張って書かないといけないし、
あとエラーが辛くて、これ自分がすごい嫌いなやつなんですけど、
エラーナンバーってやつがあってC言語って、
そのエラーが起こった時に、エラーナンバーってグローバル変数なんですけど基本的には、
グローバル変数に書き込みが入るんで、なんか変だなと思ったらそのグローバル変数のエラーナンバーっていうのを見に行くと、
何のエラーが起こったかわかるみたいな仕組みになっていて、
5だとその返り値が複数返せるから基本的に一番最後の返り値がエラーなんですよ、
なんかそういう文法ですよね、
そこ見ればいいんだけど、C言語はエラーナンバーってやつ見ないとエラーがわかんない、
なのでこれシステムコールのやつも一緒で、結局なんかエラー起こった時にCだとエラーナンバー見に行かないといけないけど、
5だと普通にエラー返ってくるからそこを見ればわかるっていう風になってる、
なんで結構ここがね、あの違いとしてあるなと思っていて、でなんでこんなことになるかっていうと、
あのC言語ってやっぱ昔からある言語なんで、
基本的にはあのCPUのレジスターに返り値入れてるんですよ、
CPUのレジスターってあの物理的に数に限りが結構少ないじゃないですか、物理的に数に結構限りがある、
なのでその2個も3個も返り値返せないんですよ、C言語って言語使用上。
なるほど、あの単純にメモリがちっちゃいってことですよね。
あのCPUのレジスターですね。
あの容量が少ないから渡せるものそんなに多くないってことですね。
そうですね、なのでもうエラーはグローバル変数に書き込むしかなかったんですね。
グローバル変数だったのは昔の話で、今はあのスレッドごとに、
あるスレッドごとに存在するグローバル変数みたいなすごい特殊な扱いになってるらしいんですけど、
けどGoだったら複数返り値返せるので、
これはなんかその今は高速化のために微妙に違うらしいんですけど、
もともとの実装だとGoはそのスタックに返り値とか全部入れてたんで、
普通にメモリに入ってたんですよね。
なので何個返しても全部スタックのメモリに積んでるだけだから、
複数の返り値を返せるみたいな感じになっていたと。
なのでここだけでもCよりGoの方がシステムプログラミングするときも、
なんかエラーの処理とかも書きやすいし、
結構Goでシステムコール直接呼び出すっていうのはメリットがあるかなと思ってます。
あと単純にGoの方がモダン言語だよねっていうところですよね。
それもあるかな。ただ正直システムコールに関しては、
Cとあんま変わんない、仕様感あんま変わんないし、
むしろドキュメント一切ないんですよ、Goのシステムコールのパッケージとかって。
Cの方を読んでくださいみたいな感じに多分なってて、
なので自分Cのマニュアルとか見に行って、Goのやつ読みに行くと、
あ、Cと全く同じだってなるのでやってたんですよ。
なのでGo側のドキュメントは一切ない、その代わり。
Cの読まないといけないけど、ただそれ以外は基本Goの方がやりやすいかなと思ってます。
だからこそさっき言っていた設計ですよね。
このファイル、この関数では絶対Cの書き方をするけど、
他のやつにはそのCの書き方は波及しないようにしておくっていう設計でカバーできるところで頑張るっていうところですよね。
そうですね。結構実はGoの標準パッケージの内部も一部そういうのあるんですよね。
実は使う分にはGoっぽい綺麗なことだけど、内部だと結局Linuxのシステムコールとか呼び出さないといけないから、
ちょっとGo的にはちょっと変なことやってるみたいな関数って実はいくつかあって、
だから実はGoの内部ではやってることを自分たちでやってるだけではあるんだよね。
けどそれを自分たちでやらないといけないっていうのがこのシステムコールを扱う場合は自分たちでやらないといけないし、
だからこそ自分はGoの標準パッケージでやってるのと同じように、
外面というか関数のインターフェースだけGoっぽくしておいて、内部ではCっぽいことやってるっていう関数が一個だけある。
そこを叩くとファイル名のリストがとりあえず返ってくるっていう風に作った感じですね。
やってることをGoの標準パッケージで作ってるみたいなことをシステムコールで扱う。
やってることはGoの標準パッケージを自分で作ったって感じですね。
そのためにCのマニュアルとかGoの実装とか読みつつ揃えてっていう。
そうですね。
わけ分かんないけどWindowsで動くみたいなことは絶対ありえない話なので。
そもそもコンパイルできないですよね。関数がないから。
なるほど。結構いい時間になってきたなと思うんですけど、まとめというか、他に話を聞きたいところとかあったりしますか?
そうですね。Webエンジニアだとシステムコールとかってそんなに普段触らないと思うんで、
そんなシステムコールなんて覚えて何になるのとかって言われること実際あるんですけど、
実際自分が出会ったこのファイル、そのディレクトリーの中にファイルが3100万ファイル入っていたって問題はもう立ち打ちできなかったんですよね。
システムコールの知識がなければ。けどシステムコールの知識が自分にたまたまあったからこういうLLSが開発できて実際に解決できたんですね。
実際に目の前に自分の目の前に起こった問題を解決できたんですね。
なので何かあった時にこういう知識がないとそもそも解決が不可能だったんですよ。
なのでそういうこともたまにあるのでやっぱりいろいろアンテナ張っておくとかもすごくいいんじゃないかなって自分は思っていたりします。
Webアプリケーションだけをいじっていて、あまりインフラにインフラまで手をいかないような人であったとしても何か何があるかわからないと。
いきなりオンプレの巨大なファイル群をどうにかしないといけないというタイミングがあるかもしれないと。
実際だって自分これシステムコールで直接自分でシステムコール叩けば解決するなんて夢にも思ってなかったんですよ。
一番最初の頃はそういう解決方法があることさえもわからなかったですかね。
できるらしいって聞いてほんまかって思いながら作ってとりあえずLSはできるようになったんで実行してみましょうってやって実行したら動いちゃったんですよ。
それでこれで解決したじゃんってなったんで。
なのでそういうことありますっていうすごいレアケースかもしれないけどそういうことあるので。
できる限り低レアというか本当にシステムコール部分のところの知識はあったら何かに役立つかもしれないよねっていうことですね。
でも何に関してもそうだなとは思います。
どれだけすごい低レアになっていくほどそれこそCPUの気持ちがわかればわかるほどきっとパソコンのことってわかると思うんですけどわからないですけど適当言ってますけど。
なのでできる限り低レアがわかるシステムそのレイヤーがどの程度かはまではわかんないですけどわかってるところが幅広いとやれること広がるよねって。
そうですね何事においても言えると思うんですけどやっぱり何か役立つことあるかもしれないんでなんかこんな使わないでしょうとか思わずにいろいろ学んでいくといいんじゃないかなと思うし。
当時これ作った時ってあんまチャットGPTとかなかった時代なんで自分で頑張って作るしかなかったんですけど今だったらAIでもある程度知識あれば作れるようになってきたと思うのでシステムコールでやればできるらしいぞってなったらじゃあちょっとAIと会話して作ってみようかとかそういうのも今は考えられると思うのでそういうふうにやったらいいんじゃないかなと思ってます。
いや非常に重要なところで今言ってもらったそのシステムコールでできるらしいぞという情報が今すごい重要なんですよね今って。
そうですね。
その情報知らなかったらプロンプトにクロードコートとかでシステムコールでこういうことしてできるかどうか調査してくださいって絶対打たないですもんね。
今はちょっと知識さえあればできるような時代になってるのでもっといろいろみんなもやったら面白いんじゃないかなって思ったりします。
なんかこれを聞いている人たちはこれでシステムコールの重要性というか勉強しがいがあるなというところをちょっと理解できてくれたかなとは思うのでね。