一般来说,使用套接字进行网络编程时,默认使用linux内核提供的网络服务。但是,现在我们自己在用户空间构建了一个tcp协议栈,并且让它为其他应用程序提供网络服务,这势必要求我们自己实现一套新的套接字接口,并且提供给其他应用程序指定使用。
但是,我们并不希望把该tcp协议栈封装成动态库的形式,因为这样一来,应用程序的编译是必须要把库一起连接进去的。那么原生网络编程开发的程序就不能基于我们tcp协议栈来运行了。
一种较好的设计思路是,把tcp协议栈剥离出来作为一个独立的组件来运行,然后通过一个中间件,把网络程序与tcp协议栈协同工作起来。这个中间件的主要工作就是负责偷龙换凤,也就是把网络程序中的内核网络服务转换成独立运行的tcp协议栈的网络服务。
核心思路:网络程序(curl)+自定义套接字库(liblevel.so)+tcp协议栈(level-ip),如下图:
curl小工具
curl是一种命令行工具,作用是发出网络请求,然后得到和提取数据,显示在"标准输出"(stdout)上面。我们直接在curl命令后加上网址和端口,就可以看到网页源码。比如抓取www.sina.com网址:
curl www.sina.com 80
下面我们来看一个curl工具的简易实现,如下图:
第3行:判断目标主机名是否合法
第11行:判断目标端口好是否合法
第16行:完成主机名到地址解析
第21行:使用socket申请一个套接字描述符
第23行:使用connect函数发起tcp连接
第30行:按照http 1.1协议来填充要发送的内容,此处为http协议的get请求
第33行:调用write来发送网络数据
第41行:在while循环中重复接收服务器返回来的网页数据,并且打印在当前控制台终端上。
这是标准的网络应用程序,使用gcc命令编译后即可运行。
gcc curl.c -o curl
liblevelip.so库
level-ip脚本
liblevelip.so库重新封装了常用的socket套接字,并借助socket原生的本地套接字接口来与tcp协议栈(level-ip)进行数据通信。以后在curl程序使用socket套接字时,优先使用该库的服务接口,而不是内核的网络服务。这是通过level-ip这个shell脚本完成的,具体命令如下:
./level-ip curl www.sina.com 80
我们来分析一下level-ip这个shell脚本的原理,如下图:
第1行:执行该shell脚本由/bin/sh程序来执行。
第3行:指定脚本如果发生错误,或者遇到不存在的变量就报错,并停止执行。
第5行:保存脚本的第一个参数到prog变量中。
第6行:去掉一个参数,即原来的1,2,依此类推。
第8行:LD_PRELOAD是一个环境变量,其指定的动态库加载等级最高。@表示第二个参数之后的全部参数。
综上所述,我们就可以确定curl程序是优先加载liblevelip.so库来使用了,通过这种打桩技术,我们可以在加载阶段替换部分系统函数的调用,比如我们常用的socket接口。
liblevelip.c文件
liblevelip.so库由liblevellip.c文件编译而来,该文件在tools文件夹中,我们逐步来分析一下这个c文件。
__libc_start_main函数
首先是__libc_start_main函数,该函数原本是glibc库里面的函数,curl程序里面的main函数就是从这里开始被调用。但是我们在liblevelip.c里面实现了这个函数,并且liblevelip.so的库加载顺序优先于glibc的动态库加载。因此在执行curl程序中的main函数之前,此函数先被执行。如下图:
第3行:dlsym函数里的第一个参数为RTLD_NEXT,这意味着我们将从其他动态库去加载__libc_start_main函数符号(比如glibc库),然后把函数句柄赋值给__start_main变量。
第7~22行:从glibc库中加载一部分linux系统原生提供的系统调用接口。因为我们的网络服务还是要依赖于一些更底层的系统调用接口的。
第24行:初始化一个链表节点lvlip_socks。
第26行:调用glibc的原生__libc_start_main接口。
socket函数
接下来就是liblevelip.so对外提供的第一个网络编程接口--socket函数了。该函数实现如下:
第3~5行:检查网络通信协议族是否为tcp协议,如果不是tcp协议,则调用内核提供的网络服务。
第9行:借助tcp本地套接字接口,与tcp协议栈建立连接,用于通信的本地文件为/tmp/lvlip.socket
第11行:申请一个lvlip_sock类型的buff用于管理socket信息。结构体类型如下:
struct lvlip_sock { struct list_head list; int lvlfd; /* For Level-IP IPC */ int fd;};
list成员变量为链表结点
lvlfd记录与tcp协议栈通信的网络文件描述符
fd记录tcp协议栈的返回状态发送socket消息给tcp协议栈第12行:记录与tcp协议栈通信的网络文件描述符到sock->lvlfd第13行:把这次的网络通信消息加入lvlip_socks链表中第14行:网络通信消息数量加1第16行:获取当前线程的pid号
第17~18行:申请ipc_msg+ipc_socket结构体长度的buff,用于发送详细的socket信息到tcp协议栈。结构体定义如下:
struct ipc_msg { uint16_t type; pid_t pid; uint8_t data[];} __attribute__((packed));
type:记录此次socket信息的具体类型
pid:记录请求网络服务的进程pid号
data:存放具体的通信内容
struct ipc_socket { int domain; int type; int protocol;} __attribute__((packed));
实际上就是soket函数的三个参数。
第23~29行:把ipc_socket作为通信的具体内容填充到ipc_msg的data区域中去
第31行:调用transmit_lvlip()函数真正给tcp协议栈发送消息,并且等待协议栈的数据回复。
此处我们就把liblevelip.so中的socket函数给剖析清楚了,其他诸如close、connect、write、read、send、sendto、recv、此处我们就把liblevelip.so中的socket函数给剖析清楚了,其他诸如close、connect、write、read、send、sendto、recv、此处我们就把liblevelip.so中的socket函数给剖析清楚了,其他诸如close、connect、write、read、send、sendto、recv、recvfrom、poll、select等函数,原理都是一样的,此处不再展开分析。
tcp协议栈(level-ip)
用户空间的level-ip协议栈,在运行之初,就已经在main函数里面创建了一系列线程。如下图:
其中第9行,在run_threads()函数里创建了一系列线程,如下图:
在这里,我们重点关注第5行创建的start_ipc_listener线程
该线程的实现如下:
第5行:指定tcp本地通信的路径文件为"/tmp/lvlip.socket",与我们前面liblevelip.so库的本地通信文件一致,这就说明它们之间确实是通过tco本地通信接口来通信的
第10行:调用socket接口开始进行tcp本地通信
第24行:调用bind函数绑定本地通信路径
第31行:调用listen函数监听指定端口,等待liblevelip.so库发起连接
第46行:如果liblevelip.so库发起连接,则调用accept函数准备开始收发信息。
第54行:每监听到一个新的连接,新创建一个socket_ipc_open函数来进行数据的具体收发。
socket_ipc_open函数主要是负责通信信息的读取,然后根据通信消息的类型不同,来进一步调用具体的处理函数,其实现如下:
第7行:调用read函数进行数据的读取
第8行:调用具体指令的回调信息
demux_ipc_socket_call的函数非常简单,实现如下:
前面我们在liblevelip.so库中调用socket()函数的时候,发送的消息类型为IPC_SOCKET,所以在此处我们进一步分析ipc_socket()这个函数。
它的具体实现如下:
我们重点是关注第7行的_socket函数,该函数就是tcp协议栈的核心接口之一了,它是整个tcp协议栈的真正入口,我们以后再来专门分析这个接口。然后第9行,ipc_write函数负责把tcp协议栈的处理结果返回给liblevelip.so库,代码较为简单,此处不再分析。
原文标题:Linux系统中间件的巧妙实现--以用户空间的tcp协议栈为例
文章出处:【微信公众号:FPGA之家】欢迎添加关注!文章转载请注明出处。
责任编辑:haq
全部0条评论
快来发表一下你的评论吧 !