自制嵌入式操作系统 DAY3

今天实现事件控制块,存储管理以及定时器。

1 事件控制块

本节代码位于12_event中

什么是事件控制块呢?

可以这样理解,前面学习我们已经知道,创建一个任务需要给这个任务分配一个任务控制块,这个任务控制块存储着关于这个任务的重要信息。那么,事件控制块就好比任务里的任务控制块。它存储着这个事件的重要信息,我们说创建一个事件(信号,邮箱,消息队列),其本质的过程就是初始化这个事件控制块。

一个任务或者中断服务子程序可以通过事件控制块ECB(Event Control Blocks)来向另外的任务发信号。这里,所有的信号都被看成是事件(Event)。一个任务还可以等待另一个任务或中断服务子程序给它发送信号。这里要注意的是,只有任务可以等待事件发生,中断服务子程序是不能这样做的。

对于处于等待状态的任务,还可以给它指定一个最长等待时间,以此来防止因为等待的事件没有发生而无限期地等下去。

多个任务可以同时等待同一个事件的发生。在这种情况下,当该事件发生后,所有等待该事件的任务中,优先级最高的任务得到了该事件并进入就绪状态,准备执行。上面讲到的事件,可以是信号量、邮箱或者消息队列等。 引用自http://blog.csdn.net/h32dong809/article/details/7082490

如下图是一个典型是事件过程。
最开始任务是task1,task1请求事件event0,此时event0没有事件发生,因此将task1加入event0的等待队列。然后运行的任务是task2,task2页请求event0,此时event0还是没有事件发生,因此task2加入到event0的等待队列。过了一段时间,task0发生event0事件,此时event0控制块有事件发生,将处于等待队列的task1唤醒,触发一次任务调度,当前任务转换为task1,此时task2还是依然处于事件控制块等待队列。

代码实现

接口定义

首先看一下事件控制块的定义,以及操作接口。当前没有一种具体的时间控制块,所以event_type_e只有一种UNKNOWN类型的事件控制块。
- event_t就是事件控制块结构定义,只包含两个字段,一个是事件控制块的类型event_type_e,另一个就是事件控制块的任务等待队列wait_list。
- event_init初始化事件控制块。
- event_wait将任务task等待事件event,state为任务等待某一种事件时的状态,这里暂时用不到,填0即可,timeout为任务task等待事件event的超时时间。 msg也是特定事件控制块所用的,这里也暂时用不上,填0即可。
- event_wakeup触发一次event事件,将它等待队列中的任务按一定规则唤醒。
- event_remove_task移除任务task中所有等待的事件。
- event_remove_all移除event中所有等待的事件。
- event_wait_count返回该事件中等待任务的个数。

event.h

  1 #ifndef EVENT_H
  2 #define EVENT_H
  3
  4 #include <stdint.h>
  5 #include "os_stdio.h"
  6 #include "lib.h"
  7 #include "task.h"
  8
  9 typedef enum event_type_tag {
 10     EVENT_TYPE_UNKNOWN = 0,
 11 }event_type_e;
 12
 13 typedef struct event_tag {
 14     event_type_e type;
 15     list_t wait_list;
 16 }event_t;
 17
 18 extern void event_init(event_t *event, event_type_e type);
 19 extern void event_wait(event_t *event, task_t *task, void *msg, uint32_t state, uint32_t timeout);
 20 extern void event_wakeup(event_t *event, void *msg, uint32_t result);
 21 extern void event_remove_task(task_t *task, void *msg, uint32_t result);
 22 extern uint32_t event_remove_all(event_t *event, void *msg, uint32_t result);
 23 extern uint32_t event_wait_count(event_t *event);
 24
 25 #endif /*EVENT_H*/

实现

  • 修改task.h, 之前任务状态只有就绪,延时和挂起,现在新增一种等待状态OS_TASK_WAIT_MASK。在task_t中增加任务控制块相关的字段,wait_event是该任务等待的事件的指针,一般来说,一个任务同一时间点只能等待一个事件。event_msg是任务像事件获取消息时的缓冲区,wait_event_result记录了任务等待事件的结果,是正常等待呢还是超时等待。

task.h

 13 #define OS_TASK_WAIT_MASK                   (0xFF << 16)
        ...
 38     /*Event control block*/
 39     struct event_tag *wait_event;
 40     void *event_msg;
 41     uint32_t wait_event_result;
 42
 43 }task_t;
  • 在task.c中对上述几个字段进行初始化

task.c

 63     /*Event control block*/
 64     task->wait_event = (event_t *)NULL;
 65     task->event_msg = (void *)0;
 66     task->wait_event_result = NO_ERROR;
 67
 68 }
  • 实现event_init接口,代码很简单,初始化event的类型和等待链表即可。
    event.c
  4 void event_init(event_t *event, event_type_e type)
  5 {
  6     event->type = type;
  7     list_init(&event->wait_list);
  8 }
  • 实现event_wait接口。1.首先将task的任务控制块相关的字段根据传入的参数相应的赋值。2.将任务task从就绪表移除,把它加入event的等待队列尾部。3.如果设置了超时,那么将该任务还要加入到延时队列中,这样当延时时间到时,能将任务从延时队列中唤醒,并且加入到就绪表。

event.c

 10 void event_wait(event_t *event, task_t *task, void *msg, uint32_t state, uint32_t timeout)
 11 {
 12     uint32_t status = task_enter_critical();
 13
 14     task->state |= state;
 15     task->wait_event = event;
 16     task->wait_event_result = NO_ERROR;
 17
 18     task_unready(task);
 19     list_append_last(&event->wait_list, &task->prio_list_node);
 20     if (timeout != 0) {
 21         task_delay_wait(task, timeout);
 22     }
 23
 24     task_exit_critical(status);
 25 }

所以在task_system_tick_handler要做一些改动,加入对任务控制块的处理。如下代码, 当任务延时时间到,并且有在等待某一个事件的话,不仅要把任务从延时队列中拿到,还需要把它从相应任务控制块中的等待队列拿掉,加入到就绪表中。
task.c

