Linux驱动实践:一步一步编写字符设备驱动程序
作 者:道哥,10+年嵌入式开发老兵,专注于:C/C++、嵌入式、Linux。
关注下方公众号,回复【书籍】,获取 Linux、嵌入式领域经典书籍;回复【PDF】,获取所有原创文章( PDF 格式)。
目录
API 函数
编写驱动程序
编写应用程序
卸载驱动模块
小结
别人的经验,我们的阶梯!
大家好,我是道哥,今天我们继续讨论: Linux 中字符设备的驱动程序。
在上一篇文章中Linux驱动实践:你知道【字符设备驱动程序】的两种写法吗?我们说过:字符设备的驱动程序,有两套不同的API
函数,并且在文中详细演示了利用旧的API
函数来编写驱动程序。
这篇文章,我们继续这个话题,实际演示一下:字符设备驱动程序的另一套API
函数的使用方法。
API 函数
这里主要关注下面这 3 个函数:
// 静态注册设备
int register_chrdev_region(dev_t from, unsigned count, const char *name);
// 动态注册设备
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
// 卸载设备
void unregister_chrdev_region(dev_t from, unsigned count);
关于静态和动态注册,主要的区别就在于:主设备号由谁来主导分配!
静态注册:由我们的驱动程序来指定主设备号,即参数1:from;
动态注册:由操作系统来分配,驱动程序提供一个变量来接收该设备号,即参数1: dev 指针;
另外,在Linux 2.6
后期的内核版本中,引入了 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;
};
与这个结构体相关的处理函数有:
void cdev_init(struct cdev *,struct file_operations *);
初始化 cdev 的成员,主要是设置 file_operations。
strcut cdev *cdev_alloc(void);
动态申请 cdev 内存。
void cdev_put(strcut cdev *p);
与 count 计数相关的操作。
int cdev_add(struct cdev *,dev_t ,unsigned );
向系统中添加一个 cdev,注册字符设备,需要在驱动被加载的时候调用。
void cdev_del(struct cdev *);
从系统中删除一个 cdev,注销字符设备,需要在驱动被卸载的时候调用。
后面在代码演示的时候,可以看到cdev
结构是如何被使用的。
编写驱动
按照惯例,我们仍然按照步骤,来讨论如何利用上述的APIs
,来手写一个字符设备的驱动程序。
以下所有操作的工作目录,都是与上一篇文章相同的,即:
~/tmp/linux-4.15/drivers/
。
创建驱动目录和驱动程序
$ cd linux-4.15/drivers/
$ mkdir my_driver2
$ cd my_driver2
$ touch driver2.c
driver2.c
文件的内容如下(不需要手敲,文末有代码下载链接):
#include
#include
#include
#include
#include
static struct cdev my_cdev;
static dev_t dev_no;
int driver2_open(struct inode *inode, struct file *file)
{
printk("driver2_open is called. \n");
return 0;
}
ssize_t driver2_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
printk("driver2_read is called. \n");
return 0;
}
ssize_t driver2_write (struct file *file, const char __user *buf, size_t size, loff_t *ppos)
{
printk("driver2_write is called. \n");
return 0;
}
static const struct file_operations driver2_ops={
.owner = THIS_MODULE,
.open = driver2_open,
.read = driver2_read,
.write = driver2_write,
};
static int __init driver2_init(void)
{
printk("driver2_init is called. \n");
// 初始化cdev结构
cdev_init(&my_cdev, &driver2_ops);
// 注册字符设备
alloc_chrdev_region(&dev_no, 0, 2, "driver2");
cdev_add(&my_cdev, dev_no, 2);
return 0;
}
static void __exit driver2_exit(void)
{
printk("driver2_exit is called. \n");
// 注销设备
cdev_del(&my_cdev);
// 注销设备号
unregister_chrdev_region(dev_no, 2);
}
MODULE_LICENSE("GPL");
module_init(driver2_init);
module_exit(driver2_exit);
这里看一下加载驱动模块时调用的 driver2_init( )
函数,其中的 cdev_init
用来把cdev
结构体与 file_operations
发生关联。
在调用 alloc_chrdev_region( )
时,操作系统分配了主设备号,并且保存在 dev_no
变量中,然后 cdev_add()
再把设备号与cdev
结构体进行关联。
创建 Makefile 文件
$ touch Makefile
内容如下:
ifneq ($(KERNELRELEASE),)
obj-m := driver2.o
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNEL_PATH) M=$(PWD) clean
endif
编译驱动模块
$ make
得到驱动程序: driver2.ko 。
加载驱动模块
在加载驱动模块之前,先来检查一下系统中,几个与驱动设备相关的地方。
先看一下 /dev 目录下,目前还没有我们的设备节点( /dev/driver2 )。
$ ll /dev/driver2
ls: cannot access '/dev/driver2': No such file or directory
再来查看一下 /proc/devices 目录下,也没有 driver2
设备的设备号。
$ cat /proc/devices
/proc/devices 文件: 列出字符和块设备的主设备号,以及分配到这些设备号的设备名称。
为了方便查看打印信息,把dmesg
输出信息清理一下:
$ sudo dmesg -c
执行如下指令,加载驱动模块:
$ sudo insmod driver2.ko
当驱动程序被加载的时候,通过 module_init( )
注册的函数 driver2_init()
将会被执行,那么其中的打印信息就会输出。
还是通过 dmesg
指令来查看驱动模块的打印信息:
$ dmesg
此时,驱动模块已经被加载了!
来查看一下 /proc/devices 目录下显示的设备号:
$ cat /proc/devices
设备已经注册了,主设备号是: 244 。
但是,此时在/dev
目录下,还没有我们需要的设备节点。
在上一篇文章中介绍过,还可以利用 Linux 用户态的 udev 服务来自动创建设备节点。
现在,我们手动创建设备节点:
$ sudo mknod -m 660 /dev/driver2 c 244 0
主设备号 244 是从 /proc/devices 查到的。
检查一下是否创建成功:
$ ll /dev/driver2
现在,设备的驱动程序已经加载了,设备节点也被创建好了,应用程序就可以来操作(读、写)这个设备了。
应用程序
应用程序仍然放在 ~/tmp/App/ 目录下。
$ mkdir ~/tmp/App/app_driver2
$ cd ~/tmp/App/app_driver2
$ touch app_driver2.c
文件内容如下:
#include
#include
#include
int main(void)
{
int ret;
int read_data[4] = { 0 };
int write_data[4] = {1, 2, 3, 4};
int fd = open("/dev/driver2", O_RDWR);
if (-1 != fd)
{
ret = read(fd, read_data, 4);
printf("read ret = %d \n", ret);
ret = write(fd, write_data, 4);
printf("write ret = %d \n", ret);
}
else
{
printf("open /dev/driver2 failed! \n");
}
return 0;
}
接下来就是编译和测试了:
$ gcc app_driver2.c -o app_driver2
$
$ sudo ./app_driver2
[sudo] password for xxx: <输入用户密码>
read ret = 0
write ret = 0
从返回值来看,成功打开了设备,并且调用读函数、写函数都成功了!
继续用dmesg
命令查看一下:
卸载驱动模块
卸载指令:
$ sudo rmmod driver2
此时,/proc/devices
下主设备号 244
的 driver2
已经不存在了。
再来看一下 dmesg
的打印信息:
可以看到:驱动程序中的 driver2_exit( ) 被调用执行了!
小结
以上就是利用“新的” API 函数,来编写字符设备的驱动程序。
代码结构还是非常清晰的,这得益于Linux
良好的驱动程序架构设计!这也是每一名架构师需要学习、努力模仿的地方。
文中的测试代码,已经放在网盘了。
在公众号【IOT物联网小镇】后台回复关键字:1120,即可获取下载地址。
谢谢!
推荐阅读
【2】C语言指针-从底层原理到花式技巧,用图文和代码帮你讲解透彻