CUBE SUGAR CONTAINER

技術系のこと書きます。

nvidia-smi(1) で GPU にパワーリミットを設定して消費電力や発熱を減らす

自宅にあるオンプレマシンでグラフィックカードを GPGPU の用途に使用していると、消費電力や発熱は切実な問題になりうる。 特に昨今は電気代の値上がりも著しいし、発熱は製品寿命の短縮や夏だと室温の上昇につながる。 そこで、今回は Linux の環境で nvidia-smi(1) を使って NVIDIA の GPU にパワーリミットを設定することで消費電力や発熱の低減を目指してみる。

使った環境は次のとおり。 Ubuntu 20.04 LTS のマシンに、Docker と nvidia-container-toolkit がインストールしてある。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.4 LTS
Release:    20.04
Codename:   focal
$ uname -srm
Linux 5.13.0-51-generic x86_64
$ docker version
Client: Docker Engine - Community
 Version:           20.10.17
 API version:       1.41
 Go version:        go1.17.11
 Git commit:        100c701
 Built:             Mon Jun  6 23:02:57 2022
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.17
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.17.11
  Git commit:       a89b842
  Built:            Mon Jun  6 23:01:03 2022
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.6.6
  GitCommit:        10c12954828e7c7c9b6e0ea9b0c02b01407d3ae1
 runc:
  Version:          1.1.2
  GitCommit:        v1.1.2-0-ga916309
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
$ nvidia-smi
Fri Jun 24 18:59:39 2022       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 515.48.07    Driver Version: 515.48.07    CUDA Version: 11.7     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  NVIDIA GeForce ...  On   | 00000000:01:00.0 Off |                  N/A |
|  0%   40C    P8     7W / 170W |     14MiB / 12288MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|    0   N/A  N/A      1087      G   /usr/lib/xorg/Xorg                  9MiB |
|    0   N/A  N/A      1232      G   /usr/bin/gnome-shell                3MiB |
+-----------------------------------------------------------------------------+

もくじ

事前準備

ベンチマーク用に PyTorch を使いたいので、あらかじめ公式の Docker イメージをプルしておく。 そして、コンテナから GPU が見えることを確認する。

$ docker pull pytorch/pytorch
$ docker run --gpus all --rm -it pytorch/pytorch nvidia-smi
Fri Jun 24 19:03:09 2022       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 515.48.07    Driver Version: 515.48.07    CUDA Version: 11.7     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  NVIDIA GeForce ...  On   | 00000000:01:00.0 Off |                  N/A |
|  0%   39C    P8     8W / 170W |     14MiB / 12288MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
+-----------------------------------------------------------------------------+

GPU で使用できるパワーリミットを確認する

まずは使っている GPU に設定できる最低・最大のパワーリミットを確認する。 今回使った製品であれば、最低が 100W で最大が 170W だと分かる。 デフォルトではパワーリミットの値は最大に設定されているはず。

$ nvidia-smi -q -d POWER

==============NVSMI LOG==============

Timestamp                                 : Fri Jun 24 19:05:09 2022
Driver Version                            : 515.48.07
CUDA Version                              : 11.7

Attached GPUs                             : 1
GPU 00000000:01:00.0
    Power Readings
        Power Management                  : Supported
        Power Draw                        : 7.75 W
        Power Limit                       : 170.00 W
        Default Power Limit               : 170.00 W
        Enforced Power Limit              : 170.00 W
        Min Power Limit                   : 100.00 W
        Max Power Limit                   : 170.00 W
    Power Samples
        Duration                          : 105.50 sec
        Number of Samples                 : 119
        Max                               : 8.42 W
        Min                               : 7.46 W
        Avg                               : 7.79 W

GPU にパワーリミットを設定する

GPU にパワーリミットを設定するには nvidia-smi(1) の -pl オプションを使う。 このオプションに、先ほど得られた設定できるパワーリミットの範囲でワット数を指定すれば良い。

たとえば、今回の環境における下限の 100W に設定するには次のようにする。

$ sudo nvidia-smi -pl 100
Power limit for GPU 00000000:01:00.0 was set to 100.00 W from 170.00 W.
All done.

もう一度 nvidia-smi(1) をオプションなしで実行してみると、ワット数の表示が 100W になっていることが確認できる。

$ nvidia-smi
Fri Jun 24 19:06:28 2022       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 515.48.07    Driver Version: 515.48.07    CUDA Version: 11.7     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  NVIDIA GeForce ...  On   | 00000000:01:00.0 Off |                  N/A |
|  0%   39C    P8     9W / 100W |     14MiB / 12288MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|    0   N/A  N/A      1087      G   /usr/lib/xorg/Xorg                  9MiB |
|    0   N/A  N/A      1232      G   /usr/bin/gnome-shell                3MiB |
+-----------------------------------------------------------------------------+

パワーリミットを設定してベンチマークを測ってみる

さて、パワーリミットを設定すれば、各瞬間の電力 (W) が減るのは分かる。 一方で、時間を通じた総和として消費電力量 (Wh) が減るのかは分からない。 そこで、パワーリミットを最小と最大に設定した状態でのパフォーマンスを調べる。

PyTorch のコンテナを起動する。

$ docker run --gpus all --rm -it pytorch/pytorch

動かすベンチマークについては、下記の PyTorch の公式チュートリアルを参照にした。

pytorch.org

下記のサンプルコードでは、内積を計算するシンプルなコードを使ってベンチマークする。

$ cat << 'EOF' > benchmark.py
import torch
import torch.utils.benchmark as benchmark


def batched_dot_bmm(a, b):
    '''Computes batched dot by reducing to bmm'''
    a = a.reshape(-1, 1, a.shape[-1])
    b = b.reshape(-1, b.shape[-1], 1)
    return torch.bmm(a, b).flatten(-3)


# Input for benchmarking
x = torch.randn(100000, 10000, device='cuda')


t = benchmark.Timer(
    stmt='batched_dot_bmm(x, x)',
    setup='from __main__ import batched_dot_bmm',
    globals={'x': x},
)

print(t.timeit(1000))
EOF

まずはパワーリミットを下限の 100W に設定した状態で実行する。 同時に、nvidia-smi(1) を実行して、消費電力が上限の 100W に張り付くことを確認しておこう。

# python benchmark.py 
<torch.utils.benchmark.utils.common.Measurement object at 0x7fd2ba7fa460>
batched_dot_bmm(x, x)
setup: from __main__ import batched_dot_bmm
  16.23 ms
  1 measurement, 1000 runs , 1 thread

