2.1 Apache Mynewt操作系统内核

Mynewt核心操作系统是一个多任务,抢占式的实时操作系统。它将调度器与典型RTOS特性有机的整合在一起,包括互斥量(mutex),信号量(semaphore),内存池等。Mynewt内核还提供了许多有有用的工具,如任务看门狗,网络协议栈内存缓冲区,时间管理API。

多任务,抢占式操作系统是一种可以实例化多个任务,且为每个任务分配优先级。高优先级的任务优先于低优先级任务运行。更进一步,如果一个低优先级任务正在运行,此时一个更高优先级的任务想要运行,那么低优先级的任务将会停止,等待更高优先级的任务运行,即较低优先级任务被高优先级任务抢占了。

为何使用操作系统?

你可能会问自己”为什么我需要一个多任务的抢占式操作系统?“,答案可能确实是你并不需要。有些应用非常简单,只需要轮询循环即可。但有更多的应用是复杂的,可能要求某些工作按时执行或在其他工作之前执行。如果你有一个简单轮询循环,你就不能轮换到服务工作,直到当前的工作完成。使用Mynewt OS,应用程序开发者不需要担心特定的任务占用太长时间或不及时执行,操作系统提供了处理这些情况的机制。

使用操作系统的另一个好处在于,它有助于包含应用程序开发人员不受其他应用程序代码编写的影响,开发人员不必担心(或更少担心)其他应用程序的不当行为,导致不受欢迎的行为或阻止他们代码的正确执行。使用操作系统的其他好处是,操作系统还提供了一些特性使得开发者可以创建自己所需要的特性。

基础应用创建

使用Mynewt Core OS创建一个应用是一个相对简单的任务,主要步骤有:

  1. 为应用安装基础的Newt工具框架(构建框架)。
  2. 创建板级支持包,若基于已有平台,直接指定即可。
  3. 在应用的main()函数中,调用sysinit()函数初始化系统与包,执行特定应用程序的初始化,然后在无限循环中等待操作系统从默认事件队列中分配事件。

使用RTOS(实时操作系统)如Mynewt,初始化应用程序模块和任务将变得略微有些复杂。必须注意的是,一个任务提供的API需要在被其他更高优先级任务调用之前完成初始化。

示例

如下所示代码,应用程序初始化系统和包,调用特定任务初始化函数,并从默认事件队列分派事件。

应用程序任务初始化函数完成数据结构的初始化,其后将任务添加到系统中。

在这个示例中,任务1释放信号量1,并等待任务2释放信号量2 。由于任务1中释放了信号量1,从而任务2得到运行,释放信号量2,等待信号量1。当信号量2释放后,任务1继续运行,如此往复。

  1. struct os_sem task1_sem;
  2. struct os_sem task2_sem;
  3. /* Task 1 handler function */
  4. void
  5. task1_handler(void *arg)
  6. {
  7. while (1) {
  8. /* Release semaphore to task 2 */
  9. os_sem_release(&task1_sem);
  10. /* Wait for semaphore from task 2 */
  11. os_sem_pend(&task2_sem, OS_TIMEOUT_NEVER);
  12. }
  13. }
  14. /* Task 2 handler function */
  15. void
  16. task2_handler(void *arg)
  17. {
  18. struct os_task *t;
  19. while (1) {
  20. /* Wait for semaphore from task1 */
  21. os_sem_pend(&task1_sem, OS_TIMEOUT_NEVER);
  22. /* Release task2 semaphore */
  23. os_sem_release(&task2_sem);
  24. }
  25. }
  26. /* Initialize task 1 exposed data objects */
  27. void
  28. task1_init(void)
  29. {
  30. /* Initialize task1 semaphore */
  31. os_sem_init(&task1_sem, 0);
  32. }
  33. /* Initialize task 2 exposed data objects */
  34. void
  35. task2_init(void)
  36. {
  37. /* Initialize task1 semaphore */
  38. os_sem_init(&task2_sem, 0);
  39. }
  40. /**
  41. * init_app_tasks
  42. *
  43. * This function performs initializations that are required before tasks run.
  44. *
  45. * @return int 0 success; error otherwise.
  46. */
  47. static int
  48. init_app_tasks(void)
  49. {
  50. /*
  51. * Call task specific initialization functions to initialize any shared objects
  52. * before initializing the tasks with the OS.
  53. */
  54. task1_init();
  55. task2_init();
  56. /*
  57. * Initialize tasks 1 and 2 with the OS.
  58. */
  59. os_task_init(&task1, "task1", task1_handler, NULL, TASK1_PRIO,
  60. OS_WAIT_FOREVER, task1_stack, TASK1_STACK_SIZE);
  61. os_task_init(&task2, "task2", task2_handler, NULL, TASK2_PRIO,
  62. OS_WAIT_FOREVER, task2_stack, TASK2_STACK_SIZE);
  63. return 0;
  64. }
  65. /**
  66. * main
  67. *
  68. * The main function for the application. This function initializes the system and packages,
  69. * calls the application specific task initialization function, then waits and dispatches
  70. * events from the OS default event queue in an infinite loop.
  71. */
  72. int
  73. main(int argc, char **arg)
  74. {
  75. /* Perform system and package initialization */
  76. sysinit();
  77. /* Initialize application specific tasks */
  78. init_app_tasks();
  79. while (1) {
  80. os_eventq_run(os_eventq_dflt_get());
  81. }
  82. /* main never returns */
  83. }

2.1.1 调度器

调度器的工作是维护任务列表,决定下一刻运行哪个任务。

描述

首先说明任务所处的几种状态:运行态(running),就绪态(ready to run)或休眠态(sleeping)。

当任务处于运行态,CPU正在执行任务的上下文。程序计数器(PC)指向要执行任务的指令,堆栈指针(SP)指向任务的栈。

而处于就绪态的任务则希望获得CPU来执行工作。

而处理休眠态的任务则没有工作需要完成,等待特定条件唤醒。

而调度器算法也比较简单:从就绪态的任务中,选择最高优先级的任务(t_prio字段),将其状态设置为运行态。

处于运行态或就绪态的任务将被保存在g_os_run_list链表中,而此列表通过优先级排序。

处于休眠态的任务则保存在g_os_sleep_list链表中。

调度器由一个特定于CPU架构的组件,这段代码负责完成任务运行的切换,此过程称为上下文切换。在上下文切换过程中,当前正在运行的任务的CPU状态将被存储,并切换到新的任务的上下文。

API

  1. struct os_task* os_sched_get_current_task(void)

返回值:返回当前运行的任务

注意:返回的任务可能是也可能不是就绪态中最高优先级的任务。

  1. void os_sched_set_current_task(struct os_task *)
  1. struct os_task* os_sched_next_task(void)
  1. void os_sched(struct os_task *)

如果需要,执行上下文切换。

如果next_t被设置,任务将被置于运行态。若next_t为NULL,就绪态最高优先级任务将被切换。当前任务切换到休眠态或新的任务切换到就绪态时此函数被调用。

此函数将调用架构特定代码来切换到新任务。

  1. // example
  2. os_error_t
  3. os_mutex_release(struct os_mutex *mu)
  4. {
  5. ...
  6. OS_EXIT_CRITICAL(sr);
  7. // Re-schedule if needed
  8. if (resched) {
  9. os_sched(rdy);
  10. }
  11. return OS_OK;
  12. }

2.1.2 任务

任务,伴随着调度器,构成了Mynewt OS的基础。任务由两个基础要素组成:一个任务堆栈和一个任务函数。任务函数基本上是一个无限循环,等待一些事件唤醒它。在Mynewt中有两种方法来传递信号给任务,告诉它有工作需要处理:事件队列和信号量。

Mynewt OS是一个多任务,抢占式的操作系统。每个任务都分配一个任务优先级(从0到255),0为最高优先级任务。如果一个更高优先级的任务需要运行,那么调度器将停下当前任务并切换到高优先级的任务。

如果任务没有被更高优先级任务抢占,任务将一直运行直到停止。开发者需要保证任务最终休眠,否则低优先级的任务将永远没有机会运行。在下列情况下,任务将被至于sleep:使用os_time_delay()进入睡眠,等待定时结束;等待一个空的事件队列或尝试获得一个互斥量或信号量。

描述

为了创建一个任务,需要定义两个数据结构:一个任务对象(struct os_task)以及相关的堆栈。关于堆栈大小的确定可能比较麻烦,一般而言,开发人员不应该在堆栈上声明大型的局部变量,这样任务堆栈就可以是有限的。然而,所有应用程序都是不同的,开发人员必选根据应用情况来选择堆栈大小。注意,堆栈的单位是os_stack_t类型(通常是32-bit),若任务中STACK_SIZE设置为64,对应的stack大小为256字节。

一个任务还需要关联一个任务函数,该函数将在任务启动时被调用,任务函数永远也不要返回。

一个新任务必须添加到调度器中,Mynewt OS才会知道它的存在,将调用os_task_init()函数。一旦os_task_init()调用,任务将处于就绪态,并添加到活跃任务列表。注意,一个任务可以在操作系统启动前或启动后初始化(os_start调用之前),但必须在操作系统系统初始化之后初始化(os_init调用之后)。在大多数示例和当前Mynewt任务,操作系统初始化,任务初始化,然后在启动操作系统。一旦操作系统启动,最高优先级的任务将优先运行。

关于任务的信息可以通过os_task_info_get_next()获得。开发者可以通过os_task_info获得所有创建任务。

以下为一个每秒开关LED的简单任务示例。

  1. /* Create a simple "project" with a task that blinks a LED every second */
  2. /* Define task stack and task object */
  3. #define MY_TASK_PRI (OS_TASK_PRI_HIGHEST)
  4. #define MY_STACK_SIZE (64)
  5. struct os_task my_task;
  6. os_stack_t my_task_stack[MY_STACK_SIZE];
  7. /* This is the task function */
  8. void my_task_func(void *arg) {
  9. /* Set the led pin as an output */
  10. hal_gpio_init_out(LED_BLINK_PIN, 1);
  11. /* The task is a forever loop that does not return */
  12. while (1) {
  13. /* Wait one second */
  14. os_time_delay(1000);
  15. /* Toggle the LED */
  16. hal_gpio_toggle(LED_BLINK_PIN);
  17. }
  18. }
  19. /* This is the main function for the project */
  20. int main(int argc, char **argv)
  21. {
  22. /* Perform system and package initialization */
  23. sysinit();
  24. /* Initialize the task */
  25. os_task_init(&my_task, "my_task", my_task_func, NULL, MY_TASK_PRIO,
  26. OS_WAIT_FOREVER, my_task_stack, MY_STACK_SIZE);
  27. /* Process events from the default event queue. */
  28. while (1) {
  29. os_eventq_run(os_eventq_dflt_get());
  30. }
  31. /* main never returns */
  32. }

API

任务状态,OS_TASK_READY=1,表示任务就绪;OS_TASK_SLEEP=2,表示任务休眠。

  1. typedef enum os_task_state os_task_state_t
  1. typedef void (* os_task_func_t)(void *)
  2. int os_task_init(struct os_task *, const char *, os_task_func_t, void *, uint8_t, os_time_t, os_stack_t *, uint16_t)

