本文主要内容:bind()的TCP层实现、端口的冲突处理,以及不同内核版本的实现差异。

内核版本:3.6

Author:zhangskd @ csdn blog

TCP层实现

SOCK_STREAM套接口的TCP层操作函数集实例为tcp_prot,其中端口绑定函数为inet_csk_get_port()。

struct proto tcp_prot = {
.name = "TCP",
.owner = THIS_MODULE,
...
.get_port = inet_csk_get_port, /* TCP层bind()相关操作 */
...
};

和较早的内核版本不同,现在系统自动选择端口时,也可以复用端口了。

/* Obtain a reference to a local port for the given sock,
* if snum is zero it means select any available local port.
*/ int inet_csk_get_port(struct sock *sk, unsigned short snum)
{
struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo; /* 指向tcp_hashinfo */
struct inet_bind_hashbucket *head;
struct hlist_node *node;
struct inet_bind_bucket *tb;
int ret, attempts = 5;
struct net *net = sock_net(sk);
int smallest_size = -1, smallest_rover; local_bh_disable(); /* 禁止下半部,防止和进程冲突 */ /* 如果snum为0,系统自动为sock选择一个端口号 */
if (! snum) {
int remaining, rover, low, high; again:
inet_get_local_port_range(&low, &high); /* 获取端口号的取值范围 */
remaining = (high - low) + 1; /* 取值范围内端口号的个数 */
smallest_rover = rover = net_random() % remaining + low; /* 随机选取范围内的一个端口 */ smallest_size = -1; do {
if (inet_is_reserved_local_port(rover)) /* 查看端口是否属于保留的 */
goto next_nolock; /* rover加1,继续 */ /* 根据端口号,确定所在的哈希桶 */
head = &hashinfo->bhash[inet_bhashfn(net, rover, hashinfo->bhash_size)];
spin_lock( &head->lock); /* 锁住哈希桶 */ inet_bind_bucket_for_each(tb, node, &head->chain) /* 从头遍历哈希桶 */
/* 如果端口被使用了 */
if (eq(ib_net(tb), net) && tb->port == rover) { if (tb->fastreuse > 0 && sk->sk_reuse && sk->sk_state != TCP_LISTEN &&
tb->num_owners < smallest_size || smallest_size == -1)) {
smallest_size = tb->num_owners; /* 记下这个端口使用者的个数 */
smallest_rover = rover; /* 记下这个端口 */ /* 如果系统绑定的端口已经很多了,那么就判断端口是否有绑定冲突*/
if (atomic_read(&hashinfo->bsockets) > (high - low) + 1 &&
! inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, false)) {
snum = smallest_rover; /* 没有冲突,使用此端口 */
goto tb_found;
}
} /* 检查是否有端口绑定冲突,该端口是否能重用 */
if (! inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, false)) {
snum = rover;
goto tb_found;
} goto next; /* 此端口不可重用,看下一个 */
} break; /* 找到了没被用的端口,退出 */
next:
spin_unlock(&head->lock);
next_nolock:
if (++rover > high)
rover = low;
} while(--remaining > 0); /* Exhausted local port range during search? It is not possible for us to be holding
* one of the bind hash locks if this test triggers, because if 'remaining' drops to zero,
* we broke out of the do/while loop at the top level, not from the 'break' statement.
*/
ret = 1;
if (remaining <= 0) { /* 完全遍历 */
if (smallest_size != -1) {
snum = smallest_rover;
goto have_snum;
}
goto fail;
} /* OK, here is the one we will use.
* HEAD is non-NULL and we hold it's mutex.
*/
snum = rover; /* 自动选择的可用端口 */ } else { /* 如果有指定要绑定的端口 */ have_snum:
head = &hashinfo->bhash[inet_bhashfn(net, snum, hashinfo->bhash_size)];
spin_lock(&head->lock);
inet_bind_bucket_for_each(tb, node, &head->chain)
if (net_eq(ib_net(tb), net) && tb->port == snum)
goto tb_found; /* 发现端口在用 */
}
tb = NULL;
goto tb_not_found; tb_found:
/* 端口上有绑定sock时 */
if (! hlist_empty(&tb->owners)) {
/* 这是强制的绑定啊,不管端口是否会绑定冲突!*/
if (sk->sk_reuse == SK_FORCE_REUSE)
goto success; if (tb->fastreuse > 0 && sk->sk_reuse && sk->sk_state != TCP_LISTEN &&
smallest_size == -1) { /* 指定端口的情况 */
goto success; } else {
ret = 1;
if (inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, true)) { /* 端口绑定冲突 */
/* 自动分配的端口绑定冲突了,再次尝试,最多重试5次。
* 我觉得以下if不必要,因为自动选择时goto tb_found之前都有检测过了。
*/
if (sk->sk_reuse && sk->sk_state != TCP_LISTEN && smallest_size != -1
&& --attempts >= 0) {
spin_unlock(&head->lock);
goto again;
}
goto fail_unlock; /* 失败 */
}
}
} tb_not_found:
ret = 1;
/* 申请和初始化一个inet_bind_bucket结构 */
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:
/* 赋值icsk中的inet_bind_bucket */
if (! inet_csk(sk)->icsk_bind_hash)
inet_bind_hash(sk, tb, snum);
WARN_ON(inet_csk(sk)->icsk_bind_hash != tb);
ret = 0; fail_unlock:
spin_unlock(&head->lock); fail:
local_bh_enable();
return ret;
}

端口区间

我们可以指定系统自动分配端口号时,端口的区间:

/proc/sys/net/ipv4/ip_local_port_range,默认为:32768 61000

也可以指定要保留的端口区间:

/proc/sys/net/ipv4/ip_local_reserved_ports,默认为空

typedef struct {
unsigned sequence;
spinlock_t lock;
}seqlock_t; struct local_ports {
seqlock_t lock; /* 顺序锁,读多写少,写优先 */
int range[2];
}; /* This struct holds the first and last local port number.
* 用于系统自动分配的端口区间。
*/
struct local_ports sysctl_local_ports __read_mostly = {
.lock = __SEQLOCK_UNLOCKED(sysctl_local_ports.lock),
.range = {32768, 61000},
};

当snum为0时,由系统自动分配端口号时,获取端口号的取值范围。

void inet_get_local_port_range(int *low, int *high)
{
unsigned int seq; do {
seq = read_seqbegin(&sysctl_local_ports.lock); *low = sysctl_local_ports.range[0];
*high = sysctl_local_ports.range[1]; } while(read_seqretry(&sysctl_local_ports.lock, seq)); /* 用顺序锁来读 */
}

端口绑定冲突

面向连接的、传输层的协议族相关的操作函数集:

/*
* Pointers to address related TCP functions
* (i.e. things that depend on the address family)
*/
struct inet_connection_sock_af_ops {
...
int (*bind_conflict) (const struct sock *sk, const struct inet_bind_bucket *tb, bool relax);
...
};
const struct inet_connection_sock_af_ops ipv4_specific = {
...
.bind_conflict = inet_csk_bind_conflict, /* 用于判断绑定端口是否冲突 */
...
};

如果不冲突,则可以绑定该端口,返回0;如果冲突,则不可绑定该端口,返回1。

