linux内核分析及应用 -- Linux 网络层数据流分析(上)

计算机网络是个庞大且复杂的话题,这不是一本书就可以说清楚的。然而,在互联网极大普及的今天,很少有应用程序不与网络打交道,我还是抱着授人以渔的思想,尝试介绍 Linux 在网络层面的整体架构,这样在解决具体问题的时候,才可以有针对性地进行分析。

本章不打算从头到尾来解释 Linux 网络从物理层到应用层的所有实现。关于理论方面的知识,可以单独学习 Andrew 的《计算机网络》,Steven 的《TCP/IP 详解卷1》,在实现方面,可以参考《深入理解 Linux 网络技术内幕》。

本章主要介绍 Linux 网络层数据的整体流转架构,包括如下内容:

  • 分析数据在网络层的流转过程。

  • socket 接口层的实现。

  • netfilter 和 lvs 的关系。

  • 一些常用的网络相关的系统参数。

  • Nginx 服务器监听 socket 初始化过程。

8.1 数据在网络层的流转

在大多数情况下,我们接触到的网络都是基于 TCP/IP 协议栈的实现,TCP/IP 协议是一个分层的网络模型(见图8-1)。所以,Linux 内核对网络层的实现也是围绕这个分层模型展开的。

图8-1 TCP/IP 网络模型的基本结构

8.1.1 sk_buff 结构

sk_buff 结构是 Linux 实现的在网络各层中流转的数据结构,这个结构相对较为复杂,我们这里略去细节,仅为说明流程进行简单介绍(代码详见:/Linux-4.5.2/include/Linux/skbuff.h)。

struct sk_buff {
    union {
        struct {
        // 内核维护了一个 sk_buff_head 链表,next 代表该 skb 的下一个元素,prev 代表上一个元素
            struct sk_buff        *next;
            struct sk_buff        *prev;
        …
        struct sock               *sk;         // L4 层对应的 socket 结构
        struct net_device         *dev;        // sbk 相关联的网络设备
        …
    unsigned int          len,                 // 整个缓冲区大小,包括 head
    data_len;                                  // 只包括数据的大小
        __u16             mac_len,             // mac 报头大小
    hdr_len;
    …
    __be16                protocol;            // 当前层的协议
    __u16                 transport_header;
    __u16                 network_header;
    __u16                 mac_header;
    …
    sk_buff_data_t        tail;
    sk_buff_data_t        end;
    unsigned char         *head,*data;
    unsigned int          truesize;            // 缓冲区总大小,包括 skb 自己
    atomic_t              users;
};

其中 tail、end、head、data 的关系如图8-2所示。

图8-2 tail、end、head、data 指针

数据往上一层传输之后,data 指针就指向当前层报头的尾端,也就是上一层报头的开始。

8.1.2 数据流转过程

在了解了 Linux 网络中传输的数据结构 skb 之后,我们来看各层的处理流程(如图8-3所示),我们以接收数据为例,网卡首先获取网络中的数据,然后经过网卡驱动解析数据,封装成链路层的数据帧给 L2 层,L2 层经过处理之后(比如 bridge 网桥),假如数据还在本机内,则传送给 L3 层,L3 层经过处理(route 路由),假如数据还在本机内,则交给 L4 层,最后交给用户态程序。同理,发送数据的过程反过来类推即可。

图8-3 Linux 对网络数据的收发流程

下面,我们自底向上对每一层内核的处理过程进行分析。

首先,我们先搞明白 Linux 如何响应网卡的请求,在系统启动的时候,会注册响应网络读写事件的软中断。

static int __init net_dev_init(void)
{
    …
    open_softirq(NET_TX_SOFTIRQ, net_tx_action);
    open_softirq(NET_RX_SOFTIRQ, net_rx_action);
    …
}

其中 net_tx_action 用于响应传输数据,net_rx_action 用于响应接收数据。

我们先来分析一下 net_rx_action:

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

    local_irq_disable();
    list_splice_init(&sd->poll_list, &list);
    local_irq_enable();

    for (;;) {
        struct napi_struct *n;
        …
        n = list_first_entry(&list, struct napi_struct, poll_list);
        budget -= napi_poll(n, &repoll);
    …
    }
    local_irq_disable();
    list_splice_tail_init(&sd->poll_list, &list);
    list_splice_tail(&repoll, &list);
    list_splice(&list, &sd->poll_list);
    if (!list_empty(&sd->poll_list))
        __raise_softirq_irqoff(NET_RX_SOFTIRQ);
    net_rps_action_and_irq_enable(sd);
}

net_rx_action 对于当前 CPU 对应 poll_list 队列中的所有 dev,调用 dev->poll 方法。该方法是由对应 dev 的驱动程序实现的,用于接收及处理报文。

net_rx_action 每次运行都有一定的限度,并不一定要将所有报文都处理完。在处理完一定数量的报文配额、或处理过程超过一定时间后,net_rx_action 便会返回。返回前触发一次 NET_RX_SOFTIRQ 软中断,等待下一次中断到来的时候继续被调度。

从 net_rx_action 的实现我们可以发现,Linux 对于网络数据的接收是中断+轮询的方式,网卡驱动通过实现 poll 方法进行轮询,结合软中断的过程。这是为了更好地提升性能,否则所有数据收发都依赖中断的方式,那么 CPU 将会在不停的中断响应当中忙死。

至于网卡驱动的实现,这里就不再进行分析了,有兴趣大家可以自己去研究一下网卡驱动,一般网卡驱动实现 poll 函数之后,会解析出 skb 结构,然后调用 netif_receive_skb 函数把数据交给网络协议栈。

在分析 netif_receive_skb 的实现之前,我们先来了解一下 Linux 对不同协议的抽象:

struct packet_type {
    __be16              type; // 协议类型
    struct net_device   *dev;
    // 协议处理函数
    int                 (*func) (struct sk_buff *,
                            struct net_device *,
                            struct packet_type *,
                            struct net_device *);
    bool                (*id_match)(struct packet_type *ptype,
                                struct sock *sk);
    void                *af_packet_priv;
    struct list_head    list;
};

每种协议都有一个 packet_type 来描述,其中,最主要的3个属性为 type、dev、func,分别代表协议的类型,注册的设备以及协议处理函数。

netif_receive_skb 最终会调用 __netif_receive_skb_core 函数,下面我们分析其执行过程:

static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
{
    struct packet_type *ptype, *pt_prev;
    rx_handler_func_t *rx_handler;
    struct net_device *orig_dev;
    bool deliver_exact = false;
    int ret = NET_RX_DROP;
    __be16 type;
    …
    orig_dev = skb->dev;

    skb_reset_network_header(skb);        // 重制 skb 头,现在 skb 指向 ip 头
    …
    // 遍历全局 ptype_all,做相应协议处理,例如 tcpdump 或 raw socket
    list_for_each_entry_rcu(ptype, &ptype_all, list)
        if (pt_prev)
            ret = deliver_skb(skb, pt_prev, orig_dev);
        pt_prev = ptype;
    }
    // 遍历设备上注册的 ptype_all 进行处理
    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;
    }

    …
    type = skb->protocol;                // 获取协议类型

    if (likely(!deliver_exact)) {        // 根据全局定义的协议进行报文处理
        deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
                        &ptype_base[ntohs(type) &
                            PTYPE_HASH_MASK]);
    }
    // 根据设备特定的协议进行报文处理
    deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
                    &orig_dev->ptype_specific);
    // 如果设备发生变化,那么还需要针对新设备的注册协议进行处理
    if (unlikely(skb->dev != orig_dev)) {
        deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
                        &skb->dev->ptype_specific);
    }

    // 处理最后一个未处理的 packet_type
    if (pt_prev) {
        if (unlikely(skb_orphan_frags(skb, GFP_ATOMIC)))
            goto drop;
        else
            ret = pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
    } else {
drop:
        atomic_long_inc(&skb->dev->rx_dropped);
        kfree_skb(skb);
        ret = NET_RX_DROP;
    }

out:
    return ret;
}

在数据包处理过程中,首先会看 ptype_all 中是否有注册的协议处理函数,如果有则调用相应的处理函数,然后再到 ptype_base 中寻找进行处理。

