链接:https://www.zhihu.com/question/57013926/answer/151506606
1.Linux 内核中使用 task_struct
作为进程描述符,该结构定义在<linux/sched.h>文件中:
struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
void *stack;
atomic_t usage;
unsigned int flags; /* per process flags, defined below */
unsigned int ptrace;
int lock_depth; /* BKL lock depth */
/* ...... */
};
可以发现 task_struct
中有一个 stack
成员,而 stack
正好用于保存内核栈地址。内核栈在进程创建时绑定在 stack
上。可以观察 fork
流程:Linux 通过 clone()
系统调用实现 fork()
,然后由 fork()
去调用 do_fork()
。定义在<kernel/fork.c>中的 do_fork()
负责完成进程创建的大部分工作,它通过调用 copy_process()
函数,然后让进程运行起来。copy_process()
完成了许多工作,这里重点看内核栈相关部分。copy_process()
调用 dup_task_struct
来创建内核栈、thread_info
和 task_struct
:
static struct task_struct *dup_task_struct(struct task_struct *orig) {
struct task_struct *tsk;
struct thread_info *ti;
unsigned long *stackend;
int err; prepare_to_copy(orig);
tsk = alloc_task_struct();
if (!tsk) return NULL;
ti = alloc_thread_info(tsk);
if (!ti) {
free_task_struct(tsk);
return NULL;
}
err = arch_dup_task_struct(tsk, orig);
if (err) goto out;
tsk->stack = ti;
err = prop_local_init_single(&tsk->dirties);
if (err) goto out;
setup_thread_stack(tsk, orig);
stackend = end_of_stack(tsk);
*stackend = STACK_END_MAGIC;
/* for overflow detection */
#ifdef CONFIG_CC_STACKPROTECTOR
tsk->stack_canary = get_random_int();
#endif
/* One for us, one for whoever does the "release_task()"
(usually parent) */
atomic_set(&tsk->usage,2);
atomic_set(&tsk->fs_excl, 0);
#ifdef CONFIG_BLK_DEV_IO_TRACE
tsk->btrace_seq = 0;
#endif
tsk->splice_pipe = NULL;
account_kernel_stack(ti, 1);
return tsk;
out:
free_thread_info(ti);
free_task_struct(tsk);
return NULL;
}
其中重点是下面部分:
tsk = alloc_task_struct();
if (!tsk) return NULL;
ti = alloc_thread_info(tsk);
if (!ti) {
free_task_struct(tsk);
return NULL;
}
err = arch_dup_task_struct(tsk, orig);
if (err) goto out;
tsk->stack = ti;
这里可以看到内核栈的创建过程。可能会疑惑为何 stack
指向了 thread_info
,那是因为在2.6以前的内核中,各个进程的 task_struct
存放在内核栈的尾端,这样做是为了在寄存器较少的体系结构中直接使用栈指针加偏移就可以算出它的位置。2.6以后使用slab分配器动态分配 task_struct
,所以只需要在栈顶创建一个 thread_info
记录 task_struct
的地址。所以这里回答了第一个问题, 每个进程都有一个单独的内核栈。
2.从内核模块编程的角度看(不涉及用户态进程),内核栈该怎么理解?和用户进程进行系统调用使用的栈空间有什么不同?
每个进程运行时都持有上下文,用于保证并行性。为了保证内核和用户态隔离,陷入内核不影响用户态,所以使用了不同的栈。内核栈只是对内核态上下文中的栈的称谓。为了方便管理用户程序,限制用户程序权限,所以区分了内核态和用户态。内核态中拥有高特权级,能够执行io等特权指令,而用户态程序想要执行特权级指令则必须陷入内核态。从用户程序角度来看,内核更类似与库文件的存在。内核通过虚拟地址访问权限来限制用户程序访问内存地址,比如内核空间的代码和数据不应该被用户程序访问到。因此内核运行时使用的栈不应该能被用户态代码访问到,否则用户态代码完全可以通过构造特定的数据控制内核(参考ret2libc)。因此,用户态使用的栈空间和内核栈并无本质区别,它们均处于同一块页表映射中,内核栈处于高特权级访问限制的虚拟地址中,防止用户态代码访问内核数据。
3.怎么理解linux内核栈空间只有4KB或8KB,linux内核编程中的堆(heap)和栈(stack)有什么区别?
内核中的资源是非常宝贵的,而一个比较大的栈空间多数时间是浪费了。那为何不设计小一点,然后保证内核调用层次低、局部变量小,做到不溢出?而内核编程中的堆和栈并非通常写程序时所说的堆和栈有严格的区分。
最高内存地址
+-------------------+
| 堆栈段 |
| | |
| | |
| | |
| v |
| |
| |
| |
| |
| ^ |
| | |
| | |
| | |
| 堆 |
|-------------------|
| BSS段 |
| |
| |
| |
| 数据段 |
|-------------------|
| 代码段 |
+-------------------+
内核中的堆和栈没有严格的地址区分,只是程序角度的不同解释而已。