2.6.35に実装されたRPSという機能について

2.6.35が出たので、Linux_2_6_35 - Linux Kernel Newbiesで変更点を眺めていたらRPS/RFSという機能が入ったことに気付く。
RPSとはReceive Packet Steeringの略称で、受信パケットの処理をマルチCPUで分散させることで、スループットの向上を図る機能であるらしい。パッチの提供主はGoogle
昔kernelの本を読んだときはキャッシュの有効活用の点からあまり分散させると良くないと聞いていたのだけど、8wayぐらいが普通にゴロゴロしている状況ではそうも言っていたれなのかもしれない。8コアの環境でのベンチマークでは2から3倍程度の性能は出たとのこと。
実装を見てみる。Linuxのネットワークスタックの動き自体の概観は送受信 - Linuxカーネルメモ が非常に参考になった。
機能の有効/無効はnet/Kconfigで制御しているのだが…、説明がない。
CONFIG_RPSでifdefされたところを追いかけただけなのだけれど、実装は net/core/dev.c に集中しているようだ。tcpudpの中にもRPS関連の関数を呼び出す部分が細かく追加されている。
パケットの振り分けのコアとなるはdev.cのget_rps_cpu()。コメントにもある通り、netif_receive_skbから呼ばれて処理をするCPUを返すという処理を行う。netif_receive_skb自体はNAPIの経路で使われる関数で、従来のnetif_rxから来たときにも同様に振り分け処理が実施される。
ハッシュ対象は典型的なIPv4/TCPの場合、srcとdstのIPアドレスとポート番号と対象としている。行きと帰りのパケットで同じCPUが選ばれると嬉しいので、src/dstを正規化した上でハッシュをとっているのが面白いと思った。
実際にハッシュ関数を呼んでいるのは以下の行

        skb->rxhash = jhash_3words(addr1, addr2, ports.v32, hashrnd);

呼び出し元のnetif_recieve_skbでは

int netif_receive_skb(struct sk_buff *skb)
{
        if (netdev_tstamp_prequeue)
                net_timestamp_check(skb);

#ifdef CONFIG_RPS
        {
                struct rps_dev_flow voidflow, *rflow = &voidflow;
                int cpu, ret;

                rcu_read_lock();

                cpu = get_rps_cpu(skb->dev, skb, &rflow);

                if (cpu >= 0) {
                        ret = enqueue_to_backlog(skb, cpu, &rflow->last_qtail);
                        rcu_read_unlock();
                } else {
                        rcu_read_unlock();
                        ret = __netif_receive_skb(skb);
                }

                return ret;
        }
#else
        return __netif_receive_skb(skb);
#endif
}

となっており、受信処理の実体を呼ぶところが、振り分け先のCPUが決定されればCPUごとのキューに追加する処理に置き変っている。
その他udpの中にも、net/ipv4/udp.cで

int udp_disconnect(struct sock *sk, int flags)
{
        struct inet_sock *inet = inet_sk(sk);
        /*
         *      1003.1g - break association.
         */

        sk->sk_state = TCP_CLOSE;
        inet->inet_daddr = 0;
        inet->inet_dport = 0;
        sock_rps_save_rxhash(sk, 0);

という処理が入っており、ソケットと処理CPUの対応をクリアする処理が入っていたりする(tcpも同様)。
フローやソケットごとに特定のCPUに処理を割り当ててキャッシュを有効に使いながらパケットをCPU間で上手く分散する処理だなと思った。考えていたより実装分量がずっと小さい、CPU毎に受信キューを持つ機構自体は従前からあったようなのでむしろこの分散処理が無かった方が不思議というか。