TCP/IP协议栈在Linux内核中的运行时序分析

1.网络体系结构模型

1.1.OSI七层模型

  OSI模型是由国际化标准组织ISO提出的网络体系结构模型。被称为开放系统互联参考模型。OSI模型总共有7层。自上而下依次为应用层、表示层、会话层、传输层、网络层、数据链路层和物理层。七层模型结构清晰。共同完成数据的传输和处理工作。

  在OSI模型中,各层的功能大致如下:

  应用层为特定类型的网络应用提供访问OSI环境的手段。

  表示层主要处理在两个通信系统中交换信息的表示方式。

  会话层允许不同主机上的各个进程之间进行会话。

  传输层负责不同主机之间两个进程的通信。功能是为端到端连接提供可靠的传输服务。

  网络层的主要任务是把网络层的协议数据单元从源端传递到目的端,为分组交换网络上的不同主机提供通信服务。

  数据链路层的主要任务是将网络层传来的IP数据报封装成帧,透明传输和差错控制等。

  物理层的任务是透明的传输比特流。

1.2.TCP/IP分层模型

  ARPA在研究ARPAnet时提出了TCP/IP模型,模型从低到高依次为网络接口层(对应OSI参考模型中的物理层和数据链路层)、网际层、传输层和应用层(对应OSI参考模型中的会话层、表示层和应用层)。TCP/IP由于得到广泛应用而成为事实上的国际标准。其中,TCP/IP模型和OSI模型的层次结构及相关的协议如图所示

  

 

 

 

  网络接口层的功能类似于OSI的物理层和数据链路层。它表示与物理网络的接口,但实际上TCP/IP本身并未真正描述这一部分,只是指出主机必须使用某种协议与网络连接,以便在其上传递IP分组。

  网际层(主机-主机)是TCP/IP体系结构的关键部分。它和OSI网络层在功能上非常相似。网际层将分组发往任何网络,并为之独立地选择合适的路由,但它不保证各个分组有序地到达,各个分组的有序交付由高层负责。网际层定义了标准的分组格式和协议,即IP。当前采用的IP协议是第4版,即IPv4,它的下一-版本是IPv6。

  传输层(应用-应用或进程进程)的功能同样和OSI中的传输层类似,即使得发送端和目的端主机上的对等实体进行会话。传输层主要使用以下两种协议:

    TCP:它是面向连接的,能够提供可靠的交付。

    UDP:它是无连接的,不保证可靠的交付,只能提供尽最大努力的交付

  应用层包含所有的高层协议,如Telnet、FTP、DNS、SMTP、HTTP等协议。

  由上图可以看出,IP协议是因特网的核心协议;TCP/IP可以为各式各样的应用提供服务,正因为如此,因特网才会发展到今天的规模。

2.Linux网络子系统

  Linux 网络子系统的顶部是系统调用接口层。它为为应用程序提供访问内核网络子系统的方法,如Socket系统调用。位于其下面的是一个协议无关层,实现一组通用函数来访问各种不同的协议:通过socket实现。然后是具体协议的实现,网络协议层用于实现各种具体的网络协议,如:TCP、UDP 等,当然还有IP。然后是设备无关层,设备无关接口将协议与各种网络设备驱动连接在一起。这一层提供一组通用函数供底层网络设备驱动程序使用,让它们可以对高层协议栈进行操作。最下面是设备驱动程序, 负责管理物理网络设备。

 

 

 

3.套接字Socket 

3.1.Socket简介

  Socket 是指网络上的两个程序过一个双向的通讯连接实现数据的交换,这个 双向链路的一端称之为一个 socket,socket 通常用来实现客户方和服务方的连接。 socket 是 TCP/IP 协议的一个十分流行的编程界面,一个 Socket 由一个 IP 地址和 一个端口号唯一确定。 但是,Socket 所支持的协议种类也不光 TCP/IP 一种,因此两者之间是没有必 然联系的。在 Java 中,socket 编程主要是基于 TCP/IP 协议的网络编程。现在, TCP/IP 协议族中的主要 socket 类型为流套接字(使用 TCP 协议)和数据报套接字 (使用 UDP 协议)以及raw套接字。

3.2.Socket数据结构

  用户使用socket系统调用编写应用程序时,通过一个数字来表示一个socket,所有的操作都在该数字上进行,这个数字称为套接字描述符。在系统调用 的实现函数里,这个数字就会被映射成一个表示socket的结构体,该结构体保存了该socket的所有属性和数据。

struct socket {  
    socket_state            state;          //描述套接字的状态
    unsigned long           flags;          //套接字的一组标志位
    const struct proto_ops *ops;            //将套接字层调用于传输层协议对应
    struct fasync_struct    *fasync_list;   //存储异步通知队列
    struct file             *file;          //套接字相关联的文件指针
    struct sock             *sk;            //套接字相关联的传输控制块
    wait_queue_head_t       wait;           //等待套接字的进程列表
    short                   type;           //套接字的类型
};  

state用于表示socket所处的状态,是一个枚举变量,其类型定义如下:
typedef enum {  
    SS_FREE = 0,            //该socket还未分配  
    SS_UNCONNECTED,         //未连向任何socket  
    SS_CONNECTING,          //正在连接过程中  
    SS_CONNECTED,           //已连向一个socket  
    SS_DISCONNECTING        //正在断开连接的过程中  
}socket_state;  

 

3.3.Socketcall系统调用

socketcall()Linux操作系统中所有的socket系统调用的总入口。sys_soketcall()函数的第一个参数即为具体的操作码,其中,每个数字可以代表一个操作码,一共17种,函数中正是通过操作码来跳转到真正的系统调用函数的。具体操作码对应情况如下:

#define SYS_SOCKET  1       /* sys_socket(2)        */
#define SYS_BIND    2       /* sys_bind(2)          */
#define SYS_CONNECT 3       /* sys_connect(2)       */
#define SYS_LISTEN  4       /* sys_listen(2)        */
#define SYS_ACCEPT  5       /* sys_accept(2)        */
#define SYS_GETSOCKNAME 6       /* sys_getsockname(2)       */
#define SYS_GETPEERNAME 7       /* sys_getpeername(2)       */
#define SYS_SOCKETPAIR  8       /* sys_socketpair(2)        */
#define SYS_SEND    9       /* sys_send(2)          */
#define SYS_RECV    10      /* sys_recv(2)          */
#define SYS_SENDTO  11      /* sys_sendto(2)        */
#define SYS_RECVFROM    12      /* sys_recvfrom(2)      */
#define SYS_SHUTDOWN    13      /* sys_shutdown(2)      */
#define SYS_SETSOCKOPT  14      /* sys_setsockopt(2)        */
#define SYS_GETSOCKOPT  15      /* sys_getsockopt(2)        */
#define SYS_SENDMSG 16      /* sys_sendmsg(2)       */
#define SYS_RECVMSG 17      /* sys_recvmsg(2)       */
sys_socketcall()函数的源代码在net/socket.c中,具体如下:
asmlinkage long sys_socketcall(int call, unsigned long __user *args)  
{  
    unsigned long a[6];  
    unsigned long a0, a1;  
    int err;  
..........................................  
  
    a0 = a[0];  
    a1 = a[1];  
  
    switch (call) {  
    case SYS_SOCKET:  
        err = sys_socket(a0, a1, a[2]);  
        break;  
    case SYS_BIND:  
        err = sys_bind(a0, (struct sockaddr __user *)a1, a[2]);  
        break;  
    case SYS_CONNECT:  
        err = sys_connect(a0, (struct sockaddr __user *)a1, a[2]);  
        break;  
    case SYS_LISTEN:  
        err = sys_listen(a0, a1);  
        break;  
    case SYS_ACCEPT:  
        err =  
            do_accept(a0, (struct sockaddr __user *)a1,  
                  (int __user *)a[2], 0);  
        break;  
    case SYS_GETSOCKNAME:  
        err =  
            sys_getsockname(a0, (struct sockaddr __user *)a1,  
                    (int __user *)a[2]);  
        break;  
.....................................  
    return err;  
}  

 

可以看到sys_socketcall()函数就是通过传递进来的call类型,来调用相应的socket相关的函数。而这里并没有出现我们所熟知得writereadaiopoll等系统调用函数,这是因为socket上面其实还有一层VFS层,内核把socket当做一个文件系统来处理,并且实现了相应得VFS方法。

 

下图显示了使用上述系统调用函数的顺序,图中蓝色大方框中的IO系统调用函数可以在任何时候调用。注意,给出的图中不是一个完整的状态流程图,仅显示了一些常见的系统调用函数:

 

 

 

4.应用层分析

4.1发送数据

sendsendtosendmsg系统调用最后都是调用sock_sendmsg()来输出数据的,要求传递给sock_sendmsg()的消息头必须是有效的。我们主要看看sys_sendmsg的源码

 

 

 

asmlinkage long sys_sendmsg(int fd, struct msghdr __user *msg, unsigned flags)
{
    struct compat_msghdr __user *msg_compat = (struct compat_msghdr __user *)msg;
    struct socket *sock;
    char address[MAX_SOCK_ADDR];
    struct iovec iovstack[UIO_FASTIOV], *iov = iovstack;
    unsigned char ctl[sizeof(struct cmsghdr) + 20]; /* 20 is size of ipv6_pktinfo */
    unsigned char *ctl_buf = ctl;
    struct msghdr msg_sys;
    int err, ctl_len, iov_size, total_len;
    err = -EFAULT;
    //用于64位系统向32位系统兼容
    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;
    // 获取文件描述符对应的套接口
    sock = sockfd_lookup(fd, &err);
    if (!sock) 
        goto out;
    //检查数据块是否超过上限值(UIO_MAXIOV)
    err = -EMSGSIZE;
    if (msg_sys.msg_iovlen > UIO_MAXIOV)
        goto out_put;

    /* Check whether to allocate the iovec area*/
    err = -ENOMEM;
    //计算iovec缓存大小
    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_put;
    }
    //分别将用户空间中的目标地址和iovec结构数组赋值到内核空间中
    if (MSG_CMSG_COMPAT & flags) {
        err = verify_compat_iovec(&msg_sys, iov, address, VERIFY_READ);
    } else
        err = verify_iovec(&msg_sys, iov, address, VERIFY_READ);
    //验证err值
    if (err < 0) 
        goto out_freeiov;
    total_len = err;

    err = -ENOBUFS;
    //检查控制信息长度是否超过INT_MAX
    if (msg_sys.msg_controllen > INT_MAX)
        goto out_freeiov;
    ctl_len = msg_sys.msg_controllen; 

    //64位系统向32位系统兼容时,需将控制缓存信息进行复制
    if ((MSG_CMSG_COMPAT & flags) && ctl_len) {
        err = cmsghdr_from_user_compat_to_kern(&msg_sys, ctl, sizeof(ctl));
        if (err)
            goto out_freeiov;
        ctl_buf = msg_sys.msg_control;
    } else if (ctl_len) {
        //在32位系统中复制控制缓存信息
        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 *) msg_sys.msg_control, ctl_len))
            goto out_freectl;
        msg_sys.msg_control = ctl_buf;
    }
    msg_sys.msg_flags = flags;

    //设置数据发送标识
    if (sock->file->f_flags & O_NONBLOCK)
        msg_sys.msg_flags |= MSG_DONTWAIT;
    //调用sock_sendmsg函数发送报文
    err = sock_sendmsg(sock, &msg_sys, total_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_put:
    sockfd_put(sock);
out:       
    return err;
}

由代码我们可以知道,起初,我们将消息头本身从用户空间复制到内核空间中,但并不包括所有的内容。接着,我们还要检测数据块的块数是否超越上线UIO_MAXIOV。当超越了该上限,则不能发送。除此之外,我们还要复制用户空间的目标地址和控制信息到内核空间中。最后,无论操作成功与否,只要有资源的申请,那么就要释放资源并且返回错误码。

 

通过以上的分析以及根据其他相应的资料,且由于应用层的系统调用过程比价清晰,给出sendmsg系统调用的过程如图所示:

 

 

 

4.2接收数据

接收数据与发送数据的系统调用过程十分相似,它们都归结到sock_sendmsg()sock_recvmsg()两个函数上。不同的只是数据的流向相反而已。下面是recvmsg的系统调用过程:

 

 

 

5.传输层分析

5.1发送数据

TCP协议对发送数据相关系统调用内核实现,虽然发送相关的系统调用接口由很多,但是到了TCP协议层,都统一由tcp_sendmsg()处理。tcp_sendmsg()函数要完成的工作就是将应用程序要发送的数据组织成skb,然后调用tcp_push函数。

tcp_sendmsg()主要做了三件事情:

1)、将数据包复制到Socket Buffer中。

2)、把Socket BUffer键入到发送队列。

 

3)、设置TCP控制块结构,用于构造TCP协议头信息。

发送数据部分的源代码:

...
    
        //查看是否立即发送数据包
            if (forced_push(tp)) {
                //设置立即发送数据包标志PSH
                tcp_mark_push(tp, skb);
                //发送数据包,实际调用的是ip_queue_xmit
                __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)
                tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH);
 
            if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
                goto do_error;
 
            mss_now = tcp_send_mss(sk, &size_goal, flags);
        }
    }
...

 

tcp_push()在判断了是否需要设置PUSH标记位之后,会调用__tcp_push_pending_frames()

static inline void tcp_push(struct sock *sk, int flags, int mss_now,
                int nonagle)
{
    struct tcp_sock *tp = tcp_sk(sk);

    if (tcp_send_head(sk)) {
        //判断是否需要设置PUSH标记
        struct sk_buff *skb = tcp_write_queue_tail(sk);
        if (!(flags & MSG_MORE) || forced_push(tp))
            tcp_mark_push(tp, skb);
        //MSG_OOB相关,忽略
        tcp_mark_urg(tp, flags, skb);
        //调用__tcp_push_pending_frames()尝试发送
        __tcp_push_pending_frames(sk, mss_now,
                      (flags & MSG_MORE) ? TCP_NAGLE_CORK : nonagle);
    }
}
__tcp_push_pending_frames()调用调用tcp_write_xmit()完成发送。

/* Push out any pending frames which were held back due to
 * TCP_CORK or attempt at coalescing tiny packets.
 * The socket must be locked by the caller.
 */
void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss,
                   int nonagle)
{
    struct sk_buff *skb = tcp_send_head(sk);
    //如果有新数据可供发送,调用tcp_write_xmit()发送
    if (skb) {
        if (tcp_write_xmit(sk, cur_mss, nonagle))
            //和PMTU相关
            tcp_check_probe_timer(sk);
    }
}

tcp_write_xmit()该函数是TCP发送新数据的核心函数,包括发送窗口判断、拥塞控制判断等核心操作都是在该函数中完成。它又将调用发送函数tcp_transmit_skb函数。

 

其中,发送数据包部分的代码如下:

