『並行プログラミング入門』を執筆しました

これは、Rust Advent Calendar 2021 (2)の25日目の記事です。
https://qiita.com/advent-calendar/2021/rust

2021年8月にオライリー・ジャパンより出版された『並行プログラミング入門』を執筆しました。

執筆から出版まで

本書のGitリポジトリのコミットログでは、2018年の2月10日に初コミットがあるため、2021年8月21日の出版日まで、実に3年6ヶ月もかかったことになります。これほどかかったのは、執筆にかけるための時間がそれほどとれなかったのと、何より内容が難しいと言うのが大きいと思います。並行プログラミングについては、Webや書籍等含めて、様々な文献で言及されてはいるものの、それを体系的にまとめているものがないため、執筆はかなり難しかったです。

f:id:ytakano:20211207231304p:plain
initial commit

『The Art of Multiprocessor Programming』という本もあるにはあるのですが、これはJavaとロックフリーに偏っていて、もっと広範囲な内容が無いという不満もありました。より具体的に言うと、アセンブリによるアトミック処理、グリーンスレッド、π計算などのトピックも並行プログラミングでは重要なトピックですが、それらの説明はありません。これらを含んだ、もっと包括的な本が出版されないかとずっと思っていたのですが、一向に出版される気配がなかったため、一念発起して書きはじめました。

書き始めたは良いものの、なかなか時間はとれず、夜寝る前の10〜11時ぐらいに毎日少しだけ書いていました。盆と正月はかき入れ時で、集中して執筆する時間がとれるチャンスですが、365日働いているようなものなので、それはそれで大変でした。また、3年間1人でずっと孤独に執筆するというのもなかなか大変な作業でした。実際、あまりにつらくて、半年ほど執筆していない期間もあったようにも思います。昔は、専門書は草や花のように勝手に生えてくるような感覚でいましたが、専門書を書くために大変な労力が注ぎ込まれているのを体感した次第です。

本書では、実装に重きを置いていますが、最も注力したのは概念的な説明でして、たとえば、並行や並列とは一体何かや、プロセスとは一体何かという根本的なところを説明するのに大変労力がかかりました。これらを説明するために、技術書だけではなく哲学書なんかにも目を通していたので、大変な時間がかかってしまいました。多くの方にとっては、こういう抽象論は興味外かもしれませんが、「Philosophy of Concurrent Programming」は本書の大きな特徴だと思います。

そんなこんなはありましたが、2021年の8月にようやく出版でき、嬉しい限りです。ただ、誤植がややあったのは申し訳ないです。今回、誤植報告をGitHubからpull requestできるようにしたのですが、読者の皆さんから多くプルリクいただき、異常な速度で正誤表が出来上がっていきました。本当に感謝しかありません。

Rustとわたし

『並行プログラミング入門』執筆に当たり重要視したのは、やはりRustです。Rustは本当に良い言語で、アセンブリのような低レベルな記述から、mapとfold、async/awaitと言った記述までカバーできて本当に素晴らしいです。asm!マクロはインラインアセンブリの記述としては突出した書きやすさだと思います。asm!目当てにRustを使っていると言っても過言ではありません(言い過ぎ)。

『並行プログラミング入門』を書くからには、OSぐらいは一度実装しておく必要があると感じ、昨年、2020年には、RustでOS、ファームウェアなどを実装してみましたが、その知見は本書にも活かされています。こういうマニアックな事をやっていると、初学者置いてけぼりな事を書きたくなるのですが、そういう欲求を抑え、重要で基本的な事を書くことに注力するのはなかなか大変でした。マニアックな技術は、論文という形でいつか発表できたらなと思います。近々、学生と共に成果発表はできそうではあります。いや、できるかな、できたらいいなあ。

