Linux设备驱动程序学习(0) -设备驱动介绍& Hello, world!模块

设备驱动的分类

字符设备:字符(char)设备是个能够像字节流(类似文件)一样被访问的设备。字符设备驱动程序通常至少要实现open、close、read和write系统调用。


块设备:一个块设备驱动程序主要通过传输固定大小的数据来访问设备。块设备和字符设备的区别仅仅在于内核内部管理数据的方式,也就是内核及驱动程序之间的软件接口,而这些不同对用户程序是透明的。在内核中,和字符驱动程序相比,块驱动程序具有完全不同的接口。

网络接口:任何网络事务都经过一个网络接口形成,即一个能够和其他主机交换数据的设备。它可以是个硬件设备,但也可能是个纯软件设备。访问网络接口的方法仍然是给它们分配一个唯一的名字(比如eth0),但这个名字在文件系统中不存在对应的节点。内核和网络设备驱动程序间的通信,完全不同于内核和字符以及块驱动程序之间的通信,内核调用一套和数据包传输相关的函数而不是read、write等。


驱动模块的特点
(1)驱动模块运行在内核空间,运行时不能依赖于任何标准C库等应用层的库、模块,所以在写驱动时所调用的函数只能是作为内核一部分的函数,即使用“EXPORT_SYMBOL”导出的函数。
 insmod使用公共内核符号表来解析模块中未定义的符号。公共内核符号表中包含了所有的全局内核项(即函数和变量的地址),这是实现模块化驱动程序所必须的。
àLinux使用模块层叠技术,我们可以将模块划分为多个层,通过简化每个层可缩短开发周期。如果一个模块需要向其他模块导出符号,则使用下面的宏:
EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);
符号必须在模块文件的全局变量部分导出,因为这两个宏将被扩展为一个特殊变量的声明,而该变量必须是全局的。


(2)驱动模块和应用程序的一个重要不同是:应用程序退出时可不管资源释放或者其他的清除工作,但模块的退出函数必须仔细撤销初始化函数所作的一切,否则,在系统重新引导之前某些东西就会残留在系统中。


(3)处理器的多种工作模式(级别)其实就是为了操作系统的用户空间和内核空间设计的。在Unix类的操作系统中只用到了两个级别:最高和最低级别。

(4)要十分注意驱动程序的并发处理。

(5)内核API中具有双下划线(_ _)的函数,通常是接口的底层组件,应慎用。

(6)内核代码不能实现浮点数运算。参考资料:http://blog.chinaunix.net/u/30180/showart.php?id=1421920


所有模块代码中都包含一下两个头文件:
#include <linux/init.h>
#include <linux/module.h>
所有模块代码都应该指定所使用的许可证:
MODULE_LICENSE("Dual BSD/GPL");


此外还有可选的其他描述性定义:
MODULE_AUTHOR("xxx");
MODULE_DESCRIPTION("xxx");
MODULE_VERSION("xxx");
MODULE_ALIAS("xxx");
MODULE_DEVICE_TABLE("xxx");


 一个简单的Makefile文件:
KERNELDIR = /home/tekkaman/working/SBC2440/linux-2.6.22.2
PWD := $(shell pwd)
INSTALLDIR = /home/tekkaman/working/rootfs/lib/modules
CROSS_COMPILE    = arm-9tdmi-linux-gnu-
CC    = $(CROSS_COMPILE)gcc
obj-m := hello.o
.PHONY: modules modules_install clean
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

modules_install:
cp hello.ko $(INSTALLDIR)
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
 obj-m := hello.o
代表了我们要构造的模块名为hell.ko,make 会在该目录下自动找到hell.c文件进行编译。如果 hello.o是由其他的源文件生成(比如file1.c和file2.c)的,则在下面加上(注意红色字体的对应关系):


hello-objs := file1.o file2.o ......

 $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
-C $(KERNELDIR) 指定了内核源代码的位置,其中保存有内核的顶层makefile文件。
M=$(PWD) 指定了模块源代码的位置


Linux内核模块的初始化出错处理一般使用“goto”语句。
通常情况下很少使用“goto”,但在出错处理是(可能是唯一的情况),它却非常有用。在大二学习C语言时,老师就建议不要使用“goto”,并说很少会用到。在这里也是我碰到的第一个建议使用“goto”的地方。“在追求效率的代码中使用goto语句仍是最好的错误恢复机制。”

以下是初始化出错处理的推荐代码示例:

struct something *item1;
struct somethingelse *item2;
int stuff_ok;


void my_cleanup(void)
{
    if (item1)


        release_thing(item1);
    if (item2)
        release_thing2(item2);
    if (stuff_ok)
        unregister_stuff();
    return;
}
int __init my_init(void)
{
    int err = -ENOMEM;
    item1 = allocate_thing(arguments);
    item2 = allocate_thing2(arguments2);
    if (!item2 || !item2)
        goto fail;
    err = register_stuff(item1, item2);
    if (!err)
        stuff_ok = 1;
    else
        goto fail;
    return 0; /* success */


 fail:
        my_cleanup( );
        return err;
}

模块参数

内核允许对驱动程序指定参数,而这些参数可在装载驱动程序模块时改变
以下是我的实验程序:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>

MODULE_LICENSE("Dual BSD/GPL");

static char *whom = "Tekkaman Ninja";
static int howmany = 1;
static int TNparam[] = {1,2,3,4};
static int TNparam_nr = 4;
module_param(howmany, int, S_IRUGO);
module_param(whom, charp, S_IRUGO);
module_param_array(TNparam , int , &TNparam_nr , S_IRUGO);

static int hello_init(void)
{
    int i;
    for (i = 0; i < howmany; i++)
        printk(KERN_ALERT "(%d) Hello, %s !\n", i, whom);
    for (i = 0; i < 8; i++)
        printk(KERN_ALERT "TNparam[%d] : %d \n", i, TNparam[i]);
    return 0;
}

static void hello_exit(void)
{
    printk(KERN_ALERT "Goodbye, Tekkaman Ninja !\n Love Linux !Love ARM ! Love KeKe !\n");
}

module_init(hello_init);
module_exit(hello_exit);

实验结果是 :

[Tekkaman2440@SBC2440V4]#cd /lib/modules/
[Tekkaman2440@SBC2440V4]#ls
cs89x0.ko hello.ko prism2_usb.ko
hello-param.ko p80211.ko
[Tekkaman2440@SBC2440V4]#insmod hello-param.ko howmany=2 whom="KeKe" TNparam=4,3,2,1
(0) Hello, KeKe !
(1) Hello, KeKe !
TNparam[0] : 4
TNparam[1] : 3
TNparam[2] : 2
TNparam[3] : 1
TNparam[4] : 1836543848
TNparam[5] : 7958113
TNparam[6] : 1836017783
TNparam[7] : 0
[Tekkaman2440@SBC2440V4]#insmod hello-param.ko howmany=2 whom="KeKe"  TNparam=4,3,2,1,5,6,7,8
TNparam: can only take 4 arguments
hello_param: `4' invalid for parameter `TNparam'
insmod: cannot insert 'hello-param.ko': Invalid parameters (-1): Invalid argument
[Tekkaman2440@SBC2440V4]#  

我这个实验除了对参数的改变进行实验外,我的一个重要的目的是测试“ module_param_array(TNparam,int,&TNparam_nr, S_IRUGO);”中&TNparam_nr对输入参数数目的限制作用。经过我的实验,表明&TNparam_nr并没有对输入参数的数目起到限制作用。真正起到限制作用的是static int TNparam[] = {1,2,3,4};本身定义的大小,我将程序进行修改:
static int TNparam[] = {1,2,3,4}; 
改为
static int TNparam[] = {1,2,3,4,5,6,7,8};
其他都不变。

编译后再进行实验,其结果是:

