int sys_fork(struct pt_regs *regs)
{
return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);
}
何谓动态增长呢?可以看到子进程初始的size为0,然后由于复制了父亲的sp以及稍后在dup_mm中复制的所有vma,因此子进程stack的flags仍然包含:
#define VM_STACK_FLAGS (VM_GROWSDOWN | VM_STACK_DEFAULT_FLAGS | VM_ACCOUNT)
这就说针对带有这个flags的vma(stack也在一个vma中!)可以动态增加其大小了,这可从do_page_fault中看到:
if (likely(vma->vm_start <= address))
goto good_area;
if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {
bad_area(regs, error_code, address);
return;
}
很清晰。
mem = mmap (NULL, size, prot,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
此调用中的size参数的获取很是复杂,你可以手工传入stack的大小,也可以使用默认的,一般而言就是默认的。这些都不重要,重要的是,这种stack不能动态增长,一旦用尽就没了,这是和生成进程的fork不同的地方。在glibc中通过mmap得到了stack之后,底层将调用sys_clone系统调用:
int sys_clone(struct pt_regs *regs)
{
unsigned long clone_flags;
unsigned long newsp;
int __user *parent_tidptr, *child_tidptr;
clone_flags = regs->bx;
//获取了mmap得到的线程的stack指针
newsp = regs->cx;
parent_tidptr = (int __user *)regs->dx;
child_tidptr = (int __user *)regs->di;
if (!newsp)
newsp = regs->sp;
return do_fork(clone_flags, newsp, regs, 0, parent_tidptr, child_tidptr);
}
因此,对于子线程的stack,它其实是在进程的地址空间中map出来的一块内存区域,原则上是线程私有的,但是同一个进程的所有线程生成的时候浅拷贝生成者的task_struct的很多字段,其中包括所有的vma,如果愿意,其它线程也还是可以访问到的,于是一定要注意。
asmlinkage int sys_set_thread_area(struct user_desc __user *u_info)
{
int ret = do_set_thread_area(current, -1, u_info, 1);
asmlinkage_protect(1, ret, u_info);
return ret;
}
int do_set_thread_area(struct task_struct *p, int idx,
struct user_desc __user *u_info,
int can_allocate)
{
struct user_desc info;
if (copy_from_user(&info, u_info, sizeof(info)))
return -EFAULT;
if (idx == -1)
idx = info.entry_number;
/*
* index -1 means the kernel should try to find and
* allocate an empty descriptor:
*/
if (idx == -1 && can_allocate) {
idx = get_free_idx();
if (idx < 0)
return idx;
if (put_user(idx, &u_info->entry_number))
return -EFAULT;
}
if (idx < GDT_ENTRY_TLS_MIN || idx > GDT_ENTRY_TLS_MAX)
return -EINVAL;
set_tls_desc(p, idx, &info, 1);
return 0;
}
fill_ldt设置GDT中第6个段描述符的基址和段限以及DPL等信息,这些信息都是从sys_set_thread_area系统调用的u_info参数中得来的。本质上,最终GDT的第6个段中描述的信息其实就是一块内存,这块内存用于存储TLS节,这块内存其实也是使用brk,mmap之类调用在主线程的堆空间申请的,只是后来调用sys_set_thread_area将其设置成了本线程的私有空间罢了,主线程或者其它线程如果愿意,也是可以通过其它手段访问到这块空间的。
明白了大致原理之后,我们来看一下一切是如何关联起来的。首先看一下Linux内核关于GDT的段定义,如下图所示:
我们发现是第六个段用于记录TLS数据,我了证实一下,写一个最简单的程序,用gdb看一下GS寄存器的值,到此我们已经知道GS寄存器表示的段描述子指向的段记录TLS数据,如下图所示:
#include
#include
#include
#include
#include
int main(int argc, char **argv)
{
int a=10, b = 0; //b保存GS寄存器表示的段的地址
//设置三个TLS变量,其中前两个使用堆内存,最后一个不使用
static pthread_key_t thread_key1;
static pthread_key_t thread_key2;
static pthread_key_t thread_key3;
char *addr1 = (char *)malloc(5);
char *addr2 = (char *)malloc(5);
memset(addr1, 0, 5);
memset(addr2, 0, 5);
strcpy(addr1, "aaaa");
strcpy(addr2, "bbbb");
pthread_key_create (&thread_key1, NULL);
pthread_key_create (&thread_key2, NULL);
pthread_key_create (&thread_key3, NULL);
pthread_setspecific (thread_key1, addr1);
pthread_setspecific (thread_key2, addr2);
pthread_setspecific (thread_key3, "1111111111");
//得到GS指示的段,也就是TLS的地址,这个需要用内嵌汇编来做
asm volatile("movl %%gs:0, %0;"
:"=r"(b) /* output */
);
printf("ok\n");
}
这个代码的含义在于,我可以通过GS寄存器访问到TLS变量,为了方便,我就没有写代码,而是通过gdb来证实,其实通过写代码取出TLS变量和通过gdb查看内存的方式效果是一样的,个人认为通过调试的方法对于理解还更好些。
load_TLS(next, cpu);
每个task_struct都有thread_struct,而该线程TLS的元数据信息就保存在thread_struct结构体的tls_array数组中:
static inline void native_load_tls(struct thread_struct *t, unsigned int cpu)
{
unsigned int i;
struct desc_struct *gdt = get_cpu_gdt_table(cpu);
for (i = 0; i < GDT_ENTRY_TLS_ENTRIES; i++)
gdt[GDT_ENTRY_TLS_MIN + i] = t->tls_array[i];
}
那么调试显示的结果,它处于GS寄存器指示tls段地址的紧接着下方4个字节的偏移处,而errno处于_thread变量下方14*4字节的位置。具体这些空间到底怎么安排的,可以看glibc的dl-reloc.c,dl-tls.c等文件,然而本人认为这没有什么意义,由于这涉及到很多关于编译,链接,重定向,ELF等知识,如果不想深度优先的迷失在这里面的化,理解原理也就够了,本人真的是没有时间再写了,回到家就要看孩子,购物,做家务....。最后给出一幅图,重定向后总的示意图如下: