Tuesday, March 20, 2007

struct errmsg

struct errmsg
00046 { 00047     int e_code;
00048     char e_msg[64];
00049 } errmsgs[] = {
00050                   { EUNDEF,   "Undefined error code" },
00051                   { ENOTFOUND,    "File not found" },
00052                   { EACCESS,  "Access violation" },
00053                   { ENOSPACE, "Disk full or allocation exceeded" },
00054                   { EBADOP,   "Illegal TFTP operation" },
00055                   { EBADID,   "Unknown transfer ID" },
00056                   { EEXISTS,  "File already exists" },
00057                   { ENOUSER,  "No such user" },
00058                   { -1,       "0" }
00059               };

Monday, March 19, 2007

在你的代码中使用Boost智能指针(2)

 例子:在容器中使用shared_ptr
 
许多容器类,包括STL,都需要拷贝操作(例如,我们插入一个存在的元素到list,vector,或者container。)当拷贝操作是非常销毁资源的时候(这些操作时必须的),典型的操作就是使用容器指针。
std::vector<CMyLargeClass *> vec;
 
vec.push_back( new CMyLargeClass("bigString") );
 

 
 
将内存管理的任务抛给调用者,我们能够使用shared_ptr来实现。
typedef boost::shared_ptr<CMyLargeClass>  CMyLargeClassPtr;
 
std::vector<CMyLargeClassPtr> vec;
 
vec.push_back( CMyLargeClassPtr(new CMyLargeClass("bigString")) );
 

 
 
当vector被销毁的时候,这个元素自动被销毁了。当然,除非有另一个智能指针引用了它,则还本能被销毁。让我们看Sample3中的使用:
void Sample3_Container()
 
{
 
  typedef boost::shared_ptr<CSample> CSamplePtr;
 
 
 
  // (A) create a container of CSample pointers:
 
  std::vector<CSamplePtr> vec;
 
 
 
  // (B) add three elements
 
  vec.push_back(CSamplePtr(new CSample));
 
  vec.push_back(CSamplePtr(new CSample));
 
  vec.push_back(CSamplePtr(new CSample));
 
 
 
  // (C) "keep" a pointer to the second:
 
  CSamplePtr anElement = vec[1];
 
 
 
  // (D) destroy the vector:
 
  vec.clear();
 
 
 
  // (E) the second element still exists
 
  anElement->Use();
 
  printf("done. cleanup is automatic\n");
 
 
 
  // (F) anElement goes out of scope, deleting the last CSample instance
 
}
 

 
 
6、 使用Boost中的智能指针,什么是正确的使用方法
 
使用智能指针的一些操作会产生错误(突出的事那些不可用的引用计数器,一些对象太容易释放,或者根本释放不掉)。Boost增强了这种安全性,处理了所有潜在存在的危险,所以我们要遵循以下几条规则使我们的代码更加安全。
 
下面几条规则是你应该必须遵守的:
 
规则一:赋值和保存 ―― 对于智能指针来说,赋值是立即创建一个实例,并且保存在那里。现在智能指针拥有一个对象,你不能手动释放它,或者取走它,这将帮助你避免意外地释放了一个对象,但你还在引用它,或者结束一个不可用的引用计数器。
 
规则二:_ptr<T> 不是T* ―― 恰当地说,不能盲目地将一个T* 和一个智能指针类型T相互转换。意思是:
 
・         当创建一个智能指针的时候需要明确写出 __ptr<T> myPtr<new T>。
 
・         不能将T*赋值给一个智能指针。
 
・         不能写ptr = NULL,应该使用ptr.reset()。
 
・         重新找回原始指针,使用ptr.get(),不必释放这个指针,智能指针会去释放、重置、赋值。使用get()仅仅通过函数指针来获取原始指针。
 
・         不能通过T*指向函数指针来代表一个__ptr<T>,需要明确构造一个智能指针,或者说将一个原始指针的所有权给一个指针指针。(见规则三)
 
・         这是一种特殊的方法来认定这个智能指针拥有的原始指针。不过在Boost:smart pointer programming techniques 举例说明了许多通用的情况。
 
规则三:非循环引用 ―― 如果有两个对象引用,而他们彼此都通过一个一个引用指针计数器,那么它们不能释放,Boost 提供了weak_ptr来打破这种循环引用(下面介绍)。
 
规则四:非临时的 share_ptr ―― 不能够造一个临时的share_ptr来指向它们的函数,应该命名一个局部变量来实现。(这可以使处理以外更安全,Boost share_ptr best practices 有详细解说)。
 
7、 循环引用
 
引用计数器是一种便利的资源管理机制,它有一个基本回收机制。但循环引用不能够自动回收,计算机很难检测到。一个最简单的例子,如下:
struct CDad;
 
struct CChild;
 
 
 
typedef boost::shared_ptr<CDad>   CDadPtr;
 
typedef boost::shared_ptr<CChild>  CChildPtr;
 
 
 
struct CDad : public CSample
 
{
 
   CChildPtr myBoy;
 
};
 
 
 
struct CChild : public CSample
 
{
 
 CDadPtr myDad;
 
};
 
 
 
// a "thing" that holds a smart pointer to another "thing":
 
 
 
CDadPtr   parent(new CDadPtr);
 
CChildPtr child(new CChildPtr);
 
 
 
// deliberately create a circular reference:
 
parent->myBoy = child;
 
child->myDad = dad;
 
 
 
// resetting one ptr...
 
child.reset();
 

         parent 仍然引用CDad对象,它自己本身又引用CChild。整个情况如下图所示:
 

如果我们调用dad.reset(),那么我们两个对象都会失去联系。但这种正确的离开这个引用,共享的指针看上去没有理由去释放那两个对象,我们不能够再访问那两个对象,但那两个对象的确还存在,这是一种非常严重的内存泄露。如果拥有更多的这种对象,那么将由更多的临界资源不能正常释放。
 
       如果不能解决好共享智能指针的这种操作,这将是一个严重的问题(至少是我们不可接受的)。因此我们需要打破这种循环引用,下面有三种方法:
 
A、   当只剩下最后一个引用的时候需要手动打破循环引用释放对象。
 
B、   当Dad的生存期超过Child的生存期的时候,Child需要一个普通指针指向Dad。
 
C、  使用boost::weak_ptr打破这种循环引用。
 
方法A和B并不是一个完美的解决方案,但是可以在不使用weak_ptr的情况下让我们使用智能指针,让我们看看weak_ptr的详细情况。
 
 
 
8、 使用weak_ptr跳出循环
 
强引用和弱引用的比较:
 
一个强引用当被引用的对象活着的话,这个引用也存在(就是说,当至少有一个强引用,那么这个对象就不能被释放)。boost::share_ptr就是强引用。相对而言,弱引用当引用的对象活着的时候不一定存在。仅仅是当它存在的时候的一个引用。
 
boost::weak_ptr<T> 是执行弱引用的智能指针。当你需要它的时候就可以使用一个强(共享)指针指向它(当对象被释放的时候,它为空),当然这个强指针在使用完毕应该立即释放掉,在上面的例子中我们能够修改它为弱指针。
 
struct CBetterChild : public CSample
 
{
 
  weak_ptr<CDad> myDad;
 
 
 
  void BringBeer()
 
  {
 
    shared_ptr<CDad> strongDad = myDad.lock(); // request a strong pointer
 
    if (strongDad)                      // is the object still alive?
 
      strongDad->SetBeer();
 
    // strongDad is released when it goes out of scope.
 
    // the object retains the weak pointer
 
  }
 
};
 

 
 
9、 Intrusive_ptr――轻量级共享智能指针
 
shared_ptr比普通指针提供了更完善的功能。有一个小小的代价,那就是一个共享指针比普通指针占用更多的空间,每一个对象都有一个共享指针,这个指针有引用计数器以便于释放。但对于大多数实际情况,这些都是可以忽略不计的。
 
intrusive_ptr 提供了一个折中的解决方案。它提供了一个轻量级的引用计数器,但必须对象本身已经有了一个对象引用计数器。这并不是坏的想法,当你自己的设计的类中实现智能指针相同的工作,那么一定已经定义了一个引用计数器,这样只需要更少的内存,而且可以提高执行性能。
 
如果你要使用intrusive_ptr 指向类型T,那么你就需要定义两个函数:intrusive_ptr_add_ref 和intrusive_ptr_release。下面是一个简单的例子解释如何在自己的类中实现:
 
#include "boost/intrusive_ptr.hpp"
 
 
 
// forward declarations
 
class CRefCounted;
 
 
 
 
 
namespace boost
 
{
 
    void intrusive_ptr_add_ref(CRefCounted * p);
 
    void intrusive_ptr_release(CRefCounted * p);
 
};
 
 
 
// My Class
 
class CRefCounted
 
{
 
  private:
 
    long    references;
 
    friend void ::boost::intrusive_ptr_add_ref(CRefCounted * p);
 
    friend void ::boost::intrusive_ptr_release(CRefCounted * p);
 
 
 
  public:
 
    CRefCounted() : references(0) {}   // initialize references to 0
 
};
 
 
 
// class specific addref/release implementation
 
// the two function overloads must be in the boost namespace on most compilers:
 
namespace boost
 
{
 
 inline void intrusive_ptr_add_ref(CRefCounted * p)
 
  {
 
    // increment reference count of object *p
 
    ++(p->references);
 
  }
 
 
 
 
 
 
 
 inline void intrusive_ptr_release(CRefCounted * p)
 
  {
 
   // decrement reference count, and delete object when reference count reaches 0
 
   if (--(p->references) == 0)
 
     delete p;
 
  }
 
} // namespace boost
 

        
 
         这是一个最简单的(非线程安全)实现操作。但作为一种通用的操作,如果提供一种基类来完成这种操作或许很有使用价值,也许在其他地方会介绍到。
 
 
 
10、 scoped_array 和 shared_array
 
scoped_array 和 shared_array和上面讲的基本上相同,只不过他们是指向数组的。就像使用指针操作一样使用operator new[] ,他们都重载了operator new[]。注意他们并不初始化分配长度。
 
 
 
11、 Boost的安装
 
www.boost.org上下载最新版本的boost,然后解压缩到你指定的目录里,解压缩后的文件目录如下:
 
Boost\     boost的源文件和头文件。
 
Doc\        HTML格式的文档。
 
Lib\         库文件(不是必需的)
 
…             其他文件("more\"里有其他资料)
 
添加目录到我们自己的IDE里:
 
VC6:在菜单Tools/Options,Directories tab, "Show Directories for... Include files",
 
VC7: 在菜单Tools/Options,  Projects/VC++ directories, "Show Directories for... Include files".
 
Boost的头文件都在boost\子目录里,例如本文档例子中有#include "boost/smart_ptr.hpp"。所以任何人当读到年的源文件的时候就立刻知道你用到了boost中的智能指针。
 
 
 
12、 关于本文档中的例子
 
本文档中的例子里有一个子目录boost\仅仅包含了本例子中使用到的一些头文件,仅仅是为了你编译这个例子,如果你需要下载完整的boost或者获取更多的资源请到www.boost.org
 
 
 
13、 VC6中min/max的灾难
 
 
 
当在VC中使用boost库,或者其他库的时候会有一些小的问题。
 
在Windows的头文件中已经定义了min 和 max宏,所以在STL中的这两个函数就调用不到了,例如在MFC中就是这样,但是在Boost中,都是使用的std::命名空间下的函数,使用Windows的函数不能够接受不同类型的参数在模板中使用,但是许多库都依赖这些。
 
虽然Boost尽量处理这些问题,但有时候遇到这样的问题的时候就需要在自己的代码中加入像下面的代码在第一个#include前加入#define _NOMINMAX。
 
#define _NOMINMAX            // disable windows.h defining min and max as macros
 
#include "boost/config.hpp"  // include boosts compiler-specific "fixes"
 
using std::min;              // makle them globally available
 
using std::max;
 

         这样操作并不是在任何时候都需要,而只有我们碰到使用了就需要加入这段代码。
 
 
 
14、 资源
 
获取更多的信息,或者有问题可以查找如下资源:
 
・         Boost home page
 
・         Download Boost
 
・         Smart pointer overview
 
・         Boost users mailing list
 
・         Boost中的智能指针(撰文  Bjorn Karlsson    翻译  曾毅)
 

   郑重声明:
                 允许复制、修改、传递或其它行为
                 但不准用于任何商业用途.
                      写于  2004/11/5  masterlee
 
在你的代码中使用Boost智能指针(1)
 

Sunday, March 18, 2007

智能指针的标准之争:Boost vs. Loki

智能指针的标准之争:Boost vs. Loki
 
撰文/马维达
 
 2001 年10 月和2002 年4 月,在美国的华盛顿和荷兰的安的列斯群岛上分别召开了两次C++标准会议。会议的内容之一是对一项新的C++特性提议――智能指针(Smart Pointer)――进行讨论。本文将对可能成为C++新标准的两种智能指针方案(Boost vs. Loki)进行介绍和分析,并给出了相应的使用实例。
 
关键词:智能指针 C++ Boost Loki
 
  在现在的标准C++中,只有一种智能指针:std::auto_ptr。其原因并非是因为auto_ptr 已足以应付所有相关的工作――实际上,auto_ptr 有一个重大的缺陷,就是它不能被用在STL 容器中――而是因为现在的C++标准在制定时并未能对智能指针进行全面的考察。按照C++标准委员会成员Herb Sutter 的说法,只有一种标准的智能指针是一件"可羞"的事情:首先,智能指针所能做的许多有用的事情,是可怜的auto_ptr 不能完成的;其次,在有些情况下使用auto_ptr 可能会造成问题,上面所说的不能在容器中使用就是一例。实际上,许多程序员已经开发了各种有用的智能指针,有些甚至在auto_ptr 被定为标准之前就已存在,但问题是,它们不是标准的。在这样的情况下,C++标准委员会考虑引入新的智能指针,也就是自然而然的事情了。目前进入委员会视野的,主要有两种智能指针方案:Boost 智能指针和Loki 智能指针。前者是由C++标准委员会库工作组发起的Boost 组织开发的,而后者由世界级的C++专家Andrei Alexandrescu 开发,并在他所著的"Modern C++ Design"一书中进行了详细的阐释。下面,让我们分别来看一看这两种方案各自的技术特点。
 

一、 Boost 智能指针
Boost 的智能指针方案实现了五种智能指针模板类,每种智能指针都用于不同的目的。这五种智能指针是:
 
  template<typename T> class scoped_ptr;
  template<typename T> class scoped_array;
  template<typename T> class shared_ptr;
  template<typename T> class shared_array;
  template<typename T> class weak_ptr;
 
下面将分别介绍它们各自的特性,并给出相应的使用实例:
 

  scoped_ptr:意在用作指向自动(栈)对象的、不可复制的智能指针。该模板类存储的是指向动态分配的对象(通过new 分配)的指针。被指向的对象保证会被删除,或是在scoped_ptr 析构时,或是通过显式地调用reset 方法。注意该模板没有"共享所有权"或是"所有权转让"语义。同时,它也是不可复制的(noncopyable)。正因为如此,在用于不应被复制的指针时,它比shared_ptr 或std:auto_ptr 要更安全。与auto_ptr一样,scoped_ptr 也不能用于STL 容器中;要满足这样的需求,应该使用shared_ptr。另外,它也不能用于存储指向动态分配的数组的指针,这样的情况应使用scoped_array。
 
下面是使用scoped_ptr 的一个简单实例:
 
class CTest
{
public:
    CTest() : m_id(0) {}
    CTest(int id) : m_id(id) {}
    ~CTest() { std::cout << "id: " << m_id << " - Destructor is being called\n"; }
    void SetId(int id) { m_id = id; }
    int GetId() { return m_id; }
    void DoSomething()
    { std::cout << "id: " << m_id << " - Doing something\n"; }
private:
    int m_id;
};
void main()
{
    boost::scoped_ptr<CTest> pTest(new CTest);
    pTest->DoSomething();
}
 
其运行结果为:
  id: 0 - Doing something
  id: 0 - Destructor is being called
(以下的几个例子所用的CTest 类的定义完全相同,为节省篇幅,不再列出――作者)
 
显然,尽管我们自己没有调用delete,pTest 仍然为我们正确地删除了它所指向的对象。看起来scoped_ptr的用途和auto_ptr 十分类似,但实际上,scoped_ptr 类型的指针的所有权不可转让,这一点是和auto_ptr相当不同的。
 

  scoped_array:该模板类与scoped_ptr 类似,但意在用于数组而不是单个对象。std::vector 可用于替换scoped_array,并且远为灵活,但其效率要低一点。在不使用动态分配时,boost::array 也可用于替换scoped_array。
 
下面是一个使用scoped_array 的实例:
 
void main()
{
    boost::scoped_array<CTest> pTest(new CTest[2]);
    pTest[0].SetId(0);
    pTest[1].SetId(1);
    pTest[0].DoSomething();
    pTest[1].DoSomething();
    std::cout << '\n';
}
 
其运行结果为:
  id: 0 - Doing something
  id: 1 - Doing something
  id: 1 - Destructor is being called
  id: 0 - Destructor is being called
scoped_array 将负责使用delete [],而不是delete 来删除它所指向的对象。
 

  shared_ptr:意在用于对被指向对象的所有权进行共享。与scoped_ptr 一样,被指向对象也保证会被删除,但不同的是,这将发生在最后一个指向它的shared_ptr 被销毁时,或是调用reset 方法时。shared_ptr符合C++标准库的"可复制构造"(CopyConstructible)和"可赋值"(Assignable)要求,所以可被用于标
准的库容器中。另外它还提供了比较操作符,所以可与标准库的关联容器一起工作。shared_ptr 不能用于存储指向动态分配的数组的指针,这样的情况应该使用shared_array。该模板的实现采用了引用计数技术,所以无法正确处理循环引用的情况。可以使用weak_ptr 来"打破循环"。shared_ptr 还可在多线程环境中使用。
 
下面的例子演示怎样将shared_ptr 用于std::vector 中:
 
typedef boost::shared_ptr<CTest> TestPtr;
void PT(const TestPtr &t)
{
    std::cout << "id: " << t->GetId() << "\t\t" << "use count: " << t.use_count() << '\n';
}
void main()
{
    std::vector<TestPtr> TestVector;
    TestPtr pTest0(new CTest(0));
    TestVector.push_back(pTest0);
    TestPtr pTest1(new CTest(1));
    TestVector.push_back(pTest1);
    TestPtr pTest2(new CTest(2));
    TestVector.push_back(pTest2);
    std::for_each(TestVector.begin(), TestVector.end(), PT);
    std::cout << '\n';
    pTest0.reset();
    pTest1.reset();
    pTest2.reset();
    std::for_each(TestVector.begin(), TestVector.end(), PT);
    std::cout << '\n';
    TestVector.clear();
    std::cout << '\n';
    std::cout << "exiting...\n";
}
 
其运行结果为:
  id: 0 use count: 2
  id: 1 use count: 2
  id: 2 use count: 2
  id: 0 use count: 1
  id: 1 use count: 1
  id: 2 use count: 1
  id: 0 - Destructor is being called
  id: 1 - Destructor is being called
  id: 2 - Destructor is being called
  exiting...
 
运行结果中的"use count"是通过shared_ptr 的use_count()方法获得的"使用计数",也就是,对所存储指针进行共享的shared_ptr 对象的数目。我们可以看到,在通过new 分配了3 个CTest 对象,并将相应的shared_ptr 对象放入TestVector 后,三个使用计数都为2;而在我们使用reset()方法复位pTest0、pTest1 和pTest2 后,TestVector 中的各个shared_ptr 对象的使用计数变成了1。这时,我们调用TestVector的clear()方法清除它所包含的shared_ptr 对象;因为已经没有shared_ptr 对象再指向我们先前分配的3个CTest 对象,这3 个对象也随之被删除,并导致相应的析构器被调用。
 

  shared_array:该模板类与shared_ptr 类似,但意在用于数组而不是单个对象。指向std::vector 的shared_ptr 可用于替换scoped_array,并且远为灵活,但其效率也要低一点。
 
下面是使用实例:
 
void main()
{
    boost::shared_array<CTest> pTest1(new CTest[2]);
    pTest1[0].SetId(0);
    pTest1[1].SetId(1);
    std::cout << "use count: " << pTest1.use_count() << "\n\n";
    boost::shared_array<CTest> pTest2(pTest1);
    std::cout << "use count: " << pTest1.use_count() << "\n\n";
    pTest1.reset();
    pTest2[0].DoSomething();
    pTest2[1].DoSomething();
    std::cout << '\n';
    std::cout << "use count: " << pTest1.use_count() << "\n\n";
}
 
其运行结果为:
  use count: 1
  use count: 2
  id: 0 - Doing something
  id: 1 - Doing something
  use count: 1
  id: 1 - Destructor is being called
  id: 0 - Destructor is being called
如此例所示,我们通过new 所分配的数组只有在指向它的pTest1 和pTest2 都被销毁或复位后才被删除。
 

  weak_ptr:该模板类存储"已由shared_ptr 管理的对象"的"弱引用"。要访问weak_ptr 所指向的对象,可以使用shared_ptr 构造器或make_shared 函数来将weak_ptr 转换为shared_ptr。指向被管理对象的最后一个shared_ptr 被销毁时将删除该对象,即使仍有weak_ptr 指向它也是如此。与原始指针不同的是,届时最后一个shared_ptr 会检查是否有weak_ptr 指向该对象,如果有的话就将这些weak_ptr 置为空。这样就不会发生使用原始指针时可能出现的"悬吊指针"(dangling pointer)情况,从而获得更高的安全水平。
  weak_ptr 符合C++标准库的"可复制构造"(CopyConstructible)和"可赋值"(Assignable)要求,所以可被用于标准的库容器中。另外它还提供了比较操作符,所以可与标准库的关联容器一起工作。
 
void main()
{
    boost::shared_ptr<CTest> pTest(new CTest);
    boost::weak_ptr<CTest> pTest2(pTest);
    if(boost::shared_ptr<CTest> pTest3 = boost::make_shared(pTest2))
        pTest3->DoSomething();
    pTest.reset();
    assert(pTest2.get() == NULL);
}
 
其运行结果为:
  id: 0 - Doing something
  id: 0 - Destructor is being called
main 函数最后的断言确认了pTest2 所存储的指针的确已被置为NULL。
 
  显然,Boost 的智能指针方案会让我们产生这样的疑问:如果我们还需要其他类型的智能指针(比如支持COM 的智能指针),是否意味着我们必须在C++中再增加智能指针类型,或是采用非标准的实现呢?在泛型技术已得到极大发展的今天,Boost 的"增加增加再增加"的思路是不能让人满意的。正是在这里,我们看到了下面将要介绍的Loki Smart Pointer 的关键点:通过基于策略(policy-based)的设计来实现通用的智能指针模板。
 

二、 Loki 智能指针
  按照美国传统辞典(双解)的解释,Loki 是"A Norse god who created discord, especially among his fellow gods."(斯堪的纳维亚的一个制造混乱的神,尤其是在其同类之间)。就其给Boost 智能指针带来的麻烦而言,Loki 智能指针倒真的当得起这个名字;而在另一方面,就其实现的优雅以及功能的强大(也就是说,它给开发者带来的好处)而言,它也的确属于"神族"。
  上面已经说过,Loki 的智能指针方案采用了基于策略的设计。其要点在于把将各功能域分解为独立的、由主模板类进行混合和搭配的策略。让我们先来看一看Loki 智能指针模板类SmartPtr 的定义:
 
template
<
    typename T,
    template <class>class OwnershipPolicy =RefCounted,
    class ConversionPolicy =DisallowConversion,
    template <class>class CheckingPolicy =AssertCheck,
    template <class>class StoragePolicy =DefaultSPStorage
>
class SmartPtr;
 
我们可以看到,除了SmartPtr 所指向的对象类型T 以外,在模板类SmartPtr 中包括了这样一些策略:OwnershipPolicy(所有权策略)、ConversionPolicy(类型转换策略)、CheckingPolicy(检查策略)、StoragePolicy(存储策略)。正是通过这样的分解,使得SmartPtr 具备了极大的灵活性。我们可以任意组合各种不同的策略,从而获得不同的智能指针实现。下面先对各个策略逐一进行介绍:
 
  OwnershipPolicy:指定所有权管理策略,可以从以下预定义的策略中选择:DeepCopy(深度复制)、RefCounted(引用计数)、RefCountedMT(多线程化引用计数)、COMRefCounted(COM 引用计数)、RefLinked(引用链接)、DestructiveCopy(销毁式复制),以及NoCopy(无复制)。
  ConversionPolicy:指定是否允许进行向被指向类型的隐式转换。可以使用的实现有AllowConversion 和DisallowConversion。
  CheckingPolicy:定义错误检查策略。可以使用AssertCheck、AssertCheckStrict、RejectNullStatic、RejectNull、RejectNullStrict,以及NoCheck。
  StoragePolicy:定义怎样存储和访问被指向对象。Loki 已定义的策略有:DefaultSPStorage、ArrayStorage、LockedStorage,以及HeapStorage。
 
除了Loki 已经定义的策略,你还可以自行定义策略。实际上,Loki 的智能指针模板覆盖了四种基本的Boost 智能指针类型:scoped_ptr、scoped_array、shared_ptr 和shared_array;至于weak_ptr,也可以通过定义相应的策略来实现其等价物。通过即将成为C++标准(C++0x)的typedef 模板特性,我们还可以利用Loki 的SmartPtr 模板来直接定义前面提到的Boost 的前四种智能指针类型。举例来说,我们可以这样定义:
 
shared_ptr:
template<typename T> // typedef 模板还不是标准的
typedef Loki::SmartPtr
<
    T,
    RefCounted, // 以下都是缺省的模板参数
    DisallowConversion,
    AssertCheck,
    DefaultSPStorage
>
shared_ptr;
 
下面是一个使用Loki "shared_ptr"的实例:
 
typedef Loki::SmartPtr<CTest> TestPtr;
void PT(const TestPtr &t)
{
    std::cout << "id: " << t->GetId() << '\n';
}
 
void main()
{
    std::vector<TestPtr> TestVector;
    TestPtr pTest0(new CTest(0));
    TestVector.push_back(pTest0);
    TestPtr pTest1(new CTest(1));
    TestVector.push_back(pTest1);
    std::for_each(TestVector.begin(), TestVector.end(), PT);
    std::cout << '\n';
    Loki::Reset(pTest0, NULL);
    Loki::Reset(pTest1, NULL);
    std::for_each(TestVector.begin(), TestVector.end(), PT);
    std::cout << '\n';
    TestVector.clear();
    std::cout << '\n';
    std::cout << "exiting...\n";
}
 
其运行结果为:
  id: 0
  id: 1
  id: 0
  id: 1
  id: 0 - Destructor is being called
  id: 1 - Destructor is being called
  exiting...
 
前面已经提到,要通过Loki 定义与Boost 的shared_ptr 功能等价的智能指针,除了第一个模板参数以外,其他的参数都可以使用缺省值,所以在上面的例子中,我们直接使用"typedef Loki::SmartPtr<CTest> TestPtr;"就可以了。非常的简单!
 
为了进一步说明Loki 的"基于策略的设计方法",让我们再来看一个更为复杂的例子:通过Loki::SmartPtr 实现线程专有存储(Thread-Specific Storage,TSS;又称线程局部存储,Thread Local Storage,TLS)。
 
所谓线程专有存储,是指这样一种机制,通过它,多线程程序可以使用一个逻辑上的全局访问点来访问线程专有的数据,并且不会给每次访问增加额外的锁定开销。举一个简单的例子,在C 语言中,我们可以通过errno变量来获取错误代码;通常errno 就是一个普通的全局变量――在单线程环境中,这当然没有什么问题,但如果
是多线程环境,这个全局的errno 变量就会给我们带来麻烦了。TSS 正是解决这一问题的有效方案。
 
显然,智能指针的语义能够很好地适用于TSS。我们可以编写一种智能指针,使得所有对其所指向对象的访问都成为线程专有的――也就是说,每个线程访问的实际上是自己专有的对象,但从程序的外表来看,却都是对同一对象的访问。有了Loki::SmartPtr,我们可以非常容易地实现这样的智能指针:如其名字所指示的,TSS 涉及的是存储问题,我们只要对Loki::SmartPtr 的StoragePolicy 进行定制就可以了,其他的工作可以交给Loki::SmartPtr 去完成。
 
在POSIX PThreads 库和Win32 中都提供了用于线程专有存储的函数,它们分别是pthread_key_create、pthread_setspecific、pthread_getspecific 和pthread_key_delete(POSIX PThreads 库),以及TlsAlloc、TlsSetvalue、TlsGetvalue 和TlsFree(Win32)。关于这些函数的详细信息,请参阅相关的文档。
 
下面给出在MSVC 6.0 下实现的用于TSS 的StoragePolicy,并通过注释逐行进行分析(这个实现使用了PThreads-Win32 库,这是一个Win32 上的PThreads 实现。使用Win32 的线程专有函数也可以实现类似的StoragePolicy,但编写在线程退出时调用的CleanupHook()却需要一点"窍门"。具体方法可参考Boost 的thread_specific_ptr 实现):
 
namespace Loki
{
    // 实现TSS 的Loki 存储策略。改编自Douglas C. Schmidt、Timothy H. Harrison
    // 和Nat Pryce 的论文"Thread-Specific Storage for C/C++"中的部分代码。
    // 使用了"Loki VC 6.0 Port"。
template <class T> class TS_SPStorage
{
public:
    typedef T* StoredType; // 被指向对象的类型。
    typedef T* PointerType; // operator->所返回的类型。
    typedef T& ReferenceType; // operator*所返回的类型。
public:
    // 构造器。对成员变量进行初始化。
    TS_SPStorage() : once_(0), key_(0), keylock_(NULL)
        { pthread_mutex_init(&keylock_, NULL); }
    // 析构器。释放先前获取的资源。
    ~TS_SPStorage()
    {
        pthread_mutex_destroy(&keylock_);
        pthread_key_delete(key_);
    }
    // 返回线程专有的被指向对象。
    PointerType operator->() const { return GetPointee(); }
    // 返回线程专有的被指向对象的引用。
    ReferenceType operator*() { return *GetPointee(); }
    // Accessors。获取线程专有的被指向对象。
    friend inline PointerType GetImpl(TS_SPStorage& sp)
    { return sp.GetPointee(); }
    // 获取线程专有的被指向对象的引用。
    // 该函数没有实现。但NoCheck 需要它才能正常工作。
    friend inline const StoredType& GetImplRef(const TS_SPStorage& sp)
    { return 0; }
protected:
    // 销毁所存储的数据。空函数。该工作将由CleanupHook()在各个线程退出时完成。
    void Destroy() {}
    // 获取当前线程专有的数据。
    PointerType GetPointee()
    {
        PointerType tss_data = NULL;
        // 使用双重检查锁定模式来在除了初始化以外的情况下避免锁定。
        // 之所以在这里,而不是在构造器中对"专有钥"进行初始化及分配TS 对象,
        // 是因为:(1) 最初创建TS 对象的线程(例如,主线程)常常并不是使用
        // 它的线程(工作线程),所以在构造器中分配一个TS 对象的实例常常并无
        // 好处,因为这个实例只能在主线程中访问。(2)在有些平台上,"专有
        // 钥"是有限的资源,所以等到对TS 对象进行第一次访问时再进行分配,有
        // 助于节省资源。
        // 第一次检查。
        if(once_ == 0)
        {
            // 加锁。确保访问的序列化。
            pthread_mutex_lock(&keylock_);
            // 双重检查。
            if(once_ == 0)
            {
                // 创建"专有钥"。
                pthread_key_create(&key_, &CleanupHook);
                // 必须在创建过程的最后出现,这样才能防止其他线程在"专有钥"
                // 被创建之前使用它。
                once_ = 1;
            }
            // 解锁。
            pthread_mutex_unlock(&keylock_);
        }
        // 从系统的线程专有存储中获取数据。注意这里不需要加锁。
        tss_data = (PointerType)pthread_getspecific(key_);
        // 检查是否这是当前线程第一次进行访问。
        if (tss_data == NULL)
        {
            // 从堆中为TS 对象分配内存。
            tss_data = new T;
            // 将其指针存储在系统的线程专有存储中。
            pthread_setspecific(key_, (void *)tss_data);
        }
        return tss_data;
    }
private:
    // 用于线程专有数据的"专有钥"。
    pthread_key_t key_;
    // "第一次进入"标志。
    int once_;
    // 用于避免在初始化过程中产生竞态情况。
    pthread_mutex_t keylock_;
    // 清扫挂钩函数,释放为TS 对象分配的内存。在每个线程退出时被调用。
    static void CleanupHook (void *ptr)
    {
        // 这里必须进行类型转换,相应的析构器才会被调用(如果有的话)
        delete (PointerType)ptr;
    }
};
 
// 用于模拟typedef template 的结构。
// 参见Herb Sutter 的"Template Typedef"一文。
struct TS_SPStorageWrapper
{
    template <class T>
    struct In
    {
        typedef TS_SPStorage<T> type;
    };
};
};
 
下面让我们来看一个使用实例。首先让我们先定义:
 
Loki::SmartPtr
<
    int,
    Loki::NoCopyWrapper,
    Loki::DisallowConversion,
    Loki::NoCheckWrapper,
    Loki::TS_SPStorageWrapper
> value;
 
其含义为:定义一种智能指针,被它指向的类型是int,OwnershipPolicy 是NoCopy,ConversionPolicy是DisallowConversion,CheckingPolicy 是NoCheck(因为TS 对象存储方式的限制,这是惟一能和TS_SPStorage 一起使用的CheckingPolicy。读者可自行尝试找到更好的解决方案),StoragePolicy 是TS_SPStorage。
然后,编写这样一个程序:
 
pthread_mutex_t io_mutex = NULL; // iostreams 不一定是线程安全的!
void *thread_proc(void *param)
{
    int id = (int)param;
    *value = 0;
    for (int i = 0; i < 3; i++)
    {
        (*value)++;
        pthread_mutex_lock(&io_mutex);
        std::cout << "thread " << id << ": " << *value << '\n';
        pthread_mutex_unlock(&io_mutex);
    }
    return NULL;
}
 
void main(int argc, char* argv[])
{
    pthread_mutex_init(&io_mutex, NULL);
    pthread_t id[3];
    for(int i = 0; i < 3; i++)
        pthread_create(&id[i], 0, thread_proc, (void *)i);
    for(int i = 0; i < 3; i++)
        pthread_join(id[i], NULL);
    pthread_mutex_destroy(&io_mutex);
}
 
其运行结果为:
  thread 0: 1
  thread 0: 2
  thread 1: 1
  thread 2: 1
  thread 1: 2
  thread 2: 2
  thread 1: 3
  thread 2: 3
  thread 0: 3
 
由此我们可以看出,尽管看起来在各个线程中访问的都是同样的*value,但实际上访问的却是各自的线程专有的对象。而且除了初始化以外,对这些对象的访问无需进行序列化。因为Loki::SmartPtr 为我们做了大量的工作,TS_SPStorage 的实现十分简洁明了。有兴趣的读者,可以对这里的实现与Boost 的thread_specific_ptr 进行比较。像这样对Loki::SmartPtr 的策略进行的定制,在理论上数目是无限的,也就是说,通过它我们可以拥有五花八门、"千奇百怪"的智能指针――了不起的Loki,不是吗?有了这样的模板类,我们就再不需要一次一次地为标准C++增加新的智能指针类型了。
 

三、 结束语
  在推进器Boost 和斯堪的纳维亚之神Loki 的战争中,谁将胜出恐怕已不言而喻。倘若扮演上帝的C++标准委员会起了偏心,硬要选中Boost 的智能指针方案的话,那么在接下来的日子里,他们就将不再是C++标准委员会,而是C++智能指针救火委员会了。而即便那样,Loki,斯堪的纳维亚之神,也仍将矗立在C++大陆上,为他的"子民"――C++程序员们――带来统一、秩序和和谐。
 
相关资源
1. Herb Sutter,The New C++: Smart(er) Pointers,见C++ User Journal 网站:http://www.cuj.com/experts/2008/sutter.htm
2. Boost 文档:http://www.boost.org
3. Andrei Alexandrescu,Modern C++ Design 及Loki,见http://www.moderncppdesign.comhttp://www.awprofessional.com/catalog/product.asp?product_id=%7B4ED3E6F3-371F-4ADC-9810-CC7B936164E3%7D
4. Douglas C. Schmidt、Timothy H. Harrison 和Nat Pryce,Thread-Specific Storage for C/C++,见http://www.cs.wustl.edu/~schmidt/http://www.flyingd&#111nkey.com/ace/(中文)。
5. Herb Sutter,Template Typedef,见http://www.gotw.ca/gotw/079.htm
6. PThreads-Win32 库,见http://sources.redhat.com/pthreads-win32/

Thursday, March 15, 2007

Xen 初学者指南

Xen 初学者指南
转[http://blog.chinaunix.net/u/17928/showart_111692.html]
 
 
Xen 初学者指南
 
作者:北南南北
来自:LinuxSir.Org
摘要: 本文是根据 《Xen v3.0 用户手册》的实践篇,将初学者需要掌握的Xen基础知识清点了一下。在本文中举了一个实例,用Slackware虚拟 Fedora ;最好能对大家有点帮助吧;
 

目录
0、本文约定
1、什么是Xen
1.1 Xen的特性
1.2 Xen的应用范围
1.3 Xen的操作系统支持和硬件支持
 
2、Xen的一点理论基础
3、Xen的安装
3.1 安装Xen的准备工作
3.2 在Redhat/Fedora 操作平台上的安装
3.3 通过Xen的二进制包来安装(几乎适用所有的Linux发行版)
3.4 通过Xen的源码包编译安装(仅供参考)
3.41 编译原理
3.42 编译过程简说
3.43 创建initrd文件
3.44 关于xen0和xenU内核说明
 
4、引导XenLinux的GRUB配置
4.1 判断系统所在的分区
4.2 查看/boot目录中xen相关的配置文件
4.3 禁掉 TLS Libraries
4.4 引导XenLinux的GRUB内容
 
5、Xen的配置和管理工具
5.1 Xen的相关文件存放位置
5.11 内核及xen自身存放于 /boot
5.12 内核模块存放于 /lib/modules
5.13 xen的配置文件及守护程序的存放位置
5.14 可执行命令存放于/usr/sbin
 
5.2 Xen服务器的启动
5.3 Xen 管理工具xm
5.31 列出所有正在运行的虚拟操作系统
5.32 通过配置文件来引导虚拟的操作系统
5.33 从终端或控制台登录正在运行的虚拟操作系统
5.34 存储正在运行的虚拟操作系统的状态及唤醒虚拟操作系统
5.35 停止正在运行的虚拟操作系统/激活停止的虚拟操作系统
5.36 调整虚拟平台/虚拟操作系统的占用内存
5.37 关闭被虚拟的系统
5.38 调整虚拟平台及虚拟操作系统的虚拟CPU个数
5.39 查看虚拟系统运行的状态
 
6、虚拟操作系统的文件存储系统
6.1 以实际物理硬盘做为虚拟操作系统文件系统
6.11 准备物理分区及创建文件系统
6.12 在物理分区上构建操作系统
6.13 虚拟操作系统的引导文件中关于文件系统的定义
 
6.2 以映像文件做为文件系统
6.21 创建映像文件
6.22 格式化映像为Linux文件系统
6.23 在映像文件上构建操作系统
6.24 虚拟操作系统的引导文件中关于文件系统的定义
 
7、实例应用:用Slackware+Xen虚拟Fedora 5.0
7.1 Fedora 安装运行于一个物理分区中
7.11 下载Fedora 5.0的基础系统
7.12 解压和提取相应文件
7.13 规划硬盘的物理分区并创建文件系统
7.14 构建Fedora 5 的基础系统
7.15 创建Fedora 5 引导文件
7.16 引导Fedora 5
7.17 虚拟操作系统网卡不能激活的处理方法
 
7.2 Fedora 安装运行于一个映像文件中
7.21 下载Fedora 5.0的基础系统
7.22 解压和提取相应文件
7.23 修改引导运行Fedora的配置文件
7.24 引导Fedora 5
7.25 网卡不能激活的处理
7.26 Fedora 映像文件太小的解决办法
 
8、强制终止正在运行的虚拟操作系统(重要)
9、关于基础系统安装后,虚拟操作系统的软件补充安装
10、关于虚拟操作系统的桌面访问及远程访问
11、常见问题处理
11.1 不能找到root分区
11.2 出现/tmp/.ICE-unix 类似的错误
11.3 Device 0 (vif) could not be connected
 
12、关于本文
13、后记
14、参考文档
15、相关文档
 

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
正文
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 

0、本文约定;
 
虚拟平台是指能支持运行Xen的真实安装的操作系统;
虚拟操作系统:是指在虚拟平台上安装和虚拟运行的操作系统;
 
比如我在Slackware 中安装了Xen,那Slackware就是虚拟平台,通过虚拟平台就可以虚拟其它操作系统了;比如通过Slackware来虚拟Debian、Fedora ... ...
 

1、什么是Xen;
 
Xen 是一个开放源代码的para-virtualizing虚拟机(VMM),或"管理程序 ",是为x86架构的机器而设计的。Xen 可以在一套物理硬件上安全的执行多个虚拟机;Xen是基于内核的虚拟程序,它和操作平台结合的极为密切,所以它占用的资源最少。
 
什么是虚拟机呢?可能大家知道VMWARE吧,是的,Xen就是类似这样的程序,比如我们可以在Fedora 上虚拟安装和使用Slackware、Debian、Gentoo ... ... 等发行版。因为Xen是基于内核的,相对VMWARE 来说,它占用的系统资源也就是VMWARE的百分之几左右。Xen是不是更有优势呢?只有您实践了才知道。这也是我写本文的最主要原因;
 

1.1 Xen的特性;
 
虚拟机的性能更接近真实硬件环境)
在真实物理环境的平台和虚拟平台间自由切换)
在每个客户虚拟机支持到 32个虚拟CPU,通过 VCPU热插拔)
支持PAE指令集的x86/32, x86/64平台
通过Intel 虚拟支持VT的支持来用虚拟原始操作系统(未经修改的)支持(包括Microsoft Windows)
优秀的硬件支持.支持几乎所有的Linux设备驱动
 

1.2 Xen的应用范围;
 
服务器整合:在虚拟机范围内,在一台物理主机上安装多个服务器, 用于演示及故障隔绝;
无硬件依赖:允许应用程序和操作系统对新硬件的移值测试;
多操作系统配置:以开发和测试为目的,同时运行多个操作系统;
内核开发:在虚拟机的沙盒中,做内核的测试和调试,无需为了测试而单独架设一台独立的机器;
集群运算:和单独的管理每个物理主机相比较,在VM级管理更加灵活,在负载均衡方面,更易于控制,和隔离;
为客户操作系统提供硬件技术支持:可以开发新的操作系统, 以得益于现存操作系统的广泛硬件支持,比如Linux;
 

1.3 Xen的操作系统支持和硬件支持;
 
请参阅: 《Xen v3.0 用户手册》
 

2、Xen的一点理论基础;
 
基于Xen的操作系统,有多个层,最底层和最高特权层是 Xen程序本身。Xen 可以管理多个客户操作系统,每个操作系统都能在一个安全的虚拟机中实现。在Xen的术语中,Domain由Xen控制,以高效的利用CPU的物理资源。每 个客户操作系统可以管理它自身的应用。这种管理包括每个程序在规定时间内的响应到执行,是通过Xen调度到虚拟机中实现。
 
当Xen启动运行后,第一个虚拟的操作系统,就是Xen本身,我们通过xm list,会发现有一个Domain 0的虚拟机。Domain 0 是其它虚拟主机的管理者和控制者,Domain 0 可以构建其它的更多的Domain ,并管理虚拟设备。它还能执行管理任务,比如虚拟机的体眠、唤醒和迁移其它虚拟机。
 
一个被称为xend的服务器进程通过domain 0来管理系统,Xend 负责管理众多的虚拟主机,并且提供进入这些系统的控制台。命令经一个命令行的工具通过一个HTTP的接口被传送到xend。
 

3、Xen的安装;
 
在写本文时,Xen的当前最新版本是xen-3.0.1,它基于的内核版本是2.6.12.6的。您可以根据自己的操作系统的情况来选择一种安装方式,适合您的就是最好的;
 

3.1 安装Xen的准备工作;
 
拥有 GRUB引导的Linux做为安装平台,还要编译工具,比如gcc、binutils 及make和automake等;开发库有zlib和python-dev等;
 
具体明细请参阅: 《Xen v3.0 用户手册》
 
由于Xen用Python 开发的,所以Python 当然也是必不可少的。如果您是新手,我建议您用自己所用的操作系统软件包管理工具来安装这些软件包。
 

3.2 在Redhat/Fedora 操作平台上的安装;
 
在Fedora/Redhat平台上安装比较简单,您可以通过yum 来在线安装Xen和支持Xen的内核;因为Fedora/Redhat已经提供对Xen的支持了;Fedora/Redhat 提供的Xen内核支持比较高;不过就目前我的测试来看好象经常会机器重启,存在的问题可能是桌面环境造成的,比如GNOME桌面,打开就有重启的现象,也 可能是Fedora/Redhat提供的Xen内有BUG;
 
安装Xen及支持Xen的请参考:《Fedora Core 5.0 用 Xen 虚拟Slackware 10.2》
 
对于Fedora 4.0及Redhat和Fedora 5.0类似;现在Yum的源上都有Xen和支持Xen的内核包;
 

3.3 通过Xen的二进制包来安装(几乎适用所有的Linux发行版);
 
通过Xen的二进制软件包来安装,这应该是通用的,几乎适合所有的Linux操作系统。由于二进制所是已经编译好的,我已经在Slackware 平台上用这种方法来安装,还是成功的。另外etony兄也在Debian上安装成功;
 
您应该到 http://www.xensource.com/downloads 去下载二进制包,文件名中带有xen-3.0.1-install字样的,比如 xen-3.0.1-install-x86_32.tgz,这个软件包表示适用x86_32位机器的。也就是我们用的普通32位PC机。如果您用的是 64位机器,应该下载文件名带有x86_64字样的软件包;
 
下载好后,就解压安装,我们还是以支持x86_32构架机器的xen-3.0.1-install-x86_32.tgz为例:
[root@localhost ~]# tar zxvf xen-3.0.1-install-x86_32.tgz
[root@localhost ~]# cd xen-3.0.1-install
[root@localhost xen-3.0.1-install]# sh install.sh
 
判断是不是安装好了,请查看/boot目录,会发现有很多文件名带有xen字样的文件,另外在/lib/moudules中也会发现有支持xen的内核模块;另外再看一看是否有/etc/xen这个目录。我想应该是有的。
 

3.4 通过Xen的源码包编译安装(仅供参考);
 
通过Xen的二进制包来安装,可能有时内核不太适应我们的需要,这时我们要通过Xen的源码包来安装。通过自己编译来安装Xen及支持Xen的内核;Xen的源码包,您可以到 http://www.xensource.com/downloads去下载。文件名带有 xen-3.0.1-src字样的,比如 xen-3.0.1-src.tgz。
 

3.41 编译原理;
 
通过Xen的源码包编译,其实也没有什么神秘的。在Xen的源码包中提供了一些内核补丁和内核配置文件等。当我们执行编译命令时,首先编译的是 Xen程序本身,然后是编译内核 。在编译内核时,程序会自动判断是否有内核源码 ,xen-3.0.1支持的内核是2.6.12.6,如果在xen的解压目录下没有,他就会自动内核的官方站 http://www.kernel.org 下载 linux-2.6.12.tar.bz2。然后就是自动解压并为此内核打补丁。然后系统会根据指令要求,然后采用相应的内核配置文件,或配置内核进行编 译。
 

3.42 编译过程简说;
 
第一步:解压软件包,查看Xen源码包所带的文件;
[root@localhost ~]# tar zxvf xen-3.0.1-src.tgz
[root@localhost ~]# cd xen-3.0.1
[root@localhost xen-3.0.1]# ls
COPYING Config.mk README docs install.sh patches xen
ChangeLog Makefile buildconfigs extras linux-2.6-xen-sparse tools
 
我们解压xen-3.0.1-src.tgz 后,进入解压目录,会看到以上的文件或文件夹。patches是内核的补丁包,linux-2.6-xen-sparse是支持Xen的内核目录树,值得注意的是内核的配置文件就在这个目录中;
[root@localhost xen-3.0.1]# ls linux-2.6-xen-sparse/arch/xen/configs/
xen0_defconfig_ia64 xen0_defconfig_x86_64 xenU_defconfig_x86_32 xen_defconfig_x86_32
xen0_defconfig_x86_32 xenU_defconfig_ia64 xenU_defconfig_x86_64 xen_defconfig_x86_64
 
看到上面所列出的内核配置文件了吧,我们可能会发现文件名带有xen0字样的和xenU字样的两类文件。在这两类内核中,我们大多会修改的内核配置文件是运行xen的操作系统的内核配置文件,另一个是用于虚拟操作系统的内核配置文件;
 
xen0字样的就是我们一般是用于我们运行xen的操作系统的内核 ,而xenU字样的就是为虚拟操作系统所提供的内核。另外还有x86_32和x86_64之分,这表示CPU的架构。
 
比如我们用的是x86架构32位的CPU,我们在编译内核的时候就要用到 xen0_defconfig_x86_32 和xenU_defconfig_x86_32 配置文件。
 
举个例子:比如我的机器架构是x86_32位的,我安装xen的操作系统是Slackware,我想在Slackware 虚拟Debian 、Gentoo、Fedora等操作系统。这时编译虚拟平台Slackware所用的内核的配置文件就是 xen0_defconfig_x86_32 ,而被虚拟平台(Debian 、Gentoo、Fedora等操作系统)所用的内核就是 xenU_defconfig_x86_32 。
 
弄明白内核配置文件有何用?我们能明白xen在编译过程中用了哪些内核配置文件,目的是我们在编译过程中可以根据自己的需要来修改它,以编译出适合我们需要的内核。
 
比如我们想让Slackware 支持xen,并且还要支持NTFS文件系统;所以我们就要修改 xen0_defconfig_x86_32这个文件。找出如下一行;
# CONFIG_NTFS_FS is not set
 
改为
CONFIG_NTFS_FS=m
 
如果您想让被虚拟的操作系统(Debian 、Gentoo、Fedora等)也支持NTFS文件系统,所以要在 xenU_defconfig_x86_32找出如下一行;
# CONFIG_NTFS_FS is not set
 
改为
CONFIG_NTFS_FS=m
 
第二步:配置内核;
 
这一步有两种方法,一个是直接修改内核配置文件,另一个是内核配置界面来配置;
 
方法一:通过修改内核配置文件;
 
Xen所带的内核配置文件位于解压目录中的linux-2.6-xen-sparse/arch/xen/configs 。我们前面已经提到了相关配置文件的用途。请仔细看前一步的说明;
 
方法二:通过内核配置界面来配置;
 
[root@localhost xen-3.0.1]# make linux-2.6-xen0-config CONFIGMODE=menuconfig
 
第三步:编译和安装Xen;
[root@localhost xen-3.0.1]# make
[root@localhost xen-3.0.1]# make install
 

3.43 创建initrd文件;
 
有的系统需要initrd-XXXX.img或initrd.gz文件才能引导起来,如果您的系统用了支持xen的支持引导不起来,就要创建一个initrd-XXXX.img或initrd.gz的文件;请参考: 《Xen v3.0 用户手册》
 

3.44 关于xen0和xenU内核说明;
 
一般的情况下会在/boot目录中有两个与xen相关的内核,有的文件名带有vmlinuz-XXXX-xen0字样的,有的带有vmlinuz-XXXX-xenU字样的;比如:
[root@localhost xen-3.0.1]# ls -la /boot/vmlinuz*xen*
lrwxrwxrwx 1 root root 21 2006-04-12 07:42 /boot/vmlinuz-2.6-xen0 -> vmlinuz-2.6.12.6-xen0
lrwxrwxrwx 1 root root 21 2006-04-12 07:49 /boot/vmlinuz-2.6-xenU -> vmlinuz-2.6.12.6-xenU
lrwxrwxrwx 1 root root 21 2006-04-12 07:42 /boot/vmlinuz-2.6.12-xen0 -> vmlinuz-2.6.12.6-xen0
lrwxrwxrwx 1 root root 21 2006-04-12 07:49 /boot/vmlinuz-2.6.12-xenU -> vmlinuz-2.6.12.6-xenU
-rw-r--r-- 1 root root 2180524 2006-04-12 07:42 /boot/vmlinuz-2.6.12.6-xen0
-rw-r--r-- 1 root root 1129950 2006-04-12 07:49 /boot/vmlinuz-2.6.12.6-xenU
 
其实就是两个与xen相关的内核,其它的都是这两个内核文件的链接;也就是vmlinuz-2.6.12.6-xen0和vmlinuz- 2.6.12.6-xenU。vmlinuz-2.6.12.6-xen0是用来引导虚拟平台的,比如我们在Slackware上安装Xen,那 Slackware就是虚拟平台;所以如果要让Slackware的xen能运行起来,必须用xen相关的内核 ,也就是vmlinuz-2.6.12.6-xen0这个内核。 而XenU字样这个内核,是用来引导虚拟操作系统用的,我们在以后会提到它。
 

4、引导XenLinux的GRUB配置;
 
一旦我们在Linux操作系统安装好Xen后,这样的系统应该被称为XenLinux。如何才能引导拥有Xen的Linux呢?这时我们要用到 GRUB系统引导管理器。我们修改一下GRUB的配置文件menu.lst或grub.conf就行了。此文件位于/boot/grub目录中;
 

4.1 判断系统所在的分区;
[root@localhost ~]# df -h
Filesystem 容量 已用 可用 已用% 挂载点
/dev/hda6 12G 6.9G 4.9G 59% /
 

4.2 查看/boot目录中xen相关的配置文件;
[root@localhost ~]# ls /boot
 
注意:我们前面已经说过了,xen提供了两个内核,一个是虚拟平台用的,也就是文件名带有xen0字样的;另一个带有xenU字样的,这是用于引导和支持虚拟操作系统用的。
 
什么虚拟平台,比如我们想用Slackware 的Xen虚拟Fedora、Gentoo .... 。那么Slackware就是虚拟平台,而Fedora、Gentoo... .. 是被虚拟的操作系统。Slackware是一个真实安装在您的机器上的操作系统,它提供的是一个运行xen的环境。为了让Slackware能支持xen ,所以必须用于支持Xen的内核,也就是在/boot中有xen0字样的文件。
 
如果您安装了Xen,会在/boot中有两个文件vmlinuz-2.6.12.6-xen0和vmlinuz-2.6.12.6-xenU,
 

4.3 禁掉 TLS Libraries ;
[root@localhost ~]#mv /lib/tls /lib/tls.disabled
 
禁掉的理由,请查看: 《Xen v3.0 用户手册》
 

4.4 引导XenLinux的GRUB内容;
title SlackXen
kernel /boot/xen-3.0.gz
module /boot/vmlinuz-2.6-xen0 root=/dev/hda6 ro console=tty0
 
如果有inintrd-2.xxxx-xen.img或initrd.gz之类的文件才能引导起来系统,还要加一行;要以/boot中的initrd相关的文件为准;
modules /boot/initrd.gz
 
有关GRUB的文档:《系统引导管理器GRUB,为初学者指南》
 
注意:与传统GRUB的定义有点不同,就是kernel 是用来定义Xen的,而Linux的内核及initrd映像是通过module 指令定义的;要仔细看好;
 

5、Xen的配置和管理工具;
 

5.1 Xen的相关文件存放位置;
 
xen安装后,文件放在什么地方了呢?主要放在以下几个目录;
 

5.11 内核及xen自身存放于 /boot
 
安装有xen的操作系统下的/boot目录中,存放xen本身及支持xen的内核文件。比如文件名带有xen0和xenU相关的文件;
 

5.12 内核模块存放于 /lib/modules
 
内核模块包括虚拟平台支持xen的内核xen0的模块,及支持虚拟操作系统所用的xenU的模块,一般的情况下是在 /lib/modules下有两个xen相关的目录存放。一个是文件夹名带有xen0字样的,另一个是xenU字样的;
 

5.13 xen的配置文件及守护程序的存放位置;
 
一般的情况下,Xen的配置文件存放于/etc/xen目录。 比如 xend-config.sxp是用于配置网络的,不过我们不必更改,用其默认的就能完成我们的需要。xmexample1 xmexample2是两个示例性的配置文件。我们在配置引导被虚拟的操作系统时,这两个文件可供参考;
 
Xen的服务器xend和xendomains启动脚本,一般是位于/etc/init.d/目录中,也就是/etc/init.d/xend ;/etc/init.d/xend负责启动xend服务器,而/etc/init.d/xendomains负责第一个虚拟的系统及其它的 Domains,也就是Domain 0 。
 

5.14 可执行命令存放于/usr/sbin
[root@localhost ~]#ls /usr/sbin/xen*
/usr/sbin/xen-bugtool /usr/sbin/xenconsoled /usr/sbin/xenmon.py /usr/sbin/xenstored
/usr/sbin/xenbaked /usr/sbin/xend /usr/sbin/xenperf /usr/sbin/xentop
[root@localhost ~]#ls /usr/sbin/xm
 

5.2 Xen服务器的启动;
 
xend服务器的启动/停止/重启/状态查询,请用下面的命令;
[root@localhost ~]# /etc/init.d/xend start 启动xend,如果 xend没有运行)
[root@localhost ~]# /etc/init.d/xend stop 停止xend,如果xend正在运行)
[root@localhost ~]# /etc/init.d/xend restart 重启正在运行的 xend,如果xend没有运行,则启动
[root@localhost ~]# /etc/init.d/xend status 查看xend状态
 
启动xendomains 服务器的启动/停止/重启/状态查询,请用下面的命令;一般的情况下,xend服务器启动了,xendomains也会自动启动。所以这个只是掌握一下就行了;
[root@localhost ~]# /etc/init.d/xend start 启动xend,如果 xend没有运行)
[root@localhost ~]# /etc/init.d/xend stop 停止xend,如果xend正在运行)
[root@localhost ~]# /etc/init.d/xend restart 重启正在运行的 xend,如果xend没有运行,则启动
[root@localhost ~]# /etc/init.d/xend status 查看xend状态
[root@localhost ~]# /etc/init.d/xendomains start
[root@localhost ~]# /etc/init.d/xendomains stop
[root@localhost ~]# /etc/init.d/xendomains restart
[root@localhost ~]# /etc/init.d/xendomains status
 

5.3 Xen 管理工具xm;
 
我们前面提到Xen的可执行命令中,我们可能看到了一些以xen开头的命令,您不妨尝试一个一个的尝试一下他们是做什么用的;比如xend是服务器的开启运行命令 ... ....
 
其中xm命令,就是管理Xen的最基本的工具;您可以通过xm --help 来获得帮助;
 
5.31 列出所有正在运行的虚拟操作系统;
[root@localhost ~]# /usr/sbin/xm list
Name ID Mem(MiB) VCPUs State Time(s)
Domain-0 0 450 1 r----- 5377.0
fc5 4 256 1 -b---- 0.1
 
列出所有正在运行的虚拟系统(也可以称为虚拟机);我们可以看到,一个名为Domain-0的系统正在运行。Domain-0就是Xen本身,也可 以称为虚拟平台内存大小,而Domain-0负责提供其它虚拟操作系统的硬件环境,其它的系统都是基于Domain-0开始的,我们看到他的ID为0就应 该知道它的权限是至高无尚的。所占用的内存是450M,不过这个是可以指定的。 虚拟的CPU个数是1个,运行状态处于r,也就是run,正在运行中;运行时间是5377秒;
 
还有一个虚拟系统fc5,他的id是4,占用内存256M,虚拟CPU个数是1个 ... ...
 

5.32 通过配置文件来引导被虚拟的操作系统;
[root@localhost ~]# /usr/sbin/xm create -c 虚拟操作系统的启动配置文件
 
比如我们要启动被虚拟的操作系统Fedora Core 5.0 ,我们要写一个启动Fedora的配置文件,比如是fc5vm.cfg。然后就可以通过下面的命令来引导Fedora了;
[root@localhost ~]# /usr/sbin/xm create -c fc5vm.cfg
 

5.33 从终端或控制台登录正在运行的虚拟操作系统;
[root@localhost ~]# /usr/sbin/xm console 正在运行的虚拟操作系统的Name或ID;
 
举例:
[root@localhost ~]# /usr/sbin/xm list
Name ID Mem(MiB) VCPUs State Time(s)
Domain-0 0 512 1 r----- 5561.9
fc5 4 256 1 -b---- 0.2
[root@localhost ~]# /usr/sbin/xm console fc5
 

5.34 存储正在运行的虚拟操作系统的状态及唤醒虚拟操作系统;
[root@localhost ~]# /usr/sbin/xm save <DomId> <File>
[root@localhost ~]# /usr/sbin/xm restore <File>
 
举例:
[root@localhost ~]# /usr/sbin/xm list
Name ID Mem(MiB) VCPUs State Time(s)
Domain-0 0 458 1 r----- 260.3
fc5 2 256 1 ------ 6.5
[root@localhost ~]# /usr/sbin/xm save 2 fc5run.save
[root@localhost ~]# /usr/sbin/xm restore fc5run.save
[root@localhost ~]# /usr/sbin/xm console fc5
 

5.35 停止正在运行的虚拟操作系统/激活停止的虚拟操作系统
[root@localhost ~]# /usr/sbin/xm pause <DomId>
[root@localhost ~]# /usr/sbin/xm unpause <DomId>
 
举例:
[root@localhost ~]# /usr/sbin/xm list
Name ID Mem(MiB) VCPUs State Time(s)
Domain-0 0 458 1 r----- 260.3
fc5 2 256 1 ------ 6.5
[root@localhost ~]# /usr/sbin/xm pause 2
[root@localhost ~]# /usr/sbin/xm unpause 2
 

5.36 调整虚拟平台/虚拟操作系统的占用内存
 
