为什么要进行资源管理

在freertos中, 高优先级的任务会抢占低优先级的任务, 中断又会抢占高优先级的任务

试想以下这个情景:

任务A正在访问变量X

这时任务B由于更高的优先级, 抢占了任务A, 同时也访问变量X(例如做了一个++操作)

任务B结束, 任务A对变量X做一个*10操作

这时问题就出现了, 往往我们使任务A进入就绪态目标是为了使现在的X变为原来的10倍, 可现在X变为了原来的10倍还要多10

这明显和我们的预期不符

那么有什么办法可以避免这种情况呢?

第一种方法是使用临界区

通过taskENTER_CRITICAL()taskEXIT_CRITICAL()划定临界区

执行里面的代码使任何其他任务和优先级相同或更低的中断都会失效

临界区所屏蔽的优先级在FreeRTOSConfig.hconfigMAX_SYSCALL_INTERRUPT_PRIORITY设置

该操作是可以嵌套的, 由内核记录嵌套的深度, 深度归0时退出临界区

用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void vPrintString( const char *pcString )
{
/* Write the string to stdout, using a critical section as a crude method of
mutual exclusion. */
taskENTER_CRITICAL();
{
printf( "%s", pcString );
fflush( stdout );
}
taskEXIT_CRITICAL();
}

void vAnInterruptServiceRoutine( void ) //中断安全版本
{
/* Declare a variable in which the return value from
taskENTER_CRITICAL_FROM_ISR() will be saved. */
UBaseType_t uxSavedInterruptStatus;
/* This part of the ISR can be interrupted by any higher priority
interrupt. */
/* Use taskENTER_CRITICAL_FROM_ISR() to protect a region of this ISR.
Save the value returned from taskENTER_CRITICAL_FROM_ISR() so it can
be passed into the matching call to taskEXIT_CRITICAL_FROM_ISR(). */
uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();
/* This part of the ISR is between the call to
taskENTER_CRITICAL_FROM_ISR() and taskEXIT_CRITICAL_FROM_ISR(), so can
only be interrupted by interrupts that have a priority above that set
by the configMAX_SYSCALL_INTERRUPT_PRIORITY constant. */
/* Exit the critical section again by calling taskEXIT_CRITICAL_FROM_ISR(),
passing in the value returned by the matching call to
taskENTER_CRITICAL_FROM_ISR(). */
taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus );
/* This part of the ISR can be interrupted by any higher priority
interrupt. */
}

但是缺点也很明显, 临界区里的代码屏蔽了所有优先级相同或更低的中断

可能导致必要的中断无法执行

所以要求临界区要尽量的短, 上面这个例子明显不符合这个要求

第二种方法是通过暂停调度器

通过 vTaskSuspendAll()xTaskResumeAll()

执行完vTaskSuspendAll()会暂停调度器的执行, 但是中断不受影响

调度器暂停了就不会有更高优先级的任务来抢占了

这个操作是可以嵌套的

内核会记录嵌套深度, 只有嵌套深度归0时, 才会恢复调度器

缺点就是终端仍有可能带来干扰, 同时恢复调度器通常是一个较为耗费时间的过程

示例:

1
2
3
4
5
6
7
8
9
10
11
void vPrintString( const char *pcString )
{
/* Write the string to stdout, suspending the scheduler as a method of
mutual exclusion. */
vTaskSuspendScheduler();
{
printf( "%s", pcString );
fflush( stdout );
}
xTaskResumeScheduler();
}

第三种方法则是使用互斥量

通过将FreeRTOSConfig.hconfigUSE_MUTEXES设置为1以启用互斥锁

互斥量可以看作是一种特殊的信号量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void prvNewPrintString( const char *pcString )
{
/* The mutex is created before the scheduler is started, so already exists
by the time this task executes.
Attempt to take the mutex, blocking indefinitely to wait for the mutex
if it is not available straight away. The call to xSemaphoreTake() will
only return when the mutex has been successfully obtained, so there is
no need to check the function return value. If any other delay period
was used then the code must check that xSemaphoreTake() returns pdTRUE
before accessing the shared resource (which in this case is standard
out). As noted earlier in this book, indefinite time outs are not
recommended for production code. */
xSemaphoreTake( xMutex, portMAX_DELAY );
{
/* The following line will only execute once the mutex has been
successfully obtained. Standard out can be accessed freely now as
only one task can have the mutex at any one time. */
printf( "%s", pcString );
fflush( stdout );
/* The mutex MUST be given back! */
}
xSemaphoreGive( xMutex );
}

不同的点在于其具有所有权和优先级继承机制

优先级继承机制可以用于解决优先级反转问题

那么什么是优先级反转呢?

先从互斥量机制说起, 两个任务在访问同一资源时, 需要先take互斥量

此时另一个任务再take该信号量时就会因为无法得到互斥量陷入阻塞状态

待互斥量被give后另一个任务又重新进入就绪态

13

试想以下情景, 现在有三个任务, LP最低优先级任务 MP中等优先级任务 HP最高优先级任务

任务LP和HP使用互斥量来控制对同一资源的使用

现在LP先进入运行态, take互斥量

紧跟着HP被触发进入就绪态, 因为高优先级原因抢占LP

HP在尝试take互斥量的时候, 因为此时互斥量在LP任务手里, 于是进入阻塞态

回到LP任务, 此时MP任务进入就绪态, 抢占LP任务

知道MP任务运行完毕回到LP任务, LP任务运行完毕才轮到HP任务

问题就出现在这里: HP任务明明是最高优先级的任务, 却到了最后才执行

15

为了缓解这种问题(无法被根除), 互斥量引入了优先级继承机制

优先级继承的工作原理是: 暂时将互斥锁的持有者的优先级提升至尝试获取同一互斥锁的最高优先级任务的优先级

持有互斥锁的低优先级任务继承了该优先级

在这种情况下上述情景将会变为:

LP任务先进入运行态

HP任务进入就绪态, 抢占LP

HP任务获取互斥锁失败, LP任务的优先级提升至HP优先级同一水平

