POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

FeedlyRSSTwitterFacebook

本記事は、原著者の許諾のもとに翻訳・掲載しております。

はじめに

cpumasks は、Linuxカーネルが提供する特別な方法で、システム内のCPUに関する情報を格納します。 cpumasks 操作に関して、APIを含む関連のソースコードとヘッダファイルは以下のとおりです。

include/linux/cpumask.h 内のコメントにあるように、CPUマスクは、システム内のCPUセットを表すのに適したビットマップ(CPU番号ごとに1つのビット位置)を提供します。CPUマスクについては、以前に カーネルのエントリポイント を扱ったパートの boot_cpu_init 関数を説明した部分で少し触れました。この関数は、最初のブートCPUをオンラインやアクティブなどにしたりする操作を行います。

set_cpu_online(cpu, true);
set_cpu_active(cpu, true);
set_cpu_present(cpu, true);
set_cpu_possible(cpu, true);

これらの関数の実装を検討する前に、まずはそれぞれのマスクについて見ていきましょう。

cpu_possible は、当該のシステム起動中、いつでもプラグインできるCPU IDのセットです。つまり、このCPUのマスクには、システム内で利用可能なCPUの最大数が含まれています。これは、 CONFIG_NR_CPUS カーネル構成オプションを介して静的に設定される NR_CPUS の値と同じです。

cpu_present マスクは、現在どのCPUがプラグインされているかを表します。

cpu_online は、 cpu_present のサブセットを表し、どのCPUがスケジューリングできるかを示します。言い換えると、このマスクのビットは、Linuxカーネルがプロセッサを利用できるかどうかをカーネルに伝えます。

最後のマスクは cpu_active です。このマスクのビットは、タスクを特定のプロセッサに移していいかどうかをLinuxカーネルに伝えます。

これらのマスクの設定は CONFIG_HOTPLUG_CPU 設定オプションに依存しており、このオプションがdisalbedな場合は possible == present かつ active == online というフラグ状態となります。これらの関数の実装については、どれも非常に似ており、いずれも2番目のパラメータをチェックします。 true であれば cpumask_set_cpu が呼び出され、そうでなければ cpumask_clear_cpu が呼び出されます。

cpumask の作成には2通りの方法があります。1つ目は cpumask_t を使う方法で、その定義は以下のとおりです。

typedef struct cpumask { DECLARE_BITMAP(bits, NR_CPUS); } cpumask_t;

これが、ビットマスクの1つの bits フィールドを含む cpumask 構造体をラップします。 DECLARE_BITMAP マクロは、以下の2つのパラメータを取り、

  • ビットマップ名
  • ビット数

与えられた名前で unsigned long の配列を作成します。その実装は、非常に簡単です。

#define DECLARE_BITMAP(name,bits) \
        unsigned long name[BITS_TO_LONGS(bits)]

この時 BITS_TO_LONGS は、以下のようになります。

#define BITS_TO_LONGS(nr)       DIV_ROUND_UP(nr, BITS_PER_BYTE * sizeof(long))
#define DIV_ROUND_UP(n,d) (((n) + (d) - 1) / (d))

私たちがベースにしているのは x86_64 アーキテクチャなので、 unsigned long は8バイトのサイズで、配列には1つの要素のみが含まれます。

(((8) + (8) - 1) / (8)) = 1

NR_CPUS は、システム内のCPUの数を表すマクロで、 include/linux/threads.h で定義されている CONFIG_NR_CPUS マクロによって設定されます。その表記は以下のとおりです。

#ifndef CONFIG_NR_CPUS
        #define CONFIG_NR_CPUS  1
#endif

#define NR_CPUS         CONFIG_NR_CPUS

CPUマスクを作成する2つ目の方法として、 DECLARE_BITMAP マクロを直接叩いて得られたビットマップを to_cpumask マクロに渡して struct cpumask * に変換する方法があります。

