並行制御:コンテキスト、割り込みマスクとアトミック変数の原理詳細解説

コンテキストと並行処理のシナリオ

コンテキスト(Context)

Linuxにおいて、コンテキストとは、特定の操作やリソースへのアクセス時に存在する環境状態を指します。これには、ユーザーID、権限レベル、現在の作業ディレクトリなどが含まれます。ファイル権限の理解と管理において、コンテキストは特に重要です。なぜなら、どの操作が許可され、どの操作が禁止されるかを決定するからです。

コンテキストの切り替え

システムが一つのユーザーから別のユーザーへ、または一つのプロセスから別のプロセスへ切り替わる際、コンテキストの切り替えが発生します。この切り替えは、現在のコンテキストの状態を保存し、新しいコンテキストの状態をロードすることを含みます。これにより、システムは異なるタスクを実行する際に、正しい環境状態を維持できます。

並行処理のシナリオ(Concurrency Scenarios)

並行処理のシナリオでは、複数のプロセスやスレッドが同じリソース(ファイル、データベースなど)に同時にアクセスする可能性があります。データの競合を防ぎ、システムの安定性を維持するため、Linuxは並行アクセスを管理するための多种のメカニズムを提供しています。

競合(Race Condition):複数のタスクが並行して実行され、同じリソースを同時に操作しようとすると、リソースの状態が不正になることがあります。このような状態の乱れを競合と呼びます。

共有リソース:複数のタスクによって同時に使用される可能性のあるリソース。

クリティカルセクション:共有リソースを操作するコードのセクション。

競合を解決するために、共有リソースを同時に使用しないように制御するメカニズムが必要となります。これが並行制御メカニズムです。

並行制御メカニズムの分類:

  • アトミック操作
  • 忙等待(ビジーウェイト)
  • ブロッキング

一般的な並行制御メカニズムの使用パターン:

/* ミューテックス問題:*/
// 並行制御メカニズムを利用可能な状態で初期化
P操作 (ロックを取得)

クリティカルセクション

V操作 (ロックを解放)

/* シグナリング問題:*/
// 並行制御メカニズムを利用不可な状態で初期化
// 先行タスク:
// ...
V操作 (シグナルを送信)
    
// 後続タスク:
P操作 (シグナルを待機)
// ...

割り込みマスク:同期メカニズムの補助手段

現在のCPUの割り込みを禁止・許可します。 local_irq_disable(); local_irq_enable(); local_irq_save(flags); local_irq_restore(flags); はCPUの割り込みフラグに関連します。 local_bh_disable(); local_bh_enable(); は割り込みの下半分(ボトムハーフ)に関連し、ソフトウェア割り込みを無効化・有効化します。

割り込みを禁止 クリティカルセクション // クリティカルセクションのコードは時間がかかりすぎず、迅速に完了する必要があります。 割り込みを許可

適用场景:割り込みコンテキストとタスクが共有リソースを共有する場合、または複数の異なる優先度の割り込みコンテキスト間で共有リソースを共有する場合。

アトミック変数

アトミック変数:読み書きが中断されない特殊な整数型変数。

// a. アトミック変数の値を設定
void atomic_set(atomic_t *v, int i); // アトミック変数vの値をiに設定
atomic_t v = ATOMIC_INIT(0); // アトミック変数vを0で初期化

v = 10; // エラー

// b. アトミック変数の値を取得
atomic_read(atomic_t *v); // アトミック変数の値を返す

// c. アトミック変数の加算・減算
void atomic_add(int i, atomic_t *v); // アトミック変数vにiを加算
void atomic_sub(int i, atomic_t *v); // アトミック変数vからiを減算

// d. アトミック変数のインクリメント・デクリメント
void atomic_inc(atomic_t *v); // アトミック変数vを1増やす
void atomic_dec(atomic_t *v); // アトミック変数vを1減らす

// e. 操作とテスト:演算後の結果が0であれば真を、そうでなければ偽を返す
int atomic_inc_and_test(atomic_t *v); // 1増やした後、結果が0かどうかをテスト
int atomic_dec_and_test(atomic_t *v); // 1減らした後、結果が0かどうかをテスト
int atomic_sub_and_test(int i, atomic_t *v); // iを減らした後、結果が0かどうかをテスト

アトミックビット操作メソッド:
// a. ビットを設定
void set_bit(nr, void *addr); // addrのnr番目のビットを1に設定
// b. ビットをクリア
void clear_bit(nr, void *addr); // addrのnr番目のビットを0にクリア
// c. ビットを反転
void change_bit(nr, void *addr); // addrのnr番目のビットを反転
// d. ビットをテスト
void test_bit(nr, void *addr); // addrのnr番目のビットが1かどうかをテスト

適用场景:共有リソースが単一の整数型変数であるミューテックスのシナリオ。

実装効果:一つのプロセスがリソースを確保すると、他のプロセスは確保に失敗する。

原理atomic_t 型のフラグ(例:work_counter)を利用します。work_counter が0であればリソースは利用可能、0より大きければ利用不可となります。

shared_counter_module.c:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/poll.h>
#include <linux/atmdev.h>
#include <asm/uaccess.h>
#include <asm/ioctl.h>

#define MAX_WORK_UNITS 10

int major = 222; // メジャー番号
int minor = 0;   // マイナー番号
int dev_count = 1; // デバイス数

struct shared_counter_dev {
    struct cdev my_cdev;
    atomic_t work_counter; // 0: 利用可能, >0: 利用中
};
struct shared_counter_dev g_shared_dev;

int counter_acquire(struct inode *inode, struct file *filep) {
    struct shared_counter_dev *dev = container_of(inode->i_cdev, struct shared_counter_dev, my_cdev);

    if (atomic_dec_and_test(&dev->work_counter)) {
        return 0; // 成功: カウンターが0になり、リソースを確保
    } else {
        atomic_inc(&dev->work_counter); // 失敗: カウンターを元に戻す
        printk(KERN_INFO "Resource is currently in use.\n");
        return -EBUSY; // リソースが使用中
    }
}

int counter_release(struct inode *inode, struct file *filep) {
    struct shared_counter_dev *dev = container_of(inode->i_cdev, struct shared_counter_dev, my_cdev);

    atomic_set(&dev->work_counter, MAX_WORK_UNITS); // リソースを解放し、利用可能状態に戻す
    return 0;
}

struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = counter_acquire,
    .release = counter_release,
};

static int __init counter_init(void) {
    int ret;
    dev_t dev_num = MKDEV(major, minor);

    ret = register_chrdev_region(dev_num, dev_count, "shared_counter");
    if (ret) {
        ret = alloc_chrdev_region(&dev_num, minor, dev_count, "shared_counter");
        if (ret) {
            printk(KERN_ERR "Failed to allocate device number.\n");
            return ret;
        }
        major = MAJOR(dev_num);
    }

    cdev_init(&g_shared_dev.my_cdev, &fops);
    g_shared_dev.my_cdev.owner = THIS_MODULE;
    ret = cdev_add(&g_shared_dev.my_cdev, dev_num, dev_count);
    if (ret) {
        printk(KERN_ERR "Failed to add cdev.\n");
        unregister_chrdev_region(dev_num, dev_count);
        return ret;
    }

    atomic_set(&g_shared_dev.work_counter, MAX_WORK_UNITS);
    printk(KERN_INFO "Shared counter module loaded.\n");
    return 0;
}

static void __exit counter_exit(void) {
    dev_t dev_num = MKDEV(major, minor);
    cdev_del(&g_shared_dev.my_cdev);
    unregister_chrdev_region(dev_num, dev_count);
    printk(KERN_INFO "Shared counter module unloaded.\n");
}

MODULE_LICENSE("GPL");
module_init(counter_init);
module_exit(counter_exit);

test_counter.c:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

int main(int argc, char *argv[]) {
    int fd;
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <device_file>\n", argv[0]);
        return 1;
    }

    fd = open(argv[1], O_RDWR);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    printf("Resource acquired successfully. Press Enter to release...\n");
    getchar();

    close(fd);
    return 0;
}

スピンロック:忙等待ベースの並行制御メカニズム


// a. スピンロックの定義
spinlock_t counter_lock;

