Linux核心網絡資料包發送(一)
- 1. 前言
- 2. 資料包發送宏觀視角
- 3. 協定層注冊
- 4. 通過 socket 發送網絡資料
-
- 4.1 `sock_sendmsg`, `__sock_sendmsg`, `__sock_sendmsg_nosec`
- 4.2 `inet_sendmsg`
- 5. 總結
1. 前言
本文首先從宏觀上概述了資料包發送的流程,接着分析了協定層注冊進核心以及被socket的過程,最後介紹了通過 socket 發送網絡資料的過程。
2. 資料包發送宏觀視角
從宏觀上看,一個資料包從使用者程式到達硬體網卡的整個過程如下:
- 使用系統調用(如
,sendto
等)寫資料sendmsg
- 資料穿過socket 子系統,進入socket 協定族(protocol family)系統
- 協定族處理:資料穿過協定層,這一過程(在許多情況下)會将資料(data)轉換成資料包(packet)
- 資料穿過路由層,這會涉及路由緩存和 ARP 緩存的更新;如果目的 MAC 不在 ARP 緩存表中,将觸發一次 ARP 廣播來查找 MAC 位址
- 穿過協定層,packet 到達裝置無關層(device agnostic layer)
- 使用 XPS(如果啟用)或散列函數選擇發送隊列
- 調用網卡驅動的發送函數
- 資料傳送到網卡的
(queue discipline,排隊規則)qdisc
- qdisc 會直接發送資料(如果可以),或者将其放到隊列,下次觸發NET_TX 類型軟中斷(softirq)的時候再發送
- 資料從 qdisc 傳送給驅動程式
- 驅動程式建立所需的DMA 映射,以便網卡從 RAM 讀取資料
- 驅動向網卡發送信号,通知資料可以發送了
- 網卡從 RAM 中擷取資料并發送
- 發送完成後,裝置觸發一個硬中斷(IRQ),表示發送完成
- 硬中斷處理函數被喚醒執行。對許多裝置來說,這會觸發 NET_RX 類型的軟中斷,然後 NAPI poll 循環開始收包
- 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
做一些錯誤檢查,然後調用
__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
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