结合Linux内核源码分析TCP协议状态机


一、准备环境

环境:Ubuntu18.04

编辑器:VSCode

内核版本:linux-4.19.194

1. 下载编译内核源码

wget https://mirrors.edge.kernel.org/pub/linux/kernel/v4.x/linux-4.19.194.tar.xz

tar xf linux-4.19.194.tar.xz
cd linux-4.19.194

# 想要编译什么架构就指定什么值,所有合法的值在源码的arch文件夹下必须存在同名的文件夹
export ARCH=x86 #x86架构

# make命令的参数可以通过 make help 获取
make x86_64_defconfig # 生成64位平台的默认配置

make # 编译内核

如果想要下载与当前正在运行的 Ubuntu 内核对应的源代码,可以使用命令 apt-get source linux-image-unsigned-$(uname -r) 来获取。

BuildYourOwnKernel

2. 分析编译时的宏定义

在第一步 下载编译内核源码 成功后,源代码目录下会生成一个名为 .config 的文件,该文件指明了编译内核时所用到的设置。可以使用下面的命令来生成编译内核代码时使用的宏定义列表:

grep "^CONFIG_.*" .config | sed "s/=y/=1/g" | sed "s/=m/_MODULE=1/g" | cat - <(echo __KERNEL__)

如果想要生成与当前正在运行的 Ubuntu 内核对应的配置,使用下面的命令:

cat /boot/config-$(uname -r) | grep "^CONFIG_.*" | sed "s/=y/=1/g" | sed "s/=m/_MODULE=1/g" | cat - <(echo __KERNEL__)

最后添加的宏 __KERNEL__ 来自源代码根目录下 Makefile 文件的443行。

将上面命令生成的结果配置到vscode中,可以更加方面有效的查看内核代码。

二、确定套接字调用对应的内核实现函数

下面以创建套接字的函数 socket 为例。

1. 确定socket调用的syscall号

// file: main.c
// compile: gcc -ggdb main.c
#include <sys/types.h>
#include <sys/socket.h>

int main(int argc, char** argv)
{
    int s = socket(AF_INET, SOCK_DGRAM, 0);
    return s;
}

GDB反汇编调试socket调用

0x7ffff7b04d50 <socket>         mov    $0x29,%eax
0x7ffff7b04d55 <socket+5>       syscall

可以发现socket调用的syscall号为0x29(即41)。

命令 man syscall 可以获取更多关于 syscall 调用的信息。

2. 确定socket对应的内核实现

系统调用表定义在 arch/x86/entry/syscall_64.c 文件中:

// file: arch/x86/entry/syscall_64.c
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
    /*
     * Smells like a compiler bug -- it doesn't work
     * when the & below is removed.
     */
    [0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_64.h>
};

代码中 #include <asm/syscalls_64.h> 对应的文件是 arch/x86/include/generated/asm/syscalls_64.h

// file: arch/x86/include/generated/asm/syscalls_64.h
#ifdef CONFIG_X86
__SYSCALL_64(41, __x64_sys_socket, )
#else /* CONFIG_UML */
__SYSCALL_64(41, sys_socket, )
#endif

arch/x86/include/generated/asm/syscalls_64.h 文件由 arch/x86/entry/syscalls/syscall_64.tbl 文件生成而来。

根据 分析编译时的宏定义 的结果,可以确定宏 CONFIG_X86 是定义了的,所以调用号41对应的实现为 __x64_sys_socket

在源码中却找不到 __x64_sys_socket 的定义,但能在文件 net/socket.c 中找到 __sys_socket 函数的定义:

// file: net/socket.c
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
    return __sys_socket(family, type, protocol);
}

分析宏 SYSCALL_DEFINE3 的定义:

// file: include/linux/syscalls.h
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)

#define SYSCALL_DEFINEx(x, sname, ...)              \
    SYSCALL_METADATA(sname, x, __VA_ARGS__)         \
    __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

接着分析宏 __SYSCALL_DEFINEx 的定义:

