目标:通过读写一个整型变量来模拟一个字符设备,实现上层的访问。
- 先回顾一下字符设备的创建方式。
四种注册设备的方式。
a). register_chrdev
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
一步到位注册设备到内核中。传递主设备号(传0表示动态分配,大于0的数字表示静态分配),注册设备的名称,及操作函数集。
b). register_chrdev_reg
int register_chrdev_region(dev_t from, unsigned count, const char *name)
这个是上个函数的升级版,from参数为主设备号,count是次设备号的个数,name是设备名称。
c). alloc_chrdev_region
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)
这个函数用于动态申请设备编号范围,传递次设备号的起始编号、个数、名称。申请到的设备号通过dev传递出来。
d).misc_register
int misc_register(struct miscdevice * misc)
在加载模块时会自动创建设备文件,为主设备号为10的字符设备。
以上四种方式中,其中都调用到了__register_chrdev_region接口, register_chrdev是最古老的方式,里面已经有了向内核注册的动作,但是存在浪费的情况,因为没有传递次设备号的个数,默认传入的就是baseminor=0,count=256,这就造成了浪费!因为一个主设备号下的所有此设备号都只能对应同一个字符设备!如果我们使用__register_chrdev_region的话,情况就变了,我们可以指定baseminor和count,这样就可以让一个主设备号下每一个次设备号都对应自己的字符设备了!
第二三种方式,其实只是申请到了设备号,但是没有像内核注册,此时需要搭配一下接口实现向内核注册。
cdev_init
cdev_add
这样就实现了同第一种方式相同的效果,完成想内核的注册。
三种接口的简单介绍
register_chrdev深入分析
但是除了最后一种方式,其他三种方式在插入驱动到内核后,都不会自动创建设备节点,需要使用mknod命令手动创建设备节点。此时可以通过 class_create 搭配 device_create 的方式来实现自动创建设备节点。
内核中定义了struct class结构体,顾名思义,一个struct class结构体类型变量对应一个类,内核同时提供了class_create函数,可以用它来创建一个类,这个类存放于sysfs下面,一旦创建好了这个类,再调用device_create函数来在/dev目录下创建相应的设备节点。这样,加载模块的时候,用户空间中的udev会自动响应device_create函数,去/sysfs下寻找对应的类从而创建设备节点。
自动创建设备节点
- 代码简单回顾
老罗给出的示例中就是利用上面第三种方式搭配cdev_init和cdev_add来注册设备的。其中用到了struct semaphore sem;
struct semaphore {
spinlock_t lock; /* 自旋锁结构体变量 */
unsigned int count; /* 用于计录资料数量 */
struct list_head wait_list; /* 内部链表结构体变量 */
};
可以看出这是一个关于锁的结构体,内核提供的关于这个结构体的操作接口有:
DECLARE_MUTEX
init_MUTEX
init_MUTEX_LOCKED
down
down_interruptible
down_killable
down_trylock
down_timeout
up
示例中提供了通过dev下的设备文件的常规访问方式,但其中没有ioctl接口,自己加入这个接口,做以回顾。
linux驱动–ioctl接口
在其对应的测试应用程序中也加入这个接口的测试代码。
示例中还提供了两外两种访问方式。sys文件系统访问和proc文件系统访问。
sys文件系统访问方式。
/**
* device_create_file - create sysfs attribute file for device.
* @dev: device.
* @attr: device attribute descriptor.
*/
int device_create_file(struct device *dev, const struct device_attribute *attr)
使用这个函数时要引用 device_create所返回的device*指针,作用是在/sys/class/下创建一个属性文件,从而通过对这个属性文件进行读写就能完成对应的数据操作。device_create函数会在/dev/目录下创建字符设备节点。它又要使用class_create创建的类。
这个函数的第二个参数可以直接定义这个结构体,如下:
/* interface for exporting device attributes */
struct device_attribute {
struct attribute attr;
ssize_t (*show)(struct device *dev, struct device_attribute *attr,
char *buf);
ssize_t (*store)(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count);
};
不同通常都是通过DEVICE_ATTR的宏函数来实现的。
#define DEVICE_ATTR(_name, _mode, _show, _store) \
struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)
通过这个宏函数可以看出,这里定义了一个device_attribute的变量,名称固定为dev_attr_##_name,这也就是device_create_file中的第二个参数没法直接找到的原因了。
在使用sys文件系统的时候,有以下常用接口:
static inline void dev_set_drvdata(struct device *dev, void *data);//设置drv变量的driver_data成员
static inline void *dev_get_drvdata(const struct device *dev);//获取drv变量的driver_data成员
上面两个接口一个设置一个获取。在初始化时调用设置接口,传入要操作的设备结构,在show和store接口中调用获取,来实际操作到这个设备结构。
这样,在插入驱动后,在sys/class目录下对应的路径下会有属性文件,对于普通的数据,可以通过echo和cat来写入和读取。
proc文件系统接口。
proc文件系统是一个虚拟文件系统,在内核起来之后才会被挂载工作。主要用来实时完成用户态与内核态之间的信息交互。逐渐被sys文件系统取代。
proc文件系统的操作很简单,常用的三个接口函数:
struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode,
struct proc_dir_entry *parent);
struct proc_dir_entry *proc_mkdir(const char *name,
struct proc_dir_entry *parent);
void remove_proc_entry(const char *name, struct proc_dir_entry *parent);
第一个是建立在proc目录下建立一个节点,name参数指定名称,mode为访问权限,parent为父目录,传NULL时,就是在/proc目录下。
第二个是在proc目录下创建一个目录,参数与第一个相同。
第三个为前两个对应的释放资源的函数接口。
不过在2.6内核以后,create_proc_entry被proc_create接口替代。
static inline struct proc_dir_entry *proc_create(const char *name, umode_t mode, struct proc_dir_entry *parent, const struct file_operations *proc_fops)
两者在使用上略有不同,在指定读写接口的方式上有所不同
struct proc_dir_entry* entry;
entry = create_proc_entry(FREG_DEVICE_PROC_NAME, 0, NULL);
if(entry) {
entry->owner = THIS_MODULE;
entry->read_proc = freg_proc_read;
entry->write_proc = freg_proc_write;
}
另一种是
struct proc_dir_entry* entry;
struct file_operations proc_fops= {
.read=freg_proc_read,
.write= freg_proc_write,
.owner=THIS_MODULE,
};
entry = proc_create(FREG_DEVICE_PROC_NAME, 0, NULL, &proc_fops);
在释放接口中调用remove_proc_entry接口即可。
此时在插入模块的时候,就可以在proc目录下看到创建的文件,可以通过cat和echo读取和写入数据。
proc文件系统讲解