上記から、1 回の演算に平均で 16.23ms かかることがわかった。

続いてはパワーリミットを上限の 170W に戻す。

$ sudo nvidia-smi -pl 170
Power limit for GPU 00000000:01:00.0 was set to 170.00 W from 100.00 W.
All done.

そして、プログラムをもう一度実行する。 今度も、nvidia-smi(1) を実行して、消費電力が上限の 170W に張り付くことを確認しておこう。

# python benchmark.py 
<torch.utils.benchmark.utils.common.Measurement object at 0x7ff3be37b460>
batched_dot_bmm(x, x)
setup: from __main__ import batched_dot_bmm
  13.76 ms
  1 measurement, 1000 runs , 1 thread

上記から、今度は 1 回の演算に平均で 13.76ms かかることがわかった。

演算にかかる電力は 170W と 100W なので約 40 % 低下している。 そして、演算にかかる時間は 13.76ms と 16.23ms なので約 18 % 増加した。 ここから、トータルの電力量は 100W において 0.6 * 1.18 = 0.708 になる。 あくまで今回の条件下ではという但し書きはつくものの、計算に使用する電力量は約 71% まで減らせたようだ。

一般に半導体のワットパフォーマンスはリニアな関係ではなく、入力する電力が大きくなるほどパフォーマンス向上の効率が悪くなると言われる。 その点からも、今回の結果には納得がいく。 また、電力量が減れば発熱も小さくなるため、暖房器具としての性能も低下するし製品寿命の延長が望める可能性もある。

まとめ

今回は nvidia-smi(1) を使って NVIDIA の GPU にパワーリミットを設定して消費電力と発熱の低減を試みた。 オンプレマシンの消費電力や発熱に悩んでいる場合には、パフォーマンスとのトレードオフはあるものの、一考の余地はあるかもしれない。

いじょう。

qrencode と viu を使ってターミナルで QR コードを作って表示する

情報共有などのために、ささっと QR コードを作って読み込ませたいときがある。 そんなときは qrencode と viu を使うとターミナル上で完結して楽そうだ。

使った環境は次のとおり。

$ sw_vers   
ProductName:    macOS
ProductVersion: 12.4
BuildVersion:   21F79
$ qrencode -V                         
qrencode version 4.1.1
Copyright (C) 2006-2017 Kentaro Fukuchi
$ viu -V
viu 1.4.0

もくじ

下準備

まずは qrencode と viu をインストールしておく。

$ brew install qrencode viu 

ターミナルで QR コードを作る

qrencode を使うと、次のように引数で指定した文字列を埋め込んだ QR コードが作れる。 -o オプションで保存先を指定する。

$ qrencode -o greet.png "Hello, World"

できあがる QR コードはこんな感じ。

ターミナルで QR コードを表示する

ターミナルで QR コードを作ったなら、表示までターミナルで完結したい気持ちもある。 そんなときは viu で開くと良い。

$ viu greet.png

こんな感じで表示できる。

viu を使って QR コードの画像を開く

目視ではかなりざらついてるように見えるけど、デバイスに読み込ませてみるとちゃんと読める。 QR コードの堅牢性おそるべし。 もし読めないときは viu コマンドの -w オプションとかを使って大き目に表示させると良い。

ワンライナーで QR コードを作って表示する

ちなみに qrencode も viu も標準入出力を読み書きできる。 そのため、両者を次のように組み合わせることも可能だ。

$ qrencode -o - "Hello, World" | viu -

これで、ファイルを介すことなく QR コードをターミナル上に表示できる。

めでたしめでたし。

子育てにかかる費用を公的データで調べる

一般的に、子どもを一人育てるのには 2,000 万円かかるとか言われている。 しかし、何も子どもが生まれた瞬間に 2,000 万円が必要になるわけではない。 もちろん、2,000 万円を均等に割った額が毎年かかるわけでもない。 そもそも、こういった数字はあくまで代表的な家庭における目安に過ぎないはず。 たとえば、子どもがどういった就学をするのかだったり、世帯収入にも依存していると考えられる。 そこで、今回は子育てにかかる費用に対する自分の解像度を上げるために、公的な統計資料を当たってみることにした。

もくじ

インターネットによる子育て費用に関する調査

まず、第1子が誕生してから中学校を卒業するまでの費用に関しては、平成21年度に内閣府が実施した調査が参考になる。 この資料は教育費に限らず、子育てにかかる全般的な費用について扱っている。

www8.cao.go.jp

上記資料の「第3章 調査結果」に掲載されているグラフを以下に引用する。 このグラフからは、それぞれの就学区分ごとに第1子の子育てをするのに年間でいくらかかっているか分かる。

出典: 平成21年度インターネットによる子育て費用に関する調査 全体版 第3章 調査結果 42 ページ 図表 1-1.第1子一人当たりの年間子育て費用額(対象者全体平均)【第1子の就学区分別】

未就園児は 84 万円、保育園・幼稚園児は 121 万円、小学生は 115 万円、中学生は 155 万円が平均で必要となるようだ。 なお、この資料では就学先が私立か国公立かを区別していない。 つまり、調査対象とした全体の平均である点に留意が必要となる。

また、上記のグラフが掲載されている次のページには、各年齢ごとに分解したグラフが掲載されている。 グラフからは、年齢が上がる毎に学校外教育費 (塾や習い事) がじわじわと増加していく様子や、学校教育費が中学校から一気に増加する様子が伺える。

世帯年収ごとの子育て費用額の比較も興味深い。 以下は「保育所・幼稚園児」について、世帯年収ごとの年間子育て費用額を比較したグラフになっている。 このグラフからは、子どもが小さいうちは世帯年収ごとの子育て費用額の差は「子どものための預貯金・保険」や「レジャー・旅行費」に大きく出ることが伺える。

出典: 平成21年度インターネットによる子育て費用に関する調査 全体版 【参考資料】 37ページ 図表 4-3.世帯年収別にみた「保育所・幼稚園児」第1子一人当たりの年間子育て費用額(対象者全体平均)

一方で、以下は「中学生」について、世帯年収ごとの年間子育て費用額を比較したグラフになっている。 こちらのグラフでは、特に高い世帯年収において、子育て費用額の差が「学校教育費」や「学校外活動費」に大きく出ている。 つまり、子どもの年齢によって資金の余裕が振り分けられる先が少し変わるようだ。 これらも、自分たちの世帯年収に合わせて考える上で参考になるだろう。

