深入Linux内核架构-进程管理和调度(二)

一、进程表示

Linux内核涉及进程和程序的所有算法都围绕一个名为task_struct的数据结构建立,该结构定义在include/sched.h中。这是系统中主要的一个结构。在阐述调度器的实现之前,了解一下Linux管理进程的方式很有必要。

task_struct包含很多成员,将进程与各个内核子系统联系起来,会逐一讨论。

task_struct定义如下:

include/sched.h

该结构的内容可以分解为各个部分,每个部分表示进程的一个特定方面。

进程和执行信息,如待决信号、使用的二进制格式(和其他系统二进制格式的任何仿真信息)、进程ID号(pid)、到父进程及其他有关进程的指针、优先级和程序执行有关的时间信息(例如CPU时间)。

有关已经分配的虚拟内存的信息。

进程身份凭据,如用户ID、组ID以及权限(权限是授予进程的特定许可。它们使得进程可以执行某些本来只能由root进程执行的操作)等。可使用系统调用查询或修改这些数据。

使用的文件包含程序代码的二进制文件,以及进程所处理的所有文件的文件系统信息,这些都必须保存下来。

线程信息记录该进程特定于CPU的运行时间数据(该结构的其余字段与所使用的硬件无关)。

在与其他应用程序协作时所需的进程之间通信有关的信息。

该进程所用的信号处理程序,用于响应到来的信号。

介绍task_struct中对进程管理的实现特别重要的一些成员。

state指定进程的当前状态,可使用如下值(这些是预处理器常数,定义在<linux/sched.h>中)

TASK_RUNNING:进程处于可运行状态。这并不意味着已经实际分配了CPU。进程可能会一直等到调度器选中它。该状态确保进程可以立即运行,而无需等待外部事件。

TASK_INTERRUPTIBLE:针对等待某事件或其他资源的睡眠进程设置的。在内核发送信号给该进程表明事件已经发生时,进程状态变为TASK_RUNNING,它只要调度器选中该进程即可恢复执行。

TASK_UNINTERRUPTIBLE:用于因内核指示而停用的睡眠进程。它们不能由外部信号唤醒,只能由内核亲自唤醒。

TASK_STOPPED 表示进程特意停止运行,例如,由调试器暂停。

TASK_TRACED 本来不是进程状态,用于从停止的进程中,将当前被调试的那些(使用ptrace机制)与常规的进程区分开来。

下列常量既可以用于struct task_struct的进程状态字段,也可以用于exit_state字段,后者明确地用于退出进程。

EXIT_ZOMBIE:僵尸状态。

EXIT_DEAD:指wait系统调用已经发出,而进程完全从系统移除之前的状态。只有多个线程对同一个进程发出wait系统调用时,该状态才有意义。

Linux提供资源限制(resource limit,rlimit)机制,对进程使用系统资源施加某些限制。该机制利用task_struct中的rlim数组,数组项类型为struct rlimit。

<linux/resource.h>

上述定义设计得通用,可以用于许多不同的资源限制。

系统调用 setrlimit 来增减当前限制,但不能超出rlim_max指定的值。getrlimits用于检查当前限制。

rlim数组中的位置标识了受限制资源的类型,这也是内核需要定义预处理器常数,将资源与位置关联起来的原因。表2-1列出可能的常数及其含义。

备注:由于Linux试图建立与特定的本地UNIX系统之间的二进制兼容性,因此不同体系结构的数值可能不同。

因为限制涉及内核的各个部分,内核必须确认子系统遵守了相应限制。

如果某一类资源没有使用限制,则将rlim_max设置为 RLIM_INFINITY 。例外情况包括下面所列举的。

打开文件的数目( RLIMIT_NOFILE ,默认限制在1 024)。

每用户的最大进程数( RLIMIT_NPROC ),定义为 max_threads/2 。 max_threads 是一个全局变量,指定在把八分之一可用内存用于管理线程信息的情况下,可以创建的线程数目。在计算时,提前给定20个线程的最小可能内存用量。

init 进程的限制在系统启动时即生效,定义在 include/asm-generic-resource.h 中的INIT_RLIMITS

2.6.24版本的内核在proc文件系统中对每个进程都包含对应的一个文件,可以查看当前的rlimit值:

1、进程类型

UNIX进程包括:由二进制代码组成的应用程序、单线程、分配给应用程序的一组资源(如内存、文件等)。新进程使用fork和exec系统调用产生的。

fork生成当前进程的一个相同副本,该副本称之为子进程。原进程的所有资源都以适当的方式复制到子进程,该系统调用之后,原来的进程有两个独立的实例。这两个实例的联系包括:同一组打开文件、同样的工作目录、内存中同样的数据(两个进程各有一份副本),等等。此外二者别无关联。

