Socket與系統調用深度分析

本文將Socket API編程接口、系統調用機制及內核中系統調用相關源代碼、 socket相關係統調用的內核處理函數結合起來分析,並在X86 64環境下Linux5.0以上的內核中進一步跟蹤驗證。linux

1. 系統調用的初始化git

在加電啓動BootLoader運行後,BootLoader對硬件初始化並把內核加載進內存而後將參數傳給內核後,內核將接過系統控制權開始運行,而內核運行的第一個函數入口就是start_kernel這個入口函數。它將進行一系列初始化,其中就包括系統調用初始化,內核最後建立系統的第0號進程rest_init,它作的一件事情是從根文件系統尋找init函數做爲系統第1號進程運行,rest_init則退化爲系統空閒時候運行的idle進程。內核啓動過程流程大體以下:github

而在start_kernel衆多初始化中,有一項初始化 tarp_init()即系統調用初始化,涉及到一些初始化中斷向量,能夠看到它在set_intr_gate設置到不少的中斷門,不少的硬件中斷,其中有一個系統陷阱門,進行系統調用的。以後還有idt_setup_tarps()即初始化中斷描述表初始化。使用gdb驗證以下:編程

系統調用初始化完成後,咱們的TCP/IP協議棧怎麼加載進內核的呢?咱們看看rest_init源碼:app

393static noinline void __init_refok rest_init(void)
394{
395    int pid;
396
397    rcu_scheduler_starting();
398    /*
399     * We need to spawn init first so that it obtains pid 1, however
400     * the init task will end up wanting to create kthreads, which, if
401     * we schedule it before we create kthreadd, will OOPS.
402     */
403    kernel_thread(kernel_init, NULL, CLONE_FS);
404    numa_default_policy();
405    pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
406    rcu_read_lock();
407    kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
408    rcu_read_unlock();
409    complete(&kthreadd_done);
410
411    /*
412     * The boot idle thread must execute schedule()
413     * at least once to get things moving:
414     */
415    init_idle_bootup_task(current);
416    schedule_preempt_disabled();
417    /* Call into cpu_idle with preempt disabled */
418    cpu_startup_entry(CPUHP_ONLINE);
419}

經過rest_init()新建kernel_initkthreadd內核線程。403行代碼 kernel_thread(kernel_init, NULL, CLONE_FS);,由註釋得調用 kernel_thread()建立1號內核線程(在kernel_init函數正式啓動),kernel_init函數啓動了init用戶程序。另外405行代碼 pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); 調用kernel_thread執行kthreadd,建立PID爲2的內核線程。rest_init()最後調用cpu_idle() 演變成了idle進程。更多細節參考:https://github.com/mengning/net/blob/master/doc/tcpip.mdsocket

至此係統調用初始化完成並且socket系統調用的中斷處理程序(TCP/IP協議棧)也已經註冊好了。tcp

2. 用戶態程序發起系統調用函數

當用戶程序調用socket()這個API時,glibc庫中會轉爲調用  __socket()函數,爲何要作這樣一步呢?覺得這樣可讓你們統一隻包括glic這個庫不用再關心其餘頭文件,而實現glic這個髒活累活交給系統級開發人員幹。_socket()這個函數定義在socket.s這個彙編文件中,它完成參數的傳遞,而後ENTER_KERNEL進入內核,代碼以下:spa

movl $SYS_ify(socketcall), %eax /* System call number in %eax.  */  
  
/* Use ## so `socket' is a separate token that might be #define'd.  */  
movl $P(SOCKOP_,socket), %ebx   /* Subcode is first arg to syscall.  */  
lea 4(%esp), %ecx       /* Address of args is 2nd arg.  */  
  
        /* Do the system call trap.  */  
ENTER_KERNEL  

其中線程

SYS_ify宏定義爲

#define SYS_ify(syscall_name)   __NR_##syscall_name;  

P宏定義爲

#define P(a, b) P2(a, b)  
#define P2(a, b) a##b  

##爲鏈接符號。

#define __NR_socketcall     102  
#define SOCKOP_socket       1  

所以,中斷號是102,子中斷號是1;

而ENTER_KERNEL是什麼呢?爲啥它就能進入內呢?請看下面定義:

# define ENTER_KERNEL int $0x80  

int $0x80是x86的軟中斷指令,使用它會使系統進入內核模式,也就是所謂的內陷。

該指令會跳轉到system_call中斷入口在kernel/arch/x86/kernel/entry_32.S:

syscall_call:  
    call *sys_call_table(,%eax,4)  

該指令又會跳轉到對應的

中斷向量表102號中斷:

.long sys_socketcall  

進入sys_socketcall()函數,根據子中斷號(socket是1)以決定走哪一個分支:kernel/net/Socket.c:

switch (call) {  
    case :  
        break;  
    case SYS_BIND:  
        …...  

 

上面這張圖是open()的系統調用示意圖,socket與之相似。

3.gdb跟蹤驗證

因爲咱們已經在menu os中集成了replyhi和hello兩個程序,這兩個通訊程序就使用了socket()API,以下圖:

 

int Replyhi()

{

        char szBuf[MAX_BUF_LEN] = "\0";

        char szReplyMsg[MAX_BUF_LEN] = "hi\0";

        int sockfd = -1;

        struct sockaddr_in serveraddr;

        struct sockaddr_in clientaddr;

        socklen_t addr_len = sizeof(struct sockaddr);

        serveraddr.sin_family = AF_INET;

        serveraddr.sin_port = htons(PORT);

        serveraddr.sin_addr.s_addr = inet_addr(IP_ADDR);

        memset(&serveraddr.sin_zero, 0, 8);

        sockfd = socket(PF_INET,SOCK_STREAM,0);

        int ret = bind( sockfd, (struct sockaddr *)&serveraddr, sizeof(struct sockaddr)); 

        if(ret == -1)

        {

            fprintf(stderr,"Bind Error,%s:%d\n", __FILE__,__LINE__);

            close(sockfd);

            return -1; 

        }

        listen(sockfd,MAX_CONNECT_QUEUE); 



    while(1)

    {

        int newfd = accept( sockfd, (struct sockaddr *)&clientaddr, &addr_len);

        if(newfd == -1) 

        { 

            fprintf(stderr,"Accept Error,%s:%d\n", __FILE__,__LINE__); 

        } 

        ret = recv(newfd,szBuf,MAX_BUF_LEN,0); 

        if(ret > 0)

        {

            printf("recv \"%s\" from %s:%d\n", szBuf, (char*)inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));                \

        }

        ret = send(newfd,szReplyMsg,strlen(szReplyMsg),0);

        if(ret > 0) 

        { 

            printf("rely \"hi\" to %s:%d\n", (char*)inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));                \

        }

        close(newfd);

    }

    close(sockfd);

    return 0;

}

其中使用了socket,bind,  listen,   accpet,    recv,   send,   close等API,都會發生系統調用,咱們只跟蹤socket()這個API的調用棧來驗證便可。運行以下命令:

qemu-system-x86_64 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -append nokaslr -s -S

而後,按照咱們前面的socketAPI系統調用棧來打斷點

gdb
file    vmlinux
b    sys_call
b    sys_socketcall
b    call  *syacall_call_table(,%eax,4)
b    SYS_SOCKET
target    remote:1234

最後發現

 

只有sys_socketcall斷點成功,其餘都直接編譯處理掉了。繼續運行內核,出現以下結果:

能夠看到內核啓動過程三次調用sys_socketcall,並且都是子終端號call=1,即SYS_SOCKET中斷處理程序分配了3個socket套接字,用於Bring  up  interface:lo  和  Bring  up  interface:  etho和List  all  interfaces。(具體我也不知道幹啥的,之後再探究)

而後內核加載完,咱們在menu os裏面運行 replyhi 程序,能夠看到又發生4次系統調用:

上面要對着子中斷號call具體是什麼,再結合replyhi程序源碼來分析,這裏就先到這兒,至少系統調用棧的驗證算是成功了。

不過這裏有一處好奇的地方:我打的斷點是sys_socketcall,  爲何實際上它在__se_sys_socketcall  出中止運行呢?爲何名稱不同???

相關文章
相關標籤/搜索