while ((skb = tcp_send_head(sk))) {
        unsigned int limit;
        //设置skb中的GSO分段信息。返回值tso_segs表示该skb中的数据需要分成几个段发送
        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;
        //检测发送窗口是否至少允许发送skb中的一个的段。如果不允许,结束发送过程
        if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now)))
            break;
        if (tso_segs == 1) {
            //tso_segs为1,说明skb只有一个段,而且长度可能小于MSS,即是一个小数据包,
            //所以需要检测nagle算法是否允许发送该skb
            if (unlikely(!tcp_nagle_test(tp, skb, mss_now,
                             (tcp_skb_is_last(sk, skb) ?
                              nonagle : TCP_NAGLE_PUSH))))
                break;
        } else {
            //tso_segs>1,需要TSO分段,判断是否需要推迟发送,这种推迟主要是为了提高GSO性能
            if (tcp_tso_should_defer(sk, skb))
                break;
        }

        //通过上面的拥塞窗口和发送窗口的检测后,我们知道,目前至少是可以发送一个
        //TCP段的。当然也有可能还可以发送更多,所以下面需要根据条件调整limit

        //如果skb有多个段,需要检查到底可以发送多少数据
        limit = mss_now;
        if (tso_segs > 1)
            //tcp_mss_split_point()返回的是发送窗口和拥塞窗口允许发送的最大字节数,
            //可能会超过skb本身的数据量,见下文
            limit = tcp_mss_split_point(sk, skb, mss_now, cwnd_quota);

        //skb的数据量超过了限定值,需要分段。这种情况只可能发生在TSO情形,因为非TSO场景,skb
        //的长度是不可能超过MSS的。此外,这种分段完全是因为拥塞控制和流量控制算法限制了发包大小,
        //所以才需要分割,和TSO本身没有任何关系
        if (skb->len > limit &&
            unlikely(tso_fragment(sk, skb, limit, mss_now)))
            break;
        //更新数据包的发送时间戳
        TCP_SKB_CB(skb)->when = tcp_time_stamp;
        //发送数据,如果返回非0,表示本次发送失败(如qdisc队列已满等),那么结束本次发送过程
        //第三个参数为1,表示让tcp_transmit_skb()发送时克隆一份skb首部
        if (unlikely(tcp_transmit_skb(sk, skb, 1, GFP_ATOMIC)))
            break;
        //发送了新数据,更新发送队列以及相关统计
        tcp_event_new_data_sent(sk, skb);
        //Nagle算法相关,如果当前发送的数据量小于MSS,认为是小包,所以更新snd_sml的值
        tcp_minshall_update(tp, mss_now, skb);
        //累加发包计数
        sent_pkts++;
    }//end of while((skb = tcp_send_head(sk)))

  tcp_transmit_skb的作用是复制或者拷贝skb,构造skb中的tcp首部,并将调用网络层的发送函数发送skb;在发送前,首先需要克隆或者复制skb,因为在成功发送到网络设备之后,skb会释放,而tcp层不能真正的释放,是需要等到对该数据段的ack才可以释放;然后构造tcp首部和选项;最后调用网络层提供的发送回调函数发送skbip层的回调函数为ip_queue_xmit

5.2接收数据

tcp_v4_rcv函数为TCP的总入口,数据包从IP层传递上来,进入该函数。tcp_v4_rcv函数只要做以下几个工作:(1) 设置TCP_CB (2) 查找控制块  (3)根据控制块状态做不同处理,包括TCP_TIME_WAIT状态处理,TCP_NEW_SYN_RECV状态处理,TCP_LISTEN状态处理 (4) 接收TCP段;当状态为TCP_LISTEN调用tcp_v4_do_rcv

/* LISTEN状态处理 */
if (sk->sk_state == TCP_LISTEN) {
    ret = tcp_v4_do_rcv(sk, skb);
    goto put_and_return;
}

对于tcp_v4_do_rcv()函数,如果状态为ESTABLISHED,即已连接状态,就会调用tcp_rcv_established()函数

if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
        struct dst_entry *dst = sk->sk_rx_dst;

        sock_rps_save_rxhash(sk, skb);
        sk_mark_napi_id(sk, skb);
        if (dst) {
            if (inet_sk(sk)->rx_dst_ifindex != skb->skb_iif ||
                !dst->ops->check(dst, 0)) {
                dst_release(dst);
                sk->sk_rx_dst = NULL;
            }
        }
        tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len);
        return 0;
}

tcp_rcv_established用于处理已连接状态下的输入,处理过程根据首部预测字段分为快速路径和慢速路径;在快路中,对是有有数据负荷进行不同处理;在慢路中,会进行更详细的校验,然后处理ack,处理紧急数据,接收数据段,其中数据段可能包含乱序的情况,最后进行是否有数据和ack的发送检查;当一切正常时,调用tcp_data_queue()方法将报文放入队列中。

tcp_data_queue作用为数据段的接收处理,当存在预期接收的数据段,就会有以下几种处理方式:

预期接收的数据段,a. 进行0窗口判断;b. 进程上下文,复制数据到用户空间;c. 不满足b或者b未完整拷贝此skb的数据段,则加入到接收队列;d. 更新下一个期望接收的序号;e. 若有fin标记,则处理finf. 乱序队列不为空,则处理乱序;g. 快速路径的检查和设置;h. 唤醒用户空间进程读取数据;

 

代码如下:

/* 预期接收的数据段 */
    if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
        /* 窗口为0,不能接收数据 */
        if (tcp_receive_window(tp) == 0)
            goto out_of_window;

        /* Ok. In sequence. In window. */
        /* 进程上下文 */

        /* 当前进程读取数据 */
        if (tp->ucopy.task == current &&
            /* 用户空间读取序号与接收序号一致&& 需要读取的数据不为0 */
            tp->copied_seq == tp->rcv_nxt && tp->ucopy.len &&
            /* 被用户空间锁定&& 无紧急数据 */
            sock_owned_by_user(sk) && !tp->urg_data) {

            /* 带读取长度和数据段长度的较小值 */
            int chunk = min_t(unsigned int, skb->len,
                      tp->ucopy.len);
            /* 设置running状态 */
            __set_current_state(TASK_RUNNING);

            /* 拷贝数据 */
            if (!skb_copy_datagram_msg(skb, 0, tp->ucopy.msg, chunk)) {
                tp->ucopy.len -= chunk;
                tp->copied_seq += chunk;
                /* 完整读取了该数据段 */
                eaten = (chunk == skb->len);

                /* 调整接收缓存和窗口 */
                tcp_rcv_space_adjust(sk);
            }
        }

        /* 未拷贝到用户空间或者未拷贝完整数据段 */
        if (eaten <= 0) {
queue_and_out:
            /* 没有拷贝到用户空间,对内存进行检查 */
            if (eaten < 0) {
                if (skb_queue_len(&sk->sk_receive_queue) == 0)
                    sk_forced_mem_schedule(sk, skb->truesize);
                else if (tcp_try_rmem_schedule(sk, skb, skb->truesize))
                    goto drop;
            }

            /* 添加到接收队列 */
            eaten = tcp_queue_rcv(sk, skb, 0, &fragstolen);
        }

        /* 更新下一个期望接收的序号*/
        tcp_rcv_nxt_update(tp, TCP_SKB_CB(skb)->end_seq);
        /* 有数据 */
        if (skb->len)
            tcp_event_data_recv(sk, skb);

        /* 标记有fin,则处理 */
        if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
            tcp_fin(sk);

        /* 乱序队列有数据,则处理 */
        if (!RB_EMPTY_ROOT(&tp->out_of_order_queue)) {

            /* 将乱序队列中的数据段转移到接收队列 */
            tcp_ofo_queue(sk);

            /* RFC2581. 4.2. SHOULD send immediate ACK, when
             * gap in queue is filled.
             */
            /* 乱序数据段处理完毕,需要立即发送ack */
            if (RB_EMPTY_ROOT(&tp->out_of_order_queue))
                inet_csk(sk)->icsk_ack.pingpong = 0;
        }

        if (tp->rx_opt.num_sacks)
            tcp_sack_remove(tp);

        /* 快路检查 */
        tcp_fast_path_check(sk);

        /* 向用户空间拷贝了数据,则释放skb */
        if (eaten > 0)
            kfree_skb_partial(skb, fragstolen);

        /* 不在销毁状态,则唤醒进程读取数据 */
        if (!sock_flag(sk, SOCK_DEAD))
            sk->sk_data_ready(sk);
        return;
    }

6.网络层分析:

6.1.发送数据

ip_queue_xmit()ip层提供给tcp层发送回调,大多数tcp发送都会使用这个回调,tcp层使用tcp_transmit_skb封装了tcp头之后,调用该函数,该函数提供了路由查找校验、封装ip头和ip选项的功能。

ip_queue_xmit()完成面向连接套接字的包输出,当套接字处于连接状态时,所有从套接字发出的包都具有确定的路由, 无需为每一个输出包查询它的目的入口,可将套接字直接绑定到路由入口上, 这由套接字的目的缓冲指针(dst_cache)来完成.ip_queue_xmit()首先为输入包建立IP包头, 经过本地包过滤器后,再将IP包分片输出(ip_fragment)

ip_queue_xmit调用函数__ip_queue_xmit()。在__ip_queue中,会调用skb_rtable函数来检查skb是否已被路由,即获取其缓存信息,将缓存信息保存在变量rt中,如果rt不为空,就直接进行packet_routed函数,如果rt不为空,就会自行ip_route_output_ports查找路由缓存。

 

packet_routed代码段首先先进行严格源路由选项的处理。如果存在严格源路由选项,并且数据包的下一跳地址和网关地址不一致,则丢弃该数据包(goto no_route)。如果没问题,就进行ip头部设置,设置完成后调用ip_local_out函数。

packet_routed:
    if (opt && opt->is_strictroute && rt->rt_dst != rt->rt_gateway)//处理严路由选项,下一跳地址必须为网管地址
        goto no_route;

    iph = (struct iphdr *) skb_push(skb, sizeof(struct iphdr) + (opt ? opt->optlen : 0));//移动skb->data向上,准备填写ip报头
    *((__u16 *)iph)    = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff));//设置报头长度为20字节,选项的长度,在ip_options_build中增加
    iph->tot_len = htons(skb->len);//总长度,skb中所有数据的长度,包括报头,选项,有效载荷
    if (ip_dont_fragment(sk, &rt->u.dst) && !ipfragok)//检查禁止了分片
        iph->frag_off = htons(IP_DF);//设置报头指示禁止分片
    else
        iph->frag_off = 0;//分片中的偏移量
    iph->ttl      = ip_select_ttl(inet, &rt->u.dst);//生存周期
    iph->protocol = sk->sk_protocol;//指示ip上层的l4协议类型
    iph->saddr    = rt->rt_src;//源地址,目的地址
    iph->daddr    = rt->rt_dst;
    skb->nh.iph   = iph;

    if (opt && opt->optlen) {//sock中设置了选项
        iph->ihl += opt->optlen >> 2;
        ip_options_build(skb, opt, inet->daddr, rt, 0);//处理选项
    }

    ip_select_ident_more(iph, &rt->u.dst, sk, skb_shinfo(skb)->tso_segs);//为ip选择id

    ip_send_check(iph);//ip报头校验和

    skb->priority = sk->sk_priority;//skb的优先级为sock的优先级,在dev_queue_xmit中用于在规则队列中排队

    return NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev, dst_output);//调用dst->output,由ip_route_output_flow设置

