[内核学习]字符设备数据传输

/ 0评 / 0赞

上一篇把设备注册出来了,这篇讲数据怎么进出:read/write 的返回值语义,copy_to_user 一族的安全职责,ioctl 命令编码,还有 mmap 零拷贝.实验模块一个设备四条通道,每条通道的边界行为都实测到,中间还有一个 f_pos 的翻车,比正文还精彩.

这篇回答几件事:

先说 read/write 的语义,这是新手问题最密集的地方.规则一共三条:返回值是实际完成的字节数,可以小于请求(短读不是错误);零表示 EOF;负数是 errno.实测一个存了 "hello xfer" 的设备,小口读:

[read 4] 4: hell
[read 4] 4: o xf
[read 4] 2: er
[read 4] 0 (EOF)

4,4,2,0 — 第三次只剩 2 字节,给 2 不给错,第四次没了,给 0.驱动侧的记账靠 file 里的 f_pos(回调参数里的 loff_t *off),读多少挪多少.写侧同理,返回值是吃进去的字节数.

然后是我翻的车,关于 f_pos 本身.测完一遍整体读,再 write 一段新数据,再 read — 满屏的 0,新数据像人间蒸发.原因:f_pos 是每个打开文件(fd)的会话状态,读到 EOF 时它停在末尾,write 只往缓冲里塞数据,根本不碰它,下一次 read 一看 *off >= len,直接 EOF.设备又不是文件,没有"重新读"的天然语义,想从头再来要么 close 重开拿新 fd,要么设备实现 lseek.这个行为让"字符设备的 offset 只是驱动自己的记账,不是文件系统那个 offset"变得非常具体.顺带一提 6.18 的一个 API 变化:no_llseek 被移除了,非 seekable 设备现在用 noop_llseek,我编译时撞上才知道.

copy 家族是内核和用户空间的安全边界.内核永远不能直接解引用用户指针 — 1.3 那篇的软件 PAN 实测已经证明,内核直接碰用户地址会当场页表级 fault.copy_to_user/copy_from_user 做三件事:陷入时安全地跨边界拷贝,逐页检查权限,失败时告诉你拷了多少.实测给 read 喂一个坏地址:

[bad read] ret=-1 errno=14 (Bad address)

驱动里 copy_to_user 返回非零(未拷完的字节数),驱动把它翻译成 -EFAULT,glibc 再翻译成 errno 14.这条链上每一环都不能省:有人偷懒用 memcpy,在他自己的 x86 老内核上恰好能跑,换到带 PAN 的 arm64 立刻死给你看.

ioctl 是带外通道,read/write 装不下的"配置与控制"走这里.命令号必须用宏编码,1.3 那篇的 FIGETBSZ 事故已经演示过裸数字的下场,这次把编码本身拆开看.内核侧把收到的 cmd 拆位打印:

ioctl cmd=0x40046b02 dir=WRITE type=0x6b nr=2 size=4   (_IOW('k', 2, int))
ioctl cmd=0x80046b01 dir=READ  type=0x6b nr=1 size=4   (_IOR('k', 1, int))
ioctl cmd=0x00006b09 dir=NONE  type=0x6b nr=9 size=0   (_IO('k', 9))

32 位命令号四段:方向(高 2 位,0x4000 写,0x8000 读),魔数 type(次高 8 位,'k' 是 0x6b),序号 nr(低 8 位),数据大小(中间 13 位... 实际按位域拼,4 字节 int 就是 0x4).用户态和内核必须 include 同一组宏定义,两边算出的 cmd 天然一致.实测 set/get 往返 777 成功,发一个驱动不认识的 _IO('k',9),返回 ENOTTY(errno 25,Inappropriate ioctl for device)— 注意是 ENOTTY 不是 EINVAL,这是历史约定,别"纠正"它.数据传输尽量用 put_user/get_user,单个标量比 copy_from_user 整结构体干净.

最后 mmap,零拷贝通道.驱动实现一个 .mmap 回调,用 remap_pfn_range 把内核持有的物理页直接映射进用户的地址空间:

	return remap_pfn_range(vma, vma->vm_start,
		virt_to_pfn((void *)shared_page), PAGE_SIZE, prot);

实测用户 mmap 完第一眼就看到内核预置的 0xcafebabe — 同一个物理页,两个地址空间,没有拷贝.用户写 0x1234 进去,内核侧立即可见.大缓冲,视频帧,DMA 共享内存,走的都是这条路:read/write 每次一个 memcpy,mmap 一次映射终身免费.代价是同步责任全给你:用户改了内核什么时候"看见",缓存属性对不对(writecombine 那个页保护参数),多进程同时映射的竞争,这些 read/write 模式下框架兜着,mmap 模式下自己兜.

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

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注