初始化任务,成功返回0,失败返回非0。参数说明:

  • t,初始化的任务
  • name,任务名
  • func,任务函数
  • arg,传递给任务函数的参数
  • prio,任务优先级
  • sanity_itvl,sanity任务检查此任务的时刻,OS_WAIT_FOREVER表示不需要检查
  • stack_bottom,指向堆栈顶部的指针
  • stack_size,堆栈的大小(os_stack_t类型)
  1. int os_task_remove(struct os_task *t)
  2. 删除特定的任务,(截至当前版本1.3.0)为实验性接口,通常不建议使用
  1. uint8_t os_task_count(void)
  2. 返回初始化的任务数
  1. struct os_task* os_task_info_get_next(const struct os_task *prev, struct os_task_info *oti)
  2. 迭代任务,返回任务的优先级、ID、状态、堆栈使用情况、上下文切换计数、Sanity检查状态、任务名称

参数:

  • prev,上一个任务的结构体指针,NULL从头开始迭代
  • oti,需要填写的OS任务信息os_task_info
  1. OS_TASK_STACK_DEFINE_NOSTATIC(__name, __size)
  2. OS_TASK_STACK_DEFINE(__name, __size)
  3. OS_TASK_PRI_HIGHEST //最高优先级任务
  4. OS_TASK_PRI_LOWEST //最低优先级任务
  5. OS_TASK_FLAG_NO_TIMEOUT
  6. OS_TASK_FLAG_SEM_WAIT //等待信号量
  7. OS_TASK_FLAG_MUTEX_WAIT //等待互斥量
  8. OS_TASK_FLAG_EVQ_WAIT //等待事件队列
  9. OS_TASK_MAX_NAME_LEN
  1. /**
  2. * Structure containing information about a running task
  3. */
  4. struct os_task {
  5. /** Current stack pointer for this task */
  6. os_stack_t *t_stackptr;
  7. /** Pointer to top of this task's stack */
  8. os_stack_t *t_stacktop;
  9. /** Size of this task's stack */
  10. uint16_t t_stacksize;
  11. /** Task ID */
  12. uint8_t t_taskid;
  13. /** Task Priority */
  14. uint8_t t_prio;
  15. /* Task state, either READY or SLEEP */
  16. uint8_t t_state;
  17. /** Task flags, bitmask */
  18. uint8_t t_flags;
  19. uint8_t t_lockcnt;
  20. uint8_t t_pad;
  21. /** Task name */
  22. const char *t_name;
  23. /** Task function that executes */
  24. os_task_func_t t_func;
  25. /** Argument to pass to task function when called */
  26. void *t_arg;
  27. /** Current object task is waiting on, either a semaphore or mutex */
  28. void *t_obj;
  29. /** Default sanity check for this task */
  30. struct os_sanity_check t_sanity_check;
  31. /** Next scheduled wakeup if this task is sleeping */
  32. os_time_t t_next_wakeup;
  33. /** Total task run time */
  34. os_time_t t_run_time;
  35. /**
  36. * Total number of times this task has been context switched during
  37. * execution.
  38. */
  39. uint32_t t_ctx_sw_cnt;
  40. STAILQ_ENTRY(os_task) t_os_task_list;
  41. TAILQ_ENTRY(os_task) t_os_list;
  42. SLIST_ENTRY(os_task) t_obj_list;
  43. };
  1. /**
  2. * Information about an individual task, returned for management APIs.
  3. */
  4. struct os_task_info {
  5. /** Task priority */
  6. uint8_t oti_prio;
  7. /** Task identifier */
  8. uint8_t oti_taskid;
  9. /** Task state, either READY or SLEEP */
  10. uint8_t oti_state;
  11. /** Task stack usage */
  12. uint16_t oti_stkusage;
  13. /** Task stack size */
  14. uint16_t oti_stksize;
  15. /** Task context switch count */
  16. uint32_t oti_cswcnt;
  17. /** Task runtime */
  18. uint32_t oti_runtime;
  19. /** Last time this task checked in with sanity */
  20. os_time_t oti_last_checkin;
  21. /** Next time this task is scheduled to check-in with sanity */
  22. os_time_t oti_next_checkin;
  23. /** Name of this task */
  24. char oti_name[OS_TASK_MAX_NAME_LEN];
  25. };

参考:

1、http://mynewt.apache.org/latest/os/core_os/task/task.html

2.1.3 互斥量

互斥量是“mutual exclusion,互相排斥”的简写,一个互斥量提供了对共享资源的互斥访问。一个互斥量提供了优先级继承以防止出现优先级反转的情况。当一个高优先级的任务等待一个低优先级有用的资源时,出现优先级反转。使用互斥量,低优先级的任务将继承任何等待互斥量的任务的最高优先级。

描述

使用互斥量的第一步是全局声明互斥量,且互斥量需要在使用前完成初始化。在任务开始运行之前完成mutex的初始化是个好的习惯,这样可以避免任务在互斥量初始化之前使用。

当一个任务想要独占一个共享资源的访问,需要通过调用os_mutex_pend()来获得互斥量。如果当前互斥量被其他任务占有(低优先级任务),请求互斥量的任务将休眠,互斥量拥有任务的优先级将被提升到请求任务的优先级。注意:如果有多个任务共同请求访问互斥量,互斥量拥有任务的优先级将提升到所有这些任务中的最高优先级。当一个任务完成共享资源的访问,它需要通过调用os_mutex_release()释放互斥量。每次释放一个互斥量,仅能有一个请求得到满足。

接下来的示例将说明优先级继承是如何工作的。

API

参考:

1、http://mynewt.apache.org/latest/os/core_os/mutex/mutex.html

2.1.4 信号量

