第13章 网络通信:数据报文的发送

第12章学习了Linux套接字的创建、监听和连接,并重点分析了TCP建立连接时的三次握手过程。
本章将从应用层到内核来研究数据包的发送过程。

13.1 发送相关接口

Linux内核为套接字提供了多个发送数据的接口,接口定义如下:

#include <sys/types.h>
#include <sys/socket.h>
 
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
  • send 只能用于处理已连接状态的套接字(注意,从第11章的内容已经知道,无论是UDP还是TCP,都可以进行连接)。
  • sendto 可以在调用时指定目的地址。如果套接字已经是连接状态,那么目的地址 dest_addr 与地址长度就应该为 NULL0,不然就可能会返回错误。
  • sendmsg 则比较特殊,无论是要发送的数据还是目的地址,都保存在 msg 中。其中 msg.msg_namemsg.msg_len 用于指明目的地址,而 msg.msg_iov 则用于保存要发送的数据。这三个系统调用都支持设置指示标志位 flags

说明

稍微现代些的系统调用,一般都会拥有或保留一个指示标志参数。通过标志位 flags,可以从容地为系统调用增加新功能,并同时兼容老版本。第1章中介绍的 dupdup2dup3 则是这方面的一个反面典型。在不支持 flag 的情况下,不得不一再创建新的 dup 接口,直到 dup3 加入了对 flag 的支持为止。

由于 socket 同时还是文件描述符,所以为文件提供的写操作(如 writewritev 等),也可以被 socket 套接字直接调用,在此就不重复叙述了。

13.2 数据包从用户空间到内核空间的流程

从13.1节可知,socket 套接字在发送数据包时有多个系统调用,既有套接字本身的发送接口,又可以重用文件描述符的写操作。这些不同的接口是否会导致数据包从用户空间发送到内核空间时走向不同的流程呢?下面让我们通过阅读源码来回答这个问题。

send 的内核实现代码如下:

SYSCALL_DEFINE4(send, int, fd, void __user *, buff, size_t, len,
        unsigned, flags)
{
    /*
    send可以视为 sendto 的一种特例,即不设置目的地址的 sendto 调用.
    所以内核实现也是让 send 直接调用 sendto.
    */
    return sys_sendto(fd, buff, len, flags, NULL, 0);
}

既然其内核实现是让 send 直接调用 sendto,那么,下面我们就来看一下 sendto 的内核实现,代码如下:

SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
       unsigned, flags, struct sockaddr __user *, addr,
       int, addr_len)
{
    struct socket *sock;
    struct sockaddr_storage address;
    int err;
    struct msghdr msg;
    struct iovec iov;
    int fput_needed;
 
    /* 长度合法性检查 */
    if (len > INT_MAX)
        len = INT_MAX;
 
    /* 从文件描述符获得套接字 socket 的结构 */
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (!sock)
        goto out;
 
    /* 将数据转换为 iovec 结构,来调用后面的 sendmsg */
    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;
 
    /* 如果设置了地址,则设置 msg_name */
    if (addr) {
        /* 将地址参数复制到内核变量中 */
        err = move_addr_to_kernel(addr, addr_len, (struct sockaddr *)&address);
        if (err < 0)
           goto out_put;
        msg.msg_name = (struct sockaddr *)&address;
        msg.msg_namelen = addr_len;
    }
 
    /* 如果 socket 设置了非阻塞,则消息的标志设置为 DONTWAIT(其实也是非阻塞的语义) */
    if (sock->file->f_flags & O_NONBLOCK)
        flags |= MSG_DONTWAIT;
    msg.msg_flags = flags;
 
    /* 调用 sock_sendmsg 来发送数据包 */
    err = sock_sendmsg(sock, &msg, len);
 
out_put:
    fput_light(sock->file, fput_needed);
out:
    return err;
}

这里又调用到 sock_sendmsg 了,从名字上就能感觉到它可能也会被第三个接口 sendmsg 所调用。下面让我们来验证这个猜想。

SYSCALL_DEFINE3(sendmsg, int, fd, struct msghdr __user *, msg, unsigned, flags)
{
    int fput_needed, err;
    struct msghdr msg_sys;
    /* 通过文件描述符获得 socket 套接字结构 */
    struct socket *sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (!sock)
        goto out;
 
    /* 调用 __sys_sendmsg 来发送数据包 */
    err = __sys_sendmsg(sock, msg, &msg_sys, flags, NULL);
 
    fput_light(sock->file, fput_needed);
out:
    return err;
}

接下来进入 __sys_sendmsg,代码如下:

static int __sys_sendmsg(struct socket *sock, struct msghdr __user *msg,
             struct msghdr *msg_sys, unsigned flags,
             struct used_address *used_address)
{
    struct compat_msghdr __user *msg_compat =
        (struct compat_msghdr __user *)msg;
    struct sockaddr_storage address;
    struct iovec iovstack[UIO_FASTIOV], *iov = iovstack;
    unsigned char ctl[sizeof(struct cmsghdr) + 20]
        __attribute__ ((aligned(sizeof(__kernel_size_t))));
    /* 20 is size of ipv6_pktinfo */
    unsigned char *ctl_buf = ctl;
    int err, ctl_len, iov_size, total_len;
 
    err = -EFAULT;
    /* 从用户空间得到用户消息 */
    if (MSG_CMSG_COMPAT & flags) {
        /* 紧凑消息类型 */
        if (get_compat_msghdr(msg_sys, msg_compat))
            return -EFAULT;
    } else if (copy_from_user(msg_sys, msg, sizeof(struct msghdr)))
        return -EFAULT;
 
    /* do not move before msg_sys is valid */
    err = -EMSGSIZE;
    /* 消息数据块个数检查 */
    if (msg_sys->msg_iovlen > UIO_MAXIOV)
        goto out;
 
    /* Check whether to allocate the iovec area */
    err = -ENOMEM;
    /* 在内核空间申请消息数据长度 */
    iov_size = msg_sys->msg_iovlen * sizeof(struct iovec);
    if (msg_sys->msg_iovlen > UIO_FASTIOV) {
        iov = sock_kmalloc(sock->sk, iov_size, GFP_KERNEL);
        if (!iov)
            goto out;
    }
 
    /* This will also move the address data into kernel space */
    /* 前面只是将消息头,或者说消息的结构体,复制到内核空间,现在是将消息的真正内容,即 iov 的内容复制到内核空间 */
    if (MSG_CMSG_COMPAT & flags) {
        err = verify_compat_iovec(msg_sys, iov,
                      (struct sockaddr *)&address,
                      VERIFY_READ);
    } else
        err = verify_iovec(msg_sys, iov,
                   (struct sockaddr *)&address,
                   VERIFY_READ);
    if (err < 0)
        goto out_freeiov;
    total_len = err;
 
    err = -ENOBUFS;
    /* 与消息数据块类似,复制控制消息块,就不详细描述了 */
    if (msg_sys->msg_controllen > INT_MAX)
        goto out_freeiov;
    ctl_len = msg_sys->msg_controllen;
    if ((MSG_CMSG_COMPAT & flags) && ctl_len) {
        err =
            cmsghdr_from_user_compat_to_kern(msg_sys, sock->sk, ctl,
                          sizeof(ctl));
        if (err)
            goto out_freeiov;
        ctl_buf = msg_sys->msg_control;
        ctl_len = msg_sys->msg_controllen;
    } else if (ctl_len) {
        if (ctl_len > sizeof(ctl)) {
            ctl_buf = sock_kmalloc(sock->sk, ctl_len, GFP_KERNEL);
            if (ctl_buf == NULL)
                goto out_freeiov;
        }
        err = -EFAULT;
        /*
         * Careful! Before this, msg_sys->msg_control contains a user pointer.
         * Afterwards, it will be a kernel pointer. Thus the compiler-assisted
         * checking falls down on this.
         */
        if (copy_from_user(ctl_buf,
                  (void __user __force *)msg_sys->msg_control,
                  ctl_len))
            goto out_freectl;
        msg_sys->msg_control = ctl_buf;
    }
 
    /* 设置消息标志 */
    msg_sys->msg_flags = flags;
 
    /* 如果套接字是非阻塞的,则设置消息标志 MSG_DONTWAIT */
    if (sock->file->f_flags & O_NONBLOCK)
        msg_sys->msg_flags |= MSG_DONTWAIT;
 
    /* 如果这次发送的目的地址与上次成功发送的目的地址一致,那就可以省略安全性检查 */
    if (used_address && msg_sys->msg_name &&
       used_address->name_len == msg_sys->msg_namelen &&
       !memcmp(&used_address->name, msg_sys->msg_name,
           used_address->name_len)) {
       /* 调用不进行安全性检查的函数 */
       err = sock_sendmsg_nosec(sock, msg_sys, total_len);
       goto out_freectl;
    }
 
    /* 调用 sock_sendmsg,需要安全性检查,最终仍然会调用到 sock_sendmsg_nosec 函数 */
    err = sock_sendmsg(sock, msg_sys, total_len);
 
    /* 如果本次发送成功,则保存当前的目的地址 */
    if (used_address && err >= 0) {
        used_address->name_len = msg_sys->msg_namelen;
        if (msg_sys->msg_name)
            memcpy(&used_address->name, msg_sys->msg_name,
                  used_address->name_len);
    }
 
out_freectl:
    if (ctl_buf != ctl)
        sock_kfree_s(sock->sk, ctl_buf, ctl_len);
out_freeiov:
    if (iov != iovstack)
        sock_kfree_s(sock->sk, iov, iov_size);
out:
    return err;
}

看完了 __sys_sendmsg,我们可以确定,无论是哪个发送数据的系统调用,最终都会调用到 sock_sendmsg。下面是 sock_sendmsg 的相关代码:

int sock_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{
    /* kiocb 为内核通用的 IO 请求结构 */
    struct kiocb iocb;
    struct sock_iocb siocb;
    int ret;
 
    /* 初始化同步的内核 IO 请求结构 */
    init_sync_kiocb(&iocb, NULL);
    iocb.private = &siocb;
 
    /* 发送消息 */
    ret = __sock_sendmsg(&iocb, sock, msg, size);
    /* 返回结果表明该消息已经加入队列,要等待完成事件 */
    if (-EIOCBQUEUED == ret)
        ret = wait_on_sync_kiocb(&iocb);
    return ret;
}

这里 __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)
{
    /* 获得套接字在 sock_sendmsg 中设置的 IO 请求 */
    struct sock_iocb *si = kiocb_to_siocb(iocb);
    sock_update_classid(sock->sk);
    /* 初始化套接字的 IO 请求字段 */
    si->sock = sock;
    si->scm = NULL;
    si->msg = msg;
    si->size = size;
    /* 根据不同的套接字类型,调用其发送数据函数 */
    return sock->ops->sendmsg(iocb, sock, msg, size);
}

到此,我们完成了数据包从用户空间到内核空间的流程跟踪。接下来的数据包发送过程,将根据不同的协议,走不同的流程。

13.3 UDP数据包的发送流程

前文已经跟踪了数据包从用户空间到内核空间的流程,本节将以比较简单的UDP协议为例,继续跟踪数据包的发送流程——因为UDP是无连接状态的协议,所以不会给我们的代码分析带来额外的麻烦。

UDP的sendmsg操作函数为udp_sendmsg,代码如下:

