进程管理

一、进程

1、程序本身并不是进程,进程是处于执行期的程序以及相关资源的总称。其资源包括:打开的文件、挂起的信号、内核内部数据、处理器状态、一个或多个具有内存映射的内存地址空间以及一个或多个执行线程、用来存放全局变量的数据段等。其实进程就是正在执行的程序代码的实时结果

2、进程的存活起始时间以及fork相关

  进程在创建它的时刻开始存活

  在Linux中同城是调用fork()系统的结果,该系统调用通过复制一个现有的进程来创建一个全新的进程。调用fork()的进程称为父进程。在该调用结束时,在返回到之前的调用fork()的位置处,父进程恢复执行,子进程开始执行。fork()系统调用从内核返回两次:一次回到父进程,另一次回到新产生的子进程。

    通常,创建新的进程都是为了立即执行新的、不同的程序,而接着调用exec()这组函数就可以创建新的地址空间,并把新的程序载入其中。最终,程序通过exit()系统调用退出执行,这个函数会终结进程并将其占用的资源释放掉。父进程可以通过wait()系统调用查

询子进程是否终结(wait()返回关于终止进程的状态)。

3、进程描述符及任务结构

   内核把进程的列表存放在叫做任务队列(task list)的双向循环链表中。链表中的每一项都是类型为task_struct(称为进程描述符)的结构。进程描述符中包含了一个具体进程的所有信息。

   taks_struct相对较大,在32位机器上,它大约有1.7KB。进程描述符中包含的数据能完整地描述一个正在执行的程序:它打开的文件、进程的地址空间、挂起的信号、进程的状态以及其他信息

 

4、分配进程描述符

    Linux通过slab分配器分配task_struct结构,这样通过预先分配和重复使用task_struct可以避免动态分配和释放所带来的资源消耗。(也就是说task_struct是预先创建好的,存在双向循环链表中,使用的时候直接给分配就行了,不用再创建)。由于是用slab分配器动态生成task_struct,所以只需要在栈底(对于向下增长的栈来说)或栈顶(对于向上增长的栈来说)创建新的结构struct threah_info(指向进程文件描述符的指针)使其在汇编代码中计算其偏移量非常容易每个任务的thread_info结构在它的内核栈的尾端分配。结构中task域中存放的是指向该任务实际task_struct的指针

 

5、进程描述符的存放

    内核把每个进程的PID存放在它们各自的进程描述符中,PID的最大值默认为32768,它实际上就是系统中允许同时存在的进程的最大数目。

    在内核中,访问任务通常需要获得指向其task_struct的指针。实际上,内核中大部分处理进程的代码都是直接通过task_struct进行的。因此通过current宏查找到当前正在运行进程的进程描述符的速度尤为重要。有的硬件体系结构可以拿出一个专门寄存器来存放指向当前进程task_struct的指针,用于加快访问速度。有些像x86这样的体系结构(其寄存器并不富裕),就只能在内核栈的尾端创建thread_info结构,通过计算偏移量间接地查找task_struct结构。

   在X86系统上,current把栈指针的后13个有效位屏蔽掉,用来计算出thread_info的偏

移,该操作通过current_thread_info()函数来完成的。最后current再从thread_info的task域中提取并返回task_struct的地址:current_thread_info()->task;

 

7、设置当前进程的状态

    内核经常需要调整某个进程的状态。这时最好使用set_task_state(task,state) /*将任务task的状态设置为state*/

8、所有进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程。该进程读取系统的初始化脚本并执行其他的相关程序,最终完成系统启动的整个过程。

9、写时拷贝

    传统的fork()系统调用直接把所有的资源复制给新创建的进程。这种实现过于简单并且效率低下,因为它拷贝的数据也许并不共享,更糟糕的情况是,如果新进程打算立即执行一个新的映像,那么所有的拷贝都将前功尽弃。Linux的fork()使用写时拷贝页实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父子进程共享一个拷贝,只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读的方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候才进行。在页根本不会被写入的情况下(举例:fork()后立即exec(),它们就无需复制了)

10、fork

   v_fork()、_clone()、fork()的底层都是clone()。这些库函数都根据各自需要的参数标志

去调用clone(),然后由clone()去调用do_fork()。do_fork()调用copy_process()函数,然后让进程开始运行。copy_process()函数完成的工作如下:

(1)调用dup_task_struct()为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同。此时,子进程和父进程的描述符是完全相同的

(2)检查并确保新创建这个子进程之后,当前用户所拥有的进程数目没有超出给它分配的资源的限制

(3)子进程着手使自己与父进程区别开来。

(4)子进程的状态被设置为不可中断,以保证它不会投入运行

(5)copy_process()调用copy_flags()以更新task_struct的flags成员。表明进程是否拥有超级用户权限的标志被清为0。

(6)调用alloc_pid()为新进程分配一个有效的PID

(7)根据传递给clone()的参数标志,copy_process()拷贝和共享打开的文件、文件系统信息、信号处理函数、进程地址空间、命名空间等。在一般情况下,这些资源会被给定进程的所有线程共享;否则,这些资源对每个进程是不同的,因此被拷贝到这里。

(8)最后copy_process()做扫尾工作并返回一个指向子进程的指针。

 再回到do_fork()函数,如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行。内核有意选择子进程首先执行(虽然想让子进程首先执行,但并非总能如此)。因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销,如果是

父进程首先执行的话,有可能会开始向地址空间写入。

二、线程

   线程是在进程中活动的对象。每个线程都拥有一个独立的程序计数器、进程栈和一组进程计数器。同一个进程中的所有线程可以共享虚拟内存、打开的文件和其他资源,但每个都拥有各自的虚拟处理

器。线程是CPU调度的最小单位。内核调度的对象是线程。

1、创建线程

   线程的创建和普通进程的创建类似,只不过在调用clone()的时候需要传递一些参数标志来指明需要共享的资源:

  clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND,0)

该代码产生的结果和调用fork()差不多,只是父子俩共享地址空间、文件系统资源、文件描述和信号处理程序。换个说法就是,新建的进程和它的父进程就是所谓的线程

   对比一个普通的fork()的实现:clone(SIGCHLD,0);

传递给clone()的参数标志决定了新创建进程的行为方式和父子进程之间共享的资源种类

    

 

2、内核线程

  内核需要经常在后台执行一些操作。这种任务可以通过内核线程完成,独立运行在内核

空间的标准进程。内核线程和普通的进程间的区别在于内核线程没有独立的地址空间(实际上指向地址空间的mm指针被设置为NULL)它们只在内核空间运行,从何来不切换到用户空间去。内核进程和普通进程一样,可以被调度,也可以被抢占

3、进程终结

  当一个进程终结的时候,内核必须释放它所占有的资源并把者件事告诉其父进程。进程的结束任务大部分都要靠do_exit()来完成,它所做的事情如下:

(1)将tast_struct中的标志成员设置为PF_EXITING

(2)调用del_timer_sync()删除任一内核定时器。根据返回的结果,它确保没有定时器在排队,也没有定时器处理程序在运行

(3)如果BSD的进程记账功能是开启的,do_exit()调用acct_updata_integrals()来输出记账信息

(4)然后调用exit_mm()函数释放进程占用的mm_struct,如果没有别的进程使用它们(也就是说,这个地址空间没有被共享),就彻底释放它们。

(5)接下来调用sem_exit()函数。如果进程排队等候IPC信号,它则离开队列

(6)调用exit_files()和exit_fs(),以分别递减文件描述符、文件系统数据的引用计数。如果某个引用计数的值降为0,那么就代表没有进程在使用相应的资源,此时可以释放

(7)接着把存放在task_struct的exit_code成员中的任务退出代码设置为由exit()提供的退出代码,或者去完成任何其他由内核机制规定的退出动作。退出代码存放在这里供父进程随时检索

(8)调用exit_notify()向父进程发送信号,给子进程重新找养父,养父为线程组中的其他线程或者为init进程,并把进程状态(存放在task_struct结构的exit_state中)设成EXIT_ZOMBIE

(9)do_exit()调用schedule()切换到新的进程。因为处于EXIT_ZOMBIE状态的进程不会再被调度,所以这是进程所执行的最后一端代码。do_exit()永不返回

至此,与进程相关的所有资源都被释放掉了(假设该进程是这些资源的唯一使用者)进程不可运行(实际上也没有地址空间让它运行)并处于EXIT_ZOMBIE退出状态。它占用的所有内存就是内核栈、thread_info结构和tast_struct结构。此时进程存在的唯一目的就是向它的父进程提供信息。父进程检索到信息后,或者通知内核那是无关的信息后,由进程所持有的剩余内存被释放,归还给系统使用

4、删除进程描述符

   调用了do_exit()之后,尽管线程已经僵死不能再运行了,但是系统还保留了它的进程描述符。这样做可以让系统有办法在子进程终结后仍能获得它的信息。因此,进程终结时的清理工作和进程描述符的删除工作被分开执行。在父进程获得已终结的子进程的信息后或者告诉内核它并不关注那些信息后,子进程的task_struct结构才被释放

   wait()这一族函数都是通过唯一的一个系统调用wait4()来实现的。它的标准动作是挂起调用它的进程,直到其中的一个子进程退出,此时函数会返回该子进程的PID,此外,调用该函数时提供的指针会包含子函数退出时的退出代码。

   当最终需要释放进程描述符时,release_task()会被调用,用以完成以下的工作:

(1)它调用__exit_signal(),该函数调用_unhash_process(),后者又调用detach_pid()从pidhash上删除该进程,同时也要从任务列表中删除该进程

(2)_exit_signal()释放目前僵死进程所使用的所有剩余资源,并进行最终统计和记录

(3)如果这个进程是线程组最后一个进程,并且领头进程已经死掉,那么release_task()就通知僵死的领头进程的父进程

(4)release_task()调用put_task_struct()释放进程内核栈和thread_info结构所占的页,并释放tast_struct所占的slab高速缓存

至此,进程描述符和所有进程独享的资源就全部被释放掉了

5、给孤儿进程寻找父进程

  解决孤儿进程的方法就是给子进程在当前线程组内找一个线程作为父亲,如果不行,就让init做它们的父进程。在do_exit()中会调用exit_notify(),该函数会调用forget_original_parent(),而后者会调用find_new_reaper()来执行寻父过程

一旦系统为进程成功的找到和设置了新的父进程,就不会再有出现驻留僵死进程的危险了,init进程会例行调用wait()来检查其子进程,清楚所有与其相关的僵死进程

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

    

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值