信号量是一个用于获得独占访问(类似mutex)的结构体,同步任务操作:与操作、或操作,将在生产者/消费者角色中使用。像Mynewt OS使用的信号量由于允许不只一个token,故而又叫做“计数”信号量。

描述

一个信号量是一个相对简单的结构,包含一个等待任务队列,信号量有用一定数量的token。只要信号量中还有token,就可以获得信号量。任何任务都可以往信号量中添加token,任何任务也可以请求信号量,然后减少token。当创建信号量时,初始信号量数量是可设置的。

当用于独占一个共享资源时,信号量只能允许一个token。在这种情况下,一个任务创建信号量,通过调用os_sem_init()设置一个token。当一个任务希望独占共享资源,通过调用os_sem_pend()请求信号量。如果信号量中还有token,那么请求任务将获得信号量,继续进行操作。如果已经没有可用的token,任务将休眠直到又有token。

使用信号量独占访问的一个常见问题是优先级反转,考虑以下这种情况:一个高优先级任务和一个低优先级任务,共享一个通过信号量加锁的资源。如果低优先级的任务获得了信号量,而高优先级的任务请求信号量,此时高优先级的任务也会不阻塞,直到低优先级的任务释放信号量。现在假设在高优先级和低优先级任务之间还有其他任务需要运行,这些任务将抢占有用信号量的低优先级任务。而高优先级的任务又被阻塞等待低优先级任务释放信号量,此时高优先级的任务的优先级反转了,实际上,它运行的优先级要低得多,它会抢占其他(较低优先级)的任务。对于这种情况,就应该使用互斥量而不是信号量,在Mynewt OS中,互斥量的优先级是可以继承的,从而避免高优先级任务被反转。

信号量也可以用于任务同步。如下是一个简单的示例:一个任务创建一个信号量,并初始化为没有token,此任务将等待信号量,处于sleep状态。当其他任务想要唤醒这个休眠中的任务,仅需要通过调用os_sem_release()增加一个token。此时,任务将被唤醒(前提是没有其他更高优先级的任务在运行)。

其他一些常用“计数”信号量的场景叫做生产者、消费者关系。生产者添加token(通过调用os_sem_release()),而消费者通过调用os_sem_pend()消耗它们。在这个关系下,生产者提供工作给消费者完成。每个添加到信号量中的token将导致消费者做任何需要的工作。一个简单的例子:每次按键,就会有一些工作要做(门铃),每次按键生产者就增加一个token,每个token都需要响铃,按键与响铃的数量是一致的。换句话说,每次调用os_sem_pend()减去一个token,而每次调用os_sem_release()增加一个token。

API

参考:

1、http://mynewt.apache.org/latest/os/core_os/semaphore/semaphore.html

2.1.5 事件队列

事件队列允许任务将传入事件序列化并简化事件处理。事件存储在一个队列中,任务从队列中取出事件并处理。事件在任务的上下文环境中处理。事件可能由系统调用,中断处理程序和其他任务。

描述

Mynewt系统的事件队列模型使用回调函数来处理事件。每个事件都与一个处理事件的回调函数相关联。这个模型支持一个没有实时性要求的库/包,在其实现中使用事件。通过使用一个应用程序事件队列取代了创建一个专用事件队列和任务来处理其事件。回调函数在应用程序创建的用于管理事件队列的任务上下文环境中运行。由于任务在创建是就分配了任务堆栈,从而这个模型减少了应用程序的内存需求。一个包若有实时性的需求,且需要在特定的任务优先级运行,那么应该创建一个专用的事件队列和任务来处理事件。

在Mynewt模型中,一个包定义了它的事件并为其实现了回调函数。没有实时性要求的包,应该使用Mynewt默认事件队列来处理其事件。Mynewt默认事件队列的回调函数在应用程序主任务上下文环境中执行。软件包可以选择性的导出一个函数,从而允许应用程序指定一个事件队列供此包使用。管理事件队列的应用任务处理函数只需要简单地从事件队列中提取事件,并在其上下文中执行事件回调函数即可。

Mynewt应用或包处理事件队列的常用方式是,在一个处于无限循环的任务中,不断调用os_eventq_get()函数从事件队列的头部取出事件,然后调用事件的回调函数来处理事件。os_eventq_get()函数若无法从事件队列中取到事件时,任务将休眠。其他任务(或中断)通过调用os_eventq_put()函数往事件队列中添加事件。os_eventq_put()函数决定了一个任务处于睡眠还是就绪。

一个任务可以使用os_eventq_run()封装函数,它将调用os_event_get()从队列中取事件并执行回调函数处理事件。

注意:

  • 只有一个任务消耗或阻塞等待事件队列的事件。(一个消费者原则)
  • 系统callout子系统使用事件作为定时器超时通知。

示例

以下为一个使用从BLE主机事件的示例。

  1. static void ble_hs_event_tx_notify(struct os_event *ev);
  2. /** OS event - triggers tx of pending notifications and indications. */
  3. static struct os_event ble_hs_ev_tx_notifications = {
  4. .ev_cb = ble_hs_event_tx_notify,
  5. };

.ev_cb指定事件的回调函数,os_event的定义如下:

  1. /**
  2. * Structure representing an OS event. OS events get placed onto the
  3. * event queues and are consumed by tasks.
  4. */
  5. struct os_event {
  6. /** Whether this OS event is queued on an event queue. */
  7. uint8_t ev_queued;
  8. /**
  9. * Callback to call when the event is taken off of an event queue.
  10. * APIs, except for os_eventq_run(), assume this callback will be called by
  11. * the user.
  12. */
  13. os_event_fn *ev_cb;
  14. /** Argument to pass to the event queue callback. */
  15. void *ev_arg;
  16. STAILQ_ENTRY(os_event) ev_next;
  17. };