ip_local_out函数调用了__ip_local_out函数,而__ip_local_out函数最终调用的是nf_hook函数并在里面调用了dst_output函数。

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);
    /* 计算校验和 */
    ip_send_check(iph);

    /* if egress device is enslaved to an L3 master device pass the
     * skb to its handler for processing
     */
    skb = l3mdev_ip_out(sk, skb);
    if (unlikely(!skb))
        return 0;

    /* 设置ip协议 */
    skb->protocol = htons(ETH_P_IP);

    /* 经过NF的LOCAL_OUT钩子点 */
    return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT,
               net, sk, skb, NULL, skb_dst(skb)->dev,
               dst_output);
}

dst_output函数调用了ip_output函数,而ip_output函数调用了ip_finish_output函数。这个函数实际上调用的是__ip_finish_output函数。紧接着, ip_finish_output调用了 ip_finish_output2()ip_finish_output2函数会检测skb的前部空间是否还能存储链路层首部。如果不够,就会申请更大的存储空间,最终会调用邻居子系统的输出函数neigh_output进行输出。输出分为有二层头缓存和没有两种情况,有缓存时调用neigh_hh_output进行快速输出,没有缓存时,则调用邻居子系统的输出回调函数进行慢速输出。不管执行哪个函数,最终都会调用dev_queue_xmit将数据包传入数据链路层。

static inline int neigh_output(struct neighbour *n, struct sk_buff *skb)
{
    const struct hh_cache *hh = &n->hh;

    /* 连接状态  &&缓存的头部存在,使用缓存输出 */
    if ((n->nud_state & NUD_CONNECTED) && hh->hh_len)
        return neigh_hh_output(hh, skb);
    /* 使用邻居项的输出回调函数输出,在连接或者非连接状态下有不同的输出函数 */
    else
        return n->output(n, skb);
}

6.2.接收数据

网络IP层的入口函数在ip_rcv函数。ip_rcv函数调用第三层协议的接收函数处理该skb包,进入第三层网络层处理。ip_rcv数首先会检查检验和等各种字段,如果数据包的长度超过最大传送单元MTU的话,会进行分片,最终到达 ip_rcv_finish 函数。

 

部分代码如下:

/* 取得传输层头部 */
    skb->transport_header = skb->network_header + iph->ihl*4;

    /* Remove any debris in the socket control block */
    /* 重置cb */
    memset(IPCB(skb), 0, sizeof(struct inet_skb_parm));

    /* 保存输入设备信息 */
    IPCB(skb)->iif = skb->skb_iif;

    /* Must drop socket now because of tproxy. */
    skb_orphan(skb);

    /* 经过PRE_ROUTING钩子点 */
    return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
               net, NULL, skb, dev, NULL,
               ip_rcv_finish);

ip_rcv_finish最终会调用dst_input函数,这个函数调用的是ip_input函数,当缓存查找没有匹配路由时将调用ip_route_input_slow(),决定该 package 将会被发到本机还是会被转发还是丢弃。

 

如果是发到本机的将会执行ip_local_deliver函数。可能会做 de-fragment(合并多个包),并调用ip_local_deliver_finiship_local_deliver_finish会调用ip_protocol_deliver_rcu函数

/* 如果忽略掉原始套接字和IPSec,则该函数仅仅是根据IP头部中的协议字段选择上层L4协议,并交给它来处理 */
static int ip_local_deliver_finish(struct sk_buff *skb)
{
    /* 跳过IP头部 */
    __skb_pull(skb, ip_hdrlen(skb));

    /* Point into the IP datagram, just past the header. */
    /* 设置传输层头部位置 */
    skb_reset_transport_header(skb);

    rcu_read_lock();
    {
        /* Note: See raw.c and net/raw.h, RAWV4_HTABLE_SIZE==MAX_INET_PROTOS */
        int protocol = ip_hdr(skb)->protocol;
        int hash;
        struct sock *raw_sk;
        struct net_protocol *ipprot;

    resubmit:
    /* 这个hash根本不是哈希值,仅仅只是inet_protos数组中的下表而已 */
        hash = protocol & (MAX_INET_PROTOS - 1);
        raw_sk = sk_head(&raw_v4_htable[hash]);

        /* If there maybe a raw socket we must check - if not we
         * don't care less
         */
    /* 原始套接字?? 忽略... */
        if (raw_sk && !raw_v4_input(skb, ip_hdr(skb), hash))
            raw_sk = NULL;
    /* 查找注册的L4层协议处理结构。 */
        if ((ipprot = rcu_dereference(inet_protos[hash])) != NULL) {
            int ret;
    /* 启用了安全策略,则交给IPSec */
            if (!ipprot->no_policy) {
                if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
                    kfree_skb(skb);
                    goto out;
                }
                nf_reset(skb);
            }
    /* 调用L4层协议处理函数 */
    /* 通常会是tcp_v4_rcv, udp_rcv, icmp_rcv和igmp_rcv */
    /* 如果注册了其他的L4层协议处理,则会进行相应的调用。 */
            ret = ipprot->handler(skb);
            if (ret < 0) {
                protocol = -ret;
                goto resubmit;
            }
            IP_INC_STATS_BH(IPSTATS_MIB_INDELIVERS);
        } else {
            if (!raw_sk) {    /* 无原始套接字,提交给IPSec */
                if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
                    IP_INC_STATS_BH(IPSTATS_MIB_INUNKNOWNPROTOS);
                    icmp_send(skb, ICMP_DEST_UNREACH,
                         ICMP_PROT_UNREACH, 0);
                }
            } else
                IP_INC_STATS_BH(IPSTATS_MIB_INDELIVERS);
            kfree_skb(skb);
        }
    }
 out:
    rcu_read_unlock();

    return 0;
}

