编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

    • Linux应用开发
    • QML基础入门

    • QT核心库实践

      • QT核心库实践
      • 核心功能基础
      • 并发与多线程
      • 文件与IO系统
        • 3.1 IO 体系全景
        • 3.2 QFile 文件读写
          • 3.2.1 基础读写
          • 3.2.2 内存映射
          • 3.2.3 QSaveFile 原子写入
          • 3.2.4 QLockFile 文件锁
        • 3.3 数据流详解
          • 3.3.1 QTextStream
          • 3.3.2 QDataStream 序列化协议
          • 3.3.3 QDataStream 事务模式
          • 3.3.4 QBuffer 内存流
        • 3.4 目录与文件信息
        • 3.5 QSettings 配置存储
        • 3.6 文件监控与临时文件
          • 3.6.1 QFileSystemWatcher 的内核实现
          • 3.6.2 QTemporaryFile 安全性
        • 3.7 QIODevice 继承设计
        • 3.8 嵌入式 IO 实战
          • 3.8.1 eMMC 磨损均衡与写入策略
          • 3.8.2 IO 性能对比(ARM Cortex-A53 + eMMC)
          • 3.8.3 文件路径可移植性
          • 3.8.4 QStandardPaths 标准路径
        • 3.9 综合案例
          • 案例:嵌入式数据采集器的完整 IO 方案
        • 3.10 速查表
      • 日期与时间处理
      • 网络与序列化
      • QT事件系统
      • 信号与槽机制
      • 多媒体的应用
      • 容器类和算法
      • 高级编程技巧
    • Linux系统编程

    • 综合项目实战

  • IoT智能硬件开发

  • Apps
  • Linux应用开发
  • QT核心库实践
杨充
2025-08-15
目录

文件与IO系统

# 第 3 章 文件与 IO 系统

Qt 的 IO 体系以 QIODevice 为统一基类,不管是文件、网络套接字、还是内存缓冲区,都用同一套 read()/write() 接口。理解 QDataStream 的序列化版本机制、QSettings 的跨平台存储策略、以及 QFile::map() 零拷贝读写——这是嵌入式读写大量日志和配置数据的命门。

# 目录介绍

  • 3.1 IO 体系全景
  • 3.2 QFile 文件读写
    • 3.2.1 基础读写
    • 3.2.2 内存映射
    • 3.2.3 QSaveFile 原子写入
    • 3.2.4 QLockFile 文件锁
  • 3.3 数据流详解
    • 3.3.1 QTextStream
    • 3.3.2 QDataStream 序列化协议
    • 3.3.3 QDataStream 事务模式
    • 3.3.4 QBuffer 内存流
  • 3.4 目录与文件信息
  • 3.5 QSettings 配置存储
  • 3.6 文件监控与临时文件
  • 3.7 QIODevice 继承设计
  • 3.8 嵌入式 IO 实战
  • 3.9 综合案例
  • 3.10 速查表

# 3.1 IO 体系全景

Qt 的 IO 类层次结构——统一接口、多种后端:

QIODevice ── 抽象基类(open/close/read/write/seek)
  ├── QFile          → 磁盘文件
  ├── QTemporaryFile → 临时文件(自动命名、自动删除)
  ├── QProcess       → 子进程的标准输入/输出
  ├── QBuffer        → 内存中的 byte array
  ├── QTcpSocket     → TCP 网络流
  └── QLocalSocket   → Unix Domain Socket

封装层:
  QTextStream → QIODevice → 文本编码/解码(UTF-8/GBK/...)
  QDataStream → QIODevice → 二进制序列化(平台无关格式)

关键设计:QFile file("data.bin"); QDataStream out(&file);——通过组合 QIODevice* 指针,一个 QDataStream 同时可以写文件、网络、或内存缓存,切换后端不需要改代码。


# 3.2 QFile 文件读写

# 3.2.1 基础读写

QFile file("/opt/app/config.ini");
if (!file.open(QIODevice::ReadWrite | QIODevice::Text)) {
    qWarning() << file.errorString();
    return;
}

QByteArray blob = file.read(4096);  // 读 4KB
file.seek(0);                        // 回到文件开头
qint64 size = file.size();          // 获取文件大小

// 逐行读——大文件友好
while (!file.atEnd()) {
    QByteArray line = file.readLine();
    if (line.isEmpty()) continue;
}

file.write("key=value\n");          // 追加写入
file.flush();                        // 强制刷盘
file.close();

最佳实践:嵌入式 eMMC 写入后必须 flush,否则断电丢数据。

# 3.2.2 内存映射

QFile::map() 把文件映射到进程地址空间——操作系统负责页缓存,零拷贝读取大文件:

QFile file("/var/log/sensor.log");
file.open(QIODevice::ReadOnly);

// 映射从 offset 0、长度 1MB
uchar* addr = file.map(0, 1024 * 1024);
if (addr) {
    // addr[0]..addr[size-1] 就是文件内容——不需要 read() syscall
    int lineCount = 0;
    for (qint64 i = 0; i < file.size(); ++i)
        if (addr[i] == '\n') ++lineCount;
    file.unmap(addr);
}

// 只适合大文件顺序读取;小文件(< 64KB)直接 readAll 更简单

mmap 的内部原理:

┌─────────────────────────────────────────────────┐
│ 用户进程地址空间                                    │
│  ┌──────────────────────┐                        │
│  │ addr = 0x7f800000    │ ← QFile::map() 返回    │
│  │ [文件内容映射区]       │                         │
│  │  addr[0] → 第1字节    │                         │
│  │  addr[99]→ 第100字节  │                         │
│  └──────────┬───────────┘                        │
│             │ 页表映射(MMU)                       │
│  ┌──────────▼───────────┐                        │
│  │ Page Cache (内核)     │ ← 操作系统管理           │
│  │ ┌───┬───┬───┬───┐   │                         │
│  │ │p0 │p1 │p2 │p3 │   │ 缺页时从磁盘加载          │
│  │ └───┴───┴───┴───┘   │                         │
│  └──────────┬───────────┘                        │
│             │                                     │
│  ┌──────────▼───────────┐                        │
│  │ eMMC / SSD / HDD     │ ← 物理存储              │
│  └──────────────────────┘                        │
└─────────────────────────────────────────────────┘

对比:
  read(4096) → user态↔kernel态拷贝 4KB → CPU L1 cache污染
  mmap读    → 无需拷贝,直接访问 Page Cache → 零CPU拷贝

mmap 性能实测(100MB 日志文件顺序扫描,ARM Cortex-A53):

方式 耗时 CPU 使用率 说明
read() 循环 820ms 45% 每次 read 触发一次 copy_to_user
QFile::map() 210ms 12% 零拷贝,仅首次缺页开销
readAll() 980ms 50% 100MB 堆分配 + 拷贝

⚠️ mmap 陷阱:

  • 不能映射比地址空间大的文件(32 位系统限制 ~3GB)
  • 写入 mmap 区域后需要 msync 确保持久化(或依赖 OS 回写)
  • 映射期间文件被 truncate → SIGBUS 信号

# 3.2.3 QSaveFile 原子写入

问题:嵌入式设备在写入配置文件时突然断电 → 文件损坏(写了一半)。

原理:QSaveFile 先写临时文件 → commit() 原子 rename → 原文件永远完整:

QSaveFile file("/etc/app/config.json");
if (!file.open(QIODevice::WriteOnly)) {
    qWarning() << "Cannot open:" << file.errorString();
    return;
}

// ① 数据写入临时文件 /etc/app/config.json.XXXXXX
file.write(jsonData);
file.flush();  // fsync 到磁盘

// ② 调用 commit 执行原子 rename
//    rename("/etc/app/config.json.XXXXXX", "/etc/app/config.json")
//    POSIX 保证 rename 是原子操作——要么成功,要么旧文件还在
if (!file.commit()) {
    qWarning() << "Commit failed:" << file.errorString();
    // 临时文件自动删除
}

QSaveFile 内部状态机:

[构造] → open() → [Writing] → commit() → [Committed]
                 ↘ cancelWriting() → 删除临时文件

为什么 rename 是原子的? 这是 POSIX 文件系统标准——inode 指针的原子交换,不涉及数据拷贝。即使断电,磁盘上的 inode 状态要么指向旧文件,要么指向新文件,不会出现"一半旧一半新"的半损坏状态。

# 3.2.4 QLockFile 文件锁

嵌入式设备上防止多实例启动:

QLockFile lockFile("/var/run/myapp.lock");

// 非阻塞尝试——如果已锁立即返回 false
if (!lockFile.tryLock(1000)) {  // 等待 1 秒
    qint64 pid;
    QString host, app;
    lockFile.getLockInfo(&pid, &host, &app);
    qWarning() << "Another instance is running (PID:" << pid << ")";
    return 1;
}

// 应用退出时自动解锁(析构 + unlink 锁文件)
// 如果进程崩溃 → 锁文件残留 → QLockFile 通过 pid 检测"僵尸锁"
// → staleLockTime 后自动清理

锁文件格式(/var/run/myapp.lock):

12345
myhostname
myapp

跨平台行为:

  • Linux: 使用 flock() 的排他锁
  • Windows: 使用 LockFileEx()
  • 检测僵尸锁:检查 pid 进程是否存在

# 3.3 数据流详解

# 3.3.1 QTextStream

自动编码转换 + 格式化输出:

QFile file("report.txt");
file.open(QIODevice::WriteOnly);
QTextStream out(&file);
out.setEncoding(QStringConverter::Utf8);

out << qSetFieldWidth(10) << left << "Timestamp"
    << qSetFieldWidth(8)  << "Value"
    << qSetFieldWidth(8)  << "Unit" << Qt::endl;
out << qSetFieldWidth(10) << "12:30:00"
    << qSetFieldWidth(8)  << qSetRealNumberPrecision(2) << 23.5
    << qSetFieldWidth(8)  << "°C" << Qt::endl;
// 输出:
// Timestamp  Value   Unit
// 12:30:00   23.50   °C

# 3.3.2 QDataStream 序列化协议

QDataStream 按平台无关的二进制格式写入,自带版本标记和大小端控制:

// 写入——平台无关
QFile file("sensor.dat");
file.open(QIODevice::WriteOnly);

QDataStream out(&file);
out.setVersion(QDataStream::Qt_6_0);    // 版本固定——保证跨平台兼容
out.setByteOrder(QDataStream::LittleEndian);

out << (qint32)42 << QString("温度传感器") << QDateTime::currentDateTime();
// 内部格式(简化):
// [4 bytes: qint32=42]
// [4 bytes: QString长度=7]
// [14 bytes: "温度传感器" (UTF-16)]
// [8 bytes: QDateTime 的 msecs]

版本兼容策略——如果部署到不同 Qt 版本的设备上:

QDataStream in(&file);
qint32 ver = in.version();        // 读出版本

qint32 id;
QString name;
QDateTime ts;

if (ver >= QDataStream::Qt_6_0) {
    in >> id >> name >> ts;       // Qt 6 格式
} else {
    in.setVersion(ver);
    quint16 len; in >> len;       // Qt 5 QDateTime 多一个长度字段
    in >> id >> name >> ts;
}

# 3.3.3 QDataStream 事务模式

Qt 5.7+ 引入了事务(Transaction)模式——解决不完整数据的问题:

QFile file("sensor.dat");
file.open(QIODevice::ReadOnly);
QDataStream in(&file);

while (!in.atEnd()) {
    in.startTransaction();        // ← 标记回滚点

    qint32 header;
    QString body;
    QDateTime ts;
    in >> header >> body >> ts;

    if (in.commitTransaction()) { // ← 全部成功 → 提交
        processRecord(header, body, ts);
    } else {
        // 数据不完整或格式错误 → 自动回滚到 startTransaction 位置
        // 等待更多数据到达(网络流场景)
        break;
    }
}

事务模式的内部实现:

startTransaction():
  → 保存当前 stream 位置 = markPosition
  → 设置 status = Ok

read 操作(如 in >> x):
  → 检测 status == Ok → 正常读取
  → 遇到 EOF/格式错 → status = ReadCorruptData

