Ethernet device polling(1)


FreeBSDでは、イーサネット高速化の為にdevice pollingという機構を実装している。
ギガビットイーサや10ギガビットイーサでは割り込み処理が重すぎる為、割り込みなど使わずにどんどんOS側からデータを取りに行った方が速いから、というのが理由らしい。
やってる事は要するに極めて短い間隔でcalloutかsoftintをかけているようなもんだが、もう少しマシなインタフェースを持たせ、オーバヘッドを最小限に留める工夫をしている(と、思う)。

どのような仕組みになっているか見ていこう。

イーサーネットデバイスドライバへは、以下の2つの関数が提供されている:

int
ether_poll_register(poll_handler_t *h, struct ifnet *ifp);

int
ether_poll_deregister(struct ifnet *ifp);

見れば分かるが、polling関数の追加と削除の機能を持つ。
dev/bge/if_bge.cのbge_ioctl()では、以下のように使われている:

        case SIOCSIFCAP:
                mask = ifr->ifr_reqcap ^ ifp->if_capenable;
#ifdef DEVICE_POLLING
                if (mask & IFCAP_POLLING) {
                         /* インタフェースにIFCAP_POLLINGフラグが立っていれば、
                            bge_poll()をpolling用関数として登録する */
                        if (ifr->ifr_reqcap & IFCAP_POLLING) {
                                error = ether_poll_register(bge_poll, ifp);
                                if (error)
                                        return (error);
                                BGE_LOCK(sc);
                                BGE_SETBIT(sc, BGE_PCI_MISC_CTL,
                                    BGE_PCIMISCCTL_MASK_PCI_INTR);
                                bge_writembx(sc, BGE_MBX_IRQ0_LO, 1);
                                ifp->if_capenable |= IFCAP_POLLING;
                                BGE_UNLOCK(sc);

                         /* インタフェースにIFCAP_POLLINGフラグが立っていなければ、
                            ether_pollからインタフェースを登録解除する */
                        } else {
                                error = ether_poll_deregister(ifp);
                                /* Enable interrupt even in error case */
                                BGE_LOCK(sc);
                                BGE_CLRBIT(sc, BGE_PCI_MISC_CTL,
                                    BGE_PCIMISCCTL_MASK_PCI_INTR);
                                bge_writembx(sc, BGE_MBX_IRQ0_LO, 0);
                                ifp->if_capenable &= ~IFCAP_POLLING;
                                BGE_UNLOCK(sc);
                        }
                }
#endif

bge_poll()の行う処理自体は割り込みハンドラとそう変わらないので省略する。

今度は、kern_poll.cを見てみる。
初期化関数であるinit_device_poll()は、以下のようになっている:

static void
init_device_poll(void)
{
        /* mutex変数の初期化 */
        mtx_init(&poll_mtx, "polling", NULL, MTX_DEF);
        /* netisrへnetisr_pollを登録 */
        netisr_register(NETISR_POLL, (netisr_t *)netisr_poll, NULL,
            NETISR_MPSAFE);
        /* netisrへnetisr_pollmoreを登録 */
        netisr_register(NETISR_POLLMORE, (netisr_t *)netisr_pollmore, NULL,
            NETISR_MPSAFE);
}
/* このマクロでカーネル起動時にinit_main.cから呼ばれるようになる */
SYSINIT(device_poll, SI_SUB_CLOCKS, SI_ORDER_MIDDLE, init_device_poll, NULL);

netisrへnetisr_pollとnetisr_pollmoreを登録したので、schednetisr()が呼ばれた次のタイムスライスでこの関数が呼ばれる事になる。

net/netisr.hを見てみると、NETISR_POLLは最小値で0、NETISR_POLLMOREは最大値で31なので、優先度はpollが最優先、pollmoreは最低になっている。

では、ether_poll_register()で登録されたbge_poll()がどのような手順で実行される事になるか見ていく。

kern/kern_clock.cのhardclock()は1tick毎(1/HZ秒毎)に実行される関数である。
ここから時間の生成やプロセススケジューリングが行われている。
この関数の真ん中辺りをみてみると、

#ifdef DEVICE_POLLING
        hardclock_device_poll();        /* this is very short and quick */
#endif /* DEVICE_POLLING */

のようにkern_poll.c内の関数を直接呼んでいる事が分かる。
以下がhardclock_device_poll()の中身:

