天天看點

Linux核心網絡資料包發送(一)1. 前言2. 資料包發送宏觀視角3. 協定層注冊4. 通過 socket 發送網絡資料5. 總結

Linux核心網絡資料包發送(一)

  • 1. 前言
  • 2. 資料包發送宏觀視角
  • 3. 協定層注冊
  • 4. 通過 socket 發送網絡資料
    • 4.1 `sock_sendmsg`, `__sock_sendmsg`, `__sock_sendmsg_nosec`
    • 4.2 `inet_sendmsg`
  • 5. 總結

1. 前言

本文首先從宏觀上概述了資料包發送的流程,接着分析了協定層注冊進核心以及被socket的過程,最後介紹了通過 socket 發送網絡資料的過程。

2. 資料包發送宏觀視角

從宏觀上看,一個資料包從使用者程式到達硬體網卡的整個過程如下:

  1. 使用系統調用(如

    sendto

    sendmsg

    等)寫資料
  2. 資料穿過socket 子系統,進入socket 協定族(protocol family)系統
  3. 協定族處理:資料穿過協定層,這一過程(在許多情況下)會将資料(data)轉換成資料包(packet)
  4. 資料穿過路由層,這會涉及路由緩存和 ARP 緩存的更新;如果目的 MAC 不在 ARP 緩存表中,将觸發一次 ARP 廣播來查找 MAC 位址
  5. 穿過協定層,packet 到達裝置無關層(device agnostic layer)
  6. 使用 XPS(如果啟用)或散列函數選擇發送隊列
  7. 調用網卡驅動的發送函數
  8. 資料傳送到網卡的

    qdisc

    (queue discipline,排隊規則)
  9. qdisc 會直接發送資料(如果可以),或者将其放到隊列,下次觸發NET_TX 類型軟中斷(softirq)的時候再發送
  10. 資料從 qdisc 傳送給驅動程式
  11. 驅動程式建立所需的DMA 映射,以便網卡從 RAM 讀取資料
  12. 驅動向網卡發送信号,通知資料可以發送了
  13. 網卡從 RAM 中擷取資料并發送
  14. 發送完成後,裝置觸發一個硬中斷(IRQ),表示發送完成
  15. 硬中斷處理函數被喚醒執行。對許多裝置來說,這會觸發 NET_RX 類型的軟中斷,然後 NAPI poll 循環開始收包
  16. poll 函數會調用驅動程式的相應函數,解除 DMA 映射,釋放資料

3. 協定層注冊

協定層分析我們将關注 IP 和 UDP 層,其他協定層可參考這個過程。我們首先來看協定族是如何注冊到核心,并被 socket 子系統使用的。

當使用者程式像下面這樣建立 UDP socket 時會發生什麼?

簡單來說,核心會去查找由 UDP 協定棧導出的一組函數(其中包括用于發送和接收網絡資料的函數),并賦給 socket 的相應字段。準确了解這個過程需要檢視

AF_INET

位址族的代碼。

核心初始化的很早階段就執行了

inet_init

函數,這個函數會注冊

AF_INET

協定族 ,以及該協定族内的各協定棧(TCP,UDP,ICMP 和 RAW),并調用初始化函數使協定棧準備好處理網絡資料。

inet_init

定義在net/ipv4/af_inet.c 。

AF_INET

協定族導出一個包含

create

方法的

struct net_proto_family

類型執行個體。當從使用者程式建立 socket 時,核心會調用此方法:

static const struct net_proto_family inet_family_ops = {
    .family = PF_INET,
    .create = inet_create,
    .owner  = THIS_MODULE,
};
           

inet_create

根據傳遞的 socket 參數,在已注冊的協定中查找對應的協定:

/* Look for the requested type/protocol pair. */
lookup_protocol:
        err = -ESOCKTNOSUPPORT;
        rcu_read_lock();
        list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {

                err = 0;
                /* Check the non-wild match. */
                if (protocol == answer->protocol) {
                        if (protocol != IPPROTO_IP)
                                break;
                } else {
                        /* Check for the two wild cases. */
                        if (IPPROTO_IP == protocol) {
                                protocol = answer->protocol;
                                break;
                        }
                        if (IPPROTO_IP == answer->protocol)
                                break;
                }
                err = -EPROTONOSUPPORT;
        }
           

然後,将該協定的回調方法(集合)賦給這個新建立的 socket:

可以在

af_inet.c

中看到所有協定的初始化參數。 下面是TCP 和 UDP的初始化參數:

/* Upon startup we insert all the elements in inetsw_array[] into
 * the linked list inetsw.
 */
static struct inet_protosw inetsw_array[] =
{
        {
                .type =       SOCK_STREAM,
                .protocol =   IPPROTO_TCP,
                .prot =       &tcp_prot,
                .ops =        &inet_stream_ops,
                .no_check =   0,
                .flags =      INET_PROTOSW_PERMANENT |
                              INET_PROTOSW_ICSK,
        },

        {
                .type =       SOCK_DGRAM,
                .protocol =   IPPROTO_UDP,
                .prot =       &udp_prot,
                .ops =        &inet_dgram_ops,
                .no_check =   UDP_CSUM_DEFAULT,
                .flags =      INET_PROTOSW_PERMANENT,
       },

