LeavaTailの日記

LeavaTailの日記

Linuxエンジニアを目指した技術者の備忘録

Linuxカーネルのファイルアクセスの処理を追いかける (1) 環境セットアップ

関連記事

概要

QEMUの vexpress-a9 (arm) で Linux Kernel v5.15を起動させながら、ファイル書き込みのカーネル処理を確認していく。

本章では、コードリーディング用にデバッグ情報を付与したLinuxカーネルのビルドBuildRootによる実行環境を構築した。

はじめに

一般的なOSはファイルという形式を通して、ハードディスクやフラッシュメモリといった記憶装置にデータを保存している。
この処理を担うのがファイルシステムと呼ばれる機構である。
一般的な利用者はこのことを意識せずに利用することができるが、ソフトウェアエンジニアは処理を理解していないとディスクIOパフォーマンスが悪化し、システム全体のパフォーマンスに大きく影響を及ぼす恐れがある。
そこで、アプリケーションがファイルを書き込んだ際にLinuxカーネルがどのような処理で記憶装置に読み書きされるかを順を追って説明する。

調査範囲

本記事では、SYSCALL_DEFINE(write)からデバイスドライバまでの処理を対象とする。

背景

一般的なOSでは、さまざまなコンポーネントから成り立っている。ファイルシステムもその一つである。 ファイルの書き込み処理一つとっても、多数のコンポーネントとの関係を持つ。

下記の図は、他サイトで掲載されているLinuxカーネルv4.10の構成図である。(2022年8月現在、Linuxカーネルv5.19.1がリリースされている)

https://www.thomas-krenn.com/de/wikiDE/images/e/e0/Linux-storage-stack-diagram_v4.10.png

"https://www.thomas-krenn.com/en/wiki/Linux_Storage_Stack_Diagram">Linux Storage Stack Diagram

このように、Linuxカーネルv4.10の時点でもたくさんのフローからファイルアクセスが成り立っている。(大まかな処理は最新カーネルでも変わらないのでこの図を基に説明を続ける)
ここでは、read(2)とwrite(2)について説明する。
read(2)

  1. VFSは、ファイルに対応するファイルシステムのread処理を呼び出す。
  2. ファイルシステムは、ファイルがキャッシュに載っているか確認する。(あればそれをApplicationに渡して終了する)
  3. ファイルシステムは、Block LayerにBIOを挿入する。
  4. Block Layerは、スケジューラによりBIOを並び替る。
  5. Block Layerは、Device DriverにRequestを発行する。
  6. Device Driverは、Physical devicesにIOを要求する。
  7. Physical devicesは、デバイスファームウェアに則りデータの読み込みをする。
  8. Physical devicesは、カーネルにIO完了通知をする。
  9. (Direct_IOでなければ)カーネルは、読み込みしたデータをPage cacheとしてキャッシュする。
  10. カーネルは、Applicationにデータを渡して終了する。

write(2)

  1. VFSは、ファイルに対応するファイルシステムのwrite処理を呼び出す。
  2. (Direct_IOでなければ)ファイルシステムは、ファイルをキャッシュにしてApplicationに完了を通知する。
  3. ファイルシステムは、Block LayerにBIOを挿入する。
  4. Block Layerは、スケジューラによりBIOを並び替る。
  5. Block Layerは、Device DriverにRequestを発行する。
  6. Device Driverは、Physical devicesにIOを要求する。
  7. Physical devicesは、デバイスファームウェアに則りデータの書き込みをする。
  8. Physical devicesは、カーネルにIO完了通知をする。

一般的なストレージに対する書き込み処理は、下記のようなライトバック方式で行われる。

カーネルの処理シーケンス

ファイルの書き込みをしたアプリケーションはページキャッシュをDirtyにするだけで処理を終了する。その後、カーネルスレッドが定期的にDirtyとなっているキャッシュを書き込む。

環境構成

本稿では、QEMUを用いて観測対象のLinuxカーネルを起動させる。 QEMUを利用することで、下記のような利点が得られる。

  • 実行環境による違いを緩和することができる
  • ホスト側から任意のタイミングでGDBでアタッチすることができる

本稿では、下記の環境で処理を確認していった。

Host側

概要 説明
Architecture x86_64
Board custom board
Linux 5.15.0-46-generic
kernel config unknown
Userland Ubuntu Desktop 22.04.1
Buildroot buildroot 2022.08.1
QEMU QEMU emulator version 7.0.0

Guest側

概要 説明
Architecture armhf
Board vexpress-a9
Linux linux-5.15
kernel config vexpress_defconfig
Userland Buildroot
Storage SD card
File-Syste ext2
Disk Scheduler MQ-DEADLINE

デバッグ機能について

vexpress_defconfigでもカーネルを起動させることができるが、デバッグ容易性のために Kconfigの変更デバッグ用のカーネルパッチを適用をする。

追加したデバッグ機能については次のRepositoryで管理している。

https://github.com/LeavaTail/buildroot-2022.08.1-qemu_arm_vexpressgithub.com

これを、buildrootディレクトリの配下にある board/qemu/arm-vexpress 以下に展開しておく。

行基板について

QEMUでは、Versatile Express motherboardとCoreTile Express A9x4 daughterboardの組み合わせをvexpress-a9というボードでサポートしている。 それぞれの機器のデータシートはArm Developerに記載されている。

下記は、Arm Developerで記載されている機器のレイアウト図を引用している。
こちらは、Versatile Express motherboardのレイアウト図である。

https://documentation-service.arm.com/static/5e9074b78259fe2368e2acd9?token= https://developer.arm.com/documentation/dui0448/i/hardware-description/overview-of-the-coretile-express-a9-4-daughterboard

こちらは、CoreTile Express A9x4 daughterboardのレイアウト図である。

https://documentation-service.arm.com/static/5e9db8569931941038de23df?token= https://developer.arm.com/documentation/dui0448/i/hardware-description/overview-of-the-coretile-express-a9-4-daughterboard

これらの情報とQEMUの公式サイトに書かれている情報を基に、vexpress-a9の概略図を示す。

vexpress-a9のレイアウト イメージ図

また、Linuxカーネル v5.15におけるメモリーマップを記す。

ARM memory Layout

作成手順

実行環境の準備

Linuxカーネルのファイルアクセスをトレースするための実行環境をBuildRootにより作成する。

  1. Buildrootを入手する。

     leava@kbuild:/work$ git clone https://github.com/buildroot/buildroot.git
     leava@kbuild:/work$ cd buildroot
     leava@kbuild:/work/buildroot$ git checkout 2022.08.1
    
  2. Buildrootのデフォルトの設定を使用する。

     leava@kbuild:/work/buildroot$ make qemu_arm_vexpress_defconfig
    
  3. Buildrootの設定を適宜修正する。

     toolchain  --->
       (glibc) C library
       [*] Enable C++ support
       [*] Build cross gdb for the host
         [*]   TUI support
    
     System configuration  --->
       /bin/sh (bash)  --->
       (root) Root password
    
     Kernel   --->
       (5.15) Kernel version
       (board/qemu/arm-vexpress/patches) Custom kernel patches
       Kernel configuration (Using a custom (def)config file)  --->
       (board/qemu/arm-vexpress/.config) Configuration file path
    
     Target packages
       [*]   Show packages that are also provided by busybox
       Debugging, profiling and benchmark  --->
         [*] blktrace
       Development tools
         [*] binutils
       Filesystem and flash utilities
         [*] mmc-utils
       Networking applications
         [*] dropbear
    
     Host utilities  ---> 
       [*] host qemu 
          *** Emulators selection ***
         [*]   Enable system emulation
         [*]   Enable Linux user-land emulation
    
  4. Buildrootの設定からユーザランドを構築する。

     leava@kbuild:/work/buildroot$ make
    
  5. Buildrootで作成した環境を実行するためのスクリプトを用意する。

