TaterLi 个人博客

[内核学习]Linux设备模型

/dev 下一个设备节点背后是一整套对象体系:kobject,kset,bus,device,driver,它们把内核里成千上万个设备组织成一棵树,顺便解决了热插拔通知,引用计数,设备驱动配对这些脏活.这篇写一个完整设备从代码里"生"出来,看着它在 sysfs,devtmpfs,uevent 里留下全套痕迹,再解剖板上一个真实串口.

这篇回答几件事:

先造设备.核心代码十几行,一个字符设备从无到有:

alloc_chrdev_region(&devt, 0, 1, "kblog");	/* 要一个主设备号 */
cdev_init(&kblog_cdev, &kblog_fops);
cdev_add(&kblog_cdev, devt, 1);		/* 注册字符设备操作集 */
kblog_cls = class_create("kblog");		/* 建类 /sys/class/kblog */
device_create(kblog_cls, NULL, devt, NULL, "kblog0");	/* 造设备 */

四步各有分工.alloc_chrdev_region 申请设备号,这里没写死 major,内核动态分配,实测给了 242.cdev_add 把 file_operations 挂进内核的字符设备表,从此 open("/dev/kblog0") 能找到你的函数.class_create 建的是"类别",纯组织用,/sys/class/kblog 这个目录.真正的暴击是最后一步 device_create,它一口气干了三件事:在 sysfs 里建设备目录,让 devtmpfs 建出 /dev/kblog0 节点,向全系统广播 uevent.实测 insmod 之后:

crw------- 1 root root 242, 0 /dev/kblog0

节点已经在了.这里有个常见误解要掰正:很多人以为是 udev 建的 /dev 节点 — 现在不是了.devtmpfs 在内核里,device_register 时节点就出来,udev 存在的意义是改权限,建符号链接,跑规则,而不是从零造节点.所以哪怕板子上没有任何用户态管理器,/dev/kblog0 照样出现,这在 initramfs 阶段调试驱动时特别有用.

uevent 那一路用 udevadm monitor --kernel 实时抓,一次 insmod 三条:

KERNEL[593.485402] add  /class/kblog (class)
KERNEL[593.485533] add  /devices/virtual/kblog/kblog0 (kblog)
KERNEL[593.485925] add  /module/devmod (module)

类出现,设备出现,模块出现,各报一次到.uevent 是内核到用户态的广播,本质是个 netlink 消息,带着 DEVNAME,MAJOR,MINOR 这些键值对,udev 收到后按规则行事.热插拔世界的"内核说,用户态听",说的就是它.

sysfs 这边,设备的目录里天生带着 dev,uevent,power,subsystem,想加自己的就用 DEVICE_ATTR 宏加一个属性:

static ssize_t level_show(...) { return sprintf(buf, "%dn", opens); }
static ssize_t level_store(...) { ... }
static DEVICE_ATTR_RW(level);
device_create_file(kblog_dev, &dev_attr_level);

cat 一下走 show,echo 一下走 store,实测 echo 7 > level,dmesg 里 store 函数的打印原样出现.注意这个宏变出的是 struct device_attribute,6.18 里 device_create_file 收的就是它,别手滑传裸 attribute 指针,类型直接拒你(我就被拒了一次).属性文件是驱动和用户态最便宜的交互通道,比 ioctl 省 binder...不对,省一个文件描述符和一堆命令号设计,简单状态查询首选它.

引用计数的长相也实地看了.file_operations 里一句 .owner = THIS_MODULE,open 这个设备时内核自动 try_module_get.实测:没人的时候 /sys/module/devmod/refcnt 是 0,一个程序 open 住不放,变 1;此时 rmmod:

rmmod: ERROR: Module devmod is in use

上一篇说引用计数是"用着就不能拔",这里的"用着"就是用户手里那个 open 着的 fd.程序一死,release 里 module_put,计数归零,rmmod 畅通,同时 /sys/class/kblog 和 /dev/kblog0 一起消失 — 卸载路径的 device_destroy 同样会广播 remove uevent,正反两个方向都是对称的.这一套保护看着烦,但它防的是最恶性的那类事故:驱动代码还在被调用,内存已经还给系统.

人造设备看完了,解剖一个真的.板上控制台的串口,设备树里:

serial@ff1a0000 {
	compatible = "rockchip,rk3399-uart", "snps,dw-apb-uart";
	...
};

sysfs 里它活了: /sys/devices/platform/ff1a0000.serial,这就是设备模型一侧的实体 — platform 总线把 DT 节点展开成 platform device(启动篇 initcall 榜单里那个 of_platform_default_populate_init 干的活).它和驱动的配对关系在三个地方都能看,组成一个三角:

/sys/devices/platform/ff1a0000.serial/driver -> ../../../bus/platform/drivers/dw-apb-uart
/sys/bus/platform/devices/ff1a0000.serial   -> ../../../devices/platform/ff1a0000.serial
/sys/bus/platform/drivers/dw-apb-uart/      里列着 ff180000.serial ff1a0000.serial ...

设备目录下有 driver 软链,总线目录下 devices 和 drivers 两边各挂链接互相可达.顺带注意驱动目录里有 bind 和 unbind 两个写文件 — 手动绑定旋钮,排试驱动或者玩设备重新绑定时会用到,echo 设备名进 unbind 能当场解绑一个活设备.

配对的规则看 compatible.DT 节点给了两个字符串,这是一份按优先级排列的候选清单:先试 "rockchip,rk3399-uart",没有驱动认;再试 "snps,dw-apb-uart",8250_dw 驱动的 of_match_table 里正好有这一条(drivers/tty/serial/8250/8250_dw.c),命中.这种"厂商串在前,通用串兜底"的写法是 DT 的惯用姿势:专用驱动优先接管,没有也能退回通用实现.RK3399 的串口就这么被通用 dw-apb-uart 驱动收编的,一个驱动服务一整族 SoC.

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

退出移动版