Kernel Basics
Last updated
Last updated
Assoc. Prof. Wiroon Sriborrirux, Founder of Advance Innovation Center (AIC) and Bangsaen Design House (BDH), Electrical Engineering Department, Faculty of Engineering, Burapha University
This chapter introduces the basics of the RT-Thread kernel, including: an introduction to the kernel, the system startup process, and some contents of the kernel configuration, laying the foundation for the following chapters.
A brief introduction to the RT-Thread kernel, starting from the software architecture to explain the composition and implementation of the real-time kernel. This part introduces some concepts and basic knowledge related to the RT-Thread kernel to beginners, so that beginners can have a preliminary understanding of the kernel, know the components of the kernel, how to start the system, memory distribution, and kernel configuration methods.
The kernel is the core of an operating system and is the most basic and important part of an operating system. It is responsible for managing system threads, inter-thread communication, system clocks, interrupts, and memory. The following figure is the RT-Thread kernel architecture diagram. You can see that the kernel is above the hardware layer. The kernel part includes the kernel library and real-time kernel implementation.
The kernel library is a small set of C library-like function implementations that ensures the kernel can run independently. The C library included in this part varies depending on the compiler. When using the GNU GCC compiler, it will carry more standard C library implementations.
Tip: C library: also called C Runtime Library, it provides functions like "strcpy", "memcpy", and some also include the implementation of "printf" and "scanf". RT-Thread Kernel Service Library only provides a small part of the C library function implementation used by the kernel. In order to avoid duplication with the standard C library, the rt_ prefix is added before these functions.
The implementation of the real-time kernel includes: object management, thread management and scheduler, inter-thread communication management, clock management and memory management, etc. The minimum resource usage of the kernel is 3KB ROM and 1.2KB RAM.
Thread is the smallest scheduling unit in RT-Thread operating system. The thread scheduling algorithm is a priority-based fully preemptive multithread scheduling algorithm. That is, except for the interrupt handling function, the code of the locked part of the scheduler and the code that prohibits interrupts, the rest of the system can be preempted, including the thread scheduler itself. It supports 256 thread priorities (it can also be changed to support a maximum of 32 or 8 thread priorities through the configuration file. The default configuration for STM32 is 32 thread priorities). 0 priority represents the highest priority, and the lowest priority is reserved for idle threads. At the same time, it also supports the creation of multiple threads with the same priority. The threads of the same priority are scheduled using the round-robin scheduling algorithm of the time slice, so that each thread runs for a corresponding time. In addition, when the scheduler is looking for the highest priority threads in the ready state, the time it takes is constant. The system does not limit the number of threads. The number of threads is only related to the specific memory of the hardware platform.
Thread management will be introduced in detail in the "Thread Management" chapter.
RT-Thread's clock management is based on the clock beat, which is the smallest clock unit in the RT-Thread operating system. RT-Thread's timer provides two types of timer mechanisms: the first type is a single-trigger timer, which will only trigger a timer event once after it is started, and then the timer will stop automatically. The second type is a periodic trigger timer, which will periodically trigger a timer event until the user manually stops the timer, otherwise it will continue to execute forever.
In addition, depending on the context in which the timeout function is executed, the RT-Thread timer can be set to HARD_TIMER mode or SOFT_TIMER mode.
Usually, a timer callback function (i.e., timeout function) is used to complete the timing service. Users can select the appropriate type of timer based on their real-time requirements for timing processing.
Timers will be explained in detail in the "Clock Management" chapter.
RT-Thread uses semaphores, mutexes and event sets to achieve synchronization between threads. Threads synchronize by acquiring and releasing semaphores and mutexes; mutexes use priority inheritance to solve the priority flipping problem common in real-time systems. The thread synchronization mechanism supports threads to acquire semaphores or mutexes by priority waiting. Threads synchronize by sending and receiving events; event sets support "or triggering" and "and triggering" of multiple events, which is suitable for situations where threads are waiting for multiple events.
The concepts of semaphores, mutexes, and event sets will be introduced in detail in the "Inter-thread Synchronization" chapter.
RT-Thread supports communication mechanisms such as mailboxes and message queues. The length of an email in a mailbox is fixed at 4 bytes; a message queue can receive messages of variable length and cache them in its own memory space. Mailboxes are more efficient than message queues. The sending actions of mailboxes and message queues can be safely used in interrupt service routines. The communication mechanism supports threads to obtain by priority waiting.
The concepts of mailboxes and message queues will be introduced in detail in the "Inter-thread Communication" chapter.
RT-Thread supports static memory pool management and dynamic memory heap management. When the static memory pool has available memory, the system will allocate memory blocks at a constant time; when the static memory pool is empty, the system will suspend or block the thread that applied for the memory block (that is, the thread will give up the application and return if it has not obtained the memory block after waiting for a period of time, or return immediately. The waiting time depends on the waiting time parameter set when applying for the memory block). When other threads release memory blocks to the memory pool, if there is a thread that is suspended to allocate memory blocks, the system will wake up this thread.
The dynamic memory heap management module provides a memory management algorithm for a small memory system and a SLAB memory management algorithm for a large memory system under different system resources.
There is also a dynamic memory heap management called memheap, which is suitable for systems with multiple addresses and non-contiguous memory heaps. Using memheap, multiple memory heaps can be "pasted" together, allowing users to operate as if they were operating one memory heap.
The concept of memory management will be explained in the "Memory Management" chapter.
RT-Thread uses PIN, I2C, SPI, USB, UART, etc. as peripheral devices, which are uniformly registered through devices. It implements a device management subsystem that can be accessed by name, and can access hardware devices according to a unified API interface. On the device driver interface, according to the characteristics of the embedded system, corresponding events can be attached to different devices. When a device event is triggered, the driver notifies the upper-level application.
The concept of I/O device management will be explained in the "Device Model" and "General Device" chapters.
Generally, understanding a code mostly starts from the startup part. This method is also used here to find the source of startup first. RT-Thread supports multiple platforms and multiple compilers, and the rtthread_startup() function is the unified startup entry specified by RT-Thread. The general execution order is: the system starts running from the startup file, then enters the startup function rtthread_startup() of RT-Thread, and finally enters the user entry function main(), as shown in the following figure:
Taking MDK-ARM as an example, the user program entry is the main() function, which is located in the main.c file. After the system starts, it starts running from the assembly code startup_stm32f103xe.s, then jumps to the C code to start the RT-Thread system, and finally enters the user program entry function main().
In order to complete the RT-Thread system function initialization before entering main(), we use the MDK extension functions $Sub$$
and $Super$$
. You can add $Sub$$
the prefix symbol to main as a new function $Sub$$main
. This $Sub$$main
can first call some function functions to be added before main (here add RT-Thread system startup, perform a series of system initialization), and then call $Super$$main
to go to main() function execution, so that users do not need to care about the system initialization operations before main().
For details on how to use the $Sub$$
and $Super$$
extended functions, see the ARM® Compiler v5.06 for µVision®armlink User Guide .
Let's take a look at this code defined in components.c:
Here $Sub$$main
the function calls the rtthread_startup() function, where the code of the rtthread_startup() function is as follows:
This part of the startup code can be roughly divided into four parts:
(1) Initialize system-related hardware;
(2) Initialize system kernel objects, such as timers, schedulers, and signals;
(3) Create the main thread and initialize various modules in the main thread in turn;
(4) Initialize the timer thread, idle thread, and start the scheduler.
Before starting the scheduler, the threads created by the system will not run immediately after executing rt_thread_startup(). They will be in the ready state waiting for system scheduling. After starting the scheduler, the system will switch to the first thread to start running. According to the scheduling rules, the thread with the highest priority in the ready queue is selected.
In rt_hw_board_init(), the system clock is set, heartbeat and serial port initialization are provided for the system, and the system input and output terminals are bound to this serial port. Subsequent system operation information will be printed out from the serial port.
The main() function is the user code entry of RT-Thread. Users can add their own applications in the main() function.
Generally, the storage space contained in MCU includes: on-chip Flash and on-chip RAM. RAM is equivalent to internal memory, and Flash is equivalent to hard disk. The compiler will classify a program into several parts and store them in different storage areas of MCU.
After the Keil project is compiled, there will be a prompt message about the space occupied by the corresponding program, as shown below:
The Program Size mentioned above contains the following parts:
1) Code: Code segment, which stores the code part of the program;
2) RO-data: read-only data segment, storing constants defined in the program;
3) RW-data: read-write data segment, storing global variables initialized to non-zero values;
4) ZI-data: 0 data segment, storing uninitialized global variables and variables initialized to 0;
After compiling the project, a .map
file will be generated, which describes the size and address occupied by each function. The last few lines of the file also explain the relationship between the above fields:
1) RO Size includes Code and RO-data, indicating the size of the Flash space occupied by the program;
2) RW Size includes RW-data and ZI-data, indicating the size of RAM occupied during runtime;
3) ROM Size includes Code, RO-data and RW-data, indicating the size of the Flash space occupied by the burning program;
Before the program runs, a file entity needs to be burned into the STM32 Flash, usually a bin or hex file. The burned file is called an executable image file. As shown in the left part of the figure below, it is the memory distribution after the executable image file is burned into the STM32. It contains two parts: RO segment and RW segment: the RO segment stores the Code and RO-data data, and the RW segment stores the RW-data data. Since ZI-data is 0, it is not included in the image file.
After power-on, STM32 starts from Flash by default. After startup, the RW-data (initialized global variables) in the RW segment will be moved to RAM, but the RO segment will not be moved. That is, the CPU execution code is read from Flash. In addition, the ZI segment is allocated according to the ZI address and size given by the compiler, and this RAM area is cleared.
The dynamic memory heap is the unused RAM space, and the memory blocks requested and released by the application all come from this space.
As in the following example:
The 128-byte memory space pointed to by the msg_ptr pointer in the code is located in the dynamic memory heap space.
Some global variables are stored in the RW segment and the ZI segment. The RW segment stores global variables with initial values (global variables in the form of constants are placed in the RO segment and are read-only). The ZI segment stores uninitialized global variables of the system, as shown in the following example:
The sensor_value is stored in the ZI segment and is automatically initialized to zero after the system starts (initialized to zero by the user program or some library functions provided by the compiler). The sensor_inited variable is stored in the RW segment, and sensor_enable is stored in the RO segment.
The automatic initialization mechanism means that the initialization function does not need to be called explicitly. It only needs to be declared through macro definition at the function definition, and it will be executed during the system startup process.
For example, in the serial port driver, a macro definition is called to inform the system to initialize the function that needs to be called. The code is as follows:
The INIT_BOARD_EXPORT(rt_hw_usart_init) at the end of the sample code indicates the use of the automatic initialization function. In this way, the rt_hw_usart_init() function will be automatically called by the system, so where is it called?
In the system startup flowchart, there are two functions: rt_components_board_init() and rt_components_init(). The functions in the background box after them are automatically initialized functions, among which:
“Board init functions” are all initialization functions declared via INIT_BOARD_EXPORT(fn).
“Pre-initialization functions” are all initialization functions declared via INIT_PREV_EXPORT(fn).
“Device init functions” are all initialization functions declared via INIT_DEVICE_EXPORT(fn).
“components init functions” are all initialization functions declared via INIT_COMPONENT_EXPORT(fn).
“Environment init functions” are all initialization functions declared via INIT_ENV_EXPORT(fn).
“Application init functions” are all initialization functions declared via INIT_APP_EXPORT(fn).
The rt_components_board_init() function is executed relatively early, mainly to initialize the relevant hardware environment. When executing this function, it will traverse the initialization function table declared by INIT_BOARD_EXPORT(fn) and call each function.
The rt_components_init() function will be called and executed in the main thread created after the operating system is running. At this time, the hardware environment and the operating system have been initialized and the application-related code can be executed. The rt_components_init() function will traverse the initialization function table declared by the remaining macros.
RT-Thread's automatic initialization mechanism uses a custom RTI symbol segment, and places the function pointers that need to be initialized at startup in this segment, forming an initialization function table. During the system startup process, the table will be traversed and the functions in the table will be called to achieve the purpose of automatic initialization.
The macro interface definition used to implement the automatic initialization function is described in detail in the following table:
Initialization Order
Macro interface
describe
1
INIT_BOARD_EXPORT(fn)
Very early initialization, when the scheduler has not yet started
2
INIT_PREV_EXPORT(fn)
Mainly used for pure software initialization, functions without too many dependencies
3
INIT_DEVICE_EXPORT(fn)
Peripheral driver initialization related, such as network card devices
4
INIT_COMPONENT_EXPORT(fn)
Component initialization, such as the file system or LWIP
5
INIT_ENV_EXPORT(fn)
System environment initialization, such as mounting the file system
6
INIT_APP_EXPORT(fn)
Application initialization, such as GUI applications
The initialization function is actively declared through these macro interfaces, such as INIT_BOARD_EXPORT(rt_hw_usart_init). The linker will automatically collect all declared initialization functions and put them into the RTI symbol segment, which is located in the RO segment of the memory distribution. All functions in the RTI symbol segment will be automatically called when the system is initialized.
The RT-Thread kernel is designed with object-oriented design ideas. System-level infrastructure is a kernel object, such as threads, semaphores, mutexes, timers, etc. Kernel objects are divided into two categories: static kernel objects and dynamic kernel objects. Static kernel objects are usually placed in the RW segment and ZI segment and initialized in the program after the system starts; dynamic kernel objects are created from the memory heap and then initialized manually.
The following code is an example of static thread and dynamic thread:
In this example, thread1 is a static thread object, and thread2 is a dynamic thread object. The memory space of thread1 object, including thread control block thread1 and stack space thread1_stack, is determined at compile time. Since there is no initial value in the code, they are all placed in the uninitialized data segment. The space used by thread2 during operation is dynamically allocated, including thread control block (the content pointed to by thread2_ptr) and stack space.
Static objects occupy RAM space, do not rely on the memory heap manager, and the memory allocation time is determined. Dynamic objects rely on the memory heap manager, apply for RAM space at runtime, and when the object is deleted, the occupied RAM space is released. Both methods have their own advantages and disadvantages, and you can choose the specific usage method according to the actual environment requirements.
RT-Thread uses a kernel object management system to access/manage all kernel objects. Kernel objects contain most of the facilities in the kernel. These kernel objects can be static objects allocated statically or dynamic objects allocated from the system memory heap.
Through this kernel object design, RT-Thread is independent of the specific memory allocation method, and the flexibility of the system is greatly improved.
RT-Thread kernel objects include: threads, semaphores, mutexes, events, mailboxes, message queues and timers, memory pools, device drivers, etc. The object container contains information about each type of kernel object, including object type, size, etc. The object container assigns a linked list to each type of kernel object, and all kernel objects are linked to the linked list. The kernel object container and linked list of RT-Thread are shown in the figure below:
The following figure shows the derivation and inheritance relationship of various kernel objects in RT-Thread. For each specific kernel object and object control block, in addition to the basic structure, there are also its own extended attributes (private attributes). For example, for the thread control block, it is extended on the basis of the base class object, adding attributes such as thread state and priority. These attributes will not be used in the operation of the base class object, but only in the operation related to the specific thread. Therefore, from the object-oriented point of view, it can be considered that each specific object is derived from the abstract object, inheriting the attributes of the basic object and extending the attributes related to itself on this basis.
In the object management module, a general data structure is defined to store the common attributes of various objects. Various specific objects only need to add some special attributes of their own on this basis to clearly express their own characteristics.
The advantages of this design approach are:
(1) The reusability and extensibility of the system are improved. It is easy to add new object categories by simply inheriting the properties of common objects and adding a small amount of extensions.
(2) Providing a unified object operation method simplifies the operation of various specific objects and improves the reliability of the system.
In the figure above, objects derived from the object control block rt_object include: thread objects, memory pool objects, timer objects, device objects, and IPC objects (IPC: Inter-Process Communication. In the RT-Thread real-time operating system, the function of the IPC object is to synchronize and communicate between threads); objects such as semaphores, mutexes, events, mailboxes and message queues, and signals are derived from the IPC object.
The data structure of the kernel object control block:
The types currently supported by kernel objects are as follows:
From the above type description, we can see that if it is a static object, the highest bit of the object type will be 1 (RT_Object_Class_Static and other object types are ORed together), otherwise it is a dynamic object. The maximum number of object classes that the system can accommodate is 127.
The data structure of the kernel object container:
A class of objects is managed by an rt_object_information structure, and each specific instance of this class of objects is attached to object_list in the form of a linked list. The memory block size of this class of objects is identified by object_size (the specific instances of each class of objects occupy the same memory block size).
Before using an uninitialized static object, you must initialize it. The initialization object uses the following interface:
When this function is called to initialize an object, the system will place the object in an object container for management, that is, initialize some parameters of the object, and then insert the object node into the object linked list of the object container. The description of the input parameters of this function is as follows:
parameter
describe
object
The object pointer that needs to be initialized must point to a specific object memory block and cannot be a null pointer or a wild pointer.
type
The type of the object must be a type other than RT_Object_Class_Static listed in the rt_object_class_type enumeration type (for static objects, or objects initialized using the rt_object_init interface, the system will identify them as RT_Object_Class_Static type)
name
The name of the object. Each object can be set with a name. The maximum length of this name is specified by RT_NAME_MAX, and the system does not care whether it is \0
terminated by ' '.
Detaching an object from the kernel object manager. Detaching an object uses the following interface:
Calling this interface can make a static kernel object detach from the kernel object container, that is, delete the corresponding object node from the kernel object container linked list. After the object is detached, the memory occupied by the object will not be released.
The above descriptions are all interfaces for object initialization and detachment, which are all based on the situation where the object-oriented memory block already exists. Dynamic objects can be applied for when needed, and the memory space can be released for other applications when not needed. The following interfaces can be used to apply for the allocation of new objects:
When calling the above interface, the system first needs to obtain object information according to the object type (especially the size information of the object type so that the system can allocate a memory data block of the correct size), and then allocate the memory space of the corresponding size of the object from the memory heap, and then perform necessary initialization on the object, and finally insert it into the object container linked list where it is located. The description of the input parameters of this function is as follows:
parameter
describe
type
The type of the allocated object can only be a type other than RT_Object_Class_Static in rt_object_class_type. And the object type allocated through this interface is dynamic, not static.
name
The name of the object. Each object can be set with a name. The maximum length of this name is specified by RT_NAME_MAX, and the system does not care whether it is \0
terminated by ' '.
return
——
The object handle allocated successfully
Allocation success
RT_NULL
Allocation Failure
For a dynamic object, when it is no longer used, you can call the following interface to delete the object and release the corresponding system resources:
When the above interface is called, the object is first detached from the object container list, and then the memory occupied by the object is released. The following table describes the input parameters of this function:
parameter
describe
object
The handle of the object
Determine whether the specified object is a system object (static kernel object). The following interface is used to identify the object:
Calling the rt_object_is_systemobject interface can determine whether an object is a system object. In the RT-Thread operating system, a system object is also a static object, and the RT_Object_Class_Static bit on the object type identifier is set. Objects that are usually initialized using the rt_object_init() method are system objects. The input parameters of this function are described in the following table:
Input parameters for rt_object_is_systemobject()
parameter
describe
object
The handle of the object
Take traversing all threads as an example:
Note: RT-Thread5.0 and higher versions
struct rt_thread
move the name member of the structure to parent. When using it, the code needs to bethread->name
changed fromthread->parent.name
, otherwise the compilation will report an error!
Let's take traversing all mutexes as an example:
An important feature of RT-Thread is its high scalability, which supports fine-tuning of the kernel and flexible disassembly of components.
Configuration is mainly performed by modifying the rtconfig.h file in the project directory. Users can conditionally compile the code by turning on/off the macro definitions in the file, ultimately achieving the purpose of system configuration and tailoring, as follows:
(1) RT-Thread kernel
(2) Inter-thread synchronization and communication part. The objects used in this part include semaphores, mutexes, events, mailboxes, message queues, signals, etc.
(3) Memory management part
(4) Kernel device objects
(5) Automatic initialization method
(6)FinSH
(7) About MCU
Note
Note: In actual applications, the system configuration file rtconfig.h is automatically generated by the configuration tool and does not need to be changed manually.
Some macro definitions are often used in RT-Thread. Here are some common macro definitions in the Keil compilation environment:
1) rt_inline, defined as follows, the static keyword is used to make the function only available in the current file; inline means inline, and after being modified with static, the compiler will be advised to perform inline expansion when calling the function.
2) RT_USED, defined as follows, is used to tell the compiler that this code is useful and should be compiled even if it is not called in the function. For example, the RT-Thread automatic initialization function uses a custom segment, and using RT_USED will keep the custom code segment.
Note: RT-Thread 5.0 and later versions have
RT_USED
changed the keyword tort_used
, so please pay attention to the modification when using it.
3) RT_UNUSED, defined as follows, indicates that the function or variable may not be used. This attribute can prevent the compiler from generating warning messages.
4) RT_WEAK, defined as follows, is often used to define functions. When linking functions, the compiler will give priority to linking functions without the keyword prefix. If it cannot find the function, it will link the function modified by weak.
Note: RT-Thread 5.0 and later versions have
RT_WEAK
changed the keyword tort_weak
, so please pay attention to the modification when using it.
5) ALIGN(n), defined as follows, is used to align the address stored in an object to n bytes when allocating address space to an object, where n can be a power of 2. Byte alignment is not only for fast CPU access, but also can effectively save storage space if used properly.
Note: RT-Thread 5.0 and later versions have
ALIGN
changed the keyword tort_align
, so please pay attention to the modification when using it.
6) RT_ALIGN(size,align), defined as follows, raises size to a multiple of the integer defined by align. For example, RT_ALIGN(13,4) returns 16.