LeavaTailの日記

LeavaTailの日記

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

Linuxカーネルのファイルアクセスの処理を追いかける (2) System call

関連記事

概要

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

本章では、ksys_write関数からvfs_write関数を呼ぶところまでを確認した。

はじめに

ユーザプロセスからファイルシステムという機構によって記憶装置上のデータをファイルという形式で書き込み・読み込みすることができる。
本調査では、ユーザプロセスがファイルに書き込み要求を実行したときにLinuxカーネルではどのような処理が実行されるかを読み解いていく。

調査対象や環境などはPart 1: 環境セットアップを参照。

ファイルアクセスの一連の処理をシーケンスとしてあらわしたときに、赤枠の部分が該当する。

調査対象(シーケンス図)

本記事では、Syscall interfaceの途中 (SYSCALL_DEFINE)からVFSの関数を呼び出すところまでとする。
ここより上のレイヤー(Application部やsystem call interfaceの上位部)の解説は今回は省略する。

ユーザプログラム

下記の記事では、システムコールについて説明されている。 システムコールを一から学習したい人におすすめ。

www.kimullaa.com

システムコールハンドラ

writeシステムコールは、SYSCALL_DEFINEマクロによって定義される。

// 656:
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
        size_t, count)
{
    return ksys_write(fd, buf, count);
}

writeシステムコールの実態は、ksys_write()となっている。
ksys_write()の定義は下記のようになっている。

// 636:
ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count)
{
    struct fd f = fdget_pos(fd);
    ssize_t ret = -EBADF;

    if (f.file) {
        loff_t pos, *ppos = file_ppos(f.file);
        if (ppos) {
            pos = *ppos;
            ppos = &pos;
        }
        ret = vfs_write(f.file, buf, count, ppos);
        if (ret >= 0 && ppos)
            f.file->f_pos = pos;
        fdput_pos(f);
    }

    return ret;
}

ksys_write()には、大きく分けて3つ処理がある。

  1. 現在のプロセスが保有しているファイルディスクリプタテーブルからfd構造体を取得する
  2. ファイルの現在のオフセットを取得する
  3. VFSのwrite処理を呼び出す

ファイルディスクリプタからfd構造体を取得する

ファイル構造体を取得するまでの流れは下記のようなものとなっており、マルチスレッドであった場合に限りファイルディスクリプタテーブルにロックをかけるといった処理がされる。
ちなみに、FMODE_PATHO_PATHフラグを指定してファイルをopenしたときに付与されるフラグであり、その場合は書き込みを失敗させる。

ファイル構造体を取得するまでのフロー

file構造体の取り扱い方については、カーネルドキュメントが用意されているので気になる人はそちらを読むことを推奨する。

ksys_write関数で実行されるfdget_pos関数について定義を確認する。
fdget_pos関数は下記のような定義である。

// 73:
static inline struct fd fdget_pos(int fd)
{
    return __to_fd(__fdget_pos(fd));
}

fdget_pos関数の処理は単純で下記の2点のみである。

  • file構造体にflagsを付与して、fd構造体を呼び出し元に返す
  • __fdget_pos関数でファイルディスクリプタからfile構造体を取得する

まずは、__to_fd関数の定義から確認する。 __to_fd関数は下記のような定義である。

// 58:
static inline struct fd __to_fd(unsigned long v)
{
    return (struct fd){(struct file *)(v & ~3),v & 3};
}

引数として与えられたvから、下位2bitに格納されたフラグを抽出する。(アドレス空間は4bytes or 8bytesの管理されるので、file構造体のアドレスに影響はない)
上記のフラグと下位2bitをマスクしたfile構造体へのポインタをfd構造体に代入する。

ちなみにfd構造体の定義は下記の通りである。

// 36:
struct fd {
    struct file *file;
    unsigned int flags;
};

ここまでの__to_fd関数の流れのイメージを下記に示す。

file構造体のアドレスとfd構造体の対応

  1. __fdget_pos関数(後述)によって、ファイルディスクリプタからファイル構造体のアドレスを取得する
  2. __fdget_pos関数は、取得したアドレスをunsigned long型の変数vに代入し、下位2bitにフラグを代入する
  3. __to_fd関数は、変数vからfile構造体とflagsを取得して、fd構造体を返す

ファイルディスクリプタからfile構造体を取得する

fdget_pos関数は、 ファイルディスクリプタからfile構造体を取得するために__fdget_pos関数を呼び出す。

