linux kernel 2.6.35中RFS特性詳解

來源:互聯網
上載者:User

轉載自www.pagefault.info

  前面介紹過google對核心協議棧的patch,RPS,它主要是為了非強制中斷的負載平衡,這次繼續來介紹google 的對RPS的增強path RFS(receive flow steering),RPS是把非強制中斷map到對應cpu,而這個時候還會有另外的效能影響,那就是如果應用程式所在的cpu和非強制中斷處理的cpu不是同一個,此時對於cpu cache的影響會很大。這裡要注意,在kernel的2.6.35中這兩個patch已經加入了。

  OK,先來描述下它是怎麼做的,其實這個補丁很簡單,想對於rps來說就是添加了一個cpu的選擇,也就是說我們需要根據應用程式的cpu來選擇非強制中斷需要被處理的cpu。這裡做法是當調用recvmsg的時候,應用程式的cpu會被儲存在一個hash table中,而索引是根據socket的rxhash進行計算的。而這個rxhash就是RPS中計算得出的那個skb的hash值。

  可是這裡會有一個問題,那就是當多個線程或者進程讀取相同的socket的時候,此時就會導致cpu id不停的變化,從而導致大量的OOO的資料包(這是因為cpu id變化,導致下面非強制中斷不停的切換到不同的cpu,此時就會導致大量的亂序的包).

  而RFS是如何解決這個問題的呢,它做了兩個表rps_sock_flow_table和rps_dev_flow_table,其中第一個rps_sock_flow_table是一個全域的hash表,這個錶針對socket的,映射了socket對應的cpu,這裡的cpu就是應用程式層期待非強制中斷所在的cpu。

struct rps_sock_flow_table {  unsigned int mask;  //hash表  u16 ents[0];};


  可以看到它有兩個域,其中第一個是掩碼,用於來計算hash表的索引,而ents就是儲存了對應socket的cpu。

  然後是rps_dev_flow_table,這個是針對裝置的,每個裝置隊列都含有一個rps_dev_flow_table(這個表主要是儲存了上次處理相同連結上的skb所在的cpu),這個hash表中每一個元素包含了一個cpu id,一個tail queue的計數器,這個值是一個很關鍵的值,它主要就是用來解決上面大量OOO的資料包的問題的,它儲存了當前的dev flow table需要處理的資料包的尾部計數。接下來我們會詳細分析這個東西。

struct netdev_rx_queue {  struct rps_map *rps_map;  //每個裝置的隊列儲存了一個rps_dev_flow_table  struct rps_dev_flow_table *rps_flow_table;  struct kobject kobj;  struct netdev_rx_queue *first;  atomic_t count;} ____cacheline_aligned_in_smp;


struct rps_dev_flow_table {  unsigned int mask;  struct rcu_head rcu;  struct work_struct free_work;  //hash表  struct rps_dev_flow flows[0];};


struct rps_dev_flow {  u16 cpu;  u16 fill;  //tail計數。  unsigned int last_qtail;};


  首先我們知道,大量的OOO的資料包的引起是因為多個進程同時請求相同的socket,而此時會導致這個socket對應的cpu id不停的切換,然後非強制中斷如果不做處理,只是簡單的調度非強制中斷到不同的cpu,就會導致順序的資料包被分發到不同的cpu,由於是smp,因此會導致大量的OOO的資料包,而在RFS中是這樣解決這個問題的,在soft_net中添加了2個域,input_queue_head和input_queue_tail,然後在裝置隊列中添加了rps_flow_table,而rps_flow_table中的元素rps_dev_flow包含有一個last_qtail,RFS就通過這3個域來控制亂序的資料包。

  這裡為什麼需要3個值呢,這是因為每個cpu上的隊列的個數input_queue_tail是一直增加的,而裝置每一個隊列中的flow table對應的skb則是有可能會被調度到另外的cpu,而dev flow table的last_qtail表示當前的flow table所需要處理的資料包隊列(backlog queue)的尾部隊列計數,也就是說當input_queue_head大於等於它的時候說明當前的flow table可以切換了,否則的話不能切換到進程期待的cpu。

  不過這裡還要注意就是最好能夠綁定進程到指定的cpu(配合rps和rfs的參數設定),這樣的話,rfs和rps的效率會更好,所以我認為像erlang這種在rfs和rps下效能應該提高非常大的.
下面就是softnet_data 的結構。

struct softnet_data {  struct Qdisc *output_queue;  struct Qdisc **output_queue_tailp;  struct list_head poll_list;  struct sk_buff *completion_queue;  struct sk_buff_head process_queue;  /* stats */  unsigned int processed;  unsigned int time_squeeze;  unsigned int cpu_collision;  unsigned int received_rps;#ifdef CONFIG_RPS  struct softnet_data *rps_ipi_list;  /* Elements below can be accessed between CPUs for RPS */  struct call_single_data csd ____cacheline_aligned_in_smp;  struct softnet_data *rps_ipi_next;  unsigned int cpu;  //最關鍵的兩個域  unsigned int input_queue_head;  unsigned int input_queue_tail;#endif  unsigned dropped;  struct sk_buff_head input_pkt_queue;  struct napi_struct backlog;};


  接下來我們來看代碼,來看核心是如何?的,先來看inet_recvmsg,也就是調用rcvmsg時,核心會調用的函數,這個函數比較簡單,就是多加了一行代碼sock_rps_record_flow,這個函數主要是將本socket和cpu設定到rps_sock_flow_table這個hash表中。

