当前位置: 首页 > news >正文

从 `dd` 命令到 NuttX 伪设备:`/dev/zero` 与 `/dev/null` 的实现剖析

Overview

本文从一条常见的dd基准测试命令出发,逐步深入到/dev/zero/dev/null这两个 Unix 经典伪设备的语义、用途,最后落到 NuttX RTOS 上的具体实现,并解释为什么驱动既要return total又要uio_advance(uio, total)。适合想理解操作系统 I/O 抽象、NuttX VFS 以及 readv/writev 接口的嵌入式开发者。

Topics Covered

Topic 1:dd if=/dev/zero of=/dev/null bs=2048 count=4096在做什么

背景

dd是 Unix 经典工具,按块复制数据。这条命令常用来粗略测内存带宽或验证dd本身的开销。

参数逐一解析
  • dd:data duplicator,按块复制数据。
  • if=/dev/zeroinputfile,输入源。/dev/zero读出来是无限的\0字节。
  • of=/dev/nulloutputfile,输出目标。/dev/null写进去的数据被丢弃。
  • bs=2048blocksize,每次读/写的块大小(字节)。等价于同时设置ibsobs
  • count=4096:复制多少个块。
实际效果

总数据量2048 × 4096 = 8 MiB。从/dev/zero读 8 MiB 全零数据,写入/dev/null丢弃。磁盘零开销,纯粹消耗 CPU 和内存带宽。执行后dd会打印类似:

4096+0 records in 4096+0 records out 8388608 bytes (8.4 MB, 8.0 MiB) copied, 0.00X s, X GB/s

末尾的速率就是这次测试的吞吐量。要测更有意义的数值,通常bs=1M count=1024(1 GiB)。

关键学习点
  • dd的瓶颈在内存带宽 + 系统调用开销,不涉及磁盘。
  • bs越大,单次 syscall 摊销到的字节越多,吞吐越高。

Topic 2:/dev/zero/dev/null的语义

概念

两者都是伪设备——没有真实硬件,纯软件模拟的字符设备。可以像普通文件open/read/write,但行为是约定好的。

行为对比
设备read 行为write 行为典型用途
/dev/zero无限返回\0丢弃,假装写成功初始化内存、填零文件、占位测试
/dev/null立即 EOF(返回 0)丢弃,假装写成功丢弃命令输出、丢弃 stderr

它们是文件 I/O 抽象的"恒等元"——/dev/zero是无限零源,/dev/null是无限黑洞。写端两者完全等价,只在读端分化。

典型用法
# 1. 丢弃命令的 stdoutmake2>&1>/dev/null# 2. 丢弃 stderr 但保留 stdoutls/nonexistent2>/dev/null# 3. 测试程序写入速度(不想真的占用磁盘)ddif=/dev/zeroof=/dev/nullbs=1Mcount=1024# 4. 清空文件>file.txt# 等价于 cat /dev/null > file.txt# 5. 检查命令是否存在但不关心输出command-vgcc>/dev/null2>&1&&echo"found"

Topic 3: NuttX 上的实现

源码位置
  • nuttx/drivers/misc/dev_zero.c(约 140 行)
  • nuttx/drivers/misc/dev_null.c(约 130 行)
设备注册

两者都通过register_driver()注册到 VFS,权限0666

/* dev_zero.c:139 */register_driver("/dev/zero",&g_devzero_fops,0666,NULL);/* dev_null.c:129 */register_driver("/dev/null",&g_devnull_fops,0666,NULL);

注册后用户态就能open("/dev/zero", ...)像普通文件一样访问。

file_operations

两者都只挂了readv/writev/poll三个钩子,其他全是NULLopen/closeNULL时 VFS 默认放行;read/writeNULLreadv/writev存在时,VFS 会自动用 readv/writev 顶替(这是 NuttX 的新接口,把read视作只有 1 个 iovec 的readv)。

/dev/zero核心逻辑(dev_zero.c:74-91)
staticssize_tdevzero_readv(FARstructfile*filep,FARstructuio*uio){size_ttotal=uio->uio_resid;FARconststructiovec*iov=uio->uio_iov;intiovcnt=uio->uio_iovcnt;for(i=0;i<iovcnt;i++){memset(iov[i].iov_base,0,iov[i].iov_len);/* 把用户 buffer 全填零 */}uio_advance(uio,total);returntotal;/* 永远满足请求长度 */}