出典: 平成21年度インターネットによる子育て費用に関する調査 全体版 【参考資料】 38ページ 図表 4-5.世帯年収別にみた「中学生」第1子一人当たりの年間子育て費用額(対象者全体平均)

ちなみに、「【参考資料】」に掲載されているグラフには子育てにかかる費用の構造が図示されており、こちらも面白い。

出典: 平成21年度インターネットによる子育て費用に関する調査 全体版 【参考資料】110 ページ掲載図表

子供の学習費調査

高校までの学習費に焦点を絞った資料としては、文部科学省が平成30年度に実施した調査が参考になる。 この資料では幼稚園から高校までの学習費を、国公立と私立で区別して調査している。

www.mext.go.jp

上記資料の「調査の結果 > 結果の概要 > 平成30年度 > 2.調査結果の概要」に掲載されているグラフを以下に引用する。

出典: 平成30年度 子供の学習費調査 2.調査結果の概要 図1−2 学校種別にみた学習費総額

上記から、国公立と私立で学習費に大きな差が生じることが確認できる。 私立中学の場合は公立の約 3 倍、私立高校でも公立の約 2 倍かかる。 場合によっては私立中学に進学を希望することもあるだろうし、私立高校であればより現実的な可能性として捉えておく必要がありそうだ。

教育費に関する調査結果

先ほどの文科省の調査は高校までが範囲だった。 高専・専修学校や大学など、高校以降の教育費については日本政策金融公庫の調査が詳しい。 この調査では「教育費負担の実態調査結果」を毎年公表している。

www.jfc.go.jp

上記で令和 3 年度の調査結果として掲載されているグラフを以下に引用する。 このグラフには、進学先別の年間の在学費用が載っている。

令和3年度 教育費負担の実態調査結果 6ページ 図−3 在学先別にみた1年間の在学費用(子供1人当たりの費用)

上記から、高専・専修学校や大学などへ進学すると、最低でも 100 万円程度は年間でかかることがわかる。 なお、上記は大学の費用を国公立と私立で区別していない。

大学に関して、国公立と私立の文理で区別したグラフが以下になる。

出典: 令和3年度 教育費負担の実態調査結果 6ページ 図−4 国公立・私立別にみた大学の在学費用(子供1人当たりの費用)

大学の中でも、国公立と私立の文理で費用の差が激しい。 国公立は平均で 103 万円だが、私立の文系は 152 万円、理系では 183 万円となっている。 なお、最近だと理系は大学院への進学が一般的になってきている点も心に留めておく必要がありそうだ。

また、上記の資料には自宅外通学に関する資料も載っている。 私自身は関東圏に居住していることもあってさほど可能性は高くないものの、それ以外の場合には参考になりそうだ。 とはいえ、関東圏に住んでいる場合であっても、海外へ留学する可能性は考慮する必要があるのだろうか・・・?

国民生活白書

最初に登場した「インターネットによる子育て費用に関する調査」は、子育てにかかる費用を広範に扱った調査だったが、対象は第1子に限定されていた。 では、第2子以降にかかる費用はどうなるのだろうか。 この点に関しては、内閣府が平成17年度に実施した国民生活白書の調査が参考になりそうだった。

warp.da.ndl.go.jp

平成17年版「子育て世代の意識と生活」の「第3章 子育てにかかる費用と時間 > 二人目・三人目の子どもにかける費用は逓減」に記述がある。

warp.da.ndl.go.jp

ここまで、一人の子どもを育てるための費用を見てきたが、更に子どもを育てた場合に子育て費用はどれくらい増加するのだろうか。一人の場合と同様に、「基本的経費」、「教育費」、「住宅関係費」に分けて、子どもを二人持つ世帯の「子どもを育てる費用」から子どもを一人持つ世帯の「子どもを育てる費用」を差し引いて、22年間分を足したものを「二人目の子ども9を育てる」費用として推計した。ここでいう「子どもを育てる費用」は、前項と同じく、付注3−1−1に掲げた費目における子どもを育てるための追加的な費用である。 その結果、一人の子どもを育てる費用の1,302万円に対して、二人目の子どもを育てる費用10は1,052万円と、20%程度節約されていることが分かった(第3−1−14図)。内訳ごとに節約の程度を見ると、二人目の基本的経費は一人目の80.0%、教育費は83.6%、住宅関係費は63.0%となっている。同様に三人目の子どもにかかる費用を推計すると、22年間で769万円となり、二人目と比べて更に27%程度節約されており、子どもの増加にともない、子どもにかかる費用は逓減していくことがうかがわれる。

上記より、第2子の子育てにかかる費用は第1子の約 8 割まで減少 (節約) し、第3子に至っては第1子の約 6 割まで減少することがわかる。 もし、すでに第1子がいる場合には、その実績を元に第2子以降でどれくらいかかりそうかを計算する上で目安にできそうだ。 ただし、この結果は単純に「減っている」のではなく、諸々の事情から「減らしている」ことも考慮する必要があるだろう。

いじょう。

まとめ

子育てにかかる費用が分からなかったので、公的データを調べてわかったことについて自分用にまとめた。

NVMe ストレージのデータを nvme-cli(1) で完全に消去する

ストレージ機器を破棄または譲渡するときには、漏えいを防ぐためにあらかじめデータを消去しておく必要がある。 このとき、データの消去は後から読み取りが難しいように実施しなければいけない。 後から読み取りが難しい形でデータを消去することは Secure Erase と呼んだりするようだ。 今回は NVMe ストレージを Secure Erase する方法について扱う。

まず、最近のマザーボードの UEFI には、ストレージのデータを Secure Erase するためのツールが付属していることがある。 もし、ツールが付属している場合には、それを利用するのが手っ取り早い。 では、付属していない場合にどうするか、というのが今回の話になる。 Unix 系の OS で NVMe ストレージを使っている場合には、nvme-cli(1) を使うのが良さそうだ。

今回使った環境は次のとおり。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.4 LTS
Release:    20.04
Codename:   focal
$ uname -srm
Linux 5.13.0-44-generic x86_64
$ nvme version
nvme version 1.9

下準備

Ubuntu の場合には apt を使ってインストールできる。

$ sudo apt-get -y install nvme-cli

データを消去する

まず、nvme list コマンドで消去したいデバイスを確認する。

