Linux TCP/IP协议栈之Socket的实现分析(Accept 接受一个连接)

简介: Tcp栈的三次握手简述进一步的分析,都是以 tcp 协议为例,因为 udp要相对简单得多,分析完 tcp,udp的基本已经被覆盖了。  这里主要是分析 socket,但是因为它将与 tcp/udp传输层交互,所以不可避免地接触到这一层面的代码,这里只是摘取其主要流程的一些代码片段,以更好地分析accept的实现过程。
Tcp栈的三次握手简述

进一步的分析,都是以 tcp 协议为例,因为 udp要相对简单得多,分析完 tcp,udp的基本已经被覆盖了。
 
这里主要是分析 socket,但是因为它将与 tcp/udp传输层交互,所以不可避免地接触到这一层面的代码,
这里只是摘取其主要流程的一些代码片段,以更好地分析accept的实现过程。

当套接字进入 LISTEN后,意味着服务器端已经可以接收来自客户端的请求。当一个 syn 包到达后,服务器认为它是一个
tcp请求报文,根据tcp协议,TCP 网络栈将会自动应答它一个 syn+ack 报文,并且将它放入 syn_table 这个 hash 表
中,静静地等待客户端第三次握手报文的来到。一个 tcp 的 syn 报文进入 tcp 堆栈后,会按以下函数调用,
最终进入 tcp_v4_conn_request:
 
tcp_v4_rcv
        ->tcp_v4_do_rcv
                ->tcp_rcv_state_process
                        ->tp->af_specific->conn_request

tcp_ipv4.c 中,tcp_v4_init_sock初始化时,有tp->af_specific = &ipv4_specific;
 
struct tcp_func ipv4_specific = {
        .queue_xmit = ip_queue_xmit,
        .send_check = tcp_v4_send_check,
        .rebuild_header = tcp_v4_rebuild_header,
        .conn_request = tcp_v4_conn_request,
        .syn_recv_sock = tcp_v4_syn_recv_sock,
        .remember_stamp = tcp_v4_remember_stamp,
        .net_header_len = sizeof(struct iphdr),
        .setsockopt = ip_setsockopt,
        .getsockopt = ip_getsockopt,
        .addr2sockaddr = v4_addr2sockaddr,
        .sockaddr_len = sizeof(struct sockaddr_in),
};
 
所以 af_specific->conn_request实际指向的是 tcp_v4_conn_request:
 
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
        struct open_request *req;
        ……
        /*  分配一个连接请求 */
        req = tcp_openreq_alloc();
        if (!req)
                goto drop;
        ……        
        /*  根据数据包的实际要素,如来源/目的地址等,初始化它*/
        tcp_openreq_init(req, &tmp_opt, skb);
 
        req->af.v4_req.loc_addr = daddr;
        req->af.v4_req.rmt_addr = saddr;
        req->af.v4_req.opt = tcp_v4_save_options(sk, skb);
        req->class = &or_ipv4;                
        ……
        /*  回送一个 syn+ack 的二次握手报文 */
        if (tcp_v4_send_synack(sk, req, dst))
                goto drop_and_free;
 
        if (want_cookie) {
                ……
        } else {                 /*  将连接请求 req 加入连接监听表 syn_table */
                tcp_v4_synq_add(sk, req);
        }
        return 0;        
}
 
syn_table 在前面分析的时候已经反复看到了。它的作用就是记录 syn 请求报文,构建一个 hash 表。
这里调用的 tcp_v4_synq_add()就完成了将请求添加进该表的操作:

static void tcp_v4_synq_add(struct sock *sk, struct open_request *req)
{
        struct tcp_sock *tp = tcp_sk(sk);
        struct tcp_listen_opt *lopt = tp->listen_opt;
       
        /*  计算一个 hash值 */
        u32 h = tcp_v4_synq_hash(req->af.v4_req.rmt_addr, req->rmt_port, lopt->hash_rnd);
 
        req->expires = jiffies + TCP_TIMEOUT_INIT;
        req->retrans = 0;
        req->sk = NULL;

        /*指针移到 hash 链的未尾*/
        req->dl_next = lopt->syn_table[h];
 
        write_lock(&tp->syn_wait_lock);

        /*加入当前节点*/
        lopt->syn_table[h] = req;
        write_unlock(&tp->syn_wait_lock);
 
        tcp_synq_added(sk);
}
 