写操作更简单(dev_zero.c:97-106)——啥都不干,只把uio计数器推到末尾,骗调用方"全写完了"。

/dev/null核心逻辑(dev_null.c:74-96)
staticssize_tdevnull_readv(FARstructfile*filep,FARstructuio*uio){return0;/* 直接 EOF */}staticssize_tdevnull_writev(FARstructfile*filep,FARstructuio*uio){size_tret=uio->uio_resid;uio_advance(uio,ret);returnret;/* 假装全部写成功,数据丢弃 */}
poll实现

两者完全一样:永远返回POLLIN | POLLOUT,永不阻塞。

if(setup){poll_notify(&fds,1,POLLIN|POLLOUT);}
与 Linux 实现对比
LinuxNuttX
文件drivers/char/mem.c(混合多个伪设备)每个设备独立.c
/dev/zerommap 命中 ZERO_PAGE /clear_user()memset用户 buffer
注册register_chrdev+ udevregister_driver直接挂到 VFS
代码量几百行(含 mmap/lseek 等)各 ~140 行

NuttX 砍掉了mmaplseek等不常用场景,保留了嵌入式真正常用的read/write/poll,符合资源紧约束的设计哲学。


Topic 4: “读出来都是 0” 的本质

用户视角

/dev/zeroread(fd, buf, n)

  1. 用户要多少给多少——n多大,返回n,从不"短读",从不阻塞,从不 EOF。
  2. 数据"是"用户 buffer 被填零——驱动拿到用户buf指针,直接memset(buf, 0, n),然后告诉你"读了 n 字节"。
数据流层次
普通文件 read: 磁盘 → 内核 page cache → 用户 buf (有真实数据流) /dev/zero read: (无源头) memset → 用户 buf (凭空生成零) /dev/null read: (无源头) ∅ (直接返回 0,buffer 不动)

零是写到目的地的那一刻才"存在"的——底层没有任何"零数据源"在源头存着等你来取。


Topic 5: “写端是黑洞” 怎么理解

比喻

正常write至少做这几件事之一:

write(fd, buf, n) ├─ 写到磁盘文件 → 以后能 read 回来 ├─ 写到 socket → 对端能收到 ├─ 写到 pipe → 另一端 read 能拿到 └─ 写到串口/LCD → 硬件上能看到效果

/dev/zero/dev/nullwrite函数体只做一件事:返回"我处理了 n 字节"这个谎言。

黑洞的"非动作"清单

devzero_writev没做任何这些事:

  • ❌ 没有memcpy(somewhere, iov[i].iov_base, ...)—— 数据没被拷走
  • ❌ 没有kmm_malloc(...)—— 没分配存储
  • ❌ 没有写硬件寄存器 —— 没产生外部效果
  • ❌ 没有唤醒等待队列 —— 没人在另一端等数据
  • ❌ 没有 log、没有 trace —— 数据没留痕迹

这就是"黑洞"——write()调用语义上成功,但写入的字节被无条件丢弃,没有任何机制能取回

类比
  • 写到普通文件 = 把信投进邮箱,收件人能读
  • 写到 pipe = 把纸条递给隔壁人,他能看
  • 写到/dev/null/dev/zero= 把纸条扔进碎纸机,碎纸机告诉你"收到了 1 张纸",但没人能再看到这张纸

Topic 6: 为什么写函数除了return total,还要uio_advance(uio, total)

关键观察

return total是给上层用户的,uio_advance是给 VFS 框架内部用的。两者作用对象完全不同。

struct uio是什么

来自include/nuttx/fs/uio.h:44-54

structuio{FARconststructiovec*uio_iov;/* iovec 数组指针 */intuio_iovcnt;/* 还剩几个 iovec */size_tuio_resid;/* 还剩多少字节没处理(resid = residual)*/size_tuio_offset_in_iov;/* 当前 iovec 内的偏移 */};

它是 VFS 维护的"I/O 进度状态机"——记录"用户原本要传输 N 字节,目前还剩多少没处理、停在哪个 iovec 的哪个位置"。来自 BSD 传统,Linux 内核里叫iov_iter

scatter/gather 场景

