LINUX设备驱动程序入门

  cheney

##内核代码库
要想为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

更详细地讲,发生的事情是:

  1. 执行体系结构相关的设置代码:

    1. 如果需要,解压缩并移动内核代码本身
    2. 初始化硬件
    3. 这可能包括底层内存管理的设置
    4. 将控制权转交给函数 start_kernel()
  2. start_kernel() 去执行以下事情(以及其他事情):

    1. 打印内核版本和命令行
    2. 启动控制台输出
    3. 启用中断
    4. 校准延迟循环
  3. 调用 rest_init(),这个函数会:

    1. 启动一个内核线程来运行 init() 函数
    2. 进入空闲循环
  4. init():

    1. 启动其他处理器(在 SMP 机器上)
    2. 启动设备子系统
    3. 挂载 root 文件系统
    4. 释放不使用的内核内存
    5. 运行 /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 &amp; 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 的脚本,运行成功。