MSI-Xについて
はじめに
自作OS(x64)にて、MSI-X割り込みに対応させる実装を行いました。日本語のソースがあまりなかったので、備忘録ついでにMSI-Xについてまとめておきます。
MSI/MSI-Xとは
MSI(Message Signaled Intterupts)及びその拡張規格であるMSI-Xは、PCI技術仕様で規定された割り込み方式です。この規格が定められる以前に使用されていたINTx割り込みの方式では、割り込み信号線が4本しか規定されておらず、複数のPCIデバイスが同じ割り込み線を共有する必要がありました。その結果、割り込みの競合が発生しやすく、システムのパフォーマンスにも悪影響を与えるものになっていました。
MSI(MSI-X)では信号線は使用せず、メモリバスへの書き込み動作によって割り込みを発生させます。これにより、INTxよりもはるかに多くの割り込みを、割り込みの共有なしに発生させることができ、一台のデバイスが複数の割り込みを持つことが可能になりました。
MSIとMSI-Xの違い
MSIとMSI-Xは下記のような違いがあります。
デバイス毎の割り込み数 | 割り込み数の制約 | ベクタの連続性 | |
---|---|---|---|
MSI | 最大16 | 2のべき乗数である必要あり | 連続している必要あり |
MSI-X | 最大2048 | 特になし | 特になし |
MSI-Xの方が登録できる割り込み数がはるかに多く、制約も少ないことがわかります。また、多くの割り込みベクターを持つことができるため、高い並列性を実現できます。
一方で、MSIに比べてMSI-Xは設定がやや複雑であり、対応するのに少し手間がかかります。
MSI-Xの基本設定
PCI Configuration Space
MSI-Xの設定を行うためには、PCI Configuration Spaceの構造を知っておく必要があります。
PCI Configuration Space
PCI Configuration Spaceは、PCI規格に準拠したデバイスが持つ256バイトの構造体です。
この構造体を読み書きすることで、デバイスの情報を取得したり、デバイスの設定を変更したりすることができます。MSI-X(MSI)の設定も、この構造体を通じて行われます。
MSI-Xの設定を行うためには、まずMSI-XのCapabilityを取得する必要があります。Capabilityは、デバイスの追加機能や拡張機能を設定するための構造体です。上図のCap.Pointerというフィールドから、CapabilityのListを取得することができます。このListからMSI-XのCapabilityを特定し設定を行います。
MSI-X Capability
MSI-X Capability
上図はMSI-XのCapabilityの構造を示しています。
Capability IDとNext Cap Ptrは、Capability構造体共通のフィールドで、それぞれCapabilityのIDと次のCapabilityのポインタを示しています。
Message Control Registerは以下のような構造になっています。
struct message_control_register {
uint16_t size_of_table : 11;
uint16_t reserved : 3;
uint16_t function_mask : 1;
uint16_t enable : 1;
}
- enable : MSI-X割り込みの発生を有効にするかのフラグ。
- function_mask : 1に設定すると、すべてのMSI-X割り込みがマスク(割り込みが発生しても割り込みハンドラが実行されないようにすること)される。
- reserved : 予約ビット。
- size_of_table : 後述するMSI-X Tableのエントリ数。
MSI-Xを有効にし、割り込みハンドラを実行させるためには、enableに1、function_maskに0を設定する必要があります。
MSI-X Table BAR IndicatorとMSI-X Table Offsetは、MSI-Xの中心となるデータ構造であるMSI-X Tableのアドレスを取得するために必要な情報です。
MSI-X Table BAR Indicatorは、MSI-X Tableが格納されているBAR(Base Address Register)のindexを表しており、MSI-X Table OffsetはそのBAR内でのオフセットを示しています。
雑に表すと、下記の計算でMSI-X Tableのアドレスを求めることができます。(詳細は実装の方で書きます)
BAR[MSI-X Table BAR Indicator] + MSI-X Table Offset
MSI-X PBA BAR IndicatorとMSI-X PBA Offsetは、MSI-X PBAというデータ構造のアドレスを取得するためのデータであり、それぞれのフィールドの意味合いはMSI-X Tableの時と同じです。
MSI-X Table
MSI-X Table
MSI-X Tableは、MSI-Xにおいて割り込み設定の中心的なデータ構造です。MSI-X Tableは以下のフィールドを持つエントリの配列で構成されています。
- Vector Control: 割り込みをマスクするためのコントロールビットを持つ。
- Message Data: 割り込みに関連するデータの設定を行う。
- Message Upper Address: Message Addressの上位部分。
- Message Address: 割り込みに関連するアドレスの設定を行う。
entry一つにつき、一つの割り込みの設定をすることができます。
それぞれのフィールドについて詳しく見ていきます。
Vector Control
Vector Controlは、割り込みをマスクするためのフィールドです。先ほどMSI-XのCapabilityにて、function_maskという全ての割り込みに対して一括でマスクを行うフィールドがありましたが、Vector Controlは個別の割り込み毎にマスクを設定することができます。
下記にVector Controlの構造を示しておきます。
struct vector_control {
uint32_t mask_bit : 1; // 1を設定するとマスクされる
uint32_t reserved : 31;
};
Message Data
Message Dataは、割り込みに関連するデータの設定を行います。Message DataのフォーマットはPCI規格で決められているわけではなく、CPU側の仕様として決められています。
今回紹介するフォーマットは、x64の仕様として決められているものです。
Message Data Layout (x64)
Message Dataは、以下のフィールドで構成されています。
- Vector: 割り込みベクタを設定する。割り込みが発生したら、ここに指定したベクタに対応する割り込みハンドラが実行される。
-
Delivery Mode: CPUに割り込みがどのように配信されるかを指定する。
- Fixed: 固定配信モード。指定されたCPUコアに対して割り込みの配送を行う。
- Lowest Priority: 最低優先度配信モード。
- SMI,NMI,INIT,ExtINT: MSIではあまり使わないので省略。
-
Trigger Mode: 割り込み信号がどのように発生するかを定義する。
- Edge Triggered: 割り込み信号のエッジ(信号の変化)によって割り込みが発生する。立ち上がりエッジ、または立ち下がりエッジの際に割り込みがトリガーされる。
- Level Triggered: 割り込み信号のレベル(信号の状態)が割り込みを発生させる。信号が特定のレベルに保持されている間、割り込みがアクティブになる。
-
Level for Trigger Mode: Trigger Modeの値によって意味合いが変わる、読み込み専用のフィールド。
- Edge Triggeredの場合: ビットが1の場合は信号がアサートされている状態、0の場合はネゲートされた状態を示す。
- Level Triggeredの場合: 1でも0でも意味をなさない。
Message Address
Message Addressは、割り込みが送信される場所の設定を行います。こちらもMessage Dataと同様にCPUの仕様として定められています。なお、x64ではMessage Upper Addressは使用しないため、特に言及はしません。
Message Address Layout (x64)
Message Addressは、以下のフィールドで構成されています。
- Destination ID: 割り込みの送信先となるプロセッサ(CPUコア)の指定をする。具体的には、APIC IDというプロセッサを一意に識別するIDを設定する。
-
RH(Redirection Hint): 割り込みが送信される時の、ルーティングヒントを提供する。
- 0: 固定ルーティング。Destination IDで指定されたプロセッサに割り込みが送信される。
- 1: リダイレクトルーティング。メッセージが最適なプロセッサにリダイレクトされる可能性がある。
-
DM(Destination Mode): 割り込みの送信モードの指定を行う。
- 0: 物理モード。Destination IDで指定されたプロセッサのみに割り込みの送信を行う。
- 1: 論理モード。論理的なグループ内の複数のプロセッサにメッセージを送信することができる。特定のプロセッサグループに割り込みを送信する際に使用される。
CapabilityとMSI-X Tableを正しく設定すれば、割り込みは動作するようになります。
MSI-X PBA
MSI-X PBA
MSI-X PBA(Pending Bit Array) は、各割り込みの発生状況を管理するためデータ構造です。
デバイスが生成した割り込み要求が処理中であるか、または処理されずに保留されているかを示すビットマップになっています。
例えば、64個の割り込みベクタを持つデバイスでは、PBAは64bitのビットマップとして実装されます。以下に簡単な例を示します。
割り込みベクタ | 状態 |
---|---|
0 | 1(保留中) |
1 | 0(クリア) |
... | ... |
63 | 0(クリア) |
割り込みが発生すると、対応するベクタのビットに1が設定されます。割り込みが終了すると、そのビットは0に設定されます。上図では、ベクタ0の割り込みが発生してまだ処理されていないことを示しています。
PBAを用いることで、割り込みコントローラやデバイスドライバはどの割り込みが保留中であるかを容易に確認でき、必要に応じて割り込みを再送信することができます。
実装
下記に実装を示します。(かなり簡潔に書いています)
struct msi_x_capability {
union {
uint32_t data;
struct {
uint32_t cap_id : 8;
uint32_t next_ptr : 8;
uint32_t size_of_table : 11;
uint32_t reserved : 3;
uint32_t function_mask : 1;
uint32_t enable : 1;
} __attribute__((packed)) bits;
} header;
union address_field {
uint32_t data;
struct {
uint32_t bar : 3;
uint32_t offset : 29;
} __attribute__((packed)) bits;
} table, pba;
};
struct msix_table_entry {
uint32_t msg_addr;
uint32_t msg_upper_addr;
uint32_t msg_data;
uint32_t vector_control;
} __attribute__((packed));
データ構造の定義です。
msi_x_capability read_msi_x_capability(const device& dev, uint8_t cap_addr)
{
msi_x_capability msix_cap{};
msix_cap.header.data = read_conf_reg(dev, cap_addr);
msix_cap.table.data = read_conf_reg(dev, cap_addr + 4);
msix_cap.pba.data = read_conf_reg(dev, cap_addr + 8);
return msix_cap;
}
void configure_msi_x_register(const device& dev, uint8_t cap_addr,uint32_t msg_addr, uint32_t msg_data)
{
// 1
auto msix_cap = read_msi_x_capability(dev, cap_addr);
// 2
msix_cap.header.bits.enable = 1;
msix_cap.header.bits.function_mask = 0;
// 3
write_conf_reg(dev, cap_addr, msix_cap.header.data);
// 4
configure_msi_x_table_entry(dev, msix_cap, cap_addr, msg_addr, msg_data);
}
- Capabilityのアドレスを用いて、MSI-X Capabilityを取得。
- MSI-Xを有効にし、割り込みのマスクをオフに設定。
- 2で変更した設定を反映。
- MSI-X Tableにエントリを登録する
void configure_msi_x_table_entry(
const device& dev,
const msi_x_capability& msix_cap,
uint8_t cap_addr,
uint32_t msg_addr,
uint32_t msg_data
)
{
// 1
uint64_t bar_addr = read_base_address_register(dev, msix_cap.table.bits.bar);
// 2
bar_addr &= ~0xfff;
bar_addr += msix_cap.table.bits.offset << 3;
auto* table_entry = reinterpret_cast<msix_table_entry*>(bar_addr);
// 3
for (size_t i = 0; i <= msix_cap.header.bits.size_of_table; ++i) {
if (table_entry[i].msg_addr != 0) {
continue;
}
table_entry[i].msg_addr = msg_addr;
table_entry[i].msg_upper_addr = 0;
table_entry[i].msg_data = msg_data;
table_entry[i].vector_control = 0;
break;
}
// 4
asm volatile("mfence" ::: "memory");
}
- MSI-X CapabilityのMSI-X Table BAR Indicatorで示されたBARを取得する。
- BARのアドレスを4KiB境界でアラインしたものとオフセットを加算して、MSI-X Tableのアドレスを取得する。
- Tableの空いているところにentryを追加する。
- メモリバリアを実行。
この書き込みが完了すれば、登録したベクタの割り込みが発生するようになります。
自分のOSでも割り込みが発生することを確認できました。ハンドラの処理をちゃんと書いていないので、適当なところに印字させているだけですが。
実装の紹介をかなり端折ったので、詳しく見たい方のために下にコードを乗せておきます。
注意点
Specifies the memory address offset for the MSI-X Table relative to the BAR base address value of the BAR number specified in MSI-X Table BAR Indicator,[2:0] above. The address is extended by appending 3 zeros to create quad-word alignment.
注意点というよりは完全に私がハマったところなのですが、MSI-X Tableのオフセットは、クワッドワードのアラインメントを確保するために、オフセット値を3ビット左シフトさせる必要があります。(私はこの仕様を見落としていて、はじめ全然動きませんでした。)
おわりに
仕様書はちゃんと読もう...!
参考
Discussion