Socket 与系统调用深度分析
本次实验主要从以下几个方面对socket和系统调用之间的关系:
- socket api 和 系统调用关系图解
- 系统调用机制
- socket相关系统调用内核函数和跟踪验证
一、socket api和系统调用关系
-
为什么有核心态和用户态
在Linux中程序运行在两个状态,内核态和用户态。在逻辑上,两个空间相互隔离,因此用户程序不能够访问内核数据,也无法直接调用内核函数。因此当用户因为某项工作必须要使用到某个内核函数时,就要用到系统调用。在Linux系统中,系统调用是用户空间访问内核空间的
唯一途径
-
什么是系统调用
从上一个问题的解释中,我们可以看到系统调用是用户访问系统内核的唯一方式,通俗的将,系统调用就是一种特殊的接口。通过这个接口,用户可以访问到内核空间。
可以看出,系统调用只是用户进程进入内核的接口层,但其本省并不是内核函数。
进入内核后,不同的系统调用号会找到各自对应的内核函数,这些内核函数被称为系统调用的“服务例程”。 -
API是什么
API-Application Programming interface,用户程序接口。通常意义上说的API并不像他的名字一样高大上。API说白了就是一些给定的服务,跟内核没有必然的联系。提供应用程序与开发人员基于某软件或者硬件的访问一组例程的能力,而无需了解其内部工作细节
-
API和系统调用有什么区别
API是函数的定义,规定了某个函数的功能,和内核无直接关系。
系统调用是通过中断向内核发出请求,实现内核提供的某些服务。 -
特殊情况
某些API所提供的功能会涉及到与内核空间进行交互。那么,这类API内部会封装系统调用。而不涉及与内核进行交互的API则不会封装系统调用。也就是说,API和系统调用并没有严格的一一对应关系,一个API可能恰好只对应一个系统调用,比如read()系统调用和read();一个API也可能由多个系统调用实现;有时候,一个API的功能可能并不需要内核提供的服务,那么此时这个API也就不需要任何的系统调用,比如abs()。另外,一个系统调用可能还被多个API内部调用。
对于编程者来说,系统调用和API都是一组函数,并无什么两样,二者关注的都是函数名、参数类型及返回值的含义;但是事实上,系统调用的实现是在内核完成的,API则是在函数库中实现的 -
图解
二 、系统调用机制
系统调用一般发生在中断的时候。当中断发生时,系统就会进入内核态指向相关的系统调用。相信考过408的同学对下图应该是很熟悉的。这个图大概的描述了如何进入内核态执行系统随后离开内核态进入用户态的整个过程。
三、socket相关系统调用内核函数和跟踪验证
本次实验室对上个实验的深入与扩展。在上个实验中,我们大致的学习了如何使用gdb进行调试,并运行了hello/hi通讯程序。当我们在命令行中输入hello后,服务端会响应hi。那么在这个简单的hello-hi的过程中发生了什么呢,或者说在当我们按下回车符的时候系统在干什么,有哪些函数被先后调用,这是本次实验我们要学习的。
-
理论
先回顾一下,程序在进行tcp/ip通信时,首先会进行三次握手,然后再发送数据,这个tcp/ip的大致流程。不难看出,在这个过程中
发送
和接受
数据以及建立连接
的函数应当被调用,其次tcp服务端应当会监听系统的某个端口以接受客户端的请求,因此监听端口
和接受请求
的函数同样会被调用,最后在通信结束后会断开连接,因此关闭tcp/ip连接
的函数也应当被调用。通过以前学习我们应当知道一个tcp/ip通信的过程是这样的:- Server将端口号和ip绑定形成服务端套接字,并监听在此端口
- Client将端口号和ip绑定形成客户端套接字,接着想Server发起请求,Server接受请求建立连接
- Server和Client互相发送数据
-
源码实现
通过对以上过程的分析,我们去hello/程序的源代码去找找,看能不能找到与上述功能描述向符合的函数
从main.c开始看起
从replyhi可以看出,在这个过程中这个函数主要调用了六个函数,分别是InitializeService()、ServiceStart()、RecvMsg()、SendMsg()、ServiceStop()、ShutdownService()
这六个函数,和我们前面分析的过程大致相符。
-
深层实现
通过上述分析,在Replyhi()这个函数中,只是简单的调用了这六个函数,这些函数并没有定义在main.c这个文件中。向上找我们可以看到在这个c文件中包含了一个头文件“syswrapper.h”。hello()同样的也包含在这个头文件中。
我们进一步的进入到该文件中去找。通过观察可以发现,主要可以分为两个部分来看。Server和Client。
- Server
在Server的实现中我们可以看到initServer()函数,这个函数主要完成了两个工作,绑定端口号以及监听端口,由
bind()
和listen()
实现。
随后是ServiceStart()
,顾名思义,开启服务。这个函数调用了accept()
函数(api)
继续向下分析,接着是RecvMsg()
和SendMsg()
函数,这两个函数依次调用了rec()
和send()
函数。
最后是ServiceStop()
和ShutdownService()
函数,调用了close()
函数用于断开连接- Client
hello()调用了如下几个函数
OpenRemoteService()
、sendMsg()
、ReceieveMsg()
、CloseRemoteService()
。从名字上可以是【看出这几个函数的大致功能是:打开一个socket连接,发送消息,接受消息以及关闭socket连接。
我们继续再深入查看这几个函数的内部实现。OpenRemoteService()
调用了prepareSocket()函数和InitClient()函数,这个函数的主要功能是将IP地址和端口号绑定形成客户端套接字。InitClient()函数调用了connect函数。CloseRemoteService()函数调用了close()函数关闭连接。- 分析
从上面对Server和Client的内部实现不难看出。我们之前分析的tcp/ip的实现过程所要调用的函数正好和上面的代码分析相对应。发送数据和接受数据对应的SendMsg和RevMsg函数。建立连接功能对应的connect函数。监听端口功能对应的listen函数。接受请求功能对应的是accept()函数。关闭连接功能对应的是ShutdownService函数和CloseRemoteService函数
-
内核跟踪
我们首先在sys_socketcall函数处建立断点
然后按c让程序继续执行。在这个过程中可以通过terminal查看到发生了如下的情况:
根据这些系统调用返回的系统调用号,我们去查看这些系统调用实现了哪些功能,根据terminal的内容我们找到socket.c文件,我么可以看到以下内容:
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;
}
通过对这些系统调用号的分析不难看出,这些系统调用发生的顺序和replyhi()和hello()函数的执行过程正好一一对应起来。