RT-Thread 学习记录之线程

RT-Thread 学习记录之线程

线程简介

线程可以理解为一个又一个的任务,在传统嵌入式裸机开发中,系统只能处理完一个任务再接着处理另一个任务,因此对时序和任务顺序的要求十分严格。但在 RT-Thread 中,我们可以创建一个又一个任务线程,通过调度算法在各个任务中快速切换,以达到实时操作的效果。

在 RT-Thread 中,线程是调度的最小单位,通过线程的设置可以看出任务的重要程度和运行环境。线程执行时的运行环境,也就是开辟的栈空间,通常称为上下文,具体来说就是各个变量和数据,比如该线程中的局部变量、寄存器状态等。

由此可大致分析出, RT-Thread 中正是采用定时器、中断和链表来进行线程间不停切换的,我们稍后做分析。

RT-Thread 中的线程有:初始状态、就绪状态、运行状态、挂起状态、关闭状态这五种状态,所谓的线程调度,就是通过调度器和 RT-Thread 提供的一些接口函数,使得线程在这五种状态中切换。

源码分析

静态线程初始化/脱离

线程控制块结构体 struct rt_thread ( 去掉多余定义后 ):

struct rt_thread
{
    /* rt 对象 */
    char        name[RT_NAME_MAX];                      /* 线程名称 */
    rt_uint8_t  type;                                   /* 对象类型 */
    rt_uint8_t  flags;                                  /* 标志位 */

    rt_list_t   list;                                   /* 对象列表 */
    rt_list_t   tlist;                                  /* 线程列表 */

    /* 栈指针和入口函数 */
    void       *sp;                                     /* 栈指针 */
    void       *entry;                                  /* 入口函数指针 */
    void       *parameter;                              /* 参数 */
    void       *stack_addr;                             /* 栈地址指针 */
    rt_uint32_t stack_size;                             /* 栈大小 */

    /* 错误码 */
    rt_err_t    error;                                  /* 错误码 */

    rt_uint8_t  stat;                                   /* 线程状态 */

    /* 优先级 */
    rt_uint8_t  current_priority;                       /* 当前优先级 */
    rt_uint8_t  init_priority;                          /* 初始优先级 */
    rt_uint32_t number_mask;

    rt_ubase_t  init_tick;                              /* 线程初始化计数值 */
    rt_ubase_t  remaining_tick;                         /* 线程剩余计数值 */

    struct rt_timer thread_timer;                       /* 内置线程定时器 */

    void (*cleanup)(struct rt_thread *tid);             /* 线程退出清除函数 */

    rt_ubase_t user_data;                             /* 用户数据 */
};

线程的控制块结构中必要的属性如上,它是操作系统用于管理线程的一个数据结构,会存放线程的一些信息,例如优先级、线程名称、线程状态等,也包含线程与线程之间连接用的链表结构,线程等待事件集合等。

其中 init_priority 是线程创建时指定的线程优先级,在线程运行过程当中是不会被改变的(除非用户执行线程控制函数进行手动调整线程优先级)。cleanup 会在线程退出时,被空闲线程回调一次以执行用户设置的清理现场等工作。最后的一个成员 user_data 可由用户挂接一些数据信息到线程控制块中,以提供类似线程私有数据的实现。

接下来我们查看静态线程初始化函数 rt_thread_init :

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)
{
    /* 线程断言检查 */
    RT_ASSERT(thread != RT_NULL);
    RT_ASSERT(stack_start != RT_NULL);

    /* 初始化线程 */
    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);
}

可分析出,在初始化静态线程时,会先调用 rt_object_init 函数将该线程对象初始化并加入系统,之后调用 _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)
{
    /* 初始化线程链表 */
    rt_list_init(&(thread->tlist));
	
    /* 指定线程入口函数和参数 */
    thread->entry = (void *)entry;
    thread->parameter = parameter;

    /* 栈空间初始化 */
    thread->stack_addr = stack_start;
    thread->stack_size = stack_size;

    /* 初始化线程栈 */
    rt_memset(thread->stack_addr, '#', thread->stack_size);

/* 栈的生长方式判断 ( 我们一般使用向下生长的方式,所以该宏不定义 ) */
#ifdef ARCH_CPU_STACK_GROWS_UPWARD
    thread->sp = (void *)rt_hw_stack_init(thread->entry, thread->parameter,
                                          (void *)((char *)thread->stack_addr),
                                          (void *)_rt_thread_exit);
#else //调用 rt_hw_stack_init 初始化栈,栈顶的值为 【栈首地址 + 栈大小 - 4】
    thread->sp = (void *)rt_hw_stack_init(thread->entry, thread->parameter,
                                          (rt_uint8_t *)((char *)thread->stack_addr + thread->stack_size - sizeof(rt_ubase_t)),
                                          (void *)_rt_thread_exit);
#endif /* ARCH_CPU_STACK_GROWS_UPWARD */

    /* 优先级初始化 */
    RT_ASSERT(priority < RT_THREAD_PRIORITY_MAX);
    thread->init_priority    = priority;
    thread->current_priority = priority;

    thread->number_mask = 0;
#if RT_THREAD_PRIORITY_MAX > 32
    thread->number = 0;
    thread->high_mask = 0;
#endif /* RT_THREAD_PRIORITY_MAX > 32 */

    /* 时间片初始化 */
    thread->init_tick      = tick;
    thread->remaining_tick = tick;

    /* 错误码和标志位初始化 */
    thread->error = RT_EOK;
    thread->stat  = RT_THREAD_INIT;

    /* 初始化线程清除退出函数和用户数据 */
    thread->cleanup   = 0;
    thread->user_data = 0;

    /* 初始化线程内置定时器 */
    rt_timer_init(&(thread->thread_timer),
                  thread->name,
                  rt_thread_timeout,
                  thread,
                  0,
                  RT_TIMER_FLAG_ONE_SHOT);
	/* 绑定钩子函数 */
    RT_OBJECT_HOOK_CALL(rt_thread_inited_hook, (thread));

    return RT_EOK;
}

由此可得静态初始化线程流程:
静态初始化线程

接下来我们分析线程脱离函数。

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

rt_thread_detach 函数如下:

rt_err_t rt_thread_detach(rt_thread_t thread)
{
    rt_base_t lock;

    /* 线程断言检查(判断是否为空、是否为线程对象、是否为系统对象) */
    RT_ASSERT(thread != RT_NULL);//是否为空
    RT_ASSERT(rt_object_get_type((rt_object_t)thread) == RT_Object_Class_Thread);
    RT_ASSERT(rt_object_is_systemobject((rt_object_t)thread));

    if ((thread->stat & RT_THREAD_STAT_MASK) == RT_THREAD_CLOSE)
        return RT_EOK;

    if ((thread->stat & RT_THREAD_STAT_MASK) != RT_THREAD_INIT)
    {
        /* 从就绪表中移除 */
        rt_schedule_remove_thread(thread);
    }

    /* 线程清理 */
    _rt_thread_cleanup_execute(thread);

    /* 释放定时器 */
    rt_timer_detach(&(thread->thread_timer));

    /* 改变线程状态为关闭状态 */
    thread->stat = RT_THREAD_CLOSE;

    /* 栈中分配的线程直接从object管理表中移除 */
    if (rt_object_is_systemobject((rt_object_t)thread) == RT_TRUE)
    {
        rt_object_detach((rt_object_t)thread);
    }
    else
    {
        /* 关闭中断 */
        lock = rt_hw_interrupt_disable();
        /* 动态分配的线程移除到僵尸列表中 */
        rt_thread_defunct_enqueue(thread);
        /* 开启中断 */
        rt_hw_interrupt_enable(lock);
    }

    return RT_EOK;
}

rt_thread_detach 函数简单分析可知线程脱离的大概流程,在进行完断言判断后会再次对线程的状态进行判断,如果处于关闭状态,会直接返回 RT_EOK ,不是关闭或初始状态则会直接从就绪列表移除该线程。之后会进行线程清理,释放定时器,再将线程的状态切换为关闭状态,从 object 管理列表中移除该线程。最后顺利则返回 RT_EOK。
线程脱离

动态线程创建/删除

我们先来看看 rt_thread_create 函数:

rt_thread_t rt_thread_create(const char *name,
                             void (*entry)(void *parameter),
                             void       *parameter,
                             rt_uint32_t stack_size,
                             rt_uint8_t  priority,
                             rt_uint32_t tick)
{
    struct rt_thread *thread;
    void *stack_start;
	
    /* 创建一个 RT_Object_Class_Thread 类型的 object 对象 */
    thread = (struct rt_thread *)rt_object_allocate(RT_Object_Class_Thread,
                                                    name);
    if (thread == RT_NULL)
        return RT_NULL;
	
    /* 申请分配线程栈空间 */
    stack_start = (void *)RT_KERNEL_MALLOC(stack_size);
    if (stack_start == RT_NULL)
    {
        /* 失败需要先删除该object并释放内存 */
        rt_object_delete((rt_object_t)thread);

        return RT_NULL;
    }

    /* 调用_rt_thread_init */
    _rt_thread_init(thread,
                    name,
                    entry,
                    parameter,
                    stack_start,
                    stack_size,
                    priority,
                    tick);

    return thread;
}

可见动态创建的线程,除了栈空间是动态申请外,与静态初始化线程基本一致。在此不做赘述。

再来看看 rt_thread_delete 函数:

rt_err_t rt_thread_delete(rt_thread_t thread)
{
    rt_base_t lock;

    /* 线程断言检查(判断是否为空、是否为线程对象、是否为系统对象 */
    RT_ASSERT(thread != RT_NULL);
    RT_ASSERT(rt_object_get_type((rt_object_t)thread) == RT_Object_Class_Thread);
    RT_ASSERT(rt_object_is_systemobject((rt_object_t)thread) == RT_FALSE);

    if ((thread->stat & RT_THREAD_STAT_MASK) == RT_THREAD_CLOSE)
        return RT_EOK;

    if ((thread->stat & RT_THREAD_STAT_MASK) != RT_THREAD_INIT)
    {
        /* 从就绪表中移除 */
        rt_schedule_remove_thread(thread);
    }

    /* 线程清理 */
    _rt_thread_cleanup_execute(thread);

    /* 释放定时器 */
    rt_timer_detach(&(thread->thread_timer));

    /* 关闭中断 */
    lock = rt_hw_interrupt_disable();

    /* 改变线程状态为关闭状态 */
    thread->stat = RT_THREAD_CLOSE;

    /* 将线程移除到僵尸列表中 */
    rt_thread_defunct_enqueue(thread);

    /* 开启中断 */
    rt_hw_interrupt_enable(lock);

    return RT_EOK;
}

动态创建的线程需要使用 rt_thread_delete 函数进行删除,与静态线程脱离不同的地方在于,动态创建的线程不属于系统对象,不会加入 object 管理表,所以在进行删除操作时,不需要将其从 object 管理列表中删除,只需要将其插入到僵尸线程列表即可。

线程分离函数可以将静态线程从 object 中移除,线程删除函数则只能将动态分配的线程移除到僵尸线程表中。

启动线程

无论我们使用哪种方式 创建/初始化 线程,刚 创建/初始化 好的线程都处于初始状态,而要让线程跑起来,则需要将其切换成就绪状态,由此来让调度器对该线程进行调度。

rt_thread_startup 函数主要作用是将当前 创建/初始化 完成的线程从 RT_THREAD_INIT 状态切换到 RT_THREAD_READY 状态。
rt_thread_startup 函数:

rt_err_t rt_thread_startup(rt_thread_t thread)
{
    /* 断言检查(是否为空、是否处于初始状态、是否为线程对象) */
    RT_ASSERT(thread != RT_NULL);
    RT_ASSERT((thread->stat & RT_THREAD_STAT_MASK) == RT_THREAD_INIT);
    RT_ASSERT(rt_object_get_type((rt_object_t)thread) == RT_Object_Class_Thread);

    /* 将当前优先级设置为初始化优先级 */
    thread->current_priority = thread->init_priority;

    /* 计算优先级 */
#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;//计算当前number_mask
#endif /* RT_THREAD_PRIORITY_MAX > 32 */

    /* 输出日志信息 */
    RT_DEBUG_LOG(RT_DEBUG_THREAD, ("startup a thread:%s with priority:%d\n",
                                   thread->name, thread->init_priority));
    /* 改变线程状态为挂起状态 */
    thread->stat = RT_THREAD_SUSPEND;
    /* 将线程切换到就绪状态 */
    rt_thread_resume(thread);
    
    /* 获得当前执行的线程句柄 */
    if (rt_thread_self() != RT_NULL)
    {
        /* 做一次调度 */
        rt_schedule();
    }

    return RT_EOK;
}

由上述代码可分析出, RT-Thread 在将线程由初始状态切换为就绪状态时,并没有直接将 stat 属性更改为 RT_THREAD_READY ,而是先将其更改为 RT_THREAD_SUSPEND 挂起状态,再由 rt_thread_resume 函数将其切换为就绪状态。此函数将恢复线程并将其放入系统就绪队列。

我们来看看 rt_thread_resume 函数:

rt_err_t rt_thread_resume(rt_thread_t thread)
{
    register rt_base_t temp;

    /* 断言检查(是否为空、是否为线程对象) */
    RT_ASSERT(thread != RT_NULL);
    RT_ASSERT(rt_object_get_type((rt_object_t)thread) == RT_Object_Class_Thread);

    /* 输出日志信息 */
    RT_DEBUG_LOG(RT_DEBUG_THREAD, ("thread resume:  %s\n", thread->name));

    /* 判断线程状态是否为挂起状态,不是则返回 -RT_ERROR */
    if ((thread->stat & RT_THREAD_STAT_MASK) != RT_THREAD_SUSPEND)
    {
        /* 输出日志信息 */
        RT_DEBUG_LOG(RT_DEBUG_THREAD, ("thread resume: thread disorder, %d\n",
                                       thread->stat));

        return -RT_ERROR;
    }

    /* 关闭中断 */
    temp = rt_hw_interrupt_disable();

    /* 将该线程从链表移除 */
    rt_list_remove(&(thread->tlist));

    /* 停止计时 */
    rt_timer_stop(&thread->thread_timer);

    /* 将该线程插入就绪链表,线程状态将被设置为就绪并从挂起链表中删除。 */
    rt_schedule_insert_thread(thread);

    /* 使能中断 */
    rt_hw_interrupt_enable(temp);
    
	/* 绑定钩子函数 */
    RT_OBJECT_HOOK_CALL(rt_thread_resume_hook, (thread));
    return RT_EOK;
}

从以上源码分析可知,在进行初始状态切换就绪状态时,需要先将其转换为挂起状态进行过渡,之后进入 rt_schedule_insert_thread 函数后将该线程插入就绪链表,线程状态被设置为就绪 。

接下来我们看看 rt_schedule_insert_thread 函数的实现:

void rt_schedule_insert_thread(struct rt_thread *thread)
{
    register rt_base_t temp;

    /* 断言检查(是否为空) */
    RT_ASSERT(thread != RT_NULL);

    /* 关闭中断 */
    temp = rt_hw_interrupt_disable();

    /* 判断线程状态 */
    if (thread == rt_current_thread)
    {
        /* 将线程状态切换为运行状态 */
        thread->stat = RT_THREAD_RUNNING | (thread->stat & ~RT_THREAD_STAT_MASK);
        goto __exit;
    }

    /* 将线程状态切换为就绪状态并插入就绪链表 */
    thread->stat = RT_THREAD_READY | (thread->stat & ~RT_THREAD_STAT_MASK);
    /* 将线程插入就绪链表 */
    rt_list_insert_before(&(rt_thread_priority_table[thread->current_priority]),
                          &(thread->tlist));
	
    /* 输出日志信息 */
    RT_DEBUG_LOG(RT_DEBUG_SCHEDULER, ("insert thread[%.*s], the priority: %d\n",
                                      RT_NAME_MAX, thread->name, thread->current_priority));

    /* 设置优先级 */
#if RT_THREAD_PRIORITY_MAX > 32
    rt_thread_ready_table[thread->number] |= thread->high_mask;
#endif /* RT_THREAD_PRIORITY_MAX > 32 */
    rt_thread_ready_priority_group |= thread->number_mask;

__exit:
    /* 使能中断 */
    rt_hw_interrupt_enable(temp);
}

在该函数中做了一次判断,并将线程状态切换为了就绪态且加入就绪链表中。

通过上述分析可大概总结出线程启动流程:
线程启动

补充

栈的生长方式

在 RT-Thread 中,栈的生长方式有两种:

  • 向下生长:递减方式,即栈顶是栈的最后一个地址,数组的最后一个元素。
  • 向上生长:递增方式,即栈顶是栈的首个元素的地址,数组首个元素。

而线程栈就是向下生长的,栈顶的值为 【栈首地址 + 栈大小 - 4】。

动态和静态线程的对比

  • 动态线程不需要输入栈的起始地址,不需要定义线程的控制块,只要指出线程栈的大小。
  • 静态线程的线程控制块和线程栈都需要静态地定义出来,而动态线程则不需要提前定义出来,是运行的时候自动分配的。
  • 运行效率上,静态线程的线程控制块和线程栈都在芯片的RAM中,速度没有区别。
  • 静态定义的空间在编译时就已经确定,它不是可以动态分配的,所以不能被释放。只能脱离线程控制器。

总结

线程是 RT-Thread 内核中极其重要的部分,这篇文章仅对线程的创建、脱离、启动等做了简介,之后会对线程的调度、状态切换等进行更详细的说明分析。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值