网站页面数量百度联盟怎么加入
基本概念之什么是驱动程序()?
驱动程序本质上是代码逻辑的集合,通常用于管理、驱动多个设备实例。某个设备要想使用驱动程序,需要实例化相应的驱动程序的结构体,并在系统中注册,获得主设备号、次设备号,并将相应的结构体、变量、函数等进行绑定。
这里重点要理解驱动程序只是代码逻辑的集体,并不等同于设备,驱动程序本身并没有主设备号、次设备号等,主设备号、次设备号是在注册设备实例时才有的概念。
Linux系统中常见的设备类型有哪些?
在 Linux 系统中,设备可以分为不同的类型,主要根据它们的工作方式、接口和功能进行分类。以下是 Linux 系统中常见的设备类型:
1. 字符设备 (Character Devices)
字符设备是按字符流方式进行数据传输的设备,每次读写都涉及一个字符或字节的数据。例如:
- 终端设备 (
tty
): 用于与用户交互的设备,如串口终端、虚拟终端。 - 串口设备 (
ttyS
): 串口通信设备,如/dev/ttyS0
。 - 键盘 (
kbd
): 连接的键盘设备。 - 鼠标 (
mouse
): 连接的鼠标设备。 - 伪设备:例如
/dev/null
、/dev/random
、/dev/zero
等,这些设备没有硬件实现,提供特殊的功能。
特点:
- 按字符流进行读写,每次读写一个字符(或字节)。
- 通过字符设备文件(如
/dev/ttyS0
)与应用程序交互。
2. 块设备 (Block Devices)
块设备是按块(通常是 512 字节或更大的单位)进行数据传输的设备。它们支持随机访问和缓存功能。例如:
- 硬盘 (
/dev/sda
,/dev/sdb
等): 计算机的主要存储设备。 - 固态硬盘 (SSD) (
/dev/sda1
,/dev/nvme0n1
): 新型存储设备,提供更高的读写速度。 - 光盘驱动器 (
/dev/cdrom
): 用于读取光盘数据的设备。 - USB 存储设备 (
/dev/sdb1
,/dev/sdc
): 连接到系统的 USB 存储设备。 - 虚拟磁盘 (
/dev/loop0
): 用于挂载虚拟磁盘映像的设备。
特点:
- 支持随机读写访问,通常用于文件系统的挂载。
- 设备文件通常位于
/dev/
目录下。
3. 网络设备 (Network Devices)
网络设备是用于网络通信的设备。它们支持网络接口的连接和数据传输。例如:
- 以太网卡 (
eth0
,eth1
): 网络接口,用于连接有线网络。 - 无线网卡 (
wlan0
,wlan1
): 用于连接无线网络。 - 虚拟网络设备 (
lo
): 回环接口,用于与本机进行网络通信。
特点:
- 用于实现计算机与外部网络的通信。
- 可以是有线或无线设备,支持 TCP/IP 等协议。
4. 输入设备 (Input Devices)
输入设备是用于将用户输入传递给计算机的设备。它们通常使用字符设备接口。例如:
- 鼠标 (
/dev/input/mice
): 用于提供用户的指针控制输入。 - 键盘 (
/dev/input/event0
): 用于接收用户的键盘输入。 - 触摸屏 (
/dev/input/eventX
): 用于接收触摸操作。
特点:
- 输入设备通常通过
/dev/input/
目录进行管理,使用字符设备接口。 - 输入事件通过设备文件传递给应用程序。
5. 虚拟设备 (Virtual Devices)
虚拟设备并不对应于物理硬件设备,而是通过软件实现的设备。例如:
- 虚拟终端 (
/dev/tty0
,/dev/tty1
): 提供与系统交互的虚拟控制台。 - 内存设备 (
/dev/mem
): 用于访问物理内存的虚拟设备。 - 随机数设备 (
/dev/random
,/dev/urandom
): 用于生成随机数的设备。
特点:
- 不依赖于硬件,而是由内核或驱动程序模拟。
- 提供系统管理或特殊功能。
6. 特殊设备 (Special Devices)
这些设备通常与硬件密切相关,但它们的作用和用法比较特殊。例如:
- 时钟设备 (
/dev/rtc
): 实时钟设备,用于管理系统时间。 - 伪设备 (
/dev/null
,/dev/zero
): 不涉及实际硬件,而是提供特殊功能,通常用于丢弃或生成数据。
7. USB 设备 (USB Devices)
USB 设备通过 USB 总线连接到计算机,涵盖了各种设备类型。例如:
- USB 存储设备 (
/dev/sda1
,/dev/sdb1
): 连接的 USB 存储。 - USB 摄像头 (
/dev/video0
): 通过 USB 接口连接的摄像头。 - USB 键盘和鼠标 (
/dev/input/eventX
): 连接的 USB 键盘和鼠标。
特点:
- 支持即插即用(plug-and-play),可以动态连接和断开。
8. 串口设备 (Serial Devices)
串口设备通过串行接口进行通信,通常用于较为传统的设备。例如:
- 串口终端 (
/dev/ttyS0
,/dev/ttyS1
): 传统的串口设备。 - 调制解调器 (
/dev/ttyUSB0
): 通过串口连接的调制解调器。
特点:
- 通过串行接口传输数据,通常用于低速数据通信。
9. SCSI 设备 (SCSI Devices)
SCSI 是一种广泛用于硬盘、光驱等设备的标准接口。例如:
- SCSI 硬盘 (
/dev/sda
,/dev/sdb
): SCSI 接口的硬盘设备。 - SCSI 光驱 (
/dev/sr0
): SCSI 接口的光驱设备。
特点:
- 高性能和扩展性,常用于服务器或工作站中。
- 支持多个设备共享同一总线。
10. PCI 设备 (PCI Devices)
PCI 总线上的设备用于高性能数据交换。例如:
- 显卡 (
/dev/video0
): 通过 PCI 接口连接的显卡设备。 - 网卡 (
/dev/eth0
): 通过 PCI 接口连接的网卡设备。 - 音频设备 (
/dev/snd/pcmC0D0p
): 通过 PCI 接口连接的音频设备。
特点:
- 高速数据传输,支持多种设备接口。
- 通过 PCI 总线连接多个设备。
11. I2C 和 SPI 设备 (I2C/SPI Devices)
I2C 和 SPI 是常用于嵌入式设备的通信协议,用于连接传感器、显示器等设备。例如:
- I2C 设备:通过 I2C 总线连接的设备,通常位于
/dev/i2c-X
。 - SPI 设备:通过 SPI 总线连接的设备,通常位于
/dev/spidevX.Y
。
特点:
- 用于短距离、高速的设备间通信。
- 主要用于嵌入式系统中的外围设备。
小结
在 Linux 系统中,设备类型非常丰富,主要包括字符设备、块设备、网络设备、输入设备、虚拟设备、USB 设备、串口设备等。每种设备都有不同的特点和用途,Linux 系统通过设备文件(通常位于 /dev/
目录下)来对这些设备进行管理和访问。
什么叫主设备号、次设备号?
主设备号、次设备号是Linux系统管理设备的一种结构,通常如果有多个设备,其驱动程序如果一样,那我们为其分配相同的主设备号,内核通过主设备号找到负责处理该设备的驱动程序。但它们的次设备号不同,通过不同的次设备号来区别它们。
当然如果设备类型相同,驱动程序也相同,你也可以为它们分配不同的主设备号,不过不建议这样做,这不符号Linux的基本设计原则。
不过也有特例,比如不同类型的设备有时会使用相同的驱动程序,但此时我们也会为它们分配不同的主设备号,以便在系统中区分。例如:块设备和字符设备通常使用不同的主设备号,即使它们底层可能由同一个驱动程序管理。
以字符设备为例,有了驱动程序后,为设备提供驱动程序并注册进系统的通常流程是怎么样的?
这里以字符设备为例。
这里以字符设备为例。
这里以字符设备为例。
注意:不同的设备类型有不同的对应下面各步骤的函数,但流程基本上是一样的。
注意:不同的设备类型有不同的对应下面各步骤的函数,但流程基本上是一样的。
注意:不同的设备类型有不同的对应下面各步骤的函数,但流程基本上是一样的。
第一步是调用函数alloc_chrdev_region()
获得可用的主设备号、次设备号。
示例代码如下:
dev_t dev;
ret = alloc_chrdev_region(&dev, 0, 3, "my_device");
函数alloc_chrdev_region()
的原型如下:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name);
参数解释
-
dev
指向一个dev_t
类型的变量,用于保存分配的设备号(包含主设备号和次设备号)。成功分配后,可以通过MAJOR(dev)
和MINOR(dev)
宏提取主设备号和次设备的起始号。 -
firstminor
表示从哪个次设备号开始分配,一般设置为0
。 -
count
表示需要分配的连续次设备号的数量。如果你需要多个次设备号,可以指定这个值(通常为1
表示只分配一个设备号)。 -
name
为设备指定一个名字,这个名字主要用于调试时显示(比如/proc/devices
中会看到)。
alloc_chrdev_region
运行后,系统为我们将要注册的设备分配了:
-
一个主设备号
主设备号是系统在字符设备表中分配的唯一标识,用于将设备与驱动程序关联。 -
一个连续的次设备号范围
根据调用时指定的count
参数,系统分配了一段连续的次设备号范围。
这意味着,分配完成后:
- 我们的设备对应的设备号范围是:
(主设备号, 起始次设备号) 到 (主设备号, 起始次设备号 + count - 1)。 - 每个次设备号可以用来表示不同的设备实例,但它们共享相同的主设备号。
这里要特别注意:变量dev
中保存的是系统为我们分享的主设备号和次设备的超始号,而不是所有的次设备号哈。
第二步是调用函数cdev_init()将描述字符设备的核心结构体cdev的实例和核心结构体file_operations的实例绑定起来
示例代码如下:
static struct cdev my_cdev; // 定义字符设备结构体
static struct file_operations my_fops = {.owner = THIS_MODULE,.open = my_open,.read = my_read,.write = my_write,.release = my_close,
};/* 核心结构体cdev和结构体file_operations的绑定 */
cdev_init(&my_cdev, &my_fops);}
这里面涉及到两个重要的结构体,第一个是struct cdev
、另一个是 struct file_operations
,下面进行介绍:
struct cdev
是 Linux 内核中的一个核心结构,用于表示字符设备的相关信息。
定义(简化版)
struct cdev {struct kobject kobj; // 内核对象,用于 sysfs 集成const struct file_operations *ops; // 设备支持的操作集struct list_head list; // 用于链接到内核的 cdev 列表dev_t dev; // 设备号unsigned int count; // 管理的次设备号范围
};
从其简化版的定义中可以看出,结构体struct cdev
的一个实例就把设备需要的最重要的信息描述完了:
cdev结构体的成员kobj这里暂时不展开叙述。
cdev结构体的成员ops实际上就是上面说到的两个重要结构体中的另一个 struct file_operations
,它里面实际上就是设备能进行的具体行为的函数的集合,比如下面是一个 struct file_operations
简化版的定义:
PS:完整定义请参考我的另一篇博文 https://blog.csdn.net/wenhao_ir/article/details/144905840
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 *);int (*open)(struct inode *, struct file *);int (*release)(struct inode *, struct file *);long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);...
};
从中我们可以看出它里面就集合了read、write、open、release这些真正涉及对设备底层操作的成员函数,这些函数其实才是某个设备驱动程序的核心。
cdev结构体的成员list_head list是用于链接到内核的 cdev 列表,这里不展开叙述。
cdev结构体的成员dev中存储了它的一个实例管理的设备号,包括主设备号和次设备号的起始号。
cdev结构体的成员count存储了次设备号的个数,通过dev中记录的次设备号的起始号和个数值,便知道了次设备号的范围。
从上面的描述可以看出,cdev结构体的一个实例确实是把设备需要的最重要的信息描述完了。而调用函数就是将其实例与结构体 struct file_operations
的实例绑定起来,结构体 struct file_operations
的实例中存储了设备能进行的具体行为的函数,比如了read、write、open这些函数。
第三步是调用函数cdev_add()将描述字符设备的核心结构体cdev的实例和设备号绑定起来
第一步中已经获取到了可用的主设备号和次设备号,这里就需要将描述字符设备的核心结构体cdev的实例与第一步中获取到的主设备号和次设备号信息填充上。
示例代码如下:
static dev_t dev;static struct cdev my_cdev; // 定义字符设备结构体static struct file_operations my_fops = {.owner = THIS_MODULE,.open = my_open,.read = my_read,.write = my_write,.release = my_close,
};// 向系统申请可用的主设备号和次设备号
ret = alloc_chrdev_region(&dev, 0, 3, "my_device");/* 核心结构体cdev和结构体file_operations的绑定 */
cdev_init(&my_cdev, &my_fops);/* 核心结构体cdev中写入设备号以及次设备号的个数 */
cdev_add(&my_cdev, dev, 3);....
在第二步中已经给出了核心结构体cdev的简化版:
struct cdev {struct kobject kobj; // 内核对象,用于 sysfs 集成const struct file_operations *ops; // 设备支持的操作集struct list_head list; // 用于链接到内核的 cdev 列表dev_t dev; // 设备号unsigned int count; // 管理的次设备号范围
};
可见,这里面的第4个成员dev和第5个成员count便是设备号信息。所以这里需要调用函数cdev_add()
向这个设备核心结构体写入设备号信息。
第四步调用函数class_create()创建设备类
设备类是内核设备模型中的一个抽象,用于将功能相似的设备分组,以便统一管理这些设备的属性和行为。通过设备类,我们可以轻松地在 /sys/class/ 下创建一个目录,并将设备文件与该类关联。
…这里未完待续…
第五步是调用函数device_create()创建设备文件
一个设备实例对就一个具体的设备文件,所以如果同一个驱动程序实例对应多个设备,便需要调用多次函数device_create()。
示例代码如下:
dev_t dev;
int ret = alloc_chrdev_region(&dev, 0, 3, "my_device"); // 分配3个次设备号
if (ret < 0) {printk(KERN_ERR "Failed to allocate device number\n");return ret;
}// 创建 cdev 并注册
cdev_init(&my_cdev, &my_fops);
ret = cdev_add(&my_cdev, dev, 3);
if (ret < 0) {printk(KERN_ERR "Failed to add cdev\n");unregister_chrdev_region(dev, 3);return ret;
}// 创建设备文件(多个次设备号,调用多次 device_create)
for (int i = 0; i < 3; i++) {// 为每个次设备号创建设备文件device_create(my_class, NULL, MKDEV(MAJOR(dev), MINOR(dev) + i), NULL, "my_device%d", i);
}
函数device_create()的介绍如下:
函数原型
struct device *device_create(struct class *cls, struct device *parent, dev_t devt,const struct attribute_group **groups, const char *devname);
参数解析:
-
my_class
(struct class *):- 这是一个指向
class
结构体的指针,表示设备所属的类。类是 Linux 设备模型中的一个概念,用来将一组具有相似功能的设备分组在一起。设备类管理着设备对象的生命周期、sysfs 属性、设备文件的创建等。 - 在例子中,
my_class
是事先通过class_create()
创建的类,可能类似于:my_class = class_create(THIS_MODULE, "my_class");
class_create()
用来创建设备类,它是 Linux 驱动程序中组织设备的基础。
- 这是一个指向
-
NULL
(struct device *):- 这个参数指定父设备。如果设备没有父设备,可以传递
NULL
。大多数设备驱动程序的设备对象不需要父设备,所以这里通常会传递NULL
。 - 父设备用于设备层次结构的管理,例如树形结构的设备关联。如果不需要设备层次结构,可以将其设为
NULL
。
- 这个参数指定父设备。如果设备没有父设备,可以传递
-
dev
(dev_t):- 这是设备的主设备号和次设备号组成的
dev_t
类型值,表示具体的设备。 dev_t
是一个 32 位的值,其中低 20 位是次设备号,高 12 位是主设备号。例如,如果你用alloc_chrdev_region()
分配了一个设备号,dev
就是该设备号。
- 这是设备的主设备号和次设备号组成的
-
**
NULL
(const struct attribute_group groups):- 这个参数是一个指向
attribute_group
结构体指针的指针,代表了设备的sysfs
属性。如果不需要额外的sysfs
属性,可以传递NULL
。 - 在许多情况下,你可能不需要为设备创建额外的
sysfs
属性,尤其是简单的字符设备。因此,可以把它设为NULL
。
- 这个参数是一个指向
-
"my_device"
(const char *):- 这是设备文件的名称,即最终会出现在
/dev/
目录下的设备文件名。在这个例子中,设备文件的名称是"my_device%d"
,因此设备文件名为/dev/my_device0
、/dev/my_device1
、/dev/my_device2
。
- 这是设备文件的名称,即最终会出现在
返回值:
device_create()
返回一个指向创建的device
结构体的指针,该结构体表示内核中创建设备对象的元数据。- 如果函数调用失败,它返回
NULL
。
驱动程序编译好后通常是以模块的形式存在的,那么怎么加载这个模块?这个模块加载时从哪里执行?
请参看博文:
以一个实际例子来学习Linux驱动程序开发之“设备类”的相关知识 【搜索关键字“驱动模块加载代码”】
为什么驱动程序模块的C文件末尾要加上MODULE_LICENSE("GPL");
关于这个问题详细的介绍见我的另一篇博文
https://blog.csdn.net/wenhao_ir/article/details/144902881
驱动程序模块加载完成,完成设备实例化注册并创建了设备文件后,怎么调用设备?
这个最简单的例子例是之前的:
“IMX6ULL开发板基础实验:Framebuffer驱动程序的简单应用实例代码详细分析”
关键代码为:
int fb_fd = open("/dev/fb0", O_RDWR);
这句代码就是把设备打开了,然后利用获得的文件描述符就可以操作设备了。