这样所有的 syn 请求都被放入这个表中,留待第三次 ack 的到来的匹配。当第三次 ack 来到后,会进入下列函数:
tcp_v4_rcv
        ->tcp_v4_do_rcv
 
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
        …… 
        if (sk->sk_state == TCP_LISTEN) {
                struct sock *nsk = tcp_v4_hnd_req(sk, skb);
        ……
}
 因为目前 sk还是 TCP_LISTEN状态,所以会进入 tcp_v4_hnd_req:
[code]static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb)
{
        struct tcphdr *th = skb->h.th;
        struct iphdr *iph = skb->nh.iph;
        struct tcp_sock *tp = tcp_sk(sk);
        struct sock *nsk;
        struct open_request **prev;
        /* Find possible connection requests. */
        struct open_request *req = tcp_v4_search_req(tp, &prev, th->source,
                                                     iph->saddr, iph->daddr);
        if (req)
                return tcp_check_req(sk, skb, req, prev);
        ……
}
 
tcp_v4_search_req 就是查找匹配 syn_table 表:
[code]static struct open_request *tcp_v4_search_req(struct tcp_sock *tp,
                                              struct open_request ***prevp,
                                              __u16 rport,
                                              __u32 raddr, __u32 laddr)
{
        struct tcp_listen_opt *lopt = tp->listen_opt;
        struct open_request *req, **prev;
 
        for (prev = &lopt->syn_table[tcp_v4_synq_hash(raddr, rport, lopt->hash_rnd)];
             (req = *prev) != NULL;
             prev = &req->dl_next) {
                if (req->rmt_port == rport &&
                    req->af.v4_req.rmt_addr == raddr &&
                    req->af.v4_req.loc_addr == laddr &&
                    TCP_INET_FAMILY(req->class->family)) {
                        BUG_TRAP(!req->sk);
                        *prevp = prev;
                        break;
                }
        }
 
        return req;
}
 
hash 表的查找还是比较简单的,调用 tcp_v4_synq_hash 计算出 hash 值,找到 hash 链入口,遍历该
链即可。 排除超时等意外因素,刚才加入 hash 表的 req 会被找到,这样,tcp_check_req()函数将会被继续调用:
struct sock *tcp_check_req(struct sock *sk,struct sk_buff *skb,
                           struct open_request *req,
                           struct open_request **prev)
{
        ……
        tcp_acceptq_queue(sk, req, child);
        ……
}
 
req 被找到,表明三次握手已经完成,连接已经成功建立,tcp_check_req 最终将调用tcp_acceptq_queue(),
把这个建立好的连接加入至 tp->accept_queue 队列,等待用户调用 accept(2)来读取之。
 
static inline void tcp_acceptq_queue(struct sock *sk, struct open_request *req,
                                         struct sock *child)
{
        struct tcp_sock *tp = tcp_sk(sk);
 
        req->sk = child;
        sk_acceptq_added(sk);
 
        if (!tp->accept_queue_tail) {
                tp->accept_queue = req;
        } else {
                tp->accept_queue_tail->dl_next = req;
        }
        tp->accept_queue_tail = req;
        req->dl_next = NULL;
}
 
sys_accept

当 listen(2)调用准备就绪的时候,服务器可以通过调用 accept(2)接受或等待(注意这个“或等
待”是相当的重要)连接队列中的第一个请求:
int accept(int s, struct sockaddr * addr ,socklen_t *addrlen);
 
accept(2)调用,只是针对有连接模式。socket 一旦经过 listen(2)调用进入监听状态后,就被动地调用
accept(2)接受来自客户端的连接请求。accept(2)调用是阻塞的,也就是说如果没有连接请求到达,它会去睡觉,
等到连接请求到来后(或者是超时)才会返回。同样地操作码 SYS_ACCEPT 对应的是函数sys_accept
 
