相关文章推荐
逼格高的核桃  ·  Exception in thread ...·  3 月前    · 
仗义的人字拖  ·  ajax ...·  3 月前    · 
斯文的佛珠  ·  Halcon ...·  1 年前    · 
买醉的香蕉  ·  Write-Output ...·  1 年前    · 
爱看书的汽水  ·  c# - ...·  1 年前    · 
  • Socket API编程接口之上可以编写基于不同网络协议的应用程序;
  • Socket接口在用户态通过系统调用机制进入内核;
  • 内核中将系统调用作为一个特殊的中断来处理,以socket相关系统调用为例进行分析;
  • socket相关系统调用的内核处理函数内部通过“多态机制”对不同的网络协议进行的封装方法;
  • 请将Socket API编程接口、系统调用机制及内核中系统调用相关源代码、 socket相关系统调用的内核处理函数结合起来分析,并在X86 64环境下Linux5.0以上的内核中进一步跟踪验证。

    实验环境: vmware 15.5下的ubuntu16.04虚拟机

    基于内核: linux 5.0.1

    内核编译方式: x86-64

    一、套接字(socket)

    套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。实质上socket就是一种为了完成两个应用程序之间的数据传输独立于协议的网络编程接口。

    套接字Socket=(IP地址:端口号),套接字的表示方法是点分十进制的IP地址后面写上端口号,中间用冒号或逗号隔开。每一个传输层连接唯一地被通信两端的两个端点(即两个套接字)所确定。

    套接字可以看成是两个网络应用程序进行通信时,各自通信连接中的一个端点。通信时,其中的一个网络应用程序将要传输的一段信息写入它所在主机的Socket中,该Socket通过网络接口卡的传输介质将这段信息发送给另一台主机的Socket中,使这段信息能传送到其他程序中。因此,两个应用程序之间的数据传输要通过套接字来完成。

    2、调用流程

    套接字调用流程如下图所示:

  • socket():创建套接字。
  • bind():指定本地地址。一个套接字用socket()创建后,它其实还没有与任何特定的本地或目的地址相关联。在很多情况下,应用程序并不关心它们使用的本地地址,这时就可以不用调用bind指定本地的地址,而由协议软件为它们选择一个。但是,在某个知名端口上操作的服务器进程必须要对系统指定本地端口。所以一旦创建了一个套接字,服务器就必须使用bind()系统调用为套接字建立一个本地地址。
  • connect():将套接字连接到目的地址。初始创建的套接字并未与任何外地目的地址关联。客户机可以调用connect()为套接字绑定一个永久的目的地址,将它置于已连接状态。对数据流方式的套接字,必须在传输数据前,调用connect()构造一个与目的地的TCP连接,并在不能构造连接时返回一个差错代码。如果是数据报方式,则不是必须在传输数据前调用connect。如果调用了connect(),也并不像数据流方式那样发送请求建连的报文,而是只在本地存储目的地址,以后该socket上发送的所有数据都送往这个地址,程序员就可以免去为每一次发送数据都指定目的地址的麻烦。
  • listen():设置等待连接状态。对于一个服务器的程序,当申请到套接字,并调用bind()与本地地址绑定后,就应该等待某个客户机的程序来要求连接。listen()就是把一个套接字设置为这种状态的函数。
  • accept():接受连接请求。服务器进程使用系统调用socket,bind和listen创建一个套接字,将它绑定到知名的端口,并指定连接请求的队列长度。然后,服务器调用accept进入等待状态,直到到达一个连接请求。
  • send()/recv()和sendto()/recvfrom():发送和接收数据 。在数据流方式中,一个连接建立以后,或者在数据报方式下,调用了connect()进行了套接字与目的地址的绑定后,就可以调用send()和reev()函数进行数据传输。
  • closesocket():关闭套接字。
  • 二、Linux内核分析:系统调用,用户态及内核态

    1、系统调用

    linux内核中设置了一组用于实现系统功能的子程序,称为系统调用。系统调用和普通库函数调用非常相似,只是系统调用由操作系统核心提供,运行于核心态,而普通的函数调用由函数库或用户自己提供,运行于用户态。
    一般进程是不能访问内核的。它不能访问内核所占内存空间也不能调用内核函数,CPU硬件决定了这些。为了和用户空间上运行的进程进行交互,内核提供了一组接口。透过该接口,应用程序可以访问硬件设备和其他操作系统资源。这组接口在应用程序和内核之间扮演了使者的角色,应用程序发送各种请求,而内核负责满足这些请求(或者让应用程序暂时搁置)。实际上提供这组接口主要是为了保证系统稳定可靠,避免应用程序滥用。并且提供了简单一致的机制供应用程序使用。
    操作系统为用户态进程与硬件设备进行交互提供了一组接口——系统调用:

  • 把用户从底层的硬件编程中解放出来
  • 极大的提高了系统的安全性
  • 使用户程序具有可移植性
  • 系统调用是用户态进入内核态的唯一入口。

    2、用户态、内核态和中断

    内核态:一般现代CPU有几种指令执行级别。在高执行级别下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别对应着内核态。
    用户态:在相应的低级别执行状态下,代码的掌控范围有限,只能在对应级别允许的范围内活动。如intel x86 CPU有四种不同的执行级别0-3,Linux只使用0级表示内核态,3级表示用户态。权限级别的划分使系统更稳定。
    区分用户态与内核态主要通过代码段选择寄存器cs和偏移量寄存器eip,cs寄存器的最低两位表明了当前代码特权级,CPU每条指令的读取都是通过cs:eip这两个寄存器一般在Linux中,(逻辑)地址空间是显著标志:0xc0000000以上的地址空间只能在内核态下访问,0x00000000-0xbfffffff的地址空间在两种状态下都能访问。
    中断处理是从用户态进入内核态的主要方式。系统调用只是一种特殊的中断。从用户态切换到内核态时:必须保存用户态的寄存器上下文,同时将内核态的寄存器相应的值放入当前CPU。中断/int指令会在堆栈上保存一些寄存器的值:如用户态栈顶地址、当前的状态字、当时cs:eip的值(当前中断程序的入口)Linux内核代码中定义了两个宏指令来进行保护和恢复。
    系统调用处理程序也其他异常处理程序的结构类似,执行下列操作

  • 在进程的内核态堆栈中保存大多数寄存器的内容,即保存恢复进程到用户态执行所需要的上下文
  • 根据用户态传递的系统调用号,确定系统
  • 调用名为系统调用服务例程的相应的C函数来处理系统调用
  • 从系统调用返回
  • 三、实例:初始化 MenuOS 系统的网络功能,跟踪分析 TCP 协议

    之前我们已经将 TCP 网络程序的服务端 replyhi 集成到 MenuOS 中了,而且可以正常的启动 TCP 服务,方便我们跟踪 socket、bind、listen、accept 几个 API 接口到内核处理函数,但是我们启动的 TCP 服务并不能正常对外提供服务,因为 MenuOS 没有初始化网络设备(包括本地回环 loopback 设备),因此它无法接收到任何网络请求。接下来我们将激活 Linux 网络设备,并将 MenuOS 系统的网络设备用简便的方式配置好,使我们将 TCP 客户端也集成进去后可以完整的运行 TCP 网络程序的服务端和客户端程序。并将 C/S 方式的网络通信程序的客户端也集成到 MenuOS 系统中,成为 MenuOS 系统的命令 hello,我们 git clone 克隆一个 linuxnet.git;进入 lab3 目录执行 make rootfs,脚本就可以帮助我们自动编译、自动生成根文件系统,还会帮我们运行起来 MenuOS 系统。运行起来的 MenuOS 中执行 help 命令可以看到其中不止有 replyhi,也有了 hello 命令,我们可以先执行 replyhi,然后执行 hello。

    因为篇幅有限,我们在这里对Socket两个非常常用的系统调用sock_create函数和sock_map_fd函数进行跟踪,具体步骤如下:

    1、初始化 MenuOS 系统的网络功能,跟踪分析 TCP 协议

    打开Ubuntu虚拟机后,进入上次实验的装有MenuOS内核的文件夹,在实验之前,首先要修改上次的~/MenuOS/menu/Makefile文件:

    1 ls
    2 cd menu
    3 sudo su // 切换至root用户以修改Makefile文件
    4 gedit Makefile // 去掉-S

    这里比较重要的是sock_alloc()和pf->create()这两个函数。其中sock_alloc()里体现了linux一切皆文件理念,即使用文件系统来管理socket。

  • sock_alloc()函数分配一个struct socket_alloc结构体,将sockfs相关属性填充在socket_alloc结构体的vfs_inode变量中,以限定后续对这个sock文件允许的操作。同时sock_alloc()最终返回socket_alloc结构体的socket变量,用于后续操作。
  • pf->create调用了inet_create()函数,而inet_create()函数一方面通过inetsw[]数组获取对应协议类型的接口操作集信息,另一方面创建struct sock类型的变量,最后是对创建的sock进行初始化。
  • 3、sock_map_fd()函数

    在~/linux-5.0.1/net/socket.c中找到了相应的函数定义,如下图(自己在里面加了一些注释):

    这个函数主要有两个部分,一个是创建file文件结构,fd文件描述符,另一部分是将file文件结构和fd文件描述符关联,同时将上一步返回的socket也一起绑定,形成一个完整的逻辑。

    参考链接:

    https://www.shiyanlou.com/courses/1198/learning/?id=9010

    https://blog.csdn.net/u010039418/article/details/79347844