kakts-log

programming について調べたことを整理していきます

unixファイルシステムについて (詳解linuxカーネル第1章5節)

積読していた詳解linuxカーネルをまた読み進め始めました。 プロセススケジューラやらファイルシステムなどについて非常に詳しく書かれていて面白いです。
今回は第1章5節 ファイルについての項目を読んで整理してみました。

詳解 Linuxカーネル 第3版

詳解 Linuxカーネル 第3版

ファイルとは (1.5.1)

ファイルとは: バイト列として構造化された情報の入れ物のことです。unix|linuxカーネルはファイルの中身には関知しません。
ユーザの側からみたとき、ファイルはツリー構造の名前空間に系統立てられ、末端以外のツリーノードはすべてディレクトリを表します。

ハードリンクとソフトリンク (1.5.2)

unix環境のファイルシステム上でディレクトリとファイル名を結びつける手法としては、一般的にハードリンクとソフトリンクの2つがある。

ハードリンクについて

ディレクトリ中のファイル名は、ファイルハードリンク(file hard link)もしくはリンク(link)と呼ばれる。
1ファイルの実体に対し、同一ディレクトリ内もしくは異なるディレクトリ内に複数のリンクを持つことができます。
つまり、1ファイルの実体が複数のファイル名を持つことができます。

一般的に パス名p1によって識別されるファイルに対して新たにパス名p2の新しいファイルハードリンクを作成するときは、以下のコマンドで行うことができる。

$ ln p1 p2

ただし、ハードリンクの制約としては主に2つあり、以下の通りとなっている。
ディレクトリのハードリンクを作成することはできない。
・ハードリンクは同一ファイルシステム内に含まれるファイルに対してのみ生成できる。

この2つの制約はシステムの運用上非常にやっかいなものとなり、ソフトリンクは、この問題を解決するために導入される

ソフトリンクについて

ソフトリンク(soft link)は、一般的にシンボリックリンク(symbolic link)と呼ばれ、上述したハードリンクによる問題を克服するために導入されました。
ソフトリンクとは、別ファイルの任意のパス名を含むファイルで、パス名はどのファイルシステムのものを参照しても良い。
さらには、存在しないファイルを指定することも可能です。

パス名p1によって識別されるファイルに対して新たにパス名p2の新しいソフトリンクを作成するときは、以下のコマンドで行うことができる。

ln -s p1 p2

-s オプションを付けるだけでソフトリンクを作成することができます。
このコマンドを実行することにより、p2のディレクトリ部分を抽出した上で、そのディレクトリ以下にp2で指定したファイル名でシンボリックリンク型のエントリを生成する。
これにより、p2への参照は自動的にp1への参照へと変換される。

ファイルの種類について (1.5.3)

unix環境において ファイルには以下の種類があります。
- 通常ファイル
- ディレクトリ - シンボリックリンク - ブロック型デバイスファイル - キャラクタ型デバイスファイル - パイプ、名前付きパイプ(named pipe, FIFO) - ソケット

あるプログラムがデバイスファイルにアクセスするとき、そのデバイスファイルに対応するI/Oデバイスに直接アクセスする(詳解linuxカーネル第13章 参照)

パイプとソケットは、プロセス間通信に利用させる特殊ファイルとなる。

ファイルディスクリプタとiノードについて (1.5.4)

ファイルシステムがファイルを操作する際に必要な情報はiノード(inode)と呼ばれるデータ構造が持っている。
各ファイルはiノードを持っていて、ファイルシステムはiノードを利用してファイル操作を行う。
ファイルシステムで使われている関数はシステムによって異なるが、POSIX標準で定められている下記のものは必ず提供されているものとなります。

  • ファイルの種類
  • ファイルに関連付けられるハードリンク数
  • ファイルのバイト長
  • バイス番号
  • inode番号
  • 所有者のユーザID
  • ユーザグループID
  • 各種タイムスタンプ(inode変更時刻 最終アクセス時刻など)
  • アクセス権とファイルのモード

詳解システムパフォーマンス第6章 CPUアーキテクチャのまとめ

詳解 システム・パフォーマンス

詳解 システム・パフォーマンス

最近発売された「詳解システムパフォーマンス」を買って読み進めています 第6章のCPUアーキテクチャの項目が勉強になったので、簡単にまとめます。

CPUの基礎用語

  • プロセッサ
    プロセッサボードのソケットなどに装着される物理チップ。 一つ以上のCPUをもつ
  • コア マルチコアプロセッサに含まれる、独立したCPUインスタンス CMP(chip level multi processing)と呼ばれるprocessorのスケーリング方法として使える
  • ハードウェアスレッド 1コアの上で複数スレッドの並列実行をサポートするCPUアーキテクチャのこと。各スレッドは独立したCPUインスタンスとして扱われる。 マルチスレッディングはこのスケーリングアプローチの内の一つ
  • CPU命令
    CPU命令セットに含まれる1個のCPUオペレーション
  • 論理CPU
    仮想プロセッサとも呼ばれる。 OSのCPUインスタンスであり、ハードウェアスレッド、コア、シングルコアプロセッサのどれかで実装される

CPU architecture

4コア、8ハードウェアスレッドをもつシングルプロセッサを考えた場合、以下のように図で表せる。
f:id:kakts:20170309235424j:plain

ハードウェアスレッドは、それぞれ1個の論理CPUとして扱うことができるため、OSからこのシングルプロセッサを見た時、8個のCPUを持っているように見える。 OSはさらにスケジューリングの最適化を行うために、どのCPUがどのコアに乗っているかといった情報を持っている場合がある。

CPU memory cache

プロセッサには様々なハードウェアキャッシュを持っており、メモリI/Oのパフォーマンス向上のために有用である。
キャッシュのサイズとI/Oスピードはトレードオフの関係にあり、高速になるほどCPUの近くに位置する。 キャッシュには、大きく分けて2種類あり、プロセッサの中、外にあるかで分けられる。
プロセッサ内にあるものは統合キャッシュと呼ばれることがあり、現在では主流となっている。

CPU run queue

スレッドには2つの実行状態があり、CPU上で実行中の場合と、CPUでの処理を待っている実行可能状態がある。
処理待ちのスレッドは、カーネルスケジューラにより、CPUのランキューにキューイングされ、順番にCPUの処理を待つ。
ランキューにキューイングされたスレッドの数は、CPUの飽和状態を示す重要なパフォーマンス指標となる。

マルチプロセッサシステムにおいて、カーネルはCPUそれぞれにランキューをもち、スレッドを同じランキューにキューイングし続けようとする。
なぜ同じランキューを選ぶかというと、CPUはスレッドのデータをキャッシュするため、パフォーマンスが良くなるからである。
この、スレッドに対して特定のランキューを選ぶアプローチをCPUアフィニティ と呼ぶ。

Goのスライスについて

Goのスライスについて

Go Slices: usage and internals - The Go Blog
Go言語において固定長のサイズの配列とは別に、要素の追加に応じて自由にサイズを拡張できるスライスという型があります。

スライスのデータ構造

スライスのデータ構造としては、ソースコードを読んでみるのが早くて、以下のurlにまとまっています。 https://github.com/golang/go/blob/master/src/runtime/slice.go#L11-L15

type slice struct {
    array unsafe.Pointer // 配列のポインタ
    len   int   // スライスが現在もっている要素数
    cap int    // スライスのサイズ
}

スライスの内部的には、配列のポインタを持っているため、従来の配列と同様の操作が行えます。

さらには、現在スライスが現在持っている要素数lenをもっていて、要素を追加するたびにこの値が増加していきます。 lenとは別に、スライスの宣言時に初期化した要素に応じてcapも決められます。 それと同時に、lenとcapをチェックし、capの値を超えたら、古いcapサイズの2倍のサイズのメモリを確保(malloc)し、同じ要素でサイズが2倍のスライスを新たに作ります。

このあたりの処理はsliceの内部実装で使われているgrowsliceという関数で実現されています。 go/slice.go at master · golang/go · GitHub
読んでみると、スライス拡張の際に新たにメモリサイズを計算し、mallocして新たにメモリが割り当てられていることが理解できると思います。

スライスの宣言

スライスを使う際に、宣言する方法は配列のものと非常に似ていて、下記のように書くことができます。 makeを使ってcapとlenを指定した上で宣言する事もできます。

// 配列の宣言
// 宣言時に[]にサイズを指定する。固定長のため、サイズが増えることはない
arr := [4]int{1, 2, 3, 4}


// スライスの宣言
// サイズは指定せず、4つの要素を指定して初期化している。
sl := []int{1, 2, 3, 4}

// makeを使って、サイズを決めた上で宣言も行える
sl := make([]int, 4, 8)

スライスの要素の操作

スライスは宣言時に自由に要素を指定することができ、指定した要素に応じて長さが決まることを説明しました。

このスライスに対して、様々な操作を行うことができます。

sl := []int{1, 2, 3, 4}
// 元のスライスの1番目から(4-1)番目の要素をもつスライスを取得
fmt.Println(sl[1:4]) //  [2 3 4]と表示
// 元のスライスの0番目から(2-1)番目の要素をもつスライスを取得
fmt.Println(sl[:2]) // [1 2]と表示

そして、もちろんスライスの特定の要素に対して値を代入することもできます。

sl := []int{1, 2, 3, 4}
sl[2] = 100
fmt.Println(sl) // [1 2 100 4]と表示

また、別の変数に対して、既に宣言したスライスの参照を渡すことができます。

sl := []int{1, 2, 3, 4}

// sl2にslの参照を渡す
sl2 := sl

fmt.Println(sl) // [1 2 3 4]と表示
fmt.Println(sl2) // [1 2 3 4]と表示

// slの特定の要素を変更すると、同じ参照を持つsl2の方でも値が変わっているのを確認できる。
sl[2] = 100
fmt.Println(sl) // [1 2 100 4]と表示
fmt.Println(sl2) // [1 2 100 4]と表示

上記のように、別の変数に参照を渡すことで、同じ実体を複数の変数から見ることができます。

appendを用いたスライスの要素の追加

既に宣言済みのスライスに対して、要素を追加する際にはappend()関数を使って行います。 ここでは、len, capが4のスライスに対して、appendを行ってみます。

sl := []int{1, 2, 3, 4}

// sl: len 4  cap 4   slは [1 2 3 4]
fmt.Println("sl: len, %d cap %d", len(sl), cap(sl), sl)

// len,capが4で要素が満杯の状態でappendを行う
sl3 = append(sl, 5)
// sl: len 5  cap 8       sl は [1 2 3 4 5]
fmt.Printf("sl: len, %d cap %d", len(sl), cap(sl))

既に要素が満杯のスライスに対して appendを行うので、前述したとおり、新たにcapサイズを2倍の8にした上で、5を追加したスライスを返しています。
内部的にはmallocをしているので、append前と後では、変数slが指すポインタアドレスは別のものになっています。
スライスを使って実装をする上で、スライスのデータ構造、内部実装を理解していないと思わぬバグになりそうなので、ここは非常に注意が必要です。

Go deferについて

Goには、deferステートメントというものがあり、deferへ渡した関数実行を、その呼び出し元の関数の終了時(return)まで遅延させることができます。

package main

import "fmt"

func main() {
  // main関数の最後に実行される
  defer fmt.Println("world.")

  fmt.Println("hello.")
}

出力
hello. world

deferステートメントの特徴として、deferに関数を渡す際、引数の値は、deferに渡すタイミングで評価されます。
以下のようなコードを書いて実行してみると、その挙動がわかって面白いです

package main

import "fmt"

func main() {
  var i = 1

  // i = 1が fmt.Printlnに渡され、最後に実行される
  defer fmt.Println("world. %d", i)
  
  // i = 2
  i++
  //  i = 2が fmt.Printlnに渡され、最後に実行される
  defer fmt.Println("world2. %d", i)

  // i = 2が渡され、その場で実行される
  fmt.Println("hello %d", i)
}

出力
hello %d 2
world2. %d 2
world. %d 1

まとめると、deferに渡した関数は、引数はその場で評価され、実行は呼び出し元の関数の終了時に行われる。
ここの挙動について簡単ですが、しっかり理解していないと複雑なコードになったときにバグの元になるため注意が必要です。