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

题目要求:

1.在深入理解Linux内核任务调度(中断处理、softirg、tasklet、wq、内核线程等)机制的基础上,分析梳理send和recv过程中TCP/IP协议栈相关的运行任务实体及相互协作的时序分析。

2.编译、部署、运行、测评、原理、源代码分析、跟踪调试等

3.应该包括时序图

 

TCP/IP协议栈简介

逻辑上分为四层,相应的各种具体协议如下所示:

 

应用层:决定了向用户提供应用服务时通信的活动

传输层:对上层应用层提供处于网络连接中的两台计算机之间的数据传输

网络层:处理在网络上流动的数据包

网络接口层:处理连接网络的硬件部分。

 

Linux操作系统简介

Linux,全称GNU/Linux,是一种免费使用和自由传播的类UNIX操作系统,主要受到Minix和Unix思想的启发,是一个基于POSIX和Unix的多用户、多任务、支持多线程和多CPU的操作系统。Linux继承了Unix以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统。

系统内核的路由转发

Linux操作系统嵌入了TCP/IP协议栈,协议软件具有路由转发功能。路由转发依赖作为路由器的主机中安装多块网卡,当某一块网卡接收到数据包后,系统内核会根据数据包的目的IP地址,查询路由表,然后根据查询结果将数据包发送到另外一块网卡,最后通过此网卡把数据包发送出去。此主机的处理过程就是路由器完成的核心功能。

通过修改Linux系统内核参数ip_forward的方式实现路由功能,系统使用sysctl命令配置与显示在/proc/sys目录中的内核参数。首先在命令行输入:cat/proc/sys/net/ipv4/ip_forwad,检查Linux内核是不是开启IP转发功能。如果结果为1,表明路由转发功能已经开启;如果结果为0,表明没有开启。出于安全考虑,Linux内核默认是禁止数据包路由转发的。在linux系统中,有临时和永久两种方法启用转发功能。

 

 

Linux网络栈的层次结构。从上而下,依次为应用层,系统调用接口层,协议无关接口层,网络栈层,设备无关接口层,设备驱动层。Linux网络栈中的socket继承自BSD,为应用层使用网络服务提供了简单的方法,它屏蔽了具体的下层协议族的差异。

 

应用层接口:位于TCP/IP协议的最上层,工作于用户空间,使用内核提供的API对底层数据结构进行操作,其

提供的函数比较多样,如:

read()/write(),send()/recv(),sendto()/recvfrom(),bind(),connect(),accept()

系统调用接口:系统调用接口是用户空间的应用程序正常访问内核的唯一途径,系统调用一般以sys开头。

协议无关接口:协议无关接口是由socket来实现的,它提供一组通用函数来支持各种不同的协议。Linux中socket结构是struct sock,这个结构定义了socket所需要的所  有状态信息,包括socke所使用的协议以及可以在socket上执行的操作。

网络协议:Linux支持多种协议,每一个协议都对应net_family[]数组中的一项,net_family[]的元素为一个结构体指针,指向一个包含注册协议信息的结构体                      net_proto_family;

设备无关接口:设备无关接口net_device实现的,任何设备与上层通信都是通过net_device设备无关接口。它将设备与具有很多功能的不同硬件连接在一起,这一层提供一组通用函数供底层网络设备驱动程序使用,让它们可以对高层协议栈进行操作。

设备驱动程序:网络体系结构的最底部是负责管理物理网络设备的设备驱动程序层。

Linux网络子系统通过这五层结构的相互交互,共同完成TCP/IP协议栈的运行。

 

TCP/IP流程

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在 Linux 系统中,socket 属于文件系统的一部分,网络通信可以被看作是对文件的读取,使得对网络的控制和对文件的控制一样方便。

Socket处理流程大致如下

Socket结构如下:

socket创建

// socket_state: socket 状态
// type: socket type 
// flags: socket flags 
// ops: 专用协议的socket的操作
// file: 与socket 有关的指针列表
// sk: 负责协议相关结构体,这样就让这个这个结构体和协议分开。
// wq: 等待队列
struct socket {  
    socket_state        state;
    kmemcheck_bitfield_begin(type);
    short           type;
    kmemcheck_bitfield_end(type);
    unsigned long       flags;
    struct socket_wq __rcu  *wq;
    struct file     *file;
    struct sock     *sk;
    const struct proto_ops  *ops;
};

Socket调用如下

socket创建

#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

bind绑定

#include <sys/types.h>
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

listen

#include <sys/types.h>
#include <sys/socket.h>

int listen(int sockfd, int backlog);

accept

#include <sys/types.h>
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

connect客户端连接函数

#include <sys/types.h>
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

读写函数及关闭函数

#include <unistd.h>

int read(int fd, char *buf, int len);
int write(int fd, char *buf, int len);
int close(int fd);

可以看到,TCP存在复杂的状态转换机制

 

socket 函数并没有为套接字绑定本地地址和端口号,对于服务器端则必须显性绑定地址和端口号。通过在应用层调用bind函数把一个本地协议地址赋予套接字。

inet_bind 函数即为bind函数的最底层实现,该函数实现了本地地址和端口号的绑定,其中还针对上层传过来的地址结构进行校验,检查是否冲突可用。需要清楚的是 sock_array数组,这其实是一个链式哈希表,里面保存的就是各个端口号的sock结构,数组大小小于端口号,所以采用链式哈希表存储。bind 函数的各层分工很明显,主要就是inet_bind函数了,在注释里说的很明确了,bind 是绑定本地地址,它不负责对端地址,一般用于服务器端,客户端是系统指定的。

然后listen 函数把一个未连接的套接字转换为一个被动套接字,指示内核应接受指向该套接字的连接请求,其内部实现归根到底就是设置 sock 结构的状态,设置其为 TCP_LISTEN。之后TCP客户可以用 connect 函数来建立与 TCP 服务器的连接,其实是客户利用 connect 函数向服务器端发出连接请求。

实质操作落到了下一层传输层tcp_connect 函数客户端通过这个函数获得对端的地址信息(ip地址和端口号),另外本地ip地址也是在这个函数中指定的。三次握手阶段起于 connect 函数,自然地,在该函数指定目的地址,以及设置标志字段,定时器以后,就需要向服务器端发送连接请求数据包。

最后调用ip_queue_xmit 函数进行数据包的发送和接收。

客户端通过connect系统调用给服务器发送一个同步报文段,使连接客户端状态转移到SYN_SENT状态,服务器端在收到客户端发来的SYN报文后状态转移到SYN_RCVD状态。数据报文段的发送和接收在 ip_queue_xmit 函数和 release_sock 函数中实现。客户端在收到确认报文段后状态转为ESTABLISHED,服务器端在收到来自客户端的报文段后也转为ESTABLISHED连接建立

最后服务器端在应用层调用accept 函数该函数返回一个已建立连接的可用于数据通信的套接字。

 

发送和接收过程的分析

 

Send发送数据,底层调用sock_sendmsg

调用tcp_sendmsg, 将所有的数据组织成发送队列,这个发送队列是struct sock结构中的一个域sk_write_queue,这个队列的每一个元素是一个skb,里面存放的就是待发送的数据。

int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
{
    struct tcp_sock *tp = tcp_sk(sk);/*进行了强制类型转换*/
    struct sk_buff *skb;
    flags = msg->msg_flags;
    ......
        if (copied)
            tcp_push(sk, flags & ~MSG_MORE, mss_now,
                 TCP_NAGLE_PUSH, size_goal);
}

然后调用了tcp_push,又调用了tcp_write_xmit,它实现了tcp的拥塞控制,然后调用了tcp_transmit_skb(sk, skb, 1, gfp)传输数据

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;
    int result;
    bool is_cwnd_limited = false, is_rwnd_limited = false;
    u32 max_segs;
    /*统计已发送的报文总数*/
    sent_pkts = 0;
    ......

    /*若发送队列未满,则准备发送报文*/
    while ((skb = tcp_send_head(sk))) {
        unsigned int limit;

        if (unlikely(tp->repair) && tp->repair_queue == TCP_SEND_QUEUE) {
            /* "skb_mstamp_ns" is used as a start point for the retransmit timer */
            skb->skb_mstamp_ns = tp->tcp_wstamp_ns = tp->tcp_clock_cache;
            list_move_tail(&skb->tcp_tsorted_anchor, &tp->tsorted_sent_queue);
            tcp_init_tso_segs(skb, mss_now);
            goto repair; /* Skip network transmission */
        }

        if (tcp_pacing_check(sk))
            break;

        tso_segs = tcp_init_tso_segs(skb, mss_now);
        BUG_ON(!tso_segs);
        /*检查发送窗口的大小*/
        cwnd_quota = tcp_cwnd_test(tp, skb);
        if (!cwnd_quota) {
            if (push_one == 2)
                /* Force out a loss probe pkt. */
                cwnd_quota = 1;
            else
                break;
        }

        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, TCP_FRAG_IN_WRITE_QUEUE,
                      skb, limit, mss_now, gfp)))
            break;

        if (tcp_small_queue_check(sk, skb, 0))
            break;

        if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
            break;

