本篇博文我会将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()函数。
在该函数中,调用了accept()这个API。
依次调用了recv和send这两个API。
调用了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的追踪验证。