wtyufdssyh 2015-04-07
一、进程的内核堆栈
1、 内核在创建进程的时候,在创建task_struct的同时,会为进程创建相应的堆栈。每个进程会有两个栈,一个用户栈,存在于用户空间,一个内核栈,存在于内核空间。当进程在用户空间运行时,cpu堆栈指针寄存器里面的内容是用户堆栈地址,使用用户栈;当进程在内核空间时,cpu堆栈指针寄存器里面的内容是内核栈空间地址,使用内核栈。
2、进程用户栈和内核栈的切换
当进程因为中断或者系统调用而陷入内核态之行时,进程所使用的堆栈也要从用户栈转到内核栈。
进程陷入内核态后,先把用户态堆栈的地址保存在内核栈之中,然后设置堆栈指针寄存器的内容为内核栈的地址,这样就完成了用户栈向内核栈的转换;当进程从内核态恢复到用户态之行时,在内核态之行的最后将保存在内核栈里面的用户栈的地址恢复到堆栈指针寄存器即可。这样就实现了内核栈和用户栈的互转。
那么,我们知道从内核转到用户态时用户栈的地址是在陷入内核的时候保存在内核栈里面的,但是在陷入内核的时候,我们是如何知道内核栈的地址的呢?
关键在进程从用户态转到内核态的时候,进程的内核栈总是空的。这是因为,当进程在用户态运行时,使用的是用户栈,当进程陷入到内核态时,内核栈保存进程在内核态运行的相关信心,但是一旦进程返回到用户态后,内核栈中保存的信息无效,会全部恢复,因此每次进程从用户态陷入内核的时候得到的内核栈都是空的。所以在进程陷入内核的时候,直接把内核栈的栈顶地址给堆栈指针寄存器就可以了。
3.内核栈的实现
内核栈在kernel-2.4和kernel-2.6里面的实现方式是不一样的。
在kernel-2.4内核里面,内核栈的实现是:
Union task_union { Struct task_struct task; Unsigned long stack[INIT_STACK_SIZE/sizeof(long)]; };其中,INIT_STACK_SIZE的大小只能是8K。
内核为每个进程分配task_struct结构体的时候,实际上分配两个连续的物理页面,底部用作task_struct结构体(大约1k),结构上面的用作堆栈(大约7k)。使用current()宏能够访问当前正在运行的进程描述符,定义如下:
#define current get_current() static inline struct task_struct * get_current(void) { struct task_struct *current; __asm__("andl %%esp,%0; ":"=r" (current) : "" (~8191UL)); return current; }
~8191UL表示最低13位为0, 其余位全为1。 %esp指向内核堆栈中,当屏蔽掉%esp的最低13后,就得到这个”两个连续的物理页面”的开头,而这个开头正好是task_struct的开始,从而得到了指向task_struct的指针。
注意:这个时候task_struct结构是在内核栈里面的,内核栈的实际能用大小大概有7K。
内核栈在kernel-2.6里面的实现是(kernel-2.6.32):
Union thread_union { Struct thread_info thread_info; Unsigned long stack[THREAD_SIZE/sizeof(long)]; };
struct thread_info { struct task_struct *task; struct exec_domain *exec_domain; __u32 flags; __u32 status; __u32 cpu; … .. };根据内核的配置,THREAD_SIZE既可以是4K字节(1个页面)也可以是8K字节(2个页面)。thread_info是52个字节长。注意:此时的task_struct结构体已经不在内核栈空间里面了。
#define current get_current() static inline struct task_struct * get_current(void) { return current_thread_info()->task; } static inline struct thread_info *current_thread_info(void) { struct thread_info *ti; __asm__("andl %%esp,%0; ":"=r" (ti) : "" (~(THREAD_SIZE - 1))); return ti; }
根据THREAD_SIZE大小,分别屏蔽掉内核栈的12-bit LSB(4K)或13-bit LSB(8K),从而获得内核栈的起始位置。
二、Linux进程创建
linux通过系统调用clone()来实现fork().然后由clone()调用do_fork().do_fork()完成了大部分的创建工作。定义在kernel/fork.c文件中。该函数调用copy_process()函数,然后让进程开始运行。copy_process()函数如下:
981static struct task_struct *copy_process(unsigned long clone_flags, 982 unsigned long stack_start, 983 struct pt_regs *regs, 984 unsigned long stack_size, 985 int __user *child_tidptr, 986 struct pid *pid, 987 int trace) 988{ p = dup_task_struct(current); //从当前进程的PCB复制一个子进程的PCB if (!p) goto fork_out; if (retval) goto bad_fork_cleanup_sighand; retval = copy_mm(clone_flags, p);
1、调用dup_task_struct()为新进程创建一个内核栈、thread_info结构和task_struct,这些与当前进程(父进程)的值相同。此时,自进程和副进程的process-discriptor相同。
static struct task_struct *dup_task_struct(struct task_struct *orig) { struct task_struct *tsk; struct thread_info *ti; 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; } *tsk = *orig; tsk->thread_info = ti; setup_thread_stack(tsk, orig); ….. } # define alloc_task_struct() kmem_cache_alloc(task_struct_cachep, GFP_KERNEL) #define alloc_thread_info(tsk) \ ((struct thread_info *) __get_free_pages(GFP_KERNEL,THREAD_ORDER)) #endif
1/执行alloc_task_struct宏,为新进程获取进程描述符,并将描述符放在局部变量tsk中。
2, 2/执行alloc_thread_info宏以获取一块空闲的内存区,用以存放新进程的thread_info结构和内核栈,并将这块内存区字段的地址放在局部变量ti中(8K 或 4K, 可配置)。
3, 3/将current进程描述符的内容复制到tsk所指向的task_struct结构中,然后把tsk->thread_info置为ti。
4, 4/把current进程的thread_info描述符的内容复制到ti中,然后把ti->task置为tsk。
5, 5/返回新进程的描述符指针tsk。
2、进程地址空间的建立
copy_process调用copy_mm,下面来分析copy_mm。
681static int copy_mm(unsigned long clone_flags, struct task_struct * tsk) 682{ 683 struct mm_struct * mm, *oldmm; 684 int retval; 685 686 tsk->min_flt = tsk->maj_flt = 0; 687 tsk->nvcsw = tsk->nivcsw = 0; 688#ifdef CONFIG_DETECT_HUNG_TASK 689 tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw; 690#endif 691 692 tsk->mm = NULL; 693 tsk->active_mm = NULL; 694 695 /* 696 * Are we cloning a kernel thread? 697 * 698 * We need to steal a active VM for that.. 699 */ 700 oldmm = current->mm; 701 if (!oldmm) 702 return 0; 703 704 if (clone_flags & CLONE_VM) { 705 atomic_inc(&oldmm->mm_users); 706 mm = oldmm; 707 goto good_mm; 708 } 709 710 retval = -ENOMEM; 711 mm = dup_mm(tsk); 712 if (!mm) 713 goto fail_nomem; 714 715good_mm: 716 /* Initializing for Swap token stuff */ 717 mm->token_priority = 0; 718 mm->last_interval = 0; 719 720 tsk->mm = mm; 721 tsk->active_mm = mm; 722 return 0; 723 724fail_nomem: 725 return retval; 726}
692,693行,对子进程或者线程的mm和active_mm初始化(NULL)。
700 - 708行,如果是创建线程,则新线程共享创建进程的mm,所以不需要进行下面的copy操作。
重点就是711行的dup_mm(tsk)。
625struct mm_struct *dup_mm(struct task_struct *tsk) 626{ 627 struct mm_struct *mm, *oldmm = current->mm; 628 int err; 629 630 if (!oldmm) 631 return NULL; 632 633 mm = allocate_mm(); 634 if (!mm) 635 goto fail_nomem; 636 637 memcpy(mm, oldmm, sizeof(*mm)); 638 639 /* Initializing for Swap token stuff */ 640 mm->token_priority = 0; 641 mm->last_interval = 0; 642 643 if (!mm_init(mm, tsk)) 644 goto fail_nomem; 645 646 if (init_new_context(tsk, mm)) 647 goto fail_nocontext; 648 649 dup_mm_exe_file(oldmm, mm); 650 651 err = dup_mmap(mm, oldmm); 652 if (err) 653 goto free_pt; 654 655 mm->hiwater_rss = get_mm_rss(mm); 656 mm->hiwater_vm = mm->total_vm; 657 658 if (mm->binfmt && !try_module_get(mm->binfmt->module)) 659 goto free_pt; 660 661 return mm;
633行,用slab分配了mm_struct的内存对象。
637行,对子进程的mm_struct进程赋值,使其等于父进程,这样子进程mm和父进程mm的每一个域的值都相同。
在copy_mm的实现中,主要是为了实现unix COW的语义,所以理论上我们只需要父子进程mm中的start_x和end_x之类的域(像start_data,end_data)相等,而对其余的域(像mm_users)则需要re-init,这个操作主要在mm_init中完成。
449static struct mm_struct * mm_init(struct mm_struct * mm, struct task_struct *p) 450{ 451 atomic_set(&mm->mm_users, 1); 452 atomic_set(&mm->mm_count, 1); 453 init_rwsem(&mm->mmap_sem); 454 INIT_LIST_HEAD(&mm->mmlist); 455 mm->flags = (current->mm) ? 456 (current->mm->flags & MMF_INIT_MASK) : default_dump_filter; 457 mm->core_state = NULL; 458 mm->nr_ptes = 0; 459 set_mm_counter(mm, file_rss, 0); 460 set_mm_counter(mm, anon_rss, 0); 461 spin_lock_init(&mm->page_table_lock); 462 mm->free_area_cache = TASK_UNMAPPED_BASE; 463 mm->cached_hole_size = ~0UL; 464 mm_init_aio(mm); 465 mm_init_owner(mm, p); 466 467 if (likely(!mm_alloc_pgd(mm))) { 468 mm->def_flags = 0; 469 mmu_notifier_mm_init(mm); 470 return mm; 471 } 472 473 free_mm(mm); 474 return NULL; 475}
其中特别要关注的是467 - 471行的mm_alloc_pdg,也就是page table的拷贝,page table负责logic address到physical address的转换。
拷贝的结果就是父子进程有独立的page table,但是page table里面的每个entries值都是相同的,也就是说父子进程独立地址空间中相同logical address都对应于相同的physical address,这样也就是实现了父子进程的COW(copy on write)语义。
事实上,vfork和fork相比,最大的开销节省就是对page table的拷贝。
而在内核2.6中,由于page table的拷贝,fork在性能上是有所损耗的,所以内核社区里面讨论过shared page table的实现(http://lwn.net/Articles/149888/)。
2、检查并确保新创建这个子进程后,当前用户所拥有的进程数目没有超过给它分配的资源的限制。
3、子进程着手使自己与父进程区别开来。进程描述符内的许多成员都要被清零或者设为初始值。那些不是集成而来的进程描述符成员,主要是统计信息。task_struct中大多数数据都依旧未被修改。
4、子进程的状态被设置为TASK_UNINTERRUPTIBLE,以确保它不会投入运行。
5、copy_process()调用COPY_FLAGS()以更新task_struct的成员。表明进程是否拥有超级用户权限的PF_SUPERIV标志被清零,表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。
6、调用alloc_pid()为新进程分配一个有效的PID.
7 、根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。在一般情况下,这些资源被给定进程的所有线程共享;否则,这些资源对每个进程是不同的,因此被拷贝到这里。
8、最后,copy_process()做扫尾工作返回一个指向子进程的指针。