// file: arch/x86/include/asm/syscall_wrapper.h
#define __SYSCALL_DEFINEx(x, name, ...)                 \
    asmlinkage long __x64_sys##name(const struct pt_regs *regs);    \
    ALLOW_ERROR_INJECTION(__x64_sys##name, ERRNO);          \
    static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \
    static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
    asmlinkage long __x64_sys##name(const struct pt_regs *regs) \
    {                               \
        return __se_sys##name(SC_X86_64_REGS_TO_ARGS(x,__VA_ARGS__));\
    }                               \
    __IA32_SYS_STUBx(x, name, __VA_ARGS__)              \
    static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))  \
    {                               \
        long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
        __MAP(x,__SC_TEST,__VA_ARGS__);             \
        __PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));   \
        return ret;                     \
    }                               \
    static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))

可以确定,宏 SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) 展开后,定义了 __x64_sys_socket

__x64_sys_socket 函数的本体就是 net/socket.c 文件中的 __sys_socket 函数。

三、分析TCP协议状态机

1. socket

调用流程如下:

__sys_socket                // net/socket.c:1355
    |- sock_create          // net/socket.c:1346
        |- __sock_create    // net/socket.c:1316

socket 函数最后会调用函数 __sock_create ,其主要代码如下:

int __sock_create(struct net *net, int family, int type, int protocol,
            struct socket **res, int kern)
{
    struct socket *sock;
    const struct net_proto_family *pf;

    sock = sock_alloc();

    pf = rcu_dereference(net_families[family]);

    pf->create(net, sock, protocol, kern);
}

sock_alloc() 函数分配套接字的一些资源后,再从 net_families 取出网络协议簇对应的处理函数,最后创建对应网络协议簇的套接字。

net_families 是一个数组,里面存放了不同的协议簇对应的处理函数。通过函数 sock_register 可以注册到此数组中。

// file: net/ipv4/af_inet.c
static const struct net_proto_family inet_family_ops = {
    .family = PF_INET,
    .create = inet_create,
    .owner = THIS_MODULE,
};

static int __init inet_init(void)
{
    (void)sock_register(&inet_family_ops);
}

对于 TCP/IPv4 来说,上面代码中的 err = pf->create(net, sock, protocol, kern) 调用的其实是 inet_create(net, sock, protocol, kern)

inet_create                             // net/ipv4/af_inet.c:1075
    |- sock_init_data                   // net/core/sock.c:347
        |- sk->sk_state = TCP_CLOSE     // net/core/sock.c:2790

综上,调用 socket() 函数后,套接字的默认状态为 TCP_CLOSE

2. listen

调用流程如下:

__sys_listen                    // net/socket.c:1525
    |- sock->ops->listen()      // net/socket.c:1516

对于 TCP/IPv4 来说,上面代码中的 sock->ops->listen() 调用的其实是 net/ipv4/af_inet.c 文件中的 inet_listen 函数。

inet_listen                                     // net/ipv4/af_inet.c:991
    |- inet_csk_listen_start                    // net/ipv4/af_inet.c:229
        |- inet_sk_state_store(sk, TCP_LISTEN)  // net/ipv4/inet_connection_sock.c:923

综上,调用 listen() 函数后,套接字的状态由 TCP_CLOSE 变为 TCP_LISTEN

3. connect

调用流程如下:

__sys_connect                // net/socket.c:1674
    |- sock->ops->connect()  // net/socket.c:1663

同理,对于 TCP/IPv4 来说,上面代码中的 sock->ops->connect() 调用的其实是 net/ipv4/af_inet.c 文件中的 inet_stream_connect 函数。

inet_stream_connect                 // net/ipv4/af_inet.c:985
    |- __inet_stream_connect        // net/ipv4/af_inet.c:719
        |- sk->sk_prot->connect()   // net/ipv4/af_inet.c:655

上面代码中的 sk->sk_prot->connect() 调用的其实是 net/ipv4/tcp_ipv4.c 文件中的 tcp_v4_connect 函数。

1. 发送SYN

tcp_v4_connect                                      // net/ipv4/tcp_ipv4.c:2460
    |- tcp_set_state(sk, TCP_SYN_SENT)              // net/ipv4/tcp_ipv4.c:280
    |- tcp_connect                                  // net/ipv4/tcp_ipv4.c:318
        |- tcp_transmit_skb                         // net/ipv4/tcp_output.c:3529
            |- __tcp_transmit_skb                   // net/ipv4/tcp_output.c:1164
                |- icsk->icsk_af_ops->queue_xmit()  // net/ipv4/tcp_output.c:1148

tcp_v4_connect 函数先将套接字的状态设置为 TCP_SYN_SENT ,然后再调用 tcp_connect 发送实际的SYN包。

在函数 __tcp_transmit_skb 中构建网络包的TCP头。构建好后,调用 icsk->icsk_af_ops->queue_xmit() 发送TCP包,而函数 icsk->icsk_af_ops->queue_xmit() 实际就是 include/net/ip.h 文件中的 ip_queue_xmit 函数。

ip_queue_xmit                                   // net/ipv4/tcp_ipv4.c:1931
    |- __ip_queue_xmit                          // include/net/ip.h:197
        |- skb_dst_set_noref(skb, &rt->dst)     // net/ipv4/ip_output.c:473
        |- ip_local_out                         // net/ipv4/ip_output.c:506

首先在函数 __ip_queue_xmit 中调用 skb_dst_set_noref(skb, &rt->dst) 设置发送的目标,然后构建 IP 头并调用函数 ip_local_out 将包含有 SYN 标志的 TCP 包发送出去。

2. 接收到SYN和ACK,并回复ACK

当 IP 层接收到 TCPv4 的包时,会调用函数 tcp_v4_rcv 来处理。

tcp_v4_rcv                                                  // net/ipv4/af_inet.c:1684
    |- tcp_v4_do_rcv                                        // net/ipv4/tcp_ipv4.c:1832
        |- tcp_rcv_state_process                            // net/ipv4/tcp_ipv4.c:1569
            |- tcp_rcv_synsent_state_process                // net/ipv4/tcp_input.c:6065
                |- tcp_finish_connect                       // net/ipv4/tcp_input.c:5898
                    |- tcp_set_state(sk, TCP_ESTABLISHED)   // net/ipv4/tcp_input.c:5695
                |- tcp_send_ack                             // net/ipv4/tcp_input.c:5928

tcp_rcv_synsent_state_process 中,先将套接字的状态设置为 TCP_ESTABLISHED ,然后再调用 tcp_send_ack 发送 ACK 完成 TCP 的三次握手。

综上,调用 connect() 函数后,套接字的状态由 TCP_CLOSE 变为 TCP_SYN_SENT ,在成功收到 ACK 后,状态再变为 TCP_ESTABLISHED

4. accept

调用流程如下:

__sys_accept4                               // net/socket.c:1630
    |- sock->ops->accept(sock, newsock)     // net/socket.c:1589

对于 TCP/IPv4 来说, sock->ops->accept() 实际调用的是 net/ipv4/af_inet.c 文件中的 inet_accept

inet_accept                     // net/ipv4/af_inet.c:987
    |- sk1->sk_prot->accept()   // net/ipv4/af_inet.c:734

对于 TCP/IPv4 来说, sk1->sk_prot->accept() 实际调用的是 net/ipv4/inet_connection_sock.c 文件中的 inet_csk_accept

struct sock *inet_csk_accept(struct sock *sk, int flags, int *err, bool kern)
{
    if (reqsk_queue_empty(queue)) {
        long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);

        /* If this is a non blocking socket don't sleep */
        error = -EAGAIN;
        if (!timeo)
            goto out_err;

        inet_csk_wait_for_connect(sk, timeo);
    }

    req = reqsk_queue_remove(queue, sk);
    newsk = req->sk;
    return newsk;
}

inet_csk_accept 首先检查监听套接字上的连接队列是否为空,为空且设置了 O_NONBLOCK 标志的话就返回错误 EAGAIN ,否则就等待直到有连接到来。

1. 接收到SYN,发送SYN和ACK

tcp_v4_rcv                                          // net/ipv4/af_inet.c:1684
    |- tcp_v4_do_rcv                                // net/ipv4/tcp_ipv4.c:1822
        |- tcp_rcv_state_process                    // net/ipv4/tcp_ipv4.c:1569
            |- icsk->icsk_af_ops->conn_request()    // net/ipv4/tcp_input.c:6051

对于 TCP/IPv4 来说,调用 icsk->icsk_af_ops->conn_request() 其实是调用 net/ipv4/tcp_ipv4.c 文件中的 tcp_v4_conn_request

tcp_v4_conn_request                                 // net/ipv4/tcp_ipv4.c:1935
    |- tcp_conn_request                             // net/ipv4/tcp_ipv4.c:1397
        |- inet_reqsk_alloc                         // net/ipv4/tcp_input.c:6461
            |- ireq->ireq_state = TCP_NEW_SYN_RECV  // net/ipv4/tcp_input.c:6374
        |- af_ops->send_synack()                    // net/ipv4/tcp_input.c:6556

在连接请求处理函数 tcp_conn_request 中,首先把新分配的套接字的 request_sock 设置为 TCP_NEW_SYN_RECV 状态。然后调用 af_ops->send_synack() 来发送 SYN 和 ACK 。对于 TCP/IPv4 来说,调用 af_ops->send_synack() 其实是调用 net/ipv4/tcp_ipv4.c 文件中的 tcp_v4_send_synack 。而函数 tcp_v4_send_synack 做的也确实是发送一个带有 SYN 和 ACK 标志的包。

2. 接收到ACK

tcp_v4_rcv                                              // net/ipv4/af_inet.c:1684
    |- tcp_check_req                                    // net/ipv4/tcp_ipv4.c:1773
        |- inet_csk(sk)->icsk_af_ops->syn_recv_sock()   // net/ipv4/tcp_minisocks.c:789
        |- inet_csk_complete_hashdance                  // net/ipv4/tcp_minisocks.c:797
    |- tcp_child_process                                // net/ipv4/tcp_ipv4.c:1792
  1. 对于 TCP/IPv4 来说,调用 inet_csk(sk)->icsk_af_ops->syn_recv_sock() 其实是调用 net/ipv4/tcp_ipv4.c 文件中的 tcp_v4_syn_recv_sock

    tcp_v4_syn_recv_sock                                    // net/ipv4/tcp_ipv4.c:1936
        |- tcp_create_openreq_child                         // net/ipv4/tcp_ipv4.c:1429
            |- inet_csk_clone_lock                          // net/ipv4/tcp_minisocks.c:452
                |- inet_sk_set_state(newsk, TCP_SYN_RECV)   // net/ipv4/inet_connection_sock.c:829

    需要注意的是,新创建的套接字的状态为 TCP_SYN_RECV

  2. 函数 inet_csk_complete_hashdance 主要做的是将请求控制块从未完成连接队列中删除,加入到已完成连接队列中:

    inet_csk_complete_hashdance                 // net/ipv4/tcp_minisocks.c:797
        |- inet_csk_reqsk_queue_drop            // net/ipv4/inet_connection_sock.c:992
            |- reqsk_queue_unlink               // net/ipv4/inet_connection_sock.c:703
        |- inet_csk_reqsk_queue_add             // net/ipv4/inet_connection_sock.c:994
  3. 函数 tcp_child_process 进一步对新创建的套接字进行处理并把状态更新为 TCP_ESTABLISHED

    tcp_child_process                               // net/ipv4/tcp_ipv4.c:1792
        |- tcp_rcv_state_process                    // net/ipv4/tcp_minisocks.c:851
            |- tcp_set_state(sk, TCP_ESTABLISHED)   // net/ipv4/tcp_input.c:6132

5. close

1. 发送FIN

tcp_close                                               // net/ipv4/tcp_ipv4.c:2458
    |- tcp_close_state                                  // net/ipv4/tcp.c:2393
        |- tcp_set_state(sk, ns)                        // net/ipv4/tcp.c:2294
    |- tcp_send_fin                                     // net/ipv4/tcp.c:2423
        |- TCP_SKB_CB(tskb)->tcp_flags |= TCPHDR_FIN    // net/ipv4/tcp_output.c:3087
    |- sk_stream_wait_close                             // net/ipv4/tcp.c:2426

