字符设备驱动(4)

-- TOC --

接上一篇,这次主要实现第1个write系统调用接口,并使用kernel提供的container_of黑函数。

代码如下:

$ cat mychar.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
MODULE_LICENSE("GPL");


#define MINOR_FIRST   3
#define MINOR_NUM     4
#define CONTENT_LEN   (1024*1024)  // 1MiB bytes space

static unsigned int major = 0;
static unsigned int minor_pos = 0; /* count of successful cdev_add */

struct file_operations mychar_fop;
struct mychar_dev {
    char *cont;
    size_t len;
    struct cdev mycdev;
} *pmychar = NULL;


int mychar_open(struct inode *inode, struct file *fp) {
    unsigned int flags;

    /* check flags */
    flags = fp->f_flags & O_ACCMODE;
    if ((flags != O_RDONLY) && (flags != O_WRONLY))
        return -EPERM;  // operation not permitted

    fp->private_data = (void *)container_of(inode->i_cdev,
                                            struct mychar_dev, mycdev);
    return 0;
}


int mychar_release(struct inode *inode, struct file *fp) {
    return 0;
}


ssize_t mychar_read(struct file *fp, char __user *buf,
                    size_t count, loff_t *f_pos) {
    unsigned long left;
    struct mychar_dev *dev=(struct mychar_dev *)fp->private_data;

    printk(KERN_INFO"[mychar] read called, count %zu.\n", count);

    if (*f_pos >= dev->len) {
        printk(KERN_INFO"[mychar] read call end, return count 0.\n");
        return 0;
    }

    /* update count if reach end */
    if ((*f_pos+count) > dev->len)
        count = dev->len - *f_pos;

    /* must use copy_to_user, can't derefer user-space */
    if ((left = copy_to_user(buf,dev->cont+*f_pos,count))) {
        if (left != count) {
            count -= left;
            printk(KERN_INFO"[mychar] copy_to_user return less than count!\n");
        }
        else return -EFAULT;
    }

    /* update *f_pos */
    *f_pos += count;

    /* return how many char readed */
    printk(KERN_INFO"[mychar] read call end, return count %zu.\n", count);
    return count;
}


/* Bug: no support concurrent writing... */
ssize_t mychar_write(struct file *fp, const char __user *buf,
                     size_t count, loff_t *f_pos) {
    unsigned long left;
    struct mychar_dev *dev=(struct mychar_dev *)fp->private_data;

    printk(KERN_INFO"[mychar] write called, count %zu.\n", count);

    /* make each write to the end of the buffer */
    *f_pos = dev->len;
    printk(KERN_INFO"[mychar] *f_pos = %llu\n", *f_pos);

    if (*f_pos >= CONTENT_LEN) {
        printk(KERN_INFO"[mychar] write call end, return count 0.\n");
        return -ENOSPC;
    }

    if ((*f_pos+count) > CONTENT_LEN)
        count = CONTENT_LEN - *f_pos;

    if ((left = copy_from_user(dev->cont+*f_pos,buf,count))) {
        if (left != count) {
            count -= left;
            printk(KERN_INFO"[mychar] copy_from_user return less than count!\n");
        }
        else return -EFAULT;
    }

    *f_pos += count;
    dev->len += count;  // update real length
    printk(KERN_INFO"[mychar] write call end, return count %zu.\n", count);
    return count;
}


struct file_operations mychar_fop = {
    .owner   = THIS_MODULE,
    .open    = mychar_open,
    .release = mychar_release,
    .read    = mychar_read,
    .write   = mychar_write,
};


static void mychar_exit(void) {
    int i;

    if (pmychar != NULL) {
        for (i=0; i<MINOR_NUM; ++i)
            if (pmychar[i].cont != NULL) kfree(pmychar[i].cont);
        for (i=0; i<minor_pos; ++i)
            cdev_del(&pmychar[i].mycdev);
        kfree(pmychar);
    }
    unregister_chrdev_region(MKDEV(major,MINOR_FIRST), MINOR_NUM);
    printk(KERN_INFO"[mychar] exit.\n");
}


static int __init mychar_init(void) {
    int rn, i;
    dev_t dev;

    /* get a major number */
    if ((rn = alloc_chrdev_region(&dev,MINOR_FIRST,MINOR_NUM,"mychar"))) {
        printk(KERN_WARNING"[mychar] can't get major number, err %d.\n", rn);
        return rn;
    }
    major = MAJOR(dev);
    printk(KERN_INFO"[mychar] major is %d, %d minor start from %d.\n",
                major, MINOR_NUM, MINOR_FIRST);

    /* alloc mychar */
    pmychar = kmalloc(MINOR_NUM*sizeof(struct mychar_dev), GFP_KERNEL);
    if (pmychar == NULL) {
        mychar_exit();
        return -ENOMEM;
    }
    memset(pmychar, 0, MINOR_NUM*sizeof(struct mychar_dev));
    for (i=0; i<MINOR_NUM; ++i)
        if ((pmychar[i].cont=kmalloc(CONTENT_LEN,GFP_KERNEL)) == NULL) {
            mychar_exit();
            return -ENOMEM;
        }

    /* init and add cdev */
    for (i=0; i<MINOR_NUM; ++i) {
        cdev_init(&pmychar[i].mycdev, &mychar_fop);
        if((rn = cdev_add(&pmychar[i].mycdev,MKDEV(major,MINOR_FIRST+i),1))){
            printk(KERN_WARNING"[mychar] cdev_add err %d, minor %d.\n", 
                        rn, i+MINOR_FIRST);
            mychar_exit();
            return rn;
        }
        ++minor_pos;
    }
    printk(KERN_INFO"[mychar] %d devices added successfully.\n",minor_pos);

    /* success return */
    return 0;
}


module_init(mychar_init);
module_exit(mychar_exit);

在init模块的时候申请内存,每个device固定1M,在exit的时候释放。

可以通过只读或只写的方式open。用container_of接口获得mychar_dev结构体的地址。此函数定义在<linux/kernel.h>中:

/**
 * container_of - cast a member of a structure out to the containing structure
 * @ptr:        the pointer to the member.
 * @type:       the type of the container struct this is embedded in.
 * @member:     the name of the member within the struct.
 *
 */
#define container_of(ptr, type, member) ({                              \
        void *__mptr = (void *)(ptr);                                   \
        BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) &&   \
                         !__same_type(*(ptr), void),                    \
                         "pointer type mismatch in container_of()");    \
        ((type *)(__mptr - offsetof(type, member))); })

做一些检查后,直接通过减法计算地址!上一篇的代码用minor number去对应地址,这两种方式都可以,也许后者在速度和可读性上更好。

write接口调用copy_from_user实现将用户控件的内容copy进device自己的1M控件内,用dev->len来记录真实的长度。这个版本write接口有一个缺陷,还不能支持并发!先记录在这里...

copy_to_usercopy_from_user接口的调用做了优化,使用其返回值,如果left非零,表示部分成功,此时更新count后返回,也许下次在进来的时候,这两个函数就出错了!在kernel搜索代码,发现很多调用这两个接口的代码,都没有这样使用,一旦left为非零,就直接返回-EFAULT。

测试:

$ sudo bash unload.sh 
$ sudo bash load.sh 
$ cp /dev/urandom mychar-6
cp: error writing 'mychar-6': No space left on device
xinlin@K:~/test/mychar4$ cat mychar-6 | wc -c
1048576

1048576 bytes 就是 1M!

更多测试就略了,各位同学自己玩...

本文链接:https://cs.pynote.net/sf/linux/dd/202112101/

-- EOF --

-- MORE --