使用STM32编写一个简单的RTOS:3.线程管理


参考资料: RTT官网文档
关键字:分析RT-Thread源码、stm32、RTOS、线程管理器。

RT-Thread的线程简介

线程(thread)是系统能够进行调度的最小单位,在linux中也是这样定义的,但是和我们RTOS中的thread更像是linux中的进程,是系统进行资源分配的基本单位,但同时也是调度的基本单位。在其他RTOS上有的将其命名为任务task。我们现在RTOS中任务的“并行”运行是系统在不断的切换任务呈现出来的,所以我们的线程栈顶需要维护保存上下文的切换前状态。在调度和对象容器中,皆需要一个维护一个链表,也要记录线程的状态及优先级等等。

我们先来看看RTT的线程有哪些状态。
RT-Thread 中线程的五种状态:初始状态、挂起状态、就绪状态、运行状态、关闭状态。
Alt
RT-Thread 提供一系列的操作系统调用接口,使得线程的状态在这五个状态之间来回切换。
几种状态间的转换关系如下图所示:
Alt
挂起的线程只能通过就绪态进入运行状态,不能直接到运行状态。
虽然定义了运行状态,但实际上这个RT_THREAD_RUNNING没引用过

RTT中有一个特殊的线程,idle空闲线程,即空闲时就会运行的线程,当就绪表中没有其他线程时,这个线程就会得到运行,而且idle线程一直是就绪状态。

另外,空闲线程在 RT-Thread 也有着它的特殊用途:
若某线程运行完毕,系统将自动删除线程:自动执行 rt_thread_exit() 函数,先将该线程从系统就绪队列中删除,再将该线程的状态更改为关闭状态,不再参与系统调度,然后挂入 rt_thread_defunct 僵尸队列(资源未回收、处于关闭状态的线程队列)中,最后空闲线程会回收被删除线程的资源。

空闲线程也提供了接口来运行用户设置的钩子函数,在空闲线程运行时会调用该钩子函数,适合钩入功耗管理、看门狗喂狗等工作。

线程的管理方式:

下图描述了线程的相关操作,包含:创建 / 初始化线程、启动线程、运行线程、删除 / 脱离线程。可以使用 rt_thread_create() 创建一个动态线程,使用 rt_thread_init() 初始化一个静态线程,动态线程与静态线程的区别是:动态线程是系统自动从动态内存堆上分配栈空间与线程句柄(初始化 heap 之后才能使用 create 创建动态线程),静态线程是由用户分配栈空间与线程句柄。

源码分析

初始化线程

这里只看一下静态初始化。

rt_err_t rt_thread_init(struct rt_thread *thread,
                        const char       *name,
                        void (*entry)(void *parameter),
                        void             *parameter,
                        void             *stack_start,
                        rt_uint32_t       stack_size,
                        rt_uint8_t        priority,
                        rt_uint32_t       tick)
{
    /* thread check */
    RT_ASSERT(thread != RT_NULL);
    RT_ASSERT(stack_start != RT_NULL);

    /* init thread object */
    rt_object_init((rt_object_t)thread, RT_Object_Class_Thread, name);

    return _rt_thread_init(thread,
                           name,
                           entry,
                           parameter,
                           stack_start,
                           stack_size,
                           priority,
                           tick);
}

参数tick是线程能持有的cpu时间,当时间走完时,线程并不会挂起,只是调用yield让出当前cpu权限,这个只对同优先级线程有效,低优先级仍然得不到cpu资源。
这里先是通过 rt_object_init将thread设置为了线程对象类。 rt_object_init在对象管理中也有分析过了,就是将thread插入对象容器相应的链表中。之后是_rt_thread_init,跟进去。