API

2.1.6 Callout

Callout(调出)是Apache Mynewt操作系统的定时器。

描述

callout是设置系统定时器的一种方式。当定时器触发时,它作为一个事件发送到任务的事件队列。

用户可以通过调用os_callout_init()完成callout框架的初始化,或调用os_callout_func_init()初始化,然后调用os_callout_reset()复位,生效。

如果用户想在定时器超时前取消,可以通过调用os_callout_reset()来重置计数,延长超时;或通过调用os_callout_stop()直接停止。

Mynewt有两种不同的数据结构可使用。第一种是struct os_callout,是一个简单的版本,可以通过os_callout_init()初始化。第二种是struct os_callout_func,如果希望在任务中使用不同类型的定时器,且并发运行。这个结构包含一个函数指针,可以从任务事件处理循环中调用。

定时器的计数单位是系统的tick。这个值取决于所运行的平台,通常应该使用系统定义的OS_TICKS_PER_SEC来转换OS节拍,更加方便阅读。

callout定时器只触发一次,若需要周期性的定时器操作,需要在超时后不断的刷新。

API

2.1.7 堆(Heap)

提供了带有锁的malloc()/free()功能。当操作系统启动时,共享资源堆需要保护,避免并发访问。os_malloc()在调用malloc()之前增加了互斥量。

2.1.8 内存池

内存池是一个固定大小元素的集合。通常,内存池在需要为给定特性数据/结构分配一定数量内存时使用。与堆不同,代码模块将会受到其他模块的限制,以便有足够的内存(即有可能分不到内存),而内存池可以确保足够的内存分配。

描述

为了创建一个内存池,开发人员需要做一些处理。

第一需要定义内存池,这是一个数据结构,它包含了内存池本身的信息(内存块的数量,块的大小,等等)。

  1. struct os_mempool my_pool;

第二,分配内存池所需要分配的内存。内存可以是静态分配的(全局变量)或动态分配的(从heap分配)。当确定内存池需要的内存数量时,简单将每个区块大小乘以区块数量是不够的,因为操作系统在实现时会有内存对齐的需求。对齐的需求叫做OS_ALIGNMENT,在os_arch.h中定义,因为它是根据平台体系而定的。内存块对齐通常是为了提高效率,但也可能因为其他原因。通常,块是按照32-bit边界对齐的。注意,内存块必须有足够的大小来容纳一个列表指针,因为需要利用这个指针链接空闲列表上的内存块?

为了简化这一点,为用户提供了两个宏定义:OS_MEMPOOL_BYTES(n, blksize)OS_MEMPOOL_SIZE(n, blksize)。第一个宏返回内存池所需要的字节数,第二个宏则返回内存池所需要的os_membuf_t元素的数量。os_membuf_t类型用于保证内存池所先试用的内存缓冲区正确对齐。

这里有一些示例。注意,如果使用自定义的malloc实现,一定要确保内存池使用的内存缓冲区是正确对齐的(即OS_ALIGNMENT)。

  1. void *my_memory_buffer;
  2. my_memory_buffer = malloc(OS_MEMPOOL_BYTES(NUM_BLOCKS, BLOCK_SIZE));
  3. //注意,动态内存分配的内存,在不用时一定要注意释放
  4. os_membuf_t my_memory_buffer[OS_MEMPOOL_SIZE(NUM_BLOCKS, BLOCK_SIZE)];

如此,内存池也定义了,内存池所需要的内存也分配了,用户需要调用os_mempool_init()来初始化内存池。

  1. os_mempool_init(&my_pool, NUM_BLOCKS, BLOCK_SIZE, my_memory_buffer, "MyPool");

一旦内存池初始化完成,开发者可以通过调用os_memblock_get()从内存池中分配内存块。当内存块不在需要时,可以通过调用os_memblock_put()释放。

API

  1. /**
  2. * Memory pool
  3. */
  4. struct os_mempool {
  5. /** Size of the memory blocks, in bytes. */
  6. uint32_t mp_block_size;
  7. /** The number of memory blocks. */
  8. uint16_t mp_num_blocks;
  9. /** The number of free blocks left */
  10. uint16_t mp_num_free;
  11. /** The lowest number of free blocks seen */
  12. uint16_t mp_min_free;
  13. /** Bitmap of OS_MEMPOOL_F_[...] values. */
  14. uint8_t mp_flags;
  15. /** Address of memory buffer used by pool */
  16. uint32_t mp_membuf_addr;
  17. STAILQ_ENTRY(os_mempool) mp_list;
  18. SLIST_HEAD(,os_memblock);
  19. /** Name for memory block */
  20. char *name;
  21. };

2.1.9 内存缓冲区(Mbufs)

mbuf是内存缓冲区(memory buffer)的简写,是网络协议栈中的一个常见概念。mbuf在遍历协议栈时用于保存数据包数据。mbuf通常保存网络协议栈包中的头信息或其他网络协议栈信息。mbuf及其相关库函数被开发来使得常见的网络协议栈操作更加高效(如剥离和添加协议头),尽可能的少拷贝。

mbuf最简单的形式实际就是一个内存块,预留了一些用于内部信息的空间和一个指针,用于将内存块链接,从而可以创建一个数据包。这是mbuf的一个非常重要的方面:将mbuf链接起来创建更大的packets(mbufs链)的能力。

为什么使用内存缓冲区

最主要的原因就是节省内存。考虑一个通常发送一个很小的数据包,偶尔也会发送大数据包的网络协议,如BLE协议就是这样一个协议。一个扁平的缓冲区在设计时需要考虑到能在缓冲区中包含最大包的大小,那么缓冲区就会被设计的很大。而使用mbuf,许多的mbuf可以链接在一起,那么即使偶尔遇到大数据包也是可以处理的,这样就可以为网络协议栈中的小的数据包空出更多的数据缓冲区。

