tcp connection setup的实现(二)

xiaoxiao2022-06-14  36

首先来看下内核如何处理3次握手的半连接队列和accept队列(其实也就是server端的三次握手的状态变换).而半连接队列和accept队列在内核如何表示,我们上次已经介绍过了,这里就不介绍了. 首先我们知道当3层的数据包到达之后会调用4层的协议handle,tcp的话就是tcp_v4_rcv.如何调用可以看我前面的[url=http://simohayha.iteye.com/blog/442721]blog[/url]: 而在tcp_v4_rcv中,则最终会调用tcp_v4_do_rcv来处理输入数据包.在看tcp_v4_do_rcv之前,我们先来看在tcp_v4_rcv中,内核如何通过4元组(目的,源端口和地址)来查找对应得sock对象. 在分析之前,我们要知道,当一对tcp连接3次握手完毕后,内核将会重新new一个socket,这个socket中的大部分域都是与主socket相同的.而把这个新的socket的状态设置为established,而主socket的状态依旧为listen状态. 而通过前面的blog分析,我们也知道在inet_hashinfo中将处于listening状态的socket和处于TCP_ESTABLISHED与TCP_CLOSE之间的状态的socket是分开的,一个是ehash,一个是listening_hash.因此通过对应的4元组查找socket也是分开在这两个hash链表中操作的. 内核是通过调用__inet_lookup来查找socket的: ///在tcp_v4_rcv中的代码片段.sk = __inet_lookup(net, &tcp_hashinfo, iph->saddr, th->source, iph->daddr, th->dest, inet_iif(skb));static inline struct sock *__inet_lookup(struct net *net, struct inet_hashinfo *hashinfo, const __be32 saddr, const __be16 sport, const __be32 daddr, const __be16 dport, const int dif){ u16 hnum = ntohs(dport); struct sock *sk = __inet_lookup_established(net, hashinfo, saddr, sport, daddr, hnum, dif); return sk ? : __inet_lookup_listener(net, hashinfo, daddr, hnum, dif);} tcp_hashinfo我们前面也已经分析过了,包含了所有tcp所用到的hash信息,比如socket,port等等.这里的查找其实就是在tcp_hashinfo中(其实是它的域ehash或者listening_hash)查找相应的socket. 我们可以看到内核在这里进行了两次查找,首先是在established状态的socket中查找,处于established状态,说明3次握手已经完成,因此这个socket可以通过简单的4元组hash在hashinfo的ehash中查找. 而当在__inet_lookup_established中没有找到时,则将会__inet_lookup_listener中查找.也就是在处于listening状态的socket中查找(这里主要是通过daddr也就是目的地址来进行匹配). 当找到对应的socket以后就会进入数据包的处理,也就是进入tcp_v4_do_rcv函数. int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb){ struct sock *rsk;..................................................///如果为TCP_ESTABLISHED状态,则进入相关处理 if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */ TCP_CHECK_TIMER(sk); if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) { rsk = sk; goto reset; } TCP_CHECK_TIMER(sk); return 0; }///进行包头的合法性校验. if (skb->len < tcp_hdrlen(skb) || tcp_checksum_complete(skb)) goto csum_err;///进入TCP_LISTEN状态. if (sk->sk_state == TCP_LISTEN) { struct sock *nsk = tcp_v4_hnd_req(sk, skb); if (!nsk) goto discard; if (nsk != sk) { if (tcp_child_process(sk, nsk, skb)) { rsk = nsk; goto reset; } return 0; } } TCP_CHECK_TIMER(sk);///进入其他状态的处理.除了ESTABLISHED和TIME_WAIT状态. if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len)) { rsk = sk; goto reset; } TCP_CHECK_TIMER(sk); return 0;......................................................................} 可以看到当进来之后,会通过判断socket的不同状态来进入不同的处理.这里其实就分了3种状态,TCP_ESTABLISHED,TCP_LISTEN和剩余的的状态. 我们这里先不分析TCP_ESTABLISHED. 我们先来看当第一个syn分解到达后,内核会做怎么样处理.首先它会进入tcp_v4_hnd_req函数,这个函数我们后面会处理,这里只需要知道当为第一个syn分节时,它会返回当前socket.因此此时nsk == sk,所以我们进入tcp_rcv_state_process函数,这个函数处理除了ESTABLISHED和TIME_WAIT状态之外的所有状态. 我们这里只看他的listen状态处理,后面的话也是遇到一个状态,我们看一个状态的处理: int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb, struct tcphdr *th, unsigned len){ struct tcp_sock *tp = tcp_sk(sk);///取得对应的inet_connection_sock . struct inet_connection_sock *icsk = inet_csk(sk); int queued = 0; tp->rx_opt.saw_tstamp = 0; switch (sk->sk_state) { case TCP_LISTEN:///当为ack分节,则返回1,而对应内核会发送一个rst给对端. if (th->ack) return 1;///如果是rst,则忽略这个分组. if (th->rst) goto discard;///是syn分组,因此调用对应的虚函数conn_request,而这个函数在tcpv4中被初始化为tcp_v4_conn_request. if (th->syn) { if (icsk->icsk_af_ops->conn_request(sk, skb) < 0) return 1; kfree_skb(skb); return 0; } goto discard;............................................................} 可以看到最终会调用tcp_v4_conn_request来处理syn分组,我们接下来就来看这个函数的实现. 先来看几个相关的函数,第一个是reqsk_queue_is_full,他来判断半连接队列是否已满.其实实现很简单,就是判断qlen和max_qlen_log的大小: static inline int reqsk_queue_is_full(const struct request_sock_queue *queue){ return queue->listen_opt->qlen >> queue->listen_opt->max_qlen_log;} 第二个是sk_acceptq_is_full,它用来判断accept队列是否已满.这个也是很简单,比较当前的队列大小sk_ack_backlog与最大的队列大小sk_max_ack_backlog. static inline int sk_acceptq_is_full(struct sock *sk){ return sk->sk_ack_backlog > sk->sk_max_ack_backlog;} 最后一个是tcp_openreq_init,它用来新建一个inet_request_sock,我们知道每次一个syn到达后,我们都会新建一个inet_request_sock,并加入到半连接队列. static inline void tcp_openreq_init(struct request_sock *req, struct tcp_options_received *rx_opt, struct sk_buff *skb){ struct inet_request_sock *ireq = inet_rsk(req); req->rcv_wnd = 0; /* So that tcp_send_synack() knows! */ req->cookie_ts = 0; tcp_rsk(req)->rcv_isn = TCP_SKB_CB(skb)->seq; req->mss = rx_opt->mss_clamp; req->ts_recent = rx_opt->saw_tstamp ? rx_opt->rcv_tsval : 0; ireq->tstamp_ok = rx_opt->tstamp_ok; ireq->sack_ok = rx_opt->sack_ok; ireq->snd_wscale = rx_opt->snd_wscale; ireq->wscale_ok = rx_opt->wscale_ok; ireq->acked = 0; ireq->ecn_ok = 0; ireq->rmt_port = tcp_hdr(skb)->source;} 接下来来看tcp_v4_conn_request的实现, int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb){ struct inet_request_sock *ireq; struct tcp_options_received tmp_opt; struct request_sock *req; __be32 saddr = ip_hdr(skb)->saddr; __be32 daddr = ip_hdr(skb)->daddr;///这个名字实在是无语,when具体表示什么不太理解,只是知道它是用来计算rtt的. __u32 isn = TCP_SKB_CB(skb)->when; struct dst_entry *dst = NULL;#ifdef CONFIG_SYN_COOKIES int want_cookie = 0;#else#define want_cookie 0 /* Argh, why doesn't gcc optimize this :( */#endif///如果是广播或者多播,则丢掉这个包. if (skb->rtable->rt_flags & (RTCF_BROADCAST | RTCF_MULTICAST)) goto drop;///判断半连接队列是否已经满掉.如果满掉并且处于非timewait状态,则丢掉这个包(如果设置了SYN Cookie则会继续进行,因为SYN Cookie不需要新分配半连接队列,详细的SYN Cookie请google) if (inet_csk_reqsk_queue_is_full(sk) && !isn) {#ifdef CONFIG_SYN_COOKIES if (sysctl_tcp_syncookies) { want_cookie = 1; } else#endif goto drop; }///如果accept队列已满,并且qlen_young大于一就丢掉这个包,这里qlen_young大于一表示在syn队列中已经有足够多的(这里不包括重传的syn)请求了. if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) goto drop; req = inet_reqsk_alloc(&tcp_request_sock_ops); if (!req) goto drop;...................................................///对tmp_opt进行初始化,而tcp_options_received中包含了tcp的一些选项信息(比如mss,窗口扩大因子等等) tcp_clear_options(&tmp_opt); tmp_opt.mss_clamp = 536; tmp_opt.user_mss = tcp_sk(sk)->rx_opt.user_mss;///对对端的tcp_options_received进行解析,并对本端得tcp_options_received进行初始化. tcp_parse_options(skb, &tmp_opt, 0);.......................................................///这里对新的req进行初始化. tcp_openreq_init(req, &tmp_opt, skb);...............................................///这里将tcp_options_received保存到req中. ireq->opt = tcp_v4_save_options(sk, skb); if (!want_cookie) TCP_ECN_create_request(req, tcp_hdr(skb)); if (want_cookie) {#ifdef CONFIG_SYN_COOKIES syn_flood_warning(skb); req->cookie_ts = tmp_opt.tstamp_ok;#endif isn = cookie_v4_init_sequence(sk, skb, &req->mss); }else if (!isn) {.............................................///计算当前一个合适的isn,并返回. isn = tcp_v4_init_sequence(skb); }///赋值发送给对端的isn tcp_rsk(req)->snt_isn = isn;///发送syn和ack(如果设置了want_cookie则不会将这个req链接到半连接队列中. if (__tcp_v4_send_synack(sk, req, dst) || want_cookie) goto drop_and_free;///将这个req链接到半连接队列中. inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT); return 0;drop_and_release: dst_release(dst);drop_and_free: reqsk_free(req);drop: return 0;} 而tcp_v4_hnd_req的主要工作是在半连接队列中看是否存在当前的socket,如果存在则说明这个有可能是最终的ack包,因此将会做一系列的合法性校验(比如重传,rst,syn等等),最终确定这个是ack后会调用对应的新建socket的虚函数syn_recv_sock. static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb){ struct tcphdr *th = tcp_hdr(skb); const struct iphdr *iph = ip_hdr(skb); struct sock *nsk; struct request_sock **prev;///通过socket,查找对应request_sock struct request_sock *req = inet_csk_search_req(sk, &prev, th->source, iph->saddr, iph->daddr); if (req)///如果存在则进入req的相关处理. return tcp_check_req(sk, skb, req, prev);///不存在,则通过inet_lookup_established查找.这是因为有可能当我们进入这个函数之前,socket的状态被改变了,也就是这个socket的状态已经不是listen了. nsk = inet_lookup_established(sock_net(sk), &tcp_hashinfo, iph->saddr, th->source, iph->daddr, th->dest, inet_iif(skb)); if (nsk) { if (nsk->sk_state != TCP_TIME_WAIT) {///非tw状态返回新的socket. bh_lock_sock(nsk); return nsk; }///如果是timewait状态则返回空. inet_twsk_put(inet_twsk(nsk)); return NULL; }#ifdef CONFIG_SYN_COOKIES if (!th->rst && !th->syn && th->ack) sk = cookie_v4_check(sk, skb, &(IPCB(skb)->opt));#endif return sk;} tcp_check_req最主要工作就是调用虚函数,新建一个socket,并返回. 先来看几个相关的函数,第一个是inet_csk_reqsk_queue_unlink,它主要用来从半连接队列unlink掉一个元素.: static inline void inet_csk_reqsk_queue_unlink(struct sock *sk, struct request_sock *req, struct request_sock **prev){ reqsk_queue_unlink(&inet_csk(sk)->icsk_accept_queue, req, prev);}static inline void reqsk_queue_unlink(struct request_sock_queue *queue, struct request_sock *req, struct request_sock **prev_req){ write_lock(&queue->syn_wait_lock);///处理链表. *prev_req = req->dl_next; write_unlock(&queue->syn_wait_lock);} 第二个是inet_csk_reqsk_queue_removed,它主要用来修改对应的qlen和qlen_young的值. static inline void inet_csk_reqsk_queue_removed(struct sock *sk, struct request_sock *req){ if (reqsk_queue_removed(&inet_csk(sk)->icsk_accept_queue, req) == 0) inet_csk_delete_keepalive_timer(sk);}static inline int reqsk_queue_removed(struct request_sock_queue *queue, struct request_sock *req){ struct listen_sock *lopt = queue->listen_opt;///如果重传数为0则说明没有重传过,因此qlen_young跟着也减一. if (req->retrans == 0) --lopt->qlen_young; return --lopt->qlen;} 最后是inet_csk_reqsk_queue_add,它用来把新的req加入到accept队列中. static inline void inet_csk_reqsk_queue_add(struct sock *sk, struct request_sock *req, struct sock *child){ reqsk_queue_add(&inet_csk(sk)->icsk_accept_queue, req, sk, child);}static inline void reqsk_queue_add(struct request_sock_queue *queue, struct request_sock *req, struct sock *parent, struct sock *child){ req->sk = child; sk_acceptq_added(parent);///可以看到刚好就是request_sock_queue的rskq_accept_head与rskq_accept_tail保存accept队列. if (queue->rskq_accept_head == NULL) queue->rskq_accept_head = req; else queue->rskq_accept_tail->dl_next = req; queue->rskq_accept_tail = req; req->dl_next = NULL;} 然后再来看tcp_check_req的实现. struct sock *tcp_check_req(struct sock *sk,struct sk_buff *skb, struct request_sock *req, struct request_sock **prev){ const struct tcphdr *th = tcp_hdr(skb); __be32 flg = tcp_flag_word(th) & (TCP_FLAG_RST|TCP_FLAG_SYN|TCP_FLAG_ACK); int paws_reject = 0; struct tcp_options_received tmp_opt; struct sock *child; tmp_opt.saw_tstamp = 0;......................................///如果只有rst和syn域则发送一个rst给对端.if (flg & (TCP_FLAG_RST|TCP_FLAG_SYN)) { TCP_INC_STATS_BH(sock_net(sk), TCP_MIB_ATTEMPTFAILS); goto embryonic_reset; }///如果是重传的syn,则重新发送syn和ack分组. if (TCP_SKB_CB(skb)->seq == tcp_rsk(req)->rcv_isn && flg == TCP_FLAG_SYN && !paws_reject) { req->rsk_ops->rtx_syn_ack(sk, req); return NULL; } ..........................................///确定有设置ack分节. if (!(flg & TCP_FLAG_ACK)) return NULL;///这里主要处理TCP_DEFER_ACCEPT被设置的情况,如果它被设置,则丢掉这个包.(这是因为TCP_DEFER_ACCEPT会等待数据真正发过来才处理的,而不是最后一个ack发过来就处理) if (inet_csk(sk)->icsk_accept_queue.rskq_defer_accept && TCP_SKB_CB(skb)->end_seq == tcp_rsk(req)->rcv_isn + 1) { inet_rsk(req)->acked = 1; return NULL; }///可以创建一个新的socket了.返回一个包含新创建的socket的request结构. child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL); if (child == NULL) goto listen_overflow;..................................#endif///创建成功,则在request_sock_queue的listen_opt中unlink掉这个req.也就是从半连接队列中删除这个req. inet_csk_reqsk_queue_unlink(sk, req, prev);///修改对应的 qlen和qlen_young的值. inet_csk_reqsk_queue_removed(sk, req);///最后加入到accept队列中.这里注意最终是将新的socket赋值给对应的req. inet_csk_reqsk_queue_add(sk, req, child); return child;listen_overflow: if (!sysctl_tcp_abort_on_overflow) { inet_rsk(req)->acked = 1; return NULL; }embryonic_reset: NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_EMBRYONICRSTS); if (!(flg & TCP_FLAG_RST)) req->rsk_ops->send_reset(sk, skb); inet_csk_reqsk_queue_drop(sk, req, prev); return NULL;} 最后我们来看内核如何创建一个新的socket,tcp 协议使用tcp_v4_syn_recv_sock来实现,它做的其实很简单就是新建一个socket,并且设置状态为TCP_SYN_RECV(在inet_csk_clone中),父socket继续处于listen状态,然后对新的socket进行一些赋值,然后对一些定时器进行初始化.这里定时器我们全部都略过了,以后会专门来分析tcp中的定时器. 最后从tcp_v4_hnd_req中返回,判断是否与父socket相等,然后调用tcp_child_process函数: 这个函数主要是完成最终的三次握手,将子socket设置为TCP_ESTABLISHED然后根据条件唤醒被accept阻塞的主socket: int tcp_child_process(struct sock *parent, struct sock *child, struct sk_buff *skb){ int ret = 0; int state = child->sk_state; if (!sock_owned_by_user(child)) {///完成最终的三次握手. ret = tcp_rcv_state_process(child, skb, tcp_hdr(skb), skb->len); /* Wakeup parent, send SIGIO */ if (state == TCP_SYN_RECV && child->sk_state != state)///唤醒阻塞的主socket. parent->sk_data_ready(parent, 0); } else { /* Alas, it is possible again, because we do lookup * in main socket hash table and lock on listening * socket does not protect us more. */ sk_add_backlog(child, skb); } bh_unlock_sock(child); sock_put(child); return ret;} 最后来分析下在tcp_rcv_state_process中的处理当前的TCP_SYN_RECV状态,它主要是为将要到来的数据传输做一些准备,设置一些相关域.: case TCP_SYN_RECV: if (acceptable) { tp->copied_seq = tp->rcv_nxt; smp_mb();///设置状态为TCP_ESTABLISHED. tcp_set_state(sk, TCP_ESTABLISHED); sk->sk_state_change(sk);///这里的wake应该是针对epoll这类的 if (sk->sk_socket) sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);///设置期望接收的isn号,也就是第一个字节的序列和窗口大小. tp->snd_una = TCP_SKB_CB(skb)->ack_seq; tp->snd_wnd = ntohs(th->window) << tp->rx_opt.snd_wscale; tcp_init_wl(tp, TCP_SKB_CB(skb)->ack_seq, TCP_SKB_CB(skb)->seq);......................................................................... break;
转载请注明原文地址: https://www.6miu.com/read-4936691.html

最新回复(0)