本文也即是《Linux Device Drivers》一书第三章Char Drivers的读书笔记之三。
这次进度有点慢,在上一次中,我们可以open和close,这次我们学习read和write。先补充一下用户测试小程序。我们增加读写的测试。先写后读。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(int argc ,char * argv[])
{
FILE * file = NULL;
char * buff = (char *) malloc ( 2048);
char * read_buf = (char *) malloc(101);
int len =0,total_len = 0;
/*我们放入一些字符,来检测写入和读取是否正确。*/
for(len = 0 ; len < 2048; len ++){
if(len < 100)
buff[len] = '#';
else
buff[len] = '*';
}
printf("************** TEST ACCESS DRIVER**************/n");
/*我们写入1500个字符,前100个为#号,后面为星号*/
printf("************** Write Test/n");
file = fopen("/dev/scull0","w");
if(file == NULL){
printf("Open scull0 error!/n");
exit(-1);
}
printf("device scull0 is open , fd = %d/n",file);
printf("write = %d/n",fwrite(buff,1,1500,file));
fclose(file);
/* 我们通过一个容量小的buff来读取,并将读取结果打印出来检测 */
printf("************** Read Test/n");
file = fopen("/dev/scull0","r");
if(file == NULL){
printf("Open scull0 error!/n");
exit(-1);
}
printf("device scull0 is open , fd = %d/n",file);
memset(read_buf,0,101);
while( (len = fread(read_buf,1,100,file)) > 0){
total_len += len;
printf("read len = %d/n", total_len);
printf("%s/n",read_buf);
memset(read_buf,0,101);
}
fclose(file);
return 0;
}
没有书,省几个钱,看pdf,但是没有ldd3的example例子,只能根据文档来自己处理,速度比较慢。在给出读写操作之前,我们先看一个 概念信号量。在以前的项目中,我们使用了spinlock,这个一个需要高速读取的例子。信号量将在我们的设备结构中即strcut scull_dev中设置,即strcut semaphore。在存储操作的过程中,例如写设备,我们不允许同时进行写的操作,这回造成数据混乱设置崩溃,因此必须加锁。如果无法得到资 源,spinlock将处于等待状态,而semaphore将处于睡眠状态,我们可以根据所需的响应要求来选择。详细可以参考http://blog.csdn.net/ShowMan/archive/2009/08/13/4442046.aspx 。我们禁止同时写,禁止在读的过程中写,禁止在写的过程中读,简单地处理,我们同时只允许一个读或者写的操作。因此我们将semaphore的初始值设置为1,实际上相当于互斥锁(Mutex)。
通过printk,我们看到当用户程序进行一个fread,他并不一定对应kernel模块中的一个read操作,因为用户程序不直接和内核模 块通信,中间是kernel,内核模块按kernel的要求而不是用户程序要求来进行。在用户程序中我们一次读取100字节。linux一个block为 4096,我们设置的一个quantum为1024。可以跟踪观察。
下面是程序头文件:
#ifndef _WEI_SCULL_H
#define _WEI_SCULL_H
#define SCULL_MAJOR 0
#define SCULL_MINOR_MIN 0
#define SCULL_DEV_NUM 4
struct scull_qset{
void ** data;
struct scull_qset * next;
};
#define SCULL_QUANTUM 1024
#define SCULL_QSET 64
struct scull_dev {
struct scull_qset * data; /* Pointer to frist quantum*/
int quantum;/* the quantum size */
int qset; /* the array size */
unsigned long size; /* 记录当前有效数据的长度*/
struct semaphore sem; /* 信号量 */
struct cdev cdev; /* Char device structure */
};
static void scull_setup_cdev(struct scull_dev * dev, int index);
int scull_trim(struct scull_dev * dev);
struct scull_qset * scull_follow(struct scull_dev * dev, int index);
int scull_open(struct inode * inode , struct file * file);
int scull_release(struct inode * inode , struct file * file);
ssize_t scull_read(struct file * filp,char __user *buff, size_t count, loff_t * offp);
ssize_t scull_write(struct file * filp, const char __user * buff, size_t count ,loff_t * offp);
#endif
下面是程序文件:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>
#include <linux/errno.h>
#include "scull.h"
MODULE_LICENSE("Dual BSD/GPL");
dev_t dev;
int is_get_dev = -1;
static int scull_major = SCULL_MAJOR;
struct file_operations scull_fops = {
.owner = THIS_MODULE,
.open = scull_open,
.release = scull_release,
.read = scull_read,
.write = scull_write,
};
struct scull_dev mydev[SCULL_DEV_NUM];
static int __init scull_init(void)
{
int i =0;
printk("Scull module init enter/n");
if(scull_major){
dev = MKDEV(scull_major,SCULL_MINOR_MIN);
is_get_dev = register_chrdev_region(dev, SCULL_DEV_NUM,"scull");
}else{
is_get_dev = alloc_chrdev_region(&dev,SCULL_MINOR_MIN, SCULL_DEV_NUM,"scull");
scull_major = MAJOR(dev);
}
if(is_get_dev < 0){
printk(KERN_WARNING "scull: can't get device major number %d/n",scull_major);
return is_get_dev;
}
for(i = 0 ; i < SCULL_DEV_NUM; i ++){
scull_setup_cdev(&mydev[i],i);
}
return 0;
}
static void __exit scull_exit(void)
{
if(is_get_dev < 0){
return ;
}else{
int i = 0;
for(i = 0; i < SCULL_DEV_NUM; i ++){
scull_trim( &mydev[i]);
cdev_del( & mydev[i].cdev );
}
unregister_chrdev_region(dev,SCULL_DEV_NUM);
printk("Scull module exit/n");
}
}
module_init(scull_init);
module_exit(scull_exit);
static void scull_setup_cdev(struct scull_dev * dev, int index)
{
int err;
int devno = MKDEV(scull_major,SCULL_MINOR_MIN + index);
sema_init( & dev->sem,1); /*我们设备初始建立的时候对设备的信号量进行初始化,该信号量只允许一 个任务占有。初始化信号量要及早进行,已满信号量在没有初始化之前,已经有进程要求获取。由于设置为1,即互斥锁的方式,也可以通过 init_MUTEX(&dev->sem)来进行初始化。*/
printk("scull%d %d,%d is %d/n", index,scull_major, SCULL_MINOR_MIN + index, devno);
cdev_init (& dev->cdev, & scull_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = & scull_fops;
err = cdev_add(&dev->cdev,devno,1);
if(err){
printk(KERN_NOTICE "scull : Err %d adding scull %d/n", err,index);
}
}
int scull_open(struct inode * inode ,struct file *filp)
{
struct scull_dev * dev;
printk("scull_open is called, node %d %d/n",imajor(inode), iminor(inode));
dev = container_of(inode->i_cdev,struct scull_dev,cdev);
filp->private_data = dev;
printk("scull: open mode is %x/n",filp->f_mode);
if((filp->f_flags & O_ACCMODE) == O_WRONLY){
printk("scull: only write: free all data/n");
scull_trim(dev);}
printk("sucll: opened.../n");return 0;
}
int scull_release(struct inode * inode, struct file * filp)
{
printk("scull: release is called/n");
return 0;
}/* 读操作:第二个参数来自用户空间的buf指针。第三个是用户空间要求读取的长度,来自应用程序或者libc。根据我们的用户测试小程序,我们预期这个值为100,我们将跟踪这个数值的变化。 第四个参数是编译量,实际对应filp->f_pos,为了保证不断通过fread命令能够全部读取所有的内容,我们每读取一次,将改变这编译值,用来记录下次应开始读取的offset。*/
/*返回实际读取的长度,如果有效读取部分,出现错误,将给出有效读取部分的长度,下次继续读取时候才报告错误。如果全部读完,将返回0。这 里有一个很好的编程技巧:如果我们要求读某个长度的内容,可能包括跨越我们定义的一些数据结构边界,或者包括多个数据结构,我们通常会经过一些复杂的计 算,获取全部所需内容,然后返回,但是这种情况,会使得我们的程序复杂,将包括一些算法计算,在日后读起来复杂。可以简单地将读取限制在一个数据结构内, 这里限制在一个quantum中,即我们的一次获取的数据不会跨越一个data(无需考虑链表结构)也不会跨越一个quantum,只需返回实际读取的字 节告诉尚未读取完毕,需要继续读取。这样整个程序非常简洁易懂。 【编程思想:对复杂数据结构的读写技巧】 */
ssize_t scull_read(struct file * filp, char __user * buf, size_t count,loff_t * f_pos )
{
struct scull_dev * dev = filp->private_data;
struct scull_qset * dptr;
int quantum = dev->quantum;
int qset = dev->qset;
int itemsize = quantum * qset ;
int item,s_pos,q_pos,rest;
ssize_t retval = 0;
printk("scull : scull_read is called/n");/*开启信号量的排斥锁,我们将禁止在读的时候进行写,或者在写的时候进行读。*/
if(down_interruptible(&dev->sem ))
return -ERESTARTSYS ;/* 这里有些有趣的现象:在我们的用户测试程序中,我们将会15次进行读取操作,每次读取100个字节。如果我们跟踪程序,我们发现scull_read实际 上只被调用2次,dev->size是实际存贮的有效数据长度,均为1500 。第一次读取是偏移量*f_pos = 0, count=4096(我们在当前位置上跟踪)。4096是kernel传递过来的需要读取的大小,和我们程序中设定的不一样,linux这样是为了更有 效地进行读取操作。由于我们每次将限定不超越一个quantum的边界,后面count修订为1024,即第一次读取了1024字节,应用无需频繁地调用 kernel模块。第二次时*f_pos=1024,count=4096,经过修正为476。linux kernel很smart。 对于loff_t的数据结构,定义在linux/asm-arm/posix_types.h L50,实际是long long*/
/* printk("scull: dev size = %ld/n", dev->size);
printk("scull: f_pos = %lld /n",*f_pos);
printk("scull: count = %d/n",count); */
/* 如果需要读取的起始位置已经超过实际最大位置,报错返回,如果读取的最后位置查过实际的最大位置,基于安全考虑,我们只给出有效的部分。*/
if(* f_pos >= dev->size)
goto out;
if(* f_pos + count > dev->size)
count = dev->size - (* f_pos);
/* found position in the data。根据已经读取的大小,也即本次读取的开始偏移量,计算属于第几个链表item,属于第几个quantum:s_pos,在quantum中从哪 个位置开始:q_pos。然后根据item,通过scull_follow()函数找到链表入口,及struct scull_qset的存贮数据块的入口。关于数据结构可以参考kernel module编程(四):设备属性和与上层应用的联系 上的图例*/
item = (long) * f_pos / itemsize;
rest = (long) * f_pos % itemsize;
s_pos = rest / quantum;
q_pos = rest % quantum;
dptr = scull_follow(dev,item);
if(dptr == NULL || ! dptr->data || ! dptr->data[s_pos])
goto out;
/* read only ip to the end of this quantum。本次读取只读取quantum剩余下来的数据,如果不能读完,下次读取从下一quantum开始读。 */
if( count > quantum - q_pos)
count = quantum - q_pos;/*copy_to_user返回尚未被复制的字节。由于之前我们已经将count计算为有效可copy的长度,正确处理,返回0,否则认为出错。*/
if(copy_to_user(buf,dptr->data[s_pos] + q_pos, count ) ){
retval = -EFAULT;
goto out;
}/*为了保证下次读取不会重复读取,将偏移量修正*/
* f_pos += count;
retval = count;
out:
up(&dev->sem ); /*释放信号量,允许其他读写操作*/
printk("scull: retval = %d/n",retval);
return retval;
}
/*这是写入操作,和读取类似,我们也将一次写操作限定在一个quantum中。*/
ssize_t scull_write(struct file * filp, const char __user * buf, size_t count, loff_t * f_pos)
{
struct scull_dev * dev = filp->private_data;
struct scull_qset * dptr;
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset;
int item,s_pos,q_pos,rest;
ssize_t retval = -ENOMEM;
int result = 0;
if(down_interruptible(&dev->sem ))
return -ERESTARTSYS ;
item = (long) * f_pos / itemsize;
rest = (long) * f_pos % itemsize;
s_pos = rest / quantum;
q_pos = rest % quantum;
/*我们将初始化存储数据的data部分。我们不放置在scull_trim()函数中,是因为我们希望在unload模块中可以通过scull_trum()清除所有kmalloc的空间。*/
if( * f_pos == 0 ){
dev->data = kmalloc(sizeof(struct scull_qset),GFP_KERNEL);
dev->data->data = NULL;
dev->data->next = NULL;
}
dptr = scull_follow(dev,item);
if(dptr == NULL)
goto out;
if(!dptr -> data){
dptr ->data = kmalloc (qset * sizeof( char *) , GFP_KERNEL);
if(dptr->data == NULL)
goto out;
memset(dptr->data,0 , qset * sizeof(char *));
}
if(!dptr->data[s_pos]){
dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
if(dptr->data[s_pos] == NULL)
goto out;
memset(dptr->data[s_pos],0,quantum);
}
if(count > quantum - q_pos)
count = quantum - q_pos;
if( copy_from_user(dptr->data[s_pos] + q_pos,buf,count ) ){ //返回不能copy的字节,故0为正确
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;
if(dev->size < (* f_pos))
dev->size = * f_pos;
out:
up(&dev->sem );
return retval;
}/*根据属于第几个链表,获得该链表的入口*/
struct scull_qset * scull_follow( struct scull_dev *dev ,int index){
int i = 0;
struct scull_qset * point ;
if(dev == NULL)
return NULL;
point = dev->data;
while( i < index){
if(point == NULL)
return NULL;
point = point->next;
i ++;
}
return point;
}
int scull_trim(struct scull_dev * dev)
{
str uct scull_qset * next ,*dptr;
int qset = dev-> qset;
int i ;
if(dev->data){
for (dptr = dev->data; dptr != NULL; dptr = next){
if(dptr->data){
for(i = 0; i < qset; i ++)
kfree(dptr->data[i]);
kfree(dptr->data);
dptr->data = NULL;
}
next = dptr->next;
kfree(dptr);
}
}dev->size = 0;
dev->quantum = SCULL_QUANTUM;
dev->qset = SCULL_QSET;
return 0;
}
在kernel module的读写中,我们应注意kernel空间和用户空间数据的分隔,以免产生安全漏洞。现在可以对/dev/scull0使用cp,cat,dd等IO命令。