また、『並行プログラミング入門』を執筆したご縁で、オライリー・ジャパンから2022年1月19日発売予定の『プログラミングRust 第2版』の査読もさせて頂きました。この本は重要なことしか書かれていないので、是非、こちらも手に取って頂ければと思います。『入門Python 3 第2版』は800ページ、『JavaScript 第7版』は784ページありますが、『プログラミングRust 第2版』は688ページしか無いので、Rustは覚える事も少なくて簡単なプログラミング言語です。688ページを2週間程度で読むのは結構大変でした。

先日、阪急梅田の紀伊国屋に行ったら、Rustの棚ができていたのでRustの注目がますます高まりつつありますね。

おまけ:Linux next with Rust

書籍の話ばかりではなんなので、もう少し技術的な話題もしたいと思います。最近、RustがLinuxカーネルに利用されそうな気配が出てきました。そこで、実際にLinux nextをRustでコンパイルして、動かしてみました。

結論から書くと以下の通りです。

  • Rust 1.57.0 Stableでコンパイル可能に
  • 動的メモリ確保に失敗した場合のパニックは対処済み
Rust 1.57.0 Stable

ちょっと前まではRustのnightlyかbetaでないとコンパイルができなかったのですが、最近、Rust 1.57.0 Stableでコンパイルできるようになっていました。ただ、Stable版だと、asm!が使えないので気になるところではあります。asm!の安定化がそろそろらしいので、大変待ち遠しいです。

動的メモリ確保とパニック

Rustのstdは動的メモリ確保に失敗した際にはパニックとなります。仮想メモリを利用している場合は、動的メモリ確保が失敗することはあまりなく、ページフォルトが起きた段階でメモリが足りなくなりスワップが発生し、更にスワップ領域も足りなくなってくるとLinuxの場合はOOMキラーに殺されます。このように、一般的な環境だと動的メモリ確保失敗の動作をパニックとするのでほとんど問題は無いのですが、カーネルだとパニックは問題で、2021年の頭頃にこの問題が指摘されていました。

2021年4月15日 パニックお断り―Linus,"Rust for Linux"の盛り上がりに釘を刺す:Linux Daily Topics|gihyo.jp … 技術評論社

この問題は、既に解決済みのようです。たとえば、Vecだとpushというメソッドがありますが、これは内部的で動的メモリ確保を行う可能性があります。LinuxのRustでは、Vecにtry_pushというResult値を返すメソッドが追加されており、カーネルモジュールなどの実装にはこれらを利用することができます。これらがRustの本家にも追加されると、ベアメタルや組み込みプログラミングにもRustが使いやすくなるので、是非導入して欲しいところです。ライセンス的に難しそうな気もしますが。

実行結果

簡単な実行結果ではありますが、カーネルコンフィグと、rust_minimalというRustで書かれたサンプルカーネルモジュールのロードとアンロードのスクリーンショットを貼っておきます。実験環境は、仮想環境内のDebian 11上で、Linux nextをRust 1.57.0でコンパイルしてインストールしています。

f:id:ytakano:20211208002823p:plain
カーネルコンフィグ
f:id:ytakano:20211208002844p:plain
Rustで実装されたカーネルモジュールのロードとアンロード

ただし、現状では、カーネルコンフィグのMODVERSIONSをオフにしないとRustが有効化されないようなので、まだまだ課題はあるかなと言う感じです。ぱっと見た感じ、足りない実装も結構ありそうな感じでした。しかし、Rustなので、カーネルコードなのに非常に読みやすいです。今後に期待ですね。

Hello, World!

サンプルコードがいくつかあったので、それをベースにmiscキャラクタデバイスを実装してみました。readすると、"Hello, World!"を表示するだけの簡単なデバイスです。コードは以下の通りで、注目すべき点はread関数と、data.write_slice(hello)?; という箇所です。readするとこの関数が呼ばれて、引数のIoBufferWriterにデータを書き出すとユーザランドにデータが渡されます。簡単ですね。

// rust_hello.rs
// SPDX-License-Identifier: GPL-2.0

//! Rust miscellaneous device sample

#![no_std]
#![feature(allocator_api, global_asm)]

use kernel::prelude::*;
use kernel::{
    file::File,
    file_operations::{FileOpener, FileOperations},
    io_buffer::IoBufferWriter,
    miscdev,
};

module! {
    type: RustMiscdev,
    name: b"rust_hello",
    author: b"Yuuki Takano",
    description: b"Hello, World! in Rust",
    license: b"GPL v2",
}

#[derive(Clone)]
struct Hello;

impl FileOpener<Hello> for Hello {
    fn open(shared: &Hello, _file: &File) -> Result<Self::Wrapper> {
        Ok(Box::try_new(shared.clone())?)
    }
}

impl FileOperations for Hello {
    kernel::declare_file_operations!(read);

    fn read(
        _shared: &Hello,
        file: &File,
        data: &mut impl IoBufferWriter,
        _offset: u64,
    ) -> Result<usize> {
        pr_info!("rust_hello: read, pos = {}", file.pos());
        if file.pos() == 0 {
            let hello = b"Hello, World!\n";
            data.write_slice(hello)?;
            Ok(hello.len())
        } else {
            Ok(0)
        }
    }
}

struct RustMiscdev {
    _dev: Pin<Box<miscdev::Registration<Hello>>>,
}

impl KernelModule for RustMiscdev {
    fn init(name: &'static CStr, _module: &'static ThisModule) -> Result<Self> {
        pr_info!("Hello, World! in Rust (init)\n");

        Ok(RustMiscdev {
            _dev: miscdev::Registration::new_pinned::<Hello>(name, None, Hello)?,
        })
    }
}

impl Drop for RustMiscdev {
    fn drop(&mut self) {
        pr_info!("Hello, World! in Rust (exit)\n");
    }
}

これを実行すると以下のようになります。catで/dev/rust_helloを実行すると、ちゃんと、"Hello, World!"が表示されます。

$ sudo insmod rust_hello.ko

$ ls /dev/rust_hello
/dev/rust_hello

$ sudo cat /dev/rust_hello
Hello, World!

$ sudo sh -c "dmesg | tail -n 1"
[512769.908740] rust_hello: rust_hello: read, pos = 0

つづいて、カーネルモジュール内でパニックを起こしてみます。コードは以下の通り修正しています。

--- a/samples/rust/rust_hello.rs
+++ b/samples/rust/rust_hello.rs
@@ -56,6 +56,7 @@ struct RustMiscdev {

 impl KernelModule for RustMiscdev {
     fn init(name: &'static CStr, _module: &'static ThisModule) -> Result<Self> {
+        panic!("panic!");
         pr_info!("Hello, World! in Rust (init)\n");

         Ok(RustMiscdev {

これをコンパイルして、insmodしてみると、insmodがセグフォで死にます。rmmod -fも効かず、大変なことになりました。カーネルプログラミングは怖いですね。

$ sudo insmod rust_hello.ko

Message from syslogd@DebianKern at Dec 13 23:58:20 ...
 kernel:[513053.695576] rust_kernel: panicked at 'panic!', samples/rust/rust_hello.rs:59:9
zsh: segmentation fault  sudo insmod rust_hello.ko

$ cat /proc/modules | grep rust
rust_hello 20480 1 - Loading 0x0000000000000000 (E+)

$ sudo rmmod -f rust_hello
rmmod: ERROR: ../libkmod/libkmod-module.c:799 kmod_module_remove_module() could not remove 'rust_hello': Device or resource busy
rmmod: ERROR: could not remove module rust_hello: Device or resource busy

上のコードを書く際、Cargo.tomlのない環境だったのでVSCode + rust-analyzerの使い方がわからず、vimで書いていました。rust-analyzerに飼い慣らされた自分には、なかなかつらい環境です。rust-analyzerを使う方法はあるような気もしますが、今回はそこまで深掘りできずです。

それでは今回はこの辺で。皆さん、メリークリスマス!良いお年を。

次回作にもご期待ください。
俺たちの戦いは始まったばかりだ!