对于状态是 TCP_ESTABLISHED 的套接字,函数 tcp_close_state 会把套接字的状态更新为 TCP_FIN_WAIT1tcp_send_fin 函数则发送带有 FIN 标记的包给对方。

2. 接收ACK

tcp_v4_rcv                                              // net/ipv4/af_inet.c:1684
    |- tcp_v4_do_rcv                                    // net/ipv4/tcp_ipv4.c:1832
        |- tcp_rcv_state_process                        // net/ipv4/tcp_ipv4.c:1569
            |- tcp_set_state(sk, TCP_FIN_WAIT2)         // net/ipv4/tcp_input.c:6175

3. 等待FIN并发送ACK

tcp_v4_rcv                                                      // net/ipv4/af_inet.c:1684
    |- tcp_v4_do_rcv                                            // net/ipv4/tcp_ipv4.c:1832
        |- tcp_rcv_state_process                                // net/ipv4/tcp_ipv4.c:1569
            |- tcp_data_queue                                   // net/ipv4/tcp_input.c:6262
                |- tcp_fin                                      // net/ipv4/tcp_input.c:4756
                    |- tcp_send_ack(sk)                         // net/ipv4/tcp_input.c:4139
                    |- tcp_time_wait(TCP_TIME_WAIT)             // net/ipv4/tcp_input.c:4140
                        |- inet_twsk_alloc                      // net/ipv4/tcp_minisocks.c:259
                            |- tw->tw_state = TCP_TIME_WAIT     // net/ipv4/inet_timewait_sock.c:175
                            |- timer_setup(tw_timer_handler)    // net/ipv4/inet_timewait_sock.c:188
                        |- inet_twsk_schedule()                 // net/ipv4/tcp_minisocks.c:319

在函数 tcp_fin 中,发送了 ACK 后,并进入 TCP_TIME_WAIT 状态。同时注册一个超时的定时器。

注意,虽然在 inet_twsk_alloc 函数中,修改的是 tw->tw_state 字段为 TCP_TIME_WAIT 状态。但是查看宏 tw_state 的定义可以知道,tw_state 指向的依旧是结构体 sock_common 中的字段 skc_state

6. shutdown

调用流程如下:

__sys_shutdown                                  // net/socket.c:1973
    |- sock->ops->shutdown()                    // net/socket.c:1965

对于 TCP/IPv4 来说,调用 sock->ops->shutdown() 其实是调用 net/ipv4/tcp_ipv4.c 文件中的 tcp_shutdown

void tcp_shutdown(struct sock *sk, int how)
{
    if (!(how & SEND_SHUTDOWN))
        return;

    /* If we've already sent a FIN, or it's a closed state, skip this. */
    if ((1 << sk->sk_state) &
        (TCPF_ESTABLISHED | TCPF_SYN_SENT |
            TCPF_SYN_RECV | TCPF_CLOSE_WAIT)) {
        /* Clear out any half completed packets.  FIN if needed. */
        if (tcp_close_state(sk))
            tcp_send_fin(sk);
    }
}

与调用 close 类似,

四、IP层及以下层的工作

1. IP层发送包

上层想要发送 IP 包,都会调用函数 ip_local_out 来发送。

ip_local_out                            // include/net/ip.h:165
    |- dst_output                       // net/ipv4/ip_output.c:125
        |- skb_dst(skb)->output()       // include/net/dst.h:455

对 IP 协议来说, skb_dst(skb)->output() 实际调用的是 net/ipv4/ip_output.c 文件中的 ip_output 函数。

ip_output                               // net/ipv4/route.c:1636
    |- ip_finish_output                 // net/ipv4/ip_output.c:408
        |- ip_finish_output2            // net/ipv4/ip_output.c:318
            |- neigh_output             // net/ipv4/ip_output.c:230
                |- neigh_hh_output      // include/net/neighbour.h:499
                    |- dev_queue_xmit   // include/net/neighbour.h:491

调用 dev_queue_xmit 是将 IP 包交给网络设备子系统来发送,而 dev_queue_xmit 直接调用的函数 __dev_queue_xmit

int dev_queue_xmit(struct sk_buff *skb)
{
    return __dev_queue_xmit(skb, NULL);
}

