1. 程式人生 > >【Linux 核心網路協議棧原始碼剖析】bind 函式剖析

【Linux 核心網路協議棧原始碼剖析】bind 函式剖析

socket 函式並沒有為套接字繫結本地地址和埠號,對於伺服器端則必須顯性繫結地址和埠號。bind 函式主要是伺服器端使用,把一個本地協議地址賦予套接字。

1、應用層——bind 函式

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
/*sockfd是由socket函式返回的套介面描述字,第二個引數是一個指向特定於協議的地址結構的指標,第三個引數是該地址結構的長度*/

bind 函式的功能則是將socket 套接字繫結指定的地址。

2、BSD Socket 層——sock_bind 函式

同樣是通過一個共同的入口函式 sys_socket(參見 socket 函式剖析
/*
 *	Bind a name to a socket. Nothing much to do here since it's
 *	the protocol's responsibility to handle the local address.
 *
 *	We move the socket address to kernel space before we call
 *	the protocol layer (having also checked the address is ok).
 */
 //bind函式對應的BSD層函式,用於繫結一個本地地址,伺服器端
 //umyaddr表示需要繫結的地址結構,addrlen表示改地址結構的長度
 //這裡的fd,即為套接字描述符
static int sock_bind(int fd, struct sockaddr *umyaddr, int addrlen)
{
	struct socket *sock;
	int i;
	char address[MAX_SOCK_ADDR];
	int err;
    //套接字引數有效性檢查
	if (fd < 0 || fd >= NR_OPEN || current->files->fd[fd] == NULL)
		return(-EBADF);
	//獲取fd對應的socket結構
	if (!(sock = sockfd_lookup(fd, NULL))) 
		return(-ENOTSOCK);
    //將地址從使用者緩衝區複製到核心緩衝區,umyaddr->address
	if((err=move_addr_to_kernel(umyaddr,addrlen,address))<0)
	  	return err;
    //轉呼叫bind指向的函式,下層函式(inet_bind)
	if ((i = sock->ops->bind(sock, (struct sockaddr *)address, addrlen)) < 0) 
	{
		return(i);
	}
	return(0);
}

sock_bind 函式主要就是將使用者緩衝區的地址結構複製到核心緩衝區,然後轉呼叫下一層的bind函式。

該函式內部的一個使用者空間與核心資料空間資料拷貝的函式

//從uaddr拷貝ulen大小的資料到kaddr,實現地址使用者空間到核心地址空間的資料拷貝
static int move_addr_to_kernel(void *uaddr, int ulen, void *kaddr)
{
	int err;
	if(ulen<0||ulen>MAX_SOCK_ADDR)
		return -EINVAL;
	if(ulen==0)
		return 0;
	//檢查使用者空間的指標所指的指定大小儲存塊是否可讀
	if((err=verify_area(VERIFY_READ,uaddr,ulen))<0)
		return err;
	memcpy_fromfs(kaddr,uaddr,ulen);//實質是memcpy函式
	return 0;
}
3、INET Socket 層——inet_bind 函式
/* this needs to be changed to disallow
   the rebinding of sockets.   What error
   should it return? */
//完成本地地址繫結,本地地址繫結包括IP地址和埠號兩個部分
static int inet_bind(struct socket *sock, struct sockaddr *uaddr,int addr_len)
{
	struct sockaddr_in *addr=(struct sockaddr_in *)uaddr;
	struct sock *sk=(struct sock *)sock->data, *sk2;
	unsigned short snum = 0 /* Stoopid compiler.. this IS ok */;
	int chk_addr_ret;

	/* check this error. */
	//在進行地址繫結時,該套接字應該處於關閉狀態
	if (sk->state != TCP_CLOSE)
		return(-EIO);
	//地址長度欄位校驗
	if(addr_len<sizeof(struct sockaddr_in))
		return -EINVAL;

    //非原始套接字型別,繫結前,沒有埠號,則繫結埠號
	if(sock->type != SOCK_RAW)
	{
		if (sk->num != 0)//從inet_create函式可以看出,非原始套接字型別,埠號是初始化為0的 
			return(-EINVAL);

		snum = ntohs(addr->sin_port);//將地址結構中的埠號轉為主機位元組順序

		/*
		 * We can't just leave the socket bound wherever it is, it might
		 * be bound to a privileged port. However, since there seems to
		 * be a bug here, we will leave it if the port is not privileged.
		 */
		 //如果埠號為0,則自動分配一個
		if (snum == 0) 
		{
			snum = get_new_socknum(sk->prot, 0);//得到一個新的埠號
		}
		//埠號有效性檢驗,1024以上,超級使用者許可權
		if (snum < PROT_SOCK && !suser()) 
			return(-EACCES);
	}
	//下面則進行ip地址繫結
	//檢查地址是否是一個本地介面地址
	chk_addr_ret = ip_chk_addr(addr->sin_addr.s_addr);
	//如果指定的地址不是本地地址,並且也不是一個多播地址,則錯誤返回
	if (addr->sin_addr.s_addr != 0 && chk_addr_ret != IS_MYADDR && chk_addr_ret != IS_MULTICAST)
		return(-EADDRNOTAVAIL);	/* Source address MUST be ours! */
	//如果沒有指定地址,則系統自動分配一個本地地址  	
	if (chk_addr_ret || addr->sin_addr.s_addr == 0)
		sk->saddr = addr->sin_addr.s_addr;//本地地址繫結
	
	if(sock->type != SOCK_RAW)
	{
		/* Make sure we are allowed to bind here. */
		cli();
	
		//for迴圈主要是檢查檢查有無衝突的埠號以及本地地址,有衝突,但不允許地址複用,肯定錯誤退出
		//成功跳出for迴圈時,已經定位到了雜湊表sock_array指定索引的連結串列的末端
		for(sk2 = sk->prot->sock_array[snum & (SOCK_ARRAY_SIZE -1)];
					sk2 != NULL; sk2 = sk2->next) 
		{
		/* should be below! */
			if (sk2->num != snum) //沒有重複,繼續搜尋下一個
				continue;//除非有重複,否則後面的程式碼將不會被執行
			if (!sk->reuse)//埠號重複,如果沒有設定地址複用標誌,退出
			{
				sti();
				return(-EADDRINUSE);
			}
			
			if (sk2->num != snum) 
				continue;		/* more than one */
			if (sk2->saddr != sk->saddr) //地址和埠一個意思
				continue;	/* socket per slot ! -FB */
			//如果狀態是LISTEN表明該套接字是一個服務端,服務端不可使用地址複用選項
			if (!sk2->reuse || sk2->state==TCP_LISTEN) 
			{
				sti();
				return(-EADDRINUSE);
			}
		}
		sti();

		remove_sock(sk);//將sk sock結構從其之前的表中刪除,inet_create中 put_sock,這裡remove_sock
		put_sock(snum, sk);//然後根據新分配的埠號插入到新的表中。可以得知系統在維護許多這樣的表
		sk->dummy_th.source = ntohs(sk->num);//tcp首部,源埠號繫結
		sk->daddr = 0;//sock結構所代表套接字的遠端地址
		sk->dummy_th.dest = 0;//tcp首部,目的埠號
	}
	return(0);
}
inet_bind 函式即為bind函式的最底層實現,該函式實現了本地地址和埠號的繫結,其中還針對上層傳過來的地址結構進行校驗,檢查是否衝突可用。需要清楚的是 sock_array陣列,這其實是一個鏈式雜湊表,裡面儲存的就是各個埠號的sock結構,陣列大小小於埠號,所以採用鏈式雜湊表儲存。

bind 函式的各層分工很明顯,主要就是inet_bind函數了,在註釋裡說的很明確了,bind 是繫結本地地址,它不負責對端地址,一般用於伺服器端,客戶端是系統指定的。

一般是伺服器端呼叫這個函式,到了這一步,伺服器端套接字綁定了本地地址資訊(ip地址和埠號),但是不知道對端(客戶端)的地址資訊。