在Linux audit 审计子系统(一)中我们分析了内核启动的初始化的实现细节与audit审计监控内核系统调用的过程,在了解了audit审计子系统内部实现细节后,本文紧接着来分析一下audit子系统内部各个线程之间如如何通过唤醒调度的。
1. audit审计监控内核系统调用
在分析audit 子系统内部线程调用关系之前,我们先回顾一下上一章节所阐述的audit审计监控内核系统调用,如下图所示:
对各个函数的实现细节及功能分析就不再阐述过多(上一章节已有描述)。
通过上面的调用栈分析,我们可以看到,audit监控系统调用是放在系统调用的入口处理函数,仅仅是记录了当前系统调用的前四个参数,在这整个调用过程中,没有任何的阻塞处理与复杂逻辑,那么我们可以任务这里对整个操作系统性能的影响几乎为0,对于操作系统内部庞大臃肿的各个子系统及内部复杂的业务来说可以忽略不计。
2. audit子系统内部线程调用分析
audit子系统内部调度,核心就是针对两个队列的处理:
-
kauditd_wait
是一个等待队列头,用于等待kauditd任务的完成或其他事件audit_backlog_wait
也是一个等待队列头,用于等待审计日志队列中的日志被处理完毕或其他事件
首先,我们先来分析一下audit子系统内部正常情况下的处理逻辑,如下图所示:
可以看到,
audit_log_format ---> audit_log_end : wake_up_interruptible(&kauditd_wait)
---> kauditd: kauditd_send_queue ----> netlink_unicaset
----> auditd(用户空间守护进程)
这里看上去是挺正常的,貌似没有什么问题,整个CPU任务切换、任务唤醒也是符合正常流程的。可是如果考虑到了audit审计日志存在大量并发的情况下,审计日志过多的场景,这种处理逻辑是存在缺陷的,通过上一章节的源码分析中,我们知道了audit审计子系统处理审计消息时,正常逻辑是audit_buffer_alloc一块内存用于存储所记录的audit_contex审计信息,直至audit_log_end处理之后才会去释放这块内存:
那么这整个处理逻辑就是串行调度唤醒的。那在内核中又是如何把audit_backlog_wait
这个队列用起来的呢?如下代码所示:
当存在大量审计日志时,会触发 (audit_backlog_limit && (skb_queue_len(&audit_queue) > audit_backlog_limit)) 这里的异常处理,这里会把当前的任务加入audit_backlog_wait这个队列中,
同时这里会从当前的任务切出去,睡眠等待直到超时后直接删除队列中的任务(这里也就解释了当队列长度超过所配置的audit_backlog_limit后,为什么会存在一些审计日志丢失的情况) 或者等待kaudit 发送完当前队列中缓存中的日志后来唤醒audit_backlog_wait处理
通过上面的分析,我们再来看下audit子系统内部各个线程状态转换与唤醒过程,如下图所示:
主要关注一下 如下几点:
-
- add_wait_queue_exclusive(&audit_backlog_wait, &wait)
- remove_wait_queue(&audit_backlog_wait, &wait);
- wake_up_interruptible(&kauditd_wait);
- wake_up(&audit_backlog_wait);
代码比较简单,上一章节也有详细分析,这里就不再过多阐述。通过对audit子系统各个线程调度的梳理,我们可以看到,如果频繁的触发while (audit_backlog_limit &&...){...}必然会存在下面的问题:
1)频繁的任务切换会导致CPU处理性能降低;
2)kauditd负载及内存占用率极高,双核配置,实测CPU占用可高达90%;四核CPU占用可高达60%;
这样也就解释了为什么当现网环境中有大量审计日志的时候,整个操作系统会变得卡顿,导致ssh或者tty终端登录会需要高到1~2分钟的现象。
3. 后续
在本文中,主要是分析了audit子系统内部各个线程之间如如何通过唤醒调度的,也确实是发现了其中的一些问题,那么我们去修改整个内核audit审计子系统的处理调度框架肯定也是不现实的(...不多说)。那么我们又如何去解决现网中的一些问题呢?个人任务可以从如下几点入手:
1)配置适当的审计队列长度及睡眠等待时间;
2)通过对审计规则的精细化配置来减少触发audit审计子系统内部的异常处理逻辑,降低任务切换的频率;
3)可以考虑使用其它工具来分担一下audit的压力,如eBPF同样也适合于长期的系统监控等。