Socket与系统调用深度分析
Posted wjt1996
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Socket与系统调用深度分析相关的知识,希望对你有一定的参考价值。
1.socket原理
1.1 计算机网络通信原理
在现行的tcp/ip协议中,总共分为四个层次,分别是网络接入层(对应iso标准的物理层和数据链路层),互联网络层(对应iso网络层),传输层(对应iso传输层)和应用层(对应iso应用层,表示层,会话层)。
网络接入层提供物理层面的链接,互联网络层实现数据报的转发与传递,传输层实现点到点的通信,应用层提供应用数据。在上述层次中,每一层都在下一层提供服务的基础上进行了封装,这样上层在调用其功能时,只需要知道该层次所提供的接口即可。
1.2 Socket简介
传输层实现端到端的通信,因此,每一个传输层连接有两个端点。那么,传输层连接的端点不单是主机,也不单是传输层的协议端口。传输层连接的端点叫做套接字(socket)。根据RFC793的定义:端口号拼接到IP地址就构成了套接字。所谓套接字,实际上是一个通信端点,每个套接字都有一个套接字序号,包括主机的IP地址与一个16位的主机端口号,即形如(主机IP地址:端口号)。例如,如果IP地址是210.37.145.1,而端口号是23,那么得到套接字就是(210.37.145.1:23)。
套接字Socket=(IP地址:端口号),套接字的表示方法是点分十进制的IP地址后面写上端口号,中间用冒号或逗号隔开。每一个传输层连接唯一地被通信两端的两个端点(即两个套接字)所确定。
套接字可以看成是两个网络应用程序进行通信时,各自通信连接中的一个端点。通信时,其中的一个网络应用程序将要传输的一段信息写入它所在主机的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设置断点跟踪,结果如图所示。(在等待配置环境的同时先用了实验楼环境来追踪)
可以发现,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; }
从而我们可以得知,Linux系统真正调用的函数实际是__sys_bind而非sys_bind,因此在追踪sys_bind时没有反馈。
在Linux系统对__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中找到了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) ", current->comm); family = PF_PACKET; } err = security_socket_create(family, type, protocol, kern); if (err) return err; sock = sock_alloc(); if (!sock) { net_warn_ratelimited("socket: no more sockets "); return -ENFILE; /* Not exactly a match, but its the closest posix thing */ } sock->type = type; #ifdef CONFIG_MODULES if (rcu_access_pointer(net_families[family]) == NULL) request_module("net-pf-%d", family); #endif rcu_read_lock(); pf = rcu_dereference(net_families[family]); err = -EAFNOSUPPORT; if (!pf) goto out_release; if (!try_module_get(pf->owner)) goto out_release; rcu_read_unlock(); err = pf->create(net, sock, protocol, kern); if (err < 0) goto out_module_put; if (!try_module_get(sock->ops->owner)) goto out_module_busy; module_put(pf->owner); err = security_socket_post_create(sock, family, type, protocol, kern); if (err) goto out_sock_release; *res = sock; return 0; out_module_busy: err = -EAFNOSUPPORT; out_module_put: sock->ops = NULL; module_put(pf->owner); out_sock_release: sock_release(sock); return err; out_release: rcu_read_unlock(); goto out_sock_release; } EXPORT_SYMBOL(__sock_create); int sock_create(int family, int type, int protocol, struct socket **res) { return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0); } EXPORT_SYMBOL(sock_create);
阅读源码可知,关于多态实现的主要方式,是通过pf->create从而调用了inet_create()函数,而inet_create()函数一方面通过inetsw[]数组获取对应协议类型的接口操作集信息,另一方面创建struct sock类型的变量,最后是对创建的sock进行初始化。
以上是关于Socket与系统调用深度分析的主要内容,如果未能解决你的问题,请参考以下文章