Linux C语言串口通信与WebSocket数据上传实战项目
使用select()可以监听多个文件描述符的状态变化,适用于多路复用场景。// 有数据可读表格:select 与 poll 对比特性selectpoll最大文件描述符数1024无限制性能随文件描述符数增加而下降性能更优使用方式每次调用需重新设置集合可复用pollfd结构Linux预定义了一系列波特率常量,例如:常量波特率B300300 bpsB12001200 bps。
简介:在Linux系统中,串口通信是嵌入式和物联网开发的基础。本项目基于C语言实现串口的打开、配置与数据读写操作,并通过队列机制提升数据处理效率。项目还集成WebSocket协议,实现将串口接收的数据实时上传至服务器,具备完整的错误处理与多线程并发机制。适用于物联网设备通信、嵌入式系统数据上传等实际应用场景。 
1. Linux串口通信基础
Linux系统中,串口通信是设备间数据交互的重要方式,广泛应用于工业控制、物联网、嵌入式系统等领域。串口通信通过异步方式传输数据,具备结构简单、成本低廉、传输距离远等优点。其基本通信要素包括起始位、数据位、校验位和停止位,构成了串行数据帧的完整格式。
常见的串口标准如 RS-232 和 RS-485 在电气特性和通信距离上有显著区别:RS-232适用于短距离点对点通信,而RS-485支持多点通信和更长的传输距离,适合工业现场环境。理解这些标准及其适用场景是进行串口编程与系统集成的基础。
2. C语言串口设备文件操作(open/read/write)
在Linux系统中,串口通信的本质是通过文件操作接口实现的。由于Linux将所有设备都抽象为文件,因此对串口的操作本质上就是对设备文件的读写。本章将从C语言的角度深入探讨如何使用标准的文件操作函数(如 open 、 read 、 write )来控制串口设备,并结合实际代码示例展示串口通信的底层实现机制。
2.1 Linux文件操作基础
Linux系统中的文件操作是所有设备通信的基础,串口设备也不例外。理解文件描述符、 open 系统调用以及设备文件路径是掌握串口通信的第一步。
2.1.1 文件描述符与open系统调用
在Linux中,文件操作是通过文件描述符(File Descriptor)来实现的。文件描述符是一个非负整数,用于标识打开的文件或设备。通过 open() 系统调用可以获取一个文件描述符。
#include <fcntl.h>
#include <unistd.h>
int fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY | O_NDELAY);
代码解释:
"/dev/ttyS0":这是串口设备文件的路径,表示第一个串口。O_RDWR:以可读可写方式打开设备。O_NOCTTY:防止设备成为控制终端。O_NDELAY:非阻塞方式打开设备,避免程序在设备不可用时卡死。
逐行分析:
- 引入
fcntl.h是为了使用open函数。 - 调用
open(),传入设备路径和标志位。 - 返回值
fd即为文件描述符,后续操作都基于此值。
2.1.2 串口设备文件的路径与权限配置
Linux系统中常见的串口设备文件路径如下:
| 设备类型 | 路径名 | 描述 |
|---|---|---|
| RS-232 | /dev/ttyS0 | 第一个串口 |
| USB转串口 | /dev/ttyUSB0 | 第一个USB转串口设备 |
| Bluetooth串口 | /dev/rfcomm0 | 蓝牙串口设备 |
权限配置说明:
默认情况下,普通用户没有权限访问串口设备。需使用以下命令修改权限:
sudo chmod 666 /dev/ttyS0
或添加用户到 dialout 组:
sudo usermod -a -G dialout $USER
2.1.3 常见错误码与处理方式
使用 open() 打开串口时可能遇到以下常见错误码:
| 错误码 | 含义 | 处理方式 |
|---|---|---|
| EACCES | 权限不足 | 修改设备权限或添加用户组 |
| ENOENT | 设备文件不存在 | 检查设备路径或驱动是否加载 |
| EBUSY | 设备被其他程序占用 | 结束占用进程或更换设备路径 |
| EIO | I/O错误 | 检查硬件连接或更换设备 |
错误处理示例:
#include <errno.h>
#include <stdio.h>
if (fd == -1) {
perror("Failed to open serial port");
switch(errno) {
case EACCES:
printf("Error: Permission denied\n");
break;
case ENOENT:
printf("Error: Device file not found\n");
break;
default:
printf("Error code: %d\n", errno);
}
return -1;
}
2.2 串口设备的打开与关闭操作
正确地打开和关闭串口设备是确保通信稳定的基础。本节将详细介绍 open() 和 close() 的使用方法及其注意事项。
2.2.1 open函数的参数设置(O_RDWR、O_NOCTTY、O_NDELAY)
open() 的参数决定了串口的访问模式和行为:
| 标志位 | 说明 |
|---|---|
| O_RDONLY | 只读方式打开 |
| O_WRONLY | 只写方式打开 |
| O_RDWR | 读写方式打开 |
| O_NOCTTY | 不将设备设置为控制终端 |
| O_NDELAY | 非阻塞模式,立即返回 |
| O_SYNC | 同步写入,确保数据写入磁盘 |
示例代码:
int fd = open("/dev/ttyUSB0", O_RDWR | O_NOCTTY | O_NDELAY);
逻辑说明:
O_RDWR:确保可以同时进行读写。O_NOCTTY:避免串口被误设为控制终端,防止程序被中断信号干扰。O_NDELAY:非阻塞打开,避免在设备未连接时卡住。
2.2.2 串口设备的独占访问与阻塞控制
Linux允许串口设备被多个进程访问,但通常建议以独占方式使用串口以避免冲突。可以通过检查设备是否已被打开,或使用锁文件机制实现。
使用 fcntl 实现文件锁示例:
#include <fcntl.h>
struct flock lock;
lock.l_type = F_WRLCK; // 写锁
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
if (fcntl(fd, F_SETLK, &lock) == -1) {
perror("Failed to lock device");
close(fd);
return -1;
}
2.2.3 close函数的正确使用与资源释放
每次打开设备后,都必须使用 close() 关闭文件描述符,释放系统资源。
#include <unistd.h>
close(fd);
注意事项:
- 未关闭文件描述符会导致资源泄漏。
- 多线程环境下需确保在所有线程退出后再调用
close()。
2.3 数据的读取与写入操作
串口通信的核心是数据的读写。Linux中使用 read() 和 write() 函数完成串口的数据传输。
2.3.1 read与write函数的使用方式
#include <unistd.h>
char buffer[256];
int n = read(fd, buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n] = '\0'; // 添加字符串结束符
printf("Received: %s\n", buffer);
}
const char *msg = "Hello Serial\n";
write(fd, msg, strlen(msg));
逻辑说明:
read()从串口读取最多255字节数据。write()发送字符串到串口。n表示实际读取的字节数。
2.3.2 阻塞与非阻塞模式下的读写行为差异
| 模式 | read()行为 | write()行为 |
|---|---|---|
| 阻塞模式 | 无数据时等待 | 数据未完全写入时等待 |
| 非阻塞模式 | 无数据时立即返回0 | 数据未完全写入时返回已写入字节数 |
切换非阻塞模式:
fcntl(fd, F_SETFL, O_NONBLOCK);
2.3.3 实际读写数据长度的判断与处理
串口通信中,一次 read() 或 write() 操作不一定能完成全部数据的传输。因此需要进行循环处理:
int total_read = 0;
int bytes_to_read = 100;
char read_buf[100];
while (total_read < bytes_to_read) {
int n = read(fd, read_buf + total_read, bytes_to_read - total_read);
if (n <= 0) {
// 错误或超时处理
break;
}
total_read += n;
}
流程图:
graph TD
A[开始读取] --> B{是否读取完整?}
B -- 是 --> C[完成]
B -- 否 --> D[继续读取]
D --> B
2.4 串口通信的同步与异步处理
在实际应用中,串口通信可能需要处理并发或异步事件。Linux提供了 select 和 poll 机制来实现异步通信。
2.4.1 同步通信的实现方式
同步通信是指主线程等待串口数据到达后继续执行。适用于简单通信任务。
char buffer[256];
int n = read(fd, buffer, sizeof(buffer));
if (n > 0) {
// 处理数据
}
优点: 简单直观
缺点: 容易造成阻塞,影响程序响应
2.4.2 异步通信与select/poll机制简介
使用 select() 可以监听多个文件描述符的状态变化,适用于多路复用场景。
#include <sys/select.h>
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
int ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
if (ret > 0 && FD_ISSET(fd, &readfds)) {
// 有数据可读
read(fd, buffer, sizeof(buffer));
}
表格:select 与 poll 对比
| 特性 | select | poll |
|---|---|---|
| 最大文件描述符数 | 1024 | 无限制 |
| 性能 | 随文件描述符数增加而下降 | 性能更优 |
| 使用方式 | 每次调用需重新设置集合 | 可复用pollfd结构 |
2.4.3 串口数据读写的超时控制
在实际通信中,常常需要设置读写超时机制以避免无限等待。
struct timeval timeout;
timeout.tv_sec = 3;
timeout.tv_usec = 0;
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
注意事项:
setsockopt()通常用于网络套接字,对于串口需使用termios设置超时时间。- 更推荐使用
select()或poll()实现超时控制。
下一章预告: 在掌握了串口的基本文件操作后,我们将深入探讨如何使用
termios接口对串口通信的参数进行精确配置,包括波特率、数据位、校验位等关键参数。
3. termios串口参数配置(波特率、校验位等)
在Linux系统中,串口通信的配置不仅限于打开和读写设备文件,更重要的是对串口的通信参数进行精细控制。这些参数包括波特率、数据位、停止位、校验方式等,直接影响数据传输的稳定性和效率。 termios 结构体是Linux中用于配置终端(包括串口)行为的核心数据结构,它允许开发者对输入输出模式、控制选项、本地行为等进行灵活设置。
本章将深入解析 termios 结构体的组成与使用方式,详细讲解波特率、数据格式、通信模式等关键配置项的设置方法,并通过代码示例说明如何在C语言中进行串口参数配置,以满足不同应用场景的需求。
3.1 termios结构体详解
在Linux系统中, termios 结构体用于控制终端设备的输入输出行为。该结构体定义在头文件 <termios.h> 中,是串口编程中最核心的数据结构之一。
3.1.1 termios结构体的四个主要成员(c_iflag、c_oflag、c_cflag、c_lflag)
termios 结构体由四个主要标志字段组成:
| 成员字段 | 作用 |
|---|---|
c_iflag |
输入模式标志,控制输入数据的处理方式 |
c_oflag |
输出模式标志,控制输出数据的处理方式 |
c_cflag |
控制模式标志,设置波特率、字符大小、停止位、校验等 |
c_lflag |
本地模式标志,控制终端的本地行为(如回显、规范模式等) |
此外,还有一个用于控制特殊字符的数组 c_cc ,用于定义如 EOF、INTR、QUIT 等特殊字符的映射。
示例代码:初始化termios结构体
#include <termios.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int fd = open("/dev/ttyUSB0", O_RDWR | O_NOCTTY | O_NDELAY);
if (fd == -1) {
perror("open");
return -1;
}
struct termios options;
tcgetattr(fd, &options); // 获取当前终端属性
// 输出原始结构体中的标志值
printf("c_iflag: %x\n", options.c_iflag);
printf("c_oflag: %x\n", options.c_oflag);
printf("c_cflag: %x\n", options.c_cflag);
printf("c_lflag: %x\n", options.c_lflag);
close(fd);
return 0;
}
代码逻辑分析:
open()打开串口设备文件。tcgetattr()获取当前串口的属性配置。- 输出
termios结构体中的各字段值,用于查看当前配置状态。
3.1.2 输入、输出、控制、本地模式标志的含义与设置方法
输入模式标志(c_iflag)
用于控制输入数据的处理方式,常见的标志包括:
| 标志名 | 含义 |
|---|---|
IGNBRK |
忽略BREAK条件 |
BRKINT |
BREAK条件产生SIGINT信号 |
IGNPAR |
忽略帧错误和奇偶校验错误 |
PARMRK |
标记奇偶错误 |
INPCK |
启用奇偶校验 |
ISTRIP |
剥离第8位 |
IXON |
启用输出流控制(XON/XOFF) |
IXOFF |
启用输入流控制 |
输出模式标志(c_oflag)
控制输出数据的处理方式,常见标志:
| 标志名 | 含义 |
|---|---|
OPOST |
启用后处理 |
ONLCR |
将换行符转换为CR+LF |
OCRNL |
将CR转换为NL |
ONOCR |
不在列0输出CR |
ONLRET |
不输出CR |
控制模式标志(c_cflag)
控制波特率、字符大小、停止位、校验等:
| 标志名 | 含义 |
|---|---|
CBAUD |
波特率掩码 |
CSIZE |
字符大小掩码(CS5~CS8) |
CSTOPB |
使用两个停止位 |
PARENB |
启用奇偶校验 |
PARODD |
奇校验(否则为偶校验) |
HUPCL |
关闭时挂断 |
CLOCAL |
忽略调制解调器状态线 |
CREAD |
启用接收器 |
本地模式标志(c_lflag)
控制终端的本地行为:
| 标志名 | 含义 |
|---|---|
ISIG |
启用信号处理(如INTR、QUIT) |
ICANON |
启用规范模式(行缓冲) |
ECHO |
启用输入字符回显 |
ECHOE |
回显擦除字符 |
ECHOK |
回显KILL字符 |
NOFLSH |
禁用刷新输入/输出队列 |
3.1.3 特殊字符控制(如EOF、INTR等)
c_cc 数组用于定义特殊字符,如:
| 字符名 | 说明 |
|---|---|
VINTR |
中断字符(默认Ctrl+C) |
VQUIT |
退出字符(默认Ctrl+\) |
VERASE |
删除字符(默认Backspace) |
VKILL |
删除行(默认Ctrl+U) |
VEOF |
文件结束符(默认Ctrl+D) |
VTIME |
非规范模式下等待时间(单位:0.1秒) |
VMIN |
非规范模式下最小读取字符数 |
例如,设置 VEOF 为Ctrl+Z:
options.c_cc[VEOF] = 26; // ASCII码26对应Ctrl+Z
3.2 波特率的设置与串口速率匹配
波特率是串口通信中最重要的参数之一,表示每秒传输的比特数。Linux中通过 termios 结构体的 c_cflag 字段设置波特率。
3.2.1 波特率常量定义(B9600、B115200等)
Linux预定义了一系列波特率常量,例如:
| 常量 | 波特率 |
|---|---|
| B300 | 300 bps |
| B1200 | 1200 bps |
| B9600 | 9600 bps |
| B19200 | 19200 bps |
| B38400 | 38400 bps |
| B57600 | 57600 bps |
| B115200 | 115200 bps |
设置波特率的示例代码:
cfsetispeed(&options, B115200); // 设置输入波特率
cfsetospeed(&options, B115200); // 设置输出波特率
3.2.2 自定义波特率的实现方式
在某些特殊设备中,可能需要设置非标准波特率。可以通过 cfsetspeed() 函数或直接修改 c_cflag 字段实现。
示例:设置自定义波特率
options.c_cflag &= ~CBAUD; // 清除波特率掩码
options.c_cflag |= B38400 | B57600; // 设置自定义波特率组合(部分平台支持)
⚠️ 注意:并非所有系统和硬件都支持自定义波特率,需查阅具体设备手册或驱动支持情况。
3.2.3 波特率设置的注意事项与常见错误
- 波特率必须一致 :发送端与接收端的波特率必须完全一致,否则会导致数据乱码。
- 波特率过高 :可能超出设备支持范围,导致通信失败。
- 波特率设置失败 :检查是否正确调用
cfsetispeed()和cfsetospeed(),并使用tcsetattr()更新配置。
3.3 数据格式的配置(数据位、停止位、校验位)
串口通信的数据格式由数据位、停止位和校验位组成,决定了数据帧的结构。
3.3.1 CSIZE位宽设置(CS5~CS8)
使用 CSIZE 宏配合 CS5 ~ CS8 设置数据位长度。
示例代码:
options.c_cflag &= ~CSIZE; // 清除数据位掩码
options.c_cflag |= CS8; // 设置8位数据位
3.3.2 停止位设置(CSTOPB)
CSTOPB 标志用于设置停止位:
- 未设置:1个停止位
- 设置:2个停止位
options.c_cflag &= ~CSTOPB; // 1个停止位
// options.c_cflag |= CSTOPB; // 2个停止位
3.3.3 校验位的启用与类型选择(PARODD、PARENB)
启用校验位需设置 PARENB ,奇校验则加 PARODD 。
示例代码:
options.c_cflag |= PARENB; // 启用校验位
options.c_cflag |= PARODD; // 奇校验(偶校验则不加)
⚠️ 注意:启用校验位时,数据位应为7位(CS7),否则可能导致通信失败。
3.4 串口通信模式的切换(原始模式与规范模式)
Linux串口通信支持两种主要模式:规范模式(Canonical Mode)和原始模式(Raw Mode)。
3.4.1 规范模式下的行缓冲机制
规范模式下,输入数据以行为单位进行处理,直到遇到换行符( \n )或EOF(Ctrl+D)才会返回数据。适合交互式终端操作。
设置规范模式:
options.c_lflag |= ICANON; // 启用规范模式
3.4.2 原始模式的配置方法与数据处理特点
原始模式下,输入数据立即返回,不进行任何处理,适用于串口通信等实时数据传输。
设置原始模式:
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // 禁用规范模式、回显、信号处理
options.c_oflag &= ~OPOST; // 禁用输出处理
3.4.3 屏蔽回显与控制字符处理
在串口通信中,通常不希望看到回显和控制字符干扰数据流。可通过以下方式禁用:
options.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL); // 禁用回显
options.c_lflag &= ~ICANON; // 禁用规范模式
options.c_lflag &= ~ISIG; // 禁用信号字符(如Ctrl+C)
总结与流程图
下面通过Mermaid流程图总结串口参数配置的基本流程:
graph TD
A[打开串口设备] --> B[获取termios结构]
B --> C[设置波特率]
C --> D[设置数据格式]
D --> E[设置通信模式]
E --> F[更新串口属性]
F --> G[进行数据读写]
通过本章的学习,读者应掌握如何使用 termios 结构体配置串口的关键参数,并能够在实际项目中灵活应用,为后续章节中的队列管理和多线程通信打下坚实基础。
4. 串口数据队列管理(queue.h实现FIFO)
4.1 队列管理在串口通信中的作用
4.1.1 数据缓冲的必要性与队列结构设计
在串口通信中,数据的发送和接收通常是以异步方式进行的。由于串口通信速率较低、数据包到达时间不规律,若不进行缓冲处理,很容易造成数据丢失或覆盖。此时,引入队列结构作为中间缓冲机制,能够有效解决这一问题。
一个典型的队列设计包括入队(enqueue)和出队(dequeue)操作。队列可以是固定大小的数组结构,也可以是链表结构,根据应用场景选择不同的实现方式。
在Linux环境下,队列的实现可以借助于 queue.h 头文件提供的宏定义,例如 TAILQ (Tail Queue)结构,它支持高效的链表操作,并且易于在多线程环境中使用。
4.1.2 队列在多线程通信中的协调作用
当串口通信被封装在独立的线程中时,主程序或其它线程可能需要访问这些数据。使用队列作为线程间通信的桥梁,可以避免直接操作共享内存带来的同步问题。
例如,在一个典型的串口数据采集系统中,一个线程负责从串口读取数据并放入队列,另一个线程则从队列中取出数据进行处理或转发。通过队列的中间缓冲,两个线程无需频繁地等待对方,从而提高系统吞吐量和响应速度。
4.1.3 FIFO与LIFO结构的适用场景对比
FIFO(First In First Out)与LIFO(Last In First Out)是两种常见的数据结构。在串口通信中,数据是按照到达顺序处理的,因此FIFO结构更为合适。
| 结构类型 | 特点 | 适用场景 |
|---|---|---|
| FIFO | 先入先出,顺序处理 | 串口数据缓存、消息队列 |
| LIFO | 后入先出,栈式处理 | 函数调用栈、命令回退机制 |
在串口应用中,如果采用LIFO结构,可能导致后到达的数据被优先处理,破坏数据的时序性。因此,FIFO结构是更合理的选择。
4.2 使用queue.h头文件实现链式队列
4.2.1 queue.h中TAILQ宏的使用方法
Linux内核提供的 queue.h 头文件定义了多种链表结构宏,其中 TAILQ 是最常用于队列管理的宏集合。它提供了队列的初始化、插入、删除等基本操作。
主要宏定义如下:
TAILQ_HEAD(name, type):定义队列头部结构TAILQ_ENTRY(type):在结构体中添加队列节点链接字段TAILQ_INIT(head):初始化队列TAILQ_INSERT_TAIL(head, elm, field):尾部插入元素TAILQ_REMOVE(head, elm, field):删除指定元素TAILQ_FOREACH(var, head, field):遍历队列
4.2.2 队列节点结构体的定义与初始化
为了使用 TAILQ 宏,首先需要定义一个节点结构体,并在其中包含 TAILQ_ENTRY 字段:
#include <sys/queue.h>
typedef struct data_node {
char data[256]; // 假设每个节点存储最多256字节数据
TAILQ_ENTRY(data_node) entries; // 队列链接字段
} DataNode;
接下来定义队列头部结构:
TAILQ_HEAD(data_queue_head, data_node);
初始化队列:
struct data_queue_head head;
TAILQ_INIT(&head);
4.2.3 入队与出队操作的实现与线程安全性
入队操作示例:
DataNode *node = (DataNode *)malloc(sizeof(DataNode));
strcpy(node->data, "Hello Serial");
TAILQ_INSERT_TAIL(&head, node, entries); // 插入到队列尾部
出队操作示例:
DataNode *first = TAILQ_FIRST(&head);
if (first) {
printf("Dequeued data: %s\n", first->data);
TAILQ_REMOVE(&head, first, entries); // 从队列中移除
free(first); // 释放内存
}
逻辑分析与参数说明:
TAILQ_INSERT_TAIL():将节点插入到队列尾部,保证FIFO顺序。TAILQ_FIRST():获取队列的第一个节点。TAILQ_REMOVE():从队列中移除指定节点,注意必须传入正确的field字段名。
⚠️注意:以上操作在单线程下是安全的,但在多线程环境下,必须配合互斥锁使用,以防止数据竞争。
4.3 队列的同步与并发控制
4.3.1 互斥锁(pthread_mutex_t)在队列操作中的应用
在多线程环境中,多个线程可能同时访问队列,导致数据竞争。为了解决这个问题,可以使用互斥锁( pthread_mutex_t )来保护队列操作。
示例代码如下:
#include <pthread.h>
pthread_mutex_t queue_mutex = PTHREAD_MUTEX_INITIALIZER;
void enqueue_safe(struct data_queue_head *head, DataNode *node) {
pthread_mutex_lock(&queue_mutex);
TAILQ_INSERT_TAIL(head, node, entries);
pthread_mutex_unlock(&queue_mutex);
}
DataNode* dequeue_safe(struct data_queue_head *head) {
pthread_mutex_lock(&queue_mutex);
DataNode *node = TAILQ_FIRST(head);
if (node) {
TAILQ_REMOVE(head, node, entries);
}
pthread_mutex_unlock(&queue_mutex);
return node;
}
逻辑分析:
pthread_mutex_lock():锁定互斥锁,防止其他线程同时操作队列。pthread_mutex_unlock():释放互斥锁,允许其他线程继续操作。
该方法可以确保队列的线程安全,但可能会引入性能瓶颈,特别是在高并发场景下。
4.3.2 条件变量(pthread_cond_t)实现的等待/通知机制
为了提高队列的并发效率,可以在队列为空时让线程等待,直到有新数据到来。此时可以使用条件变量配合互斥锁实现。
pthread_cond_t queue_not_empty = PTHREAD_COND_INITIALIZER;
DataNode* dequeue_with_wait(struct data_queue_head *head) {
pthread_mutex_lock(&queue_mutex);
while (TAILQ_EMPTY(head)) {
pthread_cond_wait(&queue_not_empty, &queue_mutex); // 等待数据
}
DataNode *node = TAILQ_FIRST(head);
TAILQ_REMOVE(head, node, entries);
pthread_mutex_unlock(&queue_mutex);
return node;
}
void enqueue_with_signal(struct data_queue_head *head, DataNode *node) {
pthread_mutex_lock(&queue_mutex);
TAILQ_INSERT_TAIL(head, node, entries);
pthread_cond_signal(&queue_not_empty); // 通知等待线程
pthread_mutex_unlock(&queue_mutex);
}
逻辑分析:
pthread_cond_wait():释放锁并等待条件满足,当被唤醒时重新获取锁。pthread_cond_signal():唤醒一个等待的线程。
这种方式避免了忙等待,提高了线程效率。
4.3.3 多线程环境下队列的性能优化策略
在高并发场景下,以下策略可用于优化队列性能:
- 使用无锁队列 :如CAS(Compare and Swap)实现的环形缓冲区,避免锁的开销。
- 限制队列长度 :设置最大容量,防止内存溢出。
- 使用读写锁 :允许并发读取,提高读操作性能。
- 批量操作 :将多个数据一次性入队/出队,减少锁竞争。
⚙️建议:在实际开发中,结合性能测试选择最优方案。
4.4 队列的异常处理与内存管理
4.4.1 内存泄漏预防与节点释放机制
在使用动态内存分配的队列中,必须确保每个节点在出队后被正确释放,否则将导致内存泄漏。
DataNode *node = dequeue_safe(&head);
if (node) {
free(node); // 必须手动释放
}
建议封装一个队列销毁函数,释放所有节点:
void destroy_queue(struct data_queue_head *head) {
DataNode *node;
while ((node = TAILQ_FIRST(head)) != NULL) {
TAILQ_REMOVE(head, node, entries);
free(node);
}
}
4.4.2 队列满溢与空读的处理逻辑
当队列已满时,继续入队可能导致数据丢失或程序崩溃。因此,应在入队前检查队列状态:
#define MAX_QUEUE_SIZE 100
int queue_size = 0;
void enqueue_with_check(struct data_queue_head *head, DataNode *node) {
pthread_mutex_lock(&queue_mutex);
if (queue_size >= MAX_QUEUE_SIZE) {
printf("Queue full, dropping data\n");
free(node);
} else {
TAILQ_INSERT_TAIL(head, node, entries);
queue_size++;
}
pthread_mutex_unlock(&queue_mutex);
}
DataNode* dequeue_with_check(struct data_queue_head *head) {
pthread_mutex_lock(&queue_mutex);
if (TAILQ_EMPTY(head)) {
pthread_mutex_unlock(&queue_mutex);
return NULL;
}
DataNode *node = TAILQ_FIRST(head);
TAILQ_REMOVE(head, node, entries);
queue_size--;
pthread_mutex_unlock(&queue_mutex);
return node;
}
4.4.3 队列大小的动态调整与限制策略
为了适应不同负载场景,可以实现动态调整队列大小的功能。例如,当系统负载高时,自动增加队列容量;当负载低时减少容量,释放内存。
graph TD
A[开始] --> B{队列是否满?}
B -->|是| C[尝试扩容]
B -->|否| D[正常入队]
C --> E{扩容是否成功?}
E -->|是| D
E -->|否| F[丢弃数据或返回错误]
D --> G[结束]
该策略通过监控队列使用情况,动态调整资源,提升系统适应性和稳定性。
5. 多线程并发处理(串口与WebSocket通信)
在现代工业控制和物联网应用中,串口通信往往需要与网络通信(如WebSocket)结合使用,实现远程数据采集与控制。为了提升系统并发处理能力与响应效率,采用多线程机制来分别处理串口通信与网络通信是常见的架构设计。本章将深入探讨如何使用C语言结合 pthread 库与 libwebsockets 库,实现串口与WebSocket的多线程协同处理。
5.1 多线程编程基础
5.1.1 pthread库的基本使用方法
POSIX线程库( pthread )是Linux下标准的线程处理接口,支持线程的创建、同步、互斥与销毁等操作。核心函数包括:
pthread_create():创建线程pthread_join():等待线程结束pthread_mutex_lock/unlock():互斥锁操作pthread_cond_wait():条件变量等待
5.1.2 线程的创建与销毁
线程创建的基本示例代码如下:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void* thread_func(void* arg) {
printf("Thread is running...\n");
return NULL;
}
int main() {
pthread_t tid;
int ret = pthread_create(&tid, NULL, thread_func, NULL);
if (ret != 0) {
perror("pthread_create");
exit(EXIT_FAILURE);
}
pthread_join(tid, NULL); // 等待线程结束
printf("Thread exited.\n");
return 0;
}
5.1.3 线程间通信与数据共享机制
线程之间通常通过共享内存进行通信。例如,定义一个全局结构体变量用于保存串口数据,并在采集线程中更新、在通信线程中读取。
typedef struct {
char data[256];
size_t len;
pthread_mutex_t lock;
} shared_data_t;
shared_data_t g_serial_data = {0};
pthread_mutex_init(&g_serial_data.lock, NULL);
5.2 串口数据采集线程的设计与实现
5.2.1 串口数据采集线程的生命周期管理
串口采集线程通常是一个循环线程,持续读取串口数据并缓存至共享队列中。其生命周期包括:
- 初始化串口设备(open + termios配置)
- 进入循环读取数据
- 检测退出条件(如标志位)
void* serial_reader_thread(void* arg) {
int fd = open_serial_port("/dev/ttyUSB0", B115200);
if (fd < 0) {
perror("open_serial_port");
return NULL;
}
char buffer[256];
while (!g_stop_flag) {
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read > 0) {
pthread_mutex_lock(&g_serial_data.lock);
memcpy(g_serial_data.data, buffer, bytes_read);
g_serial_data.len = bytes_read;
pthread_mutex_unlock(&g_serial_data.lock);
// 可通知WebSocket线程有新数据
}
}
close(fd);
return NULL;
}
5.2.2 数据采集线程与主控线程的交互
主控线程通过设置 g_stop_flag 标志来通知采集线程终止运行:
volatile int g_stop_flag = 0;
void signal_handler(int sig) {
g_stop_flag = 1;
}
5.2.3 采集频率与数据丢失的控制策略
采集频率可通过 usleep() 或定时器控制,避免过快采集导致资源浪费或数据丢失。建议设置合理缓冲区,并使用队列结构缓存数据。
5.3 WebSocket通信线程的设计与实现
5.3.1 libwebsockets库的基本使用流程
libwebsockets 是一个高性能的WebSocket库,支持客户端与服务端通信。基本流程如下:
- 初始化上下文
- 创建客户端或服务端协议
- 主循环中处理事件
- 发送与接收消息
5.3.2 客户端与服务器端的连接建立与维护
客户端连接示例代码(简化版):
struct lws_client_connect_info info;
memset(&info, 0, sizeof(info));
info.context = context;
info.address = "192.168.1.100";
info.port = 8080;
info.path = "/";
info.host = info.address;
info.origin = "origin";
info.protocol = protocols[0].name;
struct lws* wsi = lws_client_connect_via_info(&info);
if (!wsi) {
fprintf(stderr, "Failed to connect to server\n");
}
5.3.3 消息收发机制与异步处理
WebSocket通信线程通常在一个循环中等待事件并处理数据:
while (!g_stop_flag) {
lws_service(context, 50);
}
发送数据可通过如下方式:
unsigned char buf[LWS_PRE + 256];
unsigned char *p = &buf[LWS_PRE];
size_t len = prepare_data(p, sizeof(buf) - LWS_PRE);
lws_write(wsi, p, len, LWS_WRITE_TEXT);
5.4 多线程协同与资源调度
5.4.1 串口采集与WebSocket上传的协同流程
协同流程如下图所示(mermaid格式):
sequenceDiagram
participant SerialThread
participant MainThread
participant WebSocketThread
participant Server
MainThread->>SerialThread: 启动采集线程
MainThread->>WebSocketThread: 启动WebSocket线程
SerialThread->>SerialThread: 循环读取串口数据
SerialThread->>SharedQueue: 存入数据
WebSocketThread->>SharedQueue: 提取数据
WebSocketThread->>Server: 发送数据
MainThread->>WebSocketThread: 接收控制指令
MainThread->>SerialThread: 发送控制指令
5.4.2 CPU资源的合理分配与调度优先级
通过 pthread_setschedparam() 可设置线程优先级:
struct sched_param param;
param.sched_priority = 10;
pthread_setschedparam(tid, SCHED_FIFO, ¶m);
建议为采集线程设置较高优先级以保证实时性,WebSocket线程可适当降低优先级。
5.4.3 多线程程序的调试与性能优化方法
- 调试工具 :使用
gdb多线程调试、strace跟踪系统调用 - 日志记录 :添加线程ID、时间戳、操作类型等信息
- 性能分析 :使用
perf或valgrind进行性能瓶颈分析 - 内存管理 :注意线程间共享结构体的生命周期管理,避免内存泄漏
本章内容展示了多线程如何在串口通信与WebSocket通信中协同工作,通过合理设计线程结构与资源共享机制,可以构建高效、稳定的数据采集与传输系统。
简介:在Linux系统中,串口通信是嵌入式和物联网开发的基础。本项目基于C语言实现串口的打开、配置与数据读写操作,并通过队列机制提升数据处理效率。项目还集成WebSocket协议,实现将串口接收的数据实时上传至服务器,具备完整的错误处理与多线程并发机制。适用于物联网设备通信、嵌入式系统数据上传等实际应用场景。
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐

所有评论(0)