11-内存与 I/O 访问

11.1 CPU 与内存、I/O

11.1.1 内存空间与 I/O 空间

架构 I/O 访问方式 典型场景
x86 / x86_64 独立的 I/O 端口空间(in/out 指令)+ MMIO PCI 设备 BAR、串口
ARM / ARM64 / RISC-V 只有 MMIO(内存映射 I/O) 所有外设寄存器
虚拟化 端口 I/O 与 MMIO 都会被 trap QEMU 设备模型

内核中的两种地址:

地址类型 类型限定符 访问函数
I/O 端口 无(unsigned long port) inb()/outb()/inl()/outl()
I/O 内存 void __iomem * readb()/readl()/writeb()/writel()
普通内存 普通指针 直接解引用

__iomem 是 sparse 检查用的注解 ,它能捕获"把 MMIO 指针当普通指针解引用"的错误。必须用 readl()/writel() 访问。

11.1.2 内存管理单元(MMU)

  • MMU 负责虚拟地址到物理地址的转换、权限检查、cache 属性。
  • 内核空间映射在 PAGE_OFFSET 之上,用户空间在低地址。
  • 驱动不能直接把物理地址当指针用 ,必须 ioremap() 或 devm_ioremap_resource()。
  • 页大小:x86_64 默认 4K,可选 CONFIG_X86_64_16K_PAGES/CONFIG_PAGE_SIZE_64KB(其他架构)。
  • 7.2.5 支持 5 级页表(CONFIG_X86_5LEVEL)和 PAGE_TABLE_ISOLATION(KPTI)。

11.2 Linux 内存管理

分配器 适用大小 物理连续 可睡眠 头文件
kmalloc/kzalloc ≤ 几 MB(KMALLOC_MAX_SIZE) 是 取决于 flag include/linux/slab.h
vmalloc/vzalloc 大块 否 是 include/linux/vmalloc.h
kmem_cache_* 固定大小对象 是 取决于 flag include/linux/slab.h
alloc_pages/__get_free_pages 整页 是 取决于 flag include/linux/gfp.h
dma_alloc_coherent DMA 缓冲 是(对设备可见) 是 include/linux/dma-mapping.h
kvmalloc 大块,优先 kmalloc 不保证 是 include/linux/slab.h
devm_* 变体 同上 同上 同上 自动释放

7.2.5 的重要变化:

  • kmem_cache_create() 新增 4 参数变体:kmem_cache_create(name, object_size, struct kmem_cache_args *args, flags),通过 _Generic() 同时兼容旧的 5 参数形式(name, size, align, flags, ctor)。新代码应使用 struct kmem_cache_args。
  • kmalloc/vmalloc 等现在都包装为 alloc_hooks(...),支持内存分配 profiling(CONFIG_MEM_ALLOC_PROFILING)。
  • kvmalloc() 成为"不确定大小"场景的推荐 API。

11.3 内存存取

11.3.1 用户空间内存动态申请

cpp 复制代码
/* 用户空间(不属于驱动,但驱动开发必须理解) */
void *malloc(size_t size);
void *calloc(size_t n, size_t size);
void *realloc(void *p, size_t size);
void free(void *p);

void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t off);
int munmap(void *addr, size_t len);

/* 内核侧对应的 mmap 实现,见 11.4.4 */

驱动与用户空间的数据交换规则:

函数 用途
copy_to_user(dst, src, n) 内核 → 用户,失败返回未拷贝字节数
copy_from_user(dst, src, n) 用户 → 内核
get_user(x, ptr) / put_user(x, ptr) 单个简单变量
strncpy_from_user() 拷贝字符串
clear_user() 清零用户缓冲
access_ok(ptr, n) 检查用户指针范围(不能替代 copy_*_user 的失败检查)

7.2.5 中 hardened usercopy(CONFIG_HARDENED_USERCOPY)会检查拷贝是否越过 slab 对象边界,是重要的安全机制。

11.3.2 内核空间内存动态申请

cpp 复制代码
/* 基本分配 */
void *kmalloc(size_t size, gfp_t flags);
void *kzalloc(size_t size, gfp_t flags);          /* 清零 */
void *kcalloc(size_t n, size_t size, gfp_t flags);
void *krealloc(const void *p, size_t new_size, gfp_t flags);
void kfree(const void *p);