// b. スピンロックの初期化
spin_lock_init(&counter_lock);

// c. スピンロックの取得
spin_lock(&counter_lock); // ロックを取得できれば即座に返り、取得できなければロックが解放されるまでスピン(待機)する
spin_trylock(&counter_lock); // ロックを取得できれば即座に真を返し、取得できなければ偽を返す(スピンしない)

// d. スピンロックの解放
spin_unlock(&counter_lock);

#include <linux/spinlock.h>
spinlock_t型の変数counter_lockを定義し、spin_lock_init(&counter_lock)で初期化する必要がある

spin_lock(&counter_lock);
クリティカルセクション
spin_unlock(&counter_lock);

適用场景

  1. 割り込みコンテキスト間、または割り込みコンテキストとタスクコンテキスト間で共有リソースを扱う場合。
  2. タスクコンテキスト間で、クリティカルセクションの実行時間が非常に短い場合。
  3. ミューテックス問題。

unique_id_module.c:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/spinlock.h>

#define DEV_COUNT 1

int major = 333;
int minor = 0;

struct unique_id_dev {
    struct cdev cdev;
    int current_id;
    spinlock_t id_lock;
};
struct unique_id_dev g_id_dev;

int get_next_id(struct inode *inode, struct file *filep) {
    int new_id;

    spin_lock(&g_id_dev.id_lock);
    new_id = g_id_dev.current_id;
    g_id_dev.current_id++;
    spin_unlock(&g_id_dev.id_lock);

    printk(KERN_INFO "Allocated ID: %d\n", new_id);
    return new_id;
}

int release_id(struct inode *inode, struct file *filep) {
    // IDの解放は単なる概念であり、実際のリソース解放は不要。
    // ここではロックの使用例を示す。
    spin_lock(&g_id_dev.id_lock);
    // 何らかのクリーンアップ処理(例: IDの再利用ロジック)
    spin_unlock(&g_id_dev.id_lock);
    return 0;
}

struct file_operations id_fops = {
    .owner = THIS_MODULE,
    .open = get_next_id,
    .release = release_id,
};

static int __init id_init(void) {
    int ret;
    dev_t dev_num = MKDEV(major, minor);

    ret = register_chrdev_region(dev_num, DEV_COUNT, "unique_id");
    if (ret) {
        ret = alloc_chrdev_region(&dev_num, minor, DEV_COUNT, "unique_id");
        if (ret) {
            printk(KERN_ERR "Failed to get device number.\n");
            return ret;
        }
        major = MAJOR(dev_num);
    }

    cdev_init(&g_id_dev.cdev, &id_fops);
    g_id_dev.cdev.owner = THIS_MODULE;
    ret = cdev_add(&g_id_dev.cdev, dev_num, DEV_COUNT);
    if (ret) {
        printk(KERN_ERR "cdev_add failed.\n");
        unregister_chrdev_region(dev_num, DEV_COUNT);
        return ret;
    }

    g_id_dev.current_id = 0;
    spin_lock_init(&g_id_dev.id_lock);
    printk(KERN_INFO "Unique ID module initialized.\n");
    return 0;
}

static void __exit id_exit(void) {
    dev_t dev_num = MKDEV(major, minor);
    cdev_del(&g_id_dev.cdev);
    unregister_chrdev_region(dev_num, DEV_COUNT);
    printk(KERN_INFO "Unique ID module exited.\n");
}

MODULE_LICENSE("GPL");
module_init(id_init);
module_exit(id_exit);

test_id.c:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    int fd, i;
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <device_file>\n", argv[0]);
        return 1;
    }

    for (i = 0; i < 5; i++) {
        fd = open(argv[1], O_RDWR);
        if (fd < 0) {
            perror("open");
            return 1;
        }
        printf("Opened file descriptor: %d\n", fd);
        close(fd);
    }
    return 0;
}

上記の実装は、スピンロックを使用して共有リソース(ここでは一意のID)への安全なアクセスを提供し、競合を防ぎます。

タグ: Linuxカーネル 並行処理 アトミック操作 スピンロック 割り込み処理

8月15日 08:36 投稿