1. 程式人生 > 其它 >Socket介面(基於 Linux-2.4.0)

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()

函式將一個socket繫結到指定的IP和埠上。當然,系統呼叫最終都會呼叫到核心態的某個核心函式來進行處理,在系統呼叫一章我們介紹過相關的原理,所以這裡只會介紹一下這些系統呼叫最終會呼叫哪些核心函式。

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;
};

可以把這個結構體想象成鉤子,要在上面掛什麼由使用者自己決定。其比較重要的欄位是 opsskops 欄位型別為 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 socketAF_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 所指定的協議偏向於邏輯上的協議,如 TCPUDP 等。舉個栗子,如果把 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() 函式。