进程管理
博客专区 > China_OS 的博客 > 博客详情
进程管理
China_OS 发表于5年前
进程管理
  • 发表于 5年前
  • 阅读 228
  • 收藏 1
  • 点赞 0
  • 评论 0

腾讯云 技术升级10大核心产品年终让利>>>   

Linux内核架构理论学习

进程
     进程就是处于执行期的程序,但进程并不仅仅局限于一段可执行代码,通常进程还要包含其他资源,如打开的文件、挂起的信号等等。实际上,进程就是正在执行的程序代码的实时结果。执行线程,简称线程(thread),是在进程中活动的对象,每个线程拥有独立的计数器、进程栈和一组进程寄存器。内核调度的对象是线程而不是进程。linux的线程实现很特别,它对线程和进程并不特别区分,线程只不过是一种特殊的进程罢了。

     现代操作系统提供两种虚拟机制:虚拟处理器和虚拟内存。在线程之间可以共享虚拟内存,但每个线程都拥有各自的虚拟处理器。在linux中通过fork()系统调用复制一个现有的进程来创建一个全新的进程,调用fork()的进程称为父进程,新产生的进程称为子进程,在该调用结束时,在返回点这个相同的位置上,父进程恢复执行,子进程开始执行,fork()系统调用返回两次:一次回到父进程,一次回到新产生的子进程。创建进程一般都是为了立即执行,而接着调用exec()这组函数可以创建新的地址空间,并把新程序载入其中。linux中的fork()系统调用其实是由clone()系统调用实现的。最终,程序通过exit()系统调用退出执行,这会将其占用的资源释放掉,父进程可以通过wait4()系统调用查询子进程是否终结,这使得进程拥有了等待特定进程执行完毕的能力,进程退出执行后被设置为僵死状态,直到它的父进程调用wait()或waitpid()为止。

进程描述符及任务结构
     内核把进程的列表存放在叫做任务队列(task list)的双向链表中,链表的每一项都是task_struct类型,称为进程描述符的结构。进程描述符包含一个具体进程的所有信息。进程描述符中包含的数据能完整的描述一个正在执行的程序:它打开的文件、进程的地址空间、挂起的信号、进程状态等等信息。

     linux通过slab分配器分配task_struct结构,这样能够达到对象复用和缓存着色的目的。在2.6内核以前,各个进程的task_struct()存放在他们内核栈的尾端,这样可以使像x86那样寄存器较少的硬件体系结构只要通过栈指针就能计算出它的位置,而避免使用额外的寄存器,而现在使用slab分配器动态生成task_struct(),所以只需要在栈低或栈顶创建一个新的结构struct thread_info(),每个任务的thread_info结构在它的内核栈尾端分配,期中task域中的指针指向该任务实际task_struct的指针。

     内核通过一个唯一的进程标识值或PID来表示每个进程,PID是一个数,表示为pid_t隐含类型,实际上就是一个int类型,为了和老版本的unix兼容,PID的最大值默认设置为32768,这个值对于桌面系统可能够用,但是对于服务器可能需要更多的进程,这个值越小轮转一圈就越快,如果确实需要,可以修改/proc/sys/kernel/pid_max的值来提高PID的上限。内核把每个进程的PID存放在它们各自的进程描述符中。内核中大部分处理进程的代码都是直接通过task_struct进行的,因此通过current宏查找到当前正在运行进程的进程描述符的速度就显得比较重要,有些硬件体系结构含有专门的寄存器存放指向当前进程task_struct的指针,用于加快访问速度。而x86没有富余的寄存器,就只能在内核栈的尾端创建thread_info()结构,通过计算偏移间接的查找task_struct结构。

     进程描述符中的stat域描述了进程的当前状态,系统中的每个进程都必然处于五种状态之一,所以该域的值也就是以下五种状态之一了:
          1  task_running(运行):进程是可执行的,它或者正在执行,或者在运行队列中等待执行,这是进程在用户空间中执行的唯一可能的状态
          2  task_interruptible(可中断):进程正在睡眠,等待某些条件的达成,一旦某些条件达成,内核就会把进程状态设置为运行,处于此状态的进行也会因为接收到信号而提前被唤醒并随时准备投入运行。
          3  task_uninterruptible(不可中断):除了就算是接收到信号也不会被唤醒或准备投入运行外,这个状态与可中断状态相同。这个状态通常在进程必须在等待时不受干扰或等待事件很快就会发生时出现。
          4  _task_traced:被其他进程跟踪的进程。例如通过ptrace进行调试。
          5  _task_stopped(停止):进程停止执行。


     内核经常需要调整某个进程的状态,这时可以使用set_task_state(task,state)函数,该函数将指定的进程设置为指定的状态。可执行代码是进程重要的组成部分,这些代码从一个可执行文件载入到进程地址空间执行,一般程序在用户空间执行,当一个程序执行了系统调用或者触发了某个异常,它就陷入了内核空间,此时,内核代表进程执行,并处于进程上下文中,在内核退出时,程序恢复在用户空间会继续执行。系统调用和异常处理程序是对内核明确定义的接口,进程只有通过这些接口才能陷入内核。

     linux中的进程都有明显的继承关系,所有的进程都是PID为1的进程的后代,内核在系统启动的最后阶段启动init进程,该进程读取系统的初始化脚本并执行其他相关程序,最终完成系统启动的整个过程,init进程的进程描述符是作为init_task静态分配的。系统中每个进程必有一个父进程,每个进程则可以拥有0个或多个子进程。拥有同一个父进程的所有进程被称为兄弟进程,进程间的关系存放在进程描述符中,每个task_struct都包含一个指向其父进程task_struct、叫做parent的指针,还包含一个称为children的子进程链表。可以通过这种继承体系从系统中的任何一个进程出发查找到任意指定的其他进程。

