- 环境抽象层EAL
环境抽象层的任务对访问底层资源例如硬件和内存提供入口。它提供了隐藏应用和库的特殊性性的通用接口。它的责任是初始化分配资源(内存,pci设备,定时器,控制台等等)。
EAL提供的典型服务有:
l DPDK加载和启动:DPDK和程序连接成 一个单独的程序且必须通过某种方式加载。
l CPU亲和性/分配处理:EAL提供了一种机制将执行单元分配给特定的核就如同创建一个执行程序一样。
l 系统内存分配:EAL实现不同内存区域的分配,例如用于物理设备交互的内存区域。
l PCI地址的抽象:EAL提供了对PCI地址空间访问的接口。
l 追踪调试功能:日志,栈转存,异常等等。
l 公用功能:libc库不能提供的自旋锁和原子计数器
l CPU特征辨识:决定cpu运行时的特殊功能,例如:Intel AVX。决定当前CPU支持的特性以便编译对应的二进制程序。
l 中断处理:提供注册/反注册对特定中断的回调函数。
l 告警功能:设置或取消运行时特定时间告警处理的回调函数接口。
3.1. linux执行环境下的EAL
在linux用户态空间,DPDK程序使用pthread线程库最为一个用户态程序运行。设备的pci信息和地址空间通过/sys内核接口和像uio_pci_generic或者是igb_uio内核模块来获取。参阅UIO:linux内核中用户态驱动文档。这段内存是在程序中mmap的。
EAL执行物理地址的分配,从hugetblbfs管理的内存通过使用mmap()实现(使用大页来提升性能)。这些内存对于dpdk服务层都是可见的例如Mempool Library。
在这点上,dpdk服务层已经初始化了,接着通过设置线程亲和性的调用,每个执行单元将会分配给特定的逻辑核心想一个user-level线程一样运行。
定时器是由CPU的时间戳计时器(TSC)或者是通过mmap()调用的HPET内核接口提供。
3.1.1. 初始化和core运行
初始化分布是gblic的开始函数做的。检查也是在初始化时做的以确保配置文件中选择的架构宏定义是cpu支持的类型。然后,main()函数被调用。core的初始化和加载使在rte_eal_init()中做的(看API文档)。它由线程库的调用组成(pthread_self(),pthread_create(),pthread_setaffinity_np())。
注意:对象的初始化,例如内存区域,ring,mempool,lpm表和hash表,应该作为整个程序初始化的一部分在主逻辑核上完成。创建和初始化这些对象的函数不是多线程安全的。不管怎么样,一旦初始化完成,对象自身可以安全用于多线程。
3.1.2. 多进程支持
linux下EAL允许同多线程部署模式一样支持多进程。具体看2.20章节《Multi-process Support》
3.1.3. 内存映射和内存分配
大量的物理连续的内存分配是使用hugetlbfs内核文件系统来做的。EAL提供了一个API来申请指定大小的物理连续的内存块。这个API也会返回申请到的内存的物理地址给用户。
注意:内存申请是用rte_malloc库接口来做的,它也是hugetlbfs文件系统大页支持的。
3.1.4. Xen Dom0非大页支持
现存的内存管理机制是基于linux内核的大页机制。然而,Xen Dom0并不支持大页,所以要将一个新的内核模块rte_dom0_mm插入以避开这个限制。
EAL使用IOCTL接口通知内核模块rte_dom0_mm分配指定大小的内存且从这个模块获取所有内存段信息。EAL使用MMAP接口映射分配到的内存。对所有的内存段来说,在其内的物理地址都是连续的,但是实际上硬件地址只是在2MB内连续。
Fig.3.1 linux应用环境下EAL初始化过程
3.1.5. PCI设备访问
EAL使用内核提供的/sys/bus/pci扫描PCI总线上的内容。访问PCI内存,内核模块uio_pci_generic 提供了/dev/uioX设备文件以及/sys中的资源文件,它被映射以获取从用户态程序访问pci地址空间的能力。DPDK特有的igb_uio模块也提供了这一功能。这两个驱动都用到uio内核特性(用户态驱动)。
3.1.6. 每个逻辑核共享变量
注意:逻辑核就是处理器的逻辑执行单元,又是也称作硬件线程。
共享变量是默认的做法。额米格逻辑核的变量的实现是通过使用Thread Local Storage(TLS)线程局部存储?来提供每个线程的本地存储。
3.1.7. 日志
EAL提供了日志API.在linux环境下,日志默认是发送到系统日志文件和终端上.然而,用户可以使用不同的日志机制来代替DPDK提供的日志功能.
调试功能
有一些调试函数转存栈数据. rte_panic()能自动产生一个abort信号,这个信号会触发产生gdb调试用的core文件。
3.1.8. cpu特性
EAL可以在运行时查询CPU状态(通过rte_cpu_get_feature()函数)决定哪个cpu可以用。
3.1.9. 用户态中断事件
l 主线程对用户态的中断和警告处理
EAL创建一个主线程轮询UIO设备文件描述符以检测中断。EAL可以注册或者是反注册一个特定中断的回调函数,这个函数可以在主线程中异步调用。EAL也允许像NIC的中断同样的方式注册定时器中断回调函数。
注意:DPDK PMD,主线程只对连接状态改变的中断处理,例如网卡连接打开和关闭操作。
l 收包中断事件
PMD提供的收发包程序并不限制自身在轮询模式下执行。对于极小的流量减少轮询下的cpu利用率,可以中断轮询并等待wake-up事件的发送。收包中断对于此类的wake-up事件是最佳选择,单也不是唯一的。
EAL提供了事件驱动模式的API。以linuxapp为例,其运行依赖于epoll。EAL线程可以监控添加了所有wake-up事件的文件描述符对象。事件文件描述符可以创建并根据UIO/VFIO说明来映射到中断向量。对于bsdapp,kqueue是可选的,但是尚未实现。
EAL初始化事件描述符和中断向量之间的映射关系,每个设备初始化中断向量和队列之间的映射。这样,EAL实际上是忽略在指定向量上的发生的中断。eth_dev驱动会负责执行后者的映射。
注意:每个RX中断事件队列只支持VFIO,后者支持多个MSIX向量。在UIO中,收包中断和其它发生的中断共享中断向量。所以,当RX中断和LSC(连接状态改变)中断同时发生时(intr_conf.lsc==1 && intr_conf.rxq==1),只有前者生效。
使用网卡设备API控制、打开、关闭RX中断<rte_eth_dev_rx_intr_*>,如果PMD不支持则返回失败。intr_conf.rxq标识用于打开每个设备的RX中断。
3.1.10. 黑名单
EAL pci设备黑名单功能是用于标记网卡某一个端口作为黑名单,以便DPDK忽略该端口。用PCIe描述符(Domain:Bus:Device.Function)将端口标记黑名单。
3.1.11. 复杂指令集功能
i686和x86_64架构上的锁和原子操作。
3.2. 内存分段和内存区(memzone)
物理地址的映射就是EAL通过这个来实现的。物理内存可能是分隔不连续的,所有的内存都由一个内存描述符表管理,且每个描述符(rte_memseg)指向的都是一段连续的物理内存。
在这之上,memzone分配器的角色就是保存一段物理连续的内存。这些内存区的内存被申请到使用时会有一个唯一名字来标示。
rte_memzone描述符也在配置结构体中。可以使用te_eal_get_configuration()接口访问这个结构体。通过名字查找一个内存区会返回一个有内存区物理地址的描述符。
内存区可以以特定的开始地址以指定的对齐参数对齐(默认是cache_line大小的对齐)。对齐值应该是2的幂次方且不少于cache_line大小(64字节)。内存区可以是2m或者是1g的内存页,系统两者都支持。
3.3. 多线程
dpdk通常是指定核上跑指定线程以避免任务调度的开销。这个对于性能的提升很有用,但是缺少灵活性且不是总是有效的。
通过限制cpu的运行频率,电源管理有助于提升CPU效能。然而也能是利用空闲的指令周期来充分的使用CPU全部性能。
通过使用cgroup,cpu使用量可以很轻松的分配。这个提供了另外的方式来提升cpu效能,然而有一个先决条件DPDK必须处理每个核多线程之间的上下文切换。
要更加灵活,就设置线程的cpu亲和性不是对cpu而是cpu集。
3.3.1. EAL线程和逻辑核亲和性
lcore指的是EAL线程,就是一个真正的linux/freeBSD线程。EAL创建和管理eal线程,且通过remote_launch来实现任务分配。在每个EAL线程中,有一个称为_lcore_id TLS是线程的独一无二的id。一般EAL线程使用1:1来绑定物理cpu,_lcore_id通常等于CPU id。
当使用多线程时,绑定不再在线程和指定物理cpu之间总是1:1,EAL线程设为对cpu集的亲和性,而_lcore_id不再和CPU id一样。因为这个,有一个EAL选项-lcores,设置lcore的cpu亲和性。对于指定lcore ID或者是ID组,这个选项允许对EAL线程设置CPU集。
格式模板:-lcores=lcores=’<lcore_set>[@cpu_set][,<lcore_set>[@cpu_set],...]’
lcore_set 和cpu_set可以是一个数,范围或者是组。数必须是“digit([0-9]+)”,范围则是“<number>-<number>”,组则是“(<number|range>[,<number|range>,...])”。
如果@cpu_set的值没有提供,则默认将其设为lcore_set相同的值。
例如:"--lcores='1,2@(5-7),(3-5)@(0,2),(0,6),7-8'"意味着启动9个eal线程“
lcore 0 runs on cpuset 0x41 (cpu 0,6);
lcore 1 runs on cpuset 0x2 (cpu 1);
lcore 2 runs on cpuset 0xe0 (cpu 5,6,7);
lcore 3,4,5 runs on cpuset 0x5 (cpu 0,2);
lcore 6 runs on cpuset 0x41 (cpu 0,6);
lcore 7 runs on cpuset 0x80 (cpu 7);
lcore 8 runs on cpuset 0x100 (cpu 8).
使用这个选项,每个给定的lcore ID可以分配指定的cpu。也兼容corelist(‘-l‘)选项模式。
3.3.2. 非EAL线程支持
在DPDK执行上下文环境中执行用户线程(也称作非EAL线程)时可行的。在非EAL线程中,_lcore_id总是等于LCORE_ID_ANY,这个宏标示有效的非EAL线程,其值是唯一的_lcore_id。一些库会使用传统的id(例如线程标示符TID),有些是一点影响都没有,有些则是由使用限制(如timer和mempool库)。
所有的影响都在Known Issues章节中提到了。
3.3.3. 公共线程API
有两个公用API:rte_thread_set_affinity()和rte_pthread_get_affinity()。当在任意一个线程上下文环境中使用时,TLS会被设置/获取。
那些TLS包括_cpuset和_socket_id:
l _cpuset存的是线程亲和性的CPU组位图
l _socket_id存的是CPU集的NUMA节点。如果cpu集中的cpu分属不同的numa节点,则_socket_id会被设成SOCKET_ID_ANY(-1)。
3.3.4. 已知问题
l rte_mempool
rte_mempool在mempol中使用每个lcore缓存。对于非EAL线程,rte_lcore_id()返回无效值。所以目前当rte_mempool在非EAL线程中使用,put/get操作将忽视mempool缓存且 由于这个会有性能损失。对非EAL线程的mempool的cache支持现在可以了。
l rte_ring
rte_ring支持多生产者入队列和多消费者出队列。然而,它是非抢占的,这有一个消极的影响:使得rte_mempool也是非抢占的。
注意:非抢占的限制意味着:
一个线程对给定的ring执行多生产者入队列时,队列必须不被其它线程抢占执行入队列。
一个线程对给定的ring执行多消费者出队列时,队列必须不被其它先占抢占执行出队列。
忽略这个限制会引起第二个线程自旋知道第一个线程被重新调度。此外,如果第一个线程被更高优先级的上下文抢占,可能会引起死锁。
这不意味着它不能用,很简单,有必要在同样的核上减少多线程同时访问ring的场景。
- 可以用于单生产者和单消费者的场景。
- 在调度策略是SCHED_OTHER(cfs完全公平调度程序)时可以在多生产者/多消费者线程中使用。用户需要在使用前知道其性能损失。
- 在调度策略是SCHED_FIFO或者是SCHED_RR时,不能在多生产者/多消费者环境中使用。
为了rte_ring减少竞争定义了RTE_RING_PAUSE_REP_COUNT,主要是为了情况2,在N次重复暂停后放弃对ring的操作。
它增加了sched_yield()系统调用,当线程自旋等待其它线程完成对ring的操作太久时,这个给了被抢占的线程机会继续执行且完成入/出队列操作。
l rte_timer
在非EAL线程中,没有每个线程自有的日志等级和日志类型,用的是全局的日志等级。
l misc
在非EAL线程中,对rte_ring,rte_mempool,rte_timer的调试统计是不支持的。
3.3.5. cgroup控制
下面是一个简单的cgroup控制的使用例子,有两个线程(他t1和t2)在同样的core(CPU)上做包的I/O。我们期望只有50%的CPU用于包I/O:
mkdir /sys/fs/cgroup/cpu/pkt_io
mkdir /sys/fs/cgroup/cpuset/pkt_io
echo $cpu > /sys/fs/cgroup/cpuset/cpuset.cpus
echo $t0 > /sys/fs/cgroup/cpu/pkt_io/tasks
echo $t0 > /sys/fs/cgroup/cpuset/pkt_io/tasks
echo $t1 > /sys/fs/cgroup/cpu/pkt_io/tasks
echo $t1 > /sys/fs/cgroup/cpuset/pkt_io/tasks
cd /sys/fs/cgroup/cpu/pkt_io
echo 100000 > pkt_io/cpu.cfs_period_us
echo 50000 > pkt_io/cpu.cfs_quota_us
3.4. Malloc
EAL提供了一个malloc API分配任意大小内存。这个提供类似malloc函数功能的API目的是允许从大页内存中分配且便于程序移植。DPDK API参考手册详细介绍了这个函数功能。
一般的,这种分配内存的方式不能用于高速数据处理平台,因为它相对于基于pool的分配方式实在太慢了且在分配和释放时都得加锁。然而,可以在配置生成的代码中用到。
可以从DPDKAPI参考手册中查阅rte_malloc()函数的更多细节信息描述。
3.4.1. Cookies
当CONFIG_RTE_MALLOC_DEBUG选项打开时,分配的内存包含内存区域写保护以帮助识别缓存溢出。
3.4.2. 对齐和NUMA结构的限制
rte_malloc()有一个对齐参数,用于申请对齐于这个值的倍数(2的幂次方)的内存区域。
在支持NUMA的系统中,rte_malloc()函数调用会返回调用者所使用core的NUMA socket上分配的内存。提供一系列的API,以实现直接在指定的NUMA socket上分配内存,或者是在其它core所在的NUMA socket上分配,假如内存是其它一个逻辑核使用的而不是正在执行内存分配的这个核。
3.4.3. 用例
这个API用于应用程序在初始化时请求使用malloc相似的函数分配内存。
在运行是分配和释放数据,在程序的快速通道建议用mempool代替。
3.4.4. 内部实现
3.4.4.1. 数据结构
在malloc库内部有两个数据结构类型使用:
l 结构体malloc_heap:用于跟踪每个socket上的空闲内存
l 结构体 malloc_elem:分配的基本元素且库内用于跟踪空闲内存空间
结构体:malloc_heap
malloc_heap结构体用于管理每个socket上的空闲内存空间。实际上,每一个NUMA node上都有一个heap结构体对象,这样就可以实现线程从其所在运行的NUMA node上分配内存。当不能保证所使用的内存是在运行的NUMA node上,那就和在混合或者随机的node上分配内存好不到那里去。
堆结构体中的主要成员和函数描述如下:
l lock:锁成员是为了实现同步访问堆。堆中的空闲内存是一个链表维护的,为了防止两个线程同时访问这个链表就需要加锁。
l free_head:空闲内存链表头指针指向malloc_heap的空闲内存链表的第一个成员。
注意:malloc_heap结构体并不监测使用的内存块,所以这些内存块除非被重新释放否则就绝对无法接触到,重新释放就是将指向内存块的指针作为参数传给free()函数。
Fig.3.2 malloc库内的malloc heap和malloc elemets例子
结构体:malloc_elem
malloc_elem结构体用于各种内存块的通用结构,用于3中不同的方式:
- 作为一个空闲或者是分配的内存块的头-正常情况(
- 作为一个内存块内的填充头
- 作为一个内存表段的结束标记
结构体中最重要的成员和如何使用如下描述:
注意:在上面的三个使用情况中没有用到的特定成员,这些成员可以认为在那种情况下没有明确的值。例如, 对于填充头padding header,只有成员state和pad有可用的值。
l heap:这个指针是堆结构体分配的内存块的引用返回值。它用于释放的普通内存块,将其添加到堆的空闲内存链表
l prev:这个指针是指向在内存表中当前位置后面(不应该是前面吗?)紧靠着的头结构对象/内存块。当释放一个内存块的时候,这个指针指向的内存块会被检查是否也是空闲的,如果是,就将这两个空闲内存块合并成一个大的内存块。(减少内存碎片)
l next_free:这个指针用于将没有分配的内存块连接到空闲内存链表上。它只用于正常的内存块,malloc()时就查看一个合适的空闲内存块分配,free()时就是将重新释放的内存块添加到空闲链表上。
l state:这个成员有三种值:FREE,BUSY,PAD。前两个表明普通内存块的分配状态,后则表明结构体是一个在内存块起始位置填充无意义数据的尾部的虚设结构体。那就是说,数据在内存块中的开始位置不是在数据库的头上,这是由于数据对齐的限制。如此的话,填充头结构就是用于定位块内实际分配的内存的结构体头部。在内存表的尾部,结构体内这个值为BUSY,确保不会再有元素了。在释放的时候会跳过这个搜索其它的内存块来合并成大的空闲内存块。
l pad:这个代表了当前内存块开始位置填充无用段的长度。在一个正常的内存块头部,将它与头结构体尾部地址相加就是数据段开始地址,就是说这个会作为malloc的返回值给程序。在这段填充区内有虚设的头,头内成员pad有同样的值,从该头结构的地址减去pad值就是实际分配的内存块头结构地址。
l size:数据块的大小,包括自身的头部分。在内存表尾部的那个虚设结构体中,这个size是0,尽管它从没有被检查过。在一个标准的内存块释放时,这个值会代替next指针来定位靠在一起的下一个内存块,万一后者是FREE状态,那么二者就可以合二为一了。
内存分配
在EAL初始化时,所有的内存表都是组织到malloc堆下,这是会将内存表的尾部设置一个BUSY状态的虚设结构体。当CONFIG_RTE_MALLOC_DEBUG选项打开且在内存表的头部有一个FREE状态元素头,虚设机构体中就可能包含一个哨兵值。FREE元素会被加入到malloc堆的空闲链表中。
当程序调用类似malloc函数时,malloc函数会先查看调用线程的lcore_config结构体,确定该线程所在的NUMA节点。NUMA节点用作malloc_heap结构体数组的下标,且会作为其中一个参数和其它参数请求内存大小、类型、对齐值、边界一起传递给heap_alloc()函数。
heap_malloc()会先扫描heap的空闲链表,试图找到一个匹配请求存储数据大小和对齐方式、边界限制的空闲内存块,
当一个匹配的空闲元素标记时,算出的内存指针会被返回给用户。而cacheline大小的内存会在指针之前用malloc_elem装填。由于对齐和边界限制,在元素的开头和结尾会有空白空间,这回导致一下问题:
- 检查尾部空间。如果尾部空间足够大,也就是说>128字节,就会分割这个元素。如果不是,那么就忽略这个尾部空间(白白浪费掉的空间)
- 检查元素头空间。如果空间很小,就是<=128字节,就会用部分空间作为填充头结构,其它的也是浪费掉。然而,如果头空间足够大,那么就将这个空闲元素分割成两个。
从现有的元素的尾部分配内存的好处就是不用调整空闲链表来代替-空闲链表上现有元素只需要调整size变量,且其后的元素也只需将prev指针指向新产生的元素就可以了。
释放内存
要释放一段内存,数据段开始地址地址会传递给free函数。指针值减去malloc_elem结构体打下就是这个内存块的元素头。如果头中type是PAD,那就将指针减去pad值得到实际的内存块元素头结构。
从这个元素头中,我们就拿到了从堆中分配的内存块指针,且它需要在哪里释放。和prev指针一样,通过size可以计算出紧挨着的后面一个元素的头指针。检查前后元素是否是FREE,如果是就与当前的元素合并。这意味着我们不可能有两个FREE状态的元素靠在一起,它们总是会被合并成一个单独的内存块。
- Ring Labrary
ring是管理队列的。取代无限制大小的链表,rte_ring有下面的特性:
l FIFO,先进先出
l 大小是固定的,指针存在表中。
l 无锁实现
l 多个或单个消费者出队列
l 多个或单个生成者入队列
l bulk出队列-指定数目对象出队列,否则失败
l bulk入队列-同上
l burst出队列-按照指定数目最大可能得出队列,可能出的不足数
l burst入队列-按照指定数目最大可能得入队列,可能入的不足数
这个数据机构相对于链表队列的好处是:
l 更快。只需要一个大小为sizeof(void *)的原子操作CAS指令来代替多个double CAS指令。
l 比一个完全无锁队列要简单。
l 适应大量入/出对垒操作。由于指针是存储于表中,多个对象出队列就不会如链表一样会出现多个cache丢失。同样,bulk出队列的开销也不会比单个出队列大。
缺点就是:
l 大小固定
l ring会比链表队列消耗更多内存,即使一个空的ring也是包含至少N个指针大小的内存。
对于ring中生产者和消费者head、tailer指针指向的数据结构中存储的对象的简单展示:
Fig. 4.1Ring结构
4.1 Ring在FreeBSD中的应用参考
下面的代码在FreeBSD8.0中添加,用于一些网络设备的驱动(至少是intel的驱动):
l bufring.h in FreeBSD
l bufring.c in FreeBSD
4.2 linux中无锁环形缓存区
下面是描述linux无锁环形缓冲区设计的链接:http://lwn.net/Articles/340400/
4.3 其它特性
4.3.1 名字
每个ring都是通过独一无二的名字来辨别。不可能创建两个同样名字的ring(通过rte_ring_create()创建ring时,如果名字已经存在就返回空)
4.3.2 阀值
ring可以有一个阀值(临界值)。如果这个阀值配置了,一旦入队列操作到达阀值,那么生产者会得到通知。
这个机制可能用到,例如,IO压力过大时可以通知LAN 暂停。
4.3.3 调试
当调试开关打开(CONFIG_RTE_LIBRTE_RING_DEBUG设置了),ring库会存储每个ring的一些关于出入队列的个数的统计,这个统计是每个core都有的以避免同时访问或者是原子操作。
4.4 应用场景
ring库的应用包括:
l DPDK应用程序间的通讯
l 内存池的分配会用到。
4.5 (ring buffer)环形缓冲区的详细剖析
这部分讲解了ring buffer的运作。ring结构体包括两对头尾指针(head,tail)。一对用于生产者,一对用于消费者。就是下面段落中图标提到的prod_head,prod_tail,cons_head 和cons_tail。
每个图都代表了环形缓冲区的一个状态。函数本地变量在图的上方,底下则是ring结构体的内容。
4.5.1 单生产者入队列
这部分讲的是单个生产者将一个对象加入到ring中发生的事。在这个例子中,只有生产者的head和tail(prod_head与prod_tail)被修改了,有且只有一个生产者啊。
初始状态,prod_head和prod_tail指向同一个位置。
入队列第一步
首先,ring->prod_head和ring->cons_tail的值拷贝到本地变量,prod_next本地变量会指向表中的下一个元素位置,或者是bulk入队列后的几个元素
如果ring中没有足够的空间(通过检查cons_tail来判断),返回error。
入队列第二步
第二步就是修改ring结构体中ring->prod_head的值指向prod_next指向的位置。
指向要添加对象的指针被拷贝到ring中(图中obj4)。
入队列最后一步
一旦对象呗加入到ring中,ring->prod_tail就修改成与ring->prod_head指向的相同位置。入队列操作完成。
Fig. 4.2: Enqueue first step
Fig. 4.3: Enqueue second step
Fig. 4.4: Enqueue last step
4.5.2 单消费者出队列
这段讲解了当单消费者从ring中出队列一个对象时发生了啥。在这个例子中,只有ring结构体中消费者的头尾(cons_head和cons_tail)修改,当且仅当是一个消费者啊。
初始状态下,cons_head和cons_tail指向相同的位置。
出队列第一步
首先,ring->cons_head和ring->prod_tail会拷贝到本地变量cons_head和prod_tail。而本地变量cons_next会指向表中的下一个对象(就是cons_head指向的对象的下一个),或者是bulk出队列的多个对象的下一个。
如果ring中没有足够的对象(通过检测prod_tail),返回errors。
出队列第二步
第二步就是修改ring->cons_head指向和cons_next指向的相同位置。
要删除的对象(obj1)指针会拷贝到用户提供的指针上。
Fig. 4.5: Dequeue last step
Fig. 4.6: Dequeue second step
出队列最后一步
最后,ring->cons_tail修改成ring->cons_head相同的值。出队列操作完成。
Fig. 4.7: Dequeue last step
4.5.3 多生产者入队列
这部分讲了两个生产者同时添加一个对象到ring中发生的事。在本例中,只有生产者的头尾(prod_head和prod_tail)被修改了。
初始状态prod_head和prod_tail执行同一个位置。
对生产者入队列第一步(原文是消费者,整个例子下面都是用的消费者,估计是弄错了)
在两个核上,ring->prod_head和ring->cons_tail都拷贝到本地变量。本地变量prod_next执行表中下一个对象位置,或者是bulk入队列的多个对象的下一个位置。
如果没有足够的空间(检查prod_tail)就返回error。
Fig. 4.8: Multiple consumer enqueue first step
多生产者入队列第二步
第二步就是修改ring结构体的ring->prod_head指向prod_next指向的位置。这个操作时用原子操作Compare And Swap(CAS)指令,其原子的执行下面的操作:
l 如果ring->prod_head不同于本地变量prod_head,CAS操作失败,代码重新执行第一步。
l 否则,ring->prod_head设置成本地的prod_next值,CAS操作成功,继续执行。
在途中,这个操作在核1上执行成功,在核2上失败重新执行第一步。
多生产者入队列第三步
CAS操作不停的尝试直到在核2上成功。
核1更新对象(obj4)到ring中,核2更新对象(obj5)到ring中。
Fig. 4.10: Multiple consumer enqueue third step
多生产者入队列第四步
每个核都要更新ring->prod_tail。那个核上ring->prod_tail等于本地变量prod_head的才能更新。这个在本例中只有核1上可以,本步操作在核1上完成。
多生产者入队列最后一步
一旦ring->prod_tail被核1更新完成,核2就可以更新了。这个操作总在在核2上完成。
4.5.4 无符号32位索引
在先前的图中,prod_head,prod_tail,cons_head,cons_tail的值都是被一个箭头代表。在实际使用中,这些值不是在0到ring的大小减去1之间增加,这是假设的情况。值实际是在0到2^32-1之间变化,我们在访问这个值指向的指针表位置(就是ring自身)时会对这个值做掩码运算。32位无符号表明对这个数的操作(例如加和减)将自动对超出32位数字范围的数取2^32取模。
下面两个例子解释了无符号索引数在ring中如何使用的。
注意:为了简单化,我们将32位的操作用16位的操作示例代替。那4个索引值就用16位无符号整数来定义,与32位无符号整数实际上差不多。
ring包含11000个对象
ring包含12536个对象,
注意:为了简单理解,在上面的例子中我们都是用的65536取模操作。但是在实际的执行时,这是多余而低效的,因为cpu会自动在溢出时做这个。
代码总会保持生产者和消费者索引之间的位置差距在0和sizeof(ring)-1之间。感谢这个特性,我们可以在两个索引值之间做以32位模为基础的减法:这就是为啥这个索引值溢出不是问题。
在任何时间,ring中使用空间和空闲空间在0和sizeof(ring)-1之间,即使第一个减法已经溢出了。
uint32_t entries = (prod_tail - cons_head);
uint32_t free_entries = (mask + cons_tail -prod_head);
本来懂的,翻译着差点把自己搞迷糊了。很简单的东西,就是利用了无符号数的减法原理,prod_tail-cons_head就是已经使用的空间,即使出现prod_tail增长超出2^32的范围,由于无符号数的特性(涉及到补码和反码吧,这个底层的概念忘记是哪个了…..),它会变成超出数-2^32值,此时prod_tail比cons_head小,但是无符号数的相减还是会得到实际的差值。所以就如上所说,即使第一个公式溢出了,始终能得到使用的空间值。再用总空间mask-(prod_tail - cons_head)就得到空闲空间值,去掉括号就是第二个公式,看内核的kfifo就一目了然了。
4.6 参考文件
l bufring.h in FreeBSD (version 8) http://svnweb.freebsd.org/base/release/8.0.0/sys/sys/buf_ring.h?revision=199625&view=markup
l bufring.c in FreeBSD (version 8) http://svnweb.freebsd.org/base/release/8.0.0/sys/kern/subr_bufring.c?revision=199625&view=markup
l Linux Lockless Ring Buffer Design http://lwn.net/Articles/340400/