前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux系统研究 - 操作系统是如何管理tcp连接的 (1)

Linux系统研究 - 操作系统是如何管理tcp连接的 (1)

作者头像
KINGYT
发布2019-11-07 10:41:35
2.3K0
发布2019-11-07 10:41:35
举报

首先,在linux内核的网络模块里维护着一个全局实例,用来存储所有和tcp相关的socket:

代码语言:javascript
复制
// net/ipv4/tcp_ipv4.c
struct inet_hashinfo tcp_hashinfo;

其次,在该实例的内部,又根据socket类型的不同,划分成四个hashtable:

代码语言:javascript
复制
// 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实例会在下面的方法中被初始化:

代码语言:javascript
复制
// net/ipv4/tcp.c
void __init tcp_init(void)
{
        // 初始化tcp_hashinfo里的四个hashtable等信息
}

该tcp_hashinfo实例还会被赋值给下面tcp_prot实例中的对应字段:

代码语言:javascript
复制
// 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这个字段,用来判断该端口是否被占用。

来看下代码:

代码语言:javascript
复制
// 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方法:

代码语言:javascript
复制
// 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方法:

代码语言:javascript
复制
// 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的地方就是这一点点。

服务端的相关操作就是这些,我们再来看下客户端。

未完待续…

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-11-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Linux内核及JVM底层相关技术研究 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档