news 2026/11/5 23:56:22

从内核模块到DMA:Linux驱动开发入门路线与实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从内核模块到DMA:Linux驱动开发入门路线与实战指南

干了这么多年底层开发,最常被问的一句话就是“驱动到底怎么学”。说实话,每次看到这个问题,我都会想起自己刚入门时干过的蠢事:为了点亮一个LED,把寄存器地址翻来覆去地数错,最后发现是总线上一个时钟使能位没打开;为了等一个中断,在中断处理函数里死等标志位置位,结果直接触发软锁up。那会儿资料少,社区里的讨论又零散,全靠自己一行行试、一遍遍翻内核源码,走了太多弯路。后来带过一些刚入行的同事,我发现大多数人不是学不会驱动,而是被“从哪开始、按什么顺序学、遇到问题去哪查”这三件事卡住了。也正是这个原因,让我想认真写一个系列,把我这些年做驱动开发的经验、踩坑记录和理解框架完整地整理出来,这就是《驱动之路》的由来。

这个系列打算从一个最普通的内核模块讲起,一直走到字符设备、中断、并发、设备树、DMA这些真正难啃的部分,中间穿插大量我实际调试过的案例和排查思路。适合的读者很明确:写过一点C代码、对操作系统有基本概念、但从来没碰过内核的人;以及已经在写业务代码、想往底层拓展视野的上层应用开发者。如果你是内核老手,这里可能不会有太多让你惊艳的冷知识,但里面不少“为什么这么设计”的思考,或许能帮你把零散经验串成体系。今天这篇开篇,先把几个最关键的认知问题聊透,再给你一份可以照着走的路线图。

1. 为什么写《驱动之路》:先说几句心里话

1.1 驱动开发的“断层”与门槛

驱动开发这个方向,在软件开发里一直是个很尴尬的存在。它不像应用开发那样有完整的教程链条,也不像算法岗那样有大量现成开源项目可以参考。很多人在学校学过操作系统原理,知道进程、内存、文件系统,但一打开内核源码还是懵的,因为内核里那种“到处是回调函数指针、层层抽象、异步事件满天飞”的编程模型,和平时写的业务代码完全两个世界。

更麻烦的是,驱动开发历来是“师傅带徒弟”的领域。很多经验都沉淀在老工程师的脑子里,而不是写进文档里。比如某个芯片的勘误表里写了一条要绕过的坑,你如果不看芯片手册根本发现不了;再比如某个外设的驱动在某个内核版本上行为发生了变化,这类信息只存在于邮件列表的某个角落里。所以,新人在初始阶段面对的不仅是知识断层,还有信息断层。

这些年我的体会是,驱动开发真正难的不是某一门具体知识,而是知识栈的跨度。你要懂硬件,至少看得懂原理图和芯片手册;你要懂体系结构,知道MMU、cache、中断控制器是怎么协同工作的;你要懂操作系统,理解进程上下文、中断上下文、调度和同步;你还要懂内核编程的“规矩”,知道什么函数能在什么上下文里调用。这四块知识缺一块,后面都会踩坑。

1.2 这个系列打算怎么展开

《驱动之路》不是一个单篇能写完的东西,所以我把它规划成一个系列。整个系列我打算按这样的顺序推进:

  • 基础篇:搭建开发环境,编译内核,用QEMU或者一块开发板跑起来,写出第一个内核模块;
  • 字符设备篇:理解设备节点、file_operations、read/write/ioctl 这些最核心的机制,把用户态和内核态的数据通路打通;
  • 设备模型篇:理解bus、device、driver、class 的关系,搞懂设备树(DeviceTree)和platform总线,这是现代驱动开发绕不开的主干;
  • 中断与并发篇:处理中断、下半部机制、自旋锁和互斥锁的选择,解决驱动的“时间”问题;
  • 内存与DMA篇:内核内存分配、mmap、DMA一致性映射,解决驱动的“空间”和性能问题;
  • 调试与实战篇:动态调试、ftrace、crash分析,再加上几个完整的驱动调试案例,把前面的知识串起来。

每一篇我会尽量做到“不只讲API怎么用,更讲清楚API为什么这么设计”。因为内核接口变化很快,但你理解了设计动机,迁移到新版本只是时间问题。

1.3 什么样的人适合读这个系列

如果你属于下面这几类人,这个系列就是为你准备的:

  • 应用开发转底层,心里有点怵,想找个系统路线的人;
  • 嵌入式方向的学生,课程里学过单片机,但没碰过完整内核的人;
  • 已经在写驱动,但大多是复制修改别人的代码,想搞明白原理的工程师;
  • 纯粹好奇“操作系统和硬件怎么对话”的爱好者。

如果你完全没有C语言基础,那我建议先花两周补一下指针、结构体、链表这些基本概念再来看。驱动开发不是一个适合零编程基础入门的领域,但也不是什么高不可攀的东西,它更像是一门手艺——熟练之后,你会觉得它很有逻辑,甚至有一点“解题”的乐趣。

2. 驱动开发的核心认知:它到底在解决什么问题

2.1 驱动在整个系统中的角色

我常用一个比喻来跟新人解释驱动的作用:操作系统像一栋大楼,应用程序是住在里面的租户,他们只用管自己房间里的生活;内核是物业公司,负责统一管理水电、电梯、安保这些公共设施;而驱动程序,就是物业雇来的各种“工种”——电工、水管工、电梯维护员。租户不会直接去修水管,他们只是拧开水龙头,至于水是怎么来的、水管是哪家公司铺的、为什么这栋楼的水压要这样调,那是物业和水电工之间的事。

对应到技术上,应用程序通过系统调用(open、read、write)来使用设备,它看到的只是/dev/ttyUSB0、/dev/sda这样的文件;内核把系统调用转成对驱动的调用,驱动再把抽象的操作翻译成具体的硬件读写。比如你要从串口读一个字节,驱动要做的实际上是:查看串口控制器的状态寄存器,确认接收FIFO不为空,然后从数据寄存器里把那个字节取出来,再交给上层。

所以驱动的本质,是一种“协议翻译器”——它一头连着内核的抽象接口,另一头连着硬件的物理寄存器。你能不能认清这两端,决定了你能不能写出合格的驱动。

2.2 驱动开发需要的知识栈

这一步如果展开说,可以写一本书,我先搭个框架,后面每一篇会逐个深入:

  • 硬件基础:至少会看简单的电路原理图,能分辨芯片的电源域、时钟树、复位逻辑;不用会画PCB,但要看得懂芯片手册中关于寄存器的描述。
  • CPU与体系结构:理解MMU、cache、中断控制器(如GIC)的基本工作原理,知道读写外设寄存器和读写内存有什么不同。这里提醒一句:很多外设寄存器是“有副作用”的,你读一次可能就会清掉某个状态位,这跟读内存完全不一样。
  • 操作系统与内核机制:进程上下文、中断上下文、内核同步、内存管理,这是驱动开发的“内功”。驱动跑在内核态,出问题就是整个系统宕机,所以在写每一行代码之前都要想清楚:我现在在什么上下文里?能不能睡眠?要不要关中断?
  • 内核API和约定:虽然内核API版本演进较快,但内核的开发风格和设计哲学是稳定的。学会用git log和git grep去内核源码里找线索,比背API强得多,这是我反复强调的习惯。

2.3 驱动和应用开发的最大差异

很多从应用开发转过来的同事,一开始最不适应的就是“运行环境限制”。在用户态,你可以随便malloc、printf、sleep(1),甚至崩溃了也就崩一个进程;在内核里,你用的内存池有大小限制,你在中断上下文里调用一个可能睡眠的函数,系统会直接报“BUG: scheduling while atomic”,然后整个系统卡住。这种“容错率极低”的开发环境,逼着你在动手写代码之前,先把同步、内存、生命周期这些概念想清楚。