ip_protocol_deliver_rcu将输入数据包从网络层传递到传输层。

void ip_protocol_deliver_rcu(struct net *net, struct sk_buff *skb, int protocol)
{
    const struct net_protocol *ipprot;
    int raw, ret;

resubmit:
    raw = raw_local_deliver(skb, protocol);

    ipprot = rcu_dereference(inet_protos[protocol]);
    if (ipprot) {
        if (!ipprot->no_policy) {
            if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
                kfree_skb(skb);
                return;
            }
            nf_reset_ct(skb);
        }
        ret = INDIRECT_CALL_2(ipprot->handler, tcp_v4_rcv, udp_rcv,
                      skb);
        if (ret < 0) {
            protocol = -ret;
            goto resubmit;
        }
        __IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS);
    } else {
        if (!raw) {
            if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
                __IP_INC_STATS(net, IPSTATS_MIB_INUNKNOWNPROTOS);
                icmp_send(skb, ICMP_DEST_UNREACH,
                      ICMP_PROT_UNREACH, 0);
            }
            kfree_skb(skb);
        } else {
            __IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS);
            consume_skb(skb);
        }
    }
}

7.数据链路层分析:

7.1.发送数据

数据链路层的入口是dev_queue_xmit函数。这个函数调用的是__dev_queue_xmit函数。__dev_queue_xmit函数对不同类型的数据包进行不同的处理。__dev_queue_xmit会调用dev_hard_start_xmit函数获取skb

 

__dev_queue_xmit的部分代码如下

/*此处是设备没有Qdisc的,实际上没有enqueue/dequeue的规则,无法进行拥塞控制的操作,
     *对于一些loopback/tunnel interface比较常见,判断下设备是否处于UP状态*/
    if (dev->flags & IFF_UP) {
        int cpu = smp_processor_id(); /* ok because BHs are off */
 
        if (txq->xmit_lock_owner != cpu) {
 
            if (__this_cpu_read(xmit_recursion) > RECURSION_LIMIT)
                goto recursion_alert;
            skb = validate_xmit_skb(skb, dev);
            if (!skb)
                goto drop;
 
            HARD_TX_LOCK(dev, txq, cpu);
                       /*这个地方判断一下txq不是stop状态,那么就直接调用dev_hard_start_xmit函数来发送数据*/
            if (!netif_xmit_stopped(txq)) {
                __this_cpu_inc(xmit_recursion);
                skb = dev_hard_start_xmit(skb, dev, txq, &rc);
                __this_cpu_dec(xmit_recursion);
                if (dev_xmit_complete(rc)) {
                    HARD_TX_UNLOCK(dev, txq);
                    goto out;
                }
            }
            HARD_TX_UNLOCK(dev, txq);
            net_crit_ratelimited("Virtual device %s asks to queue packet!\n",
                         dev->name);
        } else {
            /* Recursion is detected! It is possible,
             * unfortunately
             */
recursion_alert:
            net_crit_ratelimited("Dead loop on virtual device %s, fix it urgently!\n",
                         dev->name);
        }
    }

dev_hard_start_xmit函数会循环调用xmit_one函数,直到将待输出的数据包提交给网络设备的输出接口,完成数据包的输出。

 

xmit_one中调用__net_dev_start_xmit函数。一旦网卡完成报文发送,将产生中断通知 CPU,然后驱动层中的中断处理程序就可以删除保存的 skb

static int xmit_one(struct sk_buff *skb, struct net_device *dev,  
            struct netdev_queue *txq, bool more)  
{  
    unsigned int len;  
    int rc;  
      
    /*如果有抓包的工具的话,这个地方会进行抓包,such as Tcpdump*/  
    if (!list_empty(&ptype_all))  
        dev_queue_xmit_nit(skb, dev);  
  
    len = skb->len;  
    trace_net_dev_start_xmit(skb, dev);  
        /*调用netdev_start_xmit,快到driver的tx函数了*/  
    rc = netdev_start_xmit(skb, dev, txq, more);  
    trace_net_dev_xmit(skb, rc, dev, len);  
  
    return rc;  
}  

7.2.接收数据

接受数据的入口函数是net_rx_action,在net_rx_action函数中会去调用设备的napi_poll函数, 它是设备自己注册的.