[Tekkaman2440@SBC2440V4]#insmod hello-param.ko howmany=2 whom="KeKe" TNparam=4,3,2,1,5,6,7,8
(0) Hello, KeKe !
(1) Hello, KeKe !
TNparam[0] : 4
TNparam[1] : 3
TNparam[2] : 2
TNparam[3] : 1
TNparam[4] : 5
TNparam[5] : 6
TNparam[6] : 7
TNparam[7] : 8
[Tekkaman2440@SBC2440V4]# 

(15)“ #include < linux/sched.h >”  最重要的头文件之一。包含驱动程序使用的大部分内核API的定义,包括睡眠函数以及各种变量声明。
(16)
#include < linux/version.h >” 包含所构造内核版本信息的头文件。

在学习过程中找到了几篇很好的参考文档:
(1)
第一章 模块(Modules) URL:http://greenlinux.blogcn.com/diary,103232026.shtml
(2)《从 2.4 到 2.6:Linux 内核可装载模块机制的改变对设备驱动的影响》
URL:http://www.ibm.com/developerworks/cn/linux/l-module26/
(3)《Linux2.6内核驱动移植参考》
URL:http://blog.chinaunix.net/u1/40912/showart_377391.html


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是一个简单的字符设备驱动程序的示例代码: ```c #include <linux/module.h> #include <linux/fs.h> #include <linux/uaccess.h> #define DEVICE_NAME "my_char_dev" #define BUFFER_SIZE 1024 static int major; static char buffer[BUFFER_SIZE]; static ssize_t my_char_dev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { ssize_t retval = 0; if (*f_pos >= BUFFER_SIZE) { goto out; } if (*f_pos + count > BUFFER_SIZE) { count = BUFFER_SIZE - *f_pos; } if (copy_to_user(buf, buffer + *f_pos, count)) { retval = -EFAULT; goto out; } *f_pos += count; retval = count; out: return retval; } static ssize_t my_char_dev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { ssize_t retval = 0; if (*f_pos >= BUFFER_SIZE) { goto out; } if (*f_pos + count > BUFFER_SIZE) { count = BUFFER_SIZE - *f_pos; } if (copy_from_user(buffer + *f_pos, buf, count)) { retval = -EFAULT; goto out; } *f_pos += count; retval = count; out: return retval; } static struct file_operations my_char_dev_fops = { .owner = THIS_MODULE, .read = my_char_dev_read, .write = my_char_dev_write, }; static int __init my_char_dev_init(void) { int retval; memset(buffer, 0, BUFFER_SIZE); major = register_chrdev(0, DEVICE_NAME, &my_char_dev_fops); if (major < 0) { printk(KERN_ERR "Failed to register char device.\n"); return major; } printk(KERN_INFO "Registered char device with major number %d.\n", major); return 0; } static void __exit my_char_dev_exit(void) { unregister_chrdev(major, DEVICE_NAME); printk(KERN_INFO "Unregistered char device with major number %d.\n", major); } module_init(my_char_dev_init); module_exit(my_char_dev_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your name"); MODULE_DESCRIPTION("A simple character device driver."); ``` 这个驱动程序只是一个非常简单的示例,它创建了一个名为 "my_char_dev" 的字符设备,具有读和写功能。当应用程序读取设备时,它从缓冲区中读取数据,并将其复制到应用程序的缓冲区中。当应用程序写入设备时,它将数据从应用程序的缓冲区中复制到驱动程序的缓冲区中。 当你编译并加载这个模块后,在终端中执行命令: ``` $ sudo mknod /dev/my_char_dev c <major number> 0 ``` 其中,`<major number>` 是上面注册的字符设备的主设备号。 然后,你就可以像操作一个普通的文件一样进行读写操作了,例如: ``` $ echo "Hello, world!" > /dev/my_char_dev $ cat /dev/my_char_dev Hello, world! ``` 当然,这个示例只是一个非常简单的驱动程序,实际的驱动程序可能涉及到更复杂的操作,例如设备的初始化,中断处理等等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值