Socket接口的分層
socket的英文原本意思是 孔 或 插座。但在計(jì)算機(jī)科學(xué)中通常被稱作為 套接字,主要用于相同機(jī)器的不同進(jìn)程間或者不同機(jī)器間的通信。Socket的使用很多網(wǎng)絡(luò)編程的書籍都有介紹,所以本文不打算介紹Socket的使用,只討論Socket的具體實(shí)現(xiàn),所以如果對Socket不太了解的同學(xué)可以先查閱Socket相關(guān)的資料或者書籍。
在Linux內(nèi)核中,Socket的實(shí)現(xiàn)分為三層,第一層是 GLIBC接口層,第二層是 BSD接口層,第三層是 具體的協(xié)議層(如Unix sokcet或者INET socket)。如下圖所示:

GLIBC層在用戶態(tài)實(shí)現(xiàn),提供一系列的socket族系統(tǒng)調(diào)用讓用戶使用。BSD層在內(nèi)核態(tài)實(shí)現(xiàn),主要是為了讓不同的協(xié)議能夠使用同一套接口來訪問而創(chuàng)造的,如上圖所示, Unix socket 和 Inet socket 都可以通過接入 BSD接口層 來向用戶提供相同的接口。 具體的協(xié)議層 是為了實(shí)現(xiàn)不同的協(xié)議或者功能而存在的,如 Unix socket 主要是用于進(jìn)程間通信,Inet socket 主要用于網(wǎng)絡(luò)數(shù)據(jù)傳輸?shù)取?/p>
socket的英文原本意思是 孔 或 插座。但在計(jì)算機(jī)科學(xué)中通常被稱作為 套接字,主要用于相同機(jī)器的不同進(jìn)程間或者不同機(jī)器間的通信。Socket的使用很多網(wǎng)絡(luò)編程的書籍都有介紹,所以本文不打算介紹Socket的使用,只討論Socket的具體實(shí)現(xiàn),所以如果對Socket不太了解的同學(xué)可以先查閱Socket相關(guān)的資料或者書籍。
在Linux內(nèi)核中,Socket的實(shí)現(xiàn)分為三層,第一層是 GLIBC接口層,第二層是 BSD接口層,第三層是 具體的協(xié)議層(如Unix sokcet或者INET socket)。如下圖所示:

GLIBC層在用戶態(tài)實(shí)現(xiàn),提供一系列的socket族系統(tǒng)調(diào)用讓用戶使用。BSD層在內(nèi)核態(tài)實(shí)現(xiàn),主要是為了讓不同的協(xié)議能夠使用同一套接口來訪問而創(chuàng)造的,如上圖所示, Unix socket 和 Inet socket 都可以通過接入 BSD接口層 來向用戶提供相同的接口。 具體的協(xié)議層 是為了實(shí)現(xiàn)不同的協(xié)議或者功能而存在的,如 Unix socket 主要是用于進(jìn)程間通信,Inet socket 主要用于網(wǎng)絡(luò)數(shù)據(jù)傳輸?shù)取?/p>
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個(gè)人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)? ? ?