/* 7.2.5 推荐:资源管理版本 */
void *devm_kmalloc(struct device *dev, size_t size, gfp_t flags);
void *devm_kzalloc(struct device *dev, size_t size, gfp_t flags);
void *devm_kcalloc(struct device *dev, size_t n, size_t size, gfp_t flags);
void *devm_krealloc(struct device *dev, void *p, size_t size, gfp_t flags);

/* 大块非连续内存 */
void *vmalloc(unsigned long size);
void *vzalloc(unsigned long size);
void vfree(const void *addr);
void *devm_kvmalloc(struct device *dev, size_t size, gfp_t flags);

/* 固定大小对象缓存 */
struct kmem_cache *kmem_cache_create(const char *name, unsigned int size,
                                     struct kmem_cache_args *args, slab_flags_t flags);
void *kmem_cache_alloc(struct kmem_cache *s, gfp_t flags);
void kmem_cache_free(struct kmem_cache *s, void *obj);
void kmem_cache_destroy(struct kmem_cache *s);

gfp 标志表(include/linux/gfp_types.h):

标志 含义 可用上下文
GFP_KERNEL 常规内核分配,可睡眠/回收 进程上下文
GFP_ATOMIC 不睡眠,可从中断分配 任何上下文
GFP_NOWAIT 不等待,快速返回 任何上下文
GFP_NOIO / GFP_NOFS 禁止触发 I/O / 文件系统回收 存储栈
__GFP_ZERO 清零(kzalloc 隐含) 组合使用
GFP_DMA / GFP_DMA32 限制在 DMA 可达区域 老式 DMA
__GFP_HIGHMEM 允许高端内存(32 位) 架构相关
__GFP_RETRY_MAYFAIL 失败前多试几次 可失败的大分配
__GFP_NOWARN 不打印失败警告 探测式分配

内存分配的选择流程:

复制代码
需要 DMA 且要求设备可见的连续物理内存?
    是 -> dma_alloc_coherent()
    否 -> 大小 <= KMALLOC_MAX_SIZE 且需要物理连续?
            是 -> kmalloc/kzalloc(优先 devm_kzalloc)
            否 -> vmalloc/kvmalloc

必须避免的错误:

  • kmalloc(GFP_KERNEL) 在中断上下文 → 睡眠警告。
  • 忘记检查返回值 → NULL 解引用。
  • kfree() 后继续使用(UAF)→ 用 KASAN 检测。
  • vmalloc 返回的内存不能用于 DMA(不保证物理连续)。
  • 分配大小要防止整数溢出:kmalloc(n * size, ...) 必须先检查 n > SIZE_MAX / size,或直接用 kcalloc()(内部有溢出检查)。

11.4 设备 I/O 端口和 I/O 内存的访问

11.4.1 Linux I/O 端口和 I/O 内存访问接口

I/O 端口(x86 专属):

cpp 复制代码
/* include/asm-generic/io.h, arch/x86/include/asm/io.h */
u8  inb(unsigned long port);
u16 inw(unsigned long port);
u32 inl(unsigned long port);
void outb(u8 val, unsigned long port);
void outw(u16 val, unsigned long port);
void outl(u32 val, unsigned long port);

/* 批量与字符串 I/O */
insb/insw/insl(port, addr, count);
outsb/outsw/outsl(port, addr, count);

I/O 内存(所有架构):

cpp 复制代码
/* 8/16/32/64 位读写 */
u8  readb(const volatile void __iomem *addr);
u16 readw(const volatile void __iomem *addr);
u32 readl(const volatile void __iomem *addr);
u64 readq(const volatile void __iomem *addr);
void writeb(u8 val, volatile void __iomem *addr);
void writew(u16 val, volatile void __iomem *addr);
void writel(u32 val, volatile void __iomem *addr);
void writeq(u64 val, volatile void __iomem *addr);

/* 带内存屏障的版本 */
readl_relaxed() / writel_relaxed()        /* 无屏障,性能更高 */
ioread32() / iowrite32()                  /* 通用别名 */

/* 批量拷贝 */
memcpy_fromio(dst, src, count);
memcpy_toio(dst, src, count);
memset_io(addr, val, count);

