原文链接

监控与调试Linux网络栈的建议

前面的文章里面已经提到,Linux网络栈很复杂,而且没有一个完整监控优化的方案。如果你真的要进行网络栈的调优,需要花费大量的时间和精力来理解清楚网络系统是如何交互的。

在这个博客里有很多例子都可以证明这一点,建议你去做下这些配置。但在做参数调优之前,需要围绕着监控开发一套前后对比参数,用来证明调优的意义

调整正在连接其他物理机的网络配置是比较危险,你自己很容易陷入断网,所以不要在生产机器上进行调整,先在新机器上,等可以的话在到生产的机器

综述

这篇文章主要讲述Intel I350控制器,并且使用igb设备驱动。

首先看下从用户程序到网络设备网络数据的路径如下:

  1. 通过系统调用(sendto, sendmsg等)写数据
  2. 数据通过socket子系统到socket的协议族(AF_INET)
  3. 协议族会将数据传递到协议层中,这时候会组成数据包(packets)
  4. 数据会经过路由层,针对首次发送会填充目的和邻居到cache,如果网络地址需要查找则发送ARP请求
  5. 经过协议层,数据包会到达设备层
  6. 发送队列会通过XPS或者hash函数进行选择
  7. 调用驱动发送函数
  8. 数据会经过发送设备所对应的队列规则(qdisc)
  9. 队列规则(qdisc)可能会直接发送数据,也或者会将数据入队,等待NET_TX去软中断发送 10.最终数据会从qdisc到达驱动 11.驱动会创建DMA映射,从而使得设备可以从RAM中读取到数据 12.驱动发信号给设备,告诉数据已经准备就绪 13.设备从RAM中获取到数据然后发送 14.一旦发送完成,设备会触发中断表示已经发送完成 15.驱动会注册发送完成的中断服务程序,对于大部分的设备来说,这个服务程序只是简单的触发NAPI执行poll循环,并通过NET_RX软中断运行 16.poll循环函数中会调用驱动释放DMA映射与数据包

详细介绍

这篇文章介绍主要针对linux内核3.13,首先我们从协议栈如何注册到内核并被socker子系统使用开始

Protocol family registration

当我们运行下面的代码来创建UDP socket,在内核会发生什么?

sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)

简单来说,内核会查找UDP协议导出的来一系列函数,这些函数做了很多工作,包括发送和接收网络数据,为了理解他们是如何工作,我们看下AF_INET协议栈代码。在内核最早初始化的时候,会执行inet_init函数,这个函数会注册AF_INET协议族,每一个协议都包含在这个族中,(TCP,UDP,ICMP,RAW),并且会调用每一协议的初始化的函数,使其准备好处理网络数据,你可以在./net/ipv4/af_inet.c中找到inet_init函数

AF_INET协议族中导出结构中包含一个创建函数,当用户创建socket的时候,这个函数会被调用.

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

inet_create函数将参数传递到socket系统调用中,并且在注册的协议中查找对应的操作,链接到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;
        }

然后,将指向特定协议的answer所包含的ops拷贝到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, 在./net/ipv4/udp.c中:

struct proto udp_prot = {
  .name		   = "UDP",
  .owner		   = THIS_MODULE,

  /* ... */

  .sendmsg	   = udp_sendmsg,
  .recvmsg	   = udp_recvmsg,

  /* ... */
};
EXPORT_SYMBOL(udp_prot);

Sending network data via a socket

用户程序想要发送UDP网络数据,会使用sendto调用,如下:

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

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中,此数据结构会嵌入到struct msghdr中,这就跟用户程序调用sendmsg来替代sendto一样,因为这两个函数都会执行到sock_sendmsg. 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);
}

如前面的介绍,sendmsg对应的是inet_sendmsg

从名字中你可以猜到,这是AF_INET协议栈的一个通用函数,这个函数首先会调用sock_rps_record_flow用来记录数据流处理的最后CPU,然后函数会在socket内部协议中查找对应的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_sendmsg, 这个函数调用就是从通用AF_INET协议族到UDP协议栈的转变

UDP protocol layer

经过变量声明和一些基本的错误检查后,udp_sendmsg函数首先要做的是检测socket是否corked,UDP corked是一个特性,它允许用户程序要求内核多次调用做数据合并,并通过一个数据包发送,用户程序有两种方式打开这个特性: 1.使用setsockopt系统调用,并传递UDP_CORK 2.当调用send,sendto,或者sendmsg时,使用MSG_MODE flags

udp_sendmsg函数会检查up->pending来决定是否要进行corked,如果需要,会直接做数据的追加。

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);
  }