// 1:
#!/bin/bash -x
(
BUILDROOT_DIR="/usr/local/src/buildroot"
BINARIES_DIR="${BUILDROOT_DIR}/output/images/"
NFSROOT="/srv/nfsroot/armhf/buildroot"
FSTYPE="ext2"
SDCARD="/tmp/${FSTYPE}.img"
EXTRA_ARGS="-nographic -s"
TARGET_ROOTFS="/dev/nfs"
EXTRA_CMDLINE="nfsroot=${NFSROOT},vers=3,tcp ip=on"
CMDLINE="console=ttyAMA0,115200 rootwait root=${TARGET_ROOTFS} rw ${EXTRA_CMDLINE}"

function gen_testimage () {
        DISTDEV="/mnt"
        mkfs.${FSTYPE} ${SDCARD}
        mount -t ${FSTYPE} -o loop ${SDCARD} ${DISTDEV}
        echo -n A > ${DISTDEV}/FILE
        umount ${DISTDEV}
}

if [ ! -e ${SDCARD} ]; then
        dd if=/dev/zero of=${SDCARD} bs=1K count=1M
        gen_testimage
elif [ -z `blkid -o value -s TYPE ${SDCARD}` ]; then
        gen_testimage
fi

cd ${BINARIES_DIR}

export PATH="/usr/local/src/buildroot/output/host/bin:${PATH}"
exec qemu-system-arm -M vexpress-a9 -smp 1 -m 1024 \
        -kernel zImage -dtb vexpress-v2p-ca9.dtb \
        -drive file=${SDCARD},if=sd,format=raw \
        -append "${CMDLINE}" \
        -net nic,model=lan9118 -net user \
        ${EXTRA_ARGS}
)

ルートファイルシステムのカスタマイズ

Buildrootで生成したルートファイルシステムNFS経由でmountできるようにカスタマイズする。

  1. Host PCに下記パッケージをインストールする。

     leava@leava-host:/srv/nfsroot$ sudo apt-get install nfs-kernel-server
    
  2. Host PCでNFSサーバの設定する

     leava@leava-host:/srv/nfsroot$ echo "/srv/nfsroot       127.0.0.1(rw,no_root_squash,no_subtree_check,insecure)" | sudo tee -a /etc/exports
     leava@leava-host:/srv/nfsroot$ sudo exportfs -v
    
  3. Host PCにBuildrootで生成したルートファイルシステムを展開する

     leava@leava-host:/srv/nfsroot$ sudo tar -xf output/images/rootfs.tar -C /srv/nfsroot/armhf/buildroot
    

テストスクリプトの作成

// 1:
#!/bin/bash

DEVFILE="/dev/mmcblk0"
DIRECTORY="/mnt"
TARGETFILE="FILE"

if [ ! -e ${DEVFILE} ]; then
        echo "Target device is not exist" 1>&2
        exit 1
fi

mountpoint -q ${DIRECTORY} || mount -t ext2 ${DEVFILE} ${DIRECTORY}

echo "Write: Test start"
mount | grep ${DIRECTORY}

sync
echo 3 > /proc/sys/vm/drop_caches

echo -n "HELLO" >> ${DIRECTORY}/${TARGETFILE}

sync
echo 3 > /proc/sys/vm/drop_caches

umount ${DIRECTORY}

調査方法

  1. QEMU上でLinuxカーネルを起動する。

     leava@leava-host:~/work$ start-qemu.sh
     ...
     [    2.193490][    T1] Run /sbin/init as init process
     Starting syslogd: OK
     Starting klogd: OK
     Running sysctl: OK
     Initializing random number generator: OK
     Saving random seed: [   34.958887][   T96] random: dd: uninitialized urandom read (512 bytes read)
     OK
     Starting rpcbind: OK
     Starting network: ip: RTNETLINK answers: File exists
     Skipping eth0, used for NFS from 10.0.2.2
     FAIL
     Starting dropbear sshd: OK
    
     Welcome to Buildroot
     buildroot login: root
     Password:
     #
    
  2. ホスト側からGDBでattachする。

     leava@leava-host:~/work$  cd /usr/local/src/buildroot/output/build/linux-5.15; ../../host/bin/arm-buildroot-linux-gnueabihf-gdb vmlinux
     GNU gdb (GDB) 10.2
     Copyright (C) 2021 Free Software Foundation, Inc.
     License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
     This is free software: you are free to change and redistribute it.
     There is NO WARRANTY, to the extent permitted by law.
     Type "show copying" and "show warranty" for details.
     This GDB was configured as "--host=x86_64-pc-linux-gnu --target=arm-buildroot-linux-gnueabihf".
     Type "show configuration" for configuration details.
     For bug reporting instructions, please see:
     <https://www.gnu.org/software/gdb/bugs/>.
     Find the GDB manual and other documentation resources online at:
         <http://www.gnu.org/software/gdb/documentation/>.
    
     For help, type "help".
     Type "apropos word" to search for commands related to "word"...
     Reading symbols from vmlinux...
     (gdb) target remote :1234
     Remote debugging using :1234
     cpu_v7_do_idle () at arch/arm/mm/proc-v7.S:78
     78              ret     lr
    
  3. 任意の関数 (ここでは、sys_writeに対して)ブレークポイントを設置する。

     (gdb) b sys_write
    
  4. プログラムの実行を再開する。

     (gdb) c
    
  5. 上記の環境で下記のコマンドを実行した場合のファイルアクセスの処理を調査する。

     # write-exec.sh
    

おわりに

本記事では、これからLinuxカーネルのファイルアクセスの処理を追いかけるための環境構築をした。
次回の記事では、作成した環境を用いて「writeシステムコールの実態からVFSレイヤまで」の処理を追いかける。

変更履歴

  • 2020/09/25: 記事公開
  • 2020/11/22: 調査対象 (Syscall Interface ~ デバイスドライバ) を追加
  • 2020/12/14: GDB接続手順の追記
  • 2020/12/17: アーキテクチャx86_64からARMに変更
  • 2020/12/18: 調査するカーネルのバージョンを5.7.19から5.10に変更
  • 2021/11/23: 環境構築をinitramfsからNFSを用いる方法に変更
  • 2022/08/21: 調査するカーネルのバージョンを5.10から5.15に変更
  • 2022/10/09: Buildroot製のルートファイルシステムに変更

参考

help2man でmanページを自動生成する

概要

automakeから、help2manによるmanページを生成する手順を確認した。

はじめに

Linuxではmanコマンドによる利用者にオンラインマニュアルを提供する機構が設けられている。

ソフトウェアパッケージの開発者は、そのソフトウェアの使い方を記したmanページを含めて配布することが望ましい。
しかし、manページはの作成は更新する頻度も多く手間がかかる

help2manは、ソフトウェアの実行結果から標準的manページを自動生成するツールである。 www.gnu.org

また、help2manautomakeを併用することで、configureファイルの生成からmanページの生成までの手順を自動化することができる。

そこで、本記事ではautomakeを使ってhelp2manでmanページを生成する手順を紹介する。

help2manの出力結果

準備

下記のリポジトリを例にhelp2manを使ってmanページを生成する手順と、automakeによりmanページを自動生成する手順をまとめる。

github.com

dumpexfatは、FAT/exFATファイルシステムイメージから情報を取得することができるプログラムである。
このプログラムは複数のオプションの指定を許しており、以下のオプションをサポートしている。

オプション (long option) 概要
-c (--cluster=index) indexで指定したクラスタ番号のデータを出力する
-f (--force) 未確保のクラスタでも出力を許す
-o (--output) 結果を指定したファイルに出力する
-s (--sector=index) indexで指定したセクタ番号のデータを出力する
-v (--verbose) メッセージを冗長に出力する

help2man とは

公式サイトに詳細な説明があるので、詳しくはそちらを参照。

www.gnu.org

help2manはプログラムの--helpオプションと--versionオプションから簡単なmanページを作成するツールである。

help2manの概略図

このとき、--help--versionは、仕様に基づいた形式*1で記述することで精度の高いmanページを生成することができる。

一般的なmanページを作るときに必要となる--helpを下記に示す。

    Usage: <コマンド名> <コマンド書式>              ★ SYNOPSIS に出力される
    <コマンド概要>                                 ★ NAMEに出力される

    <コマンド説明>                                 ★ DESCRIPTIONに出力される

    Options:                                      ★ OPTIONSに出力される
      <オプション>     <オプション概要>

    Examples:                                     ★ EXAMPLEに出力される
      <例>    <実行例の説明>

    Report bugs to: <mailing-address>             ★ REPORTING BUGSに出力される

<>で記述したものに関しては、開発者がそのプログラムにあったものに修正する必要となる。
また、その他にmanページのセクションを追加したい場合は、<セクション名>: で追加することができる。

一般的なmanページを作るときに必要となる--versionを下記に示す。

    <command> <version>

    <著作権表示>                                   ★ COPYRIGHTに出力される

    Written by <作者>                             ★ AUTHORに出力される

manページを生成する