int udp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t len)
{
    /* 从inet通用套接字得到inet套接字 */
    struct inet_sock *inet = inet_sk(sk);
    /* 从inet通用套接字得到UDP套接字 */
    struct udp_sock *up = udp_sk(sk);
    struct flowi4 fl4_stack;
    struct flowi4 *fl4;
    int ulen = len;
    struct ipcm_cookie ipc;
    struct rtable *rt = NULL;
    int free = 0;
    int connected = 0;
    __be32 daddr, faddr, saddr;
    __be16 dport;
    u8  tos;
    int err, is_udplite = IS_UDPLITE(sk);
    /* 是否有数据包聚合:或者UDP套接字设置了聚合选项,或者数据包消息指明了还有更多数据 */
    int corkreq = up->corkflag || msg->msg_flags&MSG_MORE;
    int (*getfrag)(void *, char *, int, int, int, struct sk_buff *);
    struct sk_buff *skb;
    struct ip_options_data opt_copy;
    /* 数据包长度检查 */
    if (len > 0xFFFF)
    return -EMSGSIZE;
    /* 检查消息标志,UDP不支持带外数据 */
    if (msg->msg_flags & MSG_OOB) /* Mirror BSD error message compatibility */
        return -EOPNOTSUPP;
    ipc.opt = NULL;
    ipc.tx_flags = 0;
    /* 设置正确的分片函数 */
    getfrag = is_udplite ? udplite_getfrag : ip_generic_getfrag;
    fl4 = &inet->cork.fl.u.ip4;
    if (up->pending) {
        /* 该UDP套接字还有待发的数据包 */
        lock_sock(sk);
        /*  常见的上锁双重检查机制 */
        if (likely(up->pending)) {
            /* 若待发的数据不是INET数据,则报错返回 */
            if (unlikely(up->pending != AF_INET)) {
                release_sock(sk);
                return -EINVAL;
            }
            /* 调到追加数据处 */
            goto do_append_data;
        }
        release_sock(sk);
    }
    ulen += sizeof(struct udphdr);
    if (msg->msg_name) {
        /* 若指定了目标地址,则对其进行校验 */
        struct sockaddr_in * usin = (struct sockaddr_in *)msg->msg_name;
        /* 检查长度 */
        if (msg->msg_namelen < sizeof(*usin))
            return -EINVAL;
        /* 检查协议族.目前只支持AF_INET和AF_UNSPEC协议族 */
        if (usin->sin_family != AF_INET) {
            if (usin->sin_family != AF_UNSPEC)
                return -EAFNOSUPPORT;
        }
        /* 若通过了检查,则设置目的地址与目的端口 */
        daddr = usin->sin_addr.s_addr;
        dport = usin->sin_port;
        /* 目的端口不能为0 */
        if (dport == 0)
            return -EINVAL;
    } else {
        /* 如果没有指定目的地址和目的端口,则当前套接字的状态必须是已连接,即已经调用过connect设置了目的地址 */
        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;
    }
    ipc.addr = inet->inet_saddr;
    ipc.oif = sk->sk_bound_dev_if;
    /* 设置时间戳标志 */
    err = sock_tx_timestamp(sk, &ipc.tx_flags);
    if (err)
        return err;
    /* 发送的消息包含控制数据 */
    if (msg->msg_controllen) {
        /* 虽然这个函数的名字叫作send,其实并没有任何发送动作,而只是将控制消息设置到ipc中 */
        err = ip_cmsg_send(sock_net(sk), msg, &ipc);
        if (err)
            return err;
        /* 设置释放ipc.opt的标志 */
        if (ipc.opt)
            free = 1;
        connected = 0;
    }
    if (!ipc.opt) {
        /* 如果没有使用控制消息指定IP选项,则检查套接字的IP选项设置.如果有,则使用套接字的IP选项 */
        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();
    }
    saddr = ipc.addr;
    ipc.addr = faddr = daddr;
    /* 设置了严格路由 */
    if (ipc.opt && ipc->opt->opt.srr) {
        if (!daddr)
            return -EINVAL;
        faddr = ipc.opt->opt.faddr;
        connected = 0;
    }
    tos = RT_TOS(inet->tos);
    /*
    若有下列情况之一的:
    1)套接字设置了本地路由标志.
    2)发送消息时,指明了不做路由.
    3)设置了IP严格路由选项.
    则设置不查找路由标志
    */
    if (sock_flag(sk, SOCK_LOCALROUTE) ||
        (msg->msg_flags & MSG_DONTROUTE) ||
        (ipc.opt && ipc.opt->opt.is_strictroute)) {
        tos |= RTO_ONLINK;
        connected = 0;
    }
    /* 如果目的地址是多播地址 */
    if (ipv4_is_multicast(daddr)) {
        /* 若未指定出口接口,则使用套接字的多播接口索引 */
        if (!ipc.oif)
            ipc.oif = inet->mc_index;
        /* 若源地址为0,则使用套接字的多播地址 */
        if (!saddr)
            saddr = inet->mc_addr;
        connected = 0;
    }
    /* 连接标志为真,即此次发送的数据包与上次的地址相同,则判断保存的路由缓存是否还可用.*/
    if (connected) {
        /* 从套接字检查并获得保存的路由缓存 */
        rt = (struct rtable *)sk_dst_check(sk, 0);
    }
    /* 若目前路由缓存为空,则需要查找路由 */
    if (rt == NULL) {
        struct net *net = sock_net(sk);
        fl4 = &fl4_stack;
        /* 根据套接字和数据包的信息,初始化flowi4—这是查找路由的key */
        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);
        security_sk_classify_flow(sk, flowi4_to_flowi(fl4));
        /* 查找出口路由 */
        rt = ip_route_output_flow(net, fl4, sk);
        if (IS_ERR(rt)) {
            /* 查找路由失败 */
            err = PTR_ERR(rt);
            rt = NULL;
            if (err == -ENETUNREACH)
                IP_INC_STATS_BH(net, IPSTATS_MIB_OUTNOROUTES);
            goto out;
        }
        err = -EACCES;
        /* 若路由是广播路由,并且套接字非广播套接字 */
        if ((rt->rt_flags & RTCF_BROADCAST) &&
            !sock_flag(sk, SOCK_BROADCAST))
            goto out;
        if (connected) {
            /* 若该UDP为已连接状态,则保存这个路由缓存 */
            sk_dst_set(sk, dst_clone(&rt->dst));
        }
    }
    /* 如果数据包设置了MSG_CONFIRM标志,则是要告诉链路层,对端是可达的.调到do_confrim处,
      可以发现其实现方法是在有neibour信息的情况下,直接更新neibour确认时间戳为当前时间. */
    if (msg->msg_flags&MSG_CONFIRM)
        goto do_confirm;
 back_from_confirm:
    saddr = fl4->saddr;
    if (!ipc.addr)
        daddr = ipc.addr = fl4->daddr;
    /* 没有使用cork选项或MSG_MORE标志.这也是最常见的情况. */
    if (!corkreq) {
        /* 每次都生成一个UDP数据包 */
        skb = ip_make_skb(sk, fl4, getfrag, msg->msg_iov, ulen,
                sizeof(struct udphdr), &ipc, &rt,
                msg->msg_flags);
        err = PTR_ERR(skb);
        /* 成功生成了数据包 */
        if (skb && !IS_ERR(skb)) {
            /* 发送UDP数据包 */
            err = udp_send_skb(skb, fl4);
        }
        goto out;
    }
    lock_sock(sk);
    if (unlikely(up->pending)) {
        /*
        现在马上要做cork处理,但发现套接字已经cork了.
        因此这是一个应用程序bug.释放套接字锁,并返回错误.
        */
        release_sock(sk);
        LIMIT_NETDEBUG(KERN_DEBUG "udp cork app bug 2\n");
        err = -EINVAL;
        goto out;
    }
    /*
     *    Now cork the socket to pend data.
     */
    /* 设置cork中的流信息 */
    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:
    /* 增加UDP数据长度 */
    up->len += ulen;
    /* 向IP数据包中追加新的数据 */
    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);
    if (err) // 若发生错误,则丢弃所有未决的数据包
        udp_flush_pending_frames(sk);
    else if (!corkreq) // 若不在cork即阻塞,则发送所有未决的数据包
        err = udp_push_pending_frames(sk);
    else if (unlikely(skb_queue_empty(&sk->sk_write_queue))) {
        /* 若没有未决的数据包,则重置未决标志 */
        up->pending = 0;
    }
    release_sock(sk);
