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


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发送数据

应用层发送数据时,sys_send实际只是对sys_sendto的简单封装。sys_sendto函数的作用是将数据报发送至指定的目的地址。而sys_sendto是通过调用sys_sendmsg函数进行发送数据的。sys_sendmsg函数的作用是将用户空间的信息复制到内核空间中,然后再逐级调用发包接口发送数据。 sys_sendmsg最终调用了sock_sendmsg()。sys_sendmsg函数部分源代码如下:

asmlinkage long sys_sendmsg(int fd, struct msghdr __user *msg, unsigned flags)
{
.......
//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; }
......

sock_sendmsg()又调用了__sock_sendmsg.__sock_sendmsg实际上又调用了sock->ops->sendmsg函数

static inline int __sock_sendmsg(struct kiocb *iocb, struct socket *sock,
       struct msghdr *msg, size_t size)
{
    int err = security_socket_sendmsg(sock, msg, size);
    return err ?: __sock_sendmsg_nosec(iocb, sock, msg, size);
}
 
static inline int __sock_sendmsg_nosec(struct kiocb *iocb, struct socket *sock,
        struct msghdr *msg, size_t size)
{
    struct sock_iocb *si = kiocb_to_siocb(iocb);
    si->sock = sock;
    si->scm = NULL;
    si->msg = msg;
    si->size = size;
 
    /* 调用Socket层的操作函数,如果是SOCK_STREAM,则proto_ops为inet_stream_ops,
     * 函数指针指向inet_sendmsg()。
     */
    return sock->ops->sendmsg(iocb, sock, msg, size);
} 

  sock-ops->sendmsg的意思就是根据不同的协议调用不同的sendmsg函数,对于 TCP ,调用tcp_sendmsg函数。

通过以上的分析,且由于应用层的系统调用过程比价清晰,给出sendmsg系统调用的过程如图所示:

 gdb跟踪调试结果如下:

 

4.2接收数据

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

 

  gdb跟踪调试结果如下:

 

5.传输层分析

5.1发送数据

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

发送数据部分的源代码:

...
    
        //查看是否立即发送数据包
            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函数。

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

gdb调试结果如下:

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

gdb调试结果如下:

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函数。ip_local_out函数内部调用__ip_local_out发现返回一个nf_hook函数,里面调用了dst_output。

 

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

gdb调试结果如下

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

gdb调试结果如下:

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函数, 它是设备自己注册的.

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;
    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;
    if (unlikely(napi_disable_pending(n))) {
        napi_complete(n);
        goto out_unlock;
    }
    if (n->gro_list) {
        napi_gro_flush(n, HZ >= 1000);
    }
    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 进行处理。

 

8.时序图

 

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM