Linuxカーネル5.0に近づくにつれて、旧来の方法で動作するブロックデバイスドライバは徐々に減少していっています。
そして、Ver5.0では、関数blk_init_queue()は不要として削除されてしまい、旧来の2003年からの古いコードはとうとう動作できなくなりました。
Ver4.19よりデフォルトでマルチキューブロックレイヤー(blq-mq)が採用されて、それ以降の新しいディストリビューションもデフォルトの設定でマルチキューブロックレイヤーが使用されています。

したがって、Ver4.19以降は、旧来の方式からマルチキューブロックレイヤーへの移行期間であり、Ver5.0で移行が完了ました。
これは、コードの書き換えと追加のテストが必要となる重要なイベントであり、それ自体、大小のバグの出現といくつかのサーバーのシステムダウンを引き起こします。

また、多くの組み込みシステムでは、今だにVer2.xが現役で稼働しているケースが多い現状です。
しかし、PCベースでは2022年時点でVer5.xが主流となっており、組み込みシステムでPCとの互換性が必要なケースでは、組み込みシステムでVer5.0にまで引き上げる必要があります。

古い組み込みシステム、特にSH3ベースのものでは、その多くはVer2.xベースで稼働しているケースが多く見受けられます。
そのような古いものでは、カーネルのバージョンをいきなり5.0にまで引き上げが可能かというと、結論としては可能です。

SH3を採用したT-SH7706LSRボードの組み込みLInuxのバージョンは2.6.18となっています。
実際にそのような古い組み込みシステムをいきなりVer5.0に引き上げを試みたところ、無事、Ver5.0のカーネルが、T-SH7706LSRボード(プロセッサーSH7706)で起動しました。

もちろん、Ver2.6.18のボード依存のカーネルコード差分がそのまま動作するわけでなく、いくつかの軽微なコード修正により、古いカーネルコード差分をベースとしたカーネルコード差分を加えたカーネルコード一式がVer4.xの最終バージョンである4.20で動作しました。
そして、条件つきで、無事、Ver5.0で動作しました。

基本的にVer2.6.18ベースのカーネルコード差分は、いくつかの軽微なコード修正でVer5.0に引き上げは可能ですが、前述のとおり、ブロックデバイスに限っては、マルチキューブロックレイヤー方式に新規作成し直す必要があります。

旧来のブロックデバイスのコードは読みやすかったのですが、マルチキューブロックレイヤー方式では直感的に理解しにくいものとなっています。
しかし、マルチキューブロックレイヤー方式のブロックデバイスをクリアしない限り、Ver5.0に到達することができません。

そこで、どのように動作するかを明確にするために最低限のブロックデバイスドライバを紹介していきます 。
まずは、エントリーポイント部分のブロックデバイスコードから紹介します。

static int __init sblkdev_init (void)
{
	int ret = SUCCESS;

	_sblkdev_major = register_blkdev (_sblkdev_major, _sblkdev_name);
	if (_sblkdev_major <= 0){
		printk(KERN_WARNING "sblkdev: unable to get major numbern");
		return -EBUSY;
	}

	ret = sblkdev_add_device();
	if (ret)
		unregister_blkdev(_sblkdev_major, _sblkdev_name);

	return ret;
}

static void __exit sblkdev_exit(void)
{
	sblkdev_remove_device();

	if (_sblkdev_major > 0)
		unregister_blkdev (_sblkdev_major, _sblkdev_name);
}

module_init (sblkdev_init);
module_exit (sblkdev_exit);

モジュールのロード時にはsblkdev_init()関数が起動し、アンロード時にはsblkdev_exit()関数が起動します。
register_blkdev()関数はブロックデバイスを登録することによりメジャー番号が割り当てられます。 unregister_blkdev () ではこの番号を解放します。

本モジュールにおけるプライベートな主要な構造体は下記のとおり、sblkdev_device_t です。

// The internal representation of our device
typedef struct sblkdev_device_s
{
    sector_t capacity;// Device size in bytes
    u8 * data; // The data aray. u8 - 8 bytes
    atomic_t open_counter; // How many openers

    struct blk_mq_tag_set tag_set;
    struct request_queue * queue; // For mutual exclusion

    struct gendisk * disk; // The gendisk structure
} sblkdev_device_t;

これらは、ブロックデバイスの容量、データそのもの、ディスクとキューへのポインタなど、カーネルモジュールに必要なデバイスに関するすべての情報を含んでいます。

ブロックデバイスのすべての初期化は sblkdev_add_device () 関数で行われます。

static int sblkdev_add_device (void)
{
    int ret = SUCCESS;

    sblkdev_device_t * dev = kzalloc (sizeof (sblkdev_device_t), GFP_KERNEL);
    if (dev == NULL) {
        printk (KERN_WARNING "sblkdev: unable to allocate% ld bytes  n", sizeof (sblkdev_device_t));
        return -ENOMEM;
    }
    _sblkdev_device = dev;

    do {
        ret = sblkdev_allocate_buffer (dev);
        if (ret)
            break;

        {
            struct request_queue * queue;

            dev-> tag_set.cmd_size = sizeof (sblkdev_cmd_t);
            dev-> tag_set.driver_data = dev;

            queue = blk_mq_init_sq_queue (&dev-> tag_set, &_mq_ops, 128, BLK_MQ_F_SHOULD_MERGE | BLK_MQ_F_SG_MERGE);
            if (IS_ERR (queue)) {
                ret = PTR_ERR (queue);
                printk (KERN_WARNING "sblkdev: unable to allocate and initialize tag set  n");
                break;
            }
            dev-> queue = queue;
        }
        dev-> queue-> queuedata = dev;

        {// configure disk
            struct gendisk * disk = alloc_disk (1); // only one partition
            if (disk == NULL) {
                printk (KERN_WARNING "sblkdev: Failed to allocate disk  n");
                ret = -ENOMEM;
                break;
            }

            // disk-> flags | = GENHD_FL_NO_PART_SCAN; // only one partition
            // disk-> flags | = GENHD_FL_EXT_DEVT;
            disk-> flags | = GENHD_FL_REMOVABLE;

            disk-> major = _sblkdev_major;
            disk-> first_minor = 0;
            disk-> fops = & _fops;
            disk-> private_data = dev;
            disk-> queue = dev-> queue;
            sprintf (disk-> disk_name, "sblkdev% d", 0);
            set_capacity (disk, dev-> capacity);

            dev-> disk = disk;
            add_disk (disk);
        }

        printk (KERN_WARNING "sblkdev: simple block device was created  n");
    } while (false);

    if (ret) {
        sblkdev_remove_device ();
        printk (KERN_WARNING "sblkdev: Failed add block device  n");
    }

    return ret;
}

まず、構造体のためにメモリを確保し、データ格納のためのバッファを割り当てます。
次に、リクエストキューを初期化するのですが、これは blk_mq_init_sq_queue () という関数で行うか、 blk_mq_alloc_tag_set () + blk_mq_init_queue () という二つの関数で一度に行うかのいずれかです。
ところで、blk_mq_init_sq_queue () 関数のソースを見ると、これはカーネル 4.20 で登場した blk_mq_alloc_tag_set () および blk_mq_init_queue () 関数の単なるラッパーであることが分かります。さらに、キューの多くのパラメータを隠しますが、見た目はずっとシンプルです。

上記コードではディスク容量をセットしていませんが、実情に合わせてメディアを検出などをして set_capacity関数の手前で dev->capacity にセットしておく必要があります。

このコードで重要なのは、グローバル変数 _mq_ops です。

static struct blk_mq_ops _mq_ops = {
    .queue_rq = queue_rq,
};

ここにはリクエストの処理を提供する関数がありますが、それについては後で詳しく説明します。主な内容は、リクエストハンドラへのエントリポイントを指定したことです。

キューを作成したので、ディスクのインスタンスを作成することができます。
ここでは、あまり変更せずに ディスクが割り当てられ、パラメータが設定され、ディスクがシステムに追加されます。disk-> flags というパラメータについて明らかにしておきたいと思います。これは、ディスクがリムーバブルであること、あるいは、例えば、ディスクにパーティションが含まれておらず、そこでパーティションを探す必要がないことをシステムが示すことができるようにするものです。

ディスク管理のために、_fops 構造体があります。

static const struct block_device_operations _fops = {
    .owner = THIS_MODULE,
    .open = _open,
    .release = _release,
    .ioctl = _ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl = _compat_ioctl,
#endif
};

エントリーポイント .open と .release は、単純なブロック・デバイス・モジュールにとって、カウンターのインクリメントとデクリメントのみです。
また、compat_ioctlは実装せずに残しておきました。

.ioctlを使うと、このディスクに対するシステム要求を処理することができます。
ディスクが現れると、システムはそのディスクについて情報収集をします。
いくつかの要求には答えることができます。(たとえば、新しいCDのふりをする)
一般的なルールは、自分にとって興味のない要求には答えたくない場合は、単にエラーコード-ENOTTYを返せばよいのです。
必要であれば、特定のディスクのために、独自のリクエストハンドラを追加することができます。

そこでは、追加したデバイス-リソースの解放に気を配る必要があります。

