kernel module编程(五):设备读写

    技术2022-05-11  9

      本文也即是《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命令。

      相关链接: 我的与kernel module有关的文章 我的与编程思想相关的文章


    最新回复(0)