在学习cache的时候,有看到一系列的cache策略,这里汇总cache的策略,方便记忆
cache分cacheable和non-cacheable
non-cacheable就是数据没有cache,而cacheable则又可以细分为如下
我们知道到数据miss的时候会分配cache line,那么主要有两种分配方式
读取操作miss时,分配一个cache line,并记录cache
写入操作miss时,分配一个cache line,并记录cache
数据更新也就是write的时候,cache对回写的策略如下
写操作只会更新到cache,然后标记cache line为dirty,不会更新到内存中
写操作会直接更新到cache中,并同时更新到内存中
cache的共享属性分为 inner和outer,对于共享属性,我们可以这样记忆
操作系统在实现调度的时候会使用rb-tree,可见红黑树对于操作系统而言是多么的重要,本文首先介绍一下rb-tree的基本要素,后面连载逐渐了解rb-tree
对于一个树的结构,默认的实现应该如下
struct Node { struct Node *left; /* left element */ struct Node *right; /* right element */ }
这里通过Node就是树的节点,left是树的左子树,right是树的右子树
此时我们新增一个parent的Node指针,这样我们可以查找树里面的元素就无需每次从根节点遍历,结构体如下
struct Node { struct Node *left; /* left element */ struct Node *right; /* right element */ struct Node *parent; /* parent element */ }
对于红黑树,我们需要对Node添加颜色标识,可以如下
struct Node { struct Node *left; /* left element */ struct Node *right; /* right element */ struct Node *parent; /* parent element */ int rbe_color; /* node color */ }
这样,我们基本的红黑树的数据结构就完成了,接下来说明红黑树的特性
对于红黑树的基本特性,主要是如下四点
这里以 Data Structure Visualizations 网站绘图
https://www.cs.usfca.edu/~galles/visualization/RedBlack.html
下面展示一个从1-10共10个元素的红黑树图
可以发现:
红黑树的操作主要是左旋和右旋,以及插入和删除和搜索和染色,本文仅讨论左旋和右旋基本操作,插入,删除,搜索,染色等操作后面文章详细介绍。
关于左旋和右旋操作,是在红黑树出现不平衡的情况下,主动通过左旋和右旋调整使得整个红黑树平衡
对于左旋,就是红黑树不平衡的时候,对不平衡的节点向左旋转
假设有节点 x,y,a,b,r, 对其进行围绕x的左旋,如下所示
那么其具体步骤如下
简单理解就是,基于谁的节点左旋,就是将那个节点的右节点作为自己的父节点,然后重建叶子节点
为了更清楚的了解左旋步骤,我基于最上面10个元素的示例演示,先删除了节点8,则此时的树结构如下
此时因为节点7没有左孩子,但是节点10有左孩子,这并不平衡,所以需要对节点10进行右旋,右旋我们还没解释,简单理解就是将自己的左孩子作为自己的父亲。所以右旋后结构如下
我们发现节点9不平衡,其左孩子的黑色个数是3,而其他个数是4,所以将其染色如下
现在重点来了,但是此时节点7的左孩子是空,也就是其黑色节点数是3,我们需要左旋,步骤如下:
这样左旋完成之后,结果如下
此时还是黑色个数不对,我们将节点10染色成黑色即可。如下
我们介绍了左旋,对于右旋,就是红黑树不平衡的时候,对不平衡的节点向右旋转
假设有节点 x,y,a,b,r, 对齐进行围绕x的右旋,如下所示
那么其具体步骤如下
简单理解就是,基于谁的节点右旋,就是将那个节点的左节点作为自己的父节点,然后重建叶子节点
还是为了示例,还是基于上图删掉节点8之后的红黑树,此时我删除节点6,这里节点5会取代节点6,如下
此时红黑树在节点4上不平衡了,我们需要针对节点4进行右旋
步骤如下:
这样右旋完成之后,结果如下
最后发现节点2的黑色节点不平衡,个数不对,所以将节点1染色成黑色即可完成,如下
本文作为红黑树介绍的开头,简单介绍了红黑树的基础,例如红黑树的特性,红黑树的左旋和右旋操作。
rtems不仅支持基于优先级调度的算法,也支持简单优先级调度算法,简单优先级算法相比于优先级调度算法而言减少了bitmap,直接通过优先级和队列FIFO特性来更新调度优先级。本文介绍rtems中简单优先级的实现方法
我们通过_Scheduler_simple_Initialize
函数可以知道此调度算法的结构体构造,如下
void _Scheduler_simple_Initialize( const Scheduler_Control *scheduler ) { Scheduler_simple_Context *context = _Scheduler_simple_Get_context( scheduler ); _Chain_Initialize_empty( &context->Ready ); }
首先,需要构造一个Scheduler_simple_Context结构体,其成员如下
typedef struct { /** * @brief Basic scheduler context. */ Scheduler_Context Base; /** * @brief One ready queue for all ready threads. */ Chain_Control Ready; } Scheduler_simple_Context;
可以发现,此结构体的上下文只包含了调度器上下文和一个链表,这个链表就是此调度算法的实现
我们知道此调度算法通过链表,所以需要初始化链表如下
static inline void _Chain_Initialize_empty( Chain_Control *the_chain ) { Chain_Node *head; Chain_Node *tail; _Assert( the_chain != NULL ); head = _Chain_Head( the_chain ); tail = _Chain_Tail( the_chain ); head->next = tail; head->previous = NULL; tail->previous = head; }
对于基于队列的调度算法,其schedule函数就是将最高优先级的元素出队,如下
static inline Thread_Control *_Scheduler_simple_Get_highest_ready( const Scheduler_Control *scheduler ) { Scheduler_simple_Context *context = _Scheduler_simple_Get_context( scheduler ); return (Thread_Control *) _Chain_First( &context->Ready ); }
因为队列的FIFO结构,默认最高优先级的元素在队列头。所以直接从head->next拿即可。
对于基于队列的调度算法,阻塞任务的方式就是从队列中解压出此任务,让其不在就绪队列中,如下
static inline void _Chain_Extract_unprotected( Chain_Node *the_node ) { Chain_Node *next; Chain_Node *previous; _Assert( !_Chain_Is_node_off_chain( the_node ) ); next = the_node->next; previous = the_node->previous; next->previous = previous; previous->next = next; #if defined(RTEMS_DEBUG) _Chain_Set_off_chain( the_node ); #endif }
对于基于队列的调度算法,任务就绪主要两个部分
对于遍历就绪队列的代码如下
while ( next != tail && !( *order )( key, to_insert, next ) ) { previous = next; next = _Chain_Next( next ); }
这里的order就是根据遍历的元素,找到小于等于的队列位置,如下代码实现
static inline bool _Scheduler_simple_Priority_less_equal( const void *key, const Chain_Node *to_insert, const Chain_Node *next ) { const unsigned int *priority_to_insert; const Thread_Control *thread_next; (void) to_insert; priority_to_insert = (const unsigned int *) key; thread_next = (const Thread_Control *) next; return *priority_to_insert <= _Thread_Get_priority( thread_next ); }
根据上面的操作,可以找到待插入的位置,然后执行队列的插入操作即可
static inline void _Chain_Insert_unprotected( Chain_Node *after_node, Chain_Node *the_node ) { Chain_Node *before_node; _Assert( _Chain_Is_node_off_chain( the_node ) ); the_node->previous = after_node; before_node = after_node->next; after_node->next = the_node; the_node->next = before_node; before_node->previous = the_node; }
基于队列的优先级调度算法的更新优先级,主要如下操作
这里取出操作就是阻塞操作中的解压任务_Chain_Extract_unprotected
这里找到匹配的位置插入的操作和就绪任务一致
这里执行调度的操作就是让最高优先级的任务得以调度
至此,我们完全了解的简单优先级调度算法的逻辑了,其相比于优先级调度算法而言,没有使用位图。设计更加简单,因为位图占用一定的内存资源,故此调度器算法内存占用少。而又因为位图的时间复杂度是O(1),遍历队列链表的复杂度是O(n)。
故简单优先级调度算法,减少了内存占用,牺牲了一些性能,比较适合非常简单的任务系统
在rtems中,可以通过region来分配内存,这样分配的内存可以独立于malloc申请的内存,从而实现内存区域的管理。本文基于此分享如何使用内存区域
内存区域的作用是单独的内存区域,这个区域是固定的起始地址,大小和属性。使用内存区域的情况下,可以避免内存回收,内存规整等操作带来的性能消耗,同样的,也可以为多个任务创建不同的内存区域。
当然,在rtems中,如果分配内存区域的时候内存不足,也可以将任务进入等待队列,直到内存释放后有可用内存时。
内存区域的代码如下
rtems_status_code rtems_region_create( rtems_name name, void *starting_address, uintptr_t length, uintptr_t page_size, rtems_attribute attribute_set, rtems_id *id ) { rtems_status_code return_status; Region_Control *the_region; if ( !rtems_is_name_valid( name ) ) { return RTEMS_INVALID_NAME; } if ( id == NULL ) { return RTEMS_INVALID_ADDRESS; } if ( starting_address == NULL ) { return RTEMS_INVALID_ADDRESS; } the_region = _Region_Allocate(); if ( !the_region ) return_status = RTEMS_TOO_MANY; else { _Thread_queue_Object_initialize( &the_region->Wait_queue ); if ( _Attributes_Is_priority( attribute_set ) ) { the_region->wait_operations = &_Thread_queue_Operations_priority; } else { the_region->wait_operations = &_Thread_queue_Operations_FIFO; } the_region->maximum_segment_size = _Heap_Initialize( &the_region->Memory, starting_address, length, page_size ); if ( !the_region->maximum_segment_size ) { _Region_Free( the_region ); return_status = RTEMS_INVALID_SIZE; } else { the_region->attribute_set = attribute_set; *id = _Objects_Open_u32( &_Region_Information, &the_region->Object, name ); return_status = RTEMS_SUCCESSFUL; } } _Objects_Allocator_unlock(); return return_status; }
name:内存区域名称
starting_address:需要申请的内存地址
length:内存区域大小
page_size: 设置内存区域的页大小
attribute_set: 设置内存的调度属性
id: 内存区域id
rtems提供两种线程方式:一个是fifo,也就是先进先出调度,另一个是priority,也就是基于优先级的调度,分别如下:
const Thread_queue_Operations _Thread_queue_Operations_FIFO = { .priority_actions = _Thread_queue_Do_nothing_priority_actions, .enqueue = _Thread_queue_FIFO_enqueue, .extract = _Thread_queue_FIFO_extract, .surrender = _Thread_queue_FIFO_surrender, .first = _Thread_queue_FIFO_first }; const Thread_queue_Operations _Thread_queue_Operations_priority = { .priority_actions = _Thread_queue_Priority_priority_actions, .enqueue = _Thread_queue_Priority_enqueue, .extract = _Thread_queue_Priority_extract, .surrender = _Thread_queue_Priority_surrender, .first = _Thread_queue_Priority_first };
这个初始化的线程,会在内存不足的时候阻塞。
通过_Heap_Initialize直接申请堆作为内存区域的Memory成员。地址指向starting_address
对于这块内存,可以设置region结构体的属性字段,并通过_Objects_Open_u32申请内存区域id。region结构体如下
typedef struct { Objects_Control Object; Thread_queue_Control Wait_queue; /* waiting threads */ const Thread_queue_Operations *wait_operations; uintptr_t maximum_segment_size; /* in bytes */ rtems_attribute attribute_set; Heap_Control Memory; } Region_Control;
测试分配内存区域非常简单,我们可以直接通过rtems_region_create分配即可,如下:
static const rtems_name name = rtems_build_name('K', 'Y', 'L', 'I'); #define CONFIGURE_MAXIMUM_REGIONS 1 static void test_region_create(void) { rtems_status_code sc; rtems_id id; void *seg_addr1; void *seg_addr2; sc = rtems_region_create( name, 0x4000000, 640, 16, RTEMS_DEFAULT_ATTRIBUTES, &id ); printf( "sc=%d\n", sc); sc = rtems_region_get_segment( id, 32, RTEMS_DEFAULT_OPTIONS, RTEMS_NO_TIMEOUT, &seg_addr1 ); printf( "sc=%d seg_addr=%p\n", sc, seg_addr1); sc = rtems_region_get_segment( id, 32, RTEMS_DEFAULT_OPTIONS, RTEMS_NO_TIMEOUT, &seg_addr2 ); printf( "sc=%d seg_addr=%p\n", sc, seg_addr2); sc = rtems_region_return_segment( id, seg_addr1); printf( "sc=%d\n", sc); sc = rtems_region_return_segment( id, seg_addr2); printf( "sc=%d\n", sc); }
默认情况下,需要通过一个宏设置内存区域个数CONFIGURE_MAXIMUM_REGIONS,这里定义多少就代表系统最多可以申请多少region
内存区域最小分配为64字节,准确来说是大于等于52字节,这样内存区域才能正常工作。否则rtems_region_create返回无效
通过rtems_region_get_segment可以获取region的段,这样就能直接使用region
通过rtems_region_get_segment可以释放region的段,这样下一个程序可以重新分配
内存布局如下
(gdb) x/14g 0x4000000 0x4000000: 0x0000000004000280 0x0000000000000031 0x4000010: 0x0000000004000060 0x0000000000105d68 0x4000020: 0x0000000000000000 0x0000000000000000 0x4000030: 0x0000000000000030 0x0000000000000030 0x4000040: 0x0000000000105d68 0x0000000000105d68 0x4000050: 0x0000000000000000 0x0000000000000000 0x4000060: 0x0000000000000000 0x0000000000000211
从heap block结构体可以看到,内存分配一切正常
通过上述的实验,可以清楚的了解到rtems的region的分配和原理。这里分配固定地址上的内存块,供程序使用
rtems不仅支持malloc,还支持region分配,这两个都是基于heap来分配的,而partition的内存分配是直接通过链表管理的内存池,他不会记录block信息,本文介绍partition分配内存
复用测试region的代码,稍作修改可以直接测试partition,如下
#define CONFIGURE_MAXIMUM_PARTITIONS 1 static void test_partition_create(void) { rtems_status_code sc; rtems_id id; void *partition_buf1; void *partition_buf2; sc = rtems_partition_create( name, (void* )0x4000000, 128, 32, RTEMS_DEFAULT_ATTRIBUTES, &id ); printf( "sc=%d \n", sc); sc = rtems_partition_get_buffer( id, &partition_buf1); printf( "sc=%d buf1=%p\n", sc, partition_buf1); sc = rtems_partition_get_buffer( id, &partition_buf2); printf( "sc=%d buf2=%p\n", sc, partition_buf2); sc = rtems_partition_return_buffer(id, partition_buf1); printf( "sc=%d \n", sc); sc = rtems_partition_return_buffer(id, partition_buf2); printf( "sc=%d \n", sc); return ; }
直接测试效果如下:
(gdb) x/14xg 0x4000000 0x4000000: 0x0000000004000020 0x00000000001056d0 0x4000010: 0x0000000000000000 0x0000000000000000 0x4000020: 0x0000000004000040 0x00000000001056d0 0x4000030: 0x0000000000000000 0x0000000000000000 0x4000040: 0x0000000004000060 0x00000000001056d0 0x4000050: 0x0000000000000000 0x0000000000000000 0x4000060: 0x00000000001056d8 0x0000000004000040
可以发现,我申请了128内存,按照32字节划分,那么每块都是0x20大小,如果起始地址是0x4000000,则划分为如下
0x4000000/0x4000020/0x4000040/0x4000060
值得注意的是partition分配的内存,不会带block结构体,也就少了16字节的冗余,这种情况下,也不会参与内存的回收,规整,相当于蛋糕自己随便分,整体效率是最高的。
如果留意这些分区的内存的话,可以发现管理这些内存使用的是16字节的链表。接下来分析代码
创建分区代码如下
rtems_status_code rtems_partition_create( rtems_name name, void *starting_address, uintptr_t length, size_t buffer_size, rtems_attribute attribute_set, rtems_id *id ) { Partition_Control *the_partition; if ( !rtems_is_name_valid( name ) ) { return RTEMS_INVALID_NAME; } if ( id == NULL ) { return RTEMS_INVALID_ADDRESS; } if ( starting_address == NULL ) { return RTEMS_INVALID_ADDRESS; } if ( length == 0 ) { return RTEMS_INVALID_SIZE; } if ( buffer_size == 0 ) { return RTEMS_INVALID_SIZE; } if ( length < buffer_size ) { return RTEMS_INVALID_SIZE; } /* * Ensure that the buffer size is an integral multiple of the pointer size so * that each buffer begin meets the chain node alignment. */ if ( buffer_size % CPU_SIZEOF_POINTER != 0 ) { return RTEMS_INVALID_SIZE; } if ( buffer_size < sizeof( Chain_Node ) ) return RTEMS_INVALID_SIZE; /* * Ensure that the buffer area starting address is aligned on a pointer * boundary so that each buffer begin meets the chain node alignment. */ if ( (uintptr_t) starting_address % CPU_SIZEOF_POINTER != 0 ) { return RTEMS_INVALID_ADDRESS; } #if defined(RTEMS_MULTIPROCESSING) if ( !_System_state_Is_multiprocessing ) { attribute_set = _Attributes_Clear( attribute_set, RTEMS_GLOBAL ); } #endif the_partition = _Partition_Allocate(); if ( !the_partition ) { _Objects_Allocator_unlock(); return RTEMS_TOO_MANY; } #if defined(RTEMS_MULTIPROCESSING) if ( _Attributes_Is_global( attribute_set ) && !( _Objects_MP_Allocate_and_open( &_Partition_Information, name, the_partition->Object.id, false ) ) ) { _Objects_Free( &_Partition_Information, &the_partition->Object ); _Objects_Allocator_unlock(); return RTEMS_TOO_MANY; } #endif _Partition_Initialize( the_partition, starting_address, length, buffer_size, attribute_set ); *id = _Objects_Open_u32( &_Partition_Information, &the_partition->Object, name ); #if defined(RTEMS_MULTIPROCESSING) if ( _Attributes_Is_global( attribute_set ) ) _Partition_MP_Send_process_packet( PARTITION_MP_ANNOUNCE_CREATE, the_partition->Object.id, name, 0 /* Not used */ ); #endif _Objects_Allocator_unlock(); return RTEMS_SUCCESSFUL; }
这里的重点是
_Partition_Initialize( the_partition, starting_address, length, buffer_size, attribute_set );
可以查到_Partition_Initialize调用的_Chain_Initialize,我们查看_Chain_Initialize的实现如下
void _Chain_Initialize( Chain_Control *the_chain, void *starting_address, size_t number_nodes, size_t node_size ) { size_t count = number_nodes; Chain_Node *head = _Chain_Head( the_chain ); Chain_Node *tail = _Chain_Tail( the_chain ); Chain_Node *current = head; Chain_Node *next = starting_address; _Assert( node_size >= sizeof( *next ) ); head->previous = NULL; while ( count-- ) { current->next = next; next->previous = current; current = next; next = (Chain_Node *) _Addresses_Add_offset( (void *) next, node_size ); } current->next = tail; tail->previous = current; }
可以发现, 这里定义了一个双链表,其中count就是分区的数量,根据分区的数量直接填充链表的成员,链表非常简单,如下:
typedef struct Chain_Node { /** This points to the node after this one on this chain. */ struct Chain_Node *next; /** This points to the node immediate prior to this one on this chain. */ struct Chain_Node *previous; } Chain_Node;
可以发现,和测试运行中的内存布局完全一致。
rtems_partition_get_buffer函数的实现如下:
rtems_status_code rtems_partition_get_buffer( rtems_id id, void **buffer ) { Partition_Control *the_partition; ISR_lock_Context lock_context; void *the_buffer; if ( buffer == NULL ) { return RTEMS_INVALID_ADDRESS; } the_partition = _Partition_Get( id, &lock_context ); if ( the_partition == NULL ) { #if defined(RTEMS_MULTIPROCESSING) return _Partition_MP_Get_buffer( id, buffer ); #else return RTEMS_INVALID_ID; #endif } _Partition_Acquire_critical( the_partition, &lock_context ); the_buffer = _Partition_Allocate_buffer( the_partition ); if ( the_buffer == NULL ) { _Partition_Release( the_partition, &lock_context ); return RTEMS_UNSATISFIED; } the_partition->number_of_used_blocks += 1; _Partition_Release( the_partition, &lock_context ); *buffer = the_buffer; return RTEMS_SUCCESSFUL; }
代码熟悉了就发现非常简单,我们关注_Partition_Allocate_buffer,它调用_Chain_Get_unprotected,然后调用_Chain_Get_first_unprotected
_Chain_Get_first_unprotected
的实现如下
static inline Chain_Node *_Chain_Get_first_unprotected( Chain_Control *the_chain ) { Chain_Node *head; Chain_Node *old_first; Chain_Node *new_first; _Assert( !_Chain_Is_empty( the_chain ) ); head = _Chain_Head( the_chain ); old_first = head->next; new_first = old_first->next; head->next = new_first; new_first->previous = head; #if defined(RTEMS_DEBUG) _Chain_Set_off_chain( old_first ); #endif return old_first; }
可以发现,这里get buffer的操作就是取出head,然后通过head拿到next返回,并且将head->next->next返回给head→next,相当于list的del head操作
rtems_partition_return_buffer的实现如下:
rtems_status_code rtems_partition_return_buffer( rtems_id id, void *buffer ) { Partition_Control *the_partition; ISR_lock_Context lock_context; the_partition = _Partition_Get( id, &lock_context ); if ( the_partition == NULL ) { #if defined(RTEMS_MULTIPROCESSING) return _Partition_MP_Return_buffer( id, buffer ); #else return RTEMS_INVALID_ID; #endif } _Partition_Acquire_critical( the_partition, &lock_context ); if ( !_Partition_Is_address_a_buffer_begin( the_partition, buffer ) ) { _Partition_Release( the_partition, &lock_context ); return RTEMS_INVALID_ADDRESS; } _Partition_Free_buffer( the_partition, buffer ); the_partition->number_of_used_blocks -= 1; _Partition_Release( the_partition, &lock_context ); return RTEMS_SUCCESSFUL; }
同样的,我们关注_Partition_Free_buffer,从而关注_Chain_Append_unprotected,其实现如下
static inline void _Chain_Append_unprotected( Chain_Control *the_chain, Chain_Node *the_node ) { Chain_Node *tail; Chain_Node *old_last; _Assert( _Chain_Is_node_off_chain( the_node ) ); tail = _Chain_Tail( the_chain ); old_last = tail->previous; the_node->next = tail; tail->previous = the_node; old_last->next = the_node; the_node->previous = old_last; }
这里就是对双链表做了尾插,list add tail。
至此,关于rtems partition 内存的操作介绍完毕了。可以发现,如果不需要碎片管理,则partition的内存分配相当于内存池,简单高效。