CPUマスク

はじめに

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_bitsto_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_bitsCPUマスク内のゼロ番目のビットをセットします。つまり、この時点では最初のCPUがオンラインになっています。

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

その他のcpumask API

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

#define num_online_cpus()    cpumask_weight(cpu_online_mask)

このマクロは、onlineCPUの数を返すもので、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”のために割り当てるサイズをバイト数で返す
  • その他多数

リンク