exec从一个可执行的二进制文件加载另一个应用程序,来代替当前运行的进程。加载一个新程序。因为exec并不创建新进程,所以必须首先使用fork复制一个旧的程序,然后调用exec在系统上创建另一个应用程序。

Linux还提供clone系统调用。clone的工作原理基本上与fork相同,但新进程不是独立于父进程的,而可以与其共享某些资源。可以指定需要共享和复制的资源种类,例如,父进程的内存数据、打开文件或安装的信号处理程序。

clone用于实现线程,但仅仅该系统调用不足以做到这一点,还需要用户空间库才能提供完整的实现。

2、命名空间

命名空间提供虚拟化的一种轻量级形式,使得可以从不同的方面来查看运行系统的全局属性。

1)概念

传统上,在Linux以及其他衍生的UNIX变体中,许多资源是全局管理的。例如,系统中的所有进程是通过PID标识的,这意味着内核必须管理一个全局的PID列表。而且,所有调用者通过uname系统调用返回的系统相关信息(包括系统名称和有关内核的一些信息)都是相同的。用户ID的管理方式类似,即各个用户是通过一个全局唯一的UID号标识。

全局ID使得内核可以有选择地允许或拒绝某些特权。虽然UID为0的root用户基本上允许做任何事,但其他用户ID则会受到限制。例如UID为n的用户,不允许杀死属于用户m的进程(m≠ n)。但这不能防止用户看到彼此,即用户n可以看到另一个用户m也在计算机上活动。只要用户只能操纵他们自己的进程,这就没什么问题,因为没有理由不允许用户看到其他用户的进程。

但有些情况下,这种效果可能是不想要的。如果提供Web主机的供应商打算向用户提供Linux计算机的全部访问权限,包括root权限在内。传统上,这需要为每个用户准备一台计算机,代价太高。使用KVM或VMWare提供的虚拟化环境是一种解决问题的方法,但资源分配做得不是非常好。计算机的各个用户都需要一个独立的内核,以及一份完全安装好的配套的用户层应用。

命名空间提供了一种不同的解决方案,所需资源较少。在虚拟化的系统中,一台物理计算机可以运行多个内核,可能是并行的多个不同的操作系统。而命名空间则只使用一个内核在一台物理计算机上运作,前述的所有全局资源都通过命名空间抽象起来。这使得可以将一组进程放置到容器中,各个容器彼此隔离。隔离可以使容器的成员与其他容器毫无关系。但也可以通过允许容器进行一定的共享,来降低容器之间的分隔。例如,容器可以设置为使用自身的PID集合,但仍然与其他容器共享部分文件系统。

本质上,命名空间建立了系统的不同视图。此前的每一项全局资源都必须包装到容器数据结构中,只有资源和包含资源的命名空间构成的二元组是全局唯一的。虽然在给定容器内部资源是自足的,但无法提供在容器外部具有唯一性的ID。

考虑系统上有3个不同命名空间的情况。命名空间可以组织为层次,在这里讨论这种情况。一个命名空间是父命名空间,衍生两个子命名空间。假定容器用于虚拟主机配置中,其中的每个容器必须看起来像是单独的一台Linux计算机。因此其中每一个都有自身的 init 进程,PID为0,其他进程的PID以递增次序分配。两个子命名空间都有PID为0的 init 进程,以及PID分别为2和3的两个进程。由于相同的PID在系统中出现多次,PID号不是全局唯一的。

虽然子容器不了解系统中的其他容器,但父容器知道子命名空间的存在,也可以看到其中执行的所有进程。图中子容器的进程映射到父容器中,PID为4到9。尽管系统上有9个进程,但却需要15个PID来表示,因为一个进程可以关联到多个PID。至于哪个PID是“正确”的,则依赖于具体的上下文。

如果命名空间包含的是比较简单的量,也可以是非层次的,例如UTS(UNIX Timesharing System: UNIX分时系统)命名空间。在这种情况下,父子命名空间之间没有联系。

注意:Linux系统对简单形式的命名空间的支持已有一段时间,主要是chroot系统调用。该方法可以将进程限制到文件系统的某一部分,是一种简单的命名空间机制。但真正的命名空间能够控制的功能远远超过文件系统视图。

新的命名空间用如下两种方法创建。

(1)在用 fork 或 clone 系统调用创建新进程时,有特定的选项可以控制是与父进程共享命名空间,还是建立新的命名空间。

(2)unshare 系统调用将进程的某些部分从父进程分离,其中也包括命名空间。

