TaterLi 个人博客

[内核学习]字符设备框架

字符设备是驱动入门的第一课,也是大多数简单硬件的正确形态:按字节流访问,open/read/write/close.前面几篇已经零散用过 miscdevice 和 cdev,这篇把框架本身说透:设备号体系,注册流程,以及每个驱动都要跳的那支 container_of 舞.实验是一个驱动管三个设备,同主设备号三个次设备号,各自的计数各自的身份证.

这篇回答几件事:

设备号是字符设备的门牌.内核用 dev_t 表示,高 12 位主设备号,低 20 位次设备号.major 标驱动,minor 标设备,ls -l /dev 第一列那个 242, 0 就是它俩.主设备号两条路:静态注册(老驱动写死在 Documentation/devicetree... 不对,是 devices.txt 里登记的编号,新号要找 Linux 社区要)和动态分配 alloc_chrdev_region,内核给你个空闲的,省心代价是编号每次启动可能变.实验模块走的动态路线:

chdev3 in, major=242 minors 0-2
crw------- 242, 0 /dev/kdev0
crw------- 242, 1 /dev/kdev1
crw------- 242, 2 /dev/kdev2

一次申请三个连号,同 major 不同 minor.用户态永远靠 /dev 节点找设备,节点由 devtmpfs 按 major:minor 建(3.2 讲过的链路),编号变不变用户无感 — 这就是动态主设备号能普及的根本原因.

注册骨架按顺序四步:alloc_chrdev_region 拿号,cdev_init 填 file_operations,cdev_add 把 cdev 挂进内核全局表,device_create 建设备出节点.内核收到 open("/dev/kdev1") 时,VFS 按节点 inode 里的设备号查到 cdev,把 cdev 塞进 inode->i_cdev,然后调用 fops->open.重点来了 — 一个驱动管三个设备,fops 是同一份,open 怎么知道自己开的是 kdev0 还是 kdev1?

答案在 cdev 的挂法上.每个设备一个私有结构体,cdev 内嵌其中:

struct kblog_dev {
	struct cdev cdev;	/* 内嵌,不是指针 */
	int id;
	atomic_t opens;
};

注册时每个设备各自 cdev_add(open 时内核按 minor 找到的是各自的 cdev).open 回调里一支舞跳回去:

d = container_of(inode->i_cdev, struct kblog_dev, cdev);
f->private_data = d;

container_of 是内核的第一惯用法:已知结构体某成员的地址,反推结构体本身的开头.之后 read/write/release 全从 file->private_data 取,一个 fops 服务 N 个设备互不打架.实测三个设备各自报家门:

kdev0 opens=1
kdev1 opens=1
kdev2 opens=1

计数的灵敏度也顺手验证:shell 里 exec 9</dev/kdev0 再 exec 8</dev/kdev0 长持两个 fd,再 cat 一次,输出 kdev0 opens=3 — 九号,八号,cat 自己,每次 open 都实打实进驱动.关掉两个 fd 再读,回落到 opens=1.用户态一行"cat",内核里 open/release 的对称一清二楚.

再说说 misc 这条捷径.前面几篇的小模块用的都是 misc_register:一次调用,主设备号固定 10(著名的 misc 主号),minor 动态,device_create 全免.代价是它就是为"一个驱动一个设备"设计的,没有 container_of 的余地(设备结构体是 miscdevice 本身,文件操作直接绑死),而且所有 misc 设备共享主号 10,设备多了 minor 拥挤.选型经验:临时测试模块,真只有一路设备的简单硬件,sensor 类小器件,misc 足够;多路设备,要精细控制编号,要 suspend/resume 回调配合电源管理,走完整 cdev 路线.另外别忘了 fops 里那句 .owner = THIS_MODULE — 3.1 验证过它和引用计数的关系,设备开着驱动就拔不走,这是字符设备和模块机制的安全带.

最后留几个容易忽略的细节:

退出移动版