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。

原文地址:https://www.cnblogs.com/wzzgeorge/p/12068455.html