进程创建
     许多操作系统都提供了spawn进程机制,就是首先在新的地址空间创建进程,读入可执行文件,最后开始执行。而unix采用了与众不同的方式,它把上述步骤分解到两个不同的函数中去执行:fork()和exec()。fork()通过拷贝当前进程创建一个子进程,子进程与父进程的区别仅仅在于PID、PPID和某些资源和统计量。exec()函数负责读取可执行文件并将其载入地址空间开始运行。

     传统fork()系统调用直接把所有资源复制给新创建的进程,这种实现过于简单,因为它拷贝的数据也许并不共享,linux的fork()使用写时拷贝(copy-on-write)页实现。写时拷贝页是一种可以推迟甚至免除拷贝数据的技术,内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝,只有在需要写入时,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读的方式共享。所以fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。

     linux通过clone()系统调用实现fork(),fork()根据自己的参数调用clone(),然后由clone()去调用do_fork()。do_fork()完成了创建中的大部分工作,该函数调用copy_process()函数,然后让进程开始运行。copy_process()完成以下工作:

          1  调用dup_task_struct()为新进程创建一个内核栈,它的定义在kernel/fork.c文件中。该函数调用copy_process()函数。然后让进程开始运行。从函数的名字dup就可知,此时,子进程和父进程的描述符是完全相同的。
          2  检查这个新创建的的子进程后,当前用户所拥有的进程数目没有超过给他分配的资源的限制。
          3  现在子进程开始使自己与父进程区别开来。进程描述符内的许多成员都要被清0或设为初始值。
          4  接下来子进程的状态被设置为TASK_UNINTERRUPTIBLE以保证它不会投入运行。
          5  调用copy_flags()以更新task_struct的flags成员,表明进程是否拥有超级用户权限的PF_SUPERPRIV标志被清0。表明进程还没有调用exec函数PF_FORKNOEXEC标志。
          6  调用get_pid()为新进程获取一个有效的PID.
          7  根据传递给clone()的参数标志,拷贝或共享打开的文件,文件系统信息,信号处理函数。进程地址空间和命名空间等。一般情况下,这些资源会被给定进程的所有线程共享;否则,这些资源对每个进程是不同的,因此被拷贝到这里.
          8  让父进程和子进程平分剩余的时间片
          9  最后作扫尾工作并返回一个指向子进程的指针。
 

线程在linux中的实现
     线程机制提供了在同一程序内共享内存地址空间运行的一组线程。这些线程可以共享打开的文件和其他资源,线程机制支持并发程序设计技术,在多处理器系统上,它也能保证真正的并行处理。linux实现线程的机制非常独特,从内核角度来说,它并没有线程这一概念。linux把所有的线程当进程来实现。内核并没有准备特别的调度算法或定义特别的数据结构来表示线程,相反线程仅仅被视为一个与其他进程共享某些资源的进程,每个线程都拥有唯一属于自己的task_struct,所以在内核中,它看起来像一个普通进程。对于linux来说,线程只是一种进程间共享资源的手段。

     线程的创建和普通进程的创建类似,只不过在调用clone()的时候需要传递一些参数标志来指明需要共享的资源,传递给clone()的参数标志决定了新创建进程的行为方式和父子进程之间共享的资源种类。内核经常需要在后台执行一些操作,这种任务可以通过内核线程完成,内核线程是独立运行在内核空间的标准进程。内核线程和普通进程的区别在于内核线程没有独立的地址空间,实际上它们的地址空间指针mm域被设置为null,它们只在内核空间运行,从来不切换到用户空间去,内核线程和普通进程一样可以被调度和抢占。内核线程只能由其他内核线程创建,内核是通过从kthread内核进程中衍生出所有新的内核线程来自动处理这一点的,kthread内核进程通过clone()系统调用创建新的任务,新创建的内核进程不会主动运行,需要wake_up_process()明确的唤醒它,内核线程运行后一直运行到调用do_exit()退出,或者内核的其他部分调用kthread_stop()退出。

进程终结
     当一个进程终结时,内核必须释放它所占有的资源,并把这个消息告诉父进程。一般来说,进程的析构是自身引起的,他发生在调用exit()系统调用时,当进程接受到它既不能处理也不能忽略的信号或者异常时,它还可能被动的终结。在调用了do_exit()后,尽管线程已经僵死不在运行,但是系统还是保留了它的进程描述符,这样做可以让系统在子进程终结后仍能或得它的信息,因此,进程终结时的清理工作和进程描述符的删除被分开执行,在父进程获得子进程终结的信息后,子进程的task_struct结构才会被释放。release_task()函数会释放进程描述符。
     如果父进程在子进程退出之前退出,必须有机制保证子进程能找到一个新的父进程否则这些成为孤儿的进程在退出时就会永远处于僵死状态,浪费资源。目前解决方法是:子进程在当前线程组内找一个线程作为父进程,如果不行,就选init作为他们的父进程。




共有 人打赏支持
粉丝 392
博文 377
码字总数 482277
×
China_OS
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: