2021SC@SDUSC
文章目录
一. TencentOS tiny中的进程
**进程,是竞争系统资源的最小运行单元。**TencentOS tiny支持多进程并发运行,进程可以使用或等待CPU、使用内存空间等系统资源,并独立于其它进程运行。TencentOS tiny的进程可认为是一系列独立进程的集合。每个进程在自己的环境中运行。在任何时刻,只有一个进程得到运行,由TencentOS tiny调度器决定运行哪个进程。从宏观看上去所有的进程都在同时在执行。
二. TencentOS Tiny的进程状态
进程状态切换(一副简化版的切换图):
然而进程的状态在TencentOS tiny中要复杂的多,有以下几种:
- 就绪态(K_TASK_STATE_READY):该进程在就绪列表中,就绪的进程已经具备执行的能力,只等待调度器进行调度,新创建的进程会初始化为就绪态。
- 运行态(K_TASK_STATE_READY):该状态表明进程正在执行,此时它占用处理器,其实此时的进程还是处于就绪列表中的,TencentOS调度器选择运行的永远是处于最高优先级的就绪态进程,当进程被运行的一刻,它的进程状态就变成了运行态。
- 阻塞态(K_TASK_STATE_SLEEP):如果进程当前正在休眠让出CPU使用权,那么就可以说这个进程处于休眠状态,该进程不在就绪列表中,此时进程处于阻塞列表中。
- 等待态(K_TASK_STATE_PEND):进程正在等待信号量、队列或者等待事件等状态。
- 挂起态(K_TASK_STATE_SUSPENDED):进程被挂起,此时进程对调度器而言是不可见的。
- 退出态(K_TASK_STATE_DELETED):该进程运行结束,并且被删除。
- 等待超时状态(K_TASK_STATE_PENDTIMEOUT):进程正在等待信号量、队列或者等待事件发生超时的状态。
- 阻塞挂起态(K_TASK_STATE_SLEEP_SUSPENDED):进程在阻塞中被挂起时的状态。
- 等待挂起态(K_TASK_STATE_PEND_SUSPENDED):进程正在等待信号量、队列或者等待事件时被挂起的状态。
- 等待超时挂起态(K_TASK_STATE_PENDTIMEOUT_SUSPENDED):进程正在等待信号量、队列或者等待事件发生超时,但此时进程已经被挂起的状态。
关于进程状态的源码部分:
// ready to schedule
// a task's pend_list is in readyqueue
#define K_TASK_STATE_READY (k_task_state_t)0x0000
// delayed, or pend for a timeout
// a task's tick_list is in k_tick_list
#define K_TASK_STATE_SLEEP (k_task_state_t)0x0001
// pend for something
// a task's pend_list is in some pend object's list
#define K_TASK_STATE_PEND (k_task_state_t)0x0002
// suspended
#define K_TASK_STATE_SUSPENDED (k_task_state_t)0x0004
// deleted
#define K_TASK_STATE_DELETED (k_task_state_t)0x0008
// actually we don't really need those TASK_STATE below, if you understand the task state deeply, the code can be much more elegant.
// we are pending, also we are waitting for a timeout(eg. tos_sem_pend with a valid timeout, not TOS_TIME_FOREVER)
// both a task's tick_list and pend_list is not empty
#define K_TASK_STATE_PENDTIMEOUT (k_task_state_t)(K_TASK_STATE_PEND | K_TASK_STATE_SLEEP)
// suspended when sleeping
#define K_TASK_STATE_SLEEP_SUSPENDED (k_task_state_t)(K_TASK_STATE_SLEEP | K_TASK_STATE_SUSPENDED)
// suspened when pending
#define K_TASK_STATE_PEND_SUSPENDED (k_task_state_t)(K_TASK_STATE_PEND | K_TASK_STATE_SUSPENDED)
// suspended when pendtimeout
#define K_TASK_STATE_PENDTIMEOUT_SUSPENDED (k_task_state_t)(K_TASK_STATE_PENDTIMEOUT | K_TASK_STATE_SUSPENDED)
三. TencentOS Tiny的进程调度
TencentOS Tiny中不同优先级之间的进程调度是抢占式调度机制,高优先级的进程可打断低优先级进程,低优先级进程必须在高优先级进程阻塞或结束后才能得到调度。
相同优先级之间的调度采用的是时间片轮转调度,需要注意的是,在TencentOS Tiny中,创建进程时会指定进程的时间片长度,也就是说,这里的时间片是针对同一个进程来说的, 当一个进程A执行完它的一个时间片A1,TencentOS Tiny就调度到下一个进程B执行其时间片B1
四. TencentOS Tiny维护进程的数据结构
(一)就绪列表
TencentOS tiny用c中的结构体维护一条就绪列表,用于挂载系统中的所有处于就绪态的进程,他是readyqueue_t
类型的列表,源码如下:
readyqueue_t k_rdyq;
typedef struct readyqueue_st {
k_list_t task_list_head[TOS_CFG_TASK_PRIO_MAX];
uint32_t prio_mask[K_PRIO_TBL_SIZE];
k_prio_t highest_prio;
} readyqueue_t;
对源码的解释:
-
task_list_head
是列表类型
k_list_t的数组,TencentOS tiny为每个优先级的进程都分配一个列表,系统支持最大优先级为
TOS_CFG_TASK_PRIO_MAX -
prio_mask
则是优先级掩码数组,它是一个类型为32位变量的数组**(即每一个数组单元内的值最多可以表示32个优先级)**,数组成员个数由TOS_CFG_TASK_PRIO_MAX(最大优先级)
决定,当TOS_CFG_TASK_PRIO_MAX不超过32时数组成员变量只有一个,就是32位的变量数值,那么该变量的每一位代表一个优先级。比如当TOS_CFG_TASK_PRIO_MAX为64时,prio_mask[0]变量的每一位(bit)代表0-31优先级,而prio_mask[1]变量的每一位代表32-63优先级。#define K_PRIO_TBL_SIZE ((TOS_CFG_TASK_PRIO_MAX + 31) / 32)
-
highest_prio
则是记录当前优先级列表的最高优先级,方便索引task_list_head
(二)阻塞列表
与系统时间相关的进程都会被挂载到这个列表中,可能是阻塞、有期限地等待信号量、事件、消息队列等情况
k_list_t k_tick_list;
(三)进程控制块
进程控制块里存有进程的相关信息,比如进程的栈指针,进程名称,进程状态,进程的时间片等,以后系统对进程的全部操作都可以通过这个进程控制块来实现。
相关源码展现:
可以看出TencentOS Tiny是采用了c中的结构体来维护一个进程控制块
typedef struct k_task_st {
k_stack_t *sp; /**< 进程栈指针,用于切换上下文*/
#if TOS_CFG_OBJECT_VERIFY_EN > 0u
knl_obj_t knl_obj; /**< 只是为了验证,测试当前对象是否真的是一项进程。*/
#endif
char *name; /**< 进程名称 */
k_task_entry_t entry; /**< 进程主体 */
void *arg; /**< 进程主体形参 */
k_task_state_t state; /**< 进程状态 */
k_prio_t prio; /**< 进程优先级 */
k_stack_t *stk_base; /**< 进程栈基地址 */
size_t stk_size; /**< 进程栈大小 */
k_tick_t tick_expires; /**< 进程阻塞的时间 */
k_list_t tick_list; /**< 阻塞列表 */
k_list_t pend_list; /**< 就绪、等待列表 */
#if TOS_CFG_MUTEX_EN > 0u
k_list_t mutex_own_list; /**< 进程拥有的互斥量 */
k_prio_t prio_pending; /*< 用于记录持有互斥量的进程初始优先级,在优先级继承中使用 */
#endif
pend_obj_t *pending_obj; /**< 记录进程此时挂载到的列表 */
pend_state_t pend_state; /**< 等待被唤醒的原因(状态) */
#if TOS_CFG_ROUND_ROBIN_EN > 0u
k_timeslice_t timeslice_reload; /**< 时间片初始值(重装载值) */
k_timeslice_t timeslice; /**< 剩余时间片 */
#endif
#if TOS_CFG_MSG_EN > 0u
void *msg_addr; /**< 保存接收到的消息 */
size_t msg_size; /**< 保存接收到的消息大小 */
#endif
#if TOS_CFG_EVENT_EN > 0u
k_opt_t opt_event_pend; /**< 等待事件的的操作类型:TOS_OPT_EVENT_PEND_ANY 、 TOS_OPT_EVENT_PEND_ALL */
k_event_flag_t flag_expect; /**< 期待发生的事件 */
k_event_flag_t *flag_match; /**< 等待到的事件 */
#endif
} k_task_t;
五. TencentOS Tiny的进程操作
在TencentOS tiny中,凡是使用__API__修饰的函数都是提供给用户使用的,而使用__KERNEL__修饰的代码则是给内核使用的。
在TencentOS tiny中,命名上:TOS_或tos_代表的是 TencentOS Tiny 的缩写
(一)创建进程
在TencentOS tiny中,不能创建与空闲进程K_TASK_PRIO_IDLE
相同优先级的进程,空闲进程是最低优先级的进程,相同优先级下的进程需要允许使用时间片调度,打开TOS_CFG_ROUND_ROBIN_EN
。
TencentOS的创建进程函数的参数列表:
参数 | 含义 |
---|---|
task | 进程控制块 |
name | 进程名字 |
entry | 进程主体 |
arg | 进程形参 |
prio | 优先级 |
stk_base | 进程栈基地址 |
stk_size | 进程栈大小 |
timeslice | 时间片 |
参数详解(来源TencentOS tiny开发指南):
-
task
这是一个k_task_t类型的指针,k_task_t是内核的进程结构体类型。注意:task指针,应该指向生命周期大于待创建进程体生命周期的k_task_t类型变量,如果该指针指向的变量生命周期比待创建的进程体生命周期短,譬如可能是一个生命周期极端的函数栈上变量,可能会出现进程体还在运行而k_task_t变量已被销毁,会导致系统调度出现不可预知问题。
-
name
指向进程名字符串的指针。注意:同task,该指针指向的字符串生命周期应该大于待创建的进程体生命周期,一般来说,传入字符串常量指针即可。
-
entry
进程体运行的函数入口。当进程创建完毕进入运行状态后,entry是进程执行的入口,用户可以在此函数中编写业务逻辑。
-
arg
传递给进程入口函数的参数。
-
prio
进程优先级。prio的数值越小,优先级越高。用户可以在tos_config.h中,通过TOS_CFG_TASK_PRIO_MAX来配置进程优先级的最大数值,在内核的实现中,idle进程的优先级会被分配为TOS_CFG_TASK_PRIO_MAX - 1,此优先级只能被idle进程使用。因此对于一个用户创建的进程来说,合理的优先级范围应该为[0, TOS_CFG_TASK_PRIO_MAX - 2]。另外TOS_CFG_TASK_PRIO_MAX的配置值必需大于等于8。
-
stk_base
进程在运行时使用的栈空间的起始地址。注意:同task,该指针指向的内存空间的生命周期应该大于待创建的进程体生命周期。stk_base是k_stack_t类型的数组起始地址。
-
stk_size
进程的栈空间大小。注意:因为stk_base是k_stack_t类型的数组指针,因此实际栈空间所占内存大小为stk_size * sizeof(k_stack_t)。
-
timeslice
时间片轮转机制下当前进程的时间片大小。当timeslice为0时,进程调度时间片会被设置为默认大小(TOS_CFG_CPU_TICK_PER_SECOND / 10),系统时钟滴答(systick)数 / 10。
创建进程的实现如下:首先对参数进行检查,然后调用cpu_task_stk_init函数将进程栈进行初始化,并且将传入的参数记录到进程控制块中。如果打开了TOS_CFG_ROUND_ROBIN_EN宏定义,则表示支持时间片调度,则需要配置时间片相关的信息timeslice到进程控制块中。然后调用task_state_set_ready函数将新创建的进程设置为就绪态K_TASK_STATE_READY(创建好了要设置为就绪态),再调用readyqueue_add_tail函数将进程插入就绪列表k_rdyq中。如果调度器运行起来了,则进行一次进程调度。
创建进程相关源码如下:
__API__ k_err_t tos_task_create(k_task_t *task,
char *name,
k_task_entry_t entry,
void *arg,
k_prio_t prio,
k_stack_t *stk_base,
size_t stk_size,
k_timeslice_t timeslice)
{
TOS_CPU_CPSR_ALLOC();
TOS_IN_IRQ_CHECK();
TOS_PTR_SANITY_CHECK(task);
TOS_PTR_SANITY_CHECK(entry);
TOS_PTR_SANITY_CHECK(stk_base);
if (unlikely(stk_size < sizeof(cpu_context_t))) {
return K_ERR_TASK_STK_SIZE_INVALID;
}
if (unlikely(prio == K_TASK_PRIO_IDLE && !knl_is_idle(task))) {
return K_ERR_TASK_PRIO_INVALID;
}
if (unlikely(prio > K_TASK_PRIO_IDLE)) {
return K_ERR_TASK_PRIO_INVALID;
}
task_reset(task);
#if TOS_CFG_OBJECT_VERIFY_EN > 0u
knl_object_init(&task->knl_obj, KNL_OBJ_TYPE_TASK);
#endif
task->sp = cpu_task_stk_init((void *)entry, arg, (void *)task_exit, stk_base, stk_size);
task->entry = entry;
task->arg = arg;
task->name = name;
task->prio = prio;
task->stk_base = stk_base;
task->stk_size = stk_size;
#if TOS_CFG_ROUND_ROBIN_EN > 0u
task->timeslice_reload = timeslice;
if (timeslice == (k_timeslice_t)0u) {
task->timeslice = k_robin_default_timeslice;
} else {
task->timeslice = timeslice;
}
#endif
TOS_CPU_INT_DISABLE();
task_state_set_ready(task);
readyqueue_add_tail(task);
TOS_CPU_INT_ENABLE();
if (tos_knl_is_running()) {
knl_sched();
}
return K_ERR_NONE;
}
(二)阻塞进程
进程阻塞非常简单,主要的思路:将进程从就绪列表移除,然后添加到阻塞列表中k_tick_list,如果调度器被锁,直接返回错误代码K_ERR_SCHED_LOCKED,如果阻塞时间为0,则调用tos_task_yield函数发起一次进程调度;调用tick_list_add函数将进程插入阻塞列表中,阻塞的时间delay是由用户指定的。不过需要注意的是如果进程阻塞的时间是永久阻塞TOS_TIME_FOREVER,将返回错误代码K_ERR_DELAY_FOREVER,这是因为进程阻塞是主动行为,如果永久阻塞了,将没法主动唤醒,而进程等待事件、信号量、消息队列等行为是被动行为,可以是永久等待,一旦事件发生了、信号量被释放、消息队列不为空时进程就会被唤醒,这是被动行为,这两点需要区分开来。最后调用readyqueue_remove函数将进程从就绪列表中移除,然后调用knl_sched函数发起一次进程调度,就能切换另一个进程。移除之后需要发起一次进程调度切换进程
阻塞进程相关源码如下:
__API__ k_err_t tos_task_delay(k_tick_t delay)
{
TOS_CPU_CPSR_ALLOC();
TOS_IN_IRQ_CHECK();
if (knl_is_sched_locked()) {
return K_ERR_SCHED_LOCKED;
}
if (unlikely(delay == (k_tick_t)0u)) {
tos_task_yield();
return K_ERR_NONE;
}
TOS_CPU_INT_DISABLE();
if (tick_list_add(k_curr_task, delay) != K_ERR_NONE) {
TOS_CPU_INT_ENABLE();
return K_ERR_DELAY_FOREVER;
}
readyqueue_remove(k_curr_task);
TOS_CPU_INT_ENABLE();
knl_sched();
return K_ERR_NONE;
}
(三)销毁进程
这个函数十分简单,根据传递进来的进程控制块销毁进程,也可以传递进NULL表示销毁当前运行的进程。但是不允许销毁空闲进程k_idle_task,当调度器被锁住时不能销毁自身,会返回K_ERR_SCHED_LOCKED错误代码。如果使用了互斥量,当进程被销毁时会释放掉互斥量,并且根据进程所处的状态进行销毁,比如进程处于就绪态、阻塞态、等待态,则会从对应的状态列表中移除。
销毁进程相关源码如下:
__API__ k_err_t tos_task_destroy(k_task_t *task)
{
TOS_CPU_CPSR_ALLOC();
TOS_IN_IRQ_CHECK();
if (unlikely(!task)) {
task = k_curr_task;
}
#if TOS_CFG_OBJECT_VERIFY_EN > 0u
if (!knl_object_verify(&task->knl_obj, KNL_OBJ_TYPE_TASK)) {
return K_ERR_OBJ_INVALID;
}
#endif
if (knl_is_idle(task)) {
return K_ERR_TASK_DESTROY_IDLE;
}
if (knl_is_self(task) && knl_is_sched_locked()) {
return K_ERR_SCHED_LOCKED;
}
TOS_CPU_INT_DISABLE();
#if TOS_CFG_MUTEX_EN > 0u
// when we die, wakeup all the people in this land.
if (!tos_list_empty(&task->mutex_own_list)) {
task_mutex_release(task);
}
#endif
if (task_state_is_ready(task)) { // that's simple, good kid
readyqueue_remove(task);
}
if (task_state_is_sleeping(task)) {
tick_list_remove(task);
}
if (task_state_is_pending(task)) {
pend_list_remove(task);
}
task_reset(task);
task_state_set_deleted(task);
TOS_CPU_INT_ENABLE();
knl_sched();
return K_ERR_NONE;
}
【特殊模块】 从源码学习c的技巧&语法
关于c/c中 #if { } #endif 的含义与作用:
这种形式称为**条件编译,就是说编译器会根据条件的是否成立来判断是否编译这段内容(用来控制编译器的编译)**
注意#if一定要有#endif来搭配使用,缺一不可
有这三种形式:
-
#if 常量表达式 程序文本 #endif
表示当“常量表达式”为真的时候编译“程序文本”。
这种形式在相应的代码段的“常量表达式”写成0,可以起到对多行程序进行注释的作用。
-
#if 常量表达式 程序文本1 #else 程序文本2 #endif
与"if() else"类似,当"常量表达式"为真的时候编译“程序文本1”,为假的时候编译“程序文本2”。但需要注意"if() else"是编译后执行的,#if#else是在编译器编译前进行判断的
-
#ifndef 标识符 #define 标识符 程序文本 #endif
当“标识符”没有被定义的时候编译“程序文本”。这种形式主要是用来避免重复定义的,避免头文件被重复包含