Programming + file system and Go language functions above it (1)

Various storage is connected to the computer.It is difficult to cover the types, such as hard disks, SSDs, removable SD cards, DVD-ROMs for reading, Blu-ray, and written DVD-RW.

There are various types, but all storage can only be stored in the bits.Therefore, it is necessary to have a mechanism to manage the storage space with a specific rule.

For example, suppSe you want to open a text file in your local folder in an editor, edit it, and write it.Somewhere in the storage, there should be a bit column that represents the contents of the text file, but it is necessary to find a place where the entity is located from the file name.In addition, it is necessary to read the contents from it or overwrite new contents without any inconvenience.For that reason, the file system is equipped with the OS.

The current OS has various functions, such as multi -thread, complex authority management, and network, but not all OSs from the beginning.However, file systems have been almSt always provided in mSt OSs since ancient times.

This time, I will roughly talk about what a file system is, and then explain the basic function of GO language for the implemented file operation.The explanation of the inside of the OS basically assumes Linux.

Basics of file system

In the file system for storage, the storage is first treated as a sequence of a fixed length of 512 to 4 kilobytes (sector).Then, instead of storing only the contents of the file there, we also have an area for storing file management information.This management information is called INODE (NTFS in Windows and HFS+in MacOS, but there is a mechanism that replaces the Inode, a master file table and catalog node.)

The management information of the file stored in the inode also includes the physical placement information of the actual file.Inode also has a unique identification number.If you know the identification number, you can access Inode, and if you can access Inode, you will know the location of the actual file and access the contents.

The file structure organized by the directory we usually see is realized using this device.A directory is a special file that stores the file name contained under the distribution and the list of indexes of its Inode.And the root directory is always stored in the inode of the fixed number (No. 2).The starting point INODE number is known to be number 2, so you can access the inode, and the actual file indicated there is a root directory, so you can see the index of each directory and file in the subordinate.… In that way, the file structure and the storage sector can be correspond.

Complex file system and VFS

先の説明だと、ストレージ上にファイルシステムが一つしかないように読めるかもしれませんが、実際のストレージはもっと複雑に入り組んでいます。 ファイルシステムに他のファイルシステムをぶら下げたり(マウント)、仮想的なファイルシステムがあったりします。 仮想的なファイルシステムは、物理的なストレージと対応するものではありません。 仮想的なファイルシステムとしては、たとえば/proc以下の情報があります。/proc以下は、各プロセスの詳細情報がファイルとして見られるように、カーネルが動的に作り出したファイルシステムになっています。

また最近では、ジャーナリングファイルシステムといって、 書き込み中に瞬断が発生してもストレージの管理領域と実際の内容に不整合が起きにくくする仕組みも当たり前のように利用されています。 さらに、近年ではDockerなどのコンテナがよく使われていますが、 こうしたコンテナではファイルシステムの一部を切り出して、特定のプロセスに対して、あたかもそれがファイルシステム全体であるかのように見せかける仕掛けがあります。 具体的には、chrootというシステムコールを使い、擬似的なミニOS環境を作り出してサービスを隔離する方法が使われることがあります。

These various file systems are all unified in the Linux API called VFS (Virtual File System).Therefore, there is no need to worry about the difference in the system call, whatever the target file system.Under the VFS, the device driver translates the abstract operation into an operation for each file system and actually reads it.

Go language functions that handle file directory

ファイルシステムの話はこれくらいにして、Go言語でファイルやディレクトリを扱う方法を見ていきましょう。 ファイルシステム内部は高いパフォーマンスと柔軟性を両立するために複雑な仕組みになっていますが、アプリケーションからはVFSだけしか見えないためとてもシンプルに見えます。 今回の記事ではSパッケージの基本的な機能だけを紹介します。

File creation / reading

Go言語でファイルを新しく作成するにはS.Create()を使います。 また、すでに存在しているファイルはS.Open()関数で開くことができます。