123 void task_system_tick_handler(void)
124 {
        ....
133     while (temp_node != head) {
134         task = container_of(temp_node, task_t, delay_node);
135         temp_node = temp_node->next;
136         if (--task->delay_ticks == 0) {
137
138             if (task->wait_event != (event_t *)NULL) {
139                 event_remove_task(task, (void *)0, ERROR_TIMEOUT);
140             }
        ...
149         }
150     }
151
  • 实现 event_wakeup接口。该函数逻辑很简单,就是将任务控制块event等待队列中的第一个任务拿出来,然后把相应的msg和result填到任务task相应字段, 清除任务的等待状态标志。如果任务设置了等待超时,那么这个时候还需要把任务从等待队列中移除,加入到就绪表中,触发一次任务调度。
    event.c
 27 task_t *event_wakeup(event_t *event, void *msg, uint32_t result)
 28 {
 29     list_node_t *node = (list_node_t *)NULL;
 30     task_t *task = (task_t *)NULL;
 31
 32     uint32_t status = task_enter_critical();
 33
 34     if ((node = list_remove_first(&event->wait_list)) != (list_node_t *)NULL) {
 35         task = (task_t *)container_of(node, task_t, prio_list_node);
 36         task->wait_event = (event_t *)NULL;
 37         task->event_msg = msg;
 38         task->state &= ~OS_TASK_WAIT_MASK;
 39
 40         if (task->delay_ticks != 0) {
 41             task_delay_wakeup(task);
 42         }
 43         task_ready(task);
 44     }
 45
 46     task_exit_critical(status);
 47     return task; 
 48}
  • 实现event_remove_task接口。 该接口就是将任务从自己等待的事件的等待队列中移除而已。

event.c

 49 void event_remove_task(task_t *task, void *msg, uint32_t result)
 50 {
 51     uint32_t status = task_enter_critical();
 52
 53     list_remove(&task->wait_event->wait_list, &task->prio_list_node);
 54     task->wait_event = (event_t *)NULL;
 55     task->event_msg = msg;
 56     task->wait_event_result = result;
 57     task->state &= ~OS_TASK_WAIT_MASK;
 58
 59     task_exit_critical(status);
 60 }
  • 实现event_remove_all接口。 这个接口跟event_wakeup实现很类似,只是event_wakeup只唤醒一个任务,而这个接口会把所有处于等待队列中的任务都唤醒。代码上实现就是遍历event的等待队列,将其移除并加入到就绪表中。
    event.h
 62 uint32_t event_remove_all(event_t *event, void *msg, uint32_t result)
 63 {
 64     list_node_t *node = (list_node_t *)NULL;
 65     uint32_t count = 0;
 66     uint32_t status = task_enter_critical();
 67
 68     DEBUG("%s:\n", __func__);
 69     count = list_count(&event->wait_list);
 70     while ((node = list_remove_first(&event->wait_list)) != (list_node_t *)NULL) {
 71         DEBUG("########\n");
 72         task_t *task = (task_t *)container_of(node, task_t, prio_list_node);
 73         task->wait_event = (event_t *)NULL;
 74         task->event_msg = msg;
 75         task->wait_event_result = result;
 76         task->state &= ~OS_TASK_WAIT_MASK;
 77         if (task->delay_ticks != 0) {
 78             task_delay_wakeup(task);
 79         }
 80         task_ready(task);
 81     }
 82
 83     task_exit_critical(status);
 84     return count;
 85 }

测试

task3和task2分别等待event_wait_normal事件,task1每次都会移除所有等待的事件。所以task2和task3总是在task1调用完event_remove_all才得到运行。
main.c

event_t event_wait_timeout;
event_t event_wait_normal;

void task1_entry(void *param)
{
    init_systick(10);
    event_init(&event_wait_timeout, EVENT_TYPE_UNKNOWN);

    for(;;) {
        printk("%s\n", __func__);
        uint32_t wakeup_count = event_remove_all(&event_wait_normal, (void *)0, 0);
        if (wakeup_count > 0) {
            task_sched();
            printk("wakeup_count:%d\n", wakeup_count);
            printk("count:%d\n", event_wait_count(&event_wait_normal));
        }
        task_sched();
        task_delay_s(1);
    }
}

void delay(uint32_t delay)
{
    while(delay--);
}

void task2_entry(void *param)
{
    for(;;) {

        event_wait(&event_wait_normal, g_current_task, (void *)0, 0, 0);
        task_sched();
        printk("%s\n", __func__);
        task_delay_s(1);
    }
}

void task3_entry(void *param)
{
    event_init(&event_wait_normal, EVENT_TYPE_UNKNOWN);
    for(;;) {
        event_wait(&event_wait_normal, g_current_task, (void *)0, 0, 0);
        task_sched();
        printk("%s\n", __func__);
        task_delay_s(1);
    }
}

2 信号量

本节代码位于13_semaphore中

相信本文的读者一定是对信号量的概念以及操作的是非常了解的,本文也不是写书,就不详细解释信号量的概念和操作了。如果实在是小白入门,可以上网查信号量,资料非常多,我想重新写一遍也未必解释的比之前的好。

信号量概述

信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量VI,然后将Acquire Semaphore VI以及Release Semaphore VI分别放置在每个关键代码段的首末端。确认这些信号量VI引用的是初始创建的信号量。

特性

抽象的来讲,信号量的特性如下:信号量是一个非负整数(车位数),所有通过它的线程/进程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Wait(等待) 和 Release(释放)。当一个线程调用Wait操作时,它要么得到资源然后将信号量减一,要么一直等下去(指放入阻塞队列),直到信号量大于等于一时。Release(释放)实际上是在信号量上执行加操作,对应于车辆离开停车场,该操作之所以叫做“释放”是因为释放了由信号量守护的资源

操作方式

对信号量有4种操作(#include

代码实现

接口定义

信号量无非就是PV操作,P对信号量减一,V对信号量加1。上文已经信号量操作简单解释了一下,我们来看看我们的系统中信号量的相关定义。
- sem_t 定义了信号量的结构,信号量是一种特殊的事件控制块,信号量可以理解为一种事件存在,所以它的结构中包含了一个event_t成员,如果对读者熟悉OOP,可以把信号量sem_t继承事件控制块event_t。其他的字段都是这种特殊的事件控制块独有的字段,count是信号量当前的计数值,而max_count是信号量可以操作最大的计数值。
- sem_info_t 定义了信号量的查询的结构,类似于任务查询之类的东西,并不是关键的数据结构。
- sem_init 初始化信号量sem, count是信号量初始值,max_count为信号量最大值。
- sem_acquire就是信号量的P操作,其作用就是将信号量减1,如果信号量小于0了,那么获取该信号量的任务就要进入信号量的任务等待队列,即进入到事件控制块的等待队列。wait_ticks设置等待信号量超时时间。
- sem_acquire_no_wait也是信号量的P操作,它和sem_acquire的区别是sem_acquire_no_wait也是信号量的V操作并不会进行任务的等待。如果信号量已经没有了资源,即信号量小于1,那么任务也不会等待该信号量。
- sem_release是信号量的P操作,即对信号量+1,当信号量的等待队列中存在任务时,唤醒等待队列中的第一个任务,否则就是对信号量+1,当然信号量计数值不能超过信号量的最大计数值。
- sem_get_info获取信号量sem在某一时刻的关键信息并保存到info里。
- sem_destory 销毁信号量sem。
sem.h

  1 #ifndef SEM_H
  2 #define SEM_H
  3
  4 #include "event.h"
  5 #include "config.h"
  6
  7 typedef struct sem_tag {
  8
  9     event_t event;
 10     uint32_t count;
 11     uint32_t max_count;
 12 }sem_t;
 13
 14 typedef struct sem_info_tag {
 15     uint32_t count;
 16     uint32_t max_count;
 17     uint32_t task_count;
 18 }sem_info_t;
 19
 20 extern void sem_init(sem_t *sem, uint32_t count, uint32_t max_count);
 21 extern uint32_t sem_acquire(sem_t *sem, uint32_t wait_ticks);
 22 extern uint32_t sem_acquire_no_wait(sem_t *sem);
 23 extern void sem_release(sem_t *sem);
 24 extern void sem_get_info(sem_t *sem, sem_info_t *info);
 25 extern uint32_t sem_destory(sem_t *sem);
 26
 27 #endif /*SEM_H*/

实现

  • 首先需要修改一下event.h,在event_type_e中加入信号量类型EVENT_TYPE_SEM
    event.h
  9 typedef enum event_type_tag {
 10     EVENT_TYPE_UNKNOWN  = 0,
 11     EVENT_TYPE_SEM      = 1,
 12 }event_type_e;
 13
  • sem_init函数其实现与接口描述一样,首先初始化信号量sem中的事件控制块event成员,将其type设置为EVENT_TYPE_SEM。接着就是设置sem_init的count和max_count值。
    sem.c
  5 void sem_init(sem_t *sem, uint32_t count, uint32_t max_count)
  6 {
  7     event_init(&sem->event, EVENT_TYPE_SEM);
  8
  9     if (max_count == 0) {
 10         sem->count = count;
 11     } else {
 12         sem->count = count > max_count ? max_count : count;
 13     }
 14 }
  • sem_acquire函数实现。从代码中看到如果sem的count大于0,那么就是对信号量的计数值count-1即可。如果信号量的计数值已经小于0了,那么将任务加入到信号量的等待队列中。这里的操作是调用了event_wait函数,而等待的事件控制块就是sem内部的event成员,将任务加入到了事件控制块的等待队列后,需要触发一次任务调度。
    从这个函数中我们就可以看到信号量其实就是对事件控制块的一种特殊的封装,其等待及唤醒的操作都是通过调用信号量sem内部事件控制块的函数来完成的。而信号量值负责对计数值的操作。
    sem.c
 16 uint32_t sem_acquire(sem_t *sem, uint32_t wait_ticks)
 17 {
 18     uint32_t status = task_enter_critical();
 19
 20     if (sem->count > 0) {
 21         --sem->count;
 22         task_exit_critical(status);
 23         return NO_ERROR;
 24     } else {
 25         event_wait(&sem->event, g_current_task, (void *)0, EVENT_TYPE_SEM, wait_ticks);
 26         task_exit_critical(status);
 27         task_sched();
 28         return g_current_task->wait_event_result;
 29     }
 30
 31 }
  • sem_acquire_no_wait实现与sem_acquire类似,只是sem_acquire_no_wait在信号量小于1的时候直接返回即可,并不加入到信号量的等待队列中。
    sem.c
 33 uint32_t sem_acquire_no_wait(sem_t *sem)
 34 {
 35     uint32_t status = task_enter_critical();
 36
 37     if (sem->count > 0) {
 38         --sem->count;
 39         task_exit_critical(status);
 40         return NO_ERROR;
 41     } else {
 42         task_exit_critical(status);
 43         return g_current_task->wait_event_result;
 44     }
 45 }
  • sem_release正好与sem_acquire实现相反的操作。首先判断信号量等待队列中是否有任务在等待,如果有任务等待,那么将等待队列的第一个任务唤醒即可,并且相应地触发一次任务调度。如果等待队列中不存在任务,那么就把信号量+1,然后返回即可。
    sem.c
 47 void sem_release(sem_t *sem)
 48 {
 49     uint32_t status = task_enter_critical();
 50     if (event_wait_count(&sem->event) > 0) {
 51         task_t *task = event_wakeup(&sem->event, (void *)0, NO_ERROR);
 52         if (task->prio < g_current_task->prio) {
 53             task_sched();
 54         }
 55     } else {
 56         sem->count++;
 57         if ((sem->max_count != 0) && (sem->count > sem->max_count)) {
 58             sem->count = sem->max_count;
 59         }
 60     }
 61     task_exit_critical(status);
 62 }
  • sem_get_info函数实现,很简单,无须解释。
    sem.c
 64 void sem_get_info(sem_t *sem, sem_info_t *info)
 65 {
 66     uint32_t status = task_enter_critical();
 67     info->count = sem->count;
 68     info->max_count = sem->max_count;
 69     info->task_count = event_wait_count(&sem->event);
 70     task_exit_critical(status);
 71 }
  • sem_destory将信号销毁。其操作就是将信号量等待等待队列中所有的任务移除,并且将信号量置为0。如果信号量等待队列中存在任务的话,在移除之后就触发一次任务调度。
    sem.c
 73 uint32_t sem_destory(sem_t *sem)
 74 {
 75     uint32_t status = task_enter_critical();
 76
 77     uint32_t count = event_remove_all(&sem->event, (void *)0, ERROR_DEL);
 78     sem->count = 0;
 79     task_exit_critical(status);
 80
 81     if (count > 0) {
 82         task_sched();
 83     }
 84     return count;
 85 }

测试

测试代码中定义了两个信号量sem1和sem2。task1初始化信号量为0,所以task1一旦调用sem_acquire它就会等待sem1发生,而task2中调用sem_release来释放信号量,此时会将等待中task1唤醒。task1的打印在sem_acquire后面。所以即使task1的打印会在task2的打印之后打印。
task3初始化信号量sem2为0,此时调用sem_acquire会进行到等待状态,它填写了超时时间,所以即可没有其他的任务来释放sem2,当超时时间一旦到达,task3会放弃等待sem2,继续运行。所以从log来看task2先打印,然后task1。而task3与task1和task2无关。
main.c


sem_t sem1;
sem_t sem2;

void task1_entry(void *param)
{
    init_systick(10);

    sem_init(&sem1, 0, 10);
    for(;;) {
        sem_acquire(&sem1, 0);
        printk("%s\n", __func__);
        task_delay_s(1);
    }
}

void delay(uint32_t delay)
{
    while(delay--);
}

void task2_entry(void *param)
{
    for(;;) {

        printk("%s\n", __func__);
        task_delay_s(1);
        sem_release(&sem1);
    }
}

void task3_entry(void *param)
{
    sem_init(&sem2, 0, 10);
    for(;;) {
        sem_acquire(&sem2, 500);
        printk("%s\n", __func__);
        task_delay_s(1);
    }
}

3 邮箱

在多任务操作系统中,通常需要在任务与任务之间传递一个数据(这种数据叫做消息)的方式来进行通信。为了达到这个目的,可以在内存中创建一个存储空间作为该数据的缓冲区。如果把这个缓冲区叫做消息缓冲区,那么在任务之间传递数据的一个最简单的办法就是传递消息缓冲区的指针。因此,用来传递消息缓冲区指针的数据结构就叫做消息邮箱。 引用自《嵌入式实时操作系统uc/OS-II 原理及应用(第四版)》

下图就是一次邮箱发送与获取的过程。初试状态下,邮箱里并没有数据可以读,而task1申请读取邮箱消息,所以task1进入了邮箱的等待队列。过了一会task0往邮箱里发送了一个消息,这个时候邮箱的write指针向后移一位,并且将task1从等待队列中唤醒,同时task1把数据从邮箱中读出来。

代码实现

接口定义

mbox_t定义了邮箱结构。同样邮箱是一种特殊的事件控制块,所以mbox_t和信号量一样包含了一个event_t成员。 count是邮箱当前消息的数量,read是邮箱缓冲区的读索引,write是邮箱缓冲区的写索引,max_count是邮箱缓冲区的长度,也就是邮箱最大允许容纳的消息数量。

mbox_init初始化邮箱mbox,msg_buffer事传给邮箱缓冲区的地址。
mbox_get从邮箱中读数据,msg是待读取消息的地址,wait_ticks同样是等到超时的参数。
mbox_get_no_wait从邮箱读取消息,但当邮箱没有消息时,任务不会进行等待。
mbox_send往邮箱mbox发消息,msg是消息的地址,notify_opition是发送消息的选项,后面实现会讲到是干什么的。
mbox_flush清空邮箱mbox中所有消息。
mbox_destory销毁邮箱mbox。
mbox_get_info查询邮箱某一时刻的信息。
mailbox.h

  2 #define MAILBOX_H
  3
  4 #include "event.h"
  5 #include "config.h"
  6
  7 #define MBOX_SEND_FRONT         0x12345678
  8 #define MBOX_SEND_NORMAL        0
  9 typedef struct mbox_tag {
 10
 11     event_t event;
 12     uint32_t count;
 13     uint32_t read;
 14     uint32_t write;
 15     uint32_t max_count;
 16     void **msg_buffer;
 17
 18 }mbox_t;
 19
 20 typedef struct mbox_info_tag {
 21     uint32_t count;
 22     uint32_t max_count;
 23     uint32_t task_count;
 24 }mbox_info_t;
 25
 26 extern void mbox_init(mbox_t *mbox, void **msg_buffer, uint32_t max_count);
 27 extern uint32_t mbox_get(mbox_t *mbox, void **msg, uint32_t wait_ticks);
 28 extern uint32_t mbox_get_no_wait(mbox_t *mbox, void **msg);
 29 extern uint32_t mbox_send(mbox_t *mbox, void *msg, uint32_t notify_opition);
 30 extern void mbox_flush(mbox_t *mbox);
 31 extern uint32_t mbox_destory(mbox_t *mbox);
 32 extern void mbox_get_info(mbox_t *mbox, mbox_info_t *info);

实现

  • mbox_init实现:初始化邮箱的事件控制块event,并根据传入的参数填写相应的字段。
    mbox.c
  5 void mbox_init(mbox_t *mbox, void **msg_buffer, uint32_t max_count)
  6 {
  7     event_init(&mbox->event, EVENT_TYPE_MAILBOX);
  8     mbox->msg_buffer = msg_buffer;
  9     mbox->max_count = max_count;
 10     mbox->read = 0;
 11     mbox->write = 0;
 12     mbox->count = 0;
 13 }
  • mbox_get实现:从邮箱mbox中获取消息,消息保存到msg所指向的地址中。当邮箱中有消息的时候,将邮箱消息数量count-1, 并从邮箱中消息缓冲区msg_buffer读出一个消息给msg,并且将邮箱的读指针read自增1,当read大于邮箱的最大长度时,read会指向0。也就是说,邮箱中的消息缓冲区是一个长度为max_count的循环缓冲区。
    当邮箱中没有消息的时候,系统将当前任务加入到邮箱的等待队列中,安排一次任务调度。当调度再次回到这个任务的时候,会将事件控制块中的消息赋值给msg指针并且返回。
    mbox.c
 15 uint32_t mbox_get(mbox_t *mbox, void **msg, uint32_t wait_ticks)
 16 {
 17     uint32_t status = task_enter_critical();
 18
 19     if (mbox->count > 0) {
 20
 21         mbox->count--;
 22         *msg = mbox->msg_buffer[mbox->read++];
 23         if (mbox->read >= mbox->max_count) {
 24             mbox->read = 0;
 25         }
 26         task_exit_critical(status);
 27         return NO_ERROR;
 28     } else {
 29         event_wait(&mbox->event, g_current_task, (void *)0, EVENT_TYPE_MAILBOX, wait_ticks);
 30         task_exit_critical(status);
 31         task_sched();
 32
 33         *msg = g_current_task->event_msg;
 34         return g_current_task->wait_event_result;
 35     }
 36 }
  • mbox_get_no_wait实现:该函数与mbox_get差不多,只不过在邮箱没有消息的时候不会等待任务。
    mbox.c
 38 uint32_t mbox_get_no_wait(mbox_t *mbox, void **msg)
 39 {
 40     uint32_t status = task_enter_critical();
 41
 42     if (mbox->count > 0) {
 43
 44         mbox->count--;
 45         *msg = mbox->msg_buffer[mbox->read++];
 46         if (mbox->read >= mbox->max_count) {
 47             mbox->read = 0;
 48         }
 49         task_exit_critical(status);
 50         return NO_ERROR;
 51     } else {
 52         task_exit_critical(status);
 53         return g_current_task->wait_event_result;
 54     }
 55 }
  • mbox_send实现稍微有点长,我们一点一点来看,首先判断邮箱中是否有任务在等待啊。
    如果有,那就唤醒等待中的第一个任务,然后直接把传递的消息赋值给唤醒任务控制块中的消息指针,不对邮箱的读写指针改动。
    如果没有等待的任务,首先判断邮箱内的消息已经满了,如果邮箱满了,那么就不能再往邮箱里写消息,并返回ERROR_RESOURCE_FULL。如果邮箱没满,那么就往邮箱里写一个数据。这里也分两种情况,如果notify_opition是MBOX_SEND_FRONT,那么数据往前写,然后把读指针read往前挪一个。否则就是正常写,把写指针write往后挪一个。写完后把邮箱的消息计数count+1。
    mbox.c
uint32_t mbox_send(mbox_t *mbox, void *msg, uint32_t notify_opition)
{
    uint32_t status = task_enter_critical();
    task_t *task = (task_t *)NULL;

    if (event_wait_count(&mbox->event) > 0) {
        task = event_wakeup(&mbox->event, (void *)msg, NO_ERROR);
        if (task->prio < g_current_task->prio) {
            task_sched();
        }
    } else {
        if (mbox->count >= mbox->max_count) {
            task_exit_critical(status);
            return ERROR_RESOURCE_FULL;
        }

        if (notify_opition & MBOX_SEND_FRONT) {
            if (mbox->read <= 0) {
                mbox->read = mbox->max_count - 1;
            } else {
                mbox->read--;
            }
            mbox->msg_buffer[mbox->read] = msg;
        } else {
            mbox->msg_buffer[mbox->write++] = msg;
            if (mbox->write >= mbox->max_count) {
                mbox->write = 0;
            }
        }

        mbox->count++;
    }
    task_exit_critical(status);
    return NO_ERROR;
}
  • mbox_flush 代码很简单,如果邮箱内没有等待的任务,那么把邮箱内的各个字段清0即可。
    mbox.c
 93 void mbox_flush(mbox_t *mbox)
 94 {
 95     uint32_t status = task_enter_critical();
 96
 97     if (event_wait_count(&mbox->event) == 0) {
 98         mbox->read = 0;
 99         mbox->write = 0;
100         mbox->count = 0;
101     }
102
103     task_exit_critical(status);
104 }
  • mbox_destory移除邮箱等待队列中的所有任务,并且触发一次任务调度。
    mbox.c
106 uint32_t mbox_destory(mbox_t *mbox)
107 {
108     uint32_t status = task_enter_critical();
109
110     uint32_t count = event_remove_all(&mbox->event, (void *)0, ERROR_DEL);
111
112     task_exit_critical(status);
113     if (count > 0) {
114         task_sched();
115     }
116
117     return count;
118 }
  • mbox_get_info实现代码如下:
    mbox.c
120 void mbox_get_info(mbox_t *mbox, mbox_info_t *info)
121 {
122     uint32_t status = task_enter_critical();
123     info->count = mbox->count;
124     info->max_count = mbox->max_count;
125     info->task_count = event_wait_count(&mbox->event);
126     task_exit_critical(status);
127 }

测试

测试代码中定义了一个邮箱mbox1,task1第一次往邮箱中以MBOX_SEND_NORMAL写入数据,然后延时20s,然后task1以MBOX_SEND_FRONT方式写入数据,然后延时20s。
而task2一次性读取了20个数据,因此在task1延时的20s内,task2处于等待邮箱状态,也不会执行。所以只有等到task1再次往邮箱里写数据的时候,task2才被唤醒并读取数据。log上看不出来task2延时的效率。读者只需要make run一下就能看到运行的时序是如何的。
mbox.c

mbox_t mbox1;
void *mbox1_msg_buffer[20];
uint32_t msg[20];

void task1_entry(void *param)
{
    uint32_t i = 0;
    init_systick(10);

    mbox_init(&mbox1, mbox1_msg_buffer, 20);
    for(;;) {
        printk("%s:send mbox\n", __func__);
        for (i = 0; i < 20; i++) {
            msg[i] = i;
            mbox_send(&mbox1, &msg[i], MBOX_SEND_NORMAL);
        }
        task_delay_s(20);

        printk("%s:send mbox front\n", __func__);
        for (i = 0; i < 20; i++) {
            msg[i] = i;
            mbox_send(&mbox1, &msg[i], MBOX_SEND_FRONT);
        }
        task_delay_s(20);
        task_delay_s(1);
    }
}

void task2_entry(void *param)
{
    void *msg;
    uint32_t err = 0;
    for(;;) {
        err = mbox_get(&mbox1, &msg, 10);
        if (err == NO_ERROR) {
            uint32_t value = *(uint32_t *)msg;
            printk("%s:value:%d\n", __func__, value);

        }
    }
}

4 内存分区

本节代码位于15_mem_block中

应用程序在运行中为了某种特殊需要,经常需要临时获得一些内存空间,因此作为一个比较完善的操作系统,必须具有动态分配内存的能力。对于RTOS来说,还需要保证系统在动态分配内存时,其执行时间是固定的。本文中的存储块分配与uCosII类似,采用固定大小的内存块。这样一来简化了代码实现,二来能使内存管理能够在O(1)内完成。

存储块进行两级管理,即把一个连续的内存空间分为若干个分区,每个分区又分为若干个大小的内存块。RTOS以分区为单位来管理动态内存,而任务以内存块为单位来获取和释放内存。内存分区及内存块的使用情况由内存控制块来记录。所以在代码中定义一个内存分区及其内存块的方法非常简单,只需要定义二维数组即可。例如:

uint32_t mem_buf[10][10]; /*定义了1个内存分区,每个内存分区有10个内存块,每个内存块大小为10个word*/

其分配释放过程如下图。
1. 首先图中有一个只有一个内存块的内存分区,task1申请获得一个内存块,此时内存分区内不再有内存块。
2. 过了一段时间task0申请内存块,而此时内存分区内并没有内存块可以分配,task0进入内存块的等待队列。
3. task1释放内存块,此时RTOS将唤醒task0,并且将内存块分配给task0。

相信读者从上面可以看出来存储块分配释放也是一种事件,因此存储块的实现也可以依赖于事件控制块来实现。

代码实现

结构定义

首先我们看内存分区的结构定义,
- 内存分区mem_block_t中包含了一个event_t字段,用来处理内存分区事件。
- start定义了内存分区的起始地址
- block_size定义了内存分区每一个内存快的大小。
- max_count定义了内存分区中最多有几个内存块
- block_list是内存分区中内存块链表。当内存块不被使用的使用,它就被插入到block_list中,当内存被任务申请时,它就从block_list中移除。

memblock.h

 11 typedef struct mem_block_tag {
 12     event_t event;
 13     void *start;
 14     uint32_t block_size;
 15     uint32_t max_count;
 16     list_t block_list;
 17 }mem_block_t;
 18

接口实现

mem_block_init函数初始化内存分区。
- start:内存分区的起始地址
- block_size:每一个内存块的大小
- block_cnt:内存块个数。
- 初始化过程很简单,首先根据传入的参数初始化各个字段。
- 然后初始化block_list
- 将内存块一个一个链接到block_list上,这里很巧妙的是,当内存块不使用的时候,将内存块强制转换成list_node_t结构,链接到该链表上,当内存块需要被使用的时候,根据相应的类型强制转换一下即可。这样可以省下一个list_node_t的空间。

根据上述的描述,可以看到内存分区与内存块的关系如下图。

memblock.c

  4 void mem_block_init(mem_block_t *mem_block, uint8_t *start, uint32_t block_size, uint32_t block_cnt)
  5 {
  6     uint8_t *mem_block_start = (uint8_t *)start;
  7     uint8_t *mem_block_end = mem_block_start + block_size * block_cnt;
  8
  9     if (block_size < sizeof(list_node_t)) {
 10         goto cleanup;
 11     }
 12
 13     event_init(&mem_block->event, EVENT_TYPE_MEM_BLOCK);
 14
 15     mem_block->start = start;
 16     mem_block->block_size = block_size;
 17     mem_block->max_count = block_cnt;
 18     list_init(&mem_block->block_list);
 19
 20
 21     while (mem_block_start < mem_block_end) {
 22         list_node_init((list_node_t *)mem_block_start);
 23         list_append_last(&mem_block->block_list, (list_node_t *)mem_block_start);
 24         mem_block_start += block_size;
 25     }
 26 cleanup:
 27     return;
 28
 29 }

mem_block_alloc从mem_block中分配一个内存块到mem。
- mem:分配的内存块的地址。
- wait_ticks:分配超时时间,与其他事件控制块类似。
- 分配逻辑很简单,如果内存分区还有内存块,那么直接从内存块链表中拿出第一个内存块给任务并返回。
- 如果内存块链表中不存在内存块,那么将当前任务插入到内存块等待链表尾部,并且触发一次任务调度。当任务被唤醒时,从event_msg中拿到释放的内存块地址返回。

memblock.c

 31 uint32_t mem_block_alloc(mem_block_t *mem_block, uint8_t **mem, uint32_t wait_ticks)
 32 {
 33     uint32_t status = task_enter_critical();
 34
 35     if (list_count(&mem_block->block_list) > 0) {
 36         *mem = (uint8_t *)list_remove_first(&mem_block->block_list);
 37         task_exit_critical(status);
 38         return NO_ERROR;
 39     } else {
 40         event_wait(&mem_block->event, g_current_task, (void *)0, EVENT_TYPE_MEM_BLOCK, wait_ticks);
 41         task_exit_critical(status);
 42         task_sched();
 43         *mem = g_current_task->event_msg;
 44         return g_current_task->wait_event_result;
 45     }
 46 }

mem_block_alloc_no_wait与mem_block_alloc唯一差异是mem_block_alloc_no_wait当没有可用的内存块时,不会阻塞任务的运行。

memblock.c

 48 uint32_t mem_block_alloc_no_wait(mem_block_t *mem_block, uint8_t **mem)
 49 {
 50     uint32_t status = task_enter_critical();
 51
 52     if (list_count(&mem_block->block_list) > 0) {
 53         *mem = (uint8_t *)list_remove_first(&mem_block->block_list);
 54         task_exit_critical(status);
 55         return NO_ERROR;
 56     } else {
 57         task_exit_critical(status);
 58         return ERROR_RESOURCE_FULL;
 59     }
 60
 61 }

mem_block_free释放地址为mem的内存块。内部实现很简单
- 当内存分区等待队列中存在等待任务时,唤醒等待队列中的第一个任务,然后将该内存块分配给唤醒的任务。如果唤醒的任务优先级高于当前任务,那么就触发一次任务调度。
- 当内存分区等待队列中没有任务等待时,直接将该内存块插入到内存块链表的尾部。

memblock.c

 63 void mem_block_free(mem_block_t *mem_block, uint8_t *mem)
 64 {
 65     uint32_t status = task_enter_critical();
 66     if (event_wait_count(&mem_block->event) > 0) {
 67         task_t *task = event_wakeup(&mem_block->event, (void *)mem, NO_ERROR);
 68         if (task->prio > g_current_task->prio) {
 69             task_sched();
 70         }
 71     } else {
 72         list_append_last(&mem_block->block_list, (list_node_t *)mem);
 73     }
 74
 75     task_exit_critical(status);
 76 }

mem_block_destory销毁内存块,将内存块中等待队列全部唤醒。
memblock.c

 88 uint32_t mem_block_destory(mem_block_t *mem_block)
 89 {
 90     uint32_t status = task_enter_critical();
 91
 92     uint32_t count = event_remove_all(&mem_block->event, (void *)0, ERROR_DEL);
 93
 94     task_exit_critical(status);
 95
 96     if (count > 0) {
 97         task_sched();
 98     }
 99     return count;
100 }

测试

  • task1初始化了一个内存分区mem1[20][100]。然后申请二十个内存块,然后释放。延时5s后再次分配,可以看到每次分配的内存确实就是内存分区中内存块。

main.c

 29 /*20 block of size 100 bytes*/
 30 uint8_t mem1[20][100];
 31 mem_block_t mem_block1;
 32 typedef uint8_t (*block_t)[100];
 33
 34 void task1_entry(void *param)
 35 {
 36     uint8_t i;
 37     block_t block[20];
 38     init_systick(10);
 39
 40     mem_block_init(&mem_block1, (uint8_t *)mem1, 100, 20);
 41     for(;;) {
 42         printk("%s\n", __func__);
 43         for (i = 0; i < 20; i++) {
 44             mem_block_alloc(&mem_block1, (uint8_t **)&block[i], 0);
 45             printk("block:%x, mem[i]:%x\n", block[i], &mem1[i][0]);
 46         }
 47
 48         for (i = 0; i < 20; i++) {
 49             mem_block_free(&mem_block1, (uint8_t *)block[i]);
 50         }
 51         task_delay_s(5);
 52     }
 53 }
 54                                                             

5 定时器

考虑平台硬件定时器个数限制的,RTOS 通过一个定时器任务管理软定时器组,满足用户定时需求。定时器任务会在其执行期间检查用户启动的时间周期溢出的定时器,并调用其回调函数。本文RTOS同时支持一个硬定时器组,硬定时器组是直接systick对定时器组计数,而软定时器是在定时器任务中对定时器进行操作。

定时器状态

定时器一共有5种状态,定义如下。很好理解,一共是创建,启动,运行,停止和销毁。

timer.h

  6 typedef enum timer_state{
  7     TIMER_CREATED,
  8     TIMER_STARTED,
  9     TIMER_RUNNING,
 10     TIMER_STOPPED,
 11     TIMER_DESTORYED,
 12 }timer_state_e;

其状态之间的关系如下图:

定时器运行原理

下面以软定时器组为例,来阐述定时器的运行原理。硬定时器组原理一样,只是计数的地方不一样而已。 定时器任务有一个定时器链表,下面挂了三个定时器,timer0,timer1和timer2。发生一次systick中断,就会定时器链表中所有的定时器的计数器减1,当某一个定时器定时时间到达之后,会调用挂载定时器下的定时器回调函数。如下图所示,timer0定时时间到,会调用timer0的func0,打印hello。

代码实现

定时器定义

timer_t定义了定时器的结构
- link_node用于链接到定时器链表中。
- duration_ticks 周期定时时的周期tick数
- start_delay_ticks 初次启动延后的ticks数
- delay_ticks 当前定时递减计数值
- timer_func 定时回调函数
- arg 传递给回调函数的参数
- config 定时器配置参数
- state 定时器状态

TIMER_CONFIG_TYPE_HARD和TIMER_CONFIG_TYPE_SOFT用于设置定时器是软定时器组还是硬定时器组。
timer.h

 14 typedef struct timer_tag {
 15
 16     list_node_t link_node;
 17     uint32_t duration_ticks;
 18     uint32_t start_delay_ticks;
 19     uint32_t delay_ticks;
 20     void (*timer_func)(void *arg);
 21     void *arg;
 22     uint32_t config;
 23
 24     timer_state_e state;
 25 }timer_t;
 26
 27 #define TIMER_CONFIG_TYPE_HARD      (1 << 0)
 28 #define TIMER_CONFIG_TYPE_SOFT      (0 << 0)

接口实现

timer_init根据传入的参数初始化定时器timer,并将timer的状态设置成TIMER_CREATED。
timer.c

 10 void timer_init(timer_t *timer, uint32_t delay_ticks, uint32_t duration_ticks,
 11         void(*timer_func)(void *arg), void *arg, uint32_t config)
 12 {
 13     list_node_init(&timer->link_node);
 14     timer->start_delay_ticks = delay_ticks;
 15     timer->duration_ticks  = duration_ticks;
 16     timer->timer_func = timer_func;
 17     timer->arg = arg;
 18     timer->config = config;
 19
 20     if (delay_ticks == 0) {
 21         timer->delay_ticks = duration_ticks;
 22     } else {
 23         timer->delay_ticks = timer->start_delay_ticks;
 24     }
 25
 26     timer->state = TIMER_CREATED;
 27 }

timer_module_init初始化定时器组模块,代码很简单,初始化硬定时器链表和软定时器链表,并且初始化软定时器任务timer_soft_task,timer_soft_task的优先级为1,属于较高优先级的任务。

timer.c

 29 static task_t g_timer_task;
 30 static task_stack_t g_timer_task_stack[OS_TIMERTASK_STACK_SIZE];

103 static void timer_soft_task(void *param)
104 {
105     for(;;) {
106         sem_acquire(&g_timer_tick_sem, 0);
107         sem_acquire(&g_timer_protect_sem, 0);
108
109         timer_call_func_list(&g_timer_soft_list);
110         sem_release(&g_timer_protect_sem);
111     }
112 }

123 void timer_module_init()
124 {
125     list_init(&g_timer_hard_list);
126     list_init(&g_timer_soft_list);
127     sem_init(&g_timer_protect_sem, 1, 1);
128     sem_init(&g_timer_tick_sem, 0, 0);
129
130     task_init(&g_timer_task, &timer_soft_task, (void *)0,
131             OS_TIMERTASK_PRIO, &g_timer_task_stack[OS_TIMERTASK_STACK_SIZE]);
132
133 }

timer_start启动定时器timer,只有当定时器处于TIMER_CREATED和TIMER_STOPPED的时候才能够启动定时器。启动定时器分为以下步骤:
1. 设置定时器的delay_ticks如果定时器没有起始定时时间,那么之间将周期定时时间赋给定时器的delay_ticks。
2. 将定时器的状态设置为TIMER_STARTED
3. 根据定时器的配置,将定时器插入到不同的定时器链表中。当配置为软定时器组时,将定时器插入到g_timer_hard_list中,反之,则插入到g_timer_soft_list中。
timer.c

 33 void timer_start(timer_t *timer)
 34 {
 35     switch(timer->state) {
 36     case TIMER_CREATED:
 37     case TIMER_STOPPED:
 38         timer->delay_ticks = timer->start_delay_ticks ? timer->start_delay_ticks : timer->duration_ticks;
 39         timer->state = TIMER_STARTED;
 40
 41         if (timer->config & TIMER_CONFIG_TYPE_HARD) {
 42             uint32_t status = task_enter_critical();
 43             list_append_last(&g_timer_hard_list, &timer->link_node);
 44             task_exit_critical(status);
 45         } else {
 46             sem_acquire(&g_timer_protect_sem, 0);
 47             list_append_last(&g_timer_soft_list, &timer->link_node);
 48             sem_release(&g_timer_protect_sem);
 49         }
 50         break;
 51     default:
 52         break;
 53     }
 54 }

timer_stop停止定时器timer。其操作很简单,正好与timer_start反操作,直接将定时器从相应的定时器链表中移除即可。
timer.c

 56 void timer_stop(timer_t *timer)
 57 {
 58     switch(timer->state) {
 59     case TIMER_STARTED:
 60     case TIMER_RUNNING:
 61         if (timer->config & TIMER_CONFIG_TYPE_HARD) {
 62             uint32_t status = task_enter_critical();
 63             list_remove(&g_timer_hard_list, &timer->link_node);
 64             task_exit_critical(status);
 65         } else {
 66             sem_acquire(&g_timer_protect_sem, 0);
 67             list_remove(&g_timer_soft_list, &timer->link_node);
 68             sem_release(&g_timer_protect_sem);
 69         }
 70         timer->state = TIMER_STOPPED;
 71         break;
 72     default:
 73         break;
 74     }
 75 }

timer_destory销毁定时器timer,停止定时器并且将定时器的状态设置为TIMER_DESTORYED。
timer.c

 97 void timer_destory(timer_t *timer)
 98 {
 99     timer_stop(timer);
100     timer->state = TIMER_DESTORYED;
101 }

从上文中可以看到软定时器任务timer_soft_task只做了一件事,就是调用timer_call_func_list这个函数。这个函数就是
1. 将定时器的状态设置为TIMER_RUNNING。
2. 根据传入了传入的定时器链表,遍历该定时器链表,然后调用定时器回调函数timer->timer_func(timer->arg);。
3. 根据定时器的周期定时数来判断定时器是否是周期定时器。如果duration_ticks是0,那么该定时器是一次性的定时器,在一次定时完成后,将停止定时器。如果duration_ticks不为0,那么将duration_ticks重新赋给delay_ticks,那么定时器又开始重新计数了。

timer.c

 77 static void timer_call_func_list(list_t *timer_list)
 78 {
 79     list_node_t *node;
 80     timer_t *timer;
 81
 82     for (node = timer_list->head.next; node != &(timer_list->head); node = node->next) {
 83         timer = container_of(node, timer_t, link_node);
 84         if ((timer->delay_ticks == 0) || (--timer->delay_ticks == 0)) {
 85             timer->state = TIMER_RUNNING;
 86             timer->timer_func(timer->arg);
 87             timer->state = TIMER_STARTED;
 88             if (timer->duration_ticks > 0) {
 89                 timer->delay_ticks = timer->duration_ticks;
 90             } else {
 91                 timer_stop(timer);
 92             }
 93         }
 94     }
 95 }

timer_module_tick_notify用于硬定时器组。 该函数在task_system_tick_handle中调用。这样硬定时器组就是由systick计数,而非软定时器任务。这样硬定时器的优先级的最高的,因此硬定时器组的定时器总能在较快的时间内进行相应。
timer.c

114 void timer_module_tick_notify(void)
115 {
116     uint32_t status = task_enter_critical();
117
118     timer_call_func_list(&g_timer_hard_list);
119     task_exit_critical(status);
120     sem_release(&g_timer_tick_sem);
121 }

task.c

125 void task_system_tick_handler(void)
126 {
        ......
166     timer_module_tick_notify();
167     task_exit_critical(status);
168
169     task_sched();
170 }

测试

task1中使用了3个定时器timer1,timer2和timer3。
- timer1 起始延时为100个tick(即1s),周期延时1s,即每1s回调一次timer_func1,参数为0,将其配置为硬定时器组。
- timer2 起始延时为200个tick(即2s),周期延时2s,即每1s回调一次timer_func2,参数为0,将其配置为软定时器组。
- timer3 起始延时为300个tick(即3s),没有周期延时,即只一次性调用timer_func3, 参数为0,将其配置为软定时器组。

task1延时10s后,停止timer2。
然后再延时10s,销毁timer1,此时timer1再也不会运行了。同时启动timer2。
随后延时10s,销毁timer2。至此任务中所有定时器都不再执行。
main.c

 30 timer_t timer1;
 31 timer_t timer2;
 32 timer_t timer3;
 33
 34 static void timer_func1(void *arg)
 35 {
 36     printk("timer_func1\n");
 37 }
 38
 39 static void timer_func2(void *arg)
 40 {
 41     printk("timer_func2\n");
 42 }
 43
 44 static void timer_func3(void *arg)
 45 {
 46     printk("timer_func3\n");
 47 }
 54 void task1_entry(void *param)
 55 {
 56     uint32_t stopped = 0;
 57     init_systick(10);
 58
 59     timer_init(&timer1, 100, 100, timer_func1, (void *)0, TIMER_CONFIG_TYPE_HARD);
 60     timer_start(&timer1);
 61     timer_init(&timer2, 200, 200, timer_func2, (void *)0, TIMER_CONFIG_TYPE_SOFT);
 62     timer_start(&timer2);
 63
 64     timer_init(&timer3, 300, 0, timer_func3, (void *)0, TIMER_CONFIG_TYPE_HARD);
 65     timer_start(&timer3);
 66
 67     for(;;) {
 68         if (stopped == 0) {
 69             task_delay(1000);
 70             timer_stop(&timer2);
 71             task_delay(1000);
 72             timer_start(&timer2);
 73             timer_destory(&timer1);
 74             task_delay(1000);
 75             timer_destory(&timer2);
 76             stopped = 1;
 77         }
 78     }
 79 }

6 互斥锁

优先级反转

嵌套操作

代码实现

测试

猜你喜欢

转载自blog.csdn.net/u011280717/article/details/79439236