CUBE SUGAR CONTAINER

技術系のこと書きます。

Ubuntu 24.04 LTS に TightVNC を入れて macOS から GUI で操作する

GNU/Linux ディストリビューションのシステムは、大抵の操作を CUI で完結できる。 とはいえ、たまに特定の操作を GUI のフロントエンドでやりたくなることもある。 そんなとき、わざわざ筐体にディスプレイなどをつないで操作するのは手間がかかる。 そもそも、操作したいマシンと物理的に離れていることも多い。

そのような場面で、マシンを GUI で操作する方法はいくつか考えられる。 最もシンプルなのは SSH で X11 Forwarding することだろう。 その次に、もうちょっとリッチなデスクトップ環境が使いたいときに検討する選択肢のひとつが VNC だと思う。

今回は、Ubuntu 24.04 LTS に TightVNC を入れて macOS から操作する方法について書く。 このとき、接続には SSH Port Forwarding を使うことで外部には VNC 用のポートを公開しないようにする。

VNC サーバを入れた Ubuntu の環境は次のとおり。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 24.04.2 LTS
Release:    24.04
Codename:   noble
$ uname -srm
Linux 6.8.0-57-generic x86_64
$ tightvncserver --help 2>&1 | head -n 1
TightVNC Server version 1.3.10

VNC クライアントに使った macOS の環境は次のとおり。

$ sw_vers            
ProductName:        macOS
ProductVersion:     15.4
BuildVersion:       24E248
$ ssh -V      
OpenSSH_9.9p1, LibreSSL 3.3.6

もくじ

下準備

あらかじめ TightVNC と、デスクトップ環境として xfce4 をインストールしておく。

$ sudo apt-get install tightvncserver xfce4

VNC 接続用のパスワードを設定する

まずは tightvncpasswd コマンドを実行して VNC 用のパスワードを設定する。 このパスワードは、VNC クライアントからサーバに接続する際の認証に使われる。 使用するパスワードの長さは 6 ~ 8 文字になっている必要があるらしい。 ただし、今回の構成では SSH PortForwarding することが前提なので、その部分でセキュリティを担保する。 ここで設定するパスワードに関してはおまけのような感じ。

$ tightvncpasswd 
Using password file /home/amedama/.vnc/passwd
VNC directory /home/amedama/.vnc does not exist, creating.
Password: 
Verify:   
Would you like to enter a view-only password (y/n)? n

TightVNC のサーバを起動する

次に、tightvncserver コマンドを使って TightVNC のサーバを起動する。 このとき、必ず -localhost のオプションを指定する。 これでホストの外部に VNC 用のポートが公開されない。

$ tightvncserver -localhost -nolisten tcp -nevershared :1

その他のオプションについては、-nolisten tcp では Xserver へのリモート接続を抑制している。 そして -nevershared では、複数のクライアントから接続があった際にそれぞれで状態を共有しないようにする。 末尾の :1 では、仮想ディスプレイ番号 1 番で待ち受けることを指定している。 VNC サーバが使用するポート番号は、5900 に仮想ディスプレイの番号を足したものになる。

サーバを起動すると、ループバックアドレスで TCP の 5901 番を待ち受けていることが確認できる。

$ ss -tlnp | grep tight
LISTEN 0      5          127.0.0.1:5901       0.0.0.0:*    users:(("Xtightvnc",pid=4107,fd=1))

macOS から TightVNC のサーバに接続する

次に macOS から TightVNC のサーバへ接続しにいく。 まずは macOS から TightVNC をインストールしたマシンへ SSH でログインする。 このとき、リモートの 5901 番ポートをローカルの 5901 番ポートに対応する形で Port Forwarding する。

$ ssh -L 5901:localhost:5901 <remote>

上記で <remote> の部分は TightVNC をインストールしたマシンに付与した IP アドレスやドメイン名に読みかえる。

SSH で Port Forwarding できたら、macOS の「Finder > 移動 > サーバへ接続」を開く。 「サーバへ接続」の画面が開いたらサーバとして「vnc://localhost:5901」を入力して「接続」ボタンを押下する。

Finder > 移動 > サーバへ接続

すると、パスワードの入力を求められるので最初の工程で設定した VNC 用のパスワードを入力する。

VNC 用のパスワードを入力する

すると、次のようにそっけない画面が表示される。

初期状態のデスクトップ

ひとまず、これで VNC で接続できることが確認できた。

接続時に xfce4 のデスクトップ環境を起動する

次に、VNC で接続した際に xfce4 のデスクトップ環境を起動するように設定する。 まずは、デフォルトの設定用スクリプトをバックアップしておく。

$ cp ~/.vnc/xstartup{,.bak}

もし、何かあったときはバックアップから元に戻せば良い。

その上で、xfce4 の環境を起動する内容を設定用スクリプトに書き込む。

$ cat << 'EOF' > ~/.vnc/xstartup
#!/bin/sh

xrdb "$HOME/.Xresources"
startxfce4 &
EOF

設定したら、今起動している TightVNC のサーバを停止する。 これには tightvncserver -kill :<仮想ディスプレイ番号> を実行する。

$ tightvncserver -kill :1

停止したら、改めて起動する。 これで先ほどの設定用スクリプトが読み込まれるようになる。

$ tightvncserver -localhost -nolisten tcp -nevershared :1

あとは、先ほどと同じ手順でもう一度 macOS から VNC サーバに接続する。 すると、今度は次のようにちゃんとしたデスクトップ環境が表示される。

xfce4 のデスクトップ環境が表示される

あとは、上記の画面を使って必要な操作を進める。

そして、使い終わったら VNC サーバを停止する。

$ tightvncserver -kill :1

いじょう。

macOS に LHa for UNIX をソースコードからインストールする

Unix で LZH 形式の圧縮ファイルを展開できるソフトウェアはいくつかある。 たとえば lhasa 1 や unar 2 が挙げられる。

一方で、Unix で LZH 形式の圧縮ファイルを作成できるソフトウェアは限られる。 その限られた選択肢のひとつである LHa for UNIX が先日 Homebrew の core から削除された。

理由は LHa for UNIX が独自のライセンスを採用していたため。 これにより Homebrew が定めるライセンス要件を満たさないと判断された。

github.com

上記に対応するプルリクエストは 2025-03-24 にマージされている。

github.com

今となっては LZH 形式のアーカイブを扱う人はさほど多くないとは思われる。 しかも、展開ではなく作成とくればなおさら。 とはいえ、選択肢が限られた状況で困る人もいるかもしれないのでここにメモしておく。

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

$ sw_vers
ProductName:        macOS
ProductVersion:     15.4
BuildVersion:       24E248
$ uname -srm    
Darwin 24.4.0 arm64

もくじ

事前準備

もし Xcode の Command Line Tools が入っていないときはインストールする。

$ xcode-select --install

同様に、Homebrew が入っていないときはインストールする。

$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

ただし、Homebrew のインストールで叩くコマンドは稀に変わることがあるので公式サイトを確認した方が良い。

brew.sh

LHa for UNIX のビルドに必要なパッケージをインストールする。

$ brew install autoconf automake

LHa for UNIX のリポジトリをクローンする。

$ git clone https://github.com/jca02266/lha.git
$ cd lha

上記のリポジトリはもはやほとんど更新されることが無いため、基本的に HEAD を使って問題ないと思われる。

ビルドする

ビルドに必要な一連のコマンドを叩く。 ここらへんは README を読めば書いてある。

$ aclocal
$ autoheader
$ automake -a
$ autoconf
$ sh ./configure
$ make

これで src ディレクトリ以下に lha という名前のバイナリができる。

$ file src/lha
src/lha: Mach-O 64-bit executable arm64
$ ./src/lha --help | head -n 1
LHa for UNIX version 1.14i-ac20220213 (aarch64-apple-darwin24.3.0)

念の為にテストも実行してすべてパスすることを確認しておこう。

$ make check

インストールする

あとはバイナリをパスが通っている場所に置くだけ。 手動で mv などしても良いだろうけど古式ゆかしく make install もできる。

$ sudo make install

これでデフォルトなら /usr/local/bin に入るはず。

$ lha --help | head -n 1    
LHa for UNIX version 1.14i-ac20220213 (aarch64-apple-darwin24.3.0)

LZH の圧縮ファイルを作成する

動作を確認する。 まずは適当なテキストファイルを用意する。 念の為にハッシュも見ておく。

$ echo "Hello, World" > greet.txt
$ md5sum greet.txt 
9af2f8218b150c351ad802c6f3d66abe  greet.txt

上記を LZH ファイルに圧縮する。

$ lha a greet.lzh greet.txt 
greet.txt   - Frozen(100%)o

ちゃんとできた。

$ file greet.lzh 
greet.lzh:   LHarc 1.x/ARX archive data  [lh0], 'U' OS

LZH の圧縮ファイルを展開する

次に、元のテキストファイルを削除する。

$ rm greet.txt

そして圧縮ファイルの内容を展開する。 元のファイルを削除しておかないとファイルが重複してしまう。

$ lha e greet.lzh           
greet.txt   - Melted   :  o

展開されたファイルの内容が元のファイルと同一であることを確認する。

$ cat greet.txt            
Hello, World
$ md5sum greet.txt 
9af2f8218b150c351ad802c6f3d66abe  greet.txt

別のアーカイバでも展開する

念の為に別のアーカイバも使って展開しておく。 Homebrew で unar をインストールする。

$ brew install unar

先ほど展開したテキストファイルを削除しておく。

$ rm greet.txt

unar を使って LZH ファイルを展開する。

$ unar greet.lzh
greet.lzh: LZH
  greet.txt  (13 B)... OK.
Successfully extracted to "./greet.txt".

もう一度、展開されたファイルの内容が元のファイルと同一であることを確認する。

$ cat greet.txt
Hello, World
$ md5sum greet.txt 
9af2f8218b150c351ad802c6f3d66abe  greet.txt

以上、問題なく動作しちえることが確認できた。

ASRock DeskMini X600 で省スペースな Linux マシンを組んでみた

仮想マシンをいくつか作成して、簡単な検証をする時に使う Linux マシンが欲しくなった。 結果として、ASRock DeskMini X600 というベアボーンキットを使って小型のマシンを組んだ。 今回は、その内容についてメモしておく。

ASRock DeskMini X600

もくじ

概要

前述した通り、一度に数台程度の仮想マシンを動かすためのマシンが欲しくなった。 要件として考えた項目は次の通り。

  • なるべく部屋のスペースを占有しない
  • コストパフォーマンスで見劣りしない
  • さほど消費電力が大きくない
    • 動作時に数十ワット程度を想定する
  • なるべく動作音が小さい
  • 何らかの GNU/Linux ディストリビューションが動作する
    • Ubuntu や Proxmox VE など

選択肢について

省スペースでコスパが良いとなると、中国メーカー製のミニ PC や、Lenovo の ThinkCentre シリーズが思い浮かぶ。 実際、以前に Lenovo の ThinkCentre シリーズは M75q Tiny Gen2 を購入したことがある。 とはいえ、購入したことのある機材をただ増やすだけというのもあんまり面白くない。

blog.amedama.jp

そんな折、ASRock DeskMini という省スペースなコンピュータを組むためのベアボーンキットのシリーズがあることを知った。 ベアボーンキットなので、ケース、マザーボード、電源、そして CPU クーラーがあらかじめセットになっている。 ちなみに、筐体のサイズや電源の容量が一回り大きな DeskMeet というシリーズもあるようだ。

ASRock DeskMini は CPU のソケットや使用するチップセットごとに、いくつかのモデルが用意されている。 その中でも DeskMini X600 は TDP が 65W までの Socket AM5 の AMD CPU に対応したモデルになる。 今だと発売時よりも値段が下がっていて 3 万円を切る価格で購入できる。

構成を検討する

実際にコンピュータとして動作させるには、ベアボーンキットの他に CPU、メモリ、ストレージが必要になる。 その上でパーツの構成を検討してみると、10 万円前後の予算で組めそうなことが分かった。 なお、価格は購入先とタイミングによって変動する。

パーツ 製品 価格 (購入時点)
ベアボーンキット ASRock DeskMini X600 ¥25,980
CPU AMD Ryzen 5 8600G ¥33,136
メモリ Crucial DDR5-5600 32GBx2 (SO-DIMM) ¥23,480
SSD Crucial T500 1TB ¥11,600
CPUクーラー Scythe SHURIKEN 3 ¥3,980

これなら、他の選択肢を選んだときに想定されるスペックと価格に対してさほど見劣りしない。 そして、ベアボーンキットならお手軽に自作の楽しさも味わえる点も魅力に映った。

なお、予算を下げるとしたら次のような選択肢がありそう。

  • CPU を下位モデルの Ryzen 5 8500G にする (¥-10k)
  • メモリの容量を半分の 32GB に減らす (¥-10k)
  • ベアボーンキットに付属する CPU クーラーを使用する (¥-4k)

下位モデルの CPU の Ryzen 5 8500G は、8600G と同じ 6 コア 12 スレッドながら 4 コア分が高効率コアの Zen5c になっている。 ただ、TDP はどちらも 65W と変わらないようなので上位モデルを選んだ。 ちなみに、Passmark のベンチマーク (マルチコア) で比較すると 8600G が 25338 で 8500G が 21666 となっていた。

なお、CPU としては 8000 番台と同様に Socket AM5 を採用している 7000 番台や 9000 番台も使用できる。 DeskMini X600 は dGPU の追加が難しいことから iGPU が強力な APU の 8000 番台を組み合わせることが多いようだ。 今回に関してはグラフィックス性能は不要なので [79]000 番台の CPU も選定の候補には挙がった。 ただ、選定のタイミングではコストパフォーマンスに優れたモデルが見当たらなかったため選択しなかった。 また、[79]000 番台を選ぶ場合の注意点としては、以下が挙げられる。

  • TDP が 65W のモデルを選択すること
  • 8000 番台に比べてアイドル電力が 10W ほど大きいこと

CPU クーラーに関しては、ベアボーンキットに付属する CPU クーラーは動作音が大きいというレビューが多いようだった。 そのため、評判の良さそうな Scythe SHURIKEN 3 にしている。 なお、下調べでは Noctua NH-L9a-AM5 を使っている人も多いようだった。 ただし、メーカーの互換性ページを見ると 8600G はベースクロックでの動作時に冷やしきれない (cannot handle) とある。

ncc.noctua.at

公式に互換性が確認されているのは 8500G までのようだった。 発熱は消費電力と相関するはずなので、TDP は同じでも高効率コアを採用している点で両者は特性が異なるのかもしれない。

購入後について

実際に、試算に基づいてひととおりのパーツを購入した。

購入したパーツたち

上記を組み立てていく。

まずは DeskMini X600 の背面のネジを外すと、背面のプレートごとマザーボードが引き出せる。

背面のネジを外してマザーボードを引き出す

CPU を取り付ける

CPU を取り付ける。 取り付け方は一般的な Socket AM5 と変わらない。

  1. レバーを押し下げてクリップから外す
  2. ロードプレートを持ち上げて開く
  3. CPU とソケットの角にある三角形のマークを合わせて CPU をソケットに載せる
  4. ロードプレートを閉じる
  5. レバーを押し下げてクリップに固定する
  6. ソケットを保護しているキャップを取り除く

CPU を取り付ける

なお、この後に塗布する CPU グリスのはみ出しを防ぎたいときや冷却性能を上げたいときはサーマルペーストガードやガードプレートと呼ばれる部品を使うと良い。

CPU クーラーを取り付ける

CPU を取り付けたら、次に CPU クーラーを取り付ける。 DeskMini X600 に付属している CPU クーラーは、マウントホールに取り付けられた既存のブラケットに引っかけるタイプらしい。 一方で、今回選定した Scythe SHURIKEN 3 はソケットごとに専用のマウンティングプレートを使用する。 そのため、マザーボードに取り付けられているブラケットは取り外す。

マウントホールのブラケットを取り外す

取り外したら、CPU クーラーを取り付ける。

  1. 付属する Socket AM5 用のマウンティングプレートを CPU クーラーに装着する
  2. CPU クーラーに付属するサーマルペーストを CPU の表面に塗布する
  3. 付属するネジで CPU クーラーをマザーボードに固定する

CPU クーラーを取り付ける

メモリを取り付ける

次にメモリを取り付ける。 特に気にするようなことはない。

  1. スロットの固定用のクリップを左右に広げる
  2. 切り欠きの位置を揃えて、メモリのモジュールを上からスロットへ押し込む
  3. 固定用のクリップが自動的に閉じる

メモリを取り付ける

SSD を取り付ける

次に SSD を取り付ける。 今回はヒートシンクが付属しないモデルを購入した。 そのため、別途購入したヒートシンクを取り付けた。

サーマルパッドとヒートシンクで SSD をサンドイッチにする。

SSD にヒートシンクを取り付ける

あとは M.2 スロットに差し込んで、DeskMini X600 に付属するネジでマザーボードに固定するだけ。

なお、M.2 スロットは 2 つある。 また、2.5 インチの SATA HDD/SSD も 2 つまで搭載できる。

起動する

これで必要なパーツはすべて組み込めた。 AC アダプタの電源をつないで起動する。 UEFI BIOS が立ち上がって、組み込んだパーツを認識していることを確かめる。

UEFI BIOS が起動してパーツを認識していることを確かめる

メモリをテストする

メモリの初期不良がないか memtest86+ などを使って確認する。 やり方については以下のエントリに書いた。

blog.amedama.jp

OS をインストールする

ひとまず、Ubuntu 24.04 LTS をインストールして使うことにした。 USB メモリに Ubuntu 24.04 LTS のインストーライメージを書き込む。

書き込んだ USB メモリを筐体の USB ポートに差し込んで電源を入れたらインストーラが起動する。 あとはウィザードに従ってインストールするだけ。

まとめ

今回は ASRock DeskMini X600 というベアボーンキットを使って、小型の Linux マシンを組んだことについて書いた。 今のところ、特に問題なく使えている。

後日、筐体の側面にあるベンチレーションホールには以下のマグネット式のダストフィルターを貼り付けた。 140mm ファン用のフィルターでサイズがちょうど良い。 給排気の効率は少し落ちるかもしれないけど掃除の手間を優先した。

また、普段は電源を落として、使うときだけ WoL などで電源を入れるスタイルにしている。 詳しくは以下に書いた。

blog.amedama.jp

いじょう。

Ubuntu 24.04 LTS のマシンを Wake-on-LAN (WoL) でリモートから起動する

昨今の電気代などを考えると、コンピュータは使うとき以外はシャットダウンしておきたい。 アイドル時の電力が数十ワットでも、長く付けっぱなしだと年間で数千円にはなる。 ただ、出先で使いたくなったときなど物理的に電源を入れることが難しいシチュエーションもある。 もちろんサーバ向けの筐体であれば IPMI などの遠隔管理用のインターフェイスがあらかじめ用意されているけど、コンシューマ向けではそれも難しい。 後付けで遠隔管理用のカードを買い足すのにもお金がかかる。 そういったシチュエーションでは手っ取り早く Wake-on-LAN (以下、WoL) の利用が考えられる。 WoL を使うと、同一のネットワークにつなげることさえできれば遠隔からマシンの電源を入れることができる。

WoL はマジックパケットと呼ばれるデータに NIC (Network Interface Card) が反応してマシンの電源を入れてくれる仕組みのこと。 昨今は多くの NIC が WoL に対応している。 なお、前述した通り WoL を利用するには同一ネットワーク上にマジックパケットを送ることのできるマシンが必要になる。 それにはなるべく低消費電力な筐体で VPN や SSH サーバを建てたり、あるいは TailScale などの外部サービスを利用することが考えられる 1

使った環境は次のとおり。 WoL で起動するマシンが Ubuntu 24.04 LTS になる。 使った筐体は ASRock DeskMini X600 のオンボード LAN で、チップは Realtek 社製の RTL8125BG とのこと。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=24.04
DISTRIB_CODENAME=noble
DISTRIB_DESCRIPTION="Ubuntu 24.04.2 LTS"
$ uname -srm
Linux 6.8.0-55-generic x86_64

そして WoL のマジックパケットを送るのには macOS のマシンを使った。

$ sw_vers         
ProductName:        macOS
ProductVersion:     15.3.2
BuildVersion:       24D81
$ wakeonlan --version        
wakeonlan 0.42

もくじ

下準備

WoL で起動されるマシンの側では、まず UEFI BIOS の設定で WoL を有効にする。 UEFI BIOS での設定方法は、マザーボードを製造しているメーカーやバージョンによって異なる。 参考までに、今回使用した ASRock のマザーボードであれば Advanced Mode に切り替えた上で Advanced タブから PCIE Devices Power On の項目を Enabled にすれば良い。

次に、Ubuntu 上では iproute2 と ethtool をインストールしておく。

$ sudo apt-get install iproute2 ethtool

そして、WoL を使う NIC の MAC アドレスを確認する。 MAC アドレスの確認には ip link show コマンドを使う。

$ ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp3s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 9c:6b:00:XX:XX:XX brd ff:ff:ff:ff:ff:ff

今回であれば上記 enp3s0 インターフェイスの MAC アドレス (9c:6b:00 から始まっている) を見る。 なお、上記で MAC アドレスの下位 24bit はマスクしている。

そして、マジックパケットを送って起動させる側の macOS では Homebrew で wakeonlan をインストールする。 Homebrew が入っていなければインストールする。

$ brew install wakeonlan

一度だけ WoL を有効にする

定常的に使うことは無いだろうけど、まずは動作確認のために ethtool を使ってシャットダウン後に一度だけ WoL を有効にしてみよう。

現状で WoL が有効かは ethtool にネットワークインターフェースの名前を指定することで確認できる。 以下のように Wake-on の値が d であれば WoL が無効になっている。

$ sudo ethtool enp3s0 | grep -i wake-on
    Supports Wake-on: pumbg
    Wake-on: d

一度だけ WoL を有効にするには、次のように -s オプション付きでネットワークインターフェースに wol g を指定する。 値に g を指定することで NIC がマジックパケットに反応するようになる。

$ sudo ethtool -s enp3s0 wol g

もう一度 ethtool を使って Wake-on の値が g になっていることを確認しよう。

$ sudo ethtool enp3s0 | grep -i wake-on
    Supports Wake-on: pumbg
    Wake-on: g

確認したらマシンの電源をシャットダウンする。

$ sudo shutdown -h now

シャットダウンできたら、マジックパケットを送る側のマシンに操作を移す。 wakeonlan コマンドに、先ほど確認した MAC アドレスを指定して実行しよう。

$ wakeonlan 9c:6b:00:XX:XX:XX
Sending magic packet to 255.255.255.255:9 with payload 9c:6b:00:XX:XX:XX
Hardware addresses: <total=1, valid=1, invalid=0>
Magic packets: <sent=1>

上手くいけばマジックパケットに反応してマシンが起動してくるはず。 ただし、先ほど ethtool を使って設定するやり方では永続化されないため、起動後は Wake-on の値が d に戻っている。

$ sudo ethtool enp3s0 | grep -i wake-on
    Supports Wake-on: pumbg
    Wake-on: d

Netplan で設定を永続化する

動作の確認ができたので、次は設定を永続化する。 Ubuntu 22.04 LTS 以降の Ubuntu はネットワークの管理にデフォルトで Netplan を使う。 そして、WoL の設定も Netplan で永続化できる。 Netplan で WoL を有効にするには、ネットワークインターフェースに wakeonlan: true という設定を入れれば良い。

今回使っているマシンでは /etc/netplan/01-fixed-ip-addr.yaml の設定ファイルでインターフェイスの設定をしている。 設定ファイルの名前は環境によって異なるので適宜読み替えてほしい。

初期の状態では、以下のように enp3s0 に DHCPv4 / DHCPv6 の設定を入れつつ静的に IP アドレスを振っている。

$ sudo cat /etc/netplan/01-fixed-ip-addr.yaml 
network:
    ethernets:
      enp3s0:
        dhcp4: true
        addresses:
          - 172.16.XXX.XXX/16
        dhcp4-overrides:
          use-routes: true
          use-dns: true
        dhcp6: true
    version: 2

上記に追加で wakeonlan: true の設定を入れる。

$ sudo cat /etc/netplan/01-fixed-ip-addr.yaml 
network:
    ethernets:
      enp3s0:
        dhcp4: true
        addresses:
          - 172.16.XXX.XXX/16
        dhcp4-overrides:
          use-routes: true
          use-dns: true
        dhcp6: true
        wakeonlan: true
    version: 2

この状態でマシンを再起動してみよう。

$ sudo shutdown -r now

すると、再起動した後でもネットワークインターフェイスに WoL が有効になっている。

$ sudo ethtool enp3s0 | grep -i wake-on
    Supports Wake-on: pumbg
    Wake-on: g

もちろんシャットダウンした上で WoL を使うこともできる。

$ sudo shutdown -h now

マジックパケットを送ってマシンが起動してくることを確認しよう。

$ wakeonlan 9c:6b:00:XX:XX:XX
Sending magic packet to 255.255.255.255:9 with payload 9c:6b:00:XX:XX:XX
Hardware addresses: <total=1, valid=1, invalid=0>
Magic packets: <sent=1>

ばっちりだ。

$ sudo ethtool enp3s0 | grep -i wake-on
    Supports Wake-on: pumbg
    Wake-on: g

めでたしめでたし。


  1. もし自身で適切に設定するのに自信がないときは TailScale などの実績が豊富な外部サービスを利用する方がセキュリティ的にはおすすめできる

Python: multiprocessing モジュールの開始方式 spawn / fork の違いについて