GLIBC接口層
GLIBC接口層 提供了一系列的接口函數(shù)供用戶使用(可以成為 Socket族系統(tǒng)調(diào)用),如下:
socket()
bind()
listen()
accept()
connect()
recv()
send()
recvfrom()
sendto()
...
例如 socket() 接口用于創(chuàng)建一個(gè)socket句柄,而 bind() 函數(shù)將一個(gè)socket綁定到指定的IP和端口上。當(dāng)然,系統(tǒng)調(diào)用最終都會調(diào)用到內(nèi)核態(tài)的某個(gè)內(nèi)核函數(shù)來進(jìn)行處理,在系統(tǒng)調(diào)用一章我們介紹過相關(guān)的原理,所以這里只會介紹一下這些系統(tǒng)調(diào)用最終會調(diào)用哪些內(nèi)核函數(shù)。
GLIBC層實(shí)現(xiàn)原理
我們先來看看 GLIBC 是怎么定義這些系統(tǒng)調(diào)用的吧,首先來看看 socket() 函數(shù)的定義如下:
雖然 socket() 函數(shù)是使用匯編來實(shí)現(xiàn)的,但是也比較容易理解,我們已經(jīng)知道在用戶態(tài)必須使用 int 0x80 中斷來觸發(fā)系統(tǒng)調(diào)用的,而要調(diào)用的系統(tǒng)調(diào)用編號保存在寄存器 eax 中,第一個(gè)參數(shù)保存在 ebx 寄存器中,而第二個(gè)參數(shù)保存在 ecx中。
所以從上面的代碼可以看出,調(diào)用 socket() 函數(shù)時(shí)會把 eax 的值設(shè)置為 sys_socketcall,把 ebx 的值會設(shè)置為 SOCKOP_socket,而把 ecx 的值設(shè)置為調(diào)用 socket() 函數(shù)時(shí)第一個(gè)參數(shù)的地址。然后通過代碼 int 0x80 來觸發(fā)一次系統(tǒng)調(diào)用中斷,那么最終調(diào)用的是 sys_socketcall() 內(nèi)核函數(shù),而第一個(gè)參數(shù)的值為 SOCKOP_socket,第二個(gè)參數(shù)的值為調(diào)用 socket() 函數(shù)時(shí)第一個(gè)參數(shù)的地址。
那么 bind() 函數(shù)又是怎么定義的呢?因?yàn)橛辛?socket() 函數(shù)的定義,那么所有 Socket族系統(tǒng)調(diào)用 都可以使用這個(gè)模板來實(shí)現(xiàn),例如 bind() 函數(shù)的定義如下:
可以看到,bind() 函數(shù)直接套用了 socket() 函數(shù)實(shí)現(xiàn)的模板,只是把 socket 這個(gè)名字替換成 bind 而已,替換之后 ebx 的值就會變成 SOCKOP_bind,其他都跟 socket() 函數(shù)一樣,所以這時(shí)傳給 sys_socketcall() 函數(shù)的第一個(gè)參數(shù)就變成 SOCKOP_bind了。
BSD接口層
前面說了,BSD接口層 是為了能夠使用相同的接口來操作不同協(xié)議而創(chuàng)造的。有面向?qū)ο缶幊探?jīng)驗(yàn)的讀者可能會發(fā)現(xiàn),BSD接口層 使用的技巧與面向?qū)ο蟮?接口 概念非常相似。主要的方式是 BSD接口層 定義了一些接口,具體的協(xié)議層 必須實(shí)現(xiàn)這些接口才能接入到 BSD接口層。
為了實(shí)現(xiàn)這種機(jī)制,Linux定義了一個(gè) struct socket 的結(jié)構(gòu)體,每個(gè)socket都與一個(gè) struct socket 的結(jié)構(gòu)對應(yīng),其定義如下:
可以把這個(gè)結(jié)構(gòu)體想象成鉤子,要在上面掛什么由用戶自己決定。其比較重要的字段是 ops 和 sk。ops 字段類型為 struct proto_ops,其定義了一系列操作socket的方法。而 sk 字段的類型為 struct sock, 用于保存具體協(xié)議所操作的真實(shí)對象。
我們先來看看 struct proto_ops 結(jié)構(gòu)的定義:
從上面的代碼可以看出,struct proto_ops 結(jié)構(gòu)主要是定義一系列的函數(shù)接口,每個(gè) 具體的協(xié)議層 必須提供一個(gè) struct proto_ops 結(jié)構(gòu)掛載到 struct socket 結(jié)構(gòu)的 ops 字段上。所以當(dāng)用戶調(diào)用 bind() 系統(tǒng)調(diào)用時(shí)真實(shí)調(diào)用的是:socket->ops->bind()。
sys_socketcall()函數(shù)
前面說過,所有的 Socket族系統(tǒng)調(diào)用 最終都會調(diào)用 sys_socketcall() 函數(shù)來處理用戶的請求,我們來看看 sys_socketcall() 函數(shù)的實(shí)現(xiàn):
從 sys_socketcall() 函數(shù)可以看出,根據(jù)參數(shù) call 不同的值會調(diào)用不同的內(nèi)核函數(shù),譬如 call 的值為 SYS_SOCKET 時(shí)會調(diào)用 sys_socket() 函數(shù),而 call 的值為 SYS_BIND 時(shí)會調(diào)用 sys_bind() 函數(shù)。而參數(shù) args 就是在用戶態(tài)給 Socket族系統(tǒng)調(diào)用 傳入的參數(shù)列表地址,Linux內(nèi)核會先使用 copy_from_user() 函數(shù)把這些參數(shù)復(fù)制到內(nèi)核空間。
前面說過,在用戶空間調(diào)用 socket() 系統(tǒng)調(diào)用時(shí)會把參數(shù) call 的值設(shè)置為 SOCKOP_socket,它的值跟 sys_socketcall() 函數(shù)中 SYS_SOCKET 是一致的,我們可以通過下面的代碼看出端倪:
從上面的定義可以看出,在 GLIBC 中的定義跟 Linux 內(nèi)核中的定義是一一對應(yīng)的。
所以從中得到,當(dāng)在用戶態(tài)調(diào)用 socket() 函數(shù)時(shí)實(shí)際調(diào)用的是 sys_socket() 內(nèi)核函數(shù),其他的 Socket族系統(tǒng)調(diào)用 道理與 socket() 系統(tǒng)調(diào)用一致。
通過下面一幅圖來展示 Socket族系統(tǒng)調(diào)用 的原理:

sys_socket()函數(shù)
sys_socket() 函數(shù)用于創(chuàng)建一個(gè) socket 對象,并且返回一個(gè)文件描述符。其實(shí)現(xiàn)如下:
參數(shù) family 指定 具體協(xié)議層,可以選擇的協(xié)議非常多,下面列舉幾個(gè):
例如 AF_UNIX 指定的是 Unix socket,AF_INET 指定的是 以太網(wǎng)協(xié)議 等。而參數(shù) type 用于指定傳輸數(shù)據(jù)的類型,有一下幾種選擇:
例如 SOCK_STREAM 類型指定的是流方式,而 SOCK_DGRAM 類型指定的是數(shù)據(jù)報(bào)方式等。最后一個(gè) protocol 參數(shù)看起來也是協(xié)議的意思,跟 family 好像重復(fù)了。事實(shí)上 family 所指定的協(xié)議偏向于物理介質(zhì),如 Unix socket 是用于進(jìn)程間通信的,而 Inet socket 是用于以太網(wǎng)傳輸數(shù)據(jù)的。而 protocol 所指定的協(xié)議偏向于邏輯上的協(xié)議,如 TCP、UDP 等。舉個(gè)栗子,如果把 family 比作是不同交通工具(飛機(jī)、汽車、火車等)的話,那么 protocol 就是大巴、的士和小車。
sys_socket() 函數(shù)首先調(diào)用 sock_create() 創(chuàng)建一個(gè) struct socket 結(jié)構(gòu),然后通過調(diào)用 sock_map_fd() 函數(shù)把此 struct socket 結(jié)構(gòu)與一個(gè)文件描述符關(guān)聯(lián)起來,最后把文件描述符返回給用戶。我們先來看看 sock_create() 函數(shù)的實(shí)現(xiàn):
sock_create() 函數(shù)首先調(diào)用 sock_alloc() 申請一個(gè) struct socket 結(jié)構(gòu),然后調(diào)用指定協(xié)議族的 create() 函數(shù)(net_families[family]->create())進(jìn)行進(jìn)一步的創(chuàng)建功能。net_families 變量的類型為 struct net_proto_family,其定義如下:
family 字段對應(yīng)的就是具體的協(xié)議族,而 create 字段指定了其創(chuàng)建socket的方法。一個(gè)具體協(xié)議族需要通過調(diào)用 sock_register() 函數(shù)向系統(tǒng)注冊其創(chuàng)建socket的方法。例如 Unix socket 就在初始化時(shí)通過下面的代碼注冊:
所以從上面的代碼可以指定,對于 Unix socket 的話,net_families[family]->create() 這行代碼實(shí)際調(diào)用的是 unix_create() 函數(shù)。
