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

本文介绍Linux字符设备驱动的读写操作实现细节,包括信号量使用、内存分配、数据结构处理等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  本文也即是《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.youkuaiyun.com/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有关的文章
我的与编程思想相关的文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值