一、进程的内核堆栈
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()做扫尾工作返回一个指向子进程的指针。
相关推荐
### Linux进程创建与进程通信知识点解析 #### 实验背景与目的 在本次实验中,学生通过实际操作加深了对Linux环境下进程创建与进程间通信的理解。实验的主要目标是熟悉Linux系统调用,并学习如何在Linux中创建进程。...
本实验主要涉及两个核心知识点:进程创建和多进程并发。 1. **进程创建** 在Linux环境下,进程创建主要通过`fork()`系统调用来实现。`fork()`函数会创建一个与调用进程几乎完全一样的新进程,称为子进程。新进程...
3. **进程创建方法**: 在Linux中,通常使用`fork()`系统调用来创建新进程。`fork()`会复制当前进程的所有资源,包括代码、数据、打开的文件等,创建出一个子进程。子进程和父进程拥有相同的程序计数器(PC),但...
* 进程创建:Linux 操作系统提供了多种方式来创建进程,包括使用系统调用 fork()、exec() 等。 * 进程执行:进程执行是指进程从开始执行到结束的整个过程。 * 进程暂停:进程暂停是指将进程暂停在当前状态,等待后续...
在Unix/Linux系统中,...综上所述,Unix/Linux进程池管理涉及多方面的技术,包括进程创建、任务调度、进程间通信、资源管理和错误处理。理解并掌握这些知识点对于开发高效、稳定的银行系统或其他服务型应用至关重要。
Linux 进程基本管理与进程控制 Linux 进程基本管理是计算机操作系统中一个非常重要的概念,它涉及到进程的创建、管理和控制。...通过实验和实践,我们可以更好地理解和掌握 Linux 进程基本管理和进程控制的知识点。
### Linux进程创建与并发实验知识点解析 #### 一、实验目的 本实验旨在通过实践操作加深学生对进程概念的理解,并掌握在Linux环境下如何创建进程以及探究并发执行的本质。具体目标包括: 1. **加强进程概念的理解*...
### LINUX进程管理实验知识点解析 #### 一、进程与程序的区别 在进行LINUX进程管理实验之前,首先需要理解进程与程序之间的区别。程序是指令的集合,是静态的,而进程则是程序的一次动态执行过程,具有生命周期,...
在操作系统领域,进程是资源分配的基本单位,而进程的创建是操作系统中的一项基本操作。...同时,分析实验结果可以帮助理解进程间通信、同步和竞争条件等概念,这些都是操作系统学习中的重要知识点。
本文将详细探讨Linux进程编程的基础知识,包括进程的概念、创建与管理进程、进程间通信以及线程的使用。 首先,我们需要了解什么是进程。在操作系统中,进程是程序的执行实例,它包含了一段内存空间,包括代码、...
实验二“进程创建及进程间通信1”主要涵盖了操作系统中的核心概念,如进程创建、进程同步、进程互斥以及管道通信。在这个实验中,你将深入理解Linux操作系统中进程的生命周期,以及如何通过编程实现进程间的通信。 ...
本资料集合将探讨Linux进程的生成、使用和创建过程,以及相关的源码分析。 首先,我们来了解一下进程的生成。在Linux中,新进程通常是通过fork()系统调用创建的。这个调用会复制当前进程的所有状态,包括内存映射、...
提供的"word文档供学习参考"可能包含了实验步骤、示例代码和相关理论解释,对于深入理解和实践Linux进程编程非常有帮助。 在实际操作中,结合`man`手册页和实验指导,学习者可以更全面地掌握这些概念和操作,从而...
【操作系统Linux进程实验报告】 在计算机操作系统教程中,实验报告主要关注的是Linux操作系统的进程管理。这个实验旨在让学生深入理解并熟悉Linux环境下的进程创建、执行以及管理。实验内容包括了四个关键的系统...
综上所述,操作系统实验中关于进程创建的学习,不仅增加了我们的理论知识,更重要的是培养了我们的实践技能。这样的实验课程对于计算机专业学生来说是非常宝贵的,因为它将抽象的理论知识与实际操作结合起来,让我们...
操作系统实验报告(LINUX进程间通信) 操作系统实验报告(LINUX进程间通信)是操作系统课程的一部分,涵盖了Linux进程间通信的原理和应用,包括消息队列、C/S结构等内容。下面将对这些知识点进行详细的解释。 一、...
在C语言中实现Linux进程池涉及以下几个关键知识点: 1. **进程创建与管理**:在Linux中,创建新进程主要通过`fork()`函数实现,而`exec()`系列函数可以用来加载可执行文件到新进程中。进程池需要维护一个进程列表,...
接下来,我们将详细探讨与Linux进程、线程和调度相关的知识点。 首先,Linux进程是系统分配资源的基本单位,具有独立的地址空间。进程生命周期包括就绪(ready)、运行(running)、睡眠(sleep)、停止(stopped)...
通过学习这个讲座,开发者不仅能深入了解Linux进程的生命周期、状态转换、创建和管理,还能掌握进程间的协作和资源分配,这对于系统管理和软件开发都是至关重要的。对于运维人员和Linux开发者而言,深入理解并熟练...