版本:linux 4.18.1
作为学习笔记,只讨论常规的三次握手过程。
服务端调用 listen 函数后一直处于 TCP_LISTEN 状态,等待客户端的连接请求;
上一节我们分析了第一握手,客户端发送了 SYN 包,现在我们来看下服务端接收 SYN 包后的处理过程。
// net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
struct sock *sk;
// 从 tcp_hashinfo 中的各种 hashtable 中尝试找到对应的 socket
// th->source 是发送方的本地端口
// th->dest 是接收方的本地端口
sk = __inet_lookup_skb(&tcp_hashinfo, ...);
// 找到的 sock 此时处于 TCP_LISTEN 状态
if (sk->sk_state == TCP_LISTEN) {
ret = tcp_v4_do_rcv(sk, skb);
goto put_and_return;
}
}
内核收到任何 TCP 消息,都会进入该函数。首先在全局的 tcp_hashinfo 中查找 sock,然后进行相应的处理。
看下查找过程:
// /net/ipv4/inet_hashtables.c
// __inet_lookup_skb --> __inet_lookup
static inline struct sock *__inet_lookup(...)
{
// 先在 ehash 中查找,刚接收 SYN 包,这里找不到
sk = __inet_lookup_established(net, hashinfo, saddr, sport,
daddr, hnum, dif, sdif);
if (sk)
return sk;
// 继续在 listening_hash || lhash2 中查找
return __inet_lookup_listener(net, hashinfo, skb, doff, saddr,
sport, daddr, hnum, dif, sdif);
}
// 在 listening_hash || lhash2 中查找 sock
// 服务端在调用 listen 的时候已经将 sock 加入到以上两个 hashtable
struct sock *__inet_lookup_listener(...)
{
// 根据端口号计算 hash 值
unsigned int hash = inet_lhashfn(net, hnum);
// 根据 hash 值得到 listening_hash 中的 slot
struct inet_listen_hashbucket *ilb = &hashinfo->listening_hash[hash];
// 如果 slot 指向链表包含的节点小于 10 或者不存在 lhash2,就只能通过 listening_hash 查找
if (ilb->count <= 10 || !hashinfo->lhash2)
goto port_lookup;
/* Too many sk in the ilb bucket (which is hashed by port alone).
* Try lhash2 (which is hashed by port and addr) instead.
*/
// 如果 listening_hash 中对应 slot 的节点太多, 尝试用 lhash2 查找加速查找过程
// 通过 本地端口 + 本地地址 计算 hash 值
hash2 = ipv4_portaddr_hash(net, daddr, hnum);
// 根据 hash 值得到 lhash2 中的 slot
struct inet_listen_hashbucket ilb2 = inet_lhash2_bucket(hashinfo, hash2);
if (ilb2->count > ilb->count)
goto port_lookup;
// 在该 slot 中查找 sock 信息
result = inet_lhash2_lookup(...);
if (result)
return result;
...
port_lookup:
sk_for_each_rcu(sk, &ilb->head) {
...
// 在 listening_hash 中查找 sock
}
return result;
}
服务端在某个 inet_listen_hashbucket 中找到与请求包目的端口对应的 sock,它处于 TCP_LISTEN 状态。
此后,服务端创建一个 request_sock 对象来表示这个半连接,并添加到ehash 中管理;然后构建 SYN+ACK 响应包,向客户端发送 第二次握手。
// 调用栈
tcp_v4_do_rcv -- > tcp_rcv_state_process
// net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
case TCP_LISTEN:
icsk->icsk_af_ops->conn_request(sk, skb) // tcp_v4_conn_request
...
}
// tcp_v4_conn_request --> tcp_conn_request
// net/ipv4/tcp_input.c
int tcp_conn_request(...)
{
...
// 分配一个 request_sock 对象来表示这个半连接 (状态: TCP_NEW_SYN_RECV)
req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie);
// 生成初始序列号
af_ops->init_seq(req, sk, skb); // tcp_v4_init_sequence()
// 加入 ehash,启动请求重传定时器
inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);
// 发送 SYN + ACK
af_ops->send_synack(...)
}
// net/ipv4/inet_connection_sock.c
void inet_csk_reqsk_queue_hash_add(...)
{
reqsk_queue_hash_req(req, timeout); // --> inet_ehash_insert
inet_csk_reqsk_queue_added(sk);
}
// 记录半连接数
static inline void inet_csk_reqsk_queue_added(struct sock *sk)
{
reqsk_queue_added(&inet_csk(sk)->icsk_accept_queue);
}
服务端发送第二次握手后,内核中保存的 tcp 相关信息如下所示: