首先,在linux内核的网络模块里维护着一个全局实例,用来存储所有和tcp相关的socket:
// net/ipv4/tcp_ipv4.c
struct inet_hashinfo tcp_hashinfo;
其次,在该实例的内部,又根据socket类型的不同,划分成四个hashtable:
// include/net/inet_hashtables.h
struct inet_hashinfo {
// key是由本地地址、本地端口、远程地址、远程端口组成的四元组
// value是正在建立连接或已经建立连接的socket
// 比如,当内核收到一个tcp消息时,它先从消息头里读出地址和端口等信息
// 然后用该信息到ehash里获取对应的socket
// 最后把剩余的tcp数据添加到该socket的recv buf中供用户程序读取
struct inet_ehash_bucket *ehash;
// key是本地端口
// value是使用这个端口的所有socket
// 比如,当我们用socket监听一个端口时,该socket就在bhash里
// 同理,由该监听端口建立的连接对应的那些socket也在这里
// 因为它们也都是使用同样的本地端口
struct inet_bind_hashbucket *bhash;
// key是本地地址和端口组成的二元组
// value是对应的处于listen状态的socket
struct inet_listen_hashbucket *lhash2;
// key是本地端口
// value是对应的处于listen状态的socket
struct inet_listen_hashbucket listening_hash[INET_LHTABLE_SIZE];
};
在系统启动时,这个全局的tcp_hashinfo实例会在下面的方法中被初始化:
// net/ipv4/tcp.c
void __init tcp_init(void)
{
// 初始化tcp_hashinfo里的四个hashtable等信息
}
该tcp_hashinfo实例还会被赋值给下面tcp_prot实例中的对应字段:
// net/ipv4/tcp_ipv4.c
struct proto tcp_prot = {
// 在struct sock里会通过sk_prot字段引用该tcp_prot实例
// 也就是说,如果拿到任一个struct sock实例
// 就可以通过它的sk_prot字段获取tcp_prot实例
// 进而也就可以获取tcp_hashinfo实例
.h.hashinfo = &tcp_hashinfo,
};
EXPORT_SYMBOL(tcp_prot);
好,以上就是操作系统管理tcp连接用到的全局的数据结构,接下来我们看一些具体操作。
在tcp编程中一般都分为客户端和服务端,我们先来看下服务端对应的操作。
首先,一个socket想要监听一个端口,必须要先bind一个地址,然后再执行listen操作。
其中bind操作就用到了上面的tcp_hashinfo实例里的bhash这个字段,用来判断该端口是否被占用。
来看下代码:
// net/ipv4/inet_connection_sock.c
int inet_csk_get_port(struct sock *sk, unsigned short snum)
{
// 该方法的调用栈:
// SYSCALL_DEFINE3(bind)
// __sys_bind
// inet_bind
// inet_csk_get_port
// 下面的hinfo就是全局实例tcp_hashinfo
struct inet_hashinfo *hinfo = sk->sk_prot->h.hashinfo;
// 根据端口算出hash值,然后根据这个值找到bhash中对应的slot
head = &hinfo->bhash[inet_bhashfn(net, port,
hinfo->bhash_size)];
// 遍历slot指向的链表,找到port对应的值
inet_bind_bucket_for_each(tb, &head->chain)
if (net_eq(ib_net(tb), net) && tb->l3mdev == l3mdev &&
tb->port == port)
goto tb_found;
// 如果没找到,说明现在还没有人使用这个端口,就新创建一个
// 新创建的实例就会放到bhash中,表明这个端口我在使用了
tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep,
net, head, port, l3mdev);
tb_found:
// 如果tb的owners字段不为空,则说明有人在使用这个端口
if (!hlist_empty(&tb->owners)) {
// 如果该端口被别人占用了,且不能共享使用,就返回错误给用户
if (inet_csk_bind_conflict(sk, tb, true, true))
goto fail_unlock;
}
// 省略很多无关代码
// 在该方法的最后,会调用inet_bind_hash方法
// 方法内容会在下面描述
if (!inet_csk(sk)->icsk_bind_hash)
inet_bind_hash(sk, tb, port);
}
EXPORT_SYMBOL_GPL(inet_csk_get_port);
再来看下inet_bind_hash方法:
// net/ipv4/inet_hashtables.c
void inet_bind_hash(struct sock *sk, struct inet_bind_bucket *tb,
const unsigned short snum)
{
// 保存绑定端口
inet_sk(sk)->inet_num = snum;
// tb是上面方法中获取的或创建的bhash中的一个值
// 它的owners字段存放的是所有使用该端口的sock
// 下面语句的意思是,把这个sock也加入到owner里
// 这样在其他人拿到tb时,就能知道哪些sock在使用这个tb对应的端口了
sk_add_bind_node(sk, &tb->owners);
// 将tb地址存放到sock的icsk_bind_hash字段里
// 这样以后想知道该sock对应的bhash里的值时(比如在移除owners时)
// 就可以通过下面的字段获取了
inet_csk(sk)->icsk_bind_hash = tb;
}
好,bind方法涉及到tcp_hashinfo的地方,到这里就都已经讲完了,我们再看下listen方法:
// net/ipv4/inet_hashtables.c
int __inet_hash(struct sock *sk, struct sock *osk)
{
// 该方法的调用栈:
// SYSCALL_DEFINE2(listen)
// __sys_listen
// inet_listen
// inet_csk_listen_start
// inet_hash
// __inet_hash
// hashinfo就是全局实例tcp_hashinfo
struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo;
// 根据本地端口,找到该sock在listening_hash中的slot
ilb = &hashinfo->listening_hash[inet_sk_listen_hashfn(sk)];
// 将该sock添加到slot对应的链表中
if (IS_ENABLED(CONFIG_IPV6) && sk->sk_reuseport &&
sk->sk_family == AF_INET6)
hlist_add_tail_rcu(&sk->sk_node, &ilb->head);
else
hlist_add_head_rcu(&sk->sk_node, &ilb->head);
// 以本地端口和地址作为key,将该sock加入到tcp_hashinfo里的lhash2里
inet_hash2(hashinfo, sk);
}
EXPORT_SYMBOL(__inet_hash);
listen方法涉及到tcp_hashinfo的地方就是这一点点。
服务端的相关操作就是这些,我们再来看下客户端。
未完待续…
本文分享自 Linux内核及JVM底层相关技术研究 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!