Python の標準ライブラリに含まれる multiprocessing モジュールは、マルチプロセスでの並列処理に用いられる。 Pure Python の処理を並列化しようとしたとき、今のところ最初に検討するのがマルチプロセスになるはず。 というのも、Python のマルチスレッドには多くの場合に GIL (Global Interpreter Lock) の制約があるため。 最も一般的に用いられている Python 処理系の CPython は、まだデフォルトで同時に実行できるスレッドがプロセスあたり一つに限られる 1。 つまり、マルチスレッドでは I/O バウンドな処理を高速化できても、CPU バウンドな処理を高速化できない。 そこで、マルチコア CPU の恩恵を得るためにはスレッドではなくプロセスをたくさん横に並べようという発想になる。 ただし、プロセスはスレッドに比べると生成のコストやプロセス間通信に費やすオーバーヘッドが相対的に大きいといったデメリットがある。

前置きが長くなったけど、今回の本題は multiprocessing モジュールの開始方式 (start method) の違いについて。 開始方式というのは、multiprocessing モジュールが新しくプロセスを作成するときのやり方を指している。 現状で以下の 3 つの方式が用意されていて、プラットフォームごとにデフォルトの開始方式が異なる。

  • spawn
    • Windows と macOS のデフォルト
  • fork
    • macOS を除く POSIX システムのデフォルト
  • forkserver

今回は、メジャーな OS でデフォルトになっている spawn と fork について、どのような違いがあるのか調べたので備忘録として書いておく。 どのように調べたかというと CPython の multiprocessing モジュールのソースコードを読んだ。

使った環境は次のとおり。 macOS の環境は以下を使った。

$ sw_vers
ProductName:        macOS
ProductVersion:     15.3.1
BuildVersion:       24D70
$ uname -srm                    
Darwin 24.3.0 arm64
$ python3 -V
Python 3.12.9

Linux の環境は以下を使った。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 24.04.2 LTS
Release:    24.04
Codename:   noble
$ uname -srm
Linux 6.8.0-53-generic aarch64
$ python3 -V
Python 3.12.3

もくじ

基本的な使い方について

開始方式の説明に入る前に、おさらいとして multiprocessing モジュールの基本的な使い方について見ておく。

以下のサンプルコードでは、multiprocessing モジュールで作成した子プロセスが、親プロセスから文字列を受け取って標準出力に書き出す。

import multiprocessing as mp


def f(message):
    """メッセージを受け取って標準出力に書き出す関数"""
    print("Hello,", message)


def main():
    # 子プロセスで実行する処理を定義する
    p = mp.Process(target=f, args=("World!",))
    # 子プロセスを開始する
    p.start()
    # 子プロセスの終了を待つ
    p.join()

if __name__ == "__main__":
    main()

上記を適当なファイル名で保存して実行してみよう。

$ python3 greet.py
Hello, World!

ちゃんとメッセージが表示された。

プロセスの情報を書き出してみる

とはいえ、先ほどの例だとマルチプロセスになっているのかさえよく分からない。 そこで、次はそれぞれのプロセスの情報を書き出すようにしてみよう。

以下のサンプルコードでは、それぞれのプロセスや特殊変数の内容を書き出している。

import os
import multiprocessing as mp


def print_proc_info(context):
    """プロセスに関する情報を標準出力に書き出す関数"""
    # 呼び出し元の情報を出力する
    print(context)
    # __name__ 変数の内容を出力する
    print("__name__:", __name__)
    # 親プロセスの pid を出力する
    print("parent process id:", os.getppid())
    # 自身の pid を出力する
    print("process id:", os.getpid())
    # 見えやすいようにブランクを入れる
    print("")


def f():
    """子プロセスで実行する関数"""
    # プロセスに関する情報を書き出す
    print_proc_info("### Child ###")


def main():
    # 自身のプロセスの情報を出力する
    print_proc_info("### Parent ###")
    # 子プロセスで関数 f() を実行する
    p = mp.Process(target=f)
    p.start()
    p.join()

if __name__ == "__main__":
    main()

上記を実行する。 すると、親と子のプロセスそれぞれの PID や、親プロセスの PID、そして __name__ 変数の内容が出力される。

$ python3 procinfo.py
### Parent ###
__name__: __main__
parent process id: 61809
process id: 82787

### Child ###
__name__: __mp_main__
parent process id: 82787
process id: 82789

上記を見ると、子プロセスの os.getppid() で親プロセスの PID が返されていることが分かる。

また、__name__ の内容も親プロセスと子プロセスでは変わっているようだ。 具体的には子プロセスの場合は __name__ に入っているのが "__mp_main__" になっている。 ただし、これは前述した開始方式に spawn を使った場合の特徴になる。 上記は、デフォルトの開始方式が spawn になっている macOS で実行したもの。 試しにデフォルトの開始方式が fork の Linux (Ubuntu) で実行してみよう。

$ python3 procinfo.py 
### Parent ###
__name__: __main__
parent process id: 1225
process id: 1475

### Child ###
__name__: __main__
parent process id: 1475
process id: 1476

すると、今度は __name__ に入っている内容が、どちらのプロセスも "__main__" になっていることが分かる。

ちなみに、macOS と Linux はそれぞれデフォルトの開始方式が異なっているだけに過ぎない。 つまり、明示的に別の開始方式を指定して使うことはできる。 例えば以下のコードでは set_start_method() 関数を使って明示的に開始方式に fork を指定している。

import multiprocessing as mp
import os


def print_proc_info(context):
    """プロセスに関する情報を標準出力に書き出す関数"""
    print(context)
    print("__name__:", __name__)
    print("parent process id:", os.getppid())
    print("process id:", os.getpid())
    print("")


def f():
    """子プロセスで実行する関数"""
    print_proc_info("### Child ###")


def main():
    # プロセスの開始方式に fork を指定する
    mp.set_start_method("fork")

    print_proc_info("### Parent ###")
    p = mp.Process(target=f)
    p.start()
    p.join()


if __name__ == "__main__":
    main()

上記であれば macOS 上で実行したとしても __name__ に入る内容が親子のプロセスで同じになる。

開始方式の fork について

さて、とうとう本題である開始方式の説明に入る。 まずは、より単純な開始方式の fork について。

開始方式の fork は、実装に文字通り fork というシステムコールを使っている。 システムコールは、ユーザ空間からカーネル空間の機能を利用するために使われる API の一種。 その中で fork というシステムコールは、呼び出し元のプロセスを複製して新しいプロセスを生成するのに使われる。 呼び出し元のプロセスと新しく作られるプロセスは、それぞれ親子関係を持ったプロセスになる。 もし、システムコールや fork といった単語に耳馴染みがないときは以下をおすすめしたい。

以下に、multiprocessing モジュールの開始方式として fork を用いるのと概念的に類似したサンプルコードを示す。 標準ライブラリの os モジュールには fork システムコールを薄くラップした API があることから、それを利用している。

import os
import sys


