当我重新阅读《LDD3》时,我发现了一个曾经忽略的句子:“内核具有非常小的栈,它可能只有像一个4096字节大小的页那样小。” 针对这句话,我简单地学习了一下进程的“内核栈”。
进程的“内核栈”是什么? 在每个进程的生命周期中,必然会通过系统调用陷入内核。在执行系统调用陷入内核后,这些内核代码所使用的栈并不是原先用户空间中的栈,而是一个内核空间的栈,也就是进程的“内核栈”。
例如,一个简单的字符驱动程序实现了open()
方法。在这个驱动程序挂载后,应用程序通过glib库调用Linux的open()
系统调用。当执行系统调用陷入内核后,处理器会转换为特权模式(具体的转换机制因处理器架构而异。例如,对于ARM,普通模式和用户模式的栈指针(SP)是不同的寄存器)。此时,使用的栈指针就是内核栈指针,它指向内核为每个进程分配的内核栈空间。
内核栈的作用 个人理解是:在陷入内核后,系统调用中也存在函数调用和自动变量,这些都需要栈支持。用户空间的栈显然不安全,因此需要内核栈的支持。此外,内核栈还用于保存一些系统调用前的应用层信息(例如,用户空间栈指针和系统调用参数)。
内核栈与进程结构体的关联 每个进程在创建时都会得到一个内核栈空间。内核栈和进程的对应关系是通过两个结构体中的指针成员来完成的: (1)struct task_struct 在学习Linux进程管理时,必须学习的结构体。它在内核中代表了一个进程,并记录了进程的所有状态信息。该结构体定义在Sched.h(includelinux)中。其中有一个成员void *stack
,它指向下面的内核栈结构体的“栈底”。 在系统运行时,宏current
获得的是当前进程的struct task_struct
结构体。
(2)内核栈结构体union thread_union
1. union thread_union { 2. struct thread_info thread_info; 3. unsigned long stack[THREAD_SIZE/sizeof(long)]; 4. };
其中struct thread_info是记录部分进程信息的结构体,其中包括了进程上下文信息:
1. /* 2. \* low level task data that entry.S needs immediate access to. 3. \* __switch_to() assumes cpu_context follows immediately after cpu_domain. 4. */ 5. struct thread_info { 6. unsigned long flags; /* low level flags */ 7. int preempt_count; /* 0 => preemptable, bug */ 8. mm_segment_t addr_limit; /* address limit */ 9. struct task_struct *task; /* main task structure */ 10. struct exec_domain *exec_domain; /* execution domain */ 11. __u32 cpu; /* cpu */ 12. __u32 cpu_domain; /* cpu domain */ 13. struct cpu_context_save cpu_context; /* cpu context */ 14. __u32 syscall; /* syscall number */ 15. __u8 used_cp[16]; /* thread used copro */ 16. unsigned long tp_value; 17. struct crunch_state crunchstate; 18. union fp_state fpstate __attribute__((aligned(8))); 19. union vfp_state vfpstate; 20. \#ifdef CONFIG_ARM_THUMBEE 21. unsigned long thumbee_state; /* ThumbEE Handler Base register */ 22. \#endif 23. struct restart_block restart_block; 24. };
关键是其中的task成员,指向的是所创建的进程的struct task_struct结构体
而其中的stack成员就是内核栈。从这里可以看出内核栈空间和 thread_info是共用一块空间的。如果内核栈溢出, thread_info就会被摧毁,系统崩溃了~~~
内核栈—struct thread_info—-struct task_struct三者的关系入下图:
内核栈的产生
在进程被创建的时候,fork族的系统调用中会分别为内核栈和struct task_struct分配空间,调用过程是:
fork族的系统调用—>do_fork—>copy_process—>dup_task_struct
在dup_task_struct函数中:
1. static struct task_struct *dup_task_struct(struct task_struct *orig) 2. { 3. struct task_struct *tsk; 4. struct thread_info *ti; 5. unsigned long *stackend; 6. 7. int err; 8. 9. prepare_to_copy(orig); 10. 11. **tsk = alloc_task_struct();** 12. if (!tsk) 13. return NULL; 14. 15. **ti = alloc_thread_info(tsk);** 16. if (!ti) { 17. free_task_struct(tsk); 18. return NULL; 19. } 20. 21. err = arch_dup_task_struct(tsk, orig); 22. if (err) 23. goto out; 24. 25. **tsk->stack = ti;** 26. 27. err = prop_local_init_single(&tsk->dirties); 28. if (err) 29. goto out; 30. 31. **setup_thread_stack(tsk, orig);** 32. ......
其中alloc_task_struct使用内核的slab分配器去为所要创建的进程分配struct task_struct的空间
而alloc_thread_info使用内核的伙伴系统去为所要创建的进程分配内核栈(union thread_union )空间
注意:
后面的tsk->stack = ti;语句,这就是关联了struct task_struct和内核栈
而在setup_thread_stack(tsk, orig);中,关联了内核栈和struct task_struct:
1. static inline void setup_thread_stack(struct task_struct *p, struct task_struct *org) 2. { 3. *task_thread_info(p) = *task_thread_info(org); 4. task_thread_info(p)->task = p; 5. }
内核栈的大小
由于是每一个进程都分配一个内核栈空间,所以不可能分配很大。这个大小是构架相关的,一般以页为单位。其实也就是上面我们看到的THREAD_SIZE,这个值一般为4K或者8K。对于ARM构架,这个定义在Thread_info.h (archarmincludeasm),
1. \#define THREAD_SIZE_ORDER 1 2. \#define THREAD_SIZE 8192 3. \#define THREAD_START_SP (THREAD_SIZE - 8)
所以ARM的内核栈是8KB
在(内核)驱动编程时需要注意的问题:
由于栈空间的限制,在编写的驱动(特别是被系统调用使用的底层函数)中要注意避免对栈空间消耗较大的代码,比如递归算法、局部自动变量定义的大小等等
以上是对Linux的进程内核栈的认识的详细内容。更多信息请关注PHP中文网其他相关文章!