其中 ptype_all 为一个双向链表,ptype_base 是一个哈希表,其哈希函数以协议标识符为参数,内核通常利用该哈希表判断应当接收传入的网络数据报的协议。

ptype_all 中一般注册的都是抓包程序、raw socket 等,ptype_base 则为 TCP/IP 协议栈,如 ip、arp 等。

以 IPv4 协议为例,其注册的 func 为 ip_rcv:

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

当包交到网络层之后,我们先放一放。下面再分析一下包的发送过程。

当我们用上层协议构建好报文之后,就可以调用 dev_queue_xmit 函数进行数据的发送。

dev_queue_xmit 函数会将数据提交到设备的队列中,dev->qdisc 指向一个队列的实例,里面包含了队列本身以及操作队列的方法(enqueue、dequeue、requeue)。

然后通过调用 qdisc_restart 把数据从队列中取出,提交给驱动程序的 dev->hard_start_xmit 方法进行发送。

这里省略了 dev_queue_xmit 相关的源码,大家可以自己阅读分析。

假如驱动发送数据成功后,就会发生中断,最后通过我们之前注册软中断程序 NET_TX_SOFTIRQ 进行响应,执行 net_rx_action 函数,至于 net_rx_action 的实现就留给读者自己分析了。

8.2 socket 接口层的实现

在了解了 Linux 网络层数据传输的过程后,再来分析一下常用的 sokcet 接口层,sokcet 层一般用于构建 TCP 层的连接,进行数据的读写。在 Linux 一切皆文件的思想下,socket 的读写也是对文件的读写。我们先来看一下进程和 socket 的关系(见图8-4),在进程结构(task_struct)中维护了所有打开的文件句柄 files,其中的 fd_array 字段对应一个 file 结构,file 中的 f_inode 对应的是 socket_alloc 中的 vfs_inode。

图8-4 进程和 socket 的关系

8.2.1 socket 系统初始化

在 Linux 上 socket 实现了类似的文件系统:

static struct file_system_type sock_fs_type = {
    .name =        "sockfs",
    .mount =        sockfs_mount,
    .kill_sb =      kill_anon_super,
};

其中,mount 函数指针定义了如何挂载文件系统,而 kill_sb 函数指针定义了如何删除该超级块。

对 sockfs_mount 超级块的操作进行注册(主要进行 inode 的分配和销毁):

static const struct super_operations sockfs_ops = {
    .alloc_inode          = sock_alloc_inode,
    .destroy_inode        = sock_destroy_inode,
    .statfs               = simple_statfs?
};

对 socket 文件读写则注册为:

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

系统在启动阶段,会进行 socket 子系统的初始化:

core_initcall(sock_init);
static int __init sock_init(void)
{
    int err;
    …
    skb_init();                // 初始化 skb 数据包 slab 缓存
    init_inodecache();         // 创建一块用于 socket 相关的 inode 缓存;后面创建 inode、释放
                                // inode 会使用到
    err = register_filesystem(&sock_fs_type); // 将 socket 文件系统注册到内核中
    …
    sock_mnt = kern_mount(&sock_fs_type); // 挂载 socket fs
    …
}

socket_init 主要对 socket 文件系统进行注册和挂载。

8.2.2 socket 创建

Linux 通过提供 sys_socket 系统调用来创建 socket,通过对 socket 的创建,初始化了进程和 socket 文件句柄的上下文:

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
    int retval;
    struct socket *sock;
    int flags;
    ...
    retval = sock_create(family, type, protocol, &sock);
    if (retval < 0)

    retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
    …
    return retval;
}

通过 sys_socket 系统调用发现,socket 创建过程主要分为两个步骤:

1)通过 sock_create->__sock_create->sock_alloc 分配 socket 结构。

2)进程通过 sock_map_fd 让 sock_alloc_file 申请 file 结构后,让 socket 和该文件进行关联。

因为 sock_create 执行过程比较长,这里省略了代码,通过图8-5,可以了解 sock_create 执行之后发生的事情。

图8-5 sock_create 执行过程

sock_create 通过3个步骤来完成:

1)通过 alloc_inode 调用 socket_fs 的 sock_alloc_inode,创建了 vfs_inode 结构。

2)通过 AF_INET 协议族的 inet_create 函数创建 TCP 的 sock 对象,并且和 socket 对象挂钩。

3)TCP 协议(tcp_prot)结构的 init 方法初始化 sock 对象。

其中协议族对象需要实现 net_proto_family 接口,协议对象需要实现 proto 接口:

static const struct net_proto_family inet_family_ops = {
    .family = PF_INET,
    .create = inet_create,
    .owner        = THIS_MODULE,
};

struct proto tcp_prot = {
    .name                              = "TCP",
    .owner                             = THIS_MODULE,
    .close                   = tcp_close,
    .connect                 = tcp_v4_connect,
    .disconnect              = tcp_disconnect,
    .accept                            = inet_csk_accept,
    .ioctl                   = tcp_ioctl,
    .init                    = tcp_v4_init_sock,
    .destroy                 = tcp_v4_destroy_sock,
    .shutdown                = tcp_shutdown,
    .setsockopt              = tcp_setsockopt,
    .getsockopt                        = tcp_getsockopt,
    .recvmsg                 = tcp_recvmsg,
    .sendmsg                 = tcp_sendmsg,
    .sendpage                = tcp_sendpage,
    .backlog_rcv                       = tcp_v4_do_rcv,
    .release_cb              = tcp_release_cb,
    .hash                    = inet_hash,
    .unhash                            = inet_unhash,
    .get_port                = inet_csk_get_port,
    .enter_memory_pressure                = tcp_enter_memory_pressure,
    .stream_memory_free               = tcp_stream_memory_free,
    .sockets_allocated                = &tcp_sockets_allocated,
    .orphan_count                     = &tcp_orphan_count,
    .memory_allocated                 = &tcp_memory_allocated,
    .memory_pressure                  = &tcp_memory_pressure,
    .sysctl_mem                       = sysctl_tcp_mem,
    .sysctl_wmem                      = sysctl_tcp_wmem,
    .sysctl_rmem                      = sysctl_tcp_rmem,
    .max_header                       = MAX_TCP_HEADER,
    .obj_size                 = sizeof(struct tcp_sock),
    .slab_flags               = SLAB_DESTROY_BY_RCU,
    .twsk_prot                = &tcp_timewait_sock_ops,
    .rsk_prot                 = &tcp_request_sock_ops,
    .h.hashinfo               = &tcp_hashinfo,
    .no_autobind                       = true,
#ifdef CONFIG_COMPAT
    .compat_setsockopt                 = compat_tcp_setsockopt,
    .compat_getsockopt                 = compat_tcp_getsockopt,
#endif
    .diag_destroy                      = tcp_abort,
};

8.2.3 socket 绑定

在 socket 创建之后,还需要对其与指定的地址和端口进行绑定才能使用,Linux 提供一个 sys_bind 系统调用来实现该功能:

SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
    struct socket *sock;
    struct sockaddr_storage address;
    int err, fput_needed;
    // 根据 fd 获取对应的 soket 结构
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (sock) {
        // 将用户空间的地址拷贝到内核空间
        err = move_addr_to_kernel(umyaddr, addrlen, &address);
        if (err >= 0) {
            err = security_socket_bind(sock,
                            (struct sockaddr *)&address,
                            addrlen);
            if (!err)
                // 根据指定协议域及 socket 类型进行 bind
                err = sock->ops->bind(sock,
                                (struct sockaddr *)
                                &address, addrlen);
        }
        fput_light(sock->file, fput_needed);
    }
    return err;
}

sys_bind 的核心操作为 sock->ops->bind,以 inet_stream_ops 为例,bind 函数为 inet_bind:

const struct proto_ops inet_stream_ops = {
    …
    .bind                   = inet_bind,
    …
};

inet_bind 的主要步骤分为:

1)地址类型检测:

 if (!net->ipv4.sysctl_ip_nonlocal_bind &&
    !(inet->freebind || inet->transparent) &&
    addr->sin_addr.s_addr != htonl(INADDR_ANY) &&
    chk_addr_ret != RTN_LOCAL &&
    chk_addr_ret != RTN_MULTICAST &&
    chk_addr_ret != RTN_BROADCAST)
    goto out;

2)端口范围检测:

snum = ntohs(addr->sin_port);
    err = -EACCES;
    if (snum && snum < PROT_SOCK &&
        !ns_capable(net->user_ns, CAP_NET_BIND_SERVICE))
        goto out;

3)设置源地址和接收地址:

inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;
    if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)
        inet->inet_saddr = 0;

4)检查端口是否被占用:

if ((snum || !inet->bind_address_no_port) &&
        sk->sk_prot->get_port(sk, snum)) {
        inet->inet_saddr = inet->inet_rcv_saddr = 0;
        err = -EADDRINUSE;
        goto out_release_sock;
    }

5)初始化目标地址和端口:

inet->inet_sport = htons(inet->inet_num);
    inet->inet_daddr = 0;
    inet->inet_dport = 0;

8.2.4 socket 监听

在 socket 经过 sys_bind 之后,就可以进行监听操作,Linux 提供一个 sys_listen 系统调用来执行该动作。

SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
    struct socket *sock;
    int err, fput_needed;
    int somaxconn;
    // 根据文件描述符取得 socket 对象
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (sock) {
        // 根据系统中的设置调整参数 backlog
        somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
        if ((unsigned int)backlog > somaxconn)
            backlog = somaxconn;
        err = security_socket_listen(sock, backlog);
        if (!err)
            // 调用特定协议簇的 listen 函数
            err = sock->ops->listen(sock, backlog);
        fput_light(sock->file, fput_needed);
    }
    return err;
}

sys_listen 通过 sock->ops->listen 来调用特定协议簇的 listen 函数,我们仍旧以 AF_INTET 的 inet_listen 来分析执行过程:

int inet_listen(struct socket *sock, int backlog)
{
    struct sock *sk = sock->sk;
    unsigned char old_state;
    int err;
    …
    // 假如状态不是 SS_UNCONNECTED 或者类型不是 SOCK_STREAM 则退出
    if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
        goto out;
    old_state = sk->sk_state;
    // 这里检查 sock 的状态是否是 TCP_CLOSE 或 TCP_LISTEN
    if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
        goto out;
    // 当 sock 的状态不是 TCP_LISTEN 时,进行监听初始化初始化
    if (old_state != TCP_LISTEN) {
        …
        err = inet_csk_listen_start(sk, backlog);
        …
    }
    // 设置 sock 的最大并发连接请求数
    sk->sk_max_ack_backlog = backlog;
    …
    return err;
}

inet_listen 在 inet_csk_listen_start 中初始化了连接等待队列,并且设置 sock 的状态为 TCP_LISTEN,最后将当前 socket 在 inet_hashinfo 中进行哈希处理,在 socket 的哈希表结构 inet_hashinfo 中,其成员 listening_hash[INET_LHTABLE_SIZE] 用于存放处于 TCP_LISTEN 状态的 sock 当 socket 通过 listen()调用完成等待连接队列的初始化后,需要将当前 sock 放到该结构中。

8.2.5 socket 接受连接

在 socket 处于监听状态之后,Linux 通过 sys_accept 函数来接受新客户端连接的到来:

SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr,
    int __user *, upeer_addrlen, int, flags)
{
    struct socket *sock, *newsock;
    struct file *newfile;
    int err, len, newfd, fput_needed;
    struct sockaddr_storage address;
    …
    // 根据 server_fd 获取服务端的 socket 结构
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    …
    // 给客户端分配一个新的 socket 结构
    newsock = sock_alloc();
    …
    // 把服务端的 socket 类型和 ops 赋给客户端的 socket
    newsock->type = sock->type;
    newsock->ops = sock->ops;
    __module_get(newsock->ops->owner);
    // 获取一个未使用的 fd 句柄
    newfd = get_unused_fd_flags(flags);
    …
    // 分配一个新的 file 结构
    newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name);
    …
    err = security_socket_accept(sock, newsock);
    …
    // 调用对应协议簇的 accept 函数
    err = sock->ops->accept(sock, newsock, sock->file->f_flags);
    …
    if (upeer_sockaddr) {
        if (newsock->ops->getname(newsock, (struct sockaddr *)&address,
                    &len, 2) < 0) {
            err = -ECONNABORTED;
            goto out_fd;
        }
        // 拷贝地址到用户态
        err = move_addr_to_user(&address,
                    len, upeer_sockaddr, upeer_addrlen);
        …
    }
    // 把新创建的 fd 和 file 关联
    fd_install(newfd, newfile);
    err = newfd;
out_put:
    fput_light(sock->file, fput_needed);
out:
    return err;
out_fd:
    fput(newfile);
    put_unused_fd(newfd);
    goto out_put;
}

sys_accept 的关键就是给客户端分配 socket 结构,然后执行服务端 socket 的 accept 操作,再把客户端的 socket 与新分配的 file 及文件句柄 fd 关联。下面我们仍旧以 af_net 的 inet_accept 函数来分析 accpet:

int inet_accept(struct socket *sock, struct socket *newsock, int flags)
{
    struct sock *sk1 = sock->sk;
    int err = -EINVAL;
    // 如果使用的是 TCP,则 sk_prot 为 tcp_prot,accept 为 inet_csk_accept()获取新连接的 sock
    struct sock *sk2 = sk1->sk_prot->accept(sk1, flags, &err);
    …
    WARN_ON(!((1 << sk2->sk_state) &
        (TCPF_ESTABLISHED | TCPF_SYN_RECV |
        TCPF_CLOSE_WAIT | TCPF_CLOSE)));
    // 把 sock 和 socket 嫁接起来,让它们能相互索引
    sock_graft(sk2, newsock);
    // 把新 socket 的状态设为已连接
    newsock->state = SS_CONNECTED;
    err = 0;
    release_sock(sk2);
do_err:
    return err;
}

static inline void sock_graft(struct sock *sk, struct socket *parent)
{
    write_lock_bh(&sk->sk_callback_lock);
    sk->sk_wq = parent->wq;        //设置 sock 的等待队列为 socket 的等待队列
    // 相互嫁接
    parentsk ->= sk;
    sk_set_socket(sk, parent);
    security_sock_graft(sk, parent);
    write_unlock_bh(&sk->sk_callback_lock);
}

inet_accept 通过调用具体协议层(例如 TCP)的 accept 来获取 sock 结构,然后和 scoket 结构关联起来,并且把 socket 状态设置为已连接。

最后以 TCP 层的 accept 实现 inet_csk_accept 来说明新的 sock 结构是如何获取的:

struct sock *inet_csk_accept(struct sock *sk, int flags, int *err)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    struct request_sock_queue *queue = &icsk->icsk_accept_queue;
    struct request_sock *req;
    struct sock *newsk;
    …
    error = -EINVAL;
    // socket 必须处于监听状态
    if (sk->sk_state != TCP_LISTEN)
        goto out_err;
    // 发现有 ESTABLISHED 状态的连接请求块
    if (reqsk_queue_empty(queue)) {
        // 获取等待超时时间,如果是非阻塞则为0
        long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
        // 如果是非阻塞的,则直接退出
        error = -EAGAIN;
        if (!timeo)
            goto out_err;
        // 阻塞等待,直到有全连接。如果用户有设置等待超时时间,超时后会退出
        error = inet_csk_wait_for_connect(sk, timeo);
        …
    }
     // 获取新连接的 sock,释放连接控制块
    req = reqsk_queue_remove(queue, sk);
    newsk = req->sk;
…
out:
    release_sock(sk);
    if (req)
        reqsk_put(req);
    return newsk;
…
}

在 TCP 层的 accept 主要还是等待获取可用并且状态为 ESTABLISHED 连接的过程,假如有则从 backlog 队列(全连接队列)中取出一个 ESTABLISHED 状态的连接请求块,返回它所对应的连接 sock。同时更新 backlog 队列的全连接数,通过 reqsk_queue_remove 释放取出的连接控制块:

static inline struct request_sock *reqsk_queue_remove(struct request_sock_
    queue *queue, struct sock *parent)
{
    struct request_sock *req;

    spin_lock_bh(&queue->rskq_lock);
    req = queue->rskq_accept_head;              // 第一个 ESTABLISHED 状态的连接请求块
    if (req) {
        sk_acceptq_removed(parent);                // 当前 backlog 队列的全连接数减一
        queue->rskq_accept_head = req->dl_next;
        if (queue->rskq_accept_head == NULL)
            queue->rskq_accept_tail = NULL;
    }
    spin_unlock_bh(&queue->rskq_lock);
    return req;
}