これらの関数は、構造体のS.Fileを返します。 このS.Fileは、io.Writerインタフェースとio.Readerインタフェースを実装しているので、 連載の第2回から第4回で紹介した方法を使ってデータを読み書きできます。 復習になりますが、それぞれの使い方は次のとおりです。

package main import (    "fmt"    "io"    "S") // 新規作成func open() {    file, err := S.Create("textfile.txt")    if err != nil {        panic(err)    }    defer file.ClSe()    io.WriteString(file, "New file content\n")} // 読み込みfunc read() {    file, err := S.Open("textfile.txt")    if err != nil {        panic(err)    }    defer file.ClSe()    fmt.Println("Read file:")    io.Copy(S.Stdout, file)} func main() {    open()    read()}

なお、S.Create()S.Open()S.OpenFile()をラップする便利関数だと紹介しましたが (第3回「io.Reader前編」を参照)、 このS.OpenFile()を直接使えばファイルを追記モードで開くこともできます(ファイルを追記モードで開く便利関数は標準では提供されていません)。

// 追記モードfunc append() {    file, err := S.OpenFile("textfile.txt", S.O_RDWR|S.O_APPEND, 0666)    if err != nil {        panic(err)    }    defer file.ClSe()    io.WriteString(file, "Appended content\n")} func main() {    append()}

Creating directory

Creating directoryにはS.Mkdir()S.MkdirAll()を使います。

// フォルダを1階層だけ作成S.Mkdir("setting", 0644) // 深いフォルダを一回で作成S.MkdirAll("setting/myapp/networksettings", 0644)

Delete files, move, rename

ファイルや子の要素を持たないディレクトリの削除にはS.Remove()を使います。 C言語レベルのシステムコールでは、ファイルの削除(unlink())とディレクトリの削除(rmdir())とが別の機能になっています。 しかしGo言語のS.Remove()は、先にファイルの削除を行い、失敗したらディレクトリの削除を呼び出します。 したがって、対象がどちらであっても削除可能です。

プログラミング+ ファイルシステムと、その上のGo言語の関数たち(1)

対象がディレクトリで、その子供のファイルも含めてすべて再帰的に削除するときは、S.RemoveAll()を使います。

// ファイルや空のディレクトリの削除S.Remove("server.log") // ディレクトリを中身ごと削除S.RemoveAll("workdir")

特定の長さでファイルを切り落とすS.Truncate()という関数もあります。S.FileオブジェクトのTruncate()メソッドを使うこともできます。

// 先頭100バイトで切るS.Truncate("server.log", 100) // Truncateメソッドを利用する場合file, _ := S.Open("server.log")file.Truncate(100)

ファイル名を移動・リネームするにはS.Rename()を使います。 シェルのmvコマンドでは移動先がディレクトリの場合には同じファイル名でディレクトリだけを移動できますが、S.Rename()では移動先のファイル名まで指定する必要があります。 (ドキュメントには、OSによってはリネーム先が同じディレクトリでないとダメという制約について触れられていますが、デスクトップOS(Windows、Linux、macOS)であれば問題ありません。)

Windowsの場合、S.Rename()は同一ドライブ内の移動にしか使えません。 これは、利用しているMoveFileEx()というWin32 APIで別ドライブへのコピーを許容するオプションが設定されていないためです。

// リネームS.Rename("old_name.txt", "new_name.txt") // 移動S.Rename("olddir/file.txt", "newdir/file.txt") // 移動先はディレクトリではダメS.Rename("olddir/file.txt", "newdir/") // エラー発生!

POSIX系OSであっても、マウントされていて、元のデバイスが異なる場合にはrename システムコールでの移動はできません。 下記のエラーメッセージは、macOSでtmpfs というオンメモリの一時ファイルシステム(昔の人はRAMディスクと呼んでいました)を作ってS.Rename() を実行したときに返されるエラーです。

