• 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。

  • 相关阅读:
    mysql数据库常用指令
    解决windows的mysql无法启动 服务没有报告任何错误的经验。
    “Can't open file for writing”或“operation not permitted”的解决办法
    启动Apache出现错误Port 80 in use by "Unable to open process" with PID 4!
    如何打开windows的服务services.msc
    常见的HTTP状态码 404 500 301 200
    linux系统常用的重启、关机指令
    (wifi)wifi移植之命令行调试driver和supplicant
    linux(debian)安装USB无线网卡(tp-link TL-WN725N rtl8188eu )
    alloc_chrdev_region申请一个动态主设备号,并申请一系列次设备号
  • 原文地址:https://www.cnblogs.com/wzzgeorge/p/12068455.html
Copyright © 2020-2023  润新知