int inet_csk_bind_conflict(const struct sock *sk, const struct inet_bind_bucket *tb, bool relax)
{
struct sock *sk2;
struct hlist_node *node;
int reuse = sk->sk_reuse; /* SO_REUSEADDR,表示处于TIME_WAIT状态的端口允许重用 */ /* Unlike other sk lookup places we do not check for sk_net here, since all the socks
* listed in tb->owners list belong to the same net - the one this bucket belongs to.
* 遍历此端口上的sock。
*/
sk_for_each_bound(sk2, node, &tb->owners) {
/* 冲突的条件1:不是同一socket、绑定在相同的设备上 */
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)) { /* 冲突的条件2:绑定在相同的IP上
* 冲突的条件3(符合一个即满足):
* 3.1 本socket不允许重用
* 3.2 链表中的socket不允许重用
* 3.3 链表中的socket处于监听状态
*/
if (! reuse || ! sk2->sk_reuse || sk2->sk_state == TCP_LISTEN) {
const __u32 sk2_rcv_saddr = sk_rcv_saddr(sk2); /* sk2的绑定IP */
if (! sk2_rcv_saddr || ! sk_rcv_saddr(sk) || sk2_rcv_saddr == sk_rcv_saddr(sk))
break; /* 冲突了 */
} /* 觉得这段代码有好多冗余,可以精简下:)
* 3.4 relax为false
*/
if (! relax && reuse && sk2->sk_reuse && sk2->sk_state != TCP_LISTEN) {
const __be32 sk2_rcv_saddr = sk_rcv_saddr(sk2);
if (! sk2_rcv_saddr || ! sk_rcv_saddr(sk) || sk2_rcv_saddr == sk_rcv_saddr(sk))
break;
}
}
}
return node != NULL;
}

Q: 什么情况下会出现冲突呢?

A: 同时符合以下条件才会冲突:

1. 绑定的设备相同(不允许自动选择设备)

2. 绑定的IP地址相同(不允许自动选择IP)

3 以下条件有一个成立:

3.1 要绑定的socket不允许重用

3.2 已绑定的socket不允许重用

3.3 已绑定的socket处于监听状态

3.4 relax参数为false

这样看来能够重用端口号的情况包括:

1. 绑定的设备不同

2. 绑定的IP地址不同

3. 要绑定的socket允许重用,且已绑定的socket允许重用,且已绑定的socket不处于监听状态,relex参数为true。

在这种情况下,有可能两个socket的(addr, port)、绑定的设备完全一样,所以增加了relax参数,可用于禁止。

我们看到系统自动选择端口时,relax为false,是不允许这种情况的。

inet_bind_bucket

inet_bind_bucket用来保存使用中的端口,以及绑定在该端口上的sock链表。

(1)创建

/*
* Allocate and initialize a new local port bind bucket.
* The bindhash mutex for snum's hash chain must be held here.
*/
struct inet_bind_bucket *inet_bind_bucket_create(struct kmem_cache *cachep, struct net *net,
struct inet_bind_hashbucket *head, const unsigned short snum)
{
struct inet_bind_bucket *tb = kmem_cache_alloc(cachep, GFP_ATOMIC); if (tb != NULL) {
write_pnet(&tb->ib_net, hold_net(net)); /* 指定网络命名空间 */
tb->port = snum; /* 指定绑定端口 */
tb->fastreuse = 0;
tb->num_owners = 0;
INIT_HLIST_HEAD(&tb->owners); /* 初始化owners哈希链表 */
hlist_add_head(&tb->node, &head->chain); /* 把此inet_bind_bucket实例添加到哈希桶中 */
}
return tb;
}

(2)销毁

void inet_bind_bucket_destroy(struct kmem_cache *cachep, struct inet_bind_bucket *tb)
{
if (hlist_empty(&tb->owners)) {
__hlist_del(&tb->node);
release_net(ib_net(tb));
kmem_cache_free(cachep, tb);
}
}

(3)赋值

更新赋值icsk的inet_bind_bucket类型成员icsk_bind_hash。

void inet_bind_hash(struct sock *sk, struct inet_bind_bucket *tb, const unsigned short snum)
{
struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo; /* 指向tcp_hashinfo */
atomic_inc(&hashinfo->bsockets); /* 增加总的绑定次数 */
inet_sk(sk)->inet_num = snum; /* 保存绑定的端口 */
sk_add_bind_node(sk, &tb->owners); /* 把此sock链入tb->owners哈希链表中 */
tb->num_owners++; /* 增加端口绑定次数 */
inet_csk(sk)->icsk_bind_hash = tb; /* 把此tb作为icsk成员icsk_bind_hash */
}

(4)查找

根据端口号,确定所在的哈希桶索引。

