创建进程

来源:互联网 发布:冰毒 群交 知乎 编辑:程序博客网 时间:2024/05/20 21:20

Unix操作系统紧紧依赖进程创建来满足用户的需求。例如,只要用户输入一条命令,shell进程就创建一个新进程,新进程执行shell的另一个拷贝。


传统的Unix操作系统以统一的方式对待所有的进程;子进程复制父进程所拥有的资源。这种方法使进程的创建非常慢且效率低。因为子进程需要拷贝父进程的整个地址空间。实际上,子进程几乎不必读或修改父进程拥有的所有资源,在很多情况下,子进程立即调用execve(),并清除父进程仔细拷贝过来的地址空间。


现代Unix内核通过引入三种不同的机制解决了这个问题:

  1. 写时复制技术允许父子进程读相同的物理页。只要两者中有一个试图写一个物理页。内核就把这个页的内容拷贝到一个新的物理页,并把这个新物理页分配给正在写的进程。

  2. 轻量级进程允许父子进程共享每进程在内核的很多数据结构,如页表,打开文件表及信号处理。

  3. vfork()系统调用创建的进程共享其父进程的内存地址空间。为了防止父进程重写子进程需要的数据,阻塞父进程的执行,一直到子进程退出或执行一个新程序为止。

clone()fork()vfork()系统调用


linux中,轻量级进程是由名为clone()的函数创建的,这个函数使用下列参数:

fn

指定一个由新进程执行的函数。当这个函数返回时,子进程终止。函数返回一个整数,表示子进程的退出代码。

Arg

指向传递给fn()函数的数据。

Flags

各种各样的信息。低字节指定子进程结束时发送到父进程的信号代码,通常选择SIGCHLD信号。剩余的3个字节给一clone标志组用于编码,如表3-8所示。

child_stack

表示把用户态堆栈指针赋给子进程的esp寄存器。调用进程应该总是为子进程分配新的堆栈。

Tls

表示线程局部存储段数据结构的地址,该结构是为新轻量级进程定义的。只有在CLONE_SETTLS标志被设置时才有意义。

Ptid

表示父进程的用户态变量地址,该父进程具有与新轻量级进程相同的PID。只有在CLONE_PARENT_SETTID标志被设置时才有意义。

Ctid

表示新轻量级进程的用户态变量地址,该进程具有这一类进程的PID。只有在CLONE_CHILD_SETTID标志被设置时才有意义。


实际上,clone()是在C语言库中定义的一个封装函数,它负责建立新轻量级进程的堆栈并且调用对编程者隐藏的clone()系统调用。实现clone系统调用的sys_clone()服务例程没有fnarg参数。实际上,封装函数把fn指针存放在子进程堆栈的某个位置处,该位置就是该封装函数本身返回地址存放的位置。arg指针正好存放在子进程堆栈中fn的下面。当封装函数结束时,CPU从堆栈中取出返回地址,然后执行fn(arg)函数。


传统的fork()系统调用在Linux中是用clone()实现的,其中clone()flags参数指定为SIGCHLD信号及所有清0clone()标志。而它的child_stack参数是父进程当前的堆栈指针。因此,父进程和子进程暂时共享同一个用户态堆栈。但是,要感谢写时复制机制,通常只要父子进程中有一个试图去改变栈,则立即各自得到用户态堆栈的一份拷贝。


前一节描述的vfork()系统调用在Linux中也是用clone()实现的,其中clone()的参数flags指定为SIGCHLD信号和CLONE_VMCLONE_VFORK标志,clone的参数child_stack等于父进程当前的栈指针。


do_fork()函数


do_fork()函数负责处理clone()fork()vfork()系统调用,执行时使用下列参数:

clone_flags

clone()的参数flags相同

stack_start

clone()的参数stack_start相同

regs

指向通用寄存器值的指针,通用寄存器的值是从用户态切换到内核态时被保存到内核态堆栈中的。

stack_size

未使用

parent_tidptr,child_tidptr

