Rusty Russell's "Unreliable Guide to Hacking the Linux Kernel"
作者
Rusty Russell
简介
欢迎阅读 Rusty's Remarkably Unreliable Guide to Linux Kernel Hacking。本文档描述了内核代码的常见例程和一般要求:其目标是为有经验的 C 程序员提供 Linux 内核开发的入门指南。我避免了实现细节:这就是代码的用途,我忽略了一些有用的例程。
在阅读本文之前,请了解我从未想过要写这个文档,因为我明显不够资格,但我一直想阅读它,这是唯一的方法。我希望它会成为最佳实践、常见起点和随机信息的汇编。
主要内容
参与者
在系统中,每个 CPU 都可能处于以下状态之一:
- 未与任何进程关联,为硬件中断提供服务;
- 未与任何进程关联,为软中断或任务队列提供服务;
- 在内核空间运行,与进程关联(用户上下文);
- 在用户空间运行进程。
这些之间存在一种顺序。底部两个可以相互抢占,但在它们之上是严格的层次结构:每个只能被其上方的部分抢占。例如,当一个软中断在 CPU 上运行时,没有其他软中断会抢占它,但硬件中断可以。然而,系统中的任何其他 CPU 都是独立执行的。
我们将看到用户上下文如何阻塞中断,以成为真正的不可抢占状态。
用户上下文
用户上下文是指从系统调用或其他陷阱进入时的状态:与用户空间一样,您可能会被更重要的任务和中断抢占。您可以通过调用 schedule() 进行休眠。
注意
在模块加载和卸载以及块设备层的操作中,您始终处于用户上下文中。
在用户上下文中,当前指针(指示当前执行的任务)是有效的,并且 in_interrupt()(include/linux/preempt.h)为 false。
警告
请注意,如果禁用了抢占或软中断(见下文),in_interrupt() 将返回错误的结果。
硬件中断(硬中断)
定时器滴答声、网络卡和键盘是实际硬件的例子,它们可以在任何时候产生中断。内核运行中断处理程序,为硬件提供服务。内核保证此处理程序永远不会重新进入:如果相同的中断到达,它将被排队(或丢弃)。由于它禁用了中断,因此此处理程序必须快速:通常它只是确认中断,标记要执行的“软中断”,然后退出。
您可以通过 in_hardirq() 返回 true 来判断是否在硬件中断中。
警告
请注意,如果禁用了中断(见下文),这将返回错误的结果。
软中断上下文:软中断和任务队列
每当系统调用即将返回到用户空间,或者硬件中断处理程序退出时,任何标记为挂起的“软中断”(通常由硬件中断标记)都会运行(kernel/softirq.c)。
大部分真正的中断处理工作都是在这里完成的。在转换到 SMP 的早期阶段,只有“底半部”(BH),它们没有利用多个 CPU。不久之后,我们放弃了这种限制,转而使用了“软中断”。
include/linux/interrupt.h 列出了不同的软中断。一个非常重要的软中断是定时器软中断(include/linux/timer.h):您可以注册让它在一定时间内为您调用函数。
软中断通常很难处理,因为同一个软中断会同时在多个 CPU 上运行。因此,更常用的是任务队列(include/linux/interrupt.h):它们是动态可注册的(这意味着您可以拥有尽可能多的任务队列),并且它们还保证任何任务队列一次只在一个 CPU 上运行,尽管不同的任务队列可以同时运行。
警告
名称“任务队列”是误导性的:它们与“任务”无关。
您可以通过使用 in_softirq() 宏(include/linux/preempt.h)来判断是否在软中断(或任务队列)中。
警告
请注意,如果持有底半部锁,这将返回错误的结果。
一些基本规则
没有内存保护
如果您在用户上下文或中断上下文中损坏内存,整台机器都会崩溃。您确定您不能在用户空间完成您想要的操作吗?
没有浮点或 MMX
FPU 上下文未保存;即使在用户上下文中,FPU 状态可能与当前进程不对应:您会干扰某个用户进程的 FPU 状态。如果您真的想这样做,您必须显式保存/恢复完整的 FPU 状态(并避免上下文切换)。这通常是一个坏主意;首先使用定点算术。
严格的堆栈限制
根据配置选项,大多数 32 位架构的内核堆栈约为 3K 到 6K:在大多数 64 位架构上约为 14K,并且通常与中断共享,因此您无法全部使用它。避免在堆栈上进行深度递归和大型本地数组(应该动态分配它们)。
Linux 内核是可移植的
让我们保持这种状态。您的代码应该是 64 位兼容的,并且与大小端无关。您还应该尽量减少特定于 CPU 的内容,例如内联汇编应该清晰地封装并最小化,以便于移植。通常应该将其限制在内核树的体系结构相关部分。
ioctl:不编写新的系统调用
系统调用通常如下所示:
asmlinkage long sys_mycall(int arg)
{
return 0;
}
首先,在大多数情况下,您不希望创建新的系统调用。您应该创建一个字符设备,并为其实现适当的 ioctl。这比系统调用更灵活,不必在每个体系结构的 include/asm/unistd.h 和 arch/kernel/entry.S 文件中输入,而且更有可能被 Linus 接受。
如果您的例程只是读取或写入某些参数,请考虑实现 sysfs() 接口。
在 ioctl 中,您处于用户上下文中的一个进程。当发生错误时,您返回一个负的 errno(参见 include/uapi/asm-generic/errno-base.h、include/uapi/asm-generic/errno.h 和 include/linux/errno.h),否则返回 0。
在休眠后,您应该检查是否发生了信号:处理信号的 Unix/Linux 方法是临时退出系统调用,并返回 -ERESTARTSYS 错误。系统调用入口代码将切换回用户上下文,处理信号处理程序,然后您的系统调用将被重新启动(除非用户禁用了它)。因此,您应该准备处理重新启动,例如,如果您正在操作某些数据结构的中间。
if (signal_pending(current))
return -ERESTARTSYS;
如果您正在进行较长的计算:首先考虑用户空间。如果您真的想在内核中执行它,您应该定期检查是否需要放弃 CPU(请记住每个 CPU 有合作式多任务处理)。习惯用法:
cond_resched(); /* 将休眠 */
关于接口设计的简短说明:UNIX 系统调用的座右铭是“提供机制而不是策略”。
死锁的解决方案
除非:
- 您处于用户上下文中。
- 您不拥有任何自旋锁。
- 您已启用中断(实际上,Andi Kleen 表示调度代码将为您启用它们,但这可能不是您想要的)。
您不能调用任何可能休眠的例程。请注意,某些函数可能会隐式休眠:常见的是用户空间访问函数(*_user)和没有 GFP_ATOMIC 的内存分配函数。
您应该始终使用编译内核时的 CONFIG_DEBUG_ATOMIC_SLEEP 选项,它会在您违反这些规则时发出警告。如果您违反了这些规则,最终会锁定您的计算机。
真的。
常见的内核编程例程
printk()
在 include/linux/printk.h 中定义
printk() 将内核消息输出到控制台、dmesg 和 syslog 守护进程。它对调试和错误报告很有用,并且可以在中断上下文中使用,但需要谨慎使用:如果控制台被大量的 printk 消息淹没,那么机器将无法使用。它使用的格式字符串大部分兼容 ANSI C printf,并使用 C 字符串连接来给它一个第一个“优先级”参数:
printk(KERN_INFO "i = %u\n", i);
参见 include/linux/kern_levels.h;其他 KERN_ 值;这些值被 syslog 解释为级别。特殊情况:要打印 IP 地址,请使用:
__be32 ipaddress;
printk(KERN_INFO "my ip: %pI4\n", &ipaddress);
printk() 内部使用 1K 缓冲区,并且不会捕获溢出。确保这足够大。
注意
当你开始在用户程序中将 printf 打成 printk 时,你就知道自己是一个真正的内核黑客了
标签:上下文,中断,linux,调用,黑客,内核,Linux,ChatGPT,include From: https://www.cnblogs.com/pengdonglin137/p/17889306.html