分享

Linux内核的thread

 astrotycoon 2013-09-05

  本文基于Linux 3.5.4源代码。

     对每个进程,Linux内核都把两个不同的数据结构紧凑的存放在一个单独为进程分配的内存区域中:一个是内核态的进程堆栈,另一个是紧挨着进程描述符的小数据结构thread_info,叫做线程描述符。在较新的内核代码中,这个存储区域的大小通常为8192个字节(两个页框)。在linux/arch/x86/include/asm/page_32_types.h中,

  1. #define THREAD_SIZE_ORDER    1
  2. #define THREAD_SIZE        (PAGE_SIZE << THREAD_SIZE_ORDER)
    出于效率考虑,内核让这8K空间占据连续的两个页框并让第一个页框的起始地址是213的倍数。
    内核态的进程访问处于内核数据段的栈,这个栈不同于用户态的进程所用的栈。用户态进程所用的栈,是在进程线程地址空间中;而内核栈是当进程从用户空间进入内核空间时,特权级发生变化,需要切换堆栈,那么内核空间中使用的就是这个内核栈。因为内核控制路径使用很少的栈空间,所以只需要几千个字节的内核态堆栈。需要注意的是,内核态堆栈仅用于内核例程,Linux内核另外提供了单独的硬中断栈和软中断栈。
    下图中显示了在物理内存中存放两种数据结构的方式。线程描述符驻留与这个内存区的开始,而栈顶末端向下增长。 下图摘自ULK3,但是较新的内核代码中,进程描述符task_struct结构中没有直接指向thread_info结构的指针,而是用一个void指针类型的成员表示,然后通过类型转换来访问thread_info结构。相关代码在include/linux/sched.h中:
  1. #define task_thread_info(task)    ((struct thread_info *)(task)->stack)

       0_1271584604XTJq.gif

    在这个图中,esp寄存器是CPU栈指针,用来存放栈顶单元的地址。在80x86系统中,栈起始于顶端,并朝着这个内存区开始的方向增长。从用户态刚切换到内核态以后,进程的内核栈总是空的。因此,esp寄存器指向这个栈的顶端。
    一旦数据写入堆栈,esp的值就递减。在Linux3.5.4内核中,thread_info结构是72个字节长(ULK3时代的内核中,这个结构的大小是52个字节),因此内核栈能扩展到8120个字节。thread_info结构的定义如下:

  1.     struct thread_info {
  2.     struct task_struct    *task;           /* main task structure */
  3.     struct exec_domain    *exec_domain;    /* execution domain */
  4.     __u32            flags;                /* low level flags */
  5.     __u32            status;               /* thread synchronous flags */
  6.     __u32            cpu;                  /* current CPU */
  7.     int            preempt_count;          /* 0 => preemptable<0 => BUG */
  8.     mm_segment_t            addr_limit;
  9.     struct restart_block     restart_block;
  10.     void __user             *sysenter_return;
  11. #ifdef CONFIG_X86_32
  12.     unsigned long previous_esp; /* ESP of the previous stack in
  13.                                    case of nested (IRQ) stacks
  14.                                    */
  15.     __u8                supervisor_stack[0];
  16. #endif
  17.     unsigned int        sig_on_uaccess_error:1;
  18.     unsigned int        uaccess_err:1;    /* uaccess failed */
  19. };
    Linux内核中使用一个联合体来表示一个进程的线程描述符和内核栈:
  1. union thread_union {
  2.     struct thread_info thread_info;
  3.     unsigned long stack[THREAD_SIZE/sizeof(long)];
  4. };

    下面来说说如何通过esp栈指针来获取当前在CPU上正在运行进程的thread_info结构。实际上,上面提到,thread_info结构和内核态堆栈是紧密结合在一起的,占据两个页框的物理内存空间。而且,这两个页框的起始起始地址是213对齐的。所以,内核通过简单的屏蔽掉esp的低13位有效位就可以获得thread_info结构的基地址了。在文件linux/arch/x86/include/asm/thread_info.h中,有如下代码:

  1. #ifndef __ASSEMBLY__


  2. /* how to get the current stack pointer from C */
  3. register unsigned long current_stack_pointer asm("esp") __used;

  4. /* how to get the thread information struct from C */
  5. static inline struct thread_info *current_thread_info(void)
  6. {
  7.     return (struct thread_info *)
  8.         (current_stack_pointer & ~(THREAD_SIZE - 1));
  9. }

  10. #else /* !__ASSEMBLY__ */

  11. /* how to get the thread information struct from ASM */
  12. #define GET_THREAD_INFO(reg)     \
  13.     movl $-THREAD_SIZE, reg; \
  14.     andl %esp, reg

  15. /* use this one if reg already contains %esp */
  16. #define GET_THREAD_INFO_WITH_ESP(reg) \
  17.     andl $-THREAD_SIZE, reg

  18. #endif
    在上面的代码中,当前的栈指针current_stack_pointer就是esp,
        THREAD_SIZE为8K,二进制的表示为0000 0000 0000 0000 0010 0000 0000 0000。

              ~(THREAD_SIZE-1)的结果刚好为1111 1111 1111 1111 1110 0000 0000 0000,第十三位是全为零,也就是刚好屏蔽了esp的低十三位,最终得到的是thread_info的地址。

    
    进程最常用的是进程描述符结构task_struct而不是thread_info结构的地址。为了获取当前CPU上运行进程的task_struct结构,内核提供了current宏,该宏本质上等价于
current_thread_info()->task,在include/asm-generic/current.h中定义:

  1. #define get_current() (current_thread_info()->task)
  2. #define current get_current()

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多