Linux 內核發送網絡數據

前言

我們開始今天對 Linux 內核⽹絡發送過程的深度剖析。還是按照我們之前的傳統,先從⼀段代碼作爲切⼊。

上述代碼中,調⽤ send 之後內核是怎麼樣把數據包發送出去的。本⽂基於 Linux 3.10,⽹卡驅動採⽤ Intel 的 igb 舉例。

基礎框架

源碼流程跟蹤(梳理的是從上到下的調用流程)

應用層
 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);
}

我們現在來看一下硬中斷觸發後的流程圖:

硬中斷

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 ⼤的多?

網卡啓動準備

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);
}
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 的內部不僅僅是⼀個環形隊列數組,⽽是有兩個

  1. igb_tx_buffer 數組:這個數組是內核使⽤的,通過 vzalloc 申請的。

  2. e1000_adv_tx_desc 數組:這個數組是⽹卡硬件使⽤的,硬件是可以通過 DMA 直接訪問這塊內存,通過 dma_alloc_coherent 分配。

這個時候它們之間還沒有啥聯繫。將來在發送的時候,這兩個環形數組中相同位置的指針將都將指向同⼀個 skb。這樣,內核和硬件就能共同訪問同樣的數據了,內核往 skb ⾥寫數據,⽹卡硬件負責發送。

最後調⽤ netif_tx_start_all_queues 開啓隊列,對於硬中斷的處理函數 igb_msix_ring 其實也是在 __igb_open 中註冊的。

ACCEPT 創建新 SOCKET

  1. 在發送數據之前,我們往往還需要⼀個已經建⽴好連接的 socket。

  2. 當 accept 之後,進程會創建⼀個新的 socket 出來,然後把它放到當前進程的打開⽂件列表中,專⻔⽤於和對應的客戶端通信。

假設服務器進程通過 accept 和客戶端建⽴了兩條連接,我們來簡單看⼀下這兩條連接和進程的關聯關係。


其中代表⼀條連接的 socket 內核對象更爲具體⼀點的結構圖如下。

發送數據真正開始

send 系統調⽤實現
send 系統調⽤的源碼位於⽂件 net/socket.c 中。在這個系統調⽤⾥,內部其實真正使⽤的是 sendto 系統調⽤。整個調⽤鏈條雖然不短,但其實主要只⼲了兩件簡單的事情:

  1. 第⼀是在內核中把真正的 socket 找出來,在這個對象⾥記錄着各種協議棧的函數地址。

  2. 第⼆是構造⼀個 struct msghdr 對象,把⽤戶傳⼊的數據,⽐如 buffer 地址、數據⻓度,統統都裝進去. 剩下的事情就交給下⼀層協議棧⾥的函數 inet_sendmsg 了,其中 inet_sendmsg 函數的地址是通過 socket 內核對象⾥的 ops 成員找到的。

⼤致流程:

傳輸層處理

⼤概過程如圖:

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); }

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;
}

條件都不滿⾜的話,這次的⽤戶要發送的數據只是拷⻉到內核就算完事了!

傳輸層發送


有刪減函數:

此函數說明:

先克隆⼀個新的 skb,這⾥重點說下爲什麼要複製⼀個 skb 出來呢?

在下⾯的這個源碼中,我們的知道了 queue_xmit 其實指向的是 ip_queue_xmit 函數。

⾃此,傳輸層的⼯作也就都完成了。 數據離開了傳輸層,接下來將會進⼊到內核在⽹絡層的實現⾥。

⽹絡層發送處理

在早期的時候,軟件開發者會盡量控制⾃⼰數據包尺⼨⼩於 MTU,通過這種⽅式來優化⽹絡性能。因爲分⽚會帶來兩個問題:

  1. 需要進⾏額外的切分處理,有額外性能開銷。

  2. 只要⼀個分⽚丟失,整個包都得重傳。所以避免分⽚既杜絕了分⽚開銷,也⼤⼤降低了重傳率。

鄰居⼦系統

如果查找不到,則調⽤ __neigh_create 創建⼀個鄰居。

⽹絡設備⼦系統

$ 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

繼續看發送過程

軟中斷調度

  1. 在 4.5 咱們看到了如果系統態 CPU 發送⽹絡包不夠⽤的時候,會調⽤ __netif_schedule 觸發⼀個軟中斷。該函數會進⼊到 __netif_reschedule,由它來實際發出 NET_TX_SOFTIRQ 類型軟中斷。

  2. 軟中斷是由內核線程來運⾏的,該線程會進⼊到 net_tx_action 函數,在該函數中能獲取到發送隊列,並也最終調⽤到驅動程序⾥的⼊⼝函數 dev_hard_start_xmit