我们可以调整正在运行中的虚拟平台(Domain-0)所占内存大小及虚拟操作系统所占用的内存大小;
[root@localhost ~]# /usr/sbin/xm mem-set <DomId> <Mem>
 
举例:
[root@localhost ~]# /usr/sbin/xm list
Name ID Mem(MiB) VCPUs State Time(s)
Domain-0 0 458 1 r----- 260.3
fc5 2 256 1 ------ 6.5
[root@localhost ~]# /usr/sbin/xm mem-set 2 128
 

5.37 关闭被虚拟的系统
[root@localhost ~]# /usr/sbin/xm shutdown 虚拟操作系统的Name或DomID
[root@localhost ~]# /usr/sbin/xm destroy <DomId> 立即停止虚拟的系统 (重要);
 
举例:
[root@localhost ~]# /usr/sbin/xm list
Name ID Mem(MiB) VCPUs State Time(s)
Domain-0 0 458 1 r----- 260.3
fc5 2 256 1 ------ 6.5
[root@localhost ~]# /usr/sbin/xm shutdown fc5

[root@localhost ~]# /usr/sbin/xm shutdown 2

[root@localhost ~]# /usr/sbin/xm destroy 2
 

5.38 调整虚拟平台及虚拟操作系统的虚拟CPU个数;
[root@localhost ~]# /usr/sbin/xm vcpu-set <DomId> <VCPUs>
 
举例:
[root@localhost ~]# /usr/sbin/xm list
Name ID Mem(MiB) VCPUs State Time(s)
Domain-0 0 458 1 r----- 260.3
fc5 2 256 1 ------ 6.5
[root@localhost ~]# /usr/sbin/xm vcpu-set 2 4
 

5.39 查看虚拟系统运行的状态;
[root@localhost ~]# xm top

[root@localhost ~]# xentop
 

6、虚拟操作系统的文件存储系统;
 
我们虚拟的系统应该有一个存储的地方,也就是文件系统。被虚拟的系统能安装和运行在哪些文件系统上呢?能安装和运行在一个实际的物理分区上,一个映像文件中,或NFS等网络文件系统中;
 

6.1 以实际物理硬盘做为虚拟操作系统文件系统;
 
以实际物理硬盘分区做为虚拟操作系统的文件系统,要经过硬盘分区,创建文件系统流程;
 
请参考:《Linux 创建文件系统及挂载文件系统流程详解》
 

6.11 准备物理分区及创建文件系统;
 
《Linux 查看磁盘分区、文件系统、使用情况的命令和相关工具介绍》
《实例解说 fdisk 使用方法》
《合理规划您的硬盘分区》
《Linux 创建文件系统及挂载文件系统流程详解》
 

6.12 在物理分区上构建操作系统;
 
构建操作系统,目前在一个Linux操作系统中构建另一个操作系统,主要是通过chroot工具和软件包提取工具来构建。基础的东西还是需要一点的,请参考:《通过chroot 构建Linux操作系统概要》
 
目前在开源社区中,已经有人把一些常用的发行版的基础系统做好了。我们能拿过来直接用。呵。。。。。只要有基础系统,我们就能chroot进入,我们就能用相应发行版软件包管理器来构建操作系统了。
 
操作系统之基础系统资源:
 
 
您可以直接下载您喜欢的操作系统,解压后,然后挂载映像文件,然后把映像文件的内容都拷到物理分区中就好了。不过还得做一点小小的修改,比如您下载系统映像文件中的/etc/fstab等文件。要根据您的引导的虚拟操作系统中定义的虚拟映射点来改。
 

6.13 虚拟操作系统的引导文件中关于文件系统的定义;
 
如果您的硬盘有一定的空间,就可以把被虚拟的操作系统安装在硬盘的实际物理分区中,当然您首先得准备一个硬盘分区。然后格式化成Linux的文件系 统,比如ext3或reiserfs 等;然后是在这个分区上构建您想要虚拟的操作系统,最后才是写虚拟操作系统的引导配置文件,在配置文件中,要用phy:来指定。
 
比如:
disk = ['phy:hda7,sda1,w']
中文意思就是:
disk=['phy:分区,映射点,w']
 
这行是什么意思呢?就是表示被虚拟的操作系统安装在/dev/hda3,我们要把hda3硬盘分区虚拟映射到/dev/sda1,并且是可读可写的;在这里要值得注意的是虚拟平台正在使用中的分区不能做为是映射点。
 
比如我在Slackware是位于/dev/hda6的虚拟平台,也就是真实运行的操作系统,我想用它来虚拟位于/dev/hda7分区的Fedora。所以我不能把/dev/hda6做为hda7的映射点。也就是下面一行是错误的:
disk = ['phy:hda7,hda6,w']
 
如何定义映射点,其实也比较好办,只要符合Linux设备的规则就可以;比如下面的也可以;
disk = ['phy:hda7,sda2,w']
disk = ['phy:hda7,sda3,w']
disk = ['phy:hda7,sdb1,w']
disk = ['phy:hda7,sdb2,w']
... ...
 
什么是映射点呢?也就是说通过phy定义后,被虚拟的操作系统的位于的真实的物理分区,在虚拟平台中,被映射到另一个分区;可能这样说有点不太明白。呵。。。。。。
 
举个例子吧:比如我们在Slackware虚拟Fedora ,而Fedora 位于/dev/hda7中。我们在引导Fedora的配置文件中定义Fedora所用的物理分区被映射到/dev/sda1;
disk = ['phy:hda7,sda1,w']
root = "/dev/sda1 ro"
 
当我们把Fedora引导起来的时候,Fedora的文件系统就是用被虚拟后的分区,也就是/dev/sda1。在Fedora中,我们通过df -h 查看到Fedora是位于/dev/sda1。
 
所以在引导Fedora的配置文件中,还要有一行来指定Fedora的root在哪里,就在/dev/sda1上;因为/dev/hda7已经被虚拟到了/dev/sda1。这回明白了吧;
 

6.2 以映像文件做虚拟操作系统的文件系统;
 
以映像文件做为虚拟操作系统的文件系统,这种方法是比较常用。也是比较方便和易于操作的,也就是说被虚拟的操作系统是放在了一个文件中。
 

6.21 创建映像文件;
[root@localhost ~]# dd if=/dev/zero of=fedora50.img bs=2k seek=2048k count=1
读入了 1+0 个块
输出了 1+0 个块
[root@localhost ~]# ls -lh fedora50.img
-rw-r--r-- 1 root root 4.1G 2006-04-13 01:27 fedora50.img
 
我们可以用dd来创建映像文件,上面的例子是创建了大小为4.1G的体积的,名为fedora50.img的映像文件;您可以调整上面命令参数的大小来创建您想要的体积大小的映像文件。
 

6.22 格式化映像为Linux文件系统;
 
Linux文件系统,比如ext3或reiserfs,看自己喜欢吧;您可以用mkfs.ext3或mkfs.reiserfs命令来创建文件系统,请参考:
[root@localhost ~]# /sbin/mkfs.ext3 fedora50,img
mke2fs 1.38 (30-Jun-2005)
fedora50,img is not a block special device.
Proceed anyway? (y,n) y 注:在这里输入y就时行格式化了,然后遇到提示之处,都是用回车。
 
这样fedora50.img就是ext3文件系统了,你可以用mount -o loop 来挂载使用它,在它上面存储文件等。
[root@localhost ~]# mkdir /mnt/fedora50
[root@localhost ~]# mount -o loop fedora50.img /mnt/fedora50
[root@localhost ~]# df -h
root@localhost:/opt# df -lh
Filesystem 容量 已用 可用 已用% 挂载点
/dev/hda6 12G 8.5G 3.3G 73% /
/root/fedora50,img 4.0G 129M 3.7G 4% /mnt/fedora
 
上面的一系列命令执行下去后,说明fedora50.img已经挂载到了/mnt/fedora目录中,我们可以向/mnt/fedora中存放文件。这样就写到了fedora50.img映像文件中。写完后,我们可以卸载fedora50.img;
[root@localhost ~]# umount /mnt/fedora
 

6.23 在映像文件上构建操作系统;
 
在映像文件上构建操作系统,目前在一个Linux操作系统中构建另一个操作系统,主要是通过chroot工具和软件包提取工具来构建。基础的东西还是需要一点的,请参考:
 
目前在开源社区中,已经有人把一些常用的发行版的基础系统做好了。我们能拿过来直接用。呵。。。。。只要有基础系统,我们就能chroot进入,我们就能用相应发行版软件包管理器来构建操作系统了。
 
操作系统之基础系统资源:
 
 

6.24 虚拟操作系统的引导文件中关于文件系统的定义;
disk = ['file:/opt/vmos/vmos.img,sda1,w', 'file:/opt/vmos/vmos.swap,sda2,w']
root = "/dev/sda1 ro"
 
我们还是以实例解说,把装有vmos.img的映像映射到/dev/sda1 分区,vmos.img就是虚拟操作系统root存放地。vmos.swap是被虚拟文件系统的交换分区,这个交换分区也是一个文件,被映射到了 /dev/sda2 。然后通过root= 行来指定虚拟操作系统所处的分区,这个位置就是被虚拟后的分区。
 
定义时要注意自己的这些文件存放在哪了?上面的例子表示vmos.img和vmos.swap是存放在/opt/vmos目录中。要看好了。。
 

7、实例应用:用Slackware+Xen虚拟Fedora 5.0
 

7.1 Fedora 安装运行于一个物理分区中;
 

7.11 下载Fedora 5.0的基础系统;
 
 

7.12 解压和提取相应文件;
 
我们把下载下来的Fedora 5 基础系统,存放到/opt/fedora5vm目录中;然后解压;
[root@localhost ~]# mkdir /opt/fedora5vm
[root@localhost ~]# mv fedora.fc5.20060401.img.tgz /opt/fedora5vm
[root@localhost ~]# cd /opt/fedora5vm
[root@localhost fedora5vm]# tar zxvf fedora.fc5.20060401.img.tgz
[root@localhost fedora5vm]# ls
fedora.fc5.20060401.img.tgz fedora.fc5.img fedora.fc5.xen.cfg fedora.swap
 

7.13 规划硬盘的物理分区并创建文件系统;
 
您可以在您的硬盘上创建一个物理分区,如果有空闲的物理分区可用也行。然后创建一下文件系统。分区工具用fdisk 工具就行。
 
分区工具,请参考:《实例解说 fdisk 使用方法》
创建文件系统,请参考:《Linux 创建文件系统及挂载文件系统流程详解》
 
比如我们想把Fedora 放在/dev/hda5上,我们可以用mkfs.ext3来格式化/dev/hda5。
[root@localhost ~]# /sbin/mkfs.ext3 /dev/hda5
 

7.14 构建Fedora 5 的基础系统;
 
因为我们已经下载了,Fedora Core 5的基础系统的映像文件,也做了解压。会看到 fedora.fc5.img这个映像文件; 我们把这个映像挂载,然后把它的内容复制到我们刚才创始的文件系统的分区中,也就是/dev/hda5;
[root@localhost ~]# mkdir /mnt/tmp
[root@localhost ~]# mkdir /mnt/fedora
[root@localhost ~]# mount -o loop /opt/fedora5vm/fedora.fc5.img /mnt/tmp
[root@localhost ~]# mount /dev/hda5 /mnt/fedora
 
复制fedora.fc5.img中的内容,到/dev/hda5中;
[root@localhost ~]# cp -rp /mnt/tmp/* /mnt/fedora
[root@localhost ~]# umount /mnt/tmp
 
然后是chroot 到/mnt/fedora目录中,实际就是操作Fedora 5系统,我们要创建Fedora 5的root密码;
[root@localhost ~]# chroot /mnt/fedora/
root@localhost:/#
root@localhost:/# passwd root
Changing password for user root.
New UNIX password:
Retype new UNIX password:
passwd: all authentication tokens updated successfully.
root@localhost:/# exit
 
然后我们卸载/dev/hda5分区;
 
[root@localhost ~]# umount /dev/hda5
 

7.15 创建Fedora 5 引导文件;
 
我们在解压 fedora.fc5.20060401.img.tgz 时看到一个文件fedora.fc5.xen.cfg ,这个就是Fedora 5的引导文件,人家洋人都为我们写好了。我们只是改一改就能用了;
 
我们要回到fedora.fc5.xen.cfg的存放目录/opt/fedora5vm中;复制fedora.fc5.xen.cfg名为fc5vm.cfg的文件。然后我们把fc5vm.cfg作为被虚拟的Fedora 5的引导文件;
[root@localhost ~]# cd /opt/fedora5vm
[root@localhost fedora5vm]# cp fedora.fc5.xen.cfg fc5vm.cfg
 
我们要对fc5vm.cfg做一下修改;
 
在fc5vm.cfg中,我们会看到如下的内容:
kernel = "/boot/vmlinuz-2.6-xenU"
memory = 128
name = "fedora.fc5"
nics = 1
dhcp = "dhcp"
disk = ['file:/xen/fedora/fedora.fc5.img,sda1,w', 'file:/xen/fedora/fedora.swap,sda2,w']
root = "/dev/sda1 ro"
 
首先我们看kernel 这行,这行是定义虚拟操作系统内核的,我们要用到我们安装xen是所安装的内核。要在虚拟平台/boot目录中找。比如我用Slackware虚拟 Fedora 。那虚拟平台就是Slackware。我应试在Slackware的/boot中找文件名中包含vmlinuz和xenU字样的文件。比如我找以的是:
[root@localhost ~]# ls -lh /boot/vmlinuz*xenU*
lrwxrwxrwx 1 root root 21 2006-04-12 07:49 /boot/vmlinuz-2.6-xenU -> vmlinuz-2.6.12.6-xenU
lrwxrwxrwx 1 root root 21 2006-04-12 07:49 /boot/vmlinuz-2.6.12-xenU -> vmlinuz-2.6.12.6-xenU
-rw-r--r-- 1 root root 1.1M 2006-04-12 07:49 /boot/vmlinuz-2.6.12.6-xenU
 
我们看到有类似的三个文件,实际上只有一个,也就是 vmlinuz-2.6.12.6-xenU,其它的都是他的链接文件。所以我们在kernel行中指定内核时,可以用这三个中的任何一个;比如我们用 vmlinuz-2.6-xenU;
 
memory是指定内存大小的,我们设置被虚拟的Fedora 5的虚拟内存大小是 128M;
name 是定义虚拟操作系统的名字的,可以通过xm list中显示出来,我们也改简单点,改为fc5
nics=1 不变
dhcp 这行,是用来指定获取Fedora 5系统的IP是通过DHCP获取的,不过您也可以指定IP。可以不要这行;
 
vif = ['mac=aa:00:00:00:00:11'] 用来指定Fedora 5的网卡的物理地址;可以自己定义。
ip = "192.168.1.144" 用来指定虚拟网卡的IP
netmask="255.255.255.0" 用来指定掩码
 
disk这行是用来定义Fedora所处的物理分区映射点,及物理交换分区及映射点的;因为物理分区是通过phy:来指定的。所以我们得改一改。因 为我们已经把Fedora放在了/dev/hda5了。另外交换分区在哪呢?可以通过swapon -s来查看物理交换分区,要在虚拟平台中查看;比如我得到的是/dev/hda8是交换分区。所以disk这行就应该这样写:
 
disk = ['phy:hda5,sda1,w','phy:hda8,sda2,w']
 
不过真实物理分区的映射点,比如/dev/hda5映射到了/dev/sda1,交换分区/dev/hda8映射到了/dev/sda2。映射过 后,Fedora的root文件系统就用映射后的/dev/sda1。慢慢理解;物理分区的映射点是可以在Linux系统设备定义的许可范围内进行。自己 尝试着换一换映射点。如果Fedora的root所处的物理分区的映射点改变后,Fedora的root=的值也得跟着改变。另外还要改Fedora系统 中的/etc/fstab文件;
 
