[Linux内核 | socket底层的来龙去脉] [浅析Linux sockfs文件系统] 很久之前写过以上:套接字socket的底层来龙去脉、sockfs文件系统的实现,可以作为本文的前置知识进行学习浏览。先来一张本文中核心的一张图,具体可以看后面文章的解释: 本文从socket的bind系统调用进行分析,主要是了解一下bind背后,Linux内核是如何进行端口绑定、如何管理本地众多的端口号。
先直观感受bind系统调用背后的端口管理、端口复用
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
int sockfd_one;
int err_log;
sockfd_one = socket(AF_INET, SOCK_STREAM, 0); //创建TCP套接字one
if(sockfd_one < 0)
{
perror("sockfd_one");
exit(-1);
}
// 设置本地网络信息
struct sockaddr_in my_addr;
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(8000); // 端口为8000
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 绑定,端口为8000
err_log = bind(sockfd_one, (struct sockaddr*)&my_addr, sizeof(my_addr));
if(err_log != 0)
{
perror("bind sockfd_one");
close(sockfd_one);
exit(-1);
}
int sockfd_two;
sockfd_two = socket(AF_INET, SOCK_STREAM, 0); //创建TCP套接字two
if(sockfd_two < 0)
{
perror("sockfd_two");
exit(-1);
}
// 新套接字sockfd_two,继续绑定8000端口,绑定失败
// 因为8000端口已被占用,默认情况下,端口没有释放,无法绑定
err_log = bind(sockfd_two, (struct sockaddr*)&my_addr, sizeof(my_addr));
if(err_log != 0)
{
perror("bind sockfd_two");
close(sockfd_two);
exit(-1);
}
close(sockfd_one);
close(sockfd_two);
return 0;
}
可以看到端口重复绑定导致了第二个套接字创建失败,我们通过setsockopt系统调用在创建socket后设置端口可复用:
int opt = 1;
// sockfd为需要端口复用的套接字
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&opt, sizeof(opt));
具体如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
int sockfd_one;
int err_log;
sockfd_one = socket(AF_INET, SOCK_STREAM, 0); //创建UDP套接字one
if(sockfd_one < 0)
{
perror("sockfd_one");
exit(-1);
}
// 设置本地网络信息
struct sockaddr_in my_addr;
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(8000); // 端口为8000
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 在sockfd_one绑定bind之前,设置其端口复用
int opt = 1;
setsockopt( sockfd_one, SOL_SOCKET,SO_REUSEADDR,
(const void *)&opt, sizeof(opt) );
// 绑定,端口为8000
err_log = bind(sockfd_one, (struct sockaddr*)&my_addr, sizeof(my_addr));
if(err_log != 0)
{
perror("bind sockfd_one");
close(sockfd_one);
exit(-1);
}
int sockfd_two;
sockfd_two = socket(AF_INET, SOCK_STREAM, 0); //创建UDP套接字two
if(sockfd_two < 0)
{
perror("sockfd_two");
exit(-1);
}
// 在sockfd_two绑定bind之前,设置其端口复用
opt = 1;
setsockopt( sockfd_two, SOL_SOCKET,SO_REUSEADDR,
(const void *)&opt, sizeof(opt) );
// 新套接字sockfd_two,继续绑定8000端口,成功
err_log = bind(sockfd_two, (struct sockaddr*)&my_addr, sizeof(my_addr));
if(err_log != 0)
{
perror("bind sockfd_two");
close(sockfd_two);
exit(-1);
}
printf("two socket create success!\n");
close(sockfd_one);
close(sockfd_two);
return 0;
}
如上,两个套接字绑定同一个端口都创建成功。下面将从bind出发分析bind是如何端口管理、复用的。
bind的核心执行函数
bind系统调用的核心函数调用流程如下:
SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
|
sock->ops->bind(sock,
(struct sockaddr *)
&address, addrlen);
|
inet_bind
|
inet_csk_get_port
中间的流程暂且不看,本文主要分析最重要的函数:inet_csk_get_port,从该函数出发了解本地绑定端口如何管理。以下是原函数,通过下文章节逐步分段分析
int inet_csk_get_port(struct sock *sk, unsigned short snum)
{
bool reuse = sk->sk_reuse && sk->sk_state != TCP_LISTEN;
struct inet_hashinfo *hinfo = sk->sk_prot->h.hashinfo;
int ret = 1, port = snum;
struct inet_bind_hashbucket *head;
struct net *net = sock_net(sk);
struct inet_bind_bucket *tb = NULL;
kuid_t uid = sock_i_uid(sk);
if (!port) {
head = inet_csk_find_open_port(sk, &tb, &port);
if (!head)
return ret;
if (!tb)
goto tb_not_found;
goto success;
}
head = &hinfo->bhash[inet_bhashfn(net, port,
hinfo->bhash_size)];
spin_lock_bh(&head->lock);
inet_bind_bucket_for_each(tb, &head->chain)
if (net_eq(ib_net(tb), net) && tb->port == port)
goto tb_found;
tb_not_found:
tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep,
net, head, port);
if (!tb)
goto fail_unlock;
tb_found:
if (!hlist_empty(&tb->owners)) {
if (sk->sk_reuse == SK_FORCE_REUSE)
goto success;
if ((tb->fastreuse > 0 && reuse) ||
sk_reuseport_match(tb, sk))
goto success;
if (inet_csk_bind_conflict(sk, tb, true, true))
goto fail_unlock;
}
success:
if (hlist_empty(&tb->owners)) {
tb->fastreuse = reuse;
if (sk->sk_reuseport) {
tb->fastreuseport = FASTREUSEPORT_ANY;
tb->fastuid = uid;
tb->fast_rcv_saddr = sk->sk_rcv_saddr;
tb->fast_ipv6_only = ipv6_only_sock(sk);
tb->fast_sk_family = sk->sk_family;
#if IS_ENABLED(CONFIG_IPV6)
tb->fast_v6_rcv_saddr = sk->sk_v6_rcv_saddr;
#endif
} else {
tb->fastreuseport = 0;
}
} else {
if (!reuse)
tb->fastreuse = 0;
if (sk->sk_reuseport) {
if (!sk_reuseport_match(tb, sk)) {
tb->fastreuseport = FASTREUSEPORT_STRICT;
tb->fastuid = uid;
tb->fast_rcv_saddr = sk->sk_rcv_saddr;
tb->fast_ipv6_only = ipv6_only_sock(sk);
tb->fast_sk_family = sk->sk_family;
#if IS_ENABLED(CONFIG_IPV6)
tb->fast_v6_rcv_saddr = sk->sk_v6_rcv_saddr;
#endif
}
} else {
tb->fastreuseport = 0;
}
}
if (!inet_csk(sk)->icsk_bind_hash)
inet_bind_hash(sk, tb, port);
WARN_ON(inet_csk(sk)->icsk_bind_hash != tb);
ret = 0;
fail_unlock:
spin_unlock_bh(&head->lock);
return ret;
}
端口哈希桶
在inet_csk_get_port函数中的变量声名中有如下几个结构体:
struct inet_hashinfo *hinfo = sk->sk_prot->h.hashinfo;
struct inet_bind_hashbucket *head;
struct inet_bind_bucket *tb = NULL;
其中strcut inet_hashinfo是用来封装各种协议的绑定哈希表,具体定义如下所示,这个结构体在[Linux内核角度分析服务器Listen细节] 中介绍过,具体地,struct inet_bind_hashbcket是bind相关的哈希桶,bhash_size是bind哈希桶的大小。
struct inet_hashinfo {
struct inet_ehash_bucket *ehash;
spinlock_t *ehash_locks;
unsigned int ehash_mask;
unsigned int ehash_locks_mask;
struct inet_bind_hashbucket *bhash;
unsigned int bhash_size;
struct inet_listen_hashbucket listening_hash[INET_LHTABLE_SIZE]
____cacheline_aligned_in_smp;
};
struct inet_bind_hashbcket哈希桶的具体定义如下,其中chain代表着各个桶的哈希队列,用来链接具有同一哈希值的哈希元素
struct inet_bind_hashbucket {
spinlock_t lock;
struct hlist_head chain;
};
具体每个桶结构是struct inet_bind_bucket:
struct inet_bind_bucket {
possible_net_t ib_net;
unsigned short port;
signed char fastreuse;
signed char fastreuseport;
kuid_t fastuid;
#if IS_ENABLED(CONFIG_IPV6)
struct in6_addr fast_v6_rcv_saddr;
#endif
__be32 fast_rcv_saddr;
unsigned short fast_sk_family;
bool fast_ipv6_only;
struct hlist_node node;
struct hlist_head owners;
};
初次看到这几个结构体可能比较乱,下面用图进行描述:
由上图所示,每个绑定的端口号经过哈希计算都会挂在相应的chain链表上,chain链表上是一个个的桶结构,同一个chain上的节点具有相同的哈希值(通过端口号计算),桶结构inet_bind_bucket包含对应的端口号port、owners等信息,owners对应:该端口号对应的tcp_sock实例,如果该port支持复用,那么owners可能挂着多个tcp_sock节点。
在struct inet_bind_bucket中有一个关键的成员:signed char fastreuse
为了避免每次都遍历 inet_bind_bucket 的 owners 字段 来获知是否所有的 sock 都设置了 sk_reuse 字段,并且不是在 TCP_LISTEN 状态。在 inet_bind_bucket 结构体中设置了 fastreuse 字段。如果 owners 没有元素,那么这 个字段为真。此后每次添加一个新的 sock 到 owners 中的时候,如果它设置了 sk_reuse 并且不在 TCP_LISTEN 状态,就维持 fastreuse 为真,否则设置它为假。
端口选择
继续看inet_csk_get_port函数:
在端口选择前,先确定当前该socket的“属性”,即是否可以端口复用,是否在TCP_LISTEN状态,以便后面插入到桶队列时设置fastreuse字段。
bool reuse = sk->sk_reuse && sk->sk_state != TCP_LISTEN;
进行端口选择和绑定:
端口绑定分为两种,一种指定端口,一种随机选择。如果给 bind 传 递的地址参数中,port 字段为 0,那么就会自动选择参数。
如代码所示,当端口port没有指定时,调用inet_csk_find_open_port(sk, &tb, &port):
if (!port) {
head = inet_csk_find_open_port(sk, &tb, &port);
if (!head)
return ret;
if (!tb)
goto tb_not_found;
goto success;
}
主要逻辑在net_csk_find_open_port实现,重点看端口指定,暂时不看端口不指定(其实逻辑差不多)。那么当用户指定了端口,也就是port有值时:
if (!port) {
head = inet_csk_find_open_port(sk, &tb, &port);
if (!head)
return ret;
if (!tb)
goto tb_not_found;
goto success;
}
head = &hinfo->bhash[inet_bhashfn(net, port,hinfo->bhash_size)];
inet_bind_bucket_for_each(tb, &head->chain)
if (net_eq(ib_net(tb), net) && tb->port == port)
goto tb_found;
tb_not_found:
tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep,
net, head, port);
if (!tb)
goto fail_unlock;
端口复用的解释
还是要从文章开头的图说起,bind时端口号都会经过哈希计算分配在【相应的哈希桶结构inet_bind_hashbucket】上的chain链表节点的【桶结构inet_bind_bucket上】,inet_bind_bucket 结构就是用来描述端口和 sock 之间的绑定关系的。它的 port 字段表示一个绑定的端口,而 owners 则表示绑定到这个端口之上的所有 sock,因为端口可以重用,所以同一端口可能有多个 sock 绑定。
bind端口复用的实际用途基本上也就是:
防止服务器重启时之前绑定的端口还未释放或者程序突然退出而系统没有释放端口。这种情况下如果设定了端口复用,则新启动的服务器进程可以直接绑定端口。如果没有设定端口复用,绑定会失败,提示ADDR已经在使用中!
参考:
http://blog.guorongfei.com/2014/10/24/socket-bind/
http://t.zoukankan.com/dream397-p-14555402.html
END
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8