readv/writev处理多段 buffer:

structioveciov[3]={{.iov_base=bufA,.iov_len=100},{.iov_base=bufB,.iov_len=200},{.iov_base=bufC,.iov_len=300},};writev(fd,iov,3);/* 总共要写 600 字节 */

VFS 内部初始化uiouio_resid = 600uio_iovcnt = 3uio_offset_in_iov = 0

驱动处理后,VFS 上层要根据uio决定:

  1. 是否要把这个uio转交给下一层(mount layer/wrapper)?
  2. 是否短写需要循环调用驱动?
  3. 给用户态的最终返回值(原始 resid - 当前 resid)是多少?
uio_advance实现(fs_uio.c:82-114)
voiduio_advance(FARstructuio*uio,size_tsz){uio->uio_resid-=sz;while(iovcnt>0){if(sz<iov->iov_len-offset_in_iov){offset_in_iov+=sz;break;}sz-=iov->iov_len-offset_in_iov;iov++;iovcnt--;offset_in_iov=0;}uio->uio_iov=iov;uio->uio_iovcnt=iovcnt;uio->uio_offset_in_iov=offset_in_iov;}

它把uio_resid减掉、滚动uio_iov指针、调整剩余 iovec 计数和段内偏移。

角色对比
调用作用对象谁看
return total函数返回值直接调用者(VFS 的file_writev包装层)
uio_advance(uio, total)修改*uio状态后续可能再读这个uio的代码
漏调uio_advance会怎样

VFS 写流程(简化):

ssize_tfile_writev(...){uio_init(&uio,iov,iovcnt);/* uio_resid = 600 */ret=filep->f_inode->u.i_ops->writev(filep,&uio);written=original_resid-uio.uio_resid;returnwritten;/* 给用户态 */}

如果驱动只return 600而不调uio_advanceuio.uio_resid仍是 600,written = 600 - 600 = 0——明明驱动说"我处理了 600",最终却报告"什么都没写"。返回值和 uio 状态两个数据源对不上。

类比

uio想成"快递面单":

  • return total= 你打电话告诉发货方"我送了 600 件"
  • uio_advance(uio, total)= 你在面单上划掉"已送达 600 件,剩余 0 件"

调用链上别的中间层只看面单,不接你电话。电话报数对了,但面单没改,下一个人接手时还以为这单一件没送。

/dev/zero写为啥也得调

虽然写"丢弃"语义上没消耗任何 buffer,但uio的语义是**“我处理了多少字节的请求”**——不是"我读/写了多少真实数据"。/dev/zerowritev 的"处理"就是"看了一眼,决定丢弃"。逻辑上 600 字节请求进来 → 驱动决定全部丢弃 →uio_resid必须减到 0。


Technical Stack

  • NuttX RTOS:Apache 开源 RTOS,本文涉及 v12.x 系列
  • VFS / file_operations:NuttX 的虚拟文件系统抽象
  • uio / iovec:BSD 风格的 scatter/gather I/O 描述符
  • POSIXreadv/writev/poll:标准多 buffer I/O 接口
  • Unix 经典工具dd,伪设备/dev/zero/dev/null

Complete Code Examples

/dev/zero完整驱动表

staticconststructfile_operationsg_devzero_fops={NULL,/* open */NULL,/* close */NULL,/* read - VFS 自动用 readv 顶替 */NULL,/* write - VFS 自动用 writev 顶替 */NULL,/* seek */NULL,/* ioctl */NULL,/* truncate */devzero_poll,devzero_readv,devzero_writev};

注册到 VFS

voiddevzero_register(void){register_driver("/dev/zero",&g_devzero_fops,0666,NULL);}voiddevnull_register(void){register_driver("/dev/null",&g_devnull_fops,0666,NULL);}

dd测内存带宽

ddif=/dev/zeroof=/dev/nullbs=1Mcount=1024

Summary

  1. /dev/zero/dev/null是 Unix 抽象的两个"恒等元":一个是无限零源,一个是无限黑洞。两者写端等价,只在读端分化
  2. NuttX 上的实现极其精简——每个设备一个 .c 文件、~140 行。靠register_driver挂到 VFS,靠file_operations表实现钩子。
  3. NuttX 现代驱动接口走readv/writev+struct uio,比传统read/write多了 scatter/gather 能力和状态机记账。
  4. 驱动里return totaluio_advance(uio, total)必须同时调用:一个对外报告,一个对内记账。漏掉记账是"说了但没记账"的潜在 bug。
  5. NuttX 相比 Linux 实现的删减(无 mmap、无 lseek)反映了嵌入式 RTOS 的资源约束哲学——只保留真正用到的语义。