out:
    /* 清理工作,释放各种资源,并增加相应的统计计数 */
    ip_rt_put(rt);
    if (free)
        kfree(ipc.opt);
    if (!err)
        return len;
    /*
     * 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;
do_confirm:
    dst_confirm(&rt->dst);
    if (!(msg->msg_flags&MSG_PROBE) || len)
        goto back_from_confirm;
    err = 0;
    goto out;
}

一般情况下,在使用UDP发送数据包时很少会使用CORK或MSG_MORE标志,因为我们希望在每次调用发送接口时,就发送一次UDP数据包。因此可以不必考虑CORK和MSG_MORE的情况,而继续追踪udp_send_skb

static int udp_send_skb(struct sk_buff *skb, struct flowi4 *fl4)
{
    struct sock *sk = skb->sk;
    struct inet_sock *inet = inet_sk(sk);
    struct udphdr *uh;
    int err = 0;
    int is_udplite = IS_UDPLITE(sk);
    int offset = skb_transport_offset(skb);
    int len = skb->len - offset;
    __wsum csum = 0;
    /* 创建UDP报文头部 */
    uh = udp_hdr(skb);
    uh->source = inet->inet_sport;
    uh->dest = fl4->fl4_dport;
    uh->len = htons(len);
    uh->check = 0;
    /*如果是轻量级UDP协议,则调用相应的校验和计算函数.
      想了解什么是UDP Lite,请自行wiki.
    */
    if (is_udplite)
        csum = udplite_csum(skb);
    /* 禁止了UDP校验和 */
    else if (sk->sk_no_check == UDP_CSUM_NOXMIT) {
        skb->ip_summed = CHECKSUM_NONE;
        goto send;
    } else if (skb->ip_summed == CHECKSUM_PARTIAL) {
        /* 硬件支持校验和的计算 */
        udp4_hwcsum(skb, fl4->saddr, fl4->daddr);
        goto send;
    } else {
        /* 一般情况下的校验和计算 */
        csum = udp_csum(skb);
    }
    /* 计算UDP的校验和,需要考虑伪首部 */
    uh->check = csum_tcpudp_magic(fl4->saddr, fl4->daddr, len```
 
    uh->check = csum_tcpudp_magic(fl4->saddr, fl4->daddr, len,
                    sk->sk_protocol, csum);
    /* 如果校验和为0,则需要将其设置为0xFFFF。因为UDP的零校验和,有特殊的含义,表示没有校验和。*/
    if (uh->check == 0)
        uh->check = CSUM_MANGLED_0;
send:
    /* 发送IP数据包 */
    err = ip_send_skb(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;
}

至此,UDP已经完成了自己的工作,后面的发送工作将交由IP层来负责.

在没有阅读内核源码时,我相信绝大多数的读者都会认为在使用UDP套接字时,每一次调用send都会产生一个UDP报文.事实上,在一般的项目中,UDP套接字确实也是这样使用的.然而通过阅读源码,我

13.4 TCP 数据包的发送流程

13.3 节追踪了 UDP 数据包的发送流程,本节要学习另外一个重要的传输层协议,TCP 数据包的发送流程.

TCP 的 sendmsg 操作函数为 tcp_sendmsg,代码如下:

int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t size)
{
    struct iovec *iov;
    struct tcp_sock *tp = tcp_sk(sk);
    struct sk_buff *skb;
    int iovlen, flags;
    int mss_now, size_goal;
    int sg, err, copied;
    long timeo;
 
    lock_sock(sk);
    flags = msg->msg_flags;
 
    /* 根据标志,确定发送消息的超时时间:
     * 如果设置了 MSG_DONTWAIT,则超时时间为 0。
     * 若没有设置 MSG_DONTWAIT,则使用套接字的超时时间。
     */
    timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT);
 
    /*
     * 套接字只有处于已连接(ESTABLISHED)和等待关闭(CLOSE_WAIT)的状态下,才能直接发送数据。
     * 已连接状态不用多说。等待关闭状态是指收到对端关闭 (FIN) 数据包,但本端应用还没有关闭连接时,这
     * 时仍然可以发送数据。
     * 在 TCP 协议中,发送 FIN,表示本端不会再发送数据。
     */
    if ((1 << sk->sk_state) & ~(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT))
        /* 等待连接建立。若失败则返回出错 */
        if ((err = sk_stream_wait_connect(sk, &timeo)) != 0)
            goto out_err;
 
    /* 清除 SOCK_ASYNC_NOSPACE 标志 */
    clear_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags);
 
    /* 得到当前的 MSS 长度和数据包的最大长度 */
    mss_now = tcp_send_mss(sk, &size_goal, flags);
 
    /* 准备开始发送,获得用户的数据向量地址及长度 */
    iovlen = msg->msg_iovlen;
    iov = msg->msg_iov;
    copied = 0;
    err = -EPIPE;
 
    /* 错误检查 */
    if (sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN))
        goto out_err;
 
    /* 判断出口路由是否支持分散聚合功能 */
    sg = sk->sk_route_caps & NETIF_F_SG;
 
    /* 逐个发送数据段 */
    while (--iovlen >= 0) {
        /* 得到该数据段的长度及起始地址 */
        size_t seglen = iov->iov_len;
        unsigned char __user *from = iov->iov_base;
        iov++;
 
        /* 循环以保证本数据段的数据全部被发送 */
        while (seglen > 0) {
            int copy = 0;
            /* 获得数据包的最大长度 */
            int max = size_goal;
 
            /* 获得发送队列尾部的 skb,查看是否还有剩余空间 */
            skb = tcp_write_queue_tail(sk);
            if (tcp_send_head(sk)) {
                if (skb->ip_summed == CHECKSUM_NONE)
                    max = mss_now;
                /* 得到本次需要复制的长度 */
                copy = max - skb->len;
            }
 
            /* 本 skb 的数据长度已经超过了最大长度,需要申请新的 skb */
            if (copy <= 0) {
            new_segment:
                /* 检查发送缓冲是否已经超出了限制 */
                if (!sk_stream_memory_free(sk)) {
                    /* 发送缓冲占用内存过多,需要等待 */
                    goto wait_for_sndbuf;
                }
 
                /* 申请新的 skb */
                skb = sk_stream_alloc_skb(sk,
                              select_size(sk, sg),
                              sk->sk_allocation);
                if (!skb) {
                    /* 若分配失败,则需要等待 */
                    goto wait_for_memory;
                }
 
                /* 检查硬件是否支持校验和 */
                if (sk->sk_route_caps & NETIF_F_ALL_CSUM)
                    skb->ip_summed = CHECKSUM_PARTIAL;
 
                /* 加入套接字的发送队列 */
                skb_entail(sk, skb);
                copy = size_goal;
                max = size_goal;
            }
 
            /* 复制长度不能超过数据长度 */
            if (copy > seglen)
                copy = seglen;
 
            /* 判断 skb 的线性空间是否还有空闲 */
            if (skb_availroom(skb) > 0) {
                /* 调整复制长度,不能超过空闲的空间长度 */
                copy = min_t(int, copy, skb_availroom(skb));
                /* 将数据复制到 skb 的空闲空间中 */
                err = skb_add_data_nocache(sk, skb, from, copy);
                if (err)
                    goto do_fault;
            } else {
                /* 如果该 skb 没有足够的空闲的线性空间,则把数据复制到分散聚合页中 */
                int merge = 0;
                /* 获得数据的分片个数 */
                int i = skb_shinfo(skb)->nr_frags;
                /* 获得套接字使用的页 */
                struct page *page = TCP_PAGE(sk);
                /* 获得该页已使用的偏移 */
                int off = TCP_OFF(sk);
 
                /* 判断数据包是否可以和最后一个分片聚合 */
                if (skb_can_coalesce(skb, i, page, off) &&
                    off != PAGE_SIZE) {
                    /* 若可以聚合,则设置 merge 标志 */
                    merge = 1;
                } else if (i == MAX_SKB_FRAGS || !sg) {
                    /* 已经达到分片上限,或者网络设备不支持分散聚合。这时不能再向分片增加任
                       何数据了。 */
                    /*
                     * 为了给新数据腾出空间,需要将老数据尽快发送出去。
                     * 因此设置 PUSH 标志,并更新 pushed_seq。然后跳转到 new_segment,并申请新的
                     * skb。
                     */
                    tcp_mark_push(tp, skb);
                    goto new_segment;
                } else if (page) {
                    /* 该页已满 */
                    if (off == PAGE_SIZE) {
                        put_page(page);
                        TCP_PAGE(sk) = page = NULL;
                        off = 0;
                    }
                } else
                    off = 0;
 
                /* 再次检查复制长度,不能超过该页的空闲长度 */
                if (copy > PAGE_SIZE - off)
                    copy = PAGE_SIZE - off;
 
                /* 增加发送缓存内存占用,若超出限制,则需要等待 */
                if (!sk_wmem_schedule(sk, copy))
                    goto wait_for_memory;
 
                /* 若没有可用的页,则申请新的页 */
                if (!page) {
                    /* Allocate new cache page. */
                    if (!(page = sk_stream_alloc_page(sk)))
                        goto wait_for_memory;
                }
 
                /* 将数据复制到页的相应位置 */
                err = skb_copy_to_page_nocache(sk, from, skb,
                                   page, off, copy);
                if (err) {
                    /*
                     * 即使复制失败,如果该页是新申请的,也应该让套接字拥有该页,以供未来使用。
                     */
                    if (!TCP_PAGE(sk)) {
                        TCP_PAGE(sk) = page;
                        TCP_OFF(sk) = 0;
                    }
                    goto do_error;
                }
 
                /* Update the skb. */
                if (merge) {
                    /*
                     * 若本次数据可以和最后一个分片合并,则更新最后一个分片的长度
                     */
                    skb_frag_size_add(&skb_shinfo(skb)->frags[i - 1], copy);
                } else {
                    /* 这是新的分片,需要为这个分片初始化一些页信息 */
                    skb_fill_page_desc(skb, i, page, off, copy);
                    if (TCP_PAGE(sk)) {
                        /* 该分页是之前分配的,因此增加引用即可 */
                        get_page(page);
                    } else if (off + copy < PAGE_SIZE) {
                        /* 若该分页是新分配的,但还未用完,则增加引用,并将其设置为套接字的
                           发送页,以便未来使用 */
                        get_page(page);
                        TCP_PAGE(sk) = page;
                    }
                }
 
                /* 更新套接字的发送偏移量 */
                TCP_OFF(sk) = off + copy;
            }
 
            /* 若无须复制任何数据,则清除 PUSH 标志 */
            if (!copied)
                TCP_SKB_CB(skb)->tcp_flags &= ~TCPHDR_PSH;
 
            /* 更新各种序列号 */
            tp->write_seq += copy;
            TCP_SKB_CB(skb)->end_seq += copy;
            skb_shinfo(skb)->gso_segs = 0;
 
            /* 更新复制信息 */
            from += copy;
            copied += copy;
 
            /* 判断是否完成了所有的数据拷贝 */
            if ((seglen -= copy) == 0 && iovlen == 0)
                goto out;
 
            /* 如果数据包的长度小于限制,或者设置了 MSG_OOB 标志,则继续向该数据包增加数据 */
            if (skb->len < max || (flags & MSG_OOB))
                continue;
 
            /* 如果当前序列号超过上次 push 的序列号加上通告窗口的一半,则需要将本次数据包尽快发
               送出去 */
            if (forced_push(tp)) {
                /* 将本数据包设置上 PUSH 标志,并更新 push 序列号 */
                tcp_mark_push(tp, skb);
                /* 将所有未决的数据包全都发送出去 */
                __tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);
            } else if (skb == tcp_send_head(sk)) {
                /* 如果套接字上只有当前这个数据包,就发送这一个数据包 */
                tcp_push_one(sk, mss_now);
            }
            continue;
 
/* 等待发送缓存 */
wait_for_sndbuf:
           /* 设置没有发送缓存的标志 */
           set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
/* 等待内存 */
wait_for_memory:
           /* 判断是否已经复制了部分数据 */
           if (copied) {
               /* 去掉 MSG_MORE 标志,表示尽快将复制的数据发送出去 */
               tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH);
           }
           /* 等待空闲内存,可能进入睡眠状态 */
           if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
               goto do_error;
           /* 有了空闲内存,但 MSS 可能已经发生了变化,所以需要重新获取 MSS */
           mss_now = tcp_send_mss(sk, &size_goal, flags);
       }
   }
 
/* out 是正常退出路径 */
out:
    /* 如果成功复制了数据,则调用 tcp_push 将数据包发送出去,但不保证立刻就发送 */
    if (copied)
        tcp_push(sk, flags, mss_now, tp->nonagle);
    /* 释放套接字,返回发送的字节数 */
    release_sock(sk);
    return copied;
 
    /* 复制用户数据错误 */
do_fault:
    /* 如果当前 skb 的数据长度为 0,则需要从套接字的发送队列中将其删除,并释放该 skb */
    if (!skb->len) {
        tcp_unlink_write_queue(skb, sk);
        /* It is the one place in all of TCP, except connection
         * reset, where we can be unlinking the send_head.
         */
        tcp_check_send_head(sk, skb);
        sk_wmem_free_skb(sk, skb);
    }
do_error:
    /* 若出错时已经复制了部分数据,则将已经复制的数据发送出去 */
    if (copied)
        goto out;
out_err:
    /* 若没有复制任何数据,则获取错误值,释放套接字并返回错误 */
    err = sk_stream_error(sk, flags, err);
    release_sock(sk);
    return err;
}

因为 TCP 是一种流协议,所以使用 tcp_sendmsg 发送数据时,内核只是将数据包追加到套接字的发送队列中.真正发送数据的时刻,则是由 TCP 协议来控制的,套接字只能做出指示.tcp_sendmsg 函数是

void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss,
                  int nonagle)
{
     /* 如果套接字是关闭状态,则直接返回 */
    if (unlikely(sk->sk_state == TCP_CLOSE))
        return;
     /* tcp_write_xmit 用于将 TCP 报文发送到网络上 */
    if (tcp_write_xmit(sk, cur_mss, nonagle, 0, GFP_ATOMIC)) {
              /* 如果没有要发送的数据,则重置零窗口探测定时器 */
        tcp_check_probe_timer(sk);
     }
}

下面进入 tcp_write_xmit,代码如下:

static int tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
            int push_one, gfp_t gfp)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct sk_buff *skb;
    unsigned int tso_segs, sent_pkts;
    int cwnd_quota;
    int result;
 
    sent_pkts = 0;
 
    /* 如果不是 push_one(即只发送一个数据包),则进行 MTU 探测 */
    if (!push_one) {
        /* 进行 MTU 探测 */
        result = tcp_mtu_probe(sk);
        /* 若返回为 0,则需要等待探测结果,因此不能发送数据包。 */
        if (!result) {
            return 0;
        } else if (result > 0) {
            sent_pkts = 1;
        }
    }
 
    /* 将发送队列中的数据包,循环发送出去 */
    while ((skb = tcp_send_head(sk))) {
        unsigned int limit;
 
        /* 初始化这个数据包的 TSO 状态。
         * TSO 是 TCP Segment Offload 的缩写。当 TCP 发送数据时,需
         * 要将数据拆分成 MSS 大小的数据包(即多个 skb),然后再增加
         * TCP 首部、IP 首部、计算校验和等。而当网卡支持 TSO 时,内核
         * 只需要增加 TCP 首部即可,其余工作都交由网卡来处理。
         */
        tso_segs = tcp_init_tso_segs(sk, skb, mss_now);
        BUG_ON(!tso_segs);
 
        /* 检查拥塞窗口。若为 0,则不能发送 */
        cwnd_quota = tcp_cwnd_test(tp, skb);
        if (!cwnd_quota)
            break;
 
        /* 检查发送窗口。若为 0,则不能发送 */
        if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now)))
            break;
 
        if (tso_segs == 1) {
            /*
             * 只有一个 TSO 数据段,进行 nagle 算法检查。若返回 0,则不发送
             */
            if (unlikely(!tcp_nagle_test(tp```c
        if (tso_segs > 1 && !tcp_urg_mode(tp))
            limit = tcp_mss_split_point(sk, skb, mss_now,
                            min_t(unsigned int,
                              cwnd_quota,
                              sk->sk_gso_max_segs));
        /*
         * 若数据长度大于限制,则需要分片.
         * 若分片失败,则暂不发送这个数据包.
         */
        if (skb->len > limit &&
            unlikely(tso_fragment(sk, skb, limit, mss_now, gfp)))
            break;
        /* 更新 TCP 控制块的时间戳 */
        TCP_SKB_CB(skb)->when = tcp_time_stamp;
        /* 发送数据包 */
        if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
            break;
        /* 处理发送新数据事件,如调整发送队列,重置重传定时器等 */
    }
}