另一个重要差异是,驱动要处理“并发访问”的频率远高于普通应用。用户态程序狭义的并发通常依赖锁、队列、消息传递;内核驱动的并发则跟硬件息息相关——多个CPU核同时访问同一个设备的寄存器,中断随时可能打断当前流程,DMA在后台搬运数据。这就是为什么内核里充满了自旋锁、原子操作、内存屏障这些工具,不理解这些,你写的驱动可能“偶尔能跑”,但一压测就挂。

驱动还有一个独特的地方,就是它和“具体硬件”强绑定。同一个功能的芯片,A厂和B厂的寄存器定义几乎是两个风格。这种绑定关系带来一个结果:驱动开发中“读文档、看勘误、做实验”的时间和写代码的时间差不多,甚至更多。所以在这个系列里,我也会刻意训练一种阅读方法——拿到一个新芯片,你要按什么顺序去读它的手册,以及怎么把手册描述转换成代码。

3. 从零到一的驱动实践路线:照着做就能跑

3.1 环境准备:QEMU模拟器和真实板卡怎么选

工欲善其事,必先利其器。在做任何驱动开发之前,你必须有一套能跑起来的内核环境。这里有两条路,各自优缺点我都列出来,大家按自己情况选:

对比维度QEMU模拟环境真实开发板
成本免费,一台普通电脑就够需要买板子,几百到几千不等
环境真实性外设都是模拟出来的,和真实硬件有差异真实外设,能遇到真实时序问题
调试便利性可以随时加断点,出问题容易定位调试需要接串口、JTAG,门槛稍高
外设覆盖常见外设基本都有,但深度不够取决于板卡引出哪些外设
适合人群想低成本入门、专注内核机制的初学者准备做真实产品、需要接触具体芯片的工程师

我的建议是:如果你对内核机制本身还不熟,先别急着买板子,用QEMU把一个最小系统跑起来就够了。QEMU里跑的是真的内核、真的设备模型,你学到的字符设备、设备树、中断机制,和真实环境下完全一致。等你熟悉了这套东西,再上手真实板卡,效率会高很多。我自己带过的几个新人,都是先在QEMU里把平台总线驱动的框架调通,再上板子,基本没遇到过“不知道从哪里查起”的困境。

用QEMU起一个最小系统的步骤大致如下:

  1. 准备一台Linux机器,装好git、make、gcc、flex、bison、libncurses-dev等基础包;
  2. 下载一份内核源码,我建议用长期支持版本(LTS),例如6.x系列,因为社区维护时间长、资料相对丰富;
  3. 先生成默认配置,再执行make menuconfig,按需打开你需要的驱动和QEMU模拟需要的配置项;
  4. 用make -j$(nproc)编译内核,在QEMU里启动,配合一份裁剪好的根文件系统(用busybox制作即可);
  5. 启动参数加上console=ttyS0这类参数,让内核日志输出到串口终端。

第一次启动成功,看到内核的启动日志刷屏,你的驱动学习之路就正式开始了。这个过程里如果被卡住,九成是编译依赖缺失或者文件系统制作的问题,不要灰心,这个坑几乎所有搞内核的人都踩过。

3.2 第一个驱动:从一个最简单又“完整”的模块开始

很多人一上来就想写一个大驱动,这其实是最大的认知误区。我强烈建议第一个驱动的目标不是“驱动某个设备”,而是“让内核加载并卸载你写的一段代码”,体会内核模块的生命周期。

这里分享一个最经典的内核模块代码,它没有任何实际用途,但麻雀虽小、五脏俱全:

#include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> static int __init hello_init(void) { printk(KERN_INFO "hello: module loaded\n"); return 0; } static void __exit hello_exit(void) { printk(KERN_INFO "hello: module unloaded\n"); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Some Developer"); MODULE_DESCRIPTION("A simple hello module");

编译它需要一份Makefile,同样很短:

obj-m := hello.o KDIR := /lib/modules/$(shell uname -r)/build all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean

编译完成后,你会得到一个hello.ko文件。用insmod hello.ko加载,再用rmmod hello卸载,然后执行dmesg | tail,就能看到两行日志。这个简单的流程里其实藏着驱动开发最重要的三个概念:module_init和module_exit指定了模块的入口和出口;__init和__exit告诉内核这些函数在初始化/卸载之后可以释放内存;MODULE_LICENSE 则决定了这个模块能不能使用某些标记为GPL导出的内核符号。

