首先需要注册设备号,有两个函数可以实现该功能:
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);将主设备号和次设备号转换为dev_t类型,则可以使用下面的宏:
MINOR(dev_t dev_id);
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)所以在调用该函数之前,还需要初始化file_operations结构体。一般使用C标记式结构初始化语法,如下:
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;//这一行就是将传入的第二个参数赋值给cdev的ops。
}
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>下面来讲一讲file_operations结构体成员函数的实现,因为这些函数是字符设备和内核接口,用户态应系统调用的最终实施者,实现字符设备驱动主要就是实现这些函数。一般read,write,ioctl这些函数一般都会实现。
#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结构体可以看到该结构体内函数的原型,其中有一个很重要的结构struct file,该结构体如下:
struct file {其中private_data 是一个有用的资源, 在系统调用间保留状态信息,一般在open中设置它。
/*
* 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
};
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);函数返回不能复制的字节数,完全复制返回0。
unsigned long copy_to_user(void _ _user *to, const void *from, unsigned long count);
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结构的指针, 我们可以通过该结构体找到包含字符设备的私有信息