码迷,mamicode.com
首页 > 其他好文 > 详细

内核listen的backlog和简单的三次握手分析

时间:2016-05-06 15:36:33      阅读:357      评论:0      收藏:0      [点我收藏+]

标签:

1. 背景:

        前面通过抓包分析了listen backlog对全连接和半连接的影响,本文将从内核源码(kernel 2.6.32)上简单了解下服务端三次握手的过程以及backlog在中间所起的作用。

2. 三次握手:

2.1 服务端监听:

        在system_call后,通过fd号获取相应的socket,及对backlog最值进行限制后,然后进入inet_listen函数进行处理。

int inet_listen(struct socket *sock, int backlog)
{
	struct sock *sk = sock->sk;
	unsigned char old_state;
	int err;

	lock_sock(sk);

	err = -EINVAL;
	if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
		goto out;

	old_state = sk->sk_state;
	if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
		goto out;

	/* Really, if the socket is already in listen state
	 * we can only allow the backlog to be adjusted.
	 */
	if (old_state != TCP_LISTEN) {
		err = inet_csk_listen_start(sk, backlog);
		if (err)
			goto out;
	}
	sk->sk_max_ack_backlog = backlog;
	err = 0;

out:
	release_sock(sk);
	return err;
}
        这函数进行简单的状态校验,然后进入inet_csk_listen_start初始化监听的套接字,最后注意,sk->sk_max_ack_backlog修改为backlog大小,这个变量后续将作用于sk_acceptq_is_full函数,用于判断半连接队列,因此backlog对于半连接是有作用的。

static inline int sk_acceptq_is_full(struct sock *sk)
{
	return sk->sk_ack_backlog > sk->sk_max_ack_backlog;
}
int inet_csk_listen_start(struct sock *sk, const int nr_table_entries)
{
	struct inet_sock *inet = inet_sk(sk);
	struct inet_connection_sock *icsk = inet_csk(sk);
	int rc = reqsk_queue_alloc(&icsk->icsk_accept_queue, nr_table_entries);

	if (rc != 0)
		return rc;

	/* 初始化backlog为0,回到上层后赋值,见前文。  */
	sk->sk_max_ack_backlog = 0;
	sk->sk_ack_backlog = 0;
	inet_csk_delack_init(sk);

	/* There is race window here: we announce ourselves listening,
	 * but this transition is still not validated by get_port().
	 * It is OK, because this socket enters to hash table only
	 * after validation is complete.
	 */
	/* 检查端口是否被占用。  */
	sk->sk_state = TCP_LISTEN;
	if (!sk->sk_prot->get_port(sk, inet->num)) {
		inet->sport = htons(inet->num);

		sk_dst_reset(sk);
		sk->sk_prot->hash(sk);

		return 0;
	}

	sk->sk_state = TCP_CLOSE;
	__reqsk_queue_destroy(&icsk->icsk_accept_queue);
	return -EADDRINUSE;
}
        初始化主要初始化了backlog,在上层会被重新赋值为backlog,还检查监听的端口是否被占用。此外还要注意函数reqsk_queue_alloc函数,真正初始化了一个listen_sock。listen_sock数据结果见下面代码,主要开辟了一个哈希桶记录半连接的状态。