MP任务进入就绪态, 优先级低于LP 故无法进行抢占

LP任务执行完毕, HP任务进入运行态

HP任务执行完毕, MP任务进入运行态

16

当有多个任务使用同一互斥量的时候, 继承优先级的规则是如何呢?

  • 如果任务在未释放其已持有的互斥锁的情况下获取了新的互斥锁, 则该任务的继承优先级可能进一步提升
  • 任务会保持其继承的最高优先级, 直到其释放所有的互斥锁, 且与互斥锁释放的顺序无关
  • 当一个任务同时持有多个互斥量时,只要其中任意一个互斥量曾导致它继承过更高的优先级,那么该任务会一直保持“最高的继承优先级”,即使等待这些互斥量的其他任务后来超时放弃等待,也不会立即恢复原优先级。

这意味着, 继承的任务总是保持在一个较高的优先级, 很好的缓解了优先级反转的问题

互斥锁的另一个缺陷是死锁(Deadlock, Deadly Embarce)

形成原因是由于两个任务都在等待对方归还互斥量, 导致两个任务都进入阻塞态

试想以下场景:

有任务A, 任务B, 任务A先take互斥量X, 再take互斥量Y 任务B先take互斥量Y再take互斥量X

任务A先进入运行态, take互斥量X

一个时间片过去, 任务B进入运行态, take互斥量Y

又一个时间片过去, 任务A take 互斥量Y失败, 进入阻塞态

任务B 进入运行态, take互斥量X, 进入阻塞态

这时两个任务均在等待对方归还互斥量

但我们知道, 如果take函数的等待时间均为无限等待的话, 这两个任务将永远无法执行

同样, 死锁也是无法完全消除的, 只能在设计时尽量避免

最简单的避免方法就是给take函数设置一个比预期等待时间稍长的超时时间, 让其take失败继续执行剩下的部分

对于单个任务也有可能发生死锁

情景如下:

任务A获取互斥锁X

之后任务A再获取互斥锁X, 此时X已经被占用了, 无法获取, 任务A进入阻塞态

形成死锁

解决方法是使用递归互斥锁(Recursive Mutexes)

使用方法与标准互斥锁类似, 不过递归互斥锁和它名称一样支持递归调用

take多少次就需要give多少次才能解除互斥锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/* Recursive mutexes are variables of type SemaphoreHandle_t. */
SemaphoreHandle_t xRecursiveMutex;
/* The implementation of a task that creates and uses a recursive mutex. */
void vTaskFunction( void *pvParameters )
{
const TickType_t xMaxBlock20ms = pdMS_TO_TICKS( 20 );
/* Before a recursive mutex is used it must be explicitly created. */
xRecursiveMutex = xSemaphoreCreateRecursiveMutex();
/* Check the semaphore was created successfully. configASSERT() is
described in section 11.2. */
configASSERT( xRecursiveMutex );
/* As per most tasks, this task is implemented as an infinite loop. */
for( ;; )
{
/* ... */
/* Take the recursive mutex. */
if( xSemaphoreTakeRecursive( xRecursiveMutex, xMaxBlock20ms ) == pdPASS )
{
/* The recursive mutex was successfully obtained. The task can now
access the resource the mutex is protecting. At this point the
recursive call count (which is the number of nested calls to
xSemaphoreTakeRecursive()) is 1, as the recursive mutex has
only been taken once. */
/* While it already holds the recursive mutex, the task takes the
mutex again. In a real application, this is only likely to occur
inside a sub-function called by this task, as there is no
practical reason to knowingly take the same mutex more than
once. The calling task is already the mutex holder, so the
second call to xSemaphoreTakeRecursive() does nothing more than
increment the recursive call count to 2. */
xSemaphoreTakeRecursive( xRecursiveMutex, xMaxBlock20ms );
/* ... */
/* The task returns the mutex after it has finished accessing the
resource the mutex is protecting. At this point the recursive
call count is 2, so the first call to xSemaphoreGiveRecursive()
does not return the mutex. Instead, it simply decrements the
recursive call count back to 1. */
xSemaphoreGiveRecursive( xRecursiveMutex );
/* The next call to xSemaphoreGiveRecursive() decrements the
recursive call count to 0, so this time the recursive mutex is
returned. */
xSemaphoreGiveRecursive( xRecursiveMutex );
/* Now one call to xSemaphoreGiveRecursive() has been executed for
every proceeding call to xSemaphoreTakeRecursive(), so the task
is no longer the mutex holder. */
}
}
}

相同优先级任务使用互斥锁 处理时间不均问题

情景:

任务A,B两个相同优先级任务, 两个任务都来自同一个模板, 该模板如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* The implementation of a task that uses a mutex in a tight loop. The task
creates a text string in a local buffer, then writes the string to a display.
Access to the display is protected by a mutex. */
void vATask( void *pvParameter )
{
extern SemaphoreHandle_t xMutex;
char cTextBuffer[ 128 ];
for( ;; )
{
/* Generate the text string – this is a fast operation. */
vGenerateTextInALocalBuffer( cTextBuffer ); //快操作
/* Obtain the mutex that is protecting access to the display. */
xSemaphoreTake( xMutex, portMAX_DELAY );
/* Write the generated text to the display–this is a slow operation. */
vCopyTextToFrameBuffer( cTextBuffer ); //慢操作
/* The text has been written to the display, so return the mutex. */
xSemaphoreGive( xMutex );
}
}

我们使用快操作和慢操作区分该模板中的两个惭怍

任务A先执行快操作, 之后take互斥量

一个时间片过去, 切换到任务B

任务B执行完快操作, take互斥量失败, 进入阻塞态, 在时间片结束之前切换到任务A

任务A执行完慢操作且give互斥量, 任务B回到就绪态

由于优先级相同, 任务B无法抢占任务A

任务A继续执行完快操作然后take互斥量, 时间片结束切换到任务B

任务B take互斥量失败, 又回到阻塞态

14

任务A的占用时间远大于任务B, 这是不合理的

解决方法是在合适的时机使用taskYIELD()主动切换上下文