def print_proc_info(context):
    """プロセスに関する情報を標準出力に書き出す関数"""
    # 呼び出し元の情報を出力する
    print(context)
    # __name__ 変数の内容を出力する
    print("__name__:", __name__)
    # 親プロセスの pid を出力する
    print("parent process id:", os.getppid())
    # 自身の pid を出力する
    print("process id:", os.getpid())
    # 見えやすいようにブランクを入れる
    print("")


def main():
    # プロセスを fork して親子プロセスに分かれる
    pid = os.fork()

    # 返り値で親と子それぞれの処理に分岐できる
    if pid == 0:
        # 子プロセスで実行される処理
        print_proc_info("### Child ###")
        # プロセスを終了する
        sys.exit(0)
    else:
        # 親プロセスで実行される処理
        print_proc_info("### Parent ###")
        # 子プロセスが終了するのを待つ
        os.waitpid(pid, 0)


if __name__ == "__main__":
    main()

上記のコードがどのように動くのか、最初は少しイメージしにくいかもしれないので少し補足する。 上記の os.fork() を呼んだ時点で、同じ行を実行しているプロセスが 2 つできる。 ただし、それぞれのプロセスで os.fork() の返り値が異なっている。 まず、呼び出し元となった親プロセスは返り値として新たに生成された子プロセスの PID が返される。 一方で、新たに生成された子プロセスではゼロが返される。

それぞれのプロセスはその後にある if の条件分岐にそれぞれ入って、返り値ごとに異なる処理を実行する。 親プロセスに関してはプロセスの情報を出力した後で、子プロセスが終了するまで os.waitpid() 関数がブロックする。 そして子プロセスに関してはプロセスの情報を出力した後で os.exit() 関数を使ってプロセスを終了する。 子プロセスが終了すると、親プロセスの os.waitpid() のブロックが解除されるので親プロセスも終了する。

さて、それでは上記のサンプルコードを適当なファイル名で保存して実行してみよう。

$ python3 fork.py
### Parent ###
__name__: __main__
parent process id: 61809
process id: 89320

### Child ###
__name__: __main__
parent process id: 89320
process id: 89321

先ほど multiprocessing モジュールを使ったサンプルコードを Linux 上で実行したのと同じ結果が得られることが分かる。

このように、開始方式の fork は比較的シンプルな作りをしている。 理解する上で重要なポイントは、子のプロセスはメモリなどの状態を親のプロセスから複製しているところ。 そのため親子のプロセスは、特に何もしなくても fork した時点で存在する変数などは同じものが使用できる。 これはメリットとして働く場合もある一方で、誤って同じ資源を異なるプロセスから利用してしまうことで何らかの競合が生じる恐れもある。

開始方式の spawn について

それでは、次にもう少し複雑な処理をしている開始方式の spawn について。

端的に言うと開始方式の spawn は、fork した後で子プロセスが Python のインタプリタを exec し直している (fork-exec)。 その上で、呼び出し元のモジュールを読み込み直したり、実行すべき処理や結果をプロセス間通信でやり取りする。 システムプログラミングの知識があれば、この説明だけでもある程度のイメージが付くかもしれない。 たとえばシェルなどがコマンドを実行するときにやっている処理と大して変わらない。

pipe を使ったプロセス間通信について

本丸となる開始方式の説明に入る前に、少し寄り道して理解の前提として必要な pipe について説明する。 pipe は fork と同様にシステムコールのひとつで、プロセス間通信を実現するための手段のひとつ。 典型的には fork した親子のプロセス間でデータをやり取りするのに使う。 もちろん後ほど spawn に関するサンプルコードの中で使用する。

以下に pipe を fork と組み合わせて使うサンプルコードを示す。 このコードでは親のプロセスから子のプロセスへ pipe を使ってメッセージを送っている。 メッセージを受け取った子のプロセスは、受け取ったメッセージを標準出力に書き出す。

import os


def print_proc_info(context):
    """プロセスに関する情報を標準出力に書き出す関数"""
    print(context)
    print("__name__:", __name__)
    print("parent process id:", os.getppid())
    print("process id:", os.getpid())
    print("")


def main():
    # プロセス間通信に使うパイプを用意する
    child_r, parent_w = os.pipe()

    pid = os.fork()

    if pid == 0:
        # 子プロセスで実行される処理
        print_proc_info("### Child ###")
        # 使用しないパイプは閉じる
        os.close(parent_w)
        # パイプからデータを読み出す
        with os.fdopen(child_r, "r") as pipe_r:
            # 読み出した内容を出力する
            message = pipe_r.read()
            print("Received message:", message)
    else:
        # 親プロセスで実行される処理
        print_proc_info("### Parent ###")
        # 使用しないパイプは閉じる
        os.close(child_r)
        # パイプにデータを書き込む
        with os.fdopen(parent_w, "w") as pipe_w:
            pipe_w.write("Hello, World!")
            pipe_w.flush()
        # 子プロセスの終了を待つ
        os.waitpid(pid, 0)


if __name__ == "__main__":
    main()

上記に適当な名前をつけて実行してみよう。

$ python3 pipe.py 
### Parent ###
__name__: __main__
parent process id: 61809
process id: 96303

### Child ###
__name__: __main__
parent process id: 96303
process id: 96304

Received message: Hello, World!

上記の出力から、親プロセスのメッセージを子プロセスで受信できていることが確認できる。

開始方式の spawn と概念的に類似したコード

さて、それでは本丸となるサンプルコードを示す。 これは multiprocessing モジュールの開始方式として spawn を用いる場合にやっていることと概念的にほぼ変わらない。

import os
import pickle
import sys


def print_proc_info(context):
    """プロセスに関する情報を標準出力に書き出す関数"""
    print(context)
    print("__name__:", __name__)
    print("parent process id:", os.getppid())
    print("process id:", os.getpid())
    print("")


def f(message):
    """メッセージを受け取って標準出力に書き出す関数"""
    print("Hello,", message)


def spawn_child_process(target, args):
    """multiprocessing の spawn モードと類似した子プロセスの生成を実現する関数"""
    # 親子プロセス間の通信に使うパイプを用意する
    child_r, parent_w = os.pipe()

    # プロセスを fork する
    pid = os.fork()

    if pid == 0:
        # 子プロセスで実行される処理
        # 使用しない方のパイプは閉じる
        os.close(parent_w)
        # exec 後の子プロセスでパイプのファイルディスクリプタを利用できるようにする
        os.set_inheritable(child_r, True)
        # 子プロセスは fork した直後に exec で実行するプログラムを書き換える
        # 書き換え先になるプログラムは Python インタプリタ
        # このとき __file__ を引数にして、起動するインタプリタに読み込ませる
        # つまり新しい Python のプロセスでモジュールを実行し直すのに相当する
        os.execvp(
            sys.executable,
            [
                sys.executable,
                # このモジュール (.py) を新しく生成したプロセスに読ませるための引数
                __file__,
                # fork & exec した子プロセスであることを識別するための引数をつけておく
                "--multiprocessing-fork",
                # プロセス間通信に使うパイプのファイルディスクリプタの情報も引数で渡す
                str(child_r),
            ],
        )
    else:
        # 親プロセスで実行される処理
        # 使用しない方のパイプは閉じる
        os.close(child_r)
        # 子プロセスで呼び出したい関数と引数を Pickle でシリアライズする
        pickled_data = pickle.dumps((target, args))
        # シリアライズしたバイト列をパイプに書き込む
        with os.fdopen(parent_w, "wb") as pipe_w:
            pipe_w.write(pickled_data)
            pipe_w.flush()
        # 親プロセスは子プロセスが終了するのを待つ
        os.waitpid(pid, 0)


def is_spawned_child_process():
    """fork & exec した後の子プロセスかを判断する関数"""
    return len(sys.argv) > 1 and sys.argv[1] == "--multiprocessing-fork"


def main():
    if is_spawned_child_process():
        # 子プロセスで実行される処理
        print_proc_info("### Child ###")
        # 親プロセスから引き継いだファイルディスクリプタの情報を引数から得る
        child_r = int(sys.argv[2])
        # パイプのファイルディスクリプタを開く
        with os.fdopen(child_r, "rb") as pipe_r:
            # 親プロセスから送られてくる実行すべき関数と引数の情報をデシリアライズする
            message = pipe_r.read()
            target, args = pickle.loads(message)
            # デシリアライズした関数を実行する
            target(args)
    else:
        print_proc_info("### Parent ###")
        # multiprocessing の spawn モードに類似したやり方で関数を実行する
        spawn_child_process(f, "World!")


if __name__ == "__main__":
    main()

上記には、いくつか理解する上で重要なポイントがある。

まず、子プロセスは fork した後に os.execvp() という関数を使っている。 この関数は exec と呼ばれるシステムコールの薄いラッパになっている。 exec システムコールを使うと、現在のプロセスで異なるプログラムをメモリ上にロードして新たに実行できる。 実行しているのは sys.executable なので、要するに呼び出し元の親プロセスで実行していたのと同じ Python 処理系になる。

そして、Python 処理系の引数には __file__ を渡していることから、元々のプロセスが実行していたのと同じモジュールが読み込まれる。 これによって、親のプロセスと同じ関数や変数などが読み込まれて使えるようになる。 ただし、新しいプロセスでモジュールを読み込み直しているので親プロセスとオブジェクト自体は異なっている。

さらに、親プロセスと子プロセスの間でやり取りが必要な情報は、前述した pipe を使って通信している。 具体的には、子プロセスで実行してほしい呼び出し可能オブジェクトや、その引数を Pickle でシリアライズして親プロセスから送っている。 なお、今回は単純のために子プロセスから親プロセスの方向で戻すはずの返り値やエラーなどに関する情報は省いている。

それでは、実際にコードに適当な名前をつけて実行してみよう。

$ python3 spawn.py 
### Parent ###
__name__: __main__
parent process id: 61809
process id: 98614

### Child ###
__name__: __main__
parent process id: 98614
process id: 98615

Hello, World!

上記から、ちゃんと子プロセスで処理が実行されたことが分かる。

ちなみに開始方式に spawn を使った multiprocessing のコードは、REPL で対話的に実行すると動作しない。 これは、上記のサンプルコードでやっていることを確認すれば自明で、REPL では __file__ が存在しないから。 要するに、新しく起動した Python プロセスにモジュールを読み込ませて関数などのオブジェクトの状態を復元できないことが原因のようだ。

また、開始方式に spawn を使う場合、子プロセスでは実行したくない処理を if __name__ == "__main__": のインデントの中に置く必要がある。 これについても、新しく起動した Python プロセスにモジュールを読み込ませることが理由になる。 もしインデントの外に出ていると、起動した Python プロセスにモジュールを読み込ませるタイミングでそのコードが評価されてしまう。 前述した通り、開始方式が spawn だと子プロセスの __name__"__mp_main__" になる。 そのため、子プロセスでは条件分岐のインデント内の処理が実行されなくなる。

まとめ

今回は multiprocessing モジュールの開始方式である spawn と fork がやっていることについて調べた。

まず開始方式の fork は、単純に fork(2) しているだけ。 シンプルでオーバーヘッドが少ない点がメリットになる。 その反面、プロセス間で共有するものが多いことから競合が生じやすい。

そして開始方式の spawn は、fork(2) した上で exec(2) している。 これには exec(2) したり、その後でモジュールの読み込みやプロセス間通信に費やすオーバーヘッドが大きいデメリットがある。 その反面、Python のプロセスを一から作り直していることからプロセス間で共有するものが少なくて競合が生じにくい。

なお、今回は第三の開始方式である forkserver について扱わなかった。 どうやら forkserver は子プロセスを生成するために専用のプロセスを用意して、UNIX ドメインソケットでやり取りする実装になっているようだ。 機会があれば、また改めて紹介したい。

ちなみに、一般的な処理の並列化を目的とする場合には multiprocessing モジュールよりも concurrent.features モジュールを使うのがおすすめ。 concurrent.features モジュールの ProcessPoolExecutor の方が multiprocessing モジュールよりも高レベルな API なので楽に並列化できる。

docs.python.org

参考

CPython の multiprocessing モジュールに関するソースコードは以下で読める。