/* 位操作 */
setbits32(addr, mask) / clrbits32(addr, mask) / setset32(...)

关键区别:

  • readl() 自带 __iomb() 屏障 ,保证读写顺序;readl_relaxed() 不带。
  • 驱动必须在"需要顺序保证"处使用带屏障的版本,在"已知安全"处用 relaxed 版本提升性能。
  • readl()/writel() 必须在 __iomem 指针上使用,否则 sparse 会报警。

11.4.2 申请与释放设备的 I/O 端口和 I/O 内存

cpp 复制代码
/* include/linux/ioport.h */
struct resource *request_mem_region(resource_size_t start, resource_size_t len,
                                    const char *name);
void release_mem_region(resource_size_t start, resource_size_t len);

struct resource *request_region(unsigned long start, unsigned long len,
                                const char *name);   /* I/O 端口 */
void release_region(unsigned long start, unsigned long len);

/* 7.2.5 推荐:从 platform_device 取资源 */
struct resource *platform_get_resource(struct platform_device *pdev,
                                       unsigned int type, unsigned int num);
void __iomem *devm_platform_ioremap_resource(struct platform_device *pdev,
                                             unsigned int index);
void __iomem *devm_platform_ioremap_resource_byname(struct platform_device *pdev,
                                                    const char *name);
void __iomem *devm_platform_get_and_ioremap_resource(struct platform_device *pdev,
                                                     unsigned int index,
                                                     struct resource **res);

devm_platform_ioremap_resource() 是最推荐的写法 :它一次性完成 platform_get_resource() + request_mem_region() + ioremap(),卸载时自动回滚。

cpp 复制代码
static int my_probe(struct platform_device *pdev)
{
	struct my_dev *dev;
	void __iomem *base;

	base = devm_platform_ioremap_resource(pdev, 0);
	if (IS_ERR(base))
			return PTR_ERR(base);

	dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
	if (!dev)
			return -ENOMEM;

	dev->base = base;
	platform_set_drvdata(pdev, dev);
	return 0;
}

11.4.3 设备 I/O 端口和 I/O 内存访问流程

完整流程(MMIO):

复制代码
设备树/ACPI 描述 reg = <0x10000000 0x1000>
   -> platform_device 得到 struct resource
   -> platform_get_resource() / devm_platform_ioremap_resource()
   -> request_mem_region()  (防止冲突)
   -> ioremap()             (建立虚拟映射)
   -> readl()/writel()      (访问寄存器)
   -> 卸载时 iounmap()/release_mem_region()(devm 自动完成)

完整流程(I/O 端口):

复制代码
PCI 设备的 BAR 为 IORESOURCE_IO
   -> request_region()      (占用端口范围)
   -> inb()/outb()          (直接访问,无需 ioremap)
   -> release_region()

寄存器访问的三个常见模式:

cpp 复制代码
/* 1. 读-改-写:注意竞态 */
u32 val = readl(dev->base + REG_CTRL);
val |= CTRL_ENABLE;
writel(val, dev->base + REG_CTRL);

/* 2. 读-清-写(W1C 寄存器) */
writel(status, dev->base + REG_IRQ_CLEAR);

/* 3. 轮询等待硬件就绪 */
for (i = 0; i < 1000; i++) {
	if (readl(dev->base + REG_STATUS) & STATUS_READY)
			break;
	udelay(10);
}
if (i == 1000)
	return -ETIMEDOUT;

注意 :读-改-写必须加锁(如果多个上下文会访问同一寄存器),否则会丢失更新。

11.4.4 将设备地址映射到用户空间(mmap)

驱动实现 fops->mmap:

cpp 复制代码
#include <linux/mm.h>

static int my_mmap(struct file *filp, struct vm_area_struct *vma)
{
	struct my_dev *dev = filp->private_data;
	unsigned long size = vma->vm_end - vma->vm_start;

	if (size > dev->buffer_size)
			return -EINVAL;

	vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);  /* MMIO 需非缓存 */
	vma->vm_ops = &my_vm_ops;                                  /* 可选:管理 VMA */

	if (remap_pfn_range(vma, vma->vm_start,
						virt_to_phys(dev->buffer) >> PAGE_SHIFT,
						size, vma->vm_page_prot))
			return -EAGAIN;

	return 0;
}