help2manの書式に則って、--help--versionを作成する。

    $ dumpexfat --help
    Usage: dumpexfat [OPTION]... FILE
    dump FAT/exFAT filesystem information.

      -c, --cluster=index   dump the cluster index after dump filesystem information.
      -f, --force   dump the cluster forcibly in spite of the non-allocated.
      -o, --output=file     send output to file rather than stdout.
      -s, --sector=index    dump the sector index after dump filesystem information.
      -v, --verbose Version mode.
      --help        display this help and exit.
      --version     output version information and exit.

    Examples:
      dumpexfat /dev/sda    dump FAT/exFAT filesystem information.
      dumpexfat -c 2 /dev/sda       dump FAT/exFAT filesystem information and cluster #2.

    $ dumpexfat --version
    dumpexfat 0.1

    Written by LeavaTail.

manページの作成に必要なものはこれで完了したので、help2manを実行する。

    $ help2man --no-discard-stderr -N -o dumpexfat.1 dumpexfat    

今回は下記のオプションに指定した。

  • --no-discard-stderr: stderrに出力された内容も使用する
  • -N: manページの末尾にTexinfoの情報を載せないようにする
  • -o: 出力先のファイルを指定する

automakeにhelp2manを組み込む

help2manの更新でも記述されているが、automakeと連携させる場合には、manページの依存関係にソースコードを指定することが望ましい。
そこで、このプロジェクトではMakefile.amに下記のターゲットを追加した。

    dumpexfat.1: dumpexfat$(EXEEXT)
        help2man --no-discard-stderr -N -o dumpexfat.1 ./dumpexfat

これで、automakeを実行することでhelp2manコマンドからmanページを生成できるようなMakefileが生成できるようになった。

おわりに

本記事は、automakeを使ってhelp2manでmanページを生成する手順を書き留めた。

help2manによって生成されたmanページは必要最低限度の情報が記述されているはずなので、とりあえずプロジェクトに組み込むとよいだろう。

変更履歴

  • 2020/08/10: 記事公開

*1:cpコマンドを参考にするとよい

GitHub Actionsで既存リポジトリのテストを自動化する

概要

既存のソフトウェアに対して、GitHub Actionによるテストの自動化を導入する。

はじめに

GitHubではパブリックリポジトリであれば GitHub Actions という機能が無料に使用することができる

GitHub Actions では、世の中のCI/CDサービスと同様にソフトウェアワークフロー (例えば、Masterブランチにマージする前に用意しておいたテストを流し、テストが通ったらマージを許可するなど) を簡単に自動化することができる。

GitHub Actionsは利用料金がお手軽(プライベートリポジトリでも利用時間が2000分/月)といった点や、GitHubのサービスの一つでもある点から高い統合性がみられる。
また、ソフトウェアワークフローのテンプレートも幅広いパターンが用意されており、導入の敷居が低いといった点もみられる。

そこで、本記事はソフトウェアワークフローが自動化されていないリポジトリに、GitHub Actionsを導入するまでの手順を書き留めた。

準備

下記のリポジトリを例にGitHub Actionsと連携させる手順をまとめる。

github.com

このプロジェクトは、様々なアーキテクチャ(x86/arm/arm64)用でLinuxカーネルをビルドするためのDockerイメージを管理している。
Dockerイメージの利用方法は、ホスト環境にあるlinuxトップディレクトリ(${PWD}/linux)をコンテナ内のワーキングディレクトリ(/work)と共有化して実行する。

Dockerfileからイメージを生成する。

$ docker build -t kbuild .

Dockerコンテナでカーネルをビルドする。

$ docker run --rm --name=kbuild -v ${PWD}/linux:/work -it kbuild make

これまで、このプロジェクトはテストを用意しておらず、実行のたびに問題が見つかっていた。

テストスクリプトの作成

GitHub Actionsと連携するにあたって、手動で実施するテストを設計・実装するところから始める。

このプロジェクトでは、Linuxカーネルのビルド環境を構築するDockerイメージであり、考えられるテストパターンは莫大に存在する。
今回は、LTSのカーネル *1 のみを対象として、Linuxカーネルがビルドできるかどうかを確認する。

テストの全体像は次に示す。

プロジェクトのソフトウェアワークフロー

  1. テストに必要なtarballをkernel.orgからダウンロードする。
  2. プロジェクトが提供しているDockerfileからDockerイメージを作成し、コンテナを起動させる。
  3. コンテナ内でカーネルがビルドできるかどうかテストする。

GitHub Actionsとの連携

プロジェクトのソフトウェアフローが決まったので、GitHub Actionsと連携させていく。

  1. プロジェクトトップページのActionsを選択する。
    GitHub Actionsボタン
  2. 今回はDocker imageのリポジトリなのでDocker imageの「Set up this workflow」を選択する。(自分のリポジトリに似たテンプレートがなければ、「set up a workflow yourself」で一から作成する。)
    workflowsのテンプレートを選択する
  3. templeteからworkflowsのひな形が生成されるので、必要に応じて修正する。
    workflowsのEdit画面

今回のプロジェクトでは、下記のようなworkflowを作成した。

name: Docker Image CI                          # ワークフローの名前。ページに表示される
        
on:                                            # masterブランチへのpush/PRを契機とする
  push:        
    branches: [ master ]        
  pull_request:        
    branches: [ master ]        
        
jobs:        
        
  build:                                       # ジョブのID
        
    runs-on: ubuntu-latest                     # ジョブを実行するマシンの環境
        
    steps:                                     # 一連のタスクとして下記を実行する
    - uses: actions/checkout@v2                # リポジトリからチェックアウトする 
    - name: Obtain LTS kernel                  
      run: ./tests/00_init.sh                  # tarballを取得・展開する
    - name: Execute the Docker Container
      run: ./tests/01_build.sh                 # Dockerコンテナの起動
    - name: Execute the Docker Container
      run: ./tests/10_allnobuild.sh            # カーネルのビルド            

上記のファイルを作成後、Masterブランチにコミットすると、作成したworkflowsが有効になる。
また、今回のworkflowはmasterへのpushを契機としているので、すぐに作成したテストが走る。

workflowsの結果確認

ジョブ(build)の結果を確認する。

ジョブの実行結果を確認する

今回は設定しなかったが、actions/upload-artifact@v1と指定すると成果物を残すことができる。(artifactsで確認可能? )

下記にGitHub Actionsと連携させた後の概要図は下記のようなものになった。

GitHub Actionsと連携後のソフトウェアワークフロー

  1. ユーザが対象のリポジトリに対してPushかPull Requestを投げたとき(on:以下の指定より)、GitHubがあらかじめ用意してあるjobを実行する。
  2. GithubUbuntu仮想マシンを立ち上げ(runs-on: ubuntu-latestより)、下記のタスクを実行していく。
  3. Ubuntu仮想マシン上にリポジトリをチェックアウトする。(actions/checkout@v2)
  4. 指定されたスクリプトを実行し、Docker Imageが適切かどうか検証する。

おわりに

本記事はソフトウェアワークフローが自動化されていないリポジトリGitHub Actionsを導入するところまでを書き留めた。

この記事はあくまで導入のためのフローを書き記したものであり、GitHub Actionsはまだ多彩の機能を兼ね備えている。
ドキュメントを充実しており、導入の敷居も低いのでGitHub Actionsを導入して、プロジェクトの品質を向上させていきたい。

変更履歴

  • 2020/07/13: 記事公開

参考

*1:2020年7月現在で 5.4.51, 4.19.132, 4.14.188, 4.9.230, 4.4.230

AutotoolsでMakefileを自動生成する

概要

独自のMakefileを用意している独自のソフトウェアに対して、AutotoolsによるMakefileの生成手順を確認した。

はじめに

Autotoolsはソフトウェアパッケージ開発ツールの一つである。

www.gnu.org

開発者は、configure.acMakefile.amに必要な情報を記載することで、configureと呼ばれるビルドに必要なライブラリのチェックやビルドに必要なファイルを自動生成するスクリプトファイルを生成することができる。

しかし、記述方式や生成方法が少々トリッキーなところもあり、一からautotoolsに必要なファイルを用意するとなると手間がかかる。 *1 幸いにも、Autotoolsを採用しているパッケージは多く存在するので、それらを参考にすることでとりあえず動作させることはできる。

本記事では、備忘録としてAutotoolsを使用するにあたって参考となるサイトの紹介と必要最低限度の手順を紹介する。

準備

今回は小規模ソフトウェアパッケージを想定として、Autotools の使い方をおさらいする。 説明を簡単にするために、対象を下記のようなディレクトリ構成から成るソフトウェアを対象とする。

