译|Monitoring and Tuning the Linux Networking Stack: Sending Data(一)

简介: 译|Monitoring and Tuning the Linux Networking Stack: Sending Data

TL;DR

本文解释了 Linux 内核的计算机如何发送数据包,以及当数据包从用户程序流向网络硬件时,如何监控和调优网络栈的每个组件。

本文是之前的文章 监控和调优 Linux 网络栈:接收数据 的姊妹篇。

如果不阅读内核的源代码,不深入了解到底发生了什么,就不可能调优或监控 Linux 网络栈。

希望本文能给想做这方面工作的人提供参考。

关于监控和调优 Linux 网络栈的一般性建议

正如在上一篇文章中提到的,Linux 网络栈是复杂的,没有一刀切的监控或调优解决方案。 如果您真的想调优网络栈,您别无选择,只能投入大量的时间、精力和金钱来了解网络系统的各个部分是如何交互的。

本文中提供的许多示例设置仅用于说明目的,并不是对某个配置或默认设置的推荐或反对。 在调整任何设置之前,您应该围绕您需要监控的内容制定一个参考框架,以注意到有意义的变化。

网络连接到计算机时调整网络设置是危险的;你很容易地把自己锁在外面,或者完全关闭你的网络。 不要在生产机器上调整这些设置;相反,如果可能的话,在新机器上进行调整,再投入生产中。

概览

作为参考,您可能需要手边有一份设备数据手册。 这篇文章将研究由 igb 设备驱动程序控制的 Intel I350 以太网控制器。 您可以找到该数据手册(警告:大型 PDF)供您参考

网络数据从用户程序到网络设备的流程概览:

  1. 使用系统调用(如sendtosendmsg等)写入数据。
  2. 数据通过套接字子系统传递到套接字协议族的系统(本例是 AF_INET)。
  3. 协议族通过协议层传递数据,协议层(在许多情况下)将数据转成数据包。
  4. 数据通过路由层,沿途填充目标和邻居缓存(如果是冷缓存)。 如果需要查找以太网地址,会生成 ARP 流量。
  5. 在通过协议层之后,数据包到达设备无关层。
  6. 使用 XPS(如果启用)或哈希函数选择输出队列。
  7. 调用设备驱动程序的发送函数。
  8. 然后,数据被传递到输出设备附属的排队规则(qdisc)。
  9. 如果可以,qdisc 将直接传输数据;或将其排队,等待 NET_TX 软中断期间发送。
  10. 最后,数据从 qdisc 传递给驱动程序。
  11. 驱动程序创建所需的 DMA 映射,以便设备可以从 RAM 读取数据。
  12. 驱动器向设备发送信号,表示数据准备就绪。
  13. 设备从 RAM 读取数据并传输。
  14. 传输完成后,设备发出硬中断信号,表示传输完成。
  15. 驱动程序注册的传输完成硬中断处理程序运行。 对于许多设备,此处理程序只是生成 NET_RX 软中断,触发 NAPI 轮询循环开始运行。
  16. 软中断触发轮询函数运行,并调用驱动程序以解除 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;
        }

稍后,复制 answerops 字段到套接字结构中,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 时所做的操作。内核提供此变化,是因为 sendtosendmsg 都调用到 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_sendmsgudp_sendmsg 是前面看到的 udp_prot 结构导出的。此函数调用从通用 AF_INET 协议族过渡到 UDP 协议栈

UDP 协议层

udp_sendmsg

udp_sendmsg 函数位于 ./net/ipv4/udp.c。 整个函数相当长,因此我们将探讨其中的一些部分。 如果你想完整地阅读它,请点击前面的链接。

目录
相关文章
|
6月前
|
监控 Linux
Linux的epoll用法与数据结构data、event
Linux的epoll用法与数据结构data、event
88 0
|
运维 监控 网络协议
译|llustrated Guide to Monitoring and Tuning the Linux Networking Stack: Receiving Data
译|llustrated Guide to Monitoring and Tuning the Linux Networking Stack: Receiving Data
137 0
|
Linux
linux下的内存查看(virt,res,shr,data的意义)
linux下的内存查看(virt,res,shr,data的意义)
170 0
|
SQL 存储 缓存
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(十)
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(十)
356 1
|
SQL 缓存 监控
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(十一)
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(十一)
152 0
|
缓存 监控 Linux
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(九)
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(九)
228 0
|
监控 Linux 调度
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(八)
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(八)
98 0
|
存储 Ubuntu Linux
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(七)
译|Monitoring and Tuning the Linux Networking Stack: Sending Data(七)
144 0
|
3天前
|
Linux
在 Linux 系统中,“cd”命令用于切换当前工作目录
在 Linux 系统中,“cd”命令用于切换当前工作目录。本文详细介绍了“cd”命令的基本用法和常见技巧,包括使用“.”、“..”、“~”、绝对路径和相对路径,以及快速切换到上一次工作目录等。此外,还探讨了高级技巧,如使用通配符、结合其他命令、在脚本中使用,以及实际应用案例,帮助读者提高工作效率。
18 3
|
3天前
|
监控 安全 Linux
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景,包括 ping(测试连通性)、traceroute(跟踪路由路径)、netstat(显示网络连接信息)、nmap(网络扫描)、ifconfig 和 ip(网络接口配置)。掌握这些命令有助于高效诊断和解决网络问题,保障网络稳定运行。
16 2