RFS 理解

lxgeek發表於2014-12-24
1.背景
網路卡接收一個資料包的情況下,會經過三個階段:
 
- 網路卡產生硬體中斷通知CPU有包到達
- 通過軟中斷處理此資料包
- 在使用者態程式處理此資料包
 
在SMP體系下,這三個階段有可能在3個不同的CPU上處理,如下圖所示:
 
而RFS的目標就是增加CPU快取的命中率從而提高網路延遲。當使用RFS後,其效果如下:
2.實現原理
當使用者程式呼叫 revmsg() 或者 sendmsg()的時候,RFS會將此使用者程式執行的CPU id存入hash表;
而當有關使用者程式的資料包到達的時候,RFS嘗試從hash表中取出相應的CPU id, 並將資料包放置
到此CPU的佇列,從而對效能進行優化。
 
3.重要資料結構
/*
* The rps_sock_flow_table contains mappings of flows to the last CPU
* on which they were processed by the application (set in recvmsg).
*/
struct rps_sock_flow_table {
    unsigned int mask;
    u16 ents[0];
};
#define RPS_SOCK_FLOW_TABLE_SIZE(_num) (sizeof(struct rps_sock_flow_table) + \
    ((_num) * sizeof(u16)))
View Code
結構體 rps_sock_flow_table 實現了一個hash表,RFS會將其宣告一個全域性變數用於存放所有sock對應的CPU。
 
/*
* The rps_dev_flow structure contains the mapping of a flow to a CPU, the
* tail pointer for that CPU's input queue at the time of last enqueue, and
* a hardware filter index.
*/
struct rps_dev_flow {
    u16 cpu;     //此鏈路上次使用的cpu
    u16 filter;
    unsigned int last_qtail;   //此裝置佇列入隊的sk_buff的個數
};
#define RPS_NO_FILTER 0xffff

/*
* The rps_dev_flow_table structure contains a table of flow mappings.
*/
struct rps_dev_flow_table {
    unsigned int mask;
    struct rcu_head rcu;
    struct rps_dev_flow flows[0]; //實現hash表
};     
#define RPS_DEV_FLOW_TABLE_SIZE(_num) (sizeof(struct rps_dev_flow_table) + \
    ((_num) * sizeof(struct rps_dev_flow)))
View Code
結構體 rps_dev_flow_table 是針對一個裝置佇列
 
4.具體實現
使用者程式使用revmsg() 或者 sendmsg()的時候 設定CPU id。
int inet_recvmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg,
         size_t size, int flags)
{  
    struct sock *sk = sock->sk;
    int addr_len = 0;
    int err;
   
    sock_rps_record_flow(sk);   //設定CPU id
   
    err = sk->sk_prot->recvmsg(iocb, sk, msg, size, flags & MSG_DONTWAIT,
                   flags & ~MSG_DONTWAIT, &addr_len);
    if (err >= 0)
        msg->msg_namelen = addr_len;
    return err;
}
EXPORT_SYMBOL(inet_recvmsg);
View Code
當有資料包進行了響應後,會呼叫get_rps_cpu()選擇合適的CPU id。其關鍵程式碼如下:
3117     hash = skb_get_hash(skb);
3118     if (!hash)
3119         goto done;
3120
3121     flow_table = rcu_dereference(rxqueue->rps_flow_table);     //裝置佇列的hash表
3122     sock_flow_table = rcu_dereference(rps_sock_flow_table);    //全域性的hash表
3123     if (flow_table && sock_flow_table) {
3124         u16 next_cpu;
3125         struct rps_dev_flow *rflow;
3126
3127         rflow = &flow_table->flows[hash & flow_table->mask];
3128         tcpu = rflow->cpu;  
3129
3130         next_cpu = sock_flow_table->ents[hash & sock_flow_table->mask];   //得到使用者程式執行的CPU id
3131
3132         /*
3133          * If the desired CPU (where last recvmsg was done) is
3134          * different from current CPU (one in the rx-queue flow
3135          * table entry), switch if one of the following holds:
3136          *   - Current CPU is unset (equal to RPS_NO_CPU).
3137          *   - Current CPU is offline.
3138          *   - The current CPU's queue tail has advanced beyond the
3139          *     last packet that was enqueued using this table entry.
3140          *     This guarantees that all previous packets for the flow
3141          *     have been dequeued, thus preserving in order delivery.
3142          */
3143         if (unlikely(tcpu != next_cpu) &&
3144             (tcpu == RPS_NO_CPU || !cpu_online(tcpu) ||
3145              ((int)(per_cpu(softnet_data, tcpu).input_queue_head -
3146               rflow->last_qtail)) >= 0)) {
3147             tcpu = next_cpu;
3148             rflow = set_rps_cpu(dev, skb, rflow, next_cpu);
3149         }
3150
3151         if (tcpu != RPS_NO_CPU && cpu_online(tcpu)) {
3152             *rflowp = rflow;
3153             cpu = tcpu;
3154             goto done;
3155         }
3156     }
View Code
上面的程式碼中第3145行比較難理解,資料結構 softnet_data用於管理進出的流量,他有兩個關鍵的變數:
2374 #ifdef CONFIG_RPS
2375     /* Elements below can be accessed between CPUs for RPS */
2376     struct call_single_data csd ____cacheline_aligned_in_smp;
2377     struct softnet_data *rps_ipi_next;
2378     unsigned int        cpu;
2379     unsigned int        input_queue_head;   //佇列頭,也可以理解為出隊的位置
2380     unsigned int        input_queue_tail;     //佇列尾,也可以理解為入隊的位置 
2381 #endif
View Code
表示式 (int)(per_cpu(softnet_data, tcpu).input_queue_head 求出了在tcpu 這個CPU上的出隊數目,而rflow->last_qtail
代表裝置佇列上此sock對應的最後入隊的位置,如果出隊數目大於入隊數目,那麼說明這一鏈路上的包都處理完畢,不會
出現亂序處理的包。第3143的if 語句就是為了防止亂序包的出現,假如是多程式或者多執行緒同時處理一個socket,那麼此
socket對應的CPU id就會不停變化。
 
 
參考文獻: