嵌入式Linux驱动开发指南02——内核空间基础与硬件访问

嵌入式Linux驱动开发指南02------内核空间基础与硬件访问

仓库已经开源!所有教程,主线内核移植,跑新版本imx-linux/uboot都在这里!欢迎各位大佬观摩!喜欢的话点个⭐!
仓库地址:https://github.com/Awesome-Embedded-Learning-Studio/imx-forge
静态网页:https://awesome-embedded-learning-studio.github.io/imx-forge/

从裸机到Linux的认知障碍

如果直接把裸机代码搬到 Linux 里,能不能用?

这是一个极其诱人的想法。你已经熟知了 I.MX6ULL 的每一个寄存器,知道怎么把复用功能选通、怎么把时钟门打开、怎么把 GPIO 方向设为输出。在你看来,点亮一颗 LED 只需要几行赋值代码------指针一指,数据一写,灯亮了。

但在 Linux 下,这行不通。或者说,如果不理解 Linux 眼里的世界,直接去动硬件,你得到的要么是一串刺眼的 Unable to handle kernel paging request,要么就是系统毫无反应地死锁。

这里有一个根本性的认知障碍:你在裸机时代拥有的是上帝视角,可以直接操纵物理地址;而在 Linux 里,你只是众多进程中的一个,甚至连内核自己都被 MMU(内存管理单元)挡在了物理世界之外。


第一部分:两座城池的故事

为什么要分开?

在上一章,我们站在城墙外看了一眼字符设备驱动,知道它是连接应用和硬件的"翻译官"。但在这之前,我们得先搞清楚一个问题:为什么需要这个翻译官?为什么不能让应用程序直接操作硬件?

这个问题的答案,藏在 Linux 最基本的设计哲学里------把世界分成两半

想象一下,如果所有程序都能直接访问硬盘、读取你浏览器保存的密码、或者修改其他进程的内存------这个世界会变成什么样?

答案:混乱。

任何程序都可以:

  • 窃取其他程序的隐私数据
  • 导致系统崩溃(比如随意修改内核数据结构)
  • 绕过安全限制(比如直接读取磁盘上的任何文件)

所以 Linux 建立了这套"两座城池"的制度:

  • 用户空间:普通市民生活的地方,自由但受限
  • 内核空间:政府机构所在地,特权但责任重大

两套规则

根据 ARM 官方文档,ARM32 Linux 的虚拟地址空间划分如下(标准 3GB/1GB 分割配置):

用户空间(User Space)
  • 地址范围 :0x00001000 ~ TASK_SIZE-1(通常 TASK_SIZE = 0xC0000000,即低 3GB)
  • 权限:受限(用户态)
  • 用途:进程的 text/data/heap,通过 mmap() 系统调用创建的映射
  • 限制:不能直接访问硬件、不能执行特权指令、不能访问内核内存

注意:0x00000000 ~ 0x00000fff 保留用于 CPU 向量页和空指针陷阱

内核空间(Kernel Space)

内核空间包含多个区域,从 PAGE_OFFSET(通常为 0xC0000000)开始:

区域 地址范围 用途
直接映射 RAM PAGE_OFFSET ~ high_memory-1 内核直接映射 RAM,与物理 RAM 1:1 对应
vmalloc/ioremap VMALLOC_START ~ VMALLOC_END-1 vmalloc()/ioremap() 动态映射区域
永久映射 PKMAP_BASE ~ PAGE_OFFSET-1 HIGHMEM 页的永久内核映射
模块空间 MODULES_VADDR ~ MODULES_END-1 通过 insmod 加载的内核模块
固定映射 ffc80000 ~ ffefffff fix_to_virt() 提供的固定映射

权限 :完全(内核态 Ring 0)
职责:管理系统资源、调度进程、处理中断、驱动硬件

关于 64 位系统

上述讨论仅适用于 32 位 ARM 系统

64 位系统 (如 ARM64/x86_64)使用完全不同的内存布局,称为 canonical address layout

  • 虚拟地址空间远大于 4GB(ARM64/x86_64 支持 48 位或更多虚拟地址位)
  • 用户空间和内核空间通常采用非对称布局
  • 用户空间占用较低的虚拟地址范围
  • 内核空间占用最高的虚拟地址范围
  • 中间有一段不可用的"hole"区域(canonical 地址的要求)

根据 x86_64 文档,现代 64 位系统使用 4 级或 5 级页表,支持的虚拟地址空间可达 256TB(4 级)或 128PB(5 级)。