/*
net_rx_action()会从两个地方取帧数据:
(1)非NAPI设备的中断处理流程把帧放入本地cpu私有数softnet_data 的接收队列中
(2)调用NAPI设备驱动的poll方法会直接从设备内存(或设备驱动程序接收环)中取出帧
*/
static void net_rx_action(struct softirq_action *h)
{
    struct list_head *list = &__get_cpu_var(softnet_data).poll_list;
     /*设置软中断处理程序一次允许的最大执行时间为2个jiffies*/
    unsigned long time_limit = jiffies + 2;     
    int budget = netdev_budget;//轮询配额 当数据包数超过此值 进行轮询接收 此值初始为300
    void *have;
    //禁止本地cpu中断
    local_irq_disable();
    while (!list_empty(list)) 
    {  
        struct napi_struct *n;
        int work, weight;
        if (unlikely(budget <= 0 || time_after(jiffies, time_limit)))
            goto softnet_break;
        local_irq_enable();
        //取得等待轮询设备的结构
        n = list_entry(list->next, struct napi_struct, poll_list);
        have = netpoll_poll_lock(n);
        //获得权重 处理此设备n的最大帧数
        weight = n->weight;
        work = 0;
        if (test_bit(NAPI_STATE_SCHED, &n->state)) 
        {
            //对于NAPI设备如e1000驱动此处为poll为e1000_clean() 进行轮询处理数据
            //对于非NAPI设备此处poll为process_backlog() 看net_dev_init()中的初始化
            work = n->poll(n, weight);
            trace_napi_poll(n);
        }
        //返回值work需要小于等于weight  work返回的是此设备处理的帧个数不能超过weight
         WARN_ON_ONCE(work > weight);
        budget -= work;
        //禁止本地中断
        local_irq_disable();
        //消耗完了此设备的权重
        if (unlikely(work == weight)) 
        {   
            //若此时设备被禁止了  则从链表删除此设备
            if (unlikely(napi_disable_pending(n)))
            {
                //开启本地cpu中断
                local_irq_enable();
                napi_complete(n);
                //关闭本地cpu中断
                local_irq_disable();
            } 
            else//否则将设备放到链表的最后面
                list_move_tail(&n->poll_list, list);
        }
        netpoll_poll_unlock(have);
    }
out:
    //开启本地cpu中断 
    local_irq_enable(); 
#ifdef CONFIG_NET_DMA
    dma_issue_pending_all();
#endif
    return;
softnet_break:
    __get_cpu_var(netdev_rx_stat).time_squeeze++;    
     //在这里触发下一次软中断处理
    __raise_softirq_irqoff(NET_RX_SOFTIRQ);
    goto out;
}

在设备的napi_poll函数中, 它负责调用napi_gro_receive函数。

static int napi_poll(struct napi_struct *n, struct list_head *repoll)
{
    void *have;
    int work, weight;
 
    list_del_init(&n->poll_list);
 
    have = netpoll_poll_lock(n);
 
    weight = n->weight;
 
    /* This NAPI_STATE_SCHED test is for avoiding a race
     * with netpoll's poll_napi().  Only the entity which
     * obtains the lock and sees NAPI_STATE_SCHED set will
     * actually make the ->poll() call.  Therefore we avoid
     * accidentally calling ->poll() when NAPI is not scheduled.
     */
    work = 0;
    if (test_bit(NAPI_STATE_SCHED, &n->state)) {
        work = n->poll(n, weight);  //调用网卡注册的poll函数
        trace_napi_poll(n, work, weight);
    }
 
    WARN_ON_ONCE(work > weight);
 
    if (likely(work < weight))
        goto out_unlock;
 
    /* Drivers must not modify the NAPI state if they
     * consume the entire weight.  In such cases this code
     * still "owns" the NAPI instance and therefore can
     * move the instance around on the list at-will.
     */
    if (unlikely(napi_disable_pending(n))) {
        napi_complete(n);
        goto out_unlock;
    }
 
    if (n->gro_list) {
        /* flush too old packets
         * If HZ < 1000, flush all packets.
         */
        napi_gro_flush(n, HZ >= 1000);
    }
 
    /* Some drivers may have called napi_schedule
     * prior to exhausting their budget.
     */
    if (unlikely(!list_empty(&n->poll_list))) {
        pr_warn_once("%s: Budget exhausted after napi rescheduled\n",
                 n->dev ? n->dev->name : "backlog");
        goto out_unlock;
    }
 
    list_add_tail(&n->poll_list, repoll);
 
out_unlock:
    netpoll_poll_unlock(have);
 
    return work;
}

napi_gro_receive用来将网卡上的数据包发给协议栈处理。它会调用 netif_receive_skb_core。而它会调用__netif_receive_skb_one_core,将数据包交给上层 ip_rcv 进行处理。

 

 

 

 

 

 

 

struct socket {  

    socket_state            state;          //描述套接字的状态

    unsigned long           flags;          //套接字的一组标志位

    const struct proto_ops *ops;            //将套接字层调用于传输层协议对应

    struct fasync_struct    *fasync_list;   //存储异步通知队列

    struct file             *file;          //套接字相关联的文件指针

    struct sock             *sk;            //套接字相关联的传输控制块

    wait_queue_head_t       wait;           //等待套接字的进程列表

    short                   type;           //套接字的类型

};  

 

state用于表示socket所处的状态,是一个枚举变量,其类型定义如下:

typedef enum {  

    SS_FREE = 0,            //socket还未分配  

    SS_UNCONNECTED,         //未连向任何socket  

    SS_CONNECTING,          //正在连接过程中  

    SS_CONNECTED,           //已连向一个socket  

    SS_DISCONNECTING        //正在断开连接的过程中  

}socket_state;  

版权声明:本文为silenceblog原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/silenceblog/p/14329742.html