Linux内核之进程管理

一、认识进程
  进程就是处于执行期的程序(目标代码存放在某种存储介质上),进程的另一个名字是任务(task),Linux内核通常把进程也叫做任务;但进程并不仅仅局限于一段可执行程序代码(Unix称其为代码段,text section)。通常进程还要包含其他的资源,像打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程(thread of execution),当然还包括用来存放全局变量的数据段等。实际上,进程就是正在执行的程序代码的实时结果。内核需要有效而又透明的管理所有的细节。
  执行线程,简称线程(thread),是在进程中活动的对象;进程是操作系统分配资源的基本单位,而线程是处理执行的基本单位。每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是进程。在传统的Unix系统中,一个进程只包含一个线程,但现在的系统中,包含多个线程的多线程已经司空见惯了。Linux系统对进程和线程并不特别区分,对Linux而言,线程只不过是一种特殊的进程而已。
  在现代的操作系统中,进程提供了两种虚拟机制:虚拟处理器和虚拟内存,虽然实际上可能是许多个进程正在分享一个处理器,但是虚拟处理器给进程一种假象,让这些进程觉得自己在独享处理器;而虚拟内存让进程在分配和管理内存时觉得自己拥有整个系统的所有内存资源。
  程序本身并不是进程,进程是处于执行期的程序以及相关的资源的总称,实际上,完全可能存在两个或多个不同的进程执行的是同一个程序。并且两个或两个以上并存的进程还可以共享许多诸如打开的文件、地址空间之类的资源。
  进程具有的特征:
    1)动态性:进程是程序的一次执行过程,是临时的,有生命的,是动态产生,动态消亡的;
    2)并发性:任何进程都可以同其他进程一起并发执行;
    3)独立性:进程是系统进行资源分配和调度的一个独立单位;
    4)结构性:进程由程序、数据和进程控制块三部分组成;

二、进程描述符
  内核把进程的列表存放在叫做任务队列(task list)的双向循环链表中,链表的每一项都是类型为task_struct、称为进程描述符(process descriptor)的结构中,进程描述符中包含一个具体进程的所有信息。每一个进程都有一个进程描述符,记录以下重要信息:进程标识符、进程当前状态、栈地址空间、内存地址空间、文件系统、打开的文件、信号量等。
  进程描述符在内存中的存放位置比较有特点,由于系统需要频繁的获取当前进程描述符的地址,为了提高效率,Linux巧妙的实现该功能,使用curren宏可以快速得到当前进程地址;在x86体系中,通过SP寄存器可以快速获取当前进程栈的位置;Linux在栈末端存放了一个特殊的数据结构thread_info,thread_info中存放了指向task_struct的指针,根据这个原理,首先当前进程通过SP寄存器获取栈的位置,然后根据栈大小(一般为1-2页)获取thread_info的地址,最后通过thread_info获取当前进程的地址;基于以上分析,进程的内核栈与thread_info存放在同一个页内,thread_info与内核栈共享了页,从源代码中可以看出,在分配内核栈的同时也分配了thread_info。

在这里插入图片描述
Task_Struct结构体信息:
  标识符:描述本进程的唯一标识符;
  状态:任务状态、退出代码、退出信息等;
  优先级:相对于其他进程的优先级;
  程序计数器:程序中被执行的下一条指令的地址;
  内存地址:包括程序代码和进程相关的数据的指针,还有其他进程共享内存块的指针;
  上下文数据:进程执行时处理的寄存器中的数据;
  I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表;
  记账信息:包括处理器时间总和,使用时钟总和、时间限制、记账信息等;

三、进程状态
  进程描述符中的state域描述了进程的当前状态信息,系统中的每个进程都必然处于五种状态中的一种,该域的值也必为下列五种状态标志之一:
  1)TASK_RUNNING(运行)—进程是可执行的;它或者正在执行,或者在运行队列中等待执行。这是进程在用户空间中执行的唯一可能的状态,这种状态也可以应用到内核空间中正在执行的进程;
  2)TASK_INTERRUPTIBLE(可中断)—进程正在睡吗(也就是说它被阻塞),等待某些条件的达成,一旦这些条件达成了,内核就会把进程状态设置为运行,处于此状态的进程也会因为接收到信号而提前被唤醒随时准备投入运行;
  3)TASK_UNINTERRUPTIBLE(不可中断)—除了就算是接收到信号也不会被唤醒或准备投入运行外,这个状态与可中断状态相同,这个状态通常在进程必须在等待时不受干扰或等待事件很快就会发生时出现;由于处于此状态的任务对信号不做响应,所以较之可中断状态,使用的得较少;
  4)_TASK_TRACED—被其他进程跟踪的进程,例如通过ptrace对调试程序进程跟踪;
  5)_TASK_STOPPED(停止)—进程停止执行;进程没有投入运行也不能投入运行,通常这种状态发生在接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态;

