zoukankan      html  css  js  c++  java
  • Socket与系统调用深度分析

    1.Linux系统调用

    1.1为什么用户程序不能直接访问系统内核提供的服务?

    • 这是由于在Linux中,为了更好地保护内核空间,将程序的运行空间分为内核空间和用户空间(也就是常称的内核态和用户态),它们分别运行在不同的级别上,在逻辑上是相互隔离的。
    • 用户进程在通常情况下不允许访问内核数据,也无法直接调用内核函数,它们只能在用户空间操作用户数据,调用用户空间的函数。
    • 当用户空间的进程需要获得一定的系统服务时,应用程序调用系统调用,这时操作系统就根据系统调用号(每个系统调用被赋予一个系统调用号)使用户进程进入内核空间的具体位置调用相应的内核代码。
    • 进行系统调用时,程序运行空间需要从用户空间进入内核空间,处理完后再返回到用户空间。

    1.2 socket是如何进行系统调用的

    系统调用的实质是调用内核函数,于内核态中运行,Linux中的用户通过执行一条访管指令“int $0x80”来调用系统调用,该指令会产生一个访管中断,从而让系统暂停当前的进程执行,而转去执行系统调用处理程序。通过用户态传入的系统调用号从系统调用表中找到相应的服务例程的入口并执行,完成后返回。

    系统调用号与系统调用表:Linux内核中设置了一张系统调用表,用于关联系统调用号及其相对应的服务例程入口地址,定义在./arch/x86/entry/syscalls/syscall_64.tbl文件中,每个系统调用占一个表项,一旦分配好就不可以有任何变更。

    Linux的系统调用都为SYSCALL_DEFINEx,为什么要定义成SYSCALL_DEFINE,主要就是将系统调用的参数统一变为了使用long型来接收,再强转转为int,也就是系统调用本来传下来的参数类型。详细过程可参考 https://blog.csdn.net/hxmhyp/article/details/22699669

    在net/socket.c中有一个函数SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)便是socket调用的入口。

    所有的socket系统调用的总入口是sys_socketcall(),在include/linux/Syscalls.h中定义

    @param call 标识接口编号, @param args 是接口参数指针
    

    接口编号的定义在 include/uapi/linux/net.h中定义,可以看到内核提供了20个socket系统调用的接口。

    #ifndef _UAPI_LINUX_NET_H
    #define _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)		*/
    #define SYS_SHUTDOWN	13		/* sys_shutdown(2)		*/
    #define SYS_SETSOCKOPT	14		/* sys_setsockopt(2)		*/
    #define SYS_GETSOCKOPT	15		/* sys_getsockopt(2)		*/
    #define SYS_SENDMSG	16		/* sys_sendmsg(2)		*/
    #define SYS_RECVMSG	17		/* sys_recvmsg(2)		*/
    #define SYS_ACCEPT4	18		/* sys_accept4(2)		*/
    #define SYS_RECVMMSG	19		/* sys_recvmmsg(2)		*/
    #define SYS_SENDMMSG	20		/* sys_sendmmsg(2)		*/
    

    每个系统调用都对应一个内核服务例程来实现系统调用的功能,与SYSCALL_DEFINEx对应,其命名的格式都是以"sys_开头。其代码通常放在./kernel/sys.c中,服务例程的原型声明则是放在./include/linux/syscall.h中。如sys_socket,通常格式是asmlinkage long sys_socket(int flag......)。其中的amslinkage是一个必需的限定词,用于通知编译器从堆栈中提取函数的参数,而不是从寄存器中。

    socket的内核服务例程

    * net/socket.c */
    asmlinkage long sys_socket(int, int, int);
    asmlinkage long sys_socketpair(int, int, int, int __user *);
    asmlinkage long sys_bind(int, struct sockaddr __user *, int);
    asmlinkage long sys_listen(int, int);
    asmlinkage long sys_accept(int, struct sockaddr __user *, int __user *);
    asmlinkage long sys_connect(int, struct sockaddr __user *, int);
    asmlinkage long sys_getsockname(int, struct sockaddr __user *, int __user *);
    asmlinkage long sys_getpeername(int, struct sockaddr __user *, int __user *);
    asmlinkage long sys_sendto(int, void __user *, size_t, unsigned,
    				struct sockaddr __user *, int);
    asmlinkage long sys_recvfrom(int, void __user *, size_t, unsigned,
    				struct sockaddr __user *, int __user *);
    asmlinkage long sys_setsockopt(int fd, int level, int optname,
    				char __user *optval, int optlen);
    asmlinkage long sys_getsockopt(int fd, int level, int optname,
    				char __user *optval, int __user *optlen);
    asmlinkage long sys_shutdown(int, int);
    asmlinkage long sys_sendmsg(int fd, struct user_msghdr __user *msg, unsigned flags);
    asmlinkage long sys_recvmsg(int fd, struct user_msghdr __user *msg, unsigned flags);
    

    接口编号对应的参数个数在net/socket.c文件中的nargs数组中定义

    通过以上分析,我们知道了,socket的系统调用调用socket内核,并且由用户态转为内核态,通过系统调用表,进入socket系统调用的总入口sys_socketcall(),再根据其传递的参数,调用具体的sys_socket(),过程如下图。

    2.GDB追踪socket的内核调用过程

    首先在linuxnet/lab3下面打开终端,启动上次实验部署好的TCP通信的MenuOS系统

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

    重新打开一个终端,通过以下命令用连接到MenuOS服务器,端口1234,开始进行gdb调试,这里可以打上多个断点,观察socket调用的断点,我主要以以__sys_connect为例分析其系统调用过程

    gdb
    file ~/kernel/linux-5.0.1/vmlinux
    b __sys_socket
    b __sys_connet
    target remote:1234
    c
    

    在menuOS中输入replyhi和hello,在gdb调试界面按c继续观察断点过程,找到connect断点,通过bt命令查看其系统调用栈,分析其调用过程。

    我们发现__sys_connect的调用过程为
    entry_SYSCALL_compat →
    do_fast_syscall_32 →
    do_syscall_32_irqs_on →
    __ia32_compat_sys_socketcall →
    __se_compat_sys_socketcall →
    __do_compat_sys_socketcall →
    __sys_connect

    Breakpoint 3, __sys_connect (fd=4, uservaddr=0xffdd80ac, addrlen=16)
        at net/socket.c:1646
    1646	{
    (gdb) bt
    #0  __sys_connect (fd=4, uservaddr=0xffdd80ac, addrlen=16) at net/socket.c:1646
    #1  0xffffffff818eb977 in __do_compat_sys_socketcall (args=<optimized out>, 
        call=<optimized out>) at net/compat.c:869
    #2  __se_compat_sys_socketcall (args=<optimized out>, call=<optimized out>)
        at net/compat.c:838
    #3  __ia32_compat_sys_socketcall (regs=<optimized out>) at net/compat.c:838
    #4  0xffffffff8100450b in do_syscall_32_irqs_on (regs=<optimized out>)
        at arch/x86/entry/common.c:326
    #5  do_fast_syscall_32 (regs=0x4 <irq_stack_union+4>)
        at arch/x86/entry/common.c:397
    #6  0xffffffff81c01631 in entry_SYSCALL_compat ()
        at arch/x86/entry/entry_64_compat.S:257
    #7  0x0000000000000000 in ?? ()
    

    这里系统调用的入口似乎不是 entry_INT80_32,而是从entry_SYSCALL_compat开始的,也就是说并没有通过软中断来进入内核,查阅资料才知道 x86的系统调用实现经历了int/iret 到 sysenter/sysexit 再到 syscall/sysret 的演变。在目前主流的系统调用库(glibc) 中,int 0x80 只有在硬件不支持快速系统调用(sysenter/syscall)的时候才会调用,但目前的硬件都支持快速系统调用。

    之所以提出新指令,是因为通过软中断来实现系统调用实在太慢了。于是Intelx86 CPU自Pentium II之后,开始支持新的系统调用指令sysenter/syscall。用于从低特权级切换到ring0。没有特权级别检查(CPL, DPL),也没有压栈的操作,快最重要!可以看出这里就是通过syscall来进行系统调用的,因为快嘛。关于Linux syscall的详细分析可参考https://cloud.tencent.com/developer/article/1492374

    当socket进行syscall调用时,首先会跳入entry_SYSCALL_compat开始执行,从这里开始进入系统内核,在这里主要进行内核页表切换,变量保存,内核加载和参数确认等工作。

    然后在里面执行call do_fast_syscall_32,调用do_fast_syscall_32函数,读取在entry_SYSCALL_compat中依次被压入栈的寄存器值,先取出系统调用号,再从系统调用表中取出对应的处理函数,然后通过先前寄存器中的参数调用之。

    call do_fast_syscall_32调用完毕后返回到用户态的地址,通过 do_syscall_32_irqs_on从系统调用表中找到相应的处理函数进行调用。

    系统调用表 ia32_sys_call_table在arch/x86/entry/syscall/syscall_32.tbl中,可以看出sys_socketcall的系统调用号为102号,其对应的处理函数为__ia32_compat_sys_socketcall

    执行__ia32_compat_sys_socketcall,这里对应COMPAT_SYSCALL_DEFINE2,这里就进入了socket系统调用的入口了,通过传入的参数,找到对应的socket内核函数。

    这里是SYS_CONNECT,对应__sys_connect,也就找到最终的执行函数啦,进入__sys_connect,执行socket内核函数。

    以上就是__sys_connect在内核中调用的全过程。

  • 相关阅读:
    python-通过psutil监控系统性能
    集合类和JAVA多线程
    JAVA异常和基础类库
    类设计基础与进阶
    面对对象思想
    AtCoder Beginner Contest 185
    Java概述
    友链
    牛客编程巅峰赛S2第7场
    牛客小白月赛30
  • 原文地址:https://www.cnblogs.com/yingjiehuang/p/12070420.html
Copyright © 2011-2022 走看看