Socket介面(基於 Linux-2.4.0)
Socket介面的分層
Socket的英文原本意思是 孔
或 插座
。但在電腦科學中通常被稱作為 套接字
,主要用於相同機器的不同程序間或者不同機器間的通訊。Socket的使用很多網路程式設計的書籍都有介紹,所以本文不打算介紹Socket的使用,只討論Socket的具體實現,所以如果對Socket不太瞭解的同學可以先查閱Socket相關的資料或者書籍。
在Linux核心中,Socket的實現分為三層,第一層是 GLIBC介面層
,第二層是 BSD介面層
,第三層是 具體的協議層
(如Unix sokcet或者INET socket)。如下圖所示:
GLIBC層在使用者態實現,提供一系列的socket族系統呼叫讓使用者使用。BSD層在核心態實現,主要是為了讓不同的協議能夠使用同一套介面來訪問而創造的,如上圖所示, Unix socket
Inet socket
都可以通過接入 BSD介面層
來向用戶提供相同的介面。 具體的協議層
是為了實現不同的協議或者功能而存在的,如 Unix socket
主要是用於程序間通訊,Inet socket
主要用於網路資料傳輸等。
GLIBC介面層
GLIBC介面層
提供了一系列的介面函式供使用者使用(可以成為 Socket族系統呼叫
),如下:
- socket()
- bind()
- listen()
- accept()
- connect()
- recv()
- send()
- recvfrom()
- sendto()
- ...
例如 socket()
介面用於建立一個socket控制代碼,而 bind()
GLIBC層實現原理
我們先來看看 GLIBC
是怎麼定義這些系統呼叫的吧,首先來看看 socket()
函式的定義如下:
#define P(a, b) P2(a, b) #define P2(a, b) a##b .text .globl P(__,socket) ENTRY (P(__,socket)) movl %ebx, %edx movl $SYS_ify(socketcall), %eax // 系統呼叫號 movl $P(SOCKOP_,socket), %ebx // 系統呼叫的第一個引數 lea 4(%esp), %ecx // 系統呼叫的第二個引數 int $0x80 movl %edx, %ebx cmpl $-125, %eax jae syscall_error ret
雖然 socket()
函式是使用匯編來實現的,但是也比較容易理解,我們已經知道在使用者態必須使用 int 0x80
中斷來觸發系統呼叫的,而要呼叫的系統呼叫編號儲存在暫存器 eax
中,第一個引數儲存在 ebx
暫存器中,而第二個引數儲存在 ecx
中。
所以從上面的程式碼可以看出,呼叫 socket()
函式時會把 eax
的值設定為 sys_socketcall
,把 ebx
的值會設定為 SOCKOP_socket
,而把 ecx
的值設定為呼叫 socket()
函式時第一個引數的地址。然後通過程式碼 int 0x80
來觸發一次系統呼叫中斷,那麼最終呼叫的是 sys_socketcall()
核心函式,而第一個引數的值為 SOCKOP_socket
,第二個引數的值為呼叫 socket()
函式時第一個引數的地址。
那麼 bind()
函式又是怎麼定義的呢?因為有了 socket()
函式的定義,那麼所有 Socket族系統呼叫
都可以使用這個模板來實現,例如 bind()
函式的定義如下:
#define socket bind
#include <socket.S>
可以看到,bind()
函式直接套用了 socket()
函式實現的模板,只是把 socket
這個名字替換成 bind
而已,替換之後 ebx
的值就會變成 SOCKOP_bind
,其他都跟 socket()
函式一樣,所以這時傳給 sys_socketcall()
函式的第一個引數就變成 SOCKOP_bind
了。
BSD介面層
前面說了,BSD介面層
是為了能夠使用相同的介面來操作不同協議而創造的。有面向物件程式設計經驗的讀者可能會發現,BSD介面層
使用的技巧與面向物件的 介面
概念非常相似。主要的方式是 BSD介面層
定義了一些介面,具體的協議層
必須實現這些接口才能接入到 BSD介面層
。
為了實現這種機制,Linux定義了一個 struct socket
的結構體,每個socket都與一個 struct socket
的結構對應,其定義如下:
struct socket
{
socket_state state;
unsigned long flags;
struct proto_ops *ops;
struct inode *inode;
struct fasync_struct *fasync_list;
struct file *file;
struct sock *sk;
wait_queue_head_t wait;
short type;
unsigned char passcred;
};
可以把這個結構體想象成鉤子,要在上面掛什麼由使用者自己決定。其比較重要的欄位是 ops
和 sk
。ops
欄位型別為 struct proto_ops
,其定義了一系列操作socket的方法。而 sk
欄位的型別為 struct sock
, 用於儲存具體協議所操作的真實物件。
我們先來看看 struct proto_ops
結構的定義:
struct proto_ops {
int family;
int (*release)(struct socket *sock);
int (*bind)(struct socket *sock, struct sockaddr *umyaddr,
int sockaddr_len);
int (*connect)(struct socket *sock, struct sockaddr *uservaddr,
int sockaddr_len, int flags);
int (*socketpair)(struct socket *sock1, struct socket *sock2);
int (*accept)(struct socket *sock, struct socket *newsock,
int flags);
int (*getname)(struct socket *sock, struct sockaddr *uaddr,
int *usockaddr_len, int peer);
unsigned int (*poll) (struct file *file, struct socket *sock, struct poll_table_struct *wait);
int (*ioctl)(struct socket *sock, unsigned int cmd,
unsigned long arg);
int (*listen)(struct socket *sock, int len);
int (*shutdown)(struct socket *sock, int flags);
int (*setsockopt)(struct socket *sock, int level, int optname,
char *optval, int optlen);
int (*getsockopt)(struct socket *sock, int level, int optname,
char *optval, int *optlen);
int (*sendmsg)(struct socket *sock, struct msghdr *m, int total_len, struct scm_cookie *scm);
int (*recvmsg)(struct socket *sock, struct msghdr *m, int total_len, int flags, struct scm_cookie *scm);
int (*mmap)(struct file *file, struct socket *sock, struct vm_area_struct * vma);
};
從上面的程式碼可以看出,struct proto_ops
結構主要是定義一系列的函式介面,每個 具體的協議層
必須提供一個 struct proto_ops
結構掛載到 struct socket
結構的 ops
欄位上。所以當用戶呼叫 bind()
系統呼叫時真實呼叫的是:socket->ops->bind()
。
sys_socketcall()函式
前面說過,所有的 Socket族系統呼叫
最終都會呼叫 sys_socketcall()
函式來處理使用者的請求,我們來看看 sys_socketcall()
函式的實現:
asmlinkage long sys_socketcall(int call, unsigned long *args)
{
unsigned long a[6];
unsigned long a0,a1;
int err;
if(call<1||call>SYS_RECVMSG)
return -EINVAL;
/* copy_from_user should be SMP safe. */
if (copy_from_user(a, args, nargs[call]))
return -EFAULT;
a0=a[0];
a1=a[1];
switch(call)
{
case SYS_SOCKET:
err = sys_socket(a0,a1,a[2]);
break;
case SYS_BIND:
err = sys_bind(a0,(struct sockaddr *)a1, a[2]);
break;
case SYS_CONNECT:
err = sys_connect(a0, (struct sockaddr *)a1, a[2]);
break;
...
}
return err;
}
從 sys_socketcall()
函式可以看出,根據引數 call
不同的值會呼叫不同的核心函式,譬如 call
的值為 SYS_SOCKET
時會呼叫 sys_socket()
函式,而 call
的值為 SYS_BIND
時會呼叫 sys_bind()
函式。而引數 args
就是在使用者態給 Socket族系統呼叫
傳入的引數列表地址,Linux核心會先使用 copy_from_user()
函式把這些引數複製到核心空間。
前面說過,在使用者空間呼叫 socket()
系統呼叫時會把引數 call
的值設定為 SOCKOP_socket
,它的值跟 sys_socketcall()
函式中 SYS_SOCKET
是一致的,我們可以通過下面的程式碼看出端倪:
// GLIBC 的定義
#define SOCKOP_socket 1
#define SOCKOP_bind 2
#define SOCKOP_connect 3
#define SOCKOP_listen 4
#define SOCKOP_accept 5
#define SOCKOP_getsockname 6
#define SOCKOP_getpeername 7
#define SOCKOP_socketpair 8
#define SOCKOP_send 9
#define SOCKOP_recv 10
#define SOCKOP_sendto 11
#define SOCKOP_recvfrom 12
#define SOCKOP_shutdown 13
#define SOCKOP_setsockopt 14
#define SOCKOP_getsockopt 15
#define SOCKOP_sendmsg 16
#define SOCKOP_recvmsg 17
// Linux 核心的定義
#define SYS_SOCKET 1 /* sys_socket(2) */
#define SYS_BIND 2 /* sys_bind(2) */
#define SYS_CONNECT 3 /* sys_connect(2) */
#define SYS_LISTEN 4 /* sys_listen(2) */
#define SYS_ACCEPT 5 /* sys_accept(2) */
#define SYS_GETSOCKNAME 6 /* sys_getsockname(2) */
#define SYS_GETPEERNAME 7 /* sys_getpeername(2) */
#define SYS_SOCKETPAIR 8 /* sys_socketpair(2) */
#define SYS_SEND 9 /* sys_send(2) */
#define SYS_RECV 10 /* sys_recv(2) */
#define SYS_SENDTO 11 /* sys_sendto(2) */
#define SYS_RECVFROM 12 /* sys_recvfrom(2) */
#define SYS_SHUTDOWN 13 /* sys_shutdown(2) */
#define SYS_SETSOCKOPT 14 /* sys_setsockopt(2) */
#define SYS_GETSOCKOPT 15 /* sys_getsockopt(2) */
#define SYS_SENDMSG 16 /* sys_sendmsg(2) */
#define SYS_RECVMSG 17 /* sys_recvmsg(2) */
從上面的定義可以看出,在 GLIBC 中的定義跟 Linux 核心中的定義是一一對應的。
所以從中得到,當在使用者態呼叫 socket()
函式時實際呼叫的是 sys_socket()
核心函式,其他的 Socket族系統呼叫
道理與 socket()
系統呼叫一致。
通過下面一幅圖來展示 Socket族系統呼叫
的原理:
sys_socket()函式
sys_socket()
函式用於建立一個 socket 物件,並且返回一個檔案描述符。其實現如下:
asmlinkage long sys_socket(int family, int type, int protocol)
{
int retval;
struct socket *sock;
retval = sock_create(family, type, protocol, &sock);
if (retval < 0)
goto out;
retval = sock_map_fd(sock);
if (retval < 0)
goto out_release;
out:
return retval;
out_release:
sock_release(sock);
return retval;
}
引數 family
指定 具體協議層
,可以選擇的協議非常多,下面列舉幾個:
#define AF_UNIX 1 /* Unix domain sockets */
#define AF_LOCAL 1 /* POSIX name for AF_UNIX */
#define AF_INET 2 /* Internet IP Protocol */
#define AF_AX25 3 /* Amateur Radio AX.25 */
#define AF_IPX 4 /* Novell IPX */
...
例如 AF_UNIX
指定的是 Unix socket
,AF_INET
指定的是 乙太網協議
等。而引數 type
用於指定傳輸資料的型別,有一下幾種選擇:
#define SOCK_STREAM 1 /* stream (connection) socket */
#define SOCK_DGRAM 2 /* datagram (conn.less) socket */
#define SOCK_RAW 3 /* raw socket */
#define SOCK_RDM 4 /* reliably-delivered message */
#define SOCK_SEQPACKET 5 /* sequential packet socket */
#define SOCK_PACKET 10 /* linux specific way of */
例如 SOCK_STREAM
型別指定的是流方式,而 SOCK_DGRAM
型別指定的是資料報方式等。最後一個 protocol
引數看起來也是協議的意思,跟 family
好像重複了。事實上 family
所指定的協議偏向於物理介質,如 Unix socket
是用於程序間通訊的,而 Inet socket
是用於乙太網傳輸資料的。而 protocol
所指定的協議偏向於邏輯上的協議,如 TCP
、UDP
等。舉個栗子,如果把 family
比作是不同交通工具(飛機、汽車、火車等)的話,那麼 protocol
就是大巴、的士和小車。
sys_socket()
函式首先呼叫 sock_create()
建立一個 struct socket
結構,然後通過呼叫 sock_map_fd()
函式把此 struct socket
結構與一個檔案描述符關聯起來,最後把檔案描述符返回給使用者。我們先來看看 sock_create()
函式的實現:
int sock_create(int family, int type, int protocol, struct socket **res)
{
int i;
struct socket *sock;
...
net_family_read_lock();
...
if (!(sock = sock_alloc())) {
printk(KERN_WARNING "socket: no more sockets\n");
i = -ENFILE;
goto out;
}
sock->type = type;
if ((i = net_families[family]->create(sock, protocol)) < 0) {
sock_release(sock);
goto out;
}
*res = sock;
out:
net_family_read_unlock();
return i;
}
sock_create()
函式首先呼叫 sock_alloc()
申請一個 struct socket
結構,然後呼叫指定協議族的 create()
函式(net_families[family]->create()
)進行進一步的建立功能。net_families
變數的型別為 struct net_proto_family
,其定義如下:
struct net_proto_family {
int family;
int (*create)(struct socket *sock, int protocol);
...
};
family
欄位對應的就是具體的協議族,而 create
欄位指定了其建立socket的方法。一個具體協議族需要通過呼叫 sock_register()
函式向系統註冊其建立socket的方法。例如 Unix socket
就在初始化時通過下面的程式碼註冊:
struct net_proto_family unix_family_ops = {
PF_UNIX,
unix_create
};
static int __init af_unix_init(void)
{
...
sock_register(&unix_family_ops);
...
return 0;
}
所以從上面的程式碼可以指定,對於 Unix socket
的話,net_families[family]->create()
這行程式碼實際呼叫的是 unix_create()
函式。