static int __dev_queue_xmit(struct sk_buff *skb, struct net_device *sb_dev)
{
    txq = netdev_pick_tx(dev, skb, sb_dev);
    q = rcu_dereference_bh(txq->qdisc);

    if (q->enqueue) {
        __dev_xmit_skb(skb, q, dev, txq);
        goto out;
    }

    /* The device has no queue. Common case for software devices:
     * loopback, all the sorts of tunnels...
     */
    if (dev->flags & IFF_UP) {
        dev_hard_start_xmit(skb, dev, txq, &rc);
    }
}

在函数 __dev_queue_xmit 内部:

  1. 针对有发送队列的设备

    首先调用 netdev_pick_tx 选择一个发送队列(现在的网卡一般都支持多个发送队列),然后再调用 __dev_xmit_skb 进行发送。在经过层层调用后,最后会调用的函数 dev_hard_start_xmit 发送包。

    |- __dev_xmit_skb                               // net/core/dev.c:3807
        |- q->enqueue(skb, q, &to_free)             // net/core/dev.c:3450
        |- qdisc_run                                // net/core/dev.c:3451
            |- __qdisc_run                          // include/net/pkt_sched.h:120
                |- qdisc_restart                    // net/sched/sch_generic.c:403
                    |- sch_direct_xmit              // net/sched/sch_generic.c:395
                        |- dev_hard_start_xmit      // net/sched/sch_generic.c:332

    需要注意函数 __qdisc_run 的实现:

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

    如果系统态 CPU 发送网络包不够用的时候,会调用函数 __netif_schedule 进而调用函数 __netif_reschedule 触发一个软中断 NET_TX_SOFTIRQ 。而对应的中断处理函数 net_tx_action 会重新调用 qdisc_run 继续发送剩余的网络包。

  2. 针对没有发送队列的设备

    如注释里说的回环地址,隧道网络等,就直接调用 dev_hard_start_xmit 发送包。

所以,不管设备有无发送队列,最后都会调用函数 dev_hard_start_xmit

|- dev_hard_start_xmit                      // net/core/dev.c:3262
    |- xmit_one                             // net/core/dev.c:3272
        |- netdev_start_xmit                // net/core/dev.c:3256
            |- __netdev_start_xmit          // include/linux/netdevice.h:4359
                |- ops->ndo_start_xmit      // include/linux/netdevice.h:4345

在经过层层调用后,最后调用的 ops->ndo_start_xmit() 其实是各个网卡驱动的实现了。

2. Intel的igb网卡驱动的发送流程

对网卡驱动 drivers/net/ethernet/intel/igb 来说, 函数 ops->ndo_start_xmit 调用的其实就是 igb_netdev_ops 中的 ndo_start_xmit 的值,即 drivers/net/ethernet/intel/igb/igb_main.c 文件中的 igb_xmit_frame 函数。

igb_xmit_frame              // drivers/net/ethernet/intel/igb/igb_main.c:2865
    |- igb_xmit_frame_ring  // drivers/net/ethernet/intel/igb/igb_main.c:6201
        |- igb_tx_map       // drivers/net/ethernet/intel/igb/igb_main.c:6157

函数 igb_tx_map 做的主要的工作就是把网络包数据映射到 RAM 的 DMA 区域,然后唤醒设备发送数据。

