本文主要内容: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接口中的变量与方法

    关于变量 java接口里的变量都是默认 pubic static final的 为啥? public 接口得能被所有对象调用 static 这个变量是属于接口本身,而不是实现了接口的对象的 具体来说  ...

  2. volatile足以保证数据同步吗

    上一节已经了解了java内存模型,JMM为了提高执行性能,引入了工作内存和主存两个概念,在继续讨论之前必须先搞清四种存储介质:寄存器.高级缓存.RAM和ROM. RAM与ROM大家都比较熟悉了,可以看 ...

  3. android的消息通知栏

    在android的应用层中,涉及到很多应用框架,例如:Service框架,Activity管理机制,Broadcast机制,对话框框架,标题栏框架,状态栏框架,通知机制,ActionBar框架等等. ...

  4. 实现memmove函数

    分析:memmove函数是<string.h>的标准函数,其作用是把从source开始的num个字符拷贝到destination.最简单的方法是直接复制,但是由于它们可能存在内存的重叠区, ...

  5. 15 Action View 以及监听 的使用

    menu 代码 <menu xmlns:android="http://schemas.android.com/apk/res/android" > <!-- a ...

  6. Cocos2D中Action的进阶使用技巧(二)

    大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请多提意见,如果觉得不错请多多支持点赞.谢谢! hopy ;) 上回说到解决办法是使用CCTargetedAction类. C ...

  7. 一个Bootstrap的例子--关于validate

    </pre><pre name="code" class="html"><%@ page language="java& ...

  8. Web Service进阶(四)WebService注解

    @WebService 1.serviceName: 对外发布的服务名,指定 Web Service 的服务名称:wsdl:service.缺省值为 Java 类的简单名称 + Service.(字符 ...

  9. Linux中的端口占用问题

    本文将会阐述两种解决端口占用的方法. 本文会用到的服务器端的程序如下: #include "unp.h" #include <time.h> int main(int ...

  10. 【翻译】Ext JS 6有什么新东西?

    工具包ToolKits 发布 包的命名 Fashion 图表 ItemEdit插件 网格 电子表格 可操作模式Actionable Mode和可访问性 LazyItems插件 屏幕阅读器支持可访问性 ...