Linux 调度器中的 CPU 隔离:实时场景下的干扰屏蔽方案
摘要: CPU隔离技术通过isolcpus、nohz_full和rcu_nocbs三重机制,将特定CPU核心从Linux通用调度域中移除,构建专供实时任务使用的"静默核心"。该技术可显著降低延迟抖动(从8ms降至500μs),满足工业自动化等高实时性场景需求。文章详细解析了隔离原理、配置步骤(GRUB参数、内核线程迁移、中断绑定等),并提供了实时任务编程示例和性能验证方法。随着
一、简介:为什么需要CPU隔离?
在现代工业自动化与高性能计算领域,确定性延迟(Deterministic Latency)往往比平均性能更重要。一个典型的PLC(可编程逻辑控制器)控制循环可能要求500μs内完成数据采集-计算-输出,任何抖动都可能导致机械臂失控或产线停机。
Linux作为通用操作系统,其调度器设计追求吞吐量最大化而非延迟确定性。默认情况下,内核调度器会在所有CPU核心间均衡负载,硬件中断、定时器中断、RCU回调、内核线程等都会"抢占"CPU时间,造成调度抖动(Scheduling Jitter)——这正是实时应用的最大敌人。
CPU隔离(CPU Isolation)技术通过将特定CPU核心从通用调度域中移除,配合nohz_full(全动态时钟)和rcu_nocbs(RCU回调卸载),构建出一个"静默核心"(Silent Core),专供实时任务独占使用。根据5G虚拟化PLC实测数据,经过完整CPU隔离配置后,最大处理延迟从8ms降至500μs,抖动控制在100μs以内。
二、核心概念:隔离机制的三重防线
2.1 isolcpus:调度器层面的隔离
isolcpus是最基础的隔离参数,它将指定CPU从内核的SMP负载均衡器(Symmetric Multi-Processing Balancer)中移除。被隔离的CPU不会参与CFS(完全公平调度器)的任务分配,只有显式绑定(taskset、sched_setaffinity)的任务才能运行其上。
关键特性:
-
仅隔离用户空间任务,内核线程仍可能在隔离CPU上运行
-
需配合
nohz_full才能实现完整隔离 -
从Linux 5.4开始支持
isolcpus=nohz,domain,1-3语法,分别控制nohz、调度域、以及传统隔离
2.2 nohz_full:时钟中断的终结者
传统Linux内核每秒产生250-1000次定时器中断(Tick),用于更新进程统计、触发调度检查、维护jiffies计数。nohz_full(Full Dynamic Ticks)在指定CPU上进入全动态时钟模式,当仅有一个可运行任务时,完全关闭周期时钟中断。
技术细节:
-
需要内核配置
CONFIG_NO_HZ_FULL=y -
自动启用RCU NOCB模式,将回调卸载到非隔离CPU
-
仍保留1Hz的残留时钟用于内核统计(Kernel 4.19+可完全消除)
2.3 rcu_nocbs:RCU回调的卸载
RCU(Read-Copy-Update)是Linux内核的核心同步机制,其回调处理可能引入微秒级延迟。rcu_nocbs将指定CPU的RCU回调卸载到专门的rcuog/rcuop内核线程,在隔离CPU上完全避免RCU处理开销。
协同机制:当设置nohz_full时,内核会自动将对应CPU加入rcu_nocbs集合,无需重复指定。
2.4 isolation.c:隔离策略的统一实现
从Linux 4.x开始,内核将CPU隔离逻辑统一实现在kernel/sched/isolation.c中。该文件定义了Housekeeping CPU(管家CPU)与Isolated CPU(隔离CPU)的概念框架:
-
Housekeeping CPU:运行所有内核线程、中断处理、系统服务,承担"脏活累活"
-
Isolated CPU:专用于实时负载,追求极致的静默与确定性
housekeeping_setup()函数解析启动参数,构建non_housekeeping_mask,为后续调度器、时钟子系统、RCU提供隔离掩码。
三、环境准备:构建实时Linux实验平台
3.1 硬件环境要求
| 组件 | 最低要求 | 推荐配置 | 说明 |
|---|---|---|---|
| CPU | x86_64双核 | Intel Xeon/AMD EPYC 8核+ | 需支持Intel VT-x或AMD-V |
| 内存 | 4GB | 16GB+ | 实时应用通常内存密集 |
| 存储 | 20GB SSD | NVMe SSD | 减少I/O等待 |
| 网卡 | 千兆以太网 | Intel i210/i350(支持TSN) | 工业场景需硬件时间同步 |
NUMA与超线程注意事项:
-
若启用超线程(Hyper-Threading),必须成对隔离物理核心的两个逻辑线程。通过
/sys/devices/system/cpu/cpu*/topology/thread_siblings_list查看线程对应关系 -
NUMA架构下,建议将隔离CPU与实时任务使用的内存置于同一Node,避免跨节点访问延迟
3.2 软件环境配置
操作系统选择(三选一):
-
Ubuntu Pro Real-Time(推荐新手)
# Ubuntu 22.04/24.04 LTS sudo apt install ubuntu-realtime # 内核自动包含PREEMPT_RT补丁 -
RHEL/CentOS Stream RT
sudo yum install kernel-rt kernel-rt-devel -
主线内核 + PREEMPT_RT(推荐研究者)
# 下载内核源码(以6.6为例) wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.tar.xz wget https://cdn.kernel.org/pub/linux/kernel/projects/rt/6.6/patch-6.6.14-rt21.patch.gz
关键内核配置选项:
# 必须开启
CONFIG_PREEMPT_RT=y # 实时抢占(Linux 6.12+已合入主线)[^49^]
CONFIG_NO_HZ_FULL=y # 全动态时钟
CONFIG_RCU_NOCB_CPU=y # RCU回调卸载
CONFIG_CPU_ISOLATION=y # CPU隔离支持
CONFIG_CGROUPS=y # cgroups v2支持
# 建议开启
CONFIG_IRQ_FORCED_THREADING=y # 强制线程化中断
CONFIG_HIGH_RES_TIMERS=y # 高精度定时器
3.3 工具链安装
# 性能分析工具
sudo apt install rt-tests stress-ng sysstat hwloc
# 实时任务绑定工具
sudo apt install util-linux # 包含taskset
sudo apt install tuna # 图形化IRQ与线程管理
# 开发库
sudo apt install libnuma-dev linux-headers-$(uname -r)
四、应用场景:CPU隔离在工业4.0中的实战价值
在5G虚拟化PLC(vPLC)场景中,CPU隔离技术发挥着决定性作用。某工业网关项目需在一台x86服务器上同时运行:
-
vPLC运行时:1ms控制循环,抖动要求<100μs,负责机械臂轨迹规划
-
5G UPF:用户面转发,吞吐量10Gbps,延迟要求<1ms
-
容器化监控:Prometheus+Grafana,资源占用不确定
冲突点:UPF的高频网络中断与监控容器的突发内存分配,会严重干扰vPLC的实时性。
解决方案架构:
-
CPU 0-1:Housekeeping CPUs,运行UPF、监控容器、所有内核线程与中断
-
CPU 2-3:Isolated CPUs,专供vPLC使用,关闭时钟中断,卸载RCU回调
-
中断亲和性:网卡中断绑定到CPU 0-1,禁止路由到CPU 2-3
-
内存隔离:通过
numactl将vPLC内存绑定到Node 0,与CPU 2-3物理邻近
实测效果:在8000pps(包每秒)负载下,vPLC控制循环最大延迟从8ms降至300μs,99.9百分位延迟稳定在85μs以内,满足IEC 61131-3硬实时标准。
五、实战案例:从零配置CPU隔离环境
5.1 步骤一:GRUB启动参数配置
编辑GRUB配置文件,添加"黄金组合"参数:
sudo vim /etc/default/grub
# 找到GRUB_CMDLINE_LINUX_DEFAULT,修改为:
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash \
isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3 \
rcu_nocb_poll \
irqaffinity=0,1 \
nosoftlockup \
nmi_watchdog=0 \
intel_pstate=disable \
processor.max_cstate=0"
参数详解:
| 参数 | 作用 | 取值建议 |
|---|---|---|
isolcpus=2,3 |
隔离CPU 2和3,移除出调度域 | 保留CPU 0-1给系统 |
nohz_full=2,3 |
在隔离CPU上启用全动态时钟 | 与isolcpus保持一致 |
rcu_nocbs=2,3 |
将RCU回调从隔离CPU卸载 | 自动生效,显式指定更安全 |
rcu_nocb_poll |
使用轮询代替唤醒处理RCU回调 | 避免唤醒延迟,牺牲一点CPU |
irqaffinity=0,1 |
将所有中断绑定到CPU 0-1 | 防止中断落在隔离CPU |
nosoftlockup |
禁用软锁检测(减少时钟中断) | 生产环境慎用 |
nmi_watchdog=0 |
禁用NMI看门狗 | 避免不可屏蔽中断 |
intel_pstate=disable |
禁用Intel P-State驱动 | 避免频率切换延迟 |
processor.max_cstate=0 |
禁止CPU进入深度睡眠 | 避免C-State唤醒延迟 |
更新GRUB并重启:
sudo update-grub
sudo reboot
5.2 步骤二:验证隔离配置
重启后,通过sysfs验证隔离状态:
# 查看被隔离的CPU
cat /sys/devices/system/cpu/isolated
# 预期输出:2-3
# 查看nohz_full生效情况
cat /sys/devices/system/cpu/nohz_full
# 预期输出:2-3
# 查看当前在线CPU
cat /sys/devices/system/cpu/online
# 预期输出:0-3
# 检查内核启动参数
cat /proc/cmdline | tr ' ' '\n' | grep -E "isolcpus|nohz_full|rcu_nocbs"
5.3 步骤三:迁移内核线程
即使配置了isolcpus,部分内核线程仍可能运行在隔离CPU上,必须手动迁移:
#!/bin/bash
# move_kthreads.sh - 将内核线程迁移到Housekeeping CPUs
# 目标CPU:0和1(CPU掩码0x3)
HOUSEKEEPING_MASK=0x3
echo "迁移内核线程到CPU 0-1..."
# 方法1:使用taskset(精确但繁琐)
for pid in $(ps -eo pid,comm | awk '/kworker|ksoftirq|kthread|migration|rcu/ {print $1}'); do
taskset -p $HOUSEKEEPING_MASK "$pid" 2>/dev/null
done
# 方法2:使用tuna工具(推荐)
sudo tuna --cpus=2-3 --isolate # 自动将线程移出CPU 2-3
echo "迁移完成,验证中..."
ps -eo pid,psr,comm | awk '$2 == 2 || $2 == 3 {print $0}'
# 应无输出或仅有明确绑定的实时任务
5.4 步骤四:配置中断亲和性
硬件中断是实时性的主要威胁,需强制绑定到非隔离CPU:
#!/bin/bash
# setup_irq_affinity.sh - 配置中断亲和性
# 查看当前中断分布
echo "=== 当前中断统计 ==="
cat /proc/interrupts | head -30
# 将所有可移动中断绑定到CPU 0-1(掩码0x3)
for irq_dir in /proc/irq/[0-9]*; do
irq_num=$(basename $irq_dir)
# 跳过不可修改的中断(如timer、IPI)
if [ -f "$irq_dir/smp_affinity" ]; then
current=$(cat $irq_dir/smp_affinity 2>/dev/null)
if [ "$current" != "1" ]; then # 跳过强制绑定到CPU0的中断
echo "3" | sudo tee $irq_dir/smp_affinity > /dev/null 2>&1
echo "IRQ $irq_num: $current -> 0x3"
fi
fi
done
# 禁用irqbalance服务(它会动态调整中断 affinity)
sudo systemctl stop irqbalance
sudo systemctl disable irqbalance
echo "=== 配置后中断统计 ==="
cat /proc/interrupts | head -30
注意:部分中断(如本地定时器中断LOC、IPI)无法迁移,这是硬件架构限制,不影响隔离效果。
5.5 步骤五:实时任务编程与绑定
以下是一个完整的实时任务示例,演示如何绑定到隔离CPU并设置FIFO调度策略:
/*
* rt_isolated_task.c - 运行在隔离CPU上的实时任务示例
* 编译:gcc -o rt_task rt_isolated_task.c -lpthread -lnuma
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sched.h>
#include <unistd.h>
#include <sys/mman.h>
#include <numa.h>
#include <time.h>
#include <signal.h>
#define ISOLATED_CPU 2 // 目标隔离CPU
#define RT_PRIORITY 99 // FIFO最高优先级
#define INTERVAL_US 1000 // 1ms控制周期
volatile int running = 1;
void signal_handler(int sig) {
running = 0;
}
// 锁定内存,防止页错误延迟
void lock_memory() {
if (mlockall(MCL_CURRENT | MCL_FUTURE) == -1) {
perror("mlockall failed");
exit(1);
}
printf("内存已锁定,防止交换\n");
}
// 绑定到指定CPU
void bind_to_cpu(int cpu_id) {
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(cpu_id, &cpuset);
if (pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset) != 0) {
perror("CPU绑定失败");
exit(1);
}
printf("线程绑定到CPU %d\n", cpu_id);
}
// 设置FIFO实时调度策略
void set_rt_priority(int priority) {
struct sched_param param;
param.sched_priority = priority;
if (pthread_setschedparam(pthread_self(), SCHED_FIFO, ¶m) != 0) {
perror("设置FIFO优先级失败");
exit(1);
}
printf("实时优先级设置为FIFO:%d\n", priority);
}
// NUMA内存绑定(可选,减少跨节点访问)
void bind_numa_node(int cpu_id) {
int node = numa_node_of_cpu(cpu_id);
if (node >= 0) {
nodemask_t nodemask;
nodemask_zero(&nodemask);
nodemask_set(&nodemask, node);
numa_bind(&nodemask);
printf("NUMA内存绑定到Node %d\n", node);
}
}
// 高精度延时(使用忙等避免睡眠)
void precise_sleep_us(long us) {
struct timespec start, now;
clock_gettime(CLOCK_MONOTONIC, &now);
start = now;
long target_ns = now.tv_nsec + us * 1000;
long target_sec = now.tv_sec + target_ns / 1000000000;
target_ns %= 1000000000;
// 忙等直到目标时间(避免调度器介入)
while (now.tv_sec < target_sec ||
(now.tv_sec == target_sec && now.tv_nsec < target_ns)) {
clock_gettime(CLOCK_MONOTONIC, &now);
}
}
void* rt_control_loop(void* arg) {
struct timespec start, end;
long max_latency = 0, min_latency = 1000000, total_latency = 0;
int iterations = 0;
// 配置实时环境
bind_to_cpu(ISOLATED_CPU);
set_rt_priority(RT_PRIORITY);
bind_numa_node(ISOLATED_CPU);
lock_memory();
printf("实时控制循环启动,目标周期:%d us\n", INTERVAL_US);
clock_gettime(CLOCK_MONOTONIC, &start);
while (running && iterations < 10000) {
// 记录周期开始
clock_gettime(CLOCK_MONOTONIC, &end);
// 计算实际周期(用于抖动分析)
long latency_us = (end.tv_sec - start.tv_sec) * 1000000 +
(end.tv_nsec - start.tv_nsec) / 1000;
if (latency_us > max_latency) max_latency = latency_us;
if (latency_us < min_latency) min_latency = latency_us;
total_latency += latency_us;
// 模拟控制计算(约100us处理时间)
volatile int dummy = 0;
for (int i = 0; i < 100000; i++) dummy += i;
// 精确等待到下一个周期(忙等模式)
precise_sleep_us(INTERVAL_US - 100); // 减去处理时间
start = end;
iterations++;
}
printf("\n=== 性能统计 ===\n");
printf("迭代次数:%d\n", iterations);
printf("最小周期:%ld us\n", min_latency);
printf("最大周期:%ld us\n", max_latency);
printf("平均周期:%ld us\n", total_latency / iterations);
printf("抖动范围:%ld us\n", max_latency - min_latency);
return NULL;
}
int main() {
signal(SIGINT, signal_handler);
pthread_t rt_thread;
pthread_attr_t attr;
pthread_attr_init(&attr);
// 设置栈大小(避免运行时分配)
pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN * 2);
if (pthread_create(&rt_thread, &attr, rt_control_loop, NULL) != 0) {
perror("线程创建失败");
return 1;
}
pthread_join(rt_thread, NULL);
return 0;
}
编译与运行:
gcc -o rt_task rt_isolated_task.c -lpthread -lnuma -O2
sudo ./rt_task
5.6 步骤六:性能验证与对比
使用cyclictest工具测量隔离前后的延迟差异:
# 安装rt-tests
sudo apt install rt-tests
# 测试1:非隔离CPU上的延迟(基线)
sudo cyclictest -m -S -p 99 -i 1000 -l 10000 -a 0
# -m: 锁定内存 -S: 使用sys_nanosleep -p 99: FIFO优先级99
# -i 1000: 1ms间隔 -l 10000: 10000次循环 -a 0: 绑定CPU 0
# 测试2:隔离CPU上的延迟(优化后)
sudo cyclictest -m -S -p 99 -i 1000 -l 10000 -a 2
# -a 2: 绑定到隔离CPU 2
# 测试3:多核并行压力测试(验证隔离效果)
# 终端1:在隔离CPU上运行实时任务
sudo taskset -c 2 cyclictest -m -S -p 99 -i 500 -l 100000
# 终端2:在其他CPU上施加压力
stress-ng --cpu 4 --io 4 --vm 4 --timeout 60s
预期结果对比:
| 场景 | 最小延迟 | 平均延迟 | 最大延迟 | 抖动 |
|---|---|---|---|---|
| 非隔离CPU(有负载) | 1μs | 5μs | 5000μs+ | 高 |
隔离CPU(isolcpus) |
1μs | 3μs | 50μs | 中 |
完整隔离(+nohz_full+IRQ隔离) |
1μs | 2μs | 15μs | 极低 |
六、常见问题与解答
Q1: 配置isolcpus后,为什么top仍显示隔离CPU有负载?
原因:isolcpus仅隔离用户空间任务,内核线程、中断处理、定时器中断仍可能在隔离CPU上运行。
解决:
-
检查并迁移内核线程(见5.3步骤)
-
确认
nohz_full已启用:cat /sys/devices/system/cpu/nohz_full -
使用
tuna工具彻底隔离:sudo tuna --cpus=2-3 --isolate
Q2: nohz_full配置后,dmesg显示"NO_HZ: Full dynticks CPUs: 2-3",但仍有1Hz时钟中断?
原因:早期内核(<4.19)即使配置nohz_full,仍会保留1Hz统计时钟。
解决:
-
升级内核至5.x或更新版本
-
确保仅有一个任务绑定到隔离CPU(多任务会重新启用时钟)
-
检查
CONFIG_NO_HZ_FULL=y确已编译进内核
Q3: 如何验证RCU回调已成功卸载?
验证方法:
# 查看RCU回调线程
ps aux | grep rcu
# 应看到rcuog(RCU offloading kthreads)运行在非隔离CPU
# 例如:rcuog/2, rcuog/3 应绑定到CPU 0-1
# 实时监控RCU活动
watch -n 1 'cat /proc/rcuog'
# 隔离CPU上不应有RCU回调堆积
Q4: 超线程(SMT)环境下如何正确隔离?
关键原则:必须成对隔离逻辑线程。
# 查看线程 siblings(物理核心对应关系)
cat /sys/devices/system/cpu/cpu0/topology/thread_siblings_list
# 输出示例:0,4 表示CPU 0和4是同一物理核心的两个线程
# 正确配置:隔离物理核心2(包含逻辑CPU 2和6)
GRUB_CMDLINE_LINUX="isolcpus=2,6 nohz_full=2,6 ..."
错误配置:仅隔离CPU 2而不隔离CPU 6,会导致CPU 6上的活动通过共享缓存影响CPU 2的实时性。
Q5: 容器(Docker/K8s)中如何使用CPU隔离?
方案:使用cgroups v2的cpuset控制器配合CPU隔离:
# 创建专用cgroup
sudo mkdir -p /sys/fs/cgroup/rt_tasks
echo "2-3" | sudo tee /sys/fs/cgroup/rt_tasks/cpuset.cpus
echo "0" | sudo tee /sys/fs/cgroup/rt_tasks/cpuset.mems # NUMA node 0
# 将容器放入该cgroup
docker run --cgroup-parent=/rt_tasks --privileged -it rt-app:latest
# 或使用systemd资源控制
systemctl set-property docker-xxx.scope AllowedCPUs=2-3
注意:容器内仍需设置SCHED_FIFO和内存锁定,cgroup仅限制CPU范围,不保证实时性。
七、最佳实践与调试技巧
7.1 调试清单:隔离不生效排查流程
#!/bin/bash
# debug_isolation.sh - CPU隔离问题诊断脚本
echo "=== 1. 检查启动参数 ==="
cat /proc/cmdline | tr ' ' '\n' | grep -E "isolcpus|nohz_full|rcu_nocbs"
echo -e "\n=== 2. 验证sysfs状态 ==="
echo "Isolated CPUs: $(cat /sys/devices/system/cpu/isolated)"
echo "Nohz_full CPUs: $(cat /sys/devices/system/cpu/nohz_full)"
echo -e "\n=== 3. 检查隔离CPU上的活动 ==="
for cpu in 2 3; do
echo "--- CPU $cpu ---"
echo "运行中的任务:"
ps -eo pid,psr,comm,rtprio | awk -v c=$cpu '$2==c {print $0}'
echo "中断统计:"
cat /proc/interrupts | awk -v c=$cpu 'NR==1 {print $0} {print $1, $(c+2)}' | head -10
done
echo -e "\n=== 4. 检查RCU卸载状态 ==="
grep "Offload RCU callbacks" /proc/rcuinfo 2>/dev/null || cat /proc/rcuinfo | head -20
echo -e "\n=== 5. 时钟中断检查 ==="
grep "LOC" /proc/interrupts | head -5 # 本地定时器中断应只在非隔离CPU增加
echo -e "\n=== 6. 内核线程分布 ==="
ps -eo pid,psr,comm | awk '/kworker|ksoftirq|rcu/ {print $0}' | sort -k2
7.2 性能优化建议
-
禁用C-State深度睡眠:
# 在GRUB中添加:intel_idle.max_cstate=0 processor.max_cstate=0 # 或运行时设置: sudo cpupower idle-set -D 2 # 限制最深C-State到C2 -
禁用Turbo Boost(避免频率切换不确定性)
echo 1 | sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo -
使用 hugepages 减少TLB miss:
# 预留1GB hugepages echo 1024 | sudo tee /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages # 在程序中使用mmap(MAP_HUGETLB)分配内存 -
避免系统调用:实时循环内使用
vdso提供的clock_gettime,避免进入内核。
7.3 生产环境部署建议
-
监控:使用
bpftrace或perf实时监控隔离CPU上的上下文切换次数,异常增加表明隔离失效 -
冗余:关键任务采用双核热备(CPU 2和3运行相同任务,硬件投票输出)
-
日志:将
dmesg中的nohz_full相关警告重定向到独立日志,便于审计
八、总结与应用展望
CPU隔离技术通过isolcpus、nohz_full、rcu_nocbs三重机制,结合kernel/sched/isolation.c的统一管理,为Linux实时应用提供了接近裸机的执行环境。在工业4.0、自动驾驶、高频交易等场景中,这种确定性延迟能力已成为系统设计的核心要求。
随着Linux 6.12将PREEMPT_RT合入主线,实时Linux的部署门槛大幅降低。未来,CPU隔离将与eBPF(用于内核可编程中断路由)、io_uring(异步I/O避免系统调用)、CXL内存(NUMA拓扑重构)等技术深度融合,构建出更强大的异构实时计算平台。
对于开发者而言,掌握CPU隔离不仅是优化性能的手段,更是理解Linux调度子系统、中断子系统、同步机制(RCU)的绝佳切入点。建议读者在实验环境中反复测试本文提供的代码,结合ftrace和bpftrace深入观察内核行为,将理论知识转化为实战能力。
参考资料与延伸阅读:
-
Linux内核源码:
kernel/sched/isolation.c,kernel/time/tick-sched.c,kernel/rcu/tree_nocb.c -
内核文档:
Documentation/admin-guide/kernel-parameters.rst,Documentation/timers/no_hz.rst
更多推荐



所有评论(0)