前言
该系列基于rtthread-nano的内核源码,来研究RTOS的底层逻辑,本文介绍RTT的内核对象,对于其他RTOS来说也可供参考,万变不离其宗,大家都是互相借鉴,实现不会差太多。
内核对象容器
首先要明确的一点是什么是内核对象,在RTT中,利用结构体之间的嵌套,实现了一种类似面向对象的设计。所以这里的内核对象,是指线程,信号量,互斥量,事件,邮箱,消息队列和定时器,内存池,设备驱动等等任何通过动态/静态创建出来的对象。
其次,需要知道的是,RTT是通过一个rt_object_container来管理这些所有的内核对象的。他是怎么做到的呢?我们来看一下(部分)代码就知道了(也可以直接看下面的总结)
//只展示部分代码
static struct rt_object_information rt_object_container[RT_Object_Info_Unknown] =
{
/* initialize object container - thread */
{RT_Object_Class_Thread, _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_Thread), sizeof(struct rt_thread)},
#ifdef RT_USING_SEMAPHORE
/* initialize object container - semaphore */
{RT_Object_Class_Semaphore, _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_Semaphore), sizeof(struct rt_semaphore)},
#endif
#ifdef RT_USING_MUTEX
/* initialize object container - mutex */
{RT_Object_Class_Mutex, _OBJ_CONTAINER_LIST_INIT(RT_Object_Info_Mutex), sizeof(struct rt_mutex)},
#endif
};
可以看到,本质上内核容器对象就是一个数组,里面的每个成员都是一个结构体如下所示
/**
* The information of the kernel object
*/
struct rt_object_information
{
enum rt_object_class_type type; /**< object class type */
rt_list_t object_list; /**< object list */
rt_size_t object_size; /**< object size */
};
rt_list_t又是一个双向链表,因此,rt_object_container其实就是一个rt_object_information结构体构成的数组,这个结构体里面又维护了
1.每种内核对象的类型
2.一个双向链表用于链接每个同类内核对象
3.该内核对象创建时需要的内存大小
程序运行过程中,每个创建的内核对象都会被放在这个内核对象容器中,最终形成如下结构
内核对象创建
不同的内核对象会应用不同的创建函数,例如对于动态创建来说
线程创建 ——> rt_thread_create
定时器创建 ——> rt_timer_create
但只要查看这两个创建函数的源码,都可以看到他们都调用了同一个rt_object_allocate函数,这也是上文提到的,内核对象容器的部分意义所在。它抽象出了所有内核对象的一些共同属性,这些共同属性通过rt_object_allocate函数完成初始化,然后再通过自己的函数完成特有属性的初始化。
本文重点阐述object的创建删除等操作,特定内核对象的创建将在后文表述。
动态创建
RTT在动态创建内核对象时,用到了RT_KERNEL_MALLOC来动态分配内存,这里并不做展开,后续会专门对内存管理进行说明,现在我们看一下动态创建内核对象的源码
rt_object_t rt_object_allocate(enum rt_object_class_type type, const char *name)
{
struct rt_object *object;
rt_base_t level;
struct rt_object_information *information;
RT_DEBUG_NOT_IN_INTERRUPT;
/* get object information */
information = rt_object_get_information(type);
RT_ASSERT(information != RT_NULL);
object = (struct rt_object *)RT_KERNEL_MALLOC(information->object_size);
if (object == RT_NULL)
{
/* no memory can be allocated */
return RT_NULL;
}
/* clean memory data of object */
rt_memset(object, 0x0, information->object_size);
/* initialize object's parameters */
/* set object type */
object->type = type;
/* set object flag */
object->flag = 0;
/* copy name */
rt_strncpy(object->name, name, RT_NAME_MAX);
RT_OBJECT_HOOK_CALL(rt_object_attach_hook, (object));
/* lock interrupt */
level = rt_hw_interrupt_disable();
{
/* insert object into information object list */
rt_list_insert_after(&(information->object_list), &(object->list));
}
/* unlock interrupt */
rt_hw_interrupt_enable(level);
/* return object */
return object;
}
代码做了以下这些工作:
1.通过RT_DEBUG_NOT_IN_INTERRUPT确认当前执行环境不在中断中,中断需要快速完成,动态申请内存显然是不合适的
2.根据不同对象类型动态申请一块内存
3.将该内存清零
4.给内核对象共有的属性:类型,名称,标志位赋值
5.将该内核对象插入内核对象容器
这里做一下展开说明,内核对象的结构体如下所示,他所需的大小也许是64字节:
struct rt_object
{
char name[RT_NAME_MAX]; /**< name of kernel object */
rt_uint8_t type; /**< type of kernel object */
rt_uint8_t flag; /**< flag of kernel object */
rt_list_t list; /**< list node of kernel object */
};
而一个具体的内核对象,如timer,他的结构体如下所示,他需要的内存大小远大于64字节
struct rt_timer
{
struct rt_object parent; /**< inherit from rt_object */
rt_list_t row[RT_TIMER_SKIP_LIST_LEVEL];
void (*timeout_func)(void *parameter); /**< timeout function */
void *parameter; /**< timeout function's parameter */
rt_tick_t init_tick; /**< timer timeout tick */
rt_tick_t timeout_tick; /**< timeout tick */
};
在 rt_object_allocate 函数中
object = (struct rt_object *)RT_KERNEL_MALLOC(information->object_size);
申请的内存大小是具体的(如timer)需要的内存空间,然后用object对象先将他接收,把前64个字节进行赋值,其余内存空间进行清零,在rt_timer_create则对这些未0的字节进行赋值。
内核对象创建hook
RT_OBJECT_HOOK_CALL
是 RT-Thread 中用于调用钩子函数的宏定义。创建的这个object对象传递会被传递给钩子函数,接下来简单讲一下如何注册钩子函数
void my_attach_hook(struct rt_object *object)
{
// 用户自定义的处理代码
}
rt_object_attach_sethook(my_attach_hook);
在上述代码中,my_attach_hook
是用户定义的钩子函数,实现了对附加对象事件的处理。通过调用 rt_object_attach_sethook
,将该函数注册为对象附加事件的钩子。当系统中有对象被附加时,my_attach_hook
会被自动调用。
静态创建
在内核对象进行静态创建时,内存的分配是在编译时就完成的,因此创建的时间是完全可控的,对于严格的实时操作系统来说,这样的分配方式是更值得推荐的,只是编程上会没有那么舒适我们以timer的静态创建为例
void rt_timer_init(rt_timer_t timer,
const char *name,
void (*timeout)(void *parameter),
void *parameter,
rt_tick_t time,
rt_uint8_t flag)
{
/* parameter check */
RT_ASSERT(timer != RT_NULL);
RT_ASSERT(timeout != RT_NULL);
RT_ASSERT(time < RT_TICK_MAX / 2);
/* timer object initialization */
rt_object_init(&(timer->parent), RT_Object_Class_Timer, name);
_timer_init(timer, timeout, parameter, time, flag);
}
可以看到他在创建时调用了
rt_object_init(&(timer->parent), RT_Object_Class_Timer, name);
任意静态的内核对象在创建时都会调用该函数,可以看到函数的入参是timer->paraent的地址,是在创建时就确定的一块地址空间;object_init的源码如下
void rt_object_init(struct rt_object *object,
enum rt_object_class_type type,
const char *name)
{
rt_base_t level;
struct rt_list_node *node = RT_NULL;
struct rt_object_information *information;
/* get object information */
information = rt_object_get_information(type);
RT_ASSERT(information != RT_NULL);
rt_enter_critical();
/* try to find object */
for (node = information->object_list.next;
node != &(information->object_list);
node = node->next)
{
struct rt_object *obj;
obj = rt_list_entry(node, struct rt_object, list);
if (obj) /* skip warning when disable debug */
{
RT_ASSERT(obj != object);
}
}
rt_exit_critical();
/* set object type to static */
object->type = type | RT_Object_Class_Static;
/* copy name */
rt_strncpy(object->name, name, RT_NAME_MAX);
RT_OBJECT_HOOK_CALL(rt_object_attach_hook, (object));
/* lock interrupt */
level = rt_hw_interrupt_disable();
/* insert object into information object list */
rt_list_insert_after(&(information->object_list), &(object->list));
/* unlock interrupt */
rt_hw_interrupt_enable(level);
}
这里比起动态创建值得说的有如下几点:
1.在 rt_object_init
函数中,系统会遍历对象列表,检查是否存在同名对象。为了防止在遍历过程中其他线程对对象列表进行修改,可能导致数据不一致或系统崩溃,必须在遍历前关闭调度器,进入临界区。遍历完成后,再退出临界区,恢复调度器的正常运行。这种做法确保了对象管理操作的原子性和系统的稳定性
rt_enter_critical();
/* try to find object */
for (node = information->object_list.next;
node != &(information->object_list);
node = node->next)
{
struct rt_object *obj;
obj = rt_list_entry(node, struct rt_object, list);
if (obj) /* skip warning when disable debug */
{
RT_ASSERT(obj != object);
}
}
rt_exit_critical();
2.在往内核容器中插入内核对象的时候,采取了关闭硬件中断的做法,这里和上面关闭调度器的做法产生了差异,这是因为:
关闭调度器(调度锁):
通过调用 rt_enter_critical()
和 rt_exit_critical()
函数,可以进入和退出调度临界区。在调度锁持有期间,系统停止线程调度,不会发生线程切换,但仍然响应中断。这意味着当前线程独占 CPU 资源,其他线程即使优先级更高也无法抢占执行。这种方法适用于需要防止线程切换的临界区,但对中断的响应不受影响。
关闭中断:
通过调用 rt_hw_interrupt_disable()
和 rt_hw_interrupt_enable()
函数,可以禁用和启用中断。在中断被禁用期间,系统不会响应任何中断,包括时钟中断,从而也不会进行线程调度。这种方法确保了代码执行的原子性,适用于需要防止中断干扰的关键操作,例如修改与中断服务程序共享的数据。
为何在插入内核对象时关闭中断:
在向内核对象列表中插入新对象时,涉及对全局链表的修改。如果在插入过程中发生中断,且中断服务程序也对该链表进行操作,可能导致数据不一致或系统崩溃。因此,需要通过关闭中断来保护这段临界区代码,确保插入操作的原子性。
为何在遍历对象列表时关闭调度器:
在遍历对象列表时,如果其他线程可能对该列表进行修改(例如插入或删除对象),可能导致遍历过程中的数据不一致。通过关闭调度器,可以防止其他线程在遍历期间进行修改操作,确保遍历过程的安全性。由于遍历操作可能耗时较长,关闭中断会影响系统的实时性,因此选择关闭调度器以平衡安全性和实时性。
综上所述,检测内核容器中是否存在同名对象时,仅仅设计对内核容器的遍历查询;但插入时要修改内核容器的链表,修改链表时出现问题,将造成整个系统的崩溃,需要更加严格的保护
内核对象删除
内核对象的删除相对简单,静态删除和动态删除差异不大,仅仅是动态删除时会释放动态创建时malloc出来的内存,而静态没有这个逻辑;其余则仅仅需要将内核对象容器中的链表上将该对象删掉就行了,简单看下源码
动态删除
void rt_object_delete(rt_object_t object)
{
rt_base_t level;
/* object check */
RT_ASSERT(object != RT_NULL);
RT_ASSERT(!(object->type & RT_Object_Class_Static));
RT_OBJECT_HOOK_CALL(rt_object_detach_hook, (object));
/* reset object type */
object->type = RT_Object_Class_Null;
/* lock interrupt */
level = rt_hw_interrupt_disable();
/* remove from old list */
rt_list_remove(&(object->list));
/* unlock interrupt */
rt_hw_interrupt_enable(level);
/* free the memory of object */
RT_KERNEL_FREE(object);
}
静态删除
void rt_object_detach(rt_object_t object)
{
rt_base_t level;
/* object check */
RT_ASSERT(object != RT_NULL);
RT_OBJECT_HOOK_CALL(rt_object_detach_hook, (object));
/* reset object type */
object->type = 0;
/* lock interrupt */
level = rt_hw_interrupt_disable();
/* remove from old list */
rt_list_remove(&(object->list));
/* unlock interrupt */
rt_hw_interrupt_enable(level);
}