继续跟踪TCP发送函数 tcp_transmit_skb

tcp_event_new_data_sent(sk, skb);
        /* 更新小包(即小于
MSS大小)的发送时间
 */
        tcp_minshall_update(tp, mss_now, skb);
        /* 更新发送数据包的数量
 */
        sent_pkts += tcp_skb_pcount(skb);
        /* 如果设置了
push_one标志,则只发送一个数据包。因此可直接退出
 */
        if (push_one)
            break;
    }
    /* 如果当前处于拥塞恢复的状态下,则增加这个状态下的发包数量
 */
    if (inet_csk(sk)->icsk_ca_state == TCP_CA_Recovery)
        tp->prr_out += sent_pkts;
    /* 如果发送了数据,则校验发送拥塞窗口
 */
    if (likely(sent_pkts)) {
        tcp_cwnd_validate(sk);
        return 0;
    }
    return !tp->packets_out && tcp_send_head(sk);
}

继续往下跟踪TCP的发送函数 tcp_transmit_skb,代码如下:

static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
              gfp_t gfp_mask)
{
    const struct inet_connection_sock *icsk = inet_csk(sk);
    struct inet_sock *inet;
    struct tcp_sock *tp;
    struct tcp_skb_cb *tcb;
    struct tcp_out_options opts;
    unsigned tcp_options_size, tcp_header_size;
    struct tcp_md5sig_key *md5;
    struct tcphdr *th;
    int err;
 
    BUG_ON(!skb || !tcp_skb_pcount(skb));
 
    /* 判断拥塞控制算法是否需要进行时间采样。如果需要,则获取当前时间
 */
    if (icsk->icsk_ca_ops->flags & TCP_CONG_RTT_STAMP)
        __net_timestamp(skb);
 
    /* 判断是否需要克隆这个数据包
 */
    if (likely(clone_it)) {
        /*
        如果该数据包已经被克隆了,则需要复制
SKB的私有部分。
        如未克隆,则直接克隆该数据包
        */
        if (unlikely(skb_cloned(skb)))
            skb = pskb_copy(skb, gfp_mask);
        else
            skb = skb_clone(skb, gfp_mask);
        if (unlikely(!skb))
            return -ENOBUFS;
    }
 
    inet = inet_sk(sk);
    tp = tcp_sk(sk);
    tcb = TCP_SKB_CB(skb);
    memset(&opts, 0, sizeof(opts));
 
    /* 根据
TCP包的类型计算
TCP选项部分的大小
 */
    if (unlikely(tcb->tcp_flags & TCPHDR_SYN))
        tcp_options_size = tcp_syn_options(sk, skb, &opts, &md5);
    else
        tcp_options_size = tcp_established_options(sk, skb, &opts,
                            &md5);
    /* 得到完整的
TCP首部大小
 */
    tcp_header_size = tcp_options_size + sizeof(struct tcphdr);
 
    /* 判断是否有未确认的数据包
 */
    if (tcp_packets_in_flight(tp) == 0) {
        /* 通知开始发送事件
 */
        tcp_ca_event(sk, CA_EVENT_TX_START);
        /* 若设置了
ooo_okay标志,则表明可以改变发送队列。参见内核的
XPS发送机制
 */
        skb->ooo_okay = 1;
    } else {
        /* 若清除
ooo_okay标志,则表示不能改变发送队列。参见内核的
XPS发送机制
 */
        skb->ooo_okay = 0;
    }
 
    /* 在
skb中为
TCP首部申请空间
 */
    skb_push(skb, tcp_header_size);
    /* 设置
TCP首部的起始位置
 */
    skb_reset_transport_header(skb);
    /* 将数据包加入到发送队列中
 */
    skb_set_owner_w(skb, sk);
 
    /* 构建
TCP首部,并计算校验和
 */
    th = tcp_hdr(skb);
    th->source         = inet->inet_sport;
    th->dest           = inet->inet_dport;
    th->seq            = htonl(tcb->seq);
    th->ack_seq        = htonl(tp->rcv_nxt);
    *(((__be16 *)th) + 6)    = htons(((tcp_header_size >> 2) << 12) |
                    tcb->tcp_flags);
 
    if (unlikely(tcb->tcp_flags & TCPHDR_SYN)) {
        /* RFC1323: The window in SYN & SYN/ACK segments
         * is never scaled.
         */
        th->window    = htons(min(tp->rcv_wnd, 65535U));
    } else {
        th->window    = htons(tcp_select_window(sk));
    }
    th->check          = 0;
    th->urg_ptr        = 0;
 
    /* The urg_mode check is necessary during a below snd_una win probe */
    if (unlikely(tcp_urg_mode(tp) && before(tcb->seq, tp->snd_up))) {
        if (before(tp->snd_up, tcb->seq + 0x10000)) {
            th->urg_ptr = htons(tp->snd_up - tcb->seq);
            th->urg = 1;
        } else if (after(tcb->seq + 0xFFFF, tp->snd_nxt)) {
            th->urg_ptr = htons(0xFFFF);
            th->urg = 1;
        }
    }
 
    /* 构建
TCP选项
 */
    tcp_options_write((__be32 *)(th + 1), tp, &opts);
 
    /* 如果不是
SYN数据包,则尝试设置
ECN状态
 */
    if (likely((tcb->tcp_flags & TCPHDR_SYN) == 0))
        TCP_ECN_send(sk, skb, tcp_header_size);
 
#ifdef CONFIG_TCP_MD5SIG
    /* 计算
TCP MD5 签名
 */
    if (md5) {
        sk_nocaps_add(sk, NETIF_F_GSO_MASK);
        tp->af_specific->calc_md5_hash(opts.hash_location,
                        md5, sk, NULL, skb);
    }
#endif
 
    /* 计算
TCP的校验和
 */
    icsk->icsk_af_ops->send_check(sk, skb);
 
    /* 如果有
ACK标志,则发送
ACK事件通知
 */
    if (likely(tcb->tcp_flags & TCPHDR_ACK))
        tcp_event_ack_sent(sk, tcp_skb_pcount(skb));
 
    /* 如果数据包长度大于
TCP首部,那么自然是有
TCP数据的,所以数据将发送事件通知
 */
    if (skb->len != tcp_header_size)
        tcp_event_data_sent(tp, sk);
 
    /* 增加
TCP发送数据包的统计计数
 */
    if (after(tcb->end_seq, tp->snd_nxt) || tcb->seq == tcb->end_seq)
        TCP_ADD_STATS(sock_net(sk), TCP_MIB_OUTSEGS,
                tcp_skb_pcount(skb));
 
    /* 调用
ip_queue_xmit发送数据报文
 */
    err = icsk->icsk_af_ops->queue_xmit(skb, &inet->cork.fl);
    if (likely(err <= 0))
        return err;
 
    /* 判断是否需要进入拥塞窗口来恢复状态
 */
    tcp_enter_cwr(sk, 1);
 
    /* 因为
NET_XMIT_CN 返回值,不能被看作发送错误。
 所以对于发送返回的错误,需要调用
net_xmit_eval来屏蔽该错误
 */
    return net_xmit_eval(err);
}

