JCore 是一个专注于 C 语言开发的核心库,旨在为开发者提供高效、实用的数据结构、工具和算法。通过精心设计和优化,JCore 帮助开发者在复杂的系统环境中实现高性能的代码,同时保持代码的简洁和可维护性。
JCore 的设计理念是“简单、高效、可靠”,他致力于为开发者提供以下核心价值:
- 高效的数据结构:JCore 提供了多种高效的数据结构,如动态数组、动态字符串、链表、哈希表、位图、红黑树和优先级队列,帮助开发者在不同场景下快速实现复杂的数据管理需求。
- 模板代码生成器:对动态数组、内存池、双向链表、哈希表、红黑树提供模板脚本生成代码。
- 生成的双向链表、哈希表、红黑树使用内存池分配节点和数据,节点记录序号而不是指针,大大减小了内存占用,比如常规红黑树节点占用24个字节,而使用序号的只占用8或4个字节。
- 实用的工具模块:JCore 包含了内存调试工具、日志工具、线程池等实用模块,帮助开发者快速定位和解决问题,提升开发效率。
- 跨平台支持:JCore 封装了不同操作系统的核心接口,支持跨平台开发,确保代码在不同系统环境下的兼容性和一致性(目前只封装了Posix接口)。
- 高性能与低开销:JCore 的每个模块都经过精心优化,确保在提供丰富功能的同时,保持低内存占用和高执行效率。
- 易于集成与扩展:JCore 的模块化设计使得开发者可以轻松地将所需功能集成到现有项目中,同时提供了灵活的扩展接口,满足不同项目的定制化需求。
JCore 提供了丰富的功能模块,涵盖了数据结构、实用工具、接口封装等多个方面。以下是对各个模块的简要说明,详细的接口说明请参考对应的头文件。
JCore 提供了多种实用工具模块,帮助开发者快速定位和解决问题,提升开发效率。以下是对各个工具的简要说明,详细的设计和实现原理将在后续章节中展开。
-
jhook内存调试工具:用于调试内存问题,支持内存越界和内存泄漏的检测
- 文件:
jhook/jhook.c,jhook/jlisthook.c,jhook/jtree.c,jhook/jtree.h,jhook/jtreehook.c - 特点:
- 采用代码注入的方式,无需重新编译原始程序即可调试内存问题
- 支持动态链接和库函数替换两种方式
- 提供两种实现:基于单向循环链表的
libjlisthook.so和基于红黑树的libjtreehook.so
- 文件:
-
jlog日志工具:高效的日志库,支持多线程、多输出方式
- 文件:
common/jlog_core.h,common/jlog.h,common/jlog.c - 特点:
- 支持输出到终端、文件和网络
- 使用独立线程进行日志写入,避免阻塞主线程
- 提供日志轮转、日志等级控制、性能监控等功能
- 文件:
-
jpthread线程池工具:高效的线程池和定时器实现
- 文件:
common/jpthread.h,common/jpthread.c - 特点:
- 支持立即执行、延迟执行和周期执行的任务
- 动态线程管理,空闲线程自动回收
- 资源复用,减少系统调用开销
- 文件:
C++ STL的容器内部实现原理与JCore中的数据结构的核心原理基本一致。通过JCore提供的这些数据结构,开发者可以轻松实现与C++ STL相似的功能,同时保持代码的高效和简洁。
-
数组模块:提供动态数组的两种实现
- 文件:
common/jvector.h,common/jvector.c/template/jvector.sh - C++ STL对应:
std::vector、std::array - 特点:
- jvector.h实现:不记录类型,只记录类型大小,操作void指针
- jvector.sh实现:记录类型,由用户通过运行脚本生成自己的动态数组实现
- 文件:
-
字符串模块:提供动态字符串
- 文件:
common/jstring.h,common/jstring.c - C++ STL对应:
std::string - 特点:
- 类似动态数组jvector.sh的设计,但保证字符串尾后字节为空
- 文件:
-
链表模块:提供双向链表和单向循环链表,提供双向链表的两种实现
- 文件:
common/jlist.h/template/jlist.sh - C++ STL对应:
std::list、std::forward_list、std::stack、std::queue、std::deque、std::stack - 特点:
- jlist.h实现:双向链表类似Linux内核的双向链表设计,节点需要两个指针;单向循环链表更节省内存,仅需一个指针
- jlist.sh实现:记录类型,由用户通过运行脚本生成自己的双向链表实现,使用内存池,节点记录的是整数序号而不是指针
- Makefile中使用jlist.sh自动生成了jplist.c和jplist.h,实现了节点数据是void *的双向链表
- 文件:
-
位图模块:提供位图
- 文件:
common/jbitmap.h - C++ STL对应:
std::bitset - 特点:
- 类似linux位图的接口,但未实现原子操作
- 文件:
-
哈希表模块:提供哈希表的两种实现
- 文件:
common/jhashmap.h,common/jhashmap.c/template/jhashmap.sh - C++ STL对应:
std::unordered_map、std::unordered_set - 特点:
- jhashmap.h实现:使用方式统一为类似链表的操作,降低使用难度;支持高效的插入、删除、查找和遍历操作
- jhashmap.sh实现:记录类型,由用户通过运行脚本生成自己的哈希表实现,使用内存池,节点记录的是整数序号而不是指针
- Makefile中使用jhashmap.sh自动生成了jphashmap.c和jphashmap.h,实现了节点数据是void *哈希表
- 文件:
-
红黑树模块:提供红黑树的两种实现
- 文件:
common/jrbtree.h,common/jrbtree.c/template/jrbtree.sh - C++ STL对应:
std::map、std::set、std::multimap、std::multiset - 特点:
- jrbtree.h实现:使用方式统一为类似链表的操作,降低使用难度,支持高效的插入、删除和查找操作
- jrbtree.sh实现:记录类型,由用户通过运行脚本生成自己的红黑树实现,使用内存池,节点记录的是整数序号而不是指针
- 整个节点是uint64_t或uint32_t大小,在64位系统上节点只占8/4个字节,而传统的节点需要占用24个字节(3个指针)
- Makefile中使用jrbtree.sh自动生成了jprbtree.c和jprbtree.h,实现了节点数据是void *的红黑树
- 文件:
-
优先级队列模块:提供基于最小堆的优先级队列
- 文件:
common/jpqueue.h - C++ STL对应:
std::priority_queue - 特点:
- 支持删除任意位置的元素,复杂度和队首删除的复杂度相同
- 高效的插入和删除操作
- 文件:
JCore 封装了不同操作系统的核心接口,屏蔽了系统调用的差异,支持跨平台开发。目前仅封装了部分 POSIX 接口。
-
优化模块:提供编译器的常用属性和内建函数的实现(例如位计数操作)
- 文件:
common/joptimize.h
- 文件:
-
线程模块:封装不同系统的线程和线程保护接口
- 文件:
$OSDIR/jthread.h
- 文件:
-
时间模块:封装不同系统的时间和日期接口
- 文件:
$OSDIR/jtime.h
- 文件:
-
定时器模块:封装不同系统的定时器接口
- 文件:
$OSDIR/jtimer.h
- 文件:
-
文件模块:封装不同系统的文件和目录接口
- 文件:
$OSDIR/jfs.h,$OSDIR/jfs.c,common/jfs_c.c
- 文件:
-
资源模块:获取系统资源占用情况,包括进程的 CPU 和内存占用、系统的 CPU 和内存占用、网络收发情况等
- 文件:
$OSDIR/jperf.h,$OSDIR/jperf.c
- 文件:
注:$OSDIR 表示不同系统的封装接口目录,例如:posix、windows(目前仅支持 Linux,Windows 可以编译通过但没有测试)。
-
网络模块:封装不同系统的 TCP 和 UDP 接口,提供易于使用的网络库
- 文件:
common/jsocket.h,common/jsocket.c
- 文件:
-
文件缓冲模块:提供文件读写缓冲功能,避免大量小块文件读写引起的性能下降
- 文件:
common/jfcache.h,common/jfcache.c
- 文件:
-
INI 配置模块:提供 INI 配置文件的解析和存储功能,支持转义字符的解析
- 文件:
common/jini.h,common/jini.c
- 文件:
-
内存块模块:提供内存块功能,加快小内存分配速度,减少内存碎片
- 文件:
common/jbheap.h,common/jbheap.c
- 文件:
-
内存池模块:提供内存池功能,支持固定大小内存的申请和归还,减少内存碎片,两种实现
- 文件:
common/jpheap.h,common/jpheap.c,template/jpheap.sh - 特点:
- jpheap.h实现:不记录类型,只记录类型大小,操作void指针,不支持序号和扩容接口
- jpheap.sh实现:记录类型,由用户通过运行脚本生成自己的内存池实现,提供序号接口和扩容接口
- 文件:
-
内存调试模块:提供内存调试功能,支持内存泄漏、内存越界和内存重复释放的检测
- 文件:
common/jheap.h,common/jheap.c
- 文件:
-
循环缓冲模块:提供线程安全的循环缓冲
- 文件:
common/jringbuf.h,commjringbuf.c(只有数据区,可设置数据单元大小) - 文件:
common/jringdata.h,commjringdata.c(一份索引区,一份数据区) - 特点:
- 支持单生产者、多生产者、单消费者、多消费者模型
- 多消费者支持共享模式(全局)和独占模式
- 读写支持完整读写模式(完全写入请求的数据或完整读取所需长度的数据)和部分读写模式(部分写或部分读)
- 读写条件不满足时,读写支持阻塞(超时阻塞、和一直阻塞)、重试、丢弃旧数据(写空间不足时)、直接失败的选项
- 文件:
JCore 提供了各个模块的简单测试程序,方便开发者验证功能。
- 测试动态数组模块:
test/jvector_test.c - 测试动态字符串模块:
test/jstring_test.c - 测试哈希表和内存池模块:
test/jhashmap_test.c - 测试位图模块:
test/jbitmap_test.c - 测试红黑树模块:
test/jrbtree_test.c - 测试优先级队列模块:
test/jpqueue_test.c - 测试内存调试模块:
test/jheap_debug_test.c - 测试INI配置模块:
test/jini_test.c - 测试线程池模块:
test/jpthread_test.c - 测试日志模块:
test/jlog_client_test.c,test/jlog_server_test.c - 测试网络模块:
test/jsock_client_test.c,test/jsock_udp_test.c - 测试循环缓冲模块:
test/jringbuf_test.c,test/jringdata_test.c - 测试代码模板生成:
template/test/jp*_test.c
- 内存调试工具jhook仅用约800行核心代码就实现了valgrind约26000行代码实现的memcheck的核心功能,无需重新编译程序即可完成内存越界分析和内存泄漏分析。
- 代码注入的实现原理是使用
LD_PRELOAD在程序运行前优先加载的指定的动态库,这个库中重新定义堆分配释放函数,加入调试和统计钩子。 - 内存越界分析的核心思想是分配内存时多分配一些内存,在尾部内存处memset成校验字节,如果检测到尾部校验字节不对,就判定为内存有越界。
- 内存泄漏分析的核心思想是以“分配大小和函数调用栈”为锚点,分配的内存指针挂载在锚点上,从而统计指定锚点分配了多少次,释放了多少次,如果“分配减释放”的值一直在增大,则判定该位置存在内存泄漏。
- 代码注入的实现原理是使用
- 内存调试工具jhook根据记录统计数据的数据结构,有两个实现,两者的功能和接口是完全一致的,选择其中一个实现即可。
libjlisthook.so: 由jlisthook.cjhook.cjlist.h编译生成,使用单向循环列表记录统计数据,节点占用内存空间较小,但有大量分配数据时记录统计信息较慢。libjtreehook.so: 由jtreehook.cjhook.cjtree.hjtree.c编译生成,使用红黑树记录统计数据,节点占用内存空间较大,但有大量分配数据时记录统计信息较快,统计信息打印时按分配单元大小从小到大排序。
注:用户也可以使用开源工具 valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all <bin> 命令查找内存泄漏。
- 使用jhook检测的内存泄漏
$ make O=obj check_cycle=10 check_unwind=y check_depth=10 # 编译选项
$ LD_PRELOAD=/home/lengjing/data/jcore/obj/libjtreehook.so ./yyjson twitter.json 1
[mut] read=3 parse=3 format=3 unformat=2
--------------------------------------------------------
size alloc free diff addr
1024 1 0 1 0x7f936fd1dd04:(_IO_file_doallocate+0x94) | 0x7f936fd2ded0:(_IO_doallocbuf+0x50) | 0x7f936fd2cf30:(_IO_file_overflow+0x1b0) | 0x7f936fd2b6b5:(_IO_file_xsputn+0xe5) | 0x7f936fd12972:(psiginfo+0x13512) | 0x7f936fdca27b:(__printf_chk+0xab) | 0x55895411646f:(main+0x20f) | 0x7f936fcc0083:(__libc_start_main+0xf3) | 0x55895411663e:(_start+0x2e)
631518 1 0 1 0x55895411d290:(yyjson_read_opts+0xb0) | 0x558954116358:(main+0xf8) | 0x7f936fcc0083:(__libc_start_main+0xf3) | 0x55895411663e:(_start+0x2e)
631632 1 0 1 0x55895411d64e:(yyjson_read_opts+0x46e) | 0x558954116358:(main+0xf8) | 0x7f936fcc0083:(__libc_start_main+0xf3) | 0x55895411663e:(_start+0x2e)
----------- total=1264174 peak=3894705 ------------ 使用valgrind检测的内存泄漏
$ valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all ./yyjson twitter.json 1
==240599== Memcheck, a memory error detector
==240599== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==240599== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==240599== Command: ./yyjson twitter.json 1
==240599==
[mut] read=14 parse=27 format=19 unformat=14
==240599==
==240599== HEAP SUMMARY:
==240599== in use at exit: 1,263,150 bytes in 2 blocks
==240599== total heap usage: 57 allocs, 55 frees, 7,069,897 bytes allocated
==240599==
==240599== 631,518 bytes in 1 blocks are indirectly lost in loss record 1 of 2
==240599== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==240599== by 0x11028F: yyjson_read_opts (in /home/lengjing/data/test/yyjson)
==240599== by 0x109357: main (in /home/lengjing/data/test/yyjson)
==240599==
==240599== 1,263,150 (631,632 direct, 631,518 indirect) bytes in 1 blocks are definitely lost in loss record 2 of 2
==240599== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==240599== by 0x11064D: yyjson_read_opts (in /home/lengjing/data/test/yyjson)
==240599== by 0x109357: main (in /home/lengjing/data/test/yyjson)
==240599==
==240599== LEAK SUMMARY:
==240599== definitely lost: 631,632 bytes in 1 blocks
==240599== indirectly lost: 631,518 bytes in 1 blocks
==240599== possibly lost: 0 bytes in 0 blocks
==240599== still reachable: 0 bytes in 0 blocks
==240599== suppressed: 0 bytes in 0 blocks
==240599==
==240599== For lists of detected and suppressed errors, rerun with: -s
==240599== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
make O=obj check_cycle=10 check_tofile=n check_unwind=n check_depth=1- IMake 变量
- O=xxx: 指定编译输出目录
- DESTDIR=xxx: 指定安装目录
- DEPDIR=xxx: 指定依赖包的根目录
- 私有变量
- check_cycle=N: check_cycle为几秒检测一次,默认为10s
- make传入此变量会创建一个线程每N秒检测一次内存情况,否则线程空转不检测
- check_tofile=<y|n>: 输出到文件还是终端,默认是n,输出到文件
- 设置为
y时输出到文件heap_memory_info.<pid>.log而不是终端
- 设置为
- check_unwind=<y|n>: 是否使用libunwind记录栈信息,默认是n,没有依赖其它包,不会记录函数名和偏移量
- 设置为
y时需要依赖 libunwind
- 设置为
- check_depth=N: 记录的调用栈深度,默认值为1
- 如果check_unwind未设置为y,check_depth为1时使用__builtin_return_address记录,大于1时使用backtrace记录
- check_cycle=N: check_cycle为几秒检测一次,默认为10s
jhook_init
/**
* @brief 初始化内存调试管理结构
* @param tail_num [IN] 尾部校验字节的个数,最大256,用于检测是否越界,可设置为0表示不检测越界
* @return 成功返回0; 失败返回-1
* @note 无
*/
int jhook_init(int tail_num);jhook_uninit
/**
* @brief 反初始化内存调试管理结构
* @param 无参数
* @return 无返回值
* @note 无
*/
void jhook_uninit(void);jhook_start
/**
* @brief 开始内存调试
* @param 无参数
* @return 无返回值
* @note 开始内存调试才会记录内存分配信息
*/
void jhook_start(void);jhook_stop
/**
* @brief 停止内存调试
* @param 无参数
* @return 无返回值
* @note 停止内存调试会清空所有内存分配信息
*/
void jhook_stop(void);jhook_check_bound
/**
* @brief 检查是否有内存越界
* @param 无参数
* @return 无返回值
* @note 此功能jhook_init时需要传入非0的tail_num
*/
void jhook_check_bound(void);jhook_check_leak
/**
* @brief 检查内存使用情况,查看是否有内存泄漏
* @param choice [IN] 检测打印节点信息的方式:0 未释放且有变化; 1 有变化; 2 未释放; 3 所有;
* >3 未释放数大于choice; < 0 未释放数大于-choice且有变化
* @return 无返回值
* @note "未释放"指malloc和free不相等,"有变化"指这段时间有过alloc和free
*/
void jhook_check_leak(int choice);jhook_set_flag
/**
* @brief 修改高频检测选项
* @param dup_flag [IN] 是否高频检测当前的ptr和记录的ptr重复,即使用了debug的接口分配了内存,但没有使用jheap_free_debug释放
* @param bound_flag [IN] 是否高频检测记录的内存有越界
* @return 无返回值
* @note 高频检测指每次分配内存时都检测,显著降低性能
*/
void jhook_set_flag(int dup_flag, int bound_flag);jhook_set_check
/**
* @brief 修改线程检测内存方式
* @param check_flag [IN] 是否在线程中检测内存情况
* @param check_count [IN] 线程中多长时间检测一次内存情况,为0时不修改
* @return 无返回值
* @note 线程中检测每 10ms*check_count 检测一次
*/
void jhook_set_check(int check_flag, int check_count);jhook_set_method
/**
* @brief 修改记录栈调用的方法
* @param method [IN] 栈调用记录方式,0: 使用__builtin_return_address记录; 1: 使用backtrace记录
* @return 无返回值
* @note __builtin_return_address: 只记录1层栈调用,速度较快,但对应C++来说,1层调用都是new,不好分析
* backtrace: 只记录2层调用,速度较较慢,由于backtrace内部调用malloc,需要临时关闭记录,可能造成记录遗漏
*/
void jhook_set_method(int method);jhook_set_limit
/**
* @brief 修改要跟踪的内存的范围
* @param min_limit [IN] 纳入统计的堆内存分配大小下限
* @param max_limit [IN] 纳入统计的堆内存分配大小上限
* @return 无返回值
* @note 修改范围只跟踪指定范围大小的内存,提升性能
*/
void jhook_set_limit(size_t min_limit, size_t max_limit);- jhook检查可以不用gdb,直接监测内存的变化,如下例子编译运行
$ make O=obj check_cycle=10
$ LD_PRELOAD=obj/libjtreehook.so curl www.bing.com
--------------------------------------------------------
size alloc free diff addr
12 15 1 14 0x7f88e131438f
16 1 0 1 0x7f88e13b8d1f
16 3 0 3 0x7f88e13b915a
20 1 0 1 0x7f88e13b8e45
22 2 0 2 0x7f88e13b8e45
23 3 0 3 0x7f88e13b8e45
24 3 0 3 0x7f88e138ef94
24 3 0 3 0x7f88e13b890e
24 1 0 1 0x7f88e13b8e45
25 3 0 3 0x7f88e13b8e45
26 1 0 1 0x7f88e13b8e45
32 1 0 1 0x7f88e1313ca0
38 1 0 1 0x7f88e15685b8
38 1 0 1 0x7f88e157a5df
40 1 0 1 0x7f88e15685b8
40 1 0 1 0x7f88e157a5df
48 1 0 1 0x7f88e15685b8
48 1 0 1 0x7f88e157a5df
51 4 0 4 0x7f88e13b83f7
52 2 0 2 0x7f88e13b83f7
54 10 0 10 0x7f88e13b83f7
56 2 0 2 0x7f88e13b83f7
56 1 0 1 0x7f88e156ac4a
62 1 0 1 0x7f88e13b83f7
72 2 0 2 0x7f88e156ac4a
80 2 0 2 0x7f88e12a6a4f
88 2 0 2 0x7f88e12a6a4f
88 1 0 1 0x7f88e13b6d7f
104 4 0 4 0x7f88e12a6a4f
112 2 0 2 0x7f88e12a6a4f
116 1 0 1 0x7f88e1313f55
120 2 0 2 0x7f88e12a6a4f
120 2 0 2 0x7f88e12a737a
168 2 0 2 0x7f88e12a6a4f
192 2 0 2 0x7f88e12a6a4f
192 2 0 2 0x7f88e156e31d
216 2 0 2 0x7f88e12a6a4f
240 1 0 1 0x7f88e156e31d
281 1 0 1 0x7f88e12a55c8
336 1 0 1 0x7f88e156f9db
432 2 0 2 0x7f88e12a6a4f
776 1 0 1 0x7f88e12a6a4f
784 1 0 1 0x7f88e12a6a4f
1200 1 0 1 0x7f88e1568284
1202 1 0 1 0x7f88e1568284
1210 1 0 1 0x7f88e1568284
1336 2 0 2 0x7f88e12a6a4f
1560 1 0 1 0x7f88e13b2775
----------- total=17485 peak=336755 ------------ 使用libunwind记录栈信息
$ make O=obj check_cycle=10 check_unwind=y check_depth=2
$ LD_PRELOAD=obj/libjtreehook.so curl www.bing.com
--------------------------------------------------------
size alloc free diff addr
12 12 0 12 0x7f4ba141038f:(__strdup+0x1f) | 0x7f4ba13a1948:(setlocale+0x268)
12 2 0 2 0x7f4ba141038f:(__strdup+0x1f) | 0x7f4ba13a3395:(setlocale+0x1cb5)
16 1 0 1 0x7f4ba14b4d1f:(__nss_database_lookup2+0x25f) | 0x7f4ba147627c:(sched_setaffinity+0x2a3c)
16 3 0 3 0x7f4ba14b515a:(__nss_lookup_function+0xca) | 0x7f4ba14754bf:(sched_setaffinity+0x1c7f)
20 1 0 1 0x7f4ba14b4e45:(__nss_database_lookup2+0x385) | 0x7f4ba147627c:(sched_setaffinity+0x2a3c)
22 2 0 2 0x7f4ba14b4e45:(__nss_database_lookup2+0x385) | 0x7f4ba147627c:(sched_setaffinity+0x2a3c)
23 3 0 3 0x7f4ba14b4e45:(__nss_database_lookup2+0x385) | 0x7f4ba147627c:(sched_setaffinity+0x2a3c)
24 3 0 3 0x7f4ba148af94:(tsearch+0x1c4) | 0x7f4ba14b50f1:(__nss_lookup_function+0x61)
24 3 0 3 0x7f4ba14b490e:(__gai_sigqueue+0x68e) | 0x7f4ba14b5179:(__nss_lookup_function+0xe9)
24 1 0 1 0x7f4ba14b4e45:(__nss_database_lookup2+0x385) | 0x7f4ba147627c:(sched_setaffinity+0x2a3c)
25 3 0 3 0x7f4ba14b4e45:(__nss_database_lookup2+0x385) | 0x7f4ba147627c:(sched_setaffinity+0x2a3c)
26 1 0 1 0x7f4ba14b4e45:(__nss_database_lookup2+0x385) | 0x7f4ba147627c:(sched_setaffinity+0x2a3c)
32 1 0 1 0x7f4ba140fca0:(__libc_dynarray_emplace_enlarge+0xe0) | 0x7f4ba14b3faf:(__resolv_context_put+0x138f)
38 1 0 1 0x7f4ba16655b8:(_dl_rtld_di_serinfo+0x2528) | 0x7f4ba165ee97:(_dl_catch_error+0x0)
38 1 0 1 0x7f4ba16775df:(_dl_catch_error+0x189f) | 0x7f4ba1671a92:(_dl_exception_free+0x832)
40 1 0 1 0x7f4ba16655b8:(_dl_rtld_di_serinfo+0x2528) | 0x7f4ba165ee97:(_dl_catch_error+0x0)
40 1 0 1 0x7f4ba16775df:(_dl_catch_error+0x189f) | 0x7f4ba1671a92:(_dl_exception_free+0x832)
48 1 0 1 0x7f4ba16655b8:(_dl_rtld_di_serinfo+0x2528) | 0x7f4ba165ee97:(_dl_catch_error+0x0)
48 1 0 1 0x7f4ba16775df:(_dl_catch_error+0x189f) | 0x7f4ba1671a92:(_dl_exception_free+0x832)
51 4 0 4 0x7f4ba14b43f7:(__gai_sigqueue+0x177) | 0x7f4ba14b4e6c:(__nss_database_lookup2+0x3ac)
52 2 0 2 0x7f4ba14b43f7:(__gai_sigqueue+0x177) | 0x7f4ba14b4e6c:(__nss_database_lookup2+0x3ac)
54 10 0 10 0x7f4ba14b43f7:(__gai_sigqueue+0x177) | 0x7f4ba14b4e6c:(__nss_database_lookup2+0x3ac)
56 2 0 2 0x7f4ba14b43f7:(__gai_sigqueue+0x177) | 0x7f4ba14b4e6c:(__nss_database_lookup2+0x3ac)
56 1 0 1 0x7f4ba1667c4a:(_dl_rtld_di_serinfo+0x4bba) | 0x7f4ba166ddb0:(_dl_find_dso_for_object+0x920)
62 1 0 1 0x7f4ba14b43f7:(__gai_sigqueue+0x177) | 0x7f4ba14b4e6c:(__nss_database_lookup2+0x3ac)
72 2 0 2 0x7f4ba1667c4a:(_dl_rtld_di_serinfo+0x4bba) | 0x7f4ba166ddb0:(_dl_find_dso_for_object+0x920)
80 2 0 2 0x7f4ba13a2a4f:(setlocale+0x136f) | 0x7f4ba13a33f4:(setlocale+0x1d14)
88 2 0 2 0x7f4ba13a2a4f:(setlocale+0x136f) | 0x7f4ba13a33f4:(setlocale+0x1d14)
88 1 0 1 0x7f4ba14b2d7f:(__resolv_context_put+0x15f) | 0x7f4ba14b3428:(__resolv_context_put+0x808)
104 4 0 4 0x7f4ba13a2a4f:(setlocale+0x136f) | 0x7f4ba13a33f4:(setlocale+0x1d14)
112 2 0 2 0x7f4ba13a2a4f:(setlocale+0x136f) | 0x7f4ba13a33f4:(setlocale+0x1d14)
116 1 0 1 0x7f4ba140ff55:(__libc_alloc_buffer_allocate+0x15) | 0x7f4ba14b3849:(__resolv_context_put+0xc29)
120 2 0 2 0x7f4ba13a2a4f:(setlocale+0x136f) | 0x7f4ba13a33f4:(setlocale+0x1d14)
120 2 0 2 0x7f4ba13a337a:(setlocale+0x1c9a) | 0x7f4ba13a21ce:(setlocale+0xaee)
168 2 0 2 0x7f4ba13a2a4f:(setlocale+0x136f) | 0x7f4ba13a33f4:(setlocale+0x1d14)
192 2 0 2 0x7f4ba13a2a4f:(setlocale+0x136f) | 0x7f4ba13a33f4:(setlocale+0x1d14)
192 2 0 2 0x7f4ba166b31d:(_dl_debug_state+0x113d) | 0x7f4ba166e0fd:(_dl_find_dso_for_object+0xc6d)
216 2 0 2 0x7f4ba13a2a4f:(setlocale+0x136f) | 0x7f4ba13a33f4:(setlocale+0x1d14)
240 1 0 1 0x7f4ba166b31d:(_dl_debug_state+0x113d) | 0x7f4ba166e0fd:(_dl_find_dso_for_object+0xc6d)
281 1 0 1 0x7f4ba13a15c8:(__gconv_destroy_spec+0x178) | 0x7f4ba13a1e58:(setlocale+0x778)
352 1 0 1 0x7f4ba166c9db:(_dl_allocate_tls+0x2b) | 0x7f4ba156c323:(pthread_create+0xa53)
432 2 0 2 0x7f4ba13a2a4f:(setlocale+0x136f) | 0x7f4ba13a33f4:(setlocale+0x1d14)
776 1 0 1 0x7f4ba13a2a4f:(setlocale+0x136f) | 0x7f4ba13a33f4:(setlocale+0x1d14)
784 1 0 1 0x7f4ba13a2a4f:(setlocale+0x136f) | 0x7f4ba13a33f4:(setlocale+0x1d14)
1200 1 0 1 0x7f4ba1665284:(_dl_rtld_di_serinfo+0x21f4) | 0x7f4ba165ee97:(_dl_catch_error+0x0)
1202 1 0 1 0x7f4ba1665284:(_dl_rtld_di_serinfo+0x21f4) | 0x7f4ba165ee97:(_dl_catch_error+0x0)
1210 1 0 1 0x7f4ba1665284:(_dl_rtld_di_serinfo+0x21f4) | 0x7f4ba165ee97:(_dl_catch_error+0x0)
1336 2 0 2 0x7f4ba13a2a4f:(setlocale+0x136f) | 0x7f4ba13a33f4:(setlocale+0x1d14)
1560 1 0 1 0x7f4ba14ae775:(__idna_from_dns_encoding+0x925) | 0x7f4ba1477a69:(getaddrinfo+0xa19)
----------- total=17501 peak=336771 -----------
-
使用
backtrace记录2层调用- 启动方式也可以如下(推荐)
$ gdb ./jini_test # 正常启动gdb (gdb) set environment LD_PRELOAD=./libjtreehook.so # 设置环境变量 (gdb) set args <xxx> # 设置程序运行的参数
$ make O=obj check_depth=2
$ gdb --args env LD_PRELOAD=./libjtreehook.so ./jini_test
...
(gdb) b main # 在main函数断点,准备初始化统计结构信息
Function "main" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (main) pending.
(gdb) r
Starting program: /usr/bin/env LD_PRELOAD=./libjtreehook.so ./jini_test
process 31255 is executing new program: /home/lengjing/data/jcore/obj/jini_test
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, 0x0000555555555340 in main ()
(gdb) call jhook_init(0) # 初始化统计结构信息,0表示无需校验字节,不进行越界分析,其它值表示尾部校验字节的个数
$1 = 0
(gdb) call jhook_start() # 开始统计
(gdb) call jhook_set_method(1) # 修改栈调用记录方法,改为使用backtrace记录两层调用
(gdb) b jhook_delptr # 在某个观察点设置断点,运行统计函数
Breakpoint 2 at 0x7ffff7fc35e8: file jtreehook.c, line 426.
(gdb) c
Continuing.
...
Breakpoint 2, jhook_delptr (ptr=0x816938a99eac5200) at jtreehook.c:426
426 {
(gdb) call jhook_check_leak(0) # 统计信息,0为选项,内存泄漏主要是用选项0,看diff是否一直在增加
--------------------------------------------------------
size alloc free diff addr
2 1 0 1 0x555555555e82|0x55555555542e
2 1 0 1 0x555555555e82|0x55555555544b
3 1 0 1 0x555555555e82|0x555555555411
5 1 0 1 0x555555555cce|0x555555555411
5 1 0 1 0x555555555cce|0x5555555570cb
5 1 0 1 0x555555555e82|0x5555555570cb
6 1 0 1 0x555555555d33|0x555555555411
6 1 0 1 0x555555555d33|0x55555555542e
6 1 0 1 0x555555555d33|0x55555555544b
6 1 0 1 0x555555555d33|0x555555555468
6 1 0 1 0x555555555d33|0x555555555485
8 1 0 1 0x555555555d33|0x5555555553d7
8 1 0 1 0x555555555d33|0x5555555570cb
9 1 0 1 0x55555555667b|0x555555555351
10 1 0 1 0x555555555e82|0x555555555468
10 1 0 1 0x555555555e82|0x555555555485
13 1 0 1 0x555555555e82|0x5555555553d7
14 1 0 1 0x555555556045|0x555555555494
32 1 0 1 0x555555555cba|0x555555555411
32 1 0 1 0x555555555cba|0x5555555570cb
32 1 0 1 0x555555555d1f|0x5555555553d7
32 1 0 1 0x555555555d1f|0x555555555411
32 1 0 1 0x555555555d1f|0x55555555542e
32 1 0 1 0x555555555d1f|0x55555555544b
32 1 0 1 0x555555555d1f|0x555555555468
32 1 0 1 0x555555555d1f|0x555555555485
32 1 0 1 0x555555555d1f|0x5555555570cb
32 1 0 1 0x555555556616|0x555555555351
48 1 0 1 0x555555557122|0x555555556085
1024 1 0 1 0x7ffff7e38d04|0x7ffff7e48ed0
8192 1 0 1 0x555555557136|0x555555556085
----------- total=9708 peak=9708 -----------
(gdb) x /x 0x555555557136 # 查看第1层栈调用的位置
0x555555557136 <jfcache_open+70>: 0x24448949
(gdb) x /x 0x555555556085 # 查看第2层栈调用的位置
0x555555556085 <jini_flush+149>: 0x48c58948
(gdb) c
Continuing.
Breakpoint 2, jhook_delptr (ptr=0x55555555d4a8) at jtreehook.c:426
426 {
(gdb) call jhook_check_leak(1) # 选项1表示距离上次统计,有过alloc/free的节点信息
--------------------------------------------------------
size alloc free diff addr
8192 1 1 0 0x555555557136|0x555555556085
----------- total=1516 peak=9708 ------------ 使用
__builtin_return_address记录1层调用
# 上面同样的方法,如果不运行call jhook_set_method(1),统计结果如下:
(gdb) call jhook_check_leak(0)
--------------------------------------------------------
size alloc free diff addr
2 2 0 2 0x555555555e82
3 1 0 1 0x555555555e82
5 2 0 2 0x555555555cce
5 1 0 1 0x555555555e82
6 5 0 5 0x555555555d33
8 2 0 2 0x555555555d33
9 1 0 1 0x55555555667b
10 2 0 2 0x555555555e82
13 1 0 1 0x555555555e82
14 1 0 1 0x555555556045
32 2 0 2 0x555555555cba
32 7 0 7 0x555555555d1f
32 1 0 1 0x555555556616
48 1 0 1 0x555555557122
1024 1 0 1 0x7ffff7e38d04
8192 1 0 1 0x555555557136
----------- total=9708 peak=9708 -----------
(gdb) x /x 0x555555557136
0x555555557136 <jfcache_open+70>: 0x24448949
(gdb)静态链接程序使用库函数替换法,链接成静态可执行文件时需要加上链接选项 -Wl,--wrap=free -Wl,--wrap=malloc -Wl,--wrap=calloc -Wl,--wrap=realloc -Wl,--wrap=strdup -Wl,--wrap=strndup -ljtreewrap -pthread,此时会使用自定义的函数 __wrap_xxxx 代替库函数 xxxx,如果此时要使用真正的库函数,可以使用 __real_xxxx 。
例如静态编译 LJSON 内存调试,可以如下编译: gcc -o ljson json.c jnum.c json_test.c -O2 -ffunction-sections -fdata-sections -W -Wall -L/home/lengjing/data/jcore/obj -Wl,--wrap=free -Wl,--wrap=malloc -Wl,--wrap=calloc -Wl,--wrap=realloc -Wl,--wrap=strdup -Wl,--wrap=strndup -ljtreewrap -pthread -lm 。
阶段一:判定哪个进程的哪个分配大小的内存有泄漏
- 编译资源占用小的调试程序
$ make O=obj check_cycle=10 check_tofile=y- 导出环境变量,以代码注入方式运行程序
$ export LD_PRELOAD=<jhook_path>/libjtreehook.so
$ ./<cmd>- 运行一段时间后,查看哪个记录文件表现内存一直在增长
$ grep -n 'total=' `ls -Sr <log_path>/heap_memory_info.*.log`- 辅助分析手段:对文件进行如下处理,看下哪个节点的内存疑似泄漏
- size和addr相同是同一节点,如果diff值一直增加,表明该节点可能存在内存泄漏
$ grep -E '^[0-9]+ ' heap_memory_info.<pid>.log | nl | sort -k2n -k6 -k1n | cut -f 2-- 辅助信息手段:查看进程线程相关信息
$ cat /proc/<pid>/cmdline
$ top -Hp <pid>
$ ps -Tlf <pid>阶段二:得到内存泄漏的节点的堆栈
- 修改调试程序源码只监测特定size的内存分配
--- a/jtreehook.c
+++ b/jtreehook.c
@@ -745,6 +745,7 @@ void before_main(void)
jhook_mgr_t *mgr = &s_jhook_mgr;
if (!mgr->inited) {
jhook_init(0);
+ jhook_set_limit(<size>, <size>);
jhook_start();
}
}- 如果系统不带
libunwind,需要先下载编译libunwind
$ autoreconf --install
$ mkdir build && cd build
$ ../configure --prefix=<libunwind install root>
$ make && make install- 编译记录节点调用栈的函数,调用栈层数可配
$ make O=obj DEPDIR=<libunwind install root> check_cycle=10 check_tofile=y check_unwind=y check_depth=10- 导出环境变量,以代码注入方式运行程序
$ export LD_LIBRARY_PATH=<libunwind.so path>:$LD_LIBRARY_PATH
$ export LD_PRELOAD=<jhook_path>/libjtreehook.so
$ ./<cmd>-
运行一段时间后,查看记录文件的堆栈信息和内存泄漏信息
-
如果是
C++程序,可以使用如下脚本处理文件得到真正的函数
#!/bin/bash
src=$1
if [ -z $src ]; then
echo "Usage: $0 <src>"
exit 1
fi
IFS=$'\n'
for line in $(cat $src | sed -e 's/ 0x/\n0x/g' -e 's/ |//g'); do
if [ $(echo "$line" | grep -c "^0x") -eq 1 ]; then
prefix=$(echo "$line" | cut -d '(' -f 1)
suffix=$(echo "$line" | cut -d '(' -f 2 | cut -d '+' -f 2 | cut -d ')' -f 1)
oldfunc=$(echo "$line" | cut -d '(' -f 2 | cut -d '+' -f 1)
newfunc=$(c++filt $oldfunc 2>/dev/null)
echo "$prefix $newfunc+$suffix"
else
if [ $(echo "$line" | grep -c "^[0-9]") -eq 1 ]; then
echo -e "\033[33m$line\033[0m"
else
echo -e "\033[32m$line\033[0m"
fi
fi
done- 最终发现下面的内存积攒来不及释放
1640 1072685 375670 697015
0x7f4f024de298: operator new(unsigned long)+0x18
0x7f4eea713bb0: std::__shared_ptr<erizo::DataPacket, (__gnu_cxx::_Lock_policy)2>::__shared_ptr<std::allocator<erizo::DataPacket>, erizo::DataPacket&>(std::_Sp_make_shared_tag, std::allocator<erizo::DataPacket> const&, erizo::DataPacket&)+0x30
0x7f4eea709c4d: erizo::MediaStream::deliverAudioData_(std::shared_ptr<erizo::DataPacket>)+0x3d
0x7f4eeb535912: owt_base::AudioFramePacketizer::onAdapterData(char*, int)+0x172
0x7f4eeb00ec55: non-virtual thunk to rtc_adapter::AudioSendAdapterImpl::SendRtp(unsigned char const*, unsigned long, webrtc::PacketOptions const&)+0x15
0x7f4eeb11c3c8: webrtc::DEPRECATED_RtpSenderEgress::SendPacketToNetwork(webrtc::RtpPacketToSend const&, webrtc::PacketOptions const&, webrtc::PacedPacketInfo const&)+0x58
0x7f4eeb11cd42: webrtc::DEPRECATED_RtpSenderEgress::SendPacket(webrtc::RtpPacketToSend*, webrtc::PacedPacketInfo const&)+0x252
0x7f4eeb11d087: webrtc::DEPRECATED_RtpSenderEgress::NonPacedPacketSender::EnqueuePackets(std::__1::vector<std::__1::unique_ptr<webrtc::RtpPacketToSend, std::__1::default_delete<webrtc::RtpPacketToSend> >, std::__1::allocator<std::__1::unique_ptr<webrtc::RtpPacketToSend, std::__1::default_delete<webrtc::RtpPacketToSend> > > >)+0x97
0x7f4eeb061e1e: webrtc::RTPSender::SendToNetwork(std::__1::unique_ptr<webrtc::RtpPacketToSend, std::__1::default_delete<webrtc::RtpPacketToSend> >)+0x14e
0x7f4eeb053569: webrtc::RTPSenderAudio::SendAudio(webrtc::AudioFrameType, signed char, unsigned int, unsigned char const*, unsigned long, long)+0x849
1640 753557 495014 258543
0x7f4f024de298: operator new(unsigned long)+0x18
0x7f4eea713bb0: std::__shared_ptr<erizo::DataPacket, (__gnu_cxx::_Lock_policy)2>::__shared_ptr<std::allocator<erizo::DataPacket>, erizo::DataPacket&>(std::_Sp_make_shared_tag, std::allocator<erizo::DataPacket> const&, erizo::DataPacket&)+0x30
0x7f4eea70a25e: erizo::MediaStream::onTransportData(std::shared_ptr<erizo::DataPacket>, erizo::Transport*)+0x4e
0x7f4eea6ec473: std::_Function_handler<void (std::shared_ptr<erizo::MediaStream> const&), erizo::WebRtcConnection::onTransportData(std::shared_ptr<erizo::DataPacket>, erizo::Transport*)::{lambda(std::shared_ptr<erizo::MediaStream> const&)#3}>::_M_invoke(std::_Any_data const&, std::shared_ptr<erizo::MediaStream> const&)+0x103
0x7f4eea6ea882: erizo::WebRtcConnection::forEachMediaStream(std::function<void (std::shared_ptr<erizo::MediaStream> const&)>)+0x62
0x7f4eea6ecb4a: erizo::WebRtcConnection::onTransportData(std::shared_ptr<erizo::DataPacket>, erizo::Transport*)+0x38a
0x7f4eea6c78f3: erizo::UnencryptedTransport::onIceData(std::shared_ptr<erizo::DataPacket>)+0x233
0x7f4eea6c3a85: std::_Function_handler<void (), erizo::Transport::onPacketReceived(std::shared_ptr<erizo::DataPacket>)::{lambda()#1}>::_M_invoke(std::_Any_data const&)+0xf5
0x7f4eea7243fd: boost::asio::detail::completion_handler<std::function<void ()> >::do_complete(boost::asio::detail::task_io_service*, boost::asio::detail::task_io_service_operation*, boost::system::error_code const&, unsigned long)+0x7d
0x7f4eea72504b: boost::asio::detail::task_io_service::run(boost::system::error_code&)+0x43bjlog是一个异步日志管理模块,支持多线程、多输出、环形缓冲区方式(文件、网络、控制台)的日志记录。它提供了灵活的配置选项,允许开发者根据需求定制日志的输出方式、日志等级、文件大小限制等。主要功能如下:
- 日志输出方式:支持文件、网络和控制台输出。
- 日志等级控制:支持不同日志等级的过滤。
- 日志文件管理:支持日志文件的大小限制和数量限制,自动轮转旧日志文件。
- 日志网络管理:支持心跳包,网络自动重连,支持网络地址等参数热更新。
- 多线程支持:日志写入操作在独立的线程中执行,避免阻塞主线程。
- 性能监控:支持周期性记录 CPU、内存和网络的使用情况。
- 附加信息:支持带时间戳、模块、类型的日志记录,可根据这些参数进行过滤。
graph LR
%% 初始化模块
X0[初始化接口] --> |调用| X[jlog_init]
X --> |创建| C[环形缓冲区]
X --> |启动| D{后台线程}
X --> |配置| E[输出路由]
%% 反初始化模块
Y0[反初始化接口] --> |调用| Y[jlog_uninit]
Y --> |刷新| E
Y --> |释放| C
Y --> |停止| D
%% 配置流
I[配置接口] -->|调用| J[jlog_cfg_set]
J -->|热更新| D
J -->|参数变更| F
J -->|参数变更| G
%% 核心组件
A[应用程序] -->|调用| B[jlog_vprint/jlog_print/jlog_write]
B --> C
subgraph 日志系统核心
C -->|写入日志| D
D -->|读取数据| E[输出路由]
E --> F[文件输出]
E --> G[网络输出]
E --> H[控制台输出]
end
%% 数据流细节
C -->|触发条件变量| D
D -->|溢出检测| K[丢弃策略]
D -->|性能数据| L[系统监控]
K -->|信息上报| E
L -->|信息上报| E
%% 文件模块
F --> M[日志轮转]
F --> N[目录管理]
M -->|创建文件| O[YYYY-MM-DD_HH-MM-SS-MS_j.log]
%% 网络模块
G --> P[TCP连接]
G --> Q[心跳机制]
G --> R[自动重连]
%% 样式定义
classDef process fill:#e6f3ff,stroke:#3385ff;
classDef data fill:#ffe6e6,stroke:#ff3333;
classDef config fill:#e6ffe6,stroke:#33cc33;
class X0,X,Y0,Y,A,B,I,J process;
class C,K,L data;
class F,G,H,O,P,Q,R config;
- 主写入流
应用线程 → jlog_vprint/jlog_print/jlog_write → 环形缓冲区 → 更新widx→ 触发条件变量 → 后台线程 → 无锁输出路由 → 文件/网络/控制台 → 更新ridx
- 配置更新流
jlog_cfg_set() → 配置管理 → 热加载 → 输出模块重置 → 文件重开/网络重连
- 异常处理流
文件写入失败 → 关闭文件 → 重置状态 → 下次检查创建新文件 网络发送失败 → 关闭socket → 启动重连计时 → 定时重连尝试
- 监控数据流
后台线程 → 定时采集 → 生成JSON → 走正常输出流程
- 环形缓冲区
+---------------+ 写入端 --> | widx | | 缓冲区 | 读取端 --> | ridx | +---------------+ 特性: - 写优先:保证应用线程最快速度返回 - 空间回收:当ridx追上widx时重置指针 - 溢出保护:丢弃策略保留 (1/16) 的最新数据
- 输出路由决策
if (mode == JLOG_TO_FILE) { 文件输出模块(); } else if (mode == JLOG_TO_NET) { 网络输出模块(); } else { 控制台输出(); } 特殊处理: - 模式切换时自动flush历史数据 - 网络模式优先保证心跳包
- 文件输出轮转
创建日志文件 → 写入数据 → 检查大小 → 超过阈值 → 关闭文件 → 记录文件名 → 创建新文件 → 清理旧文件 ↑____________日志轮转策略____________↓
- 网络输出重连
CONNECTING → CONNECTED → (发送数据/心跳) ↑ ↓ └──断开重连──────┘ 重连策略: - 指数退避重试(最大间隔30秒) - 心跳包维持连接活性
- 核心管理(jlog_mgr_t)
typedef struct { int inited; // 初始化标志 jlog_jbuf_t jbuf; // 环形缓冲区 jlog_jcfg_t jcfg; // 全局配置 jthread_t tid; // 后台线程 // 同步原语 jthread_mutex_t cmtx; // 配置锁 jthread_mutex_t mtx; // 缓冲区锁 jthread_cond_t cond; // 条件变量 } jlog_mgr_t;
- 缓冲区管理(jlog_jbuf_t)
- 环形缓冲区实现:
widx → 写入位置 ridx → 读取位置 tail → 缓冲区尾部
- 溢出保护机制:
- 当缓冲区使用超过 7/8 时丢弃旧日志(JLOG_BUF_FACTOR=4)
- 丢失/截断统计(loses/truncs)
- 环形缓冲区实现:
- 输出模块
-文件输出特性:
- 日志轮转:文件名格式
YYYY-MM-DD_HH-MM-SS-MS_j.log- 自动创建目录 - 文件大小限制(默认1MB)- 网络输出特性:
- 心跳包机制(HEARTBEAT_SEC=30s)
- 自动重连(RECONNECT_SEC=30s)
- 大块数据分片发送(JLOG_SEND_SIZE=64KB)
- 网络输出特性:
- 性能监控
typedef struct { int cpu_cycle; // CPU采集周期(秒) int mem_cycle; // 内存采集周期 int net_cycle; // 网络采集周期 } jlog_perf_t;
- 输出格式示例:
[2025-02-14 09:15:55.964 I N PERF] { "cpu":{ "self":24, // 本进程CPU占用百分比 "system":23 // 系统总CPU占用百分比 }, "memory":{ "self":{ "virtual":68800, // 本进程虚拟内存占用(KB) "physical":2240 // 本进程物理内存占用(KB) }, "system":{ "virtual":2227544, // 系统总虚拟内存占用(KB) "physical":1551448, // 系统总物理内存占用(KB) "total":16336840 // 系统总物理内存大小(KB) } }, "network":[ { "name":"lo", // 网络设备名 "recv":4136938169, // 网络接收速率(B/s) "send":4136938169 // 网络发送速率(B/s) }, { "name":"ens33", "recv":0, "send":0 }, { "name":"docker0", "recv":0, "send":0 } ] }
- 输出格式示例:
-
流量统计(GB/s)
每次写大小(字节) 32 64 128 256 512 1024 无任何处理单线程循环发送 0.11 0.23 0.44 0.81 1.46 2.50 jlog单线程写日志单线程发送 1.33 2.11 4.35 5.89 6.48 5.74 jlog六线程写日志单线程发送 0.61 0.70 1.40 1.89 2.17 2.14 -
流量统计(百万次/秒)
每次写大小(字节) 32 64 128 256 512 1024 无任何处理单线程循环发送 3.77 3.86 3.71 3.40 3.05 2.62 jlog单线程写日志单线程发送 22.37 23.58 29.24 21.98 12.79 5.84 jlog六线程写日志单线程发送 10.16 9.73 9.37 7.03 4.30 2.18
从性能统计可以看出,jlog可达千万次每秒和GB/s的性能,比纯粹不做任何处理的循环发送还要快一个数量级。
jlog是一个高效的日志系统实现,其设计考虑了多线程、缓冲区管理、文件I/O、网络通信等多个方面。以下是其高效设计的关键点:
- 缓冲区管理
- 环形缓冲区(Ring Buffer):
- 使用环形缓冲区(
jlog_jbuf_t)来存储日志数据,避免了频繁的内存分配和释放操作。 - 通过
widx和ridx指针管理写入和读取位置,数据零拷贝。 - 缓冲区大小可配置(
JLOG_BUF_SIZE),默认值为1 << 19(512KB),满足大多数场景需求。
- 使用环形缓冲区(
- 缓冲区溢出处理:
- 当缓冲区接近满时,丢弃旧日志数据(
jlog_buf_abandon),避免阻塞日志写入。 - 通过
JLOG_BUF_FACTOR控制丢弃的缓冲区大小,确保系统在高负载下仍能正常运行。
- 当缓冲区接近满时,丢弃旧日志数据(
- 环形缓冲区(Ring Buffer):
- 多线程设计
- 写线程与主线程分离:
- 日志写入操作由一个独立的线程(
jlog_run)处理,主线程只需将日志写入缓冲区,避免阻塞主线程。 - 使用条件变量(
jthread_cond_t)和互斥锁(jthread_mutex_t)实现线程间同步,确保数据一致性。
- 日志写入操作由一个独立的线程(
- 线程睡眠与唤醒:
- 写线程在无数据时进入睡眠状态(
jthread_cond_timedwait),减少CPU占用。 - 当缓冲区达到唤醒阈值(
JLOG_WAKE_SIZE)时,唤醒写线程进行日志写入。 - 即使未达到唤醒阈值,也定时(
JLOG_SLEEP_MS)唤醒写线程进行日志写入。
- 写线程在无数据时进入睡眠状态(
- 无锁发送:
- 写线程无锁发送,只对读索引(
ridx)进行保护和更新。
- 写线程无锁发送,只对读索引(
- 写线程与主线程分离:
- 高效的时间戳处理
- 快速时间戳生成:
- 使用
jlog_head_timestamp函数生成时间戳,避免频繁调用系统时间函数。 - 通过查表法(
ch_100_lut)快速将时间数值转换为字符串,减少计算开销。 - 根据需要选择更精确(
CLOCK_REALTIME)或者更快速的时间戳获取接口(CLOCK_REALTIME_COARSE)。
- 使用
- 时间戳缓存:
- 时间戳字符串缓存在缓冲区中(
jbuf->tbuf),避免重复生成。
- 时间戳字符串缓存在缓冲区中(
- 快速时间戳生成:
- 日志写入优化
- 批量写入:
- 日志数据以批量方式写入文件或网络(
jlog_write_file和jsocket_send),减少系统调用次数。 - 单次写入大小可配置(
JLOG_WRITE_SIZE和JLOG_SEND_SIZE),默认值为64KB。
- 日志数据以批量方式写入文件或网络(
- 文件轮转:
- 日志文件大小和数量可配置(
JLOG_DEF_FSIZE和JLOG_DEF_FCOUNT),避免单个文件过大。 - 通过
jlog_new_file实现文件轮转,确保日志文件不会无限增长。 - 缓存日志文件名,轮转时不需要再访问文件系统过滤日志文件删除。
- 日志文件大小和数量可配置(
- 批量写入:
- 网络通信优化
- 心跳机制:
- 无日志发送时才定期发送心跳包(
HEARTBEAT_SEC),检测网络连接状态。 - 当网络断开时,定期尝试重连(
RECONNECT_SEC),确保日志数据不丢失。
- 无日志发送时才定期发送心跳包(
- 批量发送:
- 日志数据以批量方式发送到网络,减少网络传输开销。
- 心跳机制:
-
结构:
[日期时间 日志级别 所属模块 日志类别] 具体日志内容 -
例子:
[2024-01-08 10:00:00.888 E session-mgr conn] {"status": "connected"} -
日志由两部分组成:格式化部分和具体日志内容,格式字段间通过空格分隔,定义如下:
- 日期时间:记录年、月、日、时、分、秒、毫秒。
- 日志级别:记录日志级别,错误、警告、信息、调试等。
- 所属模块:记录产生日志的软件模块。
- 日志类别:记录日志的类别,可用于过滤,例如区分日志是给用户看的还是给技术人员看的。
日志级别决定打印是否会输出,只有级别小于等于默认级别的日志才可以输出。日志级别也用于过滤,用户可以过滤出自己关心的日志,例如错误。
| 枚举 | 字符 | 说明 |
|---|---|---|
| JLOG_LEVEL_OFF | 关闭日志,无任何日志输出 | |
| JLOG_LEVEL_FATAL | F | 严重错误 |
| JLOG_LEVEL_ERROR | E | 一般错误 |
| JLOG_LEVEL_WARN | W | 警告 |
| JLOG_LEVEL_INFO | I | 必要的运行信息,默认开启的日志输出级别 |
| JLOG_LEVEL_DEBUG | D | 一般调试信息 |
| JLOG_LEVEL_TRACE | T | 详细调试信息,开启可能会严重影响性能 |
所属模块用于出问题时快速定位到哪个模块的代码出现问题。
所属模块使用下面结构体的指针。
typedef struct {
const char *str; // 字符串指针
int len; // 字符串长度
} jlog_str_t;日志类别主要是用于过滤特定功能的日志,这样,可以过滤只显示用户关心的日志给用户。并且,在调查某些特定功能问题的时候,技术人员也可以较快速地分析到问题点。有类别的日志一般输出的所有格式都是定义好的,日志内容是json格式。
日志类别使用下面结构体的指针。
typedef struct {
const char *str; // 字符串指针
int len; // 字符串长度
} jlog_str_t;jpthread是一个高效的多线程任务管理模块(线程池加定时器),支持立即执行、延迟执行和周期执行的任务调度。通过优先级队列、任务链表和空闲线程链表的组合,实现了任务的高效分发和线程资源的动态管理。主要特性如下:
- 任务类型支持:
- 立即执行任务(Worker)
- 一次性定时任务(Timer Once)
- 可暂停/恢复的周期任务(Timer Repeat/Paused)
- 动态线程管理:
- 最小线程数保持
- 空闲线程自动回收(10秒空闲检测)
- 任务过载时自动扩容
- 资源复用:
- 预分配内存池(线程/任务结构)
- 无系统调用开销的内存分配
- 高效调度:
- 优先级队列管理定时任务,确保任务按时执行
- 线程间条件变量通知机制,互斥锁保证多线程环境下的数据一致性
- 任务执行与调度分离设计,任务进程循环取任务执行
%% 系统核心流程图
graph LR
A1[初始化接口] --> |调用| B1[jpthread_init]
A2[反初始化接口] --> |调用| B2[jpthread_uninit]
A3[任务加入接口] --> |调用| B3[jpthread_task_add]
A4[任务移除接口] --> |调用| B4[jpthread_task_del]
A5[任务暂停接口] --> |调用| B5[jpthread_task_pause]
A6[任务恢复接口] --> |调用| B6[jpthread_task_resume]
%% 初始化模块
B1 --> |初始化| C[定时任务<br>优先级队列]
B1 --> |初始化| D[待执行任务<br>任务链表]
B1 --> |初始化| E[空闲线程<br>线程链表]
B1 --> |创建| F{主线程}
%% 反初始化模块
B2 --> |清空| C
B2 --> |清空| D
B2 --> |清空| E
B2 --> |销毁| F
%% 任务加入模块
B3 --> |任务判断| O{任务类型}
O --> |延迟任务加入| C
O --> |立即任务加入| D
O --> |立即任务唤醒执行| E
%% 任务移除模块
B4 --> |任务判断| P{任务移除方法}
P --> |任务待执行| Z2{任务直接删除}
P --> |任务执行中| R{设置任务相应状态}
%% 重复型任务暂停恢复模块
B5 --> |任务判断| Q{任务处理方法}
B6 --> |任务判断| Q
Q --> |任务待执行| S[重入优先级队列]
S --> |优先级变换| C
Q --> |任务执行中| R
%% 线程池核心模块
subgraph 线程池核心
%% 主线程模块
F --> |队列到期监控| C
F --> |线程回收管理| E
F --> |唤醒执行或销毁| E
C --> |任务到期| D
%% 任务线程模块
E --> |有任务执行唤醒| G{任务线程}
G --> |无任务执行归还| E
G --> |线程回收| Z0[销毁线程]
D --> |取任务执行| G
G --> |有任务执行| H[执行任务]
H --> |任务执行完| I{执行完成}
I --> |定时任务归还| C
I --> |其它任务销毁| Z1[销毁任务]
end
classDef interface fill:#e6f3ff,stroke:#3385ff;
classDef init fill:#e6ffe6,stroke:#33cc33;
classDef data fill:#fff0e6,stroke:#ff9933;
classDef process fill:#ffe6e6,stroke:#ff3333;
class A1,A2,A3,A4,A5,A6 interface;
class B1,B2,B3,B4,B5,B6 api;
class C,D,E data;
class F,G process;
- 初始化流程
1. 创建线程池管理结构(jpthread_mgr_t) 2. 初始化内存池(线程和任务资源) 3. 创建主线程(_thread_main) 4. 初始化优先级队列和任务链表 5. 预分配最小线程数的工作线程
- 反初始化流程
1. 停止主线程 2. 清空优先级队列和任务链表 3. 销毁所有工作线程 4. 释放内存池资源 5. 销毁互斥锁和条件变量
- 任务添加流程
应用层调用 jpthread_task_add() → 分配任务资源 → 根据类型加入队列/链表 → 唤醒工作线程 定时任务:加入优先级队列(按执行时间排序) 立即任务:加入任务链表 → 触发空闲线程或新建线程
- 任务执行流程
主线程检查优先级队列 → 到期任务移至任务链表 → 工作线程从链表取任务执行 执行完成后: - 单次任务:立即释放资源 - 周期任务:重新计算下次执行时间并加入队列 - 暂停任务:标记为暂停状态,等待恢复
- 线程管理流程
空闲线程链表 → 任务到达时唤醒 → 无空闲则新建线程 线程空闲超时(10秒)→ 销毁多余线程(保留最小线程数) 线程执行任务后 → 返回空闲链表或销毁
- 任务状态变换逻辑
IN_QUEUE(优先级队列) ↔ IN_LIST(任务链表) ↔ IN_THREAD(执行中) 暂停任务:TIMER_PAUSED → 重新调度后恢复为 TIMER_REPEAT 停止任务:STOPED → 资源释放
- 线程池管理结构(jpthread_mgr_t)
typedef struct { int running; // 运行状态 int min_threads; // 最小线程数 jpheap_mgr_t thread_pheap; // 线程资源内存池 jpheap_mgr_t task_pheap; // 任务资源内存池 struct jdlist_head thread_head; // 空闲线程链表 struct jdlist_head worker_head; // 立即任务链表 jpqueue_t timer_queue; // 定时任务优先级队列 // 同步原语 jthread_mutex_t mtx; // 全局互斥锁 jthread_cond_t cond; // 主线程条件变量 } jpthread_mgr_t;
- 任务管理结构(jpthread_task_t)
typedef struct { uint32_t id; // 任务ID jpthread_task_type type; // 任务类型(WORKER/TIMER_ONCE等) jpthread_task_state state; // 任务状态(IN_QUEUE/IN_LIST/IN_THREAD) jtime_nt_t wake_nt; // 下次执行时间 jpthread_cb exec_cb; // 任务执行回调 struct jdlist_head list; // 链表节点 } jpthread_task_t;
- 线程管理结构(jpthread_thread_t)
typedef struct { jthread_t thd; // 线程ID jthread_cond_t cond; // 线程条件变量 struct jdlist_head list; // 链表节点 } jpthread_thread_t;
- 内存池复用
- 线程资源池:预分配线程结构体,减少动态内存分配。
- 任务资源池:固定大小的任务结构体池,避免频繁申请释放。
- 无锁设计
- 优先级队列操作:仅主线程修改队列,工作线程无竞争。
- 任务链表操作:通过互斥锁保证线程安全,但执行期间无锁。
- 队列设计
- 立即任务链表:FIFO结构快速处理即时请求
- 定时优先队列:基于时间的小根堆,O(1)复杂度获取最近到期任务,O(logN)复杂度插入/删除
- 动态线程调整
- 按需创建:任务到达时优先唤醒空闲线程,不足则新建。
- 超时回收:空闲超时(10秒)自动销毁多余线程,避免抖动,降低资源占用。
- 分级条件变量:
- 全局条件变量(mgr->cond):处理定时任务事件
- 线程私有条件变量(thread->cond):响应任务分配,对比单条件变量设计减少错误唤醒
- 稳定性保障:
- 误差补偿:容忍100μs内的时间误差,减少不必要的队列调整。
- 漂移补偿:异常延迟时自动对齐系统时间
jringbuf 是一个面向工业级多线程场景的环形缓冲区管理器,支持 多生产者-多消费者(MPMC)、固定大小元素、共享/独占消费模式、历史窗口、多种读写策略(阻塞/重试/丢弃/完全读写)以及 动态添加/移除生产者/消费者。它以 元素 为操作单位,每个元素大小由 unit_size 指定(字节)。所有接口的 len、容量、返回值均以元素个数计数,内部自动管理字节偏移,保证写入/读取总是完整的元素。通过互斥锁与条件变量保证线程安全,在单生产者单消费者(SPSC)场景下将数据拷贝置于锁外,性能接近无锁实现。
- 多生产/多消费:支持任意数量的生产者和消费者(≥1),动态加入退出。
- 两种消费模式:
JRINGBUF_READ_SHARED:共享读指针,任一消费者读取都会推进全局读位置。JRINGBUF_READ_EXCLUSIVE:独立读指针,所有消费者都读过某元素后空间才释放,适合多速度消费者。
- 历史窗口:通过
hold_num保留最近写入的 元素个数,新加入的消费者可回溯消费。 - 灵活的读写策略:
- 完整读写(
JRINGBUF_COMPLETE) - 阻塞等待(
JRINGBUF_BLOCK,支持超时) - 重试有限次(
JRINGBUF_RETRY) - 缓冲区满时丢弃最旧数据(
JRINGBUF_DROP)
- 完整读写(
- 固定元素大小:所有元素等长,配置时指定
unit_size,读写操作以元素为单位。 - 线程安全停止/启动:可安全地禁止新读写并等待所有进行中的操作完成。
- 内存紧凑布局:缓冲区、消费者/生产者有效性数组、消费者读索引数组均分配在同一块连续内存中(柔性数组),减少碎片。
graph TB
subgraph 接口层
A1[jringbuf_init] --> |创建| M
A2[jringbuf_uninit] --> |销毁| M
A3[jringbuf_write] --> |写入len个元素| M
A4[jringbuf_read] --> |读取len个元素| M
A5[jringbuf_add/del_producer] --> |管理| M
A6[jringbuf_add/del_consumer] --> |管理| M
A7[jringbuf_drop_data] --> |丢弃dropped个元素| M
A8[jringbuf_size/capacity] --> |查询元素个数| M
end
subgraph 核心数据结构
M[jringbuf_t]
M --> B[数据缓冲区<br/>capacity个元素 × unit_size字节]
M --> C[消费者有效性数组<br/>max_consumers > 1]
M --> D[消费者读索引数组(独占模式)<br/>存储元素位置]
M --> E[生产者有效性数组<br/>max_producers > 1]
end
subgraph 同步与状态
M --> L[互斥锁]
M --> N[not_empty条件变量]
M --> F[not_full条件变量]
M --> S[写指针 write_index(元素数)]
M --> G[全局最小读指针 min_read_index(元素数)]
M --> H[有效数据长度 data_len(元素数)]
M --> I[惰性更新标志 min_read_stale]
M --> J[正在读写计数 rw_count]
M --> K[禁用标志 disable_rw]
end
subgraph 逻辑流程
L --> K
N --> G
F --> S
I --> G
J --> K
end
- 检查参数(容量、unit_size、最大生产者/消费者 ≥1)。
- 容量(元素个数)向上取整为2的幂(便于位掩码计算),实际字节容量 = 对齐后容量 × unit_size。
- 计算各部分偏移量:
- 数据缓冲区(字节容量)
- 消费者有效性数组(max_consumers>1 时分配,每个 uint8_t)
- 消费者读索引数组(max_consumers>1 且独占模式时分配,每个 uint32_t,存储元素位置)
- 生产者有效性数组(max_producers>1 时分配)
- 一次性分配连续内存(jheap_malloc),并按偏移量布局。
- 初始化互斥锁、条件变量(单调时钟)。
- 返回 jringbuf_t 指针。
- 加锁,增加 rw_count。
- 若 disable_rw 为真 → 解锁并返回 -1。
- 检查生产者有效性(多生产者时)。
- 若有惰性更新标志,调用 update_min_read_index() 刷新。
- 计算剩余元素空间 space = capacity - data_len。
- 若 space >= len → 跳转到写入。
- 否则根据策略处理:
- 阻塞(BLOCK):等待 not_full 条件变量(支持超时)。
- 重试(RETRY):解锁后 yield,重新尝试(循环)。
- 丢弃(DROP):按 dropped 参数至少丢弃 drop_amount 个旧元素, 更新所有消费者索引和 min_read_index,重新计算 space。
- 若最终空间仍不足 → 解锁返回 -1。
- 计算环形写位置 wpos = write_index & (capacity-1),字节偏移 = wpos * unit_size。 分两段 memcpy(总字节 = to_write * unit_size)。 (若为单生产者,memcpy 在锁外执行以减少临界区)
- 更新 write_index += to_write,data_len += to_write。
- 广播 not_empty 条件变量(若 data_len >= wake_num)。
- 减少 rw_count,解锁,返回写入元素个数。
- 加锁,增加 rw_count。
- 若 disable_rw 为真 → 解锁返回 -1。
- 检查消费者有效性(多消费者时)。
- 确定该消费者的可读元素数:
- 单消费者或共享模式:avail = data_len
- 独占模式:avail = write_index - cons_idx[consumer_id]
- 若 avail >= need(need = 完全读时 len,否则 1)→ 跳转读取。
- 否则根据策略:
- 阻塞(BLOCK):等待 not_empty 条件变量(支持超时)。
- 重试(RETRY):解锁 yield 后重试。
- 若仍不足 → 解锁返回 -1。
- 确定读指针 c_read(根据模式取自 min_read_index 或消费者私有索引)。
- 计算环形读位置 rpos = c_read & (capacity-1),字节偏移 = rpos * unit_size。 分两段 memcpy(总字节 = to_read * unit_size)。 若为单消费者,则将 min_read_lock 置 1,解锁后再拷贝以减少锁持有时间。
- 更新读位置:
- 共享/单消费者:min_read_index += to_read,data_len -= to_read。
- 独占模式:消费者索引增加 to_read;若该消费者之前是最慢的, 则设置 min_read_stale = 1(惰性更新)。
- 广播 not_full 条件变量。
- 减少 rw_count,解锁,返回读取元素个数。
- 添加生产者(
jringbuf_add_producer):在多生产者模式下,遍历有效性数组找到首个 0 位置,置 1 并增加计数,返回 ID。单生产者直接返回 0。 - 删除生产者(
jringbuf_del_producer):将对应位置置 0,减少计数;若 producer_id=-1 则全部清空。广播 not_full 唤醒等待写入的线程。 - 添加消费者(
jringbuf_add_consumer):类似生产者,初始化读位置(若独占模式,可选择从 min_read_index 或 write_index 开始)。 - 删除消费者(
jringbuf_del_consumer):清空有效性位,减少计数,并调用 update_min_read_index() 立即重新计算全局最小读指针,可能释放空间给写入端。
- 用于强制跳过指定消费者的部分或全部未读元素,或跳过所有消费者的数据。
- 在独占模式下,会逐个更新消费者索引,并调用 update_min_read_index()。
- 在共享/单消费者模式下,直接推进 min_read_index。
- 操作期间检查 min_read_lock(单消费者拷贝锁外时设置),避免与正在进行的读取冲突。
- 在独占模式下,遍历所有活跃消费者,找出最小的读索引(即最慢的消费者)。
- 若 hold_num > 0,则将最小值钳位到
write_index - hold_num以上(但不得低于 old_min),保证新消费者可读一定历史(元素个数)。 - 更新 min_read_index、data_len,清除惰性标志。
struct jringbuf {
uint32_t max_producers; // 最大生产者数量
uint32_t cur_producers; // 当前生产者数量
uint32_t max_consumers; // 最大消费者数量
uint32_t cur_consumers; // 当前消费者数量
uint32_t hold_num; // 历史窗口大小(元素个数)
uint32_t wake_num; // 唤醒窗口大小(元素个数)
enum jringbuf_read_mode read_mode; // JRINGBUF_READ_SHARED / EXCLUSIVE
uint8_t disable_rw; // 读写禁止标志
uint8_t min_read_stale; // 惰性更新标志
uint8_t min_read_lock; // 单消费者拷贝时锁定标志
uint32_t rw_count; // 正在读写的线程数
uint32_t unit_size; // 每个元素大小(字节)
uint32_t capacity; // 缓冲区元素总个数(2的幂)
uint32_t data_len; // 有效元素个数
uint32_t write_index; // 绝对写位置(元素数),单调递增,自然溢出
uint32_t min_read_index; // 全局最小读位置(元素数)
jthread_mutex_t mutex; // 互斥锁
jthread_cond_t not_empty; // 数据可用条件变量
jthread_cond_t not_full; // 空间可用条件变量
uint32_t buf_offset; // 数据缓冲区偏移(字节)
uint32_t consumer_active_offset; // 消费者有效性数组偏移
uint32_t consumer_index_offset; // 消费者读索引数组偏移(元素位置)
uint32_t producer_active_offset; // 生产者有效性数组偏移
uint32_t total_size; // 总分配字节数
uint8_t data[]; // 柔性数组起始
};JRB_BUF(rb):获取数据缓冲区起始地址。JRB_CONS_ACT(rb):获取消费者有效性数组。JRB_CONS_IDX(rb):获取消费者读索引数组(独占模式)。JRB_PROD_ACT(rb):获取生产者有效性数组。
enum jringbuf_strategy {
JRINGBUF_COMPLETE = 1, // 完全读写
JRINGBUF_BLOCK = 1 << 1, // 阻塞
JRINGBUF_RETRY = 1 << 2, // 重试
JRINGBUF_DROP = 1 << 3 // 丢弃旧数据
};enum jringbuf_read_mode {
JRINGBUF_READ_SHARED, // 共享读指针
JRINGBUF_READ_EXCLUSIVE // 独立读指针
};typedef struct jringbuf_cfg {
uint32_t capacity; // 缓冲区元素个数,内部向上对齐到2的幂
uint32_t unit_size; // 单个元素大小(字节),必须 > 0
uint32_t max_producers; // 最大生产者数量
uint32_t max_consumers; // 最大消费者数量
uint32_t hold_num; // 历史窗口大小(元素个数)
uint32_t wake_num; // 唤醒阈值(元素个数)
enum jringbuf_read_mode read_mode;
} jringbuf_cfg_t;-
锁外内存拷贝
- 单生产者时,写入 memcpy 在解锁后执行;单消费者时,读取 memcpy 也释放锁后执行,使 SPSC 场景下的锁仅保护索引更新,降低延迟。
-
惰性更新(Lazy Update)
- 独占模式下,非最慢消费者推进时不立即重算全局 min_read_index,只设 stale 标志,在下次写入或查询时一次性遍历,减少开销。
-
条件变量与精确唤醒
- 使用 not_empty 和 not_full 两个条件变量,避免无效广播,仅在数据或空间条件满足时唤醒相应线程。
-
内存紧凑布局
- 所有结构、缓冲、辅助数组在一处连续内存,提高缓存局部性,减少碎片。
-
环形寻址优化
- 容量为 2 的幂,通过位掩码 & (capacity-1) 快速计算环内位置;元素字节偏移 = 环内索引 × unit_size。
-
正在读写计数与安全停止
- stop 时设置 disable_rw,循环等待 rw_count 降为 0,确保无活跃操作后才返回。
-
避免优先级反转
- 丢弃逻辑中检查 min_read_lock(单消费者拷贝时置位),若发现则让出 CPU 重试,防止写操作长时间阻塞读。
-
动态成员管理
- 添加/删除操作只改有效性数组和计数,不进行运行时内存分配,所有槽位预先分配。
jringdata 是 jringbuf 的扩展,专为 索引+数据分离 的变长消息场景设计。它将 索引(metadata) 和 裸数据(payload) 分别存储在两个独立环形缓冲区中,通过索引记录每条消息的长度,实现 可变长数据 的高效管理。支持 多生产者-多消费者(MPMC)、共享/独占消费模式、历史窗口、多种读写策略(阻塞/重试/丢弃/完全读写)、动态添加/移除生产者/消费者,以及 连续/分散(gather/scatter)读写。所有索引操作以“索引个数”为单位,数据操作以“字节”为单位,内部自动维护索引与数据之间的一一对应关系。通过互斥锁与条件变量保证线程安全,同样在单生产者/单消费者场景下优化锁外拷贝。
- 索引与数据分离:索引缓冲区存储每个消息的元信息(如长度),数据缓冲区存储实际载荷,支持任意长度消息。
- 灵活的长度获取:通过用户提供的
get_size回调从索引中提取数据长度,默认将索引前4字节视为 uint32_t 长度。 - 两种消费模式:共享读指针(
JRINGDATA_READ_SHARED)和独立读指针(JRINGDATA_READ_EXCLUSIVE)。 - 历史窗口:通过
hold_num保留最近写入的 索引个数,新消费者可回溯历史。 - 读写策略:完全读写(COMPLETE)、阻塞(BLOCK)、重试(RETRY)、丢弃旧数据(DROP)。
- 连续与分散操作:提供
write/read(连续缓冲区)和writev/readv(指针数组)两种接口。 - 线程安全停止/启动:与
jringbuf一致。 - 内存紧凑布局:索引缓冲区、数据缓冲区、消费者读索引/读数据数组、有效性数组全部连续分配。
graph TB
subgraph 接口层
A1[jringdata_init] --> |创建| M
A2[jringdata_uninit] --> |销毁| M
A3[jringdata_write/writev] --> |写入num个索引及数据| M
A4[jringdata_read/readv] --> |读取num个索引及数据| M
A5[jringdata_add/del_producer] --> |管理| M
A6[jringdata_add/del_consumer] --> |管理| M
A7[jringdata_drop_data] --> |丢弃dropped个索引| M
A8[jringdata_size/capacity] --> |查询索引/数据大小| M
end
subgraph 核心数据结构
M[jringdata_t]
M --> B1[索引缓冲区<br/>idx_num个索引 × idx_size字节]
M --> B2[数据缓冲区<br/>capacity字节]
M --> C[消费者有效性数组]
M --> D1[消费者索引读位置数组(独占模式)]
M --> D2[消费者数据读位置数组(独占模式)]
M --> E[生产者有效性数组]
end
subgraph 同步与状态
M --> L[互斥锁]
M --> N[not_empty条件变量]
M --> F[not_full条件变量]
M --> S1[索引写指针 idx_write_index]
M --> S2[数据写指针 data_write_index]
M --> G1[索引全局最小读指针 idx_min_read_index]
M --> G2[数据全局最小读指针 data_min_read_index]
M --> H1[有效索引个数 idx_data_len]
M --> H2[有效数据字节数 data_data_len]
M --> I[惰性更新标志 min_read_stale]
M --> J[正在读写计数 rw_count]
M --> K[禁用标志 disable_rw]
M --> Fn[get_size回调函数]
end
subgraph 逻辑流程
L --> K
N --> G1
F --> S1
I --> G1
J --> K
Fn --> B1
end
- 检查参数(idx_num, idx_size, capacity, max_producers, max_consumers ≥1)。
idx_num和capacity分别向上对齐为 2 的幂。- 计算各部分偏移量:
- 索引缓冲区(idx_num × idx_size 字节)
- 数据缓冲区(capacity 字节)
- 消费者索引读位置数组(独占模式时,max_consumers × uint32_t)
- 消费者数据读位置数组(独占模式时,max_consumers × uint32_t)
- 消费者有效性数组(max_consumers>1 时)
- 生产者有效性数组(max_producers>1 时)
- 一次性分配连续内存,布局。
- 初始化互斥锁、条件变量。
- 设置
get_size回调(若未提供则使用默认:取索引前4字节)。
- 加锁,增加 rw_count,检查 disable_rw。
- 校验生产者有效性。
- 若
min_read_stale为真,调用update_min_read_index()。 - 计算索引剩余空间
idx_space = idx_total - idx_data_len,数据剩余空间data_space = data_total - data_data_len。 - 根据
complete模式确定最小需求:- 完全写:需要
num个索引和总数据len字节。 - 非完全写:只需至少 1 个索引及其对应数据。
- 完全写:需要
- 若空间不足,根据策略等待(BLOCK)或重试(RETRY),若允许 DROP 则尝试丢弃旧数据(调用
drop_old_data)。 - 丢弃成功后重新计算可写入量(
calc_write_size),可能小于请求数。 - 写入数据:
- 计算索引环形写位置
idx_wpos = idx_write_index & idx_mask,分两段 memcpy 写入索引。 - 数据部分:依次写入每个索引对应的数据块(数据长度由
get_size回调获取),分两段 memcpy。 - 对于离散模式(writev),分别从指针数组取值。
- 若为单生产者,拷贝在锁外执行。
- 计算索引环形写位置
- 更新
idx_write_index、data_write_index、idx_data_len、data_data_len。 - 若
idx_data_len >= wake_num,广播 not_empty。 - 解锁,返回写入索引个数。
- 加锁,增加 rw_count,检查 disable_rw。
- 校验消费者有效性。
- 确定消费者的索引读位置
c_idx和数据读位置c_data(共享模式取全局,独占模式取私有)。 - 计算可读索引数
avail_idx和对应数据量avail_data。 - 根据
complete模式确定最小需求(完全读需num个索引,否则需至少 1 个)。 - 若可用索引不足,按策略等待或重试;若用户提供的 data 缓冲区长度不足以容纳任一完整索引的数据,则报错。
- 调用
calc_read_size计算实际可读索引数read_num和对应数据量read_data。 - 拷贝数据:
- 索引:从索引缓冲区的
c_idx位置连续读取read_num个索引到用户缓冲区。 - 数据:按顺序读取每个索引对应的数据块,写入用户数据缓冲区(连续模式)或各指针指向的缓冲区(分散模式)。
- 若为单消费者,拷贝在锁外执行(设置 min_read_lock)。
- 索引:从索引缓冲区的
- 更新读位置:
- 共享模式:移动全局
idx_min_read_index和data_min_read_index,减少idx_data_len和data_data_len。 - 独占模式:更新该消费者的私有读位置,若该消费者为最慢则置
min_read_stale=1。
- 共享模式:移动全局
- 广播 not_full。
- 返回实际读取索引个数。
与 jringbuf 类似,但添加消费者时需同时初始化索引和数据读位置(独占模式)。
- 可丢弃指定消费者或所有消费者的部分/全部未读索引。
- 在独占模式下,逐个推进消费者读位置(索引和数据),然后更新全局
min_read_index。 - 在共享/单消费者模式下,直接推进全局读位置。
- 支持指定丢弃索引个数(
dropped),0 表示全部丢弃。
- 遍历所有活跃消费者,取最小的索引读位置作为候选
min_idx,同时记录对应的数据读位置。 - 若
hold_num > 0,则min_idx不得小于idx_write_index - hold_num(保留历史索引)。 - 同时更新
idx_min_read_index、idx_data_len、data_min_read_index、data_data_len。 - 清除惰性标志。
calc_data_len_for_idx_range:根据起始索引绝对位置和数量,遍历每个索引调用get_size累加得到数据总字节数。- 用于丢弃、更新最小读指针等场景。
struct jringdata {
// 成员管理
uint32_t max_producers;
uint32_t cur_producers;
uint32_t max_consumers;
uint32_t cur_consumers;
uint32_t hold_num; // 历史索引保留数
uint32_t wake_num; // 唤醒阈值(索引个数)
enum jringdata_read_mode read_mode;
uint8_t disable_rw;
uint8_t min_read_stale;
uint8_t min_read_lock;
uint32_t rw_count;
uint32_t total_size; // 总分配字节数
uint32_t producer_offset;
uint32_t consumer_offset;
uint32_t (*get_size)(const void *idx); // 长度回调
// 索引上下文
struct jringdata_ctx {
uint32_t total_len; // 索引总个数(2的幂)
uint32_t unit_size; // 索引结构体大小
uint32_t data_len; // 有效索引个数
uint32_t write_index; // 绝对写位置(索引序号)
uint32_t min_read_index; // 全局最小读位置(索引序号)
uint32_t buf_offset; // 索引缓冲区偏移
uint32_t read_index_offset; // 消费者读索引数组偏移
} idx_ctx;
// 数据上下文(类似,但 data 的 unit_size 恒为1)
struct jringdata_ctx {
uint32_t total_len; // 数据总字节数(2的幂)
uint32_t unit_size; // 恒为1
uint32_t data_len; // 有效数据字节数
uint32_t write_index; // 数据写位置(字节)
uint32_t min_read_index; // 数据最小读位置(字节)
uint32_t buf_offset; // 数据缓冲区偏移
uint32_t read_index_offset; // 消费者读数据数组偏移
} data_ctx;
jthread_mutex_t mutex;
jthread_cond_t not_empty;
jthread_cond_t not_full;
uint8_t data[]; // 柔性数组(索引、数据、数组等连续存放)
};JRD_IDX_BUF(rd):索引缓冲区起始。JRD_DATA_BUF(rd):数据缓冲区起始。JRD_RIDX_ARR(rd):消费者索引读位置数组(独占模式)。JRD_RDATA_ARR(rd):消费者数据读位置数组(独占模式)。JRD_PROD_ACT(rd)、JRD_CONS_ACT(rd):有效数组。
与 jringbuf_strategy 相同,但 DROP 作用于索引丢弃。
typedef struct jringdata_cfg {
uint32_t idx_num; // 索引个数,对齐到2的幂
uint32_t idx_size; // 每个索引大小(字节)
uint32_t capacity; // 数据缓冲区容量(字节),对齐到2的幂
uint32_t max_producers;
uint32_t max_consumers;
uint32_t hold_num; // 历史保留索引数
uint32_t wake_num; // 唤醒阈值(索引数)
enum jringdata_read_mode read_mode;
uint32_t (*get_size)(const void *idx); // 长度提取回调
} jringdata_cfg_t;除继承 jringbuf 的所有性能优化外,jringdata 额外具备:
- 双缓冲区分离:索引和数据独立环形,索引移动频繁,数据拷贝较大,分离后减少干扰。
- 用户自定义长度提取:
get_size回调允许用户自定义索引格式,无需额外解析。 - 惰性更新更精细:独占模式下,仅当消费者读位置与全局最小重合时才触发重新计算。
- 批量计算数据长度:通过
calc_data_len_for_idx_range一次遍历多个索引累计长度,避免反复调用。 - 分散读写支持:
writev/readv允许零拷贝收集/分散,减少内存复制次数。 - 内存布局紧凑:索引、数据、辅助数组连续,提高缓存命中率。
- Phone: +86 18368887550
- wx/qq: 1083936981
- Email: lengjingzju@163.com 3090101217@zju.edu.cn