clone()中的对应参数ptidctid相同。


do_fork()利用辅助函数copy_process()来创建进程描述符以及子进程执行所需要的所有其它内核数据结构。下面是do_fork()执行的主要步骤:

  1. 通过查找pidmap_array位图,为子进程分配新的PID

  2. 检查父进程的ptrace字段;如果它的值不等于0,说明有另外一个进程正跟踪父进程,因而,do_fork()检查debugger程序是否自己想跟踪子进程。在这种情况下,如果子进程不是内核线程,那么do_fork()函数设置CLONE_PTRACE标志。

  3. 调用copy_process()复制进程描述符。如果所有必须的资源都是可用的,该函数返回刚创建的task_struct描述符的地址,这是创建过程的关键步骤,我们将在do_fork()之后描述它。

  4. 如果设置了CLONE_STOPPED标志,或者必须跟踪子进程,即在p->ptrace中设置了PT_PTRACED标志,那么子进程的状态被设置成TASK_STOPPED,并为子进程增加挂起的SIGSTOP信号。在另外一个进程把子进程的状态恢复为TASK_RUNNING之前,子进程将一直保持TASK_STOPPED状态。

  5. 如果没有设置CLONE_STOPPED标志,则调用wake_up_new_task()函数以执行下述操作:

    a调整父进程和子进程的调度参数

    b如果子进程将和父进程运行在同一个CPU上,而且父进程和子进程不能共享同一组页表,那么,就把子进程插入父进程运行队列,插入进让子进程恰好在父进程前面,因此而迫使子进程先于父进程运行。如果子进程刷新其它地址空间,并在创建之后执行新程序,那么这种简单的处理会产生较好的性能。而如果我们让父进程先运行,那么写时复制机制将会执行一系列不必要的页复制。

    c否则,如果子进程与父进程运行在不同的CPU上,或者父进程和子进程共享同一组页表,就把子进程插入父进程运行队列的队尾。

  6. 如果CLONE_STOPPED标志被设置,则把子进程置为TASK_STOPPED状态。

  7. 如果父进程被跟踪,则把子进程的PID存入currentptrace_message字段并调用ptrace_notify()ptrace_notify()使当前进程停止运行,并向当前进程的父进程发送SIGCHLD信号。子进程的祖父进程是跟踪父进程的debugger进程。SIGCHLD信号通知debugger进程;current已经创建了一个子进程,可能通过查找current->ptrace_message字段获得子进程的PID

  8. 如果设置了CLONE_VFORK标志,则把父进程插入等待队列,并挂起父进程直到子进程释放自己的内存空间。

  9. 结束并返回子进程的PID


copy_process()函数


copy_process()创建进程描述符以及子进程执行所需要的所有其它数据结构。它的参数与do_fork()的参数相同,外加子进程的PID。下面描述copy_process()的最重要的步骤:

  1. 检查参数clone_flags所传递标志的一致性。尤其是,在下列情况下,它返回错误代码:

    aCLONE_NEWNSCLONE_FS标志都被设置

    bCLONE_THREAD标志被设置,但CLONE_SIGHAND标志被清0

    cCLONE_SIGHAND标志被设置,但CLONE_VM被清0

  2. 通过调用security_task_create()以及稍后调用的security_task_alloc()执行所有附加的安全检查。Linux2.6提供扩展安全性的钩子函数,与传统Unix相比,它具有更加强壮的安全模型。

  3. 调用dup_task_struct()为子进程获取进程描述符。该函数执行如下操作:

    a如果需要,则在当前进程中调用__unlazy_fpu(),把FPUMMXSSE/SSE2寄存器保存到父进程的thread_info结构中。稍后,dup_task_struct()装把这些值复制到子进程的thread_info结构中。

    B执行alloc_task_struct宏,为新进程获取进程描述符,并将描述符地址保存在tsk局部变量中。

    C执行alloc_thread_info宏以获取一块空闲内存区,用来存放新进程的thread_info结构和内核栈,并将这块内存区字段的地址存在局部变量ti中。正如在本章前面”标识一个进程”一节中所述:这块内存区字段的大小是8KB4KB

    Dcurrent进程描述符的内容复制tsk所指向的task_struct结构中,然后把tsk->thread_info置为ti

    Ecurrent进程的thread_info描述符的内容复制到ti所指向的结构中,然后把ti->task置为tsk

    F把新进程描述符的使用计数器置为2,用来表示进程描述符正被使用而且其相应的进程处于活动状态

    g返回新进程的进程描述符指针。

  4. 检查存放在current->signal->rlim[RLIMIT_NPROC].rlim_cur变量中的值是否小于或等于用户所拥有的进程数,如果是,则返回错误码,除非进程没有root权限。该函数从每用户数据结构user_struct中获取用户所拥有的进程数。通过进程描述符user字段的指针可以找到这个数据结构。

  5. 递增user_struct结构的使用计数器和用户所拥有的进程的计数器。

  6. 检查系统中的进程数量是否超过max_threads变量的值。这个变量的缺省值取决于系统内存容量的大小。总的原则是:所有thread_info描述符和内核线程所占用的空间不能超过物理内存大小的1/8。不过,系统管理员可以通过写/proc/sys/kernel/threads-max文件来改变这个值。

  7. 如果实现新进程的执行域和可执行格式的内核函数都包含在内核模块中,则递增它们的使用计数器。

  8. 设置与进程状态相关的几个关键字段:

    a把大内核锁计数器tsk->lock_depth初始化为-1

    btsk->did_exec字段被始化为0;它记录了进程发出的execve()系统调用的次数

    c更新从父进程复制到tsk->flags字段中的一些标志;首先清除PF_SUPERPRIV标志,该标志表示进程是否使用了某种超级用户权限。然后设置PF_FORKNOEXEC标志,它表示子进程还没有发出execve()系统调用。

  9. 把新进程的PID存入tsk->pid字段

  10. 如果clone_flags参数中的CLONE_PARENT_SHTTID标志被设置,就把子进程的PID复制到参数parent_tidptr指向的用户态变量中。

  11. 初始化子进程描述符中的list_head数据结构和自旋锁,并为与挂起信号、定时器及时间计表相关的几个字段赋初值。

  12. 调用copy_semundo(),copy_files(),copy_fs(),copy_sighand(),copy_signal(),copy_mmcopy_namespace()来创建新的数据结构,并把父进程相应数据结构的值复制到新数据结构中,除非clone_flasgs参数指出它们有不同的值。

  13. 调用copy_thread(),用发出clone()系统调用时CPU寄存器的值来初始化子进程的内核栈。不过,copy_thread()exa寄存器对应字段的值字段强行置为0。子进程描述符的thread.esp字段初始化为子进程内核栈的基地址,汇编语言函数的地址存放在thread.eip字段中。如果父进程使用I/O权限位图,则子进程获取该位图的一个拷贝。最后,如果CLONE_SETTLE标志被设置,则子进程获取由clone()系统调用的参数tls指向的用户态数据结构所表示的TLS段。

  14. 如果clone_flags参数的值被置为CLONE_CHILD_SETTIDCLONE_CHILD_CLEARTID,就把child_tidptr参数的值分别复制到tsk->set_child_tidtsk->clear_child_tid字段。这些标志说明;必须改变子进程用户态地址空间的child_tidptr所指向的变量的值,不过实际的写操作要稍后再执行。

  15. 清除子进程thread_info结构的TIF_SYSCALL_TRACE标志,以使ret_form_fork()函数不会把系统调用结束的消息通知给调试进程。

  16. clone_flags参数低位信号数字编码初始化tsk->exit_signal字段,如果CLONE_THREAD标志被置位,就把tsk->exit_signal字段初始化为-1。正如我们将在本章稍后“进程终止”一节所所见的,只有当线程组的最后一个成员“死亡”,才会产生一个信号,以通知线程组的领头进程的父进程。

  17. 调用sched_fork()完成对新进程调度程序数据结构的初始化。该函数把新进程的状态设置为TASK_RUNNING,并把thread_info结构的preempt_count字段设置为1,从而禁止内核抢占。

  18. 把新进程的thread_info结构的cpu字段设置为由smp_processor_id()所返回的本地CPU号。

  19. 初始化表示亲子关系的字段。尤其是,如果CLONE_PARENTCLONE_THREAD被设置,就用current->real_parent的值初始化tsk->real_parenttsk->parent;因此,子进程的父进程是当前进程的父进程。否则,把tsk->real_parenttsk->parent置为当前进程。

  20. 如果不需要跟踪子进程,就把tsk->ptrace字段设置为0tsk->ptrace字段会存放一些标志,而这些标志是在一个进程被另一个进程跟踪时才会用到的。采用这种方式,即使当前进程被跟踪,子进程也不会被跟踪。

  21. 执行SET_LINES宏,把新进程描述符插入进程链表。

  22. 如果子进程必须被跟踪。就把current->parent赋给tsk->parent,并将子进程插入调试程序的跟踪链表中。

  23. 调用attach_pid()把新进程描述符的PID插入pidhash[PIDTYPE_PID]散列表。

  24. 如果子进程是线程组的领头进程

    a.tsk->tgid的初值置为tsk->pid

    b tsk->group_leader的初值为tsk

    c调用三次attach_pid(),把子进程分别插入PIDTYPE_TGIDPIDTYPE_PGIDPIDTYPE_SID类型的PID散列表。

  25. 否则,如果子进程属于它的父进程的线程组

    atsk->tgid的初值设置为tsk->current->tgid

    btsk->group_leader的初值设置为current->group_leader的值。

    c.调用attach_pid(),把子进程插入PIDTYPE_TGID类型的散列表中。

  26. 现在,新进程已经被加入进程集合。递增nr_threads变量的值。

  27. 递增total_forks变量以记录被创建的进程的数量。

  28. 终止并返回子进程描述符指针(tsk)

让我们回头看看在do_fork()结束之后都发生了什么。现在,我们有了处于可运行状态的完整的子进程。但是,它还没有运行,调度程序要决定何时把CPU交给这个子进程。在以后的进程切换中,调度程序继续完善子进程;把子进程描述符thread字段的值装入几个CPU寄存器。特别是把thread.esp装入esp寄存器,把函数ret_from_fork()的地址装入eip寄存器。这个汇编语言函数调用schedule_tail()函数,用存放在栈中值再装载所有的寄存器,并强迫CPU返回到用户态。然后,在fork(),vfork()clone()系统调用时,新进程将开始执行。系统调用的返回值放在eax寄存器中;返回给子进程的值是0,返回给父进程的值是子进程的PID。回顾copy_thread()对子进程将与父进程执行相同的代码。应用程序的开发者可以按照Unix编程者熟悉的方式利用这一事实,在基于PID值的程序中插入一个条件语句使子进程与父进程有不同的行为。


内核线程


传统的Unix系统把一些重要的任务委托给周期性执行的进程,这些任务包括刷新磁盘调整缓存,交换出不用的页框,维护网络连接等等。事实上,以严格线性的方式执行这些任务的确效率不高,如果把它们放在后台调度,不管是对它们的函数还是对终端用户进程都能得到较好的响应。因为一些系统进程只运行在内核态,所以现代操作系统把它们的函数委托给内核线程,内核线程不受不必要的用户态上下文的拖累。在Linux中,内核线程在以下几方面不同于普通进程:

  1. 内核线程只运行在内核态,而普通进程既可以在内核态,也可以运行在用户态。

  2. 因为内核线程运行在内核态,它们只使用大于PAGE_OFFSET的线性地址空间。另一方面,不管在用户态还是在内核态,普通进程可以用4GB的线性地址空间。

创建一个内核线程


kernel_thread()函数创建一个新的内核线程,它接受的参数有:所要执行的内核函数的地址(fn)、要传递给函数的参数(arg)、一组clone标志(flags)。该函数本质上以下面的方式调用do_fork();