Autotools導入予定のプロジェクトのディレクトリツリー

上記のソフトウェアはGitHub上に公開してあるのでそちらを参照すると理解しやすいと思う。 (Autotoolsを導入するまでの手順とコミットを対応させて説明しているため)

github.com

このソフトウェアは、プログラムソースコードマニュアルページ多言語翻訳リソースの三つから構成される。

  • プログラムソースコード: includesrcの配下にCプログラムファイルとヘッダファイルを管理する。 main.cがプログラムのメイン部分となっており、ここからsub.cにある関数を呼び出す。
  • マニュアルページ: manの配下にマニュアルページを管理する。
  • 多言語翻訳リソース: poの配下に翻訳可能な文字列と日本語の対応関係を管理する。 gettextを利用してローカライズする。

本記事では、このソフトウェアが下記の3つの要件を達成することを目標とする。

  1. ソースコードがビルドするためのMakefileを生成できること
  2. マニュアルページをインストールできるMakefileを生成できること
  3. gettextでローカライズが可能なMakefileを生成できること

また、今回は下記ツールのバージョンで確認している。

  • GNU automake 1.16.1
  • GNU gettext-tools 0.19.8.1

手順

CプログラムのソースコードをビルドできるMakefileの生成

github.com

  1. パッケージのトップディレクトリにMakefile.amを作成する。

     bin_PROGRAMS = test
     test_SOURCES = src/main.c src/sub.c
     AM_CPPFLAGS = -I$(top_srcdir)/include -DLOCALEDIR='"$(localedir)"' -DPACKAGE='"test"'
    

    bin_PROGRAMS = ターゲット: コンパイルによって生成されるバイナリファイルを指定する。
    ターゲット_SOURCE = ソースファイル: ターゲットを生成するために必要なファイルを指定する。 AM_CPPFLAGS = オプション: コンパイラに渡すオプションを指定する。

  2. autoscanコマンドを実行して、configure.scanファイルを生成する。

     $ autoscan
    
  3. configure.scanファイルをconfigure.acにリネームする。

     $  mv configure.scan configure.ac
    
  4. configure.acファイルを必要最低限の修正する。(末尾に「★」を付けた行が対象)

     #                                               -*- Autoconf -*-
     # Process this file with autoconf to produce a configure script.
    
     AC_PREREQ([2.69])
     AC_INIT([test], [1.0], [starbow.duster@gmail.com])    ★ パッケージ名, バージョン, バグレポート用アドレスを記入
     AM_INIT_AUTOMAKE([foreign subdir-objects])            ★ 不要ファイルの自動生成を防ぐforeignとサブディレクトリに対応するためのsubdir-objectsを追加
     AC_CONFIG_SRCDIR([config.h.in])                       ★ ソースコードのディレクトリをconfig.h.inに変更しておくとrenameされた場合にも対応できる
     AC_CONFIG_HEADERS([config.h])
    
     # Checks for programs.
     AC_PROG_CC
    
     # Checks for libraries.
    
     # Checks for header files.
     AC_CHECK_HEADERS([libintl.h locale.h])
    
     # Checks for typedefs, structures, and compiler characteristics.
    
     # Checks for library functions.
     AC_CHECK_FUNCS([setlocale])
    
     AC_CONFIG_FILES([Makefile])
     AC_OUTPUT    
    
  5. プリプロセッサ用のヘッダファイルの生成する。

     $ autoheader
    
  6. automake用のm4ファイルを生成する。

     $ aclocal
    
  7. Makefile.inを生成する。

     $ automake --add-missing
    
  8. configureスクリプトを生成する。

     $ autoconf
    
  9. Makefileの生成・ソフトウェアのビルド・パッケージのインストールできることを確認する。

     $ ./configure
     $ make
     $ make install
    

マニュアルページをインストールできるMakefileを生成

github.com

「CプログラムのソースコードをビルドできるMakefileの生成」との差分のみ説明する。

  1. Makefile.amファイルに以下を追記する。

     man_MANS = man/test.1
    

    man_MANS = ターゲット: マニュアルページのパスを指定する。

gettextでローカライズが可能なMakefileを生成

github.com

「CプログラムのソースコードをビルドできるMakefileの生成」との差分のみ説明する。

  1. configure.acファイルを修正する。(末尾に「★」を付けた行が対象)

    #                                               -*- Autoconf -*-
    # Process this file with autoconf to produce a configure script.
    
    AC_PREREQ([2.69])
    AC_INIT([test], [1.0], [starbow.duster@gmail.com])    
    AM_INIT_AUTOMAKE([foreign subdir-objects])            
    AC_CONFIG_SRCDIR([config.h.in])
    AC_CONFIG_HEADERS([config.h])
    AM_GNU_GETTEXT([external])                                ★ 出力ファイルを自動生成する
    AM_GNU_GETTEXT_VERSION(0.19)                              ★ gettextのバージョンを指定する
    
    # Checks for programs.
    AC_PROG_CC
    
    # Checks for libraries.
    
    # Checks for header files.
    AC_CHECK_HEADERS([libintl.h locale.h])
    
    # Checks for typedefs, structures, and compiler characteristics.
    
    # Checks for library functions.
    AC_CHECK_FUNCS([setlocale])
    
    AC_CONFIG_FILES([po/Makefile.in Makefile])               ★ po/Makefile.inも生成する
    AC_OUTPUT    
    
  2. gettextの基礎となるファイルを生成する。

     $ autopoint
    
  3. Makevars.templateをコピーしてMakevarsを作成する。

     $ cp po/Makevars.template po/Makevars
    
  4. 利用可能な言語を記述するファイルpo/LINGUASを作成する。

     # Set of available languages
     ja
    
  5. 翻訳が必要なファイルを記述するファイルpo/POTFILES.inを作成する。

     # List of source files which contain translatable strings.
     src/main.c
    
  6. 翻訳が必要なファイルを記述するファイルMakefile.amを作成する。

     SUBDIRS = po             ★ 対象としてpo以下も適応する
     bin_PROGRAMS = test
     man_MANS = man/test.1
     test_SOURCES = src/main.c src/sub.c include/func.h
     AM_CPPFLAGS = -I$(top_srcdir)/include
    
  7. 下記のコマンドを実行して、Makefileファイルを生成する。

     $ autoheader
     $ aclocal
     $ automake --add-missing
     $ autoconf
     $ ./configure
    
  8. 多言語翻訳用テンプレートのpo/test.potを生成する。(testの部分はバイナリ名と一致する)

     $ make
    
  9. 多言語翻訳用テンプレートのpo/test.potから日本語翻訳ファイルja.poを新規作成する。

     $ cp po/test.pot po/ja.po
    
  10. 日本語翻訳ファイルja.poを修正する。(末尾に「★」を付けた行が対象)

    # SOME DESCRIPTIVE TITLE.
    # Copyright (C) YEAR Free Software Foundation, Inc.
    # This file is distributed under the same license as the test package.
    # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
    #
    #, fuzzy
    msgid ""
    msgstr ""
    "Project-Id-Version: test 1.0\n"
    "Report-Msgid-Bugs-To: starbow.duster@gmail.com\n"
    "POT-Creation-Date: 2020-06-27 01:09+0900\n"
    "PO-Revision-Date: 2020-06-27 01:10+0900\n"                      ★ このファイルの更新日時を記入する
    "Last-Translator: LeavaTail <starbow.duster@gmail.com>\n"        ★ このファイルを編集した人を記入する
    "Language-Team: ja\n"                                            ★ 翻訳後の言語担当チームを指定する
    "Language: ja\n"                                                 ★ 翻訳後の言語を指定する
    "MIME-Version: 1.0\n" 
    "Content-Type: text/plain; charset=UTF-8\n"                      ★ 文字コード(UTF-8が妥当)を指定する
    "Content-Transfer-Encoding: 8bit\n"
    
    #: src/main.c:14
    msgid "Hello,World!\n"
    msgstr "こんにちは、世界!\n"                                      ★ 日本語翻訳を記述する
    
  11. 多言語翻訳リソースの更新とコンパイル

    $  make -C po/ update-po
    

注意: make installでパッケージのインストールまでしないと、ローカライズされない。

補足

手順の効率化

autoreconfを使う

ここまで手順を紹介してきたが、その中でたくさんのコマンドが登場してきた。 configure.acMakefile.amからconfigureを生成するのに下記の4つのコマンドが必要になる。

  1. autoheader: configure.acからプリプロセッサ用のヘッダファイルの生成する。
  2. aclocal: configure.acからautomake用のm4を生成する。
  3. automake: Makefile.amなどからMakefile.inを生成する。
  4. autoconf: configure.acなどからconfigureを生成する。

