目录
一、基础知识
1.1 字符设备的定义
1.2 基本函数说明
1.2.1 MAJOR宏
1.2.2 MINOR宏
1.2.3 MKDEV宏
1.2.4 register_chrdev_region()
1.2.5 alloc_chrdev_region()
1.2.6 unregister_chrdev_region()
二、字符设备设计流程
2.1 字符设备属性构建
2.2 字符设备方法构建
2.3 字符设备申请设备号
2.4 字符设备分配存储空间
2.5 字符设备操作
2.6 字符设备清除
三、编译运行
3.1 完整代码
3.2 编译运行
3.2.1 接口创建
3.2.2 字符设备读操作
3.2.3 字符设备写操作
3.2.4 拓展--手动创建主设备号
四、小结
五、往期内容
脚本下载链接
linux下通常有三种设备:字符设备、块设备、网络设备等等。字符设备和块设备均以一个文件节点形式显示在文件系统的/dev目录下。本章节要介绍的字符(char)设备是指无需缓冲即可直接进行读写的设备, 如鼠标,键盘,串口设备等能够像字节流一样被访问的设备,由字符设备驱动程序来实现这种特性。字符设备驱动程序通常要有open, close, read, write等系统调用。
提取主设备号,主设备号用来对应驱动程序,相同主设备号的设备使用相同的驱动程序。
提取次设备号,次设备号用于区分同一类型的多个设备,与主设备号结合使用就唯一标识了
一个设备。
将主设备号和次设备号转换成dev_t类型
已知主设备号的情况下用于申请设备号。
若主设备号为0,则内核自动为设备分配设备号。
释放已经分配的设备号。
示例代码:
#define HELLO_MAJOR 0
#define HELLO_NR_DEVS 2
/* 定义主设备号和次设备号*/
int hello_major = HELLO_MAJOR;
int hello_minor = 0;
int hello_nr_devs = HELLO_NR_DEVS;
/* 将主设备号和次设备号结合,高12位为主设备号,低20位为次设备号 */
dev_t hello_devt;
...
/* 若主设备号不为0,则手动分配*/
if (hello_major){
/* 将主设备号和次设备号进行合并成dev_t类型的设备号 */
hello_dev = MKDEV(hello_major, hello_minor);
ret = register_chrdev_region(hello_dev, hello_nr_devs, "hello_chr");
}
/* 若主设备号为0,自动分配设备号 */
else{
ret = alloc_chrdev_region(&hello_dev, hello_minor, hello_nr_devs, "hello_chr");
/* 若申请成功,则利用MAJOR提取设备号 */
hello_major = MAJOR(hello_dev);
}
...
/* 释放设备号 */
unregister_chrdev_region(hello_dev, hello_nr_dev);
#include
struct hello_char_dev{
struct cdev cdev;
char c; /* 自定义属性 */
};
/* 定义指向hello_char_dev的指针 */
struct hello_char_dev *hc_devp;
int hc_open(struct inode *inode, struct file *filp){
printk(KERN_INFO "open hc_dev%d %d\n", iminor(inode), MINOR(inode->i_cdev->dev));
return 0;
}
ssize_t hc_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos){
printk(KERN_INFO "read hc_dev\n");
return 0;
}
ssize_t hc_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos){
printk(KERN_INFO "write hc_dev\n");
return count;
}
int hc_release(struct inode *inode, struct file *filp){
printk(KERN_INFO "release hc_dev\n");
return 0;
}
/* 将函数填充至结构体中 */
struct file_operations hc_fops = {
.owner = THIS_MODULE,
.read = hc_read,
.write = hc_write,
.open = hc_open,
.release = hc_release,
};
static int __init hello_init(void){
int ret = 0, i = 0;
/* 若主设备号不为0,则手动分配*/
if (hello_major){
devt = MKDEV(hello_major, hello_minor);
ret = register_chrdev_region(devt, hello_nr_devs, "hello_chr"); /* 使用指定的设备号分配 */
}
/* 自动分配设备号*/
else{
ret = alloc_chrdev_region(&devt, hello_minor, hello_nr_devs, "hello_chr");
hello_major = MAJOR(devt);
}
if (ret < 0){
printk(KERN_WARNING "hello: can't get major %d\n", hello_major);
goto fail;
}
else{
;
}
hc_devp = kzalloc(sizeof(struct hello_char_dev) * hello_nr_devs, GFP_KERNEL); /* 给字符设备分配空间 */
for (i = 0; i < hello_nr_devs; i++){
/* 初始化字符设备结构 */
cdev_init(&hc_devp[i].cdev, &hc_fops);
hc_devp[i].cdev.owner = THIS_MODULE;
/* 添加至内核 */
ret = cdev_add(&hc_devp[i].cdev, MKDEV(hello_major, hello_minor + i), 1);
}
static void __exit hello_exit(void){
int i = 0;
/* 将字符设备移除 */
for (i = 0; i < hello_nr_devs; i++){
cdev_del(&hc_devp[i].cdev);
}
/* 释放内存*/
kfree(hc_devp);
/* 移除模块时释放设备号*/
unregister_chrdev_region(devt, hello_nr_devs);
}
#include
#include
#include
#include
/* 定义主设备号和设备个数*/
#define HELLO_MAJOR 0
#define HELLO_NR_DEVS 2
int hello_major = HELLO_MAJOR;
int hello_minor = 0;
int hello_nr_devs = HELLO_NR_DEVS;
/* 将主设备号和次设备号结合,高12位为主设备号,低20位为次设备号 */
dev_t devt;
/* 利用模块参数的方式,将设备号和设备个数作为模块参数,方便在加载模块时动态调整参数值 */
module_param(hello_major, int, S_IRUGO);
module_param(hello_minor, int, S_IRUGO);
module_param(hello_nr_devs, int, S_IRUGO);
/* 实际的字符设备结构, 类似于面向对象中的继承 */
struct hello_char_dev{
struct cdev cdev;
/* 自定义属性 */
char c;
};
struct hello_char_dev *hc_devp;
int hc_open(struct inode *inode, struct file *filp){
printk(KERN_INFO "open hc_dev%d %d\n", iminor(inode), MINOR(inode->i_cdev->dev));
return 0;
}
ssize_t hc_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos){
printk(KERN_INFO "read hc_dev\n");
return 0;
}
ssize_t hc_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos){
printk(KERN_INFO "write hc_dev\n");
return count;
}
int hc_release(struct inode *inode, struct file *filp){
printk(KERN_INFO "release hc_dev\n");
return 0;
}
/* 字符设备的操作函数 */
struct file_operations hc_fops = {
.owner = THIS_MODULE,
.read = hc_read,
.write = hc_write,
.open = hc_open,
.release = hc_release,
};
static int __init hello_init(void){
int ret = 0, i = 0;
printk(KERN_INFO "---BEGIN HELLO LINUX MODULE---\n");
/* 若主设备号不为0,则手动分配*/
if (hello_major){
devt = MKDEV(hello_major, hello_minor);
ret = register_chrdev_region(devt, hello_nr_devs, "hello_chr"); /* 使用指定的设备号分配 */
}
else{
ret = alloc_chrdev_region(&devt, hello_minor, hello_nr_devs, "hello_chr"); /* 动态分配主设备号 */
hello_major = MAJOR(devt);
}
if (ret < 0){
printk(KERN_WARNING "hello: can't get major %d\n", hello_major);
goto fail;
}
else{
;
}
hc_devp = kzalloc(sizeof(struct hello_char_dev) * hello_nr_devs, GFP_KERNEL); /* 给字符设备分配空间 */
if (!hc_devp){
printk(KERN_WARNING "alloc mem failed");
ret = -ENOMEM;
goto failure_kzalloc;
}
else{
;
}
for (i = 0; i < hello_nr_devs; i++){
cdev_init(&hc_devp[i].cdev, &hc_fops); /* 初始化字符设备结构 */
hc_devp[i].cdev.owner = THIS_MODULE;
ret = cdev_add(&hc_devp[i].cdev, MKDEV(hello_major, hello_minor + i), 1); /* 添加至内核 */
if (ret){
printk(KERN_WARNING "fail add hc_dev%d", i);
}
}
printk(KERN_INFO "---END HELLO LINUX MODULE---\n");
return 0;
failure_kzalloc:
unregister_chrdev_region(devt, hello_nr_devs);
fail:
return ret;
}
static void __exit hello_exit(void){
int i = 0;
for (i = 0; i < hello_nr_devs; i++){
cdev_del(&hc_devp[i].cdev);
}
kfree(hc_devp);
unregister_chrdev_region(devt, hello_nr_devs); /* 移除模块时释放设备号*/
printk(KERN_INFO "GOODBYE LINUX\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("XZX");
MODULE_VERSION("V1.0");
将字符设备加载至内核后(具体可参考链接),通过"cat /proc/devices"命令查看设备号。
我们在/dev/目录下发现系统并没有自动生成相应的操作接口,本章节我们先利用"mknod"命令手
动创建。
上述两个文件就对应了我们在驱动中申请的字符设备。
接下来,我们对第一个字符设备(dev0)进行读操作
利用"cat /dev/hc_dev0"读取第一个字符设备,通过"sudo dmesg"查看。
我们对第二个设备(dev1)进行写操作,通过"dmesg"查看内容如下所示。
由于我们之前将主设备号作为了模块参数,所以可以在加载模块时 实时对主设备号(hello_major)进行赋值,我们将主设备号赋值为11,如下图所示。
通过"cat /proc/devices" 查看主设备号,发现设置成功,如图所示:
本章主要是针对最基础的字符设备的创建进行讲解,其中仍然有许多需要改进的地方,比如
自动生成设备节点、进一步开发read(),write()以及实现ioctl函数等,这些在后续章节都会进行补
充。
[Linux驱动开发一] 最简单的内核模块