本文档记录OpENer库实时性的研究.
前言
首先介绍一下OpENer库
OpENer库的源码: https://github.com/EIPStackGroup/OpENer
OpENer是Ethernet/IP协议从站功能实现锁需要的开源代码库.
在这个开源库的readme文件中详细介绍了在 Linux/POSIX 中任何开启 实时功能.
本文档以OpENer库的实时性设计为基础,致力于 搞清楚 OpENer库实时性的设计逻辑 及 将它移植到普通程序的方法.
解释实时性
从OpENer库的readme文档来看,要实现OpENer的实时功能需要进行一下操作.
首先在CMake中添加 OpENer_RT 的支持
对编译出来的可执行文件添加权限 sudo setcap cap_ipc_lock,cap_sys_nice+ep ./src/ports/POSIX/OpENer
.
从CMake文件看起, 定义了 -DOpENer_RT:BOOL=ON 之后会怎么样.
在CMake文件中, 仅有 source/src/ports/POSIX 中有使用到这个参数.
set( OpENer_RT OFF CACHE BOOL "Activate OpENer RT" )
if(OpENer_RT)
set( OpENer_RT_Additional_Stacksize "10240" CACHE STRING "Additional stack size above the defined minimum")
add_definitions( -DOPENER_RT )
add_definitions(-DOPENER_RT_THREAD_SIZE=${OpENer_RT_Additional_Stacksize})
endif(OpENer_RT)
如果CMake时配置了 -DOpENer_RT:BOOL=ON , 里面主要定义了两个宏 OPENER_RT
和 OPENER_RT_THREAD_SIZE
(值为 10240)
再看一下代码中的影响:
有关这两个宏的代码都在 source/src/ports/POSIX/main.cpp 中 :
#ifdef OPENER_RT
int ret;
/* Memory lock all*/
if (mlockall(MCL_CURRENT | MCL_FUTURE) == -1) {
OPENER_TRACE_ERR("mlockall failed: %m\n");
exit(-2);
}
struct sched_param param;
pthread_attr_t attr;
pthread_t thread;
ret = pthread_attr_init(&attr);
if (ret) {
OPENER_TRACE_ERR("init pthread attributes failed\n");
exit(-2);
}
/* Set stack size */
ret = pthread_attr_setstacksize(&attr,
PTHREAD_STACK_MIN + OPENER_RT_THREAD_SIZE);
if (ret) {
OPENER_TRACE_ERR("setstacksize failed\n");
exit(-2);
}
/* Set policy and priority of the thread */
ret = pthread_attr_setschedpolicy(&attr, SCHED_RR);
if (ret) {
OPENER_TRACE_ERR("setschedpolicy failed\n");
exit(-2);
}
param.sched_priority = 25;
ret = pthread_attr_setschedparam(&attr, ¶m);
if (ret) {
OPENER_TRACE_ERR("pthread setschedparam failed\n");
exit(-2);
}
/* scheduling parameters */
ret = pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
if (ret) {
OPENER_TRACE_ERR("setinheritsched failed\n");
exit(-2);
}
/* Create a thread with the specified attributes */
ret = pthread_create(&thread, &attr, executeEventLoop, NULL);
if (ret) {
OPENER_TRACE_ERR("create pthread failed\n");
exit(-2);
}
/* Join the thread */
ret = pthread_join(thread, NULL);
if (ret) {
OPENER_TRACE_ERR("join pthread failed: %m\n");
}
/* Unlock memory */
munlockall();
#else
(void) executeEventLoop(NULL);
#endif
以上代码截取自main函数中,从代码来看,如果定义了 OPENER_RT
就会 通过线程的方式执行 executeEventLoop
函数. 如果没定义 OPENER_RT
会直接调用 executeEventLoop
函数.
一步步看一下定义了 OPENER_RT
后具体执行了什么?
if (mlockall(MCL_CURRENT | MCL_FUTURE) == -1) {
OPENER_TRACE_ERR("mlockall failed: %m\n");
exit(-2);
}
mlockall
函数是 Linux 操作系统中用于内存管理的一个系统调用, 它的主要作用是将当前进程的所有内存页锁定到物理内存中, 防止它们被交换到交换空间(swap). 这对于一些对性能和实时性要求非常高的应用程序非常重要, 因为交换操作可能会导致不可预测的延迟.
两个参数的意义:
MCL_CURRENT
: 将当前内存页锁定到物理内存中.
MCL_FUTURE
: 将未来分配的内存页也锁定到物理内存中.
参考链接: https://blog.csdn.net/junjun5156/article/details/70598113
ret = pthread_attr_init(&attr);
if (ret) {
OPENER_TRACE_ERR("init pthread attributes failed\n");
exit(-2);
}
线程初始化 : pthread_attr_init
函数初始化一个线程属性对象 pthread_attr_t
,为创建线程设置默认属性。
ret = pthread_attr_setstacksize(&attr,
PTHREAD_STACK_MIN + OPENER_RT_THREAD_SIZE);
if (ret) {
OPENER_TRACE_ERR("setstacksize failed\n");
exit(-2);
}
设置线程 栈 的大小.
两个参数 :
PTHREAD_STACK_MIN
: 这是系统定义的线程栈的最小值.
OPENER_RT_THREAD_SIZE
: 这是刚刚在CMake文件中定义的宏,值为 10240 .
/* Set policy and priority of the thread */
ret = pthread_attr_setschedpolicy(&attr, SCHED_RR);
if (ret) {
OPENER_TRACE_ERR("setschedpolicy failed\n");
exit(-2);
}
设置线程调度策略.
关于线程调度策略的问题建议参考链接: https://blog.csdn.net/weixin_38184628/article/details/135589790
这里将线程的调度策略设置成 SCHED_RR(轮询). 个线程将时间片用完的时候会触发调度.
param.sched_priority = 25;
ret = pthread_attr_setschedparam(&attr, ¶m);
if (ret) {
OPENER_TRACE_ERR("pthread setschedparam failed\n");
exit(-2);
}
设置线程优先级.
pthread_attr_setschedparam
函数用于设置线程属性对象的调度参数. 具体来说, 它可以设置线程的优先级等调度相关属性.
这里将线程的优先级设置成25.
ret = pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
if (ret) {
OPENER_TRACE_ERR("setinheritsched failed\n");
exit(-2);
}
这段代码的作用是设置线程属性对象 attr
的调度继承属性, 使得新创建的线程显式地使用由 pthread_attr_setschedpolicy
和 pthread_attr_setschedparam
函数设置的调度策略和优先级, 而不是继承其创建者线程的调度属性.
ret = pthread_create(&thread, &attr, executeEventLoop, NULL);
if (ret) {
OPENER_TRACE_ERR("create pthread failed\n");
exit(-2);
}
创建一个新的线程, 启动 executeEventLoop
函数.
ret = pthread_join(thread, NULL);
if (ret) {
OPENER_TRACE_ERR("join pthread failed: %m\n");
}
阻塞当前线程,等待线程中止.
munlockall();
用于解除当前进程地址空间中所有内存页的锁定. 该函数会使进程的所有内存页变为可交换, 即允许它们被移出物理内存并写入交换空间(swap) .
这个地方是主要的工作线程因为特殊情况结束了,所以解除了 内存锁的限制.
然后就是 执行命令 sudo setcap cap_ipc_lock,cap_sys_nice+ep ./src/ports/POSIX/OpENer
.
这里直接引入chatGPT对于这条指令的解释:
功能:
cap_ipc_lock:允许进程锁定其虚拟内存页,避免这些页面被换出,适用于需要实时性能的应用。
cap_sys_nice:允许进程改变自身或其他进程的调度优先级,提升实时调度能力。
参数:
+ep:表示将能力添加为有效(effective)和可继承(permitted),这样程序在执行时会拥有这些特权。
原理:
Linux 内核通过能力机制替代传统的超级用户权限,使得程序可以拥有仅所需的特权,减少安全风险。通过 setcap 设置能力,程序在运行时将拥有相应的特权,而不需要以超级用户身份运行,从而提升了系统的安全性。