从写这个模块的那一天起,你就要养成一个习惯:每次加载模块前先看lsmod确认状态,每次改动代码后重新编译,不要在一套旧环境上反复折腾。内核模块不像用户态程序,旧模块和新内核API不匹配,很容易出现“Unknown symbol”或直接导致系统崩溃,这是新手最常遇到的两类问题。

3.3 字符设备驱动:从模块到“设备文件”的跃迁

跑通hello模块之后,下一步就是写一个真正的字符设备驱动,并让用户态程序能和它交互。字符设备是串口、GPIO、传感器这类设备在内核中的抽象形态,它的核心是一个file_operations结构体,里面有open、read、write、ioctl这些回调函数。内核在用户进程对设备文件发起系统调用时,会把这些调用转给你注册的回调。

在实现字符设备时,我建议新手选择miscdevice框架而不是直接操作register_chrdev_region加cdev_init的组合。原因很简单:miscdevice使用系统预分配的主设备号,你只需要填一个子设备号和名字,大大减少了样板代码,适合用来理解机制。当你理解清楚设备节点是怎么被内核创建出来的之后,再去研究cdev的完整流程,会顺理成章很多。

字符设备驱动里最常见的坑主要有三个:

  • 设备节点没自动出现。这通常是因为驱动里没有用class_create和device_create创建类和设备,或者用户空间的udev规则没有匹配上。解决办法是先用mknod /dev/xxx c 主设备号 次设备号手动创建,验证驱动本身是否正常。
  • read/write回调没有实现完整的返回值逻辑。用户态读数据时,驱动需要正确处理copy_to_user返回值、剩了多少数据没传、以及file->f_pos怎么更新。很多新手只写“当请求长度大于缓冲区就直接返回错误”,结果上层应用一用就出各种边界问题。
  • 并发访问没处理。设备节点可以被多个进程同时打开读写,如果你的驱动里有全局变量,一定要考虑加锁。这一步虽然有那么点“麻烦”,但恰恰是驱动和应用开发最大的分水岭——想明白并发,你的驱动才真正有“产品级”的雏形。

从这一刻起,你就不再是“写了一个内核模块”,而是“写了一个内核驱动”。用户态的echo hello > /dev/mydev和cat /dev/mydev能通过你的驱动和内核对话,这种感觉是驱动开发最迷人的时刻。

3.4 进阶之路:中断、设备树与DMA

当你把字符设备跑通以后,会自然接触到驱动开发真正的三座大山:中断、设备树和DMA。

先说中断。硬件设备要通知内核“我有数据了”,最常用的方式就是触发中断。内核里对中断的处理分上半部和下半部:上半部是中断处理函数本身,要求尽快完成,不能睡眠;下半部负责真正的耗时操作,可以用tasklet、workqueue或者threaded_irq来推迟处理。你写驱动时,要习惯“上半部只收数据、记录状态,下半部才做逻辑”这种思维模式。顺便说一个我经常强调的点:中断共享设备,在检查中断状态寄存器之后,一定要在返回前再确认一次状态,否则很容易造成中断风暴。

再说设备树。在嵌入式领域,现在的内核启动时依赖设备树来告诉内核“这块板子上有哪些设备,分别挂在什么地址”。设备树文件里的compatible字段,是驱动和硬件绑定的关键。如果你的驱动注册了of_match_table,但设备树里的compatible和它不匹配,驱动就永远不会被probe回调。这类问题的排查方式很简单——检查/sys/firmware/devicetree/base/下的节点,或者看内核启动日志里有没有出现failed to match的提示。

最后是DMA。DMA让硬件可以直接读写内存,不需要CPU逐字节搬运,是高性能设备(网卡、存储控制器)的核心机制。但DMA也带来一个经典难题:CPU的cache一致性。CPU读到的是cache里的旧数据,而硬件往内存里写的是新数据,两边不一致,数据就乱了。解决手段是使用dma_alloc_coherent申请一致性内存,或者在每次传输前后手动做cache刷新操作。新手第一次遇到“数据一半对一半乱”的问题时,多半就是忘记处理cache一致性和缓冲区对齐了。

这三座大山每一座都可以单独写一篇文章,这里先埋下伏笔,后续系列里我会逐个拆开来讲。

4. 调试手段与问题排查实录:学会和内核“对话”

4.1 常用的调试工具和方法

很多刚开始写驱动的朋友,沿用了用户态调试的习惯——加printf、打断点。在内核里,这些习惯不是不能用,但要换一套工具。

第一梯队是printk。它是内核版的printf,往内核日志缓冲区里写信息,用dmesg查看。printk里可以指定日志级别,比如KERN_INFO、KERN_ERR、KERN_DEBUG,通过调整内核的loglevel可以控制哪些日志能输出。但要注意,在中断上下文里尽量不要用printk,因为printk是有锁的,频繁调用会让中断路径发生阻塞;大量日志还会影响实时性。

第二梯队是内核的动态调试(dynamic debug)。这个机制允许你在不改代码的情况下,动态打开或关闭某些pr_debug日志。操作方式是在/sys/kernel/debug/dynamic_debug/control文件里写格式,比如file xxx.c line 123 +p。这个功能在生产环境特别实用,你可以只对怀疑的那个文件开日志,不用重新编译整个内核。

第三梯队是ftrace。ftrace能跟踪函数的调用关系和时间消耗,例如function_graph跟踪器可以打印出某条调用路径上的所有函数,这对理解一个流程“实际走到了哪一步”特别有帮助。使用方式是挂载debugfs之后,在/sys/kernel/debug/tracing里设置current_tracer和set_ftrace_filter。

第四梯队是crash和kdump。当内核panic崩溃时,kdump会把崩溃现场的内存转储到磁盘,再用crash工具分析。这是解决最棘手问题的最后手段。对一个新手来说,不一定要深入学习crash,但你要知道有这个东西,并且在关键驱动代码里加上dump_stack()或BUG_ON(),可以帮你快速定位崩溃轨迹。

4.2 三个典型故障排查实录

这个部分我挑三个我真实遇到过的、也很有代表性的问题来复盘。

案例一:probe函数不被调用。某次在一款ARM板卡上调一个以太网控制器的驱动,设备树节点和驱动代码都“看起来”没问题,但probe就是不执行。我花了大半天时间检查注册顺序、看Kconfig依赖,最后发现设备树里compatible写错了,把驱动的compatible字符串拼错了一个字母。内核匹配驱动和设备用的是完全字符串匹配,差一个字符都不行。复盘下来,最快捷的诊断方法是cat /sys/bus/platform/devices/xxx/uevent,查看设备节点上报的MODALIAS是否和驱动的模块别名一致。

案例二:中断风暴导致CPU占用率100%。某个触摸屏控制器驱动在接上设备后,系统的top显示一个CPU核心直接被打满。用cat /proc/interrupts观察,发现触摸屏中断的计数在疯狂增长。问题原因在于中断处理函数读取设备状态寄存器清除了中断标志,但后续又立即读取了一次状态寄存器去判断“是否有坐标数据”,这个多余的读操作触发了硬件重新产生中断。也就是说,如果你的中断处理函数在退出前不小心踩到了硬件的中断产生条件,就会变成无限自激。解决方式是严格按照芯片手册的中断处理流程来写,读一次、清标志、退出。

案例三:DMA传输出错,数据一半对一半乱。某个音频设备驱动用了DMA搬运PCM数据,播放出来的声音断断续续、夹杂杂音。用内存hexdump对比源数据和DMA搬运后的数据,发现有规律的错位。排查后发现是DMA缓冲区没有按cacheline对齐,导致cache回写时把邻近的数据污染了。解决方式是使用dma_alloc_coherent分配缓冲区,并在描述符里处理好地址对齐。这个案例教会我一件事:遇到“数据随机不对”的问题时,第一嫌疑永远是cache一致性和内存对齐。

