从零开始之驱动发开、linux驱动(三、最简字符驱动的使用)

 

下面对上一节的驱动程序的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去安装模块的,如下:

从零开始之驱动发开、linux驱动(三、最简字符驱动的使用)_第1张图片

而modprobe则是在 /lib/modules/ `uname -r`/  路径在区查模块的,如果没找到会出现如下的显示

从零开始之驱动发开、linux驱动(三、最简字符驱动的使用)_第2张图片

需要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段,他们里面就存放的是我们上面写的两个函数的汇编代码。

从零开始之驱动发开、linux驱动(三、最简字符驱动的使用)_第3张图片

而在使用insmod或rmmod的时候,该命令只要在.ko文件中找到相应的段,执行就可以。

当然想到这个,很明显会继续想到,如果有两个 .init.text该怎么按先后顺序执行。

 

为此我在上面代码中增加了一个函数,并声明其属性。

static int __init abcde(void)
{
     return 0;
}
module_init(abcde)

编译后,结果如下,报的错误是段属性重复定义。可见一个模块中只能有一个module_init属性的函数。

从零开始之驱动发开、linux驱动(三、最简字符驱动的使用)_第4张图片

在代码中搜索出错的描述,__inittest(void)。

从零开始之驱动发开、linux驱动(三、最简字符驱动的使用)_第5张图片

假设我们定义了两个函数分别是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属性的函数里面调用就可以了。

 

反汇编里面还有这么几个和上面相关的属性段,目前没找到相关资料,待下次找到了再来这里继续填坑。

从零开始之驱动发开、linux驱动(三、最简字符驱动的使用)_第6张图片

猜测是放的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

之后就可以看到它的使用说明,函数原型,使用必须包含的头文件等。

从零开始之驱动发开、linux驱动(三、最简字符驱动的使用)_第7张图片

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开始的。

到这里我们基本可以通过一个应用程序,使用一个驱动程序了(虽然很难简陋,但不妨碍我们学习基础功能)。

下一节则,在驱动程序中添加基本硬件操作函数。

 

你可能感兴趣的:(从零开始系列,从零开始学linux驱动)