下一步,通过下面两个可能方式来获取目的地址和端口:

1.socket本身带有目的地址,因为有时候socket曾经连接

2.地址通过auxiliary结构传递,如下代码:

/*
   *      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;
  }

重新回顾代码我们可以看到当用户调用sendto时,内核是如何组织msghdr数据结构。而目前的代码显示了内核为了得到daddr和dport是如何解析结构。

如果udp_sendmsg已经被调用但是msghdr数据结构为空,则目的地址和端口已经从socket中获取到,socket被标志位connected状态

sendmsg和recvmsg系统调用除了发送和接收数据以外,还允许用户设置和获取网络附属的数据。用户的程序在设计msghdr结构是会利用附属数据,大部分的附属数据类型都在IP的man手册里

最熟悉的辅助数据是IP_PKTINFO, 在sendmsg的情况下,该数据类型允许程序设置发送数据时要使用的结构in_pktinfo。程序可以通过在struct in_pktinfo结构中填充字段来指定要在数据包上使用的源地址。针对监听多个IP地址的服务器程序的场景,这是一个有用的选项,这时候服务器程序可能使用相同IP地址回复客户端

同样,IP_TTL和IP_TOS辅助消息允许用户在将数据从用户程序传送到sendmsg时,以每个数据包为基础设置IP数据包TTL和TOS值。请注意,如果需要,可以使用setsockopt在所有传出数据包的套接字级别上设置IP_TTL和IP_TOS,而不是在每个数据包的基础上设置IP_TTL和IP_TOS。 Linux内核使用数组将指定的TOS值转换为优先级。优先级影响数据包从排队规则传输的方式和时间。

我们可以看下内核如何处理辅助数据:

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;
}

接下来,sendmsg将检查用户是否指定了具有辅助消息的任何自定义IP选项。如果选项已设置,它们将被使用。如果不是,则使用此套接字已经使用的选项

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();
}

然后,函数检查是否设置了源记录路由(SRR)IP选项。源记录路由有两种类型:松散和严格。如果设置了此选项,则记录第一跳地址将其存储为faddr,并将套接字标记为“未连接”。这将在以后使用

ipc.addr = faddr = daddr;

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

处理SRR选项后,将从用户设置的消息中或者当前socket中去寻找TOS IP标志,如下检查:

1.socket中SO_DONTROUTE标识位是否置位**

2.当调用sendto或者sendmsg时,MSG_DONTROUTE flag是否存在**

3.或者is_strictroute是否置位

然后tos置上RTO_ONLINK,socket置为not connect状态

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;
}

接下来,代码尝试处理多播。这有点棘手,因为用户可以通过发送IP_PKTINFO消息来指定哪个源地址或者设备索引发送数据包.

如果目的地址是一个多播地址,那么:

1.将数据包写入的设备索引将设置为多播设备索引

2.数据包的源地址将被设置为组播源地址

  1. 除非用户没有通过发送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;

现在,是时候到路由了

UDP层的代码处理路由开始于快速路径,如果socket处于连接,则尝试获取路由结果.

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

如果套接字未连接,或者已经连接,但路由帮助程序sk_dst_check确定路由已过时,则代码将移到慢路径中以生成路由结构。这首先调用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);

一旦构造了这个流结构,套接字及其流结构就会传递给安全子系统,这样SELinux或SMACK等系统就可以在流结构上设置一个安全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统计计数器会递增。

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

这些统计计数器的文件位置以及其他可用计数器及其含义将在下面的UDP监视部分中讨论。

接下来,如果路由用于广播,但套接字选项SOCK_BROADCAST未在套接字上设置,则代码终止。如果套接字被视为“连接”,则路由结构将缓存在套接字上:

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));

如果用户在调用send,sendto或者sendmsg时指定了MSG_CONFIRM, 则UDP协议的处理是:

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

该标志表示系统中ARP缓存条目是否仍然有效并防止其被垃圾收集。 dst_confirm函数只是简单地在目标缓存条目上设置一个标志,当查询到邻居缓存并找到一个条目后,这个标志将被检查。我们稍后会再看到这一点。UDP网络应用程序用这个功能减少不必要的ARP通信量。

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

这段代码确认缓存条目并跳回到back_from_confirm,一旦do_confirm代码跳回到back_from_confirm,代码将尝试处理下一个UDP cork和非corked的情况。

###Fast path for uncorked UDP sockets: Prepare data for transmit

如果没有打开UDP corking的功能,数据可以打包到一个结构体sk_buff中,并传递给udp_send_skb以向下到IP协议层。这是通过调用ip_make_skb完成的。请注意,先前通过调用ip_route_output_flow生成的路由结构也被传入。它将被附加到skb上并在稍后的IP协议层中使用

/* 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拥塞
  3. UDP分片
  4. 分片,如果不支持UFO并且要传输的数据大于MTU。

大多数网络设备驱动程序不支持UFO,因为网络硬件本身不支持此功能。让我们来看一下这段代码,记得UDP是非corked。后面会看到corked的路径。

ip_make_skb函数可以在./net/ipv4/ip_output.c中找到。这个功能有点复杂。 ip_make_skb为了构建skb需要使用的底层的代码,这个skb需要一个corking的结构和队列。在套接字是非corked的情况下,corking的结构和空队列只是作为dummies。

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);

如上所述,corking结构和队列都是栈分配的; ip_make_skb完成后它们都不再需要。人造corking结构是通过调用ip_setup_cork来设置的,它分配内存并初始化结构。接下来,__ip_append_data被调用,并传入这两个参数:

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

稍后我们将看到这个函数是如何工作的,因为它在两种情况下都使用,无论套接字是否corked。目前,我们需要知道的一点是__ip_append_data会创建一个skb,向其中添加数据,并将该skb添加到传入的队列中。如果追加数据失败,将调用__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);

如果没有发生错误,skb将被传递给udp_send_skb,这会将skb传递到下一层网络协议: IP协议栈:

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

如果使用UDP拥塞(cork), 但是还没有数据被corked,路径如下:

  1. 锁住socket
  2. 错误检查,看下socket是否被重复corked
  3. 为UDP corking填写相关的流数据结构
  4. 向已经存在的数据做追加
 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);

ip_append_data函数只是做了封装,在做了下面两个检查之后会调用__ip__append_data:

  1. 检查用户是否使用MSG_PROBE标识,这个标识表示用户并不想真的发送数据
  2. 检查socket发送队列是否为空,如果为空,意味着没有corked数据在pending,调用ip_setup_cork创建cork

__ip__append_data函数在两个地方被调用,当socket是corked状态是,是通过ip_append_data调用,当socket是非corked状态时,则是通过ip_make_skb。这两次调用都会分配新缓冲来存放数据,或者向已经存在的数据做追加。

这部分工作围绕着套接字的发送队列。等待发送的现有数据(例如,如果套接字是corked)会存在于队列中,并可以追加额外数据。

这个函数比较复杂;它执行多次计算来决定如何构建skb,用于传递到更低网络层,并且还会详细检查缓冲区分配过程,当然这个对于理解网络数据如何传输不是必需的

这个函数重要部分有:

如果硬件支持的UFO,则处理UDP分片offloading。绝大多数网络硬件不支持UFO。如果你的网卡的驱动程序支持,会设置功能标志NETIF_F_UFO

处理支持集散列表IO的网卡。许多网卡都支持这个特性,并通过NETIF_F_SG进行标识。此功能可以使网卡在发包时,其中的数据分散在不同的缓冲区中; 内核不需要花费时间将多个缓冲区合并到单个缓冲区中

通过调用sock_wmalloc跟踪发送队列的大小。当一个新的skb被分配时,skb的大小会被记账到它对应的套接字,并且套接字发送队列的分配字节被增加。如果发送队列中没有足够的空间,则skb不会分配,并且返回错误。我们下面将看到如何设置套接字发送队列大小

增加错误统计。此函数中的任何错误都会增加。我们将在下面的监视部分看到如何读取这个值

函数成功完成后,将返回0,并且要传输的数据将会放入到正在等待发送队列skb中

在uncorked的情况下,持有skb的队列会被传递给上面描述的__ip_make_skb,在那里出队并准备通过udp_send_skb发送到低层。

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

现在,udp_sendmsg将继续检查__ip_append_skb的返回值(err below):

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,它会放弃corking,并下发socket发送队列中的所有数据
  2. 如果发送的数据没有MSG_MORE标志,则会调用udp_push_pending_frames,它会直接将数据传递到更底的网络层
  3. 如果发送队列为空,则标识socket不要再corkong

如果追加操作成功完成,并且没有新数据cork,代码会继续并返回追加数据的长度:

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

udp_send_skb函数是udp_sendmsg如何最终将skb发送到网络栈更底层,在这种情况下是IP协议层。这个功能做了一些重要的事情:

  1. 增加UDP头
  2. 处理校验和,包括软件校验和、硬件检验和与没有校验和
  3. 通过调研ip_send_skb试图将skb发送给IP协议层
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. 然后,如果套接字被设置为不生成校验和(通过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的校验和。最后,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,并且没有打开错误队列时,才会增加SNDBUFERRORS统计