github.com


  1. デフォルトで GIL の制約を外そうという長期的な取り組みはある (https://peps.python.org/pep-0703/)

macOS で memtest86+ の iso ファイルを USB メモリに書き込む

コンピュータを購入した直後にやることのひとつといえば、メモリに初期不良がないかを調べること。 ごく稀なことではあるけど購入した時点で故障していることがある。 初期不良は交換の対象になることから、なるべく早い段階で確認する方が良い。 あるいは、もちろん使い続けて経年で故障することもある。

memtest86+ 1 はメモリの故障を見つけるためのソフトウェアのひとつ。 様々なパターンでデータの書き込みと読み込みを繰り返すことで、メモリのデータが化けないかを確認する。 起動ディスクとして USB メモリなどに書き込んで使う。

使った環境は以下のとおり。

$ sw_vers                                                            
ProductName:        macOS
ProductVersion:     15.3.1
BuildVersion:       24D70

もくじ

memtest86+ をダウンロードする

まずは memtest86+ の ISO ファイルをダウンロードする。 使用するバージョンやダウンロード用の URL は公式サイトで確認しよう。

$ brew install wget
$ wget https://www.memtest.org/download/v7.20/mt86plus_7.20_64.iso.zip

圧縮されているので展開する。

$ unzip mt86plus_7.20_64.iso.zip 
Archive:  mt86plus_7.20_64.iso.zip
  inflating: memtest.iso

次のように ISO ファイルが得られる。

$ ls -1         
memtest.iso
mt86plus_7.20_64.iso.zip
$ file memtest.iso
memtest.iso: ISO 9660 CD-ROM filesystem data (DOS/MBR boot sector) 'MT86PLUS_64' (bootable)

ファイルを USB メモリに書き込む

次に、上記を USB メモリなどのメディアに書き込む。 使用する USB メモリは、特にこだわりが無ければ何を使っても構わない。

USB メモリを Mac のポートに差し込む前に、macOS が認識しているディスクの状態を確認しておく。 これには diskutil コマンドを使うと良い。

$ diskutil list
/dev/disk0 (internal, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      GUID_partition_scheme                        *500.3 GB   disk0
   1:             Apple_APFS_ISC Container disk1         524.3 MB   disk0s1
   2:                 Apple_APFS Container disk3         494.4 GB   disk0s2
   3:        Apple_APFS_Recovery Container disk2         5.4 GB     disk0s3

/dev/disk3 (synthesized):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      APFS Container Scheme -                      +494.4 GB   disk3
                                 Physical Store disk0s2
   1:                APFS Volume Macintosh HD            11.2 GB    disk3s1
   2:              APFS Snapshot com.apple.os.update-... 11.2 GB    disk3s1s1
   3:                APFS Volume Preboot                 7.0 GB     disk3s2
   4:                APFS Volume Recovery                1.0 GB     disk3s3
   5:                APFS Volume Data                    77.9 GB    disk3s5
   6:                APFS Volume VM                      20.5 KB    disk3s6

Mac の USB ポートに USB メモリを差し込むと /dev 以下のディスクとして認識するはず。 今回は /dev/disk4 として認識した。

$ diskutil list
/dev/disk0 (internal, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      GUID_partition_scheme                        *500.3 GB   disk0
   1:             Apple_APFS_ISC Container disk1         524.3 MB   disk0s1
   2:                 Apple_APFS Container disk3         494.4 GB   disk0s2
   3:        Apple_APFS_Recovery Container disk2         5.4 GB     disk0s3

/dev/disk3 (synthesized):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      APFS Container Scheme -                      +494.4 GB   disk3
                                 Physical Store disk0s2
   1:                APFS Volume Macintosh HD            11.2 GB    disk3s1
   2:              APFS Snapshot com.apple.os.update-... 11.2 GB    disk3s1s1
   3:                APFS Volume Preboot                 7.0 GB     disk3s2
   4:                APFS Volume Recovery                1.0 GB     disk3s3
   5:                APFS Volume Data                    77.9 GB    disk3s5
   6:                APFS Volume VM                      20.5 KB    disk3s6

/dev/disk4 (external, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:     FDisk_partition_scheme                        *60.5 GB    disk4
   1:             Windows_FAT_32 NO NAME                 60.5 GB    disk4s1

一般的には、購入した直後にそのまま使用できるように FAT32 などの形式でフォーマットされている。 その場合、ポートに USB メモリを差し込むと自動でマウントされる。 あらかじめアンマウントしておく。

$ diskutil unmountDisk /dev/disk4
Unmount of all volumes on disk4 was successful

アンマウントした後にdf コマンドなどでディスクが表示されないことを確認する。

$ df -h | grep disk4

あとはダウンロードしてきた ISO ファイルを dd コマンドを使って認識したデバイスへ書き込む。

$ sudo dd if=memtest.iso of=/dev/disk4 bs=1m

書き込んだ USB メモリを使用する

正常に書き込みが終わったら、あとはその USB メモリを使うだけ。 メモリをテストしたいコンピュータの筐体の USB ポートに USB メモリを差し込んで起動する。 もし memtest86+ が起動しないときは、ディスクの起動順序やセーフブートの設定を確認する。

memtest86+ を実行する

一晩くらい放置してテストが何度かパスすることを確認すれば、ひとまず問題ない。 エラーが出るときは故障している可能性が高い。

いじょう。

Python: 複数行の文字列のインデントを揃えて読みやすくしたい

今回は小ネタ。 インデントのあるソースコード上で、複数行の文字列を読みやすくする方法について。 毎回どうやってたっけと調べるのでメモしておく。

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

$ sw_vers
ProductName:        macOS
ProductVersion:     15.2
BuildVersion:       24C101
$ python -V         
Python 3.11.9

もくじ

複数行の文字列のインデントを揃えたくなる場面について

インデントのある場所で複数行の文字列を扱おうとすると、以下のような感じになりがち。 ここでは関数 f() の中で "a""b""c" という文字を、それぞれ独立した行で、先頭にスペースを含めずに出力したい。 設定ファイルとかテンプレートなんかを扱うときのイメージ。

def f():
    s = """a
b
c"""
    print(s)

上記はもちろん上手くいく。 ただ、ソースコード上のインデントと文字列がズレているのでちょっと読みにくく感じる。

>>> f()
a
b
c

理想としては、たとえば以下のような感じにしたい。

def f():
    s = """
    a
    b
    c
    """
    print(s)

ただ、これだと 2 つ課題がある。 まず 1 つ目が、各行の先頭にインデントに相当するスペースが入ってしまうこと。 そして 2 つ目が、先頭と末尾に空行が入ってしまうこと。

>>> f()

    a
    b
    c
    

これだと設定ファイルやテンプレートとして利用できないことが考えられる。

各行の先頭のインデントを取り除く

まず、各行の先頭にインデントに相当するスペースが入ってしまう点は textwrap モジュールに dedent() という便利な関数がある。 この関数を使うと各行に入った先頭のインデントを除去してくれる。

import textwrap

def f():
    s = """
    a
    b
    c
    """
    print(textwrap.dedent(s))

使ってみると、次のように先頭のインデントに相当するスペースが取り除かれた。

>>> f()

a
b
c

先頭と末尾の空行を取り除く

次に、先頭と末尾の空行に関しては str#strip() メソッドを使うことで取り除ける。

import textwrap

def f():
    s = """
    a
    b
    c
    """
    print(textwrap.dedent(s).strip())

やってみると、次のように先頭と末尾の空行が取り除かれた。

>>> f()
a
b
c

いじょう。