static rt_err_t _rt_thread_init(struct rt_thread *thread,
                                const char       *name,
                                void (*entry)(void *parameter),
                                void             *parameter,
                                void             *stack_start,
                                rt_uint32_t       stack_size,
                                rt_uint8_t        priority,
                                rt_uint32_t       tick)
{
    /* init thread list */
    rt_list_init(&(thread->tlist));	//调度就绪表时用到

    thread->entry = (void *)entry;
    thread->parameter = parameter;

    /* stack init */
    thread->stack_addr = stack_start;
    thread->stack_size = (rt_uint16_t)stack_size;

    /* init thread stack */
    rt_memset(thread->stack_addr, '#', thread->stack_size);
    thread->sp = (void *)rt_hw_stack_init(thread->entry, thread->parameter,
        (void *)((char *)thread->stack_addr + thread->stack_size - 4),
        (void *)rt_thread_exit);

    /* priority init */
    RT_ASSERT(priority < RT_THREAD_PRIORITY_MAX);
    thread->init_priority    = priority;
    thread->current_priority = priority;

    /* tick init */
    thread->init_tick      = tick;
    thread->remaining_tick = tick;

    /* error and flags */
    thread->error = RT_EOK;
    thread->stat  = RT_THREAD_INIT;

    /* initialize cleanup function and user data */
    thread->cleanup   = 0;
    thread->user_data = 0;

    /* init thread timer */
    rt_timer_init(&(thread->thread_timer),
                  thread->name,
                  rt_thread_timeout,
                  thread,
                  0,
                  RT_TIMER_FLAG_ONE_SHOT);

    return RT_EOK;
}

首先初始化了tlist,这个在调度的时候需要用到(插入就绪表),接着是一些入口函数的赋值,应该不难理解。

接下来就是重点了,首先将栈全部设置为字符‘#’,这个在算线程栈的最大使用率上需要用到,就是通过这个‘#’来计算的。接着,栈的初始化,这个函数我们在上下文切换时已经接触过了。现在我来看一下这个函数的具体实现。

rt_uint8_t *rt_hw_stack_init(void       *tentry,
                             void       *parameter,
                             rt_uint8_t *stack_addr,
                             void       *texit)
{
    struct stack_frame *stack_frame;
    rt_uint8_t         *stk;
    unsigned long       i;

    /* 对传入的栈指针做对齐处理 */
    stk  = stack_addr + sizeof(rt_uint32_t);
    stk  = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8);
    stk -= sizeof(struct stack_frame);

    /* 得到上下文的栈帧的指针 */
    stack_frame = (struct stack_frame *)stk;

    /* 把所有寄存器的默认值设置为 0xdeadbeef */
    for (i = 0; i < sizeof(struct stack_frame) / sizeof(rt_uint32_t); i ++)
    {
        ((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef;
    }

    /* 根据 ARM  APCS 调用标准,将第一个参数保存在 r0 寄存器 */
    stack_frame->exception_stack_frame.r0  = (unsigned long)parameter;
    /* 将剩下的参数寄存器都设置为 0 */
    stack_frame->exception_stack_frame.r1  = 0;                 /* r1 寄存器 */
    stack_frame->exception_stack_frame.r2  = 0;                 /* r2 寄存器 */
    stack_frame->exception_stack_frame.r3  = 0;                 /* r3 寄存器 */
    /* 将 IP(Intra-Procedure-call scratch register.) 设置为 0 */
    stack_frame->exception_stack_frame.r12 = 0;                 /* r12 寄存器 */
    /* 将线程退出函数的地址保存在 lr 寄存器 */
    stack_frame->exception_stack_frame.lr  = (unsigned long)texit;
    /* 将线程入口函数的地址保存在 pc 寄存器 */
    stack_frame->exception_stack_frame.pc  = (unsigned long)tentry;
    /* 设置 psr 的值为 0x01000000L,表示默认切换过去是 Thumb 模式 */
    stack_frame->exception_stack_frame.psr = 0x01000000L;

    /* 返回当前线程的栈地址       */
    return stk;
}

首先stk = stack_addr + sizeof(rt_uint32_t),获取栈顶指针,因为我们传进来的时候就-4了。接着是向下对齐,cortex-M3要求4个字节对齐,这里是8个字节对齐,例如15的话得到的地址就是8,。然后又减去sizeof(struct stack_frame),因为cortex-m3是向下生长的栈,减去的这部分是用来保存上下文环境的。
接下来是寄存器的初始化,需要注意的是LR = exit函数,PC = entry函数,因为这里的这里的退出函数exit赋值给了LR,在entry函数跑完的时候就会返回LR,而LR我们指向了exit退出函数,这就是为什么系统知道我们的线程是否退出了的原因。我们看一下exit里面做了什么操作。

static void rt_thread_exit(void)
{
    struct rt_thread *thread;
    register rt_base_t level;

    /* get current thread */
    thread = rt_current_thread;

    /* disable interrupt */
    level = rt_hw_interrupt_disable();

    /* remove from schedule */
    rt_schedule_remove_thread(thread);
    /* change stat */
    thread->stat = RT_THREAD_CLOSE;

    /* remove it from timer list */
    rt_timer_detach(&thread->thread_timer);

    if ((rt_object_is_systemobject((rt_object_t)thread) == RT_TRUE) &&
        thread->cleanup == RT_NULL)
    {
        rt_object_detach((rt_object_t)thread);
    }
    else
    {
        /* insert to defunct thread list */
        rt_list_insert_after(&rt_thread_defunct, &(thread->tlist));
    }

    /* enable interrupt */
    rt_hw_interrupt_enable(level);

    /* switch to next task */
    rt_schedule();
}

主要做了这些事情:

1,将线程从调度就绪表中移除。rt_schedule_remove_thread(thread);
2,修改线程状态为RT_THREAD_CLOSE
3,移除线程的定时器
4,从对象容器中移除,如果是动态的对象,则插入rt_thread_defunct中,将在idle中回收。

回到 _rt_thread_init中,接着是一些优先级等的赋值,之后初始化了一个只触发一次的定时器。
定时器的超时处理函数则是在超时的时候,将线程插入调度就绪表中并调度。

void rt_thread_timeout(void *parameter)
{
    struct rt_thread *thread;

    thread = (struct rt_thread *)parameter;

    /* thread check */
    RT_ASSERT(thread != RT_NULL);
    RT_ASSERT(thread->stat == RT_THREAD_SUSPEND);

    /* set error number */
    thread->error = -RT_ETIMEOUT;

    /* remove from suspend list */
    rt_list_remove(&(thread->tlist));

    /* insert to schedule ready list */
    rt_schedule_insert_thread(thread);

    /* do schedule */
    rt_schedule();
}

这个定时器在sleep,delay和ipc的时候将会用到。
rt_thread_init就介绍到这,rt_thread_create跟这个差不多,这里也不赘述了。

线程脱离

对于用 rt_thread_init() 初始化的线程,使用 rt_thread_detach() 将使线程对象在线程队列和内核对象管理器中被脱离。线程脱离函数如下:

rt_err_t rt_thread_detach(rt_thread_t thread)
{
    rt_base_t lock;

    /* thread check */
    RT_ASSERT(thread != RT_NULL);

    if ((thread->stat & RT_THREAD_STAT_MASK) != RT_THREAD_INIT)
    {
        /* remove from schedule */
        rt_schedule_remove_thread(thread);
    }

    /* release thread timer */
    rt_timer_detach(&(thread->thread_timer));

    /* change stat */
    thread->stat = RT_THREAD_CLOSE;

    /* detach object */
    rt_object_detach((rt_object_t)thread);

    if (thread->cleanup != RT_NULL)
    {
        /* disable interrupt */
        lock = rt_hw_interrupt_disable();

        /* insert to defunct thread list */
        rt_list_insert_after(&rt_thread_defunct, &(thread->tlist));

        /* enable interrupt */
        rt_hw_interrupt_enable(lock);
    }

    return RT_EOK;
}

如果线程的状态不是初始化状态,就在就绪表中移除,并清除相关标识位。接着将线程状态设置为了关闭状态,从对象容器中移除,判断cleanup,cleanup我们在init的时候赋值为0了,所以这里条件不成立。

启动线程

初始化完线程后就可以启动线程啦,下面我们看一下rt_thread_startup具体做了哪些事情。

rt_err_t rt_thread_startup(rt_thread_t thread)
{
	......
    /* set current priority to init priority */
    thread->current_priority = thread->init_priority;

    /* calculate priority attribute */
#if RT_THREAD_PRIORITY_MAX > 32
    thread->number      = thread->current_priority >> 3;            /* 5bit */
    thread->number_mask = 1L << thread->number;
    thread->high_mask   = 1L << (thread->current_priority & 0x07);  /* 3bit */
#else
    thread->number_mask = 1L << thread->current_priority;
#endif
	......
    /* change thread stat */
    thread->stat = RT_THREAD_SUSPEND;
    /* then resume it */
    rt_thread_resume(thread);
    if (rt_thread_self() != RT_NULL)
    {
        /* do a scheduling */
        rt_schedule();
    }

    return RT_EOK;
}

这个函数也比较简短,通过优先级计算出属性,用来定位在调度就绪表中的位置,在上下文一篇中我们已经了解过了,接着恢复线程到就绪状态,rt_thread_resume,然后通过rt_thread_self判断调度器是否已经启用。最后 rt_schedule调度,切换上下文。

rt_err_t rt_thread_resume(rt_thread_t thread)
{
	......
	 if (thread->stat != RT_THREAD_SUSPEND)
    {
        RT_DEBUG_LOG(RT_DEBUG_THREAD, ("thread resume: thread disorder, %d\n",
                                       thread->stat));
        return -RT_ERROR;
    }
    /* remove from suspend list */
    rt_list_remove(&(thread->tlist));

    rt_timer_stop(&thread->thread_timer);

    /* enable interrupt */
    rt_hw_interrupt_enable(temp);

    /* insert to schedule ready list */
    rt_schedule_insert_thread(thread);

    return RT_EOK;
}

resume函数里做的事情也比较少,如果不是suspend状态就不能切换到resume。接着从suspend链表中移除,实际上我们刚初始化的时候,tlist是指向自己的,而在 rt_schedule_insert_thread时才会插入就绪表,在detach的时候插入rt_thread_defunct中。接着就是将定时器停止,然后rt_schedule_insert_thread插入就绪表,并修改线程状态为RT_THREAD_READY。之后只要调用rt_schedule就可以启动该线程了。

挂起线程

挂起线程的作用是将就绪状态的线程切换为挂起状态,也就是将线程从就绪表中移除。

rt_err_t rt_thread_suspend(rt_thread_t thread)
{
    ......
    if (thread->stat != RT_THREAD_READY)
    {
        RT_DEBUG_LOG(RT_DEBUG_THREAD, ("thread suspend: thread disorder, %d\n",
                                       thread->stat));

        return -RT_ERROR;
    }

    /* disable interrupt */
    temp = rt_hw_interrupt_disable();

    /* change thread stat */
    thread->stat = RT_THREAD_SUSPEND;
    rt_schedule_remove_thread(thread);

    /* stop thread timer anyway */
    rt_timer_stop(&(thread->thread_timer));
	......
}

如果不是在就绪状态,则直接返回,不然就将线程状态改为SUSPEND挂起状态,从调度就绪表中移除,接着停止线程的定时器。

线程睡眠

rt_thread_delay/rt_thread_sleep的作用是让线程退出调度就绪表中,在tick时间后重新插入调度就绪表中等待调度。这个时候就需要用到在线程初始化时初始化的那个定时器了。

rt_err_t rt_thread_delay(rt_tick_t tick)
{
    return rt_thread_sleep(tick);
}

rt_err_t rt_thread_sleep(rt_tick_t tick)
{
    register rt_base_t temp;
    struct rt_thread *thread;

    /* disable interrupt */
    temp = rt_hw_interrupt_disable();
    /* set to current thread */
    thread = rt_current_thread;
    RT_ASSERT(thread != RT_NULL);

    /* suspend thread */
    rt_thread_suspend(thread);

    /* reset the timeout of thread timer and start it */
    rt_timer_control(&(thread->thread_timer), RT_TIMER_CTRL_SET_TIME, &tick);
    rt_timer_start(&(thread->thread_timer));

    /* enable interrupt */
    rt_hw_interrupt_enable(temp);

    rt_schedule();

    /* clear error number of this thread to RT_EOK */
    if (thread->error == -RT_ETIMEOUT)
        thread->error = RT_EOK;

    return RT_EOK;
}

参数tick就是我们要sleep的时间,具体时间跟RT_TICK_PER_SECOND这个有关系,默认是100,即1tick为10ms。也是我们sleep的最小单位。
首先将线程给切换到了挂起状态,接着设置定时器的时间,然后启动定时器,这是一个只触发一次的定时器。接着进行调度,之后就进入等待定时器timeout了,timeout前面也已经分析了,将线程重新插入到调度就绪表中,然后调度。

线程让出

该线程主动要求让出处理器资源时,它将不再占有处理器,调度器会选择相同优先级的下一个线程执行。线程调用这个接口后,这个线程仍然在就绪队列中。

rt_err_t rt_thread_yield(void)
{
	......
    /* set to current thread */
    thread = rt_current_thread;

    /* if the thread stat is READY and on ready queue list */
    if ((thread->stat & RT_THREAD_STAT_MASK) == RT_THREAD_READY &&
        thread->tlist.next != thread->tlist.prev)
    {
        /* remove thread from thread list */
        rt_list_remove(&(thread->tlist));

        /* put thread to end of ready queue */
        rt_list_insert_before(&(rt_thread_priority_table[thread->current_priority]),
                              &(thread->tlist));

        /* enable interrupt */
        rt_hw_interrupt_enable(level);

        rt_schedule();

        return RT_EOK;
    }
	......
}

首先得到了当前线程的句柄。判断当前的状态是不是就绪状态和是不是(该优先级的)就绪表中只有一个线程。如果均满足条件,则将线程移除,插到链表(同优先级)最后面,在进行调度。
可以看到,rt_thread_yield之后如果没有更高级的线程进入就绪状态,则会调度同一优先级的其他线程,如果该优先级下只有自己,那么将不做调度,也就是rt_thread_yield这个并不能让低优先级的线程有机会获得cpu的控制权,这个接口主要针对的还是同等优先级线程才有作用。

测试

关于线程管理就介绍到这里。现在我们可以接着继续完成我们的RTT-Mini了。我们只完成了上下文切换,接着要完善调度器的功能,以及引入对象容器和线程管理。篇幅已经有点长了,这里就不贴详细代码了。

app.c

#define THREAD_STACK_SIZE       256   
                             
unsigned char thread_1_stack[THREAD_STACK_SIZE];
struct thread thread1;
                 
unsigned char thread_2_stack[THREAD_STACK_SIZE];
struct thread thread2;

                             
void thread_1_entry(void *param)
{
    uint32_t i = 0;
    
    while (1) {
        printf("hello. i : %ld\r\n", i++);
        thread_yield();
    }
}


void thread_2_entry(void *param)
{
    uint32_t i = 0;
    
    while (1) {
        printf("world. i : %ld\r\n", i++);
        thread_yield();
    }
}

void app_init(void)
{
    err_t err;
    
    err = thread_init(&thread1, "th1", thread_1_entry, NULL, thread_1_stack, THREAD_STACK_SIZE, 20, 10);
    if (err == E_SUCCESS)
        thread_startup(&thread1);
    
    err = thread_init(&thread2, "th2", thread_2_entry, NULL, thread_2_stack, THREAD_STACK_SIZE, 20, 10);
    if (err == E_SUCCESS)
        thread_startup(&thread2);
}

在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值