3. Intel的igb网卡驱动的初始化

  1. 模块加载

    igb_init_module                                         // drivers/net/ethernet/intel/igb/igb_main.c:680
        |- pci_register_driver(&igb_driver)                 // drivers/net/ethernet/intel/igb/igb_main.c:676
            |- __pci_register_driver                        // include/linux/pci.h:1289
                |- driver_register                          // drivers/pci/pci-driver.c:1409
                    |- bus_add_driver                       // drivers/base/driver.c:170
                        |- driver_attach                    // drivers/base/bus.c:672
                            |- __driver_attach              // drivers/base/dd.c:922
                                |- driver_probe_device      // drivers/base/dd.c:903
                                    |- really_probe         // drivers/base/dd.c:667
                                        |- drv->probe(dev)  // drivers/base/dd.c:510

    最后调用的 drv->probe(dev) 其实就是 igb_driver 中的 probe 的值,即 drivers/net/ethernet/intel/igb/igb_main.c 文件中的 igb_probe 函数。

    igb_probe                                       // drivers/net/ethernet/intel/igb/igb_main.c:230
        |- netdev->netdev_ops = &igb_netdev_ops     // drivers/net/ethernet/intel/igb/igb_main.c:3095
        |- igb_sw_init                              // drivers/net/ethernet/intel/igb/igb_main.c:3121
            |- igb_init_interrupt_scheme            // drivers/net/ethernet/intel/igb/igb_main.c:3888
                |- igb_alloc_q_vectors              // drivers/net/ethernet/intel/igb/igb_main.c:1387
                    |- igb_alloc_q_vector           // drivers/net/ethernet/intel/igb/igb_main.c:1331
                        |- netif_napi_add(igb_poll) // drivers/net/ethernet/intel/igb/igb_main.c:1216
                            |- napi->poll = poll    // net/core/dev.c:6194

    对 Intel 的 igb 网卡来说,模块加载函数 igb_probe 做了:

    • 初始化了设备相关的操作回调函数信息 igb_netdev_ops
    • 注册了 napi 的轮询回调函数 igb_poll
  2. 启动设备

    在函数 igb_probe 中,通过语句 netdev->netdev_ops = &igb_netdev_ops 初始化了网卡相关的操作回调。在网卡启用时,比如执行命令 ifconfig eth0 up 时,系统会依次调用:

    dev_open                        // net/core/dev.c:1430
        |- __dev_open               // net/core/dev.c:1437
            |- ops->ndo_open(dev)   // net/core/dev.c:1402

    对于网卡驱动 drivers/net/ethernet/intel/igb 来说,函数 ops->ndo_open(dev) 调用的就是函数 igb_open

    igb_open                                        // drivers/net/ethernet/intel/igb/igb_main.c:2863
        |- __igb_open                               // drivers/net/ethernet/intel/igb/igb_main.c:4017
            |- igb_request_irq                      // drivers/net/ethernet/intel/igb/igb_main.c:3953
                |- igb_request_msix                 // drivers/net/ethernet/intel/igb/igb_main.c:1416
                    |- request_irq(igb_msix_ring)   // drivers/net/ethernet/intel/igb/igb_main.c:968
            |- igb_irq_enable(adapter)              // drivers/net/ethernet/intel/igb/igb_main.c:3978

    igb_open 函数调用的深处,对于多队列网卡,通过 request_irq(igb_msix_ring) 为每个队列都注册了中断处理函数 igb_msix_ring 。设置好中断处理程序后,再调用 igb_irq_enable(adapter) 来开启中断。

4. Intel的igb网卡驱动的接收流程

当网卡的某个队列中收到网络数据后,会触发一个中断,这个中断的处理函数就是在启动设备时设置的 igb_msix_ring 函数:

igb_msix_ring                                   // drivers/net/ethernet/intel/igb/igb_main.c:6621
    |- napi_schedule                            // drivers/net/ethernet/intel/igb/igb_main.c:6628
        |- __napi_schedule                      // include/linux/netdevice.h:445
            |- ____napi_schedule                // net/core/dev.c:5892

网卡队列中断处理函数最深处的调用 ____napi_schedule 主要做两件事:

  • 保存待处理的数据。
  • 触发中断 NET_RX_SOFTIRQ
static inline void ____napi_schedule(struct softnet_data *sd,
                    struct napi_struct *napi)
{
    list_add_tail(&napi->poll_list, &sd->poll_list);
    __raise_softirq_irqoff(NET_RX_SOFTIRQ);
}

而中断 NET_RX_SOFTIRQ 的处理函数是 net_rx_action

net_rx_action           // net/core/dev.c:9874
    |- napi_poll        // net/core/dev.c:6338
        |- n->poll()    // net/core/dev.c:6272

中断处理函数 net_rx_action ,最终的会调用函数 n->poll() 。这函数就是网卡驱动模块加载时注册的处理回调函数 igb_poll

