本次实验以Menu OS的replyhi/hello聊天小程序为研究对象,通过gdb调试跟踪揭示socket编程api与系统调用之间的关系。

一、实验原理

典型的 TCP 客户机和服务器应用程序会使用诸如 socket()bind()listen()accept()send() 和 receive()这样的API来实现网络通信功能。复杂的网络通信过程被封装在了这几个函数之下,简化了编程也屏蔽了细节。如今广泛使用的TCP/IP协议具有5层结构,socket层之下是复杂的tcp/ip协议层,更底层还需要网卡等硬件的协同工作。操作系统作为硬件的管理者和软件的协调者,就像一个面面具到的管家在幕后统筹着这一切。

之前所说的api只是用户态的函数,它们的主要作用是发起系统调用通知内核,具体的工作还是得由运行在内核态的内核函数来完成。下图展示了TCP/IP应用的层级结构。

下面我们在基于Linux-5.0.1内核的64位Menu OS,以一个简单的hello/replyhi聊天程序为线索,看看在程序的执行过程中有哪些系统调用和内核函数参与其中。

三、实验过程

简要回顾一下内核的编译:下载Linux-5.0.1的源码后,按默认配置生成.config文件。为了生成符号表用于调试,别忘了在.config文件中开启将相关选项:

CONFIG_DEGUB_INFO=y

在虚拟机之前,修改Menu OS根目录下的Makefile文件,在后面追加-append nokaslr参数:

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

准备工作完成后,我们不妨先来看一下,在hello聊天程序中可能调用到的内核函数。以下为linux/net/socket.c源文件中负责请求分派的方法,由宏定义实现。可以看到,用户的各种socket请求最终都被派分给相应的内核函数进行实现。

/*
 *    System call vectors.
 *
 *    Argument checking cleaned up. Saved 20% in size.
 *  This function doesn't need to set the kernel lock because
 *  it is set by the callees.
 */

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:
        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;
    case SYS_ACCEPT:
        err = __sys_accept4(a0, (struct sockaddr __user *)a1,
                    (int __user *)a[2], 0);
        break;
    case SYS_GETSOCKNAME:
        err =
            __sys_getsockname(a0, (struct sockaddr __user *)a1,
                      (int __user *)a[2]);
        break;
    case SYS_GETPEERNAME:
        err =
            __sys_getpeername(a0, (struct sockaddr __user *)a1,
                      (int __user *)a[2]);
        break;
    case SYS_SOCKETPAIR:
        err = __sys_socketpair(a0, a1, a[2], (int __user *)a[3]);
        break;
    case SYS_SEND:
        err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
                   NULL, 0);
        break;
    case SYS_SENDTO:
        err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
                   (struct sockaddr __user *)a[4], a[5]);
        break;
    case SYS_RECV:
        err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
                     NULL, NULL);
        break;
    case SYS_RECVFROM:
        err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
                     (struct sockaddr __user *)a[4],
                     (int __user *)a[5]);
        break;
    case SYS_SHUTDOWN:
        err = __sys_shutdown(a0, a1);
        break;
    case SYS_SETSOCKOPT:
        err = __sys_setsockopt(a0, a1, a[2], (char __user *)a[3],
                       a[4]);
        break;
    case SYS_GETSOCKOPT:
        err =
            __sys_getsockopt(a0, a1, a[2], (char __user *)a[3],
                     (int __user *)a[4]);
        break;
    case SYS_SENDMSG:
        err = __sys_sendmsg(a0, (struct user_msghdr __user *)a1,
                    a[2], true);
        break;
    case SYS_SENDMMSG:
        err = __sys_sendmmsg(a0, (struct mmsghdr __user *)a1, a[2],
                     a[3], true);
        break;
    case SYS_RECVMSG:
        err = __sys_recvmsg(a0, (struct user_msghdr __user *)a1,
                    a[2], true);
        break;
    case SYS_RECVMMSG:
        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:
        err = __sys_accept4(a0, (struct sockaddr __user *)a1,
                    (int __user *)a[2], a[3]);
        break;
    default:
        err = -EINVAL;
        break;
    }
    return err;
}

以此为据,我们不妨先给这些内核函数都打上断点:

当然,别忘了我们的小程序。

客户端代码如下:

#include"syswrapper.h"

#define MAX_CONNECT_QUEUE   1024


int main()
{
    char szBuf[MAX_BUF_LEN] = "\0";
    char szMsg[MAX_BUF_LEN] = "hello\0";
    OpenRemoteService();
    SendMsg(szMsg);
    RecvMsg(szBuf);
    CloseRemoteService();
    return 0;
}

服务端代码如下:

#include"syswrapper.h"

#define MAX_CONNECT_QUEUE   1024


int main()
{
    char szBuf[MAX_BUF_LEN] = "\0";
    char szReplyMsg[MAX_BUF_LEN] = "hi\0";
    InitializeService();
    while(1)
    {
        ServiceStart();
        RecvMsg(szBuf);
        SendMsg(szReplyMsg);
        ServiceStop();
    }
    ShutdownService();
    return 0;
}

具体的实现通过宏定义:

#ifndef _SYS_WRAPER_H_
#define _SYS_WRAPER_H_

#include<stdio.h>
#include<arpa/inet.h> /* internet socket */
#include<string.h>
//#define NDEBUG
#include<assert.h>

#define PORT                5001
#define IP_ADDR             "127.0.0.1"
#define MAX_BUF_LEN         1024

/* private macro */
#define PrepareSocket(addr,port)                        \
        int sockfd = -1;                                \
        struct sockaddr_in serveraddr;                  \
        struct sockaddr_in clientaddr;                  \
        socklen_t addr_len = sizeof(struct sockaddr);   \
        serveraddr.sin_family = AF_INET;                \
        serveraddr.sin_port = htons(port);              \
        serveraddr.sin_addr.s_addr = inet_addr(addr);   \
        memset(&serveraddr.sin_zero, 0, 8);             \
        sockfd = socket(PF_INET,SOCK_STREAM,0);

#define InitServer()                                    \
        int ret = bind( sockfd,                         \
                        (struct sockaddr *)&serveraddr, \
                        sizeof(struct sockaddr));       \
        if(ret == -1)                                   \
        {                                               \
            fprintf(stderr,"Bind Error,%s:%d\n",        \
                            __FILE__,__LINE__);         \
            close(sockfd);                              \
            return -1;                                  \
        }                                               \
        listen(sockfd,MAX_CONNECT_QUEUE);

#define InitClient()                                    \
        int ret = connect(sockfd,                    断电   \
            (struct sockaddr *)&serveraddr,             \
            sizeof(struct sockaddr));                   \
        if(ret == -1)                                   \
        {                                               \
            fprintf(stderr,"Connect Error,%s:%d\n",     \
                __FILE__,__LINE__);                     \
            return -1;                                  \
        }
/* public macro */
#define InitializeService()                             \
        PrepareSocket(IP_ADDR,PORT);                    \
        InitServer();

#define ShutdownService()                               \
        close(sockfd);

#define OpenRemoteService()                             \
        PrepareSocket(IP_ADDR,PORT);                    \
        InitClient();                                   \
        int newfd = sockfd;

#define CloseRemoteService()                            \
        close(sockfd); 断电

#define ServiceStart()                                  \
        int newfd = accept( sockfd,                     \
                    (struct sockaddr *)&clientaddr,     \
                    &addr_len);                         \
        if(newfd == -1)                                 \
        {                                               \
            fprintf(stderr,"Accept Error,%s:%d\n",      \
                            __FILE__,__LINE__);         \
        }
#define ServiceStop()                                   \
        close(newfd);

#define RecvMsg(buf)                                    \
       ret = recv(newfd,buf,MAX_BUF_LEN,0);             \
       if(ret > 0)                                      \
       {                                                \
            printf("recv \"%s\" from %s:%d\n",          \
            buf,                                        \
            (char*)inet_ntoa(clientaddr.sin_addr),      \
            ntohs(clientaddr.sin_port));                \
       }

#define SendMsg(buf)                                    \
        ret = send(newfd,buf,strlen(buf),0);            \
        if(ret > 0)                                     \
        {                                               \
            printf("send \"hi\" to %s:%d\n",            \
            (char*)inet_ntoa(clientaddr.sin_addr),      \
            ntohs(clientaddr.sin_port));                \
        }

#endif /* _SYS_WRAPER_H_ */

那么下面让程序跑起来吧。

首先在虚拟机终端中输入以下命令启动服务器。

$ replyhi

观察gdb调试窗口,可以看到程序首先在断点:__sys_socket 处暂停,该内核函数对应于用户程序中socket创建代码:

sockfd = socket(PF_INET,SOCK_STREAM,0);

在gdb终端输入指令 c 继续replyhi程序的运行:

$ (gdb) c

这时程序止步于断点: __sys_bind 处,这是由用户程序对bind的调用引起。

int ret = bind( sockfd, (struct sockaddr *)&serveraddr, sizeof(struct sockaddr));

此时服务器端的socket初始化完成:端口已经绑定。虚拟机控制台等待用户新的命令输入。这时我们继续执行hello程序:

$ hello

同样地程序停止在断点:__sys_socket 处。我们继续,发现下一个断点为:__sys_connect,这对应用户的connect调用:

int ret = connect(sockfd, (struct sockaddr *)&serveraddr, sizeof(struct sockaddr));  

继续执行,程序停在了断点:__sys_recvfrom 、__sys_sendto、 __sys_sendto、__sys_recvfrom 处,这分别对应服务端的recv、客户端的send、服务端的send和客户端的recv。4个断点为何以这样的顺序出现?查阅Linux用户手册不难得出结论:使用阻塞式socket时,调用recv后,如果暂时没有收到消息则线程会一直等待直到有消息前来。

最后程序来到断点:__sys_accept4处,这对应服务端使用的accept函数:

int newfd = accept( sockfd, (struct sockaddr *)&clientaddr, &addr_len);

这是由新一轮的ServiceStart()调用引起。

总体上来说,socket编程的api函数与内核实现函数有着很好的对应关系。

12-22 14:54