int reqsk_queue_alloc(struct request_sock_queue *queue, unsigned int nr_table_entries)
{
    size_t lopt_size = sizeof(struct listen_sock);
    /** struct listen_sock - listen state
     *
     * @max_qlen_log - log_2 of maximal queued SYNs/REQUESTs
     */
    /* 
     struct listen_sock {
     /* 哈希大小。  */
     u8 max_qlen_log;
     /* 3 bytes hole, try to use */
     /* 用来记录半连接数量和未超时的半连接数量。  */
     int qlen;
     int qlen_young;
     /* 将用于半连接超时重传。  */
     int clock_hand;
     /* 用于计算哈希值。  */
     u32 hash_rnd;
     u32 nr_table_entries;
     /* 柔性数组。  */
     struct request_sock *syn_table[0];
     };
     listen_sock是一个柔性数组,是一个用来存放半连接的哈希表,
     哈希表大小为backlog向上取2的幂次。
    */
    struct listen_sock *lopt;


    /* 柔性数组大小计算,在[8, sysctl_max_syn_backlog]之间的backlog向上取2的幂。  */
    nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
    nr_table_entries = max_t(u32, nr_table_entries, 8);
    nr_table_entries = roundup_pow_of_two(nr_table_entries + 1);
    lopt_size += nr_table_entries * sizeof(struct request_sock *);
    if (lopt_size > PAGE_SIZE)
        lopt = __vmalloc(lopt_size, GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO, PAGE_KERNEL);
    else
        lopt = kzalloc(lopt_size, GFP_KERNEL);
    if (lopt == NULL)
        return -ENOMEM;


    /* 哈希至少8。  */
    for (lopt->max_qlen_log = 3; (1 << lopt->max_qlen_log) < nr_table_entries; lopt->max_qlen_log++);


    get_random_bytes(&lopt->hash_rnd, sizeof(lopt->hash_rnd));
    rwlock_init(&queue->syn_wait_lock);
    queue->rskq_accept_head = NULL;
    lopt->nr_table_entries = nr_table_entries;


    write_lock_bh(&queue->syn_wait_lock);
    queue->listen_opt = lopt;
    write_unlock_bh(&queue->syn_wait_lock);

    return 0;
}

2.2 第一次握手的syn到达:

        当有报文到达,ipv4的tcp处理入口为tcp_v4_do_rcv,下面代码中去掉了一些校验的处理和已建立连接的处理,只留下监听的主要流程。第一次握手时候收到的是syn包,因此在tcp_v4_hnd_req之后走到tcp_rcv_state_process处理当前收到的包。

int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
	struct sock *rsk;

	if (sk->sk_state == TCP_LISTEN) {
		struct sock *nsk = tcp_v4_hnd_req(sk, skb);
		/* 错误,丢弃包。  */
		if (!nsk)
			goto discard;

		/* 收到ack.  */
		if (nsk != sk) {
			if (tcp_child_process(sk, nsk, skb)) {
				rsk = nsk;
				goto reset;
			}
			return 0;
		}
	}

	/* 处理报文。  */
	TCP_CHECK_TIMER(sk);
	if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len)) {
		rsk = sk;
		goto reset;
	}
	TCP_CHECK_TIMER(sk);
	return 0;

reset:
	tcp_v4_send_reset(rsk, skb);
discard:
	kfree_skb(skb);
	/* Be careful here. If this function gets more complicated and
	 * gcc suffers from register pressure on the x86, sk (in %ebx)
	 * might be destroyed here. This current version compiles correctly,
	 * but you have been warned.
	 */
	return 0;
}
        其中比较重要的函数是tcp_v4_hnd_req和tcp_rcv_state_process,先看一下tcp_v4_hnd_req。这函数返回入参sk说明当前是收到syn状态,直接进行包处理;当返回值不是sk且非空则表示当前处于最后一个ack,会创建一个新的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;
	/* 半连接中查找,找到的会进行检查,如果半连接中没有,查找已完成的连接,都不是则进入syn接收。  */
	struct request_sock *req = inet_csk_search_req(sk, &prev, th->source,
						       iph->saddr, iph->daddr);
	if (req)
		return tcp_check_req(sk, skb, req, prev);

	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) {
			bh_lock_sock(nsk);
			return nsk;
		}
		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会对syn_recv进行检查,当前暂不介绍,如果半连接都没有建立,则两个表中都查找失败,直接返回sk。

        接下来在处理包的函数tcp_rcv_state_process中的第一个状态机,进入监听状态且收到syn包的处理函数tcp_v4_conn_request。

        简化一部分代码,保留主要流程如下:

        1. 检查半连接队列是否已满,满的大小为backlog修正值向上取2的幂次,满的话会丢弃这个syn包。

        2. 检查完成队列是否空,且新的请求数是不是大于1,注意,这里新的请求指的是刚刚发来syn,还未重传或者确认的请求,新的请求在tcp_check_req接收了ack进入连接状态以后会重置qlen_young,或者在第一次重传的时候重置,个人理解是把服务端的压力分摊到客户端上,让重传发生在客户端,减少服务端的压力。

        3. 可以创建半连接,则会初始化一个request_sock,并发送syn + ack,同时将这个req添加到监听sock的syn_table中,下一次tcp_v4_hnd_req的时候会在半连接表中找到该链接。

        4. 添加进哈希表以后会激活对应socket的keepalive事件,后面会分析。