地址空间的隔离

在驱动开发中,你必须时刻记住一个重要事实:

用户空间的地址和内核空间的地址是完全隔离的,即使数字相同,指向的也是不同的物理内存。

ARM32 内存布局示意图

根据官方文档,完整的 ARM32 内存布局如下:

复制代码
用户空间 (每个进程独立)
────────────────────────────────────────────────────
0x00000000 ─┐
0x00000fff  ├─ CPU 向量页 / 空指针陷阱
           │
0x00001000 ─┐
           ├─ 用户空间映射 (TASK_SIZE-1)
           │  text/data/heap,通过 mmap() 创建
           │
0xBFFFFFFF ─┘ (TASK_SIZE-1)

内核空间 (所有进程共享)
────────────────────────────────────────────────────
           │
0xC0000000 ─┐ (PAGE_OFFSET)
           ├─ 内核直接映射 RAM (1:1)
           │
VMALLOC_START ─┐
           ├─ vmalloc()/ioremap() 动态映射区域
           │
VMALLOC_END ─┘ (0xff800000)

           ├─ 模块空间、固定映射等
           │
0xFFFFFFFF ─┘
关键点
  1. 每个进程都有独立的用户空间(0 ~ TASK_SIZE-1)
  2. 所有进程共享同一个内核空间(PAGE_OFFSET ~ 0xFFFFFFFF)
  3. 相同的虚拟地址在不同进程中可能指向不同的物理内存
  4. ioremap() 返回的地址位于 vmalloc 区域

第二部分:系统调用------城门关卡

现在问题来了:如果用户空间的程序需要读取文件,但它无权直接访问硬盘,怎么办?

答案是:走正规程序,申请内核代劳

这个"正规程序"就是系统调用(System Call)

系统调用的工作流程

复制代码
用户程序                          内核
   ↓                              ↓
open("/dev/led", O_RDWR)
   │
   ├─→ 触发软中断 (int 0x80 / syscall)
   │     (CPU 从用户态切换到内核态)
↓
内核接管执行
   │
   ├─→ 检查权限
   ├─→ 查找 /dev/led 对应的驱动
   ├─→ 调用驱动的 open() 函数
↓
返回文件描述符给用户程序

CPU 特权级(特权环)

x86/ARM 架构提供多个特权级,Linux 只用了两个:

  • Ring 0(内核态):可以执行任何指令,访问所有内存
  • Ring 3(用户态):受限,不能执行特权指令

从用户态切换到内核态的唯一合法途径就是系统调用

常见的系统调用

你可能已经用过很多系统调用,只是没意识到:

用户态函数 系统调用 作用
open() SYS_open 打开文件
read() SYS_read 读取文件
write() SYS_write 写入文件
malloc() SYS_brk 分配内存
pthread_create() SYS_clone 创建线程

第三部分:MMU------地址翻译官

为什么不能直接用物理地址?

在裸机程序里,我们定义一个宏 #define GPIO_DR 0x0209C000,然后把 0x0209C000 当作一个地址直接赋给指针,这很自然。但在 Linux 内核里,这个 0x0209C000 是一个陌生人。

为什么?因为 MMU

停下来想一想:你在裸机时代写下的那些物理地址,在 Linux 内核启动的那一刻,已经全部失效了。CPU 看到的不再是物理地址,而是虚拟地址。

问题来了 :如果我现在想操作 GPIO1 的数据寄存器(物理地址 0x0209C000),在 Linux 下该怎么找到它?

答案是:我需要向内核申请,让内核帮我把这个物理地址"翻译"成一个我可以用的虚拟地址。

MMU 干什么活的?

MMU(Memory Management Unit,内存管理单元)是现代处理器的标配,也是 Linux 内核赖以生存的基石。

根据官方文档,MMU 主要负责:

  1. 地址翻译:将程序使用的虚拟地址(Virtual Address)转换为物理内存的真实地址(Physical Address)
  2. 内存保护:控制谁能读写哪块内存,实现进程间的隔离
虚拟地址如何转换为物理地址?

官方文档描述了地址翻译的过程:

"每个内存访问都使用虚拟地址。当 CPU 解码一条读取(或写入)系统内存的指令时,它将指令中编码的虚拟地址转换为内存控制器可以理解的物理地址。"

物理系统内存被划分为页帧(page frames 或 pages)。页大小是架构特定的,有些架构允许在多个支持的值中选择页大小。

每个物理内存页可以映射为一个或多个虚拟页。这些映射由页表(page tables)描述,页表按层次结构组织:

  • 最底层页表包含软件使用的实际页的物理地址
  • 较高层页表包含属于较低层的页的物理地址
  • 指向顶层页表的指针驻留在寄存器中

当 CPU 执行地址翻译时:

  1. 使用寄存器访问顶层页表
  2. 虚拟地址的高位用于索引顶层页表中的条目
  3. 该条目用于访问层次结构中的下一级
  4. 虚拟地址的下一级位作为该级页表的索引
  5. 虚拟地址的低位定义实际页内的偏移量
TLB:地址翻译的缓存

地址翻译需要多次内存访问,而内存访问相对于 CPU 速度来说很慢。为了避免在地址翻译上浪费宝贵的处理器周期,CPU 维护着此类翻译的缓存,称为TLB(Translation Lookaside Buffer,旁路缓冲区)。

虚拟地址 vs 物理地址

官方文档解释了虚拟内存的概念:

"虚拟内存抽象了应用程序软件的物理内存细节,允许只在物理内存中保留需要的信息(需求分页),并提供了进程间保护和受控数据共享的机制。"

在 Linux 内核启动初期,它会初始化 MMU,建立页表映射。在这之后,CPU 执行的所有指令、访问的所有数据,用的全是虚拟地址

设备寄存器访问的问题

举例说明物理设备寄存器的访问:

  • I.MX6ULLGPIO1_IO03 复用寄存器 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03物理地址0X020E0068
  • 没有 MMU 的裸机环境下,你直接往 0X020E0068 写数据,信号直接传导到硬件外设
  • 但在 Linux 下 ,MMU 已经启用。如果你直接往 0X020E0068 这个虚拟地址写数据,你要么是在访问一段内核未映射的内存(触发 Page Fault),要么是在访问一段完全不相关的系统内存

结论 :我们必须通过 ioremap() 向内核申请:把「物理地址 0X020E0068」映射到「某个可用的虚拟地址 V」上。


第四部分:ioremap------建立映射关系

银行保险箱的比喻

你可以把物理内存想象成银行保险库里的保险箱 ,编号是 0X020E0068

  • 你不能直接走进金库拿着锤子去砸那个箱子(物理隔离)
  • 你需要银行柜员(MMU )给你一个临时柜台窗口(虚拟地址
  • 当你向柜员出示证件(调用 ioremap)说你要操作 0X020E0068 号箱子时,柜员会在大厅里给你指定一个 3 号窗口
  • 以后你只要跟 3 号窗口打交道,柜员会自动把你的指令传递到金库里的那个箱子

但这个比喻有个关键的细节:保险箱是静态的,而硬件寄存器是动态的。如果你对 3 号窗口的操作被柜员记在小本本上(缓存),稍后再批量提交,那硬件反应就会迟钝。

ioremap 的作用就是告诉柜员:「别缓存,我每说一句话你都要立刻跑去金库执行一遍。」

函数原型与使用

ioremap 的定义在内核的 I/O 内存接口中。从使用角度来看,它的核心参数很直观:

c 复制代码
void __iomem *ioremap(phys_addr_t phys_addr, size_t size);

核心参数

  • phys_addr :你要映射的物理起始地址。比如 GPIO 寄存器的 0X020E0068
  • size:你要映射多大的空间。一个寄存器通常是 4 字节(32位),但为了效率,有时候我们会映射一整块寄存器区域
  • 返回值void __iomem * 类型的指针。这就是那个「虚拟地址 V」。之后我们操作 V,就是在操作 phys_addr

__iomem 标记告诉编译器和静态分析工具:这是个 I/O 内存地址,不能像普通内存那样随意操作。

实际使用示例

c 复制代码
/* 寄存器物理地址 */
#define SW_MUX_GPIO1_IO03_BASE  (0X020E0068)

/* 映射后的虚拟地址指针,__iomem 标记这是个 I/O 内存地址 */
static void __iomem *SW_MUX_GPIO1_IO03;

/* 执行映射 */
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);

if (!SW_MUX_GPIO1_IO03) {
    printk("ioremap failed\n");
    return -ENOMEM;
}

这里 size 传了 4,因为我们只操作这一个 32 位的寄存器。

回到那个类比:现在 SW_MUX_GPIO1_IO03 就是那个「3 号窗口」。以后你读写 *SW_MUX_GPIO1_IO03,其实就是在读写那个远在金库里的物理寄存器。

iounmap:有借有还

当你的驱动卸载时,必须把占用的映射释放掉,把窗口还给内核大厅。这需要用到 iounmap

c 复制代码
void iounmap(volatile void __iomem *addr);
c 复制代码
/* 卸载时释放映射 */
iounmap(SW_MUX_GPIO1_IO03);

这一步如果忘了做,不仅仅是内存泄漏的问题。你映射的是设备地址,如果不释放,内核可能误以为这块地址空间还在被占用,后续其他驱动想访问这段地址时可能会出问题。

虽然现在的系统没那么脆弱,但作为工程师,我们不做这种只借不还的事。


第五部分:I/O 内存访问函数

为什么不能用指针直接读写?

现在我们已经拿到了虚拟地址指针 SW_MUX_GPIO1_IO03,是不是可以直接用 C 语言的 *= 操作符来读写了呢?

就像这样:

c 复制代码
/* ❌ 糟糕的写法 */
unsigned int val = *SW_MUX_GPIO1_IO03;
*SW_MUX_GPIO1_IO03 = val | (1 << 0);

虽然很多老旧的或者不规范的驱动确实这么干,甚至在某些简陋的硬件上也能跑,但 Linux 内核强烈反对这样做

因为硬件寄存器不是 RAM:

  • 读写副作用:有些寄存器只要一读就会清零,或者写入某个值会触发硬件动作
  • 对齐要求:硬件对 32 位访问的对齐要求比内存严格
  • 顺序保证:编译器为了优化可能会打乱指令顺序,或者把多次读写合并。但在驱动里,你必须按顺序写寄存器,比如「先设复用,再设方向,最后写数据」,顺序一乱就炸

读操作函数

根据你想读的位宽(8位、16位、32位),内核提供了三个函数:

c 复制代码
u8  readb(const volatile void __iomem *addr);  // 读 8 位(1 字节)
u16 readw(const volatile void __iomem *addr);  // 读 16 位(2 字节,w = word)
u32 readl(const volatile void __iomem *addr);  // 读 32 位(4 字节,l = long)

参数 addr 就是你用 ioremap 拿到的那个虚拟地址。

回到那个类比:如果你直接去翻 addr 指向的内存(用指针读),就像是你自己翻开了银行的柜台记录本。但 readl 就像是你正式填写了一张「取款单」,银行柜员会严格按照流程去金库执行操作,并把结果封好在信封里交给你。

写操作函数

同样的,写操作也有对应的三个函数:

c 复制代码
void writeb(u8 value,  volatile void __iomem *addr);  // 写 8 位
void writew(u16 value, volatile void __iomem *addr);  // 写 16 位
void writel(u32 value, volatile void __iomem *addr);  // 写 32 位
  • value:你要写入的值
  • addr:目标地址

为什么一定要用这些函数?

除了上面提到的顺序保证和对齐问题外,还有一个很重要的原因:可调试性

内核可以通过拦截这些函数调用来记录所有的 I/O 操作,这在排查硬件 Bug 时是救命稻草。如果你用指针强行读写,内核对你是一无所知的。

千万别混用

如果寄存器是 32 位的,你用了 writeb,可能只改了低 8 位,或者在某些架构上直接触发异常。一定要查阅芯片手册,确认寄存器的位宽。

对于 I.MX6ULL 的 GPIO 寄存器,绝大多数都是 32 位的,所以我们主要用的是 readlwritel


第六部分:实战示例

让我们把上面的知识串起来,看一个完整的例子。

映射寄存器

c 复制代码
/* 定义寄存器物理地址 */
#define GPIO1_DR_BASE    (0x0209C000)  // GPIO1 数据寄存器
#define GPIO1_GDIR_BASE  (0x0209C004)  // GPIO1 方向寄存器

/* 映射后的虚拟地址指针 */
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

/* 映射 */
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

if (!GPIO1_DR || !GPIO1_GDIR) {
    printk("ioremap failed\n");
    return -ENOMEM;
}

配置 GPIO 为输出

c 复制代码
u32 val;

/* 读取当前方向寄存器的值 */
val = readl(GPIO1_GDIR);

/* 配置 GPIO1_IO03 为输出(bit3 置 1)*/
val |= (1 << 3);

/* 写回 */
writel(val, GPIO1_GDIR);

这就是经典的**「读-改-写」**铁律。你不能直接 writel(0x08, GPIO1_GDIR),因为那样会把其他 31 个引脚的配置全冲掉。在嵌入式 Linux 这种多任务环境下,其他引脚可能正被别的驱动占用着。

