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

【版权声明:转载请保留出处: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结构的指针, 我们可以通过该结构体找到包含字符设备的私有信息

你可能感兴趣的:(内核,linux内核,linux驱动,字符设备)