root一行,来指定Fedora的所有的虚拟物理分区(就是映射点)。映射到哪里了呢?Fedora 安装到了/dev/hda5,映射后虚拟到了/dev/sda1。所以Fedora系统引导时就寻找/dev/sda1做为文件系统。
 
所以root这行可写为:
root = "/dev/sda1 ro"
 
所以我们可以这样写Fedora 5的引导配置文件;
 
第一种:如果是用DHCP来获取IP:
kernel = "/boot/vmlinuz-2.6-xenU"
memory = 128
name = "fc5"
nics = 1
vif = ['mac=aa:00:00:00:00:11']
dhcp = "dhcp"
disk = ['phy:hda5,sda1,w','phy:hda8,sda2,w']
root = "/dev/sda1 ro"
 
第二种:如果是指定IP的话:
kernel = "/boot/vmlinuz-2.6-xenU"
memory = 128
name = "fc5"
nics = 1
vif = ['mac=aa:00:00:00:00:11']
disk = ['phy:hda5,sda1,w','phy:hda8,sda2,w']
root = "/dev/sda1 ro"
ip = "192.168.1.144"
netmask="255.255.255.0"
 
再举一例:
 
比如Fedora 被安装在了/dev/hda5分区,物理交换分区为/dev/hda8。通过DHCP获得IP。我想映射/dev/hda5到/dev/hda5,并且想映射/dev/hda8到/dev/hda8。我们应该如何修改一些配置文件呢?
 
Fedora的引导文件内容应该是:
kernel = "/boot/vmlinuz-2.6-xenU"
memory = 128
name = "fc5"
nics = 1
vif = ['mac=aa:00:00:00:00:11']
dhcp = "dhcp"
disk = ['phy:hda5,hda5,w','phy:hda8,hda8,w']
root = "/dev/hda5 ro"
 
然后我们应该再把装有Fedora的/dev/hda5分区挂载,然后修改Fedora系统的/etc/fstab。注意:不是修改虚拟平台的/etc/fstab。不要弄混了!!!!
 
应该先挂载Fedora 5所处的分区:
[root@localhost ~]# mount /dev/hda5 /mnt/fedora/
 
然后修改Fedora 5的/etc/fstab文件,也就是/mnt/fedora/etc/fstab文件:找到如下两行:
/dev/sda1 / ext3 defaults 1 1
/dev/sda2 none swap sw 0 0
 
改为:
/dev/hda5 / ext3 defaults 1 1
/dev/hda8 none swap sw 0 0
 
接着再umount /mnt/fedora
[root@localhost ~]# umount /mnt/fedora
 

7.16 引导Fedora 5 ;
 
引导运行Fedora 5.0就好办了,就是用xm 工具来引导;首先要确认你的Fedora 5的配置文件放在哪里。比如我是放在了/opt/fedora5vm中。并且文件名为fc5vm.cfg。所以我就可以这样来引导Fedora 5。
[root@localhost ~]# /usr/sbin/xm create -c /opt/fedora5vm/fc5vm.cfg
 

7.17 虚拟操作系统网卡不能激活的处理方法 ;
modprobe: FATAL: Could not load /lib/modules/2.6.12.6-xenU/modules.dep:
No such file or directory
 
如果出现类似上面的现象,请复制虚拟平台的中的/lib/modues/下的2.6.12.6-xenU 到Fedora系统中;
 
方法是先挂载Fedora 所处的物理分区,然后用cp -rp 来复制;
 
首先要关掉Fedora,然后再复制;
[root@localhost ~]# xm list
Name ID Mem(MiB) VCPUs State Time(s)
Domain-0 0 462 1 r----- 2192.9
fc5 2 128 1 -b---- 6.4
 
以上面虚拟操作系统的ID为准,比如fc5的ID是 2,就要运行如下命令;
[root@localhost ~]# xm destroy 2
 
然后mount 挂载Fedora所处的物理分区/dev/hda5;
[root@localhost ~]# mount /dev/hda5 /mnt/fedora
[root@localhost ~]# mkdir /mnt/fedora/lib/modules
注:在Fedora中的/lib目录中创建modules。如果有了就不创建;
[root@localhost ~]# cp -rp /lib/modules/2.6.12.6-xenU/ /mnt/fedora/lib/modules/
[root@localhost ~]# chmod -R 755 /mnt/fedora/lib/modules/
[root@localhost ~]# umount /mnt/fedora
 

7.2 Fedora 安装运行于一个映像文件中;
 

7.21 下载Fedora 5.0的基础系统;
 
 

7.22 解压和提取相应文件;
 
我们把下载下来的Fedora 5 基础系统,存放到/opt/fedora5vm目录中;然后解压;
[root@localhost ~]# mkdir /opt/fedora5vm
[root@localhost ~]# mv fedora.fc5.20060401.img.tgz /opt/fedora5vm
[root@localhost ~]# cd /opt/fedora5vm
[root@localhost fedora5vm]# tar zxvf fedora.fc5.20060401.img.tgz
[root@localhost fedora5vm]# ls
fedora.fc5.20060401.img.tgz fedora.fc5.img fedora.fc5.xen.cfg fedora.swap
 

7.23 修改引导运行Fedora的配置文件;
 
我们从fedora.fc5.20060401.img.tgz 解压出来一个fedora.fc5.xen.cfg ,这个就是用来引导Fedora 5的配置文件。我们改一改以适合我们的需要。我们把这个文件复制为fc5vm.cfg
[root@localhost ~]# cp /opt/fedora5vm/fedora.fc5.xen.cfg /opt/fedora5vm/fc5vm.cfg
 
fc5vm.cfg 内容如下:
kernel = "/boot/vmlinuz-2.6-xenU"
memory = 128
name = "fc5"
nics = 1
vif = ['mac=aa:00:00:00:00:11']
dhcp = "dhcp"
disk = ['file:/opt/fedora5vm/fedora.fc5.img,sda1,w', 'file:/opt/fedora5vm/fedora.swap,sda2,w']
root = "/dev/sda1 ro"
 
注解:
 
首先我们看kernel 这行,这行是定义虚拟操作系统内核的,我们要用到我们安装xen是所安装的内核。要在虚拟平台/boot目录中找。比如我用Slackware虚拟 Fedora 。那虚拟平台就是Slackware。我应试在Slackware的/boot中找文件名中包含vmlinuz和xenU字样的文件。比如我找以的是:
[root@localhost ~]# ls -lh /boot/vmlinuz*xenU*
lrwxrwxrwx 1 root root 21 2006-04-12 07:49 /boot/vmlinuz-2.6-xenU -> vmlinuz-2.6.12.6-xenU
lrwxrwxrwx 1 root root 21 2006-04-12 07:49 /boot/vmlinuz-2.6.12-xenU -> vmlinuz-2.6.12.6-xenU
-rw-r--r-- 1 root root 1.1M 2006-04-12 07:49 /boot/vmlinuz-2.6.12.6-xenU
 
我们看到有类似的三个文件,实际上只有一个,也就是 vmlinuz-2.6.12.6-xenU,其它的都是他的链接文件。所以我们在kernel行中指定内核时,可以用这三个中的任何一个;比如我们用 vmlinuz-2.6-xenU;
 
memory是指定内存大小的,我们设置被虚拟的Fedora 5的虚拟内存大小是 128M;
name 是定义虚拟操作系统的名字的,可以通过xm list中显示出来,我们也改简单点,改为fc5
nics=1 不变
dhcp 这行,是用来指定获取Fedora 5系统的IP是通过DHCP获取的,不过您也可以指定IP。可以不要这行;
 
vif = ['mac=aa:00:00:00:00:11'] 用来指定Fedora 5的网卡的物理地址;可以自己定义。
ip = "192.168.1.144" 用来指定虚拟网卡的IP
netmask="255.255.255.0" 用来指定掩码
 
disk来定义Fedora 5.0 所用的文件系统,因为我们这次用的是映像文件。所以要用file:来指定,Fedora 5用的交换分区,也是一个映像文件;所以有:
 
disk = ['file:/opt/fedora5vm/fedora.fc5.img,sda1,w', 'file:/opt/fedora5vm/fedora.swap,sda2,w']
 
指定fedora.fc5.img映像文件虚拟映射到/dev/sda1;交换分区文件fedora.swap映射到了/dev/sda2。映射过 后,Fedora的root文件系统就用映射后的/dev/sda1。虚拟映射设备是可以在Linux系统设备定义的许可范围内进行。自己尝试着换一换映 射点。如果Fedora的虚拟映射设备变了,我们得改变Fedora中的/etc/fstab。
 
root一行,来指定Fedora的root位于哪个映射后的设备。映射到哪里了呢?Fedora 安装到了/dev/sda1,映射后虚拟到了/dev/sda1。所以Fedora系统引导时就寻找/dev/sda1做为文件系统。
 
所以root这行可写为:
root = "/dev/sda1 ro"
 
所以我们可以这样写Fedora 5的引导配置文件;
 
第一种:如果是用DHCP来获取IP:
kernel = "/boot/vmlinuz-2.6-xenU"
memory = 128
name = "fc5"
nics = 1
vif = ['mac=aa:00:00:00:00:11']
dhcp = "dhcp"
disk = ['file:/opt/fedora5vm/fedora.fc5.img,sda1,w', 'file:/opt/fedora5vm/fedora.swap,sda2,w']
root = "/dev/sda1 ro"
 
第二种:如果是指定IP的话:
kernel = "/boot/vmlinuz-2.6-xenU"
memory = 128
name = "fc5"
nics = 1
vif = ['mac=aa:00:00:00:00:11']
disk = ['file:/opt/fedora5vm/fedora.fc5.img,sda1,w', 'file:/opt/fedora5vm/fedora.swap,sda2,w']
root = "/dev/sda1 ro"
ip = "192.168.1.144"
netmask="255.255.255.0"
 

7.24 引导Fedora 5 ;
 
引导运行Fedora 5.0就好办了,就是用xm 工具来引导;首先要确认你的Fedora 5的配置文件放在哪里。比如我们在这个例子中是放在了/opt/fedora5vm中。并且文件名为fc5vm.cfg。所以我就可以这样来引导Fedora 5。
[root@localhost ~]# /usr/sbin/xm create -c /opt/fedora5vm/fc5vm.cfg
 

7.25 网卡不能激活的处理;
modprobe: FATAL: Could not load /lib/modules/2.6.12.6-xenU/modules.dep:
No such file or directory
 
如果出现类似上面的现象,请复制虚拟平台的中的/lib/modues/下的2.6.12.6-xenU 到Fedora系统中;
 
方法是先挂载Fedora 所处的物理分区,然后用cp -rp 来复制;
 
首先要关掉Fedora,然后再复制;
[root@localhost ~]# xm list
Name ID Mem(MiB) VCPUs State Time(s)
Domain-0 0 462 1 r----- 2192.9
fc5 2 128 1 -b---- 6.4
 
以上面虚拟操作系统的ID为准,比如fc5的ID是 2,就要运行如下命令;
[root@localhost ~]# xm destroy 2
 
然后mount 挂载Fedora所处的物理分区/dev/hda5;
[root@localhost ~]# mount -o loop /opt/fedora5vm/fedora.fc5.img /mnt/fedora
[root@localhost ~]# mkdir /mnt/fedora/lib/modules
注:在Fedora中的/lib目录中创建modules。如果有了就不创建;
[root@localhost ~]# cp -rp /lib/modules/2.6.12.6-xenU/ /mnt/fedora/lib/modules/
[root@localhost ~]# chmod -R 755 /mnt/fedora/lib/modules/
[root@localhost ~]# umount /mnt/fedora
 
然后再来引导Fedora 5,进入系统后用ifconfig来查看网卡的IP之类的,如果没有激活,请用下面的命令来加载网卡模块;在要Fedora中执行;
[root@fc5_pristine ~]# modprobe xennet
[root@fc5_pristine ~]# dhclient 如果您用DHCP获取IP,请执行;
 

7.26 Fedora 映像文件太小的解决办法;
 
我们会发现我们下载的Fedora基础系统的映像文件体积太小。体积小空间就小的了。Fedora 5.0如果只安装基础系统,倒占用不了多少空间,如果再安装一个桌面环境可能会占用大一点的地方。比如安装GNOME或KDE。
 
我们可以自己创建一个映像文件,创建好文件系统。然后从我们下载下来的Fedora映像文件中的所有文件,也就是Fedora的基础系统,复制到我们新创建的映像中。前面已经说过创建映像文件的办法了。这个应该好办吧。
[root@localhost ~]# dd if=/dev/zero of=fc5.img bs=2k seek=2048k count=1
[root@localhost ~]# ls -lh fc5.img
-rw-r--r-- 1 root root 4.1G 2006-04-13 11:22 fc5.img
[root@localhost ~]# /sbin/mkfs.ext3 fc5.img
mke2fs 1.38 (30-Jun-2005)
fc5.img is not a block special device.
Proceed anyway? (y,n) y
 
 
 
[root@localhost ~]# mkdir /mnt/tmp 注:在/mnt/中创建tmp目录
[root@localhost ~]# mkdir /mnt/fedora 注:在/mnt中创建fedora目录
[root@localhost ~]# mount -o loop fc5.img /mnt/fedora/ 注:挂载我们新创建的fc5.img文件到 /mnt/fedora
[root@localhost ~]# mount -o loop fedora.fc5.img /mnt/tmp/ 注:挂载我们下载下来的映像文件到/mnt/tmp
[root@localhost ~]# cp -rp /mnt/tmp/* /mnt/fedora/ 注:复制Fedora基础系统到新的映像文件;
[root@localhost ~]# umount /mnt/tmp 注:卸载 fedora.fc5.img ;
[root@localhost ~]# umount /mnt/fedora 注:卸载fc5.img
 
然后就是改一改引导Fedora 5的配置文件,注意改一下disk:那行;要到fc5.img指定进去。要仔细看一下fc5所处的目录;这个应该好办,不多说了;
 

8、强制终止正在运行的虚拟操作系统;(重要)
 
在xm这个管理工具中,我们把xm destroy 单列出来,就是因为这个工具是极为重要的;有时被虚拟的系统一直退不出去,但由于启动过程中遇到问题,又不能终止。这时我们要用到这个工具强制被虚拟的系统退出。
[root@localhost ~]# xm list
Name ID Mem(MiB) VCPUs State Time(s)
Domain-0 0 586 1 r----- 2236.5
fc5 1 128 1 -b---- 13.7
[root@localhost ~]# xm destroy 1

[root@localhost ~]# xm destroy fc5
 
这时您再用xm list查看,肯定fc5这个Domain已经退出。
 
xm destroy 这个指令还是极为有用的,所以我们单列出来。希望新手弟兄注意一下。
 

9、关于基础系统安装后,虚拟操作系统的软件补充安装;
 
基础系统安装好以后,下一步就是其它软件的安装。在各个系统都有软件包管理工具; 在主流发行版中,都有相应的软件包管理工具,比如Fedora有rpm 和yum 工具;Debian有 apt工具。
 
或者通过chroot来安装一些比较重要的软件包。总之方法太多了;
 
总之,后续软件的补充安装并不是什么问题;
 

10、关于虚拟操作系统的桌面访问及远程访问;
 
被虚拟的操作系统,我们要把它看成一台独立运行的计算机。计算机与计算机之间通过什么访问来?ssh 或vnc。所以我们要在被虚拟的操作系统上安装vncserver 和sshd;
 
如果您虚拟的是Fedora ,我们可以在Fedora中执行yum install vnc来安装vncserver ;
 