$ sudo nvme list
Node             SN                   Model                                    Namespace Usage                      Format           FW Rev  
---------------- -------------------- ---------------------------------------- --------- -------------------------- ---------------- --------
/dev/nvme0n1     XXXXXXXXXXXX         XXXXXXXXXXXXX                            1           1.00  TB /   1.00  TB    512   B +  0 B   XXXXXXX 

あとは、nvme format コマンドに確認したデバイスを指定して実行するだけ。 このとき、オプションとして -s 2 を指定するのが望ましい。

$ nvme format /dev/nvme0n1 -s 2

先ほどの形式だとデバイスを名前空間ブロックデバイス (/dev/nvmeXnY みたいな形式) として指定していた。 それ以外にもキャラクタデバイス (/device/nvmeX みたいな形式) も受け付けられる。 先ほどと同じように namespace を指定するときは -n オプションを指定する。

$ nvme format /dev/nvme0 -s 2 -n 1

実行すると、ストレージのデータが消去される。 なお、デバイスがシステムの起動ディスクになっていても問題ない。

-s オプションについて

先ほど指定した -s オプションについて補足する。 このオプションは、データの消去をどのように実施するかを表している。 詳しくは man ページに記載されている。

$ man 1 nvme-format

manpages.ubuntu.com

オプションに指定する値の説明を、上記から引用する。 基本的には 2 を指定することで Secure Erase できる。

           │Value │ Definition                       │
           ├──────┼──────────────────────────────────┤
           │0     │ No secure erase operation        │
           │      │ requested                        │
           ├──────┼──────────────────────────────────┤
           │1     │ User Data Erase: All user data   │
           │      │ shall be erased, contents of the │
           │      │ user data after the erase is     │
           │      │ indeterminate (e.g., the user    │
           │      │ data may be zero filled, one     │
           │      │ filled, etc). The controller may │
           │      │ perform a cryptographic erase    │
           │      │ when a User Data Erase is        │
           │      │ requested if all user data is    │
           │      │ encrypted.                       │
           ├──────┼──────────────────────────────────┤
           │2     │ Cryptographic Erase: All user    │
           │      │ data shall be erased             │
           │      │ cryptographically. This is       │
           │      │ accomplished by deleting the     │
           │      │ encryption key.                  │
           ├──────┼──────────────────────────────────┤
           │3–7   │ Reserved                         │
           └──────┴──────────────────────────────────┘

なお、ソースコードを読む限り1、これらはいずれも NVMe デバイスに発行するコマンドの引数として表現されているようだ。 言いかえると、製品のファームウェアのレベルで Secure Erase の機能が実装されていることを前提としている。 そのため、製品によってはサポートされていない可能性もある。

いじょう。


  1. 内部的には libnvme というライブラリが使われていて、ユーザ空間からは ioctl(2) でデバイスを操作する

Ubuntu で Linux カーネルのバージョンを変更する

たまに、新しい機能が使いたいなどの理由で、Linux カーネルのバージョンを新しくしたいときがある。 そんなとき、Ubuntu であればビルド済みのパッケージが提供されているため、比較的容易にカーネルを入れ替えることができる。

使った環境は次のとおり。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04 LTS
Release:    22.04
Codename:   jammy
$ uname -srm
Linux 5.15.0-33-generic aarch64

もくじ

下準備

パッケージをダウンロードするために、あらかじめ wget をインストールしておく。

$ sudo apt-get update
$ sudo apt-get install wget

パッケージをダウンロードする

Ubuntu は Kernel PPA でビルド済みの Linux カーネルを提供している。 以下のメインラインカーネルは、不具合の調査や次期リリースに向けたバージョン選定などを目的としているらしい。

kernel.ubuntu.com

上記から、お目当てのバージョンで、必要なパッケージを選んでダウンロードしてくる。 今回は、現時点 (2022-05) でメインラインカーネルとして提供されている最新バージョンの v5.18 を選んだ。

まずは、アーキテクチャに依存しない linux-headers パッケージはどの環境でも必要になる。

$ wget https://kernel.ubuntu.com/~kernel-ppa/mainline/v5.18/amd64/linux-headers-5.18.0-051800_5.18.0-051800.202205222030_all.deb

そして、次にアーキテクチャに依存したパッケージをそれぞれダウンロードする。 今回は ARM64 環境なので、それに対応するものを。 名前に 64k がつくものは ARM64 環境でページサイズを 64KB に拡張したビルドになっているが、そちらは選んでいない。

$ wget \
  https://kernel.ubuntu.com/~kernel-ppa/mainline/v5.18/arm64/linux-headers-5.18.0-051800-generic_5.18.0-051800.202205222030_arm64.deb \
  https://kernel.ubuntu.com/~kernel-ppa/mainline/v5.18/arm64/linux-image-unsigned-5.18.0-051800-generic_5.18.0-051800.202205222030_arm64.deb \
  https://kernel.ubuntu.com/~kernel-ppa/mainline/v5.18/arm64/linux-modules-5.18.0-051800-generic_5.18.0-051800.202205222030_arm64.deb

インストールする

ダウンロードが終わったら、上記を apt-get なり dpkg を使ってインストールする。

$ sudo apt-get install ./*.deb

インストールできたらシステムを再起動する。

$ sudo shutdown -r now

再起動が終わったら、新しい Linux カーネルでシステムが動作しているはず。

$ uname -srm
Linux 5.18.0-051800-generic aarch64

めでたしめでたし。

Python: 集約特徴量を作るための scikit-learn Transformer 互換クラスの実装例について

ふと、集約特徴量を作るための scikit-learn Transformer 互換な実装を巷であまり見かけないなと思った。 そこで、自作しているものを公開してみる。

使った環境は次のとおり。

$ sw_vers          
ProductName:    macOS
ProductVersion: 12.4
BuildVersion:   21F79
$ python -V                                   
Python 3.9.12
$ pip list | egrep "(pandas|scikit-learn)"
pandas                        1.4.2
scikit-learn                  1.1.1

もくじ

集約特徴量とは

まずは、このエントリで集約特徴量と呼んでいるものについて説明する。 あらかじめ断っておくと、一般的に明確な呼称と定義があるわけではない。 Pandas で言えば、groupby() してから agg() して、それを元のデータフレームあるいは別のデータフレームとマージしたカラムのこと。 これによって、行方向の加工だけでは得られない、予測にとって有益な情報を列方向 (= 別の行) から抽出できる。

たとえば Count Encoding なんかも、集約特徴量の一種と言える。 agg() で計算する統計量が、出現回数 (count) というだけ。 今回は、そういった特定の統計量に特化させるのではなく、汎用に使えるものが欲しいよねという気持ちがある。

どうして必要なのか

まず、集約特徴量を作るのに、特に大した労力は必要ない。 前述したとおり、groupby() してから agg() した上で、データフレームを merge() するだけで得られる。 では、何故わざわざ scikit-learn の Transformer 互換の実装が必要になるのか。 それは、新規のデータの予測に使う場合を考えてのこと。

たとえば、あらかじめ学習データとテストデータが静的な CSV ファイルとして与えられる状況を考えてみる。 この場合、テストデータに集約特徴量を付与するには、両者を単純に連結して計算しちゃうことも考えられる。 統計量を計算する対象が目的変数でなければ、テストデータにも同じデータはあるはずなので、できてしまう。 しかし、このやり方ではまったく新しいデータに特徴量を付与したくなったときに都合が良くない。 なぜなら、まったく新しいデータに適用するには、既存のデータで計算した統計量を何処かに保存しておく必要があるため。 統計量の入ったデータフレームをバラバラと管理するのは汎用性がないので、できればやりたくない。

じゃあ、どこに保存しておくかというと、何らかのインスタンスのメンバに入っていると都合が良さそう。 そして、こういった場合によく用いられるのが scikit-learn の Transformer API を実装したクラス、ということになる。 scikit-learn の Transformer は、学習するだけの fit() メソッドと、適用するだけの transform() メソッドがある。 これによって、fit() で統計量を計算して、transform() でデータフレームとマージ、と処理が分けられる。

下準備

前置きが長くなったけど、ここからは実際にサンプルコードを示していく。 あらかじめ、必要なパッケージをインストールしておく。

$ pip install pandas scikit-learn

実装例

以下に集約特徴量を生成するための scikit-learn Transformer 互換の API を備えたクラスを示す。 使い方については後述する。

from __future__ import annotations

from typing import Callable
from typing import Iterable
from typing import Union

import pandas as pd
from sklearn.base import TransformerMixin
from sklearn.base import BaseEstimator


class AggregationEncoder(TransformerMixin, BaseEstimator):
    """集約特徴量を生成するための scikit-learn Transformer 互換 API を備えたクラス

    pandas の DataFrame が入力と出力になる
    """

    def __init__(
        self,
        group_keys: list[Union[str, Iterable[str]]],
        group_values: list[str],
        agg_methods: list[Union[str, Callable]],
    ):
        """

        :param group_keys: グループ化に使うカラム名
        :param group_values: 集約特徴量を計算するカラム名
        :param agg_methods: 計算する集約特徴量の種類 (統計量)
        """
        self.group_keys = group_keys
        self.group_values = group_values
        self.agg_methods = agg_methods

        # エンコードしたカラム名の一覧
        self.encoded_cols_: list[str] = []
        # Aggregation したデータフレームを記録しておく場所
        self._group_df_agg: list[tuple[Union[str, Iterable[str]], pd.DataFrame]] = []

    def fit(self, input_df: pd.DataFrame) -> TransformerMixin:
        """集約特徴量の生成に使う統計量を学習する

        :param input_df: 集約特徴量を生成するための統計量を求めるデータ
        """
        for agg_key in self.group_keys:
            for agg_method in self.agg_methods:

                # 集約方法の名前を作る
                if isinstance(agg_method, str):
                    # 文字列ならそのまま使う
                    agg_method_name = agg_method
                elif callable(agg_method):
                    # 呼び出し可能オブジェクトは名前を取り出して使う
                    agg_method_name = agg_method.__name__
                else:
                    raise ValueError(
                        "'agg_methods' must be a list of str or callable objects"
                    )

                for agg_value in self.group_values:
                    # 集約特徴量を作るために必要なカラムを求める
                    need_cols = [agg_value]
                    if isinstance(agg_key, str):
                        need_cols += [agg_key]
                    elif isinstance(agg_key, Iterable):
                        need_cols += list(agg_key)
                    else:
                        raise ValueError(
                            "'group_keys' must be a list of str or iterable objects"
                        )
                    # 集約特徴量を作るための計算
                    df_group_by = input_df[need_cols].groupby(agg_key)
                    df_agg = df_group_by[[agg_value]].agg(agg_method)

                    # 追加するカラムの名前を生成する
                    agg_key_name = (
                        agg_key if isinstance(agg_key, str) else "".join(agg_key)
                    )
                    # 名前の生成パターンが気に食わないときはココを変更する
                    col_name = "agg_{0}_{1}_groupby_{2}".format(
                        agg_method_name,
                        agg_value,
                        agg_key_name,
                    )
                    df_agg.columns = [col_name]

                    self.encoded_cols_.append(col_name)
                    self._group_df_agg.append((agg_key, df_agg))
        return self

    def transform(self, input_df: pd.DataFrame) -> pd.DataFrame:
        """集約特徴量を生成する

        :param input_df: 学習済みの集約特徴量を生成する対象となるデータ
        :return: 生成された集約特徴量
        """
        # 副作用が生じないようにコピーする
        new_df = input_df.copy()

        # 生成対象のデータフレームとマージしていく
        for group_key, df_agg in self._group_df_agg:
            new_df = pd.merge(
                new_df, df_agg, how="left", right_index=True, left_on=group_key
            )

        # 元々あったカラムは落とす
        new_df.drop(input_df.columns, axis=1, inplace=True)

        return new_df

    def fit_transform(self, input_df: pd.DataFrame) -> pd.DataFrame:
        """集約特徴量を学習して、同時に生成まで行う

        :param input_df: 集約特徴量を生成する対象のデータ
        :return: 生成された集約特徴量
        """
        self.fit(input_df)
        return self.transform(input_df)

    def get_params(self, deep=True) -> dict[str, object]:
        return {
            "group_keys": self.group_keys,
            "group_values": self.group_values,
            "agg_methods": self.agg_methods,
        }

上記を aggregation.py とか適当な名前で保存しておこう。

使ってみる

上記を利用するサンプルコードをいくつか書いてみよう。 上記のファイルがあるのと同じディレクトリで Python の REPL を立ち上げる。

$ python

基本的な使い方

たとえば、以下のような Pandas のデータフレームがあるとする。

>>> data = {
...     "group": ["A", "A", "A", "B", "B", "C"],
...     "value": [10, 20, 30, 40, 50, 60],
... }
>>> import pandas as pd
>>> df = pd.DataFrame(data)
>>> df
  group  value
0     A     10
1     A     20
2     A     30
3     B     40
4     B     50
5     C     60

上記から AggregationEncoder を使って集約特徴量を計算する準備をする。 まず、グルーピングのキーとして group_keys 引数にリストで group カラムを指定する。 同様に、バリューとして group_values 引数にリストで value カラムを指定する。 計算する統計量としては、agg_methods 引数で出現回数 (count) と平均 (mean) を指定した。

>>> from aggregation import AggregationEncoder
>>> encoder = AggregationEncoder(
...     group_keys=["group"],
...     group_values=["value"],
...     agg_methods=["count", "mean"]
... )

先ほどのデータフレームに対して fit_transform() メソッドを呼び出すと、次のとおり集約特徴量が生成される。 それぞれ、グループの出現回数と平均値になっている。

>>> encoder.fit_transform(df)
   agg_count_value_groupby_group  agg_mean_value_groupby_group
0                              3                          20.0
1                              3                          20.0
2                              3                          20.0
3                              2                          45.0
4                              2                          45.0
5                              1                          60.0

まったく新しいデータに適用する

前述したとおり、scikit-learn の Transformer にしたからには、新規のデータに適用したい。 次はそれを試してみよう。

以下のとおり、新しいデータフレームを用意する。 このデータフレームには、学習したデータには含まれていなかった Z という値も含まれている。

>>> data = {
...     "group": ["A", "B", "C", "Z"],
... }
>>> test_df = pd.DataFrame(data)
>>> test_df
  group
0     A
1     B
2     C
3     Z

先ほど学習させた AggregationEncodertransform() メソッドを呼び出してデータフレームを渡す。 すると、学習データの統計量を元に集約特徴量が生成されることがわかる。 初見の値については NaN になっている。

>>> encoder.transform(test_df)
   agg_count_value_groupby_group  agg_mean_value_groupby_group
0                            3.0                          20.0
1                            2.0                          45.0
2                            1.0                          60.0
3                            NaN                           NaN

特徴量を総当り的に作る

こういった特徴量は、総当り的にばーっと作って、とりあえずモデルに突っ込むというパターンが頻出する。 なので、そういったニーズも満たせるように作ってある。

たとえば、次のようにカテゴリ変数と連続変数が 2 つずつあるようなデータフレームを考える。

>>> import numpy as np
>>> data = {
...     "group1": ["A", "A", "A", "B", "B", "C"],
...     "group2": ["x", "y", "z", "x", "y", "z"],
...     "value1": [10, 20, 30, 40, 50, 60],
...     "value2": [np.nan, 500, 400, 300, 200, 100],
... }
>>> df = pd.DataFrame(data)
>>> df
  group1 group2  value1  value2
0      A      x      10     NaN
1      A      y      20   500.0
2      A      z      30   400.0
3      B      x      40   300.0
4      B      y      50   200.0
5      C      z      60   100.0

この場合は、group_keysgroup_values に計算したいカラムをリストで放り込んでおけばいい。

>>> encoder = AggregationEncoder(
...     group_keys=["group1", "group2"],
...     group_values=["value1", "value2"],
...     agg_methods=["min", "max"]
... )

あとは、「グルーピングのキー x グルーピングのバリュー x 特徴量の種類」の組み合わせで特徴量をばーっと作れる。 今回であれば「2 x 2 x 2 = 8」カラムになる。

>>> encoder.fit_transform(df)
   agg_min_value1_groupby_group1  agg_min_value2_groupby_group1  ...  agg_max_value1_groupby_group2  agg_max_value2_groupby_group2
0                             10                          400.0  ...                             40                          300.0
1                             10                          400.0  ...                             50                          500.0
2                             10                          400.0  ...                             60                          400.0
3                             40                          200.0  ...                             40                          300.0
4                             40                          200.0  ...                             50                          500.0
5                             60                          100.0  ...                             60                          400.0

[6 rows x 8 columns]

生成されるカラム名を取得する

組み合わせが増えると、どんなカラムができるんだっけ?と後から確認したくなる。 なので、生成されるカラムの名前を encoded_cols_ メンバで得られる。

>>> from pprint import pprint
>>> pprint(encoder.encoded_cols_)
['agg_min_value1_groupby_group1',
 'agg_min_value2_groupby_group1',
 'agg_max_value1_groupby_group1',
 'agg_max_value2_groupby_group1',
 'agg_min_value1_groupby_group2',
 'agg_min_value2_groupby_group2',
 'agg_max_value1_groupby_group2',
 'agg_max_value2_groupby_group2']

複数のキーでグルーピングしたい

複数のキーを使ってグルーピングしたい、という場合もあるはず。 そんなときは group_keys の要素にリストで複数のカラムを放り込む。

>>> encoder = AggregationEncoder(
...     group_keys=[["group1", "group2"]],
...     group_values=["value1"],
...     agg_methods=["mean"]
... )

上記であれば group1group2 の両方が一致するものを、同じグループとして扱う。 今回の指定とデータフレームだと、同じグループが存在しないので value1 の値がそのまま出てしまうけど。

>>> encoder.fit_transform(df)
   agg_mean_value1_groupby_group1group2
0                                  10.0
1                                  20.0
2                                  30.0
3                                  40.0
4                                  50.0
5                                  60.0

カスタマイズした統計量を計算する

既存の統計量だけでなく、自分なりの統計量を計算したいというニーズもある。 そんなときは、各グループで層化されたデータフレームを受け取って統計量を計算する関数を用意する。 以下では、たとえば中央値を計算している。 まあ、これは単に "median" を指定すれば良いだけなんだけど。

>>> def median(x):
...     return x.quantile(0.5)
... 

上記の関数を agg_methods に放り込めば良い。

>>> encoder = AggregationEncoder(
...     group_keys=["group1"],
...     group_values=["value1"],
...     agg_methods=[median]
... )

後は何も変わらない。カラム名の中には関数名が入る。

>>> encoder.fit_transform(df)
   agg_median_value1_groupby_group1
0                              20.0
1                              20.0
2                              20.0
3                              45.0
4                              45.0
5                              60.0

まとめ

今回は、集約特徴量を作るための scikit-learn Transoformer 互換クラスの実装例を紹介した。

pivot_root について

今回は、Linux でプロセスのルートファイルシステムの場所を変更する機能の pivot_root について扱う。 プロセスのルートファイルシステムを変更するのは、古典的な chroot を使っても実現できる。 ただ、chroot は隔離したはずのルートファイルシステムから脱出できてしまう事象、いわゆる脱獄が起こりやすい仕様になっている。 そのため、Docker などの一般的な Linux コンテナの実装では pivot_root がデフォルトで使われている。 今回は、そんな pivot_root をコマンドラインツールとしての pivot_root(8) と、システムコールとしての pivot_root(2) で触ってみる。

使った環境は次のとおり。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.4 LTS
Release:    20.04
Codename:   focal
$ uname -srm
Linux 5.4.0-109-generic aarch64
$ pivot_root --version
pivot_root from util-linux 2.34
$ gcc --version
gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ ldd --version
ldd (Ubuntu GLIBC 2.31-0ubuntu9.7) 2.31
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.

もくじ

下準備

下準備として、コマンドラインツールとしての pivot_root(8) が入っている util-linux をインストールしておく。 また、システムコールとしての pivot_root(2) を含む C のソースコードをビルドするために build-essential もインストールする。

$ sudo apt-get -y install util-linux build-essential

pivot_root(8) の動作を試す

まずはコマンドラインツールとしての pivot_root(8) から使ってみよう。

pivot_root を利用するには、変更先となるルートファイルシステムが必要になるので、用意する。 今回は mktemp(1) を使ってテンポラリディレクトリを作って利用する。

$ export ROOTFS=$(mktemp -d)
$ echo $ROOTFS
/tmp/tmp.pMdpBMx0Uu

ルートファイルシステムにはプロセスが利用するプログラムとライブラリの一式が必要になる。 そこで、必要そうなコマンドラインツール本体と、ダイナミックリンクされているライブラリをコピーしておく。 コピーするツールについてはお好みで。

$ COPY_CMDS=ls,cat,rm,head,mkdir,mount,umount,df
$ IFS=","
$ for CMD in ${COPY_CMDS}
do
  cp -avL --parents $(which ${CMD}) ${ROOTFS}
  ldd $(which ${CMD}) | grep -o "/lib.*\.[0-9]\+" | xargs -I {} cp -avL --parents {} ${ROOTFS}
done

上記を実行すると、コマンドラインツールと依存しているライブラリがテンポラリディレクトリ以下にコピーされる。

$ find $ROOTFS
/tmp/tmp.pMdpBMx0Uu
/tmp/tmp.pMdpBMx0Uu/usr
/tmp/tmp.pMdpBMx0Uu/usr/bin
/tmp/tmp.pMdpBMx0Uu/usr/bin/df
/tmp/tmp.pMdpBMx0Uu/usr/bin/cat
/tmp/tmp.pMdpBMx0Uu/usr/bin/mount
/tmp/tmp.pMdpBMx0Uu/usr/bin/ls
/tmp/tmp.pMdpBMx0Uu/usr/bin/head
/tmp/tmp.pMdpBMx0Uu/usr/bin/rm
/tmp/tmp.pMdpBMx0Uu/usr/bin/umount
/tmp/tmp.pMdpBMx0Uu/usr/bin/mkdir
/tmp/tmp.pMdpBMx0Uu/lib
/tmp/tmp.pMdpBMx0Uu/lib/ld-linux-aarch64.so.1
/tmp/tmp.pMdpBMx0Uu/lib/aarch64-linux-gnu
/tmp/tmp.pMdpBMx0Uu/lib/aarch64-linux-gnu/libblkid.so.1
/tmp/tmp.pMdpBMx0Uu/lib/aarch64-linux-gnu/libpcre2-8.so.0
/tmp/tmp.pMdpBMx0Uu/lib/aarch64-linux-gnu/libmount.so.1
/tmp/tmp.pMdpBMx0Uu/lib/aarch64-linux-gnu/libc.so.6
/tmp/tmp.pMdpBMx0Uu/lib/aarch64-linux-gnu/libpthread.so.0
/tmp/tmp.pMdpBMx0Uu/lib/aarch64-linux-gnu/libdl.so.2
/tmp/tmp.pMdpBMx0Uu/lib/aarch64-linux-gnu/libselinux.so.1

続いて、unshare(8) を使って Mount Namespace を新しく用意する 1ROOTFS 変数をそのまま使い続けたいので、sudo(8) するときに -E オプションを指定して環境変数を引き継いでおく。

$ sudo -E unshare --mount

ROOTFS ディレクトリをバインドマウントすることでマウントポイントにしておく。 これは pivot_root で新しいルートファイルシステムにする場所はマウントポイントである必要があるため。

# mount --bind ${ROOTFS} ${ROOTFS}

続いて、古いルートファイルシステムをマウントする場所としてディレクトリを作っておく。 これは pivot_root を実行するときに、新しいルートファイルシステムの他に古いルートファイルシステムをマウントする場所も指定する必要があるため 2

# mkdir -p ${ROOTFS}/.old-root

満を持して pivot_root(8) を実行する。

# pivot_root ${ROOTFS} ${ROOTFS}/.old-root

これで、ルートファイルシステムが先ほどテンポラリディレクトリで作ったディレクトリに切り替わった。

# ls /
lib  usr

ただし、この時点ではまだカレントワーキングディレクトリとして古いルートファイルシステムが見えてしまっている。 なので、カレントワーキングディレクトリをルートファイルシステムに変更しておこう。

# cd /

また、この時点ではまだ先ほど指定した /.old-root に古いルートファイルシステムが残っている。

# ls /.old-root
bin  boot  dev  etc  home  lib  lost+found  media  mnt  opt  proc  root  run  sbin  snap  srv  sys  tmp  usr  var

なので、アンマウントしたい。 そのために、まずは proc ファイルシステムをマウントする。 これは、どうやら umount(8) が proc ファイルシステムを見て動作しているようなので必要になる。

# mkdir /proc
# mount -t proc proc /proc
# df -Th
Filesystem     Type      Size  Used Avail Use% Mounted on
/dev/sda1      ext4      4.7G  2.9G  1.8G  62% /.old-root
udev           devtmpfs  452M     0  452M   0% /.old-root/dev
tmpfs          tmpfs     485M     0  485M   0% /.old-root/dev/shm
tmpfs          tmpfs      97M  1.1M   96M   2% /.old-root/run
tmpfs          tmpfs     5.0M     0  5.0M   0% /.old-root/run/lock
tmpfs          tmpfs      97M     0   97M   0% /.old-root/run/user/1000
tmpfs          tmpfs     485M     0  485M   0% /.old-root/sys/fs/cgroup
/dev/loop0     squashfs  128K  128K     0 100% /.old-root/snap/bare/5
/dev/loop1     squashfs   58M   58M     0 100% /.old-root/snap/core20/1437
/dev/loop2     squashfs   58M   58M     0 100% /.old-root/snap/core20/1408
/dev/loop3     squashfs   62M   62M     0 100% /.old-root/snap/lxd/22761
/dev/loop4     squashfs   62M   62M     0 100% /.old-root/snap/lxd/22530
/dev/loop6     squashfs   39M   39M     0 100% /.old-root/snap/snapd/15541
/dev/loop5     squashfs  896K  896K     0 100% /.old-root/snap/multipass-sshfs/147
/dev/loop7     squashfs   38M   38M     0 100% /.old-root/snap/snapd/15183
/dev/sda15     vfat       98M  290K   98M   1% /.old-root/boot/efi

これで、古いルートファイルシステムがアンマウントできる。

# umount -l /.old-root

これで最低限必要な作業は一通り終わった。 マウント状況を確認すると、だいぶシンプルになっている。

# df -Th
Filesystem     Type  Size  Used Avail Use% Mounted on
/dev/sda1      ext4  4.7G  2.9G  1.8G  62% /
# cat /proc/self/mountinfo
822 713 8:1 /tmp/tmp.IXvzlsgIm8 / rw,relatime - ext4 /dev/sda1 rw
823 822 0:5 / /proc rw,relatime - proc proc rw

あとは、この状態だと /dev がなかったりするのでマウントしたり。

# mkdir /dev
# mount -t devtmpfs devtmpfs /dev

pivot_root(2) の動作を試す

続いてはシステムコールとしての pivot_root(2) を使う。

先ほどと同じように、あらかじめテンポラリディレクトリに必要なコマンドラインツールとライブラリ一式をコピーしておく。 今回は bash もコピーする。

$ export ROOTFS=$(mktemp -d)
$ COPY_CMDS=bash,ls,cat,rm,head,mkdir,mount,umount,df
$ IFS=","
$ for CMD in ${COPY_CMDS}
do
  cp -avL --parents $(which ${CMD}) ${ROOTFS}
  ldd $(which ${CMD}) | grep -o "/lib.*\.[0-9]\+" | xargs -I {} cp -avL --parents {} ${ROOTFS}
done

先ほどはコマンドラインで操作していた、以降の処理はライブラリ関数やシステムコールで実行する。 以下に、そのサンプルコードを示す。 肝心の pivot_root(2) を呼び出しているのは syscall(SYS_pivot_root, argv[1], put_old_path) のところ。 pivot_root(2) は libc のラッパー関数がないので、直接 syscall(2) を使って呼び出す必要がある。

#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sched.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/mount.h>
#include <sys/syscall.h>

int main(int argc, char *argv[]) {
    // 引数の長さをチェックする
    if (argc < 2) {
        fprintf(stderr, "Please specify the path to change root\n");
        exit(EXIT_FAILURE);
    }

    // unshare(2) で Mount Namespace を作成する
    if (unshare(CLONE_NEWNS) != 0) {
        fprintf(stderr, "Failed to create a new mount namespace: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // ルートディレクトリから再帰的にマウントのプロパゲーションを無効にする
    if (mount("none", "/", NULL, MS_REC | MS_PRIVATE, NULL) != 0) {
        fprintf(stderr, "Failed to change root filesystem propagation: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // new_root に使うディレクトリを bind mount する
    if (mount(argv[1], argv[1], NULL, MS_BIND, NULL) != 0) {
        fprintf(stderr, "Failed to bind mount new_root directory: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // put_old に使うパスを求める
    char put_old_path[256];
    if (sprintf(put_old_path, "%s/.old-root", argv[1]) < 0) {
        fprintf(stderr, "Failed to sprintf: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // put_old に使うディレクトリを作る
    if (mkdir(put_old_path, 0777) < 0) {
        fprintf(stderr, "Failed to make directory: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // pivot_root(2) したいディレクトリにカレントワーキングディレクトリを変更する
    if (chdir(argv[1]) != 0) {
        fprintf(stderr, "Failed to change directory: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // pivot_root(2) を呼び出す
    // libc のラッパー関数がないので syscall(2) で呼び出す
    if (syscall(SYS_pivot_root, argv[1], put_old_path) < 0) {
        fprintf(stderr, "Can not pivot_root: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // put_old を umount2(2) で Lazy Detach する
    if (umount2("/.old-root", MNT_DETACH) < 0) {
        fprintf(stderr, "Can not umount: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // 古いファイルシステムをマウントしていた場所は不要なので削除しておく
    if (rmdir("/.old-root") < 0) {
        fprintf(stderr, "Failed to remove directory: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // execvp(3) でシェルを起動する
    char* const args[] = {"bash", NULL};
    if (execvp(args[0], args) != 0) {
        fprintf(stderr, "Failed to exec \"%s\": %s\n", args[0], strerror(errno));
        exit(EXIT_FAILURE);
    }

    return EXIT_SUCCESS;
}

上記のソースコードをビルドする。

$ gcc --std=c11 --static -Wall pivot_root.c

上記でビルドしたバイナリを、テンポラリディレクトリを引数にして実行する。

$ sudo ./a.out ${ROOTFS}

実行すると、先ほどコマンドラインで実行した状態と同じになる 3

# ls /
lib  usr
# df -Th
Filesystem     Type  Size  Used Avail Use% Mounted on
/dev/sda1      ext4  4.7G  2.9G  1.8G  62% /
# cat /proc/self/mountinfo
822 713 8:1 /tmp/tmp.CtMCVVvCU4 / rw,relatime - ext4 /dev/sda1 rw
714 822 0:5 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw

いじょう。

まとめ

今回は pivot_root をコマンドラインツールとシステムコールから使って、プロセスのルートファイルシステムを変更してみた。


  1. 古い unshare(8) ではマウントのプロパゲーションが生じるかもしれない。必要なら $ sudo mount --make-private / を実行しておく

  2. すぐにアンマウントするなら、新しいルートファイルシステムと古いルートファイルシステムに同じ場所を指定しても良いらしい

  3. /proc や /dev はマウントしていない