static void sblkdev_remove_device (void)
{
    sblkdev_device_t * dev = _sblkdev_device;
    if (dev) {
        if (dev-> disk)
            del_gendisk (dev-> disk);

        if (dev-> queue) {
            blk_cleanup_queue (dev-> queue);
            dev-> queue = NULL;
        }

        if (dev-> tag_set.tags)
            blk_mq_free_tag_set (& dev-> tag_set);

        if (dev-> disk) {
            put_disk (dev-> disk);
            dev-> disk = NULL;
        }

        sblkdev_free_buffer (dev);

        kfree (dev);
        _sblkdev_device = NULL;

        printk (KERN_WARNING "sblkdev: simple block device was removed  n");
    }
}

この部分では、システムからディスク・オブジェクトを削除し、キューを解放し、その後にバッファ(データ領域)を解放しています。

そして、目的とするディスクアクセスをする本体のコードは、関数 queue_rq () でのリクエストの処理です。

static blk_status_t queue_rq (struct blk_mq_hw_ctx * hctx, const struct blk_mq_queue_data * bd)
{
    blk_status_t status = BLK_STS_OK;
    struct request * rq = bd-> rq;

    blk_mq_start_request (rq);

    // we can't use that thread
    {
        unsigned int nr_bytes = 0;

        if (do_simple_request (rq, & nr_bytes)! = SUCCESS)
            status = BLK_STS_IOERR;

        printk (KERN_WARNING "sblkdev: request process% d bytes  n", nr_bytes);

#if 0 // proprietary module
        blk_mq_end_request (rq, status);
#else // can set real processed bytes count
        if (blk_update_request (rq, status, nr_bytes)) // GPL-only symbol
            BUG ();
        __blk_mq_end_request (rq, status);
#endif
    }

    return BLK_STS_OK; // always return ok
}

まず始めに、パラメータについて考えてみましょう。
最初のパラメータは struct blk_mq_hw_ctx * hctx で ハードウェアキューの状態です。
この例では、ハードウェアキューを使用しないので、未使用です。
2つ目のパラメータ、const struct blk_mq_queue_data * bd は、下記のように非常にいい加減な構造のパラメータですので、あまり気にする必要はないでしょう。
一旦最初に bd->rq でリクエストを得てしまえば、2つ目のパラメータは用済みで不要となります。

struct blk_mq_queue_data {
struct request * rq;
bool last;
};

実際にはすべて同じリクエストで、blk_mq_start_request () を呼び出してカーネルに通知し、リクエストを受け取り、処理を開始します。
リクエストの処理が完了したら、関数blk_mq_end_request ()を呼び出してカーネルに通知します。

ここでちょっとした注意があり、関数 blk_mq_end_request () は、要するに blk_update_request () と __blk_mq_end_request () の呼び出しの上にあるラッパーとなっています。
blk_mq_end_request () 関数を使用する場合、実際に何バイト処理されたかを指定することはできず、すべて処理されたことになっています。

この選択肢にはもう一つ特徴があります。blk_update_request関数は、GPLのみのモジュールに対してのみエクスポートされます。
つまり、もしあなたが独自のカーネルモジュールを作りたいなら blk_update_request() を使うことはできません。

リクエストからバッファへのデータの読み書きを、 do_simple_request () 関数で記述しました。

static int do_simple_request (struct request * rq, unsigned int * nr_bytes)
{
    int ret = SUCCESS;
    struct bio_vec bvec;
    struct req_iterator iter;
    sblkdev_device_t * dev = rq-> q-> queuedata;
    loff_t pos = blk_rq_pos (rq) << SECTOR_SHIFT;
    loff_t dev_size = (loff_t)(dev->capacity << SECTOR_SHIFT);

    printk(KERN_WARNING "sblkdev: request start from sector %ld n", blk_rq_pos(rq));
    
    rq_for_each_segment(bvec, rq, iter)
    {
        unsigned long b_len = bvec.bv_len;

        void* b_buf = page_address(bvec.bv_page) + bvec.bv_offset;

        if ((pos + b_len) > dev_size)
            b_len = (unsigned long) (dev_size - pos);

        if (rq_data_dir (rq)) // WRITE
            memcpy (dev-> data + pos, b_buf, b_len);
        else // READ
            memcpy (b_buf, dev-> data + pos, b_len);

        pos + = b_len;
        * nr_bytes + = b_len;
    }

    return ret;
}

rq_for_each_segmentはすべてのbioとその中のすべてのbio_vec構造体を経て、リクエストデータを持つページにアクセスすることができます。

上記リクエストの処理は、一般にリクエストページと内部バッファの間でデータをコピーしているだけとなっていますので、この必要最低限のコードをテンプレートとして、目的とするディスクアクセスをするコードを追加していきます。