映射 MMIO 寄存器到用户空间:

cpp 复制代码
static int regs_mmap(struct file *filp, struct vm_area_struct *vma)
{
	struct my_dev *dev = filp->private_data;
	unsigned long size = vma->vm_end - vma->vm_start;

	if (size > dev->reg_size)
			return -EINVAL;

	vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
	return remap_pfn_range(vma, vma->vm_start,
						   (virt_to_phys((void __force *)dev->base)) >> PAGE_SHIFT,
						   size, vma->vm_page_prot);
}

7.2.5 要点:

  • 不要映射普通 RAM 给用户空间做 DMA :应该用 dma_mmap_coherent() 或 dma_mmap_attrs(),否则 cache 一致性无法保证。
  • MMIO 映射必须 pgprot_noncached() 或 pgprot_writecombine(),否则 CPU 缓存与设备状态不一致。
  • remap_pfn_range() 会检查 vma->vm_flags & VM_IO 等约束。
  • 7.2.5 的 fops->mmap 之外还有 fops->mmap_prepare(用于在 mmap 前做校验),以及 FOP_MMAP_SYNC 标志。
  • 用 VM_DONTEXPAND | VM_DONTDUMP 标记 VMA,避免被核心转储。

使用 vmf_insert_pfn() 的按需映射(7.2.5 推荐用于稀疏映射):

cpp 复制代码
static vm_fault_t my_fault(struct vm_fault *vmf)
{
	struct my_dev *dev = vmf->vma->vm_private_data;
	unsigned long pfn = ...;

	return vmf_insert_pfn(vmf->vma, vmf->address, pfn);
}

static const struct vm_operations_struct my_vm_ops = {
	.fault = my_fault,
};

11.5 I/O 内存静态映射

4.0 时代的做法 :在板级文件里用 iotable_init()(ARM)或 map_desc 表静态建立映射。

7.2.5 的做法 :静态映射基本消失。原因是:

  • ARM32 板级文件(arch/arm/mach-*/)大部分已删除;
  • 设备树 + ioremap() 成为唯一主路径;
  • 少数 SoC 用 CONFIG_ARCH_MMAP 类的早期映射仅用于 debug(见 11.5.1)。

早期映射(early ioremap):

cpp 复制代码
/* 在 early_initcall 之前就能访问的映射 */
void __iomem *early_ioremap(resource_size_t phys_addr, unsigned long size);
void early_iounmap(void __iomem *addr, unsigned long size);

用于 earlycon、earlyprintk 等启动早期调试(见第 20 章 DEBUG_LL/EARLY_PRINTK)。

给驱动开发者的结论 :不要试图复现静态映射 ,用设备树 + devm_platform_ioremap_resource() 即可。

11.6 DMA

11.6.1 DMA 与 Cache 一致性

DMA 的核心问题是:CPU 通过 cache 访问内存,设备直接访问物理内存,二者可能不一致。

场景 问题 解决方案
设备读 CPU 刚写的数据 CPU 数据还在 cache 里,设备读到旧值 dma_map_single(..., DMA_TO_DEVICE) 会 flush cache
CPU 读设备刚写的数据 CPU cache 命中旧值 dma_unmap_single(..., DMA_FROM_DEVICE) 会 invalidate cache
双向访问 两个方向都可能不一致 用 DMA_BIDIRECTIONAL
一致性要求高、数据量小 频繁 flush 代价大 dma_alloc_coherent() 分配非缓存内存

DMA 方向枚举(include/linux/dma-direction.h):

方向 含义
DMA_BIDIRECTIONAL 双向
DMA_TO_DEVICE 内存 → 设备
DMA_FROM_DEVICE 设备 → 内存
DMA_NONE 仅用于调试

为什么现代系统更简单:

  • CONFIG_IOMMU_SUPPORT + IOMMU 提供地址翻译与隔离,DMA 地址与物理地址解耦。
  • CONFIG_DMA_API_DEBUG 可在运行时检查映射/解映射配对、方向错误、双重释放。
  • CONFIG_DMA_CMA 提供连续内存区。

11.6.2 Linux 下的 DMA 编程