手册给出的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void vFunction( void *pvParameter )
{
extern SemaphoreHandle_t xMutex;
char cTextBuffer[ 128 ];
TickType_t xTimeAtWhichMutexWasTaken;
for( ;; )
{
/* Generate the text string – this is a fast operation. */
vGenerateTextInALocalBuffer( cTextBuffer );
/* Obtain the mutex that is protecting access to the display. */
xSemaphoreTake( xMutex, portMAX_DELAY );
/* Record the time at which the mutex was taken. */
xTimeAtWhichMutexWasTaken = xTaskGetTickCount();
/* Write the generated text to the display–this is a slow operation. */
vCopyTextToFrameBuffer( cTextBuffer );
/* The text has been written to the display, so return the mutex. */
xSemaphoreGive( xMutex );
/* If taskYIELD() was called on each iteration then this task would
only ever remain in the Running state for a short period of time,
and processing time would be wasted by rapidly switching between
tasks. Therefore, only call taskYIELD() if the tick count changed
while the mutex was held. */
if( xTaskGetTickCount() != xTimeAtWhichMutexWasTaken ) //如果结束时和获取互斥量时不位于同一时间片
{
taskYIELD();
}
}
}

当然这个示例并不能完全解决这个问题, 通过时间片来进行判断往往不够精确

但确实能在一定程度上缓解该问题

第四种方式则是使用gatekeeper任务

核心是通过唯一的一个任务来使用这个共享的资源, 任务间通过队列向GateKeeper任务传送数据

由GateKeeper任务来对共享的资源进行操作

有时为了更快的对数据进行处理通常会给GateKeeper任务一个较高的优先级, 这会导致GateKeeper任务延迟处理优先级较低的任务

大部分freertos api函数都有两个版本, 区别在于名称后面的FromISR

带有改后缀的, 称为中断安全函数

为什么执行同一功能的函数必须设计为两个版本呢

其根本原因是因为 在函数调用之中 如果使更高优先级的任务进入到就绪态, 该任务则会抢占当前任务

而在中断中不能进行上下文切换

那为了解决这个问题, freertos的设计采用了将其分为两个函数的做法

这样的优势是:

代码不用再去实现 辨别函数来源的逻辑 即不用去判定其是从中断中调用的还是从任务中调用的

确保传入的参数都是该函数会使用的, 在任务中和中断中调用函数时使用的api函数参数可能不同

不同freertos移植版本执行上下文机制不同

兼容不同架构, 中断和硬件有关

缺点呢:

在可能同时被中断和任务调用的函数中无法确定使用哪个函数

解决办法是使用 延迟中断处理, 即把中断处理函数统一由任务或进程守护任务调用, 而这些任务由中断进行唤醒

那么中断安全函数是如何解决无法在中断中切换上下文的呢

中断安全函数都有一个统一的参数xHigherPriorityTaskWoken

这个参数有两个值, pdTRUE和pdFALSE

通常初始化为pdFALSE

在执行完一个api函数(中断安全版)之后, 如果导致更高优先级的任务变为就绪态

则在api函数内部会将该参数改为pdTRUE

最后在ISR结尾处通过调用portYIELD_FROM_ISR( xHigherPriorityTaskWoken )来决定是否跳转

portYIELD_FROM_ISR是如何起作用的呢?

该函数会先判定woken参数是否为true, 如果为true则会悬起一个pendSV异常

这会导致当该ISR推出后 引起一个上下文切换

这样就避免了在中断中进行任务切换

示例如下, 使用的api函数为二进制信号量相关函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static uint32_t ulExampleInterruptHandler( void )
{
BaseType_t xHigherPriorityTaskWoken;
/* The xHigherPriorityTaskWoken parameter must be initialized to
pdFALSE as it will get set to pdTRUE inside the interrupt safe
API function if a context switch is required. */
xHigherPriorityTaskWoken = pdFALSE;
/* 'Give' the semaphore to unblock the task, passing in the address of
xHigherPriorityTaskWoken as the interrupt safe API function's
pxHigherPriorityTaskWoken parameter. */
xSemaphoreGiveFromISR( xBinarySemaphore, &xHigherPriorityTaskWoken ); //二进制信号量
/* Pass the xHigherPriorityTaskWoken value into portYIELD_FROM_ISR().
If xHigherPriorityTaskWoken was set to pdTRUE inside
xSemaphoreGiveFromISR() then calling portYIELD_FROM_ISR() will request
a context switch. If xHigherPriorityTaskWoken is still pdFALSE then
calling portYIELD_FROM_ISR() will have no effect. Unlike most FreeRTOS
ports, the Windows port requires the ISR to return a value - the return
statement is inside the Windows version of portYIELD_FROM_ISR(). */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}

在讲信号量之前先来了解下延迟中断处理

由于ISR运行时会导致内核的短暂停止

这可能会导致 任务的开始时间和执行时间的抖动

所以必须保证ISR逻辑尽量简短

但世事总不那么如意

在必须处理复杂逻辑时该怎么办呢

就只能使用延迟中断处理了

延迟中断处理在检测到中断时 唤醒处理函数

然后在处理函数中进行逻辑的处理

ISR中只需要写唤醒函数的代码即可

11

那么ISR要如何唤醒一个任务呢

这里我们使用的是 信号量(官方推荐的是使用任务通知)

信号量分为二进制信号量和计数信号量

二进制信号量可以看作一个队列长度为1的, 队列内容无关的队列, 那么计数信号量就是队列长度不为1的, 队列内容无关的队列

使用流程:

创建一个信号量

中断处理任务中调用take(获取信号量函数)

在take发现信号量中没有信号时 该任务进入阻塞状态

当检测到中断发生

ISR中调用give(写信号量函数)

此时内核发现该信号量有值了, 通知中断处理任务进入就绪态

如果中断处理任务的优先级大于现有任务

则会将xHigherPriorityTaskWoken置为pdTRUE, 然后ISR返回后切换上下文为中断处理任务

中断处理任务take到信号量

执行中断处理逻辑

在下一轮执行时, take再次发现信号量为空, 进入阻塞态

9

二进制信号量的问题也很明显

在当中断处理函数take信号量之前 如果再次发生了一个中断

那么第二个中断将被忽略 于是有个计数信号量

作用机制是相同的, 只是多了能够处理多个中断

10

api函数:

1
SemaphoreHandle_t xSemaphoreCreateBinary( void );
1
2
BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait
);
1
2
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken );
1
2
SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount );

上文还提到可以将中断处理工作延时给守护进程任务

优点在于:

能够降低资源浪费率, 无需为每个延迟中断创建单独任务

简化用户模型: 延迟中断处理函数是一个标准的C函数

缺点在于:

由于都是适用的守护进程任务, 那么无法为每个中断处理设置单独的优先级

守护进程执行函数是通过队列来获取所需要的执行函数的, 可能ISR结束后无法立即执行中断处理函数

1
2
3
4
5
6
7
BaseType_t xTimerPendFunctionCallFromISR( PendedFunction_t
xFunctionToPend,
void *pvParameter1,
uint32_t ulParameter2,
BaseType_t *pxHigherPriorityTaskWoken ); //在中断中向进程守护任务发送一个函数执行指令

void vPendableFunction( void *pvParameter1, uint32_t ulParameter2 ); //函数原型 既可以使用指针类型的参数 也可以使用非指针类型参数

ISR中也是可以使用队列的, 但如果只是用来传递信号 那还是信号量相对更佳(最佳的还是 任务通知)

在进程中使用队列也需要使用ISR专用的中断安全api函数

在UART程序中, 可以使用队列来接收UART传送过来的数据, 然后通过延迟中断处理计数 在任务中进行处理

中断是可以嵌套的

每个中断源都有数值优先级和逻辑优先级的属性

逻辑优先级高的能抢占逻辑优先级低的中断 这点和裸机一样

逻辑优先级由数字优先级控制

在CM3中, 数字优先级低的逻辑优先级越高, 这点也和裸机一样

12

对于越高的优先级对ISR的执行事件越严格, 具体表现为是否能够使用api函数

CM3中最高循序使用8位来指定每个中断优先级

通常只使用八位数字的一部分

使用规范则是左对齐, 未使用的低有效位通常置为1

软件定时器能干嘛

软件定时器功能上和硬件定时器类似, 但是软件定时器是基于系统中断由软件来模拟的定时器

不像硬件定时器一样占用硬件资源

为了启用软件定时器,需要在头文件FreeRTOSConfig.h中设置configUSE_TIMERS的值为1

和硬件定时器相同, 在经过设定的tick数之后, 会调用对应的回调函数

可以在回调函数中实现需要的逻辑

注意不要在回调函数中执行任何可能会导致其进入阻塞状态的函数

因为定时器回调函数是由进程守护任务(RTOS daemon task)执行的

进程守护任务具有和正常任务相同的优先级机制

一旦定时器回调函数进入阻塞状态, 进程守护任务也会被阻塞, 这是不合法的

软件定时器分一次性定时器和自动重载定时器

一次性定时器在运行完一次之后自动进入休眠(dormant)状态

而自动重载定时器在执行完一次后会重新开始

8

既然定时器是由freertos守护进程任务维护的, 而启动定时器等api函数总是在别的任务中执行的

那么守护进程任务是如何知道在什么时候启动定时器, 又在什么时候对定时器进行操作呢

这就要提到上一节的队列了

队列作为一种任务间的通信方式, 理所应当能够作为进程守护任务与其他任务沟通的桥梁

当其他任务调用xTimerCreate()时, 会向Timer Command Queue(定时器任务列表)中发送一条指令

当进程守护任务从列表中读取到该指令时, 会执行相应的操作

1
2
3
4
5
TimerHandle_t xTimerCreate( const char * const pcTimerName,	//定时器名, 仅用于调试
const TickType_t xTimerPeriodInTicks, //定时器时间(tick数)
const BaseType_t xAutoReload, //是否自动重载 pdTRUE/pdFALSE
void * const pvTimerID, //定时器ID, 可用于自定义操作, 其他api函数本身不会使用
TimerCallbackFunction_t pxCallbackFunction ); //回调函数指针
1
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );
1
void vTimerSetTimerID( const TimerHandle_t xTimer, void *pvNewID );
1
void *pvTimerGetTimerID( const TimerHandle_t xTimer );
1
2
3
BaseType_t xTimerChangePeriod( TimerHandle_t xTimer,
TickType_t xNewPeriod,
TickType_t xTicksToWait );
1
BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait );

上述api函数如若要在中断中使用, 均需要使用其中断专用api函数, 函数名就是原函数名后面加上FromISR

例如xTimerStartFromISR()

静态创建软件定时器xTimerCreateStatic()

什么是队列

用于任务与任务之间, 中断与中断之间, 任务与中断中间通信的一种方式

队列本身也是一种FIFO的数据结构

使用起来也是如此, 通过xQueueSend()api函数将数据发送到队列中

可以选择是以拷贝的形式还是以指针的形式存放于队列中

将数据发送进队列的时候可以选择是发送到队首还是队尾

队列的读取总是从队首读取数据

为了处理多任务间通信的问题

可以用结构体来储存数据

结构体中标明该信息的来源及数据本身

还可以使用队列集(Queue Set)的方式

不过队列集相较于结构体更不直观且效率更差

因此官方更推荐在可选的情况下使用结构体的方式

说回队列集

队列集就是多个队列(或信号量)的集合

当处于队列集中的队列收到数据时, 该队列会将其句柄发送到队列集中, 此时队列集中就有数据了

通过xQueueSelectFromSet()函数传入队列集的句柄, 以此来返回队列集中存在的队列句柄

在实际使用时可以通过返回的句柄与现有的句柄进行比较

根据不同任务的数据选择不同的处理函数

还有一种叫做Mailbox的用法

在这里, Mailbox指的是队列长度为1的队列

Mailbox的意思是像邮一样只储存一个数据, 发送数据到该队列时直接覆写, 而不是等待被接收方读取并删除 对应api函数为xQueueOverwrite() 该函数只能用于队列长度为1的队列

接受时也不进行删除, 而是直接获得一份拷贝 相关api函数为xQueuePeek()

参考文档

Mastering-the-FreeRTOS-Real-Time-Kernel.v1.1.0 (Richard Barry -and- The FreeRTOS Team)

什么是任务

在 FreeRTOS 中,任务(Task)是内核调度的基本执行单位
它本质上是一个独立运行的函数,并且拥有自己的运行环境。

FreeRTOS 为每个任务分配两块独立的内存:

  • TCB(Task Control Block):保存任务的元信息,例如优先级、状态、上下文指针等。
  • 任务栈(Stack):保存此任务的局部变量、函数调用现场和寄存器上下文。

任务之间的栈空间相互独立,因此每个任务可以像“单独运行”的程序一样执行自己的逻辑。

虽然单核 MCU 在任意时刻只能执行一条指令,但 FreeRTOS 通过优先级调度快速的上下文切换让多个任务“轮流运行”,从而表现出类似多线程的效果。

因此,任务可以理解为:FreeRTOS 中由调度器管理的独立执行单元,是整个系统中最小的可调度对象。

一个最简单的任务长这样

1
2
3
4
5
6
7
8
void StartDefaultTask(void *argument)
{
/* Infinite loop */
for(;;)
{

}
}

任务的状态机及其生命周期

一个任务包含四种状态, 刚创建时的就绪态(Ready), 执行时的运行态(Runing), 阻塞时的阻塞态(Blocked), 暂停时的挂起态(Suspended)

当然还有被delete后的状态, 这里不列入其中

如何创建一个任务

函数参数及返回值

FreeRTOS提供了6个api函数供我们使用, 分别是:

xTaskCreate(), xTaskCreateStatic(),xTaskCreateRestricted(), xTaskCreateRestrictedStatic(), xTaskCreateAffinitySet(), and xTaskCreateStaticAffinitySet()

其中最基础的是xTaskCreate(), 函数定义如下:

1
2
3
4
5
6
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
const char * const pcName,
configSTACK_DEPTH_TYPE usStackDepth,
void * pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * pxCreatedTask );

另外提一嘴, FreeRTOS这个项目使用的命名方法可以看作是一种匈牙利命名法的变体, 通过在变量名前加’i’, ‘u’ 之类的单词表示变量的类型

如uxPriority 就是指的unsigned basetype (basetype在不同的架构上位数不同, 如32位架构下是32位, 64位下则是64位)

话说回来, 介绍一下其中的参数及返回值

参数:

  • pvTaskCode: 该参数为指向任务的函数的指针
  • pcName: 用于调试时显示的任务名称, 可置NULL, configMAX_TASK_NAME_LEN记录了其最大长度
  • usStackDepth: 用于指定分配给任务使用的堆栈大小, 该值指定的是堆栈可容纳的字节数, 即 如果该参数为128, 则会分配128 * 4 个字节的堆栈空间. configSTACK_DEPTH_TYPE规定了用于保存堆栈大小的数据类型, 默认为uint16_t, 意味着如果堆栈深度乘以堆栈宽度大于65535时会发生溢出
  • pvParameters: 用于给任务的参数传值
  • uxPriority: 该任务的优先级, 范围为0到configMAX_PRIORITIES - 1
  • pxCreatedTask: 用于储存该任务的句柄, 可置NULL

返回值:

  • pdPASS: 标志着任务被成功创建
  • pdFAIL: 表明没有足够的堆内存创建任务

不同的任务创建函数

从另外五个函数中可以提取出这几个关键词, static, Restricted, AffinitySet

其中:

  • static表示使用静态的内存, 和c语言中static一个作用, 不多赘述
  • restricted表示创建的任务的权限受到限制, 无法访问系统内存
  • affinity表示可用于多核处理器的任务, 支持Symmetric Multi Processing(SMP)

创建任务的不同姿势

简单的创建几个任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int main( void )
{
/*
* Variables declared here may no longer exist after starting the FreeRTOS
* scheduler. Do not attempt to access variables declared on the stack used
* by main() from tasks.
*/
/*
* Create one of the two tasks. Note that a real application should check
* the return value of the xTaskCreate() call to ensure the task was
* created successfully.
*/
xTaskCreate( vTask1, /* Pointer to the function that implements the task.*/
"Task 1",/* Text name for the task. */
1000, /* Stack depth in words. */
NULL, /* This example does not use the task parameter. */
1, /* This task will run at priority 1. */
NULL ); /* This example does not use the task handle. */
/* Create the other task in exactly the same way and at the same priority.*/
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
/* Start the scheduler so the tasks start executing. */
vTaskStartScheduler();
/*
* If all is well main() will not reach here because the scheduler will now
* be running the created tasks. If main() does reach here then there was
* not enough heap memory to create either the idle or timer tasks
* (described later in this book). Chapter 3 provides more information on
* heap memory management.
*/
for( ;; );
}

一个任务不止能在main函数里创建, 还可以在任务之中进行任务的创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void vTask1( void * pvParameters )
{
const char *pcTaskName = "Task 1 is running\r\n";
volatile unsigned long ul; /* volatile to ensure ul is not optimized away. */
/*
* If this task code is executing then the scheduler must already have
* been started. Create the other task before entering the infinite loop.
*/
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
for( ;; )
{
/* Print out the name of this task. */
vPrintLine( pcTaskName );
/* Delay for a period. */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
/*
* This loop is just a very crude delay implementation. There is
* nothing to do in here. Later examples will replace this crude
* loop with a proper delay/sleep function.
*/
}
}
}

通过任务模板进行实例的创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
void vTaskFunction( void * pvParameters )
{
char *pcTaskName;
volatile unsigned long ul; /* volatile to ensure ul is not optimized away. */
/*
* The string to print out is passed in via the parameter. Cast this to a
* character pointer.
*/
pcTaskName = ( char * ) pvParameters;
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Print out the name of this task. */
vPrintLine( pcTaskName );
/* Delay for a period. */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
/*
* This loop is just a very crude delay implementation. There is
* nothing to do in here. Later exercises will replace this crude
* loop with a proper delay/sleep function.
*/
}
}
}

/*
* Define the strings that will be passed in as the task parameters. These are
* defined const and not on the stack used by main() to ensure they remain
* valid when the tasks are executing.
*/
static const char * pcTextForTask1 = "Task 1 is running";
static const char * pcTextForTask2 = "Task 2 is running";
int main( void )
{
/*
* Variables declared here may no longer exist after starting the FreeRTOS
* scheduler. Do not attempt to access variables declared on the stack used
* by main() from tasks.
*/
/* Create one of the two tasks. */
xTaskCreate( vTaskFunction, /* Pointer to the function that implements the task. */
"Task 1", /* Text name for the task. This is to facilitate debugging only. */
1000, /* Stack depth - small microcontrollers will use much less stack than this.*/
( void * ) pcTextForTask1, /* Pass the text to be printed into the task using the task parameter.*/
1, /* This task will run at priority 1.*/
NULL ); /* The task handle is not used in this example. */
/*
* Create the other task in exactly the same way. Note this time that
* multiple tasks are being created from the SAME task implementation
* (vTaskFunction). Only the value passed in the parameter is different.
* Two instances of the same task definition are being created.
*/
xTaskCreate( vTaskFunction,
"Task 2",
1000,
( void * ) pcTextForTask2,
1,
NULL );
/* Start the scheduler so the tasks start executing. */
vTaskStartScheduler();
/*
* If all is well main() will not reach here because the scheduler will
* now be running the created tasks. If main() does reach here then there
* was not enough heap memory to create either the idle or timer tasks
* (described later in this book). Chapter 3 provides more information on
* heap memory management.
*/
for( ;; )
{
}
}

四种状态

就绪态和运行态

那么创建完一个任务之后, 其默认会处于就绪态

那么有了就绪态, 我们创建一个任务肯定想要让他执行, 于是有了运行态

运行完毕的任务回到就绪态, 然后从就绪态中选一个任务执行, 那么最简单的一个模型诞生了

0

这里提一嘴优先级, 顾名思义, 优先级决定了哪个任务能够先执行

上文还说到, freertos通过不断地上下文切换达到”伪”多线程的目的

那么在每次上下文切换的时候都会根据优先级决定谁会被选中为下一个运行的任务

试想以下场景: 我们现在有两个优先级不同的任务, 请问这两个任务会如何执行?

答案显而易见, 低优先级的任务会被高优先级的任务饿死

那么我们两个状态的模型就不够用了

于是诞生了阻塞态挂起态

阻塞态和挂起态

当搞优先级的任务进入阻塞态后, 那么就绪态就只剩下低优先级的任务可以执行了

这样, 低优先级的任务就饿不死了

阻塞态分为两种类型:

时间(Temporal)事件同步(Synchronization)事件

同步事件可由FreeRTOS 队列、二进制信号量、计数信号量、互斥体、递归互斥体、事件组、流缓冲区、消息缓冲区和直接任务通知产生

任务可以阻塞同步事件并设置超时,从而有效地同时阻塞两种类型的事件。例如,任务可以选择等待最多 10 毫秒以便数据到达队列。如果 10 毫秒内有数据到达或 10 毫秒后没有数据到达,任务将离开阻塞状态

时间事件会在指定时间到达或过去一定时间后发生

对应void vTaskDelay( TickType_t xTicksToDelay )

void vTaskDelayUntil( TickType_t * pxPreviousWakeTime, TickType_t xTimeIncrement )函数

通过这两个函数即可设定一个任务执行的频率

其中xTicksToDelay 参数指的是所需等待的tick数, 通过pdMS_TO_TICKS()函数将毫秒数转换为tick数

tick数和mcu的频率挂钩, 所以通过函数将毫秒数进行转换可以避免因频率更改带来的错误

通过将FreeRTOSConfig.h 中的 INCLUDE_vTaskSuspend 设置为 1, 来启用 portMAX_DELAY

xTicksToWait 设置为 portMAX_DELAY 将导致任务无限期等待(不会超时)

pxPreviousWakeTime参数指的是当前的tick数, xTimeIncrement 指定多少tick产生一次事件

pxPreviousWakeTime仅需指定一次, 之后会在函数内自增

两个函数不同点在于, 前者是相对的, 而后者是绝对的

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
void vTaskFunction( void * pvParameters )
{
char * pcTaskName;
const TickType_t xDelay250ms = pdMS_TO_TICKS( 250 );
/*
* The string to print out is passed in via the parameter. Cast this to a
* character pointer.
*/
pcTaskName = ( char * ) pvParameters;
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Print out the name of this task. */
vPrintLine( pcTaskName );
/*
* Delay for a period. This time a call to vTaskDelay() is used which
* places the task into the Blocked state until the delay period has
* expired. The parameter takes a time specified in 'ticks', and the
* pdMS_TO_TICKS() macro is used (where the xDelay250ms constant is
* declared) to convert 250 milliseconds into an equivalent time in
* ticks.
*/
vTaskDelay( xDelay250ms );
}
}

void vTaskFunction( void * pvParameters )
{
char * pcTaskName;
TickType_t xLastWakeTime;
/*
* The string to print out is passed in via the parameter. Cast this to a
* character pointer.
*/
pcTaskName = ( char * ) pvParameters;
/*
* The xLastWakeTime variable needs to be initialized with the current tick
* count. Note that this is the only time the variable is written to
* explicitly. After this xLastWakeTime is automatically updated within
* vTaskDelayUntil().
*/
xLastWakeTime = xTaskGetTickCount();
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Print out the name of this task. */
vPrintLine( pcTaskName );
/*
* This task should execute every 250 milliseconds exactly. As per
* the vTaskDelay() function, time is measured in ticks, and the
* pdMS_TO_TICKS() macro is used to convert milliseconds into ticks.
* xLastWakeTime is automatically updated within vTaskDelayUntil(), so
* is not explicitly updated by the task.
*/
vTaskDelayUntil( &xLastWakeTime, pdMS_TO_TICKS( 250 ) );
}
}

挂起态

处于挂起状态的任务不可用于调度程序。进入挂起状态的唯一方法是通过调用 vTaskSuspend() API 函数,而退出状态的唯一方法是调用 vTaskResume() xTaskResumeFromISR() API 函数。大多数应用程序不使用挂起状态

1

至此, 总结出四种状态的状态转换图

删除(Delete)一个任务

vTaskDelete() API 函数删除任务。仅当 FreeRTOSConfig.h 中的 INCLUDE_vTaskDelete 设置为 1 时,vTaskDelete() API 函数才可用

void vTaskDelete( TaskHandle_t xTaskToDelete )通过传递任务句柄的指针完成删除

当任务被删除时,只有内核本身分配给任务的内存才会被自动释放。如果不再需要,在任务执行期间分配的任何内存或其他资源都必须显式释放

当任务被删除时, 内核本身分配给任务的资源由空闲任务来进行释放

这里暂且把空闲任务看作为一个最低优先级的, 只在就绪态只存在空闲任务时才执行的一个任务

因为资源需要通过空闲任务来进行释放, 所以需要保证空闲任务不被饿死, 之后会说明

调度算法

FreeRTOS通过调度器来决定什么时候执行什么任务

优先级

FreeRTOS保证总是选择位于就绪态的最高优先级的任务进入运行态

那么相同优先级的任务呢?

FreeRTOS采用循环调度的算法, 即像一个队列一样, 运行完毕的任务回到队列末尾, 位于队列首的任务被选中进入运行态

优先级的最大值由configMAX_PRIORITIES决定

优先级的范围从0到configMAX_PRIORITIES - 1, 0即最低优先级

对于优先级, 有两种调度程序可选, 分别是通用调度(Generic Scheduler)架构优化调度(Architecture-Optimized Scheduler)

通用调度没有对configMAX_PRIORITIES施加上限, 但由于更大的值意味着需要更多的RAM以及更长的最坏情况执行时间, 所以一般来说, 建议最小化configMAX_PRIORITIES

架构优化调度可以根据不同的架构优化configMAX_PRIORITIES的值, 例如在32位架构上该值为32, 64位架构上该值为64

要使用架构优化调度, 只需要在FreeRTOSConfig.h 中将 configUSE_PORT_optimized_TASK_SELECTION 设置为 1, 为0则为通用调度

但不是所有FreeRTOS端口都有架构优化的实现, 可以根据configUSE_PORT_optimized_TASK_SELECTION的默认值来判断, 如果为1, 则说明有, 反之则无

优先级不只能在任务创建时指定, 还可以在程序中动态进行修改

当 FreeRTOSConfig.h 中的 INCLUDE_vTaskPrioritySet 设置为 1 时

可以使用vTaskPrioritySet()在程序中动态修改任务的优先级

1
2
void vTaskPrioritySet( TaskHandle_t xTask,
UBaseType_t uxNewPriority );

当 FreeRTOSConfig.h 中的 INCLUDE_uxTaskPriorityGet 设置为 1 时

还可以使用uxTaskPriorityGet()动态获取任务的优先级

1
UBaseType_t uxTaskPriorityGet( TaskHandle_t xTask );

空闲任务

空闲任务, 顾名思义, 就是在没有任务可以执行时 执行的任务

其作用主要是起占位符作用, 因为FreeRTOS需要保证在任何时刻都有任务可以进入运行态

他的优先级为最低的0, 保证了不会因优先级更大而阻碍别的任务

空闲任务还有一个作用就是清除由vTaskDelete() 函数残留的内核资源

虽然空闲任务的优先级为最低的0, 但也可能存在相同的, 优先级为0的任务, 这时, 我们肯定不想空闲任务和我们的任务抢夺运行时间

FreeRTOSConfig.h 中的 configIDLE_SHOULD_YIELD决定空闲任务是否会和其他0优先级任务抢夺运行时间

configIDLE_SHOULD_YIELD置0时, 空闲任务和其他任务一样, 遵循循环调度的算法

configIDLE_SHOULD_YIELD置1时, 当有其他0优先级任务处于就绪态时, 空闲任务将在每次迭代中让步, 将运行时间让给其他任务

2

在每次进行上下文切换时(这里每个间隔称为一个时间片, 每个事件片的末尾执行一次上下文切换), 先是由循环调度运行空闲任务, 此时空闲任务发现就绪态还有其他0优先级的任务, 于是主动让出该时间片的剩余时间.

t6到t7时间段还揭示了高优先级的任务是如何抢占低优先级的任务的(详见后文)

空闲任务是可以通过钩子函数进行hook的

1
void vApplicationIdleHook( void );

空闲任务挂钩的常见用途包括:

执行低优先级、后台或连续处理功能,而无需为此目的创建应用程序任务而产生 RAM 开销。

测量空闲处理能力的量。 (只有当所有较高优先级的应用程序任务都没有要执行的工作时,空闲任务才会运行;因此,测量分配给空闲任务的处理时间量可以清楚地指示空闲处理时间。)

将处理器置于低功耗模式,在没有要执行的应用程序处理时提供一种简单且自动的节能方法(尽管可实现的节能效果低于无滴答空闲模式所实现的节能效果)。

空闲任务挂钩函数必须遵守以下规则:

空闲任务挂钩函数绝不能尝试阻止或挂起自身。

注意:**以任何方式阻止空闲任务都可能导致没有任务可进入运行状态的情况。**如果应用程序任务使用 vTaskDelete() API 函数删除自身,则空闲任务挂钩必须始终在合理的时间段内返回到其调用者。这是因为空闲任务负责清理分配给删除自身的任务的内核资源。如果空闲任务永久保留在空闲挂钩函数中,则无法进行此清理

时间片, 抢占式与协作式

时间片