牢記,這以後發送數據消耗的 CPU 就都顯示在 si (SoftIRQ)這⾥了,不會消耗⽤戶進程的系統時間了

來看 qdisc_run,它和進程⽤戶態⼀樣,也會調⽤到 __qdisc_run。

⽹卡驅動發送

  1. ⽆論是對於⽤戶進程的內核態,還是對於軟中斷上下⽂,都會調⽤到⽹絡設備⼦系統中的 dev_hard_start_xmit 函數。在這個函數中,會調⽤到驅動⾥的發送函數 igb_xmit_frame。

  2. 在驅動函數⾥,將 skb 會掛到 RingBuffer 上,驅動調⽤完畢後,數據包將真正從⽹卡發送出去。




在這⾥從⽹卡的發送隊列的 RingBuffer 中取下來⼀個元素,並將 skb 掛到元素上。

igb_tx_map 函數處理將 skb 數據映射到⽹卡可訪問的內存 DMA 區域。

發送完成硬中斷

  1. 當數據發送完成以後,其實⼯作並沒有結束。因爲內存還沒有清理。當發送完成的時候,⽹卡設備會觸發⼀個硬中斷來釋放內存。

  2. 在發送硬中斷⾥,會執⾏ RingBuffer 內存的清理⼯作

回頭看⼀下硬中斷觸發軟中斷的源碼

  1. 這⾥有個很有意思的細節,⽆論硬中斷是因爲是有數據要接收,還是說發送完成通知,從硬中斷觸發的軟中斷都是 NET_RX_SOFTIRQ。這個我們在第⼀節說過了,這是軟中斷統計中 RX 要⾼於 TX 的⼀個原因。

  2. 我們接着進⼊軟中斷的回調函數 igb_poll。在這個函數⾥,我們注意到有⼀⾏ igb_clean_tx_irq



  1. 清理 skb,解除了 DMA 映射等等。 到了這⼀步,傳輸纔算是基本完成了。

  2. 爲啥我說是基本完成,⽽不是全部完成了呢?因爲傳輸層需要保證可靠性,所以 skb 其實還沒有刪除。它得等收到對⽅的 ACK 之後纔會真正刪除,那個時候纔算是徹底的發送完畢。

疑問

我們在監控內核發送數據消耗的 CPU 時,是應該看 sy 還是 si ?

查看 /proc/softirqs,爲什麼 NET_RX 要⽐ NET_TX ⼤的多的多?

  1. 第⼀個原因是當數據發送完成以後,通過硬中斷的⽅式來通知驅動發送完畢。但是硬中斷⽆論是有數據接收,還是對於發送完畢,觸發的軟中斷都是 NET_RX_SOFTIRQ,⽽並不是 NET_TX_SOFTIRQ。

  2. 第⼆個原因是對於讀來說,都是要經過 NET_RX 軟中斷的,都⾛ ksoftirqd 內核進程。⽽對於發送來說,絕⼤部分⼯作都是在⽤戶進程內核態處理了,只有系統態配額⽤盡纔會發出 NET_TX,讓軟中斷上。綜上兩個原因,那麼在機器上查看 NET_RX ⽐ NET_TX

發送⽹絡數據的時候都涉及到哪些內存拷⻉?(這⾥內存拷⻉,我們特指待發送數據的內存拷⻉)

  1. 第⼀次拷⻉操作是內核申請完 skb 之後,這時候會將⽤戶傳遞進來的 buffer ⾥的數據內容都拷⻉到 skb 中。如果要發送的數據量⽐較⼤的話,這個拷⻉操作開銷還是不⼩的。

  2. 第⼆次拷⻉操作是從傳輸層進⼊⽹絡層的時候,每⼀個 skb 都會被克隆⼀個新的副本出來。⽹絡層以及下⾯的驅動、軟中斷等組件在發送完成的時候會將這個副本刪除。傳輸層保存着原始的 skb,在當⽹絡對⽅沒有 ack 的時候,還可以重新發送,以實現 TCP 中要求的可靠傳輸。

  3. 第三次拷⻉不是必須的,只有當 IP 層發現 skb ⼤於 MTU 時才需要進⾏。會再申請額外的 skb,並將原來的 skb 拷⻉爲多個⼩的 skb。

注意:在⽹絡性能優化中經常聽到的零拷⻉,有誇張的成分。TCP 爲了保證可靠性,第⼆次的拷⻉根本就沒法省。如果包再⼤於 MTU 的話,分⽚時的拷⻉同樣也避免不了。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/4W0xaNnZe-thtjJMxGodgA