            /* .... more protocols ... */
           

IPPROTO_UDP

協定類型有一個

ops

變量,包含很多資訊,包括用于發送和接收資料的回調函數:

const struct proto_ops inet_dgram_ops = {
	.family          = PF_INET,
	.owner           = THIS_MODULE,
	
	/* ... */
	
	.sendmsg     = inet_sendmsg,
	.recvmsg     = inet_recvmsg,
	
	/* ... */
};
EXPORT_SYMBOL(inet_dgram_ops);
           

prot

字段指向一個協定相關的變量(的位址),對于 UDP 協定,其中包含了 UDP 相關的回調函數。 UDP 協定對應的

prot

變量為

udp_prot

,定義在 net/ipv4/udp.c:

struct proto udp_prot = {
	.name        = "UDP",
	.owner           = THIS_MODULE,
	
	/* ... */
	
	.sendmsg     = udp_sendmsg,
	.recvmsg     = udp_recvmsg,
	
	/* ... */
};
EXPORT_SYMBOL(udp_prot);
           

現在,讓我們轉向發送 UDP 資料的使用者程式,看看

udp_sendmsg

是如何在核心中被調用的。

4. 通過 socket 發送網絡資料

使用者程式想發送 UDP 網絡資料,是以它使用

sendto

系統調用:

該系統調用穿過Linux 系統調用(system call)層,最後到達net/socket.c中的這個函數:

/*
 *      Send a datagram to a given address. We move the address into kernel
 *      space and check the user space data area is readable before invoking
 *      the protocol.
 */

SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
                unsigned int, flags, struct sockaddr __user *, addr,
                int, addr_len)
{
    /*  ... code ... */

    err = sock_sendmsg(sock, &msg, len);

    /* ... code  ... */
}
           

SYSCALL_DEFINE6

宏會展開成一堆宏,後者經過一波複雜操作建立出一個帶 6 個參數的系統調用(是以叫

DEFINE6

)。作為結果之一,會看到核心中的所有系統調用都帶

sys_

字首。

sendto

代碼會先将資料整理成底層可以處理的格式,然後調用

sock_sendmsg

。特别地, 它将傳遞給

sendto

的位址放到另一個變量(

msg

)中:

iov.iov_base = buff;
iov.iov_len = len;
msg.msg_name = NULL;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = NULL;
msg.msg_controllen = 0;
msg.msg_namelen = 0;
if (addr) {
        err = move_addr_to_kernel(addr, addr_len, &address);
        if (err < 0)
                goto out_put;
        msg.msg_name = (struct sockaddr *)&address;
        msg.msg_namelen = addr_len;
}
           

這段代碼将使用者程式傳入到核心的(存放待發送資料的)位址,作為

msg_name

字段嵌入到

struct msghdr

類型變量中。這和使用者程式直接調用

sendmsg

而不是

sendto

發送資料差不多,這之是以可行,是因為

sendto

sendmsg

底層都會調用

sock_sendmsg

4.1

sock_sendmsg

,

__sock_sendmsg

,

__sock_sendmsg_nosec

sock_sendmsg

做一些錯誤檢查,然後調用

__sock_sendmsg

;後者做一些自己的錯誤檢查 ,然後調用

__sock_sendmsg_nosec

__sock_sendmsg_nosec

将資料傳遞到 socket 子系統的更深處:

static inline int __sock_sendmsg_nosec(struct kiocb *iocb, struct socket *sock,
                                       struct msghdr *msg, size_t size)
{
    struct sock_iocb *si =  ....

    /* other code ... */

    return sock->ops->sendmsg(iocb, sock, msg, size);
}
           

通過前面介紹的 socket 建立過程,可以知道注冊到這裡的

sendmsg

方法就是

inet_sendmsg

4.2

inet_sendmsg

從名字可以猜到,這是

AF_INET

協定族提供的通用函數。 此函數首先調用

sock_rps_record_flow

來記錄最後一個處理該(資料所屬的)flow 的 CPU; Receive Packet Steering 會用到這個資訊。接下來,調用 socket 的協定類型(本例是 UDP)對應的

sendmsg

方法:

int inet_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg,
                 size_t size)
{
      struct sock *sk = sock->sk;

      sock_rps_record_flow(sk);

      /* We may need to bind the socket. */
      if (!inet_sk(sk)->inet_num && !sk->sk_prot->no_autobind && inet_autobind(sk))
              return -EAGAIN;

      return sk->sk_prot->sendmsg(iocb, sk, msg, size);
}
EXPORT_SYMBOL(inet_sendmsg);
           

本例是 UDP 協定,是以上面的

sk->sk_prot->sendmsg

指向的是之前看到的(通過

udp_prot

導出的)

udp_sendmsg

函數。

sendmsg()函數作為分界點,處理邏輯從 AF_INET 協定族通用處理轉移到具體的 UDP 協定的處理。

5. 總結

了解Linux核心網絡資料包發送的詳細過程,有助于我們進行網絡監控和調優。本文隻分析了協定層的注冊和通過 socket 發送資料的過程,資料在傳輸層和網絡層的詳細發送過程将在下一篇文章中分析。

參考連結:

[1] https://blog.packagecloud.io/eng/2017/02/06/monitoring-tuning-linux-networking-stack-sending-data

[2] https://segmentfault.com/a/1190000008926093

繼續閱讀