err := S.Rename("sample.rst", "/tmp/sample.rst")if err != nil {    panic(err)    // ここが実行され、コンソールに次のエラーが表示される    // rename sample.rst /tmp/sample.rst: crSs-device link}

デバイスやドライブが異なる場合にはファイルを開いてコピーする必要があります。 FreeBSDのmv コマンドも、最初にrename システムコールを試してみて(参照)、失敗したら入出力先のファイルを開いてコピーし、その後にソースファイルを消しています。

oldFile, err := S.Open("old_name.txt")if err != nil {    panic(err)}newFile, err := S.Create("/other_device/new_file.txt")if err != nil {    panic(err)}defer newFile.ClSe()_, err = io.Copy(newFile, oldFile)if err != nil {    panic(err)}oldFile.ClSe()S.Remove("old_name.txt")

Acquisition of file attributes

ファイルの属性は、S.Stat()と、S.LStat()で取得できます。 これらは対象がシンボリックLinkだった場合の挙動が異なります。S.Stat()は、指定したパスがシンボリックLinkだった場合に、そのLink先の情報を取得します。S.LStat()は、そのシンボリックLinkの情報を取得します。

すでにS.Fileを取得しているときは、このインスタンスのStat()メソッドでも属性を取得できます。

これらの返り値であるS.Fileinfo構造体からは、次の情報が取得できます。

メソッド情報
Name()stringディレクトリ部を含まないファイルの名前
Size()int64ファイルサイズ
Mode()FileModeuint32のエイリアス)ファイルのモード(0777など)
ModTime()time.Time変更日時
IsDir()boolディレクトリかどうかのフラグ

実際にどうやって使うか、サンプルを紹介しましょう。 次のコードは、S.FileinfoS.FileModeの各メソッドを実行してファイルの情報をコンソールに出力するものです。

package main import (    "fmt"    "S") func main() {    if len(S.Args) == 1 {        fmt.Printf("%s [exec file name]", S.Args[0])        S.Exit(1)    }    info, err := S.Stat(S.Args[1])    if err == S.ErrNotExist {        fmt.Printf("file not found: %s\n", S.Args[1])    } else if err != nil {        panic(err)    }    fmt.Println("Fileinfo")    fmt.Printf("ファイル名: %v\n", info.Name())    fmt.Printf("サイズ: %v\n", info.Size())    fmt.Printf("変更日時 %v\n", info.ModTime())    fmt.Println("Mode()")    fmt.Printf("ディレクトリ? %v\n", info.Mode().IsDir())    fmt.Printf("読み書き可能な通常ファイル? %v\n", info.Mode().IsRegular())    fmt.Printf("Unixのファイルアクセス権限ビット %o\n", info.Mode().Perm())    fmt.Printf("モードのテキスト表現 %v\n", info.Mode().String())}
$fileinfo move.go ⏎Fileinfo  ファイル名: move.go  サイズ: 129  変更日時 2017-01-15 10:45:33 +0900 JSTMode()  ディレクトリ? false  読み書き可能な通常ファイル? true  Unixのファイルアクセス権限ビット 644  モードのテキスト表現 -rw-r--r--

FileModeタイプ

I will add a little about the features unique to Go language.

FileModeは、実体は32ビットの非負の整数ですが、メソッドがいくつか使えます。 このようにエイリアス型であってもメソッドが追加できるのは、Go言語の特殊なオブジェクト指向の機能です。

なお、FileModeには対応する定数がいろいろ定義されていて、より詳細なファイルタイプをビット演算を使って取得できます。 詳しくはドキュメントを参照してください。

Check the existence of the file

S.Stat()は、Check the existence of the fileでもイディオムとしてよく使われます(この方法でしか存在チェックができないわけではありません)。

