Linux系统设备分类概述
Linux系统将所有外接设备和内部功能模块都抽象为文件,按照设备的工作特性和数据交互方式,主要分为字符设备、块设备和网络设备三大类,不同类别的设备对应不同的驱动开发逻辑和使用场景。

字符设备
字符设备是最常见的一类设备,它的核心特点是数据以字符流的形式顺序传输,不支持随机访问,读写操作通常按照字节逐个处理,数据传输过程中没有缓存或者只有很小的缓存。
字符设备的典型示例包括键盘、鼠标、串口、LED灯、蜂鸣器等,这类设备的操作通常是实时的,比如按下键盘按键后系统会立即接收到对应的字符信号。
在Linux中,字符设备会在/dev目录下生成对应的设备文件,设备号由主设备号和次设备号组成,主设备号用来标识驱动程序,次设备号用来标识具体的设备实例。下面是一个简单的字符设备驱动注册示例:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#define CHAR_DEV_NAME "my_char_dev"
#define CHAR_DEV_MAJOR 240
static struct cdev my_cdev;
static dev_t dev_num;
// 打开设备函数
static int char_dev_open(struct inode *inode, struct file *filp) {
printk("char device openn");
return 0;
}
// 关闭设备函数
static int char_dev_release(struct inode *inode, struct file *filp) {
printk("char device releasen");
return 0;
}
// 定义文件操作结构体
static struct file_operations char_fops = {
.owner = THIS_MODULE,
.open = char_dev_open,
.release = char_dev_release,
};
// 模块初始化函数
static int __init char_dev_init(void) {
// 分配设备号
dev_num = MKDEV(CHAR_DEV_MAJOR, 0);
int ret = register_chrdev_region(dev_num, 1, CHAR_DEV_NAME);
if (ret < 0) {
printk("register chrdev region failedn");
return ret;
}
// 初始化cdev结构体
cdev_init(&my_cdev, &char_fops);
// 添加cdev到内核
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
unregister_chrdev_region(dev_num, 1);
printk("cdev add failedn");
return ret;
}
printk("char device init successn");
return 0;
}
// 模块退出函数
static void __exit char_dev_exit(void) {
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
printk("char device exitn");
}
module_init(char_dev_init);
module_exit(char_dev_exit);
MODULE_LICENSE("GPL");
块设备
块设备与字符设备的最大区别在于,块设备以固定大小的块为单位进行数据传输,支持随机访问,内核会为块设备提供专门的缓存机制,提升数据读写效率。
常见的块设备包括硬盘、U盘、SD卡、固态硬盘等存储类设备,这类设备的最小读写单位通常是512字节或者4KB,用户可以随意跳转到设备的任意位置读取或写入数据。
块设备在/dev目录下的设备文件通常以sd、hd等开头,比如/dev/sda表示第一块SCSI硬盘,/dev/sdb1表示第二块SCSI硬盘的第一个分区。块设备的驱动开发比字符设备更复杂,需要对接内核的通用块层,处理请求队列、bio结构体等核心概念。
网络设备
网络设备是一类特殊的设备,它负责完成网络数据的发送和接收,没有对应的/dev目录下的设备文件,而是通过内核的网络协议栈进行交互,使用套接字接口和用户空间程序通信。
典型的网络设备包括网卡、无线网卡、回环设备lo等,这类设备的核心操作是数据包的收发,不遵循常规的文件读写逻辑,而是通过ifconfig、ip等命令进行配置和管理。
网络设备的驱动需要实现内核定义的net_device_ops结构体,里面包含了设备打开、关闭、发送数据包等核心回调函数,下面是一个简化的网络设备驱动结构示例:
#include <linux/module.h>
#include <linux/netdevice.h>
#define NET_DEV_NAME "my_net_dev"
// 网络设备发送数据包函数
static netdev_tx_t net_dev_xmit(struct sk_buff *skb, struct net_device *dev) {
// 模拟发送数据包逻辑
printk("net device send packetn");
dev_kfree_skb(skb);
return NETDEV_TX_OK;
}
// 定义网络设备操作结构体
static const struct net_device_ops my_netdev_ops = {
.ndo_start_xmit = net_dev_xmit,
};
// 模块初始化函数
static int __init net_dev_init(void) {
struct net_device *net_dev;
// 分配网络设备结构体
net_dev = alloc_netdev(0, NET_DEV_NAME, NET_NAME_UNKNOWN, ether_setup);
if (!net_dev) {
printk("alloc netdev failedn");
return -ENOMEM;
}
// 设置网络设备操作函数
net_dev->netdev_ops = &my_netdev_ops;
// 注册网络设备
int ret = register_netdev(net_dev);
if (ret < 0) {
free_netdev(net_dev);
printk("register netdev failedn");
return ret;
}
printk("net device init successn");
return 0;
}
// 模块退出函数
static void __exit net_dev_exit(void) {
struct net_device *net_dev = dev_get_by_name(&init_net, NET_DEV_NAME);
if (net_dev) {
unregister_netdev(net_dev);
free_netdev(net_dev);
}
printk("net device exitn");
}
module_init(net_dev_init);
module_exit(net_dev_exit);
MODULE_LICENSE("GPL");
三类设备的核心差异对比
为了更直观地理解三类设备的区别,我们可以通过下面的表格进行对比:
| 设备类型 | 数据传输单位 | 是否支持随机访问 | 是否有缓存 | 典型示例 |
|---|---|---|---|---|
| 字符设备 | 字节/字符流 | 否 | 几乎没有 | 键盘、鼠标、串口 |
| 块设备 | 固定大小的块 | 是 | 有内核缓存 | 硬盘、U盘、SD卡 |
| 网络设备 | 网络数据包 | 不适用 | 有协议栈缓存 | 网卡、回环设备 |
特殊设备的补充说明
除了上述三类核心设备之外,Linux中还有一些特殊设备,比如/dev/null、/dev/zero、/dev/random等,这些设备属于字符设备的范畴,但是功能比较特殊。/dev/null会丢弃所有写入它的数据,读取它会立即返回文件结束;/dev/zero可以无限提供空字符数据;/dev/random和/dev/urandom可以生成随机数。
在实际的Linux系统使用中,我们可以通过ls -l /dev命令查看设备文件的类型,第一个字符是c表示字符设备,是b表示块设备,网络设备不会在这个目录下显示。如果需要查看系统中的网络设备,可以使用ip addr命令。