livaの雑記帳

qemuのNVMeが微妙なlegacy割り込みをする話

 

こんな呟きを見かけたので、原因究明しました。uio_pci_genericに濡れ衣を着せられているような気がして、uioがとてもとても可哀想そうだったからです。

ただ、いくら僕がuioを心から愛しているとはいえ、原因を探っていたら土曜日が消えてしまって大変辛いので、ブログに書いて供養したいと思います。

 

前提

  • QEMU上でLinuxを動かし、uio_pci_genericを用いてNVMeデバイスを制御している。
  • NVMeデバイスはエミュレーションである(PCIパススルーではない)
  • QEMUのバージョンは2.9.0
  • Linuxカーネルは4.4.0(uioは4.4.0付属のもの)
  • uioなので、割り込みはLegacyなpin-based割り込み(MSIMSI-Xでない)

 

QEMUの挙動

とりあえずQEMUのNVMeのエミュレーションコードで、割り込み周りを探すとこんなコードが見つかります。

 

hw/block/nvme.c

static void nvme_isr_notify(NvmeCtrl *n, NvmeCQueue *cq)

{

    if (cq->irq_enabled) {

        if (msix_enabled(&(n->parent_obj))) {

            msix_notify(&(n->parent_obj), cq->vector);

        } else {

            pci_irq_pulse(&n->parent_obj);

        }

    }

}

 

 

pci_irq_pulseという関数がありますね。

 

include/hw/pci/pci.h

/*

* FIXME: PCI does not work this way.

* All the callers to this method should be fixed.

*/

static inline void pci_irq_pulse(PCIDevice *pci_dev)

{

    pci_irq_assert(pci_dev);

    pci_irq_deassert(pci_dev);

}

 

