字符设备驱动

相关概念

设备号

设备号概念

Linux系统将所有的字符设备抽象成文件呈献给用户,用户可以在/dev 目录下找到绝大部分的字符设备文件,比如/dev/tty等。应用程序访问设备的途径正是这些设备文件。这产生了一个问题:如何将这些设备文件跟具体的设备联系起来呢?事实上,Linux系统内部为了管理字符设备,给每个字符设备设置了唯一的“编号”,也叫设备号(其实就是一个32位的整数),通过这个设备号,内核可以知道哪个文件是对应哪个设备的。
每个设备号由两部分组成:主设备号和次设备号。前面说了设备号为设备文件和设备联系起来了,但是每个设备驱动可能不只处理一个设备,比如一个串口设备驱动可能要处理两个串口的收发。这时就可以将这主设备号和从设备号的功能细分了。即主设备号负责将设备文件与设备驱动联系起来,而次设备号就将设备驱动与具体的设备联系起来。

分配设备号:

静态分配设备号,用于设备号已知的情况,不常用:

int register_chrdev_region(dev_t first, unsigned int count,char* name);

动态分配设备号,用于设备号未知并由内核自行分配的情况,常用:

int alloc_chrdev_region(dev_t *dev,unsigned int firstminor,unsigned int count,char *name);

释放设备号

下面的函数用于将设备号释放,通常在驱动模块卸载的时候释放设备号:

int unregister_chrdev_region(dev_t dev, unsigned int count);

从设备号分出主、次设备号

int MAJOR(dev_t dev);//分离出主设备号
int MINOR(dev_t dev);//分离出次设备号

合成设备号

dev_t MKDEV(unsigned int major,unsigned int minor);


主要数据结构介绍

字符设备描述结构struct cdev

在内核中每个设备都由一个结构来描述,并且采用了面向对象的思想,即属性和行为集合在一个结构中。但内核并没有采用C++等面向对象语言,因此采用结构体描述,并且在结构体中包含一个函数集。
字符设备由结构体 struct cdev 来描述,其定义如下:

struct cdev{
    struct kobject kobj;      //模块kobject 
    struct module *owner;     //模块所有者
    const struct file_operations *ops;   //驱动操作函数集
    struct list_head list;    //字符设备结构链表
    dev_t dev;                //设备号
    unsigned int count;      //子设备数
};

编写驱动主要关注 ops、dev、count 这三个字段,其余字段由内核维护,其中ops 指向这个设备所支持的操作集,dev 是这个设备的设备号,count 是这个设备的子设备数比如串口设备,这个变量指出串口的个数。

打开文件结构struct file

内核中每个打开的文件都由一个struct file结构体描述,这个结构体包含了进程打开的这个文件的所有属性,因为这个结构体比较庞大,因此这里只列出与驱动编写相关的字段:

struct file {
    ...
    struct file_operations *f_op; //这个文件支持的操作
    loff_t   f_pos;               //当前读写的位置
    void     *private_data;       //可用的额外指针
    ...
}

f_op 指向这个函数可以执行的操作集, f_pos是当前文件的读写位置,private_data 可以指向驱动其他空间,驱动工程师可以将这个指针指向自己开辟的内核空间。

文件操作集合struct file_operations

每个打开的文件都会对应自己的操作集合,用以响应用户空间调用的文件系统调用。如open、read、write、close、ioctl等系统调用,最终响应这些系统调用的就是驱动中定义在file_operations 的函数。其定义如下:

truct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    int (*readdir) (struct file *, void *, filldir_t);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
/* remove by cym 20130408 support for MT660.ko */
#if 0
//#ifdef CONFIG_SMM6260_MODEM
#if 1// liang, Pixtree also need to use ioctl interface...
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
#endif
#endif
/* end remove */
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, int datasync);
    int (*aio_fsync) (struct kiocb *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **);
    long (*fallocate)(struct file *file, int mode, loff_t offset,
              loff_t len);
/* add by cym 20130408 support for MT6260 and Pixtree */
#if defined(CONFIG_SMM6260_MODEM) || defined(CONFIG_USE_GPIO_AS_I2C)
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
#endif
/* end add */
};

文件索引节点struct inode

内核用inode 结构在内部表示文件,但是不同于file结构,inode结构在系统中是唯一的,因为它描述了一个文件的存在。而file结构描述了一个进程打开文件后的状态,不同的进程可能打开同一个文件。不同的进程打开同一个文件后,每个进程都有各自的file结构,但是都指向了同一个inode 结构。inode结构中包含了大量有关文件的信息,但是这里我们只关注对驱动编写有关的两个字段:

struct inode{
    ...
    dev_t  i_rdev;        //对表示设备文件的inode结构,该字段包含了真正的设备号。
    struct cdev *i_cdev;  //当inode指向一个字符设备文件时,该字段包含了指向struct cdev结构的指针。
    ...
};

设备注册与注销

设备注册

前面提到内部用struct cdev结构来表示字符设备,在内核调用设备的操作之前,我们必须将这个结构初始化并注册到内核中。
初始化cdev:

void cdev_init(struct cdev *dev,struct file_operations *fops);

这个函数将cdev 结构与具体的file_operations 联系,如果追踪这个函数的实现可以发现,其实这个函数就是将fops赋给cdev->ops。

注册cdev:

int cdev_add(struct cdev *dev,dev_t num, unsigned int count);

所谓注册设备就是告诉内核该结构的信息。在驱动中一旦调用这个函数成功,则我们的设备就“活”了,内核就能调用它的操作,因此我们的驱动在没准备好操作集(file_operations)之前,不要调用这个函数。

设备注销

通过下面的方法将设备注销掉:

void cdev_del(struct cdev *dev);

思维导图

根据上面提到的知识,画出编写一个字符设备驱动的思维导图如下:

设备驱动模型

范例代码

最后给出一个字符设备驱动的范例代码,该驱动中开辟一块内核内存模拟设备中的内存,应用程序可以对这个虚拟设备进行读写。

#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>
#include <linux/slab.h>


int dev1_registers[5];
int dev2_registers[5];

struct cdev cdev; 
dev_t devno;

/*文件打开函数*/
int mem_open(struct inode *inode, struct file *filp)
{

    /*获取次设备号*/
    int num = MINOR(inode->i_rdev);

    if (num==0)
        filp->private_data = dev1_registers;
    else if(num == 1)
        filp->private_data = dev2_registers;
    else
        return -ENODEV;  //无效的次设备号

    return 0; 
}

/*文件释放函数*/
int mem_release(struct inode *inode, struct file *filp)
{
    return 0;
}

/*读函数*/
static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
    unsigned long p =  *ppos;
    unsigned int count = size;
    int ret = 0;
    int *register_addr = filp->private_data; /*获取设备的寄存器基地址*/

    /*判断读位置是否有效*/
    if (p >= 5*sizeof(int))
    return 0;
    if (count > 5*sizeof(int) - p)
    count = 5*sizeof(int) - p;

    /*读数据到用户空间*/
    if (copy_to_user(buf, register_addr+p, count))
    {
    ret = -EFAULT;
    }
    else
    {
    *ppos += count;
    ret = count;
    }

    return ret;
}

/*写函数*/
static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
    unsigned long p =  *ppos;
    unsigned int count = size;
    int ret = 0;
    int *register_addr = filp->private_data; /*获取设备的寄存器地址*/

    /*分析和获取有效的写长度*/
    if (p >= 5*sizeof(int))
    return 0;
    if (count > 5*sizeof(int) - p)
    count = 5*sizeof(int) - p;

    /*从用户空间写入数据*/
    if (copy_from_user(register_addr + p, buf, count))
    ret = -EFAULT;
    else
    {
    *ppos += count;
    ret = count;
    }

    return ret;
    }

    /* seek文件定位函数 */
    static loff_t mem_llseek(struct file *filp, loff_t offset, int whence)
    { 
    loff_t newpos;

    switch(whence) {
    case SEEK_SET: 
    newpos = offset;
    break;

    case SEEK_CUR: 
    newpos = filp->f_pos + offset;
    break;

    case SEEK_END: 
    newpos = 5*sizeof(int)-1 + offset;
    break;

    default: 
    return -EINVAL;
    }
    if ((newpos<0) || (newpos>5*sizeof(int)))
    return -EINVAL;

    filp->f_pos = newpos;
    return newpos;

}

/*文件操作结构体*/
static const struct file_operations mem_fops =
{
    .llseek = mem_llseek,
    .read = mem_read,
    .write = mem_write,
    .open = mem_open,
    .release = mem_release,
};

/*设备驱动模块加载函数*/
static int memdev_init(void)
{
    /*初始化cdev结构*/
    cdev_init(&cdev, &mem_fops);

    /* 注册字符设备 */
    alloc_chrdev_region(&devno, 0, 2, "memdev");
    cdev_add(&cdev, devno, 2);
}

/*模块卸载函数*/
static void memdev_exit(void)
{
    cdev_del(&cdev);   /*注销设备*/
    unregister_chrdev_region(devno, 2); /*释放设备号*/
}

MODULE_LICENSE("GPL");
module_init(memdev_init);
module_exit(memdev_exit);
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值