• 数据包发送


    解析 socket 函数

    SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
    {
      int retval;
      struct socket *sock;
      int flags;
    ......
      if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
        flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
    
      retval = sock_create(family, type, protocol, &sock);
    ......
      retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
    ......
      return retval;
    }
    int __sock_create(struct net *net, int family, int type, int protocol,
           struct socket **res, int kern)
    {
      int err;
      struct socket *sock;
      const struct net_proto_family *pf;
    ......
      sock = sock_alloc();
    ......
      sock->type = type;
    ......
      pf = rcu_dereference(net_families[family]);
    ......
      err = pf->create(net, sock, protocol, kern);
    ......
      *res = sock;
    
      return 0;
    }

    这里先是分配了一个 struct socket 结构。接下来我们要用到 family 参数。这里有一个 net_families 数组,我们可以以 family 参数为下标,找到对应的 struct net_proto_family。

    /* Supported address families. */
    #define AF_UNSPEC  0
    #define AF_UNIX    1  /* Unix domain sockets     */
    #define AF_LOCAL  1  /* POSIX name for AF_UNIX  */
    #define AF_INET    2  /* Internet IP Protocol   */
    ......
    #define AF_INET6  10  /* IP version 6      */
    ......
    #define AF_MPLS    28  /* MPLS */
    ......
    #define AF_MAX    44  /* For now.. */
    #define NPROTO    AF_MAX
    
    struct net_proto_family __rcu *net_families[NPROTO] __read_mostly;
    //net/ipv4/af_inet.c
    static const struct net_proto_family inet_family_ops = {
      .family = PF_INET,
      .create = inet_create,//这个用于socket系统调用创建
    ......
    }
    static int inet_create(struct net *net, struct socket *sock, int protocol, int kern)
    {
      struct sock *sk;
      struct inet_protosw *answer;
      struct inet_sock *inet;
      struct proto *answer_prot;
      unsigned char answer_flags;
      int try_loading_module = 0;
      int err;
    
      /* Look for the requested type/protocol pair. */
    lookup_protocol:
      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;
      }
    ......
      sock->ops = answer->ops;
      answer_prot = answer->prot;
      answer_flags = answer->flags;
    ......
      sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);
    ......
      inet = inet_sk(sk);
      inet->nodefrag = 0;
      if (SOCK_RAW == sock->type) {
        inet->inet_num = protocol;
        if (IPPROTO_RAW == protocol)
          inet->hdrincl = 1;
      }
      inet->inet_id = 0;
      sock_init_data(sock, sk);
    
      sk->sk_destruct     = inet_sock_destruct;
      sk->sk_protocol     = protocol;
      sk->sk_backlog_rcv = sk->sk_prot->backlog_rcv;
    
      inet->uc_ttl  = -1;
      inet->mc_loop  = 1;
      inet->mc_ttl  = 1;
      inet->mc_all  = 1;
      inet->mc_index  = 0;
      inet->mc_list  = NULL;
      inet->rcv_tos  = 0;
    
      if (inet->inet_num) {
        inet->inet_sport = htons(inet->inet_num);
        /* Add to protocol hash chains. */
        err = sk->sk_prot->hash(sk);
      }
    
      if (sk->sk_prot->init) {
        err = sk->sk_prot->init(sk);
      }
    ......
    }

    在 inet_create 中,我们先会看到一个循环 list_for_each_entry_rcu。在这里,第二个参数 type 开始起作用。因为循环查看的是 inetsw[sock->type]

    static struct inet_protosw inetsw_array[] =
    {
      {
        .type =       SOCK_STREAM,
        .protocol =   IPPROTO_TCP,
        .prot =       &tcp_prot,
        .ops =        &inet_stream_ops,
        .flags =      INET_PROTOSW_PERMANENT |
                INET_PROTOSW_ICSK,
      },
      {
        .type =       SOCK_DGRAM,
        .protocol =   IPPROTO_UDP,
        .prot =       &udp_prot,
        .ops =        &inet_dgram_ops,
        .flags =      INET_PROTOSW_PERMANENT,
         },
         {
        .type =       SOCK_DGRAM,
        .protocol =   IPPROTO_ICMP,
        .prot =       &ping_prot,
        .ops =        &inet_sockraw_ops,
        .flags =      INET_PROTOSW_REUSE,
         },
         {
            .type =       SOCK_RAW,
          .protocol =   IPPROTO_IP,  /* wild card */
          .prot =       &raw_prot,
          .ops =        &inet_sockraw_ops,
          .flags =      INET_PROTOSW_REUSE,
         }
    }

    socket 是用于负责对上给用户提供接口,并且和文件系统关联。而 sock,负责向下对接内核网络协议栈

    在 sk_alloc 函数中,struct inet_protosw *answer 结构的 tcp_prot 赋值给了 struct sock *sk 的 sk_prot 成员。

    tcp_prot 的定义如下,里面定义了很多的函数,都是 sock 之下内核协议栈的动作

    struct proto tcp_prot = {
      .name      = "TCP",
      .owner      = THIS_MODULE,
      .close      = tcp_close,
      .connect    = tcp_v4_connect,
      .disconnect    = tcp_disconnect,
      .accept      = inet_csk_accept,
      .ioctl      = tcp_ioctl,
      .init      = tcp_v4_init_sock,
      .destroy    = tcp_v4_destroy_sock,
      .shutdown    = tcp_shutdown,
      .setsockopt    = tcp_setsockopt,
      .getsockopt    = tcp_getsockopt,
      .keepalive    = tcp_set_keepalive,
      .recvmsg    = tcp_recvmsg,
      .sendmsg    = tcp_sendmsg,
      .sendpage    = tcp_sendpage,
      .backlog_rcv    = tcp_v4_do_rcv,
      .release_cb    = tcp_release_cb,
      .hash      = inet_hash,
        .get_port    = inet_csk_get_port,
    ......
    }

    bind 函数

    SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
    {
      struct socket *sock;
      struct sockaddr_storage address;
      int err, fput_needed;
    
      sock = sockfd_lookup_light(fd, &err, &fput_needed);
      if (sock) {
        err = move_addr_to_kernel(umyaddr, addrlen, &address);
        if (err >= 0) {
          err = sock->ops->bind(sock,
                      (struct sockaddr *)
                      &address, addrlen);
        }
        fput_light(sock->file, fput_needed);
      }
      return err;
    }

    1. sockfd_lookup_light 会根据 fd 文件描述符,找到 struct socket 结构。

    2. 然后将 sockaddr 从用户态拷贝到内核态,然后调用 struct socket 结构里面 ops 的 bind 函数。

    3. 根据前面创建 socket 的时候的设定,调用的是 inet_stream_ops 的 bind 函数,也即调用 inet_bind。

    int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
    {
      struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;
      struct sock *sk = sock->sk;
      struct inet_sock *inet = inet_sk(sk);
      struct net *net = sock_net(sk);
      unsigned short snum;
    ......
      snum = ntohs(addr->sin_port);
    ......
      inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;
      /* Make sure we are allowed to bind here. */
      if ((snum || !inet->bind_address_no_port) &&
          sk->sk_prot->get_port(sk, snum)) {
    ......
      }
      inet->inet_sport = htons(inet->inet_num);
      inet->inet_daddr = 0;
      inet->inet_dport = 0;
      sk_dst_reset(sk);
    }

    bind 里面会调用 sk_prot 的 get_port 函数,也即 inet_csk_get_port 来检查端口是否冲突,是否可以绑定。

    如果允许,则会设置 struct inet_sock 的本方的地址 inet_saddr 和本方的端口 inet_sport,对方的地址 inet_daddr 和对方的端口 inet_dport 都初始化为 0

    listen 函数

    SYSCALL_DEFINE2(listen, int, fd, int, backlog)
    {
      struct socket *sock;
      int err, fput_needed;
      int somaxconn;
    
      sock = sockfd_lookup_light(fd, &err, &fput_needed);
      if (sock) {
        somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
        if ((unsigned int)backlog > somaxconn)
          backlog = somaxconn;
        err = sock->ops->listen(sock, backlog);
        fput_light(sock->file, fput_needed);
      }
      return err;
    }

    1. 在 listen 中,我们还是通过 sockfd_lookup_light,根据 fd 文件描述符,找到 struct socket 结构。

    2. 接着,我们调用 struct socket 结构里面 ops 的 listen 函数。

    3. 根据前面创建 socket 的时候的设定,调用的是 inet_stream_ops 的 listen 函数,也即调用 inet_listen

    int inet_listen(struct socket *sock, int backlog)
    {
      struct sock *sk = sock->sk;
      unsigned char old_state;
      int err;
      old_state = sk->sk_state;
      /* Really, if the socket is already in listen state
       * we can only allow the backlog to be adjusted.
       */
      if (old_state != TCP_LISTEN) {
        err = inet_csk_listen_start(sk, backlog);
      }
      sk->sk_max_ack_backlog = backlog;
    }

    如果这个 socket 还不在 TCP_LISTEN 状态,会调用 inet_csk_listen_start 进入监听状态。

    int inet_csk_listen_start(struct sock *sk, int backlog)
    {
      struct inet_connection_sock *icsk = inet_csk(sk);
      struct inet_sock *inet = inet_sk(sk);
      int err = -EADDRINUSE;
    
      reqsk_queue_alloc(&icsk->icsk_accept_queue);
    
      sk->sk_max_ack_backlog = backlog;
      sk->sk_ack_backlog = 0;
      inet_csk_delack_init(sk);
    
      sk_state_store(sk, TCP_LISTEN);
      if (!sk->sk_prot->get_port(sk, inet->inet_num)) {
    ......
      }
    ......
    }

    这里面建立了一个新的结构 inet_connection_sock,这个结构一开始是 struct inet_sock,inet_csk 其实做了一次强制类型转换,扩大了结构

    struct inet_connection_sock 结构比较复杂各种状态的队列,各种超时时间、拥塞控制等字眼。我们说 TCP 是面向连接的,就是客户端和服务端都是有一个结构维护连接的状态,就是指这个结构。

    accept 函数

    SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *, upeer_sockaddr,
        int __user *, upeer_addrlen)
    {
      return sys_accept4(fd, upeer_sockaddr, upeer_addrlen, 0);
    }
    
    SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr,
        int __user *, upeer_addrlen, int, flags)
    {
      struct socket *sock, *newsock;
      struct file *newfile;
      int err, len, newfd, fput_needed;
      struct sockaddr_storage address;
    ......
      sock = sockfd_lookup_light(fd, &err, &fput_needed);
      newsock = sock_alloc();
      newsock->type = sock->type;
      newsock->ops = sock->ops;
      newfd = get_unused_fd_flags(flags);
      newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name);
      err = sock->ops->accept(sock, newsock, sock->file->f_flags, false);
      if (upeer_sockaddr) {
        if (newsock->ops->getname(newsock, (struct sockaddr *)&address, &len, 2) < 0) {
        }
        err = move_addr_to_user(&address,
              len, upeer_sockaddr, upeer_addrlen);
      }
      fd_install(newfd, newfile);
    ......
    }

    accept 函数的实现,印证了 socket 的原理中说的那样,原来的 socket 是监听 socket,这里我们会找到原来的 struct socket,并基于它去创建一个新的 newsock。这才是连接 socket。除此之外,我们还会创建一个新的 struct file 和 fd,并关联到 socket。

    调用 struct socket 的 sock->ops->accept,也即会调用 inet_stream_ops 的 accept 函数,也即 inet_accept

    int inet_accept(struct socket *sock, struct socket *newsock, int flags, bool kern)
    {
      struct sock *sk1 = sock->sk;
      int err = -EINVAL;
      struct sock *sk2 = sk1->sk_prot->accept(sk1, flags, &err, kern);
      sock_rps_record_flow(sk2);
      sock_graft(sk2, newsock);
      newsock->state = SS_CONNECTED;
    }

    inet_accept 会调用 struct sock 的 sk1->sk_prot->accept,也即 tcp_prot 的 accept 函数,inet_csk_accept 函数

    /*
     * This will accept the next outstanding connection.
     */
    struct sock *inet_csk_accept(struct sock *sk, int flags, int *err, bool kern)
    {
      struct inet_connection_sock *icsk = inet_csk(sk);
      struct request_sock_queue *queue = &icsk->icsk_accept_queue;
      struct request_sock *req;
      struct sock *newsk;
      int error;
    
      if (sk->sk_state != TCP_LISTEN)
        goto out_err;
    
      /* Find already established connection */
      if (reqsk_queue_empty(queue)) {
        long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
        error = inet_csk_wait_for_connect(sk, timeo);
      }
      req = reqsk_queue_remove(queue, sk);
      newsk = req->sk;
    ......
    }
    
    /*
     * Wait for an incoming connection, avoid race conditions. This must be called
     * with the socket locked.
     */
    static int inet_csk_wait_for_connect(struct sock *sk, long timeo)
    {
      struct inet_connection_sock *icsk = inet_csk(sk);
      DEFINE_WAIT(wait);
      int err;
      for (;;) {
        prepare_to_wait_exclusive(sk_sleep(sk), &wait,
                TASK_INTERRUPTIBLE);
        release_sock(sk);
        if (reqsk_queue_empty(&icsk->icsk_accept_queue))
          timeo = schedule_timeout(timeo);
        sched_annotate_sleep();
        lock_sock(sk);
        err = 0;
        if (!reqsk_queue_empty(&icsk->icsk_accept_queue))
          break;
        err = -EINVAL;
        if (sk->sk_state != TCP_LISTEN)
          break;
        err = sock_intr_errno(timeo);
        if (signal_pending(current))
          break;
        err = -EAGAIN;
        if (!timeo)
          break;
      }
      finish_wait(sk_sleep(sk), &wait);
      return err;
    }

    net_csk_accept 的实现,印证了上面我们讲的两个队列的逻辑。如果 icsk_accept_queue 为空,则调用 inet_csk_wait_for_connect 进行等待;等待的时候,调用 schedule_timeout,让出 CPU,并且将进程状态设置为 TASK_INTERRUPTIBLE。

    如果再次 CPU 醒来,我们会接着判断 icsk_accept_queue 是否为空,同时也会调用 signal_pending 看有没有信号可以处理。一旦 icsk_accept_queue 不为空,就从 inet_csk_wait_for_connect 中返回,在队列中取出一个 struct sock 对象赋值给 newsk

    connect 函数

    什么情况下,icsk_accept_queue 才不为空呢?当然是三次握手结束才可以。接下来我们来分析三次握手的过程

     三次握手一般是由客户端调用 connect 发起

    SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
        int, addrlen)
    {
      struct socket *sock;
      struct sockaddr_storage address;
      int err, fput_needed;
      sock = sockfd_lookup_light(fd, &err, &fput_needed);
      err = move_addr_to_kernel(uservaddr, addrlen, &address);
      err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen, sock->file->f_flags);
    }

    connect 函数的实现一开始你应该很眼熟,还是通过 sockfd_lookup_light,根据 fd 文件描述符,找到 struct socket 结构。

    接着,我们会调用 struct socket 结构里面 ops 的 connect 函数,根据前面创建 socket 的时候的设定,调用 inet_stream_ops 的 connect 函数,也即调用 inet_stream_connect

    /*
     *  Connect to a remote host. There is regrettably still a little
     *  TCP 'magic' in here.
     */
    int __inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
            int addr_len, int flags, int is_sendmsg)
    {
      struct sock *sk = sock->sk;
      int err;
      long timeo;
    
      switch (sock->state) {
    ......
      case SS_UNCONNECTED:
        err = -EISCONN;
        if (sk->sk_state != TCP_CLOSE)
          goto out;
    
        err = sk->sk_prot->connect(sk, uaddr, addr_len);
        sock->state = SS_CONNECTING;
        break;
      }
    
      timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);
    
      if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
    ......
        if (!timeo || !inet_wait_for_connect(sk, timeo, writebias))
          goto out;
    
        err = sock_intr_errno(timeo);
        if (signal_pending(current))
          goto out;
      }
      sock->state = SS_CONNECTED;
    }

    connect 函数的实现一开始你应该很眼熟,还是通过 sockfd_lookup_light,根据 fd 文件描述符,找到 struct socket 结构。

    接着,我们会调用 struct socket 结构里面 ops 的 connect 函数,根据前面创建 socket 的时候的设定,调用 inet_stream_ops 的 connect 函数,也即调用 inet_stream_connect

    /*
     *  Connect to a remote host. There is regrettably still a little
     *  TCP 'magic' in here.
     */
    int __inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
            int addr_len, int flags, int is_sendmsg)
    {
      struct sock *sk = sock->sk;
      int err;
      long timeo;
    
      switch (sock->state) {
    ......
      case SS_UNCONNECTED:
        err = -EISCONN;
        if (sk->sk_state != TCP_CLOSE)
          goto out;
    
        err = sk->sk_prot->connect(sk, uaddr, addr_len);
        sock->state = SS_CONNECTING;
        break;
      }
    
      timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);
    
      if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
    ......
        if (!timeo || !inet_wait_for_connect(sk, timeo, writebias))
          goto out;
    
        err = sock_intr_errno(timeo);
        if (signal_pending(current))
          goto out;
      }
      sock->state = SS_CONNECTED;
    }

    如果 socket 处于 SS_UNCONNECTED 状态,那就调用 struct sock 的 sk->sk_prot->connect,也即 tcp_prot 的 connect 函数——tcp_v4_connect 函数

    在 tcp_v4_connect 函数中,ip_route_connect 其实是做一个路由的选择。为什么呢?

    因为三次握手马上就要发送一个 SYN 包了,这就要凑齐源地址、源端口、目标地址、目标端口。

    目标地址和目标端口是服务端的,已经知道源端口是客户端随机分配的,源地址应该用哪一个呢?这时候要选择一条路由,看从哪个网卡出去,就应该填写哪个网卡的 IP 地址

    接下来,在发送 SYN 之前,我们先将客户端 socket 的状态设置为 TCP_SYN_SENT。

    然后初始化 TCP 的 seq num,也即 write_seq,然后调用 tcp_connect 进行发送

    /* Build a SYN and send it off. */
    int tcp_connect(struct sock *sk)
    {
      struct tcp_sock *tp = tcp_sk(sk);
      struct sk_buff *buff;
      int err;
    ......
      tcp_connect_init(sk);
    ......
      buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, true);
    ......
      tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);
      tcp_mstamp_refresh(tp);
      tp->retrans_stamp = tcp_time_stamp(tp);
      tcp_connect_queue_skb(sk, buff);
      tcp_ecn_send_syn(sk, buff);
    
      /* Send off SYN; include data in Fast Open. */
      err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :
            tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
    ......
      tp->snd_nxt = tp->write_seq;
      tp->pushed_seq = tp->write_seq;
      buff = tcp_send_head(sk);
      if (unlikely(buff)) {
        tp->snd_nxt  = TCP_SKB_CB(buff)->seq;
        tp->pushed_seq  = TCP_SKB_CB(buff)->seq;
      }
    ......
      /* Timer for repeating the SYN until an answer. */
      inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
              inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
      return 0;
    }

    在 tcp_connect 中,有一个新的结构 struct tcp_sock,如果打开他,你会发现他是 struct inet_connection_sock 的一个扩展,struct inet_connection_sock 在 struct tcp_sock 开头的位置,通过强制类型转换访问,故伎重演又一次

    struct tcp_sock 里面维护了更多的 TCP 的状态,咱们同样是遇到了再分析

    接下来 tcp_init_nondata_skb 初始化一个 SYN 包,tcp_transmit_skb 将 SYN 包发送出去,inet_csk_reset_xmit_timer 设置了一个 timer,如果 SYN 发送不成功,则再次发送

    TCP 层处理

    static struct net_protocol tcp_protocol = {
      .early_demux  =  tcp_v4_early_demux,
      .early_demux_handler =  tcp_v4_early_demux,
      .handler  =  tcp_v4_rcv,
      .err_handler  =  tcp_v4_err,
      .no_policy  =  1,
      .netns_ok  =  1,
      .icmp_strict_tag_validation = 1,
    }

    通过 struct net_protocol 结构中的 handler 进行接收,调用的函数是 tcp_v4_rcv。

    接下来的调用链为 tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_state_process。

    tcp_rcv_state_process,顾名思义,是用来处理接收一个网络包后引起状态变化的

    int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
    {
      struct tcp_sock *tp = tcp_sk(sk);
      struct inet_connection_sock *icsk = inet_csk(sk);
      const struct tcphdr *th = tcp_hdr(skb);
      struct request_sock *req;
      int queued = 0;
      bool acceptable;
    
      switch (sk->sk_state) {
    ......
      case TCP_LISTEN:
    ......
        if (th->syn) {
          acceptable = icsk->icsk_af_ops->conn_request(sk, skb) >= 0;
          if (!acceptable)
            return 1;
          consume_skb(skb);
          return 0;
        }
    ......
    }

    目前服务端是处于 TCP_LISTEN 状态的,而且发过来的包是 SYN,因而就有了上面的代码,调用 icsk->icsk_af_ops->conn_request 函数。

    struct inet_connection_sock 对应的操作是 inet_connection_sock_af_ops,按照下面的定义,其实调用的是 tcp_v4_conn_request

    const struct inet_connection_sock_af_ops ipv4_specific = {
            .queue_xmit        = ip_queue_xmit,
            .send_check        = tcp_v4_send_check,
            .rebuild_header    = inet_sk_rebuild_header,
            .sk_rx_dst_set     = inet_sk_rx_dst_set,
            .conn_request      = tcp_v4_conn_request,
            .syn_recv_sock     = tcp_v4_syn_recv_sock,
            .net_header_len    = sizeof(struct iphdr),
            .setsockopt        = ip_setsockopt,
            .getsockopt        = ip_getsockopt,
            .addr2sockaddr     = inet_csk_addr2sockaddr,
            .sockaddr_len      = sizeof(struct sockaddr_in),
            .mtu_reduced       = tcp_v4_mtu_reduced,
    };

    tcp_v4_conn_request 会调用 tcp_conn_request,这个函数也比较长,里面调用了 send_synack,但实际调用的是 tcp_v4_send_synack。

    具体发送的过程我们不去管它,看注释我们能知道,这是收到了 SYN 后,回复一个 SYN-ACK,回复完毕后,服务端处于 TCP_SYN_RECV

    int tcp_conn_request(struct request_sock_ops *rsk_ops,
             const struct tcp_request_sock_ops *af_ops,
             struct sock *sk, struct sk_buff *skb)
    {
    ......
    af_ops->send_synack(sk, dst, &fl, req, &foc,
                !want_cookie ? TCP_SYNACK_NORMAL :
                   TCP_SYNACK_COOKIE);
    ......
    }
    
    /*
     *  Send a SYN-ACK after having received a SYN.
     */
    static int tcp_v4_send_synack(const struct sock *sk, struct dst_entry *dst,
                struct flowi *fl,
                struct request_sock *req,
                struct tcp_fastopen_cookie *foc,
                enum tcp_synack_type synack_type)
    {......}

    都是 TCP 协议栈,所以过程和服务端没有太多区别,还是会走到 tcp_rcv_state_process 函数的,只不过由于客户端目前处于 TCP_SYN_SENT 状态,就进入了下面的代码分支

    int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
    {
      struct tcp_sock *tp = tcp_sk(sk);
      struct inet_connection_sock *icsk = inet_csk(sk);
      const struct tcphdr *th = tcp_hdr(skb);
      struct request_sock *req;
      int queued = 0;
      bool acceptable;
    
      switch (sk->sk_state) {
    ......
      case TCP_SYN_SENT:
        tp->rx_opt.saw_tstamp = 0;
        tcp_mstamp_refresh(tp);
        queued = tcp_rcv_synsent_state_process(sk, skb, th);
        if (queued >= 0)
          return queued;
        /* Do step6 onward by hand. */
        tcp_urg(sk, skb, th);
        __kfree_skb(skb);
        tcp_data_snd_check(sk);
        return 0;
      }
    ......
    }

    tcp_rcv_synsent_state_process 会调用 tcp_send_ack,发送一个 ACK-ACK,发送后客户端处于 TCP_ESTABLISHED 状态

    又轮到服务端接收网络包了,我们还是归 tcp_rcv_state_process 函数处理。由于服务端目前处于状态 TCP_SYN_RECV 状态,因而又走了另外的分支。当收到这个网络包的时候,服务端也处于 TCP_ESTABLISHED 状态,三次握手结束

    int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
    {
      struct tcp_sock *tp = tcp_sk(sk);
      struct inet_connection_sock *icsk = inet_csk(sk);
      const struct tcphdr *th = tcp_hdr(skb);
      struct request_sock *req;
      int queued = 0;
      bool acceptable;
    ......
      switch (sk->sk_state) {
      case TCP_SYN_RECV:
        if (req) {
          inet_csk(sk)->icsk_retransmits = 0;
          reqsk_fastopen_remove(sk, req, false);
        } else {
          /* Make sure socket is routed, for correct metrics. */
          icsk->icsk_af_ops->rebuild_header(sk);
          tcp_call_bpf(sk, BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB);
          tcp_init_congestion_control(sk);
    
          tcp_mtup_init(sk);
          tp->copied_seq = tp->rcv_nxt;
          tcp_init_buffer_space(sk);
        }
        smp_mb();
        tcp_set_state(sk, TCP_ESTABLISHED);
        sk->sk_state_change(sk);
        if (sk->sk_socket)
          sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);
        tp->snd_una = TCP_SKB_CB(skb)->ack_seq;
        tp->snd_wnd = ntohs(th->window) << tp->rx_opt.snd_wscale;
        tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);
        break;
    ......
    }

    socket 的 Write 操作

    对于网络包的发送,我们可以使用对于 socket 文件的写入系统调用,也就是 write 系统调用

    对于 socket 来讲,它的 file_operations 定义如下:

    static const struct file_operations socket_file_ops = {
      .owner =  THIS_MODULE,
      .llseek =  no_llseek,
      .read_iter =  sock_read_iter,
      .write_iter =  sock_write_iter,
      .poll =    sock_poll,
      .unlocked_ioctl = sock_ioctl,
      .mmap =    sock_mmap,
      .release =  sock_close,
      .fasync =  sock_fasync,
      .sendpage =  sock_sendpage,
      .splice_write = generic_splice_sendpage,
      .splice_read =  sock_splice_read,
    };

    按照文件系统的写入流程,调用的是 sock_write_iter:

    static ssize_t sock_write_iter(struct kiocb *iocb, struct iov_iter *from)
    {
      struct file *file = iocb->ki_filp;
      struct socket *sock = file->private_data;
      struct msghdr msg = {.msg_iter = *from,
               .msg_iocb = iocb};
      ssize_t res;
    ......
      res = sock_sendmsg(sock, &msg);
      *from = msg.msg_iter;
      return res;
    }

    在 sock_write_iter 中,我们通过 VFS 中的 struct file,将创建好的 socket 结构拿出来,然后调用 sock_sendmsg。

    而 sock_sendmsg 会调用 sock_sendmsg_nosec

    static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)
    {
      int ret = sock->ops->sendmsg(sock, msg, msg_data_left(msg));
    ......
    }

     tcp_sendmsg 函数

    int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
    {
      struct tcp_sock *tp = tcp_sk(sk);
      struct sk_buff *skb;
      int flags, err, copied = 0;
      int mss_now = 0, size_goal, copied_syn = 0;
      long timeo;
    ......
      /* Ok commence sending. */
      copied = 0;
    restart:
      mss_now = tcp_send_mss(sk, &size_goal, flags);
    
      while (msg_data_left(msg)) {
        int copy = 0;
        int max = size_goal;
    
        skb = tcp_write_queue_tail(sk);
        if (tcp_send_head(sk)) {
          if (skb->ip_summed == CHECKSUM_NONE)
            max = mss_now;
          copy = max - skb->len;
        }
    
        if (copy <= 0 || !tcp_skb_can_collapse_to(skb)) {
          bool first_skb;
    
    new_segment:
          /* Allocate new segment. If the interface is SG,
           * allocate skb fitting to single page.
           */
          if (!sk_stream_memory_free(sk))
            goto wait_for_sndbuf;
    ......
          first_skb = skb_queue_empty(&sk->sk_write_queue);
          skb = sk_stream_alloc_skb(sk,
                  select_size(sk, sg, first_skb),
                  sk->sk_allocation,
                  first_skb);
    ......
          skb_entail(sk, skb);
          copy = size_goal;
          max = size_goal;
    ......
        }
    
        /* Try to append data to the end of skb. */
        if (copy > msg_data_left(msg))
          copy = msg_data_left(msg);
    
        /* Where to copy to? */
        if (skb_availroom(skb) > 0) {
          /* We have some space in skb head. Superb! */
          copy = min_t(int, copy, skb_availroom(skb));
          err = skb_add_data_nocache(sk, skb, &msg->msg_iter, copy);
    ......
        } else {
          bool merge = true;
          int i = skb_shinfo(skb)->nr_frags;
          struct page_frag *pfrag = sk_page_frag(sk);
    ......
          copy = min_t(int, copy, pfrag->size - pfrag->offset);
    ......
          err = skb_copy_to_page_nocache(sk, &msg->msg_iter, skb,
                       pfrag->page,
                       pfrag->offset,
                       copy);
    ......
          pfrag->offset += copy;
        }
    
    ......
        tp->write_seq += copy;
        TCP_SKB_CB(skb)->end_seq += copy;
        tcp_skb_pcount_set(skb, 0);
    
        copied += copy;
        if (!msg_data_left(msg)) {
          if (unlikely(flags & MSG_EOR))
            TCP_SKB_CB(skb)->eor = 1;
          goto out;
        }
    
        if (skb->len < max || (flags & MSG_OOB) || unlikely(tp->repair))
          continue;
    
        if (forced_push(tp)) {
          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;
    ......
      }
    ......
    }

    msg 是用户要写入的数据,这个数据要拷贝到内核协议栈里面去发送;

    在内核协议栈里面,网络包的数据都是由 struct sk_buff 维护的,因而第一件事情就是找到一个空闲的内存空间,将用户要写入的数据,拷贝到 struct sk_buff 的管辖范围内。

    而第二件事情就是发送 struct sk_buff。

    while (msg_data_left(msg)):

    1. 第一步,tcp_write_queue_tail 从 TCP 写入队列 sk_write_queue 中拿出最后一个 struct sk_buff,在这个写入队列中排满了要发送的 struct sk_buff,为什么要拿最后一个呢?这里面只有最后一个,可能会因为上次用户给的数据太少,而没有填满
    2. 第二步,tcp_send_mss 会计算 MSS,也即 Max Segment Size。这是什么呢?这个意思是说,我们在网络上传输的网络包的大小是有限制的,而这个限制在最底层开始

    MTU(Maximum Transmission Unit,最大传输单元)是二层的一个定义。以以太网为例,MTU 为 1500 个 Byte,前面有 6 个 Byte 的目标 MAC 地址,6 个 Byte 的源 MAC 地址,2 个 Byte 的类型,后面有 4 个 Byte 的 CRC 校验,共 1518 个 Byte。

    在 IP 层,一个 IP 数据报在以太网中传输,如果它的长度大于该 MTU 值,就要进行分片传输。在 TCP 层有个 MSS(Maximum Segment Size,最大分段大小),等于 MTU 减去 IP 头,再减去 TCP 头。也就是,在不分片的情况下,TCP 里面放的最大内容。

      3. 第三步,如果 copy 小于 0,说明最后一个 struct sk_buff 已经没地方存放了,需要调用 sk_stream_alloc_skb,重新分配 struct sk_buff,然后调用 skb_entail,将新分配的 sk_buff 放到队列尾部  

    为了减少内存拷贝的代价,有的网络设备支持分散聚合(Scatter/Gather)I/O,顾名思义,就是 IP 层没必要通过内存拷贝进行聚合,让散的数据零散的放在原处,在设备层进行聚合。如果使用这种模式,网络包的数据就不会放在连续的数据区域,而是放在 struct skb_shared_info 结构里面指向的离散数据,skb_shared_info 的成员变量 skb_frag_t frags[MAX_SKB_FRAGS],会指向一个数组的页面,就不能保证连续了

       4. 在注释 /* Where to copy to? */ 后面有个 if-else 分支。if 分支就是 skb_add_data_nocache 将数据拷贝到连续的数据区域。else 分支就是 skb_copy_to_page_nocache 将数据拷贝到 struct skb_shared_info 结构指向的不需要连续的页面区域。

      5. 第五步,就是要发生网络包了。第一种情况是积累的数据报数目太多了,因而我们需要通过调用 __tcp_push_pending_frames 发送网络包。第二种情况是,这是第一个网络包,需要马上发送,调用 tcp_push_one。无论 __tcp_push_pending_frames 还是 tcp_push_one,都会调用 tcp_write_xmit 发送网络包。

    tcp_write_xmit 函数

    static bool 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;
    ......
      max_segs = tcp_tso_segs(sk, mss_now);
      while ((skb = tcp_send_head(sk))) {
        unsigned int limit;
    ......
        tso_segs = tcp_init_tso_segs(skb, mss_now);
    ......
        cwnd_quota = tcp_cwnd_test(tp, skb);
    ......
        if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now))) {
          is_rwnd_limited = true;
          break;
        }
    ......
        limit = mss_now;
            if (tso_segs > 1 && !tcp_urg_mode(tp))
                limit = tcp_mss_split_point(sk, skb, mss_now, min_t(unsigned int, cwnd_quota, max_segs), nonagle);
    
        if (skb->len > limit &&
            unlikely(tso_fragment(sk, skb, limit, mss_now, gfp)))
          break;
    ......
        if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
          break;
    
    repair:
        /* Advance the send_head.  This one is sent out.
         * This call will increment packets_out.
         */
        tcp_event_new_data_sent(sk, skb);
    
        tcp_minshall_update(tp, mss_now, skb);
        sent_pkts += tcp_skb_pcount(skb);
    
        if (push_one)
          break;
      }
    ......
    }

    TSO(TCP Segmentation Offload): 如果发送的网络包非常大,就像上面说的一样,要进行分段。分段这个事情可以由协议栈代码在内核做,但是缺点是比较费 CPU,另一种方式是延迟到硬件网卡去做,需要网卡支持对大数据包进行自动分段,可以降低 CPU 负载。

    tcp_init_tso_segs 会调用 tcp_set_skb_tso_segs -> 调用函数 tcp_mss_split_point,开始计算切分的 limit。这里面会计算 max_len = mss_now * max_segs,根据现在不切分来计算 limit,所以下一步的判断中,大部分情况下 tso_fragment 不会被调用,等待到了底层网卡来切分

    拥塞窗口的概念(cwnd,congestion window: 也就是说为了避免拼命发包,把网络塞满了,定义一个窗口的概念,在这个窗口之内的才能发送,超过这个窗口的就不能发送,来控制发送的频率

    那窗口大小是多少呢?就是遵循下面这个著名的拥塞窗口变化图:

     一开始的窗口只有一个 mss 大小叫作 slow start(慢启动)。一开始的增长速度的很快的,翻倍增长。一旦到达一个临界值 ssthresh,就变成线性增长,我们就称为拥塞避免。什么时候算真正拥塞呢?就是出现了丢包。一旦丢包,一种方法是马上降回到一个 mss,然后重复先翻倍再线性对的过程。如果觉得太过激进,也可以有第二种方法,就是降到当前 cwnd 的一半,然后进行线性增长。

    在代码中,tcp_cwnd_test 会将当前的 snd_cwnd,减去已经在窗口里面尚未发送完毕的网络包,那就是剩下的窗口大小 cwnd_quota,也即就能发送这么多了

    接收窗口rwnd 的概念(receive window),也叫滑动窗口: 如果说拥塞窗口是为了怕把网络塞满,在出现丢包的时候减少发送速度,那么滑动窗口就是为了怕把接收方塞满,而控制发送速度

    滑动窗口,其实就是接收方告诉发送方自己的网络包的接收能力,超过这个能力,我就受不了了。

    因为滑动窗口的存在,将发送方的缓存分成了四个部分:

    1. 第一部分:发送了并且已经确认的。这部分是已经发送完毕的网络包,这部分没有用了,可以回收
    2. 第二部分:发送了但尚未确认的。这部分,发送方要等待,万一发送不成功,还要重新发送,所以不能删除
    3. 第三部分:没有发送,但是已经等待发送的。这部分是接收方空闲的能力,可以马上发送,接收方收得了
    4. 第四部分:没有发送,并且暂时还不会发送的。这部分已经超过了接收方的接收能力,再发送接收方就收不了了

     因为滑动窗口的存在,接收方的缓存也要分成了三个部分:

    1. 第一部分:接受并且确认过的任务。这部分完全接收成功了,可以交给应用层了
    2. 第二部分:还没接收,但是马上就能接收的任务。这部分有的网络包到达了,但是还没确认,不算完全完毕,有的还没有到达,那就是接收方能够接受的最大的网络包数量
      第二部分:还没接收,但是马上就能接收的任务。这部分有的网络包到达了,但是还没确认,不算完全完毕,有的还没有到达,那就是接收方能够接受的最大的网络包数量
    3. 第三部分:还没接收,也没法接收的任务。这部分已经超出接收方能力

    在网络包的交互过程中,接收方会将第二部分的大小,作为 AdvertisedWindow 发送给发送方,发送方就可以根据他来调整发送速度了

    在 tcp_snd_wnd_test 函数中,会判断 sk_buff 中的 end_seq 和 tcp_wnd_end(tp) 之间的关系,也即这个 sk_buff 是否在滑动窗口的允许范围之内。如果不在范围内,说明发送要受限制了,我们就要把 is_rwnd_limited 设置为 true

    接下来,tcp_mss_split_point 函数要被调用了:

    static unsigned int tcp_mss_split_point(const struct sock *sk,
                                            const struct sk_buff *skb,
                                            unsigned int mss_now,
                                            unsigned int max_segs,
                                            int nonagle)
    {
            const struct tcp_sock *tp = tcp_sk(sk);
            u32 partial, needed, window, max_len;
    
            window = tcp_wnd_end(tp) - TCP_SKB_CB(skb)->seq;
            max_len = mss_now * max_segs;
    
            if (likely(max_len <= window && skb != tcp_write_queue_tail(sk)))
                    return max_len;
    
            needed = min(skb->len, window);
    
            if (max_len <= needed)
                    return max_len;
    ......
            return needed;
    }

    1. 判断是否会因为超出 mss 而分段

    2. 判断另一个条件,就是是否在滑动窗口的运行范围之内,如果小于窗口的大小,也需要分段,也即需要调用 tso_fragment

    在一个循环的最后,是调用 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 tcphdr *th;
        int err;
    
        tp = tcp_sk(sk);
    
        skb->skb_mstamp = tp->tcp_mstamp;
        inet = inet_sk(sk);
        tcb = TCP_SKB_CB(skb);
        memset(&opts, 0, sizeof(opts));
    
        tcp_header_size = tcp_options_size + sizeof(struct tcphdr);
        skb_push(skb, tcp_header_size);
    
        /* Build TCP header and checksum it. */
        th = (struct tcphdr *)skb->data;
        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);
    
        th->check       = 0;
        th->urg_ptr     = 0;
    ......
        tcp_options_write((__be32 *)(th + 1), tp, &opts);
        th->window  = htons(min(tp->rcv_wnd, 65535U));
    ......
        err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);
    ......
    }

    tcp_transmit_skb 这个函数比较长,主要做了两件事情,第一件事情就是填充 TCP 头,如果我们对着 TCP 头的格式

     这里面有源端口,设置为 inet_sport,有目标端口,设置为 inet_dport;有序列号,设置为 tcb->seq;

    有确认序列号,设置为 tp->rcv_nxt。我们把所有的 flags 设置为 tcb->tcp_flags。设置选项为 opts。设置窗口大小为 tp->rcv_wnd

    全部设置完毕之后,就会调用 icsk_af_ops 的 queue_xmit 方法,icsk_af_ops 指向 ipv4_specific,也即调用的是 ip_queue_xmit 函数

    const struct inet_connection_sock_af_ops ipv4_specific = {
            .queue_xmit        = ip_queue_xmit,
            .send_check        = tcp_v4_send_check,
            .rebuild_header    = inet_sk_rebuild_header,
            .sk_rx_dst_set     = inet_sk_rx_dst_set,
            .conn_request      = tcp_v4_conn_request,
            .syn_recv_sock     = tcp_v4_syn_recv_sock,
            .net_header_len    = sizeof(struct iphdr),
            .setsockopt        = ip_setsockopt,
            .getsockopt        = ip_getsockopt,
            .addr2sockaddr     = inet_csk_addr2sockaddr,
            .sockaddr_len      = sizeof(struct sockaddr_in),
            .mtu_reduced       = tcp_v4_mtu_reduced,
    }

    ip_queue_xmit 函数

    从 ip_queue_xmit 函数开始,我们就要进入 IP 层的发送逻辑了

    int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl)
    {
        struct inet_sock *inet = inet_sk(sk);
        struct net *net = sock_net(sk);
        struct ip_options_rcu *inet_opt;
        struct flowi4 *fl4;
        struct rtable *rt;
        struct iphdr *iph;
        int res;
    
        inet_opt = rcu_dereference(inet->inet_opt);
        fl4 = &fl->u.ip4;
        rt = skb_rtable(skb);
        /* Make sure we can route this packet. */
        rt = (struct rtable *)__sk_dst_check(sk, 0);
        if (!rt) {
            __be32 daddr;
            /* Use correct destination address if we have options. */
            daddr = inet->inet_daddr;
     ......
            rt = ip_route_output_ports(net, 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:
        /* OK, we know where to send it, allocate and build IP header. */
        skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0));
        skb_reset_network_header(skb);
        iph = ip_hdr(skb);
        *((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff));
        if (ip_dont_fragment(sk, &rt->dst) && !skb->ignore_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;
        ip_copy_addrs(iph, fl4);
    
        /* Transport layer set skb->h.foo itself. */
    
        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_select_ident_segs(net, skb, sk,
                     skb_shinfo(skb)->gso_segs ?: 1);
    
        /* TODO : should we use skb->sk here instead of sk ? */
        skb->priority = sk->sk_priority;
        skb->mark = sk->sk_mark;
    
        res = ip_local_out(net, sk, skb);
    ......
    }

    1.  选取路由

    也即我要发送这个包应该从哪个网卡出去

    这件事情主要由 ip_route_output_ports 函数完成

    接下来的调用链为:ip_route_output_ports->ip_route_output_flow->__ip_route_output_key->ip_route_output_key_hash->ip_route_output_key_hash_rcu

    struct rtable *ip_route_output_key_hash_rcu(struct net *net, struct flowi4 *fl4, struct fib_result *res, const struct sk_buff *skb)
    {
      struct net_device *dev_out = NULL;
      int orig_oif = fl4->flowi4_oif;
      unsigned int flags = 0;
      struct rtable *rth;
    ......
        err = fib_lookup(net, fl4, res, 0);
    ......
    make_route:
      rth = __mkroute_output(res, fl4, orig_oif, dev_out, flags);
    ......
    }

    ip_route_output_key_hash_rcu 先会调用 fib_lookup

    FIB 全称是 Forwarding Information Base,转发信息表。其实就是咱们常说的路由表

    static inline int fib_lookup(struct net *net, const struct flowi4 *flp, struct fib_result *res, unsigned int flags)
    {  struct fib_table *tb;
    ......
      tb = fib_get_table(net, RT_TABLE_MAIN);
      if (tb)
        err = fib_table_lookup(tb, flp, res, flags | FIB_LOOKUP_NOREF);
    ......
    }

    路由表可以有多个,一般会有一个主表,RT_TABLE_MAIN。然后 fib_table_lookup 函数在这个表里面进行查找

    找到了路由,然后,ip_route_output_key_hash_rcu 会调用 __mkroute_output,创建一个 struct rtable,表示找到的路由表项。

    这个结构是由 rt_dst_alloc 函数分配的

    struct rtable *rt_dst_alloc(struct net_device *dev,
              unsigned int flags, u16 type,
              bool nopolicy, bool noxfrm, bool will_cache)
    {
      struct rtable *rt;
    
      rt = dst_alloc(&ipv4_dst_ops, dev, 1, DST_OBSOLETE_FORCE_CHK,
               (will_cache ? 0 : DST_HOST) |
               (nopolicy ? DST_NOPOLICY : 0) |
               (noxfrm ? DST_NOXFRM : 0));
    
      if (rt) {
        rt->rt_genid = rt_genid_ipv4(dev_net(dev));
        rt->rt_flags = flags;
        rt->rt_type = type;
        rt->rt_is_input = 0;
        rt->rt_iif = 0;
        rt->rt_pmtu = 0;
        rt->rt_gateway = 0;
        rt->rt_uses_gateway = 0;
        rt->rt_table_id = 0;
        INIT_LIST_HEAD(&rt->rt_uncached);
    
        rt->dst.output = ip_output;
        if (flags & RTCF_LOCAL)
          rt->dst.input = ip_local_deliver;
      }
    
      return rt;
    }

    最终返回 struct rtable 实例,第一部分也就完成了

    2. 准备 IP 层的头,往里面填充内容

     服务类型设置为 tos: 标识位里面设置是否允许分片 frag_off。如果不允许,而遇到 MTU 太小过不去的情况,就发送 ICMP 报错

    TTL : 是这个包的存活时间,为了防止一个 IP 包迷路以后一直存活下去,每经过一个路由器 TTL 都减一,减为零则“死去”

    protocol: 指的是更上层的协议,这里是 TCP

    源地址和目标地址由 ip_copy_addrs 设置

    3. 调用 ip_local_out 发送 IP 包

    int ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
    {
      int err;
    
      err = __ip_local_out(net, sk, skb);
      if (likely(err == 1))
        err = dst_output(net, sk, skb);
    
      return err;
    }
    
    int __ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
    {
      struct iphdr *iph = ip_hdr(skb);
      iph->tot_len = htons(skb->len);
      skb->protocol = htons(ETH_P_IP);
    
      return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT,
               net, sk, skb, NULL, skb_dst(skb)->dev,
               dst_output);
    }

    ip_local_out 先是调用 __ip_local_out,然后里面调用了 nf_hook。这是什么呢?nf 的意思是 Netfilter,这是 Linux 内核的一个机制,用于在网络发送和转发的关键节点上加上 hook 函数,这些函数可以截获数据包,对数据包进行干预

    ip_local_out 再调用 dst_output,就是真正的发送数据

    /* Output packet to network from transport.  */
    static inline int dst_output(struct net *net, struct sock *sk, struct sk_buff *skb)
    {
      return skb_dst(skb)->output(net, sk, skb);
    }

    这里调用的就是 struct rtable 成员 dst 的 ouput 函数。

    在 rt_dst_alloc 中,我们可以看到,output 函数指向的是 ip_output

    int ip_output(struct net *net, struct sock *sk, struct sk_buff *skb)
    {
      struct net_device *dev = skb_dst(skb)->dev;
      skb->dev = dev;
      skb->protocol = htons(ETH_P_IP);
    
      return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING,
              net, sk, skb, NULL, dev,
              ip_finish_output,
              !(IPCB(skb)->flags & IPSKB_REROUTED));
    }

    在 ip_output 里面,我们又看到了熟悉的 NF_HOOK。这一次是 NF_INET_POST_ROUTING,也即 POSTROUTING 链,处理完之后,调用 ip_finish_output

    ip_finish_output 函数

    从 ip_finish_output 函数开始,发送网络包的逻辑由第三层到达第二层。ip_finish_output 最终调用 ip_finish_output2。

    static int ip_finish_output2(struct net *net, struct sock *sk, 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;
      u32 nexthop;
    ......
      nexthop = (__force u32) rt_nexthop(rt, ip_hdr(skb)->daddr);
      neigh = __ipv4_neigh_lookup_noref(dev, nexthop);
      if (unlikely(!neigh))
        neigh = __neigh_create(&arp_tbl, &nexthop, dev, false);
      if (!IS_ERR(neigh)) {
        int res;
        sock_confirm_neigh(skb, neigh);
        res = neigh_output(neigh, skb);
        return res;
      }
    ......
    }

    在 ip_finish_output2 中,先找到 struct rtable 路由表里面的下一跳,下一跳一定和本机在同一个局域网中,可以通过二层进行通信,因而通过 __ipv4_neigh_lookup_noref,查找如何通过二层访问下一跳。

    static inline struct neighbour *__ipv4_neigh_lookup_noref(struct net_device *dev, u32 key)
    {
      return ___neigh_lookup_noref(&arp_tbl, neigh_key_eq32, arp_hashfn, &key, dev);
    }

    __ipv4_neigh_lookup_noref 是从本地的 ARP 表中查找下一跳的 MAC 地址。ARP 表的定义如下

    struct neigh_table arp_tbl = {
        .family     = AF_INET,
        .key_len    = 4,    
        .protocol   = cpu_to_be16(ETH_P_IP),
        .hash       = arp_hash,
        .key_eq     = arp_key_eq,
        .constructor    = arp_constructor,
        .proxy_redo = parp_redo,
        .id     = "arp_cache",
    ......
        .gc_interval    = 30 * HZ, 
        .gc_thresh1 = 128,  
        .gc_thresh2 = 512,  
        .gc_thresh3 = 1024,
    };

    如果在 ARP 表中没有找到相应的项,则调用 __neigh_create 进行创建

    struct neighbour *__neigh_create(struct neigh_table *tbl, const void *pkey, struct net_device *dev, bool want_ref)
    {
        u32 hash_val;
        int key_len = tbl->key_len;
        int error;
        struct neighbour *n1, *rc, *n = neigh_alloc(tbl, dev);
        struct neigh_hash_table *nht;
    
        memcpy(n->primary_key, pkey, key_len);
        n->dev = dev;
        dev_hold(dev);
    
        /* Protocol specific setup. */
        if (tbl->constructor && (error = tbl->constructor(n)) < 0) {
    ......
        }
    ......
        if (atomic_read(&tbl->entries) > (1 << nht->hash_shift))
            nht = neigh_hash_grow(tbl, nht->hash_shift + 1);
    
        hash_val = tbl->hash(pkey, dev, nht->hash_rnd) >> (32 - nht->hash_shift);
    
        for (n1 = rcu_dereference_protected(nht->hash_buckets[hash_val],
                            lockdep_is_held(&tbl->lock));
             n1 != NULL;
             n1 = rcu_dereference_protected(n1->next,
                lockdep_is_held(&tbl->lock))) {
            if (dev == n1->dev && !memcmp(n1->primary_key, pkey, key_len)) {
                if (want_ref)
                    neigh_hold(n1);
                rc = n1;
                goto out_tbl_unlock;
            }
        }
    ......
        rcu_assign_pointer(n->next,
                   rcu_dereference_protected(nht->hash_buckets[hash_val],
                                 lockdep_is_held(&tbl->lock)));
        rcu_assign_pointer(nht->hash_buckets[hash_val], n);
    ......
    }

    __neigh_create 先调用 neigh_alloc,创建一个 struct neighbour 结构,用于维护 MAC 地址和 ARP 相关的信息。这个名字也很好理解,大家都是在一个局域网里面,可以通过 MAC 地址访问到,当然是邻居了。

    static struct neighbour *neigh_alloc(struct neigh_table *tbl, struct net_device *dev)
    {
      struct neighbour *n = NULL;
      unsigned long now = jiffies;
      int entries;
    ......
      n = kzalloc(tbl->entry_size + dev->neigh_priv_len, GFP_ATOMIC);
      if (!n)
        goto out_entries;
    
      __skb_queue_head_init(&n->arp_queue);
      rwlock_init(&n->lock);
      seqlock_init(&n->ha_lock);
      n->updated    = n->used = now;
      n->nud_state    = NUD_NONE;
      n->output    = neigh_blackhole;
      seqlock_init(&n->hh.hh_lock);
      n->parms    = neigh_parms_clone(&tbl->parms);
      setup_timer(&n->timer, neigh_timer_handler, (unsigned long)n);
    
      NEIGH_CACHE_STAT_INC(tbl, allocs);
      n->tbl      = tbl;
      refcount_set(&n->refcnt, 1);
      n->dead      = 1;
    ......
    }

    在 neigh_alloc 中,我们先分配一个 struct neighbour 结构并且初始化。

    这里面比较重要的有两个成员:

      一个是 arp_queue,所以上层想通过 ARP 获取 MAC 地址的任务,都放在这个队列里面。

      另一个是 timer 定时器,我们设置成,过一段时间就调用 neigh_timer_handler,来处理这些 ARP 任务。

    __neigh_create 然后调用了 arp_tbl 的 constructor 函数,也即调用了 arp_constructor,在这里面定义了 ARP 的操作 arp_hh_ops

    static int arp_constructor(struct neighbour *neigh)
    {
      __be32 addr = *(__be32 *)neigh->primary_key;
      struct net_device *dev = neigh->dev;
      struct in_device *in_dev;
      struct neigh_parms *parms;
    ......
      neigh->type = inet_addr_type_dev_table(dev_net(dev), dev, addr);
    
      parms = in_dev->arp_parms;
      __neigh_parms_put(neigh->parms);
      neigh->parms = neigh_parms_clone(parms);
    ......
      neigh->ops = &arp_hh_ops;
    ......
      neigh->output = neigh->ops->output;
    ......
    }
    
    static const struct neigh_ops arp_hh_ops = {
      .family =    AF_INET,
      .solicit =    arp_solicit,
      .error_report =    arp_error_report,
      .output =    neigh_resolve_output,
      .connected_output =  neigh_resolve_output,
    };

    __neigh_create 最后是将创建的 struct neighbour 结构放入一个哈希表,从里面的代码逻辑比较容易看出,这是一个数组加链表的链式哈希表,先计算出哈希值 hash_val,得到相应的链表,然后循环这个链表找到对应的项,如果找不到就在最后插入一项

    回到 ip_finish_output2,在 __neigh_create 之后,会调用 neigh_output 发送网络包

    static inline int neigh_output(struct neighbour *n, struct sk_buff *skb)
    {
    ......
      return n->output(n, skb);
    }

    按照上面对于 struct neighbour 的操作函数 arp_hh_ops 的定义,output 调用的是 neigh_resolve_output。

    int neigh_resolve_output(struct neighbour *neigh, struct sk_buff *skb)
    {
      if (!neigh_event_send(neigh, skb)) {
    ......
        rc = dev_queue_xmit(skb);
      }
    ......
    }

    在 neigh_resolve_output 里面,首先 neigh_event_send 触发一个事件,看能否激活 ARP

    int __neigh_event_send(struct neighbour *neigh, struct sk_buff *skb)
    {
      int rc;
      bool immediate_probe = false;
    
      if (!(neigh->nud_state & (NUD_STALE | NUD_INCOMPLETE))) {
        if (NEIGH_VAR(neigh->parms, MCAST_PROBES) +
            NEIGH_VAR(neigh->parms, APP_PROBES)) {
          unsigned long next, now = jiffies;
    
          atomic_set(&neigh->probes,
               NEIGH_VAR(neigh->parms, UCAST_PROBES));
          neigh->nud_state     = NUD_INCOMPLETE;
          neigh->updated = now;
          next = now + max(NEIGH_VAR(neigh->parms, RETRANS_TIME),
               HZ/2);
          neigh_add_timer(neigh, next);
          immediate_probe = true;
        } 
    ......
      } else if (neigh->nud_state & NUD_STALE) {
        neigh_dbg(2, "neigh %p is delayed
    ", neigh);
        neigh->nud_state = NUD_DELAY;
        neigh->updated = jiffies;
        neigh_add_timer(neigh, jiffies +
            NEIGH_VAR(neigh->parms, DELAY_PROBE_TIME));
      }
    
      if (neigh->nud_state == NUD_INCOMPLETE) {
        if (skb) {
    .......
          __skb_queue_tail(&neigh->arp_queue, skb);
          neigh->arp_queue_len_Bytes += skb->truesize;
        }
        rc = 1;
      }
    out_unlock_bh:
      if (immediate_probe)
        neigh_probe(neigh);
    .......
    }

    激活 ARP 分两种情况:

      第一种情况是马上激活,也即 immediate_probe

      另一种情况是延迟激活则仅仅设置一个 timer。

    然后将 ARP 包放在 arp_queue 上。如果马上激活,就直接调用 neigh_probe;如果延迟激活,则定时器到了就会触发 neigh_timer_handler

    在这里面还是会调用 neigh_probe

    static void neigh_probe(struct neighbour *neigh)
            __releases(neigh->lock)
    {
            struct sk_buff *skb = skb_peek_tail(&neigh->arp_queue);
    ......
            if (neigh->ops->solicit)
                    neigh->ops->solicit(neigh, skb);
    ......
    }

    solicit 调用的是 arp_solicit,在这里我们可以找到对于 arp_send_dst 的调用,创建并发送一个 arp 包,得到结果放在 struct dst_entry 里面

    static void arp_send_dst(int type, int ptype, __be32 dest_ip,
                             struct net_device *dev, __be32 src_ip,
                             const unsigned char *dest_hw,
                             const unsigned char *src_hw,
                             const unsigned char *target_hw,
                             struct dst_entry *dst)
    {
            struct sk_buff *skb;
    ......
            skb = arp_create(type, ptype, dest_ip, dev, src_ip,
                             dest_hw, src_hw, target_hw);
    ......
            skb_dst_set(skb, dst_clone(dst));
            arp_xmit(skb);
    }

    回到 neigh_resolve_output 中,当 ARP 发送完毕,就可以调用 dev_queue_xmit 发送二层网络包了

    /**
     *  __dev_queue_xmit - transmit a buffer
     *  @skb: buffer to transmit
     *  @accel_priv: private data used for L2 forwarding offload
     *
     *  Queue a buffer for transmission to a network device. 
     */
    static int __dev_queue_xmit(struct sk_buff *skb, void *accel_priv)
    {
      struct net_device *dev = skb->dev;
      struct netdev_queue *txq;
      struct Qdisc *q;
    ......
      txq = netdev_pick_tx(dev, skb, accel_priv);
      q = rcu_dereference_bh(txq->qdisc);
    
      if (q->enqueue) {
        rc = __dev_xmit_skb(skb, q, dev, txq);
        goto out;
      }
    ......
    }

    这里还有另一个变量叫做 struct Qdisc,这个是什么呢?如果我们在一台 Linux 机器上运行 ip addr,我们能看到对于一个网卡,都有下面的输出

    # ip addr
    1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
        link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
        inet 127.0.0.1/8 scope host lo
           valid_lft forever preferred_lft forever
        inet6 ::1/128 scope host 
           valid_lft forever preferred_lft forever
    2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1400 qdisc pfifo_fast state UP group default qlen 1000
        link/ether fa:16:3e:75:99:08 brd ff:ff:ff:ff:ff:ff
        inet 10.173.32.47/21 brd 10.173.39.255 scope global noprefixroute dynamic eth0
           valid_lft 67104sec preferred_lft 67104sec
        inet6 fe80::f816:3eff:fe75:9908/64 scope link 
           valid_lft forever preferred_lft forever

    qdisc 全称是 queueing discipline,中文叫排队规则: 内核如果需要通过某个网络接口发送数据包,都需要按照为这个接口配置的 qdisc(排队规则)把数据包加入队列

    接下来,__dev_xmit_skb 开始进行网络包发送

    static inline int __dev_xmit_skb(struct sk_buff *skb, struct Qdisc *q,
                     struct net_device *dev,
                     struct netdev_queue *txq)
    {
    ......
        rc = q->enqueue(skb, q, &to_free) & NET_XMIT_MASK;
        if (qdisc_run_begin(q)) {
    ......
            __qdisc_run(q);
        }
    ......    
    }
    
    void __qdisc_run(struct Qdisc *q)
    {
        int quota = dev_tx_weight;
        int packets;
         while (qdisc_restart(q, &packets)) {
            /*
             * Ordered by possible occurrence: Postpone processing if
             * 1. we've exceeded packet quota
             * 2. another process needs the CPU;
             */
            quota -= packets;
            if (quota <= 0 || need_resched()) {
                __netif_schedule(q);
                break;
            }
         }
         qdisc_run_end(q);
    }

    __dev_xmit_skb 会将请求放入队列,然后调用 __qdisc_run 处理队列中的数据。

    qdisc_restart 用于数据的发送。qdisc 的另一个功能是用于控制网络包的发送速度,因而如果超过速度,就需要重新调度,则会调用 __netif_schedule

    static void __netif_reschedule(struct Qdisc *q)
    {
        struct softnet_data *sd;
        unsigned long flags;
        local_irq_save(flags);
        sd = this_cpu_ptr(&softnet_data);
        q->next_sched = NULL;
        *sd->output_queue_tailp = q;
        sd->output_queue_tailp = &q->next_sched;
        raise_softirq_irqoff(NET_TX_SOFTIRQ);
        local_irq_restore(flags);
    }

    __netif_schedule 会调用 __netif_reschedule,发起一个软中断 NET_TX_SOFTIRQ

    NET_TX_SOFTIRQ 的处理函数是 net_tx_action,用于发送网络包

    static __latent_entropy void net_tx_action(struct softirq_action *h)
    {
        struct softnet_data *sd = this_cpu_ptr(&softnet_data);
    ......
        if (sd->output_queue) {
            struct Qdisc *head;
    
            local_irq_disable();
            head = sd->output_queue;
            sd->output_queue = NULL;
            sd->output_queue_tailp = &sd->output_queue;
            local_irq_enable();
    
            while (head) {
                struct Qdisc *q = head;
                spinlock_t *root_lock;
    
                head = head->next_sched;
    ......
                qdisc_run(q);
            }
        }
    }

    net_tx_action 还是调用了 qdisc_run,还是会调用 __qdisc_run,然后调用 qdisc_restart 发送网络包

    static inline int qdisc_restart(struct Qdisc *q, int *packets)
    {
            struct netdev_queue *txq;
            struct net_device *dev;
            spinlock_t *root_lock;
            struct sk_buff *skb;
            bool validate;
    
            /* Dequeue packet */
            skb = dequeue_skb(q, &validate, packets);
            if (unlikely(!skb))
                    return 0;
    
            root_lock = qdisc_lock(q);
            dev = qdisc_dev(q);
            txq = skb_get_tx_queue(dev, skb);
    
            return sch_direct_xmit(skb, q, dev, txq, root_lock, validate);
    }

    qdisc_restart 将网络包从 Qdisc 的队列中拿下来,然后调用 sch_direct_xmit 进行发送

    int sch_direct_xmit(struct sk_buff *skb, struct Qdisc *q,
                struct net_device *dev, struct netdev_queue *txq,
                spinlock_t *root_lock, bool validate)
    {
        int ret = NETDEV_TX_BUSY;
    
        if (likely(skb)) {
            if (!netif_xmit_frozen_or_stopped(txq))
                skb = dev_hard_start_xmit(skb, dev, txq, &ret); 
        } 
    ......
        if (dev_xmit_complete(ret)) {
            /* Driver sent out skb successfully or skb was consumed */
            ret = qdisc_qlen(q);
        } else {
            /* Driver returned NETDEV_TX_BUSY - requeue skb */
            ret = dev_requeue_skb(skb, q);
        }   
    ......
    }

    在 sch_direct_xmit 中,调用 dev_hard_start_xmit 进行发送,如果发送不成功,会返回 NETDEV_TX_BUSY。

    这说明网络卡很忙,于是就调用 dev_requeue_skb,重新放入队列。

    struct sk_buff *dev_hard_start_xmit(struct sk_buff *first, struct net_device *dev, struct netdev_queue *txq, int *ret) 
    {
        struct sk_buff *skb = first;
        int rc = NETDEV_TX_OK;
    
        while (skb) {
            struct sk_buff *next = skb->next;
            rc = xmit_one(skb, dev, txq, next != NULL);
            skb = next; 
            if (netif_xmit_stopped(txq) && skb) {
                rc = NETDEV_TX_BUSY;
                break;      
            }       
        }   
    ......
    }

    在 dev_hard_start_xmit 中,是一个 while 循环。每次在队列中取出一个 sk_buff,调用 xmit_one 发送。xmit_one->netdev_start_xmit->__netdev_start_xmit

    static inline netdev_tx_t __netdev_start_xmit(const struct net_device_ops *ops, struct sk_buff *skb, struct net_device *dev, bool more)          
    {
        skb->xmit_more = more ? 1 : 0;
        return ops->ndo_start_xmit(skb, dev);
    }

    这个时候,已经到了设备驱动层了。我们能看到,drivers/net/ethernet/intel/ixgb/ixgb_main.c 里面有对于这个网卡的操作的定义

    static const struct net_device_ops ixgb_netdev_ops = {
            .ndo_open               = ixgb_open,
            .ndo_stop               = ixgb_close,
            .ndo_start_xmit         = ixgb_xmit_frame,
            .ndo_set_rx_mode        = ixgb_set_multi,
            .ndo_validate_addr      = eth_validate_addr,
            .ndo_set_mac_address    = ixgb_set_mac,
            .ndo_change_mtu         = ixgb_change_mtu,
            .ndo_tx_timeout         = ixgb_tx_timeout,
            .ndo_vlan_rx_add_vid    = ixgb_vlan_rx_add_vid,
            .ndo_vlan_rx_kill_vid   = ixgb_vlan_rx_kill_vid,
            .ndo_fix_features       = ixgb_fix_features,
            .ndo_set_features       = ixgb_set_features,
    };

    在这里面,我们可以找到对于 ndo_start_xmit 的定义,调用 ixgb_xmit_frame

    static netdev_tx_t
    ixgb_xmit_frame(struct sk_buff *skb, struct net_device *netdev)
    {
        struct ixgb_adapter *adapter = netdev_priv(netdev);
    ......
        if (count) {
            ixgb_tx_queue(adapter, count, vlan_id, tx_flags);
            /* Make sure there is space in the ring for the next send. */
            ixgb_maybe_stop_tx(netdev, &adapter->tx_ring, DESC_NEEDED);
    
        } 
    ......
        return NETDEV_TX_OK;
    }

    在 ixgb_xmit_frame 中,我们会得到这个网卡对应的适配器,然后将其放入硬件网卡的队列中。

    发送总结

    • VFS 层:write 系统调用找到 struct file,根据里面的 file_operations 的定义,调用 sock_write_iter 函数。sock_write_iter 函数调用 sock_sendmsg 函数。
    • Socket 层:从 struct file 里面的 private_data 得到 struct socket,根据里面 ops 的定义,调用 inet_sendmsg 函数。
    • Sock 层:从 struct socket 里面的 sk 得到 struct sock,根据里面 sk_prot 的定义,调用 tcp_sendmsg 函数。
    • TCP 层:tcp_sendmsg 函数会调用 tcp_write_xmit 函数,tcp_write_xmit 函数会调用 tcp_transmit_skb,在这里实现了 TCP 层面向连接的逻辑。
    • IP 层:扩展 struct sock,得到 struct inet_connection_sock,根据里面 icsk_af_ops 的定义,调用 ip_queue_xmit 函数。
    • IP 层:ip_route_output_ports 函数里面会调用 fib_lookup 查找路由表。FIB 全称是 Forwarding Information Base,转发信息表,也就是路由表。在 IP 层里面要做的另一个事情是填写 IP 层的头。
    • 在 IP 层还要做的一件事情就是通过 iptables 规则。
    • MAC 层:IP 层调用 ip_finish_output 进行 MAC 层。
    • MAC 层需要 ARP 获得 MAC 地址,因而要调用 ___neigh_lookup_noref 查找属于同一个网段的邻居,他会调用 neigh_probe 发送 ARP。
    • 有了 MAC 地址,就可以调用 dev_queue_xmit 发送二层网络包了,它会调用 __dev_xmit_skb 会将请求放入队列。
    • 设备层:网络包的发送回触发一个软中断 NET_TX_SOFTIRQ 来处理队列中的数据。这个软中断的处理函数是 net_tx_action。
    • 在软中断处理函数中,会将网络包从队列上拿下来,调用网络设备的传输函数 ixgb_xmit_frame,将网络包发的设备的队列上去。
    在 dev_hard_start_xmit 中,是一个 while 循环。每次在队列中取出一个 sk_buff,调用 xmit_one 发送。
  • 相关阅读:
    Unity 用代码设置UGUI的渲染层级
    C/C++常考基础面试题(更新)
    运行Jar包程序Shell
    Shell编程语法
    Spring多个数据源问题:DataSourceAutoConfiguration required a single bean, but * were found
    Maven项目的发布,发布到Nexus
    Consul入门
    application.properties 改成 application.yml
    奖学金申请模板
    jvm
  • 原文地址:https://www.cnblogs.com/mysky007/p/12347293.html
Copyright © 2020-2023  润新知