8.2.6 新连接的到来

通过分析 sys_accept 函数可以发现,新连接是从 backlog 队列中取出来的,那么队列中的数据又是谁放进去的呢?

在开头通过分析 Linux 网络层数据流转过程可以发现,数据从网卡进来后,最终会调用 ip_rcv 函数,在该函数处理过程中,假如数据应该在本机内部,那么会通过 ip_rcv_finish->dst_input->ip_local_deliver 在本机内处理数据:

int ip_local_deliver(struct sk_buff *skb)
{
…
    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);
}

其中ip_local_deliver_finish->ipprot->handler(skb)->tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_state_process->icsk->icsk_af_ops->conn_request(sk,skb)->tcp_v4_conn_request->tcp_conn_request->inet_csk_reqsk_queue_add:

struct sock *inet_csk_reqsk_queue_add(struct sock *sk,
                    struct request_sock *req,
                    struct sock *child)
{
    struct request_sock_queue *queue = &csk_accept_queuecsk_accept_queue;

    spin_lock(&queue->rskq_lock);
    if (unlikely(sk->sk_state != TCP_LISTEN)) {
        inet_child_forget(sk, req, child);
        child = NULL;
    } else {
        req->sk = child;
        req->dl_next = NULL;
        if (queue->rskq_accept_head == NULL)
            queue->rskq_accept_head = req;
        else
            queue->rskq_accept_tail->dl_next = req;
        queue->rskq_accept_tail = req;
        sk_acceptq_added(sk);
    }
    spin_unlock(&queue->rskq_lock);
    return child;
}

所以,经过从网卡到 IP 层再到 TCP 层的数据流转,最终通过 inet_csk_reqsk_queue_add 把新的 sock 放入到了 csk_accept_queuecsk_accept_queue 队列中。

知道了如何将 socket 放入到 request 队列后,再来看一下前面设置的 backlog 大小有什么用,通过 ip_local_deliver_finish->ipprot->handler(skb)->tcp_v4_rcv->tcp_check_req->inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk,skb,req,NULL,req,&own_req)->tcp_v4_syn_recv_sock->sk_acceptq_is_full 来判断新建立的连接是否有可能被创建出来:

static inline bool sk_acceptq_is_full(const struct sock *sk)
{
    return sk->sk_ack_backlog > sk->sk_max_ack_backlog;
}

在了解了服务端创建 socket,并且进行绑定和监听之后,大家一定会问,客户端是如何进行 connect 操作的呢?连接建立后,数据的读写又是如何进行的呢?限于篇幅,这块工作就交给大家自己完成了,因为整个数据流转的过程已经分析过了,所以都可以依样画葫芦进行分析。

另外,需要说明的是,连接数据的流程和数据读取的流程是类似的,仅仅状态不同而已,对 IP 层来讲,业务的数据和连接建立的数据都是一样的。客户端建立连接 Linux 通过提供一个 sys_connect 操作来完成的,其最终会通过 TCP 层的 tcp_v4_connect 把 sync 数据包通过 netfilter 的 output 阶段发送出去。

8.2.7 socket 整体流程

最后我们通过图8-6总结一下整个 TCP 协议簇下 socket 的总体流程。

图8-6 socket 流程

从图中我们可以发现,TCP 本身是一个面向连接的协议,服务端在启动的时候,步骤大致如下:

1)首先通过 sys_socket 创建监听套接字,并且通过 sys_bind 绑定设备,然后使用 sys_lsten 进行监听。

2)客户端也会通过 sys_socket 创建套接字,然后通过 sys_connect 创建同服务器的连接。

3)服务器最后通过 sys_accept 来接受客户端的请求,建立连接。

以上过程就是 TCP 建立连接的三次握手过程。在连接建立之后,客户端和服务器都可以通过 sys_recvmg 和 sys_endmsg 来互相收发数据,数据最终会经过 Linux 的 netfilter 层进行过滤和转发。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值