在进程已经使用上述的两种机制之一从父进程命名空间分离后,从该进程的角度来看,改变全局属性不会传播到父进程命名空间,而父进程的修改也不会传播到子进程,至少对于简单的量是这样。对于文件系统来说,情况比较复杂,其中共享机制非常强大,带来了大量的可能性。

2)实现

命名空间的实现需要两个部分:每个子系统的命名空间结构,将此前所有的全局组件包装到命名空间中;将给定进程关联到所属各个命名空间的机制。图2-4说明具体情形。

子系统此前的全局属性现在封装到命名空间中,每个进程关联到一个选定的命名空间。每个可以感知命名空间的内核子系统都必须提供一个数据结构,将所有通过命名空间形式提供的对象集中起来。 struct nsproxy 用于汇集指向特定于子系统的命名空间包装器的指针:

<linux/nsproxy.h>

当前内核的以下范围可以感知到命名空间。

UTS命名空间包含运行内核的名称、版本、底层体系结构类型等信息。UTS是UNIX Timesharing System(UNIX分时系统)的简称。

保存在 struct ipc_namespace 中的所有与进程间通信(IPC)有关的信息。

已经装载的文件系统的视图,在 struct mnt_namespace 中给出。

有关进程ID的信息,由 struct pid_namespace 提供。

struct user_namespace 保存的用于限制每个用户资源使用的信息。

struct net_ns 包含所有网络相关的命名空间参数。

主要讲解UTS和用户命名空间。在创建新进程时可使用 fork 建立一个新的命名空间,必须提供控制该行为的适当的标志。每个命名空间都有一个对应的标志:

<linux/sched.h>

每个进程都关联到自身的命名空间视图:

<linux/sched.h>

因为使用了指针,多个进程可以共享一组子命名空间。这样,修改给定的命名空间,对所有属于该命名空间的进程都可见。

注意,对命名空间的支持必须在编译时启用,而且必须逐一指定需要支持的命名空间。但对命名空间的一般性支持总是会编译到内核中。 这使得内核不管有无命名空间,都不必使用不同的代码。除非指定不同的选项,否则每个进程都会关联到一个默认命名空间,这样可感知命名空间的代码总是可以使用。但如果内核编译时没有指定对具体命名空间的支持,默认命名空间的作用则类似于不启用命名空间,所有的属性都相当于全局的。

init_nsproxy 定义初始的全局命名空间,其中维护了指向各子系统初始的命名空间对象的指针:

<kernel/nsproxy.c>

<linux/init_task.h>

1)UTS(UNIX分时系统)命名空间

UTS命名空间几乎不需要特别的处理,因为它只需要简单量,没有层次组织。所有相关信息都汇集到下列结构的一个实例中:

<linux/utsname.h>

使用 uname 工具可以取得这些属性的当前值,也可以在 /proc/sys/kernel/ 中看到:

初始设置保存在 init_uts_ns 中:

init/version.c

相关的预处理器常数在内核中各处定义。

注意,UTS结构的某些部分不能修改。例如,把 sysname 换成 Linux 以外的其他值没有意义,但改变机器名是可以的。

内核如何创建一个新的UTS命名空间?这属于 copy_utsname 函数的职责。在某个进程调用fork 并通过 CLONE_NEWUTS 标志指定创建新的UTS命名空间时,则调用该函数。这种情况下,会生成先前的 uts_namespace 实例的一份副本,当前进程的 nsproxy 实例内部的指针会指向新的副本。由于在读取或设置UTS属性值时,内核会保证总是操作特定于当前进程的 uts_namespace 实例,在当前进程修改UTS属性不会反映到父进程,而父进程的修改也不会传播到子进程。

2)用户命名空间

用户命名空间在要求创建新的用户命名空间时,则生成当前用户命名空间的一份副本,并关联到当前进程的 nsproxy 实例。

<linux/user_namespace.h>

对命名空间中的每个用户,都有一个 struct user_struct 的实例负责记录其资源消耗,各个实例可通过散列表 uidhash_table 访问。

每个用户命名空间对其用户资源使用的统计,与其他命名空间完全无关,对root用户的统计也是如此。这是因为在克隆一个用户命名空间时,为当前用户和root都创建了新的 user_struct 实例:

kernel/user_namespace.c

alloc_uid 是一个辅助函数,对当前命名空间中给定UID的一个用户,如果该用户没有对应的user_struct 实例,则分配一个新的实例。在为root和当前用户分别设置了 user_struct 实例后,switch_uid 确保从现在开始将新的 user_struct 实例用于资源统计。实质上就是将 struct task_struct 的 user 成员指向新的 user_struct 实例。

注意:如果内核编译时未指定支持用户命名空间,那么复制用户命名空间实际上是空操作,即总是会使用默认的命名空间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值