commitTransaction():
  → status == Ok → 丢弃回滚点 → return true
  → status != Ok → seek(markPosition) → return false

关键:所有读取操作都不修改原始缓冲——只在 commit 成功后"确认"位置

适用场景:

  • 网络流:TCP 粘包 → 数据可能不完整 → 事务模式自动回滚等待
  • 文件流:损坏文件的容错读取
  • 不支持 seek 的设备(如串口):事务模式是唯一验证完整性的方式

# 3.3.4 QBuffer 内存流

不写磁盘——直接在内存中构建二进制包:

QByteArray buffer;
QBuffer io(&buffer);
io.open(QIODevice::WriteOnly);

QDataStream out(&io);
out << int32(0x48656C6F) << QString("Hello");

// 发送到网络
socket->write(buffer);

// 从网络包解析
QByteArray packet = socket->readAll();
QBuffer readBuf(&packet);
readBuf.open(QIODevice::ReadOnly);
QDataStream in(&readBuf);
qint32 header; QString msg;
in >> header >> msg;

# 3.4 目录与文件信息

// 遍历——按扩展名过滤
QDir dir("/var/log");
QStringList logFiles = dir.entryList({"*.log"}, QDir::Files, QDir::Time);
for (const auto& f : logFiles) {
    QFileInfo info(dir.filePath(f));
    qDebug() << f << info.size() / 1024 << "KB"
             << info.lastModified().toString("yyyy-MM-dd hh:mm");
}

// 文件信息——一行获取所有
QFileInfo info("/opt/app/config.xml");
info.exists();         info.isFile();          info.isDir();
info.size();           info.lastModified();
info.absolutePath();   info.fileName();        info.suffix();

# 3.5 QSettings 配置存储

嵌入式设备上保存配置——不需要自己设计文件格式:

// 写入——自动选择存储位置
QSettings settings("MyCompany", "DashboardApp");
settings.setValue("display/brightness", 85);
settings.setValue("network/host", "192.168.1.100");
settings.setValue("sensors/interval", 100);  // ms

// 读取——带默认值
int brightness = settings.value("display/brightness", 50).toInt();
QString host = settings.value("network/host", "localhost").toString();

// 存储位置(Linux):
// /home/user/.config/MyCompany/DashboardApp.conf   ← XDG 标准
// 或通过 QSettings::setPath 自定义到 /opt/config/

QSettings 跨平台存储策略:

平台 默认路径
Linux ~/.config/Organization/App.conf(INI 格式)
嵌入式 Linux(root) /etc/xdg/Organization/App.conf
Windows 注册表 HKCU\Software\Organization\App

# 3.6 文件监控与临时文件

// 文件监控——检测配置文件热更新
QFileSystemWatcher watcher;
watcher.addPath("/etc/dashboard/config.ini");
QObject::connect(&watcher, &QFileSystemWatcher::fileChanged,
    [](const QString& path) {
        qDebug() << "Config changed:" << path;
        reloadConfig();  // 触发热重载
    });

// 临时文件——自动命名 + 退出自动删除
QTemporaryFile tmp;
tmp.setFileTemplate("/tmp/sensor_XXXXXX.dat");
if (tmp.open()) {
    tmp.write(processImage());     // 写入处理后的图像
    QString path = tmp.fileName(); // 返回 /tmp/sensor_aB3f2X.dat
    uploadToServer(path);          // 上传后无需手动删除
}  // tmp 析构时自动 unlink

# 3.6.1 QFileSystemWatcher 的内核实现

QFileSystemWatcher 在 Linux 上基于 inotify:

应用层: QFileSystemWatcher::fileChanged()
         ↓
Qt 内部: QInotifyFileSystemWatcherEngine
         ↓ syscall
内核层: inotify_add_watch(fd, "/etc/config.ini", IN_MODIFY | IN_DELETE_SELF)
  → 文件被修改 → 内核发送 inotify_event
  → Qt 事件循环 epoll_wait 检测到 inotify fd 可读
  → QSocketNotifier 投递事件 → emit fileChanged()

⚠️ 注意:被监控的文件如果被删除再重建(vim 保存方式),原 inotify watch 会失效——需要在 fileChanged 回调中重新 addPath。

# 3.6.2 QTemporaryFile 安全性

QTemporaryFile 使用 POSIX 的 mkstemp() 创建:
  → 文件名: template + 6位随机字符
  → 原子创建 + O_EXCL(已存在则重试)
  → 权限: 0600(仅当前用户可读写)

好处:防止临时文件竞态条件(TOCTOU)
      即使攻击者猜到 /tmp/sensor_XXXXXX 模板
      mkstemp 保证原子创建唯一文件

# 3.7 QIODevice 继承设计

Qt IO 的精髓在于统一抽象——下面的继承树展示了组合的力量:

QIODevice(抽象基类)
  │  virtual qint64 readData(char*, qint64) = 0;
  │  virtual qint64 writeData(const char*, qint64) = 0;
  │
  ├── QFileDevice(文件系统设备)
  │     ├── QFile            → 普通文件
  │     ├── QTemporaryFile   → 临时文件
  │     └── QSaveFile        → 原子写入文件(Qt 5.1+)
  │
  ├── QBuffer                → 内存缓冲区
  ├── QProcess               → 子进程 stdin/stdout
  ├── QAbstractSocket → 网络套接字
  │     ├── QTcpSocket
  │     ├── QUdpSocket
  │     └── QSslSocket
  └── QLocalSocket           → Unix Domain Socket

继承 QIODevice 的最小实现——如果你需要自定义 IO 后端:

class RingBufferIO : public QIODevice {
    Q_OBJECT
public:
    explicit RingBufferIO(int capacity, QObject* parent = nullptr)
        : QIODevice(parent), m_capacity(capacity), m_buffer(new char[capacity]) {}

    bool isSequential() const override { return false; }

protected:
    qint64 readData(char* data, qint64 maxSize) override {
        qint64 available = (m_writePos - m_readPos + m_capacity) % m_capacity;
        qint64 n = qMin(maxSize, available);
        for (qint64 i = 0; i < n; ++i)
            data[i] = m_buffer[(m_readPos + i) % m_capacity];
        m_readPos = (m_readPos + n) % m_capacity;
        return n;
    }

    qint64 writeData(const char* data, qint64 maxSize) override {
        qint64 space = (m_readPos - m_writePos - 1 + m_capacity) % m_capacity;
        qint64 n = qMin(maxSize, space);
        for (qint64 i = 0; i < n; ++i)
            m_buffer[(m_writePos + i) % m_capacity] = data[i];
        m_writePos = (m_writePos + n) % m_capacity;
        emit readyRead();  // QIODevice 标准信号
        return n;
    }

    // QIODevice 自动提供:
    //   read(), readAll(), write(), peek(), seek(), pos(), size()...
    // 你只需实现 readData + writeData!

private:
    int m_capacity;
    qint64 m_readPos = 0, m_writePos = 0;
    QScopedArrayPointer<char> m_buffer;
};

QIODevice 自动处理的能力:

  1. 缓冲——open() 时分配内部缓冲区,减少 syscall 次数
  2. 编码——与 QTextStream 组合自动处理字符集转换
  3. 信号——readyRead() / bytesWritten() 自动 emit
  4. 错误处理——errorString() 统一错误信息接口

# 3.8 嵌入式 IO 实战

# 3.8.1 eMMC 磨损均衡与写入策略

嵌入式 eMMC 的擦写寿命通常 3000-10000 次/块——频繁写入配置文件很快就会损坏存储:

// ❌ 错误方式:每次传感器数据都写磁盘
void onSensorData(const QVector<float>& data) {
    QFile file("/data/sensor.log");
    file.open(QIODevice::WriteOnly | QIODevice::Append);
    QTextStream out(&file);
    out << QDateTime::currentDateTime().toString()
        << ", " << data[0] << Qt::endl;   // 每秒 10 次 = 864000 次/天
    file.close();
    // → 3 个月内 eMMC 特定块可能损坏
}
// ✅ 正确方式 1:内存缓冲 + 批量刷盘
class SensorLogger : public QObject {
    Q_OBJECT
public:
    SensorLogger() {
        m_flushTimer.setInterval(5000);  // 5 秒刷一次盘
        connect(&m_flushTimer, &QTimer::timeout, this, &SensorLogger::flush);
        m_flushTimer.start();
    }

    void log(const QVector<float>& data) {
        QTextStream stream(&m_buffer);
        stream << QDateTime::currentDateTime().toString(Qt::ISODate)
               << ", " << data[0] << Qt::endl;
    }

    void flush() {
        if (m_buffer.isEmpty()) return;
        QSaveFile file("/data/sensor.log");  // 原子写入
        if (file.open(QIODevice::WriteOnly | QIODevice::Append)) {
            file.write(m_buffer);
            file.commit();
        }
        m_buffer.clear();
    }

private:
    QByteArray m_buffer;     // 内存缓冲
    QTimer m_flushTimer;     // 定时刷盘
};
// ✅ 正确方式 2:环形日志文件——限制写入量
class RingLogWriter {
    static constexpr int MAX_FILES = 10;
    static constexpr int MAX_SIZE = 1024 * 1024;  // 1MB/文件

    void write(const QByteArray& data) {
        openCurrentFile();
        m_file.write(data);
        if (m_file.size() > MAX_SIZE) {
            rotate();  // 轮转到下一个文件
        }
    }

    void rotate() {
        // sensor_9.log → 删除
        // sensor_8.log → sensor_9.log
        // ...
        // sensor_0.log → sensor_1.log
        // 新建 sensor_0.log
    }
};

# 3.8.2 IO 性能对比(ARM Cortex-A53 + eMMC)

操作 方式 100KB 耗时 1MB 耗时 说明
读取 QFile::readAll() 0.8ms 6.5ms 一次 syscall
读取 QFile::read() 循环 1.2ms 9.8ms 多次 syscall
读取 QFile::map() 0.3ms 2.1ms 零拷贝
写入 QFile::write() 1.5ms 12ms 写入 Page Cache
写入 QFile::write() + flush() 15ms 130ms 强制刷盘(fsync)
写入 QSaveFile 18ms 145ms fsync + rename
写入 QDataStream 序列化 1.8ms 15ms 序列化开销 ~0.3ms

结论:嵌入式写入瓶颈在 fsync——尽量减少强制刷盘次数,用 QSaveFile 保证原子性。

# 3.8.3 文件路径可移植性

// ❌ 硬编码路径
// QFile file("C:\\Users\\admin\\config.ini");   // 只在 Windows 工作

// ❌ 手动拼接(分隔符不兼容)
// QString path = "/opt/app" + "/" + "config" + ".ini";

// ✅ 使用 QDir 或 QFileInfo 拼接
QString path = QDir("/opt/app").filePath("config.ini");
// 等价于: /opt/app/config.ini (Linux)
//       C:\opt\app\config.ini  (Windows)

// ✅ 使用 QStandardPaths 获取标准路径
QString configPath = QStandardPaths::writableLocation(
    QStandardPaths::AppConfigLocation);

# 3.8.4 QStandardPaths 标准路径

嵌入式 Linux 上不要硬编码路径——使用 QStandardPaths:

// 数据目录
QString dataPath = QStandardPaths::writableLocation(
    QStandardPaths::AppDataLocation);  // ~/.local/share/<AppName>/

// 配置目录
QString configPath = QStandardPaths::writableLocation(
    QStandardPaths::AppConfigLocation); // ~/.config/<AppName>/

// 缓存目录(可被系统清理)
QString cachePath = QStandardPaths::writableLocation(
    QStandardPaths::CacheLocation);     // ~/.cache/<AppName>/

// 运行时临时文件
QString tempPath = QStandardPaths::writableLocation(
    QStandardPaths::TempLocation);      // /tmp/

嵌入式自定义:

// 在 main() 最开始设置——覆盖默认路径
QStandardPaths::setTestModeEnabled(true);
// → 所有路径变成 /tmp/test_fake_home/...
// 适合单元测试和 CI 环境

# 3.9 综合案例

# 案例:嵌入式数据采集器的完整 IO 方案

