事件驱动的编程, 但是虽然不是所有的应用程序都是事件驱动的, 每个内核模块都是。
一个终止的应用程序可以在释放资源方面懒惰, 或者完全不做清理工作, 但是模块的退出函数必须小心恢复每个由初始化函数建立的东西, 否则会保留一些东西直到系统重启.
每一个环境是如何处理错误: 在应用程序开发中段错误是无害的, 一个调试器常常用来追踪错误到源码中的问题, 而一个内核错误至少会杀掉当前进程, 如果不终止整个系统.
A module runs in kernel space, whereas applications run in user space. This concept is at the base of operating systems theory. 一个模块在内核空间运行, 而应用程序在用户空间运行. 这个概念是操作系统理论的基础.
模块的角色是扩展内核的功能; 模块化的代码在内核空间运行. 经常地一个驱动进行之前提到的两种任务: 模块中一些的函数作为系统调用的一部分执行, 一些负责中断处理.
编写处理并发和避免竞争情况( 一个不幸的执行顺序导致不希望的行为的情形 )的代码需要仔细考虑并可能是微妙的. 正确的并发管理在编写正确的内核代码时是必须的;
应用程序存在于虚拟内存中, 有一个非常大的堆栈区. 堆栈, 当然, 是用来保存函数调用历史以及所有的由当前活跃的函数创建的自动变量. 内核, 相反, 有一个非常小的堆栈; 它可能小到一个, 4096 字节的页. 你的函数必须与这个内核空间调用链共享这个堆栈. 因此, 声明一个巨大的自动变量从来就不是一个好主意; 如果你需要大的结构, 你应当在调用时间内动态分配.
常常, 当你查看内核 API 时, 你会遇到以双下划线(__)开始的函数名. 这样标志的函数名通常是一个低层的接口组件, 应当小心使用. 本质上讲, 双下划线告诉程序员:” 如果你调用这个函数, 确信你知道你在做什么.”
内核代码不能做浮点算术. 使能浮点将要求内核在每次进出内核空间的时候保存和恢复浮点处理器的状态 — 至少, 在某些体系上. 在这种情况下, 内核代码真的没有必要包含浮点, 额外的负担不值得.
insmod 为你完成这个工作. 这个程序加载模块的代码段和数据段到内核, 接着, 执行一个类似 ld 的函数, 它连接模块中任何未解决的符号连接到内核的符号表上. 但是不象连接器, 内核不修改模块的磁盘文件, 而是内存内的拷贝. insmod 接收许多命令行选项(详情见 manpage), 它能够安排值给你模块中的参数, 在连接到当前内核之前.
你的模块确实应当指定它的代码使用哪个许可. 做到这一点只需包含一行 MODULE_LICENSE:
MODULE_LICENSE("GPL");
内核的某些别的部分会在注册完成之后马上使用任何你注册的设施. 这是完全可能的, 换句话说, 内核将调用进你的模块, 在你的初始化函数仍然在运行时. 所以你的代码必须准备好被调用, 一旦它完成了它的第一个注册. 不要注册任何设施, 直到所有的需要支持那个设施的你的内部初始化已经完成. 你也必须考虑到如果你的初始化函数决定失败会发生什么, 但是内核的一部分已经在使用你的模块已注册的设施. 如果这种情况对你的模块是可能的, 你应当认真考虑根本不要使初始化失败. 毕竟, 模块已清楚地成功输出一些有用的东西. 如果初始化必须失败, 必须小心地处理任何可能的在内核别处发生的操作, 直到这些操作已完成.
用户空间的设备驱动的方法有几个缺点. 最重要的是:
- 中断在用户空间无法用. 在某些平台上有对这个限制的解决方法, 例如在 IA32 体系上的 vm86 系统调用.
- 只可能通过内存映射 /dev/mem 来使用 DMA, 而且只有特权用户可以这样做.
- 存取 I/O 端口只能在调用 ioperm 或者 iopl 之后. 此外, 不是所有的平台支持这些系统调用, 而存取/dev/port可能太慢而无效率. 这些系统调用和设备文件都要求特权用户.
- 响应时间慢, 因为需要上下文切换在客户和硬件之间传递信息或动作.
- 更不好的是, 如果驱动已被交换到硬盘, 响应时间会长到不可接受. 使用 mlock 系统调用可能会有帮助, 但是常常的你将需要锁住许多内存页, 因为一个用户空间程序依赖大量的库代码. mlock, 也, 限制在授权用户上.
- 最重要的设备不能在用户空间处理, 包括但不限于, 网络接口和块设备.