Linux网络协议栈 -- socket bind 地址绑定
linux-dash
A beautiful web dashboard for Linux
项目地址:https://gitcode.com/gh_mirrors/li/linux-dash
免费下载资源
·
1、bind()
当创建了一个 Socket 套接字后,对于服务器来说,接下来的工作,就是调用 bind(2)为服务器指明本地址、协议端口号,常常可以看到这样的代码:
strut sockaddr_in sin;sin.sin_family = AF_INET;
sin.sin_addr.s_addr = xxx;
sin.sin_port = xxx;
bind(sock, (struct sockaddr *)&sin, sizeof(sin));
从这个系统调用中,可以知道当进行 SYS_BIND 操作的时候,:
1、对于 AF_INET 协议簇来讲,其地址格式是 strut sockaddr_in,而对于 socket 来讲,strut sockaddr 结构表示的地址格式实现了更高层次 的抽像,因为每种协议长簇的地址不一定是相同的,所以,系统调用的第三个参数得指明该协议簇的地址格式的长度。
2、进行 bind(2)系统调用时,除了地址长度外,还得向内核提供:sock 描述符、协议簇名称、本地地址、端口这些参数;
2、sys_bind()
操作 SYS_BIND 是由 sys_bind()实现的:asmlinkage long sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
{
struct socket *sock;
char address[MAX_SOCK_ADDR];
int err;
if((sock = sockfd_lookup(fd,&err))!=NULL)
{
if((err=move_addr_to_kernel(umyaddr,addrlen,address))>=0) {
err = security_socket_bind(sock, (struct sockaddr *)address, addrlen);
if (err) {
sockfd_put(sock);
return err;
}
err = sock->ops->bind(sock, (struct sockaddr *)address, addrlen);
}
sockfd_put(sock);
}
return err;
}
在 socket 的创建中,已经反复分析了 socket 与文件系统的关系,现在已知 socket的描述符号,要找出与之相关的 socket 结构,应该是件容易的事情:
struct socket *sockfd_lookup(int fd, int *err)
{
struct file *file;
struct inode *inode;
struct socket *sock;
if (!(file = fget(fd)))
{
*err = -EBADF;
return NULL;
}
inode = file->f_dentry->d_inode;
if (!S_ISSOCK(inode->i_mode)) {
*err = -ENOTSOCK;
fput(file);
return NULL;
}
sock = SOCKET_I(inode);
if (sock->file != file) {
printk(KERN_ERR "socki_lookup: socket file changed!\n");
sock->file = file;
}
return sock;
}
fget 从当前进程的 files 指针中,根据 sock 对应的描述符号,找到已打开的文件 file,再根据文件的目录项中的 inode,利用inode 与 sock 被封装在同一个结构中的事实,调用宏 SOCKET_I找到待查的 sock 结构。最后做一个小小的判断,因为正常情况下,sock 的 file 指针是 回指与之相关的 file。
接下来的工作是把用户态的地址拷贝至内核中来:
int move_addr_to_kernel(void __user *uaddr, int ulen, void *kaddr)
{
if(ulen<0||ulen>MAX_SOCK_ADDR)
return -EINVAL;
if(ulen==0)
return 0;
if(copy_from_user(kaddr,uaddr,ulen))
return -EFAULT;
return 0;
}
bind(2)第三个参数必须存在的原因之一,copy_from_user 必须知道拷贝的字节长度。
因为 sock 的 ops 函数指针集,在创建之初,就指向了对应的协议类型,例如如果类型是SOCK_STREAM,那么它就指向 inetsw_array[0].ops。也就是 inet_stream_ops:
struct proto_ops inet_stream_ops = {
.family = PF_INET,
…… .bind = inet_bind,
……
};
sys_bind()在做完了一个通用的 socket bind 应该做的事情,包括查找对应 sock 结构,拷贝地址。就
调用对应协议族的对应协议类型的 bind 函数,也就是 inet_bind。
3、inet_bind
说 bind()的最重要的作用,就是为套接字绑定地址和端口,那么要分析 inet_bind()之前,要搞清楚的一件事情就是,这个绑定,是绑定到哪儿?或者说,是绑定到内核的哪个数据结构的哪个成员变量上面??
有三个地方是可以考虑的:socket 结构,包括 sock 和 sk,inet结构,以及 protoname_sock 结构。绑定在 socket 结构上是可行 的,这样可以实现最高层面上的抽像,但是因为每一类协议簇 socket 的地址及端口表现形式差异很大,这样就得引入专门的转换处理功能。绑定在 protoname_sock 也是可行的,但是却是最笨拙的,因为例如 tcp 和 udp,它们的地址及端口表现形式是一样的,这样就浪费了空间,加大了代码 处理量。所以,inet 做为一个协议类型的抽像,是最理想的地方了,再来回顾一下它的定义:
struct inet_sock {
……
/* Socket demultiplex comparisons on incoming packets. */
__u32 daddr; /* Foreign IPv4 addr */
__u32 rcv_saddr; /* Bound local IPv4 addr */
__u16 dport; /* Destination port */
__u16 num; /* Local port */
__u32 saddr; /* Sending source */
……
};
去掉了其它成员,保留了与地址及端口相关的成员变量,从注释中,可以清楚地了解它们的作用。所以,我们说的 bind(2)之绑定,主要就是对这几个成员变量赋值的过程了。
int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
/* 获取地址参数 */
struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;
/* 获取 sock 对应的 sk */
struct sock *sk = sock->sk;
/* 获取 sk 对应的inet */
struct inet_sock *inet = inet_sk(sk);
/* 这个临时变量用来保存用户态传递下来的端口参数 */
unsigned short snum;
int chk_addr_ret;
int err;
/* 如果协议簇对应的协议自身还有 bind 函数,调用之,例如 SOCK_RAW 就还有一个raw_bind */
if (sk->sk_prot->bind) {
err = sk->sk_prot->bind(sk, uaddr, addr_len);
goto out;
}
/* 校验地址长度 */
err = -EINVAL;
if (addr_len < sizeof(struct sockaddr_in))
goto out;
/* 判断地址类型:广播?多播?单播? */
chk_addr_ret = inet_addr_type(addr->sin_addr.s_addr);
/* ipv4 有一个 ip_nonlocal_bind标志,表示是否绑定非本地址 IP地址,可以通过
* cat /proc/sys/net/ipv4/ip_nonlocal_bind 查看到。它用来解决某些服务绑定
* 动态 IP地址的情况。作者在注释中已有详细说明:
* Not specified by any standard per-se, however it breaks too
* many applications when removed. It is unfortunate since
* allowing applications to make a non-local bind solves
* several problems with systems using dynamic addressing.
* (ie. your servers still start up even if your ISDN link
* is temporarily down)
* 这里判断,用来确认如果没有开启“绑定非本地址 IP”,地址值及类型是正确的
*/
err = -EADDRNOTAVAIL;
if (!sysctl_ip_nonlocal_bind &&
!inet->freebind &&
addr->sin_addr.s_addr != INADDR_ANY &&
chk_addr_ret != RTN_LOCAL &&
chk_addr_ret != RTN_MULTICAST &&
chk_addr_ret != RTN_BROADCAST)
goto out;
/* 获取协议端口号 */
snum = ntohs(addr->sin_port);
err = -EACCES;
/* 校验当前进程有没有使用低于 1024 端口的能力 */
if (snum && snum < PROT_SOCK && !capable(CAP_NET_BIND_SERVICE))
goto out;
/* We keep a pair of addresses. rcv_saddr is the one
* used by hash lookups, and saddr is used for transmit. *
* In the BSD API these are the same except where it
* would be illegal to use them (multicast/broadcast) in
* which case the sending device address is used.
*/
lock_sock(sk);
/* 检查 socket 是否已经被绑定过了:用了两个检查项,一个是 sk 状态,另一个是是否已经绑定过端口了 当然地址本来就可以为 0,所以,不能做为检查项 */
err = -EINVAL;
if (sk->sk_state != TCP_CLOSE || inet->num)
goto out_release_sock;
/* 绑定 inet 的接收地址(地址服务绑定地址)和来源地址为用户态指定地址 */
inet->rcv_saddr = inet->saddr = addr->sin_addr.s_addr;
/* 若地址类型为广播或多播,则将地址置 0,表示直接使用网络设备 */
if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)
inet->saddr = 0; /* Use device */
/*
* 调用协议的 get_port 函数,确认是否可绑定端口,若可以,则绑定在 inet->num 之上,注意,这里虽然没有
* 把inet 传过去,但是第一个参数 sk,它本身和 inet是可以互相转化的
*/
if (sk->sk_prot->get_port(sk, snum)) {
inet->saddr = inet->rcv_saddr = 0;
err = -EADDRINUSE;
goto out_release_sock;
}
/* 如果端口和地址可以绑定,置标志位 */
if (inet->rcv_saddr)
sk->sk_userlocks |= SOCK_BINDADDR_LOCK;
if (snum)
sk->sk_userlocks |= SOCK_BINDPORT_LOCK;
/* inet的 sport(来源端口)成员也置为绑定端口 */
inet->sport = htons(inet->num);
inet->daddr = 0;
inet->dport = 0;
sk_dst_reset(sk);
err = 0;
out_release_sock:
release_sock(sk); out:
return err;
}
上述分析中,忽略的第一个细节是 capable()函数调用,它是 Linux 安全模块(LSM)的一部份,简单地讲其用来对权限做出检查, 检查是否有权对指定的资源进行操作。这里它的参数是 CAP_NET_BIND_SERVICE,表示的含义是:
/* Allows binding to TCP/UDP sockets below 1024 */
/* Allows binding to ATM VCIs below 32 */
#define CAP_NET_BIND_SERVICE 10
另一个就是协议的端口绑定,调用了协议的 get_port 函数,如果是 SOCK_STREAM的 TCP 协议,那么它就是 tcp_v4_get_port()函数。
4、协议端口的绑定
要分配这个函数,还是得先绕一些基本的东东。这里涉及到内核中提供 hash 链表的操作的 API。可以参考其它相关资料。
http://www.ibm.com/developerworks/cn/linux/kernel/l-chain/index.html
这里讲了链表的实现,顺道提了一个 hash 链表,觉得写得还不错,收藏一下。
对于 TCP已注册的端口,是采用一个 hash 表来维护的。hash 桶用 struct tcp_bind_hashbucket 结构来表示:
struct tcp_bind_hashbucket {
spinlock_t lock;
struct hlist_head chain;
};
hash 表中的每一个 hash节点,用 struct tcp_bind_hashbucket 结构来表示:
struct tcp_bind_bucket {
unsigned short port; /* 节点中绑定的端口 */
signed short fastreuse;
struct hlist_node node;
struct hlist_head owners;
};
tcp_hashinfo 的 hash 表信息,都集中封装在结构 tcp_hashinfo 当中,而维护已注册端口,只是它其中一部份:
extern struct tcp_hashinfo {
……
/* Ok, let's try this, I give up, we do need a local binding
* TCP hash as well as the others for fast bind/connect. */
struct tcp_bind_hashbucket *__tcp_bhash;
int __tcp_bhash_size;
……
} tcp_hashinfo;
#define tcp_bhash (tcp_hashinfo.__tcp_bhash)
#define tcp_bhash_size (tcp_hashinfo.__tcp_bhash_size)
其使用的 hash 函数是 tcp_bhashfn:
/* These are AF independent. */
static __inline__ int tcp_bhashfn(__u16 lport)
{
return (lport & (tcp_bhash_size - 1));
}
这样,如果要取得某个端口对应的 hash 链的首部hash 桶节点的话,可以使用:
struct tcp_bind_hashbucket *head;
head = &tcp_bhash[tcp_bhashfn(snum)];
如果要新绑定一个端口,就是先创建一个 struct tcp_bind_hashbucket 结构的 hash 节点,然后把它插入到对应的 hash 链中去:
struct tcp_bind_bucket *tb;
tb = tcp_bucket_create(head, snum);struct tcp_bind_bucket *tcp_bucket_create(struct tcp_bind_hashbucket *head,
unsigned short snum)
{
struct tcp_bind_bucket *tb = kmem_cache_alloc(tcp_bucket_cachep,
SLAB_ATOMIC);
if (tb) {
tb->port = snum;
tb->fastreuse = 0;
INIT_HLIST_HEAD(&tb->owners);
hlist_add_head(&tb->node, &head->chain);
}
return tb;
}
另外,sk 中,还级护了一个类似的 hash 链表,同时需要调用 tcp_bind_hash()函数把 hash 节点插入进去:
struct sock {
struct sock_common __sk_common;#define sk_bind_node __sk_common.skc_bind_node
……
}
/* @skc_bind_node: bind hash linkage for various protocol lookup tables */
struct sock_common {
struct hlist_node skc_bind_node;
……
}
if (!tcp_sk(sk)->bind_hash)
tcp_bind_hash(sk, tb, snum);
void tcp_bind_hash(struct sock *sk, struct tcp_bind_bucket *tb,
unsigned short snum)
{
inet_sk(sk)->num = snum;
sk_add_bind_node(sk, &tb->owners);
tcp_sk(sk)->bind_hash = tb;
}
这里,就顺道绑定了 inet 的 num成员变量,并置协议的 bind_hash 指针为当前分配的 hash 节点。而sk_add_bind_node 函数, 就是一个插入 hash 表节点的过程:
static __inline__ void sk_add_bind_node(struct sock *sk,
struct hlist_head *list)
{
hlist_add_head(&sk->sk_bind_node, list);
}
如果要遍历 hash 表的话,例如在插入之前,先判断端口是否已经在 hash表当中了。就可以调用:
#define tb_for_each(tb, node, head) hlist_for_each_entry(tb, node, head, node)
struct tcp_bind_hashbucket *head;
struct tcp_bind_bucket *tb;
head = &tcp_bhash[tcp_bhashfn(snum)];
spin_lock(&head->lock);
tb_for_each(tb, node, &head->chain)
if (tb->port == snum) found,do_something;
有了这些基础知识,再来看 tcp_v4_get_port()的实现,就要容易得多了:
static int tcp_v4_get_port(struct sock *sk, unsigned short snum)
{
struct tcp_bind_hashbucket *head;
struct hlist_node *node;
struct tcp_bind_bucket *tb;
int ret;
local_bh_disable();
/* 如果端口值为 0,意味着让系统从本地可用端口用选择一个,并置 snum为分配的值 */
if (!snum) {
int low = sysctl_local_port_range[0];
int high = sysctl_local_port_range[1];
int remaining = (high - low) + 1;
int rover;
spin_lock(&tcp_portalloc_lock);
if (tcp_port_rover < low)
rover = low;
else
rover = tcp_port_rover;
do {
rover++;
if (rover > high)
rover = low;
head = &tcp_bhash[tcp_bhashfn(rover)];
spin_lock(&head->lock);
tb_for_each(tb, node, &head->chain)
if (tb->port == rover)
goto next;
break;
next:
spin_unlock(&head->lock);
} while (--remaining > 0);
tcp_port_rover = rover;
spin_unlock(&tcp_portalloc_lock);
/* Exhausted local port range during search? */
ret = 1;
if (remaining <= 0)
goto fail;
/* OK, here is the one we will use. HEAD is
* non-NULL and we hold it's mutex.
*/
snum = rover;
} else {
/* 否则,就在 hash 表中,查找端口是否已经存在 */
head = &tcp_bhash[tcp_bhashfn(snum)];
spin_lock(&head->lock);
tb_for_each(tb, node, &head->chain)
if (tb->port == snum)
goto tb_found;
}
tb = NULL;
goto tb_not_found;
tb_found:
/* 稍后有对应的代码:第一次分配 tb 后,会调用 tcp_bind_hash加入至相应的 sk,这里先做一个判断,来确定这一步工作是否进行过*/
if (!hlist_empty(&tb->owners)) {
/* socket的 SO_REUSEADDR 选项,用来确定是否允许本地地址重用,例如同时启动多个服务器、多个套接字绑定至同一端口等等, sk_reuse 成员对应其值,因为如果一个绑定的 hash节点已经存在,而且不允许重用的话,那么则表示因冲突导致出错,调用 tcp_bind_conflict 来处理之 */
if (sk->sk_reuse > 1)
goto success;
if (tb->fastreuse > 0 &&
sk->sk_reuse && sk->sk_state != TCP_LISTEN) {
goto success;
} else {
ret = 1;
if (tcp_bind_conflict(sk, tb))
goto fail_unlock;
}
}
tb_not_found:
/* 如果不存在,则分配 hash节点,绑定端口 */
ret = 1;
if (!tb && (tb = tcp_bucket_create(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 (!tcp_sk(sk)->bind_hash)
tcp_bind_hash(sk, tb, snum);
BUG_TRAP(tcp_sk(sk)->bind_hash == tb);
ret = 0;
fail_unlock:
spin_unlock(&head->lock);
fail:
local_bh_enable();
return ret;
}
到这里,可以为这部份下一个小结了,所谓绑定,就是:
1、设置内核中 inet 相关变量成员的值,以待后用;
2、协议中,如 TCP协议,记录绑定的协议端口的信息,采用 hash 链表存储,sk 中也同时维护了这么一个链表。两者的区别应该是 前者给协议用。后者给 socket 用。
GitHub 加速计划 / li / linux-dash
6
1
下载
A beautiful web dashboard for Linux
最近提交(Master分支:3 个月前 )
186a802e
added ecosystem file for PM2 4 年前
5def40a3
Add host customization support for the NodeJS version 4 年前
更多推荐
已为社区贡献3条内容
所有评论(0)