/*
 * Hook from hardclock. Tries to schedule a netisr, but keeps track
 * of lost ticks due to the previous handler taking too long.
 *
 * hardclockからフックされる。 
 * 前のハンドラの実行に時間がかかりすぎた事によるtickのロスの動向を見ながら
 * netisrをスケジュールを行う。
 *
 * Normally, this should not happen, because polling handler should
 * run for a short time. However, in some cases (e.g. when there are
 * changes in link status etc.) the drivers take a very long time
 * (even in the order of milliseconds) to reset and reconfigure the
 * device, causing apparent lost polls.
 *
 * 通常はこれは起きてはならない。なぜなら、ポーリングハンドラは短い時間で実行される
 * べきものだから。 しかし、幾つかのケース(リンクステータスが変化したときなど)では
 * リセットと再設定に長い時間がかかり(時にはミリセカンド単位で時間がかかる)、
 * ポーリングの損失を発生させる。
 *
 * The first part of the code is just for debugging purposes, and tries
 * to count how often hardclock ticks are shorter than they should,
 * meaning either stray interrupts or delayed events.
 *
 * コードの最初の部分はデバッグの為だけのもので、hardclockのtickがあるべき間隔より
 * 短い事がどれくらいの頻度であるかを数えている。
 * これは、無効な割り込みが起きているかイベントが遅延している事を示している。
 */
void
hardclock_device_poll(void)
{
        static struct timeval prev_t, t;
        int delta;

        /* poll handlerがなければreturn */
        if (poll_handlers == 0)
                return;

        /* 短すぎるtickかどうかを調べてカウントしている */
        microuptime(&t);
        delta = (t.tv_usec - prev_t.tv_usec) +
                (t.tv_sec - prev_t.tv_sec)*1000000;
        if (delta * hz < 500000)
                short_ticks++;
        else
                prev_t = t;

        /* pendingなpollが100を超えている場合は、stallしたとみなして
           pendingをリセットする */
        if (pending_polls > 100) {
                /*
                 * Too much, assume it has stalled (not always true
                 * see comment above).
                 */
                stalled++;
                pending_polls = 0;
                phase = 0;
        }

        /* phaseが2より小さければnetisrをスケジュールする */
        if (phase <= 2) {
                if (phase != 0)
                        suspect++;
                phase = 1;
                schednetisrbits(1 << NETISR_POLL | 1 << NETISR_POLLMORE);
                phase = 2;
        }

        /* pending_pollsが0より大きいなら、lostしたpollが存在する */
        if (pending_polls++ > 0)
                lost_polls++;
}

hardclock_device_poll()からnetisrを経由して、netisr_poll()とnetisr_pollmore()が呼ばれる。
取り合えずnetisr_poll()だけ先に読み解いてみよう:

/*
 * netisr_poll is scheduled by schednetisr when appropriate, typically once
 * per tick.
 *
 * netisr_pollはschednetisrにより、一般的に1tick毎程度の間隔でスケジュールされる。
 */
static void
netisr_poll(void)
{
        int i, cycles;
        enum poll_cmd arg = POLL_ONLY;

        mtx_lock(&poll_mtx);
        /* phaseを3にする。この間はhardclockからスケジュールされない。 */
        phase = 3;
        /* このtickで始めて呼ばれた */
        if (residual_burst == 0) { /* first call in this tick */
                microuptime(&poll_start_t);
                if (++reg_frac_count == reg_frac) {
                        arg = POLL_AND_CHECK_STATUS;
                        reg_frac_count = 0;
                }

                residual_burst = poll_burst;
        }
        cycles = (residual_burst < poll_each_burst) ?
                residual_burst : poll_each_burst;
        residual_burst -= cycles;
        /* ポーリングハンドラを全部呼ぶ */
        for (i = 0 ; i < poll_handlers ; i++)
                pr[i].handler(pr[i].ifp, arg, cycles);
        /* phaseを4にする */
        phase = 4;
        mtx_unlock(&poll_mtx);
}

今回はここまで。
よくよくみると、単純に定期的にポーリングを行うというわけではないのが分かった。
どれくらいポーリングに時間がかかっているか、tickの間隔がどれくらいずれたか、カーネルの負荷はどれくらいか、などを細かく計算して最適な間隔を動的に作り出している。
きっと、10Gイーサとかでパフォーマンスを出すためにエロい人たちが頑張ってチューニングしてんだろうなぁ、と思った。