在这里插入图片描述

四、进程创建
  Unix的进程创建很特别,许多其他的操作系统都提供了产生(spawn)进程的机制,首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。Unix采用了与众不同的实现方式,它把上述步骤分解到两个单独的函数中去执行:fork()和exec()。首先,fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID(进程号,每个进程唯一)、PPID(父进程的进程号,子进程将其设置为被拷贝进程的PID)和某些资源和统计量(例如,挂起的信号,它没有必要被继承)。exec()函数负责读取可执行文件并将其载入地址空间开始运行。把这两个函数组合起来使用的效果跟其他系统使用的单一函数的效果相似。
  在这里插入图片描述
  写时复制:是一种可以推迟甚至免除拷贝数据的技术,内核此时并不复制整个进程的地址空间,而是让父进程和子进程共享一个拷贝,只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候才进行,在页根本不会被写入的情况下,它们就无须复制了;fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。在一般情况下,进程创建后都会马上运行一个可执行的文件,这种优化可以避免拷贝大量根本就不会被使用的数据,由于Unix强调进程快速执行的能力,所以这个优化是很重要的。
  Linux通过clone()系统调用来实现fork(),这个系统调用通过一系列的参数标志来指明父、子进程需要共享的资源。fork()、vfork()和_clone()库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork();do_fork()完成了创建中的大部分工作,该函数调用copy_process函数,然后让进程开始运行,copy_process()函数完成的工作步骤如下:
  1)调用dup_task_struct()为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同,此时,子进程和父进程的描述符是完全相同的;
  2)检查并确保新创建这个子进程后,当前用户所拥有的进程数目没有超出给它分配的资源的限制;
  3)子进程着手使自己与父进程区别开来,进程描述符内的许多成员都要被清0或设为初始值,那些不是继承而来的进程描述符成员,主要是统计信息。task_struct中的大多数数据都依然未被修改;
  4)子进程的状态被设置为TASK_UNINTERRUPTIBLE,以保证它不会被投入运行;
  5)copy_process()调用copy_flags()以更新task_struct的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV标志被清0。表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置;
  6)调用alloc_pid()为新进程分配一个有效的PID;
  7)根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。在一般情况下,这些资源会被给定的进程的所有线程共享;否则,这些资源对每个进程是不同的,因此被拷贝到这里;
  8)最后,copy_process()做扫尾工作并返回一个指向子进程的指针;

五、进程终结
  一个进程终归是要被终结的,内核必须释放并回收它所占用的资源并把这一不幸告知其父进程;一般来说,进程的析构是自身引起的,它发生在进程调用exit()系统调用时,既可能显式地调用这个系统调用,也可能隐式地从某个程序的主函数返回。当进程接收到它既不能处理也不能忽略的信号或异常时,它还可能被动的终结。不管进程是怎么终结的,该任务大部分都要靠do_exit()函数来完成,下面是do_exit()函数工作的步骤:
  1)将task_struct中的标志成员设置为PF_EXITING;
  2)调用del_timer_sync()删除任一内核定时器,根据返回的结果,它确保没有定时器在排队,也没有定时器处理程序在运行;
  3)如果BSD的进程记账功能是开启的,do_exit()调用acct_update_integrals()来输出记账信息;
  4)然后调用exit_mm()函数释放进程占用的mm_struct,如果没有别的进程使用他们,就彻底的释放他们;
  5)接下来调用sem_exit()函数。如果进程排队等候IPC信号,它则离开队列;
  6)调用exit_files()和exit_fs(),以分别递减文件描述符、文件系统数据的引用计数。如果其中某个引用计数的数值降为零,那么就代表没有进程在使用相应的资源,此时可以释放;
  7)接着把存放在task_struct的exit_code成员中的任务退出代码置为由exit()提供的退出代码,或者去完成任何其他由内核机制指定的退出动作,退出动作存放在这里供父进程随时检索;
  8)调用exit_notify()向父进程发送信号,给子进程重新找养父,养父为线程组中的其他线程或者为init线程,并把进程状态设成EXIT_ZOMBIE;
  9)do_exit()调用schedule()切换到新的进程,因为处于EXIT_ZOMBIE状态的进程不会再被调度,所以这是进程所执行的最后一段代码,do_exit()永不返回。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值