Linux内核网络协议栈:udp数据包发送(源码解读)

监视和调整Linux网络协议栈:接收数据

监控和调整Linux网络协议栈的图解指南:接收数据

Linux网络 - 数据包的接收过程

Linux网络协议栈:网络包接收过程

Linux内核网络协议栈:udp数据包发送(源码解读)

目录

LINUX内核网络数据包发送(一)

1. 前言

2. 数据包发送宏观视角

3. 协议层注册

4. 通过 socket 发送网络数据

4.1 sock_sendmsg, __sock_sendmsg, __sock_sendmsg_nosec

4.2 inet_sendmsg

5. 总结

Linux网络 - 数据包的发送过程

LINUX内核网络数据包发送(二)——UDP协议层分析

1. 前言

2. udp_sendmsg

2.1 UDP corking

2.2 获取目的 IP 地址和端口

2.3 Socket 发送:bookkeeping 和打时间戳

2.4 辅助消息(Ancillary messages)

2.5 设置自定义 IP 选项

2.6 多播或单播(Multicast or unicast)

2.7 路由

2.8 MSG_CONFIRM: 阻止 ARP 缓存过期

2.9 uncorked UDP sockets 快速路径:准备待发送数据

2.9.1 ip_make_skb

2.9.2 发送数据

2.10 没有被 cork 的数据时的慢路径

2.10.1 ip_append_data

2.10.2 __ip_append_data

2.10.3 Flushing corked sockets

2.11 Error accounting

3. udp_send_skb

4. 监控:UDP 层统计

4.1 /proc/net/snmp

4.2 /proc/net/udp

5. 调优:socket 发送队列内存大小

6. 总结


LINUX内核网络数据包发送(一)

http://kerneltravel.net/blog/2020/network_ljr11/


1. 前言


本文首先从宏观上概述了数据包发送的流程,接着分析了协议层注册进内核以及被socket的过程,最后介绍了通过 socket 发送网络数据的过程。

 

2. 数据包发送宏观视角


从宏观上看,一个数据包从用户程序到达硬件网卡的整个过程如下:

  1. 使用系统调用(如 sendtosendmsg 等)写数据
  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 时会发生什么?

sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)

简单来说,内核会去查找由 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:

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 变量为 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 系统调用:

ret = sendto(socket, buffer, buflen, 0, &dest, sizeof(dest));

该系统调用穿过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

 

Linux网络 - 数据包的发送过程

https://segmentfault.com/a/1190000008926093


 

 

LINUX内核网络数据包发送(二)——UDP协议层分析

http://kerneltravel.net/blog/2020/network_ljr12/


1. 前言


本文分享了Linux内核网络数据包发送在UDP协议层的处理,主要分析了udp_sendmsgudp_send_skb函数,并分享了UDP层的数据统计和监控以及socket发送队列大小的调优。

 

2. udp_sendmsg


这个函数定义在 net/ipv4/udp.c,函数很长,分段来看。

 

2.1 UDP corking


在变量声明和基本错误检查之后,udp_sendmsg 所做的第一件事就是检查 socket 是否“ 塞住”了(corked)。UDP corking 是一项优化技术,允许内核将多次数据累积成单个数据报发送。在用户程序中有两种方法可以启用此选项:

  • 使用 setsockopt 系统调用设置 socket 的 UDP_CORK 选项

  • 程序调用 sendsendto 或 sendmsg 时,带 MSG_MORE 参数

udp_sendmsg 代码检查 up->pending 以确定 socket 当前是否已被塞住(corked),如果是, 则直接跳到 do_append_data 进行数据追加(append)。

