十、中间件开发专题
0.项目仓库
1.用户态协议栈
1.1 Netmap
-
Netmap是一个高性能收发原始数据包的框架,由Luigi Rizzo等人开发完成,其包含了内核模块以及用户态库函数。其目标是,不修改现有操作系统软件以及不需要特殊硬件支持,实现用户态和网卡之间数据包的高性能传递。数据包不经过操作系统内核进行处理,用户空间程序收发数据包时,直接与网卡进行通信。
-
网卡通过循环队列(即NIC环)来管理数据包的传输,每个网卡至少维护一对NIC环,分别用以管理发送和接收两个方向的数据包。每一个环被划分为很多槽,每一个槽都包含存储数据包的缓冲区的信息:缓冲区长度、缓冲区的物理地址等。在通常情况下,主机网络堆栈可以访问NIC环上指定的槽,也就可以访问到存放数据包的缓冲区,实现数据包的发送和接收。
-
网卡所管理的内存空间是内核空间,运行在用户态的应用程序正常情况下无权访问内核空间,因此,需要进行从内核空间到用户空间的拷贝,零拷贝就是减少或消除这种拷贝,如直接缓存访问(direct buffer access)。DBA为了节省内核态和用户态之间的拷贝,可以将应用程序直接跑在内核态,如内核态的Click。也可以选择将内核中的数据包缓存区直接暴露给用户态程序,如PF_RING和Linux的PACKET_MMAP。
-
netmap也是一个基于零拷贝思想的高速网络I/O架构。当网卡运行在netmap模式下,NIC环会与主机协议栈进行断开,netmap会拷贝一份NIC环,被称作netmap环。同时,netmap还会维护一对环,用于与主机协议栈进行交互。这些环所指向的用于存储数据包内容的缓存位于共享空间,网卡直接将数据包存入这些缓存。应用程序可以通过调用netmap API访问netmap环中的数据包内容,也就可以访问用于存储数据包的缓存,也就是说,当应用程序需要访问数据包内容时,无需从内核空间到用户空间的拷贝,可以直接访问,从而实现了网络数据包的零拷贝。此外,netmap并不会将网卡寄存器和内核的关键内存区域暴露给应用程序,因而用户态的应用程序并不会导致操作系统崩溃,所以相对一些其他的零拷贝架构,netmap更为安全。
-
netmap还会通过以下几种手段来增加网络I/O的速度:
- 1)预分配固定大小的数据包存储空间,以消除每个数据包存储时动态分配内存所导致的系统开销;
- 2)让用户态程序直接访问到网络数据包缓冲区,以减少数据拷贝的开销;
- 3)使用一个轻量级的元数据表示,以屏蔽硬件相关的一些特性。该元数据表示支持在一次系统调用中处理大量数据包,从而可以减少系统调用的开销。以发包速度为例,netmap可以在900MHz单核CPU上处理10G以太网的线速(14.88Mpps)。
-
编译安装
git clone https://github.com/luigirizzo/netmap.git ./configure make -j 8 sudo make install sudo insmod netmap.ko
1.2 用户态协议栈
-
以太网帧格式
-
IP协议头格式
-
UDP头部格式
-
TCP头部格式
2.定时器
3.分布式锁
4.协程
4.1 知识准备
- 现代操作系统是分时操作系统,资源分配的基本单位是进程,CPU调度的基本单位是线程。
- C++程序运行时会有一个运行时栈,一次函数调用就会在栈上生成一个record。
- 运行时内存空间分为全局变量区(存放函数,全局变量),栈区,堆区。栈区内存分配从高地址往低地址分配,堆区从低地址往高地址分配。
- 下一条指令地址存在于指令寄存器IP,ESP寄存值指向当前栈顶地址,EBP指向当前活动栈帧的基地址。
- 发生函数调用时操作为:将参数从右往左一次压栈,将返回地址压栈,将当前EBP寄存器的值压栈,在栈区分配当前函数局部变量所需的空间,表现为修改ESP寄存器的值。
- 协程的上下文包含属于他的栈区和寄存器里面存放的值。
- 协程是为了使用异步的优势,异步操作是为了避免IO操作阻塞线程。那么协程挂起的时刻应该是当前协程发起异步操作的时候,而唤醒应该在其他协程退出,并且他的异步操作完成时。
4.2 协程实现
- 协程发起异步操作的时刻是该挂起协程的时刻,为了保证唤醒时能正常运行,需要正确保存并恢复其运行时的上下文:
- 保存当前协程的上下文(运行栈,返回地址,寄存器状态)
- 设置将要唤醒的协程的入口指令地址到IP寄存器
- 恢复将要唤醒的协程的上下文
5.连接池
5.1 资源重用
- 由于数据库连接得到重用,避免了频繁的创建、释放连接引起的性能开销,在减少系统消耗的基础上,另一方面也增进了系统运行环境的平稳性(减少内存碎片以及数据库临时进程/线程的数量)。
5.2 更快的系统响应速度
- 数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于池中备用。此时连接的初始化工作均已完成。对于业务请求处理而言,直接利用现有可用连接,避免了从数据库连接初始化和释放过程的开销,从而缩减了系统整体响应时间。
5.3 统一的连接管理,避免数据库连接泄露
- 在较为完备的数据库连接池实现中,可根据预先的连接占用超时设定,强制收回被占用连接。从而避免了常规数据库连接操作中可能出现的资源泄露。
6.RPC(远程过程调用)
6.1 RPC框架
6.2 RPC实现
- 基于TCP协议实现的RPC调用,由于TCP协议处于协议栈的下层,能够更加灵活地对协议字段进行定制,减少网络开销,提高性能,实现更大的吞吐量和并发数。
- 基于HTTP协议实现的RPC则可以使用JSON和XML格式的请求或响应数据。HTTP协议是上层协议,发送包含同等内容的信息,使用HTTP协议传输所占用的字节数会比使用TCP协议传输 所占用的字节数更高。
7.异常捕获
7.1 异常捕获原理
-
从原理上来讲,throw其实就是一个跳转,跳转到由try-catch块包围的catch块处。在这里,我们用两个函数来实现这个功能:
int setjmp(jmp_buf env); void longjmp(jmp_buf env, int val);
-
setjmp函数记录调用时的当前状态,如IP、ESP等,并且返回0。状态被写在一个jmp_buf对象中,这个对象其实是一个int数组。比较有趣的是longjmp函数,这个函数传入一个jmp_buf以及一个整形。它会立即跳转到当时的setjmp处,并且返回值是longjmp中的第二个参数。也就是说,这个函数可能会被调用两次,从某种程度上来说,有点像fork()。
-
-
在try-catch中,try函数充当着setjmp的功能。当setjmp返回0时(也就是第一次执行),执行try块的代码,当返回非0时,说明有longjmp被调用,此时发生异常,跳入catch块。同时,throw就相当于longjmp,可以跳转到包含它的catch块中。
-
longjmp的第一个参数jmp_buf,其实是在try块中创建的。我们怎么来获取到上一个try块中创建的jmp_buf呢?我们可以如同操作系统创建一个运行时栈那样,我们也创建一个try-catch堆栈。在try时,我们把创建的jmp_buf压入,在throw时,我们把jmp_buf弹出。为了线程安全,我们得为每一个线程分配这样一个栈。
Last modified on 2020-12-28