do_fork(flags|CLONE_VM|CLONE_UNTRACED,0,pregs,0,NULL,NULL);

CLONE_VM标志避免复制调用进程的页表;由于新内核线程无论如何都不会访问用户态地址空间,所以这种复制无疑会造成时间和空间的浪费。CLONE_UNTRACED标志保证不会有任何进程跟踪新内核线程,即使调用进程被跟踪。


传递给do_fork()的参数pregs表示内核栈的地址,copy_thread()函数将从这里找到为新线程初始化CPU寄存器的值。kernel_thread()函数在这个栈中保留寄存器值的目的是:

  1. 通过copy_thread()ebxedx分别设置为参数fnarg的值。

  2. eip寄存器的值设置为下面汇编语言代码段的地址:

    movl%edx,%eax

    push1%edx

    call*%ebx

    push1%eax

    calldo_exit

因此,新的内核线程开始执行fn(arg)函数,如果该函数结束,内核线程执行系统调用_exit(),并把fn()的返回值传递给它。


进程0


所有进程的祖先叫做进程0idle进程或因为历史的原因叫做swapper进程,它是在Linux的初始化阶段从无到有创建的一个内核线程。这个祖先进程使用下列静态分配的数据结构:

  1. 存放在init_task变量中的进程描述符,由INIT_TASK宏完成对它的初始化。

  2. 存放在init_thread_union变量中的thread_info描述符和内核栈,由INIT_THREAD_INFO宏完成对它们的初始化。

  3. 由进程描述符指向的下列表:

    --init_mm

    __init_fs

    __init_files

    __init_signhand

    这些表分别由下列宏初始化

    __INIT_MM

    __INIT_FS

    __INIT_FILES

    __INIT_SIGNALE

    __INIT_SIGHAND

  4. 主内核页全局目录存放在swpper_pg_dir

start_kernel()函数初始化内核需要的所有数据结构,激活中断,创建另一个叫进程1的内核线程;

kernel_thread(init,NULL,CLONE_FS|CLONE_SIGHAND);

新创建内核线程的PID1,并与进程0共享每进程所有的内核数据结构。此外,当调度程序选择到它时,init进程开始执行init()函数。


创建init进程后,进程0执行cpu_idle()函数,该函数本质上是在开中断的情况下重复执行hlt汇编语言指令。只有当没有其它进程处于TASK_RUNNING状态时,调度程序才选择进程0


在多处理器系统中,每个CPU都有一个进程0。只要打开机器电源,计算机的BIOS就启动某一个CPU,同时禁止其它CPU。运行在CPU0上的swapper进程初始化内核数据结构,然后激活其它的CPU,并通过copy_process()函数创建另外的swapper进程,把0传递给新创建的swapper进程做为它们的新PID。此外,内核把适当的CPU索引赋给内核所创建的每个进程的thread_info描述符的cpu字段。


进程1


由进程0创建的内核线程执行init()函数,init()依次完成内核初始化。init()调用execve()系统调用装入可执行程序init.结果,init内核线程变成一个普通进程,且拥有自己的每进程内核数据结构。在系统关闭之前,init进程一直存活,因为它创建和监控在操作系统外层执行的所有进程的活动。


其它内核线程


Linux使用很多其它内核线程。其中一些在初始化阶段创建,一直运行到系统关闭;而其它一些在内核必须执行一个任务时”按需”创建,这种任务在内核的执行上下文中得到很好的执行。


一些内核线程的例子是:

keventd

执行keventd_wq工作队列中的函数

kapmd

处理与高级电源管理相关的事件

kswapd

执行内存回叫。

Pdflush

刷新“脏”缓冲区中的内容到磁盘以回收内存。

Kblockd

执行kblockd_workqueue工作队列中的函数。实质上,它周期性激活块设备驱动程序。

Ksoftirqd

运行tasklet;系统中每个CPU都有这样一个内核线程。



原创粉丝点击