diff --git a/doc/source/develop/index.rst b/doc/source/develop/index.rst index 7227079db044ee5e619908f7a912fae8fe58732a..623186af81406edb0abc85cf92532eaa239ac732 100644 --- a/doc/source/develop/index.rst +++ b/doc/source/develop/index.rst @@ -9,3 +9,4 @@ subsys/index.rst driver/index.rst osserivces/index.rst + kernel/index.rst diff --git a/doc/source/develop/kernel/condition_variables.rst b/doc/source/develop/kernel/condition_variables.rst new file mode 100644 index 0000000000000000000000000000000000000000..7d64eb2adad7ae25b7a729bd32002059ea915086 --- /dev/null +++ b/doc/source/develop/kernel/condition_variables.rst @@ -0,0 +1,272 @@ +.. _kernel_condition_variables: + +同步-条件变量 +############# + +条件变量通常用于控制共享资源的访问,它允许一个线程等待其它线程创建共享资源需要的条件。 + +使用 +==== + +API +--- + +``#define K_CONDVAR_DEFINE(name)`` + +- 作用:定义一个k_condvar,并初始化 + +- name: k_condvar name + +``int k_condvar_init(struct k_condvar *condvar)`` + +- 作用: 初始化条件变量 + +- condvar: 要初始化的condvar 返回值: 0标示初始化成功 + +``int k_condvar_signal(struct k_condvar *condvar)`` + +- 作用:通知条件变量有效,最先加入条件变量列队的thread将优先获得该有效条件 + +- condvar: 有效的条件变量 返回值: 0表示成功 + +``int k_condvar_broadcast(struct k_condvar *condvar)`` + +- 作用:广播条件变量有效,所有条件变量列队的thread将获得该有效条件 + +- condvar: 有效的条件变量 返回值: 0表示成功 + +``int k_condvar_wait(struct k_condvar *condvar, struct k_mutex *mutex, k_timeout_t timeout)`` + +- 作用:等待条件变量有效 condvar:等待的条件变量 + +- mutex:资源锁,条件变量和mutex搭配使用,在等待条件变量期间该mutex会被unlock + +- timeout: 等待时间,单位ms。K_NO_WAIT不等待, K_FOREVER一直等 + +- 返回值: 0表示等待成功,-EAGAIN表示等待超时 + +使用说明 +-------- + +初始化 +~~~~~~ + +下面两种方法都可以定义&初始化一个条件变量 编译期初始化 + +:: + + K_CONDVAR_DEFINE(my_condvar); + +运行期初始化 + +:: + + struct k_condvar my_condvar; + k_condvar_init(&my_condvar); + +等待条件有效 +~~~~~~~~~~~~ + +:: + + void thread_get(void) + { + k_mutex_lock(&mutex, K_FOREVER); + + //等待使用资源的条件有效,等待器件会解锁mutex,当等到后会重新上锁Mutex + k_condvar_wait(&condvar, &mutex, K_FOREVER); + + //条件变量有效,使用共享资源 + ... + k_mutex_unlock(&mutex); + } + +通知条件变量有效 +~~~~~~~~~~~~~~~~ + +通知条件变量有效有两种方法: + +通知一次,只有一个thread可以获得条件有效 + +:: + + void worker_thread(void) + { + k_mutex_lock(&mutex, K_FOREVER); + //通知条件有效 + k_condvar_signal(&condvar); + k_mutex_unlock(&mutex); + } + +广播,所有等待该条件的thread都会获得条件有效 + +:: + + void worker_thread(void) + { + k_mutex_lock(&mutex, K_FOREVER); + //广播条件有效 + k_condvar_broadcast(&condvar); + k_mutex_unlock(&mutex); + } + +使用条件变量需要搭配mutex来控制对共享资源的互斥访问。条件变量只是用来通知条件满足,条件变量不包含条件本身。 + +实现 +==== + +struct k_condvar的结构体如下: + +:: + + struct k_condvar { + _wait_q_t wait_q; + }; + +条件变量就是由内核的wait_q实现的,条件变量的实现代码在kernel/condvar.c文件中,Zephyr中对内核对象的访问需要通过系统调用,Zephyr会将kernel.h中的API最后转化为调用系统调用的API例如: +k_condvar_init->z_impl_k_condvar_init +k_condvar_signal->z_impl_k_condvar_signal +k_condvar_broadcast->z_impl_k_condvar_broadcast +k_condvar_wait->z_impl_k_condvar_wait + + +初始化 +------ + +代码非常简单,无论是使用那种方式初始化都是初始化一个wait_q + +:: + + int z_impl_k_condvar_init(struct k_condvar *condvar) + { + //初始化wait_q + z_waitq_init(&condvar->wait_q); + + //初始化object,给userspace用,非userspace无作用 + z_object_init(condvar); + + SYS_PORT_TRACING_OBJ_INIT(k_condvar, condvar, 0); + + return 0; + } + +:: + + #define Z_CONDVAR_INITIALIZER(obj) \ + { \ + .wait_q = Z_WAIT_Q_INIT(&obj.wait_q), \ + } + +等待条件满足 +------------ + +等待条件满足,就是将thread加入到条件变量的wait_q内等待,等待过程中会将资源锁放掉,当条件满足后又重新拿资源锁 + +:: + + int z_impl_k_condvar_wait(struct k_condvar *condvar, struct k_mutex *mutex, + k_timeout_t timeout) + { + k_spinlock_key_t key; + int ret; + + //锁调度,避免后面放资源锁的时候引发调度 + key = k_spin_lock(&lock); + + //释放资源锁 + k_mutex_unlock(mutex); + + //将当前线程加入到wait_q中,挂起当前线程,切换上下文 + ret = z_pend_curr(&lock, key, &condvar->wait_q, timeout); + + //条件变量满足后,会从wait_q取出挂起的线程从这里恢复执行 + + //重新拿到资源锁 + k_mutex_lock(mutex, K_FOREVER); + + + return ret; + } + +通知条件满足 +------------ + +通知条件满足会从条件变量的wait_q中取出一个等待的thread恢复执行,实现分析如下 + +:: + + int z_impl_k_condvar_signal(struct k_condvar *condvar) + { + //锁调度 + k_spinlock_key_t key = k_spin_lock(&lock); + + //从条件变量的wait_q中选出最合适的thread + struct k_thread *thread = z_unpend_first_thread(&condvar->wait_q); + + if (thread != NULL) { + //如果存在等待条件变量的thread + + //设置返回为0 + arch_thread_return_value_set(thread, 0); + + //将其转为就绪状态 + z_ready_thread(thread); + + //引发重新调度 + z_reschedule(&lock, key); + } else { + //如果不存在等待条件变量的thread,解锁调度,退出 + k_spin_unlock(&lock, key); + } + return 0; + } + +广播条件满足 +------------ + +广播条件满足会从条件变量的wait_q中取出所有的thread恢复执行,实现分析如下 + +:: + + int z_impl_k_condvar_broadcast(struct k_condvar *condvar) + { + struct k_thread *pending_thread; + k_spinlock_key_t key; + int woken = 0; + + //锁调度 + key = k_spin_lock(&lock); + + + //从wait_q中取出所有thread进行恢复 + while ((pending_thread = z_unpend_first_thread(&condvar->wait_q)) != + NULL) { + //设置返回为0 + arch_thread_return_value_set(pending_thread, 0); + //将其转为就绪状态 + z_ready_thread(pending_thread); + } + + //引发调度 + z_reschedule(&lock, key); + + return woken; + } + +和信号量的区别 +============== + +初看条件变量总有一点信号量的影子,Zephyr中二者主要有如下区别: + +1. 信号量无法进行广播。 + +2. 多值信号量可以积压,没有消费者发送的信号依然被保存,而条件变量发送后没有消费者接受条件就过期了。 + +3. 条件变量必须搭配互斥锁使用。 + +4. Zephyr中信号量可以用于Poll,条件变量则不行。 + +参考 +==== + +https://docs.zephyrproject.org/latest/reference/kernel/synchronization/condvar.html diff --git a/doc/source/develop/kernel/data_passing.rst b/doc/source/develop/kernel/data_passing.rst new file mode 100644 index 0000000000000000000000000000000000000000..9bc02a70668aa2fa4597340b0e09662e35b3e9e3 --- /dev/null +++ b/doc/source/develop/kernel/data_passing.rst @@ -0,0 +1,109 @@ +.. _kernel_data_passing: + +数据传递 +######### + +Zephyr OS内核提供6种内核对象用于在Thread/ISR之间传递数据,分别是: + +* FIFO: 先进先出 + +* LIFO: 后进先出 + +* Stack: 堆栈 + +* Message Queue: 消息队列 + +* MailBox:邮箱 + +* Pipe:管道 + + +官方文档中列出了一个表,和清晰的说明这6种内核对象的特性 |datapassing| + +内核对象特性 +============ + +FIFO/LIFO +--------- + +FIFO使用queue实现,因此FIFO内保存的数据是离散的,不需要占用连续的物理内存。 +发送数据插入在queue list的尾部,从queue +list头部接收数据而达到先进先出的目的。 +LIFO也使用queue实现,但是是在queue +list的尾部插入数据和读取数据,因此是后进先出。LIFO的其它特性和FIFO一样 +FIFO/LIFO传递的数据,并不是将所有数据放入FIFO/LIFO,而是传递指向数据的指针。因此传递的数据可以是任意大小。 +可以在ISR中put FIFO/LIFO. 也可在ISR内get FIFO/LIFO,但不能等待。 +FIFO/LIFO不限制数据项的多少. 线程get +FIFO/LIFO后,数据项从FIFO/LIFO中删除,如果FIFO/LIFO为空,线程可进行等待。 +允许多个线程等待同一个FIFO/LIFO,当FIFO/LIFO有数据时会分配给等待时间最长的最高优先级线程。 + +Stack +----- + +既然有了LIFO为什么还要Stack呢, +lifo是将离散的内容通过lifo串接起来已后进先出的方式管理,没有数量的限制。Stack是将一片连续的内存空间已后进先出的方式管理,在初始化的时候就进行了数量限制。 +可以在ISR中push stack。 也可在ISR内pop stack,但不能等待。 +stack有大小限制,满了后无法push入数据,之后的行为有stack使用者控制,内核自己不可预期。 +允许多个线程等待同一个STACK,当STACK有数据时会分配给等待时间最长的最高优先级线程。 + +Message Queue +------------- + +Message Queue初始化时已经定义了每个Q +message大小,和message的总数量,一个message queue对应一个ring buffer +收发的message都放在ring buffer中。ring +buffer必须按N对齐,N是2的幂,message的大小必须是N的倍数。 +message可以通过线程或ISR发送到消息队列。发送message就是将message放入ring +buffer, 如果ring buffer满了,线程内可以等待发送,但ISR中发送不能等待。 +多个thread可以向同一个message queue发送message,当ring +buffer满时,所有thread都发生等待,当有ring +buffer有空间时,会让等待时间最长的最高优先级线程发送message。 +接收message类似于发送,允许多个thread从同一个message +queue接收message,当ring buffer空时,所有的thread都会等待,当ring +buffer有数据时,会让等待时间最长的最高优先级线程先获得message. +ISR也可以接收message,但不能等待。 +线程也可以peek消息队列顶部的消息,peek不会删除ring buffer中的消息。 + +MailBox +------- + +邮箱只允许线程间交换邮件,ISR不能使用邮箱 +每个邮件只能由一个线程接收,不支持点对多点或者广播。 +邮件收发线程相互知道对方,不是匿名传递。 +邮箱消息包含零个或多个字节的消息数据。消息数据的大小和格式是由应用程序定义的,每条消息的格式都可以不同。 +邮件数据的有两种形式存放: 1. +消息缓冲区由发送或接收消息的线程提供的内存区域。例如数组或结构变量。 2. +是消息块是从内存池分配的内存区域。 + +如果一条消息没有消息缓冲区和消息块,称为空消息。消息缓冲区或内存块存在但包含零字节实际数据的消息不是空消息。 +发送线程创建一条消息将其发送给邮箱时,该邮件归邮箱所有,直到将其提供给接收线程为止。 +发送线程可以将邮件发给指定线程,也可以用K_ANY将其发送给任意线程。接收线程也从指定线程接收邮件指定线程,也可以用K_ANY接收任意线程的邮件。 +邮箱可以同步发送:发送阻塞,直到接收线程接收到消息,这是有一个隐性的流控制机制:在接收线程未处理完全,发送线程是阻塞的。 +异步发送:发送不阻塞,需要邮箱的用户显示的进行流控制。 + +管道 +---- + +管道用于两个线程之间流式传递数据,可以同步也可以异步。ISR不能使用PIPE。 +如果指定了pipe的size,说明pipe是有ring +buffer的,如果未指定pipe不使用ring buffer。如果有ring +buffer,pipe发送数据没发送完的部分会放入到ring buffer中。 +同步发送时,线程可以通过管道发送全部或者部分数据,管道会尝试尝试尽可能多的发送数据,但如果不能立即满足要发送的最小长度,该发送会失败。接收的数据被复制到ring +buffer中或者直接被pipe的读取者读走。 + +使用限制 +======== + +下图说明了数据传递内核对象的使用限制,绿色的线的常规做法,红色的线的做法虽然允许,但不建议使用。 +|limit| 这里提前说一下为什么会有这些限制: +ISR里面不能等待,是因为ISR本身有时间处理的要来,另外这些对象的等待都是依赖thread +wait_q实现,ISR不会支持 +ISR不能使用mailbox/pipe,因为mailbox和pipe都是依赖thread的swap_data来交换数据,ISR不会支持。不过异步传输是否可用于ISR值得研究。 + +参考 +==== + +https://docs.zephyrproject.org/latest/reference/kernel/index.html + +.. |datapassing| image:: ../../images/develop/kernel/datapassing.png +.. |limit| image:: ../../images/develop/kernel/limit.png diff --git a/doc/source/develop/kernel/event.rst b/doc/source/develop/kernel/event.rst new file mode 100644 index 0000000000000000000000000000000000000000..bee9375de3e5c658745d45a06a21f2ec9212a71f --- /dev/null +++ b/doc/source/develop/kernel/event.rst @@ -0,0 +1,402 @@ +.. _kernel_event: + +同步-事件 +########## + +事件对象是一种线程同步对象,允许一个或者多个线程等待同一个事件对象,当事件被传递到事件对象时,满足条件的线程都变为就绪。通常使用事件来通知一系列条件已满足。在Zephyr中每个事件对象使用一个32bit数来跟踪传递的事件集,每个bit表示一个事件。事件可以由ISR或者线程发送,发送事件有两种方法: + +* 设置:覆盖现有的事件集,重写这个32bit数 + +* 发布:以bit方式\ **添加**\ 事件到事件基,注意这里只能添加,不能删除 + +线程可以等待一个或多个事件。在等待多个事件时可以指定等待其中部分或者全部事件。线程在提出等待事件对象时,可以选择在等待前清空等待事件对象上的所有事件。 + + +API +=== + +宏定义 +------ + +**K_EVENT_DEFINE(name)** + +- 静态的定义和初始化事件对象。 + +- **name**  事件对象名 + +函数 +---- + +**void k_event_init(struct k_event *event)** + +- 初始化事件对象,在使用事件对象前首先使用该函数对其进行初始化。 + +- **event** 事件对象的地址 + +**void k_event_post(struct k_event *event, uint32_t events)** + +- 发布一个或多个事件到事件对象。如果等待\ ``event``\ 的线程因为发布的事件满足等待条件,该线程被转为就绪,与设置事件不同,发布的事件是增加到事件对象中。 + +- **event**  事件对象的地址 + +- **events**  发布的事件集合 + +**void k_event_set(struct k_event *event, uint32_t events)** + +设置事件集合到事件对象,将事件对象中的事件设置为\ ``events``\ 。如果等待\ ``event``\ 的线程因为设置的事件满足等待条件,该线程被转为就绪。与发布事件不同,设置事件会取代事件对象中的事件集。 + +- **event**  事件对象的地址 +- **events**  设置的事件集合 + +**uint32_t k_event_wait(struct k_event *event, uint32_t events, bool reset, k_timeout_t timeout)** + +等待任意指定事件,在\ ``event``\ 上等待,有任意指定的事件被发送,或者是等待时间超过\ ``timeout``\ 。一个线程最多可以等待32个事件,这些事件由\ ``events``\ 中的bit位置标识。 + +**注意** +``reset = true``\ 将在等待事件前,将\ ``event``\ 中已发生的事件,使用的时候请特别注意。 + +- **event**  事件对象的地址 +- **events**  等待的事件集 +- **reset** 如果为\ ``true``\ ,等待前清空\ ``event``\ 中已发生的事件,如果为false则不清空 +- **timeout**  指定等待事件的最长事件,可以指定为\ ``K_NO_WAIT`` 和\ ``K_FOREVER`` + +- 返回值 **事件集**  等待成功后返回收到匹配的事件集 **0** 指定时间内未收到匹配事件 + +**uint32_t k_event_wait_all(struct k_event *event, uint32_t events, bool reset, k_timeout_t timeout)** + +等待所有指定事件,在\ ``event``\ 上等待,指定的所有事件被发送,或者是等待时间超过\ ``timeout``\ 。一个线程最多可以等待32个事件,这些事件由\ ``events``\ 中的bit位置标识。 + +**注意** +``reset = true``\ 将在等待事件前,将\ ``event``\ 中已发生的事件,使用的时候请特别注意。 + +- **event** 事件对象的地址 +- **events** 等待的事件集 +- **reset** 如果为\ ``true``\ ,等待前清空\ ``event``\ 中已发生的事件,如果为false则不清空 +- **timeout** 指定等待事件的最长事件,可以指定为\ ``K_NO_WAIT`` 和 ``K_FOREVER`` + +返回值 + +- **事件集** 等待成功后返回收到匹配的事件集 +- **0** 指定时间内未收到匹配事件 + +使用 +==== + +配置 +---- + +事件对象的配置选项是\ ``CONFIG_EVENTS``\ ,zephyr内核没有给事件对象配置选项设置默认值,编译时将被识别为未配置,因此要使用事件对象需要增加配置\ ``CONFIG_EVENTS=y``\ 。 + +示例 +---- + +**初始化事件对象** + +使用函数 + +.. code:: c + + struct k_event my_event; + k_event_init(&my_event); + +等效于 + +.. code:: c + + K_EVENT_DEFINE(my_event); + +**设置事件** + +下列示例在中断服务程序中设置事件为0x001 + +.. code:: c + + void input_available_interrupt_handler(void *arg) + { + /* 通知线程数据有效 */ + k_event_set(&my_event, 0x001); + + ... + } + +**发布事件** + +下面示例在中断服务程序中发布事件0x120,如果前面的设置事件未被清除,此时\ ``my_event``\ 中的事件为0x121 + +.. code:: c + + void input_available_interrupt_handler(void *arg) + { + ... + + /* notify threads that more data is available */ + + k_event_post(&my_event, 0x120); + + ... + } + +**等待事件** + +下面示例等待事件50毫秒,在50毫秒内只要前面的设置和发布示例中任意一个发生都会等待成功 + +.. code:: c + + void consumer_thread(void) + { + uint32_t events; + + events = k_event_wait(&my_event, 0xFFF, false, K_MSEC(50)); + if (events == 0) { + printk("No input devices are available!"); + } else { + /* 收到事件,根据events进行处理 */ + ... + } + ... + } + +下面示例等待事件50毫秒,在50毫秒没只有前面的设置和发布示例都发生了才会等待成功 + +.. code:: c + + void consumer_thread(void) + { + uint32_t events; + + events = k_event_wait_all(&my_event, 0x121, false, K_MSEC(50)); + if (events == 0) { + printk("At least one input device is not available!"); + } else { + /* 事件全部收齐,进行处理 */ + ... + } + ... + } + +代码分析 +======== + +.. raw:: html + + + +事件对象的代码实现在\ ``kernel\events.c``\ ,事件对象是由\ ``struct k_event``\ 进行管理 + +.. code:: c + + struct k_event { + _wait_q_t wait_q; + uint32_t events; + struct k_spinlock lock; + }; + +- ``wait_q`` 用于管理等待该事件对象的线程 + +- ``events`` 用于保存当前事件对象收到的事件 + +- ``lock`` 用于保护内核对事件对象的操作的原子性 + +事件对象的所有操作都是围绕着\ ``struct k_event``\ 进行的。 + +初始化 +------ + +函数实现代码如下,就是对\ ``struct k_event``\ 定义事件的各个成员进行初始化 + +.. code:: c + + void z_impl_k_event_init(struct k_event *event) + { + event->events = 0; + event->lock = (struct k_spinlock) {}; + + z_waitq_init(&event->wait_q); + + z_object_init(event); + } + +用宏可以达到同时定义和初始化的目的,实现如下 + +.. code:: c + + #define Z_EVENT_INITIALIZER(obj) \ + { \ + .wait_q = Z_WAIT_Q_INIT(&obj.wait_q), \ + .events = 0 \ + } + + #define K_EVENT_DEFINE(name) \ + STRUCT_SECTION_ITERABLE(k_event, name) = \ + Z_EVENT_INITIALIZER(name); + +等待事件 +-------- + +等待事件可以等待任意指定事件和全部事件,在\ ``events.c``\ 中都是由同一个内部函数\ ``k_event_wait_internal``\ 实现,只是指定的参数不一样 + +.. code:: c + + uint32_t z_impl_k_event_wait(struct k_event *event, uint32_t events, + bool reset, k_timeout_t timeout) + { + uint32_t options = reset ? K_EVENT_WAIT_RESET : 0; + + return k_event_wait_internal(event, events, options, timeout); + } + + uint32_t z_impl_k_event_wait_all(struct k_event *event, uint32_t events, + bool reset, k_timeout_t timeout) + { + /* 使用K_EVENT_WAIT_ALL选项,表示要等待所有的事件收齐 */ + uint32_t options = reset ? (K_EVENT_WAIT_RESET | K_EVENT_WAIT_ALL) + : K_EVENT_WAIT_ALL; + + return k_event_wait_internal(event, events, options, timeout); + } + +内部函数\ ``k_event_wait_internal``\ 的第三个参数\ ``opetions``\ 用于指定等待的选项,选项定义在\ ``events.c``\ 中 + +.. code:: c + + #define K_EVENT_WAIT_ANY 0x00 /* 有1个或者以上事件满足就可退出等待 */ + #define K_EVENT_WAIT_ALL 0x01 /* 所有事件满足才可退出等待 */ + #define K_EVENT_WAIT_RESET 0x02 /* 等待事件前先清空已有事件 */ + + #define K_EVENT_WAIT_MASK 0x01 /* 用于获取等待类型 */ + +.. code:: c + + static uint32_t k_event_wait_internal(struct k_event *event, uint32_t events, + unsigned int options, k_timeout_t timeout) + { + uint32_t rv = 0; + unsigned int wait_condition; + struct k_thread *thread; + + /* isr中只能不做任何等待的等待事件 */ + __ASSERT(((arch_is_in_isr() == false) || + K_TIMEOUT_EQ(timeout, K_NO_WAIT)), ""); + + /* 不允许等待的事件集为0,相当于未等待任何事件 */ + if (events == 0) { + return 0; + } + + wait_condition = options & K_EVENT_WAIT_MASK; + thread = z_current_get(); + + k_spinlock_key_t key = k_spin_lock(&event->lock); + + /* 检查是否需要清空已有事件 */ + if (options & K_EVENT_WAIT_RESET) { + event->events = 0; + } + + /* 检查事件对象已有的事件是否已经满足线程,如果满足则退出 */ + if (are_wait_conditions_met(events, event->events, wait_condition)) { + rv = event->events; + + k_spin_unlock(&event->lock, key); + goto out; + } + + /* 如果等待的超时未立即退出,则不进行等待,此时rv=0 */ + if (K_TIMEOUT_EQ(timeout, K_NO_WAIT)) { + k_spin_unlock(&event->lock, key); + goto out; + } + + /* 将线程要等待的事件集和方式保存到线程中 */ + thread->events = events; + thread->event_options = options; + + /* 等待事件发生,如果等待超时rv仍然保持为0 */ + if (z_pend_curr(&event->lock, key, &event->wait_q, timeout) == 0) { + /* 等待事件已发生,发送事件者将把满足的事件交换到线程内的events中, + rv中保存了等待到的事件 + */ + rv = thread->events; + } + + out: + /* 由于发生的事件可能会超出等待的事件,因此需要做位与返回 */ + return rv & events; + } + +发送事件 +-------- + +发送事件分为设置和发布,在\ ``events.c``\ 中都是由同一个内部函数\ ``k_event_post_internal``\ 实现,只是指定的参数不一样 + +.. code:: c + + void z_impl_k_event_post(struct k_event *event, uint32_t events) + { + k_event_post_internal(event, events, true); + } + + void z_impl_k_event_set(struct k_event *event, uint32_t events) + { + k_event_post_internal(event, events, false); + } + +内部函数\ ``k_event_post_internal``\ 的第三个参数\ ``accumulat``\ 为\ ``true``\ 时表示发送的\ ``events``\ 是添加到\ ``event``\ 内,为\ ``false``\ 时表示是覆盖\ ``event``\ 的已有事件 + +.. code:: c + + static void k_event_post_internal(struct k_event *event, uint32_t events, + bool accumulate) + { + k_spinlock_key_t key; + struct k_thread *thread; + unsigned int wait_condition; + struct k_thread *head = NULL; + + /* 上锁,保证内核对事件对象操作的原子性 */ + key = k_spin_lock(&event->lock); + + /* 检查是附加事件,还是要重置事件 */ + if (accumulate) { + /* 附加事件 */ + events |= event->events; + } + /* 对发生的事件进行更新 */ + event->events = events; + + /* 遍历事件对象中wait_q内存放等待的thread,并将事件能满足的线程加入到单链表中 */ + _WAIT_Q_FOR_EACH(&event->wait_q, thread) { + /* 获取等待类型K_EVENT_WAIT_MASK为0x01,只取最低位, + 这里wait_condition是K_EVENT_WAIT_ANY或K_EVENT_WAIT_ALL + */ + wait_condition = thread->event_options & K_EVENT_WAIT_MASK; + + /* 根据等待类型对线程进行事件匹配,匹配上的线程放入单链表head中 */ + if (are_wait_conditions_met(thread->events, events, + wait_condition)) { + thread->next_event_link = head; + head = thread; + } + } + + /* 对事件匹配上的线程单链表进行遍历,通知线程就绪 */ + if (head != NULL) { + thread = head; + do { + z_unpend_thread(thread); + arch_thread_return_value_set(thread, 0); + /* 更新线程收到的事件 */ + thread->events = events; + /* 线程恢复就绪 */ + z_ready_thread(thread); + thread = thread->next_event_link; + } while (thread != NULL); + } + /* 发送完事件后,引发调度,让刚变为就绪线程有机会执行 */ + z_reschedule(&event->lock, key); + } + +参考 +==== + +https://docs.zephyrproject.org/latest/reference/kernel/synchronization/events.html diff --git a/doc/source/develop/kernel/fifo_lifo.rst b/doc/source/develop/kernel/fifo_lifo.rst new file mode 100644 index 0000000000000000000000000000000000000000..c9774821a9703db5e14a262541ef99898532e7b4 --- /dev/null +++ b/doc/source/develop/kernel/fifo_lifo.rst @@ -0,0 +1,510 @@ +.. _kernel_fifo_lifo: + +数据传递-FIFO/LIFO +#################### + +API +--- + +相似API +~~~~~~~ + +FIFO和LIFO API都是用宏包装queue的API实现,它们有一组类似的发送接收API,这里放在一起介绍 + +**#define k_fifo_init(fifo)** **#define k_lifo_init(lifo)** + +作用:初始化一个struct k_fifo/strut k_lifo fifo/lifo: struct + +k_fifo/strut k_lifo + +**#define k_fifo_put(fifo, data)** **#define k_lifo_put(lifo, data)** + +作用:将数据放入fifo/lifo fifo/lifo:数据要放入的fifo/lifo data: + +要放入fifo/lifo的数据 + +**#define k_fifo_alloc_put(fifo, data)** **#define k_lifo_alloc_put(lifo, data)** + +作用:fifo/lifo分配一个空间,将数据放入 + +fifo/lifo:数据要放入的fifo/lifo data: 要放入fifo/lifo的数据 + +**#define k_fifo_get(fifo, timeout)** **#define k_lifo_get(lifo,timeout)** + +作用:从fifo/lifo读出数据 fifo/lifo:要读的fifo/lifo data: +读出的数据 + +FIFO特殊API +~~~~~~~~~~~ + +FIFO比LIFO多一些API +**#define k_fifo_cancel_wait(fifo)** + +作用:放弃等待,让第一个等待的fifo的thread退出pending,k_fifo_get的数据将是NULL + +fifo: 操作的fifo + +**#define k_fifo_put_list(fifo, head, tail)** + +作用:将一个单链表加入到fifo中 fifo: 操作的fifo head: 单链表的第一个节点 + +tail: 单链表的最后一个节点 + +**#define k_fifo_put_slist(fifo, list)** + +作用:将一个单链表加入到fifo中,这里单链表对象为sys_slist_t fifo: + +操作的fifo list: sys_slist_t 单链表 + +**#define k_fifo_is_empty(fifo)** + +作用:检查fifo是否为空 + +fifo: 要检查的fifo + +**#define k_fifo_peek_head(fifo)** + +作用:peek fifo头部上的节点(下个将会被read的数据),但不会将数据从fifo删除 + +fifo: 被peek的fifo 返回:返回peek的数据 + +**#define k_fifo_peek_tail(fifo)** + +作用: peek fifo尾部上的节点(FIFO中最后进入的数据),但不会将数据从fifo删除 +fifo: 被peek的fifo 返回:返回peek的数据 + +使用说明 +-------- + +可以在ISR中put fifo/lifo.也可在ISR内get fifo/fifo,但不能等待。 +fifo/lifo不限制数据项的多少。 + +初始化 +~~~~~~ + +经过初始化的fifo/lifo才能被使用,下面两种方法是一样的,显示定义 + +:: + + struct k_fifo my_fifo; + struct k_lifo my_lifo; + + k_fifo_init(&my_fifo); + k_lifo_init(&my_lifo); + +隐式定义: + +:: + + K_FIFO_DEFINE(my_fifo); + K_LIFO_DEFINE(my_lifo); + +写数据 +~~~~~~ + +这里用fifo演示put,用Lifo演示alloc_put + +:: + + //使用put发送数据,最开始的word必须保留,用于存放queue,这也就是为什么要求4对其的原因 + //使用alloc_put没有这个限制 + struct data_item_t { + void *fifo_reserved; /* 1st word reserved for use by fifo */ + ... + }; + + struct data_item_t fifo_tx_data; + char lifo_tx_data[32]; + + void producer_thread(int unused1, int unused2, int unused3) + { + while (1) { + /* create data item to send */ + //fifo_tx_data使用的是put,因此最开始的4个字节不能使用 + fifo_tx_data = ... + lifo_tx_data = ... + + /* send data to consumers */ + k_fifo_put(&my_fifo, &fifo_tx_data); + //alloc_put时,queue会从线程池中分配出内存存放lifo,所以不需要保留开始4个字节 + //在get时,queue会自动将其释放掉 + k_lifo_alloc_put(&my_lifo, &lifo_tx_data); + + ... + } + } + +读数据 +~~~~~~ + +:: + + void consumer_fifo_thread(int unused1, int unused2, int unused3) + { + struct data_item_t *rx_data; + + while (1) { + rx_data = k_fifo_get(&my_fifo, K_FOREVER); + + /* process fifo data item */ + ... + } + } + + void consumer_lifo_thread(int unused1, int unused2, int unused3) + { + struct data_item_t *rx_data; + + while (1) { + rx_data = k_lifo_get(&my_lifo, K_FOREVER); + + /* process lifo data item */ + ... + } + } + +实现 +==== + +前面已经提到过fifo/lifo都是使用queue实现,fifo/lifo是直接包queue的API,每一个fifo/lifo API都对应一个queue的API,可以说fifo/lifo就是queue的使用方法不一样,先看结构体就可以看出使用的就是queue + +FIFO/LIFO +----------- + +:: + + struct k_fifo { + struct k_queue _queue; + }; + + struct k_lifo { + struct k_queue _queue; + }; + +每个fifo/lifo都有一个对应的queue API,这里列出来,更详细的可以看include/kernel.h,下面斜体加粗的就是fifo/lifo区别所在 +k_fifo_init->k_queue_init k_fifo_cancel_wait->k_queue_cancel_wait +**k_fifo_put->k_queue_append** +**k_fifo_alloc_put->k_queue_alloc_append** +k_fifo_put_list->k_queue_append_list +k_fifo_put_slist->k_queue_merge_slist k_fifo_get->k_queue_get +k_fifo_is_empty->k_queue_is_empty k_fifo_peek_head->k_queue_peek_head +k_fifo_peek_tail->k_queue_peek_tail k_lifo_init->k_queue_init +**k_lifo_put->k_queue_prepend** +**k_lifo_alloc_put->k_queue_alloc_prepend** k_lifo_get->k_queue_get + +Queue +----- + +从上一节可以看出FIFO就是用queue实现的,因此我们继续分析queue + +Queue初始化 +~~~~~~~~~~~ + +初始化:建立一个wait_q,建立一个单项标志链表 + +添加数据到queue +~~~~~~~~~~~~~~~ + +添加数据到queue基本都是通过queue_insert完成 +k_queue_append->queue_insert: 在链表尾部插入 +k_queue_insert->queue_insert: 在指定节点后插入 +k_queue_prepend->queue_insert:在链表头部插入 +z_impl_k_queue_alloc_append->queue_insert:从thread +pool中分配节点,并插入到尾部 +z_impl_k_queue_alloc_prepend->queue_insert:从thread +pool中分配节点,并插入到头部 z_impl_k_queue_peek_head -> +z_queue_node_peek: 从链表中读取头,但不删除该节点 +z_impl_k_queue_peek_tail -> z_queue_node_peek: +从链表中读取尾,但不删除该节点 + +:: + + static s32_t queue_insert(struct k_queue *queue, void *prev, void *data, + bool alloc) + { + k_spinlock_key_t key = k_spin_lock(&queue->lock); + #if !defined(CONFIG_POLL) + struct k_thread *first_pending_thread; + + //获取等待queue的thread,如果有的话,将数据提供给该thread,引发调度,直接返回 + first_pending_thread = z_unpend_first_thread(&queue->wait_q); + + if (first_pending_thread != NULL) { + prepare_thread_to_run(first_pending_thread, data); + z_reschedule(&queue->lock, key); + return 0; + } + #endif /* !CONFIG_POLL */ + + /* Only need to actually allocate if no threads are pending */ + if (alloc) { + //要alloc节点空间 + struct alloc_node *anode; + //从线程池中alloc节点空间 + //并初始化数据 + anode = z_thread_malloc(sizeof(*anode)); + if (anode == NULL) { + k_spin_unlock(&queue->lock, key); + return -ENOMEM; + } + anode->data = data; + //初始化节点的时候,设置标志为1,表示是alloc的,在get的时候就需要释放 + sys_sfnode_init(&anode->node, 0x1); + data = anode; + } else { + //如果data中自带节点,则无需alloc + sys_sfnode_init(data, 0x0); + } + //将节点插入到指定节点之后 + //prev如果为NULL,data节点会被插入到单链表的最开始 + sys_sflist_insert(&queue->data_q, prev, data); + + //如果配置了poll,会通知queue条件已经满足 + #if defined(CONFIG_POLL) + handle_poll_events(queue, K_POLL_STATE_DATA_AVAILABLE); + #endif /* CONFIG_POLL */ + + //引发调度 + z_reschedule(&queue->lock, key); + return 0; + } + +当传入参数data是净数据时(没有在data的最开始预留node位置),那么在insert的使用需要用alloc指定要分配节点 + +从Queue中读取数据 +~~~~~~~~~~~~~~~~~ + +使用k_queue_get->z_impl_k_queue_get从queue中读取数据,读取数据时数据从queue中删除,实际的删除动作就是从链表中将节点移除 + +:: + + void *z_impl_k_queue_get(struct k_queue *queue, s32_t timeout) + { + k_spinlock_key_t key = k_spin_lock(&queue->lock); + void *data; + + if (likely(!sys_sflist_is_empty(&queue->data_q))) { + sys_sfnode_t *node; + //如果链表不为空,从链表中取出头节点 + node = sys_sflist_get_not_empty(&queue->data_q); + //从节点中取出数据,如果node是从线程池中分配的,将其释放 + data = z_queue_node_peek(node, true); + k_spin_unlock(&queue->lock, key); + //取到数据后立即返回 + return data; + } + + if (timeout == K_NO_WAIT) { + k_spin_unlock(&queue->lock, key); + return NULL; + } + + #if defined(CONFIG_POLL) + k_spin_unlock(&queue->lock, key); + + return k_queue_poll(queue, timeout); + + #else + //没有取到数据,进入等待 + //可以结合发送看,如果有线程在等待queue数据,那么新进入queue的数据是不会进入链表的 + int ret = z_pend_curr(&queue->lock, key, &queue->wait_q, timeout); + + return (ret != 0) ? NULL : _current->base.swap_data; + #endif /* CONFIG_POLL */ + } + +从node中提取数据 +~~~~~~~~~~~~~~~~ + +从链表中peek或者移出的node,需要使用下面API从node中提取数据 + +:: + + void *z_queue_node_peek(sys_sfnode_t *node, bool needs_free) + { + void *ret; + + if ((node != NULL) && (sys_sfnode_flags_get(node) != (u8_t)0)) { + //检查到flag 不为0,说明加入链表的node是从线程池中分配 + struct alloc_node *anode; + + //这里取出数据后释放node + anode = CONTAINER_OF(node, struct alloc_node, node); + ret = anode->data; + if (needs_free) { + k_free(anode); + } + } else { + //不是从线程池分配的node,直接返回 + ret = (void *)node; + } + + return ret; + } + +如果是移出的node在Insert的时候是从线程池alloc出来的,那么在这里需要needs_free=true指定要free空间。如果是Peek的node数据时,则只用提取数据,例如: + +:: + + static inline void *z_impl_k_queue_peek_head(struct k_queue *queue) + { + return z_queue_node_peek(sys_sflist_peek_head(&queue->data_q), false); + } + + static inline void *z_impl_k_queue_peek_tail(struct k_queue *queue) + { + return z_queue_node_peek(sys_sflist_peek_tail(&queue->data_q), false); + } + +放弃等待数据 +~~~~~~~~~~~~ + +k_queue_get的时候如果queue内没有数据,允许进行等待。等待期间可以使用k_queue_cancel_wait放弃等待 + +:: + + void z_impl_k_queue_cancel_wait(struct k_queue *queue) + { + k_spinlock_key_t key = k_spin_lock(&queue->lock); + #if !defined(CONFIG_POLL) + struct k_thread *first_pending_thread; + //获取正在等待的第一个线程 + first_pending_thread = z_unpend_first_thread(&queue->wait_q); + + //让该线程退出等待 + if (first_pending_thread != NULL) { + prepare_thread_to_run(first_pending_thread, NULL); + } + #else + handle_poll_events(queue, K_POLL_STATE_CANCELLED); + #endif /* !CONFIG_POLL */ + //重调度 + z_reschedule(&queue->lock, key); + } + +关于poll +~~~~~~~~ + +一般情况下queue等待数据是通过让其thread +等待queue的wait_q实现,一旦配置poll后将不再使用该机制,而是通过poll,queue +get使用k_queue_poll等待queue有数据,因此可能和其它thread等待poll条件发生冲突 + +:: + + static void *k_queue_poll(struct k_queue *queue, s32_t timeout) + { + struct k_poll_event event; + int err, elapsed = 0, done = 0; + k_spinlock_key_t key; + void *val; + u32_t start; + //初始化要等待的条件 + k_poll_event_init(&event, K_POLL_TYPE_FIFO_DATA_AVAILABLE, + K_POLL_MODE_NOTIFY_ONLY, queue); + + if (timeout != K_FOREVER) { + start = k_uptime_get_32(); + } + + do { + event.state = K_POLL_STATE_NOT_READY; + //开始poll + err = k_poll(&event, 1, timeout - elapsed); + + if (err && err != -EAGAIN) { + return NULL; + } + //通知后去链表拿数据 + key = k_spin_lock(&queue->lock); + val = z_queue_node_peek(sys_sflist_get(&queue->data_q), true); + k_spin_unlock(&queue->lock, key); + + //如果没拿到数据,说明可能被其它thread将数据拿走,需要重新等待 + if ((val == NULL) && (timeout != K_FOREVER)) { + elapsed = k_uptime_get_32() - start; + done = elapsed > timeout; + } + } while (!val && !done); + + return val; + } + +插入链表数据 +~~~~~~~~~~~~ + +queue也提供2个API将数据以链表的形式插入到queue中,区别是k_queue_append_list插入后不会影响被插入链表,而k_queue_merge_slist插入链表后会把链表清空 + +:: + + int k_queue_append_list(struct k_queue *queue, void *head, void *tail) + { + /* invalid head or tail of list */ + CHECKIF(head == NULL || tail == NULL) { + return -EINVAL; + } + + k_spinlock_key_t key = k_spin_lock(&queue->lock); + #if !defined(CONFIG_POLL) + struct k_thread *thread = NULL; + //有等待的thread全部取出来消化这个链表 + //注意,消化完后原来的链表还在 + if (head != NULL) { + thread = z_unpend_first_thread(&queue->wait_q); + } + + while ((head != NULL) && (thread != NULL)) { + prepare_thread_to_run(thread, head); + head = *(void **)head; + thread = z_unpend_first_thread(&queue->wait_q); + } + + //没有消化完的加入到queue中 + if (head != NULL) { + sys_sflist_append_list(&queue->data_q, head, tail); + } + + #else + sys_sflist_append_list(&queue->data_q, head, tail); + handle_poll_events(queue, K_POLL_STATE_DATA_AVAILABLE); + #endif /* !CONFIG_POLL */ + + z_reschedule(&queue->lock, key); + + return 0; + } + +:: + + int k_queue_merge_slist(struct k_queue *queue, sys_slist_t *list) + { + int ret; + + /* list must not be empty */ + CHECKIF(sys_slist_is_empty(list)) { + return -EINVAL; + } + + //和k_queue_append_list功能一样 + ret = k_queue_append_list(queue, list->head, list->tail); + CHECKIF(ret != 0) { + return ret; + } + //但消化完链表后,原有链表被清空 + sys_slist_init(list); + + return 0; + } + +图例 +---- + +下面用一张图简单说明FIFO/LIFO和queue操作之间的关系 |QUEUE| +可以看到FIFO是通过k_queue_appen将数据加入到queue的链表尾,LIFO通过k_queue_prepend将数据加入到queue的链表头,而FIFO/LIFO读数据都是用k_queue_get从链表头读数据,所以达到了FIFO先进先出,LIFO后进先出的目的。 + +参考 +==== + +https://docs.zephyrproject.org/latest/reference/kernel/data_passing/fifos.html +https://docs.zephyrproject.org/latest/reference/kernel/data_passing/lifos.html + +.. |QUEUE| image:: ../../images/develop/kernel/queue.png diff --git a/doc/source/develop/kernel/index.rst b/doc/source/develop/kernel/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..560b82179442d6904e93d0e18b3c67181df86c25 --- /dev/null +++ b/doc/source/develop/kernel/index.rst @@ -0,0 +1,24 @@ +.. _kernel: + +内核 +###### + +Zephyr内核分析和使用方法。 + +.. toctree:: + :maxdepth: 1 + + workq.rst + system_thread.rst + synchronization.rst + sem.rst + mutex.rst + poll.rst + event.rst + condition_variables.rst + data_passing.rst + stack.rst + fifo_lifo.rst + msgq.rst + mailbox.rst + pipe.rst diff --git a/doc/source/develop/kernel/mailbox.rst b/doc/source/develop/kernel/mailbox.rst new file mode 100644 index 0000000000000000000000000000000000000000..ba06862c572d2bc5a02b6f9c4005745cc40afaf4 --- /dev/null +++ b/doc/source/develop/kernel/mailbox.rst @@ -0,0 +1,727 @@ +.. _kernel_mailbox: + +数据传递-邮箱 +############### + +Zephyr的邮箱(mailbox)可以看作是升级版的消息队列(msgq),与msgq不一样的是:邮箱的消息可以指定发送者和接收者,邮箱消息的大小可以不固定,长度也没有对齐的要求。此外邮箱只能用于线程间交换消息,不能用于ISR内。 + +邮箱构成和特性 +=============== + +Zephyr允许定义任意数量的邮箱,最大数量只由可用内存的量决定。 +一个邮箱主要是由发送队列和消息队列两个wait_q组才,如下定义: + +:: + + struct k_mbox { + _wait_q_t tx_msg_queue; + _wait_q_t rx_msg_queue; + struct k_spinlock lock; + }; + +- 发送队列:用于保存已发送但还未被接收的消息 +- 接收队列:用于保存正在等待消息的线程 + +当发送线程发送消息到邮箱后会阻塞等待(同步发送)接收线程完成消息接收才退出等待,或是不阻塞而是等待接收线程完成消息接收后通过信号量通知(异步发送)。 +邮箱支持: + +* 一对一: A发到邮箱的消息只能B收, B只收A的消息 +* 一对多: A发到邮箱的消息任意线程都可以收,注意不是广播。 +* 多对一: B线程可以收任意线程发到邮箱中消息。 + +邮箱不支持广播,邮箱中的消息只能被一个线程使用。 + +消息 +---- + +消息描述符 +~~~~~~~~~~ + +消息是由消息描述符描述,指定消息的内存地址,并描述消息如何被邮箱处理。发送线程和接收线程在访问邮箱时都需要提供消息描述符,消息描述符相互匹配才能传递消息。 +消息描述符的结构如下: + +:: + + struct k_mbox_msg { + uint32_t _mailbox; + size_t size; + uint32_t info; + void *tx_data; + void *_rx_data; + struct k_mem_block tx_block; + k_tid_t rx_source_thread; + k_tid_t tx_target_thread; + k_tid_t _syncing_thread; + #if (CONFIG_NUM_MBOX_ASYNC_MSGS > 0) + struct k_sem *_async_sem; + #endif + }; + +``_mailbox``\ 和\ ``_rx_data``\ 这两个字段已经不再使用 +``_syncing_thread`` +用于通知发送者发送的消息已经被接收,\ ``_async_sem``\ 用于异步发送后通知消息已经完成,这两个字段都是内部使用。 + +消息描述符中容纳消息的方式有两种: + +* 缓冲:给定一段buffer,消息数据放于其中 +* 内存块:申请的一片内存块,消息数据放于其中 + +实际的消息发送二选一,如果二者都为NULL,则表示发送空消息。 + +``tx_data`` 发送缓冲消息的内存指针,如果是内存块消息或者空消息该字段为NULL,接收者不初始化该字段 +``tx_block`` 内存块id,发送内存块消息时使用该字段。如果是缓冲消息或是空消息该字段为NULL,接收者不初始化该字段。 +``size`` 消息大小,允许为0,为0时表示空消息. 接收时表示允许接收消息的最大size,如果收到的消息不是想要的发送方的size将为设为0,如果消息被接收,发送方的size将被更新为实际接收的大小。 +``info`` 发送者和接收者在消息接收时双向交换字段,32bit大小 +``tx_target_thread`` 接收消息的线程id,接收者消息不初始化该字段,接收者接收该消息后邮箱会将该字段更新为为实际接收者的id。当指定为K_ANY时该消息可以被任意线程接收。 +``rx_source_thread`` 发送消息的线程id,发送者消息不初始化该字段,接收者接收该消息后邮箱会将该字段更新为为实际发送者的id。当指定为K_ANY时该线程可接收任意消息。 + +消息的生命周期 +~~~~~~~~~~~~~~ + +创建消息:发送线程将消息发送到邮箱 消息保存在邮箱 +删除消息:接收线程从邮箱接收消息,并将其消耗 + +使用 +==== + +API +--- + +``#define K_MBOX_DEFINE(name)`` + +作用:定义一个k_mbox,并初始化 + +name: k_mbox name + +``void k_mbox_init(struct k_mbox *mbox)`` + +作用: 初始化k_mbox + +mbox: 要初始化的mbox + +返回值: 0标示初始化成功 + +``int k_mbox_put(struct k_mbox *mbox, struct k_mbox_msg *tx_msg, k_timeout_t timeout)`` +作用:同步发送消息到邮箱,会等到有线程从邮箱接收,或是超时才退出。如果发送超时消息将不会被保存在邮箱中 + +mbox: 邮箱 tx_msg: 发送消息的描述符,包含了接收者,消息信息,见前文说明 + +timeout: 等待时间,单位ms。K_NO_WAIT不等待, K_FOREVER一直等 + +返回值:0发送成果,-ENOMSG不等待发送失败,-EAGAIN 等待超时 + +``void k_mbox_async_put(struct k_mbox *mbox, struct k_mbox_msg *tx_msg, struct k_sem *sem)`` + +作用:异步发送消息到邮箱,发送后立即返回,有线程从邮箱接收该消息时使用sem通知。 + +mbox: 邮箱 tx_msg: 发送消息的描述符,包含了接收者,消息信息,见前文说明 + +sem:接收消息通知信号量 + +``int k_mbox_get(struct k_mbox *mbox, struct k_mbox_msg *rx_msg, void *buffer, k_timeout_t timeout)`` + +作用:从邮箱接收消息。 + +mbox: 邮箱 + +rx_msg: 接收消息的描述符,指定发送者,接收消息的信息,见前文说明 + +buffer: 接收消息数据存放buffer,如果为NULL,邮箱将保存该消息,消息的信息保存在rx_msg中,直到使用k_mbox_data_get消耗该消息才会从邮箱中删除 + +timeout: 等待时间,单位ms。K_NO_WAIT不等待, K_FOREVER一直等 + +返回值:0发送成果,-ENOMSG不等待发送失败,-EAGAIN 等待超时 + +``void k_mbox_data_get(struct k_mbox_msg *rx_msg, void *buffer)`` + +作用:将消息数据搬运到buffer中,并从邮箱中删除该消息。 + +rx_msg: 接收消息的描述符,包含了要接收消息的信息 + +buffer: 接收消息数据存放buffer,如果为NULL将直接丢弃该消息 + +使用说明 +-------- + +初始化 +~~~~~~ + +先定义初始化一个邮箱,下面两种方式的效果是一样的 使用宏 + +:: + + K_MBOX_DEFINE(my_mailbox); + +使用函数 + +:: + + struct k_mbox my_mailbox; + k_mbox_init(&my_mailbox); + +发送消息 +~~~~~~~~ + +同步 + +:: + + void producer_thread(void) + { + char buffer[100]; + int buffer_bytes_used; + + struct k_mbox_msg send_msg; + + while (1) { + + //准备要发送的消息数据 + ... + buffer_bytes_used = ... ; + memcpy(buffer, source, buffer_bytes_used); + + //准备消息描述符 + send_msg.info = 123; + send_msg.size = buffer_bytes_used; + send_msg.tx_data = buffer; + send_msg.tx_block.data = NULL; + send_msg.tx_target_thread = consumer_thread_id; + + //发送消息到mailbox,并等待被接收 + k_mbox_put(&my_mailbox, &send_msg, K_FOREVER); + + //接收完毕退出等待,检查info, size, tx_target_thread 的更新 + //info和接收者交换会变为456 + //size变为实际接收的30 + + /* verify that message data was fully received */ + if (send_msg.size < buffer_bytes_used) { + printf("some message data dropped during transfer!"); + printf("receiver only had room for %d bytes", send_msg.info); + } + } + } + +异步,异步发送函数\ ``k_mbox_async_put``\ 在配置了\ ``CONFIG_NUM_MBOX_ASYNC_MSGS=y``\ 后才会有效 + +:: + + void producer_thread(void) + { + char buffer[100]; + int buffer_bytes_used = 100; + + struct k_mbox_msg send_msg; + + struct k_sem rev_sem; + k_sem_init(&rev_sem, 0, 10); + + while (1) { + + //准备消息描述符 + ... + buffer_bytes_used = ... ; + memcpy(buffer, source, buffer_bytes_used); + + /* prepare to send message */ + send_msg.info = 123; + send_msg.size = buffer_bytes_used; + send_msg.tx_data = buffer; + send_msg.tx_block.data = NULL; + send_msg.tx_target_thread = consumer_thread_id; + + //发送消息到mailbox + k_mbox_async_put(&my_mailbox, &send_msg, &rev_sem); + + //等待消息被接收通知信号量 + k_sem_take(&rev_sem, K_FOREVER); + + //接收完毕退出等待,检查info, size, tx_target_thread 的更新 + //info和接收者交换会变为456 + //size变为实际接收的30 + + /* verify that message data was fully received */ + if (send_msg.size < buffer_bytes_used) { + printf("some message data dropped during transfer!"); + printf("receiver only had room for %d bytes", send_msg.info); + } + } + } + +接收消息 +~~~~~~~~ + +:: + + void consumer_thread(void) + { + struct k_mbox_msg recv_msg; + char buffer[100]; + + int i; + int total; + + while (1) { + //准备消息描述符 + recv_msg.info = 456; + recv_msg.size = 30; + recv_msg.rx_source_thread = producer_thread_id; + + //等待接收消息 + k_mbox_get(&my_mailbox, &recv_msg, buffer, K_FOREVER); + + //接收完毕退出等待,检查info, size, rx_target_thread 的更新 + //info和发收者交换会变为123 + //size为实际接收的数据长度30 + + /* verify that message data was fully received */ + if (recv_msg.info != recv_msg.size) { + printf("some message data dropped during transfer!"); + printf("sender tried to send %d bytes", recv_msg.info); + } + + /* compute sum of all message bytes (from 0 to 100 of them) */ + total = 0; + for (i = 0; i < recv_msg.size; i++) { + total += buffer[i]; + } + } + } + +实现 +==== + +该小节通过对邮箱内核代码的分析,理解Zephyr是如何实现以上描述的功能特性. +mailbox的实现代码在kernel:raw-latex:`\mailbox`.c中 ## 初始化 +前文提到过mailbox的核心就是两个wait_q,初始化则是对发送和接收的wait_q进行初始话,并初始化一个lock用于操作wait_q时锁调度 + +:: + + void k_mbox_init(struct k_mbox *mbox) + { + z_waitq_init(&mbox->tx_msg_queue); + z_waitq_init(&mbox->rx_msg_queue); + mbox->lock = (struct k_spinlock) {}; + } + + +发送消息 +-------- + +这里主要分析同步发送,\ ``k_mbox_put->mbox_message_put`` + +:: + + int k_mbox_put(struct k_mbox *mbox, struct k_mbox_msg *tx_msg, + k_timeout_t timeout) + { + //发送消息的线程在消息被接收前会被放入tx_msg_queue中挂起,这里将当前thread保存在消息中的_syncing_thread,方便消息被接收后对_syncing_thread进行恢复 + tx_msg->_syncing_thread = _current; + + int ret = mbox_message_put(mbox, tx_msg, timeout); + + return ret; + } + +消息发送的主要流程位于\ ``mbox_message_put``\ 中,有下面两种情况: + +1. 遍历邮箱\ ``rx_msg_queue``\ 中等待消息的线程,如果找到匹配的接收线程,让接收线程变为就绪,挂起当前线程。 +2. 如果没找到匹配的接收线程,将消息放入\ ``tx_msg_queue``\ ,挂起当前线程并等待超时。 + +代码详细分析如下: + +:: + + static int mbox_message_put(struct k_mbox *mbox, struct k_mbox_msg *tx_msg, + k_timeout_t timeout) + { + struct k_thread *sending_thread; + struct k_thread *receiving_thread; + struct k_mbox_msg *rx_msg; + k_spinlock_key_t key; + + //设置描述符中发送线程id为当前线程 + tx_msg->rx_source_thread = _current; + + //发送线程要交换的数据被设置为消息描述符 + sending_thread = tx_msg->_syncing_thread; + sending_thread->base.swap_data = tx_msg; + + //锁调度,开始处理消息发送 + key = k_spin_lock(&mbox->lock); + + //检查rx_msg_queue中是否有线程在等待接收邮箱消息 + _WAIT_Q_FOR_EACH(&mbox->rx_msg_queue, receiving_thread) { + //获取接收消息描述符 + rx_msg = (struct k_mbox_msg *)receiving_thread->base.swap_data; + + //将收发消息描述进行匹配 + if (mbox_message_match(tx_msg, rx_msg) == 0) { + //该消息有线程可匹配,让接收线程从挂起恢复 + z_unpend_thread(receiving_thread); + + //pend返回设置为0,表示有正常拿到消息 + arch_thread_return_value_set(receiving_thread, 0); + //让接收线程处于就绪态,下一次调度的时候接收线程就会取得消息进行处理 + z_ready_thread(receiving_thread); + + #if (CONFIG_NUM_MBOX_ASYNC_MSGS > 0) + //异步发送流程,接收线程处理消失时不做阻塞 + if ((sending_thread->base.thread_state & _THREAD_DUMMY) + != 0U) { + z_reschedule(&mbox->lock, key); + return 0; + } + #endif + + //同步发送流程,异步线程处理消息时,发送消息线程被直接挂起,异步线程处理完后会通过线程交换数据中保存的tx_msg->_syncing_thread让发送线程进入就绪继续运行 + int ret = z_pend_curr(&mbox->lock, key, NULL, K_FOREVER); + + return ret; + } + } + + //没有匹配的接收线程,同时也不等待消息发送,就立即退出 + if (K_TIMEOUT_EQ(timeout, K_NO_WAIT)) { + k_spin_unlock(&mbox->lock, key); + return -ENOMSG; + } + + #if (CONFIG_NUM_MBOX_ASYNC_MSGS > 0) + //异步发送时会用一个dummy thread来进行等待消息接收完成,真正的发送线程会立即退出 + if ((sending_thread->base.thread_state & _THREAD_DUMMY) != 0U) { + //这里是在发送线程中执行,而挂起的dummy thread,因此不会卡在这里 + z_pend_thread(sending_thread, &mbox->tx_msg_queue, K_FOREVER); + k_spin_unlock(&mbox->lock, key); + return 0; + } + #endif + //同步发送,如果没有匹配的线程接收,当前线程被加入tx_msg_queue中,等待超时。因为tx_msg是放到当前线程的交换数据内,这个操作就相当于将消息放入了邮箱 + int ret = z_pend_curr(&mbox->lock, key, &mbox->tx_msg_queue, timeout); + + return ret; + } + +接收 +---- + +消息接收的流程和发送的流程是对称的,有下面两种情况: 1. +遍历邮箱\ ``tx_msg_queue``\ 中查看是否有消息,如果找到匹配的消息,接收线程接收处理该消息后通知发送线程恢复执行。 +2. +如果没找到匹配的消息,将接收线程放入\ ``rx_msg_queue``\ ,挂起当前线程并等待接收消息超时。 + +代码详细分析如下: + +:: + + int k_mbox_get(struct k_mbox *mbox, struct k_mbox_msg *rx_msg, void *buffer, + k_timeout_t timeout) + { + struct k_thread *sending_thread; + struct k_mbox_msg *tx_msg; + k_spinlock_key_t key; + int result; + + //设置描述符中接收线程id为当前线程 + rx_msg->tx_target_thread = _current; + + //锁调度,开始处理消息接收 + key = k_spin_lock(&mbox->lock); + + //检查邮箱的tx_msg_queue中是否有可用消息 + _WAIT_Q_FOR_EACH(&mbox->tx_msg_queue, sending_thread) { + //从线程交换数据中获取发送消息描述符 + tx_msg = (struct k_mbox_msg *)sending_thread->base.swap_data; + + //将收发消息描述进行匹配 + if (mbox_message_match(tx_msg, rx_msg) == 0) { + //该消息和接收线程匹配,将发送消息的线程从邮箱tx_msg_queue中取出 + z_unpend_thread(sending_thread); + + k_spin_unlock(&mbox->lock, key); + + //处理数据,数据处理完后会让发送线程就绪并重新调度。如果这里buffer为空,会将数据保留,同时发送线程仍然处理挂起状态。 + result = mbox_message_data_check(rx_msg, buffer); + + return result; + } + } + + //没有匹配的消息,同时也不等待,退出本次接收 + if (K_TIMEOUT_EQ(timeout, K_NO_WAIT)) { + k_spin_unlock(&mbox->lock, key); + return -ENOMSG; + } + + //如果没有匹配的消息,当前线程被加入rx_msg_queue中,等待超时 + _current->base.swap_data = rx_msg; + result = z_pend_curr(&mbox->lock, key, &mbox->rx_msg_queue, timeout); + + //接收到消息进行消息处理,数据处理完后会让发送线程就绪并重新调度。如果这里buffer为空,会将数据保留,同时发送线程仍然处理挂起状态。 + if (result == 0) { + result = mbox_message_data_check(rx_msg, buffer); + } + + return result; + } + +消息匹配 +-------- + +从前面的分析看到消息匹配是使用\ ``mbox_message_match``\ 对收发消息描述符进行比较,比较过程中主要有如下事项: + +1. 比较收发的thread id是否匹配 + +2. 进行thread id交换 + +3. 进行info交换 + +4. 更新实际可以接收的size + +5. 将消息的数据地址更新到接收消息描述符内 + +6. 将发送线程id更新到接收消息描述符中 + +:: + + static int mbox_message_match(struct k_mbox_msg *tx_msg, + struct k_mbox_msg *rx_msg) + { + uint32_t temp_info; + + //匹配thread id + if (((tx_msg->tx_target_thread == (k_tid_t)K_ANY) || + (tx_msg->tx_target_thread == rx_msg->tx_target_thread)) && + ((rx_msg->rx_source_thread == (k_tid_t)K_ANY) || + (rx_msg->rx_source_thread == tx_msg->rx_source_thread))) { + + //更新收发者的id,主要是給一方是K_ANY用,否则不会有变化 + rx_msg->rx_source_thread = tx_msg->rx_source_thread; + tx_msg->tx_target_thread = rx_msg->tx_target_thread; + + //交换info + temp_info = rx_msg->info; + rx_msg->info = tx_msg->info; + tx_msg->info = temp_info; + + //计算实际可以接收的数据 + if (rx_msg->size > tx_msg->size) { + rx_msg->size = tx_msg->size; + } + + //将消息的数据地址更新到接收消息描述符内 + rx_msg->tx_data = tx_msg->tx_data; + rx_msg->tx_block = tx_msg->tx_block; + if (rx_msg->tx_data != NULL) { + rx_msg->tx_block.data = NULL; + } else if (rx_msg->tx_block.data != NULL) { + rx_msg->tx_data = rx_msg->tx_block.data; + } else { + /* no data */ + } + + //将发送线程id更新到接收消息描述符中 + rx_msg->_syncing_thread = tx_msg->_syncing_thread; + + return 0; + } + + return -1; + } + +消息处理 +-------- + +邮箱消息数据的处理有内部函数\ ``mbox_message_data_check``\ 和外部函数\ ``k_mbox_data_ge``\ t两个。在\ ``k_mbox_get``\ 传入的参数\ ``buffer=NULL``\ 时,\ ``mbox_message_data_check``\ 检查到时消息会被保留在邮箱。之后可以再通过\ ``k_mbox_data_get``\ 来读取该消息,如果此时\ ``buffer=NULL``\ 该消息就会从邮箱中取出丢弃。 +代码分析如下: + +:: + + static int mbox_message_data_check(struct k_mbox_msg *rx_msg, void *buffer) + { + if (buffer != NULL) { + //buffer不为NULL将数据读到buffer中 + k_mbox_data_get(rx_msg, buffer); + } else if (rx_msg->size == 0U) { + //buffer为空,且size为0,表示为空消息无需接收数据,直接将消息从邮箱中取出丢弃 + mbox_message_dispose(rx_msg); + } else { + //buffer为NULL且不是空消息,表示应用后续会用k_mbox_data_get取数据 + } + + return 0; + } + + void k_mbox_data_get(struct k_mbox_msg *rx_msg, void *buffer) + { + //buffer为空,不需要消息数据,直接丢弃该消息 + if (buffer == NULL) { + rx_msg->size = 0; + mbox_message_dispose(rx_msg); + return; + } + + //不是空消息,将消息数据copy到buffer中 + if ((rx_msg->tx_data != NULL) && (rx_msg->size > 0U)) { + (void)memcpy(buffer, rx_msg->tx_data, rx_msg->size); + } + + //空消息,无需接收数据,直接将消息从邮箱中取出丢弃 + mbox_message_dispose(rx_msg); + } + +mbox_message_dispose再来看取出消息丢弃的流程 + +:: + + static void mbox_message_dispose(struct k_mbox_msg *rx_msg) + { + struct k_thread *sending_thread; + struct k_mbox_msg *tx_msg; + + // 消息被接收被通知的thread会变为NULL,因此不用再丢弃,这季节返回 + if (rx_msg->_syncing_thread == NULL) { + return; + } + + if (rx_msg->tx_block.data != NULL) { + rx_msg->tx_block.data = NULL; + } + + //通过将被通知thread设置为NULL,表示该消息已经被接收,并从邮箱丢弃 + sending_thread = rx_msg->_syncing_thread; + rx_msg->_syncing_thread = NULL; + + + //更新实际接收数据的大小 + tx_msg = (struct k_mbox_msg *)sending_thread->base.swap_data; + tx_msg->size = rx_msg->size; + + #if (CONFIG_NUM_MBOX_ASYNC_MSGS > 0) + //异步发送,消息接收完毕,邮箱中已删除该消息,使用信号量进行通知 + if ((sending_thread->base.thread_state & _THREAD_DUMMY) != 0U) { + struct k_sem *async_sem = tx_msg->_async_sem; + + mbox_async_free((struct k_mbox_async *)sending_thread); + if (async_sem != NULL) { + k_sem_give(async_sem); + } + return; + } + #endif + + //同步发送,消息接收完毕,邮箱中已删除该消息,通知发送者退出等待状态,发送线程变为ready,并重新调度 + arch_thread_return_value_set(sending_thread, 0); + z_mark_thread_as_not_pending(sending_thread); + z_ready_thread(sending_thread); + z_reschedule_unlocked(); + } + +异步支持 +-------- + +邮箱支持异步,在发送者发送消息后可以立即退出不用等到被接收后才退出。通过下面手段实现异步支持: +1. 初始化时创建指定数量的异步消息,里面包含一个发送消息描述符和一个用于等待消息被接收的dummy +thread + +2. 发送异步消息时,将发送描述符存放在异步消息内,并将等待的同步_syncing_thread设置为dummy +thread,并指定通知接收完成的信号量 + +3. 接收消息完成后通过信号量通知接收已经完成 + +异步消息支持的数量受\ ``CONFIG_NUM_MBOX_ASYNC_MSGS``\ 配置限制。 +这里我们主要分析异步消息初始化和发送的流程,其它都已经在前文分析的代码中进行了注释 + +异步消息 +~~~~~~~~ + +异步消息的结构如下 + +:: + + struct k_mbox_async { + struct _thread_base thread; //dummy thread + struct k_mbox_msg tx_msg; //发送消息描述符 + }; + + +初始化 +~~~~~~ + +初始化建立异步消息的stack,并将声明好的异步消息放入stack + +:: + + static int init_mbox_module(const struct device *dev) + { + ARG_UNUSED(dev); + + //声明CONFIG_NUM_MBOX_ASYNC_MSGS个异步消息 + static struct k_mbox_async __noinit async_msg[CONFIG_NUM_MBOX_ASYNC_MSGS]; + + + int i; + + for (i = 0; i < CONFIG_NUM_MBOX_ASYNC_MSGS; i++) { + //将异步消息内的thread设置为dummy状态 + z_init_thread_base(&async_msg[i].thread, 0, _THREAD_DUMMY, 0); + //将异步消息压入堆栈内 + k_stack_push(&async_msg_free, (stack_data_t)&async_msg[i]); + } + + return 0; + } + + //在Zephyr初始化的PRE_KERNEL_1阶段会调用init_mbox_module + SYS_INIT(init_mbox_module, PRE_KERNEL_1, CONFIG_KERNEL_INIT_PRIORITY_OBJECTS); + +异步发送 +~~~~~~~~ + +异步发送和同步发送的主要流程一致,差别就是使用异步消息和dummy +thread进行等待,的代码如下: + +:: + + void k_mbox_async_put(struct k_mbox *mbox, struct k_mbox_msg *tx_msg, + struct k_sem *sem) + { + struct k_mbox_async *async; + + //分配异步消息 + mbox_async_alloc(&async); + + //初始化dummy thread的优先级 + async->thread.prio = _current->base.prio; + + //将发送消息存放在异步消息内 + async->tx_msg = *tx_msg; + //将dummy_thread做为被通知thread进行等待,当前thread调用k_mbox_async_put可立即返回 + async->tx_msg._syncing_thread = (struct k_thread *)&async->thread; + //将通知信号量保存在异步信息中 + async->tx_msg._async_sem = sem; + + //发送消息 + (void)mbox_message_put(mbox, &async->tx_msg, K_FOREVER); + } + +上面出现了对异步消息的堆栈的处理函数,其实就是对初始化时异步消息堆栈的处理,发送时pop出一个空的异步消息,接收完后将异步消息又加入到堆栈中 + +:: + + static inline void mbox_async_alloc(struct k_mbox_async **async) + { + (void)k_stack_pop(&async_msg_free, (stack_data_t *)async, K_FOREVER); + } + + static inline void mbox_async_free(struct k_mbox_async *async) + { + k_stack_push(&async_msg_free, (stack_data_t)async); + } + +从前面的分析我们知道初始化时空闲的异步消息只有\ ``CONFIG_NUM_MBOX_ASYNC_MSGS``\ 个。当空闲的异步消息用完后stack就为空了,此时再发送异步消息就会因为stack pop的特性发生阻塞,\ ``k_mbox_async_put``\ 被挂起,直到有异步消息被接收后push回stack。 + +参考 +==== + +https://docs.zephyrproject.org/latest/reference/kernel/data_passing/mailboxes.html diff --git a/doc/source/develop/kernel/msgq.rst b/doc/source/develop/kernel/msgq.rst new file mode 100644 index 0000000000000000000000000000000000000000..7ff53d20dde840620e4dad9bab23852a46f104fb --- /dev/null +++ b/doc/source/develop/kernel/msgq.rst @@ -0,0 +1,483 @@ +.. _kernel_msgq: + +数据传递-消息队列 +#################### + +使用 +==== + +API +--- + +Message queue的API有下面10个全部声明在kernel.h中,每个函数都有参数\ ``struct k_msgq *msgq``\.都是指该函数操作或者使用的msgq后面就不在单独列出说明 + +**void k_msgq_init(struct k_msgq q, char buffer, size_t msg_size, u32_tmax_msgs);** + +作用:初始化一个msgq, 内存由使用者分配 + +buffer: msgq的buffer,需要由使用者分配,大小为msg_size*max_msgs + +msg_size: msgq中每个message的大小 max_mags: msgq中最多容纳的message数量 + +**__syscall int k_msgq_alloc_init(struct k_msgq *msgq, size_t msg_size,u32_t max_msgs);** + +作用:初始化一个msgq, 内存由msgq从线程池中分配 + +msg_size: msgq中每个message的大小 + +max_mags: msgq中最多容纳的message数量 + +**int k_msgq_cleanup(struct k_msgq* msgq);** + +作用:释放k_msgq_alloc_init分配msgq内存 + +**__syscall int k_msgq_put(struct k_msgq *msgq, void*\ data, s32_t timeout);** + +作用:将message放入到msgq + +data:message数据 + +timeout:等待时间,单位ms。K_NO_WAIT不等待, K_FOREVER一直等 + +返回值:放入成功返回0 + +**__syscall int k_msgq_get(struct k_msgq *msgq, void* data, s32_t timeout);** + +作用:从msgq读出message + +data:message数据 + +timeout: 等待时间,单位ms。K_NO_WAIT不等待,K_FOREVER一直等 返回值:读出成功返回0 + +**__syscall int k_msgq_peek(struct k_msgq *msgq, void* data);** + +作用:peek msgq + +data: peek到的message + +返回: peek到数据返回0 + +**__syscall void k_msgq_purge(struct k_msgq *msgq);** + +作用:清空msgq中的message + +**__syscall u32_t k_msgq_num_free_get(struct k_msgq *msgq);** + +作用:获取msgq还可以放多少个message + +返回值:空闲数目 + +**__syscall void k_msgq_get_attrs(struct k_msgq *msgq, struct k_msgq_attrs* attrs);** + +作用:获取msgq的信息,也就是message的大小,总数量和已使用数量,都放在struct k_msgq_attrs 内 + +**__syscall u32_t k_msgq_num_used_get(struct k_msgq *msgq);** + +作用:获取msgq中有多少个message +返回值:message数目 + +使用说明 +-------- + +可以在ISR中put msgq.也可在ISR内get msgq,但不能等待。 +msgq必须事先指定message的大小和个数。大小需要是2的幂对齐。 +msgq用于异步传输小数据。msgq在读写时需要锁中断,因此不建议用来传输大数据。 + +初始化 +~~~~~~ + +初始化一个queue, 由用户分配内存 + +:: + + struct data_item_type { //message的数据结构 + u32_t field1; + u32_t field2; + u32_t field3; + }; + + char __aligned(4) my_msgq_buffer[10 * sizeof(data_item_type)]; + struct k_msgq my_msgq; + + k_msgq_init(&my_msgq, my_msgq_buffer, sizeof(data_item_type), 10); + +由message queue自己在线程池中分配 + +:: + + k_msgq_alloc_init(&my_msgq, sizeof(data_item_type), 10); + +写入message +~~~~~~~~~~~ + +运行在线程或者ISR中写入message,ISR中写入时不能发生等待。示例如下 + +:: + + void producer_thread(void) + { + struct data_item_t data; + + while (1) { + /* create data item to send (e.g. measurement, timestamp, ...) */ + data = ... + + /* send data to consumers */ + while (k_msgq_put(&my_msgq, &data, K_NO_WAIT) != 0) { + /* message queue is full: purge old data & try again */ + //这里purge并不是必要步骤,是否需要进行purge根据实际应用由用户自己选择 + k_msgq_purge(&my_msgq); + } + + /* data item was successfully added to message queue */ + } + } + +读message +~~~~~~~~~ + +可以将message从msgq读出, 之后msgq中不再有该message + +:: + + void consumer_thread(void) + { + struct data_item_t data; + + while (1) { + /* get a data item */ + k_msgq_get(&my_msgq, &data, K_FOREVER); + + /* process data item */ + ... + } + } + +也可以只是peek,该message任然保留在msgq中 + +:: + + void consumer_thread(void) + { + struct data_item_t data; + + while (1) { + /* read a data item by peeking into the queue */ + if(0 == k_msgq_peek(&my_msgq, &data)){ + /* process data item */ + } + ... + } + } + +实现 +==== + +msgq的实现代码在zephyr/kernel/msg_q.c中,msgq是以ringbuffer的模式进行管理,在初始化的时候建立ringbuffer,读写数据时都是以固定的单位大小从ringbuffer内读写数据。 +msgq的数据结构如下 + +:: + + struct k_msgq { + _wait_q_t wait_q; //wait_q用于控制msgq的等待 + struct k_spinlock lock; //msgq多线程保护锁 + size_t msg_size; // message的大小 + u32_t max_msgs; // msgq最大容纳message的个数 + char *buffer_start; //msgq ringbuffer的开始地址 + char *buffer_end; //msgq ringbuffer的结束地址 + char *read_ptr; //msgq ringbuffer的读指针 + char *write_ptr; //msgq ringbuffer的写指针 + u32_t used_msgs; //msgq中有效message的个数 + u8_t flags; //msgq的ringbuffer从线程池分配标志 + }; + +示意图 如下,每读或者写一个message,读写指针就向前移动msg_size |msgq| + +初始化/释放 +----------- + +初始化msgq就是对struct_msgq中的各成员进行初始化 + +:: + + void k_msgq_init(struct k_msgq *msgq, char *buffer, size_t msg_size, + u32_t max_msgs) + { + //初始化各成员 + msgq->msg_size = msg_size; + msgq->max_msgs = max_msgs; + msgq->buffer_start = buffer; + msgq->buffer_end = buffer + (max_msgs * msg_size); + msgq->read_ptr = buffer; + msgq->write_ptr = buffer; + msgq->used_msgs = 0; + msgq->flags = 0; // msgq ringbuffer是由使用者分配,这里设为0 + z_waitq_init(&msgq->wait_q); //初始化msgq的wait_q + msgq->lock = (struct k_spinlock) {}; + + z_object_init(msgq); + } + +k_msgq_alloc_init->z_impl_k_msgq_alloc_init, +msgq内存从线程池中分配,再使用k_msgq_init初始化 + +:: + + int z_impl_k_msgq_alloc_init(struct k_msgq *msgq, size_t msg_size, + u32_t max_msgs) + { + void *buffer; + int ret; + size_t total_size; + + //计算msg_size乘max_msgs,并检查是否溢出,实际调用的是__builtin_mul_overflow + if (size_mul_overflow(msg_size, max_msgs, &total_size)) { + ret = -EINVAL; + } else { + //从线程池中分配msgq的ringbuffer 内存 + buffer = z_thread_malloc(total_size); + if (buffer != NULL) { + //初始化各变量 + k_msgq_init(msgq, buffer, msg_size, max_msgs); + //使用K_MSGQ_FLAG_ALLOC在flags中标识ringbuffer是从线程池中分配 + msgq->flags = K_MSGQ_FLAG_ALLOC; + ret = 0; + } else { + ret = -ENOMEM; + } + } + + return ret; + } + +如果msgq是从线程池中分配的内存,可以使用k_msgq_cleanup将其释放 + +:: + + int k_msgq_cleanup(struct k_msgq *msgq) + { + //如果还有thread在等待msgq,说明不能释放,退出 + CHECKIF(z_waitq_head(&msgq->wait_q) != NULL) { + return -EBUSY; + } + + //判断alloc标志,并释放内存 + if ((msgq->flags & K_MSGQ_FLAG_ALLOC) != 0) { + k_free(msgq->buffer_start); + msgq->flags &= ~K_MSGQ_FLAG_ALLOC; + } + return 0; + } + +mssage操作 +---------- + +写msgq +~~~~~~ + +k_msgq_put->z_impl_k_msgq_put + +:: + + int z_impl_k_msgq_put(struct k_msgq *msgq, void *data, s32_t timeout) + { + //isr内写msgq不能等 + __ASSERT(!arch_is_in_isr() || timeout == K_NO_WAIT, ""); + + struct k_thread *pending_thread; + k_spinlock_key_t key; + int result; + + key = k_spin_lock(&msgq->lock); + + + if (msgq->used_msgs < msgq->max_msgs) { + //msgq中ringbuffer有空间 + + //检查是否有thread在等待读取msgq的message + pending_thread = z_unpend_first_thread(&msgq->wait_q); + if (pending_thread != NULL) { + //有线程在等message,直接将该数据提供给等待线程 + (void)memcpy(pending_thread->base.swap_data, data, + msgq->msg_size); + //等待线程拿到数据后,让等待线程ready,并重新调度 + arch_thread_return_value_set(pending_thread, 0); + z_ready_thread(pending_thread); + z_reschedule(&msgq->lock, key); + return 0; + } else { + //没有线程需要数据,则将数据放入ringbuffer + (void)memcpy(msgq->write_ptr, data, msgq->msg_size); + msgq->write_ptr += msgq->msg_size; + if (msgq->write_ptr == msgq->buffer_end) { + msgq->write_ptr = msgq->buffer_start; + } + //更新msgq剩余的message数 + msgq->used_msgs++; + } + result = 0; + } else if (timeout == K_NO_WAIT) { + //msgq ringbuffer满,且不等待就立即退出 + result = -ENOMSG; + } else { + //msgq 满,将message放入swap_data,等待其它thread来读 + _current->base.swap_data = data; + return z_pend_curr(&msgq->lock, key, &msgq->wait_q, timeout); + } + + k_spin_unlock(&msgq->lock, key); + + return result; + } + +读msgq +~~~~~~ + +k_msgq_get->z_impl_k_msgq_get + +:: + + int z_impl_k_msgq_get(struct k_msgq *msgq, void *data, s32_t timeout) + { + //isr内读msgq不能等 + __ASSERT(!arch_is_in_isr() || timeout == K_NO_WAIT, ""); + + k_spinlock_key_t key; + struct k_thread *pending_thread; + int result; + + key = k_spin_lock(&msgq->lock); + + if (msgq->used_msgs > 0) { + //ringbuffer中有数据,直接从ringbuffer中读出message + (void)memcpy(data, msgq->read_ptr, msgq->msg_size); + msgq->read_ptr += msgq->msg_size; + if (msgq->read_ptr == msgq->buffer_end) { + msgq->read_ptr = msgq->buffer_start; + } + + //更新msgq剩余的message数 + msgq->used_msgs--; + + //此时ringbuffer有空闲空间,如果有thead在等待写msgq,则在这里写入 + pending_thread = z_unpend_first_thread(&msgq->wait_q); + if (pending_thread != NULL) { + /* add thread's message to queue */ + (void)memcpy(msgq->write_ptr, pending_thread->base.swap_data, + msgq->msg_size); + msgq->write_ptr += msgq->msg_size; + if (msgq->write_ptr == msgq->buffer_end) { + msgq->write_ptr = msgq->buffer_start; + } + //更新msgq剩余的message数 + msgq->used_msgs++; + + //等待写入msgq的thread在写入msgq后变为ready,并重新调度 + arch_thread_return_value_set(pending_thread, 0); + z_ready_thread(pending_thread); + z_reschedule(&msgq->lock, key); + return 0; + } + result = 0; + } else if (timeout == K_NO_WAIT) { + /msgq ringbuffer空,且不等待就立即退出 + result = -ENOMSG; + } else { + //msgq 空,将message放入swap_data,等待其它thread来写 + _current->base.swap_data = data; + return z_pend_curr(&msgq->lock, key, &msgq->wait_q, timeout); + } + + k_spin_unlock(&msgq->lock, key); + + return result; + } + +peek msgq +~~~~~~~~~ + +也可以通过peek读message,该方式不会将message从msgq的ringbuffer中删除 +k_msgq_peek->z_impl_k_msgq_peek + +:: + + int z_impl_k_msgq_peek(struct k_msgq *msgq, void *data) + { + k_spinlock_key_t key; + int result; + + key = k_spin_lock(&msgq->lock); + + if (msgq->used_msgs > 0) { + //ringbuffer中有数据直接copy出去 + (void)memcpy(data, msgq->read_ptr, msgq->msg_size); + result = 0; + } else { + //ringbuffer中有无数据返回错误 + result = -ENOMSG; + } + + k_spin_unlock(&msgq->lock, key); + + return result; + } + +清空msgq +~~~~~~~~ + +当不需要msgq中的数据时可以使用k_msgq_purge清空 +k_msgq_purge->z_impl_k_msgq_purge + +:: + + void z_impl_k_msgq_purge(struct k_msgq *msgq) + { + k_spinlock_key_t key; + struct k_thread *pending_thread; + + key = k_spin_lock(&msgq->lock); + + //清空ringbuffer前,会先让等待读msgq的thead将message读走 + while ((pending_thread = z_unpend_first_thread(&msgq->wait_q)) != NULL) { + arch_thread_return_value_set(pending_thread, -ENOMSG); + z_ready_thread(pending_thread); + } + + //复位 ringbuffer + msgq->used_msgs = 0; + msgq->read_ptr = msgq->write_ptr; + + z_reschedule(&msgq->lock, key); + } + +获取msgq信息 +------------ + +获取msgq的信息函数实现很简单结合前面k_msgq的结构体很容易理解,这里就不再注释分析了 + +:: + + void z_impl_k_msgq_get_attrs(struct k_msgq *msgq, struct k_msgq_attrs *attrs) + { + attrs->msg_size = msgq->msg_size; + attrs->max_msgs = msgq->max_msgs; + attrs->used_msgs = msgq->used_msgs; + } + + static inline u32_t z_impl_k_msgq_num_free_get(struct k_msgq *msgq) + { + return msgq->max_msgs - msgq->used_msgs; + } + + static inline u32_t z_impl_k_msgq_num_used_get(struct k_msgq *msgq) + { + return msgq->used_msgs; + } + +参考 +==== + +https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html +https://docs.zephyrproject.org/latest/reference/kernel/data_passing/message_queues.html + +.. |msgq| image:: ../../images/develop/kernel/msgq.png diff --git a/doc/source/develop/kernel/mutex.rst b/doc/source/develop/kernel/mutex.rst new file mode 100644 index 0000000000000000000000000000000000000000..e5528d6a745db4330183787000a6b459bcc9edb9 --- /dev/null +++ b/doc/source/develop/kernel/mutex.rst @@ -0,0 +1,306 @@ +.. _kernel_mutex: + + +同步-互斥量 +########### + +使用 +==== + +API +--- + +**#define K_MUTEX_DEFINE(name)** + +* 作用:定义一个k_mutex,并初始化 +* name:k_mutex name + +**int k_mutex_init(struct k_mutex *mutex)** + +* 作用: 初始化mutex +* mutex: 要初始化的mutex +* 返回值: 0标示初始化成功 + +**int k_mutex_lock(struct k_mutex *mutex, s32_t timeout)** + +* 作用:加锁 +* mutex: 加锁的互斥量 +* timeout: 等待时间,单位ms。K_NO_WAIT不等待,K_FOREVER一直等 +* 返回值: 0表示加锁成功 + +**int k_mutex_unlock(struct k_mutex *mutex)** + +* 作用:解锁 +* mutex:解锁的互斥量 +* 返回值: 0表示解锁成功,-EPERM表示当前thread并不拥有这个互斥量,-EINVAL表示该互斥量没有被锁 + +使用说明 +-------- + +使用互斥量完成对资源的独占访问。 Mutex不能在ISR内使用。 + +初始化 +~~~~~~ + +先初始化一个互斥量,下面两种方式的效果是一样的 + +方法1,使用宏 + +:: + + K_MUTEX_DEFINE(my_mutex); + +方法2,使用函数 + +:: + + struct k_mutex my_mutex; + k_mutex_init(&my_mutex); + +资源独占访问 +~~~~~~~~~~~~ + +下列示例代码中Thread +A和B都要去访问一个IO资源,但同一时间IO只能被独占访问,因此使用互斥量包含 + +:: + + thread_A() + { + k_mutex_lock(&my_mutex, K_FOREVER); + + //Read IO + ... + + k_mutex_unlock(&my_mutex); + } + + thread_b() + { + k_mutex_lock(&my_mutex, K_FOREVER); + + //Write IO + ... + + k_mutex_unlock(&my_mutex); + } + +实现 +==== + +k_mutex结构体如下,可以看出其基本实现是用的wait_q + +:: + + struct k_mutex { + /** Mutex wait queue */ + _wait_q_t wait_q; + /** Mutex owner */ + struct k_thread *owner; //表示该mutex目前属于哪个线程 + + /** Current lock count */ + u32_t lock_count; //可重入锁用 + + /** Original thread priority */ + int owner_orig_prio; //优先级倒置用 + }; + + +可重入锁是指如果一个线程已经拥有了互斥量,那么该线程可以继续多次对该互斥量加锁,同时也要做对应次数的解锁,才能完全释放该互斥量 + + +初始化 +------ + +k_mutex_init->z_impl_k_mutex_init,详细分析见注释 + +:: + + int z_impl_k_mutex_init(struct k_mutex *mutex) + { + mutex->owner = NULL; //全新的mutex是无owner的 + mutex->lock_count = 0U; //次数也未加锁 + + z_waitq_init(&mutex->wait_q); + + z_object_init(mutex); + + return 0; + } + +用宏也可以进行初始化 + +:: + + #define _K_MUTEX_INITIALIZER(obj) \ + { \ + .wait_q = Z_WAIT_Q_INIT(&obj.wait_q), \ //等同于z_waitq_init + .owner = NULL, \ + .lock_count = 0, \ + .owner_orig_prio = K_LOWEST_THREAD_PRIO, \ + _OBJECT_TRACING_INIT \ + } + + #define K_MUTEX_DEFINE(name) \ + Z_STRUCT_SECTION_ITERABLE(k_mutex, name) = \ + _K_MUTEX_INITIALIZER(name) + +加锁 +---- + +k_mutex_lock -> z_impl_k_mutex_unlock,会做下面几件事 + +1. 如果互斥量没其它线程用,直接获得互斥量返回 + +2. 如果互斥量是本线程在用,对可重入锁自加 + +3. 如果互斥锁被其它线程用了,进行优先级倒置调整,等待其它线程解锁互斥量 + +3. 如果超时内等到其它线程解锁互斥量,回去互斥量然后返回 + +4. 如果等互斥量超时,则放弃等待,检查是否有其它线程还在等待,已等待线程的优先级重新计算要倒置的优先级,重设拥有互斥量线程的优先级 + +:: + + int z_impl_k_mutex_lock(struct k_mutex *mutex, s32_t timeout) + { + int new_prio; + k_spinlock_key_t key; + bool resched = false; + + key = k_spin_lock(&lock); + + + //当前互斥量没被锁(lock_count ==0) 或是 当前thread已经拥有该锁(mutex->owner == _current) + if (likely((mutex->lock_count == 0U) || (mutex->owner == _current))) { + + //记录thread当前的优先级,用于之后优先级倒置用 + mutex->owner_orig_prio = (mutex->lock_count == 0U) ? + _current->base.prio : + mutex->owner_orig_prio; + + mutex->lock_count++; //对于未使用的锁这里lock_count会变成1,对于重入锁,这里lock_count会在原来的基础上增加然后返回 + mutex->owner = _current; //更新owner + + k_spin_unlock(&lock, key); + + return 0; + } + + //互斥量被其它thread占用,如果不等就立即返回 + if (unlikely(timeout == (s32_t)K_NO_WAIT)) { + k_spin_unlock(&lock, key); + return -EBUSY; + } + + //如果要等,就进行判断,看自己线程的优先级和拥有互斥量的线程优先级谁高,计算一个新的优先级 + new_prio = new_prio_for_inheritance(_current->base.prio, + mutex->owner->base.prio); + + //如果互斥量拥有者线程的优先级比较低,则重设优先级,让优先级倒置 + if (z_is_prio_higher(new_prio, mutex->owner->base.prio)) { + resched = adjust_owner_prio(mutex, new_prio); + } + + //等待mutex释放,会引发调度 + int got_mutex = z_pend_curr(&lock, key, &mutex->wait_q, timeout); + + //等到mutex,返回 + if (got_mutex == 0) { + return 0; + } + + //等mutex超时 + key = k_spin_lock(&lock); + + //检查释放有其它线程在等待 + struct k_thread *waiter = z_waitq_head(&mutex->wait_q); + + //如果有其它线程在等待,比较Mutex拥有者线程和其它线程的优先级 + new_prio = (waiter != NULL) ? + new_prio_for_inheritance(waiter->base.prio, mutex->owner_orig_prio) : + mutex->owner_orig_prio; + + //重设拥有互斥量线程的优先级,并引发调度 + resched = adjust_owner_prio(mutex, new_prio) || resched; + + if (resched) { + z_reschedule(&lock, key); + } else { + k_spin_unlock(&lock, key); + } + + return -EAGAIN; + } + +解锁 +---- + +k_mutex_unlock->z_impl_k_mutex_unlock,做下面几件事 + +1. 检查解锁者合法性 +2. 接触重入锁 +3. 恢复优先级倒置 +4. 等待锁的线程获取mutex + +:: + + int z_impl_k_mutex_unlock(struct k_mutex *mutex) + { + struct k_thread *new_owner; + + //互斥量检查,不能解锁无owner的mutex + CHECKIF(mutex->owner == NULL) { + return -EINVAL; + } + + //互斥量检查,不能解锁其它thread拥有的mutex + CHECKIF(mutex->owner != _current) { + return -EPERM; + } + + //不允许解锁一个已经被完全 + __ASSERT_NO_MSG(mutex->lock_count > 0U); + + z_sched_lock(); + + + //可重入锁检查,如果没有全部解锁,直接退出 + if (mutex->lock_count - 1U != 0U) { + mutex->lock_count--; + goto k_mutex_unlock_return; + } + + + k_spinlock_key_t key = k_spin_lock(&lock); + + //mutex可重入锁已全部解完,对优先级倒置进行恢复 + adjust_owner_prio(mutex, mutex->owner_orig_prio); + + //检查释放有线程在等mutex + new_owner = z_unpend_first_thread(&mutex->wait_q); + + mutex->owner = new_owner; + + if (new_owner != NULL) { + //如果有线程在等mutex,该线程获取mutex并开始调度 + mutex->owner_orig_prio = new_owner->base.prio; + arch_thread_return_value_set(new_owner, 0); + z_ready_thread(new_owner); + z_reschedule(&lock, key); + } else { + //如果没有线程等mutex,mutex空闲 + mutex->lock_count = 0U; + k_spin_unlock(&lock, key); + } + + + k_mutex_unlock_return: + k_sched_unlock(); + return 0; + } + +参考 +==== + +https://docs.zephyrproject.org/latest/reference/kernel/synchronization/mutexes.html diff --git a/doc/source/develop/kernel/pipe.rst b/doc/source/develop/kernel/pipe.rst new file mode 100644 index 0000000000000000000000000000000000000000..35f4badbd580f20441367ef53898675509cc827b --- /dev/null +++ b/doc/source/develop/kernel/pipe.rst @@ -0,0 +1,704 @@ +.. _kernel_pipe: + + +数据同步-管道 +############### + +原理简介 +======== + +Zephyr的Pipe用于线程向线程发送字节流,不能用于ISR。一个pipe只能做到单工或是半双工。Zephyr在设计上虽然支持对同一pipe进行同时多线程发和多线程收,但一般情况下都只建议做一发一收。 + +Ring buffer工作原理 +==================== +Zephyr允许定义任意数量的管道,最大数量只由可用内存的量决定。管道的尺寸由管道的ring-buffer大小决定,管道内也可以不带ring-buffer。 +线程发送数据到管道时,如果数据没有被其它线程接收完,剩余的数据会被放入到管道内的ring-buffer。线程接收数据时会先从ring +buffer内读取,再从pipe内等待发送的线程接收数据。 +如下图示,第一次发送数据1\ :sub:`6,接收线程只收1`\ 4,剩余5,6会被放入ring +buffer。下一次接收时会先从ring buffer中读出 +5,6.再接收发送线程发送的7,8。 |image0| +`` + +收发行为 +-------- + +发送时会指定发送到pipe数据的尺寸,和允许最小传送尺寸。如果实际传送尺寸小于最小传送尺寸,且不等待传送,将立即返回失败。 +接收时会指定从pipe接收数据的尺寸,和允许最小接收尺寸。如果实际接收的尺寸小于最小接收尺寸,且不等待接收,将立即返回失败。 +从pipe收发数据,只要实际收发数据的尺寸大于指定最小收发尺寸,本次收发都是成功的。 +接收和发送都允许等待,收发数据没有达到指定的尺寸前会保持阻塞,如果超时后实际收发数据的尺寸小于指定的最小尺寸,将返回失败。 +如果实际接收和发送数据的尺寸已经满足指定的最小尺寸,将记录退出,不会保持阻塞等待收发所有指定数据。 + +使用 +==== + +API +--- + +``#define K_PIPE_DEFINE(name, pipe_buffer_size, pipe_align)`` + +作用:定义一个k_pipe,为其分配ring buffer + +name: k_pipe name + +pipe_buffer_size: pipe内ring buffer的大小 + +pipe_align: 定义静态数组座位ring buffer,该参数指定该数组的对齐大小,只能是2的幂 + +``void k_pipe_init(struct k_pipe *pipe, unsigned char *buffer, size_t size)`` + +作用:初始化k_pipe, 并为其指定ring buffer + +pipe: 要初始化的pipe + +buffer: ringbuffer地址 + +size: ring buffer大小 + +``int k_pipe_alloc_init(struct k_pipe *pipe, size_t size)`` + +作用:初始化k_pipe, 并为其分配ring buffer + +pipe: 要初始化的pipe + +size: 分配ring buffer大小 + +返回值:0表示成功,-ENOMEM表示memory不足 + +``int k_pipe_cleanup(struct k_pipe *pipe)`` +作用:释放k_pipe_alloc_init分配的ring buffer pipe: 要释放buffer的pipe +返回值:0表示成功,-EAGAIN表示目前正在使用无法释放 + +``int k_pipe_put(struct k_pipe *pipe, void *data, size_t bytes_to_write, size_t *bytes_written, size_t min_xfer, k_timeout_t timeout)`` + +作用:发送数据到pipe + +pipe: 管道 + +data: 发送数据的地址 + +bytes_to_write:发送数据的尺寸 + +bytes_written: 实际发送的尺寸 + +min_xfer: 最小发送尺寸 + +timeout:等待时间,单位ms。K_NO_WAIT不等待, K_FOREVER一直等 + +返回值:0表示成功,-EINVAL表示参数错误,-EIO表示未等待且未传送任何数据,-EAGAIN表示传送的数据单数据量小于min_xfer + +``int k_pipe_get(struct k_pipe *pipe, void *data, size_t bytes_to_read, size_t *bytes_read, size_t min_xfer, k_timeout_t timeout)`` + +作用:从pipe接收数据 + +pipe: 管道 + +data: 发送数据的地址 + +bytes_to_read: 接收数据的尺寸 bytes_read 实际接收的尺寸 min_xfer 最小接收尺寸 + +timeout:等待时间,单位ms。K_NO_WAIT不等待, K_FOREVER一直等 + +返回值:0表示成功,-EINVAL表示参数错误,-EIO表示未等待且未收到任何数据,-EAGAIN表示接收的数据单数据量小于min_xfer + +``size_t k_pipe_read_avail(struct k_pipe *pipe)`` + +作用:获取pipe内有多少有效数据,也就是ring buffer的有效数据大小 + +pipe: 管道 返回值:可读数据大小 + +``size_t k_pipe_write_avail(struct k_pipe *pipe)`` + +作用:获取可以向管道写多少数据,也就是ring buffer内的空闲空间大小 + +pipe: 管道 返回值:可写数据大小 + +使用说明 +-------- + +初始化 +~~~~~~ + +先定义初始化一个pipe,一共有三种方式 +使用宏,宏会定义一个数组作为pipe的ring buffer + +:: + + K_PIPE_DEFINE(my_pipe, 100, 4); + +使用函数初始化,需要自己定义一个数组作为ring buffer + +:: + + unsigned char __aligned(4) my_ring_buffer[100]; + struct k_pipe my_pipe; + + k_pipe_init(&my_pipe, my_ring_buffer, sizeof(my_ring_buffer)); + +使用函数初始化,由pipe自己分配内存作为ring buffer + +:: + + struct k_pipe my_pipe; + k_pipe_alloc_init(&my_pipe, 100) + +用k_pipe_alloc_init初始化的pipe,不再使用时需要用k_pipe_cleanup释放其分配的ring buffer + +:: + + k_pipe_cleanup(&my_pipe) + +写pipe +~~~~~~ + +发送数据需要分三种情况处理,如下分析 + +:: + + struct message_header { + ... + }; + + void producer_thread(void) + { + unsigned char *data; + size_t total_size; + size_t bytes_written; + int rc; + ... + + while (1) { + /* 准备要发送到pipe的数据,尺寸大于struct message_header */ + data = ...; + total_size = ...; + + /* 通过pipe发送数据 */ + rc = k_pipe_put(&my_pipe, data, total_size, &bytes_written, + sizeof(struct message_header), K_NO_WAIT); + + if (rc < 0) { + //数据头没有发送完,发送失败处理 + ... + } else if (bytes_written < total_size) { + //数据头发送完,但数据没全发送,进行处理(例如继续发送) + ... + } else { + //所有数据都发送完毕 + ... + } + } + } + +读pipe +~~~~~~ + +接收数据也分三种情况处理,如下分析 + +:: + + void consumer_thread(void) + { + unsigned char buffer[120]; + size_t bytes_read; + struct message_header *header = (struct message_header *)buffer; + + while (1) { + rc = k_pipe_get(&my_pipe, buffer, sizeof(buffer), &bytes_read, + sizeof(header), K_MSEC(100)); + + if ((rc < 0) || (bytes_read < sizeof (header))) { + //数据头未收全,接收失败处理 + ... + } else if (header->num_data_bytes + sizeof(header) > bytes_read) { + //数据头收完,但数据没收全,进行处理(例如跳到下一次接收) + ... + } else { + //所有数据接收完毕 + ... + } + } + } + +实现 +==== + +该小节通过对管道内核代码的分析,理解Zephyr是如何实现以上描述的功能特性. +pipe的实现代码在kernel:raw-latex:`\pipes`.c中 + +pipe结构体 +---------- + +一个管道主要是由ring buffer和两个wait_q组成,ring buffer最为pipe +buffer, 两个wait_q用于管理发送和接收线程,如下定义: + +:: + + struct k_pipe { + + //下面5个字段是ring buffer管理用,使用的是常规管理方法,本文不做分析 + unsigned char *buffer; + size_t size; + size_t bytes_used; + size_t read_index; + size_t write_index; + + //同步锁 + struct k_spinlock lock; + + //两个wait_q,readers用于管理读pipe的线程,writers用于管理写pipe的线程 + struct { + _wait_q_t readers; /**< Reader wait queue */ + _wait_q_t writers; /**< Writer wait queue */ + } wait_q; /** Wait queue */ + + //pipe flag,如果pipe的ring buffer是pipe分配的,该flag之为K_PIPE_FLAG_ALLOC + uint8_t flags; /**< Flags */ + }; + + +初始化 +------ + +k_pipe_init和K_PIPE_DEFINE主要就是初始化ring buffer和两个wait_q,流程简单,查看代码即可。 +这里看一下k_pipe_alloc_init和k_pipe_cleanup的流程 +k_pipe_alloc_init->z_vrfy_k_pipe_alloc_init->z_impl_k_pipe_alloc_init + +:: + + int z_impl_k_pipe_alloc_init(struct k_pipe *pipe, size_t size) + { + void *buffer; + int ret; + + if (size != 0U) { + /从线程resource_pool内分配内存作为ring buffer + buffer = z_thread_malloc(size); + if (buffer != NULL) { + //初始化pipe + k_pipe_init(pipe, buffer, size); + //标记该pipe用的ring buffer是自己分配的内存 + pipe->flags = K_PIPE_FLAG_ALLOC; + ret = 0; + } else { + ret = -ENOMEM; + } + } else { + //不需要ring buffer,直接进行初始化 + k_pipe_init(pipe, NULL, 0); + ret = 0; + } + + return ret; + } + + + int k_pipe_cleanup(struct k_pipe *pipe) + { + SYS_PORT_TRACING_OBJ_FUNC_ENTER(k_pipe, cleanup, pipe); + + //pipe使用中,不允许释放ring buffer + CHECKIF(z_waitq_head(&pipe->wait_q.readers) != NULL || + z_waitq_head(&pipe->wait_q.writers) != NULL) { + return -EAGAIN; + } + + //检查ring buffer是从线程resource_pool分配,释放内存 + if ((pipe->flags & K_PIPE_FLAG_ALLOC) != 0U) { + k_free(pipe->buffer); + pipe->buffer = NULL; + pipe->flags &= ~K_PIPE_FLAG_ALLOC; + } + + return 0; + } + + +写pipe +------ + +写pipe的函数调用关系如下: +k_pipe_put->z_vrfy_k_pipe_put->z_impl_k_pipe_put->z_pipe_put_internal +z_pipe_put_internal内主要做下面几件事 + +* 根据写pipe的数据长度,从readers wait_q中将读pipe的线程放入读pipe链表 +* 将数据依次拷贝给读pipe链表中的线程 +* 如果写数据还有剩余,将剩余数据放入pipe buffer中 +* 发送数据达到最小尺寸,立即退出发送 +* 发送数据未达到最小尺寸,等待发送超时 + +:: + + int z_pipe_put_internal(struct k_pipe *pipe, struct k_pipe_async *async_desc, + unsigned char *data, size_t bytes_to_write, + size_t *bytes_written, size_t min_xfer, + k_timeout_t timeout) + { + struct k_thread *reader; + struct k_pipe_desc *desc; + sys_dlist_t xfer_list; + size_t num_bytes_written = 0; + size_t bytes_copied; + + + //参数判断 + CHECKIF((min_xfer > bytes_to_write) || bytes_written == NULL) { + return -EINVAL; + } + + k_spinlock_key_t key = k_spin_lock(&pipe->lock); + + //将等待读pipe的thread从readers wait_q中移除并加入到链表xfer_list中 + //最后一个读pipe的thread不加入到xfer_list中,而是直接放入到reader + if (!pipe_xfer_prepare(&xfer_list, &reader, &pipe->wait_q.readers, + pipe->size - pipe->bytes_used, bytes_to_write, + min_xfer, timeout)) { + k_spin_unlock(&pipe->lock, key); + + //读pipe的总量小于写pipe的min_xfer,且写pipe不等待,立即返回失败 + //该部分原理在pipe_xfer_prepare中分析 + *bytes_written = 0; + + return -EIO; + } + + + z_sched_lock(); + k_spin_unlock(&pipe->lock, key); + + //遍历读pipe thread 链表xfer_list + struct k_thread *thread = (struct k_thread *) + sys_dlist_get(&xfer_list); + while (thread != NULL) { + //用pipe_xfer依次将写pipe的数据拷贝到读pipe thread中 + desc = (struct k_pipe_desc *)thread->base.swap_data; + bytes_copied = pipe_xfer(desc->buffer, desc->bytes_to_xfer, + data + num_bytes_written, + bytes_to_write - num_bytes_written); + + num_bytes_written += bytes_copied; + desc->buffer += bytes_copied; + desc->bytes_to_xfer -= bytes_copied; + + //读pipe的thread已经拿到全部数据,被转为就绪 + z_ready_thread(thread); + + thread = (struct k_thread *)sys_dlist_get(&xfer_list); + } + + + //最后一个等待读pipe的线程从pipe中读取数据,因为可能数据可能读不够,因此不将其转为就绪态 + if (reader != NULL) { + desc = (struct k_pipe_desc *)reader->base.swap_data; + bytes_copied = pipe_xfer(desc->buffer, desc->bytes_to_xfer, + data + num_bytes_written, + bytes_to_write - num_bytes_written); + + num_bytes_written += bytes_copied; + desc->buffer += bytes_copied; + desc->bytes_to_xfer -= bytes_copied; + } + + //写pipe数据如果还有剩余,放入pipe ring buffer + num_bytes_written += + pipe_buffer_put(pipe, data + num_bytes_written, + bytes_to_write - num_bytes_written); + + //写pipe的数据如果已经消耗完,返回成功 + if (num_bytes_written == bytes_to_write) { + *bytes_written = num_bytes_written; + k_sched_unlock(); + + return 0; + } + + //如果写pipe已经达到最小传送尺寸min_xfer,立即返回成功不再等待 + if (!K_TIMEOUT_EQ(timeout, K_NO_WAIT) + && num_bytes_written >= min_xfer + && min_xfer > 0U) { + *bytes_written = num_bytes_written; + k_sched_unlock(); + + return 0; + } + + + struct k_pipe_desc pipe_desc; + + pipe_desc.buffer = data + num_bytes_written; + pipe_desc.bytes_to_xfer = bytes_to_write - num_bytes_written; + //如果写pipe数据未被读完且需要等待超时的,将线程加入到wirters wait_q,等待读pipe线程 + if (!K_TIMEOUT_EQ(timeout, K_NO_WAIT)) { + _current->base.swap_data = &pipe_desc; + + k_spinlock_key_t key2 = k_spin_lock(&pipe->lock); + z_sched_unlock_no_reschedule(); + (void)z_pend_curr(&pipe->lock, key2, + &pipe->wait_q.writers, timeout); + } else { + k_sched_unlock(); + } + + //计算实际写pipe数据,并和min_xfer比较,检查是否达到最小传送量 + *bytes_written = bytes_to_write - pipe_desc.bytes_to_xfer; + int ret = pipe_return_code(min_xfer, pipe_desc.bytes_to_xfer, + bytes_to_write); + + return ret; + } + + +读pipe +------ + +读pipe的函数调用关系如下: +k_pipe_get->z_vrfy_k_pipe_get->z_impl_k_pipe_get +z_impl_k_pipe_get的流程和写pipe是对称的,主要做下面几件事 + +* 根据读pipe的数据长度,从writers wait_q中将写pipe的线程放入 +* 将pipe内ring buffer的数据拷贝给读pipe线程 +* 写pipe链表中写pipe线程的数据依次拷贝给读pipe线程 +* 如果写pipe线程链表的数据还有剩余,将其写到pipe buffer内 +* 接收数据达到最小尺寸,立即退出接收 +* 接收数据未达到最小尺寸,等待接收超时 + +:: + + int z_impl_k_pipe_get(struct k_pipe *pipe, void *data, size_t bytes_to_read, + size_t *bytes_read, size_t min_xfer, k_timeout_t timeout) + { + struct k_thread *writer; + struct k_pipe_desc *desc; + sys_dlist_t xfer_list; + size_t num_bytes_read = 0; + size_t bytes_copied; + + //检查参数 + CHECKIF((min_xfer > bytes_to_read) || bytes_read == NULL) { + return -EINVAL; + } + + k_spinlock_key_t key = k_spin_lock(&pipe->lock); + + //将等待写pipe的thread从writers wait_q中移除并加入到链表xfer_list中 + //最后一个读pipe的thread不加入到xfer_list中,而是直接放入到writer + //这里计算长度没有考虑pipe buffer内的数据,因此链表内待接收数据和buffer数据的总和可能会大于读pipe需要的长度 + if (!pipe_xfer_prepare(&xfer_list, &writer, &pipe->wait_q.writers, + pipe->bytes_used, bytes_to_read, + min_xfer, timeout)) { + k_spin_unlock(&pipe->lock, key); + *bytes_read = 0; + + //写pipe的总量小于读pipe的min_xfer,且写pipe不等待,立即返回失败 + //该部分原理在pipe_xfer_prepare中分析 + + return -EIO; + } + + + z_sched_lock(); + k_spin_unlock(&pipe->lock, key); + + //先从pipe buffer中读数据 + num_bytes_read = pipe_buffer_get(pipe, data, bytes_to_read); + + //遍历读pipe thread 链表xfer_list + struct k_thread *thread = (struct k_thread *) + sys_dlist_get(&xfer_list); + while ((thread != NULL) && (num_bytes_read < bytes_to_read)) { + //用pipe_xfer依次将写pipe的thread的数据拷贝到读pipe thread中 + desc = (struct k_pipe_desc *)thread->base.swap_data; + bytes_copied = pipe_xfer((uint8_t *)data + num_bytes_read, + bytes_to_read - num_bytes_read, + desc->buffer, desc->bytes_to_xfer); + + num_bytes_read += bytes_copied; + desc->buffer += bytes_copied; + desc->bytes_to_xfer -= bytes_copied; + + //由于前面从pipe buffer中还接收了数据,因此链表未遍历完就可能将需要读的数据量读够 + if (num_bytes_read == bytes_to_read) { + break; + } + + //数据被读完的thread转为就绪太 + pipe_thread_ready(thread); + + thread = (struct k_thread *)sys_dlist_get(&xfer_list); + } + + //buffer和链表内的数据都被读完了,还没读够长度,则从writer中读 + if ((writer != NULL) && (num_bytes_read < bytes_to_read)) { + desc = (struct k_pipe_desc *)writer->base.swap_data; + bytes_copied = pipe_xfer((uint8_t *)data + num_bytes_read, + bytes_to_read - num_bytes_read, + desc->buffer, desc->bytes_to_xfer); + + num_bytes_read += bytes_copied; + desc->buffer += bytes_copied; + desc->bytes_to_xfer -= bytes_copied; + } + + //由于前面从pipe buffer中还接收了数据,因此链表未遍历完就可能将需要读的数据量读够,链表中剩余的数据将放入pipe buffer + while (thread != NULL) { + desc = (struct k_pipe_desc *)thread->base.swap_data; + bytes_copied = pipe_buffer_put(pipe, desc->buffer, + desc->bytes_to_xfer); + + desc->buffer += bytes_copied; + desc->bytes_to_xfer -= bytes_copied; + + //链表中写pipe线程数据被放入pipe buffer后,该线程转为就绪态 + pipe_thread_ready(thread); + + thread = (struct k_thread *)sys_dlist_get(&xfer_list); + } + + //由于前面从pipe buffer中和链表中还接收了数据,因此writer中的数据可能会有剩,需要将writer中的剩余数据放入到pipe buffer中 + //由于pipe buffer可能无法放下wirter中所有写pipe的数据,因此wirter线程不能转为就绪 + if (writer != NULL) { + desc = (struct k_pipe_desc *)writer->base.swap_data; + bytes_copied = pipe_buffer_put(pipe, desc->buffer, + desc->bytes_to_xfer); + + desc->buffer += bytes_copied; + desc->bytes_to_xfer -= bytes_copied; + } + + //数据读全,立即返回 + if (num_bytes_read == bytes_to_read) { + k_sched_unlock(); + + *bytes_read = num_bytes_read; + + SYS_PORT_TRACING_OBJ_FUNC_EXIT(k_pipe, get, pipe, timeout, 0); + + return 0; + } + + //如果读pipe已经达到最小传送尺寸min_xfer,立即返回成功不再等待 + if (!K_TIMEOUT_EQ(timeout, K_NO_WAIT) + && num_bytes_read >= min_xfer + && min_xfer > 0U) { + k_sched_unlock(); + + *bytes_read = num_bytes_read; + + SYS_PORT_TRACING_OBJ_FUNC_EXIT(k_pipe, get, pipe, timeout, 0); + + return 0; + } + + struct k_pipe_desc pipe_desc; + + pipe_desc.buffer = (uint8_t *)data + num_bytes_read; + pipe_desc.bytes_to_xfer = bytes_to_read - num_bytes_read; + //如果读pipe未达到最小尺寸且需要等待超时的,将线程加入到readers wait_q,等待写pipe线程 + if (!K_TIMEOUT_EQ(timeout, K_NO_WAIT)) { + _current->base.swap_data = &pipe_desc; + k_spinlock_key_t key2 = k_spin_lock(&pipe->lock); + + z_sched_unlock_no_reschedule(); + (void)z_pend_curr(&pipe->lock, key2, + &pipe->wait_q.readers, timeout); + } else { + k_sched_unlock(); + } + + //计算实际读pipe数据,并和min_xfer比较,检查是否达到最小传送量 + *bytes_read = bytes_to_read - pipe_desc.bytes_to_xfer; + int ret = pipe_return_code(min_xfer, pipe_desc.bytes_to_xfer, + bytes_to_read); + + return ret; + } + +内部API +------- + +从前面分析可以看出读写pipe的操作在流程上基本是对称的,都依赖于pipe_xfer_prepare,pipe_xfer。 +另外就是操作pipe buffer: pipe_buffer_put/pipe_buffer_get,前面提到过pipe buffer的操作就是ring buffer操作,本文不做分析。 pipe_xfer_prepare传输列表准备,读写都是用的这个函数,至少传入的参数不一样 + +:: + + static bool pipe_xfer_prepare(sys_dlist_t *xfer_list, + struct k_thread **waiter, + _wait_q_t *wait_q, + size_t pipe_space, + size_t bytes_to_xfer, + size_t min_xfer, + k_timeout_t timeout) + { + struct k_thread *thread; + struct k_pipe_desc *desc; + size_t num_bytes = 0; + + //不等待的传送,检查需求是否大于最小尺寸,如果需求不满足最小尺寸就立即退出 + //对于写pipe来说:检查readers wait_q中等待读pipe线程需求的数据量是否大于要写的最小尺寸 + //对于读pipe来说:检查writers wait_q中等待写pipe线程拥有的数据量是否大于要读的最小尺寸 + if (K_TIMEOUT_EQ(timeout, K_NO_WAIT)) { + _WAIT_Q_FOR_EACH(wait_q, thread) { + desc = (struct k_pipe_desc *)thread->base.swap_data; + + num_bytes += desc->bytes_to_xfer; + + if (num_bytes >= bytes_to_xfer) { + break; + } + } + + if (num_bytes + pipe_space < min_xfer) { + return false; + } + } + + + sys_dlist_init(xfer_list); + num_bytes = 0; + + //遍历writer或reader wait_q,按照需求数量的长度将线程加入链表xfer_list内,加入的同时从wait_q中移除 + while ((thread = z_waitq_head(wait_q)) != NULL) { + desc = (struct k_pipe_desc *)thread->base.swap_data; + num_bytes += desc->bytes_to_xfer; + + if (num_bytes > bytes_to_xfer) { + //最后一个thread不会被加入到链表中,因为该thread的数据需求不会被全部满足 + break; + } + + //其它thread会被从wait_q中移除并加入到链表中,这些thread的数据需求(读或写)将会被全部满足 + z_unpend_thread(thread); + sys_dlist_append(xfer_list, &thread->base.qnode_dlist); + } + + //将最后一个thread送出,外部会对没满足完的需求另做处理 + *waiter = (num_bytes > bytes_to_xfer) ? thread : NULL; + + return true; + } + +pipe_xfer非常简单就是copy数据: + +:: + + static size_t pipe_xfer(unsigned char *dest, size_t dest_size, + const unsigned char *src, size_t src_size) + { + size_t num_bytes = MIN(dest_size, src_size); + const unsigned char *end = src + num_bytes; + + while (src != end) { + *dest = *src; + dest++; + src++; + } + + return num_bytes; + } + +但也由此可知道,虽然pipe可以用来传送大量数据,但如果为了提高效率,减少数据copy还是使用消息列队等方式直接传送内存的指针比较好。 + +参考 +==== + +https://docs.zephyrproject.org/latest/reference/kernel/data_passing/pipes.html + +.. |image0| image:: ../../images/develop/kernel/pipe-b.png diff --git a/doc/source/develop/kernel/poll.rst b/doc/source/develop/kernel/poll.rst new file mode 100644 index 0000000000000000000000000000000000000000..0178ebb0d87f92c7d50f4b4bcd2b87a3e1079c11 --- /dev/null +++ b/doc/source/develop/kernel/poll.rst @@ -0,0 +1,754 @@ +.. _kernel_poll: + +同步-轮询 +########## + +轮询API用于同时等待多个条件中的任意一个满足条件。 + +使用 +===== + +API +----- + +**void k_poll_event_init(struct k_poll_event event, u32_t type, int mode,void\ obj)** + +- 作用:初始化一个k_poll_event实例,这个实例将被k_poll轮询 + +- event:要初始化的event + +- type:event轮询条件的类型,目前支援三种类型,当使用K_POLL_TYPE_IGNORE表示该event将被禁用 + + - K_POLL_TYPE_SIGNAL:poll event 信号 + - K_POLL_TYPE_SEM_AVAILABLE: 信号量 + - K_POLL_TYPE_FIFO_DATA_AVAILABLE:FIFO,实际上FIFO使用queue实现的,真正的等待条件是queue + +mode:触发模式,目前只支持K_POLL_MODE_NOTIFY_ONLY + +obj:轮询的条件,和type要对应,可以是内核对象或者event signal + +**int k_poll(struct k_poll_event *events, int num_events, s32_ttimeout)** + +- 作用:等待一个或者多个event条件有效。 events: 等待事件的数组 + +- num_events: 等待事件的数量,也就是events的个数 + +- timeout:等待超时,单位ms。K_NO_WAIT不等待, K_FOREVER一直等 + +- 返回值:当返回0时表示一个或者多个条件有效 + +注意事项: + +- k_poll收到条件有效时,仅仅是通知到线程该内核对象有效,还需要线程使用代码主动获取内核对象。 + +- k_poll返回0时,有可能是多个条件有效,需要循环使用k_poll,每次循环后检查那个event有效,需要将其状态设置为K_POLL_STATE_NOT_READY。 + +**void k_poll_signal_init(struct k_poll_signal *signal)** + +- 作用:初始化一个poll signal, 该信号可以作为poll event的条件 + +- signal:要初始化的poll signal + +**int k_poll_signal_raise(struct k_poll_signal *signal, int result)** + +- 作用:发送poll signal + +- signal: 要发送的signal + +- result: 信号的一个标记值,poll收到信号后可以获得这个值 + +**void k_poll_signal_reset(struct k_poll_signal *signal)** + +- 作用:reset signal,如果一个signal被发送,但未被poll前,可以使用该API reset掉 + +- signal: 要reset的signal + +**void k_poll_signal_check(struct k_poll_signal *signal, unsigned int* signaled, int *result)** + +- 作用:获取被轮询信号的状态和值 + +- signal: 要获取的signal + +- signaled: 是否已发送signal + +- result: 如果已发送,这里是发送的值 + +使用说明 +-------- + +poll由于可以等待多个条件,因此可以将每个条件一个线程等的形式转化为一个线程等多个条件,减少线程节约线程堆栈。 +由于poll被通知并未获取到内核对象,因此实际使用中应劲量避免有将有竞争的内核对象做为poll条件。 + +内核对象作为poll条件 +~~~~~~~~~~~~~~~~~~~~ + +初始化poll 条件 + +:: + + struct k_poll_event events[2]; + + void poll_init(void) + { + //将my_sem做为poll条件,注意my_sem,需要单独初始化 + k_poll_event_init(&events[0], + K_POLL_TYPE_SEM_AVAILABLE, + K_POLL_MODE_NOTIFY_ONLY, + &my_sem); + + //将my_fifo做为poll条件,注意my_fifo,需要单独初始化 + k_poll_event_init(&events[1], + K_POLL_TYPE_FIFO_DATA_AVAILABLE, + K_POLL_MODE_NOTIFY_ONLY, + &my_fifo); + } + +以上初始化也可以用下面方式代替, 同样注意my_sem和my_fifo需要单独初始化 + +:: + + struct k_poll_event events[2] = { + K_POLL_EVENT_STATIC_INITIALIZER(K_POLL_TYPE_SEM_AVAILABLE, + K_POLL_MODE_NOTIFY_ONLY, + &my_sem, 0), + K_POLL_EVENT_STATIC_INITIALIZER(K_POLL_TYPE_FIFO_DATA_AVAILABLE, + K_POLL_MODE_NOTIFY_ONLY, + &my_fifo, 0), + }; + +poll等待和处理 + +:: + + void poll_thread(void) + { + for(;;) { + rc = k_poll(events, 2, K_FOREVER); + if (events[0].state == K_POLL_STATE_SEM_AVAILABLE) { + k_sem_take(events[0].sem, 0); + //handle sem + } else if (events[1].state == K_POLL_STATE_FIFO_DATA_AVAILABLE) { + data = k_fifo_get(events[1].fifo, 0); + // handle data + } + events[0].state = K_POLL_STATE_NOT_READY; + events[1].state = K_POLL_STATE_NOT_READY; + } + } + +poll 信号处理 +~~~~~~~~~~~~~ + +初始化信号,并将其作为poll条件 + +:: + + struct k_poll_signal signal; + void poll_init(void) + { + k_poll_signal_init(&signal); + + struct k_poll_event events[1] = { + K_POLL_EVENT_INITIALIZER(K_POLL_TYPE_SIGNAL, + K_POLL_MODE_NOTIFY_ONLY, + &signal), + }; + } + +线程A poll信号是否发生 + +:: + + void thread_A(void){ + while(1){ + k_poll(events, 1, K_FOREVER); + + if (events[0].signal->result == 0x1337) { + // A-OK! + } else { + // weird error + } + + events[0].signal->signaled = 0; + events[0].state = K_POLL_STATE_NOT_READY; + } + } + +发送信号 + +:: + + k_poll_signal_raise(&signal, 0x1337); + +实现 +==== + +通知机制 +-------- + +poll初始化时一个等待条件对应一个poll event poll将维护一个poll +events链表,当一个thread进行k_poll等待某些条件时,这些条件对应的的poll +event被加入到poll event链表中。同时会将等待的thread和一个poller +存储在poll event中。然后thread进入超时等待,处于pending状态。 +当等待条件发生时,等待条件会主动呼叫对应自己poll +event内poller中的callback,callback内会将poll +event的thread从pending状态变为ready状态。 +之后thread继续运行,将已经就绪的poll event从链表中取出,完成k_poll轮询。 + +数据结构及类型介绍 +------------------ + +一个poll event的结构体如下 + +:: + + struct k_poll_event { + sys_dnode_t _node; // poll event链表用 + + struct _poller *poller; //poller,存储回调和polling状态 + + u32_t tag:8; //zephyr内核没有使用,可以被用户设置 + + u32_t type:_POLL_NUM_TYPES; //poll 条件类型 + + u32_t state:_POLL_NUM_STATES; //poll event的状态 + + u32_t mode:1; //poll 模式,目前只有一种 + + u32_t unused:_POLL_EVENT_NUM_UNUSED_BITS; + + union { //保存poll条件的句柄 + void *obj; + struct k_poll_signal *signal; + struct k_sem *sem; + struct k_fifo *fifo; + struct k_queue *queue; + }; + }; + +poll条件类型定义如下: + +:: + + enum _poll_types_bits { + /* can be used to ignore an event */ + _POLL_TYPE_IGNORE, + + /* to be signaled by k_poll_signal_raise() */ + _POLL_TYPE_SIGNAL, + + /* semaphore availability */ + _POLL_TYPE_SEM_AVAILABLE, + + /* queue/fifo/lifo data availability */ + _POLL_TYPE_DATA_AVAILABLE, + + _POLL_NUM_TYPES + }; + +可以看出等待条件有3种poll signal, sem, queue(fifo/lifo是由queue实现) + +poll状态类型定义如下 + +:: + + enum _poll_states_bits { + /* default state when creating event */ + _POLL_STATE_NOT_READY, + + /* signaled by k_poll_signal_raise() */ + _POLL_STATE_SIGNALED, + + /* semaphore is available */ + _POLL_STATE_SEM_AVAILABLE, + + /* data is available to read on queue/fifo/lifo */ + _POLL_STATE_DATA_AVAILABLE, + + /* queue/fifo/lifo wait was cancelled */ + _POLL_STATE_CANCELLED, + + _POLL_NUM_STATES + }; + +一个poller的结构体如下 + +:: + + struct _poller { + volatile bool is_polling; //是否需要polling + struct k_thread *thread; //是那个thread在polling + _poller_cb_t cb; // 条件满足时使用这个cb通知条件已发生 + }; + +初始化 +------ + +k_poll_event_init其实就是将k_poll_event和等待条件建立联系,然后初始化类型和状态 + +:: + + void k_poll_event_init(struct k_poll_event *event, u32_t type, + int mode, void *obj) + { + event->poller = NULL; + /* event->tag is left uninitialized: the user will set it if needed */ + event->type = type; + event->state = K_POLL_STATE_NOT_READY; + event->mode = mode; + event->unused = 0U; + event->obj = obj; //将等待条件和poll event建立联系 + } + +等待及通知 +---------- + +k_poll等待 +~~~~~~~~~~ + +k_poll首先给要poll +的event注册poller,然后等待条件发生,细节代码比较多,这里只列出主要的分析 +k_poll->z_impl_k_poll + +:: + + int z_impl_k_poll(struct k_poll_event *events, int num_events, s32_t timeout) + { + int events_registered; + k_spinlock_key_t key; + //为poll event准备poller + struct _poller poller = { .is_polling = true, + .thread = _current, //将当前thread提供给poller,将来callback时让该thread退出pending + .cb = k_poll_poller_cb }; + + __ASSERT(!arch_is_in_isr(), ""); // isr中不允许使用k_poll + __ASSERT(events != NULL, "NULL events\n"); + __ASSERT(num_events >= 0, "<0 events\n"); + + //注册poller给poll event,并检查是否已经有就绪的条件 + events_registered = register_events(events, num_events, &poller, + (timeout == K_NO_WAIT)); + + key = k_spin_lock(&lock); + + //如果已经有就绪的条件,清除注册的poll event,并返回表示已经有条件满足 + if (!poller.is_polling) { + clear_event_registrations(events, events_registered, key); + k_spin_unlock(&lock, key); + return 0; + } + + poller.is_polling = false; + //如果不等待条件满足,直接退出 + if (timeout == K_NO_WAIT) { + k_spin_unlock(&lock, key); + return -EAGAIN; + } + + //等待条件满足,条件满足时会通过poller的callback通知该thread退出等待状态 + //等待超时会发生调度 + _wait_q_t wait_q = Z_WAIT_Q_INIT(&wait_q); + int swap_rc = z_pend_curr(&lock, key, &wait_q, timeout); + + //等待结束后将清楚掉已经注册的event + key = k_spin_lock(&lock); + clear_event_registrations(events, events_registered, key); + k_spin_unlock(&lock, key); + + return swap_rc; + } + +这里也可以看到条件满足后只是k_poll不再阻塞直接退出,也就是前面提到的k_poll等到内核对象条件满足后并不会获取内核对象(sem/queue/poll signal)。 + +条件满足通知 +~~~~~~~~~~~~ + +前面说过3种内核对象条件都会通知,3个对内对象最后都是使用signal_poll_event进行通知,k_sem_give中会通知poll,流程是z_impl_k_sem_give->handle_poll_events其实现如下 + +:: + + static inline void handle_poll_events(struct k_sem *sem) + { + #ifdef CONFIG_POLL + z_handle_obj_poll_events(&sem->poll_events, K_POLL_STATE_SEM_AVAILABLE); + #else + ARG_UNUSED(sem); + #endif + } + +k_queue在k_queue_insert/k_queue_append->queue_insert->handle_poll_events其实现如下 + +:: + + static inline void handle_poll_events(struct k_queue *queue, u32_t state) + { + z_handle_obj_poll_events(&queue->poll_events, state); + } + +:: + + void z_handle_obj_poll_events(sys_dlist_t *events, u32_t state) + { + struct k_poll_event *poll_event; + + poll_event = (struct k_poll_event *)sys_dlist_get(events); + if (poll_event != NULL) { + (void) signal_poll_event(poll_event, state); //通知条件满足 + } + } + +k_poll_signal在z_impl_k_poll_signal_raise->signal_poll_event因此无论那种条件最后都是通过signal_poll_event通知等待线程条件已经就绪,signal_poll_event中使用poller中callback对thread进行通知,前面分析可以看到callback是k_poll_poller_cb + +:: + + static int k_poll_poller_cb(struct k_poll_event *event, u32_t state) + { + struct k_thread *thread = event->poller->thread; + + if (!z_is_thread_pending(thread)) { + return 0; + } + + if (z_is_thread_timeout_expired(thread)) { + return -EAGAIN; + } + + z_unpend_thread(thread); + arch_thread_return_value_set(thread, + state == K_POLL_STATE_CANCELLED ? -EINTR : 0); + + if (!z_is_thread_ready(thread)) { + return 0; + } + //在这里让等待k_poll的线程就绪,上一节的等待thread就在这里被通知退出等待 + z_ready_thread(thread); + + return 0; + } + +整体流程 +~~~~~~~~ + +再向下分析就是poll +event的管理,这些API比较繁杂,如果分析代码那面陷入细节,下面通过两张附图说明poll的event管理过程。 +当创建一个条件对象时(sem/queue/poll signal), 会有一个对应的poll event +list, 每有一个thread对该条件对象进行poll就会加入对应的Poll +event到该链表,加入链表的顺序是按优先级由高到底排列。也就是说一旦条件对象就绪,高优先级thread会先被通知。如下图 +|poll_list| + +下图为k_poll的整体流程图,说明了操作和poll event管理的对应 |poll| +说明以上流程: +1. 初始化条件对象(sem/Queue/pollsignal),初始化对象时,该对象的poll_events链表为空 +2. 初始化poll_event,将poll_event和条件对象关联(相互指向),并初始化poll_event相关字段 +3. k_poll注册要等待的poll_event,也就是将poll_event加入到条件对象的poll_events链表中,一个条件对象一条链表,每当多一个thread等待这个条件对象时,就会插入一个节点,插入节点 + 3.a 为poller指定callback  3.b 为poller指定callback会通知的thread  3.c +将等待的poll_event按线程的优先级顺序插入到条件对象的poll_events链表中 + 3.d 为poll_event指定poller +4. k_poll的thread被z_pend_curr开始等待通知 +5. 当条件对象发生时(sem/queue/poll signal),会先从这些条件对象的结构体中找到poll_events链表,然后移除第一个节点得到poll_event再通过poller中的callback通知pending的thread就绪,到此时k_poll等待的thread等到条件恢复执行 + +poll信号 +-------- + +三个等待条件中sem和queue是对外的内核对象,会有其它文章进行分析,这里说一下专门给poll用的poll +signal实现. 数据结构如下 + +:: + + struct k_poll_signal { + /** PRIVATE - DO NOT TOUCH */ + sys_dlist_t poll_events; //前面介绍过用于串接等待该signal的poll_event为链表 + + unsigned int signaled; //是否已经发送信号 + + int result; //信号值,就是前面示例中的0x1337 + }; + + +初始化 +~~~~~~ + +k_poll_signal_init->z_impl_k_poll_signal_init + +:: + + void z_impl_k_poll_signal_init(struct k_poll_signal *signal) + { + sys_dlist_init(&signal->poll_events); //初始化链表 + signal->signaled = 0U; //无信号发送 + /* signal->result is left unitialized */ + z_object_init(signal); + } + +发送信号 +~~~~~~~~ + +k_poll_signal_raise->z_impl_k_poll_signal_raise + +:: + + int z_impl_k_poll_signal_raise(struct k_poll_signal *signal, int result) + { + k_spinlock_key_t key = k_spin_lock(&lock); + struct k_poll_event *poll_event; + + signal->result = result; //设置信号值 + signal->signaled = 1U; //有信号发送 + + //从等待的poll event list中取出第一个poll event + poll_event = (struct k_poll_event *)sys_dlist_get(&signal->poll_events); + if (poll_event == NULL) { + k_spin_unlock(&lock, key); + return 0; + } + + //通知等待该poll event的信号条件已满足,这里也就会通过poll event中poller->cb 回调k_poll_poller_cb + int rc = signal_poll_event(poll_event, K_POLL_STATE_SIGNALED); + + z_reschedule(&lock, key); + return rc; + } + +poll event +---------- + +register_events +~~~~~~~~~~~~~~~ + +将等待的poll event加入到条件对象的链表中 + +:: + + static inline int register_events(struct k_poll_event *events, + int num_events, + struct _poller *poller, + bool just_check) + { + int events_registered = 0; + + for (int ii = 0; ii < num_events; ii++) { + k_spinlock_key_t key; + u32_t state; + + key = k_spin_lock(&lock); + //使用is_condition_met查询是否条件对象已经满足 + //一些情况下在k_poll前sem/queue/poll signal已经就绪 + if (is_condition_met(&events[ii], &state)) { + //如果有条件对象就绪,就不再polling,将event状态设置为ready + set_event_ready(&events[ii], state); + //将is_polling设置为false,表示无需再polling + poller->is_polling = false; + } else if (!just_check && poller->is_polling) { //对于K_NO_WAIT的k_poll,just_check会传false进来,也就是说只执行前面的检查,看是否有条件对象就绪,如果没有不会进行event注册 + //注册poll event + int rc = register_event(&events[ii], poller); + if (rc == 0) { + events_registered += 1; + } else { + __ASSERT(false, "unexpected return code\n"); + } + } + k_spin_unlock(&lock, key); + } + +条件就绪检查 + +:: + + static inline bool is_condition_met(struct k_poll_event *event, u32_t *state) + { + switch (event->type) { + case K_POLL_TYPE_SEM_AVAILABLE: + //检查是否有信号 + if (k_sem_count_get(event->sem) > 0) { + *state = K_POLL_STATE_SEM_AVAILABLE; + return true; + } + break; + case K_POLL_TYPE_DATA_AVAILABLE: + //检查queue中是否有数据 + if (!k_queue_is_empty(event->queue)) { + *state = K_POLL_STATE_FIFO_DATA_AVAILABLE; + return true; + } + break; + case K_POLL_TYPE_SIGNAL: + //检查是否已发出poll signal + if (event->signal->signaled != 0U) { + *state = K_POLL_STATE_SIGNALED; + return true; + } + break; + case K_POLL_TYPE_IGNORE: + break; + default: + __ASSERT(false, "invalid event type (0x%x)\n", event->type); + break; + } + + return false; + } + +注册event register_event->add_event + +:: + + static inline int register_event(struct k_poll_event *event, + struct _poller *poller) + { + //根据不同的条件类型,使用add_event将event注册到条件对象的poll events链表中 + switch (event->type) { + case K_POLL_TYPE_SEM_AVAILABLE: + __ASSERT(event->sem != NULL, "invalid semaphore\n"); + add_event(&event->sem->poll_events, event, poller); + break; + case K_POLL_TYPE_DATA_AVAILABLE: + __ASSERT(event->queue != NULL, "invalid queue\n"); + add_event(&event->queue->poll_events, event, poller); + break; + case K_POLL_TYPE_SIGNAL: + __ASSERT(event->signal != NULL, "invalid poll signal\n"); + add_event(&event->signal->poll_events, event, poller); + break; + case K_POLL_TYPE_IGNORE: + /* nothing to do */ + break; + default: + __ASSERT(false, "invalid event type\n"); + break; + } + + //根系poller,之后这个event就会使用这个poller内的callback通知等待thread就绪 + event->poller = poller; + + return 0; + } + + static inline void add_event(sys_dlist_t *events, struct k_poll_event *event, + struct _poller *poller) + { + struct k_poll_event *pending; + //events是一个双向循环链表,里面放的poll event是按等待它thread的优先级从高到底排序 + //这里取最后一个节点,也就是优先级最低的节点 + //如果链表中没有节点,或者是要注册的event所属thread优先级比最低的节点的线程优先级都低,就将注册的event插入到最后 + pending = (struct k_poll_event *)sys_dlist_peek_tail(events); + if ((pending == NULL) || + z_is_t1_higher_prio_than_t2(pending->poller->thread, + poller->thread)) { + sys_dlist_append(events, &event->_node); + return; + } + + //遍历整个链表,将Poll event按thread优先级进行插入 + SYS_DLIST_FOR_EACH_CONTAINER(events, pending, _node) { + if (z_is_t1_higher_prio_than_t2(poller->thread, + pending->poller->thread)) { + sys_dlist_insert(&pending->_node, &event->_node); + return; + } + } + + sys_dlist_append(events, &event->_node); + } + +clear_event_registrations +~~~~~~~~~~~~~~~~~~~~~~~~~ + +清除event,当poll条件满足后, +k_poll恢复执行,会使用clear_event_registrations将所有注册的event全部清除 + +:: + + static inline void clear_event_registrations(struct k_poll_event *events, + int num_events, + k_spinlock_key_t key) + { + //使用clear_event_registration清除event + while (num_events--) { + clear_event_registration(&events[num_events]); + k_spin_unlock(&lock, key); + key = k_spin_lock(&lock); + } + } + static inline void clear_event_registration(struct k_poll_event *event) + { + bool remove = false; + + //移除poller + event->poller = NULL; + + //将poll event从链表中移除 + if (remove && sys_dnode_is_linked(&event->_node)) { + sys_dlist_remove(&event->_node); + } + } + +再谈k_poll +---------- + +有了poll event的管理,我们再结合之前的原理来再来分析一次k_poll +k_poll->z_impl_k_poll + +:: + + int z_impl_k_poll(struct k_poll_event *events, int num_events, s32_t timeout) + { + int events_registered; + k_spinlock_key_t key; + + //为poll event初始化poller,里面包含通知的callback函数k_poll_poller_cb + struct _poller poller = { .is_polling = true, + .thread = _current, + .cb = k_poll_poller_cb }; + + //ISR中不允许k_poll + __ASSERT(!arch_is_in_isr(), ""); + + //检查要poll event的参数 + __ASSERT(events != NULL, "NULL events\n"); + __ASSERT(num_events >= 0, "<0 events\n"); + + //注册poll event + //这里会根据timeout,通知register_events是否只是检查条件就绪,而不真正的注册poll event + events_registered = register_events(events, num_events, &poller, + (timeout == K_NO_WAIT)); + + key = k_spin_lock(&lock); + + //前面分析过在register_events时会检查条件对象, 如果条件对象满足会将is_polling设置为false + //因此检查is_polling为false时,表面已经有条件对象满足,这里就清楚已注册的event,然后直接返回表示已经等到条件对象 + if (!poller.is_polling) { + clear_event_registrations(events, events_registered, key); + k_spin_unlock(&lock, key); + return 0; + } + + poller.is_polling = false; + + //如果没有条件对象满足,而又不等待就直接退出 + if (timeout == K_NO_WAIT) { + k_spin_unlock(&lock, key); + return -EAGAIN; + } + + _wait_q_t wait_q = Z_WAIT_Q_INIT(&wait_q); + //将当前线程pending住等待条件满足 + //条件对象满足后会通过poller中的callback让这个等待的thread ready继续执行 + //在timeout 后都没有等待条件对象满足,thread也会继续执行 + int swap_rc = z_pend_curr(&lock, key, &wait_q, timeout); + + //等待到条件满足或者超时,线程退出pending继续执行 + //清除本次注册的Poll event + key = k_spin_lock(&lock); + clear_event_registrations(events, events_registered, key); + k_spin_unlock(&lock, key); + + return swap_rc; + } + +参考 +==== + +https://docs.zephyrproject.org/latest/reference/kernel/other/polling.html + +.. |poll_list| image:: ../../images/develop/kernel/poll_list.png +.. |poll| image:: ../../images/develop/kernel/poll.png diff --git a/doc/source/develop/kernel/sem.rst b/doc/source/develop/kernel/sem.rst new file mode 100644 index 0000000000000000000000000000000000000000..7611e0405fde19e059f6ef6a5b3c458014451e34 --- /dev/null +++ b/doc/source/develop/kernel/sem.rst @@ -0,0 +1,277 @@ +.. _kernel_sem: + +同步-信号量 +############ + +使用 +==== + +API +--- + +**#define K_SEM_DEFINE(name, initial_count, count_limit)** + +* 作用:声明一个k_sem,并初始化 +* name: 声明一个name的k_sem +* initial_count:初始化count +* count_limit: 允许最大的count +* 无返回值,如果初始化有问题,会在编译期间出错 + +**int k_sem_init(struct k_sem *sem, unsigned int initial_count,unsigned int limit)** + +* 作用: 初始化sem sem, 要初始化的sem +* initial_count:初始化 +* count count_limit: 允许最大的count 返回值: 0表示初始化成功 + +**int k_sem_take(struct k_sem *sem, s32_t timeout)** + +* 作用:获取信号 +* sem: 要等待的信号量 +* timeout: 等待时间,单位ms。K_NO_WAIT不等待,K_FOREVER一直等 +* 返回值: 0表示拿到sem + +**void k_sem_give(struct k_sem *sem)** + +* 作用:发送信号 + +**void k_sem_reset(struct k_sem *sem)** + +* 作用:将信号的量的count reset为0 + +**unsigned int k_sem_count_get(struct k_sem *sem)** + +* 作用:获取指定信号量当前的count值 + +使用说明 +-------- + +使用信号量来控制多个线程对一组资源的访问。 +使用信号量在生产线程和消耗线程或ISR之间同步处理。 + +初始化 +~~~~~~ + +先初始化一个信号量,下面两种方式的效果是一样的 + +方法1,使用宏 + +:: + + K_SEM_DEFINE(my_sem, 0, 10); + +方法2,使用函数 + +:: + + struct k_sem my_sem; + k_sem_init(&my_sem, 0, 10); + +发送信号量 +~~~~~~~~~~ + +允许在thread或者ISR中发送信号量,一般情况下发送信号量的thread或者ISR都是资源的生产者。 +例如中断被触发时数据有效,在ISR中通过发信号量通知接收线程接收数据。 + +:: + + void input_data_isr_handler(void *arg) + { + /* notify thread that data is available */ + k_sem_give(&my_sem); + + ... + } + +:: + + void productor_thread(void *arg) + { + while(1){ + /* prepare data */ + ... + /* notify thread that data is available */ + k_sem_give(&my_sem); + } + } + +接收信号量 +~~~~~~~~~~ + +允许在thread中接收信号量,但实际过程中ISR中接收信号量的情况几乎没有,如果一定要用,timeout只能用K_NO_WAIT。 +一般情况下接收信号量的thread是消费者。 + +:: + + void consumer_thread(void) + { + ... + while(1){ + + if (k_sem_take(&my_sem, K_MSEC(50)) != 0) { + printk("Input data not available!"); + } else { + /* fetch available data */ + ... + } + } + } + +实现 +==== + +sem结构体如下,可以看出其基本实现是用的wait_q + +:: + + struct k_sem { + _wait_q_t wait_q; + u32_t count; //记录当前sem cnt + u32_t limit; //最大cnt限制 + _POLL_EVENT; //提供给poll用 + }; + +.. _初始化-1: + +初始化 +------ + +k_sem_init -> z_impl_k_sem_init ,流程分析见注释 + +:: + + int z_impl_k_sem_init(struct k_sem *sem, unsigned int initial_count, + unsigned int limit) + { + //参数检测 + CHECKIF(limit == 0U || initial_count > limit) { + return -EINVAL; + } + + sem->count = initial_count; //设置初始化的sem cnt + sem->limit = limit; //设置sem cnt最大限制 + z_waitq_init(&sem->wait_q); //初始化wait_q + #if defined(CONFIG_POLL) + sys_dlist_init(&sem->poll_events); //如果配置了poll,由于sem可以作为poll的条件,因此这里要初始化sem的poll_event + #endif + + z_object_init(sem); + + return 0; + } + +再看一下使用宏初始化信号量的实现方法 + +:: + + #define Z_SEM_INITIALIZER(obj, initial_count, count_limit) \ + { \ + .wait_q = Z_WAIT_Q_INIT(&obj.wait_q), \ + .count = initial_count, \ + .limit = count_limit, \ + _POLL_EVENT_OBJ_INIT(obj) \ + _OBJECT_TRACING_INIT \ + } + + #define K_SEM_DEFINE(name, initial_count, count_limit) \ + Z_STRUCT_SECTION_ITERABLE(k_sem, name) = \ 定义一个k_sem变量 + Z_SEM_INITIALIZER(name, initial_count, count_limit); \ 初始化这个变量 + BUILD_ASSERT(((count_limit) != 0) && \ + ((initial_count) <= (count_limit))); + +发送 +---- + +k_sem_give -> z_impl_k_sem_give,流程分析见注释 + +:: + + void z_impl_k_sem_give(struct k_sem *sem) + { + k_spinlock_key_t key = k_spin_lock(&lock); + + //获取正在等待该sem的thread + struct k_thread *thread = z_unpend_first_thread(&sem->wait_q); + + if (thread != NULL) { + //如果存在等待sem的thread,将该thread转为就绪 + arch_thread_return_value_set(thread, 0); + z_ready_thread(thread); + } else { + //如果不存在等待sem的thread, sem cnt +1, 将资源累计,但不能藏limit + sem->count += (sem->count != sem->limit) ? 1U : 0U; + + //这里是通知poll该sem的对象条件已满足,这部分在poll分析 + handle_poll_events(sem); + } + + //重新调度,切换ready的thread上 + z_reschedule(&lock, key); + } + +接收 +---- + +k_sem_take->z_impl_k_sem_take,流程分析见注释 + +:: + + int z_impl_k_sem_take(struct k_sem *sem, s32_t timeout) + { + int ret = 0; + + //ISR中只能不等待的收取sem + __ASSERT(((arch_is_in_isr() == false) || (timeout == K_NO_WAIT)), ""); + + k_spinlock_key_t key = k_spin_lock(&lock); + + //如果sem cnt不为0,可获取信号,直接返回 + if (likely(sem->count > 0U)) { + sem->count--; + k_spin_unlock(&lock, key); + ret = 0; + goto out; + } + + //如果没有信号,且不愿意等待,直接返回 + if (timeout == K_NO_WAIT) { + k_spin_unlock(&lock, key); + ret = -EBUSY; + goto out; + } + + //没有信号,有要等待,会将等待的线程加入了等待列表中,然后重新调度切换到其它thread运行 + //等待超时或者等到sem后会从这里返回继续运行 + ret = z_pend_curr(&lock, key, &sem->wait_q, timeout); + + out: + return ret; + } + +Reset +----- + +k_sem_reset->z_impl_k_sem_reset 非常简单,将计数请0 + +:: + + static inline void z_impl_k_sem_reset(struct k_sem *sem) + { + sem->count = 0U; + } + +Get cnt +------- + +k_sem_count_get->z_impl_k_sem_count_get 也非常简单直接返回count + +:: + + static inline unsigned int z_impl_k_sem_count_get(struct k_sem *sem) + { + return sem->count; + } + +参考 +==== + +https://docs.zephyrproject.org/latest/reference/kernel/synchronization/semaphores.html diff --git a/doc/source/develop/kernel/stack.rst b/doc/source/develop/kernel/stack.rst new file mode 100644 index 0000000000000000000000000000000000000000..c68fcfe6a952a9e37fb94a943edd24984b333f74 --- /dev/null +++ b/doc/source/develop/kernel/stack.rst @@ -0,0 +1,285 @@ +.. _kernel_stack: + +数据传递-堆栈 +############### + +使用 +==== + +API +--- + +**void k_stack_init(struct k_stack stack, stack_data_t buffer, u32_t num_entries);** + +作用:初始化一个stack,stack的内存由使用者分配 + +stack: 被初始化的stack + +buffer: stack使用的内存 + +num_entries:stack的成员数量 + +**__syscall s32_t k_stack_alloc_init(struct k_stack *stack, u32_tnum_entries);** + +作用:初始化一个stack,stack使用的内存从thread pool分配 + +stack: 被初始化的stack + +num_entries:stack的成员数量 + +返回值: 0表示初始化成功 + +**int k_stack_cleanup(struct k_stack* stack);** + +作用:释放k_stack_alloc_init中为stack分配的内存 + +stack: 操作的stack + +返回值: 0表示free成功,如果有pop在等待数据时会返回非0 + +**__syscall int k_stack_push(struct k_stack *stack, stack_data_t data);** + +作用:压栈 + +stack:被压栈的stack + +data: 要push的数据 + +返回值:push成功返回0 + +**__syscall int k_stack_pop(struct k_stack *stack, stack_data_t* data, s32_t timeout);** + +作用:出栈 stack:被出栈的stack + +data: pop的数据 + +timeout: stack没数据时可等待超时,单位ms。K_NO_WAIT不等待, K_FOREVER一直等 + +返回值:pop成功返回0 + +使用说明 +-------- + +可以在ISR中push stack.也可在ISR内pop stack,但不能等待。 stack满后,push将会失败。 stack的数据成员是指针类型stack_data_t的大小和CPU位数对应 + +初始化 +~~~~~~~ + +下面两种方式都可以对stack进行初始化 使用函数 + +:: + + #define MAX_ITEMS 10 + + stack_data_t my_stack_array[MAX_ITEMS]; + struct k_stack my_stack; + + k_stack_init(&my_stack, my_stack_array, MAX_ITEMS); + +使用宏 + +:: + + K_STACK_DEFINE(my_stack, MAX_ITEMS); + +栈操作 +~~~~~~ + +堆栈当中存储的只是数据的指针 + +:: + + struct my_buffer_type { + int field1; + ... + }; + struct my_buffer_type my_buffers[MAX_ITEMS]; + + void producer_thread(int unused1, int unused2, int unused3) + { + //将数据指针入栈 + for (int i = 0; i < MAX_ITEMS; i++) { + k_stack_push(&my_stack, (stack_data_t)&my_buffers[i]); + } + } + + void consumer_fifo_thread(int unused1, int unused2, int unused3) + { + struct my_buffer_type *new_buffer; + //将数据指针出栈,并进行操作 + k_stack_pop(&buffer_stack, (stack_data_t *)&new_buffer, K_FOREVER); + new_buffer->field1 = ... + } + +实现 +==== + +数据结构 +-------- + +stack的数据结构如下,wait_q用于pop时无数据等待数据,lock用于多线程访问stack时进行线程保护 + +:: + + struct k_stack { + _wait_q_t wait_q; + struct k_spinlock lock; + stack_data_t *base, *next, *top; + u8_t flags; + }; + +base是栈底, +next是栈顶,top是堆栈最大的位置,也就是说next不能超过top.如下图 |stack| + +flag只指示该堆栈的内存是否是从线程池中分配的,如果是用k_stack_alloc_init初始化的堆栈,flag就会被设置为下面的值 + +:: + + #define K_STACK_FLAG_ALLOC ((u8_t)1) + +初始化 +------ + +初始化起始就是将stack的各个指针设置正确 + +:: + + void (struct k_stack *stack, stack_data_t *buffer, + u32_t num_entries) + { + z_waitq_init(&stack->wait_q); //初始化wait_q + stack->lock = (struct k_spinlock) {}; + stack->next = stack->base = buffer; //base指向buffer开始 + stack->top = stack->base + num_entries; //top执行buffer尾部 + + z_object_init(stack); + } + +push +---- + +k_stack_push->z_impl_k_stack_push,分析见注释 + +:: + + int z_impl_k_stack_push(struct k_stack *stack, stack_data_t data) + { + struct k_thread *first_pending_thread; + k_spinlock_key_t key; + + // stack满了,返回 + CHECKIF(stack->next == stack->top) { + return -ENOMEM; + } + + key = k_spin_lock(&stack->lock); + + //查看是否有thread已经在等待stack数据 + first_pending_thread = z_unpend_first_thread(&stack->wait_q); + + if (first_pending_thread != NULL) { + //如果有thread在等stack数据,将push的数据直接给该thread + //stack指针不做修改 + z_ready_thread(first_pending_thread); + + z_thread_return_value_set_with_data(first_pending_thread, + 0, (void *)data); + z_reschedule(&stack->lock, key); + } else { + //没有thread等stack,将数据放入stack,然后修改next指针 + *(stack->next) = data; + stack->next++; + k_spin_unlock(&stack->lock, key); + } + + return 0; + } + +pop +--- + +k_stack_pop->z_impl_k_stack_pop,分析见注释 + +:: + + int z_impl_k_stack_pop(struct k_stack *stack, stack_data_t *data, s32_t timeout) + { + k_spinlock_key_t key; + int result; + + key = k_spin_lock(&stack->lock); + + //检查stack中是否有数据,有就修改next,然后将数据传出 + if (likely(stack->next > stack->base)) { + stack->next--; + *data = *(stack->next); + k_spin_unlock(&stack->lock, key); + return 0; + } + + //如果pop不等待,又没有数据就直接退出 + if (timeout == K_NO_WAIT) { + k_spin_unlock(&stack->lock, key); + return -EBUSY; + } + + //将thread加入wait_q等待stack数据 + result = z_pend_curr(&stack->lock, key, &stack->wait_q, timeout); + if (result == -EAGAIN) { + //等待超时,则退出 + return -EAGAIN; + } + + //等待到数据就传出数据 + *data = (stack_data_t)_current->base.swap_data; + return 0; + } + +Stack从thread pool分配内存 +-------------------------- + +除了传入stack内存外,stack也可以自己从内存池中分配stack内存 +k_stack_alloc_init->z_impl_k_stack_alloc_init 分配内存并初始化stack。 +k_stack_cleanup和z_impl_k_stack_alloc_init配对使用,当不使用时使用该api来做k_free。 +分析见代码注释 + +:: + + s32_t z_impl_k_stack_alloc_init(struct k_stack *stack, u32_t num_entries) + { + void *buffer; + s32_t ret; + + //从线程池中分配内存 + buffer = z_thread_malloc(num_entries * sizeof(stack_data_t)); + if (buffer != NULL) { + k_stack_init(stack, buffer, num_entries); + //设置flags,指示该stack的buffer是从内存池中分配 + stack->flags = K_STACK_FLAG_ALLOC; + ret = (s32_t)0; + } else { + ret = -ENOMEM; + } + + + int k_stack_cleanup(struct k_stack *stack) + { + //检查wait_q,如果有线程在等待数据不能clean up stack + CHECKIF(z_waitq_head(&stack->wait_q) != NULL) { + return -EAGAIN; + } + //检查有alloc flag,对stack buffer进行释放 + if ((stack->flags & K_STACK_FLAG_ALLOC) != (u8_t)0) { + k_free(stack->base); + stack->base = NULL; + stack->flags &= ~K_STACK_FLAG_ALLOC; + } + return 0; + } + +参考 +==== + +https://docs.zephyrproject.org/latest/reference/kernel/data_passing/stacks.html + +.. |stack| image:: ../../images/develop/kernel/stack.png diff --git a/doc/source/develop/kernel/synchronization.rst b/doc/source/develop/kernel/synchronization.rst new file mode 100644 index 0000000000000000000000000000000000000000..414e7a974fd1f7bda13599ca9dfcf51cfcfa945e --- /dev/null +++ b/doc/source/develop/kernel/synchronization.rst @@ -0,0 +1,60 @@ +.. _kernel_synchronization: + +同步 +######### + +本文简要介绍Zephyr内核提供的同步手段,说明主要属性和特点。 + +因为操作系统同步手段的共性,因此本文中有一大部分内容都是操作系统的基本概念。 + +概述 +==== + +在嵌入式操作系统中,多线程被调度器同时调度,从宏观上看是线程是并行执行的,对于并行的线程当有执行步骤有先后顺序要求,执行线程任务间有配合需求以及线程间有源共享的情况时,就需要各线程能够同步。 +所有的嵌入式操作系统都会提供线程同步手段,Zephyr也不例外,Zephyr提供了信号量,互斥锁,轮询, 事件,条件变量五种内核对象作为同步手段。 + +信号量(k_sem) +============= + +信号量是用于控制多个线程对一组资源的访问,使用信号量在生产者(ISR/Thread)和消费者(thread)之间同步。 +Zephyr的信号量没有比较特殊的地方,标准的信号量接口定义在include/kernel.h中,代码实现在kernel/sem.c中,可查看k_sem相关代码。有以下特性: +* Zephyr的信号量在初始化时可以指定初始化计数值和最大计数值,生产者give时计数值+1,但不会超过最大值,消费者take时计数值-1,直到为0。一些其它的操作系统不会设置最大计数值。 +* 每次信号量give都会引发调度。 +* 如果多个线程都在等待信号量,新产生的信号量会被等待时间最长的最高优先级线程接收。 + +互斥量(k_mutex) +=============== + +Mutex主要是用于解决多线程之间资源保护的问题,当线程打算访问一个共享资源时,需要先拿到该资源的互斥锁。 +当线程拥有锁后正在访问共享资源,其它线程尝试获取锁时会被阻塞,直到访问资源的线程释放锁。 +Zephyr的互斥量没有比较特殊的地方,标准的接口定义在include/kernel.h中,代码实现在kernel/mutex.c中,可查看k_mutex相关代码。有以下特性: +* Mutex只能用于线程之间,不能用于ISR中。 +* Mutex unlock时会引发调度。 +* 低优先级线程获取锁后有高优先级线程等锁时会引起优先级倒置。 + +轮询(k_poll) +============ + +poll算是一个比较特殊的内核对象,和一般操作系统中的事件对象相似,也有点类似于posix中的poll(). +polling API对内核对象进行轮询,允许单个线程等待一个或者多个条件满足。条件类型只能是内核对象,目前支持的是Semaphore, +FIFO, poll signal三种。 例如一个线程使用polling API同时等待3个semaphore,只要有1个semaphore发生时polling API就会得到通知,之后可以由用户根据需求决定要做什么样的操作。 +接口定义在include/kernel.h中,代码实现在kernel/mutex.c中,可查看k_poll相关代码。有以下特性: +* 等待多个条件时,有一个条件满足k_poll就会返回,因此如果要组合条件,还需要使用代码配合 +* Sem/FIFO满足条件后, k_poll只是接到通知返回,线程并未获取到Sem/FIFO,还需要使用代码主动获取 + +事件(k_event) +============== +事件对象是一种线程同步对象,允许一个或者多个线程等待同一个事件对象,当事件被传递到事件对象时,满足条件的线程都变为就绪。通常使用事件来通知一系列条件已满足。 +接口定义在include/kernel.h中,代码实现在kernel/event.c中,可查看k_event相关代码。有以下特性: +* 事件可以由线程或ISR发送 +* 事件对象由32bit来标识,一个bit对应一个特定的事件,线程可以等待一个或者多个事件 + +条件变量(k_condvar) +====================== +条件变量通常用于控制共享资源的访问,它允许一个线程等待其它线程创建共享资源需要的条件 +接口定义在include/kernel.h中,代码实现在kernel/condvar.c中,可查看k_condvar相关代码 + +参考 +==== + +https://docs.zephyrproject.org/latest/reference/kernel/index.html diff --git a/doc/source/develop/kernel/system_thread.rst b/doc/source/develop/kernel/system_thread.rst new file mode 100644 index 0000000000000000000000000000000000000000..64889f3172c768394d7355f871e18fb1a1f18979 --- /dev/null +++ b/doc/source/develop/kernel/system_thread.rst @@ -0,0 +1,60 @@ +.. _kernel_system_thread: + +系统线程 +######### + +系统线程是内核初始化过程中自动启动的两个线程,Zephyr内核自动启动两个线程。 + +主线程 +====== + +Zephyr在内核启动过程中,完成\ ``_SYS_INIT_LEVEL_PRE_KERNEL_1``\ 和\ ``_SYS_INIT_LEVEL_PRE_KERNEL_2``\ 等级的基础硬件初始化后就启动主线程\ ``bg_thread_main``\ ,主线程内在主要完成以下任务: + +- ``_SYS_INIT_LEVEL_POST_KERNEL``\ 等级的设备初始化 + +- ``_SYS_INIT_LEVEL_APPLICATION``\ 等级的设备初始化 + +- 启动静态初始化线程 + +- 初始化SMP + +- 跳转到\ ``main()``\ 处理 + +这里的\ ``main()``\ 就是Zephyr的应用程序\ ``main()``\ 入口。 + +主线程的优先级由\ ``CONFIG_MAIN_THREAD_PRIORITY``\ 指定,默认配置为最高的可抢占优先级\ ``0``\ ,如果内核被配置为不支持抢占线程,则主线程优先级默认为最低的协作线程优先级\ ``-1``\ 。一般情况下不建议对主线程优先级进行配置。需要注意的是由于主线程的优先级默认比所有抢占优先级都高,应用程序在\ ``main()``\ 中不要随意做“busy +loop”,否则会导致其它低优先级抢占式线程无法运行。 + +主线程的堆栈由\ ``CONFIG_MAIN_STACK_SIZE``\ 指定,默认为1K。由于应用程序的\ ``main()``\ 是在主线程中执行,当应用程序在\ ``main()``\ 中使用大量堆栈时,需要增加\ ``CONFIG_MAIN_STACK_SIZE``\ 的大小。 + +主线程是执行内核初始化和应用程序\ ``main()``\ 时必不可少的线程, +因此在创建主线程时会指定\ ``K_ESSENTIAL``\ 表示其不能被中止,中止主线程会引发致命的系统错误。 +但是当\ ``main()``\ 返回后主线程会主动移掉\ ``K_ESSENTIAL``\ 标记此时主线程可以正常终止并且不会引发错误。 + + +空闲线程 +======== + +当系统没有其他工作要做时,休眠线程将执行。在支持硬件休眠的SOC架构下空闲线程会通过电源管理系统让SOC进入休眠以节省电量, +否则空闲线程只会执行“busy loop” 。 +只要系统在运行,空闲线程就一直存在并且永远不会终止。 + +空闲线程始终使用系统配置的最低线程优先级。 +如果系统被配置为不支持可抢占优先级时,空闲线程将变成一个协作线程,因此空闲线程会重复让出 +CPU 以允许其他线程在需要时运行。 + +空闲线程是必不可少的线程,在创建空闲线程时会指定\ ``K_ESSENTIAL``\ 表示其不能终止,中止空闲线程会引发致命的系统错误。 + +空闲线程的堆栈由\ ``CONFIG_IDLE_STACK_SIZE``\ 指定,由于空闲线程内做的工作需要堆栈不大,因此默认配置为256字节,一些不同的CPU体系架构会有稍微大一点的堆栈,但都不会超过1K字节。 + + +其它 +==== + +主线程和空闲线程是Zephyr系统不能缺少的系统线程,同时根据应用程序指定的内核和主板配置选项,也可能产生额外的系统线程。 +例如,启用系统系统工作队列会产生一个系统线程为提交给它的工作项提供服务。 + +参考 +==== + +https://docs.zephyrproject.org/latest/kernel/services/threads/system_threads.html diff --git a/doc/source/develop/kernel/workq.rst b/doc/source/develop/kernel/workq.rst new file mode 100644 index 0000000000000000000000000000000000000000..722a61d76830acab1cd6be006091c5bb60b9094a --- /dev/null +++ b/doc/source/develop/kernel/workq.rst @@ -0,0 +1,303 @@ +.. _kernel_workq: + +工作队列 +######## + +工作队列由一个专用线程和列队组成,专用线程以先进先出的方式处理发送到列队中的工作项。工作队列的线程通常被指定为低优先级线程,中断服务程序或者高优先级线程将非紧急事务转移到工作队列的专用线程中处理,以此降低对时间敏感事物的影响。在Zephyr中工作队列被作为内核对象提供出来,而其它大多数RTOS需要使用者自己使用线程和消息队列来实现工作队列。 + +|image0| + +只要内存足够在Zephyr可以创建任意数量的工作队列,一个工作队列就会有一个工作线程,工作线程的优先级可以配置为协助或者抢占。当列队中无工作项时工作线程处于睡眠状态。当使用者向工作队列发送工作项时,工作项被加入到列队同时通知工作线程处理,工作线程从列队中取出工作项执行,工作线程在执行工作项之间会使用\ ``k_yield``\ 以防止协作类型的工作线程一直占用CPU饿死其它线程。 + +一个工作队列的三大要素: + +- 工作项:中断服务程序或高优先级线程的非紧急事务 + +- 列队:用于保存还没处理的工作项 + +- 工作线程:处理工作项中携带的事务 + +工作项 +====== + +工作项带有一个工作处理函数,当工作线程处理该工作项时就调用该工作处理函数。工作项在生命周期内有如下状态: + +- 悬空:被发送者初始化 + +- 排队(**queued**):\ ``K_WORK_QUEUED`` + 工作项被发送放到列队中,但还未执行 + +- 预约(**scheduled**): ``K_WORK_DELAYED`` + 延迟工作项(后文说明)在等待超时 + +- 运行(**running**):\ ``K_WORK_RUNNING`` 工作项正在被工作线程处理 + +- 弃用(**canceling**): ``K_WORK_CANCELING`` 弃用正在被处理的工作项 + +工作项可能会同时拥有以上几种状态的组合,例如启用一个正在执行的工作项,这个工作项的状态就是\ ``K_WORK_RUNNING | K_WORK_CANCELING`` + +用户定义工作队列 +================ + +只要内存足够用户可以定义任意数量的工作队列,创建一个工作队列需要通过初始化和启动两步完成,工作队列的线程是在启动工作队列时才创建 + +.. code:: c + + /* 工作队列线程的属性 */ + #define USER_WQ_STACK_SIZE 4096 + #define USER_WQ_PRIORITY 5 + + /* 定义工作队列线程的堆栈 */ + K_THREAD_STACK_DEFINE(user_wq_stack_area, USER_WQ_STACK_SIZE); + + /* 初始化工作队列 */ + struct k_work_q user_work_q; + k_work_queue_init(&user_work_q); + + /* 启动工作队列 */ + k_work_queue_start(&user_work_q, user_wq_stack_area, + K_THREAD_STACK_SIZEOF(user_wq_stack_area), USER_WQ_PRIORITY, + NULL); + +发送工作项到指定的工作队列,工作队列线程一旦无其它前置的工作项时将立即处理该工作项。 + +.. code:: c + + /* 初始化工作项,为其指定工作函数user_work_handler */ + struct k_work work; + k_work_init(&work, user_work_handler); + + /* 将工作项发送到 user_work_q的工作队列处理 */ + k_work_submit_to_queue(&user_work_q, &work); + +对用户定义的工作队列可以进行排空操作 + +.. code:: c + + /* 排空user_work_q的工作项,第二个参数plug设置未true表示排空后仍然不接受新的工作项提交 */ + k_work_queue_drain(&user_work_q, true); + +排空函数会一直阻塞直到工作队列中所有工作项执行完成后才会返回,在排空执行期间被排空的工作队列只接受自己工作线程提交的工作项,不接受中断服务程序和其它线程提交的工作项。排空结束后工作队列会根据第二个参数\ ``plug``\ 判断是否接受新的工作项,如果\ ``false``\ 表示排空后可以立即接受新的工作项提交,如果为\ ``true``\ 需要做unplug动作做释放动作让工作队列可以继续接受新的工作项 + +.. code:: c + + /* 释放plug的工作队列,可以继续接受新的提交的工作项 */ + k_work_queue_unplug(&user_work_q); + +系统工作队列 +============ + +zephyr在\ ``POST_KERNEL``\ 初始化阶段会创建一个系统工作队列\ ``k_sys_work_q`` + +.. code:: c + + SYS_INIT(k_sys_work_q_init, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT); + +该工作队列的线程优先级通过\ ``CONFIG_SYSTEM_WORKQUEUE_PRIORITY``\ 配置,默认\ ``-1``\ ,属于不可抢占的协作线程。通过\ ``CONFIG_SYSTEM_WORKQUEUE_NO_YIELD``\ 配置系统工作队列在处理完一个工作项后是否执行\ ``k_yield``\ ,默认为\ ``n``\ 表示需要执行 + +.. code:: c + + static int k_sys_work_q_init(const struct device *dev) + { + ARG_UNUSED(dev); + struct k_work_queue_config cfg = { + .name = "sysworkq", + .no_yield = IS_ENABLED(CONFIG_SYSTEM_WORKQUEUE_NO_YIELD), + }; + + k_work_queue_start(&k_sys_work_q, + sys_work_q_stack, + K_KERNEL_STACK_SIZEOF(sys_work_q_stack), + CONFIG_SYSTEM_WORKQUEUE_PRIORITY, &cfg); + return 0; + } + +从上面的实现可以看到,系统工作队列也是使用的\ ``k_work_queue_start``\ 创建,因此其工作原理和用户定义的一致。 + +由于新建一个工作队列要创建线程,需要较多内存资源,因此一般建议只使用系统工作队列。只有当工作项会影响到系统工作队列的时才另建立工作队列处理该工作项,例如一个工作项会长时间阻塞影响系统工作队列中后续的工作项,这就需要新建工作队列来单独处理。 + +系统工作队列已经在zephyr初始化阶段创建好,因此只用初始化一个工作项进行发送执行即可,系统工作队列线程一旦无其它前置的工作项时将立即处理该工作项 + +.. code:: c + + struct k_work work; + k_work_init(&work, user_work_handler); + k_work_submit(&work); + +对工作项的操作 +============== + +无论是提交到系统工作队列还是用户工作队列的工作项都可以使用下列API进行操作 + +.. code:: c + + struct k_work_sync sync; + + /* 等待work执行完成后才返回 */ + k_work_flush(&work, &sync); + + /* 异步取消work立即返回,列队等待的work将被移除,已经再执行的work依然执行完 */ + k_work_cancel(&work); + + /* 同步取消work,列队等待的work将被移除,已经再执行的work依然执行完,等到执行完后才会返回 */ + k_work_cancel(&work, &sync); + +延迟工作项 +========== + +当中断服务程序或高优先级线程希望工作项在指定时间后才执行,可以通过延迟工作项来完成,通过设置预定时间,延迟工作项会在指定时间后才提交到工作队列中。 +前面提到过工作队列排空时不会接受工作队列线程以外的工作项提交,但延迟工作项在预约时间到时的提交不受到该限制。 + +**注意**\ :延迟工作项只是延迟提交到工作队列,因此只能保证在指定时间后执行,不能保证在指定时间点执行。例如指定20ms后执行,会在20ms后提交到工作队列,此时如果队列中有其它前置工作项,会等其它前置工作项完成后才会执行。 + +延迟工作项的使用方法如下: + +初始化 +------ + +.. code:: c + + /* 初始化一个延迟工作项 */ + struct k_work_delayable delay_work; + k_work_init_delayable(&delay_work, delay_work_handler); + +预约 +---- + +一个延迟工作项既可以预约到系统工作队列也可以预约到用户工作队列,当预约时间到的适合工作项会被提交到工作队列中 + +**系统工作队列** + +.. code:: c + + /* 300ms后将延迟工作项提交到系统工作队列中*/ + k_work_schedule(&delay_work, K_MSEC(300)); + +**用户工作队列** + +.. code:: c + + /* 300ms后将延迟工作项提交到user_work_q工作队列中*/ + k_work_schedule_for_queue(&user_work_q, &delay_work, K_MSEC(300)); + + +对一个处于预约状态尚未提交的延迟工作项再次做`k_work_schedule/k_work_schedule_for_queue`不会改变其预约时间 + +修改预约 +-------- + +对一个处于预约中的延迟工作项使用下面方法可以更改其预约提交时间 + +**系统工作队列** + +.. code:: c + + /* 将系统工作队列delay_work的预约时间修改为3000ms */ + k_work_reschedule(&delay_work, K_MSEC(3000)); + +**用户工作队列** + +.. code:: c + + + /* 将用户工作队列user_work_q中delay_work的预约时间修改为3000ms */ + + k_work_reschedule_for_queue(&user_work_q, &delay_work, K_MSEC(3000)); + +取消延迟工作项 +-------------- + +取消分为三种状态处理: + +- 工作项处在预约等待状态,取消预约定时器 + +- 工作项处于列队等待状态,从列队中移除 + +- 工作项处于运行状态,标记取消,不会中止运行 + +取消延迟工作项分为异步和同步两种,异步取消只是通知取消不会等待真正的取消就会退出。例如如果取消一个正在运行的工作项,同步取消函数会等到运行完毕后才会返回。 + +**异步取消** + +.. code:: c + + k_work_cancel_delayable(&delay_work); + +**同步取消** + +.. code:: c + + k_work_cancel_delayable_sync(&delay_work); + +等待执行 +-------- + +执行等待执行时如果延迟工作项在预约状态将取消预约并立即提交到工作队列,并等到执行完成后才返回 + +.. code:: c + + struct k_work_sync sync; + + /* 延迟工作项被立即提交到工作队列,等待执行完后才返回 */ + k_work_flush_delayable(&delay_work, &sync); + +延迟工作项状态获取 +------------------ + +下列函数获取延迟工作项的状态 + +.. code:: c + + /* 返回0表示没有工作,非0表示延迟工作项在忙,非0是由工作项的四种状态组成 */ + int busy = k_work_delayable_busy_get(&delaywork); + + /* 返回false表示没有工作,返回true表示工作项在忙 */ + bool pending = k_work_delayable_is_pending(&delaywork); + + /* 返回延迟工作项在第几个绝对tick时被提交到工作队列 */ + k_ticks_t expires = k_work_delayable_expires_get(&delaywork); + + /* 返回延迟工作项还有多少个tick才被提交到工作队列中 */ + k_ticks_t remaining = k_work_delayable_remaining_get(&delaywork); + +用户空间工作队列 +================ + +用户空间工作队列是专用在用户空间的工作队列,其内存地址被保护和内核空间隔离,一般只用于zephyr应用程序中。用户空间工作队列的基本构成和工作队列一致:拥有一个队列和工作线程,但结构更为简单,只提供以下4个API,除提交工作项的处理外无法做其它额外的操作。用户空间的工作项也只拥有\ ``K_WORK_USER_STATE_PENDING``\ 和非pending两种状态,分别代表工作项等待运行和已经运行。 + +.. code:: c + + #define USER_SP_WQ_STACK_SIZE 4096 + #define USER_SP_WQ_PRIORITY 5 + + /* 定义用户空间工作队列线程的堆栈 */ + K_THREAD_STACK_DEFINE(user_wq_space_stack_area, USER_SP_WQ_STACK_SIZE ); + + /* 启动一个用户空间的工作队列 */ + struct k_work_user_q user_space_work_q; + k_work_user_queue_start(&user_space_work_q, + user_wq_space_stack_area, + USER_SP_WQ_STACK_SIZE , USER_SP_WQ_PRIORITY , + NULL); + + /* 初始化一个用户空间的工作项 */ + struct k_work_user user_space_work; + k_work_user_init(&user_space_work, work_user_handler); + + /* 发送工作项到用户空间工作队列 */ + k_work_user_submit_to_queue(&user_space_work_q, &user_space_work); + + /* 查询用户空间的工作项是否处于pending,还未执行处于pending,正在或已经执行完成为非pending */ + bool pending = k_work_user_is_pending(&user_space_work); + +其它 +==== + +工作队列中还有一种叫触发工作,其工作项和轮询内核对象绑定,这部分实现在轮询内核对象中,本文不做介绍。 + +参考 +==== + +https://docs.zephyrproject.org/latest/kernel/services/threads/workqueue.html + +.. |image0| image:: ../../images/develop/kernel/workq.png diff --git a/doc/source/develop/osserivces/formated_output_userapi_backend.rst b/doc/source/develop/osserivces/formated_output_userapi_backend.rst index dfb0793744f757a2a2d853c3882293607c964b47..5db4c1e004a3e3810c23a76e2125573f70365f42 100644 --- a/doc/source/develop/osserivces/formated_output_userapi_backend.rst +++ b/doc/source/develop/osserivces/formated_output_userapi_backend.rst @@ -1,6 +1,6 @@ .. _osservices_formated_output_userapi_backend: -Zephyr格式化输出-用户API和后端 +格式化输出-用户API和后端 ############################### diff --git a/doc/source/develop/subsys/index.rst b/doc/source/develop/subsys/index.rst index 8ec4b5463e06ba78bb0d1f00be203a922a4b4e51..b3443fcc0b390f2317ada270048e2e6e4e4beeff 100644 --- a/doc/source/develop/subsys/index.rst +++ b/doc/source/develop/subsys/index.rst @@ -13,3 +13,4 @@ Zephyr子系统使用方法和实现原理。 storage/nvs.rst storage/nvs_analyze.rst logging/logging_usage.rst + shell/shell_usage.rst diff --git a/doc/source/develop/subsys/shell/shell_usage.rst b/doc/source/develop/subsys/shell/shell_usage.rst new file mode 100644 index 0000000000000000000000000000000000000000..3eec977d92362ac6de1fd195fa88037a7e717fd7 --- /dev/null +++ b/doc/source/develop/subsys/shell/shell_usage.rst @@ -0,0 +1,631 @@ +.. _shell_shell_usage: + +Zephyr Shell系统使用指南 +######################### + +Zephyr的shell系统提供一个类Unix shell界面,用户可以自定义shell命令和处理函数,通过shell界面调用这些处理函数。shell系统可以作为软件系统普通功能的人机界面,也可以通过shell系统埋入调试命令,方便进行软件的调试和分析。 + +Zephyr的shell系统提供以下功能: + +- 支持多实例:多个后端可以开多个shell。 + +- 与log系统合作共存。 + +- 支持静态和动态命令。 + +- 支持字典命令。 + +- 支持自动补全。 + +- 内建命令。 + +- 查看历史命令。 + +- 支持在线编辑修改命令,支持多行命令。 + +- 支持ANSI转义码。 + +- 支持通配符。 + +- 支持meta key。 + +- 支持`getopt`和`getopt_long`。 + +- 可配置剪裁。 + + +Shell系统中一些功能对内存和闪存有较大需求,可以通过禁用不需要的功能减少对内存和闪存的使用。配置`CONFIG_SHELL_MINIMAL=y`可以极大的降低存储的使用,在此基础上有选择地只启用想要的功能,以达到存储的最优使用。 + +shell命令添加方法 +================== + +实现命令处理函数 +------------------- + +shell命令处理函数的参数形式是固定的 + +.. code:: c + + static int cmd_handler(const struct shell *shell, size_t argc, char **argv) + +- **shell** – **[in]** shell的实例后端。 + +- **argc** – **[in]** 参数的个数,命令会被计入参数个数中。 + +- **argv** – **[in]** 参数字符串指针,所有参数都会以字符串的形式送到命令函数. + +例如执行shell命令\ *shell_sample 1 2 3*\ ,命令函数得到的参数如下: + +:: + + argc=4 + argv[0]="sample" + argv[1]="1" + argv[2]="2" + argv[3]="3" + +实例:这里实现三个命令函数 + +.. code:: c + + static int cmd_info_board(const struct shell *sh, size_t argc, char **argv) + { + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + shell_print(sh, CONFIG_BOARD); + + return 0; + } + + static int cmd_info_version(const struct shell *sh, size_t argc, char **argv) + { + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + shell_print(sh, "Zephyr version %s", KERNEL_VERSION_STRING); + + return 0; + } + + + static int cmd_shell_help(const struct shell *sh, size_t argc, char **argv) + { + shell_print(sh, "show help: %d", argc); + if(argc == 1){ + shell_help(sh); + return SHELL_CMD_HELP_PRINTED; + } + + for(size_t i=0; i< argc; i++){ + shell_print(sh, "check arg %d: %s", i, argv[i]); + } + + return 0; + } + +注册命令 +---------- + +使用下面三个宏注册静态的shell命令,一旦注册后无法在运行时修改注册的shell命令 +**创建命令** + +.. code:: c + + SHELL_CMD(_syntax, _subcmd, _help, _handler) + +- \**_syntax\ ** – **\ [in]** 命令符号。 + +- \**_subcmd\ ** – **\ [in]** 指向子命令,为空表示没有子命令。子命令可以再嵌入子命令。 + +- \**_help\ ** – **\ [in]** 命令帮助信息。 + +- \**_handler\ ** – **\ [in]** 命令函数,,为空表示没有命令函数。 + +**创建子命令集** + +.. code:: c + + SHELL_STATIC_SUBCMD_SET_CREATE(name, ...) + +- **name** – **[in]** 子命令集名. + +- **…** – **[in]** 由多个\ ``SHELL_CMD``\ 或\ ``SHELL_ARG_CMD``\ 组成的,并由\ ``SHELL_SUBCMD_SET_END``\ 结束。 + +**注册命令** + +.. code:: c + + SHELL_CMD_REGISTER(syntax, subcmd, help, handler) + +- **syntax** – **[in]** 命令符号 + +- **subcmd** – **[in]** 指向子命令,为空表示没有子命令。 + +- **help** – **[in]** 命令帮助信息。 + +- **handler** – **[in]** 命令函数,,为空表示没有命令函数。 + +实例:这里使用上面三个宏注册命令 + +.. code:: c + + /* SHELL_CMD 注册两个子命令, board和version,执行时会调用cmd_info_board和cmd_info_version函数 + SHELL_STATIC_SUBCMD_SET_CREATE 将子命令组装成子命令集subinfo + SHELL_SUBCMD_SET_END表示子命令集的结束 + */ + SHELL_STATIC_SUBCMD_SET_CREATE(subinfo, + SHELL_CMD(board, NULL, "Show board command.", cmd_info_board), + SHELL_CMD(version, NULL, "Show info command.", cmd_info_version), + SHELL_SUBCMD_SET_END /* Array terminated. */ + ); + + /* 注册一个根命令shell_sample,执行根命令shell_sample时会调用cmd_shell_help + shell_sample的子命令集为 + */ + SHELL_CMD_REGISTER(shell_sample, &subinfo, "Sample commands", cmd_shell_help); + +执行命令 +-------- + +上面实例中注册了根命令 *shell_sample* +和其子命令集subinfo,其命令为树机构 + +:: + + shell_sample + ├── board + └── version + +在shell中可以执行的命令如下: 执行\ *shell_sample* +会调用\ ``cmd_shell_help``\ 显示出帮助信息和参数个数 执行\ *shell_sample +board* 会调用\ ``cmd_info_board``\ 显示出开发板的字符串 +执行\ *shell_sample version* +会调用\ ``cmd_info_version``\ 显示zephyr的版本 + +命令参数 +--------- + +使用\ ``SHELL_CMD_REGISTER``\ 和\ ``SHELL_CMD``\ 注册的命令,shell系统并不会为其检查参数个数。zephyr提供了另外两个宏注册命令,在注册时可以指定参数个数。在执行shell命令时shell系统会根据指定的参数个数进行检查,如果不匹配将不执行命令函数,并进行错误提示。 +**创建带参数命令** + +.. code:: c + + SHELL_CMD_ARG(syntax, subcmd, help, handler, mand, opt) + +- **syntax** – **[in]** 命令符号。 + +- **subcmd** – **[in]** 指向子命令,为空表示没有子命令。子命令可以再嵌入子命令。 + +- **help** – **[in]** Pointer to a command help string. + +- **handler** – **[in]** 命令帮助信息。 + +- **mand** – **[in]** 必选参数个数,参数个数包含命令本身。 + +- **opt** – **[in]** 可选参数个数。 + +.. code:: c + + SHELL_CMD_ARG_REGISTER(syntax, subcmd, help, handler, mandatory, optional) + +- **syntax** – **[in]** 命令符号 + +- **subcmd** – **[in]** 指向子命令,为空表示没有子命令。 + +- **help** – **[in]** 命令帮助信息。 + +- **handler** – **[in]** 命令函数,,为空表示没有命令函数。 + +- **mandatory** – **[in]** 必选参数个数,参数个数包含命令本身。 + +- **optional** – **[in]** 可选参数个数。 + +注意无论由那种方式定义的shell命令,shell系统都会创建\ ``argc``\ 和\ ``argv``\ 并交由注册的命令函数处理。但由\ ``SHELL_CMD_ARG``\ 定义的命令会指定必选参数数量\ ``mandatory``\ 和可选参数数量\ ``optional``\ ,实际通过shell命令输入的参数数量(包含命令本身)要满足: + +:: + + mandatory <= argc <= mandatory + optional + +当不满足时,shell系统会检查参数数量出来并做如下提示 + +.. code:: c + + shell_sample_args: wrong parameter count + +当\ ``mandatory``\ 和\ ``optional``\ 均被设置为0时,Shell系统不会对参数数量进行检查。 +以下实例 + +.. code:: c + + SHELL_CMD_ARG_REGISTER(shell_sample_args, NULL, "Sample arg commands with handle", cmd_shell_help, 3, 4); + +表示\ *shell_sample_args*\ 必须有3个参数(包含shell_sample_args),但总计不能超过7个,例如 +*shell_sample_args 0* 非法 *shell_sample_args 0 1* 合法 +*shell_sample_args 0 1 2 3* 合法 *shell_sample_args 0 1 2 3 4 5 6* 非法 + + +动态命令 +========== + + +动态命令使用方法 +---------------- + +动态子命令由\ ``SHELL_DYNAMIC_CMD_CREATE``\ 生成 + +.. code:: c + + SHELL_DYNAMIC_CMD_CREATE(name, get) + +- **name** – **[in]** 动态命令入口. + +- **get** – **[in]** 一个\ ``typedef void (*shell_dynamic_get)(size_t idx, struct shell_static_entry *entry)``\ 类型的函数,根据\ ``idx``\ 返回不同的\ ``struct shell_static_entry``\ 类型的命令入口参数。 + + ``struct shell_static_entry``\ 和\ ``SHELL_CMD_ARG``\ 指定的静态命令格式一样,指定命令符号,帮助信息,子命令入口,命令函数,和参数个数 + + .. code:: c + + struct shell_static_args { + uint8_t mandatory; /*!< 必选参数个数. */ + uint8_t optional; /*!< 可选参数个数. */ + }; + + .. code:: c + + struct shell_static_entry { + const char *syntax; /*!< 命令符号字符串. */ + const char *help; /*!< 帮助信息字符串. */ + const struct shell_cmd_entry *subcmd; /*!< 子命令入口. */ + shell_cmd_handler handler; /*!< 命令函数. */ + struct shell_static_args args; /*!< 命令参数个数. */ + }; + + 从上面看到get函数返回的shell命令入口和静态命令对应的参数一模一样, + + +动态命令示例 +------------ + +如下代码片段演示了如何添加一个动态子命令: + +1. 准备一个\ ``typedef void (*shell_dynamic_get)(size_t idx, struct shell_static_entry *entry)``\ 类型的动态命令获取函数: + + .. code:: c + + static void dynamic_cmd_get(size_t idx, struct shell_static_entry *entry) + { + if(idxsyntax = cmd_name_flag? change_name[idx]:dynamic_entrys[idx].syntax; + entry->handler = dynamic_entrys[idx].handler; + entry->subcmd = dynamic_entrys[idx].subcmd; + entry->help = dynamic_entrys[idx].help; + entry->args.mandatory = dynamic_entrys[idx].args.mandatory; + entry->args.optional = dynamic_entrys[idx].args.optional; + }else{ + entry->syntax = NULL; + } + } + +2. 生成为动态子命令\ ``dynamic_set`` + + .. code:: c + + SHELL_DYNAMIC_CMD_CREATE(dynamic_set, dynamic_cmd_get); + +3. 将\ ``dynamic_set``\ 注册到根命令中 + + .. code:: c + + SHELL_CMD_REGISTER(shell_dynamic, &dynamic_set, + "Sample dynamic command usage.", NULL); + +根命令\ *shell_dynamic*\ 的子命令集为\ ``dynamic_set``\ ,子命令集中有哪些子命令是由\ ``dynamic_cmd_get``\ 的输出决定。shell系统在遍历根命令\ *shell_dynamic*\ 的子命令时,传入参数\ ``idx``\ 从0开始每次加一的循环调用\ ``dynamic_cmd_get``\ ,直到输出的\ ``entry->syntax``\ 为空。在上面的示例代码中动态子命令的个数由\ ``dynamic_cmd_num``\ 决定,默认情况下就是\ ``dynamic_entrys[]``\ 中的命令入口数量: + +.. code:: c + + /* 动态命令数组 */ + static struct shell_static_entry dynamic_entrys[]= + { + /* 改变动态命令总数 */ + { + .syntax = "total", + .handler = cmd_dynamic_total, + .subcmd = NULL, + .help = "Set total cmd number, must more than 1.", + .args.mandatory = 0, + .args.optional = 0, + }, + + /* 使用默认子命令名 */ + { + .syntax = "org_name", + .handler = cmd_dynamic_org_name, + .subcmd = NULL, + .help = "Change to org cmd name.", + .args.mandatory = 0, + .args.optional = 0, + }, + + /* 使用新的子命令名 */ + { + .syntax = "new_name", + .handler = cmd_dynamic_new_name, + .subcmd = NULL, + .help = "Change to new cmd name.", + .args.mandatory = 0, + .args.optional = 0, + }, + + /* 动态子命令的子命令演示 */ + { + .syntax = "subcmd", + .handler = NULL, + .subcmd = &shell_sample, + .help = "Show dynamic sub cmd.", + .args.mandatory = 0, + .args.optional = 0, + }, + { + .syntax = "cmd1", + .handler = NULL, + .subcmd = NULL, + .help = "Show dynamic command cmd1.", + .args.mandatory = 0, + .args.optional = 0, + }, + { + .syntax = "cmd2", + .handler = NULL, + .subcmd = NULL, + .help = "Show dynamic command cmd2.", + .args.mandatory = 0, + .args.optional = 0, + }, + { + .syntax = NULL, + } + }; + + static uint32_t dynamic_cmd_num = sizeof(dynamic_entrys)/sizeof(struct shell_static_entry); + +开机后在shell中输入\ ``shell_dynamic``\ 后按tab后可以看到所有的子命令 + +.. code:: c + + uart:~$ shell_dynamic + total org_name new_name subcmd cmd1 cmd2 + +可以在运行时改变\ ``dynamic_cmd_num``\ 的大小达到改变\ *shell_dynamic*\ 的子命令数量,例如\ *total*\ 子命令就可以达到这一目的,它对应的命令函数如下: + +.. code:: c + + static int cmd_dynamic_total(const struct shell *sh, size_t argc, char **argv) + { + if(argc < 2){ + shell_help(sh); + return SHELL_CMD_HELP_PRINTED; + } + + uint32_t num = (uint32_t)atoi(argv[1]); + if(num<1 && num > sizeof(dynamic_entrys)/sizeof(struct shell_static_entry)){ + shell_error(sh, "total set fail, must in 1~%d", sizeof(dynamic_entrys)/sizeof(struct shell_static_entry)); + return -ENOEXEC; + } + + /* 改变动态命令的数量 */ + shell_print(sh, "set total cmd num %d", num); + dynamic_cmd_num = num; + + return 0; + } + +如果执行shell命令\ *shell_dynamic total +1*\ ,\ ``cmd_dynamic_total``\ 被调用将\ ``dynamic_cmd_num``\ 修改为3,此时在shell中输入\ *shell_dynamic*\ 后按tab后就只能看到只剩3个子命令: + +:: + + uart:~$ shell_dynamic + total org_name new_name + +也可以在运行时通过改变\ ``cmd_name_flag``\ 变量的值,让动态子命令的\ ``syntax``\ 发生改变,达到动态改变动态子命令符号的目的。例如执行\ *shell_dynamic +new_name*\ 后\ ``cmd_name_flag``\ 被修改为1,\ ``dynamic_cmd_get``\ 输出的\ ``syntax``\ 将使用\ ``change_name``\ 的内容 + +.. code:: c + + char * change_name[] = { + "total_new", + "org_name_new", + "new_name_new", + "cmd1_new", + "cmd2_new", + "cmd3_new", + }; + +此时在shell中输入\ *shell_dynamic*\ 后按tab后,看到的就是新的子命令名称 + +:: + + uart:~$ shell_dynamic + total_new org_name_new new_name_new + +当然也可以修改\ ``dynamic_entrys``\ 的内容达到添加和删除动态子命令的目的,或者是修改其中的子命令函数达到修改子命令功能的目的。 + + + +内置命令 +======== + +shell系统自带内置命令,自带命令可以通过配置项进行配置是否启用来优化shell的空间占用,默认情况下\ ``CONFIG_SHELL_CMDS=y``\ 开启了部分内置命令,将其设置为\ ``n``\ 可以关闭内置命令。 + +Shell的内置命令列表: + +- **clear** :清屏 + +- **help**\ :显示shell所有根命令及帮助信息 + +- **history**\ :显示最近执行了的命令 + +- **resize**\ :改变终端尺寸。当执行较长命令后,为保证多行显示和←, →, End, Home正常,需要执行该命令重设终端尺寸,目前只有UART后端支持该命令。 + + - *resize* 命令默认是开启的\ ``CONFIG_SHELL_CMDS_RESIZE=y`` + + - 默认情况下执行\ *resize*\ 后终端的尺寸为80x24,可以通过\ ``CONFIG_SHELL_DEFAULT_TERMINAL_WIDTH=80``\ 和\ ``CONFIG_SHELL_DEFAULT_TERMINAL_HEIGHT=24``\ 配置改变 + + - *resize*\ 有一个\ *default*\ 子命令,无论默认配置为多少执行\ *resize + default*\ 后就会把终端大小设置为80X24。 + +- **select**\ :设置根,通过alt+r可以退回到主根。 + + 例如主根下按tab可以看到所有的根命令: + + :: + + uart:~$ + clear device devmem + help history kernel + log logging nrf_clock_control + resize select shell + shell_dict shell_dynamic shell_sample + shell_sample_args shell_sample_handler shell_sample_null + shell_sample_sub + + 当执行\ *select + shell_sample*\ 后,在主根下按tab就只能看到\ *shell_sample*\ 的子命令: + + :: + + uart:~$ + info subinfo arginfo + + 再按看到下面提示表示退回到默认的主根 + + :: + + Restored default root commands + + *select*\ 命令默认是关闭的,需要配置\ ``CONFIG_SHELL_CMDS_SELECT=y``\ 开启 + +        + +- **shell**\ :用于设置shell终端的属性,有如下子命令 + + - *shell backspace_mode + backspace*\ :设置Backspace按键为backspace模式,按该按键后不会删除已输入的内容 + + - *shell backspace_mode + delete*\ :设置Backspace按键为delete模式,按该按键删除已输入的内容 + + - *shell color off*\ :关闭shell终端颜色 + + - *shell color on*\ :开启shell终端颜色 + + - *shell echo + off*\ :关闭shell回显,输入的命令不会被回显。需要依赖使用的终端软件支持回显。在终端软件必须回显的情况下可以用该命令关闭shell系统的回显。 + + - *shell echo on*\ :开启shell回显,输入的命令被回显。 + + - *shell stats reset*\ :清除log系统丢消息的统计信息。 + + - *shell stats show*\ :显示log系统丢的消息。 + + + + +命令行特性 +========== + + +Zephyr的shell系统提供一个类Unix shell界面,通过该命令行界面用户可以操作Zephyr或者用户自己定义的shell命令。Zephyr的shell系统提供了一系列命令行特性方便操作shell命令。 + +文本编辑按键支持 +-------------------- + +左右移动光标:←, → 删除光标所在字符:Backspace, Delete +移动光标到行尾/首:End, Home 切换插入/覆盖模式:Insert + +自动补全 +---------- + +默认\ ``CONFIG_SHELL_TAB=y``\ 开启了tab支持 + +Tab按键支持以下特性: + +- 提示有效命令 + + 当按下tab时,会自动提示出所有有效的命令。例如敲入\ *she*\ 后按下tab会将以\ *she*\ 开头的命令都提示出来 + + :: + + uart:~$ she + shell shell_dict shell_dynamic + shell_sample shell_sample_args shell_sample_handler + shell_sample_null shell_sample_sub + +- 自动补全 + + 默认\ ``CONFIG_SHELL_TAB_AUTOCOMPLETION=y``\ 开启了自动补全,当提示命令只有一条时就会自动补全被执行。例如敲入\ *shell_sample_s* + 后按下tab会将命令自动补全到输入位置 + + :: + + uart:~$ shell_sample_sub + +历史命令 +---------- + +默认\ ``CONFIG_SHELL_HISTORY=y``\ 开启了命令历史记录。通过执行 *history* +命令可以查看历史执行过的命令。以通过↑ 和↓ 按键切换选择已经执行过的命令。当启用meta按键后也可以通过 Ctrl + n 和 Ctrl + p来切换选择。 + +通配符 +---------- + +默认\ ``CONFIG_SHELL_WILDCARD=y``\ 开启了通配符支持,shell支持两个通配符: + +- \* :匹配字符串 + +- ? :匹配单个字符 + +MetaKey +---------- + +默认情况下\ ``CONFIG_SHELL_METAKEYS=y``\ 开启了metakey的支持。shell支持的metakey和作用如下表 + ++-----------+---------------------------------------------------------+ +| Meta keys | Action | ++===========+=========================================================+ +| Ctrl + a | 移动光标到行首,等同于Home | ++-----------+---------------------------------------------------------+ +| Ctrl + b | 将光标向左移动一个字符,等同于← | ++-----------+---------------------------------------------------------+ +| Ctrl + c | 放弃当前行输入的内容,另外新开 | +| | 一行用于输入命令。类似于回Enter但不执行已经输入了的命令 | ++-----------+---------------------------------------------------------+ +| Ctrl + d | 删除光标下的字符,等同于Delete | ++-----------+---------------------------------------------------------+ +| Ctrl + e | 移动光标到行尾,等同于End | ++-----------+---------------------------------------------------------+ +| Ctrl + f | 将光标向右移动一个字符,等同于→ | ++-----------+---------------------------------------------------------+ +| Ctrl + k | 删除从光标到行尾的所有字符 | ++-----------+---------------------------------------------------------+ +| Ctrl + l | 保留当前正在输入的命令,清除屏幕其它的内容。 | ++-----------+---------------------------------------------------------+ +| Ctrl + n | 切换到上一个历史执行的命令 | ++-----------+---------------------------------------------------------+ +| Ctrl + p | 切换到下一个历史执行的命令 | ++-----------+---------------------------------------------------------+ +| Ctrl + u | 清除当前正在输入的命令 | ++-----------+---------------------------------------------------------+ +| Ctrl + w | 删除光标侧的一个单词 | ++-----------+---------------------------------------------------------+ +| Alt + b | 移动光标到前一个词 | ++-----------+---------------------------------------------------------+ +| Alt + f | 移动光标到后一个词 | ++-----------+---------------------------------------------------------+ + + + +参考 +===== +https://docs.zephyrproject.org/latest/services/shell/index.html \ No newline at end of file diff --git a/doc/source/develop/windows.rst b/doc/source/develop/windows.rst index e906d3e3615aabed7506df6a2f3393594fe1245b..58fa63ecc0e46f3ecee0deeb7aa1bc7b8cd0e478 100644 --- a/doc/source/develop/windows.rst +++ b/doc/source/develop/windows.rst @@ -20,7 +20,7 @@ Zephyr的代码可以分为Zephyr OS和Zephyr modules两大部分,都托管在 * 只包含Zephyr OS最新发行版和lts发行版,可以访问 `镜像 `_ * 包含了Zephyr OS和Zephyr modules,可以访问 `网盘 `_ 提取码: **zeph** - * 如果需要同步Zephyr OS的git仓库,可以先通过 `gitee镜像 `_ 同步,然后再同步到github上 + * 如果需要同步Zephyr OS的git仓库,可以先通过 `gitee zephyr镜像 `_ 同步,然后再同步到github上 的仓库 * Zephyr modules数量较多,但只有部分git仓库在 `gitee镜像 `_ 上存在,缺失的部分需要通过其方式 获得。在实际开发过程中,很少会使用到所有Zephyr modules, 因此可以根据需要,只准备相应的modules。 diff --git a/doc/source/images/develop/kernel/datapassing.png b/doc/source/images/develop/kernel/datapassing.png new file mode 100644 index 0000000000000000000000000000000000000000..18398d5bf6080f5232db34d1a1f7e98687e98df6 Binary files /dev/null and b/doc/source/images/develop/kernel/datapassing.png differ diff --git a/doc/source/images/develop/kernel/limit.png b/doc/source/images/develop/kernel/limit.png new file mode 100644 index 0000000000000000000000000000000000000000..fa5d833b128c41a86e87890a6d1985cc6cea1483 Binary files /dev/null and b/doc/source/images/develop/kernel/limit.png differ diff --git a/doc/source/images/develop/kernel/msgq.png b/doc/source/images/develop/kernel/msgq.png new file mode 100644 index 0000000000000000000000000000000000000000..c9299a003d59a8ef9977d116255f63d68ca13cf3 Binary files /dev/null and b/doc/source/images/develop/kernel/msgq.png differ diff --git a/doc/source/images/develop/kernel/pipe-b.png b/doc/source/images/develop/kernel/pipe-b.png new file mode 100644 index 0000000000000000000000000000000000000000..0e44baaef56b3d8ac1299c62ea0221d430e717a1 Binary files /dev/null and b/doc/source/images/develop/kernel/pipe-b.png differ diff --git a/doc/source/images/develop/kernel/poll.png b/doc/source/images/develop/kernel/poll.png new file mode 100644 index 0000000000000000000000000000000000000000..764e51aa880feca6400ddfbbc2a6aad54f9a7dd5 Binary files /dev/null and b/doc/source/images/develop/kernel/poll.png differ diff --git a/doc/source/images/develop/kernel/poll_list.png b/doc/source/images/develop/kernel/poll_list.png new file mode 100644 index 0000000000000000000000000000000000000000..2e6a2a7211eb1322fd7354c96e4d2bff134d78bc Binary files /dev/null and b/doc/source/images/develop/kernel/poll_list.png differ diff --git a/doc/source/images/develop/kernel/queue.png b/doc/source/images/develop/kernel/queue.png new file mode 100644 index 0000000000000000000000000000000000000000..ce3f1fe8e4fc519c555532b406f326d2524a0e36 Binary files /dev/null and b/doc/source/images/develop/kernel/queue.png differ diff --git a/doc/source/images/develop/kernel/stack.png b/doc/source/images/develop/kernel/stack.png new file mode 100644 index 0000000000000000000000000000000000000000..1af5a82bdd6161298a53270a2a3d0a6023811ebb Binary files /dev/null and b/doc/source/images/develop/kernel/stack.png differ diff --git a/doc/source/images/develop/kernel/workq.png b/doc/source/images/develop/kernel/workq.png new file mode 100644 index 0000000000000000000000000000000000000000..49c2fe42163b779808e0b4fc47187f075d454701 Binary files /dev/null and b/doc/source/images/develop/kernel/workq.png differ