igb_poll                                                // drivers/net/ethernet/intel/igb/igb_main.c:1216
    |- igb_clean_rx_irq                                 // drivers/net/ethernet/intel/igb/igb_main.c:7759
        |- napi_gro_receive                             // drivers/net/ethernet/intel/igb/igb_main.c:8408
            |- napi_skb_finish                          // net/core/dev.c:5631
                |- netif_receive_skb_internal           // net/core/dev.c:5600
                    |- __netif_receive_skb              // net/core/dev.c:5156
                        |- __netif_receive_skb_one_core // net/core/dev.c:5066
                            |- __netif_receive_skb_core // net/core/dev.c:4952

在函数 __netif_receive_skb_core 中:

static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc,
                    struct packet_type **ppt_prev)
{
    list_for_each_entry_rcu(ptype, &ptype_all, list) {
        if (pt_prev)
            ret = deliver_skb(skb, pt_prev, orig_dev);
        pt_prev = ptype;
    }

    list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list) {
        if (pt_prev)
            ret = deliver_skb(skb, pt_prev, orig_dev);
        pt_prev = ptype;
    }

    deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
                    &ptype_base[ntohs(type) &
                        PTYPE_HASH_MASK]);
}

第一个循环 list_for_each_entry_rcu(ptype, &ptype_all, list) 中的 ptype_all 是一个设备无关的保存有所有协议处理回调的地方,而第二个循环 list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list) 中的 skb->dev->ptype_all 是一个设备有关的保存有所有协议处理回调的地方。这两个循环就能让像 tcpdump 这样的程序监听处理到收到的所有的网络包。

后面代码 deliver_ptype_list_skb 中的 ptype_base 是保存所有支持的3层协议处理函数的地方。才是将包转发给对应L3处理函数。函数 deliver_ptype_list_skb 的调用流程如下:

deliver_ptype_list_skb      // net/core/dev.c:4905
    |- deliver_skb          // net/core/dev.c:1971
        |- pt_prev->func()  // net/core/dev.c:1956

对于 IP 协议来说,其注册到 ptype_base 的流程如下:

inet_init                               // net/ipv4/af_inet.c:1890
    |- dev_add_pack(&ip_packet_type)    // net/core/dev.c:408

dev_add_pack(&ip_packet_type) 函数内部就是把 ip_packet_type 添加到处理 ETH_P_IP 的协议链中。

static struct packet_type ip_packet_type __read_mostly = {
    .type = cpu_to_be16(ETH_P_IP),
    .func = ip_rcv,
    .list_func = ip_list_rcv,
};

5. 接收IP包

所以对 IP 来说 pt_prev->func() 实际调用的是 ip_rcv 函数。

ip_rcv                                  // ip_packet_type:1886
    |- ip_rcv_finish                    // net/ipv4/ip_input.c:526
        |- dst_input                    // net/ipv4/ip_input.c:414
            |- skb_dst(skb)->input(skb) // include/net/dst.h:461

对 TCP/IPv4 来说 skb_dst(skb)->input(skb) 调用的其实是 net/ipv4/ip_input.c 文件中的 ip_local_deliver

ip_local_deliver                // net/ipv4/route.c:1638
    |- ip_local_deliver_finish  // net/ipv4/ip_input.c:258
        |- ipprot->handler(skb) // net/ipv4/ip_input.c:215

这里调用的 ipprot->handler(skb) 就是传输层的处理回调了。 在文件 net/ipv4/af_inet.c 中的函数 inet_init 内初始化:

static int __init inet_init(void)
{
    inet_add_protocol(&icmp_protocol, IPPROTO_ICMP);
    inet_add_protocol(&udp_protocol, IPPROTO_UDP);
    inet_add_protocol(&tcp_protocol, IPPROTO_TCP);
    inet_add_protocol(&igmp_protocol, IPPROTO_IGMP)
}

五、参考资料

Linux syscall过程分析(万字长文)
Linux 内核如何处理系统调用
Monitoring and Tuning the Linux Networking Stack: Sending Data
Monitoring and Tuning the Linux Networking Stack: Receiving Data


文章作者: Kiba Amor
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来源 Kiba Amor !
  目录