Linux 服务器集群系统(七)Mosix

  集群技术和进程迁移简介

集群系统

在过去的几十年中,出现了许多支持高性能计算的计算机系统。最为普通的系统是 :

  • 大规模并行处理器(MPP)
  • 对称多处理器(SMP)
  • Cache一致性的非统一内存访问(CC-NUMA)
  • 分布式系统
  • 集群系统(Cluster )

集群系统以其较高的性价比越来越受到普遍的欢迎。集群是一种并行或分布处理的系统,它由一组互相连接的多个独立计算机的集合组成,并作为一个单独的集成的计算资源工作。这些计算机可以是单机或多处理器系统(PC、工作站或SMP),每个结点都有自己的 存储器、I/O设备和操作系统。集群对用户和应用来说是一个单一的系统,它可以提供低价高效的高性能环境和快速可靠的服务。

集群(cluster)计算技术一直是计算机界研究的一个热点问题。集群系统不但能够充分利用现有的计算资源,而且能够通过较低的软、硬件代价实现较高性 能的计算机系统。随着微处理器技术和高性能网络技术的飞速发展,集群计算逐渐成为一种有成本效益的并行/分布式计算资源。目前,最具代表性的集群系统是美 国UC Berkerly大学的NOW项目和NASA的Beowulf项目。

集群系统具有很多优点:

  • 易于同现有网络集成
  • 可伸缩性好,易于保护用户投资
  • 工作站上用户的低要求,使得盗用周期式的集群连接成为可能
  • 工作站上现有的丰富的标准成熟的开发工具

这些都使得集群系统成为一种发展趋势。





回页首


资源管理和负载平衡

集群计算系统一般是多用户(Multi-user)、分时共享(Time-sharing)的系统。集群系统的主要目标是通过网络互连实现全系统范围内的 资源的共享,同时通过高效的资源管理和任务调度技术实现资源的有效共享,从而提高资源利用率,获得高性能。为了使由独立机器组成的集群系统工作起来,且形 成对用户透明的单一系统,必须为其提供调度、负载平衡和共享服务。因此,我们认为资源的有效利用是集群系统软件研究的关键问题。

集群系统的资源管理与调度系统(RMS:Resource Management and Scheduling)是集群计算技术中一个非常重要的方面:

  • 从系统的角度来看,集群系统的资源使用率是最重要的问题。系统资源率使用越高,说明系统吞吐能力(Throughput)越大,资源共享的效果也越好。高吞吐能力计算(High-throughput Computing)的目标就是充分有效的利用集群系统中的所有资源,达到更高的总体性能。
  • 从用户的角度来看,任务的响应时间(或平均响应时间)是最为重要的问题。任务的平均响应时间是用户评价集群系统的重要指标。高性能计算(High-Performance Computing) 的目标就是尽量减少单个任务的执行时间。

集群计算系统的核心问题是资源的共享及有效利用,只有平衡的负载才能达到最大的资源使用率。因此,资源负载平衡是实现资源有效共享,提高系统资源使用率的必然要求。负载平衡的实现机制有两种:

  • 初始放置(Initial Placement)或远程执行(Remote Execution)。初始放置技术是当进程在某个计算节点产生时,根据负载平衡的要求将进程转移到其它节点远程执行的方法。
  • 进程迁移(Process Migration)。在进程运行过程中,根据系统的负载情况,通过将进程从负载较重(Over-loaded)的节点转移到另一负载较轻(Under-loaded)的节点继续运行,可以有效的实现负载的动态平衡。进程迁移技术使得动态调度成为可能。

虽然抢占式进程迁移算法比初始放置算法代价要高,但它的综合性能更为有效。Horchol-Balter和Downey的研究 认为,抢占式进程迁移算法能够减少平均延迟35-50%。进程迁移作为支持负载平衡和高容错性的一种非常有效的手段,一直受到人们的重视。





回页首


进程迁移(Process Migration)

什么是进程迁移?

进程迁移就是将一个进程从当前位置移动到指定的处理器上。它的基本思想是在进程执行过程中移动它,使得它在另一个计算机上继续存取它的所有资源并继续运行,而且不必知道运行进程或任何与其它相互作用的进程的知识就可以启动进程迁移操作,这意味着迁移是透明的。

进程迁移的好处

进程迁移是支持负载平衡和高容错性的一种非常有效的手段。对一系列的负载平衡策略的研究表明进程迁移是实现负载平衡的基础,进程迁移在很多方面具有适用性:

  • 动态负载平衡:将进程迁移到负载轻或空闲的节点上,充分利用可用资源,通过减少节点间负载的差异来全面提高性能。
  • 容错性和高可用性:某节点出现故障时,通过将进程迁移到其它节点继续恢复运行,这将极大的提高系统的可靠性和可用性。在某些关键性应用中,这一点尤为重要。
  • 并行文件IO:将进程迁移到文件服务器上进行IO,而不是通过传统的从文件服务器通过网络将数据传输给进程。对于那些需向文件服务器请求大量数据的进程,这将有效的减少了通讯量,极大的提高效率。
  • 充分利用特殊资源:进程可以通过迁移来利用某节点上独特的硬件或软件能力。
  • 内存导引(Memory Ushering)机制:当一个节点耗尽它的主存时,Memory Ushering机制将允许进程迁移到其它拥有空闲内存的节点,而不是让该节点频繁地进行分页或和外存进行交换。这种方式适合于负载较为均衡,但内存使用存在差异或内存物理配置存在差异的系统。

进程迁移的实现角度

进程迁移的实现复杂性及对OS的依赖性阻碍了进程迁移的广泛使用 ,尤其是对透明的进程迁移实现。根据应用的级别,进程迁移可以作为OS的一部分、用户空间、系统环境的一部分或者成为应用程序的一部分。

  • 用户级迁移:用户级实现较为简单,软件开发和维护也较为容易,因此,现有的很多系统都是采用用户级实现,如Condor和Utopia。但由于在用户级无 法获得Kernel的所有状态,因此,对于某类进程,无法进行迁移。另外,由于Kernel空间和User空间之间存在着壁垒,打破这个边界获得 Kernel提供的服务需要巨大的开销。因此,用户级实现效率远远低于内核级实现。
  • 应用级迁移:应用级迁移实现较为简单,可移植性好,但是需要了解应用程序语义并可能需对应用程序进行修改或重编译,透明性较差,这方面的系统有Freedman、Skordos等。
  • 内核级迁移:基于内核的实现可以充分利用OS提供的功能,全面的获取进程和OS状态,因此实现效率较高,能够为用户提供很好的透明性。但是由于需要对OS进行修改,实现较为复杂。这方面的典型系统有MOSIX和 Sprite系统。


进程状态

进程迁移的主要工作就在于提取进程状态,然后在目的节点根据进程状态再生该进程。在现实中,一个进程拥有很多状态,并且随着操作系统的演化,进程状态也越来越多样。一般来说,一个进程的状态可以分为以下几类:

  • 进程执行状态(Execution State):表示当前运行进程的处理器状态,和机器高度相关。包括内核在上下文切换时保存和恢复的信息,如通用和浮点寄存器值、栈指针、条件码等。
  • 进程控制(Process Control):操作系统系统用来控制进程的所有信,一般包括进程优先级、进程标识,父进程标识等。一旦系统编排了进程控制信息,进程迁移系统必须冻结该进程的运行。
  • 进程Memory状态和进程地址空间:包括进程的所有虚存信息,进程数据和进程的堆栈信息等,是进程状态的最主要的一部分。
  • 进程的消息(Message)状态:包括进程缓冲的消息和连接(Link)的控制信息。进程迁移中通讯连接的保持以及迁移后连接的恢复是进程迁移中一项较有挑战意义的问题。
  • 文件状态:进程的文件状态包括文件描述符和文件缓冲快。保持文件的Cache一致性和进程间文件同步访问也是进程迁移机制需要着重考虑的。

