资讯详情

Linux 3.0.101 ARM平台移植RT-Preempt实时内核实战

📅 2026/10/13 1:24:27 | 华诺云谱 👁 阅读
Linux 3.0.101 ARM平台移植RT-Preempt实时内核实战
前阵子接了一个老项目的内核改造活要把一套跑在 ARM 平台上的 Linux 3.0.101 内核移植上 RT-PreemptPREEMPT_RT补丁做成正式的实时内核。整个过程从补丁合入、内核配置到板子上测延迟前前后后折腾了不少时间。今天不聊PPT式的流程就把我在这个移植任务里实际做的事情、为什么这么做、以及踩过的坑原原本本写出来给后面接同样活的兄弟做个参考。1. 项目背景与方案拆解1.1 为什么这个老内核还要移植RT-Preempt先澄清一个概念。RT-Preempt 不是一套独立的内核它是对 Linux 内核打上一整套补丁把 Linux 变成实时抢占内核的统称。社区通常叫它 PREEMPT_RT主线的实时补丁集合。它要解决的问题很直接让关键任务在给定的时限内得到调度和完成而不是被其他进程、中断或者内核临界区无限期地挡住。很多人会问标准 Linux 内核不是已经有 CONFIG_PREEMPT 吗为什么不够普通内核的抢占模型里大部分时候进程调度延迟是可以接受的但在工业控制、机器人、自动化设备里一个中断处理函数里关着抢占锁或者一个长耗时持锁的内核路径就可能让电机控制、传感器采样的时间点上出现不可接受的抖动。RT-Preempt 做的事情本质上是三块把中断处理程序变成可调度的内核线程也就是所谓的中断线程化。把大部分自旋锁替换成支持优先级继承的 rt_mutex避免优先级反转导致的不可控延迟。在高精度定时器等细节上提供 RT 所需的时钟语义。这么说吧老内核往往是因为产品生命周期长、外围驱动代码已经稳定、验证成本高才一直没动。但当设备要过新的响应性认证或现场频繁出现偶发超时时打 RT 补丁就比换一整套新内核现实得多。3.0.101 就是典型的老但还能打的内核。1.2 3.0.101与RT补丁的版本对应关系移植 RT-Preempt 的第一步是找到和内核版本精确匹配的补丁。Linux 3.0 分支的最后一个稳定版本就是 3.0.101社区当时维护的 RT 补丁一直跟到 3.0 系列末代。我这次用的补丁是 patch-3.0.101-rt107.patch.bz2。这里有个特别容易踩的坑不能随便拿一个 3.0.x 的 RT 补丁往 3.0.101 上套。RT 补丁和内核版本是逐个提交对应的差一个小版本都可能导致大量冲突。版本对齐是移植成功的大前提。为什么选择 3.0.101 而不是新内核原因很现实。现场设备是一块 ARM 处理器板子BSP、bootloader、驱动、文件系统全部基于 3.0.101 验证过如果升级主线大版本等于把 BSP 整个重写一遍成本和时间都不可控。在保持内核版本不变的前提下只通过 RT 补丁实现实时化是投入产出比最高的方案。1.3 移植整体链路与关键决策整个移植链路我拆成四段环境准备交叉编译工具链、内核源码、RT 补丁三者的对齐和验证。源码级移植把补丁合入处理与平台代码的冲突。内核配置与编译选择实时抢占模型裁剪无关功能生成可烧写的内核镜像。板级验证与调优启动确认实时特性生效跑 Cyclictest 测延迟再根据数据做参数调整。关键决策点在于是否引入设备树Device Tree。3.0.101 时代 ARM 平台处于从 ATAG 传参向设备树过渡的阶段很多老 BSP 还在用传统的板级初始化文件。我这次采用的方式是沿用该平台原有的 ATAG 启动方式不强制引入设备树因为 RT 补丁本身不依赖设备树改动范围越小移植风险越低。如果你的平台已经原生支持设备树那继续用 DT 也没问题RT 补丁对 DT 的兼容性在 3.0 时代已经可用。2. 环境准备工具链、源码与补丁获取2.1 交叉工具链选型ARM 平台交叉编译工具链的选择直接影响 RT 补丁的编译结果。我的原则是尽量使用与 BSP 厂商提供的工具链同源的编译器不要为了版本新而乱换。3.0.101 时代主流用的是 GCC 4.5 到 4.7 区间太老的编译器不支持新语法太新的编译器也可能引入无关的编译告警甚至优化问题。我这次用的是 arm-linux-gnueabi-gcc 4.6 系列。这里提醒一句如果你的板子是硬浮点VFPv3/NEON需要选 arm-linux-gnueabihf 工具链并且在内核配置里打开 CONFIG_VFP 和 CONFIG_NEON。硬浮点和软浮点混用会导致运行时莫名其妙的 SIGILL 崩溃。我在另一块板子上就吃过这个亏折腾半天最后发现是工具链浮点 ABI 不匹配。确认工具链版本的方法非常简单arm-linux-gnueabi-gcc -v执行后看 gcc version 和配置参数里的 --with-float 选项即可。2.2 内核源码与补丁对齐源码方面我直接在原来 BSP 的 git 仓库基础上重建了一个分支保证 BSP 里的平台改动不受影响。如果你手里只有 release 源码包也可以从 kernel.org 下载干净的 linux-3.0.101.tar.xz再手动叠加 BSP 补丁。不管哪种方式最后都要确认你工作目录里的内核版本确实是 3.0.101make kernelversionRT 补丁我建议从 Linux 内核官方网站的存档目录下获取文件名格式为 patch-3.0.101-rt107.patch.bz2。下载后建议做一次 SHA256 校验避免传输损坏。这里说一下为什么命名里有 rt107这个数字是补丁的迭代版本rt 系列补丁会不断修复架构相关的问题越靠后的迭代稳定性越好。对 ARM 平台来说选择该内核版本对应的最新 RT 补丁是正解。2.3 宿主机构建环境准备编译主机我用的是 Ubuntu 12.04 时代的系统环境但考虑到现在大多数人手头是更新的发行版需要注意一点老内核的构建脚本和新的编译工具链之间偶尔会有兼容问题。我推荐的做法是准备一个独立的构建目录把内核源码放在其中避免和源码树里的 build 产物混在一起mkdir -p ~/rt-arm-build cd ~/rt-arm-build tar xf linux-3.0.101.tar.xz cd linux-3.0.101 mkdir -p ../build_output后续编译时指定 O../build_output 输出目录这样如果编译中途搞坏了删掉 build_output 就能干净重来不用重新解压源码。这个习惯帮我省了无数时间。构建主机还需要安装以下工具make、gcc宿主平台、ncurses-dev用于 menuconfig、libssl-dev老内核不太需要但加上无害。有时还缺 uuid-dev编译某些老内核的 ext4 相关代码时会用到。缺什么装什么不要硬扛。3. 补丁合入与冲突排除3.1 补丁应用的标准流程拿到 patch 文件后把它放到内核源码根目录下然后执行bzcat patch-3.0.101-rt107.patch.bz2 | patch -p1 --dry-run先用 dry-run 模拟一遍看有没有冲突、冲突在哪些文件。这一步相当重要直接打万一打坏了回滚起来很麻烦。dry-run 输出里如果全是 patching file ... 没有 FAILED 字样说明补丁能干净合入。如果出现 failed就需要逐个文件手动解决。确认没问题后正式应用bzcat patch-3.0.101-rt107.patch.bz2 | patch -p1应用完成后建议 grep 一下内核 Makefile 的 EXTRAVERSION 字段正常情况下会多出 -rt107 后缀。这是我的第一个自检项head -5 Makefile看到 EXTRAVERSION -rt107 就说明补丁主体已经合入了。3.2 ARM平台常见的补丁冲突点RT 补丁因为要改动调度器、中断子系统、核心锁机制必然动到 arch/arm 下的代码。3.0.101 的 ARM 平台冲突点主要在几个地方第一是 arch/arm/kernel/entry-armv.S 和 entry-common.S。RT 补丁会修改中断和异常处理路径让中断可以在返回用户态时被线程化调度。如果你的 BSP 动过这里的汇编代码比如加了自己的 FIQ 处理或者调试钩子冲突就出现了。第二个高频冲突点是 arch/arm/mach-xxx 下的平台初始化文件。有些平台代码自己实现了 IRQ chip 的回调RT 补丁对 irq_chip 的结构体做了扩展导致结构体初始化列表对不上。这种冲突通常看起来吓人实际上解法很机械把补丁要求新增的字段按原型补上即可。第三个是 arch/arm/mm/highmem.c。ARM 的高端内存页表操作和 RT 补丁的 preemptible kmap 机制有交互如果你的板子内存小于 1GB根本用不到 highmem最简单的做法是在内核配置里关掉 HIGHMEM直接从源头上避开冲突。我的经验法则是冲突出现时先看冲突文件属于哪个子系统再判断 BSP 在该文件上的改动是否必要。很多老 BSP 的自定义修改早就过时了在移植 RT 时完全可以放弃优先保证主线补丁的完整性。3.3 怎么验证补丁合入干净补丁应用完、冲突都解决后我习惯做一次全量编译的提前自检不等到配置完再编译。先用默认配置编译一把make ARCHarm CROSS_COMPILEarm-linux-gnueabi- O../build_output defconfig make ARCHarm CROSS_COMPILEarm-linux-gnueabi- O../build_output zImage -j8这步目的是确认补丁合入后代码本身能编译通过。很多冲突是肉眼看不出来的比如某个宏定义被重复定义或者某个函数原型变了导致调用处类型不匹配。提前编译能把这些隐藏问题一次性暴露出来。编译出现错误时先看错误文件90% 的情况是某个 BSP 改动和 RT 补丁不兼容恢复该文件到补丁版本即可。4. 内核配置实时性开关与裁剪策略4.1 PREEMPT_RT核心配置项这一步是移植成败的分水岭。RT 补丁给内核添加了一整套配置项核心是抢占模型选择。在 menuconfig 里进入 Kernel Features你会看到实时抢占模型选项CONFIG_PREEMPT_NONE服务器式几乎不抢占吞吐优先。CONFIG_PREEMPT_VOLUNTARY自愿抢占内核主动让出点可抢占。CONFIG_PREEMPT常规低延迟抢占中断返回时执行抢占调度。CONFIG_PREEMPT_RT_FULL完整RT中断线程化所有非原子临界区可抢占。RT-Preempt 移植的自然选择是最后一个CONFIG_PREEMPT_RT_FULLy这个开关会连带打开一堆依赖项比如 CONFIG_RT_MUTEXES、CONFIG_PREEMPT_RCU、CONFIG_HIGH_RES_TIMERS 等。我不建议手动逐项去开直接选择 PREEMPT_RT_FULL 后让 Kconfig 的依赖系统去自动选择最合适组合。4.2 抢占模型与HZ/NO_HZ决策实时性和高精度时钟强相关。在 3.0.101 上我建议把内核时钟频率设为 1000Hz也就是 CONFIG_HZ_1000y。1000Hz 意味着系统节拍每 1ms 一次对实时任务的计时粒度是毫秒级。如果应用对时间粒度要求更高可以开启 CONFIG_HIGH_RES_TIMERS它在 RT 补丁下会使用硬件高精度定时器实际精度可以达到微秒级。这里有个容易误解的点高精度定时器开启后内核调度节拍本身还是 HZ 控制的但 hrtimer 机制允许用户态的 clock_nanosleep、定时器、POSIX timer 走独立的硬件定时中断所以 cyclictest 这类工具能测到几十微秒的延迟。不要妄想靠调 HZ 解决所有问题HZ 只是基础节拍实时性主要靠 PREEMPT_RT_FULL 和 hrtimer。CONFIG_NO_HZ动态节拍我建议在 RT 场景下默认关闭。NO_HZ 的本意是空闲时降低节拍频率省电但它会引入 tick 调度时机的抖动对确定性有害。如果做功耗敏感设备可以后续按需打开但初版移植不要动。下列表格是我在实际配置里确认的最核心配置项可以直接作为检查清单配置项推荐值作用说明CONFIG_PREEMPT_RT_FULLy启用完整实时抢占项目核心CONFIG_HZ_1000y1ms周期节拍兼顾响应与开销CONFIG_HIGH_RES_TIMERSy启用高精度定时器微秒级延迟基础CONFIG_NO_HZn关闭动态节拍保证Tick行为确定CONFIG_IRQ_FORCED_THREADINGy强制所有中断线程化可由启动参数控制CONFIG_RT_MUTEXESy优先级继承互斥锁避免优先级反转CONFIG_DEBUG_PREEMPTn仅在调试期开启最终关闭CONFIG_LOCKDEPn调试期可开正式版必关影响性能4.3 调试开关与按需裁剪刚开始移植时我会打开 CONFIG_DEBUG_PREEMPT、CONFIG_LOCKDEP 和 CONFIG_SCHEDSTATS。这些开关能把锁使用错误、调度统计打出来对发现 BSP 驱动中的潜在问题帮助极大。但正式部署时一定要关掉它们带来的性能损耗和额外日志会污染延迟数据。另外一个重点裁剪项是 CONFIG_DEBUG_RODATA 和 CONFIG_DEBUG_SET_MODULE_RONX。在老内核上这类内存保护选项会和 RT 补丁中的某些 exec 路径产生交互如果你的平台内存资源有限直接关闭。我的原则是与实时性无关的调试功能全关与硬件无关的子系统全关能编译成模块的编译成模块。文件系统方面如果根文件系统是 jffs2/ubifs注意保留对应的 yaffs 或 ubifs 支持。RT 补丁不会影响文件系统但延迟测试时文件系统的写操作会带来突发的磁盘IO延迟我一般把日志输出到一个 tmpfs 挂载的目录减少对真实存储的依赖。5. 编译烧写与启动验证5.1 完整编译流程配置完成后编译命令我习惯写成一条完整链路cd ~/rt-arm-build/linux-3.0.101 make ARCHarm CROSS_COMPILEarm-linux-gnueabi- O../build_output menuconfig make ARCHarm CROSS_COMPILEarm-linux-gnueabi- O../build_output zImage -j8如果是设备树平台还需要make ARCHarm CROSS_COMPILEarm-linux-gnueabi- O../build_output dtbs模块编译和安装make ARCHarm CROSS_COMPILEarm-linux-gnueabi- O../build_output modules -j8 make ARCHarm CROSS_COMPILEarm-linux-gnueabi- O../build_output INSTALL_MOD_PATH../rootfs modules_install生成的 zImage 在 build_output/arch/arm/boot/ 下。校验信息cat ../build_output/arch/arm/boot/zImage | grep -a Linux | head -1输出里包含 3.0.101-rt107 字样说明实时内核镜像已经生成。这一步是我每次编译完的固定操作确认版本字符串防呆。5.2 启动引导与内核参数我的板子用的是 U-Boot 老版本引导 ATAG 内核镜像没有用设备树。在 U-Boot 环境变量里需要确认启动参数包含合适的附加参数。RT 内核常用的 bootargs 推荐isolcpus1把 CPU1 从内核调度器的普通均衡中隔离出来专供实时任务使用。irqaffinity0把大多数设备中断绑定到 CPU0减少对实时 CPU 的打扰。threadirqs确保所有中断以线程方式运行。如果内核配置里没开 CONFIG_IRQ_FORCED_THREADING这个启动参数可以在系统启动时强制所有中断线程化。在 U-Boot 提示符下设置setenv bootargs consolettyS0,115200 root/dev/mmcblk0p2 rw rootwait isolcpus1 irqaffinity0 threadirqs saveenv boot这里说一个重要细节threadirqs 参数只保证中断 handler 的线程化但 Linux 里某些硬中断上下文比如 timer tick、IPI仍然不能在普通线程里运行。RT 补丁已经处理了大多数情况所以 threadirqs 启动参数是一种兜底方案而不是全部。5.3 启动后的实时性自检内核启动后第一件事是看 dmesg确认 RT 特性生效。检查 /proc/versioncat /proc/version如果显示 Linux version 3.0.101-rt107 就说明补丁版本正确。再检查内核抢占配置zcat /proc/config.gz | grep PREEMPT如果 CONFIG_PREEMPT_RT_FULLy 存在说明配置生效。这两项通过后还要看中断线程化是否落地ps aux | grep irq/正常会看到大量类似irq/33-mmc0、irq/42-can0形式的线程。看到这些就说明中断已经被内核线程化了。这一步过了RT 移植的源码工程部分就宣告完成剩下的就是性能验证和调优。6. 延迟实测与优化调整6.1 Cyclictest的正确用法实测环节我用的工具是 rt-tests 里的 cyclictest。它是 RT 领域的事实标准工具用线程周期睡眠测量实际唤醒延迟。编译 rt-tests 时注意和内核头文件配套交叉编译命令如下./configure --hostarm-linux-gnueabi --with-cyclictest make把编译好的 cyclictest 丢到板子上的 /usr/bin 里然后开始测cyclictest -t 5 -p 80 -i 1000 -l 1000000 -n -q参数说明-t 5启动 5 个测量线程。-p 80测试线程优先级 80属于SCHED_FIFO实时优先级范围。-i 1000每个线程的测量间隔单位微秒1000us就是1ms。-l 1000000每个线程循环 100 万次。-n使用 clock_nanosleep 进行高精度定时。-q安静模式只输出统计结果。运行结束后cyclictest 会输出每个线程的 min、avg、max 延迟。重点看 max 值它代表最坏情况延迟是实时系统最关心的指标。6.2 延迟数据怎么看拿到数据先别急着高兴。min 和 avg 很好看max 才是硬指标。我在这块 ARM 板子上初始测试的结果大致如下数据仅代表该平台测试项数值最小延迟6 us平均延迟18 us最大延迟235 us99.9% 延迟37 us初始 max 235us 是不能忍的。但我不会直接去改内核而是先分析原因。RT 内核的延迟来源一般有几类关闭中断的临界区太长。这通常是某个驱动在关中断状态下做了耗时的寄存器操作。非线程化的处理器间中断IPI或时钟 tick 造成的调度延迟。内存分配在临界区里触发了页表操作。电源管理空闲模式下的唤醒延迟。235us 的毛刺更可能来自某个驱动在硬中断上下文里做了太多事。排查方法是用 ftrace 的 irqsoff 追踪器echo irqsoff /sys/kernel/debug/tracing/current_tracer echo 1 /sys/kernel/debug/tracing/tracing_on # 让系统跑一会儿 cat /sys/kernel/debug/tracing/trace追踪结果会告诉你最长的一次中断关闭发生的内核函数调用栈。我拿到这段栈后基本就能定位到具体驱动模块。通常的办法是把该驱动的中断处理改成只能做极简操作重活全丢给下半部或者工作队列。RT 内核里中断线程化本身就允许在 handler 里做更多事但如果这个 handler 还是被标注为 IRQF_NO_THREAD它仍然运行在硬中断上下文就得动手改造。6.3 我在这套环境上做的关键优化实测后我做了三个关键调整最终把 max 压到了 120us 左右正常工作场景下更稳定。第一隔离 CPU。在 bootargs 里加了isolcpus1之后实时任务绑核运行在 CPU1内核的所有普通调度活动都集中在 CPU0。这样做的逻辑是CPU1 不参与进程负载均衡不会有其他进程被调进来打断实时任务。绑定方式是在应用里用 sched_setaffinity或者在启动脚本中用 taskset 设置。第二把关键驱动的实时线程优先级提高。中断线程的名字里通常带优先级属性通过/proc/irq/N/effective_affinity和 chrt 命令调整线程调度策略。比如把 eMMC 中断线程调整到普通优先级以下把网络中断线程提高到和实时线程相同等级。优先级反转问题在自行调整时常常出现所以我更建议使用内核参数。第三关闭 CPU 空闲深度。ARM 平台的 cpuidle 会在 CPU 空闲时进入更深睡眠这会增加唤醒延迟。在 RT 场景下我干脆把 cpuidle 的深度状态打开打印cat /sys/devices/system/cpu/cpu0/cpuidle/state*/usage如果发现 CPU 频繁进入深度状态直接在启动参数里加cpuidle.off1关闭深度空闲。功耗多一点点但唤醒延迟可以显著减少。这就是实时性和功耗之间的取舍。优化后再跑一次 cyclictestmax 降到 120us 左右av 也不错。对这个应用场景控制周期是 5ms这个延迟完全在预算之内。这一步做完移植任务基本就收尾了。7. 常见问题与排查技巧实录7.1 编译阶段的经典报错我把编译期的常见报错整理成表格遇到直接对照。报错现象常见原因处理方式arch/arm/mach-xxx/gpio.c: error: unknown field nameBSP 自定义结构和RT补丁的结构体扩展冲突回到补丁前的文件手动合并RT新增字段include/linux/spinlock_types.h: 编译错误RT补丁后另一个BSP补丁仍引用旧结构体重打BSP补丁调整补丁顺序undefined reference to__rt_mutex_init依赖的锁函数在模块中未导出检查内核配置是否打开RT_MUTEXESKernel: arch/arm/boot/zImage is ready但启动卡住内核配置中漏掉了板子必需的驱动对比原BSP的defconfig补齐驱动配置编译期问题大多能通过恢复文件、调整补丁应用顺序解决。有一个比较隐蔽的问题是RT 补丁和 BSP 补丁应用顺序不同会导致不同的冲突结果。我习惯的顺序是先打内核官方基线再打 RT 补丁最后打 BSP 补丁。BSP 补丁通常是在官方内核基础上改的如果不打 RT直接打BSP没问题但打了 RT 后BSP 里涉及锁和中断初始化的改动很容易冲突。反过来如果先打 BSP 再打 RTRT 补丁会直接报大段 rejected。所以顺序上推荐最后打 BSP并针对冲突手工处理。7.2 运行阶段问题板子能启动但实时性不达标是另一个大类。我遇到过的典型问题有两个。第一个是cyclictest输出偶尔出现几万个微秒级别的毛刺。查下来是网络驱动在中断线程里做了太多协议栈处理。解决办法是把网卡中断的IRQ_AFFINITY设置成非实时核心或者干脆把 RPSReceive Packet Steering关闭。第二个是cyclictest最大延迟在高负载下暴涨。原因是 RT 任务的优先级虽然高但仍然需要和其他实时任务抢 CPU。线程化中断之后每个外设中断都变成实时优先级线程几十个中断线程同时可运行调度器压力就大了。我这里的做法是用chrt -p逐一检查并降低外设中断线程的优先级只保留最关键的几个如 PWM、编码器在最高优先级。7.3 与硬件/驱动相关的坑这个项目里最折腾的坑是某块外设驱动在中断回调里直接调用了mdelay。在 RT 内核里mdelay是忙等待本质是死循环占住 CPU而且它会屏蔽当前 CPU 的抢占调度导致同核心上的所有实时任务全部延迟。查到之后我改成使用msleep()配合线程上下文调用问题立刻消失。另一个硬件相关的坑是 DMA 内存。老驱动的 DMA 缓冲区经常用kmalloc分配但在 RT 内核中kmalloc可能触发页表操作和内存调度如果发生在实时上下文里就埋雷。建议对 DMA 使用dma_alloc_coherent或预留内存区域并确保在驱动初始化阶段就完成分配和映射运行时不再动态分配大块内存。再强调一个普遍的检查点如果你的 ARM 平台有 FIQ 中断比如某些 SoC 的快速中断接口RT 补丁对 FIQ 的支持是有限的。FIQ 永远在硬中断上下文执行不参与线程化。如果应用强依赖 FIQ那你需要评估 FIQ 处理函数本身的耗时把它控制在微秒级。能不用 FIQ 就尽量不用。8. 写在最后的一点体会移植 RT-Preempt 到 ARM 老内核这件事做完之后回头看真正难的不是把补丁打上去而是理解这颗内核为什么以这样的方式运作以及怎么在实时和原有系统稳定性之间找到平衡。我个人在实际操作中的体会是RT 补丁本质上是一套系统性的工程改造不是魔法。它的很多设计以牺牲少量吞吐为代价换来确定性所以移植时的每一个配置决策都要问自己这对实时性有什么影响。另一个经验是不要一开始就追求极限延迟数据先把功能跑对再把延迟数据测出来最后针对最大毛刺逐个击破。如果后续还要在这个系统上继续扩展可以考虑把更多应用层的实时逻辑移到独立的实时线程或 CPU 隔离环境里进一步减少干扰。甚至可以逐步评估较新内核的 RT 支持迁移成本会高一些但过程中的调试能力是可以复用的。就这样这套移植的完整路径和那些见不得光的坑都在上面了。有类似需求的同学希望你一次成功。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