linux 内核编程之字符设备驱动

来源:互联网 发布:淘宝客如何被卖家找到 编辑:程序博客网 时间:2024/04/29 11:46
【版权声明:转载请保留出处:blog.csdn.net/gentleliu。邮箱:shallnew*163.com】

首先需要注册设备号,有两个函数可以实现该功能:

int register_chrdev_region(dev_t from, unsigned count, const char *name);int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

第一个函数参数用于已知设备号的情况,from为要分配设备号的起始值,count是所请求的连续设备号的个数,name为设备名称。
第二个函数参数用于未知设备号的情况,dev用于保存申请成功后的第一个设备号,baseminor第一个要使用的次设备号,常常为0,另外参数同上。该函数可以自动避开设备号重复的冲突,会使用一个未使用的设备号。

由于对字符设备的访问是通过文件系统内的设备名称来访问的,设备名称位于目录/dev下.为了便于系统管理,设置了和设备名称一一对应的设备号,它分为主设备号和次设备号.通常来说,主设备号标示了设备对应的驱动程序,次设备号则用来分辨拥有同一个主设备号的的各个不同设备。
使用第一个函数register_chrdev_region()需要给出一个自定义的设备号,在内核中,设备号使用类型dev_t来保存,它包括了主设备号和次设备号。dev_t是一个32位的整数,其中的12位用来标示主设备号,其余的20位用来标示次设备号.我们可以使用两个宏来获得设备的主设备号及次设备号:
    MAJOR(dev_t dev_id);    MINOR(dev_t dev_id);
    将主设备号和次设备号转换为dev_t类型,则可以使用下面的宏:
    MKDEV(int major, int minor);
    其中,major为主设备号,minor为次设备号。
我们将MKDEV返回的设备号传递给register_chrdev_region()函数的第一个参数。
上面宏的实现如下:
#define MINORBITS    20#define MINORMASK    ((1U << MINORBITS) - 1)#define MAJOR(dev)    ((unsigned int) ((dev) >> MINORBITS))#define MINOR(dev)    ((unsigned int) ((dev) & MINORMASK))#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))
在分配好了设备号之后需要初始化设备结构体,使用函数
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
该函数带两参数,先看一下其结构体:
struct cdev {    struct kobject kobj;    struct module *owner;    //所属模块    const struct file_operations *ops;//文件操作结构体    struct list_head list;    dev_t dev;    //设备号    unsigned int count;};struct 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 *);    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);    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 *, struct dentry *, 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 **);};

该结构体里面的函数是字符设备驱动设计的主要内容,实现该设备驱动主要就是实现该结构体里面的函数,这些函数会在linux应用层使用函数open,close,read,write,lseek等时进行调用。
再来看以下该函数的实现:
void cdev_init(struct cdev *cdev, const struct file_operations *fops){    memset(cdev, 0, sizeof *cdev);    INIT_LIST_HEAD(&cdev->list);    kobject_init(&cdev->kobj, &ktype_cdev_default);    cdev->ops = fops;//这一行就是将传入的第二个参数赋值给cdev的ops。}
所以在调用该函数之前,还需要初始化file_operations结构体。一般使用C标记式结构初始化语法,如下:
static const struct file_operations xxx_fops ={  .owner = THIS_MODULE,  .llseek = xxx_llseek,  .read = xxx_read,  .write = xxx_write,  .ioctl = xxx_ioctl,  .open = xxx_open,  .release = xxx_release,};
之后再定义该设备结构体所有者:
cdev.owner = THIS_MODULE;

然后就可以向模快添加该设备结构体,使用函数
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
参数p为将要添加的设备结构体,dev为该设备要求的第一个设备号,count为关联到设备的设备号数目,常常为1。
其内核实现如下:
int cdev_add(struct cdev *p, dev_t dev, unsigned count){    p->dev = dev;    p->count = count;    return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);}

到此,设备创建就成功了,剩下的只需要实现设备上的操作了。该函数的调用通常发生在驱动模块加载函数中。

当你不使用设备时,可以注销设备,使用
void cdev_del(struct cdev *dev);
还应该释放原先申请的设备号:使用
void unregister_chrdev_region(dev_t from, unsigned count);
这两函数的调用通常发生在驱动模块卸载函数中。
下面给一个字符驱动编写的模板框架:
#include <linux/init.h>#include <linux/module.h>#include <linux/cdev.h>#include <linux/fs.h>#define DEV_NAME        "xxx_cdev"int     xxx_major = 0;dev_t   xxx_devno;struct xxx_cdev_t {    struct cdev cdev;} xxx_dev;int xxx_open(struct inode *inode, struct file *filep){    return 0;}static const struct file_operations xxx_fops = {    .owner = THIS_MODULE,    .open = xxx_open,};static int __init xxx_init(void){    int         ret;    if (xxx_major) {        xxx_devno = MKDEV(xxx_major, 0);        register_chrdev_region(xxx_devno, 1, DEV_NAME);    } else {        alloc_chrdev_region(&xxx_devno, 0, 1, DEV_NAME);    }    cdev_init(&xxx_dev.cdev, &xxx_fops);    xxx_dev.cdev.owner = THIS_MODULE;    if ((ret = cdev_add(&xxx_dev.cdev, xxx_devno, 1) < 0)) {        unregister_chrdev_region(xxx_devno, 1);        return ret;    }    return 0;}static void __exit xxx_exit(void){    unregister_chrdev_region(xxx_devno, 1);    cdev_del(&xxx_dev.cdev);}module_init(xxx_init);module_exit(xxx_exit);MODULE_LICENSE("GPL");
下面来讲一讲file_operations结构体成员函数的实现,因为这些函数是字符设备和内核接口,用户态应系统调用的最终实施者,实现字符设备驱动主要就是实现这些函数。一般read,write,ioctl这些函数一般都会实现。
通过上面file_operations结构体可以看到该结构体内函数的原型,其中有一个很重要的结构struct file,该结构体如下:
struct file {    /*     * fu_list becomes invalid after file_free is called and queued via     * fu_rcuhead for RCU freeing     */    union {        struct list_head    fu_list;        struct rcu_head     fu_rcuhead;    } f_u;    struct path        f_path;#define f_dentry    f_path.dentry#define f_vfsmnt    f_path.mnt    const struct file_operations    *f_op;    spinlock_t        f_lock;  /* f_ep_links, f_flags, no IRQ */    atomic_long_t        f_count;    unsigned int         f_flags;    fmode_t            f_mode;    loff_t            f_pos;    struct fown_struct    f_owner;    const struct cred    *f_cred;    struct file_ra_state    f_ra;    u64            f_version;#ifdef CONFIG_SECURITY    void            *f_security;#endif    /* needed for tty driver, and maybe others */    void            *private_data;#ifdef CONFIG_EPOLL    /* Used by fs/eventpoll.c to link all the hooks to this file */    struct list_head    f_ep_links;#endif /* #ifdef CONFIG_EPOLL */    struct address_space    *f_mapping;#ifdef CONFIG_DEBUG_WRITECOUNT    unsigned long f_mnt_write_state;#endif};
其中private_data 是一个有用的资源, 在系统调用间保留状态信息,一般在open中设置它。
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
读写函数第一个参数为文件结构体指针,第二个参数是用户空间的内存地址,在内核空间不能直接读写,第三个参数是要读写的文件字节数,第四个参数是读写位置相对于文件开头的位置。
内核空间和用户空间不能直接相互访问,要借助函数copy_to_user和copy_from_user才能实现内核空间和用户空间的内存拷贝。者两函数原型为:
unsigned long copy_from_user(void *to, const void _ _user *from, unsigned long count);unsigned long copy_to_user(void _ _user *to, const void *from, unsigned long count);
函数返回不能复制的字节数,完全复制返回0。
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
第三个参数为命令,第四个参数为要传入的参数。
编写Linux驱动时一般将文件的私有数据 private_data
指向设备结构体,在 read()、write()、ioctl()、llseek()等函数通过 private_data 访问设备结构体。
一般在open时设置private_data数据。
struct inode结构体中包含了大量有关文件的信息,该结构体中包含一个字符设备结构i_cdev。当inode指向一个字符设备文件时,该字段包含了指向struct cdev结构的指针, 我们可以通过该结构体找到包含字符设备的私有信息

2 0