现在的位置: 首页 > 综合 > 正文

(进程篇 详解 中)–深入理解linux内核

2013年09月16日 ⁄ 综合 ⁄ 共 4184字 ⁄ 字号 评论关闭

一、进程切换与硬件上下文

1,进程切换:为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复之前被挂起的某个进程,这种能力叫做进程切换或者任务切换或者上下文切换。

2,硬件上下文:

      尽管每个进程可以拥有属于自己的地址空间,但是所有进程必须共享CPU寄存器,因此,在恢复一个进程的执行前,内核必须确保每个寄存器装入了挂起进程时的值

     进程恢复执行前必须装入寄存器的那一组数据成为硬件上下文。

     进程切换可以这样表述:保存将要切换出的进程的硬件上下文,用将要切换进来的进程的硬件上下文来代替。linux2.6使用软件进行进程切换。

     进程切换只发生在内核态。

二、switch_to宏

switch_to宏用于进程切换,给定了前一个进程结构体指针prev,以及需要切换到的进程结构体指针next,从prev切换到next.
但是,实际上,switch_to宏有三个参数,除了上面说的两个参数之外,还有一个last参数.而且使用switch_to宏的时候传入的prev和last都是同一个值,比如会这么调用这个宏:
switch_to(prev,next,prev).

考虑一种场景,进程A切换到进程B,因为每个进程的空间是不同的,所以在切换之前,进程A的空间里prev=A,next=B,last=A.
一段时间之后,需要切换回到进程A,假设当前进程是C,那么对于C而言prev=C,next=A,last=C.

对比前后两种场景:
进程A切换前:prev=A,next=B,last=A
进程C切换前:prev=C,next=A,last=C

这时开始从进程C切换到进程A,注意到在切换之前switch_to宏将prev存放到了eax寄存器中,也就是在进程C切换到进程A之前,eax=C
切换之后,很显然,来到了进程A的空间,因此prev,next,last指针要回到进程A被切换出去之前的指向,因此prev=A,next=B,last=A,而eax的数据保持不变.
在switch_to宏返回之前,将eax寄存器的数据存放到last中,因此,last=eax=C.
此时,也就是进程A被切换回来之后,prev=A,next=B,last=C

从上面的分析可以看出,实际上,prev指向的是进程切换之前被切换走的进程指针,而last指向的是切换之后从哪个进程切换过来的.
两者的意义并不一样,只不过是在切换之后原先的prev无用了,可以用于保存切换之后是从哪个进程切换过来的,所以才会出现调用switch_to宏时prev和last相同的情况.

 

三、创建进程

创建进程:传统的unix操作系统以统一的方式对待所有的进程,子进程拷贝父进程所有的全部资源,这种方法使进程的创建效率非常慢。实际上,子进程不必读或者写父进程所拥有的全部资源。

现在unix内核使用三种不同的机制来解决这个问题:

1,写时复制技术允许父子进程读相同的物理页。

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

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

四、Linux中的clone()函数

int clone(int (*fn)(void *), void*child_stack, int flags, void *arg);

这里fn是函数指针,我们知道进程的4要素,这个就是指向程序的指针,就是所谓的“剧本", child_stack明显是为子进程分配系统堆栈空间(在linux下系统堆栈空间是2页面,就是8K的内存,其中在这块内存中,低地址上放入了值,这个值就是进程控制块task_struct的值),flags就是标志用来描述你需要从父进程继承那些资源, arg就是传给子进程的参数)。下面是flags可以取的值

标志                   含义

 CLONE_PARENT  创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子”

 CLONE_FS          子进程与父进程共享相同的文件系统,包括root、当前目录、umask

 CLONE_FILES     子进程与父进程共享相同的文件描述符(file descriptor)表

 CLONE_NEWNS  在新的namespace启动子进程,namespace描述了进程的文件hierarchy

 CLONE_SIGHAND  子进程与父进程共享相同的信号处理(signal handler)表

 CLONE_PTRACE  若父进程被trace,子进程也被trace

 CLONE_VFORK    父进程被挂起,直至子进程释放虚拟内存资源

 CLONE_VM          子进程与父进程运行于相同的内存空间

 CLONE_PID         子进程在创建时PID与父进程一致

 CLONE_THREAD   Linux2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群

五、do_fork()函数

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

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

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

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

如果设置了CLONE_STOPPED标志,或者必须跟踪子进程,即在p->ptrace中设置了PT_PTRACED标志,那么子进程的状态被设置成TASK_STOPPED,并为子进程增加挂起的SIGSTOP信号。在另外一个进程(不妨假设是跟踪进程或是父进程)把子进程的状态恢复为TASK_RUNNING之前(通常是通过发送SIGCONT信号),子进程将一直保持TASK_STOPPED状态。

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

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

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

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

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

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

如果设置了CLONE_VFORK标志,则把父进程插入等待队列,并挂起父进程直到子进程释放自己的内存地址空间(也就是说,直到子进程结束或执行新的程序)。

结束并返回子进程的PID。

六、copy_process()函数

copy_process()函数完成的工作很有意思:
.调用dup_task_struct()为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同。此时,子进程和父进程的描述符是完全相同的。
.检查新创建的这个子进程后,当前用户所拥有的进程数目没有超出给他分配的资源的限制。
.现在,子进程着手使自己与父进程区别开来。进程描述符内的许多成员都要被清0或设为初始值。进程描述符的成员值并不是继承而来的,主要是统计信息。进程描述符中的大多数数据都是共享的。
.接下来,子进程的状态被设置为TASK_UNINTERRUPTIBLE以保证它不会投入运行。
.copy_process()调用copy_flag()以更新task_struct的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV标志被清0.表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。
.调用get_pid()为新进程获取一个有效的PID。
.根据传递给clone()的参数标志,copy_process拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。在一般情况下,这些资源会被给定进程的所有线程共享;否则, 这些资源对每个进程是不同的,因此被拷贝到这里。
.最后,copy_process()作扫尾工作并返回一个指向子进程的指针。
再回到do_fork()函数,如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行。内核有意选择子进程执行。因此一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销,如果父进程首先执行的话,有可能会开始向地址空间写入。

抱歉!评论已关闭.