需求:
1. 传感器数据每秒 20 条 → 缓冲 5 秒 → 批量写入
2. 配置文件热更新(监控 inotify)
3. 日志环形存储(最多 10 个文件 × 1MB)
4. 原子写入保证断电不丢配置
class DataCollector : public QObject {
    Q_OBJECT

public:
    DataCollector() {
        // 1. 配置文件监控
        m_watcher.addPath("/etc/collector/config.json");
        connect(&m_watcher, &QFileSystemWatcher::fileChanged,
                this, &DataCollector::reloadConfig);

        // 2. 定时刷盘——5 秒批量写入
        connect(&m_flushTimer, &QTimer::timeout, this, &DataCollector::flushData);
        m_flushTimer.start(5000);

        // 3. 退出前最后一次刷盘
        connect(qApp, &QCoreApplication::aboutToQuit, this, &DataCollector::flushData);
    }

    void onSensorSample(const SensorSample& sample) {
        // 写入内存缓冲(不触发磁盘 IO)
        QDataStream stream(&m_dataBuffer, QIODevice::Append);
        stream << sample.timestamp << sample.value << sample.channelId;
    }

private:
    void flushData() {
        if (m_dataBuffer.isEmpty()) return;

        // 环形日志文件
        QSaveFile file(currentLogPath());
        if (file.open(QIODevice::Append)) {
            file.write(m_dataBuffer);
            if (file.commit()) {
                m_dataBuffer.clear();
                checkRotation();  // 检查是否需要轮转
            }
        }
    }

    void reloadConfig(const QString& path) {
        // 原子读取配置
        QFile file(path);
        if (!file.open(QIODevice::ReadOnly)) return;

        QByteArray data = file.readAll();
        QJsonDocument doc = QJsonDocument::fromJson(data);
        if (doc.isNull()) {
            qWarning() << "JSON parse error:" << path;
            return;
        }

        applyConfig(doc.object());

        // 被删除后重建的文件 → 重新添加监控
        if (!QFile::exists(path)) {
            m_watcher.addPath(path);
        }
    }

    void checkRotation() {
        QFileInfo info(currentLogPath());
        if (info.size() > 1024 * 1024) {  // > 1MB
            rotateLogFiles();
        }
    }

    QString currentLogPath() {
        return QDir("/var/log/collector")
            .filePath(QString("data_%1.log").arg(m_currentLogIndex));
    }

    QByteArray m_dataBuffer;
    QFileSystemWatcher m_watcher;
    QTimer m_flushTimer;
    int m_currentLogIndex = 0;
};

# 3.10 速查表

类 用途 关键方法
QIODevice IO 抽象基类 read(), write(), readyRead()
QFile 磁盘文件 open(), map(), flush()
QSaveFile 原子写入 commit(), cancelWriting()
QLockFile 进程锁 tryLock(), getLockInfo()
QTemporaryFile 临时文件 open(), fileName()
QDataStream 二进制序列化 setVersion(), startTransaction()
QTextStream 文本流 setEncoding(), qSetFieldWidth()
QBuffer 内存 IO buffer(), setData()
QDir 目录操作 entryList(), mkpath(), filePath()
QFileInfo 文件信息 exists(), size(), lastModified()
QSettings 配置持久化 setValue(), value()
QFileSystemWatcher 文件监控 addPath(), fileChanged()
QStandardPaths 标准路径 writableLocation()

IO 铁律:

  1. 嵌入式 eMMC 写入后必须 flush() + 用 QSaveFile 保证原子性
  2. 大文件顺序读用 map()(零拷贝),小文件用 readAll()
  3. QDataStream 固定版本号,避免 Qt 升级后反序列化失败
  4. 配置文件用 QFileSystemWatcher 实现热更新
  5. 频繁写入场景用内存缓冲 + 批量刷盘,保护 eMMC 寿命
  6. 不使用硬编码路径——底层用 QStandardPaths,上层用 QDir::filePath()
上次更新: 2026/07/02, 11:21:43
并发与多线程
日期与时间处理

← 并发与多线程 日期与时间处理→

最近更新
01
audit
07-27
02
C++入门教程全章思考题汇编
07-24
03
12.技术团队建设能力
07-21
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式