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值,分别调用对应底层函数,从而实现多态功能。

 

 

posted on 2019-12-19 12:01  王俊韬  阅读(299)  评论(0编辑  收藏  举报

导航