关闭

第三章:字符设备驱动程序

127人阅读 评论(0) 收藏 举报

主设备号和次设备号

对字符设备的访问是通过文件系统内的设备名称进行的,他们通常位于/dev目录,在此目录下ls –l 命令可以查看所有设备,每一行第一列”c”表示字符设备,”b”表示块设备。

主设备号标识设备对应的驱动程序,虽然现代linux运行多个驱动程序共享主设备号,但是大多数设备依然按照“一个主设备号对应一个驱动程序”的原则组织。

次设备号有内核使用,可以获得指向内核设备的一个指针。

设备编号的内部表达

在内核中,dev_t类型(<linux/types.h>中定义)用来保存设备编号(主设备号以及次设备号)。

我们应该始终使用<linux/kdev_t.h>中的宏来获得主设备号以及次设备号:

MAJOR(dev_t dev);

MINOR(dev_t dev);

同样,可以将主设备号以及次设备号转换为对应的dev_t类型:

MKDEV(int major, int minor);

分配和释放设备编号

在建立一个字符设备之前,我们的驱动程序首先要获得一个或者多个设备编号。可以使用定义在<linux/fs.h>中的函数完成该工作。

int register_chrdev_region(dev_t first, unsigned int count, char *name);

在我们提前明确知道所需要的设备编号时,此函数会工作的很好。first是所要分配的设备编号范围的起始值,通常被设置为0count是所请求的连续设备的编号的个数。name是和该编号范围关联的设备名称,会出现在/proc/devices/sysfs中。

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);

动态分配设备编号。dev是输出参数,在成功完成调用后保存已分配范围的第一个编号。firstminor应该是要使用的被请求的第一个此设备编号。

释放设备编号

void unregister_chrdev_region(dev_t first, unsigned int count);

通常,我们在模块的清楚函数中调用释放设备编号函数。

动态分配主设备号

作为驱动程序开发者,有两种选择:1.简单选定一个未被使用的编号;2.由系统动态分配一个设备编号。强烈建议使用第二种方法。

动态分配编号的缺点是:由于无法预知主设备号,所以无法预先创建设备节点。但一旦分配了设备号,就可以从/proc/devices中读取到。因此,对insmod的调用可替换为一个简单的脚本,该脚本在调用insmod之后,读取/proc/devices以获得新分配的设备编号,然后创建对应的设备文件。

一些重要的数据结构

文件操作

定义在<linux/fs.h>中的file_operations结构用来将任何驱动程序操作连接到已经申请的设备编号。

驱动程序通常的file_operations结构被初始化如下:

struct file_operations scull_fops = {
.owner  =  THIS_MODULE,
.llseek   =  scull_llseek,
.read    =  scull_read,
.write   =   scull_write,
.ioctl    =  scull_ioctl,
.open   =  scull_open,
.release  =  scull_release
}

此声明采用标准C的标记化结构初始化语法。它使驱动程序在结构发生变化时更具有可移植性。

file结构

定义在<linux/fs.h>中的struct file与用户空间的FILE没有任何关联。系统中每个打开的文件在内核空间都有一个对应的file结构。

inode结构

内核用inode结构在内部表示文件。file结构表示打开的文件描述符。对单个文件,可能会有许多个表示打开的文件描述符的file结构,但它们都指向单个inode结构。

字符设备的注册

内核内部使用包含在<linux/cdev.h>中的struct cdev结构来表示字符设备。

注册字符设备时可能用到的两个函数:

void cdev_init(struct cdev *cdev, struct file_operations *fops);

初始化以分配的结构。

int cdev_add(struct cdev *cdev, dev_t num, unsigned int count);

num是该设备对应的第一个设备编号,count是应该和该设备关联的设备编号的数量。此函数可能会返回失败,一般情况下会返回成功,返回成功以后我们的设备会被内核调用,因此一定要保证驱动程序完全准备好处理设备上的操作时才调用该函数。

从系统中移除一个字符设备调用如下函数:

void cdev_del(struct cdev *cdev);

早期的方法

注册一个字符设备驱动程序的经典方式是:
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);

和它对应的从系统中移除设备的函数是:

int unregister_chrdev(unsigned int major, const char *name);

open方法

open方法提供给驱动程序以初始化的能力,一般情况下,open应该完成以下工作:

l 检查设备特定的错误(如设备未就绪或类似的硬件问题)

l 如果设备是首次打开,则对其进行初始化。

l 如有必要,更新f_op指针。

l 分配并填写置于filp->private_data里的数据结构。

open的原型为:int (*open)(struct inode *inode, struct file *filp);

<linux/kernel.h>中的container_of宏:container_of(pointer, container_type, container_field);可以通过一个结构体中某一个成员变量的指针得到指向这个结构体的指针。方法:通过指针偏移。

release方法

release方法主要完成以下工作:

l 释放由open分配,保存的filp->private_data中所有的内容

l 在最后一次关闭操作时关闭设备

内核在进程退出的时候,通过在内部使用close系统调用自动关闭所有相关的文件。

scull的内存使用

linux中用于内存管理的两个核心函数定义在<linux/slab.h>中,它们是

void *kmalloc(size_t size, int flags);

void kfree(void *ptr);

我们不可以将非kmalloc返回的指针传递给kfree,但是可以将NULL传递给kfree

readwrite

readwrite完成的任务很相似,他们的函数原型是:

ssize_t read(struct file *filp, char __user *buff, size_t count, loff_t *offp);

ssize_t write(struct file *filp, const char __user *buff, size_t count, loff_t *offp);

filp是文件指针,count是请求传输的长度,buff是指向用户空间的缓冲区的一个指针,offp是一个指向“长偏移量类型(long offset type)”对象的指针,这个对象指明用户在文件中进行存取操作的位置。

buff是指向用户空间的指针,内核不能直接引用其内容,所以大多数readwrite方法实现的核心部分是:

unsigned long copy_to_user(void __user *to, const void *form, unsigned long count);

unsigned long copy_from_user(void *to, const void __user *from, unsigned long count);

这两个函数在函数内部进行了指针有效性的检查,如果不需要检查指针有效性的情况下,可以使用__copy_to_user, __copy_from_user函数,如果我们没有真正的检查指针就调用此函数,可能会导致内核崩溃或者建立安全漏洞。

访问用户空间的任何函数必须是可重入的,并且必须能够和其他驱动程序函数并发执行。

readvwritev

这两个函数用来一次read或者write若干个数据块。

查漏补缺

file_operations结构保存了字符驱动程序的方法,struct file表示一个打开的文件,struct inod 表示一个磁盘上的文件。

#include <asm/uaccess.h>该文件声明了在内核代码和用户空间之间移动数据的函数。

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:13782次
    • 积分:289
    • 等级:
    • 排名:千里之外
    • 原创:15篇
    • 转载:1篇
    • 译文:0篇
    • 评论:0条
    文章存档