當建立了一個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 描述符、協定簇名稱、本地位址、端口這
些參數
<b>sys_bind的實作</b>
操作 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 }<b></b>
在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(ulenMAX_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.
<b></b>
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()函數.
<b>協定端口的綁定</b>
要分析這個函數還是得先繞一些基本的東東,這裡涉及到核心中提供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)
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) {
} 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;
tb->fastreuse = 0;
} else if (tb->fastreuse && (!sk->sk_reuse || sk->sk_state == TCP_LISTEN))
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 用。