一、前情回顾

上一节《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内核网络协议栈7-socket端口管理相关推荐

  1. Linux内核网络协议栈8—socket监听

    几个问题  了解以下几个问题的同学可以直接忽略下文: 1.listen 库函数主要做了什么?  2. 什么是最大并发连接请求数?  3.什么是等待连接队列? socket 监听相对还是比较简单的,先看 ...

  2. Linux内核网络协议栈:udp数据包发送(源码解读)

    <监视和调整Linux网络协议栈:接收数据> <监控和调整Linux网络协议栈的图解指南:接收数据> <Linux网络 - 数据包的接收过程> <Linux网 ...

  3. Linux内核网络协议栈流程及架构

    文章目录 Linux内核网络报文处理流程 Linux内核网络协议栈架构 Linux内核网络报文处理流程 linux网络协议栈是由若干个层组成的,网络数据的处理流程主要是指在协议栈的各个层之间的传递. ...

  4. 深入浅出Linux内核网络协议栈|结构sk_buff|Iptables|Netfilter丨内核源码丨驱动开发丨内核开发丨C/C++Linux服务器开发

    深入浅出Linux内核网络协议栈 视频讲解如下,点击观看: 深入浅出Linux内核网络协议栈|结构sk C/C++Linux服务器开发高级架构师知识点精彩内容包括:C/C++,Linux,Nginx, ...

  5. linux内核网络协议栈--linux网络设备理解(十三)

    网络层次 linux网络设备驱动与字符设备和块设备有很大的不同. 字符设备和块设备对应/dev下的一个设备文件.而网络设备不存在这样的设备文件.网络设备使用套接字socket访问,虽然也使用read, ...

  6. Linux 内核网络协议栈运行原理

    封装:当应用程序用 TCP 协议传送数据时,数据首先进入内核网络协议栈中,然后逐一通过 TCP/IP 协议族的每层直到被当作一串比特流送入网络.对于每一层而言,对收到的数据都会封装相应的协议首部信息( ...

  7. Linux内核网络协议栈3-创建socket(1)

    1.示例及函数入口: 1) 示例代码如下: C代码   int server_sockfd = socket(AF_INET, SOCK_STREAM, 0); 2) 入口: net/Socket.c ...

  8. Linux内核网络协议栈5-socket地址绑定

    一.socket绑定入口 1.示例代码 struct sockaddr_in server_address; server_address.sin_family = AF_INET; server_a ...

  9. Linux内核网络协议栈4-创建socket(2)

    接上篇"创建socket" 一文: 5.分配sock结构: 本文中的例子会调用inet_family_ops.create方法即inet_create方法完成socket的创建工作 ...

最新文章

  1. ICML 2018大奖出炉:伯克利、MIT获最佳论文,复旦大学榜上有名
  2. Linux打开rtf文档,在linux下设置开机自动启动程序的方法_精品.rtf
  3. shell编写mysql全备和增备脚本_基于mysqldump编写自动全备增备的shell脚本
  4. java死信队列_Spring Boot系列教程之死信队列详解
  5. html空间图片,html+js实现图片预览
  6. Android实现汤姆猫小游戏
  7. 查看计算机ping,通过ping命令检测主机的存活性
  8. 密码学三大顶会和信息安全四大顶会网址
  9. 攻防世界re:logmein
  10. 计算机工作键是开声音的,笔记本电脑原来加声音要按两个键,现在只按一个键了,怎么调呢?...
  11. 迁移学习论文阅读感想(初步)
  12. 有关于进程,线程and协程
  13. python 应用程序无法正常启动 000007b_“应用程序无法正常启动(oxc000007b)”解决方案...
  14. Runnable小练习(网图下载)
  15. XMPP增加删除好友
  16. matlab做四陵锥立体图,四棱锥三棱锥立体图怎么画?
  17. (五)JMeter 断言
  18. 王道计算机组成原理第六章---总线总结
  19. matlab绿色 不伤眼,蓝光和超清哪个伤眼睛 伤害都很低不用过多担心
  20. JAVA实现远程控制(JAVA in RemoteControl)

热门文章

  1. ckeditor复制html样式丢失,Ckeditor选择html无法正常使用铬浏览器
  2. 大内存 php 干什么好 centos,解决CentOS7中php-fpm进程数过多导致服务器内存资源消耗较大的问题...
  3. Linux安装wireshark并配置权限
  4. 新型冠状病毒肺炎国内分省分日期从1.16起的全部数据爬取与整理代码(附下载)
  5. C#2.0泛型中的变化: default 关键字
  6. Kafka、 RabbitMQ、Redis、 ZeroMQ、 ActiveMQ、 Kafka/Jafka 对比
  7. Selenium +Java自动化环境安装
  8. 基于‘BOSS直聘招聘信息’分析企业到底需要什么样的PHPer
  9. Linux Shell基础 - Shell 脚本的执行方式
  10. 通过Java反射来理解泛型的本质