Writing device drivers in Linux: A brief tutorial(4)

Writing device drivers in Linux: A brief tutorial(四)

一个名为”memory”的完整的驱动:驱动的初始化部分

         现在我将要演示如何建立一个完整的设备驱动:memory.c。这个驱动允许从它读取一个字符或者向它写入一个字符。尽管在正常情况下并不是很有用,但是这个设备却提供了一个很有阐述性的例子,因为它是一个完整的设备驱动;它也很好实现,因为它并不是一个真正的硬件借口,因为它不和真正的硬件交互(除了计算机自己)

         为了开发这个驱动,几个新的在设备驱动程序中频繁出现的#include语句需要添加进来:

 

<memory initial>=

 

/* Necessaryincludes for device drivers */

#include<linux/init.h>

#include<linux/config.h>

#include<linux/module.h>

#include<linux/kernel.h> /* printk() */

#include<linux/slab.h> /* kmalloc() */

#include<linux/fs.h> /* everything... */

#include<linux/errno.h> /* error codes */

#include<linux/types.h> /* size_t */

#include<linux/proc_fs.h>

#include<linux/fcntl.h> /* O_ACCMODE */

#include<asm/system.h> /* cli(), *_flags */

#include<asm/uaccess.h> /* copy_from/to_user */

MODULE_LICENSE("DualBSD/GPL");

/* Declarationof memory.c functions */

intmemory_open(struct inode *inode, struct file *filp);

intmemory_release(struct inode *inode, struct file *filp);

ssize_tmemory_read(struct file *filp, char *buf, size_t count, loff_t *f_pos);

ssize_tmemory_write(struct file *filp, char *buf, size_t count, loff_t *f_pos);

voidmemory_exit(void);

intmemory_init(void);

/* Structurethat declares the usual file */

/* accessfunctions */

structfile_operations memory_fops = {

read:memory_read,

write:memory_write,

open:memory_open,

release:memory_release

};

/* Declarationof the init and exit functions */

module_init(memory_init);

module_exit(memory_exit);

/* Globalvariables of the driver */

/* Major number*/

intmemory_major = 60;

/* Buffer tostore data */

char*memory_buffer;

 

         在#include文件之后,声明了要定义的函数。一般用来操作文件的函数在file_operations结构体中进行了声明,我们稍后会对此进行解释。之后,是初始化和退出函数——在加载和卸载模块的时候使用——对内核的声明。最后,这个驱动的全局变量也被声明:他们之中一个是驱动的Major Number,另一个是内存区域的指针,memory_buffer,这个将会用来存储驱动的数据。

 

名为”memory”的驱动:使用文件(files)和设备进行连接

       在Unix和Linux系统中,在用户空间访问设备的方式和访问文件的方式是一样的。这些设备一般情况下都是“/dev”的子目录中的文件。

         我们使用两个数字来连接一般文件和内核模块:major number和minor number。major number在内核使用设备驱动连接到设备文件时使用。minor number是供设备内部使用的,为了简洁,本文将不会包含关于minor number的内容。

         为了达到目的,我们首先需要创建一个用来访问设备驱动的文件,通过以root身份键入以下命令:

# mknod/dev/memory c 60 0

 

         在上面,“c”表示我们要创建一个字符设备,“60”是major number,“0”是minor number。

         在设备驱动里,为了在内核空间将驱动和它在“/dev”目录下对应的文件联系起来,我们需要使用到register_chrdev函数。在调用它时需要使用到三个参数:major number,一个表示模块名称的字符串和一个将本次调用同之前定义的文件操作函数关联起来的file_operation结构体。它将使用如下方式在安装模块的时候被调用:

 

<memory init module> =

intmemory_init(void) {

intresult;

/*Registering device */

result= register_chrdev(memory_major, "memory", &memory_fops);

if(result < 0) {

printk(

"<1>memory: cannot obtain major number%d\n", memory_major);

returnresult;

}

/*Allocating memory for the buffer */

memory_buffer= kmalloc(1, GFP_KERNEL);

if(!memory_buffer) {

result = -ENOMEM;

goto fail;

}

memset(memory_buffer,0, 1);

printk("<1>Insertingmemory module\n");

return0;

fail:

memory_exit();

returnresult;

}

 

         同样地,我们需要关注kmalloc函数的使用。这个函数用来在内核空间为设备驱动分配存储空间。它的使用方法和我们所熟知的malloc函数相似。最后,如果注册major number或者分配内存失败,这个模块会相应地作出反应。

 

名为memory的设备驱动:卸载驱动

         为了在memory_exit函数当中卸载驱动,这里就需要讲到unregister_chrdev函数,这会释放内核中的major number。

        

<memory exit module> =

voidmemory_exit(void) {

/*Freeing the major number */

unregister_chrdev(memory_major,"memory");

/*Freeing buffer memory */

if(memory_buffer) {

kfree(memory_buffer);

}

printk("<1>Removingmemory module\n");

}

 

         缓存此时也被释放了,目的是为了在卸载驱动设备的时候保持内核的整洁。

 

名为“memory”的驱动设备:像打开文件一样打开设备

         在内核空间,和用户空间的打开文件函数(fopen)相对应的函数是我们之前调用register_chrdev函数时传递的file_operation结构体中的“open:”成员。在本例中,它是名为“memory_open”函数。它需要两个参数,一个是inode结构体,这个inode结构体会根据major number和minor number给内核发送信息;另一个是file类型的结构体,这个结构体包含有对一个文件进行不同操作的信息。这些函数在本文之中都不会进行深入的讲解。

         当一个文件被打开,一般情况下我们需要初始化驱动的变量或者重启设备。但是由于本文在这里讲解的是一个很简单的例子,因此这些操作没有被执行。

         memory_open函数如下所示:

 

<memory open> =

intmemory_open(struct inode *inode, struct file *filp) {

/*Success */

return0;

}

 

这些新的函数如下表所示:

 

Events

User  functions

Kernel  functions

Load module

Insmod

module_init()

Open device

fopen

file_operation:open

Read device

 

 

Write device

 

 

Close device

 

 

Remove module

rmmod

module_exit()

表  设备驱动事件和与对应的在用户空间和内核空间的交互函数

 

名为“memory”的设备驱动:像关闭文件那样关闭一个设备

         在内核空间,和用户空间的关闭文件函数(fclose)相对应的函数是我们之前调用register_chrdev函数时传递的file_operation结构体中的“release:”成员。在我们的例子中它是memory_release函数,和之前一样,它也需要一个inode结构体和file结构体作为参数。

         当一个文件被关闭的时候,它需要关闭在打开设备时所占用的存储空间和变量。但是,由于本文所举例子的简单性,这些操作将不会被执行。

         memory_release函数如下所示:

 

<memory release> =

 

intmemory_release(struct inode *inode, struct file *filp) {

/*Success */

return0;

}

 

这些新的函数如下表所示:

 

Events

User  functions

Kernel  functions

Load module

Insmod

module_init()

Open device

fopen

file_operation:open

Read device

 

 

Write device

 

 

Close device

fclose

file_operation:release

Remove module

rmmod

module_exit()

表  设备驱动事件和与对应的在用户空间和内核空间的交互函数

 

名为“memory”的驱动:读设备

         为了使用类似于用户空间函数fread函数来读设备,需要调用我们在之前调用register_chrdev函数时传入的file_operation结构体中的“write:”成员函数。在本例中,它是函数memory_read。它的参数如下:一个file结构体;一个用户空间函数(fread)可以读取数据的读缓冲区;一个与用户空间函数(fread)中的计数器具有同样值的、用来记录需要转移的字节数的计数器count;最后,是一个指明该从何处开始读数据的f_pos参数。

         在这个非常简单的例子中,memory_read函数将一个单字节从驱动缓冲区(memory_buffer)使用copy_to_user函数转移到用户空间。

 

<memory read> =

ssize_t memory_read(struct file *filp, char *buf,

size_t count, loff_t *f_pos) {

/* Transferingdata to user space */

copy_to_user(buf,memory_buffer,1);

/* Changingreading position as best suits */

if (*f_pos ==0) {

*f_pos+=1;

return1;

} else {

return0;

}

}

 

         文件读写位置(f_pos)的值是变化的。如果要读写的位置是文件的开头,这个值将会随着一次正确的读取而加一,同时,这次读取到的字节数即1也将作为函数的返回值而返回。如果读的位置不在文件的开头,函数将返回0,因为这个文件只会存储一个字节的数据。

 

这些新的函数如下表所示:

 

Events

User  functions

Kernel  functions

Load module

Insmod

module_init()

Open device

fopen

file_operation:open

Read device

fread

file_operation:read

Write device

 

 

Close device

fclose

file_operation:release

Remove module

rmmod

module_exit()

表  设备驱动事件和与对应的在用户空间和内核空间的交互函数

 

名为“memory”的驱动:写设备

 

         在用户空间使用函数fwrite或者类似的函数来写设备的时候,会调用我们在之前调用register_chrdev函数时传入的file_operation结构体中的“write:”成员。在这里,它是名为memory_write的函数,这个函数需要传入如下参数:一个file结构体、一个用户空间函数(fwrite)要写的的名为buf的写缓冲区和一个与用户空间函数(fwrite)中的计数器具有同样值的、用来记录需要转移的字节数的计数器count;最后,是一个名为f_pos的参数,这个参数指明了我们应该从什么位置开始写文件。

        

<memory write> =

ssize_tmemory_write( struct file *filp, char *buf,

size_tcount, loff_t *f_pos) {

char*tmp;

tmp=buf+count-1;

copy_from_user(memory_buffer,tmp,1);

return1;

}

 

         在本例中,函数copy_from_user用来将数据从用户空间转移到内核空间。

         这些新的函数如下表所示:

 

Events

User  functions

Kernel  functions

Load module

Insmod

module_init()

Open device

fopen

file_operation:open

Read device

fread

file_operation:read

Write device

fwrite

file_operation:write

Close device

fclose

file_operation:release

Remove module

rmmod

module_exit()

表  设备驱动事件和与对应的在用户空间和内核空间的交互函数

 

一个完整的“memory”驱动

         通过将以上所有的代码结合起来,我们就得到了一个完整的驱动:

 

<memory.c> =

<memoryinitial>

<memory initmodule>

<memory exitmodule>

<memoryopen>

<memoryrelease>

<memoryread>

<memorywrite>

 

         在使用这个模块之前,你需要像之前我们编译其他模块那样来编译它。这个模块可以用如下的方式来加载:

 

# insmodmemory.ko

 

         我们可以使用如下方式来很方便地修改设备的权限:

 

# chmod 666/dev/memory

 

         如果一切进行得顺利,你可以得到一个“/dev/memory”的设备,你可以向这个设备写入一串字符,同时,它会存储你这一串字符的最后一个字节。你可以像下面一样来执行这些操作:

 

$ echo -nabcdef >/dev/memory

 

为了查看这个设备的的内容,你可以使用简单的cat命令:

 

$ cat/dev/memory

 

         存储在设备中的字符将一直保持不变,除非你覆盖掉它或者你的模块被卸载。