static inline int inet_bhashfn(struct net *net, const __16 lport, const int bhash_size)
{
return (lport + net_hash_mix(net)) & (bhash_size - 1);
}

问题

笔者看的是3.6的版本,一开始对系统自动选择端口的逻辑感到不解。

2.6.18版本

2.6.18版本的系统自动选择端口:随机选取一个未使用端口,不允许端口复用。

由于snum为0时不进行端口复用,所以实现起来很简单。

3.2版本

查了下3.2版本,系统自动选择端口的思路是这样的:

1. 随机选取一个端口。

2. 检查其是否被使用了。

2.1 没有被使用,那么就是这个端口了,退出:)

2.2 被使用了,就检查它是否能够重用。

2.2.1 如果可以重用,则记下它的使用者个数small_size和端口号small_rover。

下次如果发现使用者更少的可重用端口,则更small_size和small_rover。

2.2.2 检查此时已绑定的端口总数,是不是超过了自动绑定区间的端口数。

2.2.2.1 超过了,说明端口基本上都被使用了,不再继续寻找没被使用过的,直接重用此时的small_rover。

退出:)

2.2.2.2 没超过,那就继续寻找没被使用过的端口。如果又发现可重用的端口时,顺便更新下small_size

和small_rover。

3. 端口++,重复1和2。

简而言之,系统自动选择端口时:尽量选择没被使用过的端口。如果实在没有,就选择使用者个数最少的可重用端口。

3.6版本

在3.3版本中加入了commit 2b05ad33。

增加了几行代码:

if (! inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, false)) {

snum = rover;

goto tb_found;

}

作者的描述如下:

tcp: bind() fix autoselection to share ports

The current code checks for conflicts when the application requests a specific port.

If there is no conflict, then the request is granted.

On the other hand, the port autoselection done by the kernel fails when all ports are bound

even when there is a port whith no conflict available.

The fix changes port autoselection to check if there is a conflict and use it if not.

作者的意思是,在系统自动选择端口时,判断可重用端口的主要条件为:

tb->fastreuse > 0 && sk->sk_reuse && sk->sk_state != TCP_LISTEN

而其实不符合此条件、但通过bind_conflict()检查的端口也是可以重用的,所以作者的出发点是正确的。

但是,简单的把上面那几行代码就这样加进去,系统自动选择端口的思路就变为:

1. 随机选取一个端口。

2. 检查其是否被使用了。

2.1 没有被使用,那么就是这个端口了,退出:)

2.2 被使用了,检查重用是否有冲突。

2.2.1 没有冲突,就重用这个端口,退出!

2.2.2 有冲突,继续遍历。

3. 端口++,重复1和2。

也就是说,系统自动选择端口时:不优先选择没被使用过的端口。只要没有冲突,直接重用端口。

这样一来,上面small_size和small_rover的代码就成了空摆设了,跟原意不一样了。我认为这个改动并不合适,

不知道当时是怎么通过的:)

