当创建了一个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 229 { 230 if( 231 return -EINVAL; 232 if( 233 return 0; 234 if(copy_from_user(kaddr,uaddr, 235 return -EFAULT; 236 return audit_sockaddr( 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 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 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 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 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 用。 01-04 04:17