Socket與內核調用深度分析


一、socket api和系統調用關系

 

1.為什么有核心態和用戶態

在Linux中程序運行在兩個狀態,內核態和用戶態。在邏輯上,兩個空間相互隔離,因此用戶程序不能夠訪問內核數據,也無法直接調用內核函數。因此當用戶因為某項工作必須要使用到某個內核函數時,就要用到系統調用。在Linux系統中,系統調用是用戶空間訪問內核空間的唯一途徑

2.什么是系統調用

從上一個問題的解釋中,我們可以看到系統調用是用戶訪問系統內核的唯一方式,通俗的將,系統調用就是一種特殊的接口。通過這個接口,用戶可以訪問到內核空間。
可以看出,系統調用只是用戶進程進入內核的接口層,但其本省並不是內核函數。
進入內核后,不同的系統調用號會找到各自對應的內核函數,這些內核函數被稱為系統調用的“服務例程”。

3.API是什么

API-Application Programming interface,用戶程序接口。通常意義上說的API並不像他的名字一樣高大上。API說白了就是一些給定的服務,跟內核沒有必然的聯系。提供應用程序與開發人員基於某軟件或者硬件的訪問一組例程的能力,而無需了解其內部工作細節

4.API和系統調用有什么區別

API是函數的定義,規定了某個函數的功能,和內核無直接關系。
系統調用是通過中斷向內核發出請求,實現內核提供的某些服務。

5.特殊情況

某些API所提供的功能會涉及到與內核空間進行交互。那么,這類API內部會封裝系統調用。而不涉及與內核進行交互的API則不會封裝系統調用。也就是說,API和系統調用並沒有嚴格的一一對應關系,一個API可能恰好只對應一個系統調用,比如read()系統調用和read();一個API也可能由多個系統調用實現;有時候,一個API的功能可能並不需要內核提供的服務,那么此時這個API也就不需要任何的系統調用,比如abs()。另外,一個系統調用可能還被多個API內部調用。
對於編程者來說,系統調用和API都是一組函數,並無什么兩樣,二者關注的都是函數名、參數類型及返回值的含義;但是事實上,系統調用的實現是在內核完成的,API則是在函數庫中實現的

6.圖解

我們可以用下面這個圖來概括它們之間的關系

.用戶態到核心態的轉換

用戶態切換到內核態的3種方式

系統調用: 這是用戶態進程主動要求切換到內核態的一種方式,用戶態進程通過系統調用申請使用操作系統提供的服務程序完成工作。而系統調用的機制其核心還是使用了操作系統為用戶特別開放的一個中斷來實現,例如Linux的int 80h中斷。

異常: 當CPU在執行運行在用戶態下的程序時,發生了某些事先不可知的異常,這時會觸發由當前運行進程切換到處理此異常的內核相關程序中,也就轉到了內核態,比如缺頁異常。

外圍設備的中斷: 當外圍設備完成用戶請求的操作后,會向CPU發出相應的中斷信號,這時CPU會暫停執行下一條即將要執行的指令轉而去執行與中斷信號對應的處理程序,如果先前執行的指令是用戶態下的程序,那么這個轉換的過程自然也就發生了由用戶態到內核態的切換。

三.Socket創建過程分析

在用戶進程中,socket(int domain, int type, int protocol) 函數用於創建socket並返回一個與socket關聯的fd,該函數實際執行的是系統調用 sys_socketcall,sys_socketcall幾乎是用戶進程socket所有操作函數的入口:

/** sys_socketcall (linux/syscalls.h)*/
asmlinkage long sys_socketcall(int call, unsigned long __user *args);

sys_socketcall 實際調用的是 SYSCALL_DEFINE2:

/** SYSCALL_DEFINE2 (net/socket.c)*/
SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
    unsigned long a[AUDITSC_ARGS];
    unsigned long a0, a1;
    int err;
    unsigned int len;
    // 省略...
    a0 = a[0];
    a1 = a[1];

    switch (call) {
    case SYS_SOCKET:
        // 與 socket(int domain, int type, int protocol) 對應,創建socket
        err = sys_socket(a0, a1, a[2]);  
        break;
    case SYS_BIND:
        err = sys_bind(a0, (struct sockaddr __user *)a1, a[2]); 
        break;
    case SYS_CONNECT:
        err = sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
        break;
// 省略...
}

在 SYSCALL_DEFINE2 函數中,通過判斷call指令,來統一處理 socket 相關函數的事務,對於socket(…)函數,實際處理是在 sys_socket 中,也是一個系統調用,對應的是 SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol):

/** SYSCALL_DEFINE3 net/socket.c*/
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
    int retval;
    struct socket *sock;
    int flags;
    // SOCK_TYPE_MASK: 0xF; SOCK_STREAM等socket類型位於type字段的低4位
    // 將flag設置為除socket基本類型之外的值
    flags = type & ~SOCK_TYPE_MASK;

    // 如果flags中有除SOCK_CLOEXEC或者SOCK_NONBLOCK之外的其他參數,則返回EINVAL
    if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
        return -EINVAL;

    // 取type中的后4位,即sock_type,socket基本類型定義
    type &= SOCK_TYPE_MASK;

    // 如果設置了SOCK_NONBLOCK,則不論SOCK_NONBLOCK定義是否與O_NONBLOCK相同,
    // 均將flags中的SOCK_NONBLOCK復位,將O_NONBLOCK置位
    if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
        flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;

    // 創建socket結構,(重點分析)
    retval = sock_create(family, type, protocol, &sock);
    if (retval < 0)
        goto out;

    if (retval == 0)
        sockev_notify(SOCKEV_SOCKET, sock);

    // 將socket結構映射為文件描述符retval並返回,(重點分析)
    retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
    if (retval < 0)
        goto out_release;
out:
    return retval;
out_release:
    sock_release(sock);
    return retval;
}

SYSCALL_DEFINE3 中主要判斷了設置的socket類型type,如果設置了除基本sock_type,SOCK_CLOEXEC和SOCK_NONBLOCK之外的其他參數,則直接返回;同時調用 sock_create 創建 socket 結構,使用 sock_map_fd 將socket 結構映射為文件描述符並返回。在分析 sock_create 之前,先看看socket結構體:

/** socket結構體 (linux/net.h)*/
struct socket {
    socket_state        state;       // 連接狀態:SS_CONNECTING, SS_CONNECTED 等
    short           type;            // 類型:SOCK_STREAM, SOCK_DGRAM 等
    unsigned long       flags;       // 標志位:SOCK_ASYNC_NOSPACE(發送隊列是否已滿)等
    struct socket_wq __rcu  *wq;     // 等待隊列
    struct file     *file;           // 該socket結構體對應VFS中的file指針
    struct sock     *sk;             // socket網絡層表示,真正處理網絡協議的地方
    const struct proto_ops  *ops;    // socket操作函數集:bind, connect, accept 等
};

socket結構體中定義了socket的基本狀態,類型,標志,等待隊列,文件指針,操作函數集等,利用 sock 結構,將 socket 操作與真正處理網絡協議相關的事務分離。
回到 sock_create 繼續看socket創建過程,sock_create 實際調用的是 __sock_create:

/** __sock_create (net/socket.c)*/
int __sock_create(struct net *net, int family, int type, int protocol,
             struct socket **res, int kern)
{
    int err;
    struct socket *sock;
    const struct net_proto_family *pf;

    // 檢查是否是支持的地址族,即檢查協議
    if (family < 0 || family >= NPROTO)
        return -EAFNOSUPPORT;
    // 檢查是否是支持的socket類型
    if (type < 0 || type >= SOCK_MAX)
        return -EINVAL;

    // 省略...

    // 檢查權限,並考慮協議集、類型、協議,以及 socket 是在內核中創建還是在用戶空間中創建
    // 可以參考:https://www.ibm.com/developerworks/cn/linux/l-selinux/
    err = security_socket_create(family, type, protocol, kern);
    if (err)
        return err;

    // 分配socket結構,這其中創建了socket和關聯的inode (重點分析)
    sock = sock_alloc();
    if (!sock) {
        net_warn_ratelimited("socket: no more sockets\n");
        return -ENFILE; /* Not exactly a match, but its the
                   closest posix thing */
    }
    sock->type = type;
    // 省略...
}

__socket_create 檢查了地址族協議和socket類型,同時,調用 security_socket_create 檢查創建socket的權限(如:創建不同類型不同地址族socket的SELinux權限也會不同)。接着,來看看 sock_alloc:

/** sock_alloc (net/socket.c)*/
static struct socket *sock_alloc(void)
{
    struct inode *inode;
    struct socket *sock;

    // 在已掛載的sockfs文件系統的super_block上分配一個inode
    inode = new_inode_pseudo(sock_mnt->mnt_sb);
    if (!inode)
        return NULL;

    // 獲取inode對應socket_alloc中的socket結構指針
    sock = SOCKET_I(inode);

    inode->i_ino = get_next_ino();
    inode->i_mode = S_IFSOCK | S_IRWXUGO;
    inode->i_uid = current_fsuid();
    inode->i_gid = current_fsgid();

    // 將inode的操作函數指針指向 sockfs_inode_ops 函數地址
    inode->i_op = &sockfs_inode_ops;

    this_cpu_add(sockets_in_use, 1);
    return sock;
}

new_inode_pseudo 函數實際調用的是 alloc_inode(struct super_block *sb) 函數:

/** alloc_inode (fs/inode.c)*/
static struct inode *alloc_inode(struct super_block *sb)
{
    struct inode *inode;

