首先来看下内核如何处理3次握手的半连接队列和accept队列(其实也就是server端的三次握手的状态变换).而半连接队列和accept队列在内核如何表示,我们上次已经介绍过了,这里就不介绍了.
首先我们知道当3层的数据包到达之后会调用4层的协议handle,tcp的话就是tcp_v4_rcv.如何调用可以看我前面的
blog:
而在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的:
Java代码
-
///在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函数.
Java代码
-
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状态处理,后面的话也是遇到一个状态,我们看一个状态的处理:
Java代码
-
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的大小:
Java代码
-
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.
Java代码
-
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,并加入到半连接队列.
Java代码
-
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的实现,
Java代码
-
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.
Java代码
-
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掉一个元素.:
Java代码
-
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的值.
Java代码
-
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队列中.
Java代码
-
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的实现.
Java代码
-
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:
Java代码
-
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状态,它主要是为将要到来的数据传输做一些准备,设置一些相关域.:
Java代码
-
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;