TL;DR
本文解释了 Linux 内核的计算机如何发送数据包,以及当数据包从用户程序流向网络硬件时,如何监控和调优网络栈的每个组件。
本文是之前的文章 监控和调优 Linux 网络栈:接收数据 的姊妹篇。
如果不阅读内核的源代码,不深入了解到底发生了什么,就不可能调优或监控 Linux 网络栈。
希望本文能给想做这方面工作的人提供参考。
关于监控和调优 Linux 网络栈的一般性建议
正如在上一篇文章中提到的,Linux 网络栈是复杂的,没有一刀切的监控或调优解决方案。 如果您真的想调优网络栈,您别无选择,只能投入大量的时间、精力和金钱来了解网络系统的各个部分是如何交互的。
本文中提供的许多示例设置仅用于说明目的,并不是对某个配置或默认设置的推荐或反对。 在调整任何设置之前,您应该围绕您需要监控的内容制定一个参考框架,以注意到有意义的变化。
网络连接到计算机时调整网络设置是危险的;你很容易地把自己锁在外面,或者完全关闭你的网络。 不要在生产机器上调整这些设置;相反,如果可能的话,在新机器上进行调整,再投入生产中。
概览
作为参考,您可能需要手边有一份设备数据手册。 这篇文章将研究由 igb
设备驱动程序控制的 Intel I350 以太网控制器。 您可以找到该数据手册(警告:大型 PDF)供您参考。
网络数据从用户程序到网络设备的流程概览:
- 使用系统调用(如
sendto
、sendmsg
等)写入数据。 - 数据通过套接字子系统传递到套接字协议族的系统(本例是
AF_INET
)。 - 协议族通过协议层传递数据,协议层(在许多情况下)将数据转成数据包。
- 数据通过路由层,沿途填充目标和邻居缓存(如果是冷缓存)。 如果需要查找以太网地址,会生成 ARP 流量。
- 在通过协议层之后,数据包到达设备无关层。
- 使用 XPS(如果启用)或哈希函数选择输出队列。
- 调用设备驱动程序的发送函数。
- 然后,数据被传递到输出设备附属的排队规则(qdisc)。
- 如果可以,qdisc 将直接传输数据;或将其排队,等待
NET_TX
软中断期间发送。 - 最后,数据从 qdisc 传递给驱动程序。
- 驱动程序创建所需的 DMA 映射,以便设备可以从 RAM 读取数据。
- 驱动器向设备发送信号,表示数据准备就绪。
- 设备从 RAM 读取数据并传输。
- 传输完成后,设备发出硬中断信号,表示传输完成。
- 驱动程序注册的传输完成硬中断处理程序运行。 对于许多设备,此处理程序只是生成
NET_RX
软中断,触发 NAPI 轮询循环开始运行。 - 软中断触发轮询函数运行,并调用驱动程序以解除 DMA 映射、释放数据包。
接下来各节会详细介绍以上整个流程。
下面探讨的协议层是 IP 和 UDP 协议层。 本文介绍的许多信息也可作为其他协议层的参考。
详细探讨
与姊妹篇类似,本文将探讨 Linux 3.13.0 版本内核,贯穿全文提供了 GitHub 代码链接和代码片段。
从如何在内核中注册协议族、套接字子系统如何使用协议族开始探讨,然后探讨协议族接收数据。
协议族注册
当用户程序中运行这样一段代码来创建 UDP 套接字时,会发生什么?
sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
简而言之,Linux 内核查找 UDP 协议栈导出的一组函数,它们处理包括发送和接收网络数据在内的许多事情。 要准确理解其工作原理,必须深入 AF_INET
地址族代码。
Linux 内核在内核初始化的早期执行 inet_init
函数。 此函数注册 AF_INET
协议族、协议族中的各种协议栈(TCP、UDP、ICMP 和 RAW),并调用初始化程序使协议栈准备好处理网络数据。 您可以在 ./net/ipv4/af_inet.c 中找到 inet_init
的代码。
AF_INET
协议族导出了一个具有 create
函数的结构。 当用户程序创建套接字时,内核会调用此函数:
static const struct net_proto_family inet_family_ops = { .family = PF_INET, .create = inet_create, .owner = THIS_MODULE, };
inet_create
函数接受传递给套接字系统调用的参数,搜索已注册的协议,以找到链接到套接字的一组操作。 看一看:
/* 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; }
稍后,复制 answer
的 ops
字段到套接字结构中,answer
持有协议栈相关的引用:
sock->ops = answer->ops;
可以在 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
,并由 ./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
的!
套接字发送网络数据
用户程序想要发送 UDP 网络数据,因此它使用 sendto
系统调用,可能像这样:
ret = sendto(socket, buffer, buflen, 0, &dest, sizeof(dest));
此系统调用经过Linux 系统调用层,并落在./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
的目标地址构造一个结构,让我们来看一下:
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; }
此段代码复制用户程序传入的 addr
到内核数据结构 address
中,然后以 msg_name
嵌入到 struct msghdr
结构中。 类似于 userland 程序不调用 sendto
,而是直接调用 sendmsg
时所做的操作。内核提供此变化,是因为 sendto
和 sendmsg
都调用到 sock_sendmsg
。
sock_sendmsg
、__sock_sendmsg
和 __sock_sendmsg_nosec
在调用 __sock_sendmsg
之前,sock_sendmsg
会执行一些错误检查,而 __sock_sendmsg
在调用 __sock_sendmsg_nosec
之前也会进行自己的错误检查。__sock_sendmsg_nosec
传递数据到更深层的套接字子系统中。
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); }
如前一节解释套接字创建时所述,注册到此套接字 ops
结构的 sendmsg
函数是inet_sendmsg
。
inet_sendmsg
从名字不难猜到,这是 AF_INET
协议族提供的一个通用函数。 此函数首先调用sock_rps_record_flow
记录最后一个处理流的 CPU;接收数据包转向会使用该信息。 接下来,查找并调用套接字的内部协议操作结构的 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 协议层 udp_sendmsg
。 udp_sendmsg
是前面看到的 udp_prot
结构导出的。此函数调用从通用 AF_INET
协议族过渡到 UDP 协议栈。
UDP 协议层
udp_sendmsg
udp_sendmsg
函数位于 ./net/ipv4/udp.c。 整个函数相当长,因此我们将探讨其中的一些部分。 如果你想完整地阅读它,请点击前面的链接。