全部博文(71)
分类: LINUX
2009-11-07 10:13:26
字符驱动基础
要访问一个字符设备,系统用户要调用一个合适的应用程序. 应用程序负责跟设备通话,但它需要得到合适驱动的身份信息. 驱动的联系详细信息通过/dev目录导出到用户空间.
从代码流程来看,字符驱动有如下内容:
1. 初始化程序,负责初始化设备并通过注册函数让驱动无缝融入到内核中.
2. 一系列入口访问点,如open(), read(), ioctl(), llseek()和write(), 它们对应于用户程序调用的跟/dev结点相关的I/O系统调用.
3. 中断程序, 后半部,定时器处理函数,助手内核线程和其他支持基础. 它们大多对用户程序透明.
从数据流程来看, 字符驱动拥有如下关键数据结构:
1. 一个per-device结构体. 驱动涉及到的信息仓库.
2. struct cdev, 内核对字符驱动的抽象. 该结构通常嵌入到per-device结构里面.
3. struct file_operations, 博爱含所有驱动入口点的地址.
4. struct file, 相关联的/dev结点信息.
驱动例子:系统CMOS
我们来实现一个字符驱动来访问系统CMOS. PC兼容机上的BIOS使用CMOS存储诸如启动选项, 引导顺序和系统时间的信息, 这些信息可以通过BIOS安装菜单配置. 我们的CMOS驱动例子可以像访问普通文件一样访问到两个PC CMOS区. 应用程序可以在/dev/cmos/0和/dev/cmos/1上操作, 并使用I/O系统调用访问2个区的数据. 因为BIOS指派CMOS区域以比特级别粒度访问, 所以驱动有比特级别访问能力. read()可以得到指定数目比特数据并能移动一定数目比特文件指针来读.
通过两个I/O地址来访问CMOS, 一个是索引寄存器,一个是数据寄存器,如下表所示. 你得指明索引寄存器的CMOS内存偏移量然后通过数据寄存器交互信息.
寄存器 名称描述
CMOS_BANK0_INDEX_PORT 在此寄存器中指定CMOS bank 0偏移量
CMOS_BANK0_DATA_PORT 从CMOS_BANK0_INDEX_PORT指定的地址读写数据.
CMOS_BANK1_INDEX_PORT 在此寄存器中指定CMOS bank 1偏移量
CMOS_BANK1_DATA_PORT 从CMOS_BANK1_INDEX_PORT指定的地址读写数据
每个驱动方法都有一个对应的系统调用, 现在逐一介绍.
驱动初始化
init()函数是注册机制的基石. 负责下列事情:
1. 请求分配主设备号
2. 为per-device结构体分配内存
3. 为字符设备cdev抽象连接入口点(open(), read()等等)
4. 关联主设备号到cdev.
5. 在/dev和/sys下创建结点.
6. 初始化硬件. 这和我们的简单的CMOS没有关系.
下面是init()函数代码:
#include
/* Per-device (per-bank) structure */
struct cmos_dev {
unsigned short current_pointer; /* Current pointer within the
bank */
unsigned int size; /* Size of the bank */
int bank_number; /* CMOS bank number */
struct cdev cdev; /* The cdev structure */
char name[10]; /* Name of I/O region */
/* ... */ /* Mutexes, spinlocks, wait
queues, .. */
} *cmos_devp;
/* File operations structure. Defined in linux/fs.h */
static struct file_operations cmos_fops = {
.owner = THIS_MODULE, /* Owner */
.open = cmos_open, /* Open method */
.release = cmos_release, /* Release method */
.read = cmos_read, /* Read method */
.write = cmos_write, /* Write method */
.llseek = cmos_llseek, /* Seek method */
.ioctl = cmos_ioctl, /* Ioctl method */
};
static dev_t cmos_dev_number; /* Allotted device number */
struct class *cmos_class; /* Tie with the device model */
#define NUM_CMOS_BANKS 2
#define CMOS_BANK_SIZE (0xFF*8)
#define DEVICE_NAME "cmos"
#define CMOS_BANK0_INDEX_PORT 0x70
#define CMOS_BANK0_DATA_PORT 0x71
#define CMOS_BANK1_INDEX_PORT 0x72
#define CMOS_BANK1_DATA_PORT 0x73
unsigned char addrports[NUM_CMOS_BANKS] = {CMOS_BANK0_INDEX_PORT,
CMOS_BANK1_INDEX_PORT,};
unsigned char dataports[NUM_CMOS_BANKS] = {CMOS_BANK0_DATA_PORT,
CMOS_BANK1_DATA_PORT,};
/*
* Driver Initialization
*/
int __init cmos_init(void)
{
int i;
/* Request dynamic allocation of a device major number */
if (alloc_chrdev_region(&cmos_dev_number, 0,
NUM_CMOS_BANKS, DEVICE_NAME) < 0) {
printk(KERN_DEBUG "Can't register device\n"); return -1;
}
/* Populate sysfs entries */
cmos_class = class_create(THIS_MODULE, DEVICE_NAME);
for (i=0; i
cmos_devp = kmalloc(sizeof(struct cmos_dev), GFP_KERNEL);
if (!cmos_devp) {
printk("Bad Kmalloc\n"); return 1;
}
/* Request I/O region */
sprintf(cmos_devp->name, "cmos%d", i);
if (!(request_region(addrports[i], 2, cmos_devp->name))) {
printk("cmos: I/O port 0x%x is not free.\n", addrports[i]);
return –EIO;
}
/* Fill in the bank number to correlate this device
with the corresponding CMOS bank */
cmos_devp->bank_number = i;
/* Connect the file operations with the cdev */
cdev_init(&cmos_devp->cdev, &cmos_fops);
cmos_devp->cdev.owner = THIS_MODULE;
/* Connect the major/minor number to the cdev */
if (cdev_add(&cmos_devp->cdev, (dev_number + i), 1)) {
printk("Bad cdev\n");
return 1;
}
/* Send uevents to udev, so it'll create /dev nodes */
class_device_create(cmos_class, NULL, (dev_number + i),
NULL, "cmos%d", i);
}
printk("CMOS Driver Initialized.\n");
return 0;
}
/* Driver Exit */
void __exit cmos_cleanup(void)
{
int i;
/* Remove the cdev */
cdev_del(&cmos_devp->cdev);
/* Release the major number */
unregister_chrdev_region(MAJOR(dev_number), NUM_CMOS_BANKS);
/* Release I/O region */
for (i=0; i
release_region(addrports[i], 2);
}
/* Destroy cmos_class */
class_destroy(cmos_class);
return();
}
module_init(cmos_init);
module_exit(cmos_cleanup);
cmos_init()做的大部分事情都是通用的,所以删除了对CMOS数据结构的引用就可以作为开发其他字符驱动的模板了.
首先, cmos_init()调用alloc_chrdev_region()动态请求一个未使用的主设备号. 如果调用成功,dev_number就包含分配到的主设备号. alloc_chrdev_region()的第二个和第三个参数分布指定了起始次设备号和支持的次设备数目. 最后的参数设备名用来在/proc/devices下面识别CMOS:
bash> cat /proc/devices | grep cmos
253 cmos
253是为CMOS设备动态分配的主设备号. 在2.6内核以前动态设备结点分配不被支持,所以字符驱动调用register_chrdev()来静态请求指定的主设备号.
cmos_dev是早先提到的per-device数据结构. cmos_fops是file_operations结构,包含驱动入口点地址. cmos_fops还有个owner域设置为THIS_MODULE,即该驱动模块的地址. 知道结构所有者身份可允许内核卸下驱动的一些日常功能如打开释放设备时的用户计数.
如你看到的,内核使用一个cdev抽象在内部表示字符设备. 字符设备通常嵌入cdev在它们的per-device结构体中. 在我们的例子中, cdev在cmos_dev里面. cmos_init()在每个支持的次设备中循环为每个关联的per-device设备包括里面的cdev分配内存. cdev_init()关联文件操作(cmos_fops)到cdev, cdev_add()关联alloc_chrdev_region()分配到的主次设备后到cdev.
class_create()为设备产生一个sysfs入口, class_device_create()产生两个uevent: cmos和cmos1.
设备驱动需要通过调用request_region()来对已经声明的范围内I/O桩地址进行操作.这个调正机制确保了其他对该区域的请求将一直失败直到通过release_region()来释放它. request_region()通常由I/O总线驱动如PCI和ISA调用, 来标记处理器上地址空间的卡上内存的所有权. cmos_init()调用request_region()来请求访问每个CMOS bank的I/O区域. request_region()的最后一个参数是/proc/ioport用的标识符, 如果你查看该文件可以看到它:
bash> grep cmos /proc/ioports
0070-0071 : cmos0
0072-0073 : cmos1
这样就完成了注册过程, 最好cmos_init()打印一条信息表示进展顺利.
打开和释放
当应用程序打卡对应的设备结点时内核调用驱动的open()函数, 你可以这样做来触发cmos_open()的执行:
bash>cat /dev/cmos/0
应用程序关闭打开的设备时内核调用release()函数. 所以在cat读完CMOS bank 0的内容后关闭关联到/dev/cmos/0的文件描述符时,内核就调用cmos_release().
下面代码显示了cmos_open()和cmos_release()的实现. 有两点值得关注, 第一个是cmos_dev的提取. 作为参数传给cmos_open()的inode包含初始化时分配的cdev结构的地址. cdev嵌入到cmos_dev内部了. 为提取出cmos_dev结构体的地址, cmos_open()使用了内核助手函数container_of().
cmos_open()里面另外一个值得留意的操作是struct file里面的private_data域的使用方法. 你可以使用这个域作为占位符以方便与内部其他驱动函数关联信息. CMOS驱动使用该域存储cmos_dev的地址. 查看cmos_release()看下怎么用private_data得到对应CMOS bank的cmos_dev结构体上的处理函数的.
/*
* Open CMOS bank
*/
int
cmos_open(struct inode *inode, struct file *file)
{
struct cmos_dev *cmos_devp;
/* Get the per-device structure that contains this cdev */
cmos_devp = container_of(inode->i_cdev, struct cmos_dev, cdev);
/* Easy access to cmos_devp from rest of the entry points */
file->private_data = cmos_devp;
/* Initialize some fields */
cmos_devp->size = CMOS_BANK_SIZE;
cmos_devp->current_pointer = 0;
return 0;
}
/*
* Release CMOS bank
*/
int
cmos_release(struct inode *inode, struct file *file)
{
struct cmos_dev *cmos_devp = file->private_data;
/* Reset file pointer */
cmos_devp->current_pointer = 0;
return 0;
}
交换数据
向量驱动函数的原型定义如下:
ssize_t aio_read(struct kiocb *iocb, const struct iovec *vector,
unsigned long count, loff_t offset);
ssize_t aio_write(struct kiocb *iocb, const struct iovec *vector,
unsigned long count, loff_t offset);
aio_read()/aio_write()的第一个参数描述了AIO操作,第二个参数是个iovec数组,最后的向量函数使用的一个关键数据结构,它包含了持有数据的缓冲区的地址和长度. include/linux/uio.h可以看到iovec的定义, drivers/net/tun.c有向量字符驱动函数的例子.
另外一个数据访问函数是mmap(),它关联设备内存到用户虚拟内存. 应用程序可以调用对应的系统调用也可以调用mmap()来直接对返回的内存区域进行操作以达到访问设备中内存效果. 不是很多的驱动实现了mmap(),所以这里并不深入探求了. 只看一下drivers/char/mem.c,这是一个mmap()实现的例子. 我们的CMOS驱动没有实现mmap().
Seek
内核使用一个内部指针追踪当前文件的访问位置,应用程序使用lseek()系统调用来请求这个内部文件指针的重定位. 通过使用lseek()你可以复位文件指针到文件内的任何偏移位置. 字符驱动中类似lseek()的就是llseek()函数, cmos_llseek()实现了这个函数.
CMOS里面的内部文件指针以比特为单位移动而不是字节.如果从CMOS驱动中读一字节数据,文件指针就会移动8位. cmos_llseek()也根据CMOS bank的大小实现了文件结束语义.
lseek()系统调用支持的三种命令:
1. SEEK_SET 设置文件指针到提供的固定偏移位置.
2. SEEK_CUR 计算相对于当前位置的偏移.
3. SEEK_END 计算相对文件末尾的偏移.这个技巧常用于创建大文件,CMOS驱动不支持SEEK_END。
下面为cmos_llseek()代码:
1 /*