概述
用户态协议栈是指把网络协议栈accept()、listen()等原本在操作系统中的接口,与应用程序放在一起,把网络协议的解析放做进程中的一部分。用户态协议栈的主要作用,是网络数据从网卡到应用程序拷贝过程中的系统调用次数,从而减少CPU上下文切换的次数,达到提高性能的目的。
有了用户态协议栈,对于网卡的想象空间也会增大,网卡的可操作性会更强。比如,可以通过控制网卡,将PC机做成交换机或路由器,可以过滤数据。另外,用户态协议栈可以用在网关上,因为当连接数增多,网关会成为性能的瓶颈,使用用户态协议栈可以提高网关的性能。
通信流程
解释其中的一些元素。
网卡
网卡不属于七层模型中的任何一层,是在物理层和数据链路层之间,其作用是将网络中的光电信号与计算机能识别的数字信号进行相互转换。
协议栈
是操作系统内核中,对网卡数据进行解析的部分,通过结构体sk_buff将网卡中的数据拷贝到协议栈(位于内核中)中。
数据拷贝过程
每次数据从网卡到达应用程序,需经历两次拷贝过程,一次是上面提及的从网卡拷贝到协议栈中,另一次是从内核协议栈拷贝到应用程序的用户空间中。
这里介绍一下零拷贝技术。可以通过mmap映射的方式,将网卡中的数据直接映射到内存中,由于行走程序可以直接访问内存,再加上DMA支持。由于映射过程使用DMA自己操作,不需要CPU干预,所以没有显式的拷贝操作,叫做零拷贝技术。
这里简单介绍一下,如何从网卡中取到一帧完整的网络数据。有三个方法,一是利用原生的socket如raw socket,二是利用开源框架如netmap,三是利用商业框架如dpdk。
这里扩展一点,驱动程序是运行在操作系统的内核中,而不是运行在对应的硬件上,包括nic子系统也是运行在内核上使网卡正常工作的。
另外,零拷贝技术还可以用在持久化操作中,例如日志落盘,先写在内存中的映射区,而后由映射区同步到磁盘中,优点是比直接操作文件快,缺点是无法保证写到磁盘中的数据是全部数据,因为在映射区中还未同步的数据有可能被覆盖或丢失。
协议栈
协议栈中的协议较多,协议格式大多容易找到,不再赘述。这里主要提示几个值得注意的地方。
IP
IP头中totlen长度为16bit,也就是说,可以支持的IP数据包最大大小为64KB,那么大家所说的MTU(一般为1500B),又是怎么回事呢?
MTU是以太网的限制,两者不在同一层中,也就是说,它们是两码事。IP层确实可以发一个64K的包,只不过,到了以太网这一层,会分成1500B一个一个的包,这种分包是开发人员无法通过措施避免的,所以要提前考虑到。
ICMP
ICMP(Internet Control Message Protocol)叫Internet控制报文协议。它是TCP/IP协议簇的一个子协议,用于在IP主机、路由器之间传递控制消息。值得注意的是,它是处于网络层的协议,而数据包的代码定义
struct icmppkt
{
struct ethhdr eh;
struct iphdr ip;
struct icmphdr icmp;
};
很容易让人看成是传输层的协议。因为传统网络层协议的结构体应该是这样
struct arppkt
{
struct ethhdr eh;
struct arphdr arp;
};
而且,更为致命的是,在IP包头的宏定义中,居然还定义了ICMP的协议号
#define IPPROTO_ICMP=1
事实上,ICMP使用IP的基本支持,就像它是一个传输层的协议,但是,ICMP实际上是IP的一个组成部分,必须由每个IP模块实现。
UDP
注意UDP头是没有id这样的字段的,也就是说UDP协议本身无法分辨具体来的包哪一个是哪一个,换句话说,就是没有数据包的概念,所以需要在应用层制定协议的时候进行区分。
交换机
交换机只能做到数据转发的功能。如果只有交换机,这个网络只适用于局域网,因为这个网络无法向外网发送数据,要想跨网段,需要有路由器。
NAT
NAT是工作在传输层的,需要对传输层的协议进行解析,最明显的标志就是需要端口。
负载均衡
这里介绍几款负载均衡的软件。
nginx:工作在应用层,对应用层协议进行解析。
haprox:工作在传输层。
lvs:工作在网络层,对IP地址进行操作。
F5:工作在数据链路层。
柔性数组
这里UDP数据包的定义用到了柔性数组,又叫零长数组。
struct udppkt
{
struct ethhdr eh;
struct iphdr ip;
struct udphdr udp;
unsigned char body[0];
};
sizeof()的时候是不占大小的。柔性数组相当于标签,可以理解为数组首地址。要使用柔性数组,需要满足两个前提,一是这个数组的空间已分配好,否则会操作未知空间,类似野指针。二是数组长度可以通过其他方式计算出来,否则会有越界的风险。这里用到的方法就是根据UDP头部中的length字段进行计算。
其他问题
ens33
ubuntu系统安装后。网卡名可能是ens33而不是eth0,这是由于在虚拟机安装的原因,物理机上就是eth0。强烈建议改为eth0形式。方法是sudo vim /etc/default/grub
,在GRUB_CMDLINE_LINUX
这一行的中间添加net.ifnames=0 biosdevname=0
。
双网卡
使用netmap时最好给虚拟机设置两个网卡,一个用于netmap,另一个用于ssh,否则在使用netmap时,ssh可能会连不上。因为网卡的数据被netmap全部接管,包括tcpdump可能也抓不到数据。如果是云服务器,就不建议使用netmap了,因为云服务器有很多东西已经虚拟完了,所以云服务器只适合处理业务。
代码实现
具体协议头的的定义不再给出,这里只给出用户态协议栈的一般实现流程。
int main()
{
struct ethhdr *eh;
struct pollfd pfd = {0};
struct nm_pkthdr h;
unsigned char *stream = NULL;
struct nm_desc *nmr = nm_open("netmap:eth0", NULL, 0, NULL);
if (nmr == NULL)
{
return -1;
}
pfd.fd = nmr->fd;
pfd.events = POLLIN;
while (1)
{
int ret = poll(&pfd, 1, -1);
if (ret < 0) continue;
if (pfd.revents & POLLIN)
{
stream = nm_nextpkt(nmr, &h);
eh = (struct ethhdr*)stream;
if (ntohs(eh->h_proto) == PROTO_IP)
{
struct udppkt *udp = (struct udppkt*)stream;
if (udp->ip.protocol == PROTO_UDP)
{
struct in_addr addr;
addr.s_addr = udp->ip.saddr;
int udp_length = ntohs(udp->udp.len);
printf("%s:%d:length:%d, ip_len:%d --> ", inet_ntoa(addr), udp->udp.source, udp_length, ntohs(udp->ip.tot_len));
udp->body[udp_length-8] = '