pci_irq_pulseはPCI Device Status Register内にある、Interrupt Status Bitを立てて、直後に下げます。(pci_irq_assertやpci_irq_deassertはhw/pci/pci.c内のpci_set_irq経由でpci_irq_handlerを呼び、pci_update_irq_statusでInterrupt Status Bitを立てます

 

で、まあFIXMEなんてコメントがついているわけですが、このFIXMEに書かれている事が本質的な答えです。

 

とはいえ、ひとまず話を続けましょう。

pci_irq_assertによってInterrupt Status Bitが立つので、QEMUはゲストOSに対して割り込みを発生させます。

 

uioの挙動

割り込みが発生すると、drivers/uio/uio.c内のuio_interrupt経由でdrivers/uio/uio_pci_generic.c内のirqhandlerが呼ばれます。

/* Interrupt handler. Read/modify/write the command register to disable                                                                                                                                     

* the interrupt. */

static irqreturn_t irqhandler(int irq, struct uio_info *info)

{

  struct uio_pci_generic_dev *gdev = to_uio_pci_generic_dev(info);

 

  if (!pci_check_and_mask_intx(gdev->pdev))

    return IRQ_NONE;

 

  /* UIO core will signal the user process. */

  return IRQ_HANDLED;

}

で、drivers/pci/pci.c内にあるpci_check_and_mask_intxは同じファイル内のpci_check_and_set_intx_maskを呼ぶわけですが・・・

static bool pci_check_and_set_intx_mask(struct pci_dev *dev, bool mask)

{

struct pci_bus *bus = dev->bus;

bool mask_updated = true;

u32 cmd_status_dword;

u16 origcmd, newcmd;

unsigned long flags;

bool irq_pending;

 

/*

* We do a single dword read to retrieve both command and status.

* Document assumptions that make this possible.

*/

BUILD_BUG_ON(PCI_COMMAND % 4);

BUILD_BUG_ON(PCI_COMMAND + 2 != PCI_STATUS);

 

raw_spin_lock_irqsave(&pci_lock, flags);

 

bus->ops->read(bus, dev->devfn, PCI_COMMAND, 4, &cmd_status_dword);

 

irq_pending = (cmd_status_dword >> 16) & PCI_STATUS_INTERRUPT;

 

/*

* Check interrupt status register to see whether our device

* triggered the interrupt (when masking) or the next IRQ is

* already pending (when unmasking).

*/

if (mask != irq_pending) {

mask_updated = false;

goto done;

}

 

origcmd = cmd_status_dword;

newcmd = origcmd & ~PCI_COMMAND_INTX_DISABLE;

if (mask)

newcmd |= PCI_COMMAND_INTX_DISABLE;

if (newcmd != origcmd)

bus->ops->write(bus, dev->devfn, PCI_COMMAND, 2, newcmd);

 

done:

raw_spin_unlock_irqrestore(&pci_lock, flags);

 

return mask_updated;

}

 

 中でPCI_STATUS_INTERRUPTが立っているかどうかを確認して、立っていなかったら(mask != irq_pending)、割り込みが発生してない旨を返しています。

 

これによってirqhandlerがIRQ_NONEを返してしまい、uioに対して割り込み待ちを掛けていて、かつゲストOSには割り込みが届いているにも関わらず、NVMeドライバにそれが通知されないわけですね。

 

試したくなる解決策1

Ahhhhh!!!! F*ck UIO!!!!と叫んだ人がいるかは分からないですが、これは割り込みが通知されているにも関わらず、きちんと通知しないuioのバグだろう、だからuioを修正するんじゃ、という解決策が思い浮かびます。

 

が、これはダメです。

 

なんでuio_pci_genericがpci_check_and_mask_intxをわざわざ呼んでいるかを考えましょう。PCIのlegacyな割り込みは、複数のデバイスが割り込み線を共有しているため、割り込みが飛んできてもデバイスドライバはそれが自分のデバイス宛の割り込みかどうかを確認しなければいけません。その確認というのがInterrupt Status Bitが立っているかどうかを読む、という物です。

つまり、irqhandlerがやっている事、ないしはpci_check_and_set_intx_maskがやっている事は全くもって正しいわけですね。

 

これはむしろ、pci_irq_pulseで、assert後にすぐdeassertしてしまうNVMeドライバが悪い!

 

試したくなる解決策2

じゃあpci_irq_pulseをpci_irq_assertに書き換えればいいじゃないか、と思うかもしれませんが、そうは問屋が卸しません。

 

pci_irq_assertしたら、どこかでpci_irq_deassertをする必要があります。PCIのlegacy割り込みはlevel triggerなので、deassertしないと無限に割り込みが飛んできます。

ならばどこかでdeassertすればいいわけですが、これが難しい。

 

QEMUxHCIエミュレーションを読むと、InterrupterのInterrupt Pendingフラグがクリアされた(デバドラが1を書き込んだ)時にpci_irq_deassert相当の事をやろうとしており、これは実機の挙動とも一致します。

一方で、NVMeの実機の挙動を見てみると、PCIの割り込みが届いて、PCI割り込みをdisableするとInterrupt Status Bitが下がってしまいます。つまり、デバイスレジスタ空間のステート遷移によってInterrupt Status Bitを下げるのではなく、PCIレジスタ空間のステート遷移によってBitを下げていると思われます。(ココらへん若干怪しいので、間違ってたらごめんなさい)

 

一方で、QEMUPCI割り込みがdisableされても、それをデバイスをエミュレーションしているクラスに通知する機構がありません。このため、QEMUのNVMeドライバを弄ってpci_irq_deassertを発行するのは正直難しいと思います。たぶんNVMeエミュレーションを書いたIntelの人も、そこら辺を悩んだ上でpci_irq_pulseを使うという暴挙に出たのでしょう。

 

本当の解決策

NVMeにおいて、本当にPCI割り込みがdisableになる事でInterrupt Status Bitを下げるのなら、QEMUPCIエミュレーションの中にデバイスに対して割り込み禁止を通知する機構を実装するのが良いでしょう。ただし、本質的な解決策ではあっても、あまり綺麗に実装できる予感がしないので、僕は遠慮しておきます。

 

現実的な解決策

実機の挙動と変わってしまいますが、INTMCというNVMeのレジスタに対する書き込みによってInterrupt Status Bitを下げるのが最も良さそうに僕は思います。

たぶん世の中のデバドラはこの修正を施してもうまく動くのではないでしょうか。

 追記:ここら辺だいぶ嘘書いてます、ごめんなさい、NVMeの仕様をちゃんと読んでればきちんとした解決策を思いつけるみたいです。by hikalium

 

真の解決策

そもそも論として、なぜこの問題が表面化していないかというと、今時NVMeのデバドラでlegacyな割り込みを使う物がないからです。まあIntelの人も「どーせpin-based割り込みなんて使われないし、適当な実装でいいや」って思って実装したんじゃないですかね。

だからMSIを使う、というのが真の解決策です。

え?uioでMSIが使えない?こんなパッチがあるじゃろ?なんで適用されてないのかよく分からんけど(流し読みした限りだとセキュリティが云々って言ってるけど、英語読むの面倒になったので誰かやり取り要約して欲しい)、まあ手元で適用したuioデバドラを動かせばいいんじゃないでしょうか。

 

ところで、vfioだったらMSIフルサポートだったりしませんかね?もしそうなら乗り換えようかな。vfioはIOMMU(Vt-D)がないと使えないっぽいから採用を見送ったのだけど、No-IOMMU modeなんてのもあるらしいし???