【深入内核】Linux 内核栈详解:你所需要知道的一切

🔍【深入内核】Linux 内核栈详解:你所需要知道的一切

内核调试经常遇到栈溢出、Oops、watchdog 死锁等问题?内核栈搞不清楚会非常致命。这篇文章将用清晰的结构,带你彻底搞懂 Linux 的 内核栈 —— 什么是内核栈、为什么重要、常见误区、如何避免踩坑。


🧠 一、什么是内核栈?

内核栈(Kernel Stack)是 Linux 为每个线程在 运行内核代码时专用的一块栈空间
当用户进程通过系统调用、中断或异常「进入内核」后,Linux 不再使用用户栈,而是切换到它自己的内核栈:

1
2
用户态 → 系统调用 / 中断 → 内核态
↘ 使用内核栈

内核栈用于保存:

  • 函数调用链(call stack)
  • 局部变量
  • 寄存器上下文
  • 中断处理信息

🧵 二、每个线程都有一块内核栈

  • 每个 task_struct 对应一块独立的内核栈。
  • 不管线程是否在用户态运行,内核栈始终分配好。
  • 当进入内核态(syscall / 异常 / 中断)时,就会切换到该线程的内核栈。

📏 三、内核栈有多大?

在 ARM64 架构下,默认是 16KB,不能扩展!

栈属性 说明
默认大小 16KB(ARM64)
分配方式 与 thread_info 共享
是否可扩展 ❌ 否
每个线程独立 ✅ 是

📦 四、内核栈中都装了什么?

  • 函数局部变量
  • 调用参数和返回地址
  • 中断上下文 / 异常上下文
  • 调用链回溯信息

举个例子,系统调用 read() 会导致内核中多层函数调用,整个过程都在内核栈中执行:

1
2
3
4
5
read()
└── vfs_read()
└── do_sync_read()
└── driver_read()
↘ buffer[], local vars 等

⚠️ 五、内核栈的常见“死亡操作”

❌ 5.1 在栈上分配大数组(>1KB)

如果你这样写:

1
char buf[16 * 4 * 1024];  // = 64KB,直接炸栈 💥

100% 会导致内核栈溢出,crash,甚至 watchdog reboot。

正确的做法:动态分配

使用 kmalloc()__get_free_pages()

1
2
3
4
5
6
char *buf = kmalloc(4096, GFP_KERNEL);
if (!buf)
return -ENOMEM;

// 使用完毕后释放
kfree(buf);

或:

1
2
3
char *buf = (char *)__get_free_pages(GFP_KERNEL, 2);  // 2^2 pages = 4 pages = 16KB
...
free_pages((unsigned long)buf, 2);

❌ 5.2 返回栈上变量的地址

1
2
3
4
char *danger() {
char temp[128];
return temp; // ❌ 返回野指针
}

🧠 六、内核栈 vs 用户栈 对比表

属性 用户栈 内核栈
使用场景 用户态代码 内核态代码
分配方式 动态(可扩展) 固定分配(每线程)
大小 几 MB 16KB(ARM64 默认)
是否可信 ❌ 不可信 ✅ 可信
是否共享 ❌ 不共享 ❌ 不共享

🛠️ 七、调试内核栈使用的方法

7.1 开启内核编译选项:

1
CONFIG_DEBUG_STACK_USAGE=y

可以在调度时打印出每个线程的最大栈使用。


7.2 使用 crash 工具:

1
crash> bt -a

查看所有进程栈回溯和使用情况。


7.3 查看 /proc

1
cat /proc/<pid>/stack

显示当前任务在内核态的调用栈。


🧨 八、内核栈溢出日志示例

一旦栈爆了,通常会看到类似错误:

1
2
3
kernel stack overflow detected
Internal error: Oops - BUG: 0 [#1] SMP
task: kworker/0:1, pid: 15, stack limit = 0xffff800011280000

此类错误多数来源于中断上下文使用了过大的栈,或者函数嵌套调用层次太深。


✅ 九、总结建议

建议 原因
🚫 避免在内核栈上分配大数组 超过 1KB 就危险
✅ 使用 kmalloc 分配大缓冲区 避免占用固定栈
🧪 开启 DEBUG_STACK_USAGE 检测 定位谁在用太多栈
⚠️ 遇到 crash 要检查是否爆栈 特别是中断/死锁类问题