由于在同构的环境下(相同或兼容的机器体系结构和指令集以及操作系统)提取和恢复进程状态相对容易,现有的工作大多是以同构环境为前提的。不过,越来越多的人开始研究异构环境下的进程迁移机制,如TUI 系统。



参考资料

  1. Rajkumar Buyya."High Performance Cluster Computing:Architecures and System,Volume 1"。Prentice HALL PTR
  2. Allen B.Downey. Mor Harchol-Balter。 "A note on "The Limited Performance Benefits of Migrating Active Processes for Load Sharing" "
  3. Peter Smith. "Heterogenous Process Migration:The Tui System"


MOSIX系统简介

MOSIX是由Jerusalem的Hebrew大学开发的,它是一个软件管理层, 为Linux内核扩充了高性能集群计算支持能力。它采用单一系统映像模式SSI(Single System Image),支持所有的UNIX接口和机制,具有如下特性:

  • 易使用性和透明性:支 持多用户和时分共享环境,基于内核的支持,透明性好,不用修改应用程序。对所有的跨机器间的操作,如网络相关的操作,MOSIX为交互用户和应用级程序提 供了一个虚拟机,看起来像单独的一个机器。当一个进程发出系统调用,是由本地内核(进程当前驻留的机器)负责执行网络范围的操作来执行该调用。

  • 动态负载平衡:根据负载的波动和资源的可用性来发起进程迁移,通过在工作站上平均分布负载来提高性能。主要的负载平衡策略是负载计算算法(load calculation algorithm)、信息散布算法和竞争性迁移考虑算法。迁移考虑算法根据获得的可用负载信息,节点的相对速度和其它参数,做出最后的迁移决定。上述算法由每个节点独立执行,是分布和未同步的。

  • 最大的全面性能:采 用自适应资源管理方法, 通过抢先式进程迁移(可以透明的在任何时候、任何地方迁移任何进程),充分利用整个系统的资源和能力。抢占式进程迁移机可以在任意时候透明的将任意用户进 程迁移到任一可用的节点上去。迁移包括在远程节点上创建一个新的进程结构,然后拷贝进程的页表和所有的脏页。迁移后,除了home节点外,将没有其它的剩 余相关性。进程在新节点上恢复运行,经过几次页故障,将进程必须的一些页传送过来。

  • 分布式控制和高度的可伸缩性:为 了利用硬件的冗余来达到高可用性,工作站之间没有主从关系。系统的组成完全是分布式的,没有一个机器拥有整个系统的所有对象的信息。控制也是分布 的,MOSIX中不存在集中的控制机构。每个节点都能够作为独立的系统运行,并且自主的做出控制决策。这种设计允许动态配置,节点可以自由的加入或退出网 络系统而不会给其它节点带来影响。通过在算法中引入随机性来达到可伸缩性,每个节点并不试图获取整个系统的全面信息,而是只根据其它节点的部分知识做出控 制选择。

  • DFSA(Direct File System Access)支持:当 前集群计算技术的发展和迁移并行进程的能力使得产生一种可伸缩的集群文件系统成为一种需要,该文件系统不仅要能支持对文件的并行访问,而且要能在进程间保 持文件的Cache一致性。传统的文件系统如NFS、AFS和CODA并不能做到这一点,因为它们依赖于集中的文件服务器。DFSA则更适合于集群系统, 它通过将进程迁移到文件而不是传统的将文件数据传送给进程的方式对集群文件系统进行更好的支持。DFSA兼容的文件系统将允许迁移进程在当前节点直接文件 IO而不用通过Home节点,这极大得减小了通信开销,充分利用了数据的本地性,特别适合于IO密集型进程。当前在Linux上还未有其它支持DFSA的 文件系统,Mosix系统则实现了MFS(Mosix File System),支持DFSA特性。

MOSIX系统最显著的特性就是透明性,分布性和配置的灵活性。用户不必重新编译他们的应用程序,或者考虑不同处理器间的负载。并行应用可以通过简单的创 建多个进程运行在MOSIX系统上,就像在单机环境中一样的。系统将试图自动优化进程的分配和平衡负载。所有的用户进程看起来都似乎运行在用户的home 工作站上。每个新的进程是创建在其父进程所处的site上,如果被迁移的进程fork一个子进程,它将同时创建一份代理(deputy)和body的拷 贝。每个用户创建的进程将运行在用户home节点上的执行环境中。迁移进程将尽可能的使用本地资源,但却是和用户工作站上的用户环境交互。


MOSIX代理远程机制


进程迁移的一个主要的要求就是透明性,也就是说,系统的功能行为不应该因为进程迁移了而改变。表面上看来,透明性在基于消息的系统如Accent、 Charlotte和V系统上是很容易实现的。在这些系统中,进程是通过消息通道,以一种统一的方式和其它进程以及系统交互的。只需保持消息通道,将消息 转发到远程进程的新位置上即可确保透明性,这通常是通过更新消息通道端点的地址实现的。例如,Charlotte在迁移时更新地址,V则一直直到旧的不正 确的地址被使用时才通过多播协议更新地址。但是,在像LINUX这样通过系统调用接口为进程提供服务的系统中,实现透明性则比较困难。

MOSIX支持单系统映象SSI(Single System Image)。单系统映象SSI(Single System Image)的概念用软件或硬件的方式给用户造成一种幻觉,使多个计算元素统一为单个计算资源。SSI系统具有如下的特性:

  • 单系统: 用户视图是一个有多个CPU的单系统
  • 单控制: 通过单一的接口从同一点获取服务
  • 对称性: 用户可以在不同的结点获得相同的服务
  • 位置透明性: 提供服务的物理设备位置透明

迁移后,进程将继续和它原有的环境相交互而独立于当前所处的节点。为了实现透明性,系统需要能够定位进程,保持用户接口;进程也不必了解它已经从一个节点 迁移到另一个节点。一个简单的方法就是在进程的"home"节点上维持一个进程结构,代表该进程,并且和进程环境交互,Sprite系统就是采用这种方法 来实现迁移的透明性的。在MOSIX中,用户的工作站就是该进程的"home"节点,"home"节点上代表该进程的结构被称为deputy。 Deputy的概念是基于这样一个观察:只有进程的系统级是节点相关的,而用户级上下文是可迁移的。用户级上下文和系统级上下文间定义了良好的接口,可以 截获它们之间的交互,并且通过网络传送这些交互动作。

在MOSIX系统的实现中,一个迁移进程是由用户级上下文(Remote:远程)和系统级上下文(Deputy:代理)这两部分组成的。Remote包含 着程序代码、用户栈、数据、进程的寄存器和内存印象,是对进程在用户态运行的封装。Deputy则是对进程在内核级运行的封装,包括进程依附资源的描述和 系统代码运行时的核心栈等。Deputy包括进程系统上下文具有节点依赖性的那部分,因此是不能被迁移的。在MOSIX中,进程在创建时所处的节点称为进 程的唯一宿主节点(UHN:Unique Home-Node),Remote可以在不同的节点之间多次迁移,Deputy却只能保留在宿主节点上。

用户级上下文和系统级上下文之间具有良好定义的接口,在Linux中,进程只有通过系统调用才能进入核心态。因此,我们能够截获它们之间的每个互操作,并且通过网络转发操作。这功能由链接层(link layer)来实现,它拥有一个通讯通道来进行交互操作。Remote通过Deputy进行节点依赖性的工作,如环境变量和IO。如果Deputy所在节点出现故障,Remote将不能正常运行。



Deputy是远程进程在UHN上的表示体。进程的整个用户空间是驻留在远程节点上的,deputy并不拥有自己的内存映射。相反,它和内核线程很相似,共享内核的映射空间。

在许多内核活动中,例如系统调用的执行,必须在内核空间和用户空间之间互相传送数据。一般来说,这都是通过内核原语copy_to_user( )和copy_from_user( )完成的。在MOSIX中,任何内核中的内存操作,如果需要访问用户空间,deputy就必须和remote通讯,请求remote传送必要的数据。一个 系统调用中可能会重复好几次这样的远程拷贝操作。远程拷贝带来的开销很具体,主要在于网络延迟。为了消除不必要的远程拷贝,MOSIX实现了一个特别的 cache,在初始请求系统调用时预取尽可能多的数据,deputy在调用结束时缓存部分返回给remote的数据,从而减少交互的次数。

为了防止在缺少内存映射的情况下删除和覆盖掉内存映射文件,deputy拥有一个特殊的表,表中的文件都被映射到remote内存。迁移进程的用户寄存器 通常都是处于remote上下文的控制中。但是,每个寄存器或某些寄存器的组合可以临时为deputy拥有,处于deputy的操纵之下。

客户进程(remote)不能访问同一节点上中运行的其它进程(本地的或来自其它节点的),反之也一样。它们并不属于当前运行的远程节点上的任何用户,本 地进程也不会向它们发送信号或操纵它们。它们的内存无法被当前节点上的进程访问,只有本地系统管理员才能把它们强制迁移出去。

当进程逻辑上处于停止或睡眠状态时,可能要执行一些MOSIX操作。进程将会在它们的"睡眠"之中运行MOSIX操作,然后继续睡眠,直到它们等待的事件 发生。例如,可能当进程处于睡眠态时被完成了进程迁移。因此,MOSIX维持了一个逻辑状态,描述了其它的进程该如何看待该进程,而不是根据它此刻的瞬间 状态。



MOSIX连接层

作为一个集群系统,节点之间要经常合作,不时的散布和收集负载信息,获取其它节点的情况。另外,我们也已经看到,在MOSIX中,一个迁移进程实际上是由 deputy和remote两部分组成的。Deputy和remote是两个单独的进程,分别位于不同的节点,但在逻辑上,它们却是看出一个独立的进程 的。它们之间经常要通过频繁的通讯来合作。因此,通信机制是至关重要的。MOSIX提供了一个连接层抽象(linker layer),在套接字接口之上封装了一层,提供了一系列类似BSD SOCKET的接口函数用于在进程间接收和发送MOSIX相关数据的。MOSIX连接是位于内核中的,对用户态进程是不可见的。

作为一个集群系统,MOSIX中的每个节点都被分配一个唯一的节点号,节点号是一个整型值,从1开始连续分配的。0表示的是当前节点。通过节点号抽象,可 以很方便的定位某个节点。但是通过网络发送和接收数据,都是以IP地址来寻找节点的。因此,MOSIX系统提供了一系列函数用于在节点号和IP地址间的互 相转换和匹配。每个节点在/etc/目录下都存在着一个配置文件mosix.map,保存着整个集群系统节点的IP地址信息。该文件的每一行都包含3个 域,分别为节点范围的起始号,IP地址或主机名,该范围中的节点数目。例如某系统的配置文件如下:

1  172.26.4.138  1

2  172.26.4.139  2

4  MOSIX_4    1

机器MOSIX_4的IP为172.26.4.22。在这个系统中,节点1的IP为172.26.4.138。 节点2,3的IP则分别为172.26.4.139,172.26.4.140。节点4的IP则为172.26.4.22。

数据结构

struct mosix_link表示一条MOSIX连接,它维持着该连接的状态和一些控制信息。定义如下:

struct mosix_link {
struct socket *sock; /* 套接字控制结构,用于通讯,通过它进行实际的网络操作*/
int flags; /* 状态标志 */
int dlen; /*悬挂数据的长度 */
int peer; /* 连接另一端的节点号*/
char *hideptr; /*指向当前隐藏数据的指针*/
char *hidebuf; /*指向隐藏数据缓冲区起始地址的指针 */
int hidelen; /* 隐藏数据的长度*/
char head[COMM_DEF_HEADSIZE]; /* 存放着消息头*/
};
struct comm_header {消息格式
unsigned short olen;//Option data length--
unsigned short hlen;
int type;
int dlen;
int regs;
int dfsalen;
};
#define COMM_HLEN (sizeof(struct comm_header))



MOSIX 中,每个进程的控制块中都保持着一条MOSIX连接contact(struct mosix_link *contact),用于deputy和remote之间的通讯。进程的remote部分可以在节点间多次迁移,但是deputy总能根据它的 contact连接来定位remote部分;remote也能根据它的contact连接和deputy部分通讯合作。对于未迁移的进程,contact 总是为NULL。





回页首


MOSIX连接层接口

