第三类法庭国语10:linux驱动入门

来源:百度文库 编辑:偶看新闻 时间:2024/04/28 23:27:17

linux驱动入门(转)

用来防止用户程序直接访问内核中关键性数据结构和硬件设备是驱动程序的主要目的之一,所以,一个设计精良的驱动通常应该隐藏了硬件的复杂性和多变性。例如,一个程序写数据到磁盘时,只需要打开一个文件并执行写命令即可,而不必了解磁盘中的每个扇区的大小是512字节或者是1024字节,细节性的问题则交由驱动程序处理。此外,驱动程序还可以隐藏硬件的多变性(可能来自于不同厂家,甚至同一厂家不同型号)并给用户提供一个统一的访问接口。这也是Unix/Linux信条之一“一切皆文件”的赖以存在的基础。1、可加载模块(loadable module)Linux采取了“宏内核”的结构,并附带一个设计精良的接口以实现在系统运行时动态的加载或移除驱动模块。这种富有弹性的设计,为最终用户,甚至也为程序的开发过程带来了极大的便利。基于此功能,在开发驱动的过程中,开发人员不用在每一次驱动程序的修改后重启系统就能对其进行测试。当然,驱动程序也可以以静态方式编译进内核,而且,许多关键性的驱动也需要这样编译。比如,对于无盘工作站而言,由于系统启动最初就需要通过网卡从其它共享文件系统中加载所需的资源,此时必须将网卡驱动静态编译进内核,因为可加载模块都是系统启动后才进行动态加载。通常使用启动脚本装载动态驱动模块,当然,也可以使用相关命令在需要时再进行加载。此外,内核也可以在某个服务需要某个特殊模块时自动请求加载所需的模块。虽然前文中一直称驱动程序为可加载模块,但内核模块并没有确定的术语,硬件驱动(device drivers)、可加载内核模块(LKM,loadable kernel modules)、内核模块(kernel modules)、可加载模块(loadable modules)、驱动模块(driver modules)和模块(modules)等都常用来表示可动态加载进内核的硬件驱动,后文中则不加区别的使用它们。2、硬件驱动结构尽管Linux/Unix驱动程序模块的开发一直处于不断地演进中,但其基本结构并没有太大改变。硬件设备大体上可分为两大类:字符设备和块设备。
  • 字符设备是以串行流式数据序列进行数据存取的设备,字符设备驱动负责实现这种行为;通常字符设备驱动至少需要实现 open、 close、read和 write等系统调用。常见的字符设备如控制台( /dev/console )和串口( /dev/ttyS0 )。
  • 块设备通常是可编址的,其数据存取也通常以固定大小的数据块进行,但数据块的存入位置则可能是随机的。在大部分Unix系统中, 一个块设备传送一个或多个长度经常是512字节(或者其2次幂倍)的整块数据,但Linux允许一次传递任意字节的数据,其跟字符设备的区别仅在于内核内部对数据的管理方式上和驱动程序的接口实现上有所不同。磁盘是常见的块设备。
3、一个驱动程序的例子因为Linux支持可加载式硬件驱动,所以很容易构建出一个关于简易驱动框架来说明驱动程序的结构。下面就是这样一个关于字符型设备驱动程序的例子:

/* Example Minimal Character Device Driver */
#include
static int __init hello_init(void)
{
printk("Hello Device Driver World!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk("Goodbye, Cruel World!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_AUTHOR("Marion");
MODULE_DESCRIPTION("An example of device driver!");
MODULE_LICENSE("GPL");
/* End the hello*/

上面这个例子虽然短小,但却充分具备了让内核加载或卸载以及初始化或退出的程序结构。不同于标准的二进制可执行程序,设备驱动是一种特殊的二进制模块,它不能简单的通过shell执行。2.6系列内核的模块通常是内核对象 (kernel object)格式,这些模块在完成编译后通常以.ko为后缀。创建内核对象步骤和编译选项非常复杂,本文仅描述其大体过程,以帮助读者简单了解内核对象的构建步骤。4、模块构建基础驱动程序的编译必须针对于特定的内核进行。尽管在另一个不同的内核上编译的驱动模块也可以运行,但如果不确定此模块是否依赖于编译时的内核的某些特性时,将会给后来运行过程来带来很大风险。因此,最稳妥的办法还是基于某内核自身的代码树(Source tree)构建内核对象,这也可以保证在开发人员改变了内核的配置后,驱动也会在新配置的内核的基础上进行重建。如果需要在不同的内核上构建内核对象,则必须确保驱动程序构建时的配置所使用的编译选项、引用内核头文件的位置和内核配置选项在驱动实际运行的内核上做了同样的配置。为了基于前文中的例子构建驱动程序,大致要经过以下几个步骤:(1)在内核源码目录中的.../driver/char目录中创建一个名为examples的子目录;
(2)在内核配置文件中添加一个菜单项,以允许在编译内核时可以选择编译examples(编译进内核或编译成模块);
(3)在.../driver/char/Makefile文件中添加一个子目录项examples以对应于前述第二步骤的菜单项;
(4)为.../driver/char/examples目录创建一个makefile文件,在其中为前述第二个步骤中创建的菜单项添加hello.o模块对象以进行编译;
(5)添加驱动程序源码;下面详细描述前面的几个步骤:首先,在内核源码目录的.../driver/char目录中创建examples子目录,而后再创建两个文件:一个是前面例子中的驱动源码,一个是为其创建的makefile文件。makefile文件非常简单,其内容只有如下一行即可:
obj-$(CONFIG_EXAMPLES) += hello.o
添加菜单项至内核配置工具的过程可能稍有些绕。首先需要在.../driver/char/Kconfig文件中添加一个"config"项以启用前文中的examples配置项。添加位置是menu "Character devices"一行的后面,添加内容如下:config EXAMPLES
tristate "Enable Examples"
default m
---help---
   Enable compilation option for driver examples
而后回到内核目录中,运行内核编译命令make gconfig(需要xwindow的支持)后,可以Device Drivers->Character drivers找到我们添加的“Enable Examples”项,默认为“-”(通过default项指定),即编译为内核模块。如果指定为“对号”则表示以静态方式编译进内核;如果为空,则表示不编译此项。如下图所示:接下来还需要在.../drivers/char/Makefile文件中添加一个选项,以指示内核编译程序在我们选择了 CONFIG_EXAMPLES时会到examples子目录中编译hello1模块。这需要在 “obj-$(CONFIG_IPMI_HANDLER)      += ipmi/”一行附近添加如下行:obj-$(CONFIG_EXAMPLES)          += examples/至此,此示例驱动构建基础结构已经完成,而且会在内核编译过程中自动选择此项进行编译了。在执行完前面的"make gconfig"命令后,此时再执行如下命令即可完成驱动模块的编译。# make modules

CHK     include/linux/version.h
CHK     include/linux/utsrelease.h
CC [M] drivers/char/examples/hello.o
Building modules, stage 2.
MODPOST
CC      drivers/char/examples/hello.mod.o
LD [M] drivers/char/examples/hello.ko
如果您的编译过程显示有如上信息,则表示内核模块编译完成。接下来就可以使用如下命令安装刚刚编译完成的内核模块:
# make modules_install
使用此种方式安装时,安装过程会重新安装所有已编译的内核模块,包括此前编译的其它模块,而这并非是必须的。在一个通过标准方式安装的Linux系统上,内核模块通常位于/lib/modules//…之中,其中的即当前系统运行中的内核版本号,并且此目录的结构组织方式跟内核源代码树的结构是类似的。通常使用“make modules_install”命令安装的模块就位于此目录中。因此,在单独安装某个或某些内核模块时,可以通过在此目录中创建跟编译时内核源码树中一样的内核模块驱动相关的目录,并把编译完成的*.ko文件复制到新建的对应目录中来实现。5、加载/卸载内核模块安装完成后,便可以手动加载或卸载这些模块了,这可以使用modprobe实现。我们首先去加载hello模块。# modprobe hello
# tail -1 /var/log/messages
Sep 14 22:06:23 localhost kernel: Hello Device Driver World!
此模块在加载时会调用模块初始化函数,程序中使用module_init()宏(macro)来指定的模块初始化函数,如 module_init(hello_init)。在此模块中,初始化函数仅用来打印一行信息至系统日志,信息内容是在hello_init()中定义好的。在实际驱动程序编写中,初始化函数常用来执行资源分配及硬件设备初始化。接下来可以使用lsmod命令以格式化列表的形式显示系统中加载的所有模块。如果其中有hello模块出现则表示前面的加载是成功的。例如:# lsmod
Module                  Size Used by
hello                   5632 0
ipv6                  274208 18
autofs4                25092 2
i2c_core               25344 1 i2c_piix4
…………
其中Used by一列表示当前模块正在被使用的信息,以及依赖于当前模块的其它模块。如最后一行表示i2c_piix4模块依赖于i2c_core模块。内核模块的卸载可以通过使用modprobe的-r选项来实现。# modprobe -r hello
# tail -1 /var/log/messages
Sep 14 22:14:33 localhost kernel: Goodbye, Cruel World!
hello模块退出时会调用exit例行函数,这使用module_exit()宏来实现。其工作方式类似前面的加载过程。

如何编写Linux设备驱动程序 - 框架入门序言  Linux是Unix操作系统的一种变种,在Linux下编写驱动程序的原理和思想完全类似 于其他的Unix系统,但它dos或window环境下的驱动程序有很大的区别。在Linux环境下设计驱动程序,思想简洁,操作方便,功能也很强大,但 是支持函数少,只能依赖kernel中的函数,有些常用的操作要自己来编写,而且调试也不方便。本人这几周来为实验室自行研制的一块多媒体卡编制了驱动程 序,获得了一些经验,愿与Linux fans共享,有不当之处,请予指正。  以下的一些文字主要来源于khg,johnsonm的Write linux device driver,Brennan's Guide to Inline Assembly,The Linux A-Z,还有清华BBS上的有关device driver的一些资料. 这些资料有的已经过时,有的还有一些错误,我依据自己的试验结果进行了修正.  一、Linux device driver 的概念  系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的 接口.设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个设备文件, 应用程序可以象操作普通文件一样对硬件设备进行操作.设备驱动程序是内核的一部分,它完成以下的功能:  1.对设备初始化和释放.  2.把数据从内核传送到硬件和从硬件读取数据.  3.读取应用程序传送给设备文件的数据和回送应用程序请求的数据.  4.检测和处理设备出现的错误.  在Linux操作系统下有两类主要的设备文件类型,一种是字符设备,另一种是块设备.字符设 备和块设备的主要区别是:在对字符设备发出读/写请求时,实际的硬件I/O一般就紧接着发生了,块设备则不然,它利用一块系统内存作缓冲区,当用户进程对 设备请求能满足用户的要求,就返回请求的数据,如果不能,就调用请求函数来进行实际的I/O操作.块设备是主要针对磁盘等慢速设备设计的,以免耗费过多的 CPU时间来等待.  已经提到,用户进程是通过设备文件来与实际的硬件打交道.每个设备文件都都有其文件属性 (c/b),表示是字符设备还蔤强樯璞?另外每个文件都有两个设备号,第一个是主设备号,标识驱动程序,第二个是从设备号,标识使用同一个设备驱动程序的 不同的硬件设备,比如有两个软盘,就可以用从设备号来区分他们.设备文件的的主设备号必须与设备驱动程序在登记时申请的主设备号一致,否则用户进程将无法 访问到驱动程序.  最后必须提到的是,在用户进程调用驱动程序时,系统进入核心态,这时不再是抢先式调度.也就是说,系统必须在你的驱动程序的子函数返回后才能进行其他的工作.如果你的驱动程序陷入死循环,不幸的是你只有重新启动机器了,然后就是漫长的fsck.//hehe  读/写时,它首先察看缓冲区的内容,如果缓冲区的数据  如何编写Linux操作系统下的设备驱动程序
  二、实例剖析  我们来写一个最简单的字符设备驱动程序。虽然它什么也不做,但是通过它可以了解Linux的 设备驱动程序的工作原理.把下面的C代码输入机器,你就会获得一个真正的设备驱动程序.不过我的kernel是2.0.34,在低版本的kernel上可 能会出现问题,我还没测试过.//xixi  #define __NO_VERSION__
#include
#include
  char kernel_version [] = UTS_RELEASE;  这一段定义了一些版本信息,虽然用处不是很大,但也必不可少.Johnsonm说所有的驱动程序的开头都要包含,但我看倒是未必.  由于用户进程是通过设备文件同硬件打交道,对设备文件的操作方式不外乎就是一些系统调用,如 open,read,write,close...., 注意,不是fopen, fread,但是如何把系统调用和驱动程序关联起来呢?这需要了解一个非常关键的数据结构:struct file_operations {int (*seek) (struct inode * ,struct file *, off_t ,int);
int (*read) (struct inode * ,struct file *, char ,int);
int (*write) (struct inode * ,struct file *, off_t ,int);
int (*readdir) (struct inode * ,struct file *, struct dirent * ,int);
int (*select) (struct inode * ,struct file *, int ,select_table *);
int (*ioctl) (struct inode * ,struct file *, unsigned int ,unsigned long);
int (*mmap) (struct inode * ,struct file *, struct vm_area_struct *);
int (*open) (struct inode * ,struct file *);
int (*release) (struct inode * ,struct file *);
int (*fsync) (struct inode * ,struct file *);
int (*fasync) (struct inode * ,struct file *,int);
int (*check_media_change) (struct inode * ,struct file *);
int (*revalidate) (dev_t dev);
}
  这个结构的每一个成员的名字都对应着一个系统调用.用户进程利用系统调用在对设备文件进行诸 如read/write操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数. 这是linux的设备驱动程序工作的基本原理.既然是这样,则编写设备驱动程序的主要工作就是编写子函数,并填充file_operations的各个 域.  相当简单,不是吗?  下面就开始写子程序.#include
#include
#include
#include
#include
unsigned int test_major = 0;
static int read_test(struct inode *node,struct file *file,
char *buf,int count)
{
int left;if (verify_area(VERIFY_WRITE,buf,count) == -EFAULT )
return -EFAULT;
for(left = count ; left > 0 ; left--)
{
__put_user(1,buf,1); //有错误应写为:__put_user(1,buf);
buf++;
}
return count;
}
  这个函数是为read调用准备的.当调用read时,read_test()被调用,它把用 户的缓冲区全部写1.buf 是read调用的一个参数.它是用户进程空间的一个地址.但是在read_test被调用时,系统进入核心态.所以不能使用buf这个地址,必须用 __put_user(),这是kernel提供的一个函数,用于向用户传送数据.另外还有很多类似功能的函数.请参考.在向用户空间拷贝数据之前,必须 验证buf是否可用。
  这就用到函数verify_area.static int write_tibet(struct inode *inode,struct file *file,
const char *buf,int count)
{
return count;
}
static int open_tibet(struct inode *inode,struct file *file )
{
MOD_INC_USE_COUNT;
return 0;
}
static void release_tibet(struct inode *inode,struct file *file )
{
MOD_DEC_USE_COUNT;
}
  这几个函数都是空操作.实际调用发生时什么也不做,他们仅仅为下面的结构提供函数指针。struct file_operations test_fops = {
NULL,
read_test,
write_test,
NULL, /* test_readdir */
NULL,
NULL, /* test_ioctl */
NULL, /* test_mmap */
open_test,
release_test, NULL, /* test_fsync */
NULL, /* test_fasync */
/* nothing more, fill with NULLs */
};
  设备驱动程序的主体可以说是写好了。现在要把驱动程序嵌入内核。驱动程序可以按照两种方式编 译。一种是编译进kernel,另一种是编译成模块 (modules),如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且不能动态的卸载,不利于调试,所以推荐使用模块方式。int init_module(void)
{
int result;
result = register_chrdev(0, "test", &test_fops);if (result < 0) {
printk(KERN_INFO "test: can't get major number\n");
return result;
}
if (test_major == 0) test_major = result; /* dynamic */
return 0;
}
  在用insmod命令将编译好的模块调入内存时,init_module 函数被调用。在这里,init_module只做了一件事,就是向系统的字符设备表登记了一个字符设备。register_chrdev需要三个参数,参 数一是希望获得的设备号,如果是零的话,系统将选择一个没有被占用的设备号返回。参数二是设备文件名,参数三用来登记驱动程序实际执行操作的函数的指针。  如果登记成功,返回设备的主设备号,不成功,返回一个负值。void cleanup_module(void)
{
unregister_chrdev(test_major, "test");
}
  在用rmmod卸载模块时,cleanup_module函数被调用,它释放字符设备test在系统字符设备表中占有的表项。  一个极其简单的字符设备可以说写好了,文件名就叫test.c吧。  下面编译  $ gcc -O2 -DMODULE -D__KERNEL__ -c test.c  得到文件test.o就是一个设备驱动程序。  如果设备驱动程序有多个文件,把每个文件按上面的命令行编译,然后  ld -r file1.o file2.o -o modulename.  驱动程序已经编译好了,现在把它安装到系统中去。  $ insmod -f test.o以上编译不好用,使用以下两条语句
echo "obj-m:=test.o" > Makefile
make -C /usr/src/kernel/2.6..../ M=`pwd` modules
最后
insmod -f test.ko
  如果安装成功,在/proc/devices文件中就可以看到设备test,并可以看到它的主设备号。
要卸载的话,运行
  $ rmmod test  下一步要创建设备文件。  mknod /dev/test c major minor  c 是指字符设备,major是主设备号,就是在/proc/devices里看到的。  用shell命令  $ cat /proc/devices | awk "\\$2==\"test\" {print \\$1}" 就可以获得主设备号,可以把上面的命令行加入你的shell script中去。  minor是从设备号,设置成0就可以了。  我们现在可以通过设备文件来访问我们的驱动程序。写一个小小的测试程序。#include
#include
#include
#include
main()
{
int testdev;
int i;
char buf[10];
testdev = open("/dev/test",O_RDWR);if ( testdev == -1 )
{
printf("Cann't open file \n");
exit(0);
}
read(testdev,buf,10);for (i = 0; i < 10;i++)
printf("%d\n",buf[i]);
close(testdev);
}
  编译运行,看看是不是打印出全1 ?  以上只是一个简单的演示。真正实用的驱动程序要复杂的多,要处理如中断,DMA,I/O port等问题。这些才是真正的难点。请看下节,实际情况的处理。  如何编写Linux操作系统下的设备驱动程序  三、设备驱动程序中的一些具体问题  1. I/O Port.  和硬件打交道离不开I/O Port,老的ISA设备经常是占用实际的I/O端口,在linux下,操作系统没有对I/O口屏蔽,也就是说,任何驱动程序都可对任意的I/O口操作,这样就很容易引起混乱。每个驱动程序应该自己避免误用端口。  有两个重要的kernel函数可以保证驱动程序做到这一点。  1)check_region(int io_port, int off_set)  这个函数察看系统的I/O表,看是否有别的驱动程序占用某一段I/O口。  参数1:io端口的基地址,  参数2:io端口占用的范围。  返回值:0 没有占用, 非0,已经被占用。  2)request_region(int io_port, int off_set,char *devname)  如果这段I/O端口没有被占用,在我们的驱动程序中就可以使用它。在使用之前,必须向系统登记,以防止被其他程序占用。登记后,在/proc/ioports文件中可以看到你登记的io口。  参数1:io端口的基地址。  参数2:io端口占用的范围。  参数3:使用这段io地址的设备名。  在对I/O口登记后,就可以放心地用inb(), outb()之类的函来访问了。  在一些pci设备中,I/O端口被映射到一段内存中去,要访问这些端口就相当于访问一段内 存。经常性的,我们要获得一块内存的物理地址。在dos环境下,(之所以不说是dos操作系统是因为我认为DOS根本就不是一个操作系统,它实在是太简 单,太不安全了)只要用段:偏移就可以了。在window95中,95ddk提供了一个vmm 调用 _MapLinearToPhys,用以把线性地址转化为物理地址。但在Linux中是怎样做的呢?  2.内存操作  在设备驱动程序中动态开辟内存,不是用malloc,而是kmalloc,或者用 get_free_pages直接申请页。释放内存用的是 kfree,或free_pages. 请注意,kmalloc等函数返回的是物理地址!而malloc等返回的是线性地址!关于kmalloc返回的是物理地址这一点本人有点不太明白:既然从 线性地址到物理地址的转换是由386cpu硬件完成的,那样汇编指令的操作数应该是线性地址,驱动程序同样也不能直接使用物理地址而是线性地址。但是事实 上kmalloc返回的确实是物理地址,而且也可以直接通过它访问实际的RAM,我想这样可以由两种解释,一种是在核心态禁止分页,但是这好像不太现实; 另一种是linux的页目录和页表项设计得正好使得物理地址等同于线性地址。我的想法不知对不对,还请高手指教。  言归正传,要注意kmalloc最大只能开辟128k-16,16个字节是被页描述符结构占用了。kmalloc用法参见khg.  内存映射的I/O口,寄存器或者是硬件设备的RAM(如显存)一般占用F0000000以上的地址空间。在驱动程序中不能直接访问,要通过kernel函数vremap获得重新映射以后的地址。  另外,很多硬件需要一块比较大的连续内存用作DMA传送。这块内存需要一直驻留在内存,不能被交换到文件中去。但是kmalloc最多只能开辟128k的内存。  这可以通过牺牲一些系统内存的方法来解决。  具体做法是:比如说你的机器由32M的内存,在lilo.conf的启动参数中加上mem=30M,这样linux就认为你的机器只有30M的内存,剩下的2M内存在vremap之后就可以为DMA所用了。  请记住,用vremap映射后的内存,不用时应用unremap释放,否则会浪费页表。  3.中断处理  同处理I/O端口一样,要使用一个中断,必须先向系统登记。int request_irq(unsigned int irq ,void(*handle)(int,void *,struct pt_regs *),unsigned int long flags,const char *device);irq: 是要申请的中断。handle:中断处理函数指针。flags:SA_INTERRUPT 请求一个快速中断,0 正常中断。device:设备名。
  如果登记成功,返回0,这时在/proc/interrupts文件中可以看你请求的中断。  4.一些常见的问题。  对硬件操作,有时时序很重要。但是如果用C语言写一些低级的硬件操作的话,gcc往往会对你 的程序进行优化,这样时序就错掉了。如果用汇编写呢,gcc同样会对汇编代码进行优化,除非你用volatile关键字修饰。最保险的办法是禁止优化。这 当然只能对一部分你自己编写的代码。如果对所有的代码都不优化,你会发现驱动程序根本无法装载。这是因为在编译驱动程序时要用到gcc的一些扩展特性,而 这些扩展特性必须在加了优化选项之后才能体现出来。以上有多出错误:    1、struct file_operations 该结构的版本,由于不同的linux版本,会有出入,具体请参看/usr/src/linux-source/include/linux/fs.h中详细描述。    2、上面给的编译方法,对有的版本来说有问题,编译出来的.o文件,不能被insmod,可以参考使用以下编译方法:              #echo "obj-m:=test.o" > Makefile              #make -c /usr/src/kernels/linux-source/ M=`pwd` modules

    3、得到主设备号的方法也有些小问题:

         $ cat /proc/devices | awk "\\$2==\"test\" {print \\$1}" /*这个不好用*/
        $ cat /proc/devices | awk '{if($2=="test") print $1;}' /*ok*/

    4、还可能存在其他编写上的小错误。