本文将围绕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_STAR
的 63:48
为用户代码的代码段。这些数据将加载至 CS
和 SS
段选择符,由提供将系统调用返回至相应特权级的用户代码功能的 sysret
指令使用。 同时从内核代码来看, 当用户空间应用程序执行系统调用时,MSR_STAR
的 47: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#include #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。