接上一篇文章 Linux系统研究 - 操作系统是如何管理tcp连接的 (1),我们再来继续讲。
客户端第一步要做的事就是连接服务器,所以我们看下对应的connect方法:
// net/ipv4/inet_hashtables.c
int __inet_hash_connect(struct inet_timewait_death_row *death_row,
struct sock *sk, u32 port_offset,
int (*check_established)(struct inet_timewait_death_row *,
struct sock *, __u16, struct inet_timewait_sock **))
{
// 该方法的调用栈
// SYSCALL_DEFINE3(connect)
// __sys_connect
// inet_stream_connect
// __inet_stream_connect
// tcp_v4_connect
// inet_hash_connect
// __inet_hash_connect
// hinfo是全局的tcp_hashinfo实例
struct inet_hashinfo *hinfo = death_row->hashinfo;
// 一般来说,connect操作我们都不会主动指定本地端口
// 而是让操作系统帮我们自由挑选
// 下面的方法就是用于获取操作系统自由挑选的本地端口的范围
// 该范围默认是 [32768-60999]
// 当前范围可由以下命令查看:
// $ cat /proc/sys/net/ipv4/ip_local_port_range
inet_get_local_port_range(net, &low, &high);
// 依次检测范围内的端口,找到第一个可以使用的
// 第一个要检测的端口
port = low + offset;
for (i = 0; i < remaining; i += 2, port += 2) {
// 找到该端口对应的bhash中的slot
head = &hinfo->bhash[inet_bhashfn(net, port, hinfo->bhash_size)];
// 遍历该slot指向的链表,查看是否有人已经在使用该端口
inet_bind_bucket_for_each(tb, &head->chain) {
if (net_eq(ib_net(tb), net) && tb->l3mdev == l3mdev && tb->port == port) {
// 如果该端口已经被人使用
// 那就检查一下使用者中是否有处于连接状态的socket
// 且该socket的tcp四元组和我们的socket的tcp四元组完全一致(tcp四元组唯一确定一个tcp连接)
// 如果有,则该端口不可用
// 如果没有,则可用
if (!check_established(death_row, sk,
port, &tw))
goto ok;
goto next_port;
}
}
// 如果该端口没人用,我们就在bhash中新创建一个对象,表示我们要用
tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep,
net, head, port, l3mdev);
goto ok;
next_port:
}
ok:
// 该方法上面有讲过,主要就是将该socket与tb实例联系起来
// 详情可参考上面
inet_bind_hash(sk, tb, port);
if (sk_unhashed(sk)) {
// 该方法下面会详细看
inet_ehash_nolisten(sk, (struct sock *)tw);
}
}
再来看下上面提到的inet_ehash_nolisten方法:
bool inet_ehash_insert(struct sock *sk, struct sock *osk)
{
// hashinfo就是全局的tcp_hashinfo实例
struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo;
// 根据本地和远程的地址端口信息算出该socket的hash值
// 并保存到sk的sk_hash字段里,供后续使用
sk->sk_hash = sk_ehashfn(sk);
// 根据该hash值找到ehash中对应的slot
head = inet_ehash_bucket(hashinfo, sk->sk_hash);
// 把该socket加入到该slot指向的链表中
if (ret)
__sk_nulls_add_node_rcu(sk, list);
}
bool inet_ehash_nolisten(struct sock *sk, struct sock *osk)
{
bool ok = inet_ehash_insert(sk, osk);
}
由上可见,tcp_hashinfo在connect操作里的作用是,先根据bhash和ehash里的信息,为该次connect操作挑选出一个合适的本地端口(该端口的使用也会被记录在bhash里),然后在syn消息发送给服务器之前,将该socket放入到ehash中,这样当内核收到服务器的应答消息时,就可以找到对应的socket了。
connect操作最终会发syn消息给服务器,所以下面我们就来看下服务器在收到这个syn消息时是如何处理的。
在此之前,我们先讲一些铺垫性的内容。
当操作系统收到任意tcp的消息时,都会调用下面的方法,找到该tcp消息所属的socket,然后再根据该socket的当前状态和tcp消息的内容做后续处理:
// 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, skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
}
再看下__inet_lookup_skb方法:
// include/net/inet_hashtables.h
static inline struct sock *__inet_lookup_skb(struct inet_hashinfo *hashinfo,
struct sk_buff *skb,
int doff,
const __be16 sport,
const __be16 dport,
const int sdif,
bool *refcounted)
{
const struct iphdr *iph = ip_hdr(skb);
return __inet_lookup(dev_net(skb_dst(skb)->dev), hashinfo, skb,
doff, iph->saddr, sport,
iph->daddr, dport, inet_iif(skb), sdif,
refcounted);
}
该方法又调用了__inet_lookup方法:
static inline struct sock *__inet_lookup(struct net *net,
struct inet_hashinfo *hashinfo,
struct sk_buff *skb, int doff,
const __be32 saddr, const __be16 sport,
const __be32 daddr, const __be16 dport,
const int dif, const int sdif,
bool *refcounted)
{
u16 hnum = ntohs(dport);
struct sock *sk;
// 该方法会根据本地和远程的地址端口信息
// 从tcp_hashinfo的ehash中找对应的socket
sk = __inet_lookup_established(net, hashinfo, saddr, sport,
daddr, hnum, dif, sdif);
// 如果在ehash中没有找到对应的socket,则调用下面的方法
// 从tcp_hashinfo的lhash2中找对应的处于listen状态的socket
return __inet_lookup_listener(net, hashinfo, skb, doff, saddr,
sport, daddr, hnum, dif, sdif);
}
好,铺垫性内容结束。
当服务端收到客户端发来的syn包后,会先通过上述方法,在lhash2中找到对应的listen状态的socket(listen方法把这个socket放入到lhash2中的),然后执行下面的逻辑:
// net/ipv4/tcp_input.c
int tcp_conn_request(struct request_sock_ops *rsk_ops,
const struct tcp_request_sock_ops *af_ops,
struct sock *sk, struct sk_buff *skb)
{
// 该方法的调用栈
// tcp_v4_rcv
// tcp_v4_do_rcv
// tcp_rcv_state_process
// tcp_v4_conn_request
// 该方法的参数sk就是上面找到的处于listen状态的socket
// 服务端在收到syn消息后,并不是直接创建一个struct sock
// 而是创建一个struct request_sock,表示该socket还处于tcp三次握手过程中
struct request_sock *req;
req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie);
if (fastopen_sk) {
} else {
if (!want_cookie)
// 该方法会根据本地和远程的地址端口信息
// 将request_sock放到tcp_hashinfo里的ehash里
// 这样后续的消息就可以找到这个request_sock了
inet_csk_reqsk_queue_hash_add(sk, req,
tcp_timeout_init((struct sock *)req));
}
}
服务端在处理完syn消息后,会发synack给客户端,客户端收到synack消息后,会再次发ack给服务端,同时将客户端的socket状态设置为TCP_ESTABLISHED。
由于客户端处理synack消息的逻辑不涉及到tcp_hashinfo里的内容,所以这里就不详细说了。
再看下服务端在收到ack消息之后的逻辑。
服务端在收到ack消息后,会先通过上面介绍过的__inet_lookup_skb方法,找到刚刚创建的request_sock,然后执行如下逻辑:
// net/ipv4/tcp_ipv4.c
struct sock *tcp_v4_syn_recv_sock(const struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
struct dst_entry *dst,
struct request_sock *req_unhash,
bool *own_req)
{
// 该方法的调用栈
// tcp_v4_rcv
// tcp_check_req
// tcp_v4_syn_recv_sock
// 收到客户端的ack消息表示该tcp连接建立成功
// 下面的方法会根据request_sock创建一个真正的struct sock
struct sock *newsk;
newsk = tcp_create_openreq_child(sk, req, skb);
// 该方法会把新创建的socket放到tcp_hashinfo的bhash中
// 对应的端口就是监听socket所使用的端口
// 此时该端口对应的bhash中的value中的owners字段里包含监听socket和这个新创建的socket
if (__inet_inherit_port(sk, newsk) < 0)
goto put_and_exit;
// 在上面创建request_sock时,把它放到tcp_hashinfo的ehash里
// 到这里request_sock的任务已经完成
// 所以下面的方法会把request_sock从ehash中移除
// 而把新创建的socket放到ehash里
*own_req = inet_ehash_nolisten(newsk, req_to_sk(req_unhash));
}
到现在一个完整的tcp连接已经建立好了,我们再重新梳理下整个思路。
首先,服务端的socket先执行了bind操作,把它自己放到了tcp_hashinfo的bhash中,然后执行了listen操作,把它自己放到了tcp_hashinfo的lhash2中。
然后,客户端执行connect方法,把对应的socket放到了bhash和ehash中,然后发了syn消息给服务端。
服务端收到syn后,先从lhash2中找到对应的listen状态的socket,然后又根据该socket和syn消息创建了request_sock,并放入ehash中,最后发synack给客户端。
客户端收到synack后,先从ehash中找到对应的socket,然后把其状态设置为TCP_ESTABLISHED,最后又返回ack给服务端。
服务端收到ack后,会先从ehash中找到之前创建的request_sock,然后根据该request_sock,创建真正的sock,最后将request_sock从ehash中移除,将新创建的sock放到bhash和ehash中。
至此,一个tcp连接就建立成功。
再之后,就是tcp连接的数据传输过程了,当操作系统收到对方发来的数据时,先根据tcp消息头里的地址端口等信息,从ehash中找到对应的socket,然后将该数据添加到这个socket的接受缓冲区里,这样用户就可以通过read等方法获取这些数据了。
这就是在tcp连接建立成功之后,tcp内的逻辑对tcp_hashinfo的使用。
下面我们再来看下在tcp的关闭流程中,tcp_hashinfo是如何被使用的。
假设客户端先调用了close方法,主动关闭了连接,看下对应代码:
// net/ipv4/tcp.c
void tcp_close(struct sock *sk, long timeout)
{
// 该方法的调用栈
// SYSCALL_DEFINE1(close)
// __close_fd
// filp_close
// fput
// fput_many
// ____fput
// __fput
// sock_close
// __sock_release
// inet_release
// tcp_close
// 下面的方法会将socket状态设置为TCP_FIN_WAIT1
} else if (tcp_close_state(sk)) {
// 发fin消息给对方
tcp_send_fin(sk);
}
}
服务端在收到fin包的处理逻辑为:
// net/ipv4/tcp_input.c
void tcp_fin(struct sock *sk)
{
// 该方法的调用栈
// tcp_v4_rcv
// tcp_v4_do_rcv
// tcp_rcv_established
// tcp_data_queue
// tcp_fin
switch (sk->sk_state) {
case TCP_ESTABLISHED:
tcp_set_state(sk, TCP_CLOSE_WAIT);
}
该方法将服务端socket的状态设置为TCP_CLOSE_WAIT,然后返回ack给客户端。
客户端收到ack后的处理逻辑:
// net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
// 该方法的调用栈
// tcp_v4_rcv
// tcp_v4_do_rcv
// tcp_rcv_state_process
switch (sk->sk_state) {
case TCP_FIN_WAIT1: {
tcp_set_state(sk, TCP_FIN_WAIT2);
break;
}
}
该方法收到ack后,将客户端对应的socket的状态设置为TCP_FIN_WAIT2。
此时,假设服务器的应用层也调用了socket的close方法,该方法会执行以下逻辑:
// net/ipv4/tcp.c
void tcp_close(struct sock *sk, long timeout)
{
// 该方法的调用栈
// SYSCALL_DEFINE1(close)
// __close_fd
// filp_close
// fput
// fput_many
// ____fput
// __fput
// sock_close
// __sock_release
// inet_release
// tcp_close
// 下面的方法会将socket状态设置为TCP_LAST_ACK
} else if (tcp_close_state(sk)) {
// 发fin消息给对方
tcp_send_fin(sk);
}
}
客户端收到fin消息的处理逻辑:
// net/ipv4/tcp_input.c
void tcp_fin(struct sock *sk)
{
// 该方法调用栈
// tcp_v4_rcv
// tcp_v4_do_rcv
// tcp_rcv_state_process
// tcp_data_queue
// tcp_fin
switch (sk->sk_state) {
case TCP_FIN_WAIT2:
// 发送ack给对方
tcp_send_ack(sk);
// 进入time wait逻辑处理
tcp_time_wait(sk, TCP_TIME_WAIT, 0);
}
}
继续看下tcp_time_wait方法:
// net/ipv4/tcp_minisocks.c
void tcp_time_wait(struct sock *sk, int state, int timeo)
{
// 类似于三次握手时服务端创建了request_sock
// 这里也根据当前sock创建了一个inet_timewait_sock
// 对应于处于time wait状态时的socket
struct inet_timewait_sock *tw;
tw = inet_twsk_alloc(sk, tcp_death_row, state);
if (tw) {
// 从sock拷贝各种必要数据到inet_timewait_sock
// 进行time wait定时,超时后会调用inet_twsk_kill方法
// 将inet_timewait_sock从ehash和bhash中移除
inet_twsk_schedule(tw, timeo);
// 该方法会将sock从ehash中移除
// 将inet_timewait_sock加入到ehash和bhash中
inet_twsk_hashdance(tw, sk, &tcp_hashinfo);
}
// 该方法会将sock从bhash中移除,并将其销毁
tcp_done(sk);
}
好,客户端的逻辑就全部完成了,我们再看下服务端逻辑。
当服务器处于TCP_LAST_ACK状态时,收到客户端的ack消息,进行下面的处理:
// net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
// 该方法调用栈
// tcp_v4_rcv
// tcp_v4_do_rcv
// tcp_rcv_state_process
switch (sk->sk_state) {
case TCP_LAST_ACK:
if (tp->snd_una == tp->write_seq) {
// 该方法及其底层方法会将该sock从ebash和bhash中移除
tcp_done(sk);
}
}
}
到这里,一个tcp连接就完全关闭了,并且前后端的socket都已经从tcp_hashinfo的ehash和bhash中移除。
现在系统又回到tcp连接之前的状态,即只有一个服务端的socket处于listen状态,该socket同时被存放于tcp_hashinfo的bhash、lhash2及listening_hash里。
我们看下当该listen状态的socket关闭的时候,对应的有关tcp_hashinfo的处理:
// net/ipv4/tcp.c
void tcp_set_state(struct sock *sk, int state)
{
// 该方法的调用栈
// SYSCALL_DEFINE1(close)
// __close_fd
// filp_close
// fput
// fput_many
// ____fput
// __fput
// sock_close
// __sock_release
// inet_release
// tcp_close
// tcp_set_state
switch (state) {
case TCP_CLOSE:
// 下面的方法会将socket从lhash2及listening_hash里移除
sk->sk_prot->unhash(sk);
// 下面方法会将socket从bhash中移除
if (inet_csk(sk)->icsk_bind_hash &&
!(sk->sk_userlocks & SOCK_BINDPORT_LOCK))
inet_put_port(sk);
}
}
至此,所有socket都已关闭,且tcp_hashinfo又回到了最原始的空状态。
上文用了大量的篇幅讲述在tcp的各种操作中,tcp_hashinfo是如何被使用的。其实回过头来看一下,tcp_hashinfo在这其中的作用还是非常简单的,其主要目的就是辅助操作系统在各种情况下找到对应的socket。
比如,在syn消息来时,要找到对应的listen状态的socket,用了tcp_hashinfo中的lhash2。
比如,在syn消息之后的所有后续消息来时,要找到其对应的消息处理socket,用了tcp_hashinfo中的ehash。
比如,在绑定端口或挑选端口时,要用到bhash来查询端口是否被占用。
好,就这么多吧,文章到此就结束了。
总体来说该篇文章是以tcp_hashinfo这个全局实例为中心,看了一下操作系统是如何管理tcp连接的。
希望此文章能给同样处于内核研究的同学一些帮助。
本文分享自 Linux内核及JVM底层相关技术研究 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!