#yum install vnc
 
如果您虚拟的是 Debian ,您可以用apt-get install vncserver 来安装vncserver ;
 
#apt-get install vncserver
 
当然客户端也要安装vncview才行,您要自己在客户端上安装vncview;
 
关于vnc远程桌面的访问,您可以参考这篇:《Fedora Core 5.0 用 Xen 虚拟Slackware 10.2》
 
另外sshd服务器,也要安装openssh 的软件包;这个也省略不说了,比较简单,再说目前大多基础系统已经提供这个软件包了。并且在被虚拟的系统在启动时,sshd也自动运行了;连接sshd服务器的命令是;
#ssh 用户名@ip
 
比如
#ssh root@192.168.1.12
 

11、常见问题处理;
 

11.1 不能找到root分区;
 
表现如下症状;
 
VFS: Cannot open root device "sda8" or unknown-block(2,0)
Please append a correct "root=" boot option
Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(2,0)
 
引导虚拟操作系统的配置文件有问题,主要查看disk:那行和root那行是否用的是一个虚拟映射点。
 
比如是由于引导Fedora 的配置文件中disk行和root行如下:
disk = ['phy:hda5,sda1,w','phy:hda8,sda2,w']
root = "/dev/sda8 ro"
 
我们发现/dev/hda5被虚拟映射到/dev/sda1了。也就是说,被虚拟的操作系统引导运行,它的root应该位于/dev/sda1。而在这里定义的却是/dev/sda8。所以应该改为
disk = ['phy:hda5,sda1,w','phy:hda8,sda2,w']
root = "/dev/sda1 ro"
 

11.2 出现/tmp/.ICE-unix 类似的错误;
 
chown: changing ownership of `/tmp/.ICE-unix': Read-only file system
 
再比较/tmp/xxxx之类的不能写入,只读之类的,请用xm destrony 先关掉被虚拟的操作系统。然后把被虚拟的系统的分区或映像文件挂载,删除/tmp目录中的临时文件。注意.file是隐藏的。您可以通过ls -la来查看。然后再来删除;
 
另外如果一直出现这个错误,说明文件系统有问题了,这在ext3的文件系统中是经常发生的。您可以用/sbin/fsck.ext3 工具来修复文件系统。当然您的文件系统不能处于挂载状态。应该先umout 才能进行;
 
比如修复一个分区,应该类似如下的命令;
 
[root@localhost ~]# /sbin/fsck.ext3 /dev/hda5
 
如果中间遇到是否修复时,按提示的就是y或yes吧;
 
如果被虚拟的文件系统位于一个映像文件,也是用这种办法,比如:
 
[root@localhost ~]# /sbin/fsck.ext3 fc5.img
 

11.3 Device 0 (vif) could not be connected
 
Warning: The nics option is deprecated. Please use an empty vif entry instead:
 
vif = [ '' ]
 
Error: Device 0 (vif) could not be connected. Hotplug scripts not working.
 
如果出现上面这种错误,应该看一下配置文件中有没有 vif=['']的定义;
 

 发表于: 2006-05-12,修改于: 2006-05-12 13:16 已浏览553次,有评论0条 推荐 投诉

Wednesday, March 14, 2007

为Linux应用构造有限状态机

 
 
 
为Linux应用构造有限状态机 
 
    文档选项
  
将此页作为电子邮件发送
 

拓展 Tomcat 应用
  
下载 IBM 开源 J2EE 应用服务器 WAS CE 新版本 V1.1
 
 
 
 
 
级别: 初级
 
肖文鹏 (xiaowp@263.net), 自由软件爱好者
 
2004 年 10 月 01 日
有限自动机(Finite Automata Machine)是计算机科学的重要基石,它在软件开发领域内通常被称作有限状态机(Finite State Machine),是一种应用非常广泛的软件设计模式(Design Pattern)。本文介绍如何构建基于状态机的软件系统,以及如何利用Linux下的工具来自动生成实用的状态机框架。
 
一、什么是状态机
 
有限状态机是一种用来进行对象行为建模的工具,其作用主要是描述对象在它的生命周期内所经历的状态序列,以及如何响应来自外界的各种事件。在面向对象的软件系统中,一个对象无论多么简单或者多么复杂,都必然会经历一个从开始创建到最终消亡的完整过程,这通常被称为对象的生命周期。一般说来,对象在其生命期内是不可能完全孤立的,它必须通过发送消息来影响其它对象,或者通过接受消息来改变自身。在大多数情况下,这些消息都只不过是些简单的、同步的方法调用而已。例如,在银行客户管理系统中,客户类(Customer)的实例在需要的时候,可能会调用帐户(Account)类中定义的getBalance()方法。在这种简单的情况下,类Customer并不需要一个有限状态机来描述自己的行为,主要原因在于它当前的行为并不依赖于过去的某个状态。
 
遗憾的是并不是所有情况都会如此简单,事实上许多实用的软件系统都必须维护一两个非常关键的对象,它们通常具有非常复杂的状态转换关系,而且需要对来自外部的各种异步事件进行响应。例如,在VoIP电话系统中,电话类(Telephone)的实例必须能够响应来自对方的随机呼叫,来自用户的按键事件,以及来自网络的信令等。在处理这些消息时,类Telephone所要采取的行为完全依赖于它当前所处的状态,因而此时使用状态机就将是一个不错的选择。
 
游戏引擎是有限状态机最为成功的应用领域之一,由于设计良好的状态机能够被用来取代部分的人工智能算法,因此游戏中的每个角色或者器件都有可能内嵌一个状态机。考虑RPG游戏中城门这样一个简单的对象,它具有打开(Opened)、关闭(Closed)、上锁(Locked)、解锁(Unlocked)四种状态,如图1所示。当玩家到达一个处于状态Locked的门时,如果此时他已经找到了用来开门的钥匙,那么他就可以利用它将门的当前状态转变为Unlocked,进一步还可以通过旋转门上的把手将其状态转变为Opened,从而成功地进入城内。
 

图1 控制城门的状态机
 
 
在描述有限状态机时,状态、事件、转换和动作是经常会碰到的几个基本概念。
状态(State) 指的是对象在其生命周期中的一种状况,处于某个特定状态中的对象必然会满足某些条件、执行某些动作或者是等待某些事件。
事件(Event) 指的是在时间和空间上占有一定位置,并且对状态机来讲是有意义的那些事情。事件通常会引起状态的变迁,促使状态机从一种状态切换到另一种状态。
转换(Transition) 指的是两个状态之间的一种关系,表明对象将在第一个状态中执行一定的动作,并将在某个事件发生同时某个特定条件满足时进入第二个状态。
动作(Action) 指的是状态机中可以执行的那些原子操作,所谓原子操作指的是它们在运行的过程中不能被其他消息所中断,必须一直执行下去。
 

 回页首
 
 
 
 
 
二、手工编写状态机
 
与其他常用的设计模式有所不同,程序员想要在自己的软件系统中加入状态机时,必须再额外编写一部分用于逻辑控制的代码,如果系统足够复杂的话,这部分代码实现和维护起来还是相当困难的。在实现有限状态机时,使用switch语句是最简单也是最直接的一种方式,其基本思路是为状态机中的每一种状态都设置一个case分支,专门用于对该状态进行控制。下面的代码示范了如何运用switch语句,来实现图1中所示的状态机:switch (state)  {
  // 处理状态Opened的分支
  case (Opened): {
    // 执行动作Open
    open();
    // 检查是否有CloseDoor事件
    if (closeDoor()) {
      // 当前状态转换为Closed
      changeState(Closed)
    }
    break;
  }
  // 处理状态Closed的分支
  case (Closed): {
    // 执行动作Close
    close();
    // 检查是否有OpenDoor事件
    if (openDoor()) {
      // 当前状态转换为Opened
      changeState(Opened);
    }
    // 检查是否有LockDoor事件
    if (lockDoor()) {
      // 当前状态转换为Locked
      changeState(Locked);
    }
    break;
  }
  // 处理状态Locked的分支
  case (Locked): {
    // 执行动作Lock
    lock();
    // 检查是否有UnlockDoor事件
    if (unlockDoor()) {
      // 当前状态转换为Unlocked
      changeState(Unlocked);
    }
    break;
  }
  // 处理状态Unlocked的分支
  case (Unlocked): {
    // 执行动作Unlock
    unlock();
    // 检查是否有LockDoor事件
    if (lockDoor()) {
      // 当前状态转换为Locked   
      changeState(Locked)
    }
    // 检查是否有OpenDoor事件   
    if (openDoor()) {
      // 当前状态转换为Opened
      changeSate(Opened);
    }
    break;
  }
}
 
 
 
使用switch语句实现的有限状态机的确能够很好地工作,但代码的可读性并不十分理想,主要原因是在实现状态之间的转换时,检查转换条件和进行状态转换都是混杂在当前状态中来完成的。例如,当城门处于Opened状态时,需要在相应的case中调用closeDoor()函数来检查是否有必要进行状态转换,如果是的话则还需要调用changeState()函数将当前状态切换到Closed。显然,如果在每种状态下都需要分别检查多个不同的转换条件,并且需要根据检查结果让状态机切换到不同的状态,那么这样的代码将是枯燥而难懂的。从代码重构的角度来讲,此时更好的做法是引入checkStateChange()和performStateChange()两个函数,专门用来对转换条件进行检查,以及激活转换时所需要执行的各种动作。这样一来,程序结构将变得更加清晰:switch (state)  {
  // 处理状态Opened的分支
  case (Opened): {
    // 执行动作Open
    open();
    // 检查是否有激发状态转换的事件产生
    if (checkStateChange()) {
      // 对状态机的状态进行转换
      performStateChange();
    }
    break;
  }
  // 处理状态Closed的分支
  case (Closed): {
    // 执行动作Close
    close();
    // 检查是否有激发状态转换的事件产生
    if (checkStateChange()) {
      // 对状态机的状态进行转换
      performStateChange();
    }
    break;
  }
  // 处理状态Locked的分支
  case (Locked): {
    // 执行动作Lock
    lock();
    // 检查是否有激发状态转换的事件产生
    if (checkStateChange()) {
      // 对状态机的状态进行转换
      performStateChange();
    }
    break;
  }
  // 处理状态Unlocked的分支
  case (Unlocked): {
    // 执行动作Lock
    unlock();
    // 检查是否有激发状态转换的事件产生
    if (checkStateChange()) {
      // 对状态机的状态进行转换
      performStateChange();
    }
    break;
  }
}
 
 
 
但checkStateChange()和performStateChange()这两个函数本身依然会在面对很复杂的状态机时,内部逻辑变得异常臃肿,甚至可能是难以实现。
 
在很长一段时期内,使用switch语句一直是实现有限状态机的唯一方法,甚至像编译器这样复杂的软件系统,大部分也都直接采用这种实现方式。但之后随着状态机应用的逐渐深入,构造出来的状态机越来越复杂,这种方法也开始面临各种严峻的考验,其中最令人头痛的是如果状态机中的状态非常多,或者状态之间的转换关系异常复杂,那么简单地使用switch语句构造出来的状态机将是不可维护的。
 

 回页首
 
 
 
 
 
三、自动生成状态机
 
为实用的软件系统编写状态机并不是一件十分轻松的事情,特别是当状态机本身比较复杂的时候尤其如此,许多有过类似经历的程序员往往将其形容为"毫无创意"的过程,因为他们需要将大量的时间与精力倾注在如何管理好状态机中的各种状态上,而不是程序本身的运行逻辑。作为一种通用的软件设计模式,各种软件系统的状态机之间肯定会或多或少地存在着一些共性,因此人们开始尝试开发一些工具来自动生成有限状态机的框架代码,而在Linux下就有一个挺不错的选择──FSME(Finite State Machine Editor)。
 

图2 可视化的FSME
 
 
FSME是一个基于Qt的有限状态机工具,它能够让用户通过图形化的方式来对程序中所需要的状态机进行建模,并且还能够自动生成用C++或者Python实现的状态机框架代码。下面就以图1中城门的状态机为例,来介绍如何利用FSME来自动生成程序中所需要的状态机代码。
 
3.1状态机建模
 
首先运行fsme命令来启动状态机编辑器,然后单击工具栏上的"New"按钮来创建一个新的状态机。FSME中用于构建状态机的基本元素一共有五种:事件(Event)、输入(Input)、输出(Output)、状态(State)和转换(Transition),在界面左边的树形列表中可以找到其中的四种。
状态建模
在FSME界面左边的树形列表中选择"States"项,然后按下键盘上的Insert键来插入一个新的状态,接着在右下方的"Name"文本框中输入状态的名称,再在右上方的绘图区域单击该状态所要放置的位置,一个新的状态就创建好了。用同样的办法可以添加状态机所需要的所有状态,如图3所示。
 

图3 状态建模
 
事件建模
 
在FSME界面左边的树形列表中选择"Events"项,然后按下键盘上的Insert键来添加一个新的事件,接着在右下方的"Name"文本框中输入事件的名称,再单击"Apply"按钮,一个新的事件就创建好了。用同样的办法可以添加状态机所需要的所有事件,如图4所示。
 

图4 事件建模
 
转换建模
 
状态转换是整个建模过程中最重要的一个部分,它用来定义有限状态机中的一个状态是如何切换到另一个状态的。例如,当用来控制城门的状态机处于Opened状态时,如果此时有Close事件产生,那么状态机的当前状态将切换到Closed状态,这样一个完整的过程在状态机模型中可以用closeDoor这样一个转换来进行描述。
 
要在FSME中添加这样一个转换,首先需要在界面左边的树形列表中选择"States"下的"Opened"项,然后按下键盘上的Insert键来添加一个新的转换,接着在右下角的"Name"文本框中输入转换的名字"closeDoor",在"Condition"文本框中输入"Close"表明触发该转换的条件是事件Close的产生,在"Target"下拉框中选择"Closed"项表明该转换发生后状态机将被切换到Closed状态,最后再单击"Apply"按钮,一个新的状态转换关系就定义好了,如图5所示。用同样的办法可以添加状态机所需要的所有转换。
 

图5 转换建模
 
 
3.2 生成状态机框架
 
使用FSME不仅能够进行可视化的状态机建模,更重要的是它还可以根据得到的模型自动生成用C++或者Python实现的状态机框架。首先在FSME界面左边的树形列表中选择"Root"项,然后在右下角的"Name"文本框中输入状态机的名字"DoorFSM",再从"Initial State"下拉列表中选择状态"Opened"作为状态机的初始化状态,如图6所示。
 

图6 设置初始属性
 
 
在将状态机模型保存为door.fsm文件之后,使用下面的命令可以生成包含有状态机定义的头文件:[xiaowp@linuxgam code]$ fsmc door.fsm -d -o DoorFSM.h
 
 
 
进一步还可以生成包含有状态机实现的框架代码:[xiaowp@linuxgam code]$ fsmc door.fsm -d -impl DoorFSM.h -o DoorFSM.cpp
 
 
 
如果想对生成的状态机进行验证,只需要再手工编写一段用于测试的代码就可以了:/*
 * TestFSM.cpp
 * 测试生成的状态机框架
 */
#include "DoorFSM.h"
int main()
{
  DoorFSM door;
  door.A(DoorFSM::Close);
  door.A(DoorFSM::Lock);
  door.A(DoorFSM::Unlock);
  door.A(DoorFSM::Open);
}
 
 
 
有限状态机是由事件来进行驱动的,在FSME生成的状态机框架代码中,方法A()可以被用来向状态机发送相应的事件,从而提供状态机正常运转所需要的"动力"。状态机负责在其内部维护一个事件队列,所有到达的事件都会先被放到事件队列中进行等候,从而能够保证它们将按照到达的先后顺序被依次处理。在处理每一个到达的事件时,状态机都会根据自己当前所处的状态,检查与该状态对应的转换条件是否已经被满足,如果满足的话则激活相应的状态转换过程。
 
使用下面的命令能够将生成的状态机框架和测试代码编译成一个可执行文件:[xiaowp@linuxgam code]$ g++ DoorFSM.cpp TestFSM.cpp -o fsm
 
 
 
由于之前在用fsmc命令生成状态机代码时使用了-d选项,生成的状态机框架中会包含一定的调试信息,包括状态机中每次状态转换时的激活事件、转换前的状态、所经历的转换、转换后的状态等,如下所示:[xiaowp@linuxgam code]$ ./fsm
DoorFSM:event:'Close'
DoorFSM:state:'Opened'
DoorFSM:transition:'closeDoor'
DoorFSM:new state:'Closed'
DoorFSM:event:'Lock'
DoorFSM:state:'Closed'
DoorFSM:transition:'lockDoor'
DoorFSM:new state:'Locked'
DoorFSM:event:'Unlock'
DoorFSM:state:'Locked'
DoorFSM:transition:'unlockDoor'
DoorFSM:new state:'Unlocked'
DoorFSM:event:'Open'
DoorFSM:state:'Unlocked'
DoorFSM:transition:'openDoor'
DoorFSM:new state:'Opened'
 
 
 
3.3 定制状态机
 
目前得到的状态机已经能够响应来自外部的各种事件,并适当地调整自己当前所处的状态,也就是说已经实现了状态机引擎的功能,接下来要做的就是根据应用的具体需求来进行定制,为状态机加入与软件系统本身相关的那些处理逻辑。在FSME中,与具体应用相关的操作称为输出(Output),它们实际上就是一些需要用户给出具体实现的虚函数,自动生成的状态机引擎负责在进入或者退出某个状态时调用它们。
 
仍然以控制城门的那个状态机为例,假设我们希望在进入每个状态时都添加一部分处理逻辑。首在FSME界面左边的树形列表选择"Outputs"项,然后按下键盘上的Insert键来添加一个新的输出,接着在右下方的"Name"文本框中输入相应的名称,再单击"Apply"按钮,一个新的输出就创建好了,如图7所示。用同样的办法可以添加状态机所需要的所有输出。
 

图7 添加输出
 
 
当所有的输出都定义好之后,接下来就可以为状态机中的每个状态绑定相应的输出。首先在FSME界面左侧的"States"项中选择相应的状态,然后从右下角的"Available"列表框中选择与该状态对应的输出,再单击"<"按钮将其添加到"In"列表中,如图8所示。用同样的办法可以为状态机中的所有状态设置相应的输出,同一个状态可以对应有多个输出,其中In列表中的输出会在进入该状态时被调用,而Out列表中的输出则会在退出该状态时被调用,输出调用的顺序是与其在In或者Out列表中的顺序相一致的。
 

图8 为状态设置输出
 
 
由于对状态机模型进行了修改,我们需要再次生成状态机的框架代码,不过这次不需要加上-d参数:[xiaowp@linuxgam code]$ fsmc door.fsm -o DoorFSM.h
[xiaowp@linuxgam code]$ fsmc door.fsm -d -impl DoorFSM.h -o DoorFSM.cpp
 
 
 
我们在新的状态机模型中添加了enterOpend、enterClosed、enterLocked和enterUnlocked四个输出,因此生成的类DoorFSM中会包含如下几个纯虚函数:      virtual void enterOpened() = 0;
   virtual void enterLocked() = 0;
   virtual void enterUnlocked() = 0;
   virtual void enterClosed() = 0;
  
 
 
 
显然,此时生成的状态机框架不能够再被直接编译了,我们必须从类DoorFSM派生出一个子类,并提供对这几个纯虚函数的具体实现:/*
 * DoorFSMLogic.h
 * 状态机控制逻辑的头文件
 */
#include "DoorFSM.h"
class DoorFSMLogic : public DoorFSM
{
 
 protected:
  virtual void enterOpened();
  virtual void enterLocked();
  virtual void enterUnlocked();
  virtual void enterClosed();
};
 
 
 
正如前面所提到过的,这几个函数实际上代表的正是应用系统的处理逻辑,作为例子我们只是简单地输出一些提示信息:/*
 * DoorFSMLogic.cpp
 * 状态机控制逻辑的实现文件
 */
#include "DoorFSMLogic.h"
#include <iostream>
void DoorFSMLogic::enterOpened()
{
    std::cout << "Enter Opened state." << std::endl;
}
void DoorFSMLogic::enterClosed()
{
    std::cout << "Enter Closed state." << std::endl;
}
void DoorFSMLogic::enterLocked()
{
    std::cout << "Enter Locked state." << std::endl;
}
void DoorFSMLogic::enterUnlocked()
{
    std::cout << "Enter Unlocked state." << std::endl;
}
 
 
 
同样,为了对生成的状态机进行验证,我们还需要手工编写一段测试代码:/*
 * TestFSM.cpp
 * 测试状态机逻辑
 */
#include "DoorFSMLogic.h"
int main()
{
  DoorFSMLogic door;
  door.A(DoorFSM::Close);
  door.A(DoorFSM::Lock);
  door.A(DoorFSM::Unlock);
  door.A(DoorFSM::Open);
}
 
 
 
使用下面的命令能够将生成的状态机框架和测试代码编译成一个可执行文件:[xiaowp@linuxgam code]$ g++ DoorFSM.cpp DoorFSMLogic.cpp TestLogic.cpp -o logic
 
 
 
运行结果如下所示:[xiaowp@linuxgam code]$ ./logic
Enter Closed state.
Enter Locked state.
Enter Unlocked state.
Enter Opened state.
 
 
 
本文涉及代码下载: code.zip
 

 回页首
 
 
 
 
 
四、小结
 
在面向对象的软件系统中,有些对象具有非常复杂的生命周期模型,使用有限状态机是描述这类对象最好的方法。作为一种软件设计模式,有限状态机的概念虽然不算复杂,实现起来也并不困难,但它的问题是当状态机的模型复杂到一定的程度之后,会带来实现和维护上的困难。Linux下的FSME是一个可视化的有限状态机建模工具,而且支持状态机框架代码的自动生成,借助它可以更加轻松地构建基于有限状态机的应用系统。
 
 
 
参考资料
从Wiki百科全书 http://en.wikipedia.org/wiki/Finite_state_automaton开始,你可以了解到许多同状态机相关的计算理论知识。
 
状态机是UML的一个重要组成部分,Robert C. Martin在他的文章UML Tutorial: Finite State Machines中,介绍了如何使用UML语言来对状态机进行建模,你可以通过网址 http://www.objectmentor.com/resources/articles/umlfsm.pdf可以找到这一文档。
 
FSME是Linux下一个基于Qt的状态机建模工具,它能够自动生成状态机框架代码,并且同时支持C++和Python语言,通过网站 http://fsme.sourceforge.net/你可以了解到有关FSME的更多信息,并能够下载最新版本的FSME。
 
Qfsm也是一个运行在Linux下的状态机建模工具,它不仅提供了可视化的状态机编辑器,而且还能够对生成的状态机进行实时模拟,通过网站 http://qfsm.sourceforge.net/可以了解到Qfsm的更多信息。
 
 
 
关于作者
 
  
本文作者肖文鹏是一名自由软件爱好者,主要从事操作系统和分布式计算环境的研究,喜爱Linux和Python。你可以通过 xiaowp@263.net与他取得联系。

安装 fsme 1.04

fsme1.04
1. donload the lib "cairo"  (http://cairographics.org/releases/)
2. install cairo
3. add the directory containing `cairo.pc' to the PKG_CONFIG_PATH environment variable
      ]# export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig/
