本文鏈接地址: linux kernel 2.6.35中RFS特性詳解
前面我介紹過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。
1
2
3
4
5
|
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需要處理的數據包的尾部計數。接下來我們會詳細分析這個東西。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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 的結構。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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部分相關的代碼我就不分析了,因爲具體的邏輯和原理都是在協議棧部分實現的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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表。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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中做的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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。
1
2
3
4
5
6
7
8
9
|
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
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時已經分析過了,這裏就不分析了。
1
2
3
4
5
6
7
8
9
|
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; } } |