news 2026/10/27 2:01:37

FreeRTOS——什么是任务切换

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FreeRTOS——什么是任务切换

任务切换的核心

1.什么是任务切换
任务切换概念

单片机正常情况下只有一个主程序在跑。在引入FreeRTOS之后,系统会有多个任务,每个任务都像一段独立的小程序,有自己的栈空间,自己的执行现场。

但CPU同一时刻只能执行同一个任务。所谓任务切换,就是:
CPU暂停当前任务,保存它的运行现场,再恢复另一个的任务的现场,让那个任务继续运行
这个过程也叫上下文切换(Context Switch)。

从任务自身角度来说,它不知道切换发生过,只感觉自己一直在运行,切换是内核和硬件悄悄完成的。
举个例子:

  • 你在写TaskA的代码,写到一半被打断
  • 内核把你的“写到哪了、变量是多少、栈顶在哪”记下来
  • 换TaskB运行一段时间
  • 再切回 TaskA 时,从上次停下的地方继续写
    这个“记下来”和“恢复回来”,就是任务切换。
切换时需要保存和恢复内容:

任务切换,本质是保存旧任务现场,恢复新任务的现场。

什么是"现场"
现场就是任务被切走时,CPU和任务栈里与执行相关的关键信息。主要包括:

内容作用
程序计数器PC记录任务执行到哪条指令
栈指针SP当前任务栈的位置
通用寄存器保存任务运行时的中间数据
状态寄存器xPSR保存CPU状态信息

在FreeRTOS,这些现场主要保存在两处:

  1. 任务栈里
  2. TCB的pxTop0fStack中

TCB第一个成员就是栈顶指针:

1volatileStackType_t*pxTopOfStack;/* Points to the location of the las

切换时,旧任务的栈顶地址会写回这里,方便下次恢复。

Cortex-M3 上哪些寄存器要保存
本工程使用的是 ARM Cortex-M3,相关代码在port.c。
进入中断时,硬件会自动把一部分寄存器压栈:

寄存器是否硬件自动保存
R0是
R1是
R2是
R3是
R12是
LR是
PC是
xPSR是
R4 ~ R11否,软件保存

所以在xPortPendSVHandler()里,软件只需要保存 R4 ~ R11:

1 mrs r0, psp 2 isb 3 4 ldr r3, =pxCurrentTCB /* Get the location of the current TCB. */ 5 ldr r2, [r3] 6 7 stmdb r0!, {r4-r11} /* Save the remaining registers. */ 8 str r0, [r2] /* Save the new top of stack into the first

恢复时,再把它们弹回来:

1 ldr r1, [r3] 2 ldr r0, [r1] /* The first item in pxCurrentTCB is the ta 3 ldmia r0!, {r4-r11} /* Pop the registers and the critical nestin 4 msr psp, r0 5 isb

任务栈里保存的内容
任务创建时,内核会先把栈布置成“像刚从异常返回一样”。对应函数是pxPortInitialiseStack():

1StackType_t*pxPortInitialiseStack(StackType_t*pxTopOfStack,TaskFunction_t pxCode,void*pvP2{34pxTopOfStack--;5*pxTopOfStack=portINITIAL_XPSR;6pxTopOfStack--;7*pxTopOfStack=((StackType_t)pxCode)&portSTART_ADDRESS_MASK;8pxTopOfStack--;9*pxTopOfStack=(StackType_t)prvTaskExitError;/* 对应压入LR */10pxTopOfStack-=5;11*pxTopOfStack=(StackType_t)pvParameters;/* 对应硬件保存5个寄存器 */12pxTopOfStack-=8;/* 留8个位置给软件手动压栈 */1314returnpxTopOfStack;15}

栈帧布局如下:

所以一次完整切换,实际上保存的是:

硬件保存:R0,R1,R2,R3,R12,LR,PC,xPSR 软件保存:R4~R11 TCB记录:pxTopOfStack
恢复现场数据流向

当调度器决定要运行 TaskB 时,它会找到 TaskB 的 TCB,取出里面的pxTopOfStack(也就是 TaskB 的栈顶指针,指向 TaskB 在 RAM 里的那块区域)。
接下来,FreeRTOS 的PendSV异常服务程序(汇编代码)会做以下事情:

  • 软件出栈(R4 ~ R11):执行ldmia r0!, {r4-r11}。这句汇编的意思是:把r0指向的 RAM 地址里的数据,连续装载(Load)到 CPU 的R4到R11寄存器里,并同步更新r0的地址。
  • 修改 SP 指针:把更新后的地址写进硬件寄存器PSP(进程栈指针)。
  • 硬件出栈(R0~R3, R12, LR, PC, xPSR):当汇编指令执行到最后,触发异常返回(比如bx r14)。此时,Cortex-M3 的硬件会自动接管,它会顺着PSP指向的 RAM 地址,自动把剩下的 8 个寄存器(R0~R3, R12, LR, PC, xPSR)的值弹回 CPU 中。
任务指针,TCB

当前运行的任务指针
除了寄存器和栈,内核还要知道“当前是谁在运行”。
这个指针就是:

1PRIVILEGED_DATA TCB_t*volatilepxCurrentTCB=NULL;

切换前:

1 pxCurrentTCB -> TaskA

切换后:

1 pxCurrentTCB -> TaskB

所以任务切换时,真正变化的核心内容可以总结为:

项目切换前切换后
当前任务TaskATaskB
栈顶指针TaskA 的栈顶TaskB 的栈顶
CPU 寄存器TaskA 的现场TaskB 的现场

任务管理块TCB:
FreeRTOS 每创建一个任务,内核都会为它分配一块管理结构,叫做TCB(Task Control Block,任务控制块)。
可以把 TCB 理解成任务的“档案”:内核靠它知道任务是谁、优先级多少、栈在哪、当前处于什么状态。
源码中类型名是TCB_t,定义在tasks.c。
TCB 里最重要的几个成员

1typedefstructtskTaskControlBlock2{3volatileStackType_t*pxTopOfStack;/* 任务栈顶指针 */4ListItem_t xStateListItem;/* 状态链表节点 */5ListItem_t xEventListItem;/* 事件链表节点 */6UBaseType_t uxPriority;/* 任务优先级 */7StackType_t*pxStack;/* 任务栈起始地址 */8charpcTaskName[16];/* 任务名 */9...10}TCB_t;

pxTopOfStack——任务栈顶
是FreeRTOS定义在RAM内存里的一个软件变量(指针),这是TCB里最重要的成员,而且必须是结构体第一个成员。
作用:

  • 记录任务栈当前用到哪里
  • 任务切换时,保存/恢复栈顶就靠它
  • 本质上是:保存SP的值
    任务切换时,汇编代码会直接读取TCB第一个成员:
ldr r0, [r1] /* 从TCB取出栈顶 */

xStateListItem——状态节点
嵌在TCB里链表节点,表示任务当前状态、
任务处于不同状态时,会被挂到不同链表:

状态挂到哪个链表
就绪就绪链表
阻塞延时链表
挂起挂起链表

xEventListItem— 事件节点
也是嵌在 TCB 里的链表节点,表示任务是否在等待事件。
比如:

  • 等队列数据
  • 等信号量
    一个任务可以同时:
  • 用xStateListItem挂到延时链表(等超时)
  • 用xEventListItem挂到队列等待链表(等数据)

uxPriority— 优先级
任务优先级,数字越大优先级越高。
调度器会优先运行优先级高的任务。

pxStack— 栈起始地址
记录任务栈从哪里开始。
和pxTopOfStack的区别:

成员含义
pxStack栈底
pxTopOfStack当前栈顶

pcTaskName— 任务名
任务名字,主要用于调试。

几个关键关系
任务句柄就是 TCB 指针

1TaskHandle_t xHandle;2xTaskCreate(Task1,"Task1",128,NULL,1,&xHandle);

这里的xHandle,本质上就是:

1TaskHandle_t->TCB*

当前运行任务
内核里有一个全局指针:

1TCB_t*volatilepxCurrentTCB=NULL;

它指向当前正在运行的任务 TCB。
任务切换完成后,pxCurrentTCB就会指向新的任务。

TCB 和任务栈
每个任务都有两部分:

1 TCB -> 管理信息 2 任务栈 -> 运行时数据

关系如下:

1TCB2├─ pxStack->栈底3└─ pxTopOfStack->当前栈顶

TCB 整体结构

1TCB2├─ pxTopOfStack 栈顶,任务切换时用3├─ xStateListItem 状态节点,挂就绪/延时/挂起链表4├─ xEventListItem 事件节点,挂队列/信号量链表5├─ uxPriority 优先级6├─ pxStack 栈起始地址7└─ pcTaskName 任务名

总结
TCB 就是 FreeRTOS 管理任务的核心结构,每个任务一个 TCB。
最需要记住的 3 点:

  1. pxTopOfStack:任务切换的关键
  2. xStateListItem/xEventListItem:任务挂到内核链表上的“挂钩”
  3. pxCurrentTCB:指向当前正在运行的任务
    一句话总结:

TCB 是任务档案,任务栈是运行空间,链表节点是排队位置。

2.当前任务与就绪链表
pxCurrentTCB作用

内核里有一个全局指针

PRIVILEGED_DATA TCB_t * volatile pxCurrentTCB = NULL;

它指向当前正在CPU上运行的任务TCB

pxCurrentTCB————>某个任务TCB

作用很简单:告诉内核“现在是谁在跑”

场景pxCurrentTCB 指向
TaskA 正在运行TaskA 的 TCB
切换到 TaskB 后TaskB 的 TCB
调度器还没启动NULL

任务切换完成后,pxCurrentTCB会更新为新任务:

1taskSELECT_HIGHEST_PRIORITY_TASK();// 内部会更新 pxCurrentTCB

PendSV 切换任务时,也是通过它找到新任务的栈:

1 ldr r1, [r3] // 读取 pxCurrentTCB 2 ldr r0, [r1] // 读取 pxTopOfStack
pxReadyTasksLists[]就绪链表

所有准备好、可以随时运行的任务,都放在就绪链表里。

1PRIVILEGED_DATAstaticList_t pxReadyTasksLists[configMAX_PRIORITIES]={0};

这是一个数组,每个优先级对应一条链表:

1 pxReadyTasksLists[0] -> 优先级 0 的就绪任务 2 pxReadyTasksLists[1] -> 优先级 1 的就绪任务 3 pxReadyTasksLists[2] -> 优先级 2 的就绪任务 4 ... 5 pxReadyTasksLists[55] -> 优先级 55 的就绪任务

要点:

  • 一个优先级一条链表
  • 同优先级多个任务挂在同一条链表里
  • 调度器选任务时,只从就绪链表里选
任务如何进入就绪链表

从xTaskCreate()开始看最清楚。
创建任务的入口

1xTaskCreate(Task1,"Task1",128,NULL,1,NULL);

这个函数内部主要做 3 件事:

1prvInitialiseNewTask(pxTaskCode,pcName,(uint32_t)usStackDepth,pvParameters,uxPriority,pxCreatedTask,pxNewTCB,NULL);/* 初始化TCB */2prvAddNewTaskToReadyList(pxNewTCB);/* 挂入就绪链表 */

流程如下:

1 xTaskCreate() 2 | 3 |-- ① 分配 TCB 和任务栈 4 |-- ② prvInitialiseNewTask() 初始化 TCB 5 └-- ③ prvAddNewTaskToReadyList() 挂入就绪链表

第一步:分配内存

pxNewTCB = pvPortMalloc(sizeof(TCB_t));//分配TCB pxNewTCB -> pxStack = pvPortMalloc(栈大小);//分配任务栈

每个任务都需要两块内存:

TCB ————> 管理信息 任务栈 ————> 运行数据

第二步:初始化 TCB
prvInitialiseNewTask()负责把 TCB 填好,主要工作:

1pxNewTCB->uxPriority=uxPriority;// 设置优先级2strcpy(pxNewTCB->pcTaskName,pcName);// 设置任务名34vListInitialiseItem(&(pxNewTCB->xStateListItem));// 初始化状态节点5vListInitialiseItem(&(pxNewTCB->xEventListItem));// 初始化事件节点67listSET_LIST_ITEM_OWNER(&(pxNewTCB->xStateListItem),pxNewTCB);8listSET_LIST_ITEM_OWNER(&(pxNewTCB->xEventListItem),pxNewTCB);910pxNewTCB->pxTopOfStack=pxPortInitialiseStack(...);// 初始化任务栈

第三步:挂入就绪链表
prvAddNewTaskToReadyList()负责把新任务挂到就绪链表:

1prvAddTaskToReadyList(pxNewTCB);

这个函数里还会处理pxCurrentTCB:

1if(pxCurrentTCB==NULL)2{3pxCurrentTCB=pxNewTCB;
  • 第一个创建的任务,会直接成为pxCurrentTCB
  • 同时初始化所有内核链表(就绪链表、延时链表等)

真正挂链:prvAddTaskToReadyList()

1#defineprvAddTaskToReadyList(pxTCB)\2taskRECORD_READY_PRIORITY((pxTCB)->uxPriority);\3vListInsertEnd(&(pxReadyTasksLists[(pxTCB)->uxPriority]),&((pxTCB)->xStateListItem));

两步:
① 记录最高就绪优先级

1taskRECORD_READY_PRIORITY(pxTCB->uxPriority);

例如优先级 2 的任务就绪,就记住“当前最高就绪优先级至少是 2”。

② 挂到对应优先级链表

1vListInsertEnd(&(pxReadyTasksLists[uxPriority]),&(pxTCB->xStateListItem));

把任务的xStateListItem插到对应优先级的就绪链表里。

完整创建流程
以创建一个优先级为 1 的任务为例:

1 xTaskCreate(Task1, "Task1", 128, NULL, 1, NULL) 2 | 3 |-- 分配 TCB + 栈/* 本质只是分配内存 */ 4 | 5 |-- prvInitialiseNewTask() 6 | |-- 设置优先级 = 1 7 | |-- 设置任务名 = "Task1" 8 | |-- 初始化 xStateListItem 9 | |-- 初始化任务栈 10 | 11 |-- prvAddNewTaskToReadyList() 12 |-- pxCurrentTCB = 新任务 (如果是第一个任务) 13 |-- taskRECORD_READY_PRIORITY(1) /* 记录最高就绪优先级 */ 14 |-- vListInsertEnd( pxReadyTasksLists[1], xStateListItem ) 15 | 16 v 17 pxReadyTasksLists[1]: Task1 <--> 哨兵

任务创建完成后,就已经在就绪链表里等着运行了。

其他进入就绪链表的场景
不只是创建任务,以下情况也会进入就绪链表,本质都是调用prvAddTaskToReadyList():
以下是图片中提取的内容:

场景说明
任务创建xTaskCreate()
延时结束vTaskDelay() 时间到
等到队列数据队列收到数据,任务被唤醒
等到信号量信号量释放,任务被唤醒

taskSELECT_HIGHEST_PRIORITY_TASK()如何选出下一个任务

任务切换时,vTaskSwitchContext()会调用这个宏,决定下一个运行谁:

1#definetaskSELECT_HIGHEST_PRIORITY_TASK()2{3UBaseType_t uxTopPriority=uxTopReadyPriority;/* */4/* 找到最高优先级非空链表 */5while(listLIST_IS_EMPTY(&(pxReadyTasksLists[uxTopPriority])))6{7--uxTopPriority;8}910listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB,&(pxReadyTasksLists[uxTopPriority]));11uxTopReadyPriority=uxTopPriority;12}

第 1 步:找最高优先级非空链表

1while(listLIST_IS_EMPTY(&(pxReadyTasksLists[uxTopPriority])))2{3--uxTopPriority;4}
  • 从记录的最高优先级往下找,直到找到有任务的就绪链表。
  • 实际在任务状态更新时,有时uxTopReadyPriority已自动切到下一个最高优先级任务就绪链表(硬件优化),
  • 但是当不开启硬件优化时,判空延迟更新uxTopReadyPriority,需要判空来兜底容错。
1uxTopReadyPriority=523pxReadyTasksLists[5]->空,继续4pxReadyTasksLists[4]->空,继续5pxReadyTasksLists[3]->有 TaskA,停止

第 2 步:同优先级任务轮转选取

1listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB,&(pxReadyTasksLists[uxTopPriority]));

在同优先级链表中取下一个任务,更新pxCurrentTCB。同优先级多个任务会轮流执行。
总体流程:

1 xTaskCreate 创建 TaskA 2 | 3 |-- 初始化 TCB 4 |-- prvAddTaskToReadyList() 5 | └-- TaskA 挂入 pxReadyTasksLists[2] 6 | 7 |-- vTaskStartScheduler() 启动调度器 8 | 9 v 10 taskSELECT_HIGHEST_PRIORITY_TASK() 11 | 12 |-- 找到 pxReadyTasksLists[2] 非空 13 |-- 选出 TaskA 14 |-- pxCurrentTCB = TaskA 15 | 16 v 17 TaskA 开始运行
3 .任务切换的 C 语言层
vTaskSwitchContext()的作用

PendSV 中断里会调用这个函数:

1 bl vTaskSwitchContext

源码如下:

1voidvTaskSwitchContext(void)2{3if(uxSchedulerSuspended!=(UBaseType_t)pdFALSE)/* 调度器是否被挂起 */4{5xYieldPending=pdTRUE;6}7else8{9xYieldPending=pdFALSE;10taskCHECK_FOR_STACK_OVERFLOW();/* 检查栈是否溢出 */11taskSELECT_HIGHEST_PRIORITY_TASK();/* 选出下一个任务,更新`pxCurrentTCB` */12}13}

它做什么
vTaskSwitchContext()不负责保存/恢复寄存器,只做三件事:

步骤代码作用
1检查调度器是否挂起挂起时不切换
2taskCHECK_FOR_STACK_OVERFLOW()检查栈是否溢出
3taskSELECT_HIGHEST_PRIORITY_TASK()选出下一个任务,更新pxCurrentTCB

它不做什么

  • 不操作 PSP 栈指针
  • 不保存 R4-R11
  • 不恢复寄存器
    这些都由 PendSV 的汇编代码完成。

在切换流程中的位置

1 PendSV 中断 2 | 3 |-- 保存旧任务 R4-R11 <- 汇编层 4 | 5 |-- vTaskSwitchContext() <- C 语言层 6 | └-- taskSELECT_HIGHEST_PRIORITY_TASK() 7 | └-- pxCurrentTCB = 新任务 8 | 9 └-- 恢复新任务 R4-R11 <- 汇编层

一句话:

C 层负责“选题”,汇编层负责“换场”。

调度器挂起时为什么不切换

什么是调度器挂起
调用vTaskSuspendAll会挂起调度器

1 void vTaskSuspendAll( void ) 2 { 3 ++uxSchedulerSuspended; 4 }

uxSchedulerSuspended是挂起调度器:

  • 每调用一次vTaskSuspendAll(),计数 +1
  • 每调用一次vTaskResumeAll(),计数 -1
  • 计数为0时,调度器恢复正常。

挂起时为什么不切换
调度器挂起期间,内核可能在批量修改任务链表。如果这时任务发生任务切换,链表可能处于不完整状态,导致系统出错。
所以vTaskSwitchContext()里第一件事开始检查:

1 if( uxSchedulerSuspended != pdFALSE ) 2 { 3 xYieldPending = pdTRUE; // 不切换,只记个标记 4 } 5 else 6 { 7 taskSELECT_HIGHEST_PRIORITY_TASK(); // 正常切换 8 }

挂起期间:切换请求被延迟,不会立刻执行。

xYieldPending作用

1 static volatile BaseType_t xYieldPending = pdFALSE;

一个延迟切换标记:

  • pdFALSE:没有待处理的切换请求
  • pdTRUE:有切换请求,但当前没执行,先记录

什么时候置位
情况1:调度器挂起期间收到切换请求

1 if( uxSchedulerSuspended != pdFALSE ) 2 { 3 xYieldPending = pdTRUE; // 现在不能切,先记着 4 }

情况2:调度器恢复后,发现有更高优先级任务

1 if( pxTCB->uxPriority > pxCurrentTCB->uxPriority ) 2 { 3 xYieldPending = pdTRUE; 4 }

情况3:挂起期间积累的tick需要切换

1 if( xTaskIncrementTick() != pdFALSE ) 2 { 3 xYieldPending = pdTRUE; 4 }

什么时候执行切换
调度器恢复后,vTaskResumeAll()会检查这个标记:

1 if( xYieldPending != pdFALSE ) 2 { 3 #if( configUSE_PREEMPTION != 0 ) 4 { 5 xAlreadyYielded = pdTRUE; 6 } 7 #endif 8 taskYIELD_IF_USING_PREEMPTION(); 9 }

如果xYieldPending= pdTRUE,就触发真正的任务切换。
SysTick里也会检查:

1 if( xYieldPending != pdFALSE ) 2 { 3 xSwitchRequired = pdTRUE; // 触发 PendSV 4 }

完整流程示例:

1 调度器正常运行 2 | 3 |-- vTaskSuspendAll() uxSchedulerSuspended = 1 4 | 5 |-- 期间某操作需要切换 6 | └-- vTaskSwitchContext() 7 | └-- xYieldPending = pdTRUE (不切换,只标记) 8 | 9 |-- xTaskResumeAll() uxSchedulerSuspended = 0 10 └-- 检查 xYieldPending == pdTRUE 11 └-- taskYIELD_IF_USING_PREEMPTION() 12 └-- 触发 PendSV, 真正切换 13 | 14 v 15 新任务开始运行

三个变量关系

1 uxSchedulerSuspended -> 调度器是否挂起 2 xYieldPending -> 是否有待处理的切换请求 3 pxCurrentTCB -> 当前运行的是哪个任务
变量作用
uxSchedulerSuspended控制能不能切换
xYieldPending记录有没有切换请求被延迟
pxCurrentTCB记录当前运行谁

配合关系:

1 切换请求到来 2 | 3 |-- 调度器未挂起 -> 立刻切换,更新 pxCurrentTCB 4 | 5 └-- 调度器已挂起 -> xYieldPending = pdTRUE,等恢复后再切

一句话总结:

  • vTaskSwitchContext()负责选题;调度器挂起时切换会被推迟,并用xYieldPending记住这个请求,等调度器恢复后再执行。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 2:01:24

07-06-源码-.NET Core GC的极致优化技术

.NET Core GC 的极致优化技术篇章&#xff1a;07-源码深度-.NET Core GC | 阅读时间&#xff1a;约 40 分钟一、引言.NET Core GC 的极致性能不仅来自高级算法&#xff0c;还来自大量底层的微优化技术。每项微优化单独看可能只提升 1-5%&#xff0c;但累积起来产生了巨大的性能…

作者头像 李华
网站建设 2026/10/27 2:00:36

ATMEL 单片机新手入门与快速上手指南

摘要:本文面向零基础嵌入式开发者,以 ATMEL 系列单片机为对象,完整梳理从入门到上手的实战路径。内容涵盖芯片核心特点与应用场景、开发环境与工具链安装、最小系统电路连接、LED 闪烁程序编写与烧录、GPIO 输入输出控制、定时器中断、串口通信、常见编译报错排查、仿真器调…

作者头像 李华
网站建设 2026/10/27 2:00:17

Modbus 通信从入门到实战:RTU、TCP、寄存器地址与 C# 解析一次讲透

一句话理解&#xff1a; Modbus 就像设备之间约定好的一套“问答规则”——上位机按照规则询问温度、压力或设备状态&#xff0c;仪表、PLC 等设备再按照规则返回数据。真正难的不是“发几个字节”&#xff0c;而是弄清楚读哪个地址、用什么功能码、数据如何解释&#xff0c;以…

作者头像 李华
网站建设 2026/10/27 1:58:27

一文看懂 Qwen3.8-Omni:面向音视频智能体的全模态生产框架

Qwen3.8-Omni 的核心目标&#xff0c;是让模型能够围绕音视频素材完成一套闭环任务&#xff1a;读懂多媒体内容、规划执行方案、调用配套工具、核验输出结果&#xff0c;再依据反馈持续迭代调整。这篇论文把模型训练体系、插件能力、实时调度框架整合在一起&#xff0c;探索全模…

作者头像 李华
网站建设 2026/10/27 1:58:27

Java没饱和!2026年AI风口下,复合型人才才是香饽饽,建议收藏!

初级Java开发虽饱和&#xff0c;但懂AI的复合型人才供不应求。文章指出Java生态成熟、应用广泛&#xff0c;且AI升级带来新需求。2026年入行需掌握扎实后端基础、AI协同及智能体实战能力&#xff0c;零基础半年可学&#xff0c;AIJava是高薪赛道。 最近很多人都在问&#xff1a…

作者头像 李华
网站建设 2026/10/27 1:58:15

商品名里的小于号,别让HTML把它读成标签

把商品资料整理成详情页说明时&#xff0c;一句“尺寸 < 10”或包含与号的名称&#xff0c;不应该被网页当成HTML结构。若直接把资料字符串拼进标签&#xff0c;原本要显示的文字可能被解释为标记。图片已经做好&#xff0c;并不表示旁边的文字也能原样、安全地进入页面&…

作者头像 李华