包头mbuf

并不是所有的mbuf都是平等的。在mbuf链中的第一个mbuf是一个特殊的mbuf,叫做“包头内存缓冲区”。这个mbuf之所以特殊的原因在于,它包含了mbuf链所包含的所有数据的长度(也就是说整个包的长度)。包头mbuf也可能包含一个用户自定义的结构(称作用户头),这样就可以将网络协议特定的信息传递到网络协议栈的各层。包中的任何mbuf(在mbuf链中,而不是第一个)都是包的一个部分,这些包也叫做普通mbuf。一个普通mbuf不包含任何包头或用户包头结构,只包含基础的mbuf头(struct os_mbuf)。图1中描述了这两种mbuf。主要,括号中数字或文本表示结构或元素的大小,MBLEN是mbuf池使用的内存池内存块的长度。

Packet header mbuf

普通mbuf

下面我们来深入研究mbuf结构,图2展示了一个普通的mbuf,在os_mbuf结构中划分了不同的字段。

OS mbuf structure

  1. /**
  2. * Chained memory buffer.
  3. */
  4. struct os_mbuf {
  5. /**
  6. * Current pointer to data in the structure
  7. */
  8. uint8_t *om_data;
  9. /**
  10. * Flags associated with this buffer, see OS_MBUF_F_* defintions
  11. */
  12. uint8_t om_flags;
  13. /**
  14. * Length of packet header
  15. */
  16. uint8_t om_pkthdr_len;
  17. /**
  18. * Length of data in this buffer
  19. */
  20. uint16_t om_len;
  21. /**
  22. * The mbuf pool this mbuf was allocated out of
  23. */
  24. struct os_mbuf_pool *om_omp;
  25. SLIST_ENTRY(os_mbuf) om_next;
  26. /**
  27. * Pointer to the beginning of the data, after this buffer
  28. */
  29. uint8_t om_databuf[0];
  30. };
  • om_data,指向数据缓冲区的指针。通常,从mbuf池中分配mbuf,om_data指针将指向数据缓冲的起始,但是在某些情况下,也有特例,如在数据包中添加一个协议头。
  • om_flags,mbuf库内使用的标志,目前还未具体定义标志。
  • om_pkthdr_len,mbuf中所有数据包头的长度。普通mbuf设置为0,因为普通mbuf并没有包头或用户包头。对于包头mbuf而言,将被设置为包头结构的长度(16)再加上用户包头的大小。注意,这个字段可以将包头mbuf与普通mbuf分开(为0即为普通mbuf,否则为包头mbuf)。
  • om_len,包含了数据缓冲区中的用户数据量。
  • om_omp,为一个os_mbuf_pool类型,指向mbuf池的指针,这个mbuf从外部分配。
  • om_next,一个链表元素,用于连接mbuf。

在图2中,还显示了普通mbuf在os_mbuf结构中拥有实际的值。此mbuf从地址0x1000开始,一共256字节。此示例中,用户拷贝了33字节到数据缓存区(从0x1010开始)。

Packet

在图3中,我们可以看到一个包头mbuf和两个普通mbuf链接在一起。在这个示例中,用户帧头定义了8个字节(16+8=24)。在此图中,我们看到了许多不同的mbuf,有不同的om_data指针和长度,对于所有的mbuf而言,内存块的总长度为128字节。

mbuf池

mbuf像内存块一样几种到mbuf池。mbuf池本身具有一个指向内存池的指针。在这个内存池中的内存块是实际的mbuf,普通mbuf或包头mbuf。因此,必须正确地调整内存块及对应的内存池。换言之,组成mbuf池所使用的内存池的内存块大小至少是:sizeof(struct os_mbuf) + sizeof(struct os_mbuf_pkthdr) + sizeof(struct user_defined_header)。例如,开发人员希望mbuf中至少包含64个字节的用户数据,且都有一个12字节的用户头,那么内存块的大小至少是:64+12+16+8,100字节。虽然开销不小,但是mbuf库提供的灵活性使得这个开销是值得的。

创建mbuf池

一个mbuf池的创建是比较简单的:创建一个内存池,然后用这个内存池创建mbuf池。一旦开发人员确定了每个mbuf所需的用户数据的大小(基于应用程序、协议栈),就可以调整内存块,以便能够最高效率的使用内存块。

在下面的例子中,应用程序需要每个mbuf包含64字节的用户数据,用户头结构体为struct user_hdr。注意,我们没有显示用户数据头结构,因为在我们创建内存池时并不需要立即考虑到,只需要考虑进来即可(先构思整体框架,构架每个部分,再对每个部分进行填充)。

在这个示例中,我们使用宏MBUF_PKTHDR_OVERHEAD来表示每个mbuf头的大小,使用MBUF_MEMBLOCK_OVERHEAD来表示每个内存块的开销。而MBUF_BUF_SIZE用于表示应用程序所需要的有效负载数量(本例中按照32位对齐)。以上所有内存的总和即为所需要的内存块大小,用MBUF_MEMBLOCK_OVERHEAD表示。

  1. #define MBUF_PKTHDR_OVERHEAD sizeof(struct os_mbuf_pkthdr) + sizeof(struct user_hdr)
  2. #define MBUF_MEMBLOCK_OVERHEAD sizeof(struct os_mbuf) + MBUF_PKTHDR_OVERHEAD
  3. #define MBUF_NUM_MBUFS (32)
  4. #define MBUF_PAYLOAD_SIZE (64)
  5. #define MBUF_BUF_SIZE OS_ALIGN(MBUF_PAYLOAD_SIZE, 4)
  6. #define MBUF_MEMBLOCK_SIZE (MBUF_BUF_SIZE + MBUF_MEMBLOCK_OVERHEAD)
  7. #define MBUF_MEMPOOL_SIZE OS_MEMPOOL_SIZE(MBUF_NUM_MBUFS, MBUF_MEMBLOCK_SIZE)
  8. struct os_mbuf_pool g_mbuf_pool;
  9. struct os_mempool g_mbuf_mempool;
  10. os_membuf_t g_mbuf_buffer[MBUF_MEMPOOL_SIZE];
  11. void
  12. create_mbuf_pool(void)
  13. {
  14. int rc;
  15. rc = os_mempool_init(&g_mbuf_mempool, MBUF_NUM_MBUFS,
  16. MBUF_MEMBLOCK_SIZE, &g_mbuf_buffer[0], "mbuf_pool");
  17. assert(rc == 0);
  18. rc = os_mbuf_pool_init(&g_mbuf_pool, &g_mbuf_mempool, MBUF_MEMBLOCK_SIZE,
  19. MBUF_NUM_MBUFS);
  20. assert(rc == 0);
  21. }

然后,定义全局的os_mbuf_pool以及os_mempool,并完成内存池及mbuf池的初始化。

系统mbuf

系统mbuf简写为msys,是一组构建在mbuf代码之上的API。msys的基本想法如下:开发人员可以创建不同大小的mbuf池,并将其注册到msys中。应用程序使用msys的API,而msys的代码将选择最小的能够满足需求的mbuf池。

在一个简单的示例中,用户在msys中注册了3个mbuf池,一个为32字节的mbuf,一个为256字节,一个为2048字节。如果用户请求一个10字节的mbuf,那么将使用32字节的mbuf。若请求一个33字节,那么就只有分配256字节的mbuf。如果要求的mbuf数据大小任何注册的mbuf,那么最大的内存池将被使用。虽然这种行为在所有情况下都不是最优的,但是却能在一定程度上简化使用。

如果选择的mbuf池时空的,msys代码将不会从较大的池中分配mbuf。类似的,msys的代码不会将许多较小的mbuf链接起来以适应请求的尺寸。虽然这种行为会在将来发生变化,但目前的代码只会返回NULL。

需注意,这里并没有添加任何关于msys的API描述,因为msys API的使用方式与mbuf API完全相同。唯一的区别在于mbuf池需要通过调用os_msys_register()注册到系统mbuf。

使用mbuf

有两种mbuf分配的基本API:os_mbuf_get()os_mbuf_get_pkthdr()。第一个API获得一个普通mbuf,后者获得一个包头mbuf。通常应用开发者将很少使用os_mbuf_get_pkthdr,如果有,需要调用os_mbuf_get()处理分配和链接mbuf操作。建议使用所提供的API复制到mbuf链中,并操作mbuf。

  1. void mbuf_usage_example1(uint8_t *mydata, int mydata_length)
  2. {
  3. int rc;
  4. struct os_mbuf *om;
  5. /* get a packet header mbuf*/
  6. om = os_mbuf_get_pkthdr(&g_mbuf_pool, sizeof(struct user_hdr));
  7. if(om)
  8. {
  9. rc = os_mbuf_copyinto(om, 0, mydata, len);
  10. if(rc)
  11. {
  12. return -1;
  13. }
  14. /* send packet to networking interface */
  15. send_pkt(om);
  16. }
  17. }

Mqueue及使用

mqueue结构允许任务在接受数据时唤醒。通常,这些数据是按照特定形式通过网络传输。一个常见的网络协议栈的操作是,在队列中放置一个包,并将一个事件发布到该队列的监视任务。当任务处理事件时,处理数据包队列上的每个数据包。

下面的代码展示了如何使用mqueue,在这个示例中:

  • 数据包接受并放到接受队列
  • 任务处理队列中处理每个数据包(增加接收计数)
  1. uint32_t pkts_rxd;
  2. struct os_mqueue rxpkt_q;
  3. struct os_eventq my_task_evq;
  4. /**
  5. * Removes each packet from the receive queue and processes it.
  6. */
  7. void
  8. process_rx_data_queue(void)
  9. {
  10. struct os_mbuf *om;
  11. while ((om = os_mqueue_get(&rxpkt_q)) != NULL) {
  12. ++pkts_rxd;
  13. os_mbuf_free_chain(om);
  14. }
  15. }
  16. /**
  17. * Called when a packet is received.
  18. */
  19. int
  20. my_task_rx_data_func(struct os_mbuf *om)
  21. {
  22. int rc;
  23. /* Enqueue the received packet and wake up the listening task. */
  24. rc = os_mqueue_put(&rxpkt_q, &my_task_evq, om);
  25. if (rc != 0) {
  26. return -1;
  27. }
  28. return 0;
  29. }
  30. void
  31. my_task_handler(void *arg)
  32. {
  33. struct os_event *ev;
  34. struct os_callout_func *cf;
  35. int rc;
  36. /* Initialize eventq */
  37. os_eventq_init(&my_task_evq);
  38. /* Initialize mqueue */
  39. os_mqueue_init(&rxpkt_q, NULL);
  40. /* Process each event posted to our eventq. When there are no events to
  41. * process, sleep until one arrives.
  42. */
  43. while (1) {
  44. os_eventq_run(&my_task_evq);
  45. }
  46. }

API

2.1.10 CPU时间模块

Mynewt的cputime模块提供了高精度时间和定时器的支持。

cputime模块提供了高分辨率的时间和定时器,使用前必须使用时钟频率初始化cputime模块,调用os_cputime_init()函数。此模块将使用硬件抽象层时钟hal_timer,以便访问硬件定时器,使用OS_CPUTIME_TIMER_NUM系统配置。