// 982:
unsigned long __fdget_pos(unsigned int fd)
{
    unsigned long v = __fdget(fd);
    struct file *file = (struct file *)(v & ~3);

    if (file && (file->f_mode & FMODE_ATOMIC_POS)) {
        if (file_count(file) > 1) {
            v |= FDPUT_POS_UNLOCK;
            mutex_lock(&file->f_pos_lock);
        }
    }
    return v;
}

__fdget_pos関数の処理は下記の2点のみである。

  • __fdget関数でファイルディスクリプタからfile構造体のアドレスを取得する (unsigned long型bに代入していたり、~3とAND演算しているのは前述の理由)
  • 同じファイルが複数openされている場合、オフセットのためのmutex_lockを取得する (取得したロックは、ksys_write関数の最後に実行しているfdput_pos関数で解放する)

まず、__fdget関数とその呼び出し先の__fget_light関数の定義を確認する。

// 971:
unsigned long __fdget(unsigned int fd)
{
    return __fget_light(fd, FMODE_PATH);
}
// 954:
static unsigned long __fget_light(unsigned int fd, fmode_t mask)
{
    struct files_struct *files = current->files;
    struct file *file;

    if (atomic_read(&files->count) == 1) {
        file = files_lookup_fd_raw(files, fd);
        if (!file || unlikely(file->f_mode & mask))
            return 0;
        return (unsigned long)file;
    } else {
        file = __fget(fd, mask, 1);
        if (!file)
            return 0;
        return FDPUT_FPUT | (unsigned long)file;
    }
}

__fget_light関数では、現在のプロセスのファイルディスクリプタテーブルを取得して、状況に応じてロックを取得する。

ここで、task_struct構造体からfile構造体までの関係性についておさらいする。

  • current変数は、現在のプロセスのtask_struct構造体を指す
  • task_struct構造体は、現在openしているファイルを管理するためにfiles_struct型のポインタを変数filesで管理する
  • files_struct構造体は、fdtable型の変数fdtでファイルディスクリプタのテーブルを管理する
  • fdtable構造体は、file構造体の配列を管理する。

これらの関係を図示したものが以下である。

ファイルディスクリプタからfile構造体を取得するまでの各構造体の関係性

__fget_light関数でも上記の規則と同様に、current変数からfiles_struct構造体を取得している。
しかし、シングルスレッドマルチスレッドの場合で、files_struct以降の取り扱いが変わる。

シングルスレッドの場合

シングルスレッドの場合 (つまり、atomic_read(&files->count) == 1) は、対象のファイルディスクリプタテーブルを保有しているは自分のみである。 そのため、それぞれの変数に対して、排他制御を意識する必要はない

files_lookup_fd_raw関数の定義は以下のようになっている。

// 83:
static inline struct file *files_lookup_fd_raw(struct files_struct *files, unsigned int fd)
{
    struct fdtable *fdt = rcu_dereference_raw(files->fdt);

    if (fd < fdt->max_fds) {
        fd = array_index_nospec(fd, fdt->max_fds);
        return rcu_dereference_raw(fdt->fd[fd]);
    }
    return NULL;
}

files_lookup_fd_raw関数は、Linuxカーネル v5.10まではfcheck_files関数という名前で定義されていた。
fcheck_files関数についての注意点は、カーネルドキュメントにも記載されている。

4 To look up the file structure given an fd, a reader must use either fcheck() or fcheck_files() APIs.
These take care of barrier requirements due to lock-free lookup. https://www.kernel.org/doc/Documentation/filesystems/files.txt

file構造体を取得した後に、そのファイルのopenフラグを確認する。 FMODE_PATHフラグが立っていた場合、ファイル操作は無効となっているので0を返す必要がある。

マルチスレッドの場合

マルチスレッドの場合 (つまり、atomic_read(&files->count) > 1) は、排他制御を意識する必要がある

__fget関数と呼び出し先の__fget_files関数の定義は以下のようになっている。

// 867:
static inline struct file *__fget(unsigned int fd, fmode_t mask,
                  unsigned int refs)
{
    return __fget_files(current->files, fd, mask, refs);
}
// 844:
static struct file *__fget_files(struct files_struct *files, unsigned int fd,
                 fmode_t mask, unsigned int refs)
{
    struct file *file;

    rcu_read_lock();
loop:
    file = files_lookup_fd_rcu(files, fd);
    if (file) {
        /* File object ref couldn't be taken.
        * dup2() atomicity guarantee is the reason
        * we loop to catch the new file (or NULL pointer)
        */
        if (file->f_mode & mask)
            file = NULL;
        else if (!get_file_rcu_many(file, refs))
            goto loop;
    }
    rcu_read_unlock();

    return file;
}