至此,TCP也完成了自己的工作,IP层将负责后面的数据包发送工作.

13.5 IP数据包的发送流程

前面两节分别分析学习了UDP和TCP的发送流程。它们在完成各自的工作,并构建对应的首部以后,就将数据包传递给了IP网络层。一般情况下,UDP和TCP使用不同的网络层接口函数来将数据包传递给网络层。下文将分别对UDP和TCP进行详细介绍。

13.5.1 ip_send_skb源码分析

UDP调用 ip_send_skb 将数据包传给网络层,下面是其源码分析:

int ip_send_skb(struct sk_buff *skb)
{
    struct net *net = sock_net(skb->sk);
    int err;
    /* ip_local_out为本机发送IP数据包函数 */
    err = ip_local_out(skb);
    if (err) {
        /* 发送错误 */
        if (err > 0) {
            /* 利用net_xmit_errno转换发送错误值 */
            err = net_xmit_errno(err);
        }
        if (err)
            IP_INC_STATS(net, IPSTATS_MIB_OUTDISCARDS);
    }
    return err;
}

进入 ip_local_out,代码如下:

int ip_local_out(struct sk_buff *skb)
{
    int err;
    /* Linux内核代码充斥了大量的封装函数,如ip_local_out、__ip_local_out,等等 */
    /* 检查netfilter在本机的发送路径 */
    err = __ip_local_out(skb);
    /* 若err为1,则表示通过了netfilter检查 */
    if (likely(err == 1)) {
        /* 调用路由输出函数,发送数据包 */
        err = dst_output(skb);
    }
    return err;
}

进入 __ip_local_out,代码如下:

int __ip_local_out(struct sk_buff *skb)
{
    /* 得到IP首部 */
    struct iphdr *iph = ip_hdr(skb);
    /* 计算IP报文的总长度 */
    iph->tot_len = htons(skb->len);
    /* 计算IP报文的校验和 */
    ip_send_check(iph);
    /* 检查netfilter的localout路径 */
    return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT, skb, NULL,
               skb_dst(skb)->dev, dst_output);
}

Netfilter跳过说明

Netfilter的源码并不复杂,并且由于与当前主题的相关度并不高,所以在此就不对Netfilter的相关代码进行跟踪分析了。我们可以假设在没有使用Netfilter或没有对应的规则时,nf_hook 会返回1。这样发送数据包的关键就在于 dst_output 函数了。

dst_output 函数的实现为 skb_dst(skb)->output(skb)。其中 skb_dst(skb) 为这个数据包找到的路由缓存,output 为其实现发送功能的函数指针。这里面又涉及一个内核常用的编程技巧,利用函数指针将两个层次或功能模块进行隔离解耦。对于内核来说,无论是要发送出去的数据包,还是接收到的数据,在构建完IP报文后,都要通过查找路由来确定下一步的流程。而通过查找到的路由缓存的 inputoutput 函数指针,就可以确定后续的处理。内核提供了几个公共的路由输出函数,应用于不同的场景的路由,如 dst_discard 用于失效的路由,ip_rt_bug 用于非预期的输出,ip_mc_output 用于本机多播输出,而 ip_output 则用于本机向外发送数据包。

因此,对于本机发出的数据包,其路由输出函数即为 ip_output,它的实现非常简单,代码如下:

int ip_output(struct sk_buff *skb)
{
    /* 得到发送设备 */
    struct net_device *dev = skb_dst(skb)->dev;
    /* 增加IP数据包发送统计计数 */
    IP_UPD_PO_STATS(dev_net(dev), IPSTATS_MIB_OUT, skb->len);
    /* 设置数据包的出口设备 */
    skb->dev = dev;
    /* 设置数据包的协议为IP协议 */
    skb->protocol = htons(ETH_P_IP);
    /* 进行Netfilter在POST ROUTING上的检查 */
    return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING, skb, NULL, dev,
              ip_finish_output,
              !(IPCB(skb)->flags & IPSKB_REROUTED));
}

通过了Netfilter在POST ROUTING上的检查后,数据包将进入 ip_finish_output,代码如下:

static int ip_finish_output(struct sk_buff *skb)
{
#if defined(CONFIG_NETFILTER) && defined(CONFIG_XFRM)
    /* 如果是路由缓存表示需要变换 */
    if (skb_dst(skb)->xfrm != NULL) {
        /* 设置上重新选路的标志 */
        IPCB(skb)->flags |= IPSKB_REROUTED;
        return dst_output(skb);
    }
#endif
    /* 如果数据包长度超过MTU,并且数据包不是GSO数据包 */
    if (skb->len > ip_skb_dst_mtu(skb) && !skb_is_gso(skb)){
        /* 执行IP分片,因不是本文重点,故略过 */
        return ip_fragment(skb, ip_finish_output2);
    }
    else {
        /* 进入真正的三层发送函数 */
        return ip_finish_output2(skb);
    }
}

继续进入 ip_finish_output2,代码如下:

static inline int ip_finish_output2(struct sk_buff *skb)
{
    struct dst_entry *dst = skb_dst(skb);
    struct rtable *rt = (struct rtable *)dst;
    struct net_device *dev = dst->dev;
    unsigned int hh_len = LL_RESERVED_SPACE(dev);
    struct neighbour *neigh;
    /* 根据路由类型是多播或广播,来增加相应的计数 */
    if (rt->rt_type == RTN_MULTICAST) {
        IP_UPD_PO_STATS(dev_net(dev), IPSTATS_MIB_OUTMCAST, skb->len);
    } else if (rt->rt_type == RTN_BROADCAST)
        IP_UPD_PO_STATS(dev_net(dev), IPSTATS_MIB_OUTBCAST, skb->len);
    /* 检查数据包的首部是否还有存放二层首部的空间 */
    if (unlikely(skb_headroom(skb) < hh_len && dev->header_ops)) {
        struct sk_buff *skb2;
        /* 重新申请一个足够空间的skb */
        skb2 = skb_realloc_headroom(skb, LL_RESERVED_SPACE(dev));
        if (skb2 == NULL) {
            kfree_skb(skb);
            return -ENOMEM;
        }
        /* 如果原数据包属于某个套接字,则将新数据包也设置成归属于这个套接字 */
        if (skb->sk)
            skb_set_owner_w(skb2, skb->sk);
        /* 释放原数据包的内存空间,让原数据包的skb指针指向新数据包的内存空间 */
        kfree_skb(skb);
        skb = skb2;
    }
    rcu_read_lock();
    /* 获得路由的neighbour信息 */
    neigh = dst_get_neighbour(dst);
    if (neigh) {
        /* 调用neighbour层的输出接口.是否能够立刻发送,依赖于neighbour的状态 */
        int res = neigh_output(neigh, skb);
        rcu_read_unlock();
        return res;
    }
    rcu_read_unlock();
    /* 若该路由没有neighbour的信息,则输出报错 */
    if (net_ratelimit())
        printk(KERN_DEBUG "ip_finish_output2: No header cache and no neighbour!\n");
    kfree_skb(skb);
    return -EINVAL;
}

13.5.2 ip_queue_xmit源码分析

13.5.1节的 ip_send_skb 是UDP调用的IP层的输出接口,而TCP调用的IP层输出接口则为 ip_queue_xmit。下面来看看相应的源码:

int ip_queue_xmit(struct sk_buff *skb, struct flowi *fl)
{
    struct sock *sk = skb->sk;
    struct inet_sock *inet = inet_sk(sk);
    struct ip_options_rcu *inet_opt;
    struct flowi4 *fl4;
    struct rtable *rt;
    struct iphdr *iph;
    int res;
    /* 判断数据包是否有路由,如果已经有了,就直接跳到packet_routed */
    rcu_read_lock();
    inet_opt = rcu_dereference(inet->inet_opt);
    fl4 = &fl->u.ip4;
    rt = skb_rtable(skb);
    if (rt != NULL)
        goto packet_routed;
    /* 从套接字获得合法的路由(需要检查是否过期) */
    rt = (struct rtable *)__sk_dst_check(sk, 0);
    if (rt == NULL) {
        __be32 daddr;
        daddr = inet->inet_daddr;
        /* 如果有IP严格路由选项,则使用选项中的地址作为目的地址进行路由查询 */
        if (inet_opt && inet_opt->opt.srr)
            daddr = inet_opt->opt.faddr;
        /* 进行路由查找 */
        rt = ip_route_output_ports(sock_net(sk), fl4, sk,
                     daddr, inet->inet_saddr,
                     inet->inet_dport,
                     inet->inet_sport,
                     sk->sk_protocol,
                     RT_CONN_FLAGS(sk),
                     sk->sk_bound_dev_if);
        if (IS_ERR(rt))
            goto no_route;
        /* 根据路由的接口的特性设置套接字特性 */
        sk_setup_caps(sk, &rt->dst);
    }
    /* 给数据包设置路由 */
    skb_dst_set_noref(skb, &rt->dst);
packet_routed:
    /* 如果有IP严格路由选项 */
    if (inet_opt && inet_opt->opt.is_strictroute && fl4->daddr != rt->rt_gateway)
        goto no_route;
    /* 分配IP首部和选项空间 */
    skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0));
    /* 设置IP首部位置 */
    skb_reset_network_header(skb);
    /* 得到数据包IP首部的指针 */
    iph = ip_hdr(skb);
    /* 构建IP首部 */
    *((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff));
    /* 如不能分片,则在IP首部设置IP_DF标志 */
    if (ip_dont_fragment(sk, &rt->dst) && !skb->local_df)
        iph->frag_off = htons(IP_DF);
    else
        iph->frag_off = 0;
    iph->ttl      = ip_select_ttl(inet, &rt->dst);
    iph->protocol = sk->sk_protocol;
    iph->saddr    = fl4->saddr;
    iph->daddr    = fl4->daddr;
    /* Transport layer set skb->h.foo itself. */
    /* 构建IP选项 */
    if (inet_opt && inet_opt->opt.optlen) {
        iph->ihl += inet_opt->opt.optlen >> 2;
        ip_options_build(skb, &inet_opt->opt, inet->inet_daddr, rt, 0);
    }
    /* 选择合适的IP identifier */
    ip_select_ident_more(iph, &rt->dst, sk,
               (skb_shinfo(skb)->gso_segs ?: 1) - 1);
    /* 根据套接字选项,设置数据包的优先级和标记 */
    skb->priority = sk->sk_priority;
    skb->mark = sk->sk_mark;
    /* 发送数据包 */
    res = ip_local_out(skb);
    rcu_read_unlock();
    return res;