一致性 DMA(推荐用于设备与 CPU 都要频繁访问的小缓冲)
cpp 复制代码
#include <linux/dma-mapping.h>

struct my_dev *dev;

/* 分配:返回 CPU 可见虚拟地址 + 设备可见 DMA 地址 */
dma_addr_t dma_handle;
void *cpu_addr;
size_t size = 4096;

cpu_addr = dma_alloc_coherent(dev->dev, size, &dma_handle, GFP_KERNEL);
if (!cpu_addr)
	return -ENOMEM;

/* 资源管理版本 */
cpu_addr = dmam_alloc_coherent(dev->dev, size, &dma_handle, GFP_KERNEL);

/* 使用:CPU 直接读写 cpu_addr,设备用 dma_handle */
memset(cpu_addr, 0, size);
writel(dma_handle, dev->base + REG_DMA_ADDR);

/* 释放 */
dma_free_coherent(dev->dev, size, cpu_addr, dma_handle);
流式 DMA(用于大块、方向明确的传输)
cpp 复制代码
dma_addr_t dma_addr;

/* 映射 */
dma_addr = dma_map_single(dev->dev, buf, size, DMA_TO_DEVICE);
if (dma_mapping_error(dev->dev, dma_addr))
        return -ENOMEM;

/* 启动设备传输 */
writel(dma_addr, dev->base + REG_SRC);
writel(size, dev->base + REG_LEN);
writel(DMA_START, dev->base + REG_CTRL);

/* 传输完成后解除映射(会同步 cache) */
dma_unmap_single(dev->dev, dma_addr, size, DMA_TO_DEVICE);
分散/聚集(scatter-gather)DMA
cpp 复制代码
#include <linux/scatterlist.h>

struct sg_table sgt;
struct scatterlist *sg;
int nents, i;

if (sg_alloc_table(&sgt, nr_pages, GFP_KERNEL))
	return -ENOMEM;

sg_set_buf(sgt.sgl, buf, len);          /* 或 sg_set_page() */

/* 7.2.5 推荐:dma_map_sgtable() 会自动设置 nents */
if (dma_map_sgtable(dev->dev, &sgt, DMA_TO_DEVICE, 0)) {
	sg_free_table(&sgt);
	return -ENOMEM;
}

/* 遍历映射后的段 */
for_each_sgtable_dma_sg(&sgt, sg, i) {
	writel(sg_dma_address(sg), dev->base + REG_DESC_ADDR);
	writel(sg_dma_len(sg), dev->base + REG_DESC_LEN);
}

dma_unmap_sgtable(dev->dev, &sgt, DMA_TO_DEVICE, 0);
sg_free_table(&sgt);

7.2.5 的关键变化:

  • dma_map_sg() 已不推荐直接使用,改用 dma_map_sgtable() :后者会返回错误码并自动设置 nents,避免了旧接口"返回 0 表示失败"的歧义。
  • dma_map_sgtable() 的返回值为 0 表示成功,负数表示失败。
  • 新增 for_each_sgtable_dma_sg() / for_each_sgtable_sg() 等迭代器。
  • dma_set_mask_and_coherent(dev, mask) 是设置 DMA 掩码的推荐方式。
  • dma_mapping_error() 必须检查,否则在 IOMMU 开启时可能拿到错误地址。
设置 DMA 掩码
cpp 复制代码
/* 设备支持 64 位寻址 */
if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64))) {
	/* 回退到 32 位 */
	if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32)))
			return -EIO;
}

必须显式设置 :默认掩码可能过小,导致 DMA 分配失败或静默降级。7.2.5 中不调用 dma_set_mask*() 而使用 64 位 DMA 会触发警告。

DMA 编程检查清单
检查项 说明
分配时是否检查返回值 dma_alloc_coherent 返回 NULL,dma_map_single 要 dma_mapping_error()
方向是否正确 写反会导致 cache 不同步
映射/解映射是否配对 CONFIG_DMA_API_DEBUG 会报错
是否在原子上下文用 GFP_KERNEL 会睡眠警告
是否设置了 DMA 掩码 大地址设备会失败
vmalloc 内存是否用于 DMA 不可以(非物理连续)
是否在映射期间 kfree() 严重 UAF
是否处理了 IOMMU 映射失败 必须检查错误