控制 LED

c 复制代码
/* 点亮 LED(假设低电平点亮)*/
val = readl(GPIO1_DR);
val &= ~(1 << 3);  // bit3 清零
writel(val, GPIO1_DR);

/* 熄灭 LED */
val = readl(GPIO1_DR);
val |= (1 << 3);   // bit3 置一
writel(val, GPIO1_DR);

释放映射

c 复制代码
/* 卸载时 */
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);

第七部分:内核编程的限制与数据传递

内核空间的限制

虽然内核空间有特权,但这并不意味着你可以为所欲为。内核编程有严格的限制:

1. 不能使用标准 C 库
c 复制代码
#include <stdio.h>    // ❌ 不能用
#include <string.h>   // ❌ 不能用
#include <stdlib.h>   // ❌ 不能用

// 但可以使用内核提供的函数
#include <linux/string.h>  // strcpy, strlen, memcpy...
#include <linux/slab.h>    // kmalloc, kfree
#include <linux/printk.h>  // printk, pr_info

内核有自己的一套函数库,功能类似但名字可能不同。

2. 不能做浮点运算

内核默认不保存浮点寄存器(为了提高切换效率)。如果你使用浮点数,需要显式保存/恢复浮点上下文,这很麻烦且慢。

解决方法:在驱动中使用定点数或整数运算。

3. 栈空间有限

用户空间的栈通常是几 MB,但内核栈很小(通常 8KB 或 16KB)。

c 复制代码
// ❌ 危险!可能栈溢出
void dangerous_function(void) {
    char huge_buffer[10000];  // 太大了
    // ...
}

// ✅ 正确:使用堆
void safe_function(void) {
    char *buffer = kmalloc(10000, GFP_KERNEL);
    // ...
    kfree(buffer);
}
4. 不能睡眠(某些上下文)

在中断处理函数、软中断等上下文中,代码不能"睡眠"(不能调用可能阻塞的函数)。

c 复制代码
// ❌ 在中断上下文中会崩溃
irqreturn_t my_irq_handler(int irq, void *dev_id) {
    msleep(1000);  // 不能睡眠!
    return IRQ_HANDLED;
}

// ✅ 正确:使用延迟而非睡眠
irqreturn_t my_irq_handler(int irq, void *dev_id) {
    mdelay(1000);  // 忙等待,不睡眠
    return IRQ_HANDLED;
}

数据传递:越过城墙的方式

既然用户空间和内核空间是隔离的,那么两者之间如何传递数据?

方式 1:系统调用参数(简单数据)

对于小数据量(如整数、指针),直接通过系统调用参数传递:

c 复制代码
// 用户空间
int fd = open("/dev/led", O_RDWR);  // fd 通过寄存器返回

// 驱动中
static int led_open(struct inode *inode, struct file *filp) {
    // filp 已经是内核空间的数据结构了
    return 0;
}
方式 2:copy_to_user / copy_from_user(数据块)

对于大量数据(如缓冲区),使用专门的拷贝函数:

c 复制代码
// 驱动中
static ssize_t led_read(struct file *filp, char __user *buf,
                         size_t count, loff_t *ppos)
{
    char kernel_data[] = "LED status: ON";

    // 安全地从内核复制到用户空间
    if (copy_to_user(buf, kernel_data, strlen(kernel_data))) {
        return -EFAULT;  // 拷贝失败
    }

    return strlen(kernel_data);
}

为什么不能用 memcpy?

memcpy 不做安全检查。如果用户传一个恶意的内核地址,memcpy 会乖乖地把内核数据拷贝过去------这是安全漏洞。

copy_to_user 会:

  1. 检查目标地址是否在用户空间
  2. 检查地址是否可写
  3. 处理页错误(如果用户空间页面被换出)

常见错误与解决方案

错误 1:忘记 ioremap 直接用物理地址

c 复制代码
/* ❌ 错误 */
#define GPIO1_DR  0x0209C000
u32 val = readl(GPIO1_DR);  // 段错误!

后果 :触发内核 Oops(内核崩溃),因为 0x0209C000 在虚拟地址空间里没有被映射。

正确做法 :先用 ioremap 建立映射。

错误 2:用指针而不是 readl/writel

c 复制代码
/* ❌ 错误 */
u32 val = *GPIO1_DR;  // 危险!
*GPIO1_DR = 0x08;

后果:可能在某些硬件上能跑,但不符合内核规范。可能导致编译器优化出问题,或者在某些架构上触发异常。