4.3 常见问题速查:新手必看的排查清单

我整理了一份驱动开发常见的故障速查表,每一条都是“问题—可能原因—解决思路”的形式,建议收藏备用:

现象可能原因解决思路
insmod提示Unknown symbol模块依赖的内核符号未导出或版本不匹配modprobe加载依赖包,检查/proc/kallsyms有没有该符号
加载模块后设备节点不存在驱动里没创建class/device,或udev规则缺失先用mknod手动创建验证,再排udev规则
probe回调未执行compatible不匹配、设备树节点disabled检查/sys/firmware/devicetree/base/下节点和/sys/bus/platform/devices/的uevent
中断计数暴涨中断共享、中断处理流程里没有正确清标志检查/proc/interrupts,按芯片手册清中断,避免重复触发
读/写数据随机出错DMA cache一致性问题、缓冲区未对齐改用dma_alloc_coherent,或按cacheline对齐做flush/invalidate
系统一加载驱动就死机中断上下文里睡眠、自旋锁使用不当、越界访问用dump_stack+crash分析转储,检查所有可能睡眠的调用路径
驱动偶发卡死并发访问未加锁、锁顺序不一致用lockdep打开内核锁依赖检查,修复锁顺序

这张表不是教科书里抄来的,每一行背后都是有人熬过夜换来的教训。驱动开发的排查过程,本质上就是“缩小嫌疑范围”——用dmesg、/proc、/sys和trace这些“探针”,一步一步逼近出问题的硬件或代码路径。

5. 后面的话:一点个人习惯和提醒

文章最后,我不做总结,只想分享一个陪伴我很多年的工作习惯,也算是给《驱动之路》系列立一个基调。我每次拿到一个新的开发板或者新芯片,第一件事不是翻手册写驱动,而是先花半天到一天时间,把它的设备树、时钟树、复位控制、启动日志完整地读一遍。这就好比搬进新房子,先检查水路、电路、承重墙,再决定怎么装修。驱动开发里,硬件细节决定成败,你越早建立起对硬件的“体感”,后面越少吃“代码没问题但硬件不给力”的苦。

第二个习惯是“保持怀疑”。内核代码和芯片手册也有bug,厂商的参考驱动也未必是精品。遇到诡异问题,我习惯同时开一个终端看dmesg -w,一个终端打开源码目录,随时git grep搜索相关调用路径。这种多窗口交叉验证的办法,陪我解决了无数个看似无解的问题。

《驱动之路》这个系列我打算慢慢写,不求快,但求每个机制都讲透。希望读到这里的你,也能从第一个hello模块开始,一步步把驱动这条难走但风景很好的路走下来。下一篇,我们直接从“搭建最小驱动开发环境”开始动手。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/5 23:56:13

串口通讯原理与实战:UART、RS232、RS485硬件协议详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/5 23:56:13

嵌入式MCU自研轻量级HTTP客户端:内存池、状态机与超时机制实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/23 13:26:24

网页水印生成:四层防御模型对抗截图录屏与DOM审查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/23 13:15:08

内存芯片供需失衡:AI与云计算推动行业变革

1. 行业现状&#xff1a;内存芯片供需失衡加剧去年全球内存芯片市场已经出现明显供不应求的局面&#xff0c;而今年情况更加严峻。根据最新行业数据显示&#xff0c;数据中心对内存芯片的采购量同比激增40%&#xff0c;预计将占据全年总产量的70%以上。这种结构性变化正在深刻影…

作者头像 李华
网站建设 2026/10/24 14:25:52

第25讲:顺序表专题

目录 课前准备顺序表概念及结构顺序表分类实现动态顺序表 正文开始 课前预备 1. 课程目标 C语言语法基础到数据结构与算法&#xff0c;前面已经掌握并具备了扎实的C语言基础&#xff0c;为什么要学习数据结构课程&#xff1f;–通讯录项目 2. 需要的储备知识 简单了解&…

作者头像 李华
网站建设 2026/11/5 7:08:03

Windows输入法注入原理与DLL进程通信实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华