Socket与系统调用深度分析

  本篇博文我会将Socket API编程接口、系统调用机制及内核中系统调用相关源代码、 socket相关系统调用的内核处理函数结合起来分析,并且在Linux-5.0.1的系统内核中完成对系统调用的追踪与验证,其中将会包括四个需要验证的部分,第一:Socket API编程接口之上可以编写基于不同网络协议的应用程序;第二:Socket接口在用户态通过系统调用机制进入内核;第三:内核中将系统调用作为一个特殊的中断来处理,以socket相关系统调用为例进行分析;第四:socket相关系统调用的内核处理函数内部通过“多态机制”对不同的网络协议进行的封装方法。  

  首先我们先来看,Socket API接口是怎样基于不同的网络协议来进行网络编程的。

  关于socket编程我们有两种通信协议可以进行选择。一种是数据报通信,另一种就是流通信。
  数据报通信
  数据报通信协议,就是我们常说的UDP(User Data Protocol 用户数据报协议)。UDP是一种无连接的协议,这就意味着我们每次发送数据报时,需要同时发送本机的socket描述符和接收端的socket描述符。因此,我们在每次通信时都需要发送额外的数据。
  流通信
  流通信协议,也叫做TCP(Transfer Control Protocol,传输控制协议)。和UDP不同,TCP是一种基于连接的协议。在使用流通信之前,我们必须在通信的一对socket之间建立连接。其中一个socket作为服务器进行监听连接请求。另一个则作为客户端进行连接请求。一旦两个socket建立好了连接,他们可以单向或双向进行数据传输。
  我们进行socket编程使用UDP还是TCP呢。选择基于何种协议的socket编程取决于你的具体的客户端-服务器端程序的应用场景。下面我们简单分析一下TCP和UDP协议的区别:
  (1)在UDP中,每次发送数据报时,需要附带上本机的socket描述符和接收端的socket描述符。而由于TCP是基于连接的协议,在通信的socket对之间需要在通信之前建立连接,因此会有建立连接这一耗时存在于TCP协议的socket编程;
  (2)在UDP中,数据报数据在大小上有64KB的限制。而TCP中也不存在这样的限制。一旦TCP通信的socket对建立了连接,他们之间的通信就类似IO流,所有的数据会按照接受时的顺序读取;
  (3)UDP是一种不可靠的协议,发送的数据报不一定会按照其发送顺序被接收端的socket接受。然后TCP是一种可靠的协议。接收端收到的包的顺序和包在发送端的顺序是一致的。
  在我们的实验中,我们创建的是一个利用socket的基于TCP的连接,接下来我们结合源码,接口来进行整个hello/hi的实现过程的调用分析与追踪。上次实验我们实现了menuos的调试环境的配置,再来回忆一下我们实现的主要是在menuos中增加了replyhi和hello两条命令,其结果大致如下:

  因此我们进入linuxnet/lab3目录下的main.c来观察当我们输入replyhi和hello之后,内核到底做了哪些事。

replyhi

  我们看到main函数中,当我们输入replyhi之后,程序调用了StartReolyhi这个函数,我们再上溯到这个函数当中:

  我们看到,当满足建立连接条件(fork()得到的进程数为0)时,程序又调用了Replyhi()这个函数,我们继续上溯到该函数中:

  至此,我们发现Replyhi函数中,依次调用了InitializeService()、ServiceStart()、RecvMsg()、SendMsg()、ServiceStop()以及最后的ShutdownService()函数,我们依次来看这些函数究竟是如何调用socket API的。

  我们打开这些函数定义的头文件“syswrapper.h”,首先是InitializeService()函数的定义:

  其中又调用了两个函数PrepareSocket()函数和InitServer()函数

  在源码中我们看到,当连接建立之初时,代码调用了socket(PF_INET,SOCK_STREAM,0)函数,bind()函数和listen()函数。

  之后是ServiceStart()函数:

  在该函数中,调用了accept()这个API。

  再之后是RecvMsg()和SendMsg()函数:

  依次调用了recv和send这两个API。

  之后是ServiceStop()函数:

  调用了close API。

  在最后的ShutdownService()函数中:

  同样调用了close API,结束socket连接。

 

hello

  我们看hello中调用了哪些函数,分别调用了OpenRemoteService()、SendMsg()、RecvMsg()、CloseRemoteService()这几个函数,我们依次到头文件当中寻找这些函数的定义。

  首先是OpenRemoteService()

  PrepareSocket()函数我们在replyhi的部分已经分析过了,我们主要来看InitClient()函数部分:

  我们看到在该函数当中调用了socket API点的connect接口。

  接下来的RecvMsg()和SendMsg()函数和我们在上一部分中提到的一样,我们看一下结束函数部分CloseRemoteService()。

  在该函数中,和服务端一样,同样是调用了socket的close方法。

  至此我们已经弄清楚了利用socket在TCP协议的基础上是如何建立连接的,总结大致如下:

(1)服务端
    加载套接字库,创建套接字(socket());
    绑定套接字到一个IP地址和一个端口上(bind());
    将套接字设置为监听模式等待连接请求(listen());
    请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept());
    用返回的套接字和客户端进行通信(send()/recv());
    返回,等待另一个连接请求;
    关闭套接字,关闭加载的套接字库(closesocket())。
  (2)客户端
    加载套接字库,创建套接字(socket());
    向服务器发出连接请求(connect());
    和服务器进行通信(send()/recv());
    关闭套接字,关闭加载的套接字库(closesocket())。

   接下来我们将sys_socketcall这个函数打上断点,然后执行我们的replyhi和hello,观察一共发生了几次系统调用,并观察他们的call值分别是多少。

  首先我们进入gdb模式,读取vmlinux的内容,用target remote:1234和我们之前搭建好的menuos进行连接,之后给sys_socketcall打上断点,按c执行下去

  执行过程中一共捕捉到14次断点,捕捉结果如下:

   一共捕捉到了14次系统调用,我们根据返回的call值到源代码中查找这些sys_socketcall到底是在实现哪些功能,我们根据提示找到SYSCALL_DEFINE2所在的代码,在目录LinuxKernel/linux-5.0.1/net/socket.c中,下面是截取的SYSCALL_DEFINE2定义的部分。

SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
    unsigned long a[AUDITSC_ARGS];
    unsigned long a0, a1;
    int err;
    unsigned int len;

    if (call < 1 || call > SYS_SENDMMSG)
        return -EINVAL;
    call = array_index_nospec(call, SYS_SENDMMSG + 1);

    len = nargs[call];
    if (len > sizeof(a))
        return -EINVAL;

    /* copy_from_user should be SMP safe. */
    if (copy_from_user(a, args, len))
        return -EFAULT;

    err = audit_socketcall(nargs[call] / sizeof(unsigned long), a);
    if (err)
        return err;

    a0 = a[0];
    a1 = a[1];

    switch (call) {
    case SYS_SOCKET:                                                      #call=1
        err = __sys_socket(a0, a1, a[2]);
        break;
    case SYS_BIND:                                                        #call=2
        err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
        break;
    case SYS_CONNECT:                                                     #call=3
        err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
        break;
    case SYS_LISTEN:                                                      #call=4
        err = __sys_listen(a0, a1);
        break;
    case SYS_ACCEPT:                                                      #call=5
        err = __sys_accept4(a0, (struct sockaddr __user *)a1,
                    (int __user *)a[2], 0);
        break;
    case SYS_GETSOCKNAME:                                                 #call=6
        err =
            __sys_getsockname(a0, (struct sockaddr __user *)a1,
                      (int __user *)a[2]);
        break;
    case SYS_GETPEERNAME:                                                 #call=7
        err =
            __sys_getpeername(a0, (struct sockaddr __user *)a1,
                      (int __user *)a[2]);
        break;
    case SYS_SOCKETPAIR:                                                  #call=8
        err = __sys_socketpair(a0, a1, a[2], (int __user *)a[3]);
        break;
    case SYS_SEND:                                                        #call=9
        err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
                   NULL, 0);
        break;
    case SYS_SENDTO:                                                      #call=10
        err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
                   (struct sockaddr __user *)a[4], a[5]);
        break;
    case SYS_RECV:                                                        #call=11
        err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
                     NULL, NULL);
        break;
    case SYS_RECVFROM:                                                    #call=12
        err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
                     (struct sockaddr __user *)a[4],
                     (int __user *)a[5]);
        break;
    case SYS_SHUTDOWN:                                                    #call=13
        err = __sys_shutdown(a0, a1);
        break;
    case SYS_SETSOCKOPT:                                                  #call=14
        err = __sys_setsockopt(a0, a1, a[2], (char __user *)a[3],
                       a[4]);
        break;
    case SYS_GETSOCKOPT:                                                  #call=15
        err =
            __sys_getsockopt(a0, a1, a[2], (char __user *)a[3],
                     (int __user *)a[4]);
        break;
    case SYS_SENDMSG:                                                     #call=16
        err = __sys_sendmsg(a0, (struct user_msghdr __user *)a1,
                    a[2], true);
        break;
    case SYS_SENDMMSG:                                                    #call=17
        err = __sys_sendmmsg(a0, (struct mmsghdr __user *)a1, a[2],
                     a[3], true);
        break;
    case SYS_RECVMSG:                                                     #call=18
        err = __sys_recvmsg(a0, (struct user_msghdr __user *)a1,
                    a[2], true);
        break;
    case SYS_RECVMMSG:                                                    #call=19
        if (IS_ENABLED(CONFIG_64BIT) || !IS_ENABLED(CONFIG_64BIT_TIME))
            err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1,
                         a[2], a[3],
                         (struct __kernel_timespec __user *)a[4],
                         NULL);
        else
            err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1,
                         a[2], a[3], NULL,
                         (struct old_timespec32 __user *)a[4]);
        break;
    case SYS_ACCEPT4:                                                      #call=20
        err = __sys_accept4(a0, (struct sockaddr __user *)a1,
                    (int __user *)a[2], a[3]);
        break;
    default:
        err = -EINVAL;
        break;
    }
    return err;
}

   在上面的代码中,我已经将call值写在了每个case的后面,根据之前的14次断点,我们对应每个call值对应的系统调用,原来的断点值序列为:1,1,1,1,2,4,5,1,3,10,9,10,9,5。

对应的系统调用分别为:socket,socket,socket,socket,bind,listen,accept,socket,connect,sendto,send,sendto,send,accept。

  前三次系统调用socket为系统初始化,首先是服务端的初始化:第4-8个系统调用,之后是客户端的初始化:7-14个系统调用。从第四个socket开始是socket的初始化,依次是bind,listen,accept,我们对应之前总结的TCP服务端的建立连接过程,正是这几个步骤,没有问题;之后看客户端,依次是socket初始化,connet,以及在两端之间输入的两句话hello和hi,对应两个sendto和send,最后是accept表示套接字建立完成,正是服务端的socket步骤。

  至此我们完成了socket的hello/hi的追踪验证。

posted on 2019-12-17 21:02  wudizs  阅读(549)  评论(0编辑  收藏  举报

导航