进程就是处理执行期的程序(目标代码存放在某种存储介质上)。查进程并不仅仅局限于一段可执行程序代码。通常进程包括:

  • 打开的文件
  • 挂起的信号
  • 内核内部数据
  • 处理器状态
  • 地址空间
  • 一个或多个执行线程
  • 存放全局变量的数据段

      对linux而言,线程是特殊的进程,并不特别区分。在现代操作系统中,进程提供两种虚拟机制:虚拟处理器和虚拟内存。虽然实际上可能是许多进程正在分享同一个处理器,但虚拟处理器给进程一种假象,让这些进程觉得自己在独享处理器。而虚拟内存让进程在获取和使用内存时觉得自己拥有整个系统的所有内存资源。同一个进程中的多个线程可以共享虚拟内存,但拥有各自的虚拟处理器。

进程描述符及任务结构

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

      进程描述符结构图:

      linux内核--进程管理_处理器

      内核通过一个惟一的进程标识值(process identification value)或PID来标识每一个进程。内核把每个进程的PID存放在它们各自的进程描述符中。linux系统默认的PID最大值为32768,也可以通过修改内核参数kernel.pid_max = 65535来提高上限。

      进程描述符中的state域描述了进程的当前状态,系统中的每个进程必然处理5种进程状态中的一种:

  • task_running(运行):进程是可执行的,它或者正在执行,或者在运行队列中等待执行。这是进程在用户空间中执行惟一可能的状态;也可以应用到内核空间中正在执行的进程
  • task_interruptible(可中断):进程正在睡眠(也就是说它被阻塞),等待某些条件的达成。一旦这些条件达成,内核应该会把进程状态设置为运行。处于些状态的进程也会因为接收到信号而提前被响醒并投入运行。
  • task_uninterruptible(不可中断):除了不会因为接收到信号而被响醒从而投入运行外,这个状态与可打断状态相同。这个状态通常在进程必须等待时不受干扰或等待事件很快就会发生时出现。
  • task_zombie(僵死):该进程已经结束了,但是其父进程还没有调用wait4()系统调用,为了父进程能够获知它的消息,子进程的进程描述符仍然被保留着。一旦父进程调用了wait4(),进程描述符就会被释放。
  • task_stopped(停止):进程停止执行;进程没有投入运行也不能投入运行。通常这种状态发生在接收到SIGSTOP、SIGTSTP、SIGTIN、SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。

       进程的状态转换图:

linux内核--进程管理_空间_02

      linux系统进程之间存在一个显显的继承关系,所有进程都是PID为1的init进程的后代,内核在系统启动的最后阶段启动init进程,该 进程读取系统的初始化脚本并执行其它的相关程序,最终完成系统启动的整个过程。系统中的每个进程必有一个父进程,相应的,每个进程也可以拥有零个或多个子进程。进程间的关系存放在进程描述符中,每个task_struct都包含一个指向其父进程task_struct、叫做parent的指针,还包含 一个称为children的子进程链表。

 

进程创建

      linux进程创建使用两个函数fork()和exec()来完成。首先,fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID(每个进程唯一)、PPID(父进程的进程号)和某些资源和统计量。exec()函数负责读取可执行文件并将其载入地址空间开始运行。

      传统的fork()系统调用直接把所有的资源复制给新创建的进程,实种实现过于简单并且效率低下,因为它拷贝的数据也许并不共享,而且,如果新进程打算立即执行一个新的映像,那么所有的拷贝都将前功尽弃。linux fork()使用写时拷贝(copy-on-write)而实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个地址空间,而是让父进程和子进程同时共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在些这前,只是以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。在页根本不会被写入的情况下,举例来说:fork()后立即调用exec(),fork()的实际开销就是复制父进程的页表以及给子进程创建惟一的进程描述符。在一般情况下,进程创建后都会马上运行一个可执行的文件,这种优化可以避免拷贝大量根本就不会使用的数据。

线程在linux中的实现

      线程机制提供了同一程序内共享共享内存地址空间运行的一组线程。这些线程还可以共享打开的文件和其它资源。线程机制支持并发程序设计技术,在多处理器系统上,它也能保证真正的并行处理。linux把所有线程都当作进程来实现,线程仅仅被视为一个与其它进程共享某些资源的进程。每个线程都拥有惟一隶属于自己的task_struct,所以在内核中,它看起来就像一个普通的进程。

       对于linux来说,线程只是一种进程间共享资源的手段,假如有一个包含4个线程的进程,通常会有一个包含指向4个不同线程进程的指针的进程描述符。该描述符负责描述像地址空间、打开的文件这样的共享资源。

进程终结

      当一个进程终结时,内核必须释放它所占有的资源,并把这一不幸告知其父进程。当父进程在子进程之前退出时,必须有机制来保证子进程找到一个新的父亲,否则这些成为孤儿的进程就会在退出时永远处理僵死状态,白白的消耗内存。对于这个问题,解决的方法是给子进程在当前线程组内找一个线程作为父行,如果不行,就让init进程来作为它们的父亲。