文章目录
设备号
常用接口
设备号申请/释放
register_chrdev_region函数
alloc_chrdev_region函数
unregister_chrdev_region函数
其它宏定义
字符设备的定义
cdev_alloc函数
字符设备的绑定
cdev_init函数
字符设备注册/注销
cdev_add函数
cdev_del函数
file_operations结构
实现一个简单的字符设备驱动
以前的接口
register_chrdev函数
unregister_chrdev函数
测试流程及结果
总结
点击下方阅读原文可访问文中超链接
设备号
设备号(统称,由主设备号和次设备号组成)是一种资源,更是应用层与驱动层之间的纽带,设备号由主设备号和次设备号组成,主设备号用于区分是哪类设备,次设备号用于区分某一类设备中的各个子设备;设备号的分配规则是从大往小分配,其中某些设备号是系统已经约定好了的预分配给指定设备;
当向内核申请了设备号后,就可以在/proc/devices
文件中看到对应的主设备号及设备名,此时使用mknod
也可以在/dev
目录生成设备节点,不过这个节点是个空节点,没有任何用;设备号被注销后,/proc/devices
中也会自动删除,如下:
常用接口
设备号申请/释放
位于头文件:include/linux/fs.h
register_chrdev_region函数
向内核申请指定的设备号,如果指定的设备号已存在则会申请失败。
int register_chrdev_region(dev_t, unsigned, const char *);
示例用法:
//MKDEV是一个宏,用于将主次设备号合并成设备号
//第二个参数表示申请多少个设备号
//第三个参数是设备名
register_chrdev_region(MKDEV(247,0),1,"test_char_dev");
alloc_chrdev_region函数
向内核申请设备号,并且由内核自动分配可用的设备号。
int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);
示例用法:
//devno存储申请的设备号
//第二个参数表示次设备号的起始值
//第三个参数表示申请多少个设备号
//第四个参数是设备名
alloc_chrdev_region(&devno,0,1,"test_char_dev");
unregister_chrdev_region函数
用于注销由register_chrdev_region
和alloc_chrdev_region
申请的设备号,前面也说了设备号是一种资源,所以要养成良好的习惯,不用就要归还。
void unregister_chrdev_region(dev_t, unsigned);
示例用法:
//devno申请的设备号
//第二个参数表示申请的个数,申请了多少就要注销多少
unregister_chrdev_region(devno,1);
其它宏定义
位于头文件:include/linux/kdev_t.h
用于提取设备号的主设备号。
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
用于提取设备号的次设备号。
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
字符设备的定义
cdev_alloc函数
位于头文件:include/linux/cdev.h
使用动态内存的方式定义一个字符设备。
struct cdev *cdev_alloc(void);
或者直接定义为静态全局变量
static struct cdev chr_dev;
字符设备的绑定
cdev_init函数
将一个字符设备与file_operations
结构绑定起来。
void cdev_init(struct cdev *, const struct file_operations *);
字符设备注册/注销
cdev_add函数
向内核注册字符设备。
int cdev_add(struct cdev *, dev_t, unsigned);
cdev_del函数
注销一个字符设备。
void cdev_del(struct cdev *);
file_operations结构
位于头文件:include/linux/fs.h
这个结构体包含了一系列的函数指针,基本上每一个函数指针都对应了应用层一个接口函数,比如调用应用层的open
函数时,最终就会调用到这个结构体里面的open
函数指针。我们就可以通过实现一些需要的函数来操作硬件。
示例代码:
static int test_open (struct inode *inode, struct file *file)
{
return 0;
}
static int test_close (struct inode *inode, struct file *file)
{
return 0;
}
static const struct file_operations fops = {
.owner = THIS_MODULE,
.open = test_open,
.release = test_close,
};
实现一个简单的字符设备驱动
一个简单的字符设备驱动大致分为以下几个步骤:
向内核申请设备号
定义一个字符设备对象
绑定字符设备对象与file_operations结构
向内核注册字符设备
实现file_operations结构体内的部分接口
简单源码如下(注意未实现各种错误及边界检查,实际应用时应完善,以保证程序的健壮性):
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#define DEBUG(fmt, ...) \
do{ \
if(if_debug) \
printk(KERN_INFO "DEBUG > " fmt, ##__VA_ARGS__); \
}while(0)
bool if_debug = false;
/*定义一个bool类型的变量,作为一个模块入参,在装载模块时可赋值*/
module_param(if_debug, bool, S_IRUSR);
#define DEV_COUNT 1
/*设备号*/
static dev_t devno;
/*定义一个字符设备*/
static struct cdev *char_dev;
static int test_open (struct inode *inode, struct file *file)
{
DEBUG("open test..\r\n");
return 0;
}
static int test_close (struct inode *inode, struct file *file)
{
DEBUG("close test..\r\n");
return 0;
}
static const struct file_operations fops = {
.owner = THIS_MODULE,
.open = test_open,
.release = test_close,
};
static int __init test_init(void)
{
/*申请设备号*/
alloc_chrdev_region(&devno,0,DEV_COUNT,"test_char_dev");
/*使用动态内存定义一个字符设备*/
char_dev = cdev_alloc();
/*绑定字符设备和file_operations结构*/
cdev_init(char_dev,&fops);
/*注册字符设备*/
cdev_add(char_dev,devno,DEV_COUNT);
return 0;
}
static void __exit test_exit(void)
{
/*注销一个字符设备*/
cdev_del(char_dev);
/*注销申请的设备号*/
unregister_chrdev_region(devno,DEV_COUNT);
}
/*此宏声明内核模块的初始化入口点*/
module_init(test_init);
/*此宏声明内核模块的退出入口点*/
module_exit(test_exit);
/*声明开源协议*/
MODULE_LICENSE("GPL");
/*声明作者*/
MODULE_AUTHOR("wei");
/*声明模块的描述*/
MODULE_DESCRIPTION("this is a test driver");
以前的接口
同样是创建字符设备,但是内核还有两个接口,可以大大简化上面的代码。
register_chrdev函数
此函数可一步到位,完成了设备号的申请,字符设备与file_operations
结构的绑定以及字符设备的注册全部操作,只不过这个函数会自动申请256个主设备号相同的设备号(次设备号为0到255),如果没有这么多同类子设备的话会造成空间的浪费。
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
示例代码:
//第一个参数为主设备号,如果为0则内核自动分配
//第二个参数为设备名
//第三个参数为file_operations结构体
register_chrdev(247,"test_char_dev",fops);
unregister_chrdev函数
用于注销由register_chrdev
申请的资源。
void unregister_chrdev(unsigned int major, const char *name)
示例用法:
//第一个参数为主设备号
//第二个参数为设备名
unregister_chrdev(247,"test_char_dev");
第二种更简洁的写法:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#define DEBUG(fmt, ...) \
do{ \
if(if_debug) \
printk(KERN_INFO "DEBUG > " fmt, ##__VA_ARGS__); \
}while(0)
bool if_debug = false;
/*定义一个bool类型的变量,作为一个模块入参,在装载模块时可赋值*/
module_param(if_debug, bool, S_IRUSR);
#define CHRDEV_NAME "test_char_dev"
/*主设备号*/
static dev_t major;
static int test_open (struct inode *inode, struct file *file)
{
DEBUG("open test..\r\n");
return 0;
}
static int test_close (struct inode *inode, struct file *file)
{
DEBUG("close test..\r\n");
return 0;
}
static const struct file_operations fops = {
.owner = THIS_MODULE,
.open = test_open,
.release = test_close,
};
static int __init test_init(void)
{
major = register_chrdev(0,CHRDEV_NAME,&fops);
return 0;
}
static void __exit test_exit(void)
{
unregister_chrdev(major,CHRDEV_NAME);
}
/*此宏声明内核模块的初始化入口点*/
module_init(test_init);
/*此宏声明内核模块的退出入口点*/
module_exit(test_exit);
/*声明开源协议*/
MODULE_LICENSE("GPL");
/*声明作者*/
MODULE_AUTHOR("wei");
/*声明模块的描述*/
MODULE_DESCRIPTION("this is a test driver");
驱动写好后,还需要让应用程序能够使用,而应用程序是通过设备节点来访问的,所以接下来创建设备节点,以让应用程序可以访问写好的驱动。使用mknod
命令创建设备节点:
mknod 设备名 设备类型 主设备号 次设备号
示例用法:
mknod /dev/test_char_dev c 247 0
此程序没有实现任何的硬件操作,下面写个简单的应用程序测试一下驱动是否能正常工作,因为只实现了open
和release
接口,所以在应用程序中也只能使用这两个接口。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc,char *argv[])
{
int fd;
fd = open("/dev/test_char_dev",O_RDONLY);
if(fd < 0)
{
perror("open");
return -1;
}
close(fd);
return 0;
}
测试流程及结果
# 装载驱动
insmod test.ko if_debug=1
# 查看申请的设备号
cat /proc/devices
# 创建设备节点(主设备号由上条命令查看,次设备号与驱动程序一致)
mknod /dev/test_char_dev c 247 0
# 执行应用程序
./app_test
# 运行结果会打印出驱动程序中的两个调试信息
DEBUG > open test..
DEBUG > close test..
# 卸载驱动
rmmod test.ko
# 删除设备节点
rm /dev/test_char_dev
总结
我根据自己的理解,画了一个关系图。
测试源码获取:点我