Socket层实现系列 — bind()的实现(二)的更多相关文章

  1. Socket层实现系列 — bind()的实现(一)

    bind()函数的使用方法很简单,但是它是怎么实现的呢? 笔者从应用层出发,沿着网络协议栈,分析了bind()的系统调用.Socket层实现,以及它的TCP层实现. 本文主要内容:bind()的系统调 ...

  2. Socket层实现系列 — send()类发送函数的实现

    主要内容:socket发送函数的系统调用.Socket层实现. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 发送流程图 以下是send().sendt ...

  3. Socket层实现系列 — connect()的实现

    主要内容:connect()的Socket层实现.期间进程的睡眠和唤醒. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 应用层 int connect( ...

  4. Socket层实现系列 — getsockname()和getpeername()的实现

    本文主要介绍了getsockname()和getpeername()的内核实现. 内核版本:3.6 Author:zhangskd @ csdn blog 应用层 int getsockname(in ...

  5. Socket层实现系列 — listen()的实现

    本文主要分析listen()的内核实现,包括它的系统调用.Socket层实现.半连接队列,以及监听哈希表. 内核版本:3.6 Author:zhangskd @ csdn blog 应用层 int l ...

  6. Socket层实现系列 — 信号驱动的异步等待

    主要内容:Socket的异步通知机制. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 概述 socket上定义了几个IO事件:状态改变事件.有数据可读事 ...

  7. Socket层实现系列 — 睡眠驱动的同步等待

    主要内容:Socket的同步等待机制,connect和accept等待的实现. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 概述 socket上定义了 ...

  8. Socket层实现系列 — accept()的实现(一)

    本文主要介绍了accept()的系统调用.Socket层实现,以及TCP层实现. 内核版本:3.6 Author:zhangskd @ csdn blog 应用层 int accept(int soc ...

  9. Socket层实现系列 — accept()的实现(二)

    本文主要分析accept()的阻塞等待和唤醒. 内核版本:3.6 Author:zhangskd @ csdn blog 等待队列 (1)socket的等待队列 /* * @sk_wq: sock w ...

随机推荐

  1. Java学习之运算符使用注意的问题

    运算符使用注意的问题 运算符(掌握) (1)算术运算符 A:+,-,*,/,%,++,-- B:+的用法 a:加法 b:正号 c:字符串连接符 C:/和%的区别 数据做除法操作的时候,/取得是商,%取 ...

  2. SQL Server 索引维护(1)——系统常见的索引问题

    前言: 在很多系统中,比如本人目前管理的数据库,索引经常被滥用,甚至使用DTA(数据库引擎优化顾问)来成批创建索引(DTA目前个人认为它的真正用处应该是在发现缺失的统计信息,在以前的项目中,用过一次D ...

  3. Ubuntu下配置Telnet服务器

    1. 首先介绍linux中的守护进程 在Linux系统中有一个特殊的守护进程inetd(InterNET services Daemon),它用于Internet标准服务,通常在系统启动时启动.通过命 ...

  4. Unable to access the IIS metabase.You do not have sufficient privilege

    今天在用vs打开以前老代码的时候报如下问题,无法打开工程了,从提示来不大可能是因为vs的版本引起的,本身我用的是最新版的vs. 网上查了下解决方法如下:找到你电脑中的如下路径"C:\Wind ...

  5. 【iOS 开发】iOS 开发 简介 (IOS项目文件 | MVC 模式 | 事件响应机制 | Storyboard 控制界面 | 代码控制界面 | Retina 屏幕图片适配)

    一. iOS 项目简介 1. iOS 文件简介 创建一个 HelloWorld 项目, 在这个 IOS 项目中有四个目录 : 如下图; -- HelloWorldTests 目录 : 单元测试相关的类 ...

  6. 【一天一道LeetCode】#155. Min Stack

    一天一道LeetCode 本系列文章已全部上传至我的github,地址:ZeeCoder's Github 欢迎大家关注我的新浪微博,我的新浪微博 欢迎转载,转载请注明出处 (一)题目 Design ...

  7. 美国康奈尔大学BioNB441元胞自动机MATLAB应用

    美国康奈尔大学BioNB441在Matlab中的元胞自动机 介绍 元胞自动机(CA)是用于计算计划利用当地的规则和本地通信.普遍CA定义一个网格,网格上的每个点代表一个有限数量的状态中的细胞.过渡规则 ...

  8. listview下拉刷新上拉加载扩展(一)

    前两篇实现了listview简单的下拉刷新和上拉加载,功能已经达到,单体验效果稍简陋,那么在这篇文章里我们来加一点效果,已达到我们常见的listview下拉刷新时的效果: 首先,在headview的x ...

  9. (八十七)AutoLayout的简介与实例

    AutoLayout是继AutoResizing之后的一种自动布局方法,解决了AutoResizing无法处理控件间相互关系的问题. AutoLayout在storyboard中通过底部工具条设置,底 ...

  10. Caffe框架,训练model并测试数据

    1. 训练model #!/usr/bin/env sh ./build/tools/caffe train --solver=examples/focal_length/focal_solver.p ...