4. PLCnext运行时架构解析
PLCnext Control 的运行时并不是“一个固件跑所有逻辑”,而是把通用计算能力与硬实时控制能力叠放在同一台设备上。理解它的关键,是分清四个纵向层次:底层是 Linux 系统层,其上运行实时任务层,再往下通过 Axioline 总线连接 I/O 层,而用户应用则通过应用执行模型被调度到合适的执行环境中。资源隔离机制则保证这些部分互不干扰。
Linux 系统层:通用计算与服务承载
Linux 系统层基于 Yocto 构建的嵌入式发行版,提供文件系统、网络协议栈、SSH、Docker 运行时、包管理以及 systemd 服务管理。它负责非实时任务:Web 服务、数据库、MQTT 客户端、日志归档、固件升级等。这一层的时间尺度是毫秒到秒级,调度由 CFS(完全公平调度器)完成,不保证确定性。因此,任何对抖动敏感的控制逻辑都不应直接放在普通 Linux 进程中执行。
系统层还提供设备抽象:Axioline 总线驱动、PROFINET 协议栈、OPC UA 服务器等以内核模块或用户态服务形式存在。用户可以通过 /dev/axl* 或 GDS(Global Data Space)接口访问底层数据,但直接操作寄存器并不推荐,因为会绕过运行时的一致性保护。
实时任务层:确定性执行的核心
实时任务层由 PLCnext Runtime 提供,包含一个实时内核补丁(PREEMPT_RT)和实时调度器。它把 CPU 时间划分为周期性时间片,典型周期为 1 ms、2 ms、4 ms、8 ms,抖动可控制在数十微秒量级。实时任务以优先级抢占方式运行,高于所有普通 Linux 线程。
实时层内部又分为几个执行单元:
- Real-Time Task(RTT):用户编写的 IEC 61131-3 程序或 C++ 实时任务,按固定周期触发。
- Communication Task(CT):处理 PROFINET、EtherCAT 等现场总线的周期数据交换,通常与 RTT 同步。
- System Task:负责看门狗、诊断、时钟同步等基础服务。
这些任务共享一个实时域,但通过优先级和周期分组隔离。例如,一个 1 ms 的运动控制任务可以抢占 10 ms 的逻辑任务,而不会导致后者丢失周期。
Axioline I/O 层:从总线到过程数据
Axioline 是 Phoenix Contact 的本地总线,用于连接分布式 I/O 模块。在 PLCnext 中,Axioline 层由总线控制器和 GDS 组成。GDS 是一块共享内存区域,实时任务和 I/O 驱动都通过它交换过程数据,避免系统调用和内存拷贝带来的抖动。
每个 I/O 模块在 GDS 中映射为一段固定偏移的字节区。例如,一个 16 点数字量输入模块占用 2 字节,一个模拟量输入模块占用 4 字节(16 位值 + 状态)。实时任务在每个周期开始时读取输入映像,执行逻辑,然后写回输出映像。总线驱动在周期末尾将输出映像同步到物理模块。整个过程是“读—算—写”的原子操作,保证输入输出的一致性。
应用执行模型:组件与容器
PLCnext 的应用执行模型基于“组件”概念。一个组件是一个独立的可执行单元,可以包含实时任务、非实时任务、Web 服务、配置接口等。组件通过 ACF(Application Component Framework)注册到运行时,并声明自己的资源需求:CPU 核、内存、周期、优先级、I/O 访问权限。
组件有两种部署形态:
- 原生组件:以共享库或可执行文件形式运行在 Linux 用户空间,通过 ACF 与实时层交互。
- 容器化组件:以 Docker 容器运行,适合非实时应用,如数据分析、可视化、云连接。容器与实时层通过 GDS 或 OPC UA 交换数据。
应用执行模型的核心是“声明—分配—隔离”。组件在启动时声明资源,运行时根据声明把实时任务绑定到指定 CPU 核,把非实时任务放入普通调度队列,把 I/O 访问限制在授权范围内。
资源隔离机制:CPU、内存与 I/O
资源隔离通过三个维度实现:
- CPU 隔离:通过
isolcpus内核参数把某些核从通用调度器中移除,专门分配给实时任务。例如,4 核 CPU 中,核 0 跑 Linux 系统,核 1 跑实时任务,核 2 和核 3 跑容器。实时任务使用sched_setaffinity绑定到核 1,避免被其他进程干扰。 - 内存隔离:实时任务使用预分配的内存池,避免运行时动态分配。容器使用 cgroup 限制内存上限,防止某个容器耗尽系统内存导致实时任务失败。
- I/O 隔离:GDS 的访问权限由 ACF 管理。一个组件只能读写自己声明的 I/O 区域,越界访问会被拒绝并记录诊断事件。
下面这张图展示了四个层次之间的数据流与隔离边界。实时任务层通过 GDS 与 Axioline I/O 层交换过程数据,Linux 系统层通过 ACF 管理组件生命周期,容器化组件则通过 OPC UA 或共享内存与实时层通信。
实际配置示例:把实时任务绑定到隔离核
假设设备有 4 个 CPU 核,我们希望核 0 处理系统服务,核 1 专用于 1 ms 实时任务,核 2 和核 3 运行容器。首先在内核启动参数中加入 isolcpus=1,2,3,把核 1、2、3 从通用调度器中移除。然后在组件配置中声明实时任务绑定到核 1:
<Component Name="MotionCtrl">
<RealTimeTask Name="AxisTask" Period="1ms" Priority="80">
<CpuAffinity>1</CpuAffinity>
<MemoryPool Size="4MB"/>
<IOAccess>
<Input Offset="0x0000" Length="2"/>
<Output Offset="0x0100" Length="2"/>
</IOAccess>
</RealTimeTask>
</Component>
启动后,可以通过 taskset -cp <pid> 确认实时任务确实运行在核 1 上,通过 cyclictest -p 80 -t 1 -i 1000 测量抖动。在正常负载下,最大抖动应低于 50 μs;如果超过 100 μs,需要检查是否有其他进程抢占了核 1,或者 GDS 访问是否发生了冲突。
资源隔离的边界与常见误区
资源隔离不是绝对的。Linux 系统层仍然可以访问所有内存和 I/O,只是实时任务通过优先级和 CPU 绑定获得了优先权。如果系统层出现内核死锁或内存耗尽,实时任务仍可能受影响。因此,生产环境中应限制容器内存、关闭不必要的系统服务、把日志写入独立分区。
另一个常见误区是把所有逻辑都塞进一个实时任务。正确做法是按周期分组:1 ms 任务只做运动控制和高速 I/O,10 ms 任务做逻辑联锁,100 ms 任务做数据记录和通信。这样既能保证确定性,又能避免单个任务过长导致周期丢失。
最后,Axioline I/O 的 GDS 映射是静态的,模块插拔后需要重新生成配置。如果运行中更换模块,必须通过 ACF 重新加载组件,否则 GDS 偏移会错位,导致读写到错误的过程数据。这一点在调试阶段尤其容易忽略。