23、嵌入式与实时应用中的 POSIX 线程及消息队列

嵌入式与实时应用中的 POSIX 线程及消息队列

1. 条件变量概述

条件变量是多线程编程中用于线程间同步的重要工具。以下是条件变量的基本操作函数:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init (pthread_cond_t *cond, const pthread_condattr_t *cond_attr);
int pthread_cond_destroy (pthread_cond_t *cond);
int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mutex,
const struct timespec *abstime);
int pthread_cond_signal (pthread_cond_t *cond);
int pthread_cond_broadcast (pthread_cond_t *cond);

条件变量的基本操作是信号(signal)和等待(wait)。信号操作会唤醒一个等待在该条件变量上的线程,线程唤醒的顺序取决于调度策略。线程也可以执行定时等待操作,如果在指定的时间间隔内条件变量未收到信号,等待操作将返回错误。此外,线程还可以广播一个条件,这会唤醒所有等待在该条件变量上的线程。

1.1 条件变量属性

Pthreads 并未为条件变量定义任何必需的属性,但至少有一个可选属性。RTAI Pthreads 提供了初始化和销毁条件变量属性对象的函数,但未实现该可选属性。

2. 用户空间中的 Pthreads

2.1 数据采集应用示例

为了练习在用户空间中使用 Pthreads,我们重新实现了一个四通道模拟数据采集应用。每个通道生成一个指定幅度的简单锯齿波,采集到的数据将显示在屏幕上,而不是写入文件。与之前的实现不同的是,现在每个通道都有自己的线程,而不是由一个任务管理所有四个通道。这样做的动机是,只管理一个通道的线程可能比管理“n”个通道的线程更简单。

系统架构如下:

graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    A(Command):::process -->|命令| B(Ch 0):::process
    A -->|命令| C(Ch 1):::process
    A -->|命令| D(Ch 2):::process
    A -->|命令| E(Ch 3):::process
    B -->|数据| F(data_point_t):::process
    C -->|数据| F
    D -->|数据| F
    E -->|数据| F
    F -->|数据| G(Display):::process

2.2 代码分析

2.2.1 数据结构

data_acq.c 文件中,靠近顶部的第 21 行有一个 channel_t 数据结构数组。查看 data_acq.h 文件,会发现 channel_t 中的许多字段已被移除,因为它们现在可以作为 data_acq() 线程内的局部变量。同时,为了支持 Pthreads,添加了其他字段。注意, data_point_t 类型定义中包含一个 #ifdef 来指定时间戳的替代定义,这是因为在内核空间中处理时间的方式略有不同。

2.2.2 main() 函数

main() 函数从第 166 行开始,它首先使用 channel[] 数组初始化四个数据采集通道。对于每个通道,创建一个互斥锁、一个条件变量和一个线程,并将通道的 channel_t 结构作为参数传递给线程。

接下来,初始化显示,使用 Curses 库。然后创建 Command() 线程,用于监控键盘的操作员输入。最后, main() 调用 Display() 函数,该函数成为主线程,等待通道线程的数据并显示它。

2.2.3 Display() 函数

Display() 函数从第 146 行开始,这是我们使用互斥锁和条件变量的第一个示例。显示线程锁定与显示数据结构关联的互斥锁( m_display ),并等待相应的条件变量( c_display )。 pthread_cond_wait() 函数不会返回,直到其他线程(即通道线程之一)发出信号,表示已将数据放入显示结构中。唤醒后,显示线程将数据复制到局部变量,然后解锁互斥锁。

2.2.4 data_acq() 函数

data_acq() 函数从第 26 行开始,首先从作为参数传递的 channel_t 结构中复制一些字段,这主要是为了提高代码的可读性。我们保持相同的约定,即采样周期为零表示通道被禁用。

data_acq() 线程有两种唤醒情况:
- 正在采样且采样周期已过期。
- 收到 Command() 线程的消息,要求更改其采样参数之一。

data_acq() 通过 channel_t 结构中的缓冲区接收 Command() 线程的消息。由于该缓冲区由两个线程访问,因此必须使用互斥锁进行保护。在访问消息缓冲区之前,我们锁定 channel_t 结构中关联的互斥锁。但在 Command() 线程发送消息之前,尝试读取消息缓冲区是没有意义的,因此我们等待 channel_t 中的条件变量,让 Command() 线程在有变化时发出信号。

如果通道未采样( sample_period == 0 ),则可以直接调用 pthread_cond_wait() ,直到操作员决定打开该通道。如果通道正在采样,我们可以调用 pthread_cond_timedwait() ,将采样周期作为超时值。当它返回时,状态值将指示调用是否超时( status == ETIMEOUT )或是否有人发出信号( status == 0 )。

