一、前情回顾
上一节《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的操作流程,unicast multicast等等的区别。
很清楚的分析了一个数据包如何通过查询路由进入内核ipsec协议栈的处理、Linux 内核ipsec协议栈详细的加解密流程以及加解密完后如何将数据包发送出去。 文档中前半部分主要介绍一些关键的数据结构,及其相互之间的...
linux内核分析----初始化 linux内核分析----初始化 linux内核分析----初始化 linux内核分析----初始化
《Linux内核网络栈源代码情景分析》主要对Linux1.2.13内核协议栈的全部源代码做了详细的分析,该版本所有代码都在一个文件夹中,每种协议的实现都只有一个文件与之对应,分析该版本源代码可以方便读者迅速掌握Linux...
Linux内核源代码导读-陈香兰-中国科学技术大学-进程管理
本书主要对 Linux 1.2.13 内核协议栈的全部源代码做了详细的分析, 该版本所有代码都在一个文件夹中,每种协议的实现都只有一个文件与之对应,分析该版本源代码可以方便读者迅速掌握 Linux 网络协议结构。...
《linux内核网络部分源码分析》,很不粗的linux内核和网络的资料,希望对你的学习工作有所帮助。
Linux内核网络部分源码分析-唐文
3.3 timestack数据包-Wireshark3.4 内核协议栈相关主要源码Time_wait状态生成及快速回收相关代码:开启timestamps引起的丢
详细描述了linux内核协议栈的实现原理及相关数据结构,为linux内核协议栈分析人员提供了重要参考。
主要对Linux1.2.13内核协议栈的全部源代码做了详细的分析,该版本所有代码都在一个文件夹中,每种协议的实现都只有一个文件与之对应,分析该版本源代码可以方便读者迅速掌握Linux网络协议结构。 压缩包最后一部分。
[Linux内核源码].linux-2.6.16.18.tar.bz2.part3.rar [Linux内核源码].linux-2.6.16.18.tar.bz2.part3.rar [Linux内核源码].linux-2.6.16.18.tar.bz2.part3.rar [Linux内核源码].linux-2.6.16.18.tar.bz2.part3.rar
文件名:linux-5.15.118.tar.xz 文件发布日期: 2023-06-21 说明: 该版本是长期支持版本.
---------------------------------------------------Linux 中的各种栈:进程栈 线程栈 内核栈 中断栈
基于Linux内核的用户态网络协议栈的实现.pdf
此Linux内核学习资料包中有Linux内核--网络栈实现分析(二)--数据包的传递过程(上).pdf Linux内核--网络栈实现分析(三)--驱动程序层+链路层(上).pdf Linux内核--网络栈实现分析(四)--网络层之IP协议(上)....
[Linux内核源码].linux-2.6.16.18.tar.bz2.part2.rar [Linux内核源码].linux-2.6.16.18.tar.bz2.part2.rar [Linux内核源码].linux-2.6.16.18.tar.bz2.part2.rar [Linux内核源码].linux-2.6.16.18.tar.bz2.part2.rar
linux内核模块编程,编写一个模块,实现文件的读取。
linux-2.6.18内核基础上分析网络协议栈,适合初学者看
深入,对于想深入了解linux内核协议栈的有帮助