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

Socket与系统调用深度分析

系统调用

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

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

【Socket系统调用】Socket与系统调用深度分析_第1张图片

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

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

【Socket系统调用】Socket与系统调用深度分析_第2张图片

进入调试

这时候虚拟机进入停止在一个黑屏界面,等待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断点,查看虚拟机窗口,可以看到虚拟机正在启动本地换回接口

【Socket系统调用】Socket与系统调用深度分析_第3张图片

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

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

【Socket系统调用】Socket与系统调用深度分析_第4张图片

在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=, protocol=, type=, 
    family=) 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 , 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)\n",
                             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\n");
                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

【Socket系统调用】Socket与系统调用深度分析_第5张图片

gdb捕获了6次sys_socketcall,分别是

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

作者:SA19225176,万有引力丶

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

你可能感兴趣的:(【Socket系统调用】Socket与系统调用深度分析)