files_lookup_fd_rcu関数は、files_lookup_fd_raw関数(シングルスレッドの場合を参照)のラッパー関数である。

シングルスレッドの場合と大きく異なるのは、rcu_read_lock/rcu_read_unlock命令を発行している点である。

  • rcu_read_lock()プリエンプションを無効にする命令
  • rcu_read_unlock()プリエンプションを有効にする命令

コメントにも書いてあるが、dup2()が要因でファイルカウントがおかしくなることがあるため、file構造体の取得をループさせることがある。
file構造体の取得後にプリエンプションが発生して、別にfile構造体を操作されると整合性がとれなくなってしまう。

__fget_files関数ではこの関数内でFMODE_PATHフラグの確認もしている。 シングルスレッドの場合と同様に、上記のフラグが立っていたら0を返す。

また、マルチスレッドの場合にはFDPUT_FPUTフラグを立てる。

ファイルの現在のオフセットを取得する

file構造体には、現在のオフセットをpos変数によって管理している。

file_ppos関数は、file構造体のpos変数を返す関数である。 このとき、stream-likeのファイルの場合は、オフセットを持たないのでNULLを返す必要がある。

// 618:
static inline loff_t *file_ppos(struct file *file)
{
    return file->f_mode & FMODE_STREAM ? NULL : &file->f_pos;
}

VFSのwrite処理を呼び出す

ここまでで、file構造体とオフセットが取得できたのでvfs_write関数でVFSのレイヤに移る(次回説明)。
vfs_write関数による書き込みをした後は、オフセットの更新と後処理(参照カウンタやロックの解放)が必要になる。

// 78:
static inline void fdput_pos(struct fd f)
{
    if (f.flags & FDPUT_POS_UNLOCK)
        __f_unlock_pos(f.file);
    fdput(f);
}

FDPUT_POS_UNLOCKは、__fdget_pos関数内でfile_count(file) > 1)の場合に立てられるフラグである。
このとき、__f_unlock_pos関数でファイルオフセット用のmutexを解放する。

// 1009:
void __f_unlock_pos(struct file *f)
{
    mutex_unlock(&f->f_pos_lock);
}

その後、fdput関数でファイルの参照カウンタを一つ減らす。

// 43:
static inline void fdput(struct fd fd)
{
    if (fd.flags & FDPUT_FPUT)
        fput(fd.file);
}

実際には、fput関数で参照カウンタを減らす操作をしているが、ここでは割愛する。

おわりに

本記事では、writeシステムコールの実態からvfs_write関数を呼び出すまでを解説した。 次回の記事では、vfs_write関数から各ファイルシステムのwrite操作を呼び出すまでの処理を追いかける。

変更履歴

  • 2020/11/29: 記事公開
  • 2022/08/18: カーネルバージョンを5.15に変更

参考

GitHub ActionsでChangeLogからReleasesを自動化する

概要

本記事では、GitHub ActionsでReleasesを生成する手順を確認した。

RepositoryにあるChangeLogからReleaseの説明文を自動生成する。

はじめに

ソフトウェア開発者 (ユーザ) は、ソフトウェアをリリースする際に幾つかの作業を実施する必要がある。 しかし、それらの作業は、ある程度決められた作業を実施する必要がある。 ヒューマンエラーやユーザの手間を考慮すると、これらの作業を自動化できるとうれしい。

本記事では、GitHubReleases機能を使用しているプロジェクトを対象とする。
Releases機能は、GitHub上にあるリポジトリと紐づけられていて、リポジトリのタグに対して任意のファイルやメッセージを残すことができる。

この機能を使うユースケースとして、下記の作業が考えられる。

  • ユーザは、ChangeLog(または、NEWSなど)に更新点を書き記す。
  • ユーザは、任意のコミットにタグを作成する。
  • ユーザは、GitHubにあるリモートリポジトリにタグをプッシュする。
  • ユーザは、該当リポジトリのパッケージを作成する。
  • ユーザは、Releases 機能からメッセージ(リリースノート)の追加や任意のファイル(パッケージ)を添付する。

本記事では、「パッケージを作成する」作業と「Releases 機能」における作業をGitHub Actionsを利用して自動化を目指す。*1
下記は、本記事を適用したときのソフトウェアのリリースするまでの手順を示したものである。

Releasesするまで手順

  1. ユーザは、ChangeLogに更新点を書き記す。
  2. ユーザは、任意のコミットにタグを作成する。
  3. ユーザは、GitHubにあるリモートリポジトリにタグをプッシュする。
  4. GitHub Actionsは、Change Logからメッセージ(リリースノート)の追加とパッケージを生成し、添付する。