正确做法 :使用 readl/writel

错误 3:忘记 iounmap

c 复制代码
/* ❌ 错误 */
static int __init led_init(void) {
    GPIO1_DR = ioremap(...);
    // ...
    return 0;
}

static void __exit led_exit(void) {
    // 忘记 iounmap
}

后果:内存泄漏,重复加载驱动时可能会失败。

正确做法 :在 exit 函数里调用 iounmap

错误 4:直接覆盖寄存器值

c 复制代码
/* ❌ 错误 */
writel(0x08, GPIO1_GDIR);  // 危险!

后果:把 GPIO1 的其他 31 个引脚的配置全冲掉了,可能导致系统其他功能异常。

正确做法:读-改-写。

c 复制代码
/* ✅ 正确 */
u32 val = readl(GPIO1_GDIR);
val |= (1 << 3);
writel(val, GPIO1_GDIR);

错误 5:直接访问用户空间指针

c 复制代码
// ❌ 错误示例
static ssize_t bad_read(struct file *filp, char __user *buf,
                         size_t count, loff_t *ppos)
{
    char data[] = "Hello";
    memcpy(buf, data, strlen(data));  // 危险!buf 是用户空间指针
    return strlen(data);
}

// ✅ 正确做法
static ssize_t good_read(struct file *filp, char __user *buf,
                          size_t count, loff_t *ppos)
{
    char data[] = "Hello";
    if (copy_to_user(buf, data, strlen(data))) {
        return -EFAULT;  // 正确处理拷贝失败
    }
    return strlen(data);
}

本章小结

核心概念

  1. 两座城池:用户空间(受限)和内核空间(特权)
  2. 城门关卡:系统调用是唯一合法的跨越方式
  3. 地址隔离:相同的虚拟地址在不同空间指向不同的物理内存
  4. MMU 映射:CPU 使用虚拟地址,物理地址需要被映射
  5. 硬件访问 :使用 ioremapreadl/writel 访问硬件寄存器
  6. 安全传递 :使用 copy_to_usercopy_from_user 在空间之间传递数据

编程规则

  • ✅ 驱动运行在内核空间,有完全权限
  • ✅ 使用内核提供的函数,不用标准 C 库
  • ✅ 永远不要直接访问用户空间指针
  • ✅ 限制栈空间使用,大对象用堆分配
  • ✅ 注意上下文(某些地方不能睡眠)
  • ✅ 使用 ioremap 访问硬件,readl/writel 读写寄存器

记住那个银行保险箱的比喻

  • 物理地址 = 保险箱编号(0x020E0068 号箱)
  • 虚拟地址 = 柜台窗口(3 号窗口)
  • MMU = 银行柜员(帮你找到对应的箱子)
  • ioremap = 向柜员申请窗口
  • readl/writel = 正规的填单操作
  • 指针直接读写 = 自己翻柜台记录本(违规操作)

参考文档:Linux 内核官方文档


相关阅读

  1. 嵌入式Linux学习指南之设备树------Linux内核设备树编译机制深度解析 - 相似度 100%
  2. 入门 · 环境搭建 · 00 · Qt6 安装踩坑指南 - 相似度 80%
  3. 深入理解Linux模块------模块参数与内核调试:让模块"活"起来的魔法 - 相似度 80%
相关推荐
少司府2 小时前
C++基础入门:内存管理
c语言·开发语言·c++·内存管理·delete·new·malloc
踏着七彩祥云的小丑2 小时前
嵌入式——小白入门
嵌入式硬件
鱼很腾apoc2 小时前
【学习篇】第17期 C++入门必看——类和对象全站最详篇
c语言·开发语言·学习·算法·青少年编程
Sakuyu434682 小时前
C语言基础(一)
c语言·开发语言
码农的神经元2 小时前
2026 MathorCup C 题实战复盘:从高血脂风险预警到 6 个月干预优化的建模思路与 Python 落地
c语言·开发语言·python
萑澈2 小时前
实践教程:我如何用 n8n 自动化“软著申请”中最头疼的文档撰写工作
运维·elasticsearch·自动化
zzzsde2 小时前
【Linux】进程信号(1)理解信号及信号产生的方式
linux·运维·服务器·算法
DBA大董2 小时前
TDengine3.x 数据文件详解
大数据·linux·时序数据库·dba·tdengine
生万千欢喜心3 小时前
Linux 安装金蝶天燕中间件 AAS-V9.0.zip
java·linux