  首先要提一下,這裡這兩個flow table的初始化都是放在sys中初始化的,不過sys部分相關的代碼我就不分析了,因為具體的邏輯和原理都是在協議棧部分實現的。

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;  //設定hash表  sock_rps_record_flow(sk);  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;}


  然後就是rps_record_sock_flow,這個函數主要是得到全域的rps_sock_flow_table,然後調用rps_record_sock_flow來對rps_sock_flow_table進行設定,這裡會將socket的sk_rxhash傳遞進去當作hash的索引,而這個sk_rxhash其實就是skb裡面的rxhash,skb的rxhash就是rps中設定的hash值,這個值是根據四元組進行hash的。這裡用這個當索引一個是為了相同的socket都能落入一個index。而且下面的非強制中斷上下文也比較容易存取這個hash表。

struct rps_sock_flow_table *rps_sock_flow_table __read_mostly;static inline void sock_rps_record_flow(const struct sock *sk){#ifdef CONFIG_RPS  struct rps_sock_flow_table *sock_flow_table;  rcu_read_lock();  sock_flow_table = rcu_dereference(rps_sock_flow_table);  //設定hash表  rps_record_sock_flow(sock_flow_table, sk->sk_rxhash);  rcu_read_unlock();#endif}


其實所有的事情都是rps_record_sock_flow中做的

static inline void rps_record_sock_flow(struct rps_sock_flow_table *table, u32 hash){  if (table && hash) {    //擷取索引。    unsigned int cpu, index = hash & table->mask;    /* We only give a hint, preemption can change cpu under us */    //擷取cpu    cpu = raw_smp_processor_id();    //儲存對應的cpu,如果等於當前cpu,則說明已經設定過了。    if (table->ents[index] != cpu)    //否則設定cpu    table->ents[index] = cpu;  }}


  上面是進程上下文做的事情,也就是設定對應的進程所期待的cpu,它用的是rps_sock_flow_table,而接下來就是非強制中斷上下文了,rfs這個patch主要的工作都是在非強制中斷上下文做的。不過看這裡的代碼之前最好能夠瞭解下RPS補丁,因為RFS就是對rps做了一點小改動。

  主要是兩個函數,第一個是enqueue_to_backlog,這個函數我們知道是用來將skb掛在到對應cpu的input queue上的,這裡我們就關注他的一個函數就是input_queue_tail_incr_save,他就是更新裝置的input_queue_tail以及softnet_data的input_queue_tail。

if (skb_queue_len(&sd->input_pkt_queue)) {  enqueue:    __skb_queue_tail(&sd->input_pkt_queue, skb);    //這個函數更新對應裝置的rps_dev_flow_table中的input_queue_tail以及dev flow table的last_qtail    input_queue_tail_incr_save(sd, qtail);    rps_unlock(sd);    local_irq_restore(flags);    return NET_RX_SUCCESS;}


  第二個是get_rps_cpu,這個函數我們知道就是得到非強制中斷應該啟動並執行cpu,這裡我們就看RFS添加的部分,這裡它是這樣計算的,首先會得到兩個flow table,一個是sock_flow_table,另一個是裝置的rps_flow_table(skb對應的裝置隊列中對應的flow table),這裡的邏輯是這樣子的取出來兩個cpu,一個是根據rps計算資料包前一次被調度過的cpu(tcpu),一個是應用程式期望的cpu(next_cpu),然後比較這兩個值,如果 1 tcpu未設定(等於RPS_NO_CPU)
2 tcpu是離線的 3 tcpu的input_queue_head大於rps_flow_table中的last_qtail 的話就調度這個skb到next_cpu.
而這裡第三點input_queue_head大於rps_flow_table則說明在當前的dev flow table中的資料包已經發送完畢,否則的話為了避免亂序就還是繼續使用tcpu.

got_hash:flow_table = rcu_dereference(rxqueue->rps_flow_table);sock_flow_table = rcu_dereference(rps_sock_flow_table);if (flow_table && sock_flow_table) {  u16 next_cpu;  struct rps_dev_flow *rflow;  //得到flow table  rflow = &flow_table->flows[skb->rxhash & flow_table->mask];  tcpu = rflow->cpu;  /得到next_cpu  next_cpu = sock_flow_table->ents[skb->rxhash & sock_flow_table->mask];  //條件  if (unlikely(tcpu != next_cpu) &&     (tcpu == RPS_NO_CPU || !cpu_online(tcpu) ||     ((int)(per_cpu(softnet_data, tcpu).input_queue_head -     rflow->last_qtail)) >= 0)) {    //設定tcpu    tcpu = rflow->cpu = next_cpu;    if (tcpu != RPS_NO_CPU)    //更新last_qtail    rflow->last_qtail = per_cpu(softnet_data, tcpu).input_queue_head;  }  if (tcpu != RPS_NO_CPU && cpu_online(tcpu)) {    *rflowp = rflow;    //設定返回cpu,以供非強制中斷重新調度    cpu = tcpu;    goto done;  }}

………………………………

  最後我們來分析下第一次資料包到達協議棧而應用程式還沒有調用rcvmsg讀取資料包,此時會發生什麼問題,當第一次進來時tcpu是RPS_NO_CPU,並且next_cpu也是RPS_NO_CPU,此時會導致跳過rfs處理,而是直接使用rps的處理,也就是上面代碼的緊接著的部分,下面這段代碼前面rps時已經分析過了,這裡就不分析了。

map = rcu_dereference(rxqueue->rps_map);if (map) {  tcpu = map->cpus[((u64) skb->rxhash * map->len) >> 32];  if (cpu_online(tcpu)) {    cpu = tcpu;    goto done;  }}

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.