Linux设备驱动开发入门(上)

知识准备
要开发Linux设备驱动,需要掌握以下知识:
• C编程 需要掌握深入一些的C语言知识,比如,指针的使用,位处理函数,等。
• 微处理器编程 需要理解微机的内部工作原理:存贮器地址,中断,等。这些内容对一个汇编程序员应该比较熟悉。
Linux下有好几种不同的设备。为简单起见,本文只涉及以模块形式加载的字符设备。使用2.6.x的内核。(特别是Debian Sarge使用的2.6.8内核。)


用户空间和内核空间
当你开发设备驱动时,需要理解“用户空间”和内核空间之间的区别。

 • 内核空间 :Linux操作系统,特别是它的内核,用一种简单而有效的方法管理机器的硬件,给用户提供一个简捷而统一的编程接口。同样的,内核,特别是它的设备驱动程序,是连接最终用户/程序员和硬件的一坐桥或者说是接口。任何子程序或者函数只要是内核的一部分(例如:模块,和设备驱动),那它也就是内核空间的一部分。
• 用户空间. 最终用户的应用程序,像UNIX的shell或者其它的GUI的程序(例如,gedit),都是用户空间的一部分。很显然,这些应用程序需要和系统的硬件进行交互。但是,他们不是直接进行,而是通过内核支持的函数进行。
它们的关系可以通过下图表示:

图1: 应用程序驻留在用户空间, 模块和设备驱动驻留在内核空间

用户空间和内核空间之间的接口函数
内核在用户空间提供了很多子程序或者函数,它们允许用户应用程序员和硬件进行交互。通常,在UNIX或者Linux系统中,这种交互是通过函数或者子程序进行的以便文件的读和写操作。这是因为从用户的视角看,UNIX的设备就是一个个文件。
从另一方面看,在Linux内核空间同样提供了很多函数或者子程序以在底层直接地对硬件进行操作,并且允许从内核向用户空间传递信息。
通常,用户空间的每个函数(用于使用设备或者文件的),在内核空间中都有一个对应的功能相似并且可将内核的信息向用户传递的函数。这种关系可从下表看出来。目前这个表是空的,在我们后面每个表项都会填入对应的函数。


内核空间和硬件设备之间的接口函数


在内核空间同样有可以控制设备或者在内核和硬件之间交换信息的函数。表2解释了这些概念。同样的,这个表将在介绍到相应内容时填写上。

第一个驱动:在用户空间加载和卸载驱动
这一节将向你展示如何开发你的第一个Linux设备驱动,该驱动作为一个内核模块存在。
首先,写一个文件名为nothing.c的文件,代码如下:
<nothing.c> =
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");

内核从2.6.x开始,编译模块变得稍微复杂些。首先,你需要有一份完整的,编译了的内核源代码树。如果你使用的是DebianSarge系统,你可以按照附录B(在本文末尾)的步骤进行操作。
在以下的内容里,假设你使用的是2.6.8内核。接下来,你需要撰写一个Makefile。本例子所用的makefile文件名称为Makefile,内容如下:
<Makefile1> =
obj-m := nothing.o
和之前版本的内核不同,你需要使用和你当前系统所用内核版本相同的代码来编译将要加载和使用的模块。编译该模块,可以使用以下命令:
$ make -C /usr/src/kernel-source-2.6.8 M=`pwd` modules
这个非常简单的模块在加载之后,将属于内核空间,是内核空间的一部分。
在用户空间,你可以以root账号加载该模块,命令如下:
# insmod nothing.ko
insmod命令用于将模块安装到内核里。但是这个特殊的模块不常用。
要查看模块是否已经安装完成,可以通过查看所有已安装模块来进行:
# lsmod
最后,模块可以通过以下命令从内核中移除:
# rmmod nothing
同样的,使用lsmod命令,可以用于验证该模块已不在内核中。

“hello world”驱动:在内核空间加载和移除驱动