#define to_cpumask(bitmap)                                              \
        ((struct cpumask *)(1 ? (bitmap)                                \
                            : (void *)sizeof(__check_is_bitmap(bitmap))))

ここでは、常に true である三項演算子が確認できます。 __check_is_bitmap インライン関数の定義は以下のとおりです。

static inline int __check_is_bitmap(const unsigned long *bitmap)
{
        return 1;
}

このインライン関数は、毎回 1 を返します。ここでこの関数が必要なのは、与えられた bitmap がビットマップかどうかをコンパイル時に確認するためです。つまり、 cpu_possible_bits to_cpumask マクロに渡して、 unsigned long 配列を struct cpumask * に変換することで与えられた bitmap が、 unsigned long * 型であることをチェックしています。

cpumask API

これまで見てきたようにいくつかの方法で私たちはCPUマスクを作成できるわけですが、一方でLinuxカーネルはCPUマスクを操作するためのAPIを提供します。前述の関数の1つ、 set_cpu_online を検討してみましょう。この関数は2つのパラメータを取ります。

  • CPUの番号
  • CPUの状態

その実装は以下のとおりです。

void set_cpu_online(unsigned int cpu, bool online)
{
    if (online) {
        cpumask_set_cpu(cpu, to_cpumask(cpu_online_bits));
        cpumask_set_cpu(cpu, to_cpumask(cpu_active_bits));
    } else {
        cpumask_clear_cpu(cpu, to_cpumask(cpu_online_bits));
    }
}

この関数はまず、第2のパラメータ state をチェックし、その値に応じて cpumask_set_cpu または cpumask_clear_cpu を呼び出します。これを見ると、 cpumask_set_cpu 内の第2パラメータが struct cpumask * へキャストされていることが分かります。今回の事例では、これは cpu_online_bits で、次のとおり定義されたビットマップです。

static DECLARE_BITMAP(cpu_online_bits, CONFIG_NR_CPUS) __read_mostly;

cpumask_set_cpu 関数は set_bit 関数への呼び出しを1回だけ行います。

static inline void cpumask_set_cpu(unsigned int cpu, struct cpumask *dstp)
{
        set_bit(cpumask_check(cpu), cpumask_bits(dstp));
}

set_bit 関数もまた2つのパラメータを取り、与えられたビット(第1パラメータ)をメモリ(第2パラメータまたは cpu_online_bits ビットマップ)内にセットします。ここで、 set_bit が呼び出される前に、2つのパラメータが次のマクロに渡されることが分かります。

  • cpumask_check
  • cpumask_bits

この2つのマクロについて考えましょう。まず、今回の事例では、 cpumask_check は何もしないのであれば、与えられたパラメータを返すのみとなります。第2の cpumask_bits は、与えられた struct cpumask * 構造体の bits フィールドのみを返します。

#define cpumask_bits(maskp) ((maskp)->bits)

では、 set_bit の実装を見てみましょう。

 static __always_inline void
 set_bit(long nr, volatile unsigned long *addr)
 {
         if (IS_IMMEDIATE(nr)) {
                asm volatile(LOCK_PREFIX "orb %1,%0"
                        : CONST_MASK_ADDR(nr, addr)
                        : "iq" ((u8)CONST_MASK(nr))
                        : "memory");
        } else {
                asm volatile(LOCK_PREFIX "bts %1,%0"
                        : BITOP_ADDR(addr) : "Ir" (nr) : "memory");
        }
 }

この関数は、見た目は難しそうですが、実はそれほどでもありません。まず、この関数は nr つまりビット番号を IS_IMMEDIATE マクロに渡します。このマクロは、単にGCC内部の関数 __builtin_constant_p を呼び出します。

define IS_IMMEDIATE(nr)    (__builtin_constant_p(nr))

builtin_constant_p は与えられたパラメータがコンパイル時に既知の定数であるかチェックします。今回の事例では cpu はコンパイル時定数ではないので、 else の項が実行されます。

asm volatile(LOCK_PREFIX "bts %1,%0" : BITOP_ADDR(addr) : "Ir" (nr) : "memory");

これがどのように動くのか、段階を追って考えましょう。

LOCK_PREFIX は、x86の lock 命令です。この命令はCPUに、命令が実行されている間システムを占有するように命じます。これにより、CPUはメモリアクセスを同期させることができ、複数プロセッサ(または、例えばDMAコントローラなどのデバイス)による1つのメモリセルへの同時アクセスが回避されます。

BITOP_ADDR は、与えられたパラメータを (*(volatile long *) へキャストして、制約条件 +m を加えます。 + は、この命令によってこのオペランドの読み出しと書き込みの両方が行われることを意味します。 m は、これがメモリオペランドであることを示します。 BITOP_ADDR は、次のとおり定義されます。

#define BITOP_ADDR(x) "+m" (*(volatile long *) (x))

次は、 memory clobberです。これは、アセンブリコードが入力オペランドと出力オペランドに列挙されているもの以外の項目に対してメモリの書き込みまたは読み出しを行う(例えば、入力パラメータの1つによって示されているメモリにアクセスする)ということをコンパイラに伝えます。

Ir は、イミディエイト(即値)レジスタコマンドです。

bts 命令は、与えられたビットをビット列にセットして、与えられたビットの値を CF フラグに格納します。したがって、CPUの番号を渡したことになり、この番号は今回の事例ではゼロなので、 set_bit が実行された後に、 cpu_online_bits CPUマスク内のゼロ番目のビットをセットします。つまり、この時点では最初のCPUがオンラインになっています。

CPUマスクには、 set_cpu_* APIの他に、CPUマスク操作のために、もう1つ別のAPIがあります。それについて簡単に触れます。

その他のcpumask API

CPUマスクには、例えば次のような、さまざまな状態のCPUの数を取得するためのマクロもあります。

#define num_online_cpus()    cpumask_weight(cpu_online_mask)

このマクロは、 online CPUの数を返すもので、 cpu_online_mask ビットマップを渡して cpumask_weight 関数を呼び出します。 cpumask_weight 関数は、次の2つのパラメータを使って bitmap_weight を1回呼び出してから、与えられたビットマップ内のビットの数を計算します。

  • cpumask bitmap
  • nr_cpumask_bits – このケースでは NR_CPUS
static inline unsigned int cpumask_weight(const struct cpumask *srcp)
{
    return bitmap_weight(cpumask_bits(srcp), nr_cpumask_bits);
}

num_online_cpus の他に、全てのCPU状態についてのマクロもあります。

  • num_possible_cpus
  • num_active_cpus
  • cpu_online
  • cpu_possible
  • その他多数

上記のマクロに加えて、Linuxカーネルは、 cpumask の操作のために、次のAPIを提供します。

  • for_each_cpu – マスク内の全CPUに渡って繰り返す
  • for_each_cpu_not -相補(ビット反転)マスク内の全CPUに渡って繰り返す
  • fcpumask_clear_cpu – CPUマスク内の1つのCPUをクリアする
  • cpumask_test_cpu – マスク内のつのCPUをテストする
  • cpumask_setall – マスク内の全CPUをセットする
  • cpumask_size – “struct cpumask”のために割り当てるサイズをバイト数で返す
  • その他多数

リンク

監修者
監修者_古川陽介
古川陽介
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
複合機メーカー、ゲーム会社を経て、2016年に株式会社リクルートテクノロジーズ(現リクルート)入社。 現在はAPソリューショングループのマネジャーとしてアプリ基盤の改善や運用、各種開発支援ツールの開発、またテックリードとしてエンジニアチームの支援や育成までを担う。 2019年より株式会社ニジボックスを兼務し、室長としてエンジニア育成基盤の設計、技術指南も遂行。 Node.js 日本ユーザーグループの代表を務め、Node学園祭などを主宰。