pthread_cond_timedwait() 唤醒后,我们需要计算下一次唤醒时间,因为定时等待操作使用绝对时间作为超时参数。因此,我们将采样周期转换为纳秒,并添加到超时参数中。

如果状态指示超时,我们对消息缓冲区不感兴趣,因此可以立即解锁互斥锁。然后,我们需要独占访问显示数据结构,因此锁定其互斥锁,将相关信息复制到显示结构中,发出其条件变量的信号,然后解锁互斥锁。

如果状态为零, Command() 线程已将消息放入消息缓冲区。在这种情况下,我们在处理消息时保持互斥锁锁定。这里的主要复杂性在于,如果消息更改了采样周期,我们必须计算新的唤醒时间。

2.2.5 Command() 线程

Command() 线程从第 108 行开始, getstr() 函数会阻塞线程,直到操作员按下 <Enter> 键。返回后,我们解析命令字符串。命令语法类似于之前在恒温器中添加可编程参数的操作,有四个有效命令:
- 'c' – 通道。 Command() 线程内部处理此命令,后续命令将指向此通道,直到通道值更改。
- 'q' – 退出。停止所有操作。调用 exit() 函数会取消所有线程,这可能不是停止系统的最干净方式。更干净的解决方案是让 Command() 线程通知其他线程是时候退出,并让它们自行终止。
- 'r' – 范围。此通道生成数据的最大值。
- 's' – 采样周期。以毫秒为单位设置当前通道的采样周期。

为了方便起见, Command() 线程不进行错误或合理性检查。除了 'c' 'q' (注意区分大小写),所有感知到的命令令牌都将传递给当前通道,并且不检查通道值是否在范围内。键盘输入不会回显,因为这需要在两个线程之间共享显示,以便 Command() 线程可以将光标移动到命令行字段,并在输入每个字符时恢复它,这反过来又要求显示由互斥锁保护。

2.3 编译和运行

查看 Makefile ,忽略注释部分。 data_acq 的编译命令中使用 -l 选项添加了两个库: pthread 库包含 Pthreads 函数, curses 是 Curses 库。此外,还有一个编译时符号 _REENTRANT ,其作用是提醒编译器使用可重入版本的函数,以确保线程安全。

操作步骤如下:
1. 执行 make data_acq 编译程序。
2. 运行程序。
3. 输入命令 c 0 s 500 启动通道 0 的采样。
4. 输入类似的命令启动其他通道的采样。

2.4 多线程程序调试

在用户空间中运行 Pthreads 的好处是可以使用 DDD/GDB 调试程序。多线程会给调试过程带来一些复杂性,但幸运的是,GDB 提供了处理这些问题的功能。

调试步骤如下:
1. 在 DDD 下运行 data_acq ,并在 main() 函数开头附近的 channel[i].number = i; 行设置初始断点。
2. 在运行程序之前,执行 View->Execution Window 打开一个单独的窗口,用于程序输出,因为 Curses 输出在 GDB 控制台窗口中效果不佳。
3. 运行程序,当程序在断点处停止时,在 GDB 控制台窗口中会看到以下消息:

[New Thread 1024 (runnable)]
[Switching to Thread 1024 (runnable)]

这表明 GDB 已经识别到正在运行多线程环境。
4. 继续运行程序,当再次在断点处停止时,会看到另外两条不同编号的“New Thread”消息。其中一条是由 for 循环创建的通道线程,另一条是 Pthreads 为自身使用创建的线程。
5. 删除断点,让程序继续运行到显示循环顶部的 while (1) 语句。此时,所有线程都已创建。执行 Status->Threads ,会看到所有线程及其当前执行点的列表,当前执行的线程会高亮显示。注意,四个通道线程都处于相同的执行点。
6. 执行 Status->Backtrace 显示当前线程的调用栈,这表明我们在 main() 函数中,并且 main() 是从 __libc_start_main() 调用的。选择一个位于 sigsuspend.c:48 的线程,Backtrace 显示会立即更改,显示该线程的调用栈。因此,可以将任何线程设置为“活动线程”并查看其状态。
7. 如果在 data_acq() 函数中设置断点,当执行 data_acq() 的四个线程中的任何一个到达该点时,程序将停止,此时可以检查该线程的局部变量。但请注意,只有当前执行线程的局部变量是可见的。

3. 迁移到内核空间

3.1 代码修改

现在需要重新构建 data_acq.c 以使其在 RTAI Pthreads 下运行,具体操作如下:
1. 将 data_acq.c 复制到 data_acq_rt.c
2. main() 函数转变为 init_module() 函数。除了创建 Command() 线程外,现在还必须显式创建 Display() 线程。将 printf() 替换为 printk() ,并创建一个空的 cleanup_module() 函数。
3. 由于在内核空间中无法进行屏幕 I/O 操作,且无法访问 Curses 库,因此需要将 Display() Command() 的实际功能在内核空间和用户空间之间进行拆分。可以使用 RTAI FIFOs 在用户空间线程和在内核空间运行的“伙伴”线程之间进行通信。从 main()/init_module() 中移除 Curses 初始化调用,并用两个 rtf_create() 调用来代替。 data_acq.h 文件中包含了两个 FIFOs 的 #define 定义。

