天天看點

Linux 程序棧和線程棧的差別

注:本文所涉及的環境為Linux, 下文讨論的棧跟核心棧,沒有任何的關系,關于核心棧,請參考《深入Linux核心架構》中的2.4.1 程序複制

這裡有如下幾個問題,線程棧的空間是開辟在那裡的? 線程棧之間可以互訪嗎?為什麼在使用pthread_attr_setstack函數時,需要設定棧的大小,而程序task_struct的 mm_struct *mm 成員中卻并沒有卻并沒有stack_size這個成員項,怎麼儲存的棧大小呢?

程序棧:

        程序使用者空間的管理在task_struct 的mm_struct *mm成員中展現, mm中的成員定義了使用者空間的布局情況如圖一。 使用者空間的棧起始于STACK_TOP, 如果設定了PF_RANDOMIZE,則起始點會減少一個小的随機量,每個體系結構都必須定義STACK_TOP, 大多數都設定為TASK_SIZE, 在32位機上該值為0XC0000000。經過随機處理後,程序棧的起始位址将存放在mm->start_stack中,可以通過cat /proc/xxx/stat 檢視。

      如圖一,棧從上而下擴充,而用于記憶體映射的區域起始于mm->mmap_base, mm->mmap_base通過調用mmap_base函數來初始化,為了確定棧不與mmap區域不發生沖突,兩者之間設定了一個安全間隙。mmap_base函數源代碼如下:

#define MIN_GAP (128*1024*1024) 
#define MAX_GAP (TASK_SIZE/6*5)
static inline unsigned long mmap_base(struct mm_struct *mm)
{
  unsigned long gap = current->signal->rlim[RLIMIT_STACK].rlim_cur; // rlim_cur 預設為8388608,及8M, 可以使用 getrlimit(RLIMIT_STACK, &limit) 檢視
  unsigned long random_factor = 0;
  if (current->flags & PF_RANDOMIZE)
    random_factor = get_random_int() % (1024*1024);
  if (gap < MIN_GAP) // 通過MIN_GAP來保證,程序棧的大小至少為128MB
    gap = MIN_GAP;
  else if (gap > MAX_GAP) // 棧的最大空間為TASK_SIZE/6*5, 及2.5G
    gap = MAX_GAP;
  return PAGE_ALIGN(TASK_SIZE - gap - random_factor); // 通過保留random_factor空間大小的間隙來防止棧溢出
}
           
Linux 程式棧和線程棧的差別

圖 一 IA-32計算機上虛拟位址空間的布局

線程棧:

        線程包含了表示程序内執行環境必需的資訊,其中包括程序中标示線程的線程ID,一組寄存器值,棧,排程優先級和政策, 信号屏蔽字,errno變量以及線程私有資料。程序的所有資訊對該程序的所有線程都是共享的,包括可執行的程式文本,程式的全局記憶體和堆記憶體,棧以及檔案描述符,是以線程的mm_struct *mm指針變量和所屬程序的mm指針變量相同。

       在建立線程的時候,可以通過pthread_attr_t來初始化線程的屬性,包括線程的棧布局資訊,如棧起始位址stackaddr, 棧大小stacksize。 具體需要通過方法

int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
// 注:stackaddr 指向為該線程開辟的空間,該空間可以使用malloc或者mmap來開辟,而不能來自程序的棧區。開辟的stackaddr所指向的動态空間需要自己負責釋放。
           

當然也可将線程棧的空間管理交給系統,如果想改變系統預設的棧大小8MB,可以通過

int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
// 注:stacksize最小值為16384,機關為位元組
           

由上面的API接口,可以得到,線程棧的stacksize是儲存在pthread_attr_t中的,可以通過人為的指定,也可以通過在建立線程的時候讀取系統的配置檔案來初始化stacksize,當初始化完棧的起始位址,和大小後,便可以通過

int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
           

來初始化線程棧末尾之後用以避免棧溢出的緩沖區的大小,如果應用程式溢出到此緩沖區中,這個錯誤可能會導緻 SIGSEGV 信号被發送給該線程, 進而造成段錯誤,緩沖區預設設定為PAGESIZE個位元組。因為線程的mm->start_stack和所屬程序相同,是以線程棧的起始位址并沒有存放在task_struct中,應該隻是使用attr中的stackaddr,來初始化task_struct->thread-> sp(sp指向struct pt_regs對象,該結構體用于儲存使用者程序或者線程的寄存器現場)。

總結:線程棧的空間開辟在所屬程序的堆區,線程與其所屬的程序共享程序的使用者空間,是以線程棧之間可以互訪。線程棧的起始位址和大小存放在pthread_attr_t 中,棧的大小并不是用來判斷棧是否越界,而是用來初始化避免棧溢出的緩沖區的大小(或者說安全間隙的大小)

ps: 文中如有錯誤的地方,請各位随時提出來,我将第一時間更改,謝謝。

繼續閱讀