1.1 Linux进程管理

进程管理在任何操作系统上都是最重要事情。高效的进程管理能够确保应用高效稳定的运行。
Linux的进程管理方式类似于Unix的进程管理方式,包含进程调度、中断处理、信号、进程优先级、进程切换、进程状态、进程的内存等等。
在本节中,将会讨论Linux进程管理的实现基础,这会帮助你理解Linux内核如何处理影响系统性能的进程。

1.1.1 什么是进程


进程是在处理器中执行的实例,内核调度各类资源来满足进程的需求。
所有运行在Linux操作系统的进程都被task_struct这个结构体管理,task_struct也被称为进程描述符。进程描述符包含一个进程运行所需的所有信息,比如进程的id、进程的属性以及构建进程的资源。如果你知道进程的结构,你就能知道对于进程执行和性能最重要的是什么。下图1-2展示了进程信息相关结构的概览。

task_struct的结构

1.1.2 进程的生命周期


每个进程都有自己生命周期,比如创建、执行、终止和删除。在系统运行过程中,这些阶段反复执行成千上万次。因此,从性能的角度来看,进程的生命周期十分重要。
下图1-3展示了一般进程的生命周期

典型的进程生命周期

当一个进程创建一个新的进程,创建进程的进程(父进程)使用名为fork()的系统调用。当fork()被调用的时候,它会为新创建的进程(子进程)获得一个进程描述符,并且设置新的进程ID。复制父进程的进程描述符给子进程。这时候,不会复制父进程的地址空间,而是父子进程使用同样的地址空间。

exec()系统调用把新程序复制到子进程的地址空间。由于共享同样的地址空间,写入新进程的数据会引发页错误的异常。此时,内核给子进程分配新的物理页。

这个延迟的操作叫做Copy On Write。子进程和父进程执行的程序通常不一样,它执行自己的程序。这个操作避免了不必要的开销,因为,复制整个地址空间是很慢且低效率的,还会消耗很多的处理器时间和资源。

当程序执行完成,子进程使用exit()系统调用终止。exit()会释放进程的大部分数据结构,并且把这个终止的消息通知给父进程。这时候,子进程被称为zombie process(僵尸进程)。

直到父进程通过wait()系统调用知悉子进程终止之前,子进程都不会被完全的清除。一旦父进程知道子进程终止,它会清除子进程的所有数据结构和进程描述符。

1.1.3 线程


线程是单个进程中生成的执行单元。多个线程在同一个进程中并发运行。它们共享内存、地址空间、打开文件等等资源。还能访问同样的应用数据集。线程也被称为轻量级进程(Light Weight Process)。由于线程间共享资源,线程不能同时改变它们共享的资源。互斥、锁、序列化等等都是由用户应用程序来实现。

从性能的角度看,创建线程比创建进程更加低消耗,因为创建线程不需要复制资源。另一方面,从进程和线程在调度上看,他们拥有相似的行为。内核用类似的方法来处理他们。

进程和线程

在当前的Linux实现中,线程由POSIX(Portable Operating System Interface for UNIX,可移植操作系统接口)的兼容库(pthread)提供。Linux支持多线程。如下是已经被广泛使用的。

  • LinuxThreads

从Linux Kernel2.0以后,LinuxThreads就是Linux上的默认线程实现方法了。LinuxThreads和POSIX标准有一些不兼容的地方。本地POSIX线程库(Native POSIX Thread Library,NPTL)正在取代LinuxThreads。LinuxThreads将会被未来的Linux企业发行版所抛弃。

  • Native POSIX Thread Library(NPTL)

NPTL最开始是有红帽公司开发的,它和POSIX标准更加兼容。由于它在kernel2.6中具备增强的clone()新系统调用、信号处理实现等,它比LinuxThreads具备更好的性能和可扩展性。
NPTL与LinuxThreads有一些不兼容,依赖于LinuxThreads的应用可能没法在用NPTL实现的平台上工作。

  • Next Generation POSIX Thread(NGPT)

NGPT是IBM开发的POSIX线程库,它目前处于维护中,没有更多的开发计划。

使用LD_ASSUME_KERNEL环境变量,你可以为你的应用程序选择使用哪一种线程库。

1.1.4 进程优先级和nice级别


进程优先级由动态优先级和静态优先级决定,它是决定进程在CPU中执行顺序的数字。优先级越高的进程被处理器执行的机会越大。

根据进程的行为,内核使用启发式算法决定开启或关闭动态优先级。可以通过nice级别直接修改进程的静态优先级,拥有越高静态优先级的进程会获得更长的时间片(时间片是进程在处理器中的执行时间)。

Linux支持的nice级别从19(最低优先级)到-20(最高优先级),默认只是0。只有root身份的用户才能把进程的nice级别调整为负数(让其具备较高优先级)。

1.1.5 切换上下文


在进程执行过程中,进程的信息存放在处理器的寄存器和缓存中。这部分执行中进程存放在寄存器中的数据就叫做context,上下文。在切换进程中,正在处理的进程上下文被保存起来,把下一个要执行的进程的上下文恢复到寄存器。上下文通常存储在进程描述符和内核态栈中。进程切换就叫做上下文切换(context switching)。因为处理器每次上下文切换都要为新进程刷新寄存器和缓存,可能引发性能上的问题,所以应该尽量避免太多的上下文切换。
下图1-5描述上下文切换是如何工作的。
切换上下文

1.1.6 中断处理


中断处理是最高优先级别的任务之一。中断通常由I/O设备产生,譬如网络接口、键盘、磁盘控制器。中断处理器把键盘输入、网络帧到达这类事件通知给内核,它告诉内核尽快中断进程执行,因为某些设备需要快速的回应。这对系统稳定性是一个挑战。当中断信号到达内核,内核必须切换当前执行中的进程到新的进程,处理中断。这就意味着会发生上下文切换,同时也意味着大量的的中断会导致系统性能下降。

在Linux中有两类中断,硬中断是由设备产生的需要做出响应的中断,例如磁盘I/O中断,网卡中断,键盘和鼠标中断。软中断用于任务处理,可以推迟,例如TCP/IP操作、SCSI协议操作。可以在/proc/interrupts中看到相关的硬中断信息。

在多处理器环境中,中断由各个处理器自行解决。把中断绑定到单个物理处理器上可以增强系统性能。更多细节,参考4.4.2,CPU affinity for interrupt handling。

1.1.7 进程状态


每个进程都有自己的状态,显示进程中当前发生的事情,进程执行时进程状态发生改变。可能的状态列表如下:

  • TASK_RUNNING

    在这个状态中,进程正在CPU中执行,或者在运行队列(run queue)中等待运行。

  • TASK_STOPPED

    进程由于特定的信号(如SIGINT、SIGSTOP)而挂起就会处于这个状态,等待恢复信号,比如SINCONT。

  • TASK_INTERRUPTIBLE

    在此状态中,进程挂起并且等待一个特定的条件。假如进程处于TASK_INTERRUPTIBLE状态并且收到一个停止信号,进程状态会发生改变,操作会中断。TASK_INTERRUPTIBLE的典型例子是等待键盘中断。

  • TASK_UNINTERRUPTIBLE

    类似于TASK_INTERRUPTIBLE。当进程处于TASK_INTERRUPTIBLE 状态可以被中断,发送一个信号给TASK_UNINTERRUPTIBLE却不会有任何反应。TASK_UNINTERRUPTIBLE最典型的例子是进程等待磁盘I/O操作。

  • TASK_ZOMBIE

    进程在使用exit()系统调用退出以后,父进程应该知道进程终结。在TASK_ZOMBIE状态中,进程在等待父进程收到通知并释放所有的数据结构。

进程状态

僵尸进程
当进程已经收到信号而终止,正常情况下,完全结束之前,它有一些时间来完成所有的任务(例如关闭打开的文件)。在这个很短的的正常的时间片里,这个进程是僵尸。
当进程完成了所有的关闭操作, 它向父进程报告它即将终结。有时候,僵尸进程不能够结束它自己,这个状态下,它就显示状态为Z(zombie)。
因为它已经死了,所以不可能使用kill命令杀死这种进程。如果无法摆脱僵尸进程,可以杀死僵尸进程的父进程,这样僵尸进程也会消失。然后,如果僵尸进程的父进程是init,你就别这么做了,init是非常重要的进程,你可能要重启才能摆脱僵尸进程了。

1.1.8 进程内存段


进程使用自己的内存区域处理任务,任务种类由场景和进程用途决定。进程有不同的工作特性和不同数据大小要求,进程必须处理各种大小的数据。为满足这一要求,Linux内核给各个进程使用动态内存分配机制。进程内存分配结构如下图1-7。

进程地址空间

进程内存区域包含如下段:

  • 文本
    存储可执行代码的区域
  • 数据
    数据段由如下三个区域构成
    • Data:存储初始化数据,比如静态变量
    • BSS:存储初始化0数据,数据初始化为0
    • Heap(堆):根据需要使用malloc()分配动态内存。堆向高地址空间增长。

  • 该区域存储局部变量、函数参数和函数的返回地址。栈向低地址空间增长。

用户进程的地址空间分配可以使用pmap命令显示出来。你可以使用ps命令显示总的段大小。参考2.3.10,“pmap”和2.3.10,“ps和pstree”。

1.1.8 Linux的CPU调度


计算机的最基本功能就是计算,为了实现计算功能,必须要有办法管理计算资源、处理器、计算任务,也就是常说的进程和线程。感谢Ingo Molnar所做出的伟大贡献,Linux内核使用O(1)而不是O(n)来实现CPU调度。O(1)就是静态算法,意味着处理器选择和调用进程开始执行的时间是一个常数,不论多少进程数量的情况下都是如此。

新的调度器十分好用,不用考虑进程数量和处理器数量,强制使用很小的开销。这个算法使用两个进程优先级数组:

  • active
  • expired

由于调度器根据进程的优先级和先前阻塞率来位分配时间片,进程的优先级被放在一个active数组中。当时间片到期,它们被重新分配新的时间片,并且放置到expired数组上。当所有active数组上的进程都到期,active和expired数组发生对换,重启算法。对于一般的交互进程(对应实时进程),高优先级的进程通常会比低优先级进程分配更多的时间片,但是并不意味着完全不给低优先级进程机会。这个算法大大提高Linux内核的可扩展性,对于包含各种大量进程和线程以及处理器的工作需求,尤其如此。新的O(1)CPU调度算法是为内核2.6设计的,兼容内核2.4的版本。如下1.8展示和Linux的CPU调度器是如何工作的。

Linux内核2.6调度器

新调度器的另外一个大有有点是支持非均匀内存架构(Non-Uniform Memory Architecture,NUMA)和对称多线程处理器,例如Intel的超线程技术。
支持NUMA保证了正常情况下不会出现负载均衡的情况,除非一个节点负担过重。这个机制保证了在NUMA系统中,比较缓慢的链路负载较小。尽管在一个组中的处理器调度的每一个处理,会被负载均衡,但是调度器的组只会在节点负载过高和要求负载均衡的时候产生。

基于8路NUMA和超线程的cpu调度架构

results matching ""

    No results matching ""