LINUX设备驱动程序入门
##内核代码库
要想为linux 2.6.x 内核构造驱动模块,必须首先在自己的系统中配置并构造好内核树。跟之前的linux内核版本不同,先前的模块编写只需要有一套内核的头文件就够了,但是因为2.6内核的模块要和内核源代码树中的目标文件连接,通过这种方式可以得到一个更加健壮的模块装载器,但也需要这些目标文件存在于内核目录树中。
先查看自己系统的版本 $ uname -r
查看是否已经获取过内核代码 $ ls /usr/src
搜索可以下载的内核代码版本 $ apt-cache search linux-source
下载对应自己内核版本的版本 $ apt-get install linux-source-[version-对应的版本]
安装好后,在 /usr/src/linux-source-[version]
下有:
-
Makefile:这个文件是整个源代码树的顶层 makefile。它定义了很多实用的变量和规则,比如默认的 gcc 编译标记。
-
Documentation/:这个目录中包含很多关于配置内核、运行 ramdisk 等任务的实用信息(但通常是过时的)。不过,与不同配置选项相应的帮助条目并不在这里 —— 它们在每个源代码目录的 Kconfig 文件中。
-
arch/:所有与体系结构相关的代码都在这个目录以及 include/asm- 目录中。在此目录中,每种体系结构都有自己的目录。例如,用于基于 PowerPC 的计算机的代码位于 arch/ppc 目录中。在这些目录里,可以找到底层内存管理、中断处理、早期初始化、汇编例程,等等。
-
crypto/:这是内核本身所用的加密 API。
-
drivers/:按照惯例,在此目录的子目录中可以找到运行外围设备的代码。包括视频驱动程序、网卡驱动程序、底层 SCSI 驱动程序,以及其他类似的驱动程序。例如,在 drivers/net 中可以找到大部分网卡驱动程序。将一类驱动程序组合在一起的某些更高层代码,可能会(也可能不会)像底层驱动程序本身那些包含在同一目录中。
-
fs/:通用文件系统的代码(称做 VFS,即 Virtual File System)和各个不同文件系统的代码都可以在这个目录中找到。ext2 文件系统是在 Linux 中最常广泛使用的文件系统之一;在 fs/ext2 中可以找到读取 ext2 格式的代码。并不是所有文件系统都会编译或运行;对某些寻找内核项目的人而言,更生僻的文件系统永远都是理想的候选者。
-
include/:在 .c 文件的开头所包含的大部分头文件都可以在这个目录中找到。 asm- 目录下是与体系结构相关的包含(include )文件。部分内核构建过程创建从 asm 指定 asm- 的符号链接。这样,无需将其固定编码到 .c 文件 #include 就可以获得用于那个体系结构的正确文件。其他目录中包含的是 非-体系结构-相关 的头文件。如果在不只一个 .c 文件中使用了某个结构体、常量或者变量,那么它可能应该放入其中一个头文件中。
-
init/:这个目录中的文件包括 main.c、创建 早期用户空间(early userspace) 的代码,以及其他初始化代码。可以认为 main.c 是内核“粘合剂(glue)”。在下一部分将深入讨论 main.c。早期用户空间提供了 Linux 内核引导起来时所需要的功能,而这些功能并不需要在内核本身运行。
-
ipc/:IPC 的意思是 进程间通信(interprocess communication)。它包含了共享内存、信号量以及其他形式 IPC 的代码。
-
kernel/:不适合放在任何其他位置的通用内核级代码位于此处。这里有高层系统调用代码,以及 printk() 代码、调度程序、信号处理代码,等等。文件名包含很多信息,所以可以使用 ls kernel/,并非能常准确地猜到每个文件的功能。
-
lib/:这里是对所有内核代码都通用的实用例程。常见的字符串操作、调试例程,以及命令行解析代码都位于此处。
-
mm/:这个目录中是高层次内核管理代码。联合使用这些例程以及底层的与体系结构相关的例程(通常位于 arch//mm/ 目录中)来实现虚拟内存(Virtual memory,VM)。在这里会完成早期内存管理(在内存子系统完全建立起来之前需要它),以及文件的内存映射、页高速缓存管理、内存分配、RAM 中页的清除(还有很多其他事情)。
-
net/:这里是高层网络代码。底层网络驱动程序与此层次代码交换数据包,这个层次的代码可以根据数据包将数据传递给用户层应用程序,或者丢弃数据,或者在内核中使用它。net/core 包含大部分不同的网络协议都可以使用的代码,和某些位于 net/ 目录本身中的文件一样。特定的网络协议在 net/ 的子目录下实现。例如,在 net/ipv4 目录中可以找到 IP(版本 4)代码。
-
scripts/:这个目录中包含的脚本可用于内核的构建,但并不将任何代码加入到内核本身之中。例如,各种配置工具可以将它们的文件放在这里。
-
security/:在这里可以找到不同 Linux 安全模型的代码,比如 NSA Security-Enhanced Linux 以及套接字和网络安全钩子函数(hooks),以及其他安全选项。
-
sound/:这里放置的是声卡驱动程序和其他与声音相关的代码。
-
usr/:此目录中的代码用于构建包含 root 文件系统映像的 cpio-格式 的归档文件,用于早期用户空间。
-
init/main.c 文件是整个 Linux 内核的中央联结点。每种体系结构都会执行一些底层设置函数,然后执行名为 start_kernel 的函数(在 init/main.c 中可以找到这个函数)。
##执行
代码的执行顺序大致如下:
Architecture-specific set-up code (in arch//*)
|
v
The function start_kernel() (in init/main.c)
|
v
The function init() (in init/main.c)
|
v
The user level "init" program
更详细地讲,发生的事情是:
-
执行体系结构相关的设置代码:
- 如果需要,解压缩并移动内核代码本身
- 初始化硬件
- 这可能包括底层内存管理的设置
- 将控制权转交给函数 start_kernel()
-
start_kernel() 去执行以下事情(以及其他事情):
- 打印内核版本和命令行
- 启动控制台输出
- 启用中断
- 校准延迟循环
-
调用 rest_init(),这个函数会:
- 启动一个内核线程来运行 init() 函数
- 进入空闲循环
-
init():
- 启动其他处理器(在 SMP 机器上)
- 启动设备子系统
- 挂载 root 文件系统
- 释放不使用的内核内存
- 运行 /sbin/init(或者 /etc/init,或者…)
此时,用户级 init 程序正在运行;它将完成启动网络设备并在控制台上运行 getty (登录程序)等任务。
加入自己的 printk (类似标准库中的printf函数,由于内核不能依赖标准库运行,只能另外编写一个同样功能的函数),并观察那个子系统的 printk 相对于自己的 printk 何时出现,就可以指出那个子系统是在 start_kernel() 中还是在 init() 中初始化的。例如,如果想要知道 ALSA 声音系统何时被初始化,那么将 printk 加入到 start_kernel() 和 init() 的起始处,然后找到“Advanced Linux Sound Architecture […]” 相对于您的 printk 在何处打印出来。
##编写
一个完整的内核模块如:
#include <linux/init.h>;
#include <linux/module.h>;
MODULE_LICENSE("Dual BSD/GPL");
static int hello_init(void)
{
printk(KERN_ALERT "Hello, world\n");
return 0;
}
static void hello_exit(void)
{
printk(KERN_ALERT"Goodbye, cruel world\n");
}
module_init(hello_init);
module_exit(hello_exit);
在模块被装载时,hello_init函数将被调用;
在模块被卸载时,hello_exit函数将被调用;
内核构造系统总是使用看起来很奇怪的GNU make 的扩展语法
为了编译hello 模块的Makefile的写法:
# 如果已定义了KERNELRELEASE,则说明是从内核构造系统调用的,
# 因此可利用其内建语句:obj-m := hello.o
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
# 该语句使用了GNU make的扩展语法,说明了有一个模块要从
# 目标文件hello.o中构造,而从该目标文件中构造的模块名称
# 为hello.ko。如果我们要构造的模块名称为modules.ko,并由
# 两个源文件file1.c & file2.c生成,则这里应该写为:
# obj-m := module.o
# module-objs := file1.o file2.o
# 否则,是直接从命令行调用的,此时要调用内核构造系统。
else
KERNELDIR ?= /usr/src/linux-headers-$(shell uname -r)
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD)
# 此命令首先改变目录到-C选项指定的位
# 置(即内核源代码目录),其中保存有
# 内核的顶层Makefile文件。 M=选项让
# 该Makefile在构造modules目标之前返
# 回到模块源代码目录。然后,modules目
# 标指向obj-m变量中设定的模块,此例
# 中,我们设置成了hello.o
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
endif
在一个典型的构造过程中,该Makefile将被读取两次。
当从命令行执行make命令时,该Makefile将会被调用,此时是第一次读取该Makefile,变量KERNELRELEASE没有设置,ifneq条件不满足,所以会运行else分支,根据变量KERNELDIR指定的路径找到内核源码树。接着第二次运行make命令,以便运行内核构造系统,在第二次读取该Makefile文件时,它设置了obj-m,而内核的Makefile真正负责构造此模块。
Ubuntu 桌面版可能不能很好的打印内核输出,解决方法是在终端中
$ dmesg -c 清除一下系统的开机信息
然后
$ insmod helloword.ko
$ dmesg
就可以出现信息了。信息多了就用dmesg -c来清除一下就好了。开两个终端可能会更方便一点。
分配和释放设备编号
常用的就这三个函数:
#include <linux/fs.h>;
>< first 要分配的设备编号的起始范围
>> count 连续设备编号的个数
>> name 指向关联的设备
<< 成功为 0 ;错误为负码
== 获取一个或多个设备编号
int register_chrdev_region(dev_t first , unsigned int count , char *name);
<< dev 获取到的第一个编号
>< firstminor 请求的第一个次设备编号
>> count 连续设备编号的个数
>> name 指向关联的设备
== 动态获取一个或多个设备编号
int alloc_chrdev_region(dev_t *dev , unsigned int firstminor, unsigned int count ,char *name);
>> first 要释放的设备编号的起始范围
>> count 连续设备编号的个数
== 释放一个或多个设备编号
void unregister_chrdev_region(dev_t first , unsigned int count );
##后注
下载了《linux设备驱动程序》的配套代码,编译有错误,根据网上的方法修改后,编译成功。
跳转到root账户运行 scull 的脚本,运行成功。