没有合适的资源?快使用搜索试试~ 我知道了~
资源推荐
资源详情
资源评论
深入浅出Linux设备驱动编程之引言
目前,Linux 软件工程师大致可分为两个层次:
(1)Linux 应用软件工程师(Application Software Engineer):主要利用 C 库函数和 Linux API 进行应
用软件的编写;
(2)Linux 固件工程师(Firmware Engineer):主要进行 Bootloader、Linux 的移植及 Linux 设备驱动
程序的设计。
一般而言,固件工程师的要求要高于应用软件工程师的层次,而其中的 Linux 设备驱动编程又是 Linux
程序设计中比较复杂的部分,究其原因,主要包括如下几个方面:
(1)设备驱动属于 Linux 内核的部分,编写 Linux 设备驱动需要有一定的 Linux 操作系统内核基础;
(2)编写 Linux 设备驱动需要对硬件的原理有相当的了解,大多数情况下我们是针对一个特定的嵌入
式硬件平台编写驱动的;
(3)Linux 设备驱动中广泛涉及到多进程并发的同步、互斥等控制,容易出现 bug;
(4)由于属于内核的一部分,Linux 设备驱动的调试也相当复杂。
目前,市面上的Linux 设备驱动程序参考书籍非常稀缺,少有的经典是由 Linux 社区的三位领导者
Jonathan Corbet、Alessandro Rubini、Greg Kroah-Hartman 编写的《Linux Device Drivers》(目前该书已经出
版到第 3 版,中文译本由中国电力出版社出版)。该书将 Linux 设备驱动编写技术进行了较系统的展现,
但是该书所列举实例的背景过于复杂,使得读者需要将过多的精力投放于对例子背景的理解上,很难完全
集中精力于 Linux 驱动程序本身。往往需要将此书翻来覆去地研读许多遍,才能有较深的体会。
(《Linux Device Drivers》中英文版封面)
本文将仍然秉承《Linux Device Drivers》一书以实例为主的风格,但是实例的背景将非常简单,以求
使读者能将集中精力于 Linux 设备驱动本身,理解 Linux 内核模块、Linux 设备驱动的结构、Linux 设备驱
动中的并发控制等内容。另外,与《Linux Device Drivers》所不同的是,针对设备驱动的实例,本文还给
出了用户态的程序来访问该设备,展现设备驱动的运行情况及用户态和内核态的交互。相信阅读完本文将
为您领悟《Linux Device Drivers》一书中的内容打下很好的基础。
本文中的例程除引用的以外皆由笔者亲自调试通过,主要基于的内核版本为 Linux 2.4,例子要在其他
内核上运行只需要做少量的修改。
构建本文例程运行平台的一个较好方法是:在 Windows 平台上安装 VMWare 虚拟机,并在 VMWare 虚拟
机上安装 Red Hat。注意安装的过程中应该选中"开发工具"和"内核开发"二项(如果本文的例程要在特定的
嵌入式系统中运行,还应安装相应的交叉编译器,并包含相应的 Linux 源代码),如下图:
深入浅出Linux设备驱动编程之内核模块
Linux 设备驱动属于内核的一部分,Linux 内核的一个模块可以以两种方式被编译和加载:
(1)直接编译进 Linux 内核,随同 Linux 启动时加载;
(2)编译成一个可加载和删除的模块,使用 insmod 加载(modprobe 和 insmod 命令类似,但依赖于
相关的配置文件),rmmod 删除。这种方式控制了内核的大小,而模块一旦被插入内核,它就和内核其他
部分一样。
下面我们给出一个内核模块的例子:
#include <linux/module.h> //所有模块都需要的头文件
#include <linux/init.h> // init&exit 相关宏
MODULE_LICENSE("GPL");
static int __init hello_init (void)
{
printk("Hello module init\n");
return 0;
}
static void __exit hello_exit (void)
{
printk("Hello module exit\n");
}
module_init(hello_init);
module_exit(hello_exit);
分析上述程序,发现一个 Linux 内核模块需包含模块初始化和模块卸载函数,前者在 insmod 的时候运
行,后者在 rmmod 的时候运行。初始化与卸载函数必须在宏 module_init 和 module_exit 使用前定义,否则
会出现编译错误。
程序中的 MODULE_LICENSE("GPL")用于声明模块的许可证。
如果要把上述程序编译为一个运行时加载和删除的模块,则编译命令为:
gcc -D__KERNEL__ -DMODULE -DLINUX -I /usr/local/src/linux2.4/include -c -o hello.o hello.c
由此可见,Linux 内核模块的编译需要给 gcc 指示-D__KERNEL__ -DMODULE -DLINUX 参数。-I 选项跟
着 Linux 内核源代码中 Include 目录的路径。
下列命令将可加载 hello 模块:
insmod ./hello.o
下列命令完成相反过程:
rmmod hello
如果要将其直接编译入 Linux 内核,则需要将源代码文件拷贝入 Linux 内核源代码的相应路径里,并修
改 Makefile。
我们有必要补充一下 Linux 内核编程的一些基本知识:
内存
在Linux 内核模式下,我们不能使用用户态的 malloc()和 free()函数申请和释放内存。进行内核编程时,
最常用的内存申请和释放函数为在 include/linux/kernel.h 文件中声明的 kmalloc()和 kfree(),其原型为:
void *kmalloc(unsigned int len, int priority);
void kfree(void *__ptr);
kmalloc 的 priority 参数通常设置为 GFP_KERNEL,如果在中断服务程序里申请内存则要用 GFP_ATOMIC
参数,因为使用 GFP_KERNEL 参数可能会引起睡眠,不能用于非进程上下文中(在中断中是不允许睡眠
的)。
由于内核态和用户态使用不同的内存定义,所以二者之间不能直接访问对方的内存。而应该使用 Linux
中的用户和内核态内存交互函数(这些函数在 include/asm/uaccess.h 中被声明):
unsigned long copy_from_user(void *to, const void *from, unsigned long n);
unsigned long copy_to_user (void * to, void * from, unsigned long len);
copy_from_user、copy_to_user 函数返回不能被复制的字节数,因此,如果完全复制成功,返回值为 0。
include/asm/uaccess.h 中定义的 put_user 和 get_user 用于内核空间和用户空间的单值交互(如 char、int、
long)。
这里给出的仅仅是关于内核中内存管理的皮毛,关于 Linux 内存管理的更多细节知识,我们会在本文
第 9 节《内存与 I/O 操作》进行更加深入地介绍。
输出
在内核编程中,我们不能使用用户态 C 库函数中的 printf()函数输出信息,而只能使用 printk()。但是,
内核中 printk()函数的设计目的并不是为了和用户交流,它实际上是内核的一种日志机制,用来记录下日志
信息或者给出警告提示。
每个 printk 都会有个优先级,内核一共有 8 个优先级,它们都有对应的宏定义。如果未指定优先级,内核
会选择默认的优先级 DEFAULT_MESSAGE_LOGLEVEL。如果优先级数字比 int console_loglevel 变量小的
话,消息就会打印到控制台上。如果 syslogd 和 klogd 守护进程在运行的话,则不管是否向控制台输出,消
息都会被追加进/var/log/messages 文件。klogd 只处理内核消息,syslogd 处理其他系统消息,比如应用程
序。
模块参数
2.4 内核下,include/linux/module.h 中定义的宏 MODULE_PARM(var,type) 用于向模块传递命令行参数。var
为接受参数值的变量名,type 为采取如下格式的字符串[min[-max]]{b,h,i,l,s}。min 及 max 用于表示当参数
为数组类型时,允许输入的数组元素的个数范围;b:byte;h:short;i:int;l:long;s:string。
在装载内核模块时,用户可以向模块传递一些参数:
insmod modname var=value
如果用户未指定参数,var 将使用模块内定义的缺省值。
深入浅出Linux设备驱动之字符设备驱动程序
Linux 下的设备驱动程序被组织为一组完成不同任务的函数的集合,通过这些函数使得 Windows 的设备操
作犹如文件一般。在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬
件设备进行操作,如 open ()、close ()、read ()、write () 等。
Linux 主要将设备分为二类:字符设备和块设备。字符设备是指设备发送和接收数据以字符的形式进行;而
块设备则以整个数据缓冲区的形式进行。字符设备的驱动相对比较简单。
下面我们来假设一个非常简单的虚拟字符设备:这个设备中只有一个 4 个字节的全局变量 int
global_var,而这个设备的名字叫做"gobalvar" 。对"gobalvar"设备的读写等操作即是对其中全局变量
global_var 的操作。
驱动程序是内核的一部分,因此我们需要给其添加模块初始化函数,该函数用来完成对所控设备的初
始化工作,并调用 register_chrdev() 函数注册字符设备:
static int __init gobalvar_init(void)
{
if (register_chrdev(MAJOR_NUM, " gobalvar ", &gobalvar_fops))
{
//…注册失败
}
else
{
//…注册成功
}
}
其中,register_chrdev 函数中的参数 MAJOR_NUM 为主设备号,"gobalvar"为设备名,gobalvar_fops 为
包含基本函数入口点的结构体,类型为 file_operations。当 gobalvar 模块被加载时,gobalvar_init 被执行,
它将调用内核函数 register_chrdev,把驱动程序的基本入口点指针存放在内核的字符设备地址表中,在用户
进程对该设备执行系统调用时提供入口地址。
与模块初始化函数对应的就是模块卸载函数,需要调用 register_chrdev()的"反函数" unregister_chrdev():
static void __exit gobalvar_exit(void)
{
if (unregister_chrdev(MAJOR_NUM, " gobalvar "))
{
//…卸载失败
}
else
{
//…卸载成功
}
}
随着内核不断增加新的功能,file_operations 结构体已逐渐变得越来越大,但是大多数的驱动程序只是
利用了其中的一部分。对于字符设备来说,要提供的主要入口有:open ()、release ()、read ()、write ()、ioctl
()、llseek()、poll()等。
open()函数 对设备特殊文件进行 open()系统调用时,将调用驱动程序的 open () 函数:
int (*open)(struct inode * ,struct file *);
其中参数 inode 为设备特殊文件的 inode (索引结点) 结构的指针,参数 file 是指向这一设备的文件结构
的指针。open() 的主要任务是确定硬件处在就绪状态、验证次设备号的合法性( 次设备号可以用
MINOR(inode-> i - rdev) 取得)、控制使用设备的进程数、根据执行情况返回状态码(0 表示成功,负数表示
存在错误) 等;
release()函数 当最后一个打开设备的用户进程执行 close ()系统调用时,内核将调用驱动程序的 release
() 函数:
void (*release) (struct inode * ,struct file *) ;
release 函数的主要任务是清理未结束的输入/输出操作、释放资源、用户自定义排他标志的复位等。
read()函数 当对设备特殊文件进行 read() 系统调用时,将调用驱动程序 read() 函数:
ssize_t (*read) (struct file *, char *, size_t, loff_t *);
用来从设备中读取数据。当该函数指针被赋为 NULL 值时,将导致 read 系统调用出错并返回-EINVAL
("Invalid argument,非法参数")。函数返回非负值表示成功读取的字节数(返回值为"signed size"数据类
型,通常就是目标平台上的固有整数类型)。
globalvar_read 函数中内核空间与用户空间的内存交互需要借助第 2 节所介绍的函数:
static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off)
{
…
copy_to_user(buf, &global_var, sizeof(int));
…
}
剩余56页未读,继续阅读
资源评论
yunux
- 粉丝: 44
- 资源: 5
上传资源 快速赚钱
- 我的内容管理 展开
- 我的资源 快来上传第一个资源
- 我的收益 登录查看自己的收益
- 我的积分 登录查看自己的积分
- 我的C币 登录后查看C币余额
- 我的收藏
- 我的下载
- 下载帮助
安全验证
文档复制为VIP权益,开通VIP直接复制
信息提交成功