接续 总结(三)
其实,用户空间,使用ring_buffer__poll(rb, N) 通过设置不同的 N,在网络流量不大,计算机负载不高的环境,也基本能达到目的。但,也明显存在一些看不下去的问题:1)handle_pkt_event 回调函数直接执行了耗时的 printf 和 pcap_dump 操作。这些操作会阻塞 BPF 程序的数据消费路径,肯定要影响整体数据吞吐能力;2)屏幕上打印日志和写入 PCAP 文件明显属于典型的 I/O 密集型操作,放在同一个线程中串行执行,容易成为性能瓶颈;3)直接进行 I/O 操作可能导致程序在某些情况下丢包或崩溃(如磁盘、终端卡顿等);4)主线程负担太重,承担了太多职责(接收事件、处理事件、输出结果)。5)主线程退出,可能仍有部分数据未处理完,导致数据丢失或文件损坏。僵尸线程、资源没有彻底释放这些。运行了该程序,经常关机时间都要延长去处理野线程;等等。
根据上面分析,如下是想做的优化和想达到的目的:
1)回调函数只负责将任务放入队列并立即返回。这样,队列推送是快速的内存操作(加锁 + 指针操作),几乎不占用时间。数据处理被卸载到专用线程,主线程或事件循环不再阻塞。目的是,减少回调函数的执行时间,提高事件处理的并发性。降低延迟,提高吞吐量;
2)I/O 操作并行化。屏幕输出与 PCAP 写入分别由两个独立线程完成,两者互不干扰,可充分利用多核 CPU 的并行计算能力。目的是,多线程并行处理不同类型的 I/O,提高系统资源利用率。如果今后需要 同时开启屏幕输出和 PCAP 输出时,性能提升更为明显;
3)使用带锁的任务队列机制,保证线程间安全通信。队列具有缓冲能力,在短时间内突发大量事件时,不会导致回调函数堆积。目的是,减少因瞬时负载过高导致的丢包或异常,提升系统的稳定性;
主线程仅负责事件分发和管理,其他线程各自专注特定任务。目的是,降低主线程负担,以后要是有心情升级时,具有很方便的扩展性;
4)让程序退出更加安全可靠。向队列发送 TASK_EXIT 信号通知工作线程退出,确保所有已提交的任务都处理完毕再清理资源,主线程等待所有异步任务完成后才退出;
5)也还有很多可以优化的。如:限制队列长度,防止内存无限增长、使用线程池:对于 PCAP 写入等操作,可引入线程池进一步提高吞吐、使用无锁队列,用 CAS 实现无锁队列来减少锁竞争、批量处理任务,将多个小任务合并成一个批次处理,减少系统调用开销、动态优先级调整,不同线程设置不同优先级,以确保关键任务及时执行。等等。但考虑到这个项目非正规生产环境使用,且环境网络现实,暂时就放弃了。
哦,先说结果吧。实现了上述优化措施后,使用 /usr/bin/time -v 简单测试,那些目的基本达到
注意,是使用的 /usr/bin/time -v ,它会记录并报告一个命令执行期间的资源使用情况。他是 GNU time 工具,不是 shell 内建的 time 命令。
Pending async tasks at exit: 0
Command being timed: "./tc"
User time (seconds): 0.02
System time (seconds): 0.03
Percent of CPU this job got: 0%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:15.59
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 13396
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 0
Minor (reclaiming a frame) page faults: 1566
Voluntary context switches: 861
Involuntary context switches: 9789
Swaps: 0
File system inputs: 0
File system outputs: 0
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 4096
Exit status: 0
Pending async tasks at exit: 0
程序干净地退出了,再没有遗留未处理的任务,感觉轻松。
User time (seconds): 0.02 System time (seconds): 0.03
用户态只用了 0.02 秒、内核态只用了0.03秒。相对 用户态那么多功能,也只 0.02 秒,内核态简单几句话,就用了 0.03 秒。体会到内核态虽然话不多,但其中调用了不少系统调用。
Percent of CPU this job got: 0%
程序共运行了 15.59 秒,但只用了 0.02 + 0.03 的CPU时间。CPU负担没有什么。估计程序大部分时间都没有主动计算,等待网络包到来(I/O 等待)。
Maximum resident set size (kbytes): 13396
程序高峰时,13MB 的内存,BPF 应用中应该属于属于正常范围。
Major (requiring I/O) page faults: 0
数据都在内存中加载好了,程序没有触发磁盘 I/O 缺页。很好!
Voluntary context switches: 861
Involuntary context switches: 9789
程序主动让出 CPU,自愿上下文切换次数 861 估计是为了等待网络事件,应该正常。但, CPU 被强制抢占,非自愿上下文切换次数 9789 感觉有点高,程序频繁被其他进程/线程打断,电脑里的进程、线程都在抢资源。是不是因为我是在配置不高的电脑里,运行的虚拟机导致的?有时间会用其他分析工具再看看。
还有些指标可以忽略, 现在的系统通常不统计的值,基本废弃。还出现在屏幕上,估计是为保留作兼容性用途。
当然,最后, Exit status: 0 表示程序正常退出了。好!
优化的设计及实现:
依据前面已表述的优化方向,加快用户空间收到数据包数据后的处理速度,即所谓提高消费效率,快速消费。
笼统说,就是使用 多线程 + 异步队列。异步获取数据、异步屏幕输出、异步写入PCAP文件;简化 handle_pkt_event() 回调,仅做最基础的拷贝动作。顺便添加些功能:消费速率统计、添加序号字段,可在用户态直观的看到 seq_num 是否连续…。即通过这些添加修改,达到目的:
1) 批处理消费:
使用 epoll_wait() 替代 poll。epoll_wait是epoll机制的核心系统调用,负责实际的事件等待和获取。本程序就是用他监听环形缓冲区和退出信号,实现了BPF+epoll的高效事件驱动架构。同时,通过event_fd实现信号安全的中断机制。epoll_wait 后是循环消费所有可用事件。核心逻辑可以看epoll_thread_func 函数。这个函数中注意一点,阻塞等待事件我设置的是最多返回 1 个事件,所以使用了do { … } while (consumed > 0)。这样,即时多个事件同时到达,下一次循环会继续处理。确保不遗漏事件,确保一次性处理完当前所有事件。(epoll 使用默认的 水平触发 模式。只要缓冲区中有未处理的数据,epoll 会持续通知。即使一次 epoll_wait() 只消费一个事件,后续仍会再次触发事件通知。)
2)分离的生产者/消费者队列避免竞争:
分离的生产者/消费者队列设计是核心优化手段之一。优化设计中,是通过这些机制避免竞争:分离队列,不同输出模式(屏幕打印/写入pcap)维护独立的任务队列;细粒度锁,每个队列有独立的互斥锁(pthread_mutex_t);条件变量:每个队列有独立的条件变量(pthread_cond_t)实现高效线程同步。可以通过 struct TaskQueue 和 struct Context 这两个东西理解。
在这个程序范围内,生产者应该就是那个回调函数(handle_pkt_event),而 消费者就是那些实现输出的工作线程。生产者做了这些事情:根据output_mode选择目标队列(screen_queue或pcap_queue)、获取队列专用锁(pthread_mutex_lock)、将任务添加到队列尾部、通过队列专用条件变量通知消费者(pthread_cond_signal)、释放锁(pthread_mutex_unlock。消费者的事情:获取队列专用锁、检查队列是否为空,若空则等待条件变量(pthread_cond_wait)、从队列头部取出任务、释放锁、处理任务。采取了这些措施,避免锁竞争:独立锁范围:screen_queue和pcap_queue的操作互不干扰,允许并行处理、缩短临界区:每个锁仅保护单个队列,临界区代码量最小化。
3) 上下文管理统一、 任务根据类型模块化、线程职责单一。
整个程序使用了一个统一的 Context 结构体贯穿所有模块,作为全局状态和资源管理的核心载体。这种设计,通过单一上下文对象贯穿整个系统生命周期,既保证了新增的那些功能中数据访问的一致性,又避免了原来程序里全局变量散落遍地的乱象。上下文中的双队列结构则实现了不同输出模式的状态隔离,达到 统一管理,分离处理 的想法。
杜绝了多全局变量导致的隐式依赖。访问入口唯一化,handle_pkt_event() 中传入 ctx,用于判断输出模式并推任务;所有线程通过ctx->field访问共享数据,epoll_thread_func, screen_output_thread 都接收 ctx 指针;
4)全面的错误统计、序号连续性检查;
1、2应该是性能优化,3是可靠性优化,4是可维护性优化。
679

被折叠的 条评论
为什么被折叠?