autoreconfはこれらの処理を自動的に実行してくれるコマンドである。

OSSライセンスについて

OSS利用者にとって一番気を付けなければいけないことがOSSライセンスである。

  • GNU automakeはGPLv2+
  • GNU AutoconfはGPLv3
  • GNU gettext-toolsはGPLv3+

調べてみると、このあたりを言及している記事はいくつか見つけることができた。
それらによると、GNU automakeとGNU Autoconfを利用することに関しては、強制的にGPLライセンスにされることはないらしい。

どういうことかというと、autotoolsによって生成されるのは全てテキスト(スクリプト)ファイルであり、そこにはライセンス条項がきちんと記載されている。そう、例のGPL文が載っている…のに続いて、As a special exception...というパラグラフがある。

As a special exception to the GNU General Public License, if you distribute this file as part of a program that contains a configuration script generated by Autoconf, you may include it under the same distribution terms that you use for the rest of that program.

趣旨としては、autoconfで自動生成された*1ものであり、かつ、配布物の一部として含まれる場合は、ライセンスはGPLにしなくてもいいよということみたいだ。これなら、会社とかプロプラな組織でも安心してautotoolsが使えるね! https://kuenishi.hatenadiary.jp/entry/20080303/1204474761

一方でgettext-toolsを利用することに関しては、強制的にGPLライセンスにされると思われる。
以下のようなGPL違反の問題があげられている。

各国語対応のため,gettextパッケージ(GPL-2ライセンス)のソースコードの一部(libintl)を利用したにもかかわらず,該当の配布物(ライセンスを明確にしていないソースコード・非GPLソースコード,およびバイナリのみ公開のライブラリを含む)がGPLでの配布ではなかった https://gihyo.jp/news/report/2019/03/0401

注意: あくまでどちらも私の理解を記述しているだけなので、利用時にはライセンス表記やライセンサーに問い合わせることを推奨する。

おわりに

本記事では、小規模ソフトウェアパッケージを想定として、Autotoolsの手順を紹介した。

今回は、「ソースコードがビルドできる」「マニュアルページをインストールできる」「gettextでローカライズが可能」を目的としたため、最小限のconfigure.acMakefile.amのパラメータを変更した。 しかし、これらのファイルには多数のパラメータが存在しており、さまざまな状況下でも対応することができる。

実際に中/大規模のプロジェクトでAutotoolsを利用するときには、automakeの公式ページや他プロジェクトを確認しておくとよいだろう。

変更履歴

  • 2020/06/27: 記事公開

参考

Automake関連

Gettext関連

OSSライセンス関連

*1:cmakeやMesonなど別のツールに乗り換えるのも手ではあるが

QEMUでx86_64用Linuxカーネルを起動する

関連記事

概要

x86_64用にLinuxカーネルをビルド、BuildRootで構築したルートファイルシステムを用意する。
また、QEMUでこれらをバイナリを動かし、ルートファイルシステムがマウントできることを確認した。

はじめに

x86Intelが開発したマイクロプロセッサの命令セットアーキテクチャであり、x86_64x86を64ビットに拡張した命令セットアーキテクチャである。
x86はパーソナルコンピュータやサーバなど幅広く使われている。

一方、プロセッサエミュレータQEMUは、ブートローダの再設定などせずにカーネルramdiskを直接ロードすることができる。 そこで、プロセッサエミュレータでもあるQEMUを用いてx86_64用にビルドされたLinuxカーネルを動かす方法を解説する。

本記事では、以下の動作をする環境を目指す。

カーネル起動ワークフローとメモリマップイメージ図

環境構成

ホスト環境x86_64アーキテクチャに構築する。

実施環境

起動対象のLinuxカーネルは、ホストOS(Ubuntu 18.04)上にインストールしたQEMUから動作させる。 また、今回はクロスコンパイラ環境をDockerで構築した。

ルートファイルシステムとして、Buildrootで生成したルートファイルシステムの非圧縮のcpio形式のアーカイブを利用する。

本記事は、下記の環境とソフトウェアバージョンに基づいて説明する。

環境 パラメータ
ホスト環境 x86_64
ホストOS Ubuntu 20.04
Buildroot buildroot-2020.02.8
QEMU QEMU emulator version 4.2.1
linux 5.7
Docker version 19.03.13
Docker image ubuntu:20.04