当一个模块设备驱动被加载到内核时,一些通常要做的事情包括:设备复位,初始化RAM,初始化中断,初始化输入/输出端口,等。
这些动作在内核空间进行,通过下面将介绍的两个函数进行:module_init 和module_exit;它们和用户空间的用于安装和卸载模块的命令insmod 和 rmmod对应。也可以说,用户空间的命令insmod 和rmmod使用内核空间的函数 module_init和module_exit进行。
我们通过一个最基本的hello world 程序,看实际的例子:
<hello.c> =
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("Dual BSD/GPL");
static int hello_init(void) {
printk("<1> Hello world!/n");
return 0;
}
static void hello_exit(void) {
printk("<1> Bye, cruel world/n");
}
module_init(hello_init);
module_exit(hello_exit);
实际的函数hello_init和hello_exit可以用任何其他名称。但是为了使系统能够正确的识别它们是加载和卸载函数,需要把它们作为module_init和module_exit的参数。
以上代码里还包括了printk函数。它和我们非常熟悉的printf函数很相似,只是它只在内核内有效。符号<1> 表示该消息的优先级(数字)。这样就可以通过内核的日志文件里看到该消息,该消息也会在系统控制台中显示。
这个模块可以使用和之前那个相同的命令进行编译,当然前提是把它的名字加在Makefile文件里。
<Makefile2> =
obj-m := nothing.o hello.o
本文中,把写makefile的事情留给读者自行练习。
当模块被加载或是卸除时,在printk声明里的消息将打印在系统控制台上。如果这个消息没有在控制台上显示,可以通过dmesg命令,或者查看系统的日志文件cat /var/log/syslog命令看到。

一个完整的驱动“memory”:驱动的初始化


现在我开始构建一个完整的设备驱动:memory.c。可以从这个设备读取和写入一个字符。虽然这个设备没时么用途,但提供了个很好的样例,它是一个完整的驱动;很容易实现,因为它不操作实际的硬件设备(它是电脑内部模拟的硬件)。
要开发驱动,一些在设备驱动中很常见的#include声明,需要首先要加进来:
<memory initial> =
/* Necessary includes for device drivers */
#include <linux/init.h>
#include <linux/config.h>
#include <linux/module.h>
#include <linux/kernel.h> /* printk() */
#include <linux/slab.h> /* kmalloc() */
#include <linux/fs.h> /* everything... */
#include <linux/errno.h> /* error codes */
#include <linux/types.h> /* size_t */
#include <linux/proc_fs.h>
#include <linux/fcntl.h> /* O_ACCMODE */
#include <asm/system.h> /* cli(), *_flags */
#include <asm/uaccess.h> /* copy_from/to_user */
MODULE_LICENSE("Dual BSD/GPL");
/* Declaration of memory.c functions */
int memory_open(struct inode *inode, struct file *filp);
int memory_release(struct inode *inode, struct file
*filp);

ssize_t memory_read(struct file *filp, char *buf, size_t
count, loff_t *f_pos);
ssize_t memory_write(struct file *filp, char *buf,
size_t count, loff_t *f_pos);
void memory_exit(void);
int memory_init(void);
/* Structure that declares the usual file */
/* access functions */
struct file_operations memory_fops = {
read: memory_read,
write: memory_write,
open: memory_open,
release: memory_release
};
/* Declaration of the init and exit functions */
module_init(memory_init);
module_exit(memory_exit);
/* Global variables of the driver */
/* Major number */
int memory_major = 60;
/* Buffer to store data */
char *memory_buffer;
在#include之后,就是即将定义的函数的声明。通用的用于处理文件的函数在file_operations里声明。这些在过后会讲解。接下来是初始化和卸载函数--在模块加载和卸载时执行--对内核声明。最后,是该驱动的全局变量声明:一个是“主设备号”,另外一个是内存指针,memory_buffer,将用于存储该驱动的数据。


“memory”驱动:连接到设备


在UNIX和Linux中,设备可以用和文件一样的方式从用户空间访问。这些设备文件通常在/dev目录下。
要把一般文件和内核模块链接在一起需要两个数据:主设备号和从设备号。主设备号用于内核把文件和它的驱动链接在一起。从设备号用于设备内部使用,为简单起见,本文并不对它进行解释。
需要创建一个文件(该设备文件用于和设备驱动操作),
# mknod /dev/memory c 60 0
其中,c说明创建的是字符设备,60是主设备号,0是从设备号。
在这个驱动里,register_chrdev函数用于在内核空间,把驱动和/dev下设备文件链接在一起。它又三个参数:主设备号,模块名称和一个file_operations结构的指针。在安装模块时将调用该函数:
<memory init module> =
int memory_init(void) {
int result;
/* Registering device */
result = register_chrdev(memory_major, "memory",
&memory_fops);
if (result < 0) {
printk(
"<1>memory: cannot obtain major number %d/n",
memory_major);
return result;
}
/* Allocating memory for the buffer */
memory_buffer = kmalloc(1, GFP_KERNEL);
if (!memory_buffer) {
result = -ENOMEM;
goto fail;
}
memset(memory_buffer, 0, 1);
printk("<1>Inserting memory module/n");
return 0;
fail:
memory_exit();
return result;
}
以上代码使用了kmalloc函数。这个函数工作在内核空间,用于为该驱动程序的缓冲区分配内存。它和我们熟悉的malloc函数很相似。最后,如果注册主设备号或者分配内存失败,模块将退出。


“memory”驱动:卸载驱动


为通过memory_exit函数卸载模块,需要定义unregsiter_chrdev函数。这将释放驱动之前向内核申请的主设备号。
<memory exit module> =
void memory_exit(void) {
/* Freeing the major number */
unregister_chrdev(memory_major, "memory");
/* Freeing buffer memory */
if (memory_buffer) {
kfree(memory_buffer);
}
printk("<1>Removing memory module/n");
}
为了完全的卸载该驱动,缓冲区也需要通过该函数进行释放。


“momory”驱动:像文件一样打开设备


内核空间打开文件的函数是open,和用户空间打开文件的函数fopen对应:在file_operations结构里,用于调用register_chrdev。在本例里,是memory_open函数。它又几个参数:一个inode结构,该结构向内核发送主设备号和从设备号的信息;另外是一个file结构,用于说明,该设备文件允许
哪些操作。所有这些函数在本文中都未做深入的讲解。
当设备文件被打开后,通常就需要初始化驱动的各个变量,对设备进行复位。但在本例中,这些操作都没进行。
memory_open函数定义如下:
<memory open> =
int memory_open(struct inode *inode, struct file *filp)
{
/* Success */
return 0;
}

“monory”驱动:像文件一样关闭设备


在内核空间里,和用户空间里关闭文件的fclose对应的函数是release:它也是file_operations结构体的成员,用于调用register_chrdev。本例中,它是函数memory_release,和上面的相似,它也有inode和file两个参数。当设备文件关闭后,通常需要释放该设备使用的内存,释放各种操作该设备相关的变量。但是,为简单起见,例子里没有进行这些操作。
memory_release函数定义如下:
<memory release> =
int memory_release(struct inode *inode, struct file
*filp) {
/* Success */
return 0;
}

设备驱动事件和在内核空间和用户空间之间实现该功能的函数
Events User functionsKernel functions
Load module insmod module_init()
Open device fopen file_operations: open
Read device
Write device
Close device fclose file_operations: release
Remove modulermmod module_exit()


"memory"驱动:读取设备


和用户空间函数fread类似,内核空间里,读取设备文件使用read函数:read是file_operations的成员,用于调用register_chrdev。
本例中,是memory_read函数。它的参数有:一个file结构;一个缓冲区(buf),用户空间的fread函数将从该缓冲区读数据;一个记录要传输的字节数
量的计数器(count),它和用户空间的fread使用的计数器值相同;最后一个
参数(f_pos)指示从哪里开始读取该设备文件。
本例中,memory_read函数通过copy_to_user函数从驱动的缓冲区(memory_buffer)向用户空间传送一个简单的字节:
<memory read> =
ssize_t memory_read(struct file *filp, char *buf,
size_t count, loff_t *f_pos) {
/* Transfering data to user space */
copy_to_user(buf,memory_buffer,1);
/* Changing reading position as best suits */
if (*f_pos == 0) {
*f_pos+=1;
return 1;
} else {
return 0;
}
}
设备文件的读取位置(f_pos)也改变了。如果起始点是文件的开头,那么f_pos的值将增加1,如果要读取的字节读取正常,则返回值为1。如果读取位置不是文件开头,则是文件的末尾,返回值将是0,(因为文件只存储了1个字节)。

“memory”驱动:向设备写数据


和用户空间里写文件的fwrite对应,内核空间里是write:write是file_operations的成员,用于调用register_chrdev。本例中是memory_write函数,它有如下几个参数:一个file结构;buf,一个缓冲区,用户空间函数fwrite将向该该缓冲区写数据;count,统计将传送的字节数的计数器,和用户空间函数fwrite的计数器有相同的数值;最后是f_pos,指示从哪里开始写文件。
<memory write> =
ssize_t memory_write( struct file *filp, char *buf,
size_t count, loff_t *f_pos) {
char *tmp;
tmp=buf+count-1;
copy_from_user(memory_buffer,tmp,1);
return 1;
}
本例中,函数copy_from_user从用户空间传送数据到内核空间。


完整的“memory”驱动


把以上各部分代码整合起来,一个完整的驱动就完成了:
<memory.c> =
<memory initial>
<memory init module>
<memory exit module>
<memory open>
<memory release>
<memory read>
<memory write>
在该模块使用之前,你需要和刚才那个模块一样,进行模块编译。编译好后,用以下命令进行加载:
# insmod memory.ko
并且最后是取出设备文件的保护:
# chmod 666 /dev/memory
如果以上步骤一切正常,此时你就可以向设备/dev/memory写一串字符,并且它将把你写入的最后一个字符存储起来。你可以按下例操作:
$ echo -n abcdef >/dev/memory
使用cat检查设备的内容:
$ cat /dev/memory
存储的字符将不会改变,直到该字符被覆盖,或者是该模块被卸载。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值