11.6.3 QEMU 中的 DMA 实验

QEMU 中可用的 DMA 实验路径:

  1. CONFIG_DMA_API_DEBUG=y + 故意写错方向 :观察 dmesg 中的 DMA API debug 报告。
  2. virtio 设备 :virtio-blk/virtio-net 内部使用 DMA,可用 -trace 观察。
  3. 自定义 platform 驱动 + dma_alloc_coherent:在 QEMU 中验证分配与释放配对。
  4. CONFIG_DMA_CMA :在 menuconfig 中配置 CMA 区域,观察 /proc/meminfo 中的 CmaTotal。
bash 复制代码
# QEMU 启动参数:给 DMA 实验留内存
qemu-system-x86_64 -m 2G ... 
# guest 中
cat /proc/meminfo | grep -i cma
cat /sys/kernel/debug/dma-api/             # 需要 CONFIG_DMA_API_DEBUG
dmesg | grep -i dma

11.7 常见错误

错误 现象 修正
直接解引用物理地址 oops 或访问错误地址 ioremap() 后访问
用普通指针访问 __iomem sparse 警告,可能优化掉 用 readl()/writel()
MMIO 映射忘了 pgprot_noncached() 读到陈旧数据 加 noncached 属性
kmalloc 大小溢出 堆破坏 用 kcalloc() 或先检查
中断上下文 GFP_KERNEL 睡眠警告 用 GFP_ATOMIC
dma_map_single 未检查 dma_mapping_error 静默失败 必须检查
DMA 方向写反 数据损坏 对照设备文档
vmalloc 内存用于 DMA DMA 失败/数据错乱 用 dma_alloc_coherent
忘记 dma_set_mask_and_coherent 大地址分配失败 probe 中显式设置
mmap 直接映射普通 RAM 做 DMA cache 不一致 用 dma_mmap_coherent
kfree 后继续使用 UAF 用 KASAN 检测

11.8 本章实验清单

实验 目的 关键观察
devm_platform_ioremap_resource 映射 学习 MMIO 映射流程 readl 能读到寄存器值
自定义 mmap 驱动 学习用户空间映射 用户程序可直接读写映射区
MMIO mmap(noncached) 学习缓存属性 用户读写寄存器实时生效
dma_alloc_coherent + DMA_API_DEBUG 学习一致性 DMA debugfs 中无错误报告
故意写错 DMA 方向 观察调试机制 dmesg 报 DMA API 错误
sg_table 映射 学习分散聚集 DMA for_each_sgtable_dma_sg 遍历正确
内存泄漏检测 学习 kmemleak /sys/kernel/debug/kmemleak 为空

11.9 总结

  1. I/O 端口用 inb/outb,I/O 内存用 readl/writel 且必须带 __iomem。
  2. 现代驱动用 devm_platform_ioremap_resource() 一站式完成资源申请与映射。
  3. 内核内存分配按"是否 DMA、是否需物理连续、是否可睡眠"三维度选择 API。
  4. kmem_cache_create() 在 7.2.5 中新增了 struct kmem_cache_args 形式的 4 参数接口。
  5. DMA 的核心是 cache 一致性;一致性 DMA 用 dma_alloc_coherent,流式 DMA 用 dma_map_single/dma_map_sgtable。
  6. dma_map_sgtable() 取代了 dma_map_sg(),dma_mapping_error() 必须检查。
  7. QEMU + CONFIG_DMA_API_DEBUG + CONFIG_DMA_CMA 可以完整复现原书的 DMA 实验,无需真实硬件。
相关推荐
Snasph4 小时前
14-Linux 网络设备驱动
linux 驱动学习
Snasph5 小时前
18-ARM Linux 设备树
linux 驱动学习
Snasph5 小时前
20-Linux 芯片级移植及底层驱动
linux 驱动学习
Snasph6 小时前
12- Linux 设备驱动的软件架构思想
linux 驱动学习
Snasph7 小时前
23-从 Linux 4.0 到 7.2.5 的迁移
linux 驱动学习
Snasph11 天前
01-设备驱动概述与开发环境构建
linux 驱动学习
Snasph11 天前
05-Linux 文件系统与设备文件
linux 驱动学习
Snasph11 天前
04-Linux 内核模块
linux 驱动学习