ロスコンパイラ環境の構築手順

  1. ホスト環境にDockerをインストールする。 docs.docker.com

  2. ロスコンパイラ環境を構築する。(作成が億劫な人は、著者自作Dockerfile(https://github.com/LeavaTail/kernel-build)を使用してほしい)

     leava@ubuntu-bionic:~$ docker run --rm --name=kbuild -h "kbuild" -v /srv:/work -it ubuntu:20.04 /bin/bash 
    
  3. コンテナ環境下にカーネルのビルドに必要なパッケージをインストールする。(今回はUbuntuコンテナを利用する)

     root@kbuild:/# apt install git bc bison flex libssl-dev make libncurses-dev libelf-dev file wget cpio unzip rsync build-essential
    
  4. カーネルソースの取得

     root@kbuild:/# cd /work
     root@kbuild:/work# git clone https://github.com/torvalds/linux.git
     root@kbuild:/work# cd linux
     root@kbuild:/work/linux# git checkout -b v5.7 refs/tags/v5.7
    
  5. ロスコンパイラ環境用の環境変数を設定する。

     root@kbuild:/work/linux# export ARCH="x86"
    
  6. x86Linuxカーネル用のコンフィグを生成する。

     root@kbuild:/work/linux# make x86_64_defconfig
    
  7. カーネルをビルドする。

     root@kbuild:/work/linux# make -j `getconf _NPROCESSORS_ONLN` bzImage
    

ルートファイルシステムの構築

Buildrootを用いてrootfsを構築する。

  1. Buildrootをインターネットからダウンロード、ファイルを解凍する。

     leava@ubuntu-bionic:~$ wget https://buildroot.org/downloads/buildroot-2020.02.8.tar.gz
     leava@ubuntu-bionic:~$ tar zxvf  buildroot-2020.02.8.tar.gz && cd buildroot-2020.02.8
    
  2. x86_64専用のデフォルトコンフィグqemu_x86_64_defconfigを利用する。

     leava@ubuntu-bionic:~/buildroot-2020.02.8$ make qemu_x86_64_defconfig
    
  3. Buildrootのビルド

     leava@ubuntu-bionic:~/buildroot-2020.02.8$ make
    
  4. ルートファイルシステムの確認

     leava@ubuntu-bionic:~/buildroot-2020.02.8$ ls -l output/images/rootfs.ext4
     lrwxrwxrwx 1 root root 11 Sep  8 15:45 output/images/rootfs.ext4 -> rootfs.ext2
    

カーネルの起動

  1. x86QEMUをインストールする。

     leava@ubuntu-bionic:~$ sudo apt install qemu-system-x86
    
  2. 作成したカーネルQEMUで実行する。

     leava@ubuntu-bionic:~$ qemu-system-x86_64 \
         -kernel /srv/linux/arch/x86/boot/bzImage \
         -drive file=/srv/buildroot-2020.02.8/output/images/rootfs.ext2,if=ide,format=raw
         -nographic \
         -append "root=/dev/sda console=ttyS0"
    

おわりに

本記事では、QEMUx86_64用Linuxカーネルを起動させる手順を説明した。
Buildrootで構築した場合、自動でセットアップしてくれるため非常に使いやすく便利である。

変更履歴

  • 2020/05/26: 記事公開
  • 2020/12/11: ブログタイトルを "x86_64用" に訂正
  • 2020/12/14: GDBスタブの手順を削除

参考

QEMULinuxカーネルを起動する

linuxカーネルデバッグ

Raspberry Pi 3 Model B をネットワークブートで起動させる (U-Boot編)

関連記事

概要

Raspberry Pi 3 Model B からSDカードに保存したU-Boot (32bit) を起動し、メインラインカーネル (32bit) をネットワークブートするための環境を構築する方法を解説する。

Raspberry Pi 3Bのネットワークブート概要図 (U-boot)

Raspberry Pi 3B にはU-Bootのバイナリを格納したSDカードを差し込み、U-Bootを利用してネットワーク上にあるネットワークサーバからブートイメージとルートファイルシステムを取得する。
ネットワークブート用のサーバはUbuntu 18.04 LTS上にdnsmasqとnfs-kernel-serverをインストールし、メインラインカーネル (32bit) *1をビルドしておく。

はじめに

組込みシステムの開発段階やデバッグ段階では、ネットワーク経由でシステムを起動(ネットワークブート)できることが理想的である。
ネットワークブートができる環境が構築されていれば、組込みシステムのファームウェア再書き込みせずにデータを共有することができる。

Raspberry PiARMプロセッサを搭載したシングルボードコンピュータの一つで、手軽に入手できる点や世の中に情報が多い点から組込みシステムの入門として用いられることが多い。

しかし、Raspberry Pi 3B のROMには幾つかの不具合が報告されており、ROMによるネットワークブートが安定しない。

一方で、Raspberry Pi 3B のROMから別のブートローダをロードし、そのブートローダからネットワークブートする手法が存在する。 Das U-Boot(U-Boot)は組込み系で広く利用されているブートローダの一つであり、Raspberry Pi 3B でも利用することができる。

U-Bootには機能として、簡易のコマンドやドライバが組み込まれており、TFTPを利用してネットワークブートすることができる。

U-Bootでネットワークブートを用いたRaspberry Pi 3Bの起動シーケンスを下記に示す。

Raspberry Pi 3Bの起動シーケンス

  1. Raspberry Pi 3Bに電源投入すると、GPUが起動する。
  2. GPUは1段目のブートローダを起動。One-Time Programmable (OTP) メモリのブートモードに従ったメディア(今回はSD card)から、2段目のブートローダbootcode.binを取得する。
  3. bootcode.binを実行する。
  4. bootcode.binSDRAMを有効化する。
  5. bootcode.binファームウェアstart.elfを読み込む。
  6. start.elfを実行する。
  7. start.elfconfig.txtを指定されたメディア(今回はSD card)から取得する。
  8. start.elfcmdline.txtを指定されたメディア(今回はSD card)から取得する。
  9. start.elfconfig.txtのkernelパラメータで指定されたu-boot.binを指定されたメディア(今回はSD card)から取得する。
  10. u-bootを実行する。
  11. u-bootTFTP経由でzImageを取得する。
  12. 取得したzImageSDRAMの指定場所(${kernel_addr_r})に読み込む。
  13. u-bootTFTP経由でbcm2837-rpi-3-b.dtbを取得する。
  14. 取得したbcm2837-rpi-3-b.dtbSDRAMの指定場所(${fdt_addr_r})に読み込む。
  15. ARM coreにリセット指令を発行し、${kernel_addr_r}番地からプログラムを実行する。

下記は、Raspberry Pi 3Bが起動するまでに必要となるファイル群である。

ファイル名 格納先 概要
config.txt SD card システム構成パラメータ
cmdline.txt SD card カーネルコマンドライン(もしかしたら不要)
start.elf SD card 一般的なRaspberry Piファームウェア
fixup.dat SD card start.elfのリンカファイル
bootcode.bin SD card SoCに備わっているブートローダ
zImage Server カーネルイメージ
bcm2710-rpi-3-b.dtb Server バイスリーファイル (bcm2837-rpi-3-b.dtb でも代用可能)

そこで本記事では、Raspberry Pi 3B でメインラインカーネルをネットワークブートするための環境を構築する方法を紹介する。

各手法における特徴

前回の記事では、Raspberry Pi 3Bをネットワークブートする手法としてROM内のファームウェアを利用した。 ネットワークブートのための手法はいくつか存在しているが、それぞれ特徴が異なる。

ROM内のファームウェアを利用した場合の特徴としては以下の点があげられる。

  • SDカード周りのデバッグやドライバの開発などに向いている。
  • 運用中はSDカードが不要なので、SDカードを用意するコストを最小限に抑えることができる。

一方、U-Bootを利用した場合の特徴としては以下の点があげられる。

  • 起動対象の差を設定によって吸収できる。
  • (Raspberry Pi 3B の場合) 不具合の影響が小さい。
  • U-Bootに備わっている簡易コマンドを利用することで、別プログラムをロードしたり多段ブートすることを容易である。

実行環境

実験環境は前回構築した環境をそのまま使用する。

ネットワーク構成図

ネットワークブートサーバの詳細は下記のとおりである。

名前 詳細
OS Ubuntu 18.04 LTS
Kernel 5.3.0-40-generic
NIC (ホームセグメント側) eth0
NIC (開発側セグメント) eth1
IPアドレス (eth0) 192.168.1.11
IPアドレス (eth1) 172.16.1.1
ホスト名 server
Raspberry Pi 3Bのブートイメージ格納予定 /srv/boot
Raspberry Pi 3Bのルートファイルシステム格納予定 /srv/rootfs
シリアルデバイスファイル /dev/ttyUSB0
シリアルポートのボーレート 115200

また、使用するRaspberry Pi 3Bの詳細は下記のとおりである。

名前 詳細
ファームウェア 4.19.97-v7+
NIC eth0
IPアドレス 172.16.1.2 (DHCP)
ホスト名 raspberry
ストレージ HIDISC microSDHCカード 8GB CLASS10 UHS-1対応
バイスファイル /dev/sdb1

目標とする環境

本記事が目標とするネットワークブートの構成は下記のとおりである。

ネットワークブートの動作イメージ

Raspberry Pi 3Bに挿入してあるU-Boot(32bit)がネットワークブートを担うこと」と「起動するカーネルはメインラインカーネルのARM(32bit)」以外は前回の記事と同様のことを目指す。

今回使用したソフトウェアのバージョンは下記のとおりである。

名前 バージョン
U-Boot v2020.07-rc1
mainline kernel v5.7-rc3
dnsmasq v2.79-1
nfs-common 1:1.3.4-2.1ubuntu5.2

Raspberry Pi 3Bの設定

ネットワークブートサーバの設定は、システム構成パラメータの更新U-Bootの構築の2ステップ必要になる。

システム構成パラメータの更新

  1. config.txtから読み込むカーネルファイルをU-Bootに変更する。

     user@server:~$ mount /dev/sdb1 /mnt/tmp
     user@server:~$ echo 'kernel=u-boot.bin' > /mnt/tmp/config.txt
     user@server:~$ umount /mnt/tmp
    

U-Bootの構築

今回の手順では、別のマシン(ネットワークブートサーバ)でU-Bootをビルドし、バイナリをSDカードにコピーする。

  1. リポジトリをダウンロードする。

      user@server:~$ git clone git://git.denx.de/u-boot.git
      user@server:~$ cd u-boot
    
  2. ARMクロスコンパイラ(32bit)をインストールする。

      user@server:~$ apt-get install gcc-arm-linux-gnueabi
    
  3. Raspberry Pi 3B (32bit) 用の.configファイルを生成する。

      user@server:~$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- USE_PRIVATE_LIBGCC=yes rpi_3_32b_defconfig
    
  4. U-Bootをビルドする。

      user@server:~$ make
    
  5. 生成したバイナリ(u-boot.bin)をSDカードにコピーする。

      user@server:~$ mount /dev/sdb1 /mnt/tmp
      user@server:~$ cp u-boot.bin /mnt/tmp/
      user@server:~$ umount /mnt/tmp
    

ネットワークブートサーバの設定

ネットワークブートサーバの設定は、ブートイメージの作成ルートファイルシステムの取得DHCP/TFTPサーバの構築NFSサーバの構築の4ステップ必要になる。

ブートイメージの作成以外は、前回の記事で構築した環境をそのまま利用する。

ブートイメージの作成

  1. メインラインカーネルをビルドするのに必要なパッケージをインストールする。

      user@server:~$ sudo apt install git bc bison flex libssl-dev make
    
  2. mainlineのkernelのソースコードをクローンする。

      user@server:~$ git clone --depth=1 https://github.com/torvalds/linux.git
      user@server:~$ cd linux
    
  3. ARM用の.configファイルを生成する。

      user@server:~$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- defconfig
    
  4. カーネルをビルドする。

      user@server:~$ make -j$(nproc) ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- zImage modules dtbs
    
  5. 生成したカーネルイメージとDevice Tree blobsをtftpサーバの公開ディレクトリにコピーする。

      user@server:~$ sudo cp arch/arm/boot/dts/*.dtb /srv/tftpboot/
      user@server:~$ sudo cp arch/arm/boot/zImage /srv/tftpboot/
    

実行結果

  1. minicom経由でアクセスする。

     U-Boot 2020.04-00687-gd16d37bcd4 (Apr 29 2020 - 09:27:20 +0000)
    
     DRAM:  948 MiB
     RPI 3 Model B (0xa32082)
     MMC:   mmc@7e202000: 0, sdhci@7e300000: 1
     Loading Environment from FAT... *** Warning - bad CRC, using default environment
    
     In:    serial
     Out:   vidconsole
     Err:   vidconsole
     Net:   No ethernet found.
     starting USB...
     Bus usb@7e980000: scanning bus usb@7e980000 for devices... 3 USB Device(s) found
            scanning usb for storage devices... 0 Storage Device(s) found
    
  2. Device Tree blobsをメモリ上に展開する。

     U-Boot> dhcp ${fdt_addr_r} ${fdtfile}
     Waiting for Ethernet connection... done.
     BOOTP broadcast 1
     DHCP client bound to address 172.16.1.2 (6 ms)
     Using smsc95xx_eth device
     TFTP from server 172.16.1.1 our IP address is 172.16.1.2
     Filename 'bcm2837-rpi-3-b.dtb'.
     Load address: 0x2600000
     Loading: ##################################################  13.8 KiB
              1 MiB/s
     done
    
  3. カーネルイメージをメモリ上に展開する。

     U-Boot> tftp ${kernel_addr_r} zImage
     Waiting for Ethernet connection... done.
     Using smsc95xx_eth device
     TFTP from server 172.16.1.1; our IP address is 172.16.1.2
     Filename 'zImage'.
     Load address: 0x80000
     Loading: ##################################################  9.3 MiB
              2.7 MiB/s
     done
    
  4. カーネルパラメータを設定する。

     U-Boot> setenv bootargs root=/dev/nfs nfsroot=${serverip}:/srv/rootfs,vers=3,proto=tcp rw rootwait ip=${ipaddr}:${serverip}:${gatewayip}:${netmask}::eth0:off nfsrootdebug
    
  5. 指定番地にあるカーネルを起動させる。

     U-Boot> bootz ${kernel_addr_r} - ${fdt_addr_r}                                                                                                                            
     Kernel image @ 0x080000 [ 0x000000 - 0x953200 ]
     ## Flattened Device Tree blob at 02600000
        Booting using the fdt blob at 0x2600000
        Using Device Tree in place at 02600000, end 02606725
    
     Starting kernel ...
    
     <<< snip >>>
    
     Raspbian GNU/Linux 10 raspberrypi ttyS0
    
     raspberrypi login: 
    

おわりに

本記事では、U-Bootを利用してRaspberry Pi 3Bをネットワークブートする方法を紹介した。

U-Bootを利用したことで、Raspberry Pi 3Bで報告されている不具合を回避しながらも、SDカードの書き込み寿命の浪費を防ぐことができる。
U-Bootにはあらかじめスクリプトを書いておき、起動時に自動で実行する機構もあるので、そちらを利用するとより一層便利になる。

変更履歴

  • 2020/04/30: 記事公開
  • 2022/06/07: 章構成の修正

参考

SDカードを使わず、network bootでRaspberry Pi 3Bを起動する

SDカードにU-Bootのみ格納し、network bootでRaspberry Pi 3Bを起動する

SDカードを使わず、network bootでRaspberry Pi 3B+を起動する

Raspberry Pi の起動シーケンス

Raspberry Pi のブートで必要なファイル郡

U-Bootに関する説明資料

*1:Raspbianにあるカーネルはそのままだと起動できなかった

Raspberry Pi 3 Model B をネットワークブートで起動させる

関連記事

概要

Raspberry Pi 3 Model BからLinuxカーネル (Raspbian)をネットワークブートするための環境を構築する方法を解説する。

Raspberry Pi 3Bのネットワークブート概要図

Raspberry Pi 3Bは、SDカードを挿入せずにネットワーク上にあるネットワークサーバからブートイメージとルートファイルシステムを取得する。
また、ネットワークブート用のサーバとして、Ubuntu 18.04 LTS上にdnsmasqとnfs-kernel-serverをインストールした。

はじめに

組込みシステムの開発段階やデバッグ段階では、ネットワーク経由でシステムを起動(ネットワークブート)できることが理想的である。
ネットワークブートができる環境が構築されていれば、組込みシステムのファームウェア再書き込みせずにデータを共有することができる。

Raspberry PiARMプロセッサを搭載したシングルボードコンピュータの一つで、手軽に入手できる点や世の中に情報が多い点から組込みシステムの入門として用いられることが多い。

Raspberry Pi 3Bについて

Raspberry Pi 3 Model B(Raspberry Pi 3B)は複数のブートモードがある。下記は、Raspberry Pi 3Bの起動シーケンスを示している。

Raspberry Pi 3Bの起動シーケンス

Raspberry Pi 3B では、電源投入されるとOne-Time Programmable (OTP) メモリをロードする。
OTPメモリにはブートモードを決定するフラグが保存されており、Raspberry Pi 3B のGPUはこのフラグを基にブートを試みる。

各ブートモードは、ブートに必要なファイル (ブートイメージ) をそれぞれの方法でメモリにロードする。

Raspberry Pi 3B のブートイメージは下記のファイルがある。

状態 概要
config.txt システム構成パラメータ
start.elf 一般的なRaspberry Piファームウェア
fixup.dat start.elfのリンカファイル
bcm2710-rpi-3-b.dtb ハードウェアの構成情報(デバイスリーファイル)
cmdline.txt カーネルコマンドライン
kernel7.img カーネルイメージ
bootcode.bin ブートローダ

Raspberry Pi 3Bのネットワークブートでは、TFTPでブートイメージを取得する。 ネットワークブートを用いたRaspberry Pi 3Bの起動シーケンスを下記に示す。

ネットワークブートのシーケンス

  1. Raspberry Pi 3BではオンボードEthernetドライバの初期化をした後、LAN内にDHCPリクエスをブロードキャストする。
  2. DHCPリクエストを受け取ったネットワークブートサーバは、TFTPサーバのIPアドレスを送信する。
  3. Raspberry Pi 3Bは、TFTPサーバのIPアドレスからブートイメージを取得し、カーネルをブートする。

しかし、Raspberry Pi 3Bのネットワークブートには幾つかのバグが報告されている。

Known problems

  • DHCP requests time out after five tries
  • TFTP server on separate subnet not supported
  • DHCP relay broken
  • Raspberry Pi Boot string
  • DHCP UUID constant
  • ARP check can fail to respond in the middle of TFTP transaction
  • DHCP request/reply/ack sequence not correctly implemented

このため、Raspberry Pi 3Bでネットワークブートを試している人は少ない。*1 そこで本記事では、Raspberry Pi 3BからRaspbianをネットワークブートするための環境を構築する方法を紹介する。

実行環境

実験環境として、セグメントが二つに分かれている下記のネットワーク構成のものを使用する。

ネットワーク構成図

ホームセグメントは、一般的なPCや家電製品などが接続されており家庭用ルータによって管理されている。 開発用セグメントは、Raspberry Pi 3B が接続されておりネットワークブートサーバによって管理されている。

本運用では、ホームセグメントにあるクライアントPC (Windows 10 Home) からネットワークブートサーバ(Ubuntu 18.04 LTS) にSSH経由でアクセスする。 また、ネットワークブートサーバとRaspberry Pi 3Bはシリアル接続されている。

ネットワークブートサーバの詳細は下記のとおりである。

名前 詳細
OS Ubuntu 18.04 LTS
Kernel 5.3.0-40-generic
NIC (ホームセグメント側) eth0
NIC (開発側セグメント) eth1
IPアドレス (eth0) 192.168.1.11
IPアドレス (eth1) 172.16.1.1
ホスト名 server
Raspberry Pi 3Bのブートイメージ格納予定 /srv/boot
Raspberry Pi 3Bのルートファイルシステム格納予定 /srv/rootfs
シリアルデバイスファイル /dev/ttyUSB0
シリアルポートのボーレート 115200

また、使用するRaspberry Pi 3Bの詳細は下記のとおりである。

名前 詳細
ファームウェア 4.19.97-v7+
NIC eth0
IPアドレス 172.16.1.2 (DHCP)
ホスト名 raspberry
ストレージ なし

Raspberry Pi 3Bの初期セットアップ

本記事が目標とするネットワークブートの構成は下記のとおりである。

ネットワークブートのフロー図

Raspberry Pi 3BのUSBブートフラグをONにし、ネットワークブートで起動できるように設定をする。

  • 注意: USBブートフラグをONにするために、Raspbianの入ったSDカードが必要になる。

ネットワークブートサーバには、DockerコンテナとしてDHCP/TFTPサーバを構築しブートイメージを格納しておく。
また、ホストにNFSサーバを構築しルートファイルシステムを格納しておく。

今回は、Raspbian Buster Liteのブートイメージとルートファイルシステムを利用する。

この状態で、SDカードからRaspbianを起動する

  1. 現在のブートフラグを確認する (OPT bitより、Bit29がネットワークブートのフラグとなっている)

     pi@raspberry:~$ vcgencmd otp_dump | grep 17:
     > 17:1020000a    # 現在はOFFになっている
    
  2. /boot/config.txtを修正する

    pi@raspberry:~$ echo "program_usb_boot_mode=1" | sudo tee -a /boot/config.txt 
    
  3. 再起動する

     pi@raspberry:~$ sudo reboot
    
  4. 再度、ブートフラグを確認する

     pi@raspberry:~$ vcgencmd otp_dump | grep 17:
     > 17:3020000a    # 現在はONになっている
    

上記の設定により、OTPメモリにUSBブートフラグが設定された。
これ以降ネットワークブートが可能になったので、Raspberry Pi 3BからSDカードを抜いておく

ネットワークブートサーバの設定

ネットワークブートサーバの設定は、「ブートイメージとルートファイルシステムの取得」と「DHCP/TFTPサーバの構築」、「NFSサーバの構築」の3ステップ必要になる。

ブートイメージとルートファイルシステムの取得

  1. Raspbianのイメージを取得する

     user@server:~$ wget https://downloads.raspberrypi.org/raspbian_lite/images/raspbian_lite-2020-02-14/2020-02-13-raspbian-buster-lite.zip
     user@server:~$ unzip 2020-02-13-raspbian-buster-lite.zip
    
  2. 取得したディスクイメージをマウントする

     user@server:~$ sudo kpartx -a 2020-02-13-raspbian-buster-lite.img 
     user@server:~$ sudo mount /dev/mapper/loop0p1 /mnt/boot
     user@server:~$ sudo mount /dev/mapper/loop0p2 /mnt/rootfs
    
  3. ディスクイメージをTFTPサーバとNFSサーバの公開先ディレクトリにコピーする

     user@server:~$ sudo rsync -av /mnt/boot /srv/tftpboot/
     user@server:~$ sudo rsync -av /mnt/rootfs /srv/rootfs
    
  4. ディスクイメージをアンマウントする

     user@server:~$ sudo umount /mnt/rootfs
     user@server:~$ sudo umount /mnt/boot
     user@server:~$ sudo kpartx -d 2020-02-13-raspbian-buster-lite.img 
    
  5. カーネルコマンドラインを修正し、NFSルートとシリアルコンソールを設定する

     user@server:~$ sudo mv /srv/tftpboot/cmdline.txt /srv/tftpboot/cmdline.txt.old
     user@server:~$ echo "console=serial0,115200 console=tty1 root=/dev/nfs nfsroot=172.16.1.1:/srv/rootfs,vers=3,proto=tcp rw ip=dhcp rootwait elevator=deadline" | sudo tee /srv/tftpboot/cmdline.txt 
    
  6. UARTを有効にする

     user@server:~$ echo "enable_uart=1" | sudo tee -a /srv/tftpboot/config.txt 
    
  7. Raspberry Pi 3B側のファイルシステムテーブルを修正する

     user@server:~$ sudo mv /srv/rootfs/etc/fstab /srv/rootfs/etc/fstab.old
     user@server:~$ echo "proc            /proc           proc    defaults          0       0" | sudo tee /srv/rootfs/etc/fstab 
    

DHCP/TFTPサーバの構築

著者が使用しているDockerイメージはGitHubにて公開しているので、構築が手間な方はそちらを使用してほしい。

  1. ベースとして使用するAlpine Linuxのイメージを取得する

     user@server:~$ docker pull alpine:3.11.3
    
  2. Dockerコンテナを起動させる

     user@server:~$ docker run --privileged --net=host -v /srv/tftpboot:/srv  -it alpine:3.11.3 /bin/sh
    
  3. dnsmasqをインストールする

     / # apk update
     / # apk add dnsmasq
    
  4. dnsmasqの設定ファイルを修正する (修正箇所は下記のとおり)

     @@ -10 +10 @@
     -#port=5353
     +port=0
     @@ -21 +21 @@
     -#bogus-priv
     +bogus-priv
     @@ -106 +106 @@
     -#interface=
     +interface=eth1
     @@ -157 +157 @@
     -#dhcp-range=192.168.0.50,192.168.0.150,12h
     +dhcp-range=172.16.1.2, 172.16.1.65, 12h
     @@ -477 +477 @@
     -#pxe-service=x86PC, "Boot from local disk"
     +pxe-service=0, "Raspberry Pi Boot"
     @@ -499 +499 @@
     -#enable-tftp
     +enable-tftp
     @@ -502 +502 @@
     -#tftp-root=/var/ftpd
     +tftp-root=/srv
     @@ -664 +664 @@
     -#log-dhcp
     +log-dhcp
    
  5. dnsmasqを起動させる

     / # dnsmasq
    
  6. dockerコンテナからデタッチする

     / # <Ctrl-p> <Ctrl-q>
    

NFSサーバの構築

NFSサーバもDockerコンテナとして管理しても良いが、ディレクトリ共有のオーバーヘッドを懸念して、ホストに直接立てることにした。

  1. NFSサーバのnfs-kernel-serverをインストールする

     user@server:~$ sudo apt install nfs-kernel-server 
    
  2. NFSで公開するディレクトリを追加する

     user@server:~$ echo "/srv/rootfs  172.16.1.0/255.255.255.0(rw,sync,no_root_squash,no_subtree_check)" | sudo tee -a 
     /etc/exports 
     user@server:~$ sudo exportfs -ra
    

実行結果

minicom経由でアクセスする。

    user@server:~$ sudo minicom /dev/ttyUSB0 
    <--snip-->
    Raspbian GNU/Linux 10 raspberrypi ttyS0

    raspberrypi login: 

おわりに

本記事では、Raspberry Pi 3BにSDカードを挿入せずブートすることができるネットワークブートの方法を紹介した。
このように、バグが報告されているRaspberry Pi 3Bでもネットワークブートすることができた。

今回は、ネットワークブートサーバにdnsmasqとnfs-kernel-serverをインストールする手法を紹介したが、その他のパッケージ (isc-dhcp-serverやtftpd-hpa)を用いても構築することができる。
また、公開されているRaspbian Buster Liteのディスクイメージのルートファイルシステムをそのまま使用しているので、SSHホストキーの再生成は実施しておくとよい。

変更履歴

  • 2020/03/15: 記事公開
  • 2022/06/07: 章構成の修正

参考

SDカードを使わず、network bootでRaspberry Pi 3Bを起動する

SDカードにU-Bootのみ格納し、network bootでRaspberry Pi 3Bを起動する

SDカードを使わず、network bootでRaspberry Pi 3B+を起動する

Raspberry Pi の起動シーケンス

Raspberry Pi のブートで必要なファイル郡

付録

Dockerで使用したオプションは以下のとおりである。

オプション名 詳細 指定理由
--privileged コンテナを特権モードで動作させる DHCPリレーサービスで物理ノードを参照するため。
指定しないと、ARP-cache injection failed: Operation not permitted dockerとエラーになってしまう
--net=host ホスト側のネットワークスタックをコンテナに接続する ホスト側のネットワークセグメントでDHCPを公開する範囲を制限するため。
-v /srv/tftpboot:/srv ホスト側のブートイメージ格納先をマウントする コンテナ内のTFTPサービスからブートイメージを参照するため。

dnsmasqで指定したオプションは以下のとおりである。

パラメータ 詳細
port 0 DNS サーバーは不要なので無効化しておく
bogus-priv プライベートIPの逆引きは上位DNSに転送しない
interface eth1 開発用セグメントを指定する
dhcp-range 172.16.1.2, 172.16.1.65, 12h DHCPで払い出すIPアドレスの範囲を指定する
pxe-service Raspberry Pi Boot Raspberry Pi では「Raspberry Pi Boot」と指定されているパケットのみ受け付ける
enable-tftp TFTPサーバを有効化する
tftp-root /srv TFTPサーバが提供するルートディレクトリをコンテナ起動時にマウントしたディレクトリを指定する。
log-dhcp dhcp関連の詳細なログを出力する

*1:Raspberry Pi 3 Model B+ではバグが修正されているので、そちらで実施している人が多い