驱动性能优化与安全加固:从锁竞争到漏洞防护的实战指南
驱动跑起来了不等于跑得好——自旋锁争用把CPU耗在空转上,copy_to_user每次4字节搬数据,中断处理占着CPU不让出,DMA缓冲区没做边界检查直接被恶意设备改掉内核数据,性能和安全是驱动开发的两条腿,缺一条都站不稳,基于Linux 6.8内核,从性能优化的测量方法论讲起,过锁竞争、数据传输、中断延迟、内存管理四大优化方向,再从安全威胁模型出发,覆盖输入验证、权限控制、DMA安全、漏洞防护,最后给工具对比表和速查表。
一、性能优化核心原则
先测量再优化
"感觉这里慢是最差的优化理由。内核性能问题往往跟直觉相反——你觉得瓶颈在中断处理,实际瓶颈在锁竞争;你觉得该换零拷贝,实际瓶颈在内存分配。不测量就优化,大概率白费功夫还引入新bug。
优化流程
三大分析工具
| 工具 | 定位 | 典型场景 | 开销 |
|---|---|---|---|
| perf | 采样分析,找热点函数 | 哪个函数占CPU最多 | 低(采样率可调) |
| ftrace | 函数追踪,看调用链和耗时 | 函数被谁调的、耗时多少 | 中(需启用tracer |
| eBPF | 动态注入追踪点,几乎零开销 | 实时监控内核事件 | 极低(JIT编译) |
二、锁竞争优化
问题:自旋锁持锁时间过长
自旋锁的设计前提是临界区极短——几条指令的事。但实际代码里经常看到临界区塞了一堆操作:打印日志、分配内存、甚至调用可能睡眠的函数。持锁时间一长,其他CPU就在那空转,性能直接崩。
优化1:缩小临界区
只锁真正需要保护的数据,把不依赖锁的操作移到外面:
// 优化前:临界区太大
spin_lock(&drv->lock);
data = drv->buffer[idx]; // 读数据
process_data(data); // 处理数据(不需要锁)
drv->count++; // 更新计数
spin_unlock(&drv->lock);
// 优化后:只锁共享数据
spin_lock(&drv->lock);
data = drv->buffer[idx];
drv->count++;
spin_unlock(&drv->lock);
process_data(data); // 移到锁外```
### 优化2:读写锁替代自旋。
读多写少的场景,用读写锁让多个读者并行:
```c
// 自旋锁:读者也互斥
spin_lock(&lock);
val = shared_data; // 多个读者也要排队
spin_unlock(&lock);
// 读写锁:读者并发
rwlock_t rwlock;
DEFINE_RWLOCK(rwlock);
// 读端:多个读者可并行
read_lock(&rwlock);
val = shared_data;
read_unlock(&rwlock);
// 写端:独占
write_lock(&rwlock);
shared_data = new_val;
write_unlock(&rwlock);
优化3:per-cpu变量避免。
每个CPU一份副本,彻底消除锁竞争:
// 定义per-cpu变量
static DEFINE_PER_CPU(struct stats, cpu_stats);
// 更新:不需要锁,操作本CPU副本
void update_stats(int delta)
{
// 禁止抢占,确保不会被迁移到其他CPU
preempt_disable();
this_cpu_inc(cpu_stats.count);
this_cpu_add(cpu_stats.total, delta);
preempt_enable();
}
// 汇总:遍历所有CPU
u64 get_total_count(void)
{
u64 total = 0;
int cpu;
for_each_possible_cpu(cpu)
total += per_cpu(cpu_stats.count, cpu);
return total;
}
优化4:RCU替代读写。
读端零开销,写端延迟回收:
// RCU保护的数据结构struct config {
int mode;
int threshold;
struct rcu_head rcu;
};
static struct config __rcu *cur_config;
// 读端:无锁,极快
int get_mode(void)
{
struct config *cfg;
int mode;
rcu_read_lock();
cfg = rcu_dereference(cur_config);
mode = cfg->mode; // 读端完全不加载 rcu_read_unlock();
return mode;
}
// 写端:分配新结构,原子替换,延迟释放旧结构
void set_mode(int new_mode)
{
struct config *old, *new_cfg;
new_cfg = kmalloc(sizeof(*new_cfg), GFP_KERNEL);
*new_cfg = *rcu_dereference(cur_config);
new_cfg->mode = new_mode;
old = rcu_replace_pointer(cur_config, new_cfg, true);
synchronize_rcu(); // 等待所有读者退。 kfree(old);
}
四种优化对比
| 优化方式 | 适用场景 | 读开销 | 写开销 | 实现复杂度 |
|---|---|---|---|---|
| 缩小临界区 | 所有锁场景 | 不变 | 不变 | |
| 读写锁 | 读多写少 | 低(并行读) | ||
| per-cpu | 统计计数/每CPU独立 | 无锁 | 无锁 | |
| RCU | 读极多写极少 | 无锁 | 高(延迟回收) |
三、数据传输优先
零拷贝:DMA-BUF/splice/sendfile
传统数据路径:内核缓冲区 。CPU拷贝到用户空。。CPU拷贝回内核(或另一个设备)。零拷贝的目标是让数据不经过CPU搬运。
DMA-BUF:跨设备共享缓冲区,V4L2采集→GPU处理→DRM显示全程零拷贝:
// 导出端:V4L2驱动导出DMA-BUF fd
int v4l2_export_buf(struct file *file, void *priv,
struct v4l2_exportbuffer *exp)
{
// vb2核心层完成导。 return vb2_core_expbuf(&q->vb2_buf,
exp->dmabuf_fd, exp->type,
exp->index, exp->plane,
exp->flags, exp->mem_offset);
}
// 导入端:GPU/DRM通过fd导入
struct dma_buf *dmabuf = dma_buf_get(fd);
struct dma_buf_attachment *attach;
attach = dma_buf_attach(dmabuf, dev);
sgt = dma_buf_map_attachment(attach, DMA_BIDIRECTIONAL);
// 拿到scatterlist,直接用DMA访问同一块物理内存```
**splice/sendfile**:文件到socket的零拷贝,内核内部直接搬运页缓存。
```c
// sendfile:文件→socket零拷贝
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
// splice:任意两个fd之间零拷贝
ssize_t splice(int fd_in, off_t *off_in,
int fd_out, off_t *off_out,
size_t len, unsigned int flags);
减少copy_to_user/copy_from_user调用
每次copy_to_user/copy_from_user都有用户空间访问检查和可能的缺页中断。批量传输比多次小块传输高效得多。
// 差:每次拷贝4字节
for (i = 0; i < count; i++) {
if (copy_to_user(&user_buf[i], &kern_buf[i], 4))
return -EFAULT;
}
// 好:一次拷贝全部
if (copy_to_user(user_buf, kern_buf, count * 4))
return -EFAULT;
scatter-gather DMA
连续的虚拟地址不需要连续的物理页面,scatter-gather DMA让硬件直接遍历散列表,省掉大块连续内存分配:
// 分配scatter-gather列表
int nents = dma_map_sg(dev, sg_list, nents, DMA_TO_DEVICE);
// nents是硬件可用的DMA映射段数,可能少于原始段。// 因为相邻物理页可能被合并
// 遍历映射后的段
for_each_sg(sg_list, sg, nents, i) {
dma_addr_t addr = sg_dma_address(sg);
size_t len = sg_dma_len(sg);
// 把addr/len填入硬件描述。}
四、中断与延迟优化
线程化中断:request_threaded_irq
硬中断上下文不能睡眠,耗时操作必须延后处理。request_threaded_irq把中断处理拆成上半部(硬中断)和下半部(内核线程):
// 上半部:只做最紧急的硬件应答
static irqreturn_t my_hardirq(int irq, void *dev_id)
{
struct my_dev *dev = dev_id;
u32 status = readl(dev->regs + IRQ_STATUS);
if (!(status & IRQ_PENDING))
return IRQ_NONE;
writel(IRQ_ACK, dev->regs + IRQ_CLEAR); // 应答硬件
dev->irq_status = status;
return IRQ_WAKE_THREAD; // 唤醒线程化下半部
}
// 下半部:可以睡眠,做耗时处理
static irqreturn_t my_thread_fn(int irq, void *dev_id)
{
struct my_dev *dev = dev_id;
// 可以睡眠:互斥锁、内存分配、IO操作
mutex_lock(&dev->mtx);
process_interrupt(dev);
mutex_unlock(&dev->mtx);
return IRQ_HANDLED;
}
// 注册线程化中。int ret = request_threaded_irq(irq, my_hardirq, my_thread_fn,
IRQF_ONESHOT, "my_dev", dev);
IRQF_ONESHOT保证线程化下半部执行完之前不会再次触发该中断,避免中断风暴。
NAPI:网络驱动的中断+轮询混合
高负载下网卡每个包都触发中断,CPU光处理中断就忙不过来。NAPI(New API)的做法:第一个包触发中断,之后切换到轮询模式批量收包,包少了再切回中断:
// NAPI轮询函数
static int my_napi_poll(struct napi_struct *napi, int budget)
{
struct my_dev *dev = container_of(napi, struct my_dev, napi);
int rx_count = 0;
while (rx_count < budget) {
struct sk_buff *skb = receive_packet(dev);
if (!skb)
break;
netif_receive_skb(skb);
rx_count++;
}
if (rx_count < budget) {
// 包收完了,切回中断模块 napi_complete_done(napi, rx_count);
enable_interrupt(dev); // 重新使能中断
}
return rx_count;
}
// 中断处理:调度NAPI轮询
static irqreturn_t my_net_irq(int irq, void *dev_id)
{
struct my_dev *dev = dev_id;
disable_interrupt(dev); // 关中断,切轮。 napi_schedule(&dev->napi); // 调度轮询
return IRQ_HANDLED;
}
// 初始化NAPI
netif_napi_add(dev->netdev, &dev->napi, my_napi_poll);
中断合并(coalescing)
网卡和NVMe控制器都支持中断合并:攒够N个请求或等够T微秒再触发一次中断,减少中断频率。
// ethtool配置网卡中断合并
// ethtool -C eth0 rx-usecs 50 rx-frames 64
// 50微秒。4帧,哪个先到触发中断
// NVMe设置中断合并
struct nvme_completion_queue {
u16 intr_vector;
u16 intr_coal_thresh; // 完成多少个命令后触发中断
};
IRQ亲和性绑定CPU
把特定中断绑定到指定CPU,减少跨CPU缓存失效和锁竞争。
# 查看当前中断亲和。cat /proc/irq/32/smp_affinity
# 把IRQ 32绑定到CPU 0。(位掩码b0101 = 5。echo 5 > /proc/irq/32/smp_affinity
# 驱动中设。irq_set_affinity_hint(irq, cpumask_of(cpu));
五、内存管理优先
内存。mempool)避免分配失败
GFP_KERNEL在内存紧张时可能睡眠甚至失败。mempool预分配一批对象作为紧急储备,保证关键路径不失败:
// 创建内存池:最少保护个对。mempool_t *pool = mempool_create(8, mempool_alloc_slab,
mempool_free_slab, my_cachep);
// 从内存池分配:不会失。void *obj = mempool_alloc(pool, GFP_KERNEL);
if (!obj) // 实际上不会走到这。 return -ENOMEM;
// 归还
mempool_free(obj, pool);
// 销。mempool_destroy(pool);
slab缓存预分支
频繁分配/释放固定大小对象,用slab缓存避免反复调用底层分配器:
// 创建专用slab缓存
struct kmem_cache *my_cache = kmem_cache_create("my_driver_obj",
sizeof(struct my_obj), 0,
SLAB_HWCACHE_ALIGN, NULL);
// 从缓存分支struct my_obj *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
// 归还缓存
kmem_cache_free(my_cache, obj);
// 销毁缓。kmem_cache_destroy(my_cache);
SLAB_HWCACHE_ALIGN让对象对齐到缓存行,避免false sharing。
减少内存碎片
- 用
get_zeroed_page()/__get_free_pages()分配2的幂次页面,避免碎片 - 用
alloc_pages(GFP_KERNEL | __GFP_COMP)分配复合。- 用kmalloc分配小对象而不是整。- 用mempool和slab缓存保持对象复用
高阶页分支
单页分配器最。KB(默认配置),大块连续内存需要高阶分配:
// 分配4个连续页。6KB。struct page *pages = alloc_pages(GFP_KERNEL, 2); // 2^2 = 4。if (!pages)
return -ENOMEM;
void *vaddr = page_address(pages);
// 使用...
__free_pages(pages, 2); // 释放,注意order要匹```
高阶分配在系统运行久了之后容易失败。如果必须用大块连续内存,在模块初始化时尽早分配,或者考虑用scatter-gather代替连续内存。
## 六、性能分析工具
### perf:采样分支
```bash
# 记录性能数据
perf record -g ./my_test # -g记录调用。perf record -e cpu-clock -g -p 1234 # 追踪指定进程
# 分析热点
perf report # 交互式报。perf report --stdio # 文本输出
# 实时热点
perf top # 类似top,但按函数采```
### ftrace:函数追。
```bash
# 追踪指定函数
echo my_driver_func > /sys/kernel/debug/tracing/set_ftrace_filter
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
# ... 运行测试 ...
cat /sys/kernel/debug/tracing/trace
# 函数耗时统计
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo my_driver_func > /sys/kernel/debug/tracing/set_graph_function
# 函数耗时直方图(6.8内核。echo 'hist:keys=func,latency:sort=latency' > \
/sys/kernel/debug/tracing/events/my_driver/trigger
eBPF:bcc工具。
# 安装bcc工具
# apt install bpfcc-tools
# 追踪内核函数耗时
funclatency-bpfcc -p 1234 do_sys_open
# 追踪块设备IO延迟
biosnoop-bpfcc
# 追踪内存分配
memleak-bpfcc -p 1234
# 追踪锁竞。offcputime-bpfcc -p 1234
/proc接口
# 中断统计
cat /proc/interrupts # 硬中断计数cat /proc/softirqs # 软中断计数
# 查看哪个CPU处理哪个中断
watch -n1 "cat /proc/interrupts | grep my_dev"
工具对比
| 工具 | 优势 | 局限 | 适用场景 |
|---|---|---|---|
| perf | 采样开销低,支持硬件事件 | 需要调试符号 | 找热点函数 |
| ftrace | 内核原生,无需额外安装 | 只能追踪内核函数 | 函数调用链分支 |
| eBPF/bcc | 几乎零开销,可编程 | 学习曲线陡 | 实时监控、自定义追踪 |
| /proc | 零开销,最简单 | 只有汇总数据 | 快速查看当前内存状态 |
七、驱动安全威胁模块
攻击。
驱动运行在内核态,一个漏洞就是整个系统的灾难。驱动的攻击面比想象的大。
常见漏洞类型
| 漏洞类型 | 典型成因 | 危害等级 | 内核案例 |
|---|---|---|---|
| 缓冲区越界 | 未校验数组下标 | 严重 | CVE-2023-3106 |
| 整数溢出 | size计算溢出后分配小缓冲 | 严重 | CVE-2022-4280 |
| 竞态条 | 多线程访问共享数据未加锁 | CVE-2023-0386 | |
| 信息泄露 | 未初始化栈变量拷贝到用户空间 | CVE-2023-0590 | |
| 权限提升 | 缺少capable()检 | 严重 | CVE-2023-0179 |
八、输入验证与边界检。
copy_from_user后必须验。
用户空间可以传入任何值,不验证就是给攻击者送钥匙:
// 危险:直接信任用户数。struct my_config cfg;
if (copy_from_user(&cfg, user_ptr, sizeof(cfg)))
return -EFAULT;
buffer[cfg.index] = cfg.value; // cfg.index可能越界。
// 安全:验证所有字。struct my_config cfg;
if (copy_from_user(&cfg, user_ptr, sizeof(cfg)))
return -EFAULT;
if (cfg.index >= ARRAY_SIZE(buffer)) // 边界检。 return -EINVAL;
if (cfg.value > MAX_VALUE) // 范围检。 return -ERANGE;
buffer[cfg.index] = cfg.value;
整数溢出检。
分配缓冲区前必须检查size计算是否溢出。
// 危险:整数溢。int count;
if (copy_from_user(&count, user_ptr, sizeof(count)))
return -EFAULT;
// count = 0x40000001, count * 4 = 0x432位溢出)
buf = kmalloc(count * sizeof(u32), GFP_KERNEL); // 分配4字节
// 但后续写入count个u32,越界!
// 安全:用kmalloc_array检查溢buf = kmalloc_array(count, sizeof(u32), GFP_KERNEL);
if (!buf)
return -ENOMEM;
// kmalloc_array内部会检查count * sizeof(u32)是否溢出
安全的ioctl实现
static long my_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct my_dev *dev = file->private_data;
void __user *uptr = (void __user *)arg;
// 第一步:检查cmd合法。 if (_IOC_TYPE(cmd) != MY_IOC_MAGIC)
return -ENOTTY;
if (_IOC_NR(cmd) > MY_IOC_MAXNR)
return -ENOTTY;
switch (cmd) {
case MY_IOCTL_SET_CONFIG: {
struct my_config cfg;
// 第二步:检查用户空间指针可访问
if (!access_ok(uptr, sizeof(cfg)))
return -EFAULT;
// 第三步:拷贝并验。 if (copy_from_user(&cfg, uptr, sizeof(cfg)))
return -EFAULT;
// 第四步:边界检。 if (cfg.index >= MAX_INDEX)
return -EINVAL;
if (cfg.size == 0 || cfg.size > MAX_SIZE)
return -EINVAL;
// 第五步:权限检。 if (cfg.privileged && !capable(CAP_SYS_ADMIN))
return -EPERM;
// 第六步:加锁操作共享数据
mutex_lock(&dev->mtx);
dev->config = cfg;
mutex_unlock(&dev->mtx);
break;
}
default:
return -ENOTTY;
}
return 0;
}
九、权限控制
CAP_SYS_ADMIN检。
不是谁都能操作你的设备,capable()是第一道防线:
static long my_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
// 危险操作必须检查权限 switch (cmd) {
case MY_IOCTL_RESET:
case MY_IOCTL_FLASH_FW:
case MY_IOCTL_DEBUG:
if (!capable(CAP_SYS_ADMIN))
return -EPERM;
break;
default:
break;
}
// ...
}
capable(CAP_SYS_ADMIN)是内核最常用的权限检查,基本等价。需要root"。更细粒度的可以用CAP_NET_ADMIN、CAP_SYS_RAWIO等。
设备节点权限
控制谁能打开设备文件。
// 初始化时设置设备节点权限
static int my_dev_init(void)
{
struct device *dev;
dev = device_create(my_class, NULL, devt, NULL, "mydev");
// 默认crw-rw----,属主root,组device
// 修改权限:只允许root访问
dev_set_mode(dev, 0600); // crw-------
// 或者用devtmpfs自动设置
// 在class_create时指。}
也可以通过udev规则动态调整:
# /etc/udev/rules.d/99-mydev.rules
KERNEL=="mydev", MODE="0660", GROUP="device"
seccomp过滤
seccomp限制用户空间程序能调用哪些系统调用,即使被攻破也无法调用ioctl。
// 用户空间设置seccomp过滤
#include <seccomp.h>
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);
// 只允许read/write/exit
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
seccomp_load(ctx);
SELinux策略
SELinux在强制访问控制层面限制进程对设备节点的访问:
# SELinux策略示例:只允许特定域访问mydev
allow my_app_domain mydev_device_t:chr_file { read write ioctl };
十、DMA安全
IOMMU/SMMU:设备DMA地址隔离
没有IOMMU的系统中,DMA设备可以访问任意物理地址——恶意设备或被攻破的固件能直接读写内核内存。IOMMU(Intel。SMMU(ARM)在设备和物理内存之间加了一层地址翻译,把DMA限制在授权范围内存
// 驱动中使用IOMMU。.8内核自动处理。// dma_alloc_coherent内部会通过IOMMU映射
void *vaddr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
// dma_handle是设备看到的IOVA(IO Virtual Address。// IOMMU把IOVA翻译成物理地址,设备只能访问这块内存
// 启用IOMMU:内核启动参数// intel_iommu=on (x86)
// arm-smmu.enable=1 (ARM)
dma_set_mask_and_coherent
告诉内核设备支持多宽的DMA地址。
// 设置DMA地址掩码
int ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64));
if (ret) {
// 64位DMA不支持,尝试32。 ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));
if (ret)
return ret; // 设备不支持DMA
}
DMA缓冲区边界检。
设备可能写入超出分配范围的数据,驱动必须做边界防护:
// 分配DMA缓冲区时多留一些guard空间
#define BUF_SIZE 4096
#define GUARD_SIZE 256
void *vaddr = dma_alloc_coherent(dev, BUF_SIZE + GUARD_SIZE,
&dma_handle, GFP_KERNEL);
// 只告诉设备BUF_SIZE的大。// 设备写入超过BUF_SIZE的部分落入guard区域
// 驱动检查guard区域是否被改写,检测越界写。if (memcmp(vaddr + BUF_SIZE, guard_pattern, GUARD_SIZE) != 0) {
dev_err(dev, "DMA buffer overflow detected!\n");
// 触发安全告警
}
防止DMA攻击
| 攻击方式 | 防御措施 |
|---|---|
| 恶意设备读取任意内存 | 启用IOMMU/SMMU,限制DMA地址范围 |
| DMA写入覆盖内核数据 | IOMMU隔离 + 缓冲区边界检查 |
| DMA重放攻击 | 使用IOMMU的无效化队列(Invalidation Queue |
| Thunderbolt/USB4 DMA攻击 | 内核CONFIG_IOMMU_DEFAULT_PASSTHROUGH=n |
十一、漏洞防护最佳实。
栈保护:CONFIG_STACKPROTECTOR
编译器在函数栈帧中插入canary值,函数返回时检查canary是否被覆盖:
# 内核配置
CONFIG_STACKPROTECTOR=y
CONFIG_STACKPROTECTOR_STRONG=y # 更强的保护,更多函数插入canary
# 驱动编译时自动启用,无需额外代码
# 如果栈溢出覆盖了canary,内核打印:
# stack-protector: Kernel stack is corrupted in: my_func+0x5c/0x60
堆保护:CONFIG_SLAB_FREELIST_HARDENED
硬化slab分配器的空闲链表,防止堆溢出篡改空闲对象指针。
# 内核配置
CONFIG_SLAB_FREELIST_HARDENED=y
CONFIG_SLAB_FREELIST_RANDOM=y # 随机化空闲链表顺。CONFIG_HARDENED_USERCOPY=y # 加强copy_to/from_user检```
### 用户空间访问:access_ok + copy_from_user
两步验证,缺一不可。
```c
// 第一步:检查用户空间地址范围是否合法
if (!access_ok(uptr, size))
return -EFAULT;
// 第二步:安全拷贝(内部处理缺页等异常。if (copy_from_user(kbuf, uptr, size))
return -EFAULT;
access_ok做快速范围检查,copy_from_user做实际拷贝并处理缺页。.8内核中access_ok的语义是检查[uptr, uptr+size)是否在用户空间地址范围内存
竞态条件:正确的锁使用
// 典型竞态:检查与使用分离(TOCTOU)
// 危险
if (dev->running) { // 检。 // 另一个线程可能在这里把dev->running设为0并释放资。 writel(data, dev->regs); // 使用——可能访问已释放的内存!
}
// 安全:加锁保护mutex_lock(&dev->mtx);
if (dev->running) {
writel(data, dev->regs);
}
mutex_unlock(&dev->mtx);
安全审计清单
| 检查项 | 审计要点 | 通过标准 |
|---|---|---|
| ioctl参数验证 | 所有字段都做了边界检查 | 无未验证的用户输入 |
| 整数溢出 | size计算用kmalloc_array/sizes_mul | 无乘法溢出风险 |
| 权限检查 | 危险操作有capable() | 无未授权操作 |
| 锁保护 | 共享数据访问在锁内 | 无竞态条件 |
| 用户空间访问 | access_ok + copy_from_user | 无直接解引用用户指针 |
| DMA安全 | 启用IOMMU,缓冲区有边界检查 | 无越界DMA访问 |
| 信息泄露 | 拷贝到用户空间前清零 | 无未初始化数据泄露 |
| 固件验证 | 加载固件前校验签名 | 无未签名固件加载 |
十二、常见问题
*Q1:perf采样不到内核函数
需要安装linux-perf包和调试符号。Debian/Ubuntu:apt install linux-perf-6.8 dbgsym。确认/proc/sys/kernel/kptr_restrict设为0,否则非root看不到内核符号地址。
*Q2:ioctl被非root用户滥用
在ioctl处理函数开头加capable(CAP_SYS_ADMIN)检查。对于部分操作需要权限、部分不需要的场景,按cmd分支检查,不要一刀切。
*Q3:DMA缓冲区被恶意设备篡改
启用IOMMU/SMMU,内核启动参数加intel_iommu=on(x86)或确认arm-smmu已加载(ARM)。IOMMU把DMA地址空间限制在驱动显式映射的范围内,设备无法越界访问题
*Q4:per-cpu变量在进程上下文和中断上下文都能用吗
进程上下文用this_cpu_inc()前要preempt_disable(),防止被抢占后迁移到其他CPU。中断上下文天然不会被抢占,直接用就行。.8内核推荐用this_cpu_inc()代替__this_cpu_inc(),前者有抢占检查看
*Q5:RCU回调在什么上下文执行
call_rcu的回调在软中断上下文执行,不能睡眠。如果回调里需要睡眠,用call_rcu_sched。.8内核的rcu_barrier配合工作队列。
十三、总结
速查看
| 场景 | 优化/防护手段 | 关键API |
|---|---|---|
| 锁竞争 | 缩小临界区 | 把无关操作移到锁外 |
| 锁竞 | 读写 | DEFINE_RWLOCK / read_lock / write_lock |
| 锁竞 | per-cpu变量 | DEFINE_PER_CPU / this_cpu_inc |
| 锁竞 | RCU | rcu_read_lock / rcu_dereference / synchronize_rcu |
| 数据拷贝 | 零拷贝DMA-BUF | dma_buf_get / dma_buf_attach |
| 数据拷贝 | splice/sendfile | splice() / sendfile() |
| 数据拷贝 | scatter-gather | dma_map_sg / for_each_sg |
| 中断延迟 | 线程化中 | request_threaded_irq |
| 中断延迟 | NAPI | netif_napi_add / napi_schedule |
| 中断延迟 | 中断合并 | ethtool -C / NVMe coalescing |
| 中断延迟 | IRQ亲和 | irq_set_affinity_hint |
| 内存分配 | 内存 | mempool_create / mempool_alloc |
| 内存分配 | slab缓存 | kmem_cache_create / kmem_cache_alloc |
| 输入验证 | 边界检 | access_ok + copy_from_user |
| 输入验证 | 整数溢出 | kmalloc_array / size_mul |
| 权限控制 | capable检 | capable(CAP_SYS_ADMIN) |
| DMA安全 | IOMMU隔离 | dma_alloc_coherent(自动IOMMU映射) |
| 漏洞防护 | 栈保护 | CONFIG_STACKPROTECTOR_STRONG |
| 漏洞防护 | 堆保护 | CONFIG_SLAB_FREELIST_HARDENED |
| 性能分析 | perf | perf record -g / perf report |
| 性能分析 | ftrace | set_ftrace_filter / function_graph |
| 性能分析 | eBPF | funclatency-bpfcc / biosnoop-bpfcc |
本文首发于linuxros.cn,转载请注明出处。