tcp_transmit_skb是tcp发送数据位于传输层的最后一步,先对TCP数据段的头部进行处理,然后调用了网络层提供的发送接口icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);实现数据发送。数据离开传输层。

static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,
                  int clone_it, gfp_t gfp_mask, u32 rcv_nxt)
{
    
    skb_push(skb, tcp_header_size);
    skb_reset_transport_header(skb);
    ......
    /* 构建TCP头部和校验和 */
    th = (struct tcphdr *)skb->data;
    th->source        = inet->inet_sport;
    th->dest        = inet->inet_dport;
    th->seq            = htonl(tcb->seq);
    th->ack_seq        = htonl(rcv_nxt);

    tcp_options_write((__be32 *)(th + 1), tp, &opts);
    skb_shinfo(skb)->gso_type = sk->sk_gso_type;
    if (likely(!(tcb->tcp_flags & TCPHDR_SYN))) {
        th->window      = htons(tcp_select_window(sk));
        tcp_ecn_send(sk, skb, th, tcp_header_size);
    } else {
        /* RFC1323: The window in SYN & SYN/ACK segments
         * is never scaled.
         */
        th->window    = htons(min(tp->rcv_wnd, 65535U));
    }
    ......
    icsk->icsk_af_ops->send_check(sk, skb);

    if (likely(tcb->tcp_flags & TCPHDR_ACK))
        tcp_event_ack_sent(sk, tcp_skb_pcount(skb), rcv_nxt);

    if (skb->len != tcp_header_size) {
        tcp_event_data_sent(tp, sk);
        tp->data_segs_out += tcp_skb_pcount(skb);
        tp->bytes_sent += skb->len - tcp_header_size;
    }

    if (after(tcb->end_seq, tp->snd_nxt) || tcb->seq == tcb->end_seq)
        TCP_ADD_STATS(sock_net(sk), TCP_MIB_OUTSEGS,
                  tcp_skb_pcount(skb));

    tp->segs_out += tcp_skb_pcount(skb);
    /* OK, its time to fill skb_shinfo(skb)->gso_{segs|size} */
    skb_shinfo(skb)->gso_segs = tcp_skb_pcount(skb);
    skb_shinfo(skb)->gso_size = tcp_skb_mss(skb);

    /* Leave earliest departure time in skb->tstamp (skb->skb_mstamp_ns) */

    /* Cleanup our debris for IP stacks */
    memset(skb->cb, 0, max(sizeof(struct inet_skb_parm),
                   sizeof(struct inet6_skb_parm)));

    err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);
    ......
}

然后到网络层,ip_queue_xmit(skb) 检查skb->dst路由信息。如果没有,比如套接字的第一个包,就使用ip_route_output()选择一个路由。

接着填充IP包的各个字段,如版本、包头长度、TOS等。

如需分片——即当报文的长度大于mtu,gso的长度不为0就会调用 ip_fragment 进行分片,否则就调用ip_finish_output2把数据发送出去。ip_fragment 函数,会检查 IP_DF 标志位,如果待分片IP数据包禁止分片,则调用 icmp_send()向发送方发送一个原因为需要分片而设置了不分片标志的目的不可达ICMP报文,并丢弃报文,即设置IP状态为分片失败,释放skb,返回消息过长错误码。

接下来就用 ip_finish_ouput2 设置链路层报文头。如果链路层报头缓存有(即hh不为空),就拷贝到skb里。如果无,就调用neigh_resolve_output,使用 ARP 获取

调用ip_queue_xmit,

int __ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *f
l, 
            __u8 tos) 
{ 
//路由寻找成功 
packet_routed: 
    //...预处理... 
    res = ip_local_out(net, sk, skb); 
    rcu_read_unlock(); 
    return res; 
no_route: 
    rcu_read_unlock(); 
    IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES); 
    kfree_skb(skb); 
    return -EHOSTUNREACH; 
}

会调用skb_rtable,实际上找路由缓存。

调用ip_local_out发送,里面调用了dst_output,实质是调用ip_finish_output。

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; 
}
static int ip_finish_output(struct net *net, struct sock *sk, struct sk_b
uff *skb) 
{ 
    int ret; 
    ret = BPF_CGROUP_RUN_PROG_INET_EGRESS(sk, skb); 
    switch (ret) { 
    case NET_XMIT_SUCCESS: 
        return __ip_finish_output(net, sk, skb); 
case NET_XMIT_CN:
        return __ip_finish_output(net, sk, skb) ? : ret; 
    default: 
        kfree_skb(skb); 
        return ret; 
    } 
}

为了正确接收和发送IP分组,链路层需要在一开始就将网络相关的部分进行初始化,主要工作是为网络协议注册中断、设置定时器、注册软中断等。在链路层的帧处理由中断事件驱动。中断会将帧复制到sk_buff数据结构中。 

 

关于中断处理:

1、硬中断处理

当数据帧从网线到达网卡上的时候,第一站是网卡的接收队列。网卡在分配给自己的RingBuffer中寻找可用的内存位置,找到后DMA引擎会把数据DMA到网卡之前关联的内存里,这个时候CPU都是无感的。当DMA操作完成以后,网卡会像CPU发起一个硬中断,通知CPU有数据到达。

当RingBuffer满的时候,新来的数据包将被丢弃。ifconfig查看网卡的时候,可以里面有个overruns,表示因为环形队列满被丢弃的包。通过ethtool命令可加大环形队列的长度。

网卡的硬中断注册的处理函数是igb_msix_ring。

Linux在硬中断里只完成简单必要的工作,剩下的大部分的处理都是转交给软中断。只记录了一个寄存器,修改了一下CPU的poll_list,然后发出个软中断。

2、ksoftirqd内核线程处理软中断

软中断和硬中断看起来调用了同一个函数local_softirq_pending。但使用方式不同,硬中断位置是为了写入标记,这里仅仅只读取。如果硬中断中设置了NET_RX_SOFTIRQ,这里自然能读取的到。接下来真正进入线程函数中run_ksoftirqd处理

static void run_ksoftirqd(unsigned int cpu)
{
    local_irq_disable();
    if (local_softirq_pending()) {
        __do_softirq();
        rcu_note_context_switch(cpu);
        local_irq_enable();
        cond_resched();
        return;
    }
    local_irq_enable();
}

在__do_softirq中,判断根据当前CPU的软中断类型,调用其注册的action方法。

asmlinkage void __do_softirq(void)
{
    do {
        if (pending & 1) {
            unsigned int vec_nr = h - softirq_vec;
            int prev_count = preempt_count();

            ...
            trace_softirq_entry(vec_nr);
            h->action(h);
            trace_softirq_exit(vec_nr);
            ...
        }
        h++;
        pending >>= 1;
    } while (pending);
}  

核心函数net_rx_action

static void net_rx_action(struct softirq_action *h)
{
    struct softnet_data *sd = &__get_cpu_var(softnet_data);
    unsigned long time_limit = jiffies + 2;
    int budget = netdev_budget;
    void *have;

    local_irq_disable();

    while (!list_empty(&sd->poll_list)) {
        ......
        n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);

        work = 0;
        if (test_bit(NAPI_STATE_SCHED, &n->state)) {
            work = n->poll(n, weight);
            trace_napi_poll(n);
        }

        budget -= work;
    }
}

函数开头time_limit和budget用来控制net_rx_action函数主动退出,目的是保证网络包的接收不霸占CPU不放。 下次网卡再有硬中断的时候再处理剩下的数据包。其中budget可以通过内核参数调整。 这个函数中剩下的核心逻辑是获取到当前CPU变量softnet_data,对其poll_list进行遍历, 然后执行到网卡驱动注册到的poll函数。

 

话题回到链路层:调用dev_queue_xmit,实质是调用__dev_queue_xmit,它根据不同的情况会调用__dev_xmit_skb或sch_direct_xmit,最终会调用dev_hard_start_xmit函数,该函数最终会调用xmit_one来发送一到多个数据包。

 

接收过程类似,一个 package 到达机器的物理网络适配器,当它接收到数据帧时,就会触发一个中断,并将通过 DMA 传送到位于 linux kernel 内存中的 rx_ring。

网卡发出中断,通知 CPU 有个 package 需要它处理。中断处理程序主要进行以下一些操作,包括分配 skb_buff 数据结构,并将接收到的数据帧从网络适配器I/O端口拷贝到skb_buff 缓冲区中;从数据帧中提取出一些信息,并设置 skb_buff 相应的参数,这些参数将被上层的网络协议使用,例如skb->protocol;

终端处理程序经过简单处理后,发出一个软中断(NET_RX_SOFTIRQ),通知内核接收到新的数据帧。

内核 2.5 中引入一组新的 API 来处理接收的数据帧,即 NAPI。所以,驱动有两种方式通知内核:(1) 通过以前的函数netif_rx;(2)通过NAPI机制。该中断处理程序调用 Network device的 netif_rx_schedule 函数,进入软中断处理流程,再调用 net_rx_action 函数。

该函数关闭中断,获取每个 Network device 的 rx_ring 中的所有 package,最终 pacakage 从 rx_ring 中被删除,进入 netif _receive_skb 处理流程。

netif_receive_skb 是链路层接收数据报的最后一站。它根据注册在全局数组 ptype_all 和 ptype_base 里的网络层数据报类型,把数据报递交给不同的网络层协议的接收函数(INET域中主要是ip_rcv和arp_rcv)。该函数主要就是调用第三层协议的接收函数处理该skb包,进入第三层网络层处理。

 

IP 层的接收入口函数 ip_rcv 。首先会做包括 package checksum 在内的各种检查,如果需要会做 IP defragment(将多个分片合并),然后 packet 调用已经注册的 Pre-routing netfilter hook ,完成后最终到达 ip_rcv_finish 函数。

static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
    struct net_device *dev = skb->dev;
    int ret;
    /* if ingress device is enslaved to an L3 master device pass the
     * skb to its handler for processing
     */
    skb = l3mdev_ip_rcv(skb);
    if (!skb)
        return NET_RX_SUCCESS;
    ret = ip_rcv_finish_core(net, sk, skb, dev);
    if (ret != NET_RX_DROP)
        ret = dst_input(skb);
    return ret;
}

调用dst_input,再调用ip_local_deliver,如果分片,就调用ip_defrag。须在路由选在子系统中查找,确定是发给当前主机还是转发。如果是当前主机则依次调用方法ip_local_deliver()和ip_local_deliver_finish()。如果需要转发则调用ip_forward()函数。

static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
    __skb_pull(skb, skb_network_header_len(skb));
    rcu_read_lock();
    ip_protocol_deliver_rcu(net, skb, ip_hdr(skb)->protocol);
    rcu_read_unlock();
    return 0;
}

int ip_local_deliver(struct sk_buff *skb)
{
    /*    Reassemble IP fragments. */
    struct net *net = dev_net(skb->dev);
    if (ip_is_fragment(ip_hdr(skb))) {
        if (ip_defrag(net, skb, IP_DEFRAG_LOCAL_DELIVER))
            return 0;
    }
    return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
               net, NULL, skb, skb->dev, NULL,
               ip_local_deliver_finish);
}

TCP协议首先会检查套接字状态,若有已连接状态的套接字,则返回该套接字;若无套接字,则检查是否有监听状态的套接字,并作下一步处理。

若上述步骤未出错,则进入到套接字的状态检查,根据套接字的状态不同,做不同的预处理,最终进入到tcp_v4_do_rcv()函数中进行下一步处理。

在分组处理完成之后,需要将数据交给应用层,调用的是tcp_data_queue()函数,将接收并处理好的skb结构插入接收队列当中,等待应用层的处理。

tcp_data_queue主要把非乱序包copy到ucopy或者sk_receive_queue中,并调用tcp_fast_path_check尝试开启快速路径。对重传包设置dsack,并快速ack回去。对于乱序包,如果包里有部分旧数据也设置dsack,并把乱序包添加到ofo队列中。

void tcp_rcv_established(struct sock *sk, struct sk_buff *skb)
{
    //...其他处理...
    tcp_data_queue(sk, skb);
    //...其他处理...
}

static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
    //...执行插入队列...
    tcp_data_queue_ofo(sk, skb);//对乱序包进行处理
}

应用层

用户端接收数据时,会使用msghdr来构造消息,其对应的内核结构为user_msghdr;其中msg_iov向量指向了多个数据区,msg_iovlen标识了数据区个数;在通过系统调用进入内核后,该结构中的信息会拷贝给内核的msghdr结构。

在套接字发送接收系统调用流程中,send/recv,sendto/recvfrom,sendmsg/recvmsg最终都会使用内核中的msghdr来组织数据。

其中msg_iter为指向数据区域的向量汇总信息,其中数据区指针可能包含一个或者多个数据区,对于send/sendto其只包含了一个数据区。

struct msghdr {
     /* 指向socket地址结构 */
     void    *msg_name;    /* ptr to socket address structure */
     /* 地址结构长度 */
     int    msg_namelen;    /* size of socket address structure */
     /* 数据 */
     struct    iov_iter msg_iter;    /* data */
     /* 控制信息 */
     void    *msg_control;    /* ancillary data */
     /* 控制信息缓冲区长度 */
     __kernel_size_t  msg_controllen;  /* ancillary data buffer length */
     /* 接收信息的标志 */
     unsigned int    msg_flags;    /* flags on received message */
     /* 异步请求控制块 */
     struct kiocb    *msg_iocb;    /* ptr to iocb for async requests */
};

 

GDB调试

 

整体过程的大致时序图:

posted on 2021-01-29 17:04  实名上网  阅读(298)  评论(0)    收藏  举报