下面对上一节的驱动程序的open和write函数增加了打印信息,使用时可以看到系统的调用。
同时对注册和注销函数也增加了打印信息,使用时可以看到系统的调用。
#include /* 包含file_operation结构体 */
#include /* 包含module_init module_exit */
#include /* 包含LICENSE的宏 */
/* 定义一个打开设备的,open函数 */
static int first_drv_open(struct inode *inodep, struct file *filep)
{
printk("first_drv_open\n");
return 0;
}
/* 定义一个打开设备的,write函数 */
static ssize_t first_drv_write(struct file *filep, const char __user * buf, size_t len, loff_t *ppos)
{
printk("first_drv_write\n");
return 0;
}
/* 把自己定义的函数接口使用file_operations结构体封装起来,方便管理和使用 */
static const struct file_operations first_drv_file_operation = {
.owner = THIS_MODULE,
.open = first_drv_open,
.write = first_drv_write,
};
/* 注册驱动打包好的驱动程序 */
static int __init first_drv_init(void)
{
register_chrdev(111,"first_drv",&first_drv_file_operation);
printk("first_drv_init\n");
return 0;
}
/* 卸载打包好的驱动程序 */
static void __exit first_drv_exit(void)
{
unregister_chrdev(111,"first_drv_init");
printk("first_drv_exit\n");
}
/* 声明函数属性 */
module_init(first_drv_init);
module_exit(first_drv_exit);
MODULE_LICENSE("GPL");
既然有了驱动程序,那驱动程序如何装载呢
前一节我们说了几个模块驱动使用的几个命令,中insmod和modprob就是用来安装驱动到内核中的。
两者的区别是insmod是按用户指定的pathname去安装模块的,如下:
而modprobe则是在 /lib/modules/ `uname -r`/ 路径在区查模块的,如果没找到会出现如下的显示
需要wo'me我们把魔模块放入 /lib/modules/ `uname -r`/路径中,然后用depmod生成模块的依赖性信息,之后使用modprobe xxxx安装
我们/lib/modules/3.16.57/modules.dep里面的内容如下,它只依赖自己本身。
first_drv.ko:
为了开发方便,今后所有的模块我都默认放在driver目录下,使用insmod安装
前面的驱动代码中,我们为每个函数都添加了打印调试信息,
上面的两幅图片分别是安装和卸载时的打印信息。我们可以看到安装时就是调用了,下面函数first_drv_init。
static int __init first_drv_init(void)
{
register_chrdev(111,"first_drv",&first_drv_file_operation);
printk("first_drv_init\n");
return 0;
}
卸载时是调用了下面函数first_drv_exit
static void __exit first_drv_exit(void)
{
unregister_chrdev(111,"first_drv");
printk("first_drv_exit\n");
}
使用反汇编查看.ko文件
arm-none-linux-gnueabi-objdump first_drv.ko -D |less
可以看到里面有.init.text 和.exit.text段,他们里面就存放的是我们上面写的两个函数的汇编代码。
而在使用insmod或rmmod的时候,该命令只要在.ko文件中找到相应的段,执行就可以。
当然想到这个,很明显会继续想到,如果有两个 .init.text该怎么按先后顺序执行。
为此我在上面代码中增加了一个函数,并声明其属性。
static int __init abcde(void)
{
return 0;
}
module_init(abcde)
编译后,结果如下,报的错误是段属性重复定义。可见一个模块中只能有一个module_init属性的函数。
在代码中搜索出错的描述,__inittest(void)。
假设我们定义了两个函数分别是aaaa和bbbb,并用module_init声明了
/* 系统给的定义 */
#define module_init(initfn)
static inline initcall_t __inittest(void) \
{ return initfn; } \
int init_module(void) __attribute__((alias(#initfn)));
一个文件定义两个module_init( )
int aaaaa(void)
{
return 'a';
}
module_init(aaaaa);
int bbbbb(void)
{
return 'b';
}
module_init(bbbbb)
static inline initcall_t __inittest(void)
{
return aaaaa;
}
int init_module(void) __attribute__((alias("aaaaa")));
static inline initcall_t __inittest(void)
{
return bbbbb;
}
int init_module(void) __attribute__((alias("bbbbb")));
/* 上面函数中,alias属性的作用是使用init init_module(void)函数名来给alisa里面的函数再起一个别名 */
可以发现我们定义了两个static inline initcall_t __inittest(void) 函数
也定义了两个int init_module(void) 函数,所以报函数重定义
对比上面的报错信息,总共两个报错,分别是__inittest和init_module两个函数被重复定义,和我们的分析完全吻合。
其实这页比较符合情况,只声明module_init属性声明的函数,其它的函数在这个module_init属性的函数里面调用就可以了。
反汇编里面还有这么几个和上面相关的属性段,目前没找到相关资料,待下次找到了再来这里继续填坑。
猜测是放的init和exit函数的地址
接下来我们看一下驱动程序如何被应用程序使用的。
下面是一个最简单的应用程序,
#include
#include
#include
#include
#include
int main(void)
{
char buf[10];
/* 以可读可写方式打开/dev/目录下的xxx设备,open的返回值是一个文件描述符 */
int fd = open("/dev/xxx", O_RDWR);
if(fd < 0) /* 文件描述符小于0表示打开文件失败 */
{
printf("open /dev/xxx fail\n");
return -1;
}
/* 该文件中写入5个字节,写入的内容是buf中的前五个字节 */
write(fd, buf, 5);
return 0;
}
~
文件的操作函数通常我们都是使用系统调用或c库,系统调用相关的函数使用的头文件可以通过man xxx 2查找,比如:
man 2 open
之后就可以看到它的使用说明,函数原型,使用必须包含的头文件等。
C库则是使用man 3 xxx,比如:
man 3 printf
编译该应用程序,生成对应的可执行程序。
arm-none-linux-gnueabi-gcc first_drv_app.c -o first_drv_app
执行(执行前必须先安装驱动程序),发现打开文件失败
很明显,我们在/dev/xxx下没有相应的设备文件,所以肯定open失败
那么我们创建一个设备文件
命令: mknod
格式: 设备类型 主设备号 次设备号
mknod /dev/xxx c 111 0
我们在驱动学习中通常只会用到两种设备类型 c 字符型 b块型
这里,因为我们在注册设备时用的chrdev_register所以肯定这里是字符型。
设备号我们在驱动中定义的主设备号是111,所以这里要使用对应的驱动就要指定对应的主设备号,从设备号因为我们驱动程序没指定所以,默认是0.
创建好设备的节点后,继续执行
发现可以打印出我们驱动中,所做的调试信息
应用程序,注释掉write
发现只运行驱动的open,表明其一一对应性。
这里说明一点,open函数必须要有,因为只有使用open函数在/dev/目录下打开了某个设备xxx(通过该设备的设备类型【字符型】,设备号查找到到设备,并把这些信息放入当前进程打开文件的记录表中),得到当前应用程序对该设备节点的信息后,把这个设备记录表数组的下标返回,我们用fd接收数组下标,通常称作fd为设备描述符,提供给后面的其它系统调用函数,使用设备描述符fd来索引节点信息。
一般来说,在一个进程中,设备描述符0,1,2分别是标准输入,标准输出,标准错误。我们自己打开的文件一般都是从数组下标3开始的。
到这里我们基本可以通过一个应用程序,使用一个驱动程序了(虽然很难简陋,但不妨碍我们学习基础功能)。
下一节则,在驱动程序中添加基本硬件操作函数。