int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
	...

	/* TW buckets are converted to open requests without
	 * limitations, they conserve resources and peer is
	 * evidently real one.
	 */
	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 backlog is full. If we have already queued enough
	 * of warm entries in syn queue, drop request. It is better than
	 * clogging syn queue with openreqs with exponentially increasing
	 * timeout.
	 */
	/* 当接收队列已满,且新的未重试请求数量大于1,服务端会暂时丢弃这个报文,
	 * 个人理解是为了降低服务端的压力,将重传放到客户端(重传syn),
	 * 而不是服务端(syn + ack)。
	 */
	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;

#ifdef CONFIG_TCP_MD5SIG
	tcp_rsk(req)->af_specific = &tcp_request_sock_ipv4_ops;
#endif

	tcp_clear_options(&tmp_opt);
	tmp_opt.mss_clamp = 536;
	tmp_opt.user_mss  = tcp_sk(sk)->rx_opt.user_mss;

	tcp_parse_options(skb, &tmp_opt, 0);

	if (want_cookie && !tmp_opt.saw_tstamp)
		tcp_clear_options(&tmp_opt);

	tmp_opt.tstamp_ok = tmp_opt.saw_tstamp;

	tcp_openreq_init(req, &tmp_opt, skb);

	ireq = inet_rsk(req);
	ireq->loc_addr = daddr;
	ireq->rmt_addr = saddr;
	ireq->no_srccheck = inet_sk(sk)->transparent;
	ireq->opt = tcp_v4_save_options(sk, skb);

	if (security_inet_conn_request(sk, skb, req))
		goto drop_and_free;

	if (!want_cookie)
		TCP_ECN_create_request(req, tcp_hdr(skb));

	...

	/* 发送syn + ack。  */
	if (__tcp_v4_send_synack(sk, req, dst) || want_cookie)
		goto drop_and_free;

	/* 添加req到半连接的syn_table中,同时启动一个超时检测的定时器。  */
	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;
}

        第一次握手主要有一个细节,就是上面提到的,队列已满时候,会适当丢弃syn。

2.3 第三次握手:

        第三次握手时候服务端收到客户端反馈的ack,这时候依旧是在tcp_v4_do_rcv中处理这个包,但是不同的是在tcp_v4_hnd_req的时候,会在syn_table中查找到req,这时候会进入tcp_check_req检查这个半连接。详细检查可以见参考文档[2],当检查完成后,会创建一个子sock用来处理连接,主要处理在tcp_v4_syn_recv_sock函数中完成。当子sock创建失败,如接受队列已满,则会根据系统的sysctl_tcp_abort_on_overflow标志判断是否需要向对端发送RST,或者是简单丢弃该包,等待后续的重传。

        如果子sock建立成功了,则会从哈希桶中移除,且减少相应的半连接计数,移除相应的定时器。

	child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL);
	if (child == NULL)
		goto listen_overflow;

	inet_csk_reqsk_queue_unlink(sk, req, prev);
	inet_csk_reqsk_queue_removed(sk, 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;
}
2.4 服务端的超时重传:

        定时器总入口为tcp_keepalive_timer,对应的监听状态的入口为inet_csk_reqsk_queue_prune。

void inet_csk_reqsk_queue_prune(struct sock *parent,
				const unsigned long interval,
				const unsigned long timeout,
				const unsigned long max_rto)
{
	struct inet_connection_sock *icsk = inet_csk(parent);
	struct request_sock_queue *queue = &icsk->icsk_accept_queue;
	struct listen_sock *lopt = queue->listen_opt;
	int max_retries = icsk->icsk_syn_retries ? : sysctl_tcp_synack_retries;
	int thresh = max_retries;
	unsigned long now = jiffies;
	struct request_sock **reqp, *req;
	int i, budget;

	if (lopt == NULL || lopt->qlen == 0)
		return;

	/* Normally all the openreqs are young and become mature
	 * (i.e. converted to established socket) for first timeout.
	 * If synack was not acknowledged for 1 second, it means
	 * one of the following things: synack was lost, ack was lost,
	 * rtt is high or nobody planned to ack (i.e. synflood).
	 * When server is a bit loaded, queue is populated with old
	 * open requests, reducing effective size of queue.
	 * When server is well loaded, queue size reduces to zero
	 * after several minutes of work. It is not synflood,
	 * it is normal operation. The solution is pruning
	 * too old entries overriding normal timeout, when
	 * situation becomes dangerous.
	 *
	 * Essentially, we reserve half of room for young
	 * embrions; and abort old ones without pity, if old
	 * ones are about to clog our table.
	 */
	/* 当半连接数量增大,但是young增大的速度比较平缓,这时候thresh会逐渐变小,
	 * 半连接就更容易过期。
	 */
	if (lopt->qlen>>(lopt->max_qlen_log-1)) {
		int young = (lopt->qlen_young<<1);

		while (thresh > 2) {
			if (lopt->qlen < young)
				break;
			thresh--;
			young <<= 1;
		}
	}

	if (queue->rskq_defer_accept)
		max_retries = queue->rskq_defer_accept;

	budget = 2 * (lopt->nr_table_entries / (timeout / interval));
	i = lopt->clock_hand;

	/* 遍历桶。  */
	do {
		reqp=&lopt->syn_table[i];
		while ((req = *reqp) != NULL) {
			/* 已经达到超时时间。  */
			if (time_after_eq(now, req->expires)) {
				int expire = 0, resend = 0;

				/* 超时和过期的计算。  */
				syn_ack_recalc(req, thresh, max_retries,
					       queue->rskq_defer_accept,
					       &expire, &resend);
				req->rsk_ops->syn_ack_timeout(parent, req);
				if (!expire &&
				    (!resend ||
				     !inet_rtx_syn_ack(parent, req) ||
				     inet_rsk(req)->acked)) {
					/* 超时重传。  */
					unsigned long timeo;

					/* 首次超时则这个请求不再是young。  */
					if (req->num_timeout++ == 0)
						lopt->qlen_young--;
					/* 超时时间指数增加。  */
					timeo = min(timeout << req->num_timeout,
						    max_rto);
					req->expires = now + timeo;
					reqp = &req->dl_next;
					continue;
				}

				/* Drop this request */
				/* 过期。  */
				inet_csk_reqsk_queue_unlink(parent, req, reqp);
				reqsk_queue_removed(queue, req);
				reqsk_free(req);
				continue;
			}
			reqp = &req->dl_next;
		}

		i = (i + 1) & (lopt->nr_table_entries - 1);

	} while (--budget > 0);

	lopt->clock_hand = i;

	/* 刷新定时事件。  */
	if (lopt->qlen)
		inet_csk_reset_keepalive_timer(parent, interval);
}

3. 参考文献:

[1]. 函数调用关系: http://dedecms.com/knowledge/servers/linux-bsd/2012/1217/17745_3.html

[2]. http://blog.csdn.net/zhangskd/article/details/17923917

[3]. 定时器:http://blog.csdn.net/zhangskd/article/details/35281345

内核listen的backlog和简单的三次握手分析

标签:

原文地址:http://blog.csdn.net/mercy_pm/article/details/51303017

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!