字符设备驱动
Linux字符设备驱动结构
cdev结构体在Linux内核中,使用cdev结构体描述一个字符设备,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;
- };
cdev结构体的dev_t成员定义了设备号,为32位,其中12位为主设备号,20位为次设备号。使用下列宏可以从dev_t获得主设备号和次设备号:
- #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
- #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
- #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
而使用下列宏可以通过主设备号和次设备号生成dev_t:
- #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
cdev结构体的另一个重要成员file_operations定义了字符设备驱动提供给虚拟文件系统的接口函数。
Linux内核提供了一组函数用于操作cdev结构体:
- void cdev_init(struct cdev *, const struct file_operations *);
- struct cdev *cdev_alloc(void);
- void cdev_put(struct cdev *p);
- int cdev_add(struct cdev *, dev_t, unsigned);
- void cdev_del(struct cdev *);
cdev_init()函数用于初始化cdev的成员,并建立cdev和file_operations之间的连接,其源代码如下所示:
- 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 */
- }
cdev_alloc()函数用于动态申请一个cdev内存,其源码如下所示:
- struct cdev *cdev_alloc(void)
- {
- struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
- if (p) {
- INIT_LIST_HEAD(&p->list);
- kobject_init(&p->kobj, &ktype_cdev_dynamic);
- }
- return p;
- }
cdev_add()函数和cdev_del()函数分别向系统添加和删除一个cdev,完成字符设备的注册和注销。对cdev_add()的调用通常发生在字符设备驱动模块加载函数中,而对cdev_del()函数的调用则通常发生在字符设备驱动模块卸载函数中。
分配和释放设备号在调用cdev_add()函数向系统注册字符设备之前,应首先调用register_chrdev_region()或alloc_chrdev_region()函数向系统申请设备号,这两个函数的原型为:
- extern int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);
- extern int register_chrdev_region(dev_t, unsigned, const char *);
register_chrdev_region()函数用于已知起始设备的设备号情况,而alloc_chrdev_region()用于设备号未知,向系统动态申请未被占用的设备号的情况,函数成功调用之后,会把得到的设备号放入第一个参数dev中。alloc_chrdev_region()相比register_chrdev_region()的优点在于它会自动避开设备号重复的冲突。
相应地,在调用cdev_del()函数从系统注销字符设备之后,unregister_chrdev_region()应该被调用以释放原先申请的设备号,这个函数的原型为:
- extern void unregister_chrdev_region(dev_t, unsigned);
file_operations结构体中的成员函数是字符设备驱动程序设计的主体内容,这些函数实际会在应用程序进行Linux的open()、write()、read()、close()等系统调用时最终被内核调用。file_operations结构体目前已经比较庞大,它的定义如下所示(下面抄录的是5.3版本的源码,aio_read()似乎变成了read_iter()):
- 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 (*read_iter) (struct kiocb *, struct iov_iter *);
- ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
- int (*iopoll)(struct kiocb *kiocb, bool spin);
- int (*iterate) (struct file *, struct dir_context *);
- int (*iterate_shared) (struct file *, struct dir_context *);
- __poll_t (*poll) (struct file *, struct poll_table_struct *);
- 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 *);
- unsigned long mmap_supported_flags;
- int (*open) (struct inode *, struct file *);
- int (*flush) (struct file *, fl_owner_t id);
- int (*release) (struct inode *, struct file *);
- int (*fsync) (struct file *, loff_t, loff_t, 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 (*setfl)(struct file *, unsigned long);
- 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 **, void **);
- long (*fallocate)(struct file *file, int mode, loff_t offset,
- loff_t len);
- void (*show_fdinfo)(struct seq_file *m, struct file *f);
- #ifndef CONFIG_MMU
- unsigned (*mmap_capabilities)(struct file *);
- #endif
- ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
- loff_t, size_t, unsigned int);
- loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
- struct file *file_out, loff_t pos_out,
- loff_t len, unsigned int remap_flags);
- int (*fadvise)(struct file *, loff_t, loff_t, int);
- };
下面我们对file_operations结构体中的主要成员进行分析。
llseek()函数用来修改一个文件的当前读写位置,并将新位置返回,在出错时,这个函数返回一个负值。
read()函数用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值。它与用户空间程序中的read()和fread()对应。
write()函数向设备发送数据,成功时改函数返回写入的字节数。如果此函数未被实现,当用户进行write()系统调用时,将得到-EINVAL返回值。它与用户空间应用程序中的write()和fwrite()对应。
read()和write()如果返回0,则暗示end-of-file(EOF)。
unlocked_ioctl()提供设备相关控制命令的实现(既不是读操作,也不是写操作),当调用成功时,返回给调用程序一个非负值。它与用户空间应用程序调用的fcntl()和ioctl()对应。
mmap()函数将设备内存映射到进程的虚拟地址空间中,如果设备驱动未实现此函数,用户进行mmap()函数调用时将获得-ENODEV返回值。这个函数对于帧缓冲等设备特别有意义,帧缓冲被映射到用户空间后,应用程序可以直接访问它而无须在内核和应用间进行内存复制。它与用户空间应用程序中的mmap()函数对应。
当用户空间调用Linux API函数open()打开设备文件时,设备驱动的open()函数最终被调用。驱动程序可以不实现这个函数,在这种情况下,设备打开操作永远不成功。与open()函数对应的是release()函数。
poll()函数一般用于询问设备是否可被非阻塞地立即读写。当询问的条件未触发时,用户空间进行select()和poll()系统调用将引起进程的阻塞。
aio_read()和aio_write()函数分别对文件描述符对应的设备进行异步读、写操作。设备实现这两个函数后,用户空间可以对设备文件描述符执行SYS_io_setup、SYS_io_submit、SYS_io_getevents、SYS_io_destroy等系统调用进行读写。
Linux字符设备驱动的组成在Linux中,字符设备驱动由如下几个部分组成。
1.字符设备驱动模块加载与卸载函数
在字符设备驱动模块加载函数中应该实现设备号的申请和cdev的注册,而在卸载函数中应实现设备号的释放和cdev的注销。
Linux内核的编码习惯是为设备定义一个设备相关的结构体,该结构体包含设备所涉及的cdev、私有数据及锁等信息。常见的设备结构体、模块加载和卸载函数形式如下面代码所示:
- /* 设备结构体 */
- struct xxx_dev_t
- {
- struct cdev cdev;
- /* ... 附加内容 ... */
- } xxx_dev;
- static int __init xxx_init(void)
- {
- /* ... 附加内容 ... */
- cdev_init(&xxx_dev.cdev, &xxx_fops); /* 初始化cdev */
- xxx_dev.cdev.owner = THIS_MODULE;
- /* 获取字符设备号 */
- if (xxx_major)
- {
- register_chrdev_region(xxx_dev_no, 1, DEV_NAME);
- }
- else
- {
- alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME);
- }
- ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); /* 注册设备 */
- /* ... 附加内容 ... */
- }
- /* 设备驱动模块卸载函数 */
- static void __exit xxx_exit(void)
- {
- unregister_chrdev_region(xxx_dev_no, 1); /* 释放占用的设备号 */
- cdev_del(&xxx_dev.cdev); /* 注销设备 */
- /* ... 附加内容 ... */
- }
2.字符设备驱动的file_operations结构体中的成员函数
file_operations结构体中的成员函数是字符设备驱动与内核虚拟文件系统的接口,是用户空间对Liux进行系统调用最终的落实者。大多数字符设备驱动会实现read()、write()和ioctl()函数,常见的字符设备驱动的这3个函数的形式如下面代码所示:
- /* 读设备 */
- ssize_t xxx_read(struct file* filp, char __user* buf, size_t count, loff_t* f_ops)
- {
- /* ... 附加内容 ... */
- copy_to_user(buf, from, n);
- /* ... 附加内容 ... */
- }
- /* 写设备 */
- ssize_t xxx_write(struct file* filp, const char __user* buf, size_t count, loff_t* f_ops)
- {
- /* ... 附加内容 ... */
- copy_from_user(to, buf, n);
- /* ... 附加内容 ... */
- }
- /* ioctl函数 */
- long xxx_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
- {
- /* ... 附加内容 ... */
- switch (cmd)
- {
- case XXX_CMD1:
- /* code */
- break;
- case XXX_CMD2:
- /* code */
- break;
- default:
- /* 不能支持的指令 */
- return -ENOTTY;
- }
- return 0;
- }
设备驱动的读函数中,filp是文件结构体指针,buf是用户空间内存的地址,该地址在内核空间不宜直接读写,count是要读的字节数,f_pos是读的位置相对于文件开头的偏移。
由于用户空间不能直接访问内核空间的内存,因此借助了函数copy_from_user()完成用户空间缓存区到内核空间的复制,以及copy_to_user()完成内核空间到用户空间缓冲区的复制。
完成内核空间和用户空间内存复制的copy_from_user()和copy_to_user()的原型分别为:
- unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
- unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
上述函数均返回不能被复制的字节数,因此,如果完全复制成功,返回值为0.如果复制失败,则返回负值。
如果要复制的内存是简单类型,如char、int、long等,则可以使用简单的put_user()和get_user()。
I/O控制函数的cmd参数为事先定义的I/O控制命令,而arg为对应于改命令的参数。例如对于串行设备,如果SET_BAUDRATE是一道设置波特率的命令,那后面的arg就应该是波特率值。
在字符设备驱动中,需要定义一个file_operations的实例,并将具体设备驱动的函数赋值给file_operations的成员。
下图为字符设备驱动的结构、字符设备驱动与字符设备以及字符设备驱动与用户空间访问该设备的程序之间的关系。
globalmem虚拟设备实例描述
从本章开始,后续的章节都将基于虚拟的globalmem设备进行字符设备驱动的讲解。globalmem意味着“全局内存”,在globalmem字符设备驱动中会分配一片大小为GLOBALMEM_SIZE(4KB)的内存空间,并在驱动中提供针对该片内存的读写、控制和定位函数,以供用户空间的进程能通过Linux系统调用获取或设置这片内存的内容。
实际上,这个虚拟的globalmem设备几乎没有任何实用价值,仅仅是一种为了讲解问题的方便而凭空制造的设备。
本章将给出globalmem设备驱动的雏形,而后续章节会在这个雏形的基础上添加并发与同步控制等复杂功能。
globalmem设备驱动
头文件、宏及设备结构体在globalmem字符设备驱动中,应包含它要使用的头文件,并定义globalmem设备结构体及相关宏。
- #include <linux/module.h>
- #include <linux/fs.h>
- #include <linux/init.h>
- #include <linux/cdev.h>
- #include <linux/slab.h>
- #include <linux/uaccess.h>
- #define GLOBALMEM_SIZE 0x1000
- #define MEM_CLEAR 0x01
- #define GLOBALMEM_MAJOR 230
- static int globalmem_major = GLOBALMEM_MAJOR;
- module_param(globalmem_major, int, S_IRUGO);
- struct globalmem_dev
- {
- struct cdev cdev;
- unsigned char mem[GLOBALMEM_SIZE];
- };
从代码中可以看出,定义的globalmem_dev设备结构体包含了对应于globalmem字符设备的cdev、使用的内存mem。当然,程序中并不一定要把mem和cdev包含在一个设备结构体中,但这样定义的好处在于,它借用了面向对象程序设计中“封装”的思想,体现了一种良好的编程习惯。
加载与卸载设备驱动globalmem设备驱动的模块加载和卸载函数遵循上述类似的模板。
- static void globalmem_setup_cdev(struct globalmem_dev* dev, int index)
- {
- int err;
- int devno = MKDEV(globalmem_major, index);
- cdev_init(&dev->cdev, &globalmem_fops);
- dev->cdev.owner = THIS_MODULE;
- err = cdev_add(&dev->cdev, devno, 1);
- if (err)
- printk(KERN_NOTICE "Error %d adding globalmem%d", err, index);
- }
- static int __init globalmem_init(void)
- {
- int ret;
- dev_t devno = MKDEV(globalmem_major, 0);
- if (globalmem_major)
- ret = register_chrdev_region(devno, 1, "globalmem");
- else
- {
- ret = alloc_chrdev_region(&devno, 0, 1, "globalmem");
- globalmem_major = MAJOR(devno);
- }
- if (ret < 0)
- return ret;
- globalmem_devp = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
- if (!globalmem_devp)
- {
- ret = -ENOMEM;
- goto fail_malloc;
- }
- globalmem_setup_cdev(globalmem_devp, 0);
- return 0;
- fail_malloc:
- unregister_chrdev_region(devno, 1);
- return ret;
- }
- module_init(globalmem_init);
- static void __exit globalmem_exit(void)
- {
- cdev_del(&globalmem_devp->cdev);
- kfree(globalmem_devp);
- unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);
- }
- module_exit(globalmem_exit);
第1~10行的globalmem_setup_cdev()函数完成cdev的初始化和添加,17~22行完成了设备号的申请,第27行调用kzalloc()申请了一份globalmem_dev结构体的内存并清0。在cdev_init()函数中,与globalmem的cdev关联的file_operations结构体如下所示。
- static const struct file_operations globalmem_fops = {
- .owner = THIS_MODULE,
- .llseek = globalmem_llseek,
- .read = globalmem_read,
- .write = globalmem_write,
- .unlocked_ioctl = globalmem_ioctl,
- .open = globalmem_open,
- .release = globalmem_release
- };
globalmem设备驱动的读写函数主要是让设备结构体的mem数组与用户空间交互数据,并随着访问的字节数变更更新文件读写偏移位置。
- static ssize_t globalmem_read(struct file* filp, char __user* buf, size_t size, loff_t* ppos)
- {
- unsigned long p = *ppos;
- unsigned int count = size;
- int ret = 0;
- struct globalmem_dev* dev = filp->private_data;
- if (p >= GLOBALMEM_SIZE)
- return 0;
- if (count > GLOBALMEM_SIZE - p)
- count = GLOBALMEM_SIZE - p;
- if (copy_to_user(buf, dev->mem +p, count) )
- ret = -EFAULT;
- else
- {
- *ppos += count;
- ret = count;
- printk(KERN_INFO "read %u byte(s) from %lu\n", count, p);
- }
- return ret;
- }
*ppos是要读的位置相对于文件开头的偏移,如果该偏移大于或等于GLOBALMEM_SIZE,意味着已经到达文件末尾,所以返回0(EOF)。
- static ssize_t globalmem_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;
- struct globalmem_dev* dev = filp->private_data;
- if (p >= GLOBALMEM_SIZE)
- return 0;
- if (count > GLOBALMEM_SIZE - p)
- count = GLOBALMEM_SIZE -p;
- if (copy_from_user(dev->mem + p, buf, count) )
- ret = -EFAULT;
- else
- {
- *ppos += count;
- ret = count;
- printk(KERN_INFO "write %u byte(s) from %lu\n", count, p);
- }
- return ret;
- }
seek()函数对文件定位的起始地址可以是文件开头(SEEK_SET,0)、当前位置(SEEK_CUR,1)和文件尾(SEEK_END,2),假设globalmem支持从文件开头和当前位置的相对偏移。
在定位的时候,应该检查用户请求的合法性,若不合法,函数返回-EINVAL,合法时更新文件的当前位置并返回该位置。
- static loff_t globalmem_llseek(struct file* filp, loff_t offset, int orig)
- {
- loff_t ret = 0;
- switch (orig)
- {
- case 0: /* 从文件开头位置seek */
- if (offset < 0 || offset > GLOBALMEM_SIZE)
- {
- ret = -EINVAL;
- break;
- }
- filp->f_pos = offset;
- ret = filp->f_pos;
- break;
- case 1:
- if ( (filp->f_pos + offset) < 0 || (filp->f_pos + offset) > GLOBALMEM_SIZE)
- {
- ret = -EINVAL;
- break;
- }
- filp->f_pos += offset;
- ret = filp->f_pos;
- break;
- default:
- ret = -EINVAL;
- break;
- }
- return ret;
- }
1.globalmem设备驱动的ioctl()函数
globalmem设备驱动的ioctl()函数接受MEM_CLEAR命令,这个命令会将全局内存的有效数据长度清0,对于设备不支持的命令,ioctl()函数应该返回-EINVAL。
- static long globalmem_ioctl(struct file* filp, unsigned int cmd, unsigned long arg)
- {
- struct globalmem_dev* dev = filp->private_data;
- switch (cmd)
- {
- case MEM_CLEAR:
- memset(dev->mem, 0, GLOBALMEM_SIZE);
- printk(KERN_INFO "globalmem is set to zero\n");
- break;
- default:
- return -EINVAL;
- }
- return 0;
- }
在上述程序中,MEM_CLEAR被宏定义为0x01,实际上这并不是一种值得推荐的方法,简单地对命令定义为0x0、0x1、0x2等类似值会导致不同地设备驱动拥有相同的命令号。如果设备A、B都支持0x0、0x1、0x2这样的命令,就会造成命令码的污染。因此,Linux内核推荐采用一套统一的ioctl()命令生成方式。
设备类型 | 序列号 | 方向 | 数据尺寸 |
---|---|---|---|
8位 | 8位 | 2位 | 13/14位 |
之前的代码中都使用了filp->private_data获取globalmem_dev的实例指针。实际上,大多数Linux驱动遵循一个“潜规则”,那就是将文件的私有数据private_data指向设备结构体,再用read()、write()、ioctl()、llseek()等函数通过private_data访问设备结构体。私有数据的概念在Linux驱动的各个子系统中广泛存在,实际上体现了Linux的面向对象的设计思想。对于globalmem驱动而言,私有数据的设置是在globalmem_open()中完成的。
- static int globalmem_open(struct inode* inode, struct file* filp)
- {
- filp->private_data = globalmem_devp;
- return 0;
- }
为了方便复现实验,以下给出完整代码。
- #include <linux/module.h>
- #include <linux/fs.h>
- #include <linux/init.h>
- #include <linux/cdev.h>
- #include <linux/slab.h>
- #include <linux/uaccess.h>
- #define GLOBALMEM_SIZE 0x1000
- #define MEM_CLEAR 0x01
- #define GLOBALMEM_MAJOR 230
- static int globalmem_major = GLOBALMEM_MAJOR;
- module_param(globalmem_major, int, S_IRUGO);
- struct globalmem_dev
- {
- struct cdev cdev;
- unsigned char mem[GLOBALMEM_SIZE];
- };
- struct globalmem_dev* globalmem_devp;
- static ssize_t globalmem_read(struct file* filp, char __user* buf, size_t size, loff_t* ppos);
- static ssize_t globalmem_write(struct file* filp, const char __user* buf, size_t size, loff_t* ppos);
- static loff_t globalmem_llseek(struct file* filp, loff_t offset, int orig);
- static long globalmem_ioctl(struct file* filp, unsigned int cmd, unsigned long arg);
- static int globalmem_open(struct inode* inode, struct file* filp);
- static int globalmem_release(struct inode* inode, struct file* filp);
- static const struct file_operations globalmem_fops = {
- .owner = THIS_MODULE,
- .llseek = globalmem_llseek,
- .read = globalmem_read,
- .write = globalmem_write,
- .unlocked_ioctl = globalmem_ioctl,
- .open = globalmem_open,
- .release = globalmem_release
- };
- static ssize_t globalmem_read(struct file* filp, char __user* buf, size_t size, loff_t* ppos)
- {
- unsigned long p = *ppos;
- unsigned int count = size;
- int ret = 0;
- struct globalmem_dev* dev = filp->private_data;
- if (p >= GLOBALMEM_SIZE)
- return 0;
- if (count > GLOBALMEM_SIZE - p)
- count = GLOBALMEM_SIZE - p;
- if (copy_to_user(buf, dev->mem +p, count) )
- ret = -EFAULT;
- else
- {
- *ppos += count;
- ret = count;
- printk(KERN_INFO "read %u byte(s) from %lu\n", count, p);
- }
- return ret;
- }
- static ssize_t globalmem_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;
- struct globalmem_dev* dev = filp->private_data;
- if (p >= GLOBALMEM_SIZE)
- return 0;
- if (count > GLOBALMEM_SIZE - p)
- count = GLOBALMEM_SIZE -p;
- if (copy_from_user(dev->mem + p, buf, count) )
- ret = -EFAULT;
- else
- {
- *ppos += count;
- ret = count;
- printk(KERN_INFO "write %u byte(s) from %lu\n", count, p);
- }
- return ret;
- }
- static loff_t globalmem_llseek(struct file* filp, loff_t offset, int orig)
- {
- loff_t ret = 0;
- switch (orig)
- {
- case 0: /* 从文件开头位置seek */
- if (offset < 0 || offset > GLOBALMEM_SIZE)
- {
- ret = -EINVAL;
- break;
- }
- filp->f_pos = offset;
- ret = filp->f_pos;
- break;
- case 1:
- if ( (filp->f_pos + offset) < 0 || (filp->f_pos + offset) > GLOBALMEM_SIZE)
- {
- ret = -EINVAL;
- break;
- }
- filp->f_pos += offset;
- ret = filp->f_pos;
- break;
- default:
- ret = -EINVAL;
- break;
- }
- return ret;
- }
- static long globalmem_ioctl(struct file* filp, unsigned int cmd, unsigned long arg)
- {
- struct globalmem_dev* dev = filp->private_data;
- switch (cmd)
- {
- case MEM_CLEAR:
- memset(dev->mem, 0, GLOBALMEM_SIZE);
- printk(KERN_INFO "globalmem is set to zero\n");
- break;
- default:
- return -EINVAL;
- }
- return 0;
- }
- static int globalmem_open(struct inode* inode, struct file* filp)
- {
- filp->private_data = globalmem_devp;
- return 0;
- }
- static int globalmem_release(struct inode* inode, struct file* filp)
- {
- return 0;
- }
- static void globalmem_setup_cdev(struct globalmem_dev* dev, int index)
- {
- int err;
- int devno = MKDEV(globalmem_major, index);
- cdev_init(&dev->cdev, &globalmem_fops);
- dev->cdev.owner = THIS_MODULE;
- err = cdev_add(&dev->cdev, devno, 1);
- if (err)
- printk(KERN_NOTICE "Error %d adding globalmem%d", err, index);
- }
- static int __init globalmem_init(void)
- {
- int ret;
- dev_t devno = MKDEV(globalmem_major, 0);
- if (globalmem_major)
- ret = register_chrdev_region(devno, 1, "globalmem");
- else
- {
- ret = alloc_chrdev_region(&devno, 0, 1, "globalmem");
- globalmem_major = MAJOR(devno);
- }
- if (ret < 0)
- return ret;
- globalmem_devp = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
- if (!globalmem_devp)
- {
- ret = -ENOMEM;
- goto fail_malloc;
- }
- globalmem_setup_cdev(globalmem_devp, 0);
- return 0;
- fail_malloc:
- unregister_chrdev_region(devno, 1);
- return ret;
- }
- module_init(globalmem_init);
- static void __exit globalmem_exit(void)
- {
- cdev_del(&globalmem_devp->cdev);
- kfree(globalmem_devp);
- unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);
- }
- module_exit(globalmem_exit);
- MODULE_AUTHOR("HanHan");
- MODULE_LICENSE("GPL v2");
globalmem驱动在用户空间中的验证
在源目录下执行make指令编译驱动,makefile内容如下(源文件名为main.c):
- KVERS = $(shell uname -r)
- # Kernel modules
- obj-m += main.o
- build: kernel_modules
- kernel_modules:
- make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules
- $ make
命令加载模块,通过insmod命令。再通过cat /proc/devices命令查看,可以发现主设备号为230的globalmem字符设备驱动。
- $ sudo insmod main.ko
- $ cat /proc/devices
- 230 globalmem
接下来创建/dev/globalmem设备节点:
- $ sudo mknod -m 0666 /dev/globalmem c 230 0
接下来写了一段验证程序,通过printk和程序输出验证驱动。主要关注的点,一是加载的时机;二是用户程序调用和驱动程序之间的对应关系;三是指针的移动。
- #include <stdio.h>
- #include <string.h>
- #include <fcntl.h>
- #include <unistd.h>
- #include <sys/ioctl.h>
- char buf[1024];
- int main()
- {
- int res;
- int fd = open("/dev/globalmem", O_RDWR);
- if (fd == -1)
- {
- printf("open failed.\n");
- return -1;
- }
- res = snprintf(buf, sizeof(buf), "Hello World");
- write(fd, buf, res);
- read(fd, buf, res);
- printf("read buf: %s\n", buf);
- memset(buf, 0x00, sizeof(buf) );
- lseek(fd, 0, 0);
- read(fd, buf, res);
- printf("read buf: %s\n", buf);
- memset(buf, 0x00, sizeof(buf) );
- lseek(fd, 0, 0);
- ioctl(fd, 0x01);
- read(fd, buf, res);
- printf("read buf: %s\n", buf);
- res = close(fd);
- if (res == -1)
- {
- printf("close failed.\n");
- return -1;
- }
- return 0;
- }
使用dmesg命令查看期间的打印信息:
- $ dmesg
- [26666.982294] globalmem : init
- [26699.877585] globalmem_write: *ppos = 0
- [26699.877632] write 11 byte(s) from 0
- [26699.877636] globalmem_write: *ppos = 11
- [26699.877637] globalmem_read: read 11 byte(s) from 11
- [26699.877725] globalmem_write: *ppos = 0
- [26699.877726] globalmem_read: read 11 byte(s) from 0
- [26699.877730] globalmem is set to zero
- [26699.877731] globalmem_write: *ppos = 0
- [26699.877731] globalmem_read: read 11 byte(s) from 0
支持N个globalmem设备的globalmem驱动
如果globalmem不止包括一个设备,而是同时包括两个或两个以上的设备,采用private_data的优势就会集中显现出来。只需简单地修改globalmem_init()、globalmem_exit()和globalmem_open(),就可以轻松地让globalmem驱动中包含N个相同的设备(次设备号分为0~N),如下代码将修改的部分标出。
- #include <linux/module.h>
- #include <linux/fs.h>
- #include <linux/init.h>
- #include <linux/cdev.h>
- #include <linux/slab.h>
- #include <linux/uaccess.h>
- #define GLOBALMEM_SIZE 0x1000
- #define MEM_CLEAR 0x01
- #define GLOBALMEM_MAJOR 230
- #define DEVICE_NUM 10
- static int globalmem_major = GLOBALMEM_MAJOR;
- module_param(globalmem_major, int, S_IRUGO);
- struct globalmem_dev
- {
- struct cdev cdev;
- unsigned char mem[GLOBALMEM_SIZE];
- };
- struct globalmem_dev* globalmem_devp;
- static ssize_t globalmem_read(struct file* filp, char __user* buf, size_t size, loff_t* ppos);
- static ssize_t globalmem_write(struct file* filp, const char __user* buf, size_t size, loff_t* ppos);
- static loff_t globalmem_llseek(struct file* filp, loff_t offset, int orig);
- static long globalmem_ioctl(struct file* filp, unsigned int cmd, unsigned long arg);
- static int globalmem_open(struct inode* inode, struct file* filp);
- static int globalmem_release(struct inode* inode, struct file* filp);
- static const struct file_operations globalmem_fops = {
- .owner = THIS_MODULE,
- .llseek = globalmem_llseek,
- .read = globalmem_read,
- .write = globalmem_write,
- .unlocked_ioctl = globalmem_ioctl,
- .open = globalmem_open,
- .release = globalmem_release
- };
- static ssize_t globalmem_read(struct file* filp, char __user* buf, size_t size, loff_t* ppos)
- {
- unsigned long p = *ppos;
- unsigned int count = size;
- int ret = 0;
- struct globalmem_dev* dev = filp->private_data;
- if (p >= GLOBALMEM_SIZE)
- return 0;
- if (count > GLOBALMEM_SIZE - p)
- count = GLOBALMEM_SIZE - p;
- if (copy_to_user(buf, dev->mem +p, count) )
- ret = -EFAULT;
- else
- {
- *ppos += count;
- ret = count;
- printk(KERN_INFO "read %u byte(s) from %lu\n", count, p);
- }
- return ret;
- }
- static ssize_t globalmem_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;
- struct globalmem_dev* dev = filp->private_data;
- if (p >= GLOBALMEM_SIZE)
- return 0;
- if (count > GLOBALMEM_SIZE - p)
- count = GLOBALMEM_SIZE -p;
- if (copy_from_user(dev->mem + p, buf, count) )
- ret = -EFAULT;
- else
- {
- *ppos += count;
- ret = count;
- printk(KERN_INFO "write %u byte(s) from %lu\n", count, p);
- }
- return ret;
- }
- static loff_t globalmem_llseek(struct file* filp, loff_t offset, int orig)
- {
- loff_t ret = 0;
- switch (orig)
- {
- case 0: /* 从文件开头位置seek */
- if (offset < 0 || offset > GLOBALMEM_SIZE)
- {
- ret = -EINVAL;
- break;
- }
- filp->f_pos = offset;
- ret = filp->f_pos;
- break;
- case 1:
- if ( (filp->f_pos + offset) < 0 || (filp->f_pos + offset) > GLOBALMEM_SIZE)
- {
- ret = -EINVAL;
- break;
- }
- filp->f_pos += offset;
- ret = filp->f_pos;
- break;
- default:
- ret = -EINVAL;
- break;
- }
- return ret;
- }
- static long globalmem_ioctl(struct file* filp, unsigned int cmd, unsigned long arg)
- {
- struct globalmem_dev* dev = filp->private_data;
- switch (cmd)
- {
- case MEM_CLEAR:
- memset(dev->mem, 0, GLOBALMEM_SIZE);
- printk(KERN_INFO "globalmem is set to zero\n");
- break;
- default:
- return -EINVAL;
- }
- return 0;
- }
- static int globalmem_open(struct inode* inode, struct file* filp)
- {
- struct globalmem_dev* dev = container_of(indoe->i_cdev, struct globalmem_dev, cdev);
- filp->private_data = dev;
- return 0;
- }
- static int globalmem_release(struct inode* inode, struct file* filp)
- {
- return 0;
- }
- static void globalmem_setup_cdev(struct globalmem_dev* dev, int index)
- {
- int err;
- int devno = MKDEV(globalmem_major, index);
- cdev_init(&dev->cdev, &globalmem_fops);
- dev->cdev.owner = THIS_MODULE;
- err = cdev_add(&dev->cdev, devno, 1);
- if (err)
- printk(KERN_NOTICE "Error %d adding globalmem%d", err, index);
- }
- static int __init globalmem_init(void)
- {
- int ret, i;
- dev_t devno = MKDEV(globalmem_major, 0);
- if (globalmem_major)
- ret = register_chrdev_region(devno, DEVICE_NUM, "globalmem");
- else
- {
- ret = alloc_chrdev_region(&devno, 0, DEVICE_NUM, "globalmem");
- globalmem_major = MAJOR(devno);
- }
- if (ret < 0)
- return ret;
- globalmem_devp = kzalloc(sizeof(struct globalmem_dev) * DEVICE_NUM, GFP_KERNEL);
- if (!globalmem_devp)
- {
- ret = -ENOMEM;
- goto fail_malloc;
- }
- for (i = 0; i < DEVICE_NUM; i++)
- {
- globalmem_setup_cdev(globalmem_devp + i, i);
- }
- return 0;
- fail_malloc:
- unregister_chrdev_region(devno, 1);
- return ret;
- }
- module_init(globalmem_init);
- static void __exit globalmem_exit(void)
- {
- int i;
- for (i = 0; i < DEVICE_NUM; i++)
- {
- cdev_del(&globalmem_devp->cdev);
- }
- kfree(globalmem_devp);
- unregister_chrdev_region(MKDEV(globalmem_major, 0), DEVICE_NUM);
- }
- module_exit(globalmem_exit);
- MODULE_AUTHOR("HanHan");
- MODULE_LICENSE("GPL v2");
以上globalmem_init()和globalmem_exit()的修改都比较直观,globalmem_open()新增的代码让我琢磨了一会。以下是我理解的一些猜测,待验证:单个设备驱动里private_data可以直接指向globalmem_devp,而多设备不行,因为还要区分次设备号。indoe->i_cdev成员应该是cdev_add时绑定了globalmem_devp全局数组中的某个地址,所以可以通过indoe->i_cdev找到这个相关的地址。而通过container_of函数找到这个地址非常严谨,因为不一定默认cdev成员就在新结构体的开头(这一点在看Android HAL代码时就觉得有点奇怪,现在对比一下的确是有点不严谨)。
可以通过创建多个设备节点进行验证:
- $ sudo mknod -m 0666 /dev/globalmem c 230 0
- $ sudo mknod -m 0666 /dev/globalmem1 c 230 1
并可以使用cat命令进行快速验证:
- $ sudo echo "hello world" > /dev/globalmem1
- $ cat /dev/globalmem1
- hello world
总结和思考
文中的那张图片对字符设备驱动总结的很好,cdev对应了一个字符设备,主要包括了设备号(dev_t)和相应的操作(file_operations)。module_init宏指定驱动加载函数,主要需要指定以下工作:register_chrdev_region、cdev_init和cdev_add。module_exit宏指定驱动卸载函数,主要需要指定以下工作:cdev_del和unregister_chrdev_region。module_init对应的printk消息能看到,但是module_exit还不清楚发生在什么条件,需要继续学习。
验证驱动的实验中感受到驱动设备和文件很好的匹配关系——“一切都是文件”,基本文件的操作函数都能很好的工作,比如fgets等。
并且文末还提及了支持多个相同设备的驱动,感觉非常全面的介绍了字符设备驱动的总体概貌。希望继续学习之后能看得懂以及分析真正生产环境中的驱动代码。