本作業は下記のリポジトリのworkflowに導入済みである。本記事では、これを基に説明する。 github.com

リポジトリでは、automakeを使用しているため、下記のmakeターゲットがデフォルトで用意されている。

  • make: ソフトウェアをビルドする
  • make install: 成果物をユーザの環境にインストールする
  • make dist: 配布用パッケージを生成する (パッケージ名-バージョン名.tar.gz)

Git-flowは下記のような形式を採用しており、適当なタイミングでtag(図中の灰色吹き出し)を作成している。
このtagが作成されたタイミングで、そのバージョンにおけるパッケージとChangeLogを残すようにしている。

本プロジェクトのGit-flow

また、図中の白吹き出しはブランチを表している。

  • mainブランチ: 製品として常に安定した状態を保つ。
  • bugfixブランチ: リリース後に、不具合を修正する。
  • developブランチ: 次のリリースに向けた作業をする。
  • featureブランチ: 各機能における開発をする。

自動化のための作業

Releases手順の自動化に向けて、あらかじめ以下の作業を完了させておく必要がある。

  • ChangeLogから変更内容を取得するスクリプトの作成
  • Releases 機能を自動的に設定するworkflowの作成

ChangeLogから変更内容を取得するスクリプトの作成

Releases のリリースノートに記述するための、該当バージョンの更新点をChangeLogから抽出する必要がある。

今回は、下記のようなフォーマットのChangeLogをユーザが作成している場合を考える。

# Changelog
## [1.1.0] - 2020-10-01
### Added
- 新機能C
- 新機能D

### Changed
- 既存機能Zの修正

### Removed
- 既存機能Yの削除

## [1.0.1] - 2020-09-11
### Fixed
- 既存機能Xの修正

## [1.0.0] - 2020-09-01
### Added
- 新機能A
- 新機能B

この形式は、第2レベルの見出しに[バージョン] - 日付が記述されていて、第3レベルの見出しに変更の種類(AddedやChanged)が記述されている。
リリースノートとして必要となるのは、該当バージョンにおける第2レベルの見出し以下の内容となる。

そのため、該当バージョンの第2レベルの見出しの検索とその内容を取得する必要がある。 第2レベルの見出しの検索は^##grepすることで取得できる。

    $ grep -n "^## " ChangeLog.md
    2## [1.1.0] - 2020-10-01
    13:## [1.0.1] - 2020-09-11
    17:## [1.0.0] - 2020-09-01

ここから、awkコマンドでパターンマッチを行い、該当バージョン(VERSION)内容の先頭と末尾の行番号を取得する。

    $ grep -n "^## " ChangeLog.md |\
        awk -F: -v version=${VERSION} '/'"${VERSION}"'/ \
            { start = $1 + 1; getline; end = $1 - 1 } \
            END { print start, end }' )
   sed -n ${sline},${eline}p ${FILE}

例えばv1.1.0の場合、先頭は3行目、末尾12行目が得られる。
ここから、ChangeLogの3行目から12行目を表示することで該当バージョン(v1.1.0)の変更内容を取得できる。

上記の内容を踏まえて、下記のようなChangeLogから変更内容を取得するスクリプトget_changelog.shを用意する。

#!/bin/bash
FILE=CHANGELOG.md
VER=`echo $1 | tr -d "refs/tags/"`    # i.e. v1.0, v20.15.10
VERSION=`echo ${VER} | tr -d v`       # i.e. 1.0, 20.15.10

read sline eline <<< \
    $( grep -n "^## " ${FILE} | \
   awk -F: -v version=${VERSION} '/'"${VERSION}"'/ \
      { start = $1 + 1; getline; end = $1 - 1 } \
      END { print start, end }' )
sed -n ${sline},${eline}p ${FILE}

Releases 機能を自動的に設定するworkflowの作成

タグのプッシュを契機に、Releases 機能を自動的に設定するworkflowの作成する必要がある。

GitHubでは、Releasesを生成するActions と ReleasesにファイルをアップロードするActionsが用意されている。

github.com github.com

create-releaseを利用して、Markdownファイルで記述されたリリースノート(body.md)からReleasesの作成する場合には下記のように記述する。 (Example workflowを参照)

- name: Create Release
  id: create_release
  uses: actions/create-release@v1
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 
  with:
    tag_name: ${{ github.ref }}                      
    release_name: Release ${{ github.ref }}      # Releasesのタイトル (i.e. Release v1.0.0)
    body_path: "body.md"                         # Releasesのリリースノート
    draft: false
    prerelease: false

