SylixOS 线程
SylixOS 为多线程操作系统,系统能同时创建多个线程,具体最大线程数量取决于系统内存的大小以及编译 SylixOS 时的相关配置。
创建线程
线程属性
每一个 SylixOS 线程都有自己的属性,包括线程的优先级、栈信息、线程参数等。
#include <SylixOS.h>
ULONG Lw_ThreadAttr_Build(PLW_CLASS_THREADATTR pthreadattr,
size_t stStackByteSize,
UINT8 ucPriority,
ULONG ulOption,
PVOID pvArg);
函数 Lw_ThreadAttr_Build 原型分析:
- 此函数成功返回 ERROR_NONE,失败返回错误号。
- 输出参数 pthreadattr 返回生成的属性块。
每一个线程属性块都由结构体 LW_CLASS_THREADATTR 组成,该结构体成员如下所示:
typedef struct {
PLW_STACK THREADATTR_pstkLowAddr; /* 全部栈区低内存起始地址 */
size_t THREADATTR_stGuardSize; /* 栈警戒区大小 */
size_t THREADATTR_stStackByteSize; /* 全部栈区大小(字节) */
UINT8 THREADATTR_ucPriority; /* 线程优先级 */
ULONG THREADATTR_ulOption; /* 任务选项 */
PVOID THREADATTR_pvArg; /* 线程参数 */
PVOID THREADATTR_pvExt; /* 扩展数据段指针 */
} LW_CLASS_THREADATTR;
对于 SylixOS 应用开发的用户,通常只需要关心加粗部分,其他成员操作系统会默认地设置。
- 参数 stStackByteSize 是栈的大小(字节)。
- 参数 ucPriority 是线程优先级。
为了合理地使用线程优先级,SylixOS 设置了一些常用的优先级值(数值越小线程优先级越高,系统优先调度),如下:
宏名 | 值 |
---|---|
LW_PRIO_HIGHEST | 0 |
LW_PRIO_LOWEST | 255 |
LW_PRIO_EXTREME | LW_PRIO_HIGHEST |
LW_PRIO_CRITICAL | 50 |
LW_PRIO_REALTIME | 100 |
LW_PRIO_HIGH | 150 |
LW_PRIO_NORMAL | 200 |
LW_PRIO_LOW | 250 |
LW_PRIO_IDLE | LW_PRIO_LOWEST |
在应用开发中,一般情况下任务的优先级应该在 LW_PRIO_ HIGH 与 LW_PRIO_LOW 之间,这样做为了尽可能的不对系统内核线程造成影响。
- 参数 ulOption 是线程选项。
- 参数 pvArg 是线程参数。
宏名 | 解释 |
---|---|
LW_OPTION_THREAD_STK_CHK | 运行时对线程栈进行检查 |
LW_OPTION_THREAD_STK_CLR | 在线程建立时栈数据清零 |
LW_OPTION_THREAD_USED_FP | 保存浮点运算器 |
LW_OPTION_THREAD_SUSPEND | 建立线程后阻塞 |
LW_OPTION_THREAD_INIT | 初始化线程 |
LW_OPTION_THREAD_SAFE | 建立的线程为安全模式 |
LW_OPTION_THREAD_DETACHED | 线程不允许被合并 |
LW_OPTION_THREAD_UNSELECT | 此线程不使用 select 功能 |
LW_OPTION_THREAD_NO_MONITOR | 内核跟踪器对此线程不起效 |
LW_OPTION_THREAD_ UNPREEMPTIVE | 任务不可抢占(当前不支持) |
LW_OPTION_THREAD_SCOPE_PROCESS | 进程内竞争(当前不支持) |
说明:
SylixOS 提供了一个快速获得系统默认属性块的函数 Lw_ThreadAttr_GetDefault,此函数返回值是线程属性块,此属性块默认设置线程栈为 4K 大小,优先级为 LW_PRIO_NORMAL,选项为 LW_OPTION_THREAD_STK_CHK,读者对返回的属性块可做适当修改,通常可对线程选项(选项之间以“或”的形式赋值)、线程参数做修改。
SylixOS 提供了下面一组函数来对线程的属性块进行修改:
#include <SylixOS.h>
ULONG Lw_ThreadAttr_SetGuardSize(PLW_CLASS_THREADATTR pthreadattr,
size_t stGuardSize);
ULONG Lw_ThreadAttr_SetStackSize(PLW_CLASS_THREADATTR pthreadattr,
size_t stStackByteSize);
ULONG Lw_ThreadAttr_SetArg(PLW_CLASS_THREADATTR pthreadattr,
PVOID pvArg);
Lw_ThreadAttr_SetGuardSize 函数对线程属性块的栈警戒区大小进行修改,参数 stGuardSize 指定新的栈警戒区大小,Lw_ThreadAttr_SetStackSize 函数对线程属性块的栈大小进行修改,参数 stStackByteSize 指定新的栈大小,Lw_ThreadAttr_SetArg 函数可以设置线程的启动参数 pvArg。
线程栈
每个线程都有独立的栈区,这些区域用于线程的函数调用、分配自动变量、函数返回值等。每一个线程控制块保存了栈区的起始位置、终止位置以及栈警戒点(用于栈溢出检查)。线程是 SylixOS 调度的基本单位,当发生任务调度时,线程栈区将保存线程的当前环境(用于上下文恢复)。因此线程栈的设置必须合理,太大将浪费内存空间,太小可能会引起栈溢出。SylixOS 中所有的线程都在同一地址空间运行,为了实时性要求线程之间没有地址保护机制,因此栈溢出将导致不可预知的错误。
栈大小的设置没有可以套用的公式,通常根据经验设置一个较大的值,以存储空间换取可靠性,可以在 Shell 环境下通过 ss 命令查看系统每一个任务栈的使用情况。
线程创建
#include <SylixOS.h>
LW_HANDLE Lw_Thread_Create(CPCHAR pcName,
PTHREAD_START_ROUTINE pfuncThread,
PLW_CLASS_THREADATTR pthreadattr,
LW_OBJECT_ID *pulId);
函数 Lw_Thread_Create 原型分析:
- 此函数成功返回一个创建成功的线程 ID(LW_HANDLE 类型),失败返回错误号。
- 参数 pcName 是线程的名字。
- 参数 pfuncThread 是线程入口函数,即线程代码段起始地址。
- 参数 pthreadattr 是线程的属性块指针(为 NULL 时将使用默认的属性块)。
- 参数 pulId 是线程 ID 指针,内容同返回值相同,可以为 NULL。
此函数可以创建一个 SylixOS 线程,通过下面的实例我们来看一下 SylixOS 如何创建一个线程。
程序清单 线程创建实例
#include <SylixOS.h>
PVOID tTest (PVOID pvArg)
{
while (1) {
printf("thread running...\n");
sleep(1);
}
}
int main (int argc, char *argv[])
{
LW_CLASS_THREADATTR threadattr;
LW_HANDLE hThreadId;
Lw_ThreadAttr_Build(&threadattr,
4 * LW_CFG_KB_SIZE,
LW_PRIO_NORMAL,
LW_OPTION_THREAD_STK_CHK,
LW_NULL);
hThreadId = Lw_Thread_Create("t_test", tTest, &threadattr, LW_NULL);
if (hThreadId == LW_OBJECT_HANDLE_INVALID) {
return (PX_ERROR);
}
Lw_Thread_Join(hThreadId, LW_NULL);
return (ERROR_NONE);
}
在 SylixOS Shell 下运行这段程序:
#./Thread_Creation
thread running...
thread running...
#ts
thread show >>
NAME TID PID PRI STAT LOCK SAFE DELAY PAGEFAILS FPU CPU
--------------- ------- ----- --- ---- ---- ---- ---------- --------- --- ---
……
Thread_Creation 4010041 21 200 JOIN 0 0 1 USE 1
t_test 4010042 21 200 SLP 0 445 0 USE 1
……
程序输出结果和预期的一样每隔 1 秒打印一次结果,从 ts 命令的输出结果可以看出创建了“t_test”线程(ID: 4010034 优先级 : 200),说明我们的线程创建成功。
这段程序用到上面提到的两个函数,线程属性的创建,分配了 4*LW_CFG_KB_SIZE(LW_CFG_KB_SIZE 是 SylixOS 内置的宏,值为 1024)栈大小、线程优先级是 LW_PRIO_NORMAL、线程选项 LW_OPTION_THREAD_STK_CHK,没有线程参数。创建的线程名字是“t_test”,此线程没有做任何实质性的事情只是一个简单的打印,但这已经足以说明 SylixOS 线程的创建过程及方法。
线程初始化
#include <SylixOS.h>
LW_HANDLE Lw_Thread_Init(CPCHAR pcName,
PTHREAD_START_ROUTINE pfuncThread,
PLW_CLASS_THREADATTR pthreadattr,
LW_OBJECT_ID *pulId);
ULONG Lw_Thread_Start(LW_OBJECT_HANDLE ulId);
函数 Lw_Thread_Init 原型分析:
- 此函数成功时返回线程的 ID,失败时返回错误号。
- 参数 pcName 是线程名字。
- 参数 pfuncThread 是线程的入口函数。
- 参数 pthreadattr 是线程属性(为 NULL 时将使用默认的属性块)。
- 参数 pulId 是 ID 指针,可以为 NULL。
函数 Lw_Thread_Start 原型分析:
- 此函数成功时返回 ERROR_NONE,失败时返回错误号。
- 参数 ulId 是线程的 ID。
Lw_Thread_Init 函数会像 Lw_Thread_Create 函数一样创建一个线程,但是两者有一个本质的区别,那就是 Lw_Thread_Init 函数创建的线程只是处于一个初始态,也就是线程并没有就绪,调度器并不会把 CPU 的使用权分配给此线程,只有当调用了 Lw_Thread_Start 函数之后线程才处于就绪态,才能被调度器进行调度。
下面程序展示了线程初始化函数的使用。
程序清单 线程初始化实例(Thread_Initialization)
#include <SylixOS.h>
PVOID tTest (PVOID pvArg)
{
while (1) {
printf("Thread running...\n");
sleep(1);
}
return (LW_NULL);
}
int main (int argc, char *argv[])
{
LW_CLASS_THREADATTR threadattr;
LW_HANDLE hThreadId;
INT iRet;
Lw_ThreadAttr_Build(&threadattr,
4 * LW_CFG_KB_SIZE,
LW_PRIO_NORMAL,
LW_OPTION_THREAD_STK_CHK,
LW_NULL);
hThreadId = Lw_Thread_Init("t_test", tTest, &threadattr, LW_NULL);
if (hThreadId == LW_OBJECT_HANDLE_INVALID) {
return (PX_ERROR);
}
iRet = Lw_Thread_Start(hThreadId);
if (iRet) {
return (PX_ERROR);
}
Lw_Thread_Join(hThreadId, LW_NULL);
return (ERROR_NONE);
}
在 SylixOS Shell 下运行这段程序:
#./Thread_Initialization
Thread running...
Thread running...
#ts
thread show >>
NAME TID PID PRI STAT LOCK SAFE DELAY PAGEFAILS FPU CPU
--------------------- ------- ----- --- ---- ---- ---- ------ ---------- --- ---
……
Thread_Initialization 4010046 22 200 JOIN 0 0 1 USE 1
t_test 4010047 22 200 SLP 0 958 0 USE 1
……
程序输出结果和预期的一样每隔 1 秒打印一次结果,从 ts 命令的输出结果可以看出创建了“t_test”线程(ID: 4010015 优先级 : 200),说明我们的线程创建成功。
通过比较上面两个程序清单,我们发现运行结果是一样的,并且创建线程属性也是一样,因此,从运行的行为上看,两段程序的效果是一样的,在使用上一般我们会用程序清单 线程初始化实例(Thread_Initialization)的方法去主动控制我们的线程什么时候进入就绪态,对于某些情况这种方法是很有用的。
注意:
需要注意的是上面介绍的创建线程函数都不可以在中断上下文中调用。
线程句柄检查
#include <SylixOS.h>
BOOL Lw_Thread_Verify(LW_OBJECT_HANDLE ulId);
函数 Lw_Thread_Verify 原型分析:
- 此函数成功时返回 LW_TRUE,失败时返回 LW_FLASH。
- 参数 ulId 是待检查的线程句柄。
下面程序展示了线程句柄检查函数的使用。
程序清单 线程句柄检查实例
#include "SylixOS.h"
static LW_HANDLE hThreadId = 0;
PVOID tTest (PVOID pvArg)
{
BOOL bRet;
printf("Thread running...\n");
bRet = Lw_Thread_Verify(hThreadId);
if (bRet == LW_TRUE) {
printf("Thread ID check success.\n");
} else {
printf("Thread ID check failed.\n");
}
return (LW_NULL);
}
int main (int argc, char *argv[])
{
LW_CLASS_THREADATTR threadattr;
INT iRet;
Lw_ThreadAttr_Build(&threadattr,
4 * LW_CFG_KB_SIZE,
LW_PRIO_NORMAL,
LW_OPTION_THREAD_STK_CHK,
LW_NULL);
hThreadId = Lw_Thread_Init("t_test", tTest, &threadattr, LW_NULL);
if (hThreadId == LW_OBJECT_HANDLE_INVALID) {
return (PX_ERROR);
}
iRet = Lw_Thread_Start(hThreadId);
if (iRet) {
return (PX_ERROR);
}
Lw_Thread_Join(hThreadId, LW_NULL);
return (ERROR_NONE);
}
在 SylixOS Shell 下运行这段程序:
#./ThreadID_Check
Thread running...
Thread ID check success.
程序输出结果"Thread ID check success.",说明线程 ID 有效,检查成功。
控制线程
线程挂起与恢复
线程挂起是使指定的线程处于非就绪态,处于挂起状态的线程被调度器忽略,相对“静止”了下来,以便于调试等,直到挂起被解除。
#include <SylixOS.h>
ULONG Lw_Thread_Suspend(LW_OBJECT_HANDLE ulId);
ULONG Lw_Thread_Resume(LW_OBJECT_HANDLE ulId);
函数 Lw_Thread_Suspend 原型分析:
- 此函数成功返回 ERROR_NONE,失败返回错误号。
- 参数 ulId 是线程 ID。
函数 Lw_Thread_Resume 原型分析:
- 此函数成功返回 ERROR_NONE,失败返回错误号。
- 参数 ulId 是线程 ID。
挂起是任务的一种二进制无记忆状态,任务被挂起以前不会检查之前是否被挂起或解除挂起,因此:
- 重复挂起某个任务和一次挂起的效果是一样的。
- 如果挂起和解除挂起由不同的任务完成,必须确保按照正确的顺序进行。
我们看下面的情形,线程 T1 挂起自身之前发送消息给某一线程 T2,T2 收到这个消息后去解除 T1 线程的挂起状态。
……
T1:发送消息给T2线程
T1:调用Lw_Thread_Suspend挂起自己
……
T2:收到T1的消息
T2:调用Lw_Thread_Resume解除T1挂起状态
……
上面情形,我们仔细分析之后会发现存在着竞争风险,T1 刚好发送完消息,这时高优先级的 T2 收到消息进行线程挂起解除,这个时候的解除没有任何效果,而随后 T1 开始挂起便进入了无限的挂起状态,我们应该小心这种情况的出现。
另外,我们还要注意的是挂起时不要使系统陷入死锁,这通常需要防止线程在获得某个互斥访问的系统资源后被挂起,尤其在异步挂起的时候需要特别小心这种情况的发生。
为了避免竞争的风险,我们可以使挂起状态附加到延时状态与阻塞状态上,使线程进入“延时挂起”或者“阻塞挂起”状态,附加挂起的状态,与线程原来的延迟和阻塞相互没有影响,也就是说挂起状态与延时或者阻塞状态可以并存。
- 挂起期间延时线程仍然计算延时,如果延时到时,任务便进入只挂起状态。
- 阻塞线程在挂起期间如果等待条件满足,则解除阻塞,进入只挂起状态。
- 如果延时到时或者等待条件出现之时线程被解除挂起,则线程回到原来的延时阻塞状态。
注意:
Lw_Thread_Suspend 函数和 Lw_Thread_Resume 函数可以在中断中调用,线程挂起是无条件的。SylixOS 中查看线程此时所处的状态可通过 Shell 命令 ts 查看“STAT”列,如下,线程“t_test”处于“SLP”状态。
#./Thread_Initialization
Thread running...
Thread running...
#ts
thread show >>
NAME TID PID PRI STAT LOCK SAFE DELAY PAGEFAILS FPU CPU
--------------------- ------- ----- --- ---- ---- --- ------ --------- --- ---
……
Thread_Initialization 4010046 22 200 JOIN 0 0 1 USE 1
t_test 4010047 22 200 SLP 0 958 0 USE 1
……
线程延时
线程延时是让线程处于睡眠状态,从而调度器可以调度其他线程,当线程睡眠结束后,重新恢复运行。
#include <SylixOS.h>
VOID Lw_Time_Sleep(ULONG ulTick);
VOID Lw_Time_SSleep(ULONG ulSeconds);
VOID Lw_Time_MSleep(ULONG ulMSeconds);
函数原型分析:
- Lw_Time_Sleep 延时单位是 Tick。
- Lw_Time_SSleep 延时单位是秒。
- Lw_Time_MSleep 延时单位是毫秒。
在 SylixOS 中使用 Lw_Time_Sleep 系列函数可以获得默认的最小延时是 1Tick,如果指定 ulTick 是 0 则 SylixOS 不会进行延时,也不会影响当前线程的行为,如果想要一个更短的时间延时(例如:500ns)。可以调用 nanosleep 函数。
#include <time.h>
int nanosleep(const struct timespec *rqtp,
strutc timespec *rmtp);
函数 nanosleep 原型分析:
- 此函数成功返回 0,失败返回-1 并设置错误号。
- 参数 rqtp 是睡眠的时间。
- 参数 rmtp 保存剩余的时间。
此函数属于 POSIX 标准,要求等待的时间由结构体 timespec 的指针 rqtp 表示,结构体以秒和纳秒表示时间。与上面三个函数不同的是,如果 rmtp 不是 NULL,则通过其返回剩余的时间。需要注意的是,此函数可以被信号唤醒,如果被信号唤醒,错误号 errno 为 EINTR。关于信号的详细讲解见“信号系统”。
注意:
如果 nanosleep 睡眠过程中没有被信号中断,则 rmtp 总返回 0,否则将返回被信号中断的时间点到延时完成的时间间隔。
线程互斥
互斥访问是操作系统中一个经典的理论问题,用于实现对共享资源的一致性访问,SylixOS 中实现了不同的函数来提供多种互斥机制。
- 线程锁:Lw_Thread_Lock。
- 中断锁:Lw_Interrupt_Lock。
- 信号量:Lw_Semaphore_Wait。
当访问的共享资源时间很长时,信号量的方法很有效。例如,当一个线程想要申请使用被信号量锁定的一块共享区时,那么此时这个线程就会被阻塞,从而节约了 CPU 周期。关于信号量的详细使用见“线程间通信”。
利用锁中断的方法会增加系统的中断响应延迟,对于一般线程而言,锁中断不是一个好的方法,一般应用程序开发 SylixOS 不建议使用锁中断。
下面我们看一下线程锁,在 SylixOS 中允许调用线程锁函数来使调度器失效,当线程调用线程锁函数后,调度器暂时失效,即使有高优先级线程就绪,线程也不会被调出处理器,直到线程调用线程解除函数,此种互斥方案,为系统引入了优先级延迟(调度延迟);实时性要求高的高优先级线程必须等到线程解除锁定后才能被调度,这样在一定程度上牺牲了 SylixOS 优越的实时性能,因此这种方法也是不建议用的。
基于多方的考虑,SylixOS 的应用程序开发一般使用信号量的方法来实现互斥访问。
注意:
线程锁函数只是锁定当前 CPU 的调度,不会影响其他 CPU 的调度。在线程锁定期间不能使用阻塞函数,线程锁定不会锁定中断,中断发生时,中断服务程序照常调用。
线程结束
线程结束意味着线程生命周期终止。线程结束包括运行结束退出、线程退出和线程删除 3 种情况。
线程删除
线程删除是将线程的资源返还给操作系统,删除后的线程不能再被调度。
#include <SylixOS.h>
ULONG Lw_Thread_Delete(LW_OBJECT_HANDLE *pulId, PVOID pvRetVal);
ULONG Lw_Thread_ForceDelete(LW_OBJECT_HANDLE *pulId, PVOID pvRetVal);
函数原型分析:
- 两个函数成功返回 ERROR_NONE,失败返回错误号。
- 参数 pulId 是将要删除的线程的句柄。
- 参数 pvRetVal 是返回给 JOIN 函数的值。
调用上面的两个函数可使线程结束,并释放线程资源,由于 SylixOS 支持进程,所以删除线程只能是同一个进程中的线程,而且主线程只能由其自己来删除。
主动删除其他正在执行的线程,可能造成其加锁的资源得不到释放,或者原子操作被打断,所以除非确保安全,否则 SylixOS 以及任何其他操作系统都不推荐直接使用线程删除函数调用。在应用程序设计时,应考虑使用“请求”删除方式,当线程自己发现无事可做或者被请求删除时,由线程自己删除自己(线程退出)。
线程退出
#include <SylixOS.h>
ULONG Lw_Thread_Exit(PVOID pvRetVal);
函数 Lw_Thread_Exit 原型分析:
- 此函数成功返回 ERROR_NONE,失败返回错误号。
- 参数 pvRetVal 是返回给 JOIN 函数的值。
线程取消
#include <SylixOS.h>
ULONG Lw_Thread_Cancel(LW_OBJECT_HANDLE *pulId);
函数 Lw_Thread_Cancel 原型分析:
- 此函数成功返回 ERROR_NONE,失败返回错误号。
- 参数 pulId 是取消的线程 ID。
线程取消的方法是向目标线程发送 Cancel 信号,但如何处理 Cancel 信号则由目标线程自己决定,需要注意的是,线程取消是一个复杂的过程,需要考虑资源的一致性问题。
CPU 亲和性
SylixOS 是支持多核调度的 SMP 实时操作系统,为了满足不同线程在不同 CPU 上的运行,支持了手动 CPU 亲和模型,用户可以通过操作系统提供的亲和接口方便地实现线程的灵活运行。
以下函数设置指定线程到指定 CPU 集中:
#include <SylixOS.h>
ULONG Lw_Thread_SetAffinity(LW_OBJECT_HANDLE ulId,
size_t stSize,
const PLW_CLASS_CPUSET pcpuset)
函数 Lw_Thread_SetAffinity 原型分析:
- 此函数成功返回 ERROR_NONE,失败返回错误号。
- 参数 ulId 是需要亲和的线程句柄。
- 参数 stSize 是 CPU 集大小。
- 参数 pcpuset 是需要亲和的 CPU 集。
以下函数获取指定线程的亲和情况:
#include <SylixOS.h>
ULONG Lw_Thread_GetAffinity(LW_OBJECT_HANDLE ulId,
size_t stSize,
PLW_CLASS_CPUSET pcpuset)
函数 Lw_Thread_GetAffinity 原型分析:
- 此函数成功返回 ERROR_NONE,失败返回错误号。
- 参数 ulId 是需要亲和的线程句柄。
- 参数 stSize 是 CPU 集大小。
- 参数 pcpuset 是亲和的 CPU 集。
CPU 集合的设置通过下面一组宏定义。
将 n 号 CPU 设置到 CPU 集 p 中。
#define LW_CPU_SET(n, p)
将 n 号 CPU 从 CPU 集 p 中删除。
#define LW_CPU_CLR(n, p)
查看 n 号 CPU 是否在 CPU 集 p 中。
#define LW_CPU_ISSET(n, p)
将 CPU 集 p 清零。
#define LW_CPU_ZERO(p)
异构算力簇
SylixOS V3.1.1版本及其之后的版本,全面支持异构多算力簇调度,同时满足强实时性计算要求,成为全球首个支持大小核调度的强实时系统。
SylixOS 对大小核调度的支持分为两部分:“核算力簇亲和调度”与“算力感知自动调度适配器”,简单的说前者为后者的工作提供支撑。“内核算力簇亲和调度” 实现了被调度任务与不同的算力核心可控亲和度调度;“算力感知自动调度适配器”是一个内核模块,它精确测量各任务对算力的使用与需求,实时对 CPU 总算力进行动态分配,使应用程序无感使用大小核架构处理器系统。
SylixOS 为每个任务提供了多种模式的可控亲和度设置能力, 以大小核两个算力簇为例:
调度模式 | 说明 |
---|---|
自由调度 | 此任务可以运行在任何一个 CPU 核心上,由操作系统选择最合适的 CPU 核心使用。此模式为任务的默认状态。 |
倾向使用小核心 | 此任务与小核心算力簇WEAK亲和,在不破坏实时性的原则下,操作系统会尽量将其调度到小核心算力簇。 |
仅使用小核心 | 此任务与小核心算力簇 STRONG 亲和,此时操作系统仅会将此任务调度到小核心以节约能源。在若干小核心内依然采用优先级抢占的实时调度。 |
倾向使用大核心 | 此任务与大核心算力簇 WEAK 亲和,在不破坏实时性的原则下,操作系统会尽量将其调度到大核心算力簇,使此任务获得更多的算力支持。 |
仅使用大核心 | 此任务与大核心算力簇 STRONG 亲和,此时操作系统仅会将此任务调度到大核心以达到最强算力给予。在若干大核心内依然采用优先级抢占的实时调度。 |
操作系统提供了调度模式的宏定义如下:
#define LW_HETRCC_NON_AFFINITY 0 /* 自由调度 */
#define LW_HETRCC_WEAK_AFFINITY 1 /* 推荐亲和调度 */
#define LW_HETRCC_STRONG_AFFINITY 2 /* 强算力簇亲和调度 */
三个宏定义的含义如下:
- LW_HETRCC_NON_AFFINITY:代表线程可以自由被调度,无任何亲和情况。
- LW_HETRCC_WEAK_AFFINITY:代表线程为“弱亲和”,线程将“尽可能”进行亲和,这意味着如果调度条件不允许做指定核心的亲和,则会放弃亲和。
- LW_HETRCC_STRONG_AFFINITY:代表线程为“强亲和”,线程将总是能够亲和到指定核心。
SylixOS 为用户提供算力亲和函数,下面函数可以设置线程的算力亲和。
#include <SylixOS.h>
ULONG Lw_Thread_SetHetrcc(LW_OBJECT_HANDLE ulId, INT iMode, ULONG ulHccId)
函数 Lw_Thread_SetHetrcc 原型分析:
- 参数 ulId 是需要设置的线程句柄。
- 参数 iMode 是设置模式。
- 参数 ulHccId 是需要亲和的算力簇 ID,该 ID 值越大代表算力越强。
下面函数可以获取线程的算力亲和信息。
#include <SylixOS.h>
ULONG Lw_Thread_GetHetrcc(LW_OBJECT_HANDLE ulId, INT *piMode, ULONG *pulHccId)
函数Lw_Thread_GetHetrcc原型分析:
- 参数 ulId 是需要设置的线程句柄。
- 参数 piMode 用于获取模式。
- 参数 pulHccId 用于获取亲和的算力簇 ID。
下面函数可以获取当前系统最大异构算力簇 ID。
#include <SylixOS.h>
ULONG Lw_Cpu_MaxHetrcc(VOID);
函数 Lw_Cpu_MaxHetrcc 原型分析:
- 函数返回最大异构算力簇 ID 号 (编号越大算力越强)。
下面函数可以获取当前系统在线 CPU 最大异构算力簇 ID 号。
#include <SylixOS.h>
ULONG Lw_Cpu_UpMaxHetrcc(VOID);
函数 Lw_Cpu_UpMaxHetrcc 原型分析:
- 函数返回最大异构算力簇 ID 号 (编号越大算力越强)。
下面函数可以获取指定异构算力簇 ID 当前激活的 CPU 数量。
#include <SylixOS.h>
ULONG Lw_Cpu_UpCurHetrcc(ULONG ulHccId);
函数 Lw_Cpu_UpCurHetrcc 原型分析:
- 函数返回 CPU 个数。
- 参数 ulHccId 是算力簇 ID。
多线程安全
多线程模型与生俱来的优势使得 SMP 多核处理器实现真实的并发执行,但多线程带来便利的同时也引入了一些问题,例如全局资源互斥访问的问题。为了能够安全地访问这些资源,需要程序设计中考虑避免竞争条件和死锁。
多线程安全是在多线程并发执行的情况下,一种资源可以安全地被多个线程使用的一种机制。多线程安全包括代码临界区的保护和可重入性等。代码的临界区,指处理时不可分割的代码。一旦这部分代码开始执行,则不允许任何中断打入。为确保临界区代码的执行不被中断,在进入临界区之前必须关闭中断,而临界区代码执行完后,要立即开中断。
“可重入性”是指函数可以被多个线程调用,而不会破坏数据。不管有多少个线程使用,可重入函数总是为每个线程得到预期结果。允许重入的函数称为“可重入函数”,否则为“不可重入函数”。
由于代码重入性问题源于多线程并行运行,因此又称代码重入性为“多线程安全”。
在 SylixOS 中,可能会引起代码重入的场合包括:多线程调度、中断服务程序调度、信号处理函数调度。
考虑下面两个函数:
char *ctime (const time_t *time)
{
static char cTimeBuffer[sizeof(ASCBUF)];
…… /* 将字符串写入buffer中 */
return (cTimeBuffer);
}
char *ctime_r (const time_t *time, char *buffer)
{
…… /* 将字符串写入buffer中 */
return (buffer);
}
两个函数都将参数 time 表示的时间转换成字符串返回。ctime 函数定义字符串为局部静态缓冲区,考虑多个线程“同时”调用 ctime,很显然,对于不同线程的调用都将使 ctime 函数修改同一字符串缓冲区,因此 ctime 函数是不可重入函数。相比之下,ctime_r 函数的字符串缓冲区由调用者分配,不同线程运行修改其各自的缓冲区,因此对于多线程调用是安全的。
有些函数不可避免地要使用全局的或者静态的变量,如 malloc 函数等,为了保护全局变量不被破坏,互斥锁是一种选择。
除了上面所介绍的实现函数重入性的方法外,SylixOS 还提供了一种“线程私有数据”机制来实现函数重入,这种保护牺牲了系统的实时性并且仅针对单 CPU 系统有效,所以除非必须这样做,否则 SylixOS 不推荐使用这种方式。
线程私有数据就是线程上下文记录的一个 unsigned long 型数值(可以将其看成指向一个全局变量的指针)和保存全局变量值的临时变量(用来恢复线程上下文中全局变量的值)。每次线程被调入处理器时,系统根据该指针自动从线程上下文装入全局变量的值,相应地,任务被调出处理器时,系统根据该指针自动将全局变量的值保存到线程上下文。
下面一组函数实现了对线程私有数据的操作。
#include <SylixOS.h>
ULONG Lw_Thread_VarAdd(LW_HANDLE ulId, ULONG *pulAddr);
ULONG Lw_Thread_VarDelete(LW_HANDLE ulId, ULONG *pulAddr);
ULONG Lw_Thread_VarSet(LW_HANDLE ulId, ULONG *pulAddr, ULONG ulValue);
ULONG Lw_Thread_VarGet(LW_HANDLE ulId, ULONG *pulAddr);
ULONG Lw_Thread_VarInfo(LW_HANDLE ulId, ULONG *pulAddr[], INT iMaxCounter);
函数原型分析:
- 参数 ulId 是线程的句柄。
- 参数 pulAddr 是私有数据地址。
- 参数 ulValue 是设置的值。
- 参数 iMaxCounter 是地址列表大小。
调用 Lw_Thread_VarAdd 函数可以声明一个线程私有数据,此函数成功返回 ERROR_NONE,失败返回错误号;调用 Lw_Thread_Delete 函数将删除一个已经声明的线程私有数据,此函数成功返回 ERROR_NONE,失败返回错误号;调用 Lw_Thread_VarSet 函数可以设置线程私有数据的值;调用 Lw_Thread_VarGet 函数可以获得线程私有数据的值,此函数返回私有数据的值或者 0;调用 Lw_Thread_VarInfo 函数将获得线程私有数据的信息,此函数返回私有数据的个数。
线程私有数据实现可重入的过程如下:
INT _G_iGlobal;
VOID func (VOID)
{
……
if (Lw_Thread_VarAdd(threadId, (ULONG *)&_G_iGlobal) != 0) {
…… /* 错误处理 */
}
_G_iGlobal++; /* 全局变量处理 */
……
}
显然,对于每次根据 func 函数生成的新线程运行,都将对全局变量 iGlobal 进行加操作,声明了线程私有数据后,系统将在线程上下文切换时保存和装入各个线程拥有的全局变量 iGlobal 的副本,这样不同的线程对全局变量 iGlobal 的修改将互不影响,如下图所示。
需要注意的是,线程私有数据必须在对其进行任何赋值之前声明。
Lw_Thread_VarSet 函数和 Lw_Thread_VarGet 函数通常用于使参于某项工作的多个协作线程可以相互得到其他线程的线程私有数据值,具有线程间通信的功能,如下所示是一个私有数据实现线程间通信的例子。
说明:
线程中若用到私有变量,则在多处理器上运行无效,需要在单处理上运行,修改配置如下:在 libsylixos/SylixOS/config/mp/mp_cfg.h 中将宏 LW_CFG_SMP_EN 修改为 0。
程序清单 私有数据实现线程间通信(Private_Data_Communication)
#include <SylixOS.h>
#include <stdio.h>
INT _G_iGlobal = 0;
PVOID tTest (PVOID pvArg)
{
while (1) {
fprintf(stdout, "tTest global value: %d\n", _G_iGlobal);
sleep(1);
}
return (LW_NULL);
}
int main (int argc, char *argv[])
{
LW_HANDLE hId;
hId = Lw_Thread_Create("t_test", tTest, NULL, NULL);
if (hId == LW_HANDLE_INVALID) {
return (PX_ERROR);
}
if (Lw_Thread_VarAdd(hId, (ULONG *)&_G_iGlobal) != 0){
return (PX_ERROR);
}
while (1) {
Lw_Thread_VarSet(hId, (ULONG *)&_G_iGlobal, 55);
sleep(1);
}
Lw_Thread_Join(hId, NULL);
return (ERROR_NONE);
}
在 SylixOS Shell 下运行程序,结果如下:
# ./Private_Data_Communication
tTest global value: 55
tTest global value: 55
每增加一个线程私有数据,将使线程上下文切换增加 unsigned long 型的内存复制开销,这是线程私有数据的不足,相比前文的基于局部动态变量的方法也存在局限性,在需要跨越函数范围时,必须使用全局变量,而普遍使用的“全局变量+互斥”的方法比线程私有数据的方法要繁琐一些,而且互斥也需要一定的运行开销。