MOSIX连接层提供了一系列接口用于集群间进程之间互相发送和接收数据。从这些接口的名字上,我们就很容易判断出它们的功能。

  • comm_open():为MOSIX 通讯打开一条连接
    mosix_link * comm_open(int mos, mosix_addr *maddr, unsigned long timo)
    如果mos > 0,则连接到第#mos号节点上的迁移守护进程。
    如果mos = COMM_TOADDR,则连接maddr->saddr中给定的地址。
    如果mos = COMM_ACCEPT,则打开一个SOCKET,并且可以接受连接。
    如果mos = COMM_MIGD,则为迁移守护进程设置SOCKET。
    如果mos = COMM_INFOD,则为信息守护进程设置SOCKET。
    如果mos = COMM_LOOSE,则允许连接多个守护进程。

  • comm_use() :为进程设置新的连接并返回旧的连接
    mosix_link *comm_use(struct task_struct *p, mosix_link *mlink)
    进程p将使用mlink连接进行通讯。

  • comm_close():关闭Mosix通讯连接
    void comm_close(mosix_link *mlink)
    调用comm_shutdown(mlink)关闭对连接的写入,然后通过sock_release(mlink->sock)释放该连接上的套接字。

  • comm_accept() :在Mosix通讯套接字上接受连接
    int comm_accept(mosix_link *ml, mosix_link **mlp, mosix_addr *ma, unsigned long t)
    接受建立连接请求,mlp指向建立的新连接,连接的另一端的地址保存在ma中。

  • comm_waitaccept() :在Mosix通讯套接字上接受连接
    static int
    comm_waitaccept()comm_waitaccept()调用comm_accept接收连接请求并建立一条新的连接,然后comm_use这条新的连接并关闭旧的连接。

  • comm_send() :发送一条消息 (head + data)
    int comm_send(int type, void *head, int hlen, void *data, int dlen, int uspace)
    如果要发送的数据来自用户空间(uspace),且可能会导致远程page-fault(!dirty_all_remote_pages),则先把数据从用户空间拷入内核,然后检查地址范围的有效性(!ucache_ok)。
    如果type & COMM_MFREGS或则type & COMM_MFIDENT,则分别通过comm_packregs(0, 0)和comm_packident(0)进行压缩,可能产生选项数据。
    将数据按照消息格式组装起来,然后调用套接字操作集上的sendmsg操作将消息发送出去。

  • comm_sendto() :发送一个数据报
    int comm_sendto(int mos, void *data, int len, mosix_link *mlink, mosix_addr *to)
    通过mlink连接,发送len长的数据到to指明的地址,发送的数据在data中。
    to指明目的地址,如果为空,则数据是发送给节点mos上的负载信息守护进程(INFO_DAEMON)。
    调用套接字操作集上的sendmsg操作将消息发送到指定的地址。

  • comm_dorecv() :从连接中可靠的读取数据
    int comm_dorecv(struct socket *sock, struct msghdr *msg, int len)
    根据msg,通过调用套接字操作集上的recvmsg操作从网络连接上读取数据。它读的是字节流,并没有格式的。

  • comm_recv() :接收消息头
    int comm_recv(void **headp, int *hlen)
    如果当前进程的连接处于等待接受连接请求状态(COMM_WAITACCEPT),则等待直到接受请求。然后通过comm_dorecv()接收消息头长 度(COMM_HLEN)的数据,得到消息头的实际长度(hlen+olen),然后准备空间存放消息头数据(comm_mkhead),再通过 comm_dorecv()接收实际的消息头数据,并根据其中的信息,调用不同的解压处理程序。因为为了减少传输的数据量,消息数据发送前都经过了压缩。
    如果有数据COMM_MFDATA,则将数据长度保存在连接的数据长度中(mlink->dlen = header.dlen),这样,随后调用comm_copydata和comm_recvdata时,我们将知道应该能够从网络中读取多少数据。

  • comm_copydata():从消息中拷贝数据
    int comm_copydata(void *data, int len, int uspace) 返回0表示成功。
    从消息中拷贝长度为len的数据。读入的数据保存在data指向的buffer中。Uspace表明data是指向用户空间还是内核空间。成功时返回0。
    如果要data指向用户空间(uspace),且会导致远程page-fault(!dirty_all_remote_pages),则返回内存不足错误。
    如果连接中不存在数据,但是隐藏数据缓冲区不为空,则从隐藏数据缓冲区拷贝len长数据到data中。否则则通过comm_dorecv函数从网络中读取数据。

  • comm_recvdata():从连接中将所有数据读取到已分配缓冲区中/*
    int comm_recvdata(void **data) 返回0表示成功。
    如果连接中存在隐藏数据(COMM_HIDEDATA),则data指向隐藏数据缓冲区,然后置连接的隐藏数据缓冲区为NULL。
    否则,调用comm_malloc分配内存,通过comm_dorecv试图将连接所有的数据(mlink->dlen)都接收放入到其中。data将指向分配的内存缓冲区。

  • comm_recvfrom():接收数据报
    int comm_recvfrom(void *data, int len, mosix_link *mlink, mosix_addr *from, unsigned long timo)
    读取长为len的数据放在data中,from返回接收数据报的源地址。timo指明超时值,单位为微秒。返回接收数据的长度( >0)或者错误。
    调用套接字操作集上的recvmsg操作来接收数据,数据报的源地址返回在from中。如果接收到数据,则通过net_to_mos检查该源地址是否属于MOSIX节点。
    如果不属于MOSIX节点,则comm_recvfrom()返回错误。

  • comm_free() :释放不再被使用的消息头或数据。
    void comm_free(void *head)
    如果当前进程的该连接是COMM_INFOLINK,则释放head,返回。
    首先检查head是否超出连接消息头范围。如果head指向消息头的起始地址,则标记该消息头不再被使用(~COMM_HEADINUSE)。否则释放head所指的内存空间。

  • comm_mkhead() :准备指定大小的消息头空间
    void *comm_mkhead(int hlen)
    从内核中分配hlen大小的GFP_KERNEL内存空间,mlink->head指向分配的起始地址。

  • comm_flushdata():清空前一个消息剩余的数据
    void comm_flushdata(int dlen)
    如果当前连接是隐藏数据(mlink->flags & COMM_HIDEDATA),则从隐藏数据中flush长度为dlen的数据。否则,调用comm_dorecv从连接中读取长度为dlen的数据。

  • comm_peek():检查该连接是否有数据悬挂
    int comm_peek(void)
    调用套接字操作集上的poll函数来判断是否有数据悬挂。

  • comm_poll():等待直到有通讯事件、中断或MOSIX事件产生
    int comm_poll(int mask, int interruptible, unsigned long timo)
    interruptible指明是否可以被中断,timeo设置等待超时时间。

  • comm_wait() :等待消息到达或者MOSIX事件产生
    int comm_wait(void)
    返回1表示消息到达,返回0则表示先产生了一个事件。

  • comm_send_urgent():使用紧急数据(OOB)发送事件通知
    int comm_send_urgent(void)
    紧急事件通知只能由代理发送。MOSIX中将OOB数据定为0xdb。它只发送一个字节的有效数据(0xdb),通过将msghdr的msg_flags标志位设置MSG_OOB|MSG_NOSIGNAL来表示紧急情况。

  • comm_test_urgent():检查是否悬挂了紧急数据(OOB)
    int comm_test_urgent(void)
    通过设置MSG_OOB|MSG_PEEK|MSG_DONTWAIT|MSG_NOSIGNAL属性来调用套接字操作集上的recvmsg操作来接收数据。如果接收到的长度为1且等于0xdb,则表明悬挂了紧急数据。

  • comm_take_urgent():从流中取出悬挂的OOB数据,以免被转化为普通数据处理
    void comm_take_urgent(void)
    调用者必须确保流中没有其它的数据。

首 先以MSG_OOB|MSG_DONTWAIT|MSG_NOSIGNAL属性读取一字节,判断是否有OOB数据,然后设置套接字 SO_OOBINLINE选项为on,让带外数据保留在正常的输入队列中。然后调用套接字操作集上的recvmsg操作来读取这一个字节的带外数据。最后 关闭套接字的SO_OOBINLINE选项。



MOSIX进程迁移机制

迁移守护进程

在 内核成功装入内存以及一些关键硬件(如已经在低层设置过内存管理器MMU)后,内核将跳转到start_kernel完成其余的系统初始化工作。 start_kernel在完成初始化内核自身的部分组件-内存、硬件中断、调度程序,分析内核各种选项,测试CPU的缺陷后,将转而执行init函数, 在其中通过init_mosix()函数进行MOSIX系统的初始化。

MOSIX系统初始化时,将会首先产生三个守护进程 mosix_mig_daemon和mosix_info_daemon,mosix_mem_daemon。它们都是作为一个内核态线程运行的,没有虚 拟存储空间,直接使用物理地址空间。这三个守护进程必须驻留在宿主节点(UHN)中而不能被迁移走。

迁移守护进程 mosix_mig_daemon的主要功能则是处理迁移请求。它在固定的迁移端口(COMM_MIGD)监听迁移请求,每收到一个迁移请求,则fork 一个相应的子进程去处理该请求。整个过程就如同C/S结构网络编程中socket( )、bind( ),listen( )、accept( )、fork( ) 一致。当一个进程A需要迁移到另一个节点时,它将首先向该节点上的迁移守护进程建立一条连接。迁移守护进程将accept该请求,建立一条连接, 并fork一个子进程B执行mig_remote_receive_proc函数,处理该进程的迁移工作。子进程利用从父进程继承下来的刚刚建立的那条连 接,接收进程A的状态,并据此修改自身的进程状态。子进程B是迁移守护进程mosix_mig_daemon通过user_thread创建的,它最终退 出时会进入用户态,而此时它的进程状态已经被要迁移的进程A的状态所修改,因此,它会从A被迁移前暂停的地方继续恢复运行。此时,进程A将会一直运行在核 心态,成为deputy部分,而进程B则成为remote部分,通过它们之间建立的那条网络连接互相通讯。关于user_thread的细节以后将会解 释。





回页首


MOSIX进程迁移的一般过程

1 选择要迁移进程和目标节点:

MOSIX 中选择迁移进程有两种方式。一种是用户明确控制,通过MOSIX附带的系统命令migrate Pid Node,将进程标识符为Pid的进程迁移到节点Node上。命令mosrun 则允许在指定的节点上初始运行进程。另一种方式则是由负载平衡模块进行自动调度,根据集群的负载情况和历史信息,按照特定的平衡算法,选择某个进程迁移到 比较空闲的节点上,以达到整个集群的最佳性能。每个进程都有一个MOSIX控制块(mosix_task),它的成员whereto如果不为0,则为该进 程需要迁往的目的节点。

Mosix是采用接收者启动的方式。在Linux中,所有进程部分时间运行于用户模式,部分时间运 行于系统模式。进程不能被抢占,只要能够运行它们就不能被停止( 除非时间片被用完)。当进程必须等待某个系统事件时,它才决定释放出CPU。因此,如果负载平衡模块在核心态中,决定了要迁移走本地进程a。它只是通过 mosix_do_add_to_whereto( )设置进程a的whereto,标识它将要被迁移,而无法立即将进程迁移走。如果a能被Mosix事件唤醒的话,还会将其放入运行队列中。由于计算进程优 先级时(goodness( )),具有Mosix内容的进程的优先级会被提升。这样,在下一次进程调度(schedule( ))中, 具有Mosix事件的进程将更容易获得CPU。当进程a被调度选中执行时,在返回用户态之前,将会首先处理完所有能够处理的Mosix事件,并且判断是否 被选中迁移。如果被选中,则向目的节点的迁移守护进程发出迁移请求。如果该进程A需要被迁移,将会在此时被迁移走。因此,从概念上来说,进程可以说是"主 动"迁移,即迁移进程的动作实际发生在当前进程的上下文中,迁移的也是当前进程的内容,然后,该进程实际变为远程进程的代理(deputy)。

2 向目标节点发送迁移请求并协商:

当 节点A的调度函数schedule( )调度进程a运行时,在a退出核心态之前,OS将会检查该标志位,发现进程a要被迁移到节点B,则会向节点B上的迁移守护进程 mosix_mig_daemon发出迁移请求。 节点B上的迁移守护进程接收到该请求后,将会派生(fork)出一个新的进程b来处理迁移请求,自己则继续在迁移端口监听迁移请求信息。b被标志为 Remote进程。此时,进程b和进程a之间已经建立一条TCP的通讯连接,用于相互间交换信息。进程b则首先向B的负载平衡模块询问,是否要接受该请 求。负载平衡模块根据平衡算法来决定接收或拒绝迁移请求。

3 提取并传送进程状态:

当节点B同意进程a迁移到本节点后,将向节点A发出同意应答。A收到同意应答后,则开始获取a的进程状态,并通过a与b之间打开的连接将状态传送到进程b。进程b则接收进程a的状态,并据此修改自身的进程状态。

4 恢复进程的执行:

a 的进程状态迁移完后,a将成为进程的deputy部分,一直处于核心态的deputy_main_loop循环中,直到进程死亡或被迁移回home节点。 在节点B上,进程b被改为就绪状态,加入运行队列中,等待被调度执行。当b被调度后,返回到用户空间后,将从a迁移前的用户指令处继续往下执行。至此,整 个迁移过程完毕。





回页首


进程状态提取和恢复

在 Linux中,一个进程有关的所有信息都保存在进程控制块(struct task_struct)中,包括进程调度、进程标识和用户标识、信号量处理、文件系统管理、内存管理、进程标志等等信息。进程标识符pid唯一的标识着 一个进程。全局变量current指针指向当前正在运行进程的进程控制块。MOSIX是采用抢先式进程迁移机制,迁移的是当前的活跃进程。因此,通过全局 变量current,我们很容易获取到正在迁移进程的的进程状态信息。

进程内存状态:

task_struct 的数据成员mm指向关于存储管理的struct mm_struct结构。它包含着进程内存管理的很多重要数据,如进程代码段、数据段、未初始化数据段、调用参数区和进程环境区的起始地址和结束地址,栈 的起始地址和进程空间的大小。用户程序和代码一般占据虚拟地址空间的下方,这一部分是内存映射部分,其内容来自映射文件。界于用户执行映象和堆栈之间的部 分是堆,在用户程序中动态申请的地址空间(如malloc)来源于堆。虚拟内存结构mm_struct中的start_brk和brk记录有关堆的信息。 其中start_brk是用户虚拟地址空间初始化时,堆的终止位置;brk是当前堆的终止位置。


进程虚拟地址空间布局

下面列出了struct mm_struct结构的一些重要成员:

struct mm_struct {
struct vm_area_struct * mmap; /* 指向VMA双向链表的指针*/
struct vm_area_struct * mmap_avl; /*指向VMA段AVL树的指针*/
struct vm_area_struct * mmap_cache; /*保存上次 find_vma 的结果 */
pgd_t * pgd; /*进程页目录的起始地址*/
atomic_t mm_users; /*用户空间的用户数 */
atomic_t mm_count; /* "struct mm_struct" 的引用计数 */
int map_count; /* VMAs的个数 */
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
mm_context_t context; /* 体系结构相关的 MM上下文 */
struct rw_semaphore mmap_sem; /*读写信号量*/
};

MOSIX中进程的mm_struct结构中提取内存状态,并被封装成struct mm_stats_h结构,和MIG_MM_STATS 消息一起发送到目的进程去(参见mig_send_mm_stats())

struct mm_stats_h
{
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long task_size;
};

进程虚地址空间:

为 了能以自然的"方式"管理进程虚空间,Linux定义了虚存段VMA(vitual memory area),一个VMA是进程的一段连续的虚存空间,在这段虚存里的所有单元拥有相同的特征,如相同的访问权限、属于同一进程等。VMA由struct vm_area_struct描述,域vm_start 和vm_end是两个虚拟地址,它们描述了一个虚拟内存区域在进程虚拟地址空间中的位置;域vm_file记录了该区域所映射的文件,而 vm_offset 表示该区域在映射文件中的开始位置。如果这两个域存在,它们和vm_start 和vm_end一起,描述了文件vm_file和虚拟内存之间的映射关系。域vm_next是个指针,用来将同一进程的所有虚拟内存区域连接成一个链表, 表头位于mm_struct结构的mmap成员。因此,能够从mmap表头出发,依次获得所有的虚拟内存区域的状态。下面列出了struct vm_area_struc的一些主要成员。

struct vm_area_struct {
struct mm_struct * vm_mm; /* 所属的地址空间,回指mm_struct结构. */
unsigned long vm_start; /* 开始地址. */
unsigned long vm_end; /* 结束地址*/
struct vm_area_struct *vm_next; /*指向本进程下一个VMA ,按地址排序 */
pgprot_t vm_page_prot; /*该VMA的访问属性. */
unsigned long vm_flags; /* 标志位 */
struct vm_operations_struct * vm_ops; /*该VMA的操作集*/
struct file * vm_file; /* 指向映射文件 (可以为 NULL). */
/* AVL树,按地址排序*/
short vm_avl_height;
struct vm_area_struct * vm_avl_left;
struct vm_area_struct * vm_avl_right;
/*共享VMA*/
struct vm_area_struct *vm_next_share;
struct vm_area_struct **vm_pprev_share;
}

物理页面:

Linux 采用"按需调页"(demand paging)算法,支持三层页式存储管理策略。第一级是页目录(pgd),第二级是页中间目录(pmd),第三级是页表(pte)。页表由页表项组成, 每个页表项保存着物理内存页的首址和页属性。task_struct的数据成员mm的pgd成员为进程页目录的起始地址。Linux提供了一系列宏将虚地 址转换成物理地址。下面给出一段示例性代码,将虚地址vaddr转换成物理地址addr:

unsigned char* GeuphyAddr(struct vm_area_struct* vma,unsigned long vaddr)
{
pgd_t *pgdir; pmd_t *pmdir; pte_t *pte;
pgdir = pgd_offset(vma->vm_mm, vaddr);
if(pgd_none(*pgdir) ||pgd_bad(*pgdir))
return -EINVAL;
pmdir = pmd_offset(pgdir, vaddr);
if(pmd_none(*pmdir) ||pmd_bad(*pmdir))
return -EINVAL;
pte = pte_offset(pmdir, vaddr);
if(pte_present(*pte) )
{addr=(unsigned char )pte_page(*pte);return addr;}
return -EINVAL;
}

对进程的每个VMA,以物理页面大小(I386结构为4K)为间隔,分别进行三级地址映射,得到物理页面的起始地址,将物理内存依次拷贝到远程节点。远程节点则分配物理页面,在目的进程b上建立相应的页表项。下面给出一段示例性代码,接收一个页面:

int  receive_page(unsigned long addr)
{
pgd_t * pgd; pmd_t * pmd; pte_t * pte;
unsigned long page = 0;
struct vm_area_struct * vma;
struct task_struct *p = current
if(!(vma = find_vma(p->mm, addr)))
return -EINVAL;
if(!(page = __get_free_page(GFP_KERNEL)))
return -EINVAL;
/*从接收到的数据中拷贝PAGE_SIZE 大小的数据到page指向的区域*/
CopyData((void *)page, PAGE_SIZE, 0)) ;
/*建立并设置新的页表项 */
pgd = pgd_offset(p->mm, addr);
if (!(pmd = pmd_alloc(pgd, addr))) return -EINVAL;
if (!(pte = pte_alloc(pmd, addr))) return -EINVAL;
if (!pte_none(*pte)) return -EINVAL;
set_pte(pte, pte_mkdirty(mk_pte(page, vma->vm_page_prot)));
}

由于物理页面的迁移很费时间,为了给其它进程必要的运行时间,MOSIX并不会一次将所有页面完全迁移,而是迁移一定数目的页面后,就再次调度schedule( ),给予其它进程运行的机会。

浮点处理器状态

task_struct 结构中,used_math成员表明进程是否使用了浮点运算器,struct thread_struct 类型的thread成员则保存了该进程和CPU特定相关的状态。如果进程使用了浮点运算器的话,则也要迁移浮点处理器状态,这可以从 thread.i387结构中获得。

union i387_union {
struct i387_fsave_struct fsave;
struct i387_fxsave_struct fxsave;
struct i387_soft_struct soft;
};

局部描述符表(LDT)

局 部描述符表是一个段,它其中存放的是局部的、不需要全局共享的段描述符。例如,可以为每个正在运行的任务定义一个LDT,其中包含仅能被该任务使用的段描 述符,如任务的代码段、数据段、堆栈段以及一些门描述符等。struct mm_struct的成员context 保存着体系结构相关的MM上下文。但是i386 结构不存在 MMU 上下文, 而是将段信息保存在其中。LDT保存于mm的segment成员中。每个进程最多有LDT_ENTRIES (8192)个条目,每个条目大小为LDT_ENTRY_SIZE (8)。

其它进程状态信息

包括其它的一些进程状态,如进程的时间数据成员,进程标识,进程资源管理等。

  • 资源管理信息(limits)
    结构rlimit中定义了与进程相关的资源限制信息。包括一个用户可以拥有的进程数、一个进程可以打开的文件数、地址空间限制、文件大小、数据大小等。进 程可以随意减少自己的当前资源限制,也可以增加自己的当前资源限制,但增加的总数不能超过最大值,而且最大总数不能修改。一个进程只能修改自己的资源限 制。 进程中与资源控制相关的是数组rlim,其定义如下:
    struct rlimit {
    long rlim_cur; /* 当前值 */
    long rlim_max; /* 最大值 */
    };

    task_struct中rlim[RLIM_NLIMITS]数组保存了该进程得资源限制信息。迁移时,将从中取得CPU时间 (RLIMIT_CPU)、最大数据长度(RLIMIT_CPU)、栈大小上界(RLIMIT_CPU)、驻留进程集上界(RLIMIT_CPU),地址 空间界限(RLIMIT_CPU)等限制信息,传送到目的进程。
  • 进程标识信息
    包括进程的用户标识(uid)、用户组标识(gid)、进程的组标识号(pgrp)、 会话标识号(session)、可执行文件名(comm)。
  • 时间数据成员
    包括it_virt_value;和it_prof_value软定时值。
  • MOSIX控制相关状态




回页首


迁移限制

MOSIX的抢先式进程迁移机制进程支持对绝大多数进程的迁移,但是并不是所有的进程都能被迁移的。具有下面这些情况的进程是无法被迁移的。

  • 进程以共享内存的方式使用文件或者使用了SysV IPC的共享内存。
  • 进程映射了一些设备内存(device memory)。
  • 进程处于8086仿真模式。
  • 进程为内核守护进程。
  • 进程的部分内存被mlock系统调用锁住。
  • 进程与其它线程共享它的内存。
  • 进程为实时调度的进程。因为迁移后无法精确控制时间。
  • 进程被允许直接使用in/out IO指令(通过ioperm系统调用)。
  • init(1号)进程不能被迁移。
  • 进程映射硬件内存来进行原始(raw )I/O。
MOSIX对系统调用的处理


由于MOSIX的一个实现目标就是透明性,给用户一个单系统印象(SSI)。MOSIX实现了迁移进程的位置无关性,用户和应用并不需要知道进程已 被迁移走。即使节点A上的进程a被迁移走,A上的用户通过ps -ax命令仍能看到进程a,似乎进程a并未迁移走 。另外,在前面介绍的迁移过程中,并没有迁移进程的打开文件。但是remote进程仍能正常处理文件的读写,并保持与其它进程的网络通讯。通过 deputy和remote的交互配合,MOSIX系统实现了很高的透明性。

大多数处理器都有几种执行状态,如:核心态和 用户态。操作系统内核运行在核心态,用户进程运行在用户态。用户进程不能直接执行运行在核心态的内核代码或者存取操作系统内核的数据结构。这种方法保护了 操作系统内核,提高了系统的安全性。系统运行模式的区分虽然带来了安全性,但同时也带来了不便。因为系统的许多工作必须由内核代码完成(如创建进程、分配 内存、驱动设备等),用户程序无法、也不能直接做这些工作。但用户程序又不得不做这些工作(如创建子进程等),因此操作系统必须提供一种机制让用户程序能 在用户代码段中调用操作系统内核的函数。这就是通过系统调用。在MOSIX中,remote部分是对用户级的封装,而deputy则是进程内核级的封装, 进程通过系统调用进入核心态,在MOSIX中是怎么处理的呢?

当进程迁移成功后,deputy和remote之间仍然保持 着迁移中建立起来的那条通讯连接。 Remote则按照程序流程,恢复执行,当遇到系统相关的系统调用或资源请求时,如果不能在本地处理,则向deputy发送请求,然后等待Deputy处 理后的结果。Deputy在进程迁移后,一直处于核心态中,等待remote发送的请求,然后根据请求类型,作出相应的处理,将处理结果回复给 remote。

系统调用的处理

MOSIX 中,位置透明性是通过将节点相关的系统调用转发到home节点上的deputy来实现的。系统调用是用户上下文和系统上下文之间的一种同步的交互形式。进 程执行的所有系统调用都被远程节点的连接层所截获。如果是节点无关的,则在远程节点上本地执行。否则,系统调用将被转发给deputy,deputy则代 表home节点上的进程执行该系统调用。结果返回给Remote,Remote然后从系统调用返回,继续执行用户代码。

我 们知道,进程是通过系统调用进入核心态的,请求OS提供服务。在Linux中,系统调用处理程序地址都保存在表sys_call_table中。 【kernel/entry.S】中定义了系统调用的入口ENTRY(system_call),通过系统调用号查找sys_call_table表,找 到对应的系统调用处理程序并调用之。MOSIX中则定义了一个相应的remote_sys_call_table,并修改了 ENTRY(system_call)。

在ENTRY(system_call)中,首先判断进程是否为Remote。如 果是,则查找remote_sys_call_table;否则查找sys_call_table。对于本地的非remote进程,对系统调用的处理和通 常的一样,大家都很熟悉。我们主要介绍对remote进程系统调用的处理情况。remote_sys_call_table中保存的为对应的系统调用的 remote版本,对于大多数系统相关的调用的处理都是remote发送请求给deputy,再由deputy调用本地相应的处理函数处理的,然后将结果 返回给remote。

对于remote进程的各种系统调用,处理方式也各不一样。有的只是直接调用原有的系统调用,并不作 改变,像remote_sys_brk,remote_sys_mprotect等。有的则作些额外处理,然后通过 remote_standard_system_call来实现。remote_standard_system_call将系统调用的参数封装成 syscall_h包,然后向Deputy发送请求,请求类型为REM_SYSCALL,然后等待Deputy的回复消息 (remote_wait(REM_SYSCALL|REPLY,,) )。Deputy一直在deputy_main_loop中一直循环等待消息的到来,当收到请求后,判断其类型,并作相应的处理。对于 REM_SYSCALL请求,则调用deputy_syscall( )处理。deputy_syscall则从传入的参数中取出系统调用号,拷贝寄存器值,然后根据系统调用号查找系统调用表syscall_table中对 应的处理例程,由它完成真正的处理过程。然后,开中断,通过deputy_reply(REM_SYSCALL,,)发回响应消息和处理结果。

deputy这种方式将会给系统调用的执行带来不少的开销,主要是网络延迟。而系统调用是比较频繁的,因此这会给进程的执行带来一部分代价。





回页首


从系统调用返回

在系统调用最终真正返回,回到用户态之前,将会转到straight_to_mosix标号,调用mosix_pre_usermode_actions,在返回用户态之前作些MOSIX系统相关的处理工作。首先对当前进程的性质进行判断。

如 果当前进程是REMOTE,则调用remote_pre_usermode_actions()。首先检查deputy是否还需要它在核心态中为其做某些 处理工作,如果是,则睡眠等待直到deputy的允许进程继续运行。然后检查是否处于错误的节点,如果是,则设置需要迁移回home节点。如果出现紧急事 件,如前面设置需要迁移回home节点,则通知deputy有紧急事件发生。最后,如果存在未处理的异步信号或内核强制投递的信号,则将信号转发给 deputy处理。

如果是DEPUTY,则调用deputy_main_loop(),根据DEPUTY的特性,我们知道 它只是REMOTE在HOME节点的剩余,它应该主要处理和REMOTE的交互,而不是作实际的进程处理工作(计算或IO)。所以DEPUTY进程应该在 deputy_main_loop()中循环等待和REMOTE的交互,直到意外死亡或不再是DEPUTY。这就是我们前面所说为什么deputy一直处 于核心态的原因。

当前进程应该是本地的普通节点,作local_pre_usermode_actions()。首先做负 载信息的衰退工作。如果进程被选中迁移了,则调用follow_whereto()迁移之。每个进程都不是独立的,不可能不受系统运行其它进程的影响。如 果其它进程对本进程告知了某些请求,则必须检查并处理它。下面是一些考虑的请求:
DREQ_CHECKCONF:请求该进程检查MOSIX配置。
DREQ_CAPCNG:进程的权能被改变了。
DREQ_DFSASYNC:DFSA改变,需要被同步
DREQ_CHECKSTAY:检查进程是否需要继续呆在当前节点。



MOSIX对信号机制的处理

LINUX信号机制

信号是异步的进程间通讯机制,是在软件层次上对中断机制的一种模拟。LINUX内核的信号机制符合POSIX.4的规定,这是POSIX.1标准的一个超集。

每个进程的task_struct结构中都有个指针sig,指向一个singal_struct结构,结构中的数组action[]相当于一个信号向量表,每个元素确定了进程接收到一个具体的信号时应该采取的行动。

struct signal_struct {
atomic_t count;
struct k_sigaction action[_NSIG];
spinlock_t siglock;
};

那么系统如何判断一个进程是否有信号在等 待处理呢?这是通过task_struct结构中的sigpending成员。task_struct结构中的blocked成员则为屏蔽信号的集 合,pending成员则为信号队列,每产生一个信号则把它挂入这个队列,信号位图signal也保存在其中。

用 户常常要自己定义对信号的处理程序,并且用户的处理函数是位于用户空间的。LINUX提供了系统调用signal(sys_signal)和 sigaction(sys_sigaction 或sys_rt_sigaction)为信号设置处理向量。用户设置信号处理的时机我们是不能确定的,可以在进程迁移前,也可以在进程迁移之后,进程可以 在不同的节点间多次迁移,因此,如何保证信号不被丢失并且都能被正确处理就很重要。并且我们注意到,进程在迁移时,并不将信号向量表迁移到目标进程,而只 是将进程的异步信号和强制信号信息传送到目标进程【参见mig_send_misc()和mig_do_receive_misc()】。

struct asig_h
{
unsigned int sigs;/*信号*/
int nforced;/*内核发送的强制信号的个数*/
};
struct mosix_task
{。。。。。。。
uint32_t asig; /*到达REMOTE的信号 */
siginfo_t *forced_sigs; /* REMOTE强制信号信息*/
int nforced_sigs; /* REMOTE强制信号的个数 */
short sigmig; /* 迁移时接收的信号 */
}


int  mig_send_misc(int credit)
{ struct mig_misc_h m;
register struct task_struct *p = current;
………………..
m.asig.sigs = p->mosix.asig;
m.asig.nforced = p->mosix.nforced_sigs;
forced_sigs = p->mosix.forced_sigs;
sti();
if(comm_send(MIG_MISC, &m, sizeof(m), forced_sigs,
m.asig.nforced * sizeof(siginfo_t), 0))
………………
}

因此,我们可以说进程信号处理的状态是保 留在DEPUTY方的。这样做也是很自然的。首先在MOSIX中,对于REMOTE进程,几乎所有的系统调用都是请求DEPUTY来处理,和信号相关的一 些系统调用也不了例外。例如,sigprocmask()改变本进程得信号屏蔽位图,sigpending()检查有哪些信号已到达而未被处 理,signal()和sigaction()安装信号处理程序。其次,在不少内核操作中,进程进入睡眠以后刚被唤醒时,都会检测信号的存在从而提前返回 到用户空间。而DEPUTTY和REMOTE可以分别看作对系统上下文和用户上下文的抽象,所以DEPUTY保留着信号处理的状态。





回页首


信号响应

对信号的检测和响应总是发生在系统空间,通常发生在两种情况下:第一,当前进程由于系统调用、中断或异常而进入系统空间以后,从系统空间返回到用户空间的前夕。第二,当前进程在内核中进入睡眠以后刚被唤醒时,由于信号的存在而提前返回到用户空间。

当 进程由于中断进入系统空间以后,中断处理程序服务完后,将会转到入口ret_from_intr 。当进程由于异常而进入系统空间后,将会跳到error_code从而最终转到ret_from_exception处理【参见entry.S】。如果中 断或异常发生于用户空间,则转移到ret_check_reschedule,否则发生于内核空间,则到达restore_all。当进程由于系统调用进 入系统空间,将最终走到ret_from_sys_call。

ENTRY(ret_from_sys_call)
。。。。。
ret_check_reschedule:
cli # need_resched and signals atomic test
cmpl $0,need_resched(%ebx)/*判断是否需要调度*/
jne reschedule
cmpl $0,sigpending(%ebx) /*判断是否有悬挂的信号/
jne signal_return /*如果有信号待处理,则跳到signal_return */
straight_to_mosix:
call SYMBOL_NAME(mosix_pre_usermode_actions)
testl %eax,%eax
jne ret_from_sys_call
restore_all:
RESTORE_ALL
ALIGN
signal_return:
sti # 开中断
handler
testl $(VM_MASK),EFLAGS(%esp)/*是否处于VM86模式*/
movl %esp,%eax
jne v86_signal_return /*是VM86模式的话,则转到v86_signal_return */
call SYMBOL_NAME(do_signal) /* do_signal 对信号进行处理*/
jmp straight_to_mosix

从代码中我们可以看到,如果有信号待处理,则在退出系统空间前,会跳到signal_return,调用do_signal处理信号。

我 们看看do_signal ,它对信号作出具体的反应。如果当前进程是REMOTE,它只是简单的返回0。否则,该函数根据当前进程的signal域,确定进程收到了那些信号。对进 程收到的每一个信号,从进程的信号等待队列中找到该信号对应的附加信息,从进程的sig域的action数组中找到信号的处理程序及其相关的信息。于是, 如果用户设置了信号处理程序(在用户空间中),则最终会通过函数handle_signal()准备好对处理程序的执行 。

用户提供的信号处理程序是在用户空间执行的,而且执行完毕以后还还要回到系统空间。LINUX实现的机制如下:

  1. 用户空间堆栈中为信号处理程序的执行预先创建一个框架,框架中包括一个作为局部量的数据结构,并把系统空间的"原始框架"保存在这个数据结构中
  2. 在信号处理程序中插入对系统调用sigreturn()的调用
  3. 将系统空间堆栈中"原始框架"修改成为执行信号处理程序所需的框架
  4. "返回"到用户空间,但是却执行信号处理程序
  5. 信号处理程序执行完毕后,通过系统调用sigreturn()重返系统空间
  6. 在系统调用sigreturn()中从用户空间恢复"原始框架"
  7. 最后再返回到用户空间,继续执行原先的用户程序

对 于本地进程,这是在handle_signal()中由setup_rt_frame()或setup_frame()作出安排的。但是,对于 DEPUTY进程,则是通过mosix_deputy_setup_frame()实现的。因为,我们已经知道,DEPUTY是永远运行在核心态中的;进 程迁移后,代码段和数据段等都完全迁移到远程REMOTE进程。因此,信号处理程序必然是在REMOTE进程上运行的。那么,这又是如何实现的呢。

mosix_deputy_setup_frame() 通过deputy_request()函数向REMOTE发送DEP_SETUPFRAME请求,REMOTE将在remote_wait()函数中接收 到该请求,调用remote_setup_frame()来响应该请求。REMOTE进程在remote_setup_frame()中,根据 DEPUTY传来的参数,通过setup_rt_frame()或setup_frame()安排好一个框架。

这 样,当REMOTE进程从系统空间返回到用户空间时,将执行信号处理程序。然后,将通过sigreturn()系统调用重返系统空间。 sys_sigreturn()的作用就是从用户空间执行信号处理程序的框架中恢复当初系统空间中的原始框架。它通过 restore_sigcontext() 恢复框架的。但是对于DEPUTY进程,则是通过mosix_deputy_restore_sigcontext()函数来恢复系统空间的原始框架的 【参见sys_sigreturn()】。这里,因为是REMOTE进程调用sigreturn()系统调用,因此根据我们前面对系统调用的分 析,REMOTE进程向DEPUTY进程发送REM_SYSCALL请求,DEPUTY将在通过deputy_syscall()函数中调用 sys_sigreturn()来响应该请求。

mosix_deputy_restore_sigcontext() 则向REMOTE进程发送DEP_RESTORESIGCONTEXT请求。REMOTE在向DEPUTY发送REM_SYSCALL请求后,将处于 remote_wait()循环中等待REM_SYSCALL请求的应答【参见remote_standard_system_call()】。 REMOTE在remote_wait()中接收到DEP_RESTORESIGCONTEXT请求后,则通过 remote_restore_sigcontext()函数调用restore_sigcontext()真正恢复核心空间的原始框架。此后,当 REMOTE进程从系统空间返回后,将回到信号处理前原先的用户空间处继续往下执行。





回页首


信号发送

发 送一个信号给进程可以在用户空间通过系统调用发送,如通过sys_kill和 syr_rt_sigqueueinfo调用发送。内核也可以通过force_sig()和force_sig_info()向进程强制发送信号,将屏蔽 位强制清除,不允许目标进程忽略该信号。

在用户空间向一个进程发送信号由系统调用sys_kill()实现。 该函数调用函数kill_something_info(),它根据情况,或者向单个进程发送信号(kill_proc_info()),或者向一个进程 组中的所有进程发送信号(kill_pg_info()), 最终都会调用函数send_sig_info()来完成真正的信号发送。kill_pg_info()中,通过 for_each_local_task(p)来查找属于同组的进程。这是因为MOSIX中, 信号只会发给本地进程,而不会发送给REMOTE进程的。对于REMOTE进程,当通过kill()发送信号时,根据我们前面对系统调用的分析,我们知道 最终将是由DEPUTY来调用sys_kill()。因此,信号是被挂入DEPUTY的task_struct结构中的pending队列里。





回页首


异步和强制信号的处理

内 核也会向进程发送信号,例如当页面异常而又无法恢复时,do_page_fault()页面异常处理程序会通过force_sig()zx向当前进程发送 一个SIGBUS信号。内核发送的信号一般都是需要立即作出反应的。MOSIX对系统发送信号的处理方式也和用户发送信号有所不同,内核发送的"强制"信 号都保存在mosix_task的forced_sigs指针中。

我们首先看看函数force_sig_info()。如果信号的目的地为REMOTE进程,则:

  1. 如果处于中断服务中,则系统panic
  2. 得到进程已有的强制信号数(n= t->mosix.nforced_sigs)并试图分配内存(x = kmalloc((n + 1) * sizeof(siginfo_t), GFP_KERNEL))用于保存这n+1个信号信息
  3. 如果内存分配失败,则尽量再次发送该信号send_sig(sig, t, 0);返回0
  4. 因为分配内存返回时,可能已经处理了一部分信号,因此要进行检测。如果是,则释放刚刚申请的内存,跳到第二步
  5. 将保存的siginfo_t和新的info拷贝到分配的内存中,保存在mosix结构的forced_sigs中,并增加强制信号计数t->mosix.forced_sigs = x; t->mosix.nforced_sigs++;

我 们前面已经分析过,REMOTE进程从系统空间返回到用户态之前,将会调用remote_pre_usermode_actions()函数。 remote_pre_usermode_actions()函数将会检测当前进程是否有异步或"强制"信号待处理。如果有,它会通过函数 transfer_signals_to_deputy()发送REM_ASIG请求将信号传递给DEPUTY处理【参见 remote_pre_usermode_actions()】。DEPUTY则会通过函数 deputy_analyse_remote_signals()来处理REM_ASIG请求。它首先从连接中获得"强制"信号信息,通过 force_sig_info()向当前进程(即DEPUTY本身)发送强制信息。然后得到每个信号,依次处理,一般都是通过send_sig发给当前进 程。



关于作者


黄翊,男,国防科大计算机学院硕士研究生,对操作系统和集群技术感兴趣,欢迎您通过 easysing@yahoo.com.与他联系。






  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值