int udp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
                size_t len)
{

    /* variables and error checking ... */

  fl4 = &inet->cork.fl.u.ip4;
  if (up->pending) {
          /*
           * There are pending frames.
           * The socket lock must be held while it's corked.
           */
          lock_sock(sk);
          if (likely(up->pending)) {
                  if (unlikely(up->pending != AF_INET)) {
                          release_sock(sk);
                          return -EINVAL;
                  }
                  goto do_append_data;
          }
          release_sock(sk);
  }

2.2 获取目的 IP 地址和端口


接下来获取目的 IP 地址和端口,有两个可能的来源:

  • 如果之前 socket 已经建立连接,那 socket 本身就存储了目标地址

  • 地址通过辅助结构(struct msghdr)传入,正如我们在 sendto 的内核代码中看到的那样

具体逻辑:

/*
 *      Get and verify the address.
 */
  if (msg->msg_name) {
          struct sockaddr_in *usin = (struct sockaddr_in *)msg->msg_name;
          if (msg->msg_namelen < sizeof(*usin))
                  return -EINVAL;
          if (usin->sin_family != AF_INET) {
                  if (usin->sin_family != AF_UNSPEC)
                          return -EAFNOSUPPORT;
          }

          daddr = usin->sin_addr.s_addr;
          dport = usin->sin_port;
          if (dport == 0)
                  return -EINVAL;
  } else {
          if (sk->sk_state != TCP_ESTABLISHED)
                  return -EDESTADDRREQ;
          daddr = inet->inet_daddr;
          dport = inet->inet_dport;
          /* Open fast path for connected socket.
             Route will not be used, if at least one option is set.
           */
          connected = 1;
  }

UDP 代码中出现了 TCP_ESTABLISHED!UDP socket 的状态使用了 TCP 状态来描述。上面的代码显示了内核如何解析该变量以便设置 daddr 和 dport

如果没有 struct msghdr 变量,内核函数到达 udp_sendmsg 函数时,会从 socket 本身检索目的地址和端口,并将 socket 标记为“已连接”。

 

2.3 Socket 发送:bookkeeping 和打时间戳


接下来,获取存储在 socket 上的源地址、设备索引(device index)和时间戳选项(例如SOCK_TIMESTAMPING_TX_HARDWARESOCK_TIMESTAMPING_TX_SOFTWARESOCK_WIFI_STATUS):

ipc.addr = inet->inet_saddr;

ipc.oif = sk->sk_bound_dev_if;

sock_tx_timestamp(sk, &ipc.tx_flags);

2.4 辅助消息(Ancillary messages)


除了发送或接收数据包之外,sendmsg 和 recvmsg 系统调用还允许用户设置或请求辅助数据。用户程序可以通过将请求信息组织成 struct msghdr 类型变量来利用此辅助数据。一些辅助数据类型记录在IP man page中 。

辅助数据的一个常见例子是 IP_PKTINFO。对于 sendmsgIP_PKTINFO 允许程序在发送数据时设置一个 in_pktinfo 变量。程序可以通过填写 struct in_pktinfo 变量中的字段来指定要在 packet 上使用的源地址。如果程序是监听多个 IP 地址的服务端程序,那这是一个很有用的选项。在这种情况下,服务端可能想使用客户端连接服务端的那个 IP 地址来回复客户端,IP_PKTINFO 非常适合这种场景。

setsockopt 可以在socket 级别设置发送包的 IP_TTL和 IP_TOS。而辅助消息允许在每个数据包级别设置 TTL 和 TOS 值。Linux 内核会使用一个数组将 TOS 转换为优先级,后者会影响数据包如何以及何时从 qdisc 中发送出去。

可以看到内核如何在 UDP socket 上处理 sendmsg 的辅助消息:

if (msg->msg_controllen) {
        err = ip_cmsg_send(sock_net(sk), msg, &ipc,
                           sk->sk_family == AF_INET6);
        if (err)
                return err;
        if (ipc.opt)
                free = 1;
        connected = 0;
}

解析辅助消息的工作是由 ip_cmsg_send 完成的,定义在 net/ipv4/ip_sockglue.c 。传递一个未初始化的辅助数据,将会把这个 socket 标记为“未建立连接的”。

 

2.5 设置自定义 IP 选项


接下来,sendmsg 将检查用户是否通过辅助消息设置了的任何自定义 IP 选项。如果设置了 ,将使用这些自定义值;如果没有,那就使用 socket 中(已经在用)的参数:

if (!ipc.opt) {
        struct ip_options_rcu *inet_opt;

        rcu_read_lock();
        inet_opt = rcu_dereference(inet->inet_opt);
        if (inet_opt) {
                memcpy(&opt_copy, inet_opt,
                       sizeof(*inet_opt) + inet_opt->opt.optlen);
                ipc.opt = &opt_copy.opt;
        }
        rcu_read_unlock();
}

接下来,该函数检查是否设置了源记录路由(source record route, SRR)IP 选项。SRR 有两种类型:宽松源记录路由和严格源记录路由。如果设置了此选项,则会记录第一跳地址并将其保存到 faddr,并将 socket 标记为“未连接”。这将在后面用到:

ipc.addr = faddr = daddr;

if (ipc.opt && ipc.opt->opt.srr) {
        if (!daddr)
                return -EINVAL;
        faddr = ipc.opt->opt.faddr;
        connected = 0;
}

处理完 SRR 选项后,将处理 TOS 选项,这可以从辅助消息中获取,或者从 socket 当前值中获取。然后检查:

  1. 是否(使用 setsockopt)在 socket 上设置了 SO_DONTROUTE,或
  2. 是否(调用 sendto 或 sendmsg 时)指定了 MSG_DONTROUTE 标志,或
  3. 是否已设置了 is_strictroute,表示需要严格的 SRR 任何一个为真,tos 字段的 RTO_ONLINK 位将置 1,并且 socket 被视为“未连接”:
tos = get_rttos(&ipc, inet);
if (sock_flag(sk, SOCK_LOCALROUTE) ||
    (msg->msg_flags & MSG_DONTROUTE) ||
    (ipc.opt && ipc.opt->opt.is_strictroute)) {
        tos |= RTO_ONLINK;
        connected = 0;
}

2.6 多播或单播(Multicast or unicast)


接下来代码开始处理 multicast。这有点复杂,因为用户可以通过 IP_PKTINFO 辅助消息 来指定发送包的源地址或设备号,如前所述。

如果目标地址是多播地址:

  1. 将多播设备(device)的索引(index)设置为发送(写)这个 packet 的设备索引,并且

  2. packet 的源地址将设置为 multicast 源地址

如果目标地址不是一个组播地址,则发送 packet 的设备制定为 inet->uc_index(单播), 除非用户使用 IP_PKTINFO 辅助消息覆盖了它。

if (ipv4_is_multicast(daddr)) {
        if (!ipc.oif)
                ipc.oif = inet->mc_index;
        if (!saddr)
                saddr = inet->mc_addr;
        connected = 0;
} else if (!ipc.oif)
        ipc.oif = inet->uc_index;

2.7 路由


现在开始路由,UDP 层中处理路由的代码以快速路径(fast path)开始。如果 socket 已连接,则直接尝试获取路由:

if (connected)
        rt = (struct rtable *)sk_dst_check(sk, 0);

如果 socket 未连接,或者虽然已连接,但路由辅助函数 sk_dst_check 认定路由已过期,则代码将进入慢速路径(slow path)以生成一条路由记录。首先调用 flowi4_init_output 构造一个描述此 UDP 流的变量:

if (rt == NULL) {
        struct net *net = sock_net(sk);

        fl4 = &fl4_stack;
        flowi4_init_output(fl4, ipc.oif, sk->sk_mark, tos,
                           RT_SCOPE_UNIVERSE, sk->sk_protocol,
                           inet_sk_flowi_flags(sk)|FLOWI_FLAG_CAN_SLEEP,
                           faddr, saddr, dport, inet->inet_sport);

然后,socket 及其 flow 实例会传递给安全子系统,这样 SELinux 或 SMACK 这样的系统就可以在 flow 实例上设置安全 ID。接下来,ip_route_output_flow 将调用 IP 路由代码,创建一个路由实例:

security_sk_classify_flow(sk, flowi4_to_flowi(fl4));
rt = ip_route_output_flow(net, fl4, sk);

如果创建路由实例失败,并且返回码是 ENETUNREACH, 则 OUTNOROUTES 计数器将会加 1。

if (IS_ERR(rt)) {
  err = PTR_ERR(rt);
  rt = NULL;
  if (err == -ENETUNREACH)
    IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES);
  goto out;
}

这些统计计数器所在的源文件、其他可用的计数器及其含义,将在下面的 UDP 监控部分分享。

接下来,如果是广播路由,但 socket 的 SOCK_BROADCAST 选项未设置,则处理过程终止。如果 socket 被视为“已连接”,则路由实例将缓存到 socket 上:

err = -EACCES;
if ((rt->rt_flags & RTCF_BROADCAST) &&
    !sock_flag(sk, SOCK_BROADCAST))
        goto out;
if (connected)
        sk_dst_set(sk, dst_clone(&rt->dst));

2.8 MSG_CONFIRM: 阻止 ARP 缓存过期


如果调用 sendsendto 或 sendmsg 的时候指定了 MSG_CONFIRM 参数,UDP 协议层将会如下处理:

  if (msg->msg_flags&MSG_CONFIRM)
          goto do_confirm;
back_from_confirm:

该标志提示系统去确认一下 ARP 缓存条目是否仍然有效,防止其被垃圾回收。 do_confirm 标签位于此函数末尾处:

do_confirm:
        dst_confirm(&rt->dst);
        if (!(msg->msg_flags&MSG_PROBE) || len)
                goto back_from_confirm;
        err = 0;
        goto out;

dst_confirm 函数只是在相应的缓存条目上设置一个标记位,稍后当查询邻居缓存并找到 条目时将检查该标志,我们后面一些会看到。此功能通常用于 UDP 网络应用程序,以减少不必要的 ARP 流量。此代码确认缓存条目然后跳回 back_from_confirm 标签。一旦 do_confirm 代码跳回到 back_from_confirm(或者之前就没有执行到 do_confirm ),代码接下来将处理 UDP cork 和 uncorked 情况。

 

2.9 uncorked UDP sockets 快速路径:准备待发送数据


如果不需要 corking,数据就可以封装到一个 struct sk_buff 实例中并传递给 udp_send_skb,离 IP 协议层更进了一步。这是通过调用 ip_make_skb 来完成的。

先前通过调用 ip_route_output_flow 生成的路由条目也会一起传进来, 它将保存到 skb 里。

/* Lockless fast path for the non-corking case. */
if (!corkreq) {
        skb = ip_make_skb(sk, fl4, getfrag, msg->msg_iov, ulen,
                          sizeof(struct udphdr), &ipc, &rt,
                          msg->msg_flags);
        err = PTR_ERR(skb);
        if (!IS_ERR_OR_NULL(skb))
                err = udp_send_skb(skb, fl4);
        goto out;
}

ip_make_skb 函数将创建一个 skb,其中需要考虑到很多的事情,例如:

  1. MTU

  2. UDP corking(如果启用)

  3. UDP Fragmentation Offloading(UFO)

  4. Fragmentation(分片):如果硬件不支持 UFO,但是要传输的数据大于 MTU,需要软件做分片

大多数网络设备驱动程序不支持 UFO,因为网络硬件本身不支持此功能。我们来看下这段代码,先看 corking 禁用的情况。

 

2.9.1 ip_make_skb


定义在net/ipv4/ip_output.c,这个函数有点复杂。

构建 skb 的时候,ip_make_skb 依赖的底层代码需要使用一个 corking 变量和一个 queue 变量 ,skb 将通过 queue 变量传入。如果 socket 未被 cork,则会传入一个假的 corking 变量和一个空队列。

现在来看看假 corking 变量和空队列是如何初始化的:

struct sk_buff *ip_make_skb(struct sock *sk, /* more args */)
{
        struct inet_cork cork;
        struct sk_buff_head queue;
        int err;

        if (flags & MSG_PROBE)
                return NULL;

        __skb_queue_head_init(&queue);

        cork.flags = 0;
        cork.addr = 0;
        cork.opt = NULL;
        err = ip_setup_cork(sk, &cork, /* more args */);
        if (err)
                return ERR_PTR(err);

如上所示,cork 和 queue 都是在栈上分配的,ip_make_skb 根本不需要它。 ip_setup_cork 初始化 cork 变量。接下来,调用__ip_append_data 并传入 cork 和 queue 变 量:

err = __ip_append_data(sk, fl4, &queue, &cork,
                       &current->task_frag, getfrag,
                       from, length, transhdrlen, flags);

我们将在后面看到这个函数是如何工作的,因为不管 socket 是否被 cork,最后都会执行它。

现在,我们只需要知道__ip_append_data 将创建一个 skb,向其追加数据,并将该 skb 添加 到传入的 queue 变量中。如果追加数据失败,则调用__ip_flush_pending_frame 丢弃数据 并向上返回错误(指针类型):

if (err) {
        __ip_flush_pending_frames(sk, &queue, &cork);
        return ERR_PTR(err);
}

最后,如果没有发生错误,__ip_make_skb 将 skb 出队,添加 IP 选项,并返回一个准备好传递给更底层发送的 skb:

return __ip_make_skb(sk, fl4, &queue, &cork);

2.9.2 发送数据


如果没有错误,skb 就会交给 udp_send_skb,后者会继续将其传给下一层协议,IP 协议:

err = PTR_ERR(skb);
if (!IS_ERR_OR_NULL(skb))
        err = udp_send_skb(skb, fl4);
goto out;

如果有错误,错误计数就会有相应增加。后面的“错误计数”部分会详细介绍。

 

2.10 没有被 cork 的数据时的慢路径


如果使用了 UDP corking,但之前没有数据被 cork,则慢路径开始:

  1. 对 socket 加锁
  2. 检查应用程序是否有 bug:已经被 cork 的 socket 是否再次被 cork
  3. 设置该 UDP flow 的一些参数,为 corking 做准备
  4. 将要发送的数据追加到现有数据

udp_sendmsg 代码继续向下看,就是这一逻辑:

  lock_sock(sk);
  if (unlikely(up->pending)) {
          /* The socket is already corked while preparing it. */
          /* ... which is an evident application bug. --ANK */
          release_sock(sk);

          LIMIT_NETDEBUG(KERN_DEBUG pr_fmt("cork app bug 2\n"));
          err = -EINVAL;
          goto out;
  }
  /*
   *      Now cork the socket to pend data.
   */
  fl4 = &inet->cork.fl.u.ip4;
  fl4->daddr = daddr;
  fl4->saddr = saddr;
  fl4->fl4_dport = dport;
  fl4->fl4_sport = inet->inet_sport;
  up->pending = AF_INET;

do_append_data:
  up->len += ulen;
  err = ip_append_data(sk, fl4, getfrag, msg->msg_iov, ulen,
                       sizeof(struct udphdr), &ipc, &rt,
                       corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);

2.10.1 ip_append_data


这个函数简单封装了__ip_append_data,在调用后者之前,做了两件重要的事情:

  1. 检查是否从用户传入了 MSG_PROBE 标志。该标志表示用户不想真正发送数据,只是做路径探测(例如,确定PMTU)

  2. 检查 socket 的发送队列是否为空。如果为空,意味着没有 cork 数据等待处理,因此调用 ip_setup_cork 来设置 corking

一旦处理了上述条件,就调用__ip_append_data 函数,该函数包含用于将数据处理成数据包的大量逻辑。

 

2.10.2 __ip_append_data


如果 socket 是 corked,则从 ip_append_data 调用此函数;如果 socket 未被 cork,则从 ip_make_skb 调用此函数。在任何一种情况下,函数都将分配一个新缓冲区来存储传入的数据,或者将数据附加到现有数据中。这种工作的方式围绕 socket 的发送队列。等待发送的现有数据(例如,如果 socket 被 cork) 将在队列中有一个对应条目,可以被追加数据。

这个函数很复杂,它执行很多计算以确定如何构造传递给下面的网络层的 skb。

该函数的重点包括:

  1. 如果硬件支持,则处理 UDP Fragmentation Offload(UFO)。绝大多数网络硬件不支持 UFO。如果你的网卡驱动程序支持它,它将设置 NETIF_F_UFO 标记位
  2. 处理支持分散/收集( scatter/gather)IO 的网卡。许多 卡都支持此功能,并使用 NETIF_F_SG 标志进行通告。支持该特性的网卡可以处理数据 被分散到多个 buffer 的数据包;内核不需要花时间将多个缓冲区合并成一个缓冲区中。避 免这种额外的复制会提升性能,大多数网卡都支持此功能
  3. 通过调用 sock_wmalloc 跟踪发送队列的大小。当分配新的 skb 时,skb 的大小由创建它 的 socket 计费(charge),并计入 socket 发送队列的已分配字节数。如果发送队列已经 没有足够的空间(超过计费限制),则 skb 并分配失败并返回错误。我们将在下面的调优部分中看到如何设置 socket 发送队列大小(txqueuelen)
  4. 更新错误统计信息。此函数中的任何错误都会增加“discard”计数。我们将在下面的监控部分中看到如何读取此值

函数执行成功后返回 0,以及一个适用于网络设备传输的 skb。

  • 在 unorked 情况下,持有 skb 的 queue 被作为参数传递给上面描述的__ip_make_skb,在那里 它被出队并通过 udp_send_skb 发送到更底层。

  • 在 cork 的情况下,__ip_append_data 的返回值向上传递。数据位于发送队列中,直到 udp_sendmsg 确定是时候调用 udp_push_pending_frames 来完成 skb,后者会进一步调用 udp_send_skb

 

2.10.3 Flushing corked sockets


现在,udp_sendmsg 会继续,检查__ip_append_skb 的返回值(错误码):

if (err)
        udp_flush_pending_frames(sk);
else if (!corkreq)
        err = udp_push_pending_frames(sk);
else if (unlikely(skb_queue_empty(&sk->sk_write_queue)))
        up->pending = 0;
release_sock(sk);

我们来看看每个情况:

  1. 如果出现错误(错误为非零),则调用 udp_flush_pending_frames,这将取消 cork 并从 socket 的发送队列中删除所有数据

  2. 如果在未指定 MSG_MORE 的情况下发送此数据,则调用 udp_push_pending_frames,它将数据传递到更下面的网络层

  3. 如果发送队列为空,请将 socket 标记为不再 cork

如果追加操作完成并且有更多数据要进入 cork,则代码将做一些清理工作,并返回追加数据的长度:

ip_rt_put(rt);
if (free)
        kfree(ipc.opt);
if (!err)
        return len;

这就是内核如何处理 corked UDP sockets 的。

 

2.11 Error accounting


如果:

  1. non-corking 快速路径创建 skb 失败,或 udp_send_skb 返回错误,或
  2. ip_append_data 无法将数据附加到 corked UDP socket,或
  3. 当 udp_push_pending_frames 调用 udp_send_skb 发送 corked skb 时后者返回错误

仅当返回的错误是 ENOBUFS(内核无可用内存)或 socket 已设置 SOCK_NOSPACE(发送队列已满)时,SNDBUFERRORS 统计信息才会增加:

/*
 * ENOBUFS = no kernel mem, SOCK_NOSPACE = no sndbuf space.  Reporting
 * ENOBUFS might not be good (it's not tunable per se), but otherwise
 * we don't have a good statistic (IpOutDiscards but it can be too many
 * things).  We could add another new stat but at least for now that
 * seems like overkill.
 */
if (err == -ENOBUFS || test_bit(SOCK_NOSPACE, &sk->sk_socket->flags)) {
        UDP_INC_STATS_USER(sock_net(sk),
                        UDP_MIB_SNDBUFERRORS, is_udplite);
}
return err;

我们接下来会在后面的数据监控里看到如何读取这些计数。

 

3. udp_send_skb


udp_sendmsg 通过调用 udp_send_skb 函数将 skb 送到下一网络层,在本文中是 IP 协议层。这个函数做了一些重要的事情:

  1. 向 skb 添加 UDP 头
  2. 处理校验和:软件校验和,硬件校验和或无校验和(如果禁用)
  3. 调用 ip_send_skb 将 skb 发送到 IP 协议层
  4. 更新发送成功或失败的统计计数器

首先,创建 UDP 头:

static int udp_send_skb(struct sk_buff *skb, struct flowi4 *fl4)
{
                /* useful variables ... */

        /*
         * Create a UDP header
         */
        uh = udp_hdr(skb);
        uh->source = inet->inet_sport;
        uh->dest = fl4->fl4_dport;
        uh->len = htons(len);
        uh->check = 0;

接下来,处理校验和。有几种情况:

  1. 首先处理 UDP-Lite 校验和
  2. 接下来,如果 socket 校验和选项被关闭(setsockopt 带 SO_NO_CHECK 参数),它将被标记为校 验和关闭
  3. 接下来,如果硬件支持 UDP 校验和,则将调用 udp4_hwcsum 来设置它。请注意,如果数 据包是分段的,内核将在软件中生成校验和,可以在 udp4_hwcsum 的源代码中看到这一点
  4. 最后,通过调用 udp_csum 生成软件校验和
if (is_udplite)                                  /*     UDP-Lite      */
        csum = udplite_csum(skb);

else if (sk->sk_no_check == UDP_CSUM_NOXMIT) {   /* UDP csum disabled */

        skb->ip_summed = CHECKSUM_NONE;
        goto send;

} else if (skb->ip_summed == CHECKSUM_PARTIAL) { /* UDP hardware csum */

        udp4_hwcsum(skb, fl4->saddr, fl4->daddr);
        goto send;

} else
        csum = udp_csum(skb);

接下来,添加了伪头 :

uh->check = csum_tcpudp_magic(fl4->saddr, fl4->daddr, len,
                              sk->sk_protocol, csum);
if (uh->check == 0)
        uh->check = CSUM_MANGLED_0;

如果校验和为 0,则根据 RFC 768,校验为全 1( transmitted as all ones (the equivalent in one’s complement arithmetic))。最后,将 skb 传递给 IP 协议层并增加统计计数:

send:
  err = ip_send_skb(sock_net(sk), skb);
  if (err) {
          if (err == -ENOBUFS && !inet->recverr) {
                  UDP_INC_STATS_USER(sock_net(sk),
                                     UDP_MIB_SNDBUFERRORS, is_udplite);
                  err = 0;
          }
  } else
          UDP_INC_STATS_USER(sock_net(sk),
                             UDP_MIB_OUTDATAGRAMS, is_udplite);
  return err;

如果 ip_send_skb 成功,将更新 OUTDATAGRAMS 统计。如果 IP 协议层报告错误,并且错误 是 ENOBUFS(内核缺少内存)而且错误 queue(inet->recverr)没有启用,则更新 SNDBUFERRORS

接下来看看如何在 Linux 内核中监视和调优 UDP 协议层。

 

4. 监控:UDP 层统计


两个非常有用的获取 UDP 协议统计文件:

  • /proc/net/snmp
  • /proc/net/udp

 

4.1 /proc/net/snmp


监控 UDP 协议层统计:

cat /proc/net/snmp | grep Udp\:

要准确地理解这些计数,需要仔细地阅读内核代码。一些类型的错误计数并不是只出现在一种计数中,而可能是出现在多个计数中。

  • InDatagrams: Incremented when recvmsg was used by a userland program to read datagram. Also incremented when a UDP packet is encapsulated and sent back for processing.
  • NoPorts: Incremented when UDP packets arrive destined for a port where no program is listening.
  • InErrors: Incremented in several cases: no memory in the receive queue, when a bad checksum is seen, and if sk_add_backlog fails to add the datagram.
  • OutDatagrams: Incremented when a UDP packet is handed down without error to the IP protocol layer to be sent.
  • RcvbufErrors: Incremented when sock_queue_rcv_skb reports that no memory is available; this happens if sk->sk_rmem_alloc is greater than or equal to sk->sk_rcvbuf.
  • SndbufErrors: Incremented if the IP protocol layer reported an error when trying to send the packet and no error queue has been setup. Also incremented if no send queue space or kernel memory are available.
  • InCsumErrors: Incremented when a UDP checksum failure is detected. Note that in all cases I could find, InCsumErrors is incremented at the same time as InErrors. Thus, InErrors - InCsumErros should yield the count of memory related errors on the receive side.

UDP 协议层发现的某些错误会出现在其他协议层的统计信息中。一个例子:路由错误 。 udp_sendmsg 发现的路由错误将导致 IP 协议层的 OutNoRoutes 统计增加。

 

4.2 /proc/net/udp


监控 UDP socket 统计:

cat /proc/net/udp

每一列的意思:

  • sl: Kernel hash slot for the socket
  • local_address: Hexadecimal local address of the socket and port number, separated by :.
  • rem_address: Hexadecimal remote address of the socket and port number, separated by :.
  • st: The state of the socket. Oddly enough, the UDP protocol layer seems to use some TCP socket states. In the example above, 7 is TCP_CLOSE.
  • tx_queue: The amount of memory allocated in the kernel for outgoing UDP datagrams.
  • rx_queue: The amount of memory allocated in the kernel for incoming UDP datagrams.
  • tr, tm->when, retrnsmt: These fields are unused by the UDP protocol layer.
  • uid: The effective user id of the user who created this socket.
  • timeout: Unused by the UDP protocol layer.
  • inode: The inode number corresponding to this socket. You can use this to help you determine which user process has this socket open. Check /proc/[pid]/fd, which will contain symlinks to socket[:inode].
  • ref: The current reference count for the socket.
  • pointer: The memory address in the kernel of the struct sock.
  • drops: The number of datagram drops associated with this socket. Note that this does not include any drops related to sending datagrams (on corked UDP sockets or otherwise); this is only incremented in receive paths as of the kernel version examined by this blog post.

打印这些计数的代码在net/ipv4/udp.c。

 

5. 调优:socket 发送队列内存大小


发送队列(也叫“写队列”)的最大值可以通过设置 net.core.wmem_max sysctl 进行修改。

$ sudo sysctl -w net.core.wmem_max=8388608

sk->sk_write_queue 用 net.core.wmem_default 初始化, 这个值也可以调整。

调整初始发送 buffer 大小:

$ sudo sysctl -w net.core.wmem_default=8388608

也可以通过从应用程序调用 setsockopt 并传递 SO_SNDBUF 来设置 sk->sk_write_queue 。通过 setsockopt 设置的最大值是 net.core.wmem_max

不过,可以通过 setsockopt 并传递 SO_SNDBUFFORCE 来覆盖 net.core.wmem_max 限制, 这需要 CAP_NET_ADMIN 权限。

每次调用__ip_append_data 分配 skb 时,sk->sk_wmem_alloc 都会递增。正如我们所看到 的,UDP 数据报传输速度很快,通常不会在发送队列中花费太多时间。

 

6. 总结


本文重点分析了数据包在传输层(UDP协议)的发送过程,并进行了监控和调优,后面数据包将到达 IP 协议层,下次再分享,感谢阅读。

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

 

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
管理系统,作为一种高效的企业运营管理工具,旨在通过集成化、系统化的手段,对组织内部的各类资源进行规划、协调、控制和优化,以实现企业战略目标,提升运营效率,增强核心竞争力。以下是对管理系统的详细介绍: 一、定义与构成 管理系统是指由硬件设备、软件应用、数据资源、人员以及相关管理制度共同构建的,用于处理、监控、分析和决策各类业务活动的综合信息系统。它通常包括以下几个核心组成部分: 数据采集模块:负责从各类业务环节中实时、准确地收集信息,形成企业的基础数据资源。 数据分析模块:运用统计学、人工智能等技术对数据进行深度挖掘和智能分析,提供决策支持。 业务流程管理模块:设计、执行、监控和优化业务流程,确保各项任务按照预定规则高效运转。 决策支持模块:基于数据分析结果,为管理者提供直观的可视化报告,辅助其进行科学决策。 用户界面与交互模块:提供友好的人机交互界面,方便用户操作使用。 二、主要类型与功能 管理系统根据所针对的管理对象和领域,可分为多种类型,如: 人力资源管理系统(HRM):涵盖招聘、培训、绩效考核、薪酬福利等人力资源全流程管理,提升人才效能。 客户关系管理系统(CRM):集中管理客户信息,优化销售、营销和服务流程,提升客户满意度和忠诚度。 供应链管理系统(SCM):整合供应商、制造商、分销商、零售商等供应链各环节,实现物流、资金流、信息流的协同运作。 企业资源计划系统(ERP):对企业内部财务、生产、采购、库存、销售等各项资源进行全面集成管理,提高整体运营效率。 项目管理系统(PM):对项目全生命周期进行规划、跟踪、控制,确保项目按时、按质、按预算完成。 三、价值与优势 提高效率:自动化工作流程、标准化业务操作,显著减少人工干预,提升工作效率。 优化决策:实时数据分析与预测,提供精准的决策依据,助力管理层做出明智选择。 资源整合:打破部门壁垒,实现信息共享,优化资源配置,降低运营成本。 合规风控:内置法规遵循机制,强化内部控制,降低经营风险。 持续改进:通过对系统数据的持续监控与分析,驱动业务流程持续优化,促进企业创新与发展。 总的来说,管理系统作为现代企业管理的重要工具,以其强大的数据处理能力、智能化的决策支持和高效的业务流程管理,有力推动了企业的数字化转型,助力企业在日益激烈的市场竞争中保持竞争优势。
毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管理系统。毕业设计管
Linux内核网络协议栈是指在操作系统内核中实现的一系列网络协议和功能。它负责处理网络数据包的收发、路由选择、协议解析等一系列操作,以实现网络通信和数据传输。 Linux内核网络协议栈包括以下几个层次: 1. 网络接口层(Network Interface Layer):负责处理物理网络接口的驱动程序和硬件设备的通信。它提供了对底层网络设备的抽象,如以太网、Wi-Fi、蓝牙等。 2. 网络层(Network Layer):负责处理IP协议相关的操作,包括IP地址分配、路由选择和IP数据包的转发等。其中主要的协议有IPv4和IPv6。 3. 传输层(Transport Layer):负责处理端到端的数据传输,主要通过TCP(传输控制协议)和UDP(用户数据报协议)来实现。TCP提供可靠的、面向连接的数据传输,而UDP提供无连接的、不可靠的数据传输。 4. 应用层(Application Layer):负责处理特定应用程序的数据传输和协议,如HTTP、FTP、DNS等。应用层协议依赖于传输层和网络层的支持,通过这些协议实现应用程序之间的通信。 此外,Linux内核还提供了一些额外的功能,如网络地址转换(NAT)、防火墙(iptables)、网络隧道(Tunneling)等,以满足网络通信和安全的需求。 总之,Linux内核网络协议栈是一个复杂而庞大的系统,通过不同层次的协议和功能实现了网络通信的各个方面。它为应用程序提供了丰富的网络功能,使得Linux成为一个强大的网络操作系统。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值