一切皆文件之字符设备
- 文件系统
- 2023-05-28
- 95热度
- 0评论
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "mychardev"
#define BUFFER_SIZE 1024
static char device_buffer[BUFFER_SIZE];
static int open_count = 0;
static int device_open(struct inode *inode, struct file *file)
{
if (open_count > 0)
return -EBUSY;
open_count++;
return 0;
}
static int device_release(struct inode *inode, struct file *file)
{
open_count--;
return 0;
}
static ssize_t device_read(struct file *file, char __user *buffer, size_t length, loff_t *offset)
{
int bytes_to_read;
if (*offset >= BUFFER_SIZE)
return 0;
bytes_to_read = min(length, (size_t)(BUFFER_SIZE - *offset));
if (copy_to_user(buffer, device_buffer + *offset, bytes_to_read))
return -EFAULT;
*offset += bytes_to_read;
return bytes_to_read;
}
static ssize_t device_write(struct file *file, const char __user *buffer, size_t length, loff_t *offset)
{
int bytes_to_write;
if (*offset >= BUFFER_SIZE)
return 0;
bytes_to_write = min(length, (size_t)(BUFFER_SIZE - *offset));
if (copy_from_user(device_buffer + *offset, buffer, bytes_to_write))
return -EFAULT;
*offset += bytes_to_write;
return bytes_to_write;
}
static struct file_operations fops = {
.open = device_open,
.release = device_release,
.read = device_read,
.write = device_write,
};
static int __init chardev_init(void)
{
int ret = register_chrdev(0, DEVICE_NAME, &fops);
if (ret < 0) {
printk(KERN_ALERT "Failed to register char device\\n");
return ret;
}
printk(KERN_INFO "Char device driver registered,major:
return 0;
}
static void __exit chardev_exit(void)
{
unregister_chrdev(0, DEVICE_NAME);
printk(KERN_INFO "Char device driver unregistered\\n");
}
module_init(chardev_init);
module_exit(chardev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver");
下面是一个加载字符设备驱动后的测试示例。
mknod /dev/mychardev c MAJOR_NUMBER 0 #MAJOR_NUMER为注册字符设备时获得的主设备号
echo "Hello, world!" > /dev/mychardev # 写入数据到设备
cat /dev/mychardev # 从设备读取数据
注册字符设备驱动
(1)先调用__register_chrdev_region分配一个strcut char_device_strcut的实例,这个实例表示一个字符设备驱动,在函数中会填充cd数据结构。系统为了管理设备,为每个设备编了号,每个设备又分为主设备号和次设备号,主设备号用来区分不同类似的设备,而次设备号用来区分同一类型的多个设备,如IIC驱动的主设备号是100,IIC有3个设备IIC-0,IIC-1,IIC-2,这3个设备共用一套驱动。可以通过cat /proc/devices 查看已加载的驱动设备主设备号。
static struct char_device_struct {
struct char_device_struct *next; //指向下一个字符设备
unsigned int major; //主设备号
unsigned int baseminor; //次设备起始值
int minorct; //次设备数量
char name[64]; //设备或驱动的名称
struct cdev *cdev; /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
(2)调用cdev_alloc分配设备一个cdev实例,cdev描述了一个字符设备。
struct cdev {
struct kobject kobj; //内核对象,通过他将设备统一加到设备驱动模型中管理
struct module *owner;
const struct file_operations *ops; //文件系统与设备直接的操作函数集合
struct list_head list;//所有的字符设备驱动形成链表
dev_t dev; //字符设备的设备号,由主设备和次设备构成
unsigned int count;
} __randomize_layout;
(3)填充用户注册的驱动函数操作集合,用户注册的open/read/write等函数,这是文件系统与设备的沟通桥梁。
(4)将cdev添加到strutct kobj_map *cdev_map全局列表中,kobj_map中有255个probe,每个probe对应一个主设备,相当于字符设备的数量不能超过255。每个主设备下可以由多个次设备。
创建设备节点
insmod加载完成驱动后,通常需要使用mknod创建一个设备节点,这样用户就可以通过文件系统节点的方式访问设备,下面是mknod的使用示例。
mknod /dev/xxx 设备类型 主设备号 从设备号
从上图的流程图可知,/dev是挂载了tmpfs类型的文件系统。在系统启动初始化的时候会调用shmem_init注册一个tmpfs类型的文件系统。
在使用mknode在/dev下创建设备驱动节点,与前面文件系统创建一个新文件类似,关键点就是为文件创建一个dentry和inode,然后填充inode对应的数据,我们需要重点关注的是inode填充的i_fop的操作函数集合是def_chr_fops,这个file_operations对应的open是chardev_open,因此后续在用户打开文件时将会调用到该函数。
驱动系统调用
上一小节中,使用mknod创建设备节点时,inode->i_fop填充的是def_char_fops,在文件系统打开时就会调用到chrdev_open函数,打开的流程与前面分析的流程一致,这里就不再分析了,我们这里重点关注跟设备驱动相关的差异。chrdev_open函数如下:
static int chrdev_open(struct inode *inode, struct file *filp)
{
const struct file_operations *fops;
struct cdev *p;
struct cdev *new = NULL;
int ret = 0;
spin_lock(&cdev_lock);
p = inode->i_cdev; //获取文件对应的cdev,如果为空则需要进行查找。
if (!p) {
struct kobject *kobj;
int idx;
spin_unlock(&cdev_lock);
kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx); //从cdev_map中查找kobj
if (!kobj)
return -ENXIO;
new = container_of(kobj, struct cdev, kobj);//获取到cdev
spin_lock(&cdev_lock);
/* Check i_cdev again in case somebody beat us to it while
we dropped the lock. */
p = inode->i_cdev;
if (!p) {
inode->i_cdev = p = new; //将cdev赋值给inode
list_add(&inode->i_devices, &p->list);//将inode添加到设备列表中
new = NULL;
} else if (!cdev_get(p))
ret = -ENXIO;
} else if (!cdev_get(p))
ret = -ENXIO;
spin_unlock(&cdev_lock);
cdev_put(new);
if (ret)
return ret;
ret = -ENXIO;
fops = fops_get(p->ops); //获取驱动注册的file_operations
if (!fops)
goto out_cdev_put;
replace_fops(filp, fops); //将struct file中的f_op更新跟驱动的注册的file_operations
if (filp->f_op->open) {
ret = filp->f_op->open(inode, filp); //调用驱动注册的open函数
if (ret)
goto out_cdev_put;
}
return 0;
out_cdev_put:
cdev_put(p);
return ret;
}
chrdev_open函数关键的作用先从cdev_map中找到设备的cdev,然后填充到inode中,这样下次操作设备文件节点时就不用再查找了,接下来非常关键的作用是将strcut file中的f_op由原来inode->i_fop指向的file_operations(def_chr_fops)替换为驱动注册的file_operations,这样后续用户在进程中再操作该文件节点时,对文件的open/read/write等操作经过VFS后就直接调用到驱动注册的file_opearations操作集合了,不会再经过def_chr_fops。