upload-release-assetを利用して、ファイルをアップロードする場合も同様にExample workflowが用意されているのそれに従って記述する。

ただし、Automakeで生成される配布物パッケージ名はパッケージ名-バージョン名.tar.gzとなっており、可変のファイル名である。
upload-release-assetはアップロードするファイルの名前に正規表現をサポートしていないため、Example workflowに一工夫しなければいけない。

コミュニティで提案されているのは、前のステップにて該当ファイルを環境変数に代入する方法である。

- name: Get Name of Artifact
  run: |
    ARTIFACT_PATHNAME=$(ls debugfatfs-*.tar.gz | head -n 1)
    ARTIFACT_NAME=$(basename $ARTIFACT_PATHNAME)
    echo "ARTIFACT_NAME=${ARTIFACT_NAME}" >> $GITHUB_ENV
    echo "ARTIFACT_PATHNAME=${ARTIFACT_PATHNAME}" >> $GITHUB_ENV
- name: Upload Release Asset
  id: upload-release-asset
  uses: actions/upload-release-asset@v1
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  with:
    upload_url: ${{ steps.create_release.outputs.upload_url }}
    asset_path: ${{ env.ARTIFACT_PATHNAME }}
    asset_name: ${{ env.ARTIFACT_NAME }}
    asset_content_type: application/gzip

上記のActionsを用いて、Releases 機能を自動的に設定するworkflowを用意する。

name: Create Releases

on:
  push:
    tags:
      - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: setup environment           # パッケージ生成のための環境構築
      run: |
        sudo apt-get update
        sudo apt-get install autoconf automake libtool help2man make
    - run:  script/bootstrap.sh
    - run: ./configure
    - run: make
    - run: make dist          # 配布物パッケージの生成 i.e. debugfatfs-0.1.0.tar.gz
      env:
        CI: true
    - run: |                  # 更新内容を一時的にbody.mdとして保存しておく
        ./get_changelog.sh ${{ github.ref }} > body.md
    - name: Create Release
      id: create_release
      uses: actions/create-release@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 
      with:
        tag_name: ${{ github.ref }}                      
        release_name: Release ${{ github.ref }}      # Releasesのタイトル (i.e. Release v1.0.0)
        body_path: "body.md"                         # Releasesのリリースノート
        draft: false
        prerelease: false
    - name: Get Name of Artifact
      run: |
        ARTIFACT_PATHNAME=$(ls debugfatfs-*.tar.gz | head -n 1)      # 正規表現で成果物パッケージのファイル名を取得する
        ARTIFACT_NAME=$(basename $ARTIFACT_PATHNAME)
        echo "ARTIFACT_NAME=${ARTIFACT_NAME}" >> $GITHUB_ENV         # 成果物パッケージのファイル名の環境変数に設定する
        echo "ARTIFACT_PATHNAME=${ARTIFACT_PATHNAME}" >> $GITHUB_ENV
    - name: Upload Release Asset
      id: upload-release-asset
      uses: actions/upload-release-asset@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        upload_url: ${{ steps.create_release.outputs.upload_url }}
        asset_path: ${{ env.ARTIFACT_PATHNAME }}
        asset_name: ${{ env.ARTIFACT_NAME }}
        asset_content_type: application/gzip

実行例

# Changelog

## [0.1.0] - 2020-10-24

### Added

- Print main Boot Sector field
- Print any cluster
- Print Directory list
- Backup or restore FAT volume
- Convert into update latter
- Create file
- Remove file
- Change any FAT entry
- Change any allocation bitmap
- Trim deleted directory entry

## Initial Version
  • tag (v0.1.0)を作成し、リモートリポジトリにプッシュする。

     $ git tag v0.1.0
     $ git push origin --tags
    

タグ (v0.1.0)をプッシュした後のリポジトリ

Releases v0.1.0の概要

おわりに

本記事では、ユーザがタグを作成したタイミングでChangeLogからReleasesを自動的に生成するworkflowを作成した。

この手順では、ChangeLogの生成がユーザの手作業で書き記す必要があるが、制度の良いcommitを生成しているのであればこの作業も自動化しても良いだろう。 今回は、正規表現を利用してアップロードファイルを指定しているのでひと手間かかったが、GitHub Actionsは様々なworkflowが用意されているので、任意の作業の自動化が容易である。

このように作業の自動化が容易であるため、定型的な作業がGitHub Actions (などのサービス)を利用して可能な限り自動化していくとよい。

変更履歴

  • 2020/10/25: 記事公開

参考

*1:ChangeLogの生成を自動化に関しては「keep a changelog」に則り実施していない。

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カーネルデバッグ