info, err := S.Stat(ファイルパス)if err == S.ErrNotExist {    // ファイルが存在しない} else if err != nil {    // それ以外のエラー} else {    // 正常ケース}

存在チェックそのもののシステムコールは提供されていません。 PythonのS.PATH.exists() も内部で同じシステムコールを呼ぶS.stat() を使っていますし、C言語でもstat() や、access() を代わりに使います。access() は現在のプロセスの権限でアクセスできるかどうかを診断するシステムコールで、Go言語はsyscall.Access() としてPOSIX系OSで使えます。

However, it is currently mainstream that the existing check itself is unnecessary.Detailed explanation is node.It is written in the JS documentation.

Even if you check the existence and confirm that there is a file, it may be possible that other processes and threads will erase files before subsequent files.It is recommended to use the file operation function directly and write a code that handles errors correctly.

Get OS -specific file attributes

ファイル属性にはOS固有のものもあります。それらを取得するにはS.Fileinfo.Sys()を使います。S.Fileinfo.Sys()は、ドキュメントにも「interface{}を返す」としか書かれておらず、使い方に関する情報がいっさいない機能です。 基本的には下記のようにOS固有の構造体にキャストして利用します。

// WindowsinternalStat := info.Sys().(syscall.Win32FileAttributeData) // Windows以外internalStat := info.Sys().(*syscall.Stat_t)

Go言語のランタイムの挙動としては、まずこのOS固有の構造体を取得し、共通情報をS.Fileinfoに転記する処理になっています。S.Fileinfoに転記されないOS固有のデータとしては下記のようなものがあります。

  
意味WindowsLinuxmacOS
デバイス番号DevDev
inode番号InoIno
ブロックサイズBlksizeBlksize
ブロック数BlocksBlocks
Linkされている数NLinkNLink
ファイル作成日時CreatinTimeBirthtimespec
最終アクセス日時LastAccessTimeAtimAtimespec
属性変更日時CtimCtimespec

この表はWindows(64ビット)、Linux(64ビット)、macOS(64ビット)の情報から作りました。 その他のOSやプロセッサの実装の詳細はGo言語のソースに含まれるsyscall/ztypes_(OS)_(プロセッサ).goというファイルを参照してください。

この表を見ると、Windowsで取得できる属性が極端に少ないのですが、これは内部ロジックの違いでOSから渡ってくる情報があまりSys()に残されていないためです。 Windows用の\ S.Stat()実装内部で使っているsyscall.GetFileinformationByHandle()を使うと、デバイス番号にあたるVolumeSerialNumberInoにあたるFileIndexHighFileIndexLow、Link数を表すNumberOfLinksが得られます。

The reason why the file creation date and time cannot be obtained with Linux is that, from the comments of Mr. Ozaki of Ruby's Issue, it seems that it cannot be used from the application because it has information in the kernel, but there is no mouth to take it out.。

File attribute settings

属性の取得と比べると、設定に関する機能はシンプルです。 モード変更とオーナー変更はS.Fileの同名のメソッドでも行えます。

// ファイルのモードを変更S.Chmod("setting.txt", 0644) // ファイルのオーナーを変更S.Chown("setting.txt", S.Getuid(), S.Getgid()) // ファイルの最終アクセス日時と変更日時を変更S.Chtimes("setting.txt", time.Now(), time.Now())

Link

Hard links and symbolic links can also be created from GO language.

// ハードLinkS.Link("oldfile.txt", "newfile.txt") // シンボリックLinkS.Symlink("oldfile.txt", "newfile-symlink.txt") // シンボリックLinkのLink先を取得link, err := S.ReadLink("newfile-sysmlink.txt")

WindowsではPOSIXのように気軽にLinkを使えないイメージがありますが、ハードLinkもシンボリックLinkもVista以降では問題なく使用できます (内部ではCreateHardLinkWやCreateSymbolicLinkWを呼び出して作成します)。 なお、WindowsでシンボリックLinkを作成するにはSeCreateSymbolicLinkPrivilege権限が必要です。

Acquisition of directory information

ディレクトリ一覧の取得はSパッケージ直下の関数としては提供されていません。 ディレクトリをS.Open()で開き、S.Fileのメソッドを使って、ディレクトリ内のファイル一覧を取得します。

package main import (    "fmt"    "S") func main() {    dir, err := S.Open("/")    if err != nil {        panic(err)    }    fileInfS, err := dir.Readdir(-1)    if err != nil {        panic(err)    }    for _, fileInfo := range fileInfS {        if fileInfo.IsDir() {            fmt.Printf("[Dir]%s\n", fileInfo.Name())        } else {            fmt.Printf("[File] %s\n", fileInfo.Name())        }    }}

Readdir()メソッドはS.Fileinfoの配列を返します。 ファイル名しか必要がないときはReaddirnames()メソッドを使えます。 このメソッドは文字列の配列を返します。

Readdir()Readdirnames()は数値を引数に取ります。正の整数を与えると、最大でその個数の要素だけを返します。 0以下の数値を渡すと、ディレクトリ内の全要素を返します。

Speed up file operation inside the OS

Finally, I will also mention the faster file operation performed inside the OS.You don't need to be aware of a normal application, but you may need to worry about implementing a database management system.

For the CPU, the reading and writing of the disk are very slow processing, and it is a task that you want to do as much as possible to the end.Therefore, Linux uses the buffer provided inside VFS to avoid operations on disks as much as possible.

If you read and write a file with Linux, the data is first stored in the buffer.Therefore, if you write the data to the file, the application will return to the application when it is stored in the buffer.When you read the data from the file, you can be stored in the buffer once, and if you are already on the buffer and have not written on the file (the buffer is fresh), you only access the buffer.Therefore, the file input / output by the application is actually an input / output with the buffer prepared by Linux.Synchronization between the buffer and the actual storage is performed asynchronously without the application.

Go言語で、ストレージへの書き込みを確実に保証したい場合は、S.FileSync()メソッドを呼びます。

file.Sync()

参考までに、最近のコンピュータで利用されているさまざまなキャッシュとそのレイテンシの関係を紹介します。1 この表からわかるように、すでにメインメモリのバッファに載っているデータなら200サイクルで取得できます。 しかし、実際のストレージからウェブブラウザのキャッシュを取得してくるとしたら、その5万倍も遅い数値になってしまいます。

種類何をキャッシュするかどこにキャッシュするかレイテンシ(サイクル数)誰が制御するか
CPUレジスタ4バイト/8バイトのデータCPU内蔵のレジスタ0コンパイラ
L1キャッシュ64バイトブロックCPU内蔵のL1キャッシュ4CPU
L2キャッシュ64バイトブロックCPU内蔵のL2キャッシュ10CPU
L3キャッシュ64バイトブロックCPU内蔵のL3キャッシュ50CPU
仮想メモリ4KBのページ(と呼ばれるメモリブロック)メインメモリ200CPU+OS
バッファキャッシュファイルの一部メインメモリ200OS
ディスクキャッシュディスクのセクターディスクコントローラ100,000コントローラファームウェア
ブラウザキャッシュウェブページローカルディスク10,000,000ウェブブラウザ

By the way, there are other files in Linux's file access speeding mechanism besides cache.For example, if the storage is a hard disk, you can save the time to move the head, so that the operation is organized in a file with a file close to the center, and the processing of the disk input / output efficiency (elevator processing) is performed.However, the elevator processing takes time to organize the operation.If the storage is SSD, such a waiting process will be overhead, so settings that do not use elevator processing will improve the throughput.

Summary and next notice

The management of the file system that the OS does inside is very advanced.There are many books related to operating systems and Linux kernels, so if you want to know more, you should refer to that.In addition, the second special feature of the Software Design magazine at the Technical Review Company, which is currently on sale, is an introduction of a Linux file system by Naohiro Aota.Here, new file systems and journaling mechanisms that are not booked are explained in detail.

The application can operate files with extremely simple functions, thanks to the OS working under the water.If you try to do a little detailed operation, there are some situations where you have to worry about the difference between the OS, but it is not difficult, even though it is a little troublesome.If you look for GitHub etc., you may find a code that absorbs the difference between the OS.

Next time, I will introduce a slightly advanced file operation that cannot be realized with the OS package alone.

footnote

  1. Computer Systems: A programmer's perspective 3rd edition (Pearson Education Limited) ISBN 978-1-292-10176-7↩