• Socket与系统调用深度分析


    本文将围绕linux平台上socket编程,以用户程序中的socket()接口调用为例,分析该API编程接口、系统调用机制及内核中系统调用相关源代码、 相关系统调用的内核处理函数。

    一、socket()接口

    int socket( int domain, int type, int protocol)

    功能:创建一个新的套接字,返回套接字描述符,失败返回-1。

    参数说明:

    domain:用于设置网络通信的域,函数socket()根据这个参数选择通信协议的族。通信协议族在文件sys/socket.h中定义。

    type:用于设置套接字通信的类型,主要有SOCKET_STREAM(流式套接字)、SOCK——DGRAM(数据包套接字)等。

    protocol:用于制定某个协议的特定类型,即type类型中的某个类型。通常某协议中只有一种特定类型,这样protocol参数仅能设置为0;

    举例:s=socket(PF_INET,SOCK_STREAM,0)

    二、系统调用机制

    1.系统调用初始化

      X86_64系统上电后,socket有关系统调用初始化过程为:start_kernel --> trap_init --> cpu_init --> syscall_init 。系统调用初始化syscall_init()函数在linux/arch/x86/kernel/cpu/common.c中定义,代码如下:

     1 void syscall_init(void)
     2 {
     3     wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS);
     4     wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
     5 
     6 #ifdef CONFIG_IA32_EMULATION
     7     wrmsrl(MSR_CSTAR, (unsigned long)entry_SYSCALL_compat);
     8     /*
     9      * This only works on Intel CPUs.
    10      * On AMD CPUs these MSRs are 32-bit, CPU truncates MSR_IA32_SYSENTER_EIP.
    11      * This does not cause SYSENTER to jump to the wrong location, because
    12      * AMD doesn't allow SYSENTER in long mode (either 32- or 64-bit).
    13      */
    14     wrmsrl_safe(MSR_IA32_SYSENTER_CS, (u64)__KERNEL_CS);
    15     wrmsrl_safe(MSR_IA32_SYSENTER_ESP,
    16             (unsigned long)(cpu_entry_stack(smp_processor_id()) + 1));
    17     wrmsrl_safe(MSR_IA32_SYSENTER_EIP, (u64)entry_SYSENTER_compat);
    18 #else
    19     wrmsrl(MSR_CSTAR, (unsigned long)ignore_sysret);
    20     wrmsrl_safe(MSR_IA32_SYSENTER_CS, (u64)GDT_ENTRY_INVALID_SEG);
    21     wrmsrl_safe(MSR_IA32_SYSENTER_ESP, 0ULL);
    22     wrmsrl_safe(MSR_IA32_SYSENTER_EIP, 0ULL);
    23 #endif
    24 
    25     /* Flags to clear on syscall */
    26     wrmsrl(MSR_SYSCALL_MASK,
    27            X86_EFLAGS_TF|X86_EFLAGS_DF|X86_EFLAGS_IF|
    28            X86_EFLAGS_IOPL|X86_EFLAGS_AC|X86_EFLAGS_NT);
    29 }
      这两个函数执行系统调用入口的初始化:
    wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS);
    wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);

      第一个特殊模块集寄存器MSR_STAR63:48 为用户代码的代码段。这些数据将加载至 CSSS 段选择符,由提供将系统调用返回至相应特权级的用户代码功能的 sysret 指令使用。 同时从内核代码来看, 当用户空间应用程序执行系统调用时,MSR_STAR47:32 将作为 CS and SS段选择寄存器的基地址。第二行代码中我们将使用系统调用入口entry_SYSCALL_64 填充 MSR_LSTAR 寄存器。

    2.执行系统调用

      glibc库对系统调用进行了封库,对于任何一个系统调用,最终都会调用 DO_CALL函数。在用户态进程里调用如open函数 会在glibc中将系统调用名称转换为系统调用号并存放到寄存器rax,然后调用syscall指令,syscall 指令从特殊模块寄存器 MSR_LSTAR 中取出函数 entry_SYSCALL_64 的入口地址并执行该函数。

      

      entry_SYSCALL_64 在 arch/x86/entry/entry_64.S 汇编文件中定义,包含了系统调用整个生命周期的管理,包括系统调用前的运行环境保存,执行系统调用,系统调用之后的恢复。entry_SYSCALL_64的源代码如下:

     1 ENTRY(entry_SYSCALL_64)
     2     UNWIND_HINT_EMPTY
     3     /*
     4      * Interrupts are off on entry.
     5      * We do not frame this tiny irq-off block with TRACE_IRQS_OFF/ON,
     6      * it is too small to ever cause noticeable irq latency.
     7      */
     8 
     9     swapgs
    10     /* tss.sp2 is scratch space. */
    11     movq    %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
    12     SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
    13     movq    PER_CPU_VAR(cpu_current_top_of_stack), %rsp
    14 
    15     /* Construct struct pt_regs on stack */
    16     pushq    $__USER_DS                /* pt_regs->ss */
    17     pushq    PER_CPU_VAR(cpu_tss_rw + TSS_sp2)    /* pt_regs->sp */
    18     pushq    %r11                    /* pt_regs->flags */
    19     pushq    $__USER_CS                /* pt_regs->cs */
    20     pushq    %rcx                    /* pt_regs->ip */
    21 GLOBAL(entry_SYSCALL_64_after_hwframe)
    22     pushq    %rax                    /* pt_regs->orig_ax */
    23 
    24     PUSH_AND_CLEAR_REGS rax=$-ENOSYS
    25 
    26     TRACE_IRQS_OFF
    27 
    28     /* IRQs are off. */
    29     movq    %rax, %rdi
    30     movq    %rsp, %rsi
    31     call    do_syscall_64        /* returns with IRQs disabled */
    32 ......

       在调用函数 do_syscall_64 之前, entry_SYSCALL_64做了一些准备工作。在控制器由用户态转到内核态后,并不是立即就执行内核态系统调用表中的内核函数,原因是在系统调用完成之后还要返回用户态,因此在调用内核系统调用函数之前,必须做一些准备工作,保存用户态的信息(堆栈, 寄存器)待系统调用完之后恢复现场等等。

      然后在do_syscall_64 函数中,从rax 里面拿出系统调用号,根据系统调用号在系统调用表 sys_call_table 中找到相应的函数进行调用,并将寄存器中保存的参数取出来,作为函数参数。64位的系统调用表定义在面 arch/x86/entry/syscalls/syscall_64.tbl 文件中,在编译过程中,会将 syscall_64.tbl 生成头文件 unistd_64.h 。arch/x86/entry/syscall_64.c 文件里包含了这个头文件,并定义了一个表,sys_系统调用也都在这个表里面。

     1 #ifdef CONFIG_X86_64
     2 __visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
     3 {
     4 ...
     5     if (likely(nr < NR_syscalls)) {
     6         nr = array_index_nospec(nr, NR_syscalls);
     7         regs->ax = sys_call_table[nr](regs);       //查询系统调用表
     8 ...
     9 }
    10 #endif

    三、socket系统调用

      上文已经介绍了整个系统调用的基本流程,那么用户程序调用函数 socket() 的流程是怎么样的呢?在linux中所有有关socket的系统调用(包括socket、bind、listen等)共用一个系统调用号112,系统调用名称为socketcall。内核执行函数entry_SYSCALL_64时,从寄存器rax中得知系统调用号为112,然后在系统调用表sys_call_table中找到112对应处理函数sys_socketcall的入口地址,并跳转执行。该函数在 linux-5.0.1/net/socket.c 中定义:

    SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
     {
         ......
         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;
       ......
       }
       ......
     } 

      该函数会根据参数 call 选择对应的处理函数,对应关系在 linux-5.0.1/include/uapi/linux/net.h 中定义:

    #include <linux/socket.h>
    #include <asm/socket.h>
    
    #define NPROTO        AF_MAX
    
    #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)        */

      可以看到socket() 调用最终会跳转到__sys_socket 中运行,而__sys_socket 函数在/linux-5.0.1/net/socket.c 中定义:

    int __sys_socket(int family, int type, int protocol)
    {
        int retval;
        struct socket *sock;
        int flags;
    
        /* Check the SOCK_* constants for consistency.  */
        BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
        BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
        BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
        BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);
    
        flags = type & ~SOCK_TYPE_MASK;
        if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
            return -EINVAL;
        type &= SOCK_TYPE_MASK;
    
        if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
            flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
    
        retval = sock_create(family, type, protocol, &sock);
        if (retval < 0)
            return retval;
    
        return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
    }
    
    SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
    {
        return __sys_socket(family, type, protocol);
    }

      sock_create内部就是使用文件系统中的数据结构inode为socket套接字分配了文件描述符。socket套接字与普通的文件在内部存储结构上是一致的,甚至文件描述符和套接字描述符是通用的,但是套接字于文件还是有特殊之处,因此定义了结构体struct socket。

  • 相关阅读:
    设备像素比devicePixelRatio简单介绍
    详解事件代理委托
    原生dom事件注册和移除事件的封装
    idataway_前端代码规范
    170307、1分钟实现“延迟消息”功能
    170306、wamp中的Apache开启gzip压缩提高网站的响应速度
    170303、PHP微信公众平台开发接口 SDK完整版
    170302、 Apache 使用localhost(127.0.0.1)可以访问,使用本机局域网IP(192.168.2.*)不能访问
    170301、使用Spring AOP实现MySQL数据库读写分离案例分析
    170228、Linux操作系统安装ELK stack日志管理系统--(1)Logstash和Filebeat的安装与使用
  • 原文地址:https://www.cnblogs.com/wzzgeorge/p/12068455.html
Copyright © 2020-2023  润新知