API

  1. int os_cputime_init(uint32_t clock_freq)

初始化cputime模块,必须在os_init之后再调用,且相关定义器的操作必须在此之后。

参数:

  • clock_freq,所需要的cputime的频率,单位为Hz
  1. uint32_t os_cputime_get32(void)

返回cputime的低32位。

  1. uint32_t os_cputime_nsecs_to_ticks(uint32_t nsecs)
  2. uint32_t os_cputime_ticks_to_nesec(uint32_t ticks)

纳秒时间与cputime的tick之间的转换,如果定义了OS_CPUTIME_FREQ_PWR2定义,则未定义此函数。

2.1.11 OS时间

Mynewt操作系统的系统时间。

描述

Mynewt操作系统包含一个递增的时间,用于驱动操作系统的调度器以及时间延迟。时间是固定大小的(如32位),最终会回滚到零。时间从0回到0称为操作系统的时间纪元。

OS时间刻度根据特定架构而定,在os_arch.h中定义OS_TICKS_PER_SEC。

Mynewt操作系统还提供了设置以及检索wallclock时间(在其他操作系统中也称为本地时间或一天中的时间)的接口。

数据结构

Mynewt中的时间格式为os_time_t

  1. struct os_timeval{
  2. int64_t tv_sec; /* 从1970年1月1日起的秒数 */
  3. int32_t tv_usec; /* 浮点部分秒数 */
  4. };
  5. struct os_timeval tv = {1457400000, 0}; /* 01:20:00 Mar 8 2016 UTC */

wallclock使用struct os_timevalstruct os_timezone来表示,os_timeval从1970年1月1日0时(UTC)开始。

struct os_timezone用于指定本地时间与UTC时间的偏移量(即时区)。注意,若本地时间低于UTC,则tz_minuteswest是正数,否则为负数。

  1. struct os_timezone {
  2. int16_t tz_minuteswest;
  3. int16_t tz_dsttime;
  4. };
  5. /* Pacific Standard Time is 08:00 hours west of UTC */
  6. struct os_timezone PST = { 480, 0 };
  7. struct os_timezone PDT = { 480, 1 };
  8. /* Indian Standard Time is 05:30 hours east of UTC */
  9. struct os_timezone IST = { -330, 0 };
函数 描述
os_time_advance() 增加系统的OS时间tick
os_time_delay() 将当前任务休眠特定tick
os_time_get() 获得当前的OS时间值
os_time_ms_to_ticks() 转换毫秒数到系统tick
os_get_uptime_usec() 获得启动耗时
os_gettimeofday() 得到当前的时间,填充特定的时间和时区结构
os_settimeofday() 将一天的时间设置给特定的时间结构

宏定义

os时间提供了几个宏定义用于评估彼此时间:

  • OS_TIME_TICK_LT,评估t1是否在t2前,结果为true
  • OS_TIME_TICK_GT,如果t1在t2后,结果为true
  • OS_TIME_TICK_GEQ,如果t1等于t2或在t2后,结果为true

注意:对于所有这些宏的使用,传递值都应该是os_time_t类型。

2.1.12 守护任务(Sanity)

Sanity任务是一个软件看门狗任务,它周期性的检查系统的状态,确保所有东西都正常运行。

在典型的系统设计中,分为了多个阶段的看门狗:

  • 内部看门狗,通常为MCU的看门狗,在操作系统的核心喂狗。内部看门狗将周期性的触发,表示操作系统正常运行。
  • 外部看门狗,是一种通常运行较慢的看门狗,目的是为系统失控时提供硬件复位。
  • sanity看门狗,sanity是最不常用的看门狗,应用于应用程序。

描述

Mynewt操作系统通过使用系统空闲任务检查应SANITY_INTERVAL系统配置,设置了执行sanity任务的周期。

默认情况下,每个操作系统任务都提供了周期性sanity检查,通过在os_task_init()中传入sanity_itvl参数。

  1. int os_task_init(struct os_task *t, char *name, os_task_func_t func,
  2. void *arg, uint8_t prio, os_time_t sanity_itvl, os_stack_t *bottom,
  3. uint16_t stack_size);

sanity_itvl使用系统时钟tick,任务创建时需要注册到sanity任务中。

检查正常任务

任务初始化后注册了sanity_itvl秒之后将运行sanity任务,为了完成此功能,需要调用os_sanity_task_checkin()函数,将重置该任务的sanity检查。如下示例中是一个使用callout每隔50秒调用sanity的示例:

  1. #define TASK1_SANITY_CHECKIN_ITVL (50 * OS_TICKS_PER_SEC)
  2. struct os_eventq task1_evq;
  3. static void
  4. task1(void *arg)
  5. {
  6. struct os_task *t;
  7. struct os_event *ev;
  8. struct os_callout c;
  9. /* Get current OS task */
  10. t = os_sched_get_current_task();
  11. /* Initialize the event queue. */
  12. os_eventq_init(&task1_evq);
  13. /* Initialize the callout */
  14. os_callout_init(&c, &task1_evq, NULL);
  15. /* reset the callout to checkin with the sanity task
  16. * in 50 seconds to kick off timing.
  17. */
  18. os_callout_reset(&c, TASK1_SANITY_CHECKIN_ITVL);
  19. while (1) {
  20. ev = os_eventq_get(&task1_evq);
  21. /* The sanity timer has reset */
  22. if (ev->ev_arg == &c) {
  23. os_sanity_task_checkin(t);
  24. } else {
  25. /* not expecting any other events */
  26. assert(0);
  27. }
  28. }
  29. /* Should never reach */
  30. assert(0);
  31. }