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

Linux 下 C++ 操作串口并彻底释放控制权的总结

文章目录

    • 0. 引言
    • 1. 问题描述
    • 2. 原因分析
    • 3. 解决方案
    • 4. 代码示例
      • 4.1 代码说明
      • 4.2 关键点解释
    • 5. 总结
    • 6. 附录:进一步优化建议

0. 引言

在 Linux 系统下,通过 C++ 操作串口并确保其正确释放控制权,是一个常见需处理的问题。本文将通过实际案例,分析在多线程环境下串口无法释放控制权的原因,并提供解决方案。

1. 问题描述

在使用 C++ 打开并操作串口(例如 /dev/ttyUSB0)时,调用 close(fd) 返回 0,但串口控制权未能被彻底释放。这导致其他程序无法正常访问该串口设备。经排查发现,问题出在读取数据的线程仍在阻塞的 read() 调用中,导致串口无法真正释放。

2. 原因分析

在多线程程序中,文件描述符(fd)是被所有线程共享的。当一个线程在阻塞状态下调用 read(fd, ...) 时,另一个线程尝试关闭该文件描述符(调用 close(fd))会导致以下问题:

  • 阻塞的 read() 调用:读取线程仍在等待数据,无法及时响应关闭信号。
  • 资源未完全释放:由于读取线程仍持有对文件描述符的引用,系统无法完全释放串口资源,导致其他程序无法访问。

3. 解决方案

为确保串口控制权能够被彻底释放,需要在关闭文件描述符前,确保所有使用该串口的线程已停止操作。具体步骤如下:

  • 使用线程同步机制:引入一个线程安全的标志位,通知读取线程停止读取并退出。
  • 中断阻塞的 read() 调用:通过关闭文件描述符,强制阻塞在 read() 的读取线程中断 read() 调用,使其返回错误。
  • 等待读取线程结束:在关闭文件描述符后,使用 std::thread::join() 等待读取线程完成,确保所有资源被正确释放。

4. 代码示例

以下是一个完整的 C++ 示例,展示如何在多线程环境下正确管理串口的打开、读取和关闭,确保串口控制权的彻底释放。代码中的日志统一使用 fprintf 进行打印。

#include <iostream>
#include <fcntl.h>      // For open()
#include <termios.h>    // For termios structures and functions
#include <unistd.h>     // For close()
#include <cstring>      // For memset()
#include <cerrno>       // For errno
#include <thread>       // For std::thread
#include <atomic>       // For std::atomic// Atomic flag to control the reading thread
std::atomic<bool> keepReading(true);// Function executed by the reading thread
void readThreadFunction(int fd) {char buffer[256];while (keepReading.load()) {ssize_t bytesRead = read(fd, buffer, sizeof(buffer));if (bytesRead > 0) {// Process the read datafprintf(stdout, "Read %zd bytes: ", bytesRead);fwrite(buffer, 1, bytesRead, stdout);fprintf(stdout, "\n");} else if (bytesRead == -1) {if (errno == EINTR) {// Interrupted by a signal, continue readingcontinue;} else if (errno == EBADF) {// File descriptor was closed, exit the loopbreak;} else {fprintf(stderr, "Read error: %s\n", strerror(errno));break;}} else {// EOF reachedbreak;}}fprintf(stdout, "Read thread exiting.\n");
}int main() {const char* portname = "/dev/ttyUSB0";  // Serial port device nameint fd = open(portname, O_RDWR | O_NOCTTY | O_NDELAY); // Open the serial portif (fd == -1) {fprintf(stderr, "Unable to open port %s: %s\n", portname, strerror(errno));return -1;}// Clear the O_NDELAY flag to make read() blockingif (fcntl(fd, F_SETFL, 0) == -1) {fprintf(stderr, "fcntl failed: %s\n", strerror(errno));close(fd);return -1;}struct termios options;if (tcgetattr(fd, &options) < 0) {fprintf(stderr, "Failed to get attributes: %s\n", strerror(errno));close(fd);return -1;}// Set baud rates to 115200if (cfsetispeed(&options, B115200) < 0 || cfsetospeed(&options, B115200) < 0) {fprintf(stderr, "Failed to set baud rate: %s\n", strerror(errno));close(fd);return -1;}// Configure 8N1options.c_cflag &= ~PARENB; // No parityoptions.c_cflag &= ~CSTOPB; // 1 stop bitoptions.c_cflag &= ~CSIZE;options.c_cflag |= CS8;     // 8 data bits// Enable the receiver and set local modeoptions.c_cflag |= (CLOCAL | CREAD);// Set raw input mode (non-canonical, no echo)options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);options.c_iflag &= ~(IXON | IXOFF | IXANY); // Disable software flow controloptions.c_oflag &= ~OPOST;                  // Disable output processing// Apply the settingsif (tcsetattr(fd, TCSANOW, &options) < 0) {fprintf(stderr, "Failed to set attributes: %s\n", strerror(errno));close(fd);return -1;}fprintf(stdout, "Serial port opened and configured successfully.\n");// Start the reading threadstd::thread reader(readThreadFunction, fd);// Simulate main thread workfprintf(stdout, "Press Enter to stop reading and close the serial port...\n");std::cin.get();// Signal the reading thread to stopkeepReading.store(false);// Close the file descriptor to interrupt read()if (close(fd) < 0) {fprintf(stderr, "Failed to close port: %s\n", strerror(errno));}// Wait for the reading thread to finishif (reader.joinable()) {reader.join();}fprintf(stdout, "Serial port closed and control released.\n");return 0;
}

4.1 代码说明

  • 打开串口

    int fd = open(portname, O_RDWR | O_NOCTTY | O_NDELAY);
    
    • O_RDWR: 以读写模式打开。
    • O_NOCTTY: 串口不会成为调用进程的控制终端。
    • O_NDELAY: 打开时不阻塞。
  • 配置串口参数
    使用 termios 结构体设置波特率为 115200,数据位为 8 位,无奇偶校验和 1 位停止位(8N1 模式)。

  • 启动读取线程

    std::thread reader(readThreadFunction, fd);
    

    创建一个新线程来处理串口数据的读取。

  • 关闭串口
    在主线程接收到用户输入后,设置 keepReadingfalse,并关闭文件描述符 fd,这将中断阻塞在 read() 的读取线程。

  • 等待线程结束
    使用 reader.join() 等待读取线程安全退出,确保资源的正确释放。

4.2 关键点解释

  • 线程同步
    使用 std::atomic<bool> 类型的 keepReading 变量,确保主线程与读取线程之间的同步。当主线程需要关闭串口时,通过设置 keepReadingfalse,通知读取线程退出循环。

  • 中断阻塞的 read() 调用
    关闭文件描述符 close(fd) 会导致阻塞在 read(fd, ...) 的读取线程中断 read() 调用,read() 返回 -1errno 被设置为 EBADF。读取线程检测到 EBADF 后,退出循环。

  • 等待线程结束
    使用 std::thread::join() 等待读取线程完成,确保所有资源在释放前已被正确处理。

5. 总结

在 Linux 下使用 C++ 操作串口时,尤其是在多线程环境中,需要特别注意资源的正确管理和线程的同步。通过以下步骤,可以确保串口控制权的正确释放:

  • 使用阻塞式 read()

    • 简化线程同步和资源管理。
    • 通过关闭文件描述符中断 read() 调用,使读取线程能够及时响应并退出。
  • 引入线程同步机制

    • 使用 std::atomic<bool> 或其他线程安全的标志位,通知读取线程停止读取并退出。
  • 等待读取线程结束

    • 使用 std::thread::join() 等方法,确保所有读取线程在关闭串口前已安全退出。

通过遵循上述步骤,可以有效避免串口资源被占用或无法释放的问题,确保系统中其他进程能够正常访问串口设备。

6. 附录:进一步优化建议

虽然本文聚焦于解决多线程读取导致串口无法释放的问题,但在实际应用中,可能还需要考虑以下优化:

  • 使用 RAII 模式管理资源

    • 通过封装串口操作和线程管理在一个类中,利用构造函数和析构函数自动管理资源,提升代码的健壮性和可维护性。
  • 处理异常和错误

    • 在多线程环境中,确保所有可能的异常和错误都被正确捕获和处理,避免资源泄漏和程序崩溃。
  • 使用高级库

    • 考虑使用成熟的串口通信库,如 libserial,这些库封装了更多的细节,提供更可靠的串口管理。

相关文章:

  • c#中字符串处理的技巧集合
  • socket网络编程
  • MySQL高阶2004-职员招聘人数
  • 713. 乘积小于 K 的子数组 滑动窗口
  • Python Pandas数据处理效率提升指南
  • 【笔记】自动驾驶预测与决策规划_Part4_时空联合规划
  • 【GUI设计】基于Matlab的图像去噪GUI系统(8),matlab实现
  • 构建企业数字化转型的战略基石——TOGAF框架的深度解析
  • 物理学基础精解【26】
  • 录屏+GIF一键生成,2024年费软件大揭秘
  • Kubernetes 中 Pod 和 Node 的关系详解
  • 代码整洁之道 — 1 命名规范
  • C++——有3个学生,每个学生的数据包括:学号、姓名、3门课的成绩,从键盘输入三个学生的数据。要求打印学生三门课的平均分。
  • SpringBoot使用EasyPoi根据模板导出word or pdf
  • 什么是网络准入控制系统?2024年有哪些好用的网络准入控制系统?
  • 【Linux系统编程】快速查找errno错误码信息
  • 【译】理解JavaScript:new 关键字
  • 4月23日世界读书日 网络营销论坛推荐《正在爆发的营销革命》
  • angular组件开发
  • CNN 在图像分割中的简史:从 R-CNN 到 Mask R-CNN
  • eclipse(luna)创建web工程
  • Git初体验
  • JavaScript类型识别
  • KMP算法及优化
  • Spring核心 Bean的高级装配
  • 猴子数据域名防封接口降低小说被封的风险
  • 开发基于以太坊智能合约的DApp
  • 力扣(LeetCode)21
  • 前端技术周刊 2019-02-11 Serverless
  • 微信小程序上拉加载:onReachBottom详解+设置触发距离
  • 小程序01:wepy框架整合iview webapp UI
  • 这几个编码小技巧将令你 PHP 代码更加简洁
  • ​ 无限可能性的探索:Amazon Lightsail轻量应用服务器引领数字化时代创新发展
  • ​软考-高级-信息系统项目管理师教程 第四版【第14章-项目沟通管理-思维导图】​
  • ![CDATA[ ]] 是什么东东
  • #FPGA(基础知识)
  • #LLM入门|Prompt#2.3_对查询任务进行分类|意图分析_Classification
  • #单片机(TB6600驱动42步进电机)
  • (2)(2.4) TerraRanger Tower/Tower EVO(360度)
  • (AtCoder Beginner Contest 340) -- F - S = 1 -- 题解
  • (el-Transfer)操作(不使用 ts):Element-plus 中 Select 组件动态设置 options 值需求的解决过程
  • (react踩过的坑)Antd Select(设置了labelInValue)在FormItem中initialValue的问题
  • (Redis使用系列) Springboot 使用redis实现接口幂等性拦截 十一
  • (笔记)第三期书生·浦语大模型实战营(十一卷王场)--书生入门岛通关第1关Linux 基础知识
  • (分布式缓存)Redis分片集群
  • (分享)一个图片添加水印的小demo的页面,可自定义样式
  • (附源码)springboot 校园学生兼职系统 毕业设计 742122
  • (机器学习-深度学习快速入门)第三章机器学习-第二节:机器学习模型之线性回归
  • (一)kafka实战——kafka源码编译启动
  • (一)python发送HTTP 请求的两种方式(get和post )
  • (一)面试需要掌握的技巧
  • (转) SpringBoot:使用spring-boot-devtools进行热部署以及不生效的问题解决
  • (转)IIS6 ASP 0251超过响应缓冲区限制错误的解决方法
  • (转)树状数组
  • (最优化理论与方法)第二章最优化所需基础知识-第三节:重要凸集举例