    // 如果文件系統的超級塊已經指定了alloc_inode的函數,則調用已經定義的函數去分配inode
    // 對於sockfs,已經將alloc_inode指向sock_alloc_inode函數指針
    if (sb->s_op->alloc_inode)
        inode = sb->s_op->alloc_inode(sb);
    else
        // 否則在公用的 inode_cache slab緩存上分配inode
        inode = kmem_cache_alloc(inode_cachep, GFP_KERNEL);

    if (!inode)
        return NULL;

    // 編譯優化,提高執行效率,inode_init_always正常返回0
    if (unlikely(inode_init_always(sb, inode))) {
        if (inode->i_sb->s_op->destroy_inode)
            inode->i_sb->s_op->destroy_inode(inode);
        else
            kmem_cache_free(inode_cachep, inode);
        return NULL;
    }

    return inode;
}

四.跟蹤分析Linux/Socket系統調用

我們首先在 sys_socketcall 處建立斷點

 

 

 

 捕獲到sys_socketcall,對應的內核處理函數為SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
在net/socket.c中查看SYSCALL_DEFINE2相關的源代碼如下:

SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
    unsigned long a[AUDITSC_ARGS];
    unsigned long a0, a1;
    int err;
    unsigned int len;

    if (call < 1 || call > SYS_SENDMMSG)
        return -EINVAL;
    call = array_index_nospec(call, SYS_SENDMMSG + 1);

    len = nargs[call];
    if (len > sizeof(a))
        return -EINVAL;

    /* copy_from_user should be SMP safe. */
    if (copy_from_user(a, args, len))
        return -EFAULT;

    err = audit_socketcall(nargs[call] / sizeof(unsigned long), a);
    if (err)
        return err;

    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 __user *)a1, a[2]);
        break;
    case SYS_CONNECT:
        err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
        break;
    case SYS_LISTEN:
        err = __sys_listen(a0, a1);
        break;
    case SYS_ACCEPT:
        err = __sys_accept4(a0, (struct sockaddr __user *)a1,
                    (int __user *)a[2], 0);
        break;
    case SYS_GETSOCKNAME:
        err =
            __sys_getsockname(a0, (struct sockaddr __user *)a1,
                      (int __user *)a[2]);
        break;
    case SYS_GETPEERNAME:
        err =
            __sys_getpeername(a0, (struct sockaddr __user *)a1,
                      (int __user *)a[2]);
        break;
    case SYS_SOCKETPAIR:
        err = __sys_socketpair(a0, a1, a[2], (int __user *)a[3]);
        break;
    case SYS_SEND:
        err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
                   NULL, 0);
        break;
    case SYS_SENDTO:
        err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
                   (struct sockaddr __user *)a[4], a[5]);
        break;
    case SYS_RECV:
        err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
                     NULL, NULL);
        break;
    case SYS_RECVFROM:
        err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
                     (struct sockaddr __user *)a[4],
                     (int __user *)a[5]);
        break;
    case SYS_SHUTDOWN:
        err = __sys_shutdown(a0, a1);
        break;
    case SYS_SETSOCKOPT:
        err = __sys_setsockopt(a0, a1, a[2], (char __user *)a[3],
                       a[4]);
        break;
    case SYS_GETSOCKOPT:
        err =
            __sys_getsockopt(a0, a1, a[2], (char __user *)a[3],
                     (int __user *)a[4]);
        break;
    case SYS_SENDMSG:
        err = __sys_sendmsg(a0, (struct user_msghdr __user *)a1,
                    a[2], true);
        break;
    case SYS_SENDMMSG:
        err = __sys_sendmmsg(a0, (struct mmsghdr __user *)a1, a[2],
                     a[3], true);
        break;
    case SYS_RECVMSG:
        err = __sys_recvmsg(a0, (struct user_msghdr __user *)a1,
                    a[2], true);
        break;
    case SYS_RECVMMSG:
        if (IS_ENABLED(CONFIG_64BIT) || !IS_ENABLED(CONFIG_64BIT_TIME))
            err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1,
                         a[2], a[3],
                         (struct __kernel_timespec __user *)a[4],
                         NULL);
        else
            err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1,
                         a[2], a[3], NULL,
                         (struct old_timespec32 __user *)a[4]);
        break;
    case SYS_ACCEPT4:
        err = __sys_accept4(a0, (struct sockaddr __user *)a1,
                    (int __user *)a[2], a[3]);
        break;
    default:
        err = -EINVAL;
        break;
    }
    return err;
}

SYSCALL_DEFINE2根據不同的call來進入不同的分支,從而調用不同的內核處理函數,如__sys_bind, __sys_listen等等內核處理函數。

在SYSCALL_DEFINE2調用的與socket相關的函數們都打上斷點,再進行調試

 

 

 

在Qemu中輸入replyhi,發現gdb捕獲到以上斷點,一直到sys_accept4函數停止。說明此時服務器處於阻塞狀態,一直在等待客戶端連接,在Qemu中輸入hello,同樣捕獲斷點,可以看到客戶端發起連接,發送接收數據,整個過程與TCP socket通信流程完全相同,至此,整個追蹤過程結束。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM