当创建了一个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 描述符、协议簇名称、本地地址、端口这
些参数
sys_bind的实现
操作 SYS_BIND 是由 sys_bind()实现的
1335 asmlinkage long sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
1336 {
1337 struct socket *sock;
1338 char address[MAX_SOCK_ADDR];
1339 int err, fput_needed;
1340
1341 if((sock = sockfd_lookup_light(fd, &err, &fput_needed))!=NULL)
1342 {
1343 if((err=move_addr_to_kernel(umyaddr,addrlen,address))>=0) {
1344 err = security_socket_bind(sock, (struct sockaddr *)address, addrlen);
1345 if (!err)
1346 err = sock->ops->bind(sock,
1347 (struct sockaddr *)address, addrlen);
1348 }
1349 fput_light(sock->file, fput_needed);
1350 }
1351 return err;
1352 }
在socket 的创建中已经反复分析了 socket 与文件系统的关系,现在已知socket的描述符号,要找出与之相关的socket结构
应该是件容易的事情
490 static struct socket *sockfd_lookup_light(int fd, int *err, int *fput_needed)
491 {
492 struct file *file;
493 struct socket *sock;
494
495 *err = -EBADF;
496 file = fget_light(fd, fput_needed);
497 if (file) {
498 sock = sock_from_file(file, err);
499 if (sock)
500 return sock;
501 fput_light(file, *fput_needed);
502 }
503 return NULL;
504 }
fget 从当前进程的 files 指针中,根据 sock 对应的描述符号,找到已打开的文件 file,再根据文件的
目录项中的 inode,利用inode 与 sock 被封装在同一个结构中的事实,调用宏 SOCKET_I找到待查
的 sock 结构。最后做一个小小的判断,因为正常情况下,sock 的 file 指针是 回指与之相关的 file.
接下来的工作是把用户态的地址拷贝至内核中来
228 int move_addr_to_kernel(void __user *uaddr, int ulen, void *kaddr)
229 {
230 if(ulen<0||ulen>MAX_SOCK_ADDR)
231 return -EINVAL;
232 if(ulen==0)
233 return 0;
234 if(copy_from_user(kaddr,uaddr,ulen))
235 return -EFAULT;
236 return audit_sockaddr(ulen, kaddr);
237 }
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.
inet_bind
说 bind(2)的最重要的作用就是为套接字绑定地址和端口,那么要分析inet_bind()之前,要搞清楚
的一件事情就是,这个绑定是绑定到哪儿?或者说是绑定到内核的哪个数据结构的哪个成员变量上面?
有三个地方是可以考虑的:socket 结构,包括 sock 和 sk,inet结构,以及 protoname_sock 结构。
绑定在 socket 结构上是可行的,这样可以实现最高层面上的抽像,但是因为每一类协议簇 socket 的地址及端口表现
形式差异很大,这样就得引入专门的转换处理功能。绑定在 protoname_sock 也是可行的,但是却是最笨拙的,因为
例如 tcp 和 udp,它们的地址及端口表现形式是一样的,这样就浪费了空间加大了代码处理量。
所以inet 做为一个协议类型的抽像是最理想的地方了,再来回顾一下它的定义
108 struct inet_sock {
114 /* Socket demultiplex comparisons on incoming packets. */
115 __u32 daddr;
116 __u32 rcv_saddr;
117 __u16 dport;
118 __u16 num;
119 __u32 saddr;
去掉了其它成员保留了与地址及端口相关的成员变量,从注释中可以清楚地了解它们的作用。所以我们说的 bind(2)之
绑定主要就是对这几个成员变量赋值的过程了.
397 int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
398 {
/* 获取地址参数 */
399 struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;
/* 获取 sock 对应的 sk */
400 struct sock *sk = sock->sk;
/* 获取 sk 对应的inet */
401 struct inet_sock *inet = inet_sk(sk);
/* 这个临时变量用来保存用户态传递下来的端口参数 */
402 unsigned short snum;
403 int chk_addr_ret;
404 int err;
405
/* 如果协议簇对应的协议自身还有bind函数调用之,例如 SOCK_RAW 就还有一个raw_bind */
406 /* If the socket has its own bind function then use it. (RAW) */
407 if (sk->sk_prot->bind) {
408 err = sk->sk_prot->bind(sk, uaddr, addr_len);
409 goto out;
410 }
411 err = -EINVAL;
/* 校验地址长度 */
412 if (addr_len < sizeof(struct sockaddr_in))
413 goto out;
414 /* 判断地址类型:广播?多播?单播? */
415 chk_addr_ret = inet_addr_type(addr->sin_addr.s_addr);
416
/* ipv4 有一个 ip_nonlocal_bind标志,表示是否绑定非本地址 IP地址,可以通过
* cat /proc/sys/net/ipv4/ip_nonlocal_bind查看到。
* 它用来解决某些服务绑定动态 IP地址的情况。作者在注释中已有详细说明.
* 这里判断,用来确认如果没有开启“绑定非本地址 IP”,地址值及类型是正确的
417 /* Not specified by any standard per-se, however it breaks too
418 * many applications when removed. It is unfortunate since
419 * allowing applications to make a non-local bind solves
420 * several problems with systems using dynamic addressing.
421 * (ie. your servers still start up even if your ISDN link
422 * is temporarily down)
423 */
424 err = -EADDRNOTAVAIL;
425 if (!sysctl_ip_nonlocal_bind &&
426 !inet->freebind &&
427 addr->sin_addr.s_addr != INADDR_ANY &&
428 chk_addr_ret != RTN_LOCAL &&
429 chk_addr_ret != RTN_MULTICAST &&
430 chk_addr_ret != RTN_BROADCAST)
431 goto out;
/* 获取协议端口号 */
433 snum = ntohs(addr->sin_port);
434 err = -EACCES;
/* 校验当前进程有没有使用低于 1024 端口的能力 */
435 if (snum && snum < PROT_SOCK && !capable(CAP_NET_BIND_SERVICE))
436 goto out;
437
438 /* We keep a pair of addresses. rcv_saddr is the one
439 * used by hash lookups, and saddr is used for transmit.
440 *
441 * In the BSD API these are the same except where it
442 * would be illegal to use them (multicast/broadcast) in
443 * which case the sending device address is used.
444 */
445 lock_sock(sk);
446
447 /* Check these errors (active socket, double bind). */
448 err = -EINVAL;
/* 检查socket是否已经被绑定过了: 用了两个检查项, 一个是 sk 状态, 另一个是是否已经绑定过端口了
当然地址本来就可以为0,所以不能做为检查项 */
449 if (sk->sk_state != TCP_CLOSE || inet->num)
450 goto out_release_sock;
451 /* 绑定inet的接收地址(地址服务绑定地址)和来源地址为用户态指定地址 */
452 inet->rcv_saddr = inet->saddr = addr->sin_addr.s_addr;
/* 若地址类型为广播或多播,则将地址置 0,表示直接使用网络设备 */
453 if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)
454 inet->saddr = 0; /* Use device */
455
/* 调用协议的 get_port 函数,确认是否可绑定端口.
* 若可以, 则绑定在 inet->num 之上, 注意这里虽然没有把inet传过去,但是第一个参数sk
* 它本身和 inet是可以互相转化的 */
456 /* Make sure we are allowed to bind here. */
457 if (sk->sk_prot->get_port(sk, snum)) {
458 inet->saddr = inet->rcv_saddr = 0;
459 err = -EADDRINUSE;
460 goto out_release_sock;
461 }
462 /* 如果端口和地址可以绑定,置标志位 */
463 if (inet->rcv_saddr)
464 sk->sk_userlocks |= SOCK_BINDADDR_LOCK;
465 if (snum)
466 sk->sk_userlocks |= SOCK_BINDPORT_LOCK;
/* inet的 sport(来源端口)成员也置为绑定端口 */
467 inet->sport = htons(inet->num);
468 inet->daddr = 0;
469 inet->dport = 0;
470 sk_dst_reset(sk);
471 err = 0;
472 out_release_sock:
473 release_sock(sk);
474 out:
475 return err;
476 }
上述分析中忽略的第一个细节是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[/code]
另一个就是协议的端口绑定,调用了协议的get_port函数,如果是SOCK_STREAM的TCP协议,那么它
就是tcp_v4_get_port()函数.
协议端口的绑定
要分析这个函数还是得先绕一些基本的东东,这里涉及到内核中提供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 用。
阅读(518) | 评论(0) | 转发(0) |