• 【Socket系统调用】Socket与系统调用深度分析


    Socket与系统调用深度分析

    系统调用

    在一开始,应用程序是可以直接控制硬件的,这就需要程序员有很高的编程能力,否则一旦程序出了问题,会将整个系统Crash。

    在现在的操作系统中,用户程序运行在用户态,而要进行诸如Socket磁盘I/O这样的一些操作,这需要切换到内核态,再进行进行相应的操作,而这一过程则是系统调用system call。有了操作系统分离了内核和用户态,应用程序就无法直接进行硬件资源的访问,需要经过系统调用来进行。

    每次的系统调用,都会从用户态转换到内核态,运行完任务后,回到用户态。这中间的过程需要上下文切换(保存寄存器信息),也就是切换状态是需要消耗资源和时间的。

    Socket

    Socket是程序实现端到端通信的地址,互联网中每台设备有自己的IP地址,但每台设备上运行着许多程序。同时不同的程序都可能有通信的需求,这就需要一个套接字(Socket)来区分不同的程序。

    一个套接字由IP地址和端口号组成。

    IP address: port

    实验

    开启上次实验编译好的MenuOS系统

    上次实验编译了一个带调试功能,且带有TCP服务器和客户端的MenuOS系统

    进入LinuxKernel目录,启动虚拟机。

    jett@ubuntu:~$ cd LinuxKernel
    jett@ubuntu:~/LinuxKernel$ qemu-system-i386 -kernel linux-5.4.2/arch/x86/boot/bzImage -initrd rootfs.img -append "root=/dev/sda init=/init nokaslr" -s -S
    

    进入调试

    这时候虚拟机进入停止在一个黑屏界面,等待gdb的接入和下一步指令。

    新开一个终端窗口,进入gdb调试。

    接着分别

    • 导入符号表
    • 连接调试服务器
    • 设置断点
    jett@ubuntu:~/LinuxKernel$ gdb
    (gdb) file ~/LinuxKernel/linux-5.4.2/vmlinux
    Reading symbols from ~/LinuxKernel/linux-5.4.2/vmlinux...done.
    (gdb) target remote:1234
    Remote debugging using :1234
    0x0000fff0 in ?? ()
    (gdb) break start_kernel
    Breakpoint 1 at 0xc1db5885: file init/main.c, line 576.
    

    然后输入c让系统继续执行,执行到断点start_kernel ()则说明成功。

    (gdb) c
    Continuing.
    
    Breakpoint 1, start_kernel () at init/main.c:576
    576	{
    

    添加新断点sys_bind, sys_listen, sys_socketcall

    (gdb) break sys_bind 
    Breakpoint 2 at 0xc179beb0: file net/socket.c, line 1656.
    (gdb) break sys_listen 
    Breakpoint 3 at 0xc179bf60: file net/socket.c, line 1688.
    (gdb) break sys_socketcall 
    Breakpoint 4 at 0xc179ce00: file net/socket.c, line 2818.
    

    c让系统继续执行

    (gdb) c
    Continuing.
    
    Breakpoint 4, __se_sys_socketcall (call=1, args=-1075909056) at net/socket.c:2818
    2818	SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
    

    系统进入sys_socketcall断点,查看虚拟机窗口,可以看到虚拟机正在启动本地换回接口

    再次继续执行,可以看到总共进了三次sys_socketcall,另外两次分别是启动以太网口和建立连接,这个在上周就已经发现了。

    接着成功进入系统,可以查看之前编译进系统的TCP服务端和客户端

    在MenuOS中输入replyhi启动TCP服务端

    sys_socketcall

    可以在gdb中发现再次进入sys_socketcall断点

    Breakpoint 4, __se_sys_socketcall (call=1, args=-1075910240) at net/socket.c:2818
    2818	SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
    

    可以看到,断点停在__se_sys_socketcall中,位置是net/socket.c的2818行,找到函数如下:

    2818 SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
    2819 {
    2820         unsigned long a[AUDITSC_ARGS];
    2821         unsigned long a0, a1;
    2822         int err;
    2823         unsigned int len;
    2824 
    2825         if (call < 1 || call > SYS_SENDMMSG)
    2826                 return -EINVAL;
    2827         call = array_index_nospec(call, SYS_SENDMMSG + 1);
    2828 
    2829         len = nargs[call];
    2830         if (len > sizeof(a))
    2831                 return -EINVAL;
    2832 
    2833         /* copy_from_user should be SMP safe. */
    2834         if (copy_from_user(a, args, len))
    2835                 return -EFAULT;
    2836 
    2837         err = audit_socketcall(nargs[call] / sizeof(unsigned long), a);
    2838         if (err)
    2839                 return err;
    2840 
    2841         a0 = a[0];
    2842         a1 = a[1];
    2843 
    2844         switch (call) {
    2845         case SYS_SOCKET:
    2846                 err = __sys_socket(a0, a1, a[2]);
    2847                 break;
    2848         case SYS_BIND:
    2849                 err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
    2850                 break;
    2851         case SYS_CONNECT:
    2852                 err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
    2853                 break;
    2854         case SYS_LISTEN:
    2855                 err = __sys_listen(a0, a1);
    2856                 break;
    2857         case SYS_ACCEPT:
    2858                 err = __sys_accept4(a0, (struct sockaddr __user *)a1,
    2859                                     (int __user *)a[2], 0);
    2860                 break;
    2861         ...
    2930         default:
    2931                 err = -EINVAL;
    2932                 break;
    2933         }
    2934         return err;
    2935 }
    

    函数内部主体是一个switch语句,根据我们的call参数来进行选择,通过gdb我们可以看到,这时(call=1, args=-1075910240),具体要根据1是哪个case来追踪调用。

    可以在gdb中输入n(next)来继续往下执行,跟踪这次调用进入到哪个case,也可以去找case的定义。

    而这些case的定义并不在socket.c中。我们可以在/LinuxKernel/linux-5.4.2/include/linux下找到socket.h文件。

    #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)        */
    

    所以实质则是进入了__sys_socket(a0, a1, a[2]);函数内。

    __sys_socket

    Continuing
    
    Breakpoint 5, __sys_socket (family=2, type=1, protocol=0) at net/socket.c:1492
    1492	{
    

    输入s(step in)进入函数调用,或者直接查看net/socket.c的1492行。

    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));
    }
    

    函数sock_create创建了一个对应类型的套接字,sock_map_fd将套接字连接到一个文件描述符中返回。

    socket_create

    继续进入socket_create函数:

    (gdb) step
    sock_create (res=<optimized out>, protocol=<optimized out>, type=<optimized out>, 
        family=<optimized out>) at net/socket.c:1511
    1511		retval = sock_create(family, type, protocol, &sock);
    

    函数调用了__sock_create函数

    int sock_create(int family, int type, int protocol, struct socket **res)
    {
            return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
    }
    

    __sock_create

    设置断点,进入函数查看

    Breakpoint 7, __sock_create (net=0xc1d60dc0 <init_net>, family=2, type=1, protocol=0, 
        res=0xc71e7f40, kern=0) at net/socket.c:1349
    1349	{
    

    __sock_create函数定义如下:

    int __sock_create(struct net *net, int family, int type, int protocol,
                             struct socket **res, int kern)
    {
            int err;
            struct socket *sock;
            const struct net_proto_family *pf;
            // 检查协议地址和socket类型
            if (family < 0 || family >= NPROTO)
                    return -EAFNOSUPPORT;
            if (type < 0 || type >= SOCK_MAX)
                    return -EINVAL;
    
            if (family == PF_INET && type == SOCK_PACKET) {
                    pr_info_once("%s uses obsolete (PF_INET,SOCK_PACKET)
    ",
                                 current->comm);
                    family = PF_PACKET;
            }
    
            err = security_socket_create(family, type, protocol, kern);
            if (err)
                    return err;
            
            // 分配socket对象
            sock = sock_alloc();
            if (!sock) {
                    net_warn_ratelimited("socket: no more sockets
    ");
                    return -ENFILE; 
            }
    
            sock->type = type;
    
    #ifdef CONFIG_MODULES
            if (rcu_access_pointer(net_families[family]) == NULL)
                    request_module("net-pf-%d", family);
    #endif
    
            rcu_read_lock();
            // 得到相应的协议族的操作
            pf = rcu_dereference(net_families[family]);
            err = -EAFNOSUPPORT;
            if (!pf)
                    goto out_release;
    
            if (!try_module_get(pf->owner))
                    goto out_release;
    
            rcu_read_unlock();
            // 使用指针函数创建套接字
            err = pf->create(net, sock, protocol, kern);
            if (err < 0)
                    goto out_module_put;
    
            if (!try_module_get(sock->ops->owner))
                    goto out_module_busy;
    
            module_put(pf->owner);
            err = security_socket_post_create(sock, family, type, protocol, kern);
            if (err)
                    goto out_sock_release;
            *res = sock;
    
            return 0;
    
    out_module_busy:
            err = -EAFNOSUPPORT;
    out_module_put:
            sock->ops = NULL;
            module_put(pf->owner);
    out_sock_release:
            sock_release(sock);
            return err;
    
    out_release:
            rcu_read_unlock();
            goto out_sock_release;
    }
    

    其中sock_alloc函数:该函数分配一个新的inode与其绑定的套接字对象

    struct socket *sock_alloc(void)
    {
            struct inode *inode;
            struct socket *sock;
    
            inode = new_inode_pseudo(sock_mnt->mnt_sb);
            if (!inode)
                    return NULL;
    
            sock = SOCKET_I(inode);
    
            inode->i_ino = get_next_ino();
            inode->i_mode = S_IFSOCK | S_IRWXUGO;
            inode->i_uid = current_fsuid();
            inode->i_gid = current_fsgid();
            inode->i_op = &sockfs_inode_ops;
    
            return sock;
    }
    

    接着pf = rcu_dereference(net_families[family]);得到相应的协议族操作net_family_ops

    这里的指针函数指向inet_family_ops,相当于调用inet_family_ops.create

    所以综上__sock_create函数进行了

    • 协议类型的检查
    • 创建套接字对象
    • 根据不同的协议族类型,使用函数指针初始化对应的套接字
    • 做了各种错误处理
    • 返回套接字

    整个sys_socketcall调用的主要过程

    sys_socketcall
    |
    switch:
        case SYS_SOCKET:
        __sys_socket
               |
               +---sock_create
               |             |
               |             +---__sock_create
               |                         |
               |                         +---sock_alloc()
               |                         +---rcu_dereference(net_families[family])
               |                         +---pf->create(net, sock, protocol, kern)
               +---sock_map_fd
        case SYS_BIND:
        ...
    

    继续执行

    对下列case中的函数调用打上断点,这里还可以发现,系统调用中的sendsendto都是通过__sys_sendto系统调用来实现的,recv同样。

    (gdb) b sys_socketcall
    Breakpoint 10 at 0xc179ce00: file net/socket.c, line 2818.
    (gdb) b __sys_socket
    Breakpoint 11 at 0xc179ba70: file net/socket.c, line 1492.
    (gdb) b __sys_bind
    Breakpoint 12 at 0xc179bde0: file net/socket.c, line 1634.
    (gdb) b __sys_connect
    Breakpoint 13 at 0xc179c1c0: file net/socket.c, line 1811.
    (gdb) b __sys_listen
    Breakpoint 14 at 0xc179bed0: file net/socket.c, line 1668.
    (gdb) b __sys_accept4
    Breakpoint 15 at 0xc179bf70: file net/socket.c, line 1707.
    (gdb) b __sys_sendto
    Breakpoint 16 at 0xc179c480: file net/socket.c, line 1923.
    (gdb) b __sys_recvfrom
    Breakpoint 17 at 0xc179c5e0: file net/socket.c, line 1984.
    

    gdb捕获了4次sys_socketcall,分别是

    • SYS_SOCKET
    • SYS_BIND
    • SYS_LISTEN
    • SYS_ACCEPT

    正好对应TCP服务端中应用程序的4个过程,然后阻塞等待客户端请求

    在MenuOS中,启动客户端hello

    gdb捕获了6次sys_socketcall,分别是

    • SYS_SOCKET(客户端)
    • SYS_CONNECT(客户端)
    • SYS_SEND(客户端)
    • SYS_RECV(服务端)
    • SYS_SEND(服务端)
    • SYS_RECV(客户端)

    作者:SA19225176,万有引力丶

    参考资料来源:USTC Socket与系统调用深度分析

  • 相关阅读:
    JVM的基础知识
    tmux常用命令
    JAVA基础—方法覆写、多态
    datetime模块
    time()函数
    Packet Tracer 思科模拟器入门教程 之二 交换机的基本配置与管理
    单元测试前篇
    em
    视口
    浮动
  • 原文地址:https://www.cnblogs.com/Axi8/p/12070578.html
Copyright © 2020-2023  润新知