asmlinkage long sys_accept(int fd, struct sockaddr __user *upeer_sockaddr, int __user
*upeer_addrlen) {
        struct socket *sock, *newsock;
        int err, len;
        char address[MAX_SOCK_ADDR];
 
        sock = sockfd_lookup(fd, &err);
        if (!sock)
                goto out;
 
        err = -ENFILE;
        if (!(newsock = sock_alloc())) 
                goto out_put;
 
        newsock->type = sock->type;
        newsock->ops = sock->ops;
 
        err = security_socket_accept(sock, newsock);
        if (err)
                goto out_release;
 
        /*
         * We don't need try_module_get here, as the listening socket (sock)
         * has the protocol module (sock->ops->owner) held.
         */
        __module_get(newsock->ops->owner);
 
        err = sock->ops->accept(sock, newsock, sock->file->f_flags);
        if (err                 goto out_release;
 
        if (upeer_sockaddr) {
                if(newsock->ops->getname(newsock, (struct sockaddr *)address, &len, 2)                        err = -ECONNABORTED;
                        goto out_release;
                }
                err = move_addr_to_user(address, len, upeer_sockaddr, upeer_addrlen);
                if (err                         goto out_release;
        }
 
        /* File flags are not inherited via accept() unlike another OSes. */
 
        if ((err = sock_map_fd(newsock))                 goto out_release; 
        security_socket_post_accept(sock, newsock);
 
out_put:
        sockfd_put(sock);
out:
        return err;
out_release:
        sock_release(newsock);
        goto out_put;
}[/code]
 
代码稍长了点,逐步来分析它。
 
一个 socket,经过 listen(2)设置成 server 套接字后,就永远不会再与任何客户端套接字建立连接了。
因为一旦它接受了一个连接请求,就会创建出一个新的socket,新的 socket 用来描述新到达的连接,而原先的 server
套接字并无改变,并且还可以通过下一次 accept(2)调用 再创建一个新的出来,就像母鸡下蛋一样,“只取蛋,不杀鸡”,
server 套接字永远保持接受新的连接请求的能力。
 
函数先通过 sockfd_lookup(),根据 fd,找到对应的 sock,然后通过 sock_alloc分配一个新的 sock。
接着就调用协议簇的 accept()函数:
/*
*        Accept a pending connection. The TCP layer now gives BSD semantics.
*/
 
int inet_accept(struct socket *sock, struct socket *newsock, int flags)
{
        struct sock *sk1 = sock->sk;
        int err = -EINVAL;
        struct sock *sk2 = sk1->sk_prot->accept(sk1, flags, &err);
 
        if (!sk2)
                goto do_err;
 
        lock_sock(sk2);
 
        BUG_TRAP((1 sk_state) &
                 (TCPF_ESTABLISHED | TCPF_CLOSE_WAIT | TCPF_CLOSE));
 
        sock_graft(sk2, newsock);
 
        newsock->state = SS_CONNECTED;
        err = 0;
        release_sock(sk2); do_err:
        return err;
}

函数第一步工作是调用协议的 accept 函数,然后调用 sock_graft()函数,
接下来设置新的套接字的状态为 SS_CONNECTED.
/*
*        This will accept the next outstanding connection.
*/
struct sock *tcp_accept(struct sock *sk, int flags, int *err)
{
        struct tcp_sock *tp = tcp_sk(sk);
        struct open_request *req;
        struct sock *newsk;
        int error;
 
        lock_sock(sk);
 
        /* We need to make sure that this socket is listening,
         * and that it has something pending.
         */
        error = -EINVAL;
        if (sk->sk_state != TCP_LISTEN)
                goto out;
 
        /* Find already established connection */
        if (!tp->accept_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;
 
                error = wait_for_connect(sk, timeo);
                if (error)
                        goto out;
        }
 
        req = tp->accept_queue;
        if ((tp->accept_queue = req->dl_next) == NULL)
                tp->accept_queue_tail = NULL; 
        newsk = req->sk;
        sk_acceptq_removed(sk);
        tcp_openreq_fastfree(req);
        BUG_TRAP(newsk->sk_state != TCP_SYN_RECV);
        release_sock(sk);
        return newsk;
 
out:
        release_sock(sk);
        *err = error;
        return NULL;
}

tcp_accept()函数,当发现 tp->accept_queue 准备就绪后,就直接调用
        req = tp->accept_queue;
        if ((tp->accept_queue = req->dl_next) == NULL)
                tp->accept_queue_tail = NULL;
 
        newsk = req->sk;
出队,并取得相应的 sk。 否则,就在获取超时时间后,调用 wait_for_connect 等待连接的到来。这也是说,
强调“或等待”的原因所在了。
 
OK,继续回到 inet_accept 中来,当取得一个就绪的连接的 sk(sk2)后,先校验其状态,再调用sock_graft()函数。
 
在 sys_accept 中,已经调用了 sock_alloc,分配了一个新的 socket 结构(即 newsock),但 sock_alloc
必竟不是 sock_create,它并不能为 newsock 分配一个对应的 sk。所以这个套接字并不完整。
另一方面,当一个连接到达到,根据客户端的请求,产生了一个新的 sk(即 sk2,但这个分配过程
没有深入 tcp 栈去分析其实现,只分析了它对应的 req 入队的代码)。呵呵,将两者一关联,就 OK
了,这就是 sock_graft 的任务:
static inline void sock_graft(struct sock *sk, struct socket *parent)
{
        write_lock_bh(&sk->sk_callback_lock);
        sk->sk_sleep = &parent->wait;
        parent->sk = sk;
        sk->sk_socket = parent;
        write_unlock_bh(&sk->sk_callback_lock);
}
这样,一对一的联系就建立起来了。这个为 accept 分配的新的 socket 也大功告成了。接下来将其状
态切换为 SS_CONNECTED,表示已连接就绪,可以来读取数据了——如果有的话。
 
顺便提一下,新的 sk 的分配,是在:
tcp_v4_rcv
        ->tcp_v4_do_rcv
                     ->tcp_check_req
                              ->tp->af_specific->syn_recv_sock(sk, skb, req, NULL);
即 tcp_v4_syn_recv_sock函数,其又调用 tcp_create_openreq_child()来分配的。
 
struct sock *tcp_create_openreq_child(struct sock *sk, struct open_request *req, struct sk_buff *skb)
{
        /* allocate the newsk from the same slab of the master sock,
         * if not, at sk_free time we'll try to free it from the wrong
         * slabcache (i.e. is it TCPv4 or v6?), this is handled thru sk->sk_prot -acme */
        struct sock *newsk = sk_alloc(PF_INET, GFP_ATOMIC, sk->sk_prot, 0);
 
        if(newsk != NULL) {
                          ……
                         memcpy(newsk, sk, sizeof(struct tcp_sock));
                         newsk->sk_state = TCP_SYN_RECV;
                          ……
}
等到分析 tcp 栈的实现的时候,再来仔细分析它。但是这里新的 sk 的有限状态机被切换至了
TCP_SYN_RECV(按我的想法,似乎应进入 establshed 才对呀,是不是哪儿看漏了,只有看了后头的代码再来印证了)
 
回到 sys_accept 中来,如果调用者要求返回客户端的地址,则调用新的 sk 的getname 函数指针,
也就是 inet_getname:
/*
*        This does both peername and sockname.
*/
int inet_getname(struct socket *sock, struct sockaddr *uaddr,
                        int *uaddr_len, int peer)
{
        struct sock *sk                = sock->sk;
        struct inet_sock *inet        = inet_sk(sk);
        struct sockaddr_in *sin        = (struct sockaddr_in *)uaddr;
 
        sin->sin_family = AF_INET;
        if (peer) {
                if (!inet->dport ||
                    (((1 sk_state) & (TCPF_CLOSE | TCPF_SYN_SENT)) &&
                     peer == 1))
                        return -ENOTCONN;
                sin->sin_port = inet->dport;
                sin->sin_addr.s_addr = inet->daddr;
        } else {
                __u32 addr = inet->rcv_saddr;                 if (!addr)
                        addr = inet->saddr;
                sin->sin_port = inet->sport;
                sin->sin_addr.s_addr = addr;
        }
        memset(sin->sin_zero, 0, sizeof(sin->sin_zero));
        *uaddr_len = sizeof(*sin);
        return 0;
}

函数的工作是构建 struct sockaddr_in  结构出来,接着在 sys_accept中,调用 move_addr_to_user()
函数来拷贝至用户空间:
int move_addr_to_user(void *kaddr, int klen, void __user *uaddr, int __user *ulen)
{
        int err;
        int len;
 
        if((err=get_user(len, ulen)))
                return err;
        if(len>klen)
                len=klen;
        if(len MAX_SOCK_ADDR)
                return -EINVAL;
        if(len)
        {
                 if(copy_to_user(uaddr,kaddr,len))
                        return -EFAULT;
        }
        /*
         *        "fromlen shall refer to the value before truncation.."
         *                        1003.1g
         */
        return __put_user(klen, ulen);
}
也就是调用 copy_to_user的过程了。
 
sys_accept 的最后一步工作,是将新的 socket 结构,与文件系统挂钩:
        if ((err = sock_map_fd(newsock))                 goto out_release;
 
函数 sock_map_fd 在创建 socket 中已经见过了。
 
小结:
accept 有几件事情要做
1. 要 accept需要三次握手完成, 连接请求入tp->accept_queue队列(新为客户端分析的 sk, 也在其中), 其才能出队
2. 为 accept分配一个sokcet结构, 并将其与新的sk关联
3. 如果调用时,需要获取客户端地址,即第二个参数不为 NULL,则从新的 sk 中,取得其想的葫芦;
4. 将新的 socket 结构与文件系统挂钩;

相关实践学习
容器服务Serverless版ACK Serverless 快速入门:在线魔方应用部署和监控
通过本实验,您将了解到容器服务Serverless版ACK Serverless 的基本产品能力,即可以实现快速部署一个在线魔方应用,并借助阿里云容器服务成熟的产品生态,实现在线应用的企业级监控,提升应用稳定性。
云原生实践公开课
课程大纲 开篇:如何学习并实践云原生技术 基础篇: 5 步上手 Kubernetes 进阶篇:生产环境下的 K8s 实践 相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
目录
相关文章
|
3天前
|
机器学习/深度学习 人工智能 网络协议
TCP/IP五层(或四层)模型,IP和TCP到底在哪层?
TCP/IP五层(或四层)模型,IP和TCP到底在哪层?
32 4
|
3天前
|
网络协议 Java Linux
【探索Linux】P.29(网络编程套接字 —— 简单的TCP网络程序模拟实现)
【探索Linux】P.29(网络编程套接字 —— 简单的TCP网络程序模拟实现)
14 0
|
3天前
|
网络协议 算法 Linux
【探索Linux】P.27(网络编程套接字 —— UDP协议介绍 | TCP协议介绍 | UDP 和 TCP 的异同)
【探索Linux】P.27(网络编程套接字 —— UDP协议介绍 | TCP协议介绍 | UDP 和 TCP 的异同)
16 0
|
3天前
|
缓存 Linux
linux性能分析之内存分析(free,vmstat,top,ps,pmap等工具使用介绍)
这些工具可以帮助你监视系统的内存使用情况、识别内存泄漏、找到高内存消耗的进程等。根据具体的问题和需求,你可以选择使用其中一个或多个工具来进行内存性能分析。注意,内存分析通常需要综合考虑多个指标和工具的输出,以便更好地理解系统的行为并采取相应的优化措施。
32 6
|
3天前
|
监控 Linux 测试技术
性能分析之Linux系统平均负载案例分析
【4月更文挑战第20天】在上文性能基础之理解Linux系统平均负载和CPU使用率中,我们详细介绍了 Linux 系统平均负载的相关概念,本文我们来做几个案例分析,以达到加深理解。
42 2
性能分析之Linux系统平均负载案例分析
|
3天前
|
数据可视化 数据挖掘 Linux
如何在Linux部署DataEase数据分析服务并实现无公网IP远程分析内网数据信息
如何在Linux部署DataEase数据分析服务并实现无公网IP远程分析内网数据信息
|
3天前
|
网络协议 Ubuntu Unix
Linux 下使用 socket 实现 TCP 客户端
Linux 下使用 socket 实现 TCP 客户端
|
3天前
|
网络协议 Ubuntu Unix
Linux 下使用 socket 实现 TCP 服务端
Linux 下使用 socket 实现 TCP 服务端
|
3天前
|
网络协议 Linux
Linux内核源码剖析之TCP保活机制(KeepAlive)
总之,TCP保活机制通过定期发送保活探测报文,以检测空闲连接是否仍然活跃。这种机制在网络通信中有助于及时检测和关闭不再使用的连接,从而节省资源并提高连接的可靠性。
24 0
|
3天前
|
网络协议 Linux SDN
虚拟网络设备与Linux网络协议栈
在现代计算环境中,虚拟网络设备在实现灵活的网络配置和隔离方面发挥了至关重要的作用🔧,特别是在容器化和虚拟化技术广泛应用的今天🌐。而Linux网络协议栈则是操作系统处理网络通信的核心💻,它支持广泛的协议和网络服务🌍,确保数据正确地在网络中传输。本文将深入分析虚拟网络设备与Linux网络协议栈的关联,揭示它们如何共同工作以支持复杂的网络需求。