`
diecui1202
  • 浏览: 97933 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

Linux内核网络协议栈7-socket端口管理

阅读更多

一、前情回顾

上一节《socket 地址绑定 》中提到,应用程序传递过来的端口在内核中需要检查端口是否可用:

if (sk->sk_prot->get_port(sk, snum)) {
    inet->saddr = inet->rcv_saddr = 0;
    err = -EADDRINUSE;
    goto out_release_sock;
}

 

 

按照前面的例子来分析,这里是调用了 tcp_prot 结构变量中的 get_prot 函数指针,该函数位于 net/ipv4/Inet_connection_sock.c 中;这个函数比较长,也是我们今天要分析的重点;

 

二、端口的管理

1 、端口管理数据结构

Linux 内核将所有 socket 使用时的端口通过一个哈希表来管理,该哈希表存放在全局变量 tcp_hashinfo 中,通过 tcp_prot 变量的 h 成员引用,该成员是一个联合类型;对于 tcp 套接字类型,其引用存放在 h. hashinfo 成员中;下面是 tcp_hashinfo 的结构体类型:

 

struct inet_hashinfo {
       struct inet_ehash_bucket  *ehash;
       rwlock_t                     *ehash_locks;
       unsigned int                ehash_size;
       unsigned int                ehash_locks_mask;
 
       struct inet_bind_hashbucket    *bhash;//管理端口的哈希表
       unsigned int                bhash_size;//端口哈希表的大小
 
       struct hlist_head         listening_hash[INET_LHTABLE_SIZE];
       rwlock_t                     lhash_lock ____cacheline_aligned;
       atomic_t                     lhash_users;
       wait_queue_head_t           lhash_wait;
       struct kmem_cache                 *bind_bucket_cachep;//哈希表结构高速缓存
}
 

 

端口管理相关的,目前可以只关注加注释的这三个成员,其中 bhash 为已经哈希表结构, bhash_size 为哈希表的大小;所有哈希表中的节点内存都是在 bind_bucket_cachep 高速缓存中分配;

 

下面看一下 inet_bind_hashbucket 结构体:

 

struct inet_bind_hashbucket {
       spinlock_t            lock;
       struct hlist_head  chain;
};
struct hlist_head {
       struct hlist_node *first;
};
struct hlist_node {
       struct hlist_node *next, **pprev;
};
 

 

inet_bind_hashbucket 是哈希桶结构, lock 成员是用于操作时对桶进行加锁, chain 成员是相同哈希值的节点的链表;示意图如下:

 

2 、默认端口的分配

当应用程序没有指定端口时(如 socket 客户端连接到服务端时,会由内核从可用端口中分配一个给该 socket );

看看下面的代码 ( 参见 net/ipv4/Inet_connection_sock.c: inet_csk_get_port() 函数 )

 

if (!snum) {
    int remaining, rover, low, high;
 
    inet_get_local_port_range(&low, &high);
    remaining = (high - low) + 1;
    rover = net_random() % remaining + low;
 
    do {
        head = &hashinfo->bhash[inet_bhashfn(rover, hashinfo->bhash_size)];
        spin_lock(&head->lock);
        inet_bind_bucket_for_each(tb, node, &head->chain)
            if (tb->ib_net == net && tb->port == rover)
                goto next;
        break;
    next:
        spin_unlock(&head->lock);
        if (++rover > high)
            rover = low;
    } while (--remaining > 0);
 
    ret = 1;
    if (remaining <= 0)
        goto fail;
 
    snum = rover;
}
 

 

这里,随机端口的范围是 32768~61000 ;上面代码的逻辑如下:

1)   [32768, 61000] 中随机取一个端口 rover

2)   计算该端口的 hash 值,然后从全局变量 tcp_hashinfo 的哈希表 bhash 中取出相同哈希值的链表 head

3)   遍历链表 head ,检查每个节点的网络设备是否和当前网络设置相同,同时检查节点的端口是否和 rover 相同;

4)   如果相同,表明端口被占用,继续下一个端口;如果和链表 head 中的节点都不相同,则跳出循环,继续后面的逻辑;

 

inet_bind_bucket_foreach 宏利用《 创建 socket 》一文中提到的 container_of 宏来实现 的,大家可以自己看看;

 

3 、端口重用

当应用程序指定端口时,参考下面的源代码:

else {
    head = &hashinfo->bhash[inet_bhashfn(snum, hashinfo->bhash_size)];
    spin_lock(&head->lock);
    inet_bind_bucket_for_each(tb, node, &head->chain)
        if (tb->ib_net == net && tb->port == snum)
            goto tb_found;
}
 

 

此时同样会检查该端口有没有被占用;如果被占用,会检查端口重用(跳转到 tb_found ):

tb_found:
       if (!hlist_empty(&tb->owners)) {
              if (tb->fastreuse > 0 &&
                  sk->sk_reuse && sk->sk_state != TCP_LISTEN) {
                     goto success;
              } else {
                     ret = 1;
                     if (inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb))
                            goto fail_unlock;
              }
       }
 

 

1)    端口节点结构

struct inet_bind_bucket {
       struct net             *ib_net;//端口所对应的网络设置
       unsigned short            port;//端口号
       signed short         fastreuse;//是否可重用
       struct hlist_node  node;//作为bhash中chain链表的节点
       struct hlist_head  owners;//绑定在该端口上的socket链表
};
 

 

前面提到的哈希桶结构中的 chain 链表中的每个节点,其宿主结构体是 inet_bind_bucket ,该结构体通过成员 node 链入链表;


2)    检查端口是否可重用

这里涉及到两个属性,一个是 socket sk_reuse ,另一个是 inet_bind_bucket fastreuse

sk_reuse 可以通过 setsockopt() 库函数进行设置,其值为 0 1 ,当为 1 时,表示当一个 socket 进入 TCP_TIME_WAIT 状态 ( 连接关闭已经完成 ) 后,它所占用的端口马上能够被重用,这在调试服务器时比较有用,重启程序不用进行等待;而 fastreuse 代表该端口是否允许被重用:

l  当该端口第一次被使用时( owners 为空),如果 sk_reuse 1 socket 状态不为 TCP_LISTEN ,则设置 fastreuse 1 ,否则设置为 0

l  当该端口同时被其他 socket 使用时( owners 不为空),如果当前端口能被重用,但是当前 socket sk_reuse 0 或其状态为 TCP_LISTEN ,则将 fastreuse 设置为 0 ,标记为不能重用;


3)    当不能重用时,再次检查冲突

此时会调用 inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb) 再次检查端口是否冲突;回想《 创建 socket 》一文中提到,创建 socket 成功后,要使用相应的协议来初始化 socket ,对于 tcp 协议来说,其初始化方法是 net/ipv4/Tcp_ipv4.c:tcp_v4_init_sock() ,其中就做了如下一步的设置:

icsk->icsk_af_ops = &ipv4_specific;
 
struct inet_connection_sock_af_ops ipv4_specific = {
       .queue_xmit    = ip_queue_xmit,
       .send_check          = tcp_v4_send_check,
       .rebuild_header      = inet_sk_rebuild_header,
       .conn_request        = tcp_v4_conn_request,
       .syn_recv_sock     = tcp_v4_syn_recv_sock,
       .remember_stamp        = tcp_v4_remember_stamp,
       .net_header_len     = sizeof(struct iphdr),
       .setsockopt      = ip_setsockopt,
       .getsockopt     = ip_getsockopt,
       .addr2sockaddr      = inet_csk_addr2sockaddr,
       .sockaddr_len        = sizeof(struct sockaddr_in),
       .bind_conflict          = inet_csk_bind_conflict,
#ifdef CONFIG_COMPAT
       .compat_setsockopt = compat_ip_setsockopt,
       .compat_getsockopt = compat_ip_getsockopt,
#endif
};
 

 

下面看看这里再次检查冲突的代码:

int inet_csk_bind_conflict(const struct sock *sk,
                        const struct inet_bind_bucket *tb)
{
       const __be32 sk_rcv_saddr = inet_rcv_saddr(sk);
       struct sock *sk2;
       struct hlist_node *node;
       int reuse = sk->sk_reuse;
 
       sk_for_each_bound(sk2, node, &tb->owners) {
              if (sk != sk2 &&
                  !inet_v6_ipv6only(sk2) &&
                  (!sk->sk_bound_dev_if ||
                   !sk2->sk_bound_dev_if ||
                   sk->sk_bound_dev_if == sk2->sk_bound_dev_if)) {
                     if (!reuse || !sk2->sk_reuse ||
                         sk2->sk_state == TCP_LISTEN) {
                            const __be32 sk2_rcv_saddr = inet_rcv_saddr(sk2);
                            if (!sk2_rcv_saddr || !sk_rcv_saddr ||
                                sk2_rcv_saddr == sk_rcv_saddr)
                                   break;
                     }
              }
       }
       return node != NULL;
}
 

 

上面函数的逻辑是:从 owners 中遍历绑定在该端口上的 socket ,如果某 socket 跟当前的 socket 不是同一个,并且是绑定在同一个网络设备接口上的,并且它们两个之中至少有一个的 sk_reuse 表示自己的端口不能被重用或该 socket 已经是 TCP_LISTEN 状态了,并且它们两个之中至少有一个没有指定接收 IP 地址,或者两个都指定接收地址,但是接收地址是相同的,则冲突产生,否则不冲突。

也就是说,不使用同一个接收地址的 socket 可以共用端口号,绑定在不同的网络设备接口上的 socket 可以共用端口号,或者两个 socket 都表示自己可以被重用,并且还不在 TCP_LISTEN 状态,则可以重用端口号。

 

4 、新建 inet_bind_bucket

当在 bhash 中没有找到指定的端口时,需要创建新的桶节点,然后挂入 bhash 中:

tb_not_found:
       ret = 1;
       if (!tb && (tb = inet_bind_bucket_create(hashinfo->bind_bucket_cachep,
                                   net, head, snum)) == NULL)
              goto fail_unlock;
       if (hlist_empty(&tb->owners)) {
              if (sk->sk_reuse && sk->sk_state != TCP_LISTEN)
                     tb->fastreuse = 1;
              else
                     tb->fastreuse = 0;
       } else if (tb->fastreuse &&
                 (!sk->sk_reuse || sk->sk_state == TCP_LISTEN))
              tb->fastreuse = 0;
success:
       if (!inet_csk(sk)->icsk_bind_hash)
              inet_bind_hash(sk, tb, snum);
 

 

有兴趣的可以自己看看这段代码的实现,这里就不再展开了。

分享到:
评论

相关推荐

    linux-socket-file-transfer.rar_arm linux socket_linux arm pc soc

    1. **Socket概念**:Socket是进程间通信的一种形式,它允许两个网络应用程序通过TCP/IP协议栈进行通信。在Linux系统中,Socket接口被封装在`&lt;sys/socket.h&gt;`头文件中。 2. **ARM Linux**:ARM是一种广泛应用于...

    linux TCP IP协议栈源码解析

    - `net/`目录下包含所有网络协议栈的源代码,如`tcp.c`处理TCP协议,`ipv4/`处理IPv4。 - `socket`层是用户空间与内核空间通信的接口,通过系统调用如`socket()`、`bind()`、`connect()`等实现。 - `sk_buff`...

    linux网络协议栈(UDP收发)

    接收到`skb`后,`netif_rx`函数负责将其送入内核网络协议栈。`netif_rx`会检查当前CPU的输入包队列`input_pkt_queue`,如果队列未满,则直接将`skb`插入队列;若已满,会触发软中断`napi_schedule`,并在稍后处理这...

    linux 内核架构图

    3. **各种子系统**:包括内存管理、进程调度、文件系统、网络协议栈等,每个子系统负责处理特定类型的任务。 4. **设备驱动程序**:直接与硬件通信,实现对硬件设备的支持。 #### 三、具体子系统详解 ##### 3.1 ...

    Linux-Socket-服务器编程实例.pptx

    Linux Socket服务器编程是网络通信的基础,它涉及到操作系统内核、网络协议栈以及C语言编程。在本实例中,我们将创建一个简单的TCP服务器,它监听客户端的连接请求,并在连接建立后,向客户端发送特定的字符串并关闭...

    追踪Linux TCPIP代码运行--基于2.6内核

    通过对 `socket` 和 `sock` 结构体的详细解析,我们可以更深入地理解Linux内核如何管理和实现网络通信。这些结构体不仅为应用程序提供了标准的接口,还确保了底层协议的具体实现细节不会影响到上层应用的开发。在...

    linux内核设计与实现第三版(陈莉君译 经典)

    最后,书中还涉及了文件系统、网络协议栈、安全性和性能分析等方面的内容。在文件系统部分,读者将学习到VFS(虚拟文件系统)如何抽象各种底层文件系统,以及挂载和卸载文件系统的机制。网络部分涵盖了从网络接口层...

    Linux协议栈阅读笔记

    - 它贯穿于整个网络协议栈中,用于存储和管理数据包的信息。 **特点**: - 套接字缓存使得网络数据包的处理更为高效,减少了不必要的复制操作,提高了整体性能。 #### 四、套接字地址 **定义与结构**: - **`...

    ipv4_linux内核skb处理流程图_

    总结来说,Linux内核中的skb处理流程是一个复杂而精细的过程,涉及了网络协议栈的各个层次,确保了数据包从网络到应用的正确传输。通过理解这一流程,开发者能更好地优化网络应用性能,排查问题,以及实现特定的网络...

    linux socket详细分析

    Linux Socket是操作系统提供的网络通信接口,它允许应用程序通过网络协议(如TCP/IP)进行通信。本文将深入探讨Linux Socket的工作原理,主要包括以下几个方面:socket发送数据的函数流程、sys_socket流程、以及...

    linux socket基础文档

    在设计模式中,Socket被视为门面模式的一个实例,它隐藏了底层复杂的网络协议细节,简化了网络编程过程。 2. **Socket模型**:Socket可以视为一种特殊的文件。在Unix/Linux哲学中,“一切皆文件”,这意味着Socket...

    嵌入式Linux系统开发技术详解--基于ARM(完整版)

    嵌入式Linux开发涉及多个层面,包括硬件接口、操作系统内核、设备驱动、文件系统、网络协议栈以及应用程序开发等。以下将围绕这些关键知识点进行详细介绍: 1. **硬件接口**:在基于ARM的嵌入式系统中,理解硬件...

    windows-Linux Socket.rar

    在TCP/IP协议栈中,Socket提供了应用程序与网络协议的交互接口,使得应用程序可以通过Socket发送和接收数据。 2. **Windows Socket (Winsock)** Windows操作系统提供了Winsock API供开发者进行网络编程。Winsock...

    linux socket 实现原理

    这些API是用户程序与内核网络协议栈之间的桥梁,允许数据的发送和接收,以及对网络连接的管理等。 Linux内核通过统一的系统调用接口来处理所有与socket相关操作。在内核版本2.6.12中,系统调用的入口函数是`sys_...

    lab8-SOCKET编程原理.ppt

    如果客户端没有进行 bind() 调用,或调用了 bind() 但没有指定具体地址或端口号,则由系统内核自动确定地址和端口。 * 由 connect() 确定。 面向连接的 C/S 程序工作流程图(TCP): WSAStartup() -&gt; socket() -&gt; ...

    Linux网络socket核心技术

    socket可以支持多种协议栈,如TCP/IP协议栈,使得不同主机间的进程能够通过网络进行通信。 #### 二、创建socket 创建socket是网络编程的第一步,主要通过`socket()`函数完成: ```c #include &lt;sys/socket.h&gt; int...

Global site tag (gtag.js) - Google Analytics