1. 程式人生 > >走進Linux核心網路 套接字的祕密—socket與sock

走進Linux核心網路 套接字的祕密—socket與sock

雙十一

今天是雙十一,記得還在念書的時候,每次都會參加京東圖書滿200-100的活動,然後腦子一熱屯一堆書。印象中人文類的書基本都會看,而電子資訊類的可能就只看了一半,甚至買過哪些都忘了。究其原因,主要是人文類的通常都是自己想看的,而技術類的就跟風看哪個銷量大買哪個,比如下面這本《UNIX 網路程式設計》

如果現在讓我給別人推薦學習網路程式設計的書,我一定不會推薦這本。為什麼呢? 倒不是因為書中內容說的不對,而是我認為這本書更適合當工具書查閱,而不適合逐章學習。就像《新華字典》裡漢字很多,但也沒見誰是用它來學認字的。不過,對於當年的我來說,《UNIX 網路程式設計》中提供的例子倒也能提供最基本的套接字程式設計體驗。比如從中我第一次接觸到了套接字

的概念,也接觸到了 socket()bind() , listen()accept()send() , recv() 這些函式的使用方法。不過有些問題當時的我並不明白,比如建立套接字時為什麼要返回一個描述符,為什麼傳送接收時要填寫這個描述符,難道套接字就是這個描述符?

:儘管“套接字”就是英文socket的翻譯,但本文中,為了避免和之後的socket混淆,所以在從網路程式設計的角度描述時,使用的是中文“套接字”。

套接字究竟是什麼

如果你知道Linux系統中程序間通訊的方式,就應該知道套接字也是其中一種。但套接字特別之處在於它不僅可以用來實現同一臺主機上程序間的通訊,還可以用來實現主機間的程序間

的通訊。通訊的雙方各自開啟一個套接字,套接字之間通過通訊鏈路相連。

如果把兩個套接字之間的‘連線’比喻成‘水管’,那麼套接字就是‘水龍頭’

Unix有一句格言:everything is a file,即‘萬物皆檔案’,套接字也不例外。那麼如何把套接字和檔案聯絡起來呢? 答案就是通過下面這張圖。 file 其中task_struct表示一個程序,files_struct中的fd_array[]表示該程序開啟的所有描述符,對於套接字來說,與其他型別檔案的區別就是最終f_op指向的是socket_file_ops。不過,可以看到,這裡的socket_file_ops只有一些通用的操作,並沒有sendrecv

。特有的操作通過 socketcall() 區分的。

socket 和 sock

終於到今天的主角了。實際上,對每一個新建立的套接字,核心協議棧都會建立struct socketstruct sock兩個資料結構。這兩個結構就像孿生兄弟,struct socket面向使用者空間,struct sock面向核心空間。

struct socket

struct socket簡化版的結構如下:

<net.h>
struct socket {
	unsigned long flags;
	const struct proto_ops *ops;
	struct file   *file;
	struct sock   *sk;
    short type;
};

其中type表示協議,這是在建立套接字的時候的protocol引數確定的

int socket(int domain, int type, int protocol);

file指標指向上面那張圖中的struct file結構,通過它,socket便與檔案系統關聯了起來。 sk指向孿生的兄弟sock結構。 socket結構中最重要的要數ops指標了,根據協議型別,它指向一種特定協議的實現。比如TCP的就是inet_stream_ops,ICMP、UDP協議對應inet_dgram_ops,RAWIP對應的是inet_sockraw_ops同樣地,這些也都在建立套接字的時候就決定了。 struct proto_ops的簡化版本的結構如下

<net.h>
struct proto_ops{
   int family;
   int (*bind)(struct socket *sock, struct sockaddr *myaddr, int sockaddr_len);
   int (*connect)(struct socket *sock,struct sockaddr *vaddr,int sockaddr_len, int flags);
   int	(*accept)(struct socket *sock, struct socket *newsock, int flags);
   int	(*sendmsg)(struct socket *sock, struct msghdr *m, size_t total_len);
}

其中的介面名字是不是很熟悉?是的,它們和進行網路程式設計時呼叫的C庫中函式名字是一樣的。以sendmsg為例,真實的呼叫過程是這樣 sendmsg

即當用戶呼叫sendmsg時,核心會找到描述符fd對應的struct socket結構,然後呼叫sock->ops->sendmsg執行特定協議的傳送。

那麼,ops欄位什麼時候被賦值呢?答案是,在建立struct sock結構前。

struct sock

struct sock的簡化結構如下圖所示

struct sock_common {
	struct proto		*skc_prot;
};
struct sock {
	struct sock_common	__sk_common;

	struct sk_buff_head	sk_receive_queue;
	struct sk_buff_head	sk_write_queue;
};

其中最重要的欄位就是skc_prot,它也是協議相關的。作為struct socket結構的孿生兄弟,struct sock結構也是在使用者建立套接字時就建立的。 sock-create

sock_alloc建立了struct socket結構,隨後,根據使用者傳入的family,查詢陣列net_families,找到對應的函式指標,呼叫create.

net_families儲存著核心啟動時註冊(通過sock_register)的 socket protocol handler,比如以下幾種:

static const struct net_proto_family inet_family_ops = {
	.family = PF_INET,
	.create = inet_create,
	.owner	= THIS_MODULE,
};
static const struct net_proto_family netlink_family_ops = {
	.family = PF_NETLINK,
	.create = netlink_create,
	.owner	= THIS_MODULE,	/* for consistency 8) */
};
static const struct net_proto_family packet_family_ops = {
	.family =	PF_PACKET,
	.create =	packet_create,
	.owner	=	THIS_MODULE,
};
static const struct net_proto_family unix_family_ops = {
	.family = PF_UNIX,
	.create = unix_create,
	.owner	= THIS_MODULE,};

如果我們建立套接字時指定的family是PF_INET,那麼此時我們就會呼叫inet_create

static int inet_create(struct net *net, struct socket *sock, int protocol,
		       int kern)
{
	struct sock *sk;
	struct inet_protosw *answer;
	
	list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
		err = 0;
		/* Check the non-wild match. */
		if (protocol == answer->protocol) {
			if (protocol != IPPROTO_IP)
				break;
		} else {
			/* Check for the two wild cases. */
			if (IPPROTO_IP == protocol) {
				protocol = answer->protocol;
				break;
			}
			if (IPPROTO_IP == answer->protocol)
				break;
		}
		err = -EPROTONOSUPPORT;
	}

	sock->ops = answer->ops;
	sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);
    if (sk->sk_prot->init) {
		err = sk->sk_prot->init(sk);
	}
}

首先是從inetsw陣列搜尋匹配對應的協議,inetsw稱為協議開關表,核心啟動時,通過inet_register_protosw進行註冊。比如下面幾項都會被註冊 inetsw

inetsw中註冊的每種協議都有opsprot兩個欄位,前者與struct socket結構關聯到一起,後者與struct sock關聯到一起。在inet_create中,struct socketops欄位和struct socksk_prot欄位被賦值。

以我們建立的套接字型別是TCP為例,此時struct socketstruct sock的關係如下relate

還是繼續剛才sendmsg的過程,由於struct socketops指向inet_stream_ops,所以實際呼叫的就是inet_sendmsg inet_sendmsg

結果,最終是呼叫其孿生兄弟struct socksk_protsendmsg,即tcp_sendmsg,這時,才是資料真正開始協議棧旅程的地方……