试想当有两个优先级相同的, 连续运行(不会进入阻塞态)的任务, 如果正在运行中的那一个任务一直不主动退出的话, 那另一个任务将会被饿死

如何使两个同级的任务”同时”执行呢?

于是有了时间片(time slice)

时间片是一段时间, 在一段时间后, 即时间片的末尾, 调用一次上下文切换, 通过循环调度, 使就绪态中的任务进入运行态

时间片的长度由configTICK_RATE_HZ决定, configTICK_RATE_HZ被设置为100Hz时, 指一个时间片为1 / 100(s), 即10ms

configTICK_RATE_HZ 的最佳值取决于应用程序,但典型值为 100

3

抢占式调度

抢占式即高优先级会直接打断低优先级任务的执行, 上文默认是使用的抢占式算法

高优先级的任务通常标志着 紧急, 重要

因此需要保证高优先级任务从 进入就绪态 到 从就绪态进入运行态 的时间

任务的抢占是可以嵌套的, 这意味着在低优先级任务被高优先级任务抢占时候, 高优先级的任务是可以被更高优先级的任务所抢占

即使遇到时间片结束时的上下文切换, 下一个时间片仍然会执行更高优先级的任务, 因为此时就绪态中最高优先级的任务仍然是他自己

三种调度算法

是否使用时间片以及是否使用抢占式调度可以通过FreeRTOSConfig.h中configUSE_PREEMPTIONconfigUSE_TIME_SLICING的值进行配置

第三个配置常量 configUSE_TICKLESS_IDLE 也会影响调度算法, 他决定了时间片末尾的上下文切换请求在空闲时是否会关闭 以最大限度地降低功耗, 默认为0(关闭)

configUSE_PREEMPTIONconfigUSE_TIME_SLICING的不同配置决定了程序使用哪一种调度算法

4

带有时间切片的固定优先级抢占式调度(Prioritized Preemptive Scheduling with Time Slicing)

5

这也是最为常见的一种算法

无时间分片的优先抢占式调度(Prioritized Preemptive Scheduling without Time Slicing)

6

由于没有时间片提供的定期的上下文切换, 导致空闲任务一直占着cpu导致任务2执行时间远小于空闲任务(configIDLE_SHOULD_YIELD为0的情况)

协同调度(Cooperative Scheduling)

7

协同调度算法下的任务只有在一个任务运行结束时才会进行切换

可用于数据传输的情况

例如任务1将要发送’abcdefg’

任务2将要发送’1234567890’

我们肯定不想在任务2发送期间被任务1抢占使得发送出的数据被破坏

但这是一个比较奇怪的例子, 使用无时间分片的优先抢占式调度, 然后将两者的任务优先级置为相同 也可以解决这个问题, 但这样必须限制发送数据的任务为相同优先级

而且进程间通信往往有更好的方法

TLS与可重入

抄录

线程本地存储(TLS)允许应用程序开发人员在每个任务的任务控制块中存储任意数据。此功能最常用于存储通常由不可重入函数存储在全局变量中的数据。可重入函数是可以从多个线程安全运行而没有任何副作用的函数。当在没有线程本地存储的多线程环境中使用不可重入函数时,必须特别注意检查临界区内这些函数调用的带外结果。过度使用临界区会降低 RTOS 性能,因此线程本地存储通常优于使用临界区。到目前为止,线程本地存储最常见的用途是 C 标准库和 POSIX 系统使用的 ISO C 标准中使用的 errno 全局变量。 errno 全局用于为常见标准库函数(例如 strtof 和 strtol)提供扩展结果或错误代码

大多数嵌入式 libc 实现都提供 API 以确保不可重入函数可以在多线程环境中正常工作。 FreeRTOS 支持两个常用开源库的重入 API:newlib 和 picolibc。这些预构建的 C 运行时线程本地存储实现可以通过在其项目的 FreeRTOSConfig.h 文件中定义下面列出的相应宏来启用

应用程序开发人员可以通过在 FreeRTOSConfig.h 文件中定义以下宏来实现线程本地存储: 将 configUSE_C_RUNTIME_TLS_SUPPORT 定义为 1 以启用 C 运行时线程本地存储支持。将 configTLS_BLOCK_TYPE 定义为 c 类型,该类型应用于存储 C 运行时线程本地存储数据。将 configINIT_TLS_BLOCK 定义为初始化 C 运行时线程本地存储块时应运行的 C 代码。将 configSET_TLS_BLOCK 定义为切换新任务时应运行的 c 代码 将 configDEINIT_TLS_BLOCK 定义为在取消初始化 C 运行时线程本地存储块时应运行的 c 代码

上下文切换原理

当在任务中手动调用vTaskStartScheduler()第一次启动调度器时会触发SVC异常

OS收到请求后在SVC Handler中做好上下文切换的准备, 同时悬起一个pendSV异常

当CPU退出SVC且被嵌套的中断例程执行结束后, 进入pendSV, 进行上下文切换

在任务中手动调用taskYIELD()时会直接悬起一个pendSV异常

一个时间片末尾产生的Systick中断现象与手动调用taskYIELD()相同

参考文档

FreeRTOS 从入门到精通5–任务管理这件事(上)

FreeRTOS 从入门到精通6–任务管理这件事(下)(对比PLC,安卓)

Mastering-the-FreeRTOS-Real-Time-Kernel.v1.1.0 (Richard Barry -and- The FreeRTOS Team)

Cortex-M3权威指南(Joseph Yiu 著 宋岩 译 www.ouravr.com热心网友 校对)

概述

个人感觉麻烦的点在于环境的搭建(又回到最初一瓶农夫山泉搁那配一天环境的日子,一个点没注意就出问题,甚至桌上的农夫山泉都换成了怡宝),本文着重记录下自己踩得坑,对于漏洞的详细分析可以看看参考文章.然后关于IOT通信还涉及到http协议和cgi,我也以我的理解简单介绍下

还介绍了纯rop链和shellcode两种漏洞利用方法,提到shellcode来getshell时出现的一些问题

阅读全文 »

Here's something encrypted, password is required to continue reading.
阅读全文 »