no_route:
    rcu_read_unlock();
    IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);
    kfree_skb(skb);
    return -EHOSTUNREACH;
}

ip_queue_xmit 最终也是调用 ip_local_out 发送本机的数据包。该函数已经在前面跟踪分析过了,所以在此就不再重复了。

13.6 底层模块数据包的发送流程

13.5节分析了IP网络层的数据包的发送流程,并最终跟踪到其调用邻居模块的发送接口。为什么内核会有一个邻居模块呢?本质上数据包的发送和接收都依赖于数据链路层(二层)的地址即硬件地址,网卡只接受二层目的地址为自己地址的数据包(或者多播、广播地址)。所谓的IP地址(三层)只是一个逻辑地址,其实际用途是用来寻径的。那么内核在发送数据包的时候,就需要填充正确的二层硬件地址才能将数据包成功地发送出去。这里就有了一个需求,即需要将三层网络地址“映射”为正确的二层硬件地址。对于IPv4来说,这是由ARP协议来实现的,而对于IPv6来说,其邻居发现协议是由ICMPv6来实现的。因此,对于内核来说,一方面是为了屏蔽不同的邻居协议的实现细节;另一方面,使用同一个邻居模块,对外可以保证相同的邻居状态机和一致的接口。

13.5节中,二层数据包的发送接口为 neigh_output,其源码如下:

static inline int neigh_output(struct neighbour *n, struct sk_buff *skb)
{
    struct hh_cache *hh = &n->hh;
    /*
    若邻居状态为连接状态:永久邻居,不需要ARP,可到达三种情况,
    并且存在硬件地址,则直接调用neigh_hh_output来发送.
    不然则通过邻居的输出函数发送—会根据邻居状态使用不同的接口.
    */
    if ((n->nud_state & NUD_CONNECTED) && hh->hh_len)
        return neigh_hh_output(hh, skb);
    else
        return n->output(n, skb);
}

先跟踪第一种情况,来查看 neigh_hh_output 的代码:

static inline int neigh_hh_output(struct hh_cache *hh, struct sk_buff *skb)
{
    unsigned seq;
    int hh_len;
    /*
    使用seqlock读取硬件地址.
     seqlock一般用在频繁读操作,偶尔写操作的情况下.读操作并不会真正地上锁,因此不会阻塞其他读操
     作和写操作,并通过序号来保证读出数据的完整性;写操作会使用spinlock来保证同一时间只有一个写
     操作.
    */
    do {
       int hh_alen;
       seq = read_seqbegin(&hh->hh_lock);
       hh_len = hh->hh_len;
       hh_alen = HH_DATA_ALIGN(hh_len);
       memcpy(skb->data - hh_alen, hh->hh_data, hh_alen);
    } while (read_seqretry(&hh->hh_lock, seq));
    /* 保存硬件地址 */
    skb_push(skb, hh_len);
    /* 调用底层发送数据包接口 */
    return dev_queue_xmit(skb);
}

下面再来分析邻居在不同状态下的发送接口,在此,以IPv4的ARP协议为例来进行分析。

首先我们来看看 NUD_NOARP 状态,代码如下:

neigh->nud_state = NUD_NOARP;
neigh->ops = &arp_direct_ops;
neigh->output = neigh_direct_output;

NUD_NOARP 状态下,neigh 的发送接口为 neigh_direct_output,代码如下:

int neigh_direct_output(struct neighbour *neigh, struct sk_buff *skb)
{
    return dev_queue_xmit(skb);
}

这个函数“码如其名”,就是直接调用底层的发送接口。

再来看看 NUD_VALID 状态和其余状态,代码如下:

if (dev->header_ops->cache)
    neigh->ops = &arp_hh_ops;
else
    neigh->ops = &arp_generic_ops;
if (neigh->nud_state & NUD_VALID)
    neigh->output = neigh->ops->connected_output;
else
    neigh->output = neigh->ops->output;

若网卡提供了首部缓存的功能,则邻居的操作函数为 arp_hh_ops,不然则为 arp_generic_ops。对于 arp_generic_ops 来说,若邻居状态为 NUD_VALID,则输出函数为 neigh_connected_output(与 NUD_NOARP 状态相同),其余状态则为 neigh_resolve_output,代码如下:

int neigh_resolve_output(struct neighbour *neigh, struct sk_buff *skb)
{
    struct dst_entry *dst = skb_dst(skb);
    int rc = 0;
    if (!dst)
        goto discard;
     /* 根据```c
     /* 根据具体的邻居发现协议,发送探测邻居数据包。对于IPv4来说,就是ARP请求。如果成功得到邻
       居的地址,则返回成功(数值0),不然则返回错误值
 */
    if (!neigh_event_send(neigh, skb)) {
        /* 有了邻居即对端硬件地址,就可以发送数据包了
 */
        int err;
        struct net_device *dev = neigh->dev;
        unsigned int seq;
        /* 如果网卡有地址缓存功能,并且邻居模块没有对应的硬件地址,则调用网卡功能,填充二层硬件地址
 */
        if (dev->header_ops->cache && !neigh->hh.hh_len)
            neigh_hh_init(neigh, dst);
        /* 下面的代码与neigh_hh_output类似,利用seqlock在无锁的条件下,保证二层地址读取的完整性。
 */
        do {
            __skb_pull(skb, skb_network_offset(skb));
            seq = read_seqbegin(&neigh->ha_lock);
            err = dev_hard_header(skb, dev, ntohs(skb->protocol),
                          neigh->ha, NULL, skb->len);
        } while (read_seqretry(&neigh->ha_lock, seq));
        /* 若成功读取了硬件地址,则调用底层发送函数,将数据包发送出去。
 */
        if (err >= 0)
            rc = dev_queue_xmit(skb);
        else
            goto out_kfree_skb;
    }
out:
    return rc;
discard:
    NEIGH_PRINTK1("neigh_resolve_output: dst=%p neigh=%p\n",
              dst, neigh);
out_kfree_skb:
    rc = -EINVAL;
    kfree_skb(skb);
    goto out;
}

邻居模块除了相应的发送过程外,更为重要的是邻居模块状态变迁的状态机,以及邻居发现协议的实现。但是这两部分与本章的主题关联并不大,代码也不复杂,熟悉相关协议的读者可以很容易看懂该部分代码。

邻居模块调用的底层发送接口为 dev_queue_xmit,实质上数据包会首先进入内核的TC模块的队列(默认情况下,网卡使用的TC队列为PFIFO_FAST队列),然后根据队列算法,让合适的数据包出队(之所以说合适的,是因为不同的TC队列,其出队的算法不同),并调用网卡的发送函数,将数据包发送到网卡。如果这时网卡满足发送条件,则数据包将会被真正地发送出去。若不满足发送条件,则将利用发送软中断,过段时间再进行下一轮尝试。