日常生活中使用Linux的设备有很多。对操作系统来说,可以抽象为三大类型,分别是字符设备、块设备、网络设备。简单说,字符设备是以字节为单位进行I/O传输的设备,常见的字符设备有鼠标、键盘、触摸屏等。块设备是以块为单位进行I/O传输的设备,常见的快设备是磁盘。网络设备是一类比较特殊的设备,涉及网络协议层,单独把它们分为一类设备。
想要学习Linux内核,最好从学习字符设备驱动开始。而为了写好字符设备驱动,需要从以下几个方面入手。
第一,学习Linux内核字符设备驱动的架构,包括字符设备驱动是如何组织的,应用程序是如何与驱动交互的。
第二,学习Linux内核字符设备驱动相关的API,包括字符设备相关的基础知识,如字符设备的描述、设备号的管理、file_operations的实现、ioctl交互的设计和Linux设备模型的管理等。
第三,学习Linux内核内存管理相关的API,包括设备的数据如何与用户程序交互,如设备的内存如何映射到用户空间。
第四,学习Linux内核管理中断相关的API,包括中断管理相关的接口函数,如注册中断、编写中断等。
第五,学习Linux内核中同步和锁等相关的API。Linux是多进程、多用户的操作系统,支持内核抢占,需处理同步、异步、竞争等问题。
第六,学习编写驱动所需的芯片原理。设备驱动用于运行设备,需要认真研究设备的数据手册。
一个简单的字符设备
char_device.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/fs.h>
#define DEMO_NAME "char_device"
static dev_t dev;
static struct cdev *char_device;
static signed count = 1;
static int char_device_open(struct inode *inode, struct file *file)
{
int major = MAJOR(inode->i_rdev);
int minor = MINOR(inode->i_rdev);
printk("%s: major=%d, minor=%d\n", __func__, major, minor);
return 0;
}
static ssize_t char_device_read(struct file *file, char __user *buf,size_t lbuf, loff_t *ppos)
{
printk("%s enter\n",__func__);
return 0;
}
static int char_device_release(struct inode *inode, struct file *file)
{
return 0;
}
static ssize_t char_device_write(struct file *file, const char __user *buf, size_t count, loff_t *f_pos)
{
printk("%s enter\n",__func__);
return 0;
}
static const struct file_operations char_device_fops = {
.owner = THIS_MODULE,
.open = char_device_open,
.release = char_device_release,
.read = char_device_read,
.write = char_device_write
};
static int __init simple_char_device_init(void)
{
int ret;
ret = alloc_chrdev_region(&dev, 0, count, DEMO_NAME);
if (ret) {
printk("failed to allocate char device region");
return ret;
}
char_device = cdev_alloc();
if (!char_device) {
printk("cdev_alloc fialed\n");
goto unregister_chrdev;
}
cdev_init(char_device, &char_device_fops);
ret = cdev_add(char_device, dev, count);
if (ret) {
printk("cdev_add failed\n");
goto cdev_fail;
}
printk("succeeded register char device: %s\n", DEMO_NAME);
printk("Major number = %d,minor number = %d\n", MAJOR(dev), MINOR(dev));
return 0;
cdev_fail:
cdev_del(char_device);
unregister_chrdev:
unregister_chrdev_region(dev, count);
return ret;
}
static void __exit simple_char_device_exit(void) {
printk("removing device\n");
if (char_device)
cdev_del(char_device);
unregister_chrdev_region(dev, count);
}
module_init(simple_char_device_init);
module_exit(simple_char_device_exit);
MODULE_AUTHOR("fkq");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("simple character device");
Makefile
KVERS = $(shell uname -r)
obj-m := char_device.o
all:
$(MAKE) -C /lib/modules/$(KVERS)/build M=$(PWD) modules
clean:
$(MAKE) -C /lib/modules/$(KVERS)/build M=$(PWD) clean
rm -rf *.ko;
执行make命令进行编译
{ fukaiqiang@ubuntu /home/fukaiqiang/linux/char_device }
$make
make -C /lib/modules/5.13.0-44-generic/build M=/home/fukaiqiang/linux/char_device modules
make[1]: 进入目录“/usr/src/linux-headers-5.13.0-44-generic”
CC [M] /home/fukaiqiang/linux/char_device/char_device.o
MODPOST /home/fukaiqiang/linux/char_device/Module.symvers
CC [M] /home/fukaiqiang/linux/char_device/char_device.mod.o
LD [M] /home/fukaiqiang/linux/char_device/char_device.ko
BTF [M] /home/fukaiqiang/linux/char_device/char_device.ko
Skipping BTF generation for /home/fukaiqiang/linux/char_device/char_device.ko due to unavailability of vmlinux
make[1]: 离开目录“/usr/src/linux-headers-5.13.0-44-generic”
会在当前目录下生成char_device.ko内核模块文件。然后通过insmod命令加载char_device.ko内核模块。
$sudo insmod char_device.ko
接着使用dmesg命令来查看内核日志
[ 2842.706783] succeeded register char device: char_device
[ 2842.706785] Major number = 234,minor number = 0
以上两句内核日志,正好是在char_device.c中书写的:系统为这个设备分配的主设备号是234,分配的次设备号是0.查看/proc/devices这个proc虚拟文件系统的devices节点信息,可以看到生成了char_device为名称的设备,主设备号是234.
$cat /proc/devices
Character devices:
...
234 char_device
...
生成的设备都需要在/dev/目录下生成对应的节点,这里只能手动生成。
$sudo mknod /dev/char_device c 234 0
通过ls命令查看下节点信息
$ls -al /dev/
...
crw-r--r-- 1 root root 234, 0 6月 26 20:37 char_device
...
编写测试程序
char_device_test.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define DEMO_DEV_NAME "/dev/char_device"
int main() {
char buffer[64];
int fd;
fd = open(DEMO_DEV_NAME, O_RDONLY);
if (fd < 0) {
printf("open device %s failed\n", DEMO_DEV_NAME);
return -1;
}
read(fd, buffer, 64);
close(fd);
return 0;
}
编译测试程序,生成可执行文件
gcc char_device_test.c -o char_device_test
执行可执行文件,查看内核日志进行查看。
[ 8071.270374] char_device_open: major=234, minor=0
[ 8071.270382] char_device_read enter
到此时,测试程序已经成功访问了内核模块char_device。
详解
通过以上示例,你会发现创建一个字符设备驱动,是有步骤可循的。下面就来总结下。
- 分配设备号
linux提供了两个接口函数完成设备号的申请。设备号分别主设备号和次设备号,主设备号只能有一个,次设备号可以有多个。
int register_chrdev_region(dev_t from, unsigned count, const char *name)
register_chrdev_region需要指定主设备号看,可以连续分配多个。内核文档documentation/devices.txt描述了系统中已经分配的主设备号,假如使用此函数指定主设备号,应该避免使用系统已占用的设备号。
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
alloc_chrdev_region自动分配一个主设备号,避免和系统已占用的设备号发生冲突。
假如想释放设备号,可以调用以下函数:
void unregister_chrdev_region(dev_t from, unsigned count)
设备号的抽象为dev_t,定义在include/linux/types.h中:
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
typedef unsigned int __u32;
所以,dev_t其实就是unsigned int类型,是一个32位的数据类型。这32位数据构成了主设备号和次设备号两部分,其中高12位为主设备号,低20位为次设备号。因此Linux系统中主设备号范围为0~4095,所以大家在选择主设备号的时候一定不要超过这个范围。在文件include/linux/kdev_t.h中提供了几个关于设备号的宏:
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma)<<MINORBITS)|(mi))
宏MINORBITS表示次设备号位数,一共是20位。
宏MINORMASK表示次设备号掩码。
宏MAJOR用于从devt中获取主设备号,将devt右移20位即可。
宏MINOR用于从dev_t中获取次设备号,取dev_t的低20位的值即可。
宏MKDEV用于将给定的主设备号和次设备号的值组合成dev_t类型的设备号。
- 抽象字符设备
字符设备需要用一个数据结构来进行抽象和描述,在linux里它就是cdev数据结构。
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
kojb: 用于linux设备驱动模型。
owner:字符设备驱动所在的内核模块对象指针。
ops:字符设备驱动中最关键的一个操作函数,在和应用程序交互过程中起枢纽作用。
list:用来将字符设备串成一个链表。
dev: 字符设备的设备号,由主设备号和次设备号组成。
count:同属于主设备号的次设备号的个数。
数据结构有了之后,还需创建,有两种方式,一种是使用全局静态变量,一种是使用内核提供的cdev_alloc接口函数。
static struct cdev device_char_cdev;
或者
struct char_cdev = cdev_alloc();
创建完成之后,还需要建立设备与驱动操作方法集file_operations之间的连接关系,函数为cdev_init。
void cdev_init(struct cdev *cdev, struct file_operations *fops)
除此之外,还需要把创建的字符设备添加到系统中,函数为
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
p 表示设备的cdev数据结构,dev 表示设备的设备号,count 表示主设备号可以有多少个次设备号。
如果想从系统中删除此设备,可以使用以下函数
void cdev_del(struct cdev *p)
- 创建字符设备操作方法集
static const struct file_operations char_device_fops = {
.owner = THIS_MODULE,
.open = char_device_open,
.release = char_device_release,
.read = char_device_read,
.write = char_device_write
};
这里创建了类型为file_operations的字符设备操作方法集,里面是多个函数指针。这个方法集通过cdev_init方法和设备建立连接关系,当用户空间的程序通过open打开设备的时候,就会执行char_device_open方法。
fd = open(DEMO_DEV_NAME, O_RDONLY);
open方法的第一个参数是设备文件名,第二个参数用来指定文件打开的属性。若open方法执行成功,会返回一个文件描述符,否则返回-1。
以下是常用的一些file_operations接口。
<include/linux/fs.h>
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long,loff_t);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*mremap)(struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t,unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t,unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
open: 打开设备
release:关闭设备
llseek:修改文件的当前读写位置,并返回新位置。
read:从设备驱动中读取数据到用户空间,并返回成功读取的字节数。若为负数,则说明读取失败。
write: 把用户空间的数据写入设备,并返回成功写入的字节数。
poll:方法用来查询设备是否可以立即读写。
unlocked_ioctl和compat_ioctl:提供与设备相关的控制命令的实现。
mmap:将设备内存映射到进程的虚拟地址空间中。
aio_read和aio_write:异步IO读写方法。
fsync:实现了一种称为异步通知的特性。
- 为设备添加节点
设备要想获取上层的访问,必须创建节点,所谓节点即设备文件,它是连接内核空间驱动和用户空间应用程序的桥梁。节点都存放于/dev/目录中。
drwxr-xr-x 2 root root 4040 7月 5 23:17 char/
crw--w---- 1 root tty 5, 1 7月 5 23:17 console
第一列中的d代表块设备,c代表字符设备。字符设备这里显示主设备号和次设备号。主设备号代表一类设备,次设备号代表同一个类的不同个体,每个次设备号都有一个不同的设备节点。
节点的生成有两种方式,一种是使用mknod命令手动生成,一种是使用udev机制动态生成。这里主要介绍第一种。
sudo mknod /dev/char_device c 234 0
参考
- 奔跑吧内核 入门篇第2版 第六章 字符设备驱动详解
- https://blog.csdn.net/weixin_44502943/article/details/121427675
网友评论