3.2 内核空间函数修改

  • Display() 函数 :内核空间的 Display() 函数只需将接收到的数据点复制到一个 FIFO 中。
  • Command() 函数 :将 Command() 函数中的 getstr() 调用替换为 rtf_get() 调用。
  • data_acq() 函数 :由于 ftime() 函数在内核空间中不可用,需要使用替代方法来读取时间。RTAI Pthreads 定义了以下函数:
void clock_gettime (int clock_id, struct timespec *current_time);

该函数似乎不是 Posix 标准的一部分,在可访问的文档中也未提及。 struct timespec time.h 中定义,有两个 long int 字段:
- tv_sec :自 1970 年 1 月 1 日午夜以来的秒数。
- tv_nsec :自 tv_sec 以来的纳秒数。
这与 data_acq.c 使用的 timeb 结构几乎相同。 clock_id 实际上是一个枚举,其唯一有效的值是 CLOCK_REALTIME 。需要将 data_acq() 函数中的两个 ftime() 调用替换为 clock_gettime() 调用。

3.3 头文件处理

RTAI Pthreads 有自己的 pthread.h 版本,名为 rtai_pthread.h ,但它不在 /usr/src/rtai/include 目录中,而是在 /usr/src/rtai/posix/include 目录中。有以下几种处理方式:
- 指定完整路径 :在 #include 指令中指定完整路径,但通常不建议这样做。
- 添加到包含路径 :将 /usr/src/rtai/posix/include 添加到编译器标志的包含路径中。
- 复制文件 :将 rtai_pthread.h 复制到 /usr/src/rtai/include 目录中。

3.4 内核模块构建与加载

logger.c 文件实现了 Command() Display() 在用户空间的“伙伴”线程,它展示了需要从 data_acq_rt.c 中移出的功能。

打开 Makefile ,删除用 # 注释的部分,该部分用于构建内核模块 data_acq_rt.o ,然后再次运行 make

data_acq_rt.o 需要以下模块:
| 模块名称 | 说明 |
| ---- | ---- |
| rtai | 基础 RTAI 模块 |
| rtai_sched | 调度相关模块 |
| rtai_utils | 为 pthread 提供实用函数 |
| rtai_pthread | RTAI 的 Pthreads 模块 |
| rtai_fifos | RTAI 的 FIFO 模块 |

加载这些模块的最简单方法是执行以下命令:

modprobe rtai_pthread
insmod rtai_fifos
insmod ./data_acq_rt.o

rtai_pthread rtai_fifos 相互独立,因此可以在使用 modprobe 加载一个模块后,使用 insmod 加载另一个模块,当然第二个模块也可以使用 modprobe 加载。

然后运行 ./logger ,如果一切正常,应该能够输入 c 0 s 500 并看到通道 0 开始“采集数据”。如果出现问题,可以在 data_acq_rt.c 的关键位置添加 rt_printk() 语句来查看具体情况。

4. 消息队列

4.1 概述

消息队列是 POSIX 标准中可选实时扩展的一部分,标准 Linux(至少在 2.4 内核版本中)不包含实时扩展,但 RTAI 实现了 Posix 消息队列。相应的头文件是 rtai_pqueue.h ,与 rtai_pthread.h 一样,它位于 /usr/src/rtai/posix/include 目录中,而不是 /usr/src/rtai/include 目录中。

消息队列与 RTAI FIFOs 非常相似,但它们是全双工的,并且处理离散消息而不是连续的字节流。不幸的是,它们仅适用于内核空间中的任务。

4.2 消息队列 API

消息队列的 API 相对简单直接,以下是相关函数:

mqd_t mq_open (char *mq_name, int oflags, mode_t permissions, struct mq_attr *mq_attr);
int mq_close (mqd_t mq);
int mq_unlink (char *mq_name);
size_t mq_receive (mqd_t mq, char *msg_buffer, size_t buflen, unsigned int *msgprio);
int mq_send (mqd_t mq, const char *msg, size_t msglen, unsigned int msgprio);
int mq_notify (mqd_t mq, const struct sigevent *notification);
int mq_getattr (mqd_t mq, struct mq_attr *attrbuf);
int mq_setattr (mqd_t mq, const struct mq_attr *new_attrs, struct mq_attr *old_attrs);

4.3 消息队列操作流程

graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    A(打开消息队列 mq_open):::process --> B(发送或接收消息 mq_send/mq_receive):::process
    B --> C{是否需要关闭}:::process
    C -->|是| D(关闭消息队列 mq_close):::process
    C -->|否| B
    D --> E{是否需要销毁}:::process
    E -->|是| F(删除消息队列 mq_unlink):::process
    E -->|否| G(结束):::process

4.4 详细操作说明

  • 打开消息队列 :使用 mq_open() 函数打开消息队列,需要指定队列名称、访问模式和权限,还可以包含一个属性结构。打开消息队列类似于打开文件。
  • 关闭消息队列 mq_close() 函数关闭消息队列,但不会销毁它,只是断开与队列的链接,队列及其包含的任何消息仍然可供其他链接和其他线程使用。
  • 删除消息队列 mq_unlink() 函数销毁消息队列并释放其资源。如果在调用 mq_unlink() 时队列还有打开的链接,队列将被标记为在最后一个链接关闭时销毁。一旦队列被标记为销毁,就不能再打开新的链接。
  • 接收和发送消息 mq_receive() mq_send() 函数用于在队列上进行接收和发送操作。除非在打开链接时设置了 O_NONBLOCK 标志,否则这两个操作都是阻塞的。消息按照优先级顺序放入队列,即高优先级消息将插入到已有的低优先级消息之前,相同优先级的消息按照 FIFO 顺序排队。
  • 异步通知 mq_notify() 函数允许线程指定一个异步回调函数,当有消息到达队列时调用。 struct sigevent 结构包含要调用的函数指针等信息。RTAI 实现了 mq_notify() 函数以保证完整性,但实际上不进行异步通知。

综上所述,通过对用户空间和内核空间的 Pthreads 应用以及消息队列的介绍,我们可以看到在嵌入式与实时应用中如何利用这些技术实现多线程编程和线程间通信,从而构建高效、稳定的系统。

内容概要:本文设计了一种基于PLC的全自动洗衣机控制系统内容概要:本文设计了一种,采用三菱FX基于PLC的全自动洗衣机控制系统,采用3U-32MT型PLC作为三菱FX3U核心控制器,替代传统继-32MT电器控制方式,提升了型PLC作为系统的稳定性自动化核心控制器,替代水平。系统具备传统继电器控制方式高/低水,实现洗衣机工作位选择、柔和过程的自动化控制/标准洗衣模式切换。系统具备高、暂停加衣、低水位选择、手动脱水及和柔和、标准两种蜂鸣提示等功能洗衣模式,支持,通过GX Works2软件编写梯形图程序,实现进洗衣过程中暂停添加水、洗涤、排水衣物,并增加了手动脱水功能和、脱水等工序蜂鸣器提示的自动循环控制功能,提升了使用的,并引入MCGS组便捷性灵活性态软件实现人机交互界面监控。控制系统通过GX。硬件设计包括 Works2软件进行主电路、PLC接梯形图编程线关键元,完成了启动、进水器件选型,软件、正反转洗涤部分完成I/O分配、排水、脱、逻辑流程规划水等工序的逻辑及各功能模块梯设计,并实现了大形图编程。循环小循环的嵌; 适合人群:自动化套控制流程。此外、电气工程及相关,还利用MCGS组态软件构建专业本科学生,具备PL了人机交互C基础知识和梯界面,实现对洗衣机形图编程能力的运行状态的监控操作。整体设计涵盖了初级工程技术人员。硬件选型、; 使用场景及目标:I/O分配、电路接线、程序逻辑设计及组①掌握PLC在态监控等多个方面家电自动化控制中的应用方法;②学习,体现了PLC在工业自动化控制中的高效全自动洗衣机控制系统的性可靠性。;软硬件设计流程 适合人群:电气;③实践工程、自动化及相关MCGS组态软件PLC的专业的本科生、初级通信联调工程技术人员以及从事;④完成PLC控制系统开发毕业设计或工业的学习者;具备控制类项目开发参考一定PLC基础知识。; 阅读和梯形图建议:建议结合三菱编程能力的人员GX Works2仿真更为适宜。; 使用场景及目标:①应用于环境MCGS组态平台进行程序高校毕业设计或调试运行验证课程项目,帮助学生掌握PLC控制系统的设计,重点关注I/O分配逻辑、梯形图实现方法;②为工业自动化领域互锁机制及循环控制结构的设计中类似家电控制系统的开发提供参考方案;③思路,深入理解PL通过实际案例理解C在实际工程项目PLC在电机中的应用全过程。控制、时间循环、互锁保护、手动干预等方面的应用逻辑。; 阅读建议:建议结合三菱GX Works2编程软件和MCGS组态软件同步实践,重点理解梯形图程序中各环节的时序逻辑互锁机制,关注I/O分配硬件接线的对应关系,并尝试在仿真环境中调试程序以加深对全自动洗衣机控制流程的理解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值