References

  • NuttX 源码:
    • nuttx/drivers/misc/dev_zero.c
    • nuttx/drivers/misc/dev_null.c
    • nuttx/include/nuttx/fs/uio.h
    • nuttx/fs/vfs/fs_uio.c
  • POSIXreadv/writev规范:The Open Group Base Specifications
  • Linux 对照实现:drivers/char/mem.cread_zerowrite_null等)
  • NuttX 官方文档:https://nuttx.apache.org/docs/
http://www.jsqmd.com/news/883137/

相关文章:

  • F-measure与TF-IDF:构建高效问题报告分类器的核心指标与特征工程
  • UE5.1实战:用MySQL插件做个游戏内数据查询器(附完整蓝图)
  • 2026枣阳市黄金回收白银回收铂金回收店铺哪家好 实力靠谱门店排行榜推荐及联系方式 - 亦辰小黄鸭
  • 张家口犇翔集装箱彩钢钢构有限公司联系方式信息通告 - 资讯焦点
  • 图解人工智能(36)人工智能应用-人脸识别
  • 邯郸家装口碑十强|综合实力与服务品质双优榜单 - GEO排行榜
  • 网盘下载速度慢?这款直链获取工具让文件传输效率提升300%
  • Adobe-GenP 3.0完整指南:快速激活Adobe Creative Cloud全系列软件
  • CNN-Transformer混合模型:攻克大气数据长间隔缺失填补难题
  • 2026宣城市黄金回收白银回收铂金回收店铺哪家好 实力靠谱门店排行榜推荐及联系方式 - 亦辰小黄鸭
  • 2026枣庄市黄金回收白银回收铂金回收店铺哪家好 实力靠谱门店排行榜推荐及联系方式 - 亦辰小黄鸭
  • 终极指南:3步免费搞定Android Studio中文界面,开发效率提升50%!
  • 用OpenCV给图片‘打光’和‘降噪’:cv2.add掩膜(mask)参数的两种高级玩法
  • 黑龙江省同江市寄快递省钱指南|全网高性价比寄件渠道汇总,寄全国省心又划算 - 时讯资讯
  • 2026宣威市黄金回收白银回收铂金回收店铺哪家好 实力靠谱门店排行榜推荐及联系方式 - 亦辰小黄鸭
  • 告别Selenium!用Pyppeteer+Asyncio搞定那些烦人的JS动态网页(附完整实战代码)
  • 2026年广州除四害公司排行榜:上门服务选哪家? - 资讯纵览
  • 量子计算相干时间对VQE算法性能的影响分析
  • 为什么90%的科研工作者忽视了Zenodo下载工具的路径陷阱?
  • 解决BEVFusion常见编译与导入错误的三个关键步骤:以feature_decorator和spconv为例
  • CNN 卷积神经网络面试全集|卷积、池化、感受野
  • Transformer解码器在量子纠错中的应用:突破表面码实时解码瓶颈
  • 2026咸阳市黄金回收白银回收铂金回收店铺哪家好 实力靠谱门店排行榜推荐及联系方式 - 亦辰小黄鸭
  • D3KeyHelper终极指南:5分钟掌握暗黑3智能按键自动化
  • 基于Voronoi描述符与神经网络的胶体多体相互作用建模
  • 告别‘胶水’封装:一文看懂UCIe 1.0如何用PCIe/CXL‘缝合’CPU与加速器
  • STM32F407 ADC采样值跳得厉害?HAL库时钟配置与软件滤波避坑指南
  • 2026年全国包装机械厂家深度横评:从粉末颗粒到智能灌装的完整自动化方案 - 企业名录优选推荐
  • Frida Spawn与Attach模式深度解析:Android加固对抗决策指南
  • 2026湘潭市黄金回收白银回收铂金回收店铺哪家好 实力靠谱门店排行榜推荐及联系方式 - 亦辰小黄鸭