4. install fsme1.04
      qmake
       make
   will bin/fsme
5. cd /usr/bin
      ln -s /usr/local/fsm/fsmc-1.0.4/fsmc
       ln -s /usr/local/fsm/fsme-1.0.4/bin/fsme

Thursday, March 08, 2007

SVN 命令

Tortoise   SVN 命令
检出
svn checkout [-N] [--ignore-externals] [-r rev] URL PATH
 
如果希望只检出顶级目录被选中,使用-N选项。
 
如果希望忽略外部被选中,使用--ignore-externals选型。
 
如果你正在检出特定的修订版本,在URL后使用-r指定。
更新
svn info URL_of_WC
svn update [-r rev] PATH
 
更新多个项目在Subversion还不是原子操作,所以TortoiseSVN会首先找到版本库的HEAD修订版本,然后将所有项目更新到特定修订版本,防止出现混合修订版本的工作副本。
 
如果只有一个项目被选中更新,或选中的项目来自不同的版本库,TortoiseSVN只会更新到HEAD。
 
没有使用命令行选项,更新到修订版本也实现了更新命令,但提供了更多的选项。
更新到版本
svn info URL_of_WC
svn update [-r rev] [-N] [--ignore-externals] PATH
 
如果希望只更新顶级目录,使用-N选项。
 
如果希望忽略外部被选中,使用--ignore-externals选型。
提交
 
在TortoiseSVN,提交对话框使用Subversion命令,第一部分是检查工作副本哪些文件可能被提交,然后你可以检查列表,比较与BASE的区别,选择你希望提交包含的项目。
svn status -v PATH
 
如果选择了显示未版本控制的文件,TortoiseSVN会遵循忽略规则显示工作目录中所有未版本控制的文件和文件夹。这个特性在Subversion中没有等价操作,因为svn status 命令不扫描未版本控制的文件夹。
 
如果你选择了未版本控制的文件和文件夹,这些项目都会先增加到你的工作副本。
svn add PATH...
 
当你点击确认,开始执行Subversion提交。如果你不修改所有的文件检查框,TortoiseSVN 会递归提交工作副本。如果你取消选择一些文件,那么就必须使用非递归提交 (-N) ,每个路径都必须在命令行上单独指定。
svn commit -m "LogMessage" [-N] [--no-unlock] PATH...
 
日志消息是日志编辑框的内容。它可以为空。
 
如果选择了保持锁,就使用--no-unlock开关。
差异
svn diff PATH
 
如果你在右键菜单使用差异,就会将修改的文件与基础版本比较。控制台的命令输出也是执行这个操作,输出统一差异格式。然而,TortoiseSVN不使用它。TortoiseSVN 使用TortoiseMerge (或者你选择的比较差异程序)可视化的全文显示差异,所以它没有控制台等价操作。
 
你可以使用TortoiseSVN,比较任意两个文件的差异,不管他们是否受版本控制。TortoiseSVN只是把这两个文件传递给已经选择的比较差异程序,让它比较差异。
显示日志
svn log -v -r 0:N --limit 100 [--stop-on-copy] PATH
  或者
svn log -v -r M:N [--stop-on-copy] PATH
 
默认情况下,TortoiseSVN尝试用--limit方法取得100个日志消息。如果设置了让它使用旧借口,那么就使用第二种个是获得100个日志消息。
 
如果选择了停止于复制/改名,就使用--stop-on-copy开关。
检查所作的修改
svn status -v PATH
  或者
svn status -u -v PATH
 
只在你的工作副本执行初始的状态检查。如果你点击检查版本库,那么也检查版本库,察看哪些文件会被更新操作修改,它需要-u开关。
 
如果选择了显示未版本控制的文件,TortoiseSVN会遵循忽略规则显示工作目录中所有未版本控制的文件和文件夹。这个特性在Subversion中没有等价操作,因为svn status 命令不扫描未版本控制的文件夹。
版本图
 
版本图是TortoiseSVN特有的,命令行客户端没有等价实现。
 
TortoiseSVN执行了这些操作
svn info URL_of_WC
svn log -v URL
 
其中URL是版本库的 根,返回分析数据。
版本库浏览器
svn info URL_of_WC
svn list [-r rev] -v URL
 
你可以使用svn info检查版本库的根,它在版本库浏览器的顶级显示。你不能浏览它的上级目录。同样,这个命令返回所有显示在版本库浏览器的锁信息。
 
给出URL和可选的版本号,svn list列出目录中的内容。
编辑冲突
 
这个命令没有控制台等价实现。它调用TortoiseMerge或者外部三路差异/合并工具察看棘手的冲突,挑选出冲突行。
已解决
svn resolved PATH
改名
svn rename CURR_PATH NEW_PATH
删除
svn delete PATH
恢复
svn status -v PATH
 
首先开始状态检查,察看你的工作副本有哪些项目可以被撤销。你可以复审文件列表,检查这些文件的修改,然后选择你要撤销的项目。
 
当你点击确认时,开始Subversion撤销操作。如果你不修改所有的文件检查框,TortoiseSVN 会递归撤销 (-R)工作副本的修改。如果你取消选择一些文件,那么就必须使用非递归撤销 ,每个路径都必须在命令行上单独指定。"
svn revert [-R] PATH...
清理
svn cleanup PATH
获得锁
svn status -v PATH
 
首先开始状态检查,察看你的工作副本有哪些项目可以被加锁。你可以选择想加锁的项目。
svn lock -m "LockMessage" [--force] PATH...
 
加锁信息是加锁编辑框的内容。它可以为空。"
 
如果选择了强制锁定 ,就使用--force开关。
释放锁
svn unlock PATH
分支/标记
svn copy -m "LogMessage" URL URL
  或
svn copy -m "LogMessage" URL@rev URL@rev
  或
svn copy -m "LogMessage" PATH URL
 
分支/标签对话框在版本库执行复制。有三个单选按钮:
版本库中的最新版本
指定版本库中的版本
工作副本
 
对应上面的三个命令行参数。
 
日志消息是日志编辑框的内容。它可以为空。
切换
svn info URL_of_WC
svn switch [-r rev] URL PATH
合并
svn merge [--dry-run] --force From_URL@revN To_URL@revM PATH
 
Dry run与使用--dry-run选项的merge相同。
svn diff From_URL@revN To_URL@revM
 
Unified diff显示了用来合并的区别操作。
输出
svn export [-r rev] [--ignore-externals] URL Export_PATH
 
这个形式是当从一个未版本控制目录访问,并且文件夹作为目标。
 
导出一个工作副本到一个目录没有使用Subversion的库,所以没有等同的命令行匹配。
 
TortoiseSVN做的只是将所有文件复制到一个新的位置,并且会显示操作的过程。未版本控制的文件/文件夹也可以被导出。
 
在两种情况下,如果Omit externals被选中,就相当于使用了--ignore-externals选项。
重新定位
svn switch --relocate From_URL To_URL
在当前位置创建版本库
svnadmin create --fs-type fsfs PATH
  或
svnadmin create --fs-type bdb PATH
添加
svn add PATH...
 
如果选择了一个文件夹,TortoiseSVN会首先会递归的访问可以添加的条目。
导入
svn import -m LogMessage PATH URL
 
日志消息是日志编辑框的内容。它可以为空。
追溯
svn blame -r N:M -v PATH
svn log -r N:M PATH
 
如果你使用TortoiseBlame来查看追溯信息,文件日志也需要在工具提上上显示日志信息,如果你以文件方式查看追溯,这个信息不是必须的。
加入忽略列表
svn propget svn:ignore PATH > tempfile
{编辑新的忽略内容到tempfile文件中}
svn propset svn:ignore -F tempfile PATH
 
因为svn:ignore通常是多行的,这里是通过文件显示,而不是直接使用命令行操作。
创建补丁
svn diff PATH > patchfile
 
TortoiseSVN通过比较工作副本和它的基础版本产生一个标准区别格式(unified diff format)的补丁文件。
应用补丁(Apply Patch)
 
如果补丁和工作副本不是同一版本的话,那么应用补丁会是一件很棘手的事情。幸运的是,你可以使用 TortoiseMerge(在Subversion中没有等同的工具)。