Socket与系统调用深度分析
1.socket原理
1.1 计算机网络通信原理
在现行的tcp/ip协议中,总共分为四个层次,分别是网络接入层(对应iso标准的物理层和数据链路层),互联网络层(对应iso网络层),传输层(对应iso传输层)和应用层(对应iso应用层,表示层,会话层)。
网络接入层提供物理层面的链接,互联网络层实现数据报的转发与传递,传输层实现点到点的通信,应用层提供应用数据。在上述层次中,每一层都在下一层提供服务的基础上进行了封装,这样上层在调用其功能时,只需要知道该层次所提供的接口即可。
1.2 Socket简介
传输层实现端到端的通信,因此,每一个传输层连接有两个端点。那么,传输层连接的端点不单是主机,也不单是传输层的协议端口。传输层连接的端点叫做套接字(socket)。根据RFC793的定义:端口号拼接到IP地址就构成了套接字。套接字Socket=(IP地址:端口号),套接字的表示方法是点分十进制的IP地址后面写上端口号,中间用冒号或逗号隔开。每一个传输层连接唯一地被通信两端的两个端点(即两个套接字)所确定。所谓套接字,实际上是一个通信端点,每个套接字都有一个套接字序号,包括主机的IP地址与一个16位的主机端口号,即形如(主机IP地址:端口号)。例如,如果IP地址是127.0.0.1,而端口号是80,那么得到套接字就是(127.0.0.1:80)。
套接字可以看成是两个网络应用程序进行通信时,各自通信连接中的一个端点。通信时,其中的一个网络应用程序将要传输的一段信息写入它所在主机的Socket中,该Socket通过网络接口卡的传输介质将这段信息发送给另一台主机的Socket中,使这段信息能传送到其他程序中。因此,两个应用程序之间的数据传输要通过套接字来完成。
如上图所示,一般调用socket函数的通信流程为,服务端通过socket结构,用bind函数绑定ip地址和端口号,再通过listen监听通信,等待用户端连接请求,而用户端使用connect建立连接。连接建立后,用户端和服务端都可以使用write()和read()来进行信息的发送与接收,在传输完毕时,用户端先断开连接,服务器端接收到断开连接信号后,也断开连接。
2.内核与系统调用原理
2.1内核
内核,是一个操作系统的核心。是基于硬件的第一层软件扩充,提供操作系统的最基本的功能,是操作系统工作的基础,它负责管理系统的进程、内存、内核体系结构、设备驱动程序、文件和网络系统,决定着系统的性能和稳定性。内核将操作系统分为目态和管态,目态运行的程序受到权限限制,部分功能直接使用,在它们需要使用这些被限制的功能时,通过系统调用来实现。
2.2 系统调用
操作系统中的状态分为管态(核心态)和目态(用户态)。目态的进程运行受到权限限制,不能使用特权指令。(特权指令:一类只能在核心态下运行而不能在用户态下运行的特殊指令。不同的操作系统特权指令会有所差异,但是一般来说主要是和硬件相关的一些指令。)当目态进程需要使用特权指令时,他们通过访管指令将系统从目态转化为管态,从而获得权限执行特权指令。(访管指令:本身是一条特殊的指令,但不是特权指令,是在目态下使用的指令)访管指令能引起访管异常,通过中断机制实现功能。用户程序只在用户态下运行,有时需要访问系统核心功能,这时通过系统调用接口使用系统调用。
3.内核中断简介
中断是指由于接收到来自外围硬件(相对于中央处理器和内存)的异步信号或来自软件的同步信号,而进行相应的硬件/软件处理。发出这样的信号称为进行中断请求(interrupt request,IRQ)。硬件中断导致处理器通过一个上下文切换(context switch)来保存执行状态(以程序计数器和程序状态字等寄存器信息为主);软件中断则通常作为CPU指令集中的一个指令,以可编程的方式直接指示这种上下文切换,并将处理导向一段中断处理代码。
系统调用本身就是一种中断,在Linux中,当进程使用系统调用时,执行int $0x80,产⽣向量为128的编程异常,内核保存现场,并从eax寄存器中读取系统调用号,传入system_call函数,执行对应系统调用。
4.Socket系统调用具体分析
在lab3所提供的main.c文件中,我们可以发现和socket有关的如socket(),listen(),bind()以及close()等几个函数。根据查阅资料,得知Linux中所有和socket有关的系统调用统一使用了sys_socketcall()作为入口。于是我们在gdb中,分别对sys_socketcall,sys_bind,sys_listen设置断点跟踪,结果如图所示。(在等待配置环境的同时先用了实验楼环境来追踪)
可以发现,start_kernal函数在启动虚拟机被且仅被调用一次,sys_socketcall调用了很多次,而listen和bind却没有反馈。
继续研究,sys_socketcall总是在调用一个名为syscall_define2的函数,在这里查阅资料可知,syscall_defineX()为Linux中系统调用的统一接口,X为参数数量,这里syscall_define2表示系统调用参数有两个,第一个为系统调用名,第二个为类型+参数名。syscall_define2(socketcall,...)为socket相关的系统调用入口,其源码为
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;
}
在这里我们可以看到,syscall_define2(socketcall,...)通过switch的方式,分别对传入的系统调用做对应处理。但它并没有直接实现对应的功能,而是继续调用函数。
从而我们可以得知,Linux系统真正调用的函数实际是__sys_bind而非sys_bind,因此在追踪sys_bind时没有反馈。
在配置好的Linux系统使用gdb对__sys_bind和__sys_listen进行追踪,结果如下:
继续分析调用结果,通过对照socketcall源码与gdb追踪结果,可以发现,系统调用顺序为:
socket
socket
socket
socket
connect
socket
bind
listen
accpet
socket
connect
send_to
send
accpet
由此我们知道,在实验三中进程实现hello和replyhi的过程中,系统调用的顺序为,socket->connect->bind->listen->accept->send_to->send
由此,我们弄清了实验三中实现hi与hello的网络通信中,socket几个相关函数的运行顺序。
在关于对不同协议的多态处理中,我在net/socket.c中,沿着源码从socket找到了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;
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;
sock = sock_alloc();
阅读源码可知,传参里有一个名为protocol的参数,显然和协议名有关。
在这里发现调用了security_socket_create函数,继续追踪,但找不到下文,查阅资料得知,它定义在./linux/include/linux/security.h,提供了从security_socket_create
到 security_ops
结构中动态安装的函数的间接调用。对它的研究暂时超过了笔者的能力,只好暂时放弃。但从上述追踪结果,以及之前的Linux编程风格,我们可以猜测,socket根据protocol值,分别调用对应底层函数,从而实现多态功能。