Linux 內核發送網絡數據
前言
我們開始今天對 Linux 內核⽹絡發送過程的深度剖析。還是按照我們之前的傳統,先從⼀段代碼作爲切⼊。
上述代碼中,調⽤ send 之後內核是怎麼樣把數據包發送出去的。本⽂基於 Linux 3.10,⽹卡驅動採⽤ Intel 的 igb 舉例。
基礎框架
- 我們看到⽤戶數據被拷⻉到內核態,然後經過協議棧處理後進⼊到了 RingBuffer 中。隨後⽹卡驅動真正將數據發送了出去。當發送完成的時候,是通過硬中斷來通知 CPU,然後清理 RingBuffer。
源碼流程跟蹤(梳理的是從上到下的調用流程)
應用層
1. while(1)
{
sendto(socketfd, SendBuff, sizeof(SendBuff), 0, (struct sockaddr*)
&ser_addr, sizeof(struct sockaddr));
}
系統調用
2. SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t,
len,unsigned, flags, struct sockaddr __user *, addr,int, addr_len) {
err = sock_sendmsg(sock, &msg, len); }
int sock_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{
ret = __sock_sendmsg(&iocb, sock, msg, size);
}
static inline int __sock_sendmsg(struct kiocb *iocb, struct socket *sock,struct msghdr *msg, size_t size)
{
int err = security_socket_sendmsg(sock, msg, size);
return err ?: __sock_sendmsg_nosec(iocb, sock, msg, size);
}
static inline int __sock_sendmsg_nosec(struct kiocb *iocb, struct socket *sock,
struct msghdr *msg, size_t size)
{
si->msg = msg;
si->size = size;
return sock->ops->sendmsg(iocb, sock, msg, size);
}
協議棧
3. int inet_sendmsg(struct kiocb *iocb, struct socket *sock, struct
msghdr *msg, size_t size) { return sk->sk_prot->sendmsg(iocb,
sk, msg, size); }
傳輸層
4. int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr
*msg, size_t size) { if (forced_push(tp)) {
tcp_mark_push(tp, skb);
__tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH); } else if (skb == tcp_send_head(sk))
tcp_push_one(sk, mss_now); continue; }
void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss,int nonagle)
{
if (tcp_write_xmit(sk, cur_mss, nonagle, 0, GFP_ATOMIC))
tcp_check_probe_timer(sk);
}
static int tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
int push_one, gfp_t gfp)
{
if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
break;
}
static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
gfp_t gfp_mask)
{
//調用網絡層發送接口
err = icsk->icsk_af_ops->queue_xmit(skb, &inet->cork.fl);
if (likely(err <= 0))
return err;
}
網絡層
5. int ip_queue_xmit(struct sk_buff *skb, struct flowi *fl) { res =
ip_local_out(skb); rcu_read_unlock(); return res; }
int ip_local_out(struct sk_buff *skb)
{
err = __ip_local_out(skb);
if (likely(err == 1))
err = dst_output(skb);
}
/* Output packet to network from transport. */
static inline int dst_output(struct sk_buff *skb)
{
return skb_dst(skb)->output(skb);
}
static int ip_finish_output(struct sk_buff *skb)
{
return ip_finish_output2(skb);
}
static inline int ip_finish_output2(struct sk_buff *skb)
{
if (dst->hh) {
int res = neigh_hh_output(dst->hh, skb);
}
}
鏈路層
5. static inline int neigh_hh_output(const struct hh_cache *hh, struct sk_buff *skb)
{
unsigned int seq;
int hh_len;
skb_push(skb, hh_len);
return dev_queue_xmit(skb);
}
Linux 內核網絡子系統
6. int dev_queue_xmit(struct sk_buff *skb) { if (q->enqueue) { rc =
__dev_xmit_skb(skb, q, dev, txq); goto out; } }
static inline int __dev_xmit_skb(struct sk_buff *skb, struct Qdisc *q,
struct net_device *dev,
struct netdev_queue *txq)
{
if (sch_direct_xmit(skb, q, dev, txq, root_lock))
}
int sch_direct_xmit(struct sk_buff *skb, struct Qdisc *q,
struct net_device *dev, struct netdev_queue *txq,
spinlock_t *root_lock)
{
HARD_TX_LOCK(dev, txq, smp_processor_id());
if (!netif_tx_queue_frozen_or_stopped(txq))
ret = dev_hard_start_xmit(skb, dev, txq);
}
int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev,
struct netdev_queue *txq)
{//調用驅動裏的回調發送函數ndo_start_xmit,將數據包傳給網卡設備
skb_len = skb->len;
rc = ops->ndo_start_xmit(skb, dev);
}
驅動程序
7. static netdev_tx_t igb_xmit_frame(struct sk_buff *skb,
struct net_device *netdev) { return igb_xmit_frame_ring(skb, igb_tx_queue_mapping(adapter, skb)); }
netdev_tx_t igb_xmit_frame_ring(struct sk_buff *skb,
struct igb_ring *tx_ring)
{//igb_tx_map函數準備給設備發送的數據
igb_tx_map(tx_ring, first, hdr_len);
}
-
雖然數據這時已經發送完畢,但是其實還有⼀件重要的事情沒有做,那就是釋放緩存隊列等內存。
-
那內核是如何知道什麼時候才能釋放內存的呢,當然是等⽹絡發送完畢之後。⽹卡在發送完畢的時候,會給 CPU 發送⼀個硬中斷來通知 CPU。
我們現在來看一下硬中斷觸發後的流程圖:
硬中斷
static irqreturn_t igb_msix_ring(int irq, void *data)
{
napi_schedule(&q_vector->napi);
return IRQ_HANDLED;
}
static inline void napi_schedule(struct napi_struct *n)
{
if (napi_schedule_prep(n))
__napi_schedule(n);
}
void __napi_schedule(struct napi_struct *n)
{
____napi_schedule(&__get_cpu_var(softnet_data), n);
local_irq_restore(flags);
}
/* Called with irq disabled */
static inline void ____napi_schedule(struct softnet_data *sd,
struct napi_struct *napi)
{
list_add_tail(&napi->poll_list, &sd->poll_list);
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}
軟中斷
static void net_rx_action(struct softirq_action *h)
{
if (test_bit(NAPI_STATE_SCHED, &n->state)) {
work = n->poll(n, weight);
trace_napi_poll(n);
}
}
static bool igb_clean_tx_irq(struct igb_q_vector *q_vector)
{
do {
/* free the skb */
dev_kfree_skb_any(tx_buffer->skb);
/* unmap skb header data */
dma_unmap_single(tx_ring->dev,
dma_unmap_addr(tx_buffer, dma),
dma_unmap_len(tx_buffer, len),
DMA_TO_DEVICE);
/* clear tx_buffer data */
tx_buffer->skb = NULL;
dma_unmap_len_set(tx_buffer, len, 0);
/* clear last DMA location and unmap remaining buffers */
while (tx_desc != eop_desc) {
}
}
注意,雖然是發送數據,但是硬中斷最終觸發的軟中斷卻是 NET_RX_SOFTIRQ,⽽並不是 NET_TX_SOFTIRQ
在服務器上查看 /proc/softirqs,爲什麼 NET_RX 要⽐ NET_TX ⼤的多?
- 在服務器上查看 /proc/softirqs,爲什麼 NET_RX 要⽐ NET_TX ⼤的多的多傳輸完成最終會觸發 NET_RX,⽽不是
NET_TX。 所以⾃然你觀測 /proc/softirqs 也就能看到 NET_RX 更多了。
網卡啓動準備
-
現在的服務器上的⽹卡⼀般都是⽀持多隊列的。每⼀個隊列上都是由⼀個 RingBuffer 表示的,開啓了多隊列以後的的⽹卡就會對應有多個 RingBuffer。
-
⽹卡在啓動時最重要的任務之⼀就是分配和初始化 RingBuffer,理解了 RingBuffer 將會⾮常有助於後⾯我們掌握髮送。因爲今天的主題是發送,所以就以傳輸隊列爲例,我們來看下⽹卡啓動時分配 RingBuffer 的實際過程。
-
在⽹卡啓動的時候,會調⽤到 __igb_open 函數,RingBuffer 就是在這⾥分配的。
static int igb_open(struct net_device *netdev)
{
//分配傳輸描述符數組
/* allocate transmit descriptors */
err = igb_setup_all_tx_resources(adapter);
//分配接收描述符數組
/* allocate receive descriptors */
err = igb_setup_all_rx_resources(adapter);
//開啓全部隊列
netif_tx_start_all_queues(netdev);
}
- 在上⾯ __igb_open 函數調⽤ igb_setup_all_tx_resources 分配所有的傳輸 RingBuffer, 調⽤
igb_setup_all_rx_resources 創建所有的接收 RingBuffer。
static int igb_setup_all_tx_resources(struct igb_adapter *adapter)
{//有⼏個隊列就構造⼏個 RingBuffer
for (i = 0; i < adapter->num_tx_queues; i++) {
err = igb_setup_tx_resources(adapter->tx_ring[i]);
}
}
真正的 RingBuffer 構造過程是在 igb_setup_tx_resources 中完成的。
int igb_setup_tx_resources(struct igb_ring *tx_ring)
{
struct device *dev = tx_ring->dev;
int size;
//1.申請 igb_tx_buffer 數組內存
size = sizeof(struct igb_buffer) * tx_ring->count;
tx_ring->buffer_info = vzalloc(size);
if (!tx_ring->buffer_info)
goto err;
//2.申請 e1000_adv_tx_desc DMA 數組內存
/* round up to nearest 4K */
tx_ring->size = tx_ring->count * sizeof(union e1000_adv_tx_desc);
tx_ring->size = ALIGN(tx_ring->size, 4096);
tx_ring->desc = dma_alloc_coherent(dev,
tx_ring->size,
&tx_ring->dma,
GFP_KERNEL);
if (!tx_ring->desc)
goto err;
//初始化隊列成員
tx_ring->next_to_use = 0;
tx_ring->next_to_clean = 0;
return 0;
}
從上述源碼可以看到,實際上⼀個 RingBuffer 的內部不僅僅是⼀個環形隊列數組,⽽是有兩個
-
igb_tx_buffer 數組:這個數組是內核使⽤的,通過 vzalloc 申請的。
-
e1000_adv_tx_desc 數組:這個數組是⽹卡硬件使⽤的,硬件是可以通過 DMA 直接訪問這塊內存,通過 dma_alloc_coherent 分配。
這個時候它們之間還沒有啥聯繫。將來在發送的時候,這兩個環形數組中相同位置的指針將都將指向同⼀個 skb。這樣,內核和硬件就能共同訪問同樣的數據了,內核往 skb ⾥寫數據,⽹卡硬件負責發送。
最後調⽤ netif_tx_start_all_queues 開啓隊列,對於硬中斷的處理函數 igb_msix_ring 其實也是在 __igb_open 中註冊的。
ACCEPT 創建新 SOCKET
-
在發送數據之前,我們往往還需要⼀個已經建⽴好連接的 socket。
-
當 accept 之後,進程會創建⼀個新的 socket 出來,然後把它放到當前進程的打開⽂件列表中,專⻔⽤於和對應的客戶端通信。
假設服務器進程通過 accept 和客戶端建⽴了兩條連接,我們來簡單看⼀下這兩條連接和進程的關聯關係。
其中代表⼀條連接的 socket 內核對象更爲具體⼀點的結構圖如下。
發送數據真正開始
send 系統調⽤實現
send 系統調⽤的源碼位於⽂件 net/socket.c 中。在這個系統調⽤⾥,內部其實真正使⽤的是 sendto 系統調⽤。整個調⽤鏈條雖然不短,但其實主要只⼲了兩件簡單的事情:
-
第⼀是在內核中把真正的 socket 找出來,在這個對象⾥記錄着各種協議棧的函數地址。
-
第⼆是構造⼀個 struct msghdr 對象,把⽤戶傳⼊的數據,⽐如 buffer 地址、數據⻓度,統統都裝進去. 剩下的事情就交給下⼀層協議棧⾥的函數 inet_sendmsg 了,其中 inet_sendmsg 函數的地址是通過 socket 內核對象⾥的 ops 成員找到的。
⼤致流程:
-
從源碼可以看到,我們在⽤戶態使⽤的 send 函數和 sendto 函數其實都是 sendto 系統調⽤實現的。send
只是爲了⽅便,封裝出來的⼀個更易於調⽤的⽅式⽽已。 -
在 sendto 系統調⽤⾥,⾸先根據⽤戶傳進來的 socket 句柄號來查找真正的 socket 內核對象。接着把⽤戶請求的
buff、len、flag 等參數都統統打包到⼀個 struct msghdr 對象中。接着調⽤了 sock_sendmsg => __sock_sendmsg ==> __sock_sendmsg_nosec。在__sock_sendmsg_nosec 中,調⽤將會由系統調⽤進⼊到協議棧,我們來看它的源碼。
-
通過前面的 socket 內核對象結構圖,我們可以看到,這⾥調⽤的是 sock->ops->sendmsg 實際執⾏的是
inet_sendmsg。這個函數是 AF_INET 協議族提供的通⽤發送函數。
傳輸層處理
-
在進⼊到協議棧 inet_sendmsg 以後,內核接着會找到 socket 上的具體協議發送函數。對於 TCP 協議來說,那就是 tcp_sendmsg(同樣也是通過 socket 內核對象找到的)。
-
在這個函數中,內核會申請⼀個內核態的 skb 內存,將⽤戶待發送的數據拷⻉進去。注意這個時候不⼀定會真正開始發送,如果沒有達到發送條件的話很可能這次調⽤直接就返回了。
⼤概過程如圖:
-
inet_sendmsg 函數的源碼
-
在這個函數中會調⽤到具體協議的發送函數。同樣前面的 socket 內核對象結構圖,我們看到對於 TCP 協議下的 socket 來說,來說 sk->sk_prot->sendmsg 指向的是 tcp_sendmsg(對於 UPD 來說是 udp_sendmsg)。
-
理解對 socket 調⽤ tcp_write_queue_tail 是理解發送的前提。如上所示,這個函數是在獲取 socket
發送隊列中的最後⼀個 skb。 skb 是 struct sk_buff 對象的簡稱,⽤戶的發送隊列就是該對象組成的⼀個鏈表。
-
再接着看 tcp_sendmsg 的其它部分
tcp_sendmsg(struct kiocb *iocb,struct sock *sk, struct msghdr *msg, size_t size){
//獲取用戶傳遞過來的數據和標誌
iov = msg->msg_iov;
//用戶數據地址
iovlen =msg->msg_iovlen;
//數據塊數爲1
flags = msg->msg_flags;
//各種標誌
//遍歷用戶層的數據塊
while (--iovlen >= 0) {
//待發送數據塊的地址
unsigned char __user *from = iov->iov_base; while (seglen >0) {
//需要申請新的
skb if (copy <= 0) {
//申請skb,並添加到發送隊列的尾部
skb = sk_stream_alloc_skb(sk, select_size(sk, sg), sk->sk_allocation);
//把 skb 掛到socket的發送隊列上
skb_entail(sk, skb); }
// skb 中有足夠的空間
if (skb_availroom(skb) > 0) {
//拷貝用戶空間的數據到內核空間,同時計算校驗和
//from是用戶空間的數據地址
skb_add_data_nocache(sk, skb, from, copy); }
- 這個函數⽐較⻓,不過其實邏輯並不複雜。其中 msg->msg_iov 存儲的是⽤戶態內存的要發送的數據的 buffer。接下來在內核態申請內核內存,⽐如 skb,並把⽤戶內存⾥的數據拷⻉到內核態內存中。這就會涉及到⼀次或者⼏次內存拷⻉的開銷。
- ⾄於內核什麼時候真正把 skb 發送出去。在 tcp_sendmsg 中會進⾏⼀些判斷。
int tcp_sendmsg(){
if (forced_push(tp)) {
tcp_mark_push(tp, skb);
__tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);
} else if (skb == tcp_send_head(sk))
tcp_push_one(sk, mss_now);
continue;
}
- 只有滿⾜ forced_push(tp) 或者 skb ==tcp_send_head(sk) 成⽴的時候,內核纔會真正啓動發送數據包。其中 forced_push(tp) 判斷的是未發送的數據數據是否已經超過最⼤窗⼝的⼀半了。
條件都不滿⾜的話,這次的⽤戶要發送的數據只是拷⻉到內核就算完事了!
傳輸層發送
-
假設現在內核發送條件已經滿⾜了,我們再來跟蹤⼀下實際的發送過程。對於上節函數中,當滿⾜真正發送條件的時候,⽆論調⽤的是__tcp_push_pending_frames 還是 tcp_push_one 最終都實際會執⾏到 tcp_write_xmit。所以我們直接從 tcp_write_xmit 看起,這個函數處理了傳輸層的擁塞控制、滑動窗⼝相關的⼯作。滿⾜窗⼝要求的時候,設置⼀下 TCP 頭然後將 skb 傳到更低的⽹絡層進⾏處理。
-
來看下 tcp_write_xmit 的源碼
- 可以看到我們之前在⽹絡協議⾥學的滑動窗⼝、擁塞控制就是在這個函數中完成的,這部分就不過多展開了,感興趣同學⾃⼰找這段源碼來讀。我們今天只看發送主過程,那就⾛到了 tcp_transmit_skb。
有刪減函數:
此函數說明:
先克隆⼀個新的 skb,這⾥重點說下爲什麼要複製⼀個 skb 出來呢?
-
是因爲 skb 後續在調⽤⽹絡層,最後到達⽹卡發送完成的時候,這個 skb 會被釋放掉。⽽我們知道 TCP 協議是⽀持丟失重傳的,在收到對⽅的 ACK 之前,這個 skb 不能被刪除。所以內核的做法就是每次調⽤⽹卡發送的時候,實際上傳遞出去的是 skb 的⼀個拷⻉。等收到 ACK 再真正刪除。
-
修改 skb 中的 TCP header,根據實際情況把 TCP 頭設置好。這⾥要介紹⼀個⼩技巧,skb 內部其實包含了⽹絡協議中所有的 header。在設置 TCP 頭的時候,只是把指針指向 skb 的合適位置。後⾯再設置 IP 頭的時候,在把指針挪⼀挪就⾏,避免頻繁的內存申請和拷⻉,效率很⾼。
-
tcp_transmit_skb 是發送數據位於傳輸層的最後⼀步,接下來就可以進⼊到⽹絡層進⾏下⼀層的操作了。調⽤了⽹絡層提供的發送接⼝ icsk->icsk_af_ops->queue_xmit()。
在下⾯的這個源碼中,我們的知道了 queue_xmit 其實指向的是 ip_queue_xmit 函數。
⾃此,傳輸層的⼯作也就都完成了。 數據離開了傳輸層,接下來將會進⼊到內核在⽹絡層的實現⾥。
⽹絡層發送處理
-
Linux 內核⽹絡層的發送的實現位於 net/ipv4/ip_output.c 這個⽂件。傳輸層調⽤到的 ip_queue_xmit 也在這⾥。(從⽂件名上也能看出來進⼊到 IP 層了,源⽂件名已經從 tcp_xxx 變成了 ip_xxx。)
-
在⽹絡層⾥主要處理路由項查找、IP 頭設置、netfilter 過濾、skb 切分(⼤於 MTU 的話)⼏項⼯作,處理完這些⼯作後會交給更下層的鄰居⼦系統來處理。
-
ip_queue_xmit 已經到了⽹絡層,在這個函數⾥我們看到了⽹絡層相關的功能路由項查找,如果找到了則設置到 skb 上(沒有路由的話就直接報錯返回了)。
-
在 Linux 上通過 route 命令可以看到你本機的路由配置。
-
在路由表中,可以查到某個⽬的⽹絡應該通過哪個 Iface(⽹卡),哪個 Gateway(⽹卡)發送出去。查找出來以後緩存到 socket 上,下次再發送數據就不⽤查了。
-
接着把路由表地址也放到 skb ⾥去。
-
接下來就是定位到 skb ⾥的 IP 頭的位置上,然後開始按照協議規範設置 IP header
-
在 ip_local_out => __ip_local_out => nf_hook 會執⾏ netfilter 過濾。如果你使⽤ iptables 配置了⼀些規則,那麼這⾥將檢測是否命中規則。
-
如果你設置了⾮常複雜的 netfilter 規則,在這個函數這⾥將會導致你的進程 CPU 開銷會極⼤增加。
-
此函數找到這個 skb 的路由表(dst 條⽬) ,然後調⽤路由表的 output ⽅法。這⼜是⼀個函數指針,指向的是 ip_output
⽅法。
-
在 ip_output 中進⾏⼀些簡單的,統計⼯作,再次執⾏ netfilter 過濾。過濾通過之後回調 ip_finish_output。
-
在 ip_finish_output 中我們看到,如果數據⼤於 MTU 的話,是會執⾏分⽚的。
-
在 ip_finish_output2 中,終於發送過程會進⼊到下⼀層,鄰居⼦系統中。
在早期的時候,軟件開發者會盡量控制⾃⼰數據包尺⼨⼩於 MTU,通過這種⽅式來優化⽹絡性能。因爲分⽚會帶來兩個問題:
-
需要進⾏額外的切分處理,有額外性能開銷。
-
只要⼀個分⽚丟失,整個包都得重傳。所以避免分⽚既杜絕了分⽚開銷,也⼤⼤降低了重傳率。
鄰居⼦系統
- 鄰居⼦系統是位於⽹絡層和數據鏈路層中間的⼀個系統,其作⽤是對⽹絡層提供⼀個封裝,讓⽹絡層不必關⼼下層的地址信息,讓下層來決定發送到哪個 MAC 地址。⽽且這個鄰居⼦系統並不位於協議棧 net/ipv4/ ⽬錄內,⽽是位於
net/core/neighbour.c。因爲⽆論是對於 IPv4 還是 IPv6 ,都需要使⽤該模塊。
- 在鄰居⼦系統⾥主要是查找或者創建鄰居項,在創造鄰居項的時候,有可能會發出實際的 arp 請求。然後封裝⼀下 MAC
頭,將發送過程再傳遞到更下層的⽹絡設備⼦系統。⼤致流程如圖。
- 理解了⼤致流程,我們再回頭看源碼。在上⾯⼩節 ip_finish_output2 源碼中調⽤了__ipv4_neigh_lookup_noref。它是在 arp 緩存中進⾏查找,其第⼆個參數傳⼊的是路由下⼀跳 IP 信息。
如果查找不到,則調⽤ __neigh_create 創建⼀個鄰居。
- 有了鄰居項以後,此時仍然還不具備發送 IP 報⽂的能⼒,因爲⽬的 MAC 地址還未獲取。調⽤ dst_neigh_output 繼續傳遞
skb。
-
調⽤ output,實際指向的是 neigh_resolve_output。在這個函數內部有可能會發出 arp ⽹絡請求。
-
當獲取到硬件 MAC 地址以後,就可以封裝 skb 的 MAC 頭了。最後調⽤ dev_queue_xmit 將 skb 傳遞給 Linux
⽹絡設備⼦系統。
⽹絡設備⼦系統
-
鄰居⼦系統通過 dev_queue_xmit 進⼊到⽹絡設備⼦系統中來。
-
⽹卡啓動準備⾥,⽹卡是有多個發送隊列的。上⾯對 netdev_pick_tx 函數的調⽤就是選擇⼀個隊列進⾏發送。netdev_pick_tx 發送隊列的選擇受 XPS 等配置的影響,⽽且還有緩存,也是⼀套⼩複雜的邏輯。這⾥我們只關注兩個邏輯,⾸先會獲取⽤戶的 XPS 配置,否則就⾃動計算了。代碼⻅ netdev_pick_tx => __netdev_pick_tx。
- 然後獲取與此隊列關聯的 qdisc。在 linux 上通過 tc 命令可以看到 qdisc 類型
$ tc qdisc
qdisc noqueue 0: dev lo root refcnt 2
qdisc fq_codel 0: dev ens33 root refcnt 2 limit 10240p flows 1024 quantum 1514 target 5.0ms interval 100.0ms memory_limit 32Mb ecn
-
⼤部分的設備都有隊列(迴環設備和隧道設備除外),所以現在我們進⼊到__dev_xmit_skb。
上述代碼中分兩種情況,1 是可以 bypass(繞過)排隊系統的,另外⼀種是正常排隊。我們只看第⼆種情況。 -
先調⽤ q->enqueue 把 skb 添加到隊列⾥。然後調⽤ __qdisc_run 開始發送。
-
在上述代碼中,我們看到 while 循環不斷地從隊列中取出 skb 並進⾏發送。注意,這個時候其實都佔⽤的是⽤戶進程的系統態時間 (sy)。只有當 quota ⽤盡或者其它進程需要 CPU 的時候才觸發軟中斷進⾏發送。
-
所以這就是爲什麼⼀般服務器上查看 /proc/softirqs,⼀般 NET_RX 都要⽐ NET_TX ⼤的多的第⼆個原因。對於讀來說,都是要經過 NET_RX 軟中斷,⽽對於發送來說,只有系統態配額⽤盡才讓軟中斷上。
繼續看發送過程
- qdisc_restart 從隊列中取出⼀個 skb,並調⽤ sch_direct_xmit 繼續發送。
軟中斷調度
-
在 4.5 咱們看到了如果系統態 CPU 發送⽹絡包不夠⽤的時候,會調⽤ __netif_schedule 觸發⼀個軟中斷。該函數會進⼊到 __netif_reschedule,由它來實際發出 NET_TX_SOFTIRQ 類型軟中斷。
-
軟中斷是由內核線程來運⾏的,該線程會進⼊到 net_tx_action 函數,在該函數中能獲取到發送隊列,並也最終調⽤到驅動程序⾥的⼊⼝函數 dev_hard_start_xmit
-
函數⾥在軟中斷能訪問到的 softnet_data ⾥設置了要發送的數據隊列,添加到了 output_queue ⾥了。緊接着觸發了 NET_TX_SOFTIRQ 類型的軟中斷。(T 代表 transmit 傳輸)
-
我們直接從 NET_TX_SOFTIRQ softirq 註冊的回調函數 net_tx_action 講起。⽤戶態進程觸發完軟中斷之後,會有⼀個軟中斷內核線程會執⾏到 net_tx_action。
牢記,這以後發送數據消耗的 CPU 就都顯示在 si (SoftIRQ)這⾥了,不會消耗⽤戶進程的系統時間了
- 軟中斷這⾥會獲取 softnet_data。前⾯我們看到進程內核態在調⽤ __netif_reschedule 的時候把發送隊列寫到 softnet_data 的 output_queue ⾥了。 軟中斷循環遍歷 sd->output_queue 發送數據幀。
來看 qdisc_run,它和進程⽤戶態⼀樣,也會調⽤到 __qdisc_run。
- 然後⼀樣就是進⼊ qdisc_restart =>sch_direct_xmit,直到驅動程序函數 dev_hard_start_xmit。
⽹卡驅動發送
-
⽆論是對於⽤戶進程的內核態,還是對於軟中斷上下⽂,都會調⽤到⽹絡設備⼦系統中的 dev_hard_start_xmit 函數。在這個函數中,會調⽤到驅動⾥的發送函數 igb_xmit_frame。
-
在驅動函數⾥,將 skb 會掛到 RingBuffer 上,驅動調⽤完畢後,數據包將真正從⽹卡發送出去。
-
其中 ndo_start_xmit 是⽹卡驅動要實現的⼀個函數,是在 net_device_ops 中定義的
-
在 igb ⽹卡驅動源碼中,我們找到了
-
也就是說,對於⽹絡設備層定義的 ndo_start_xmit, igb 的實現函數是 igb_xmit_frame。這個函數是在⽹卡驅動初始化的時候被賦值的。
-
所以在上⾯⽹絡設備層調⽤ ops->ndo_start_xmit 的時候,會實際上進⼊ igb_xmit_frame 這個函數中。我們進⼊這個函數來看看驅動程序是如何⼯作的
在這⾥從⽹卡的發送隊列的 RingBuffer 中取下來⼀個元素,並將 skb 掛到元素上。
igb_tx_map 函數處理將 skb 數據映射到⽹卡可訪問的內存 DMA 區域。
- 當所有需要的描述符都已建好,且 skb 的所有數據都映射到 DMA 地址後,驅動就會進⼊到它的最後⼀步,觸發真實的發送。
發送完成硬中斷
-
當數據發送完成以後,其實⼯作並沒有結束。因爲內存還沒有清理。當發送完成的時候,⽹卡設備會觸發⼀個硬中斷來釋放內存。
-
在發送硬中斷⾥,會執⾏ RingBuffer 內存的清理⼯作
回頭看⼀下硬中斷觸發軟中斷的源碼
-
這⾥有個很有意思的細節,⽆論硬中斷是因爲是有數據要接收,還是說發送完成通知,從硬中斷觸發的軟中斷都是 NET_RX_SOFTIRQ。這個我們在第⼀節說過了,這是軟中斷統計中 RX 要⾼於 TX 的⼀個原因。
-
我們接着進⼊軟中斷的回調函數 igb_poll。在這個函數⾥,我們注意到有⼀⾏ igb_clean_tx_irq
-
清理 skb,解除了 DMA 映射等等。 到了這⼀步,傳輸纔算是基本完成了。
-
爲啥我說是基本完成,⽽不是全部完成了呢?因爲傳輸層需要保證可靠性,所以 skb 其實還沒有刪除。它得等收到對⽅的 ACK 之後纔會真正刪除,那個時候纔算是徹底的發送完畢。
疑問
我們在監控內核發送數據消耗的 CPU 時,是應該看 sy 還是 si ?
-
在⽹絡包的發送過程中,⽤戶進程(在內核態)完成了絕⼤部分的⼯作,甚⾄連調⽤驅動的事情都⼲了。 只有當內核態進程被切⾛前纔會發起軟中斷。發送過程中,絕⼤部分(90%)以上的開銷都是在⽤戶進程內核態消耗掉的。只有⼀少部分情況下才會觸發軟中斷(NET_TX 類型),由軟中斷 ksoftirqd 內核進程來發送。
-
所以,在監控⽹絡 IO 對服務器造成的 CPU 開銷的時候,不能僅僅只看 si,⽽是應該把 si、sy 都考慮進來
查看 /proc/softirqs,爲什麼 NET_RX 要⽐ NET_TX ⼤的多的多?
-
第⼀個原因是當數據發送完成以後,通過硬中斷的⽅式來通知驅動發送完畢。但是硬中斷⽆論是有數據接收,還是對於發送完畢,觸發的軟中斷都是 NET_RX_SOFTIRQ,⽽並不是 NET_TX_SOFTIRQ。
-
第⼆個原因是對於讀來說,都是要經過 NET_RX 軟中斷的,都⾛ ksoftirqd 內核進程。⽽對於發送來說,絕⼤部分⼯作都是在⽤戶進程內核態處理了,只有系統態配額⽤盡纔會發出 NET_TX,讓軟中斷上。綜上兩個原因,那麼在機器上查看 NET_RX ⽐ NET_TX
發送⽹絡數據的時候都涉及到哪些內存拷⻉?(這⾥內存拷⻉,我們特指待發送數據的內存拷⻉)
-
第⼀次拷⻉操作是內核申請完 skb 之後,這時候會將⽤戶傳遞進來的 buffer ⾥的數據內容都拷⻉到 skb 中。如果要發送的數據量⽐較⼤的話,這個拷⻉操作開銷還是不⼩的。
-
第⼆次拷⻉操作是從傳輸層進⼊⽹絡層的時候,每⼀個 skb 都會被克隆⼀個新的副本出來。⽹絡層以及下⾯的驅動、軟中斷等組件在發送完成的時候會將這個副本刪除。傳輸層保存着原始的 skb,在當⽹絡對⽅沒有 ack 的時候,還可以重新發送,以實現 TCP 中要求的可靠傳輸。
-
第三次拷⻉不是必須的,只有當 IP 層發現 skb ⼤於 MTU 時才需要進⾏。會再申請額外的 skb,並將原來的 skb 拷⻉爲多個⼩的 skb。
注意:在⽹絡性能優化中經常聽到的零拷⻉,有誇張的成分。TCP 爲了保證可靠性,第⼆次的拷⻉根本就沒法省。如果包再⼤於 MTU 的話,分⽚時的拷⻉同樣也避免不了。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/4W0xaNnZe-thtjJMxGodgA