2006年06月14日

       一种后续遍历二叉树的非递归算法:采用一个附加的标志位,等当前节点的右子树也遍历完毕才遍历该元素。实现代码中有一个缺点是对每一个节点的遍历均要两次判断,第一次遍历右子树,并修改标志位,第二次才是真正的遍历。这里有一个问题就是:当当前节点没有右子树的时候没有必要进行这样两次的判断(至少所有的叶子节点没有必要)。这里给出后序遍历二叉树的另外一种实现策略,这个策略不需要标志位信息,实现思想为,每次遍历当前节点,做如下判断:

1)  如当前节点右子树为空,或者当前节点右子树存在但已经遍历,则遍历当前节点,出栈;

2)  若当前节点右子树不为空,但是还没有遍历,则遍历右子树,不访问栈顶元素,访问右子树。

实现中采用了一个指针指向当前栈顶元素的右子树(有的话)。

实现代码为:

//BinTreeNode.h

 

#ifndef _BIN_TREE_NODE_H_

#define _BIN_TREE_NODE_H_

 

class Element

{

public:

       Element(int val = -1, Element* lchild = 0,Element* rchild = 0)

              :_val(val), _lchild(lchild),_rchild(rchild)

       {

       }

public:

       int _val;

           Element* _lchild;

       Element* _rchild;

};

 

typedef Element* TreeNode;

 

#endif //~_BIN_TREE_NODE_H_
 
//TreeRetrive.h

 

#if !defined _TREE_RETRIVE_H_

#define _TREE_RETRIVE_H_

 

#include "BinTreeNode.h"

 

#include <stack>

#include <iostream>

using namespace std;

 

TreeNode BuildTree()

{

       TreeNode node = new Element[6];

       for (int i = 1; i < 7; ++i)

       {

              node[i - 1]._val = i;

       }

 

       node[0]._lchild = &node[1];

       node[0]._rchild = &node[2];

       node[1]._lchild = &node[3];

       node[1]._rchild = &node[4];

       node[2]._lchild = 0;

       node[2]._rchild = &node[5];

       node[3]._lchild = 0;

       node[3]._rchild = 0;

       node[4]._lchild = 0;

       node[4]._rchild = 0;

       node[5]._lchild = 0;

       node[5]._rchild = 0;

 

       return node;

}

 

void PostRetrive(const TreeNode root)

{

       if (root)

       {

              PostRetrive(root->_lchild);

              PostRetrive(root->_rchild);

              cout<<root->_val<<" ";

       }

}

 

void PostTreeRetrive(const TreeNode root)

{

       TreeNode r = root;

       TreeNode p = NULL;

 

       stack<TreeNode> tree;

      

       while ((!tree.empty()) || (r != NULL))

       {    

              while (r != NULL)

              {

                     tree.push(r);

                     r = r->_lchild;

              }

             

              if (!tree.empty())

              {

                     r = tree.top();

                     if ((r->_rchild == p) || (r->_rchild == NULL))

                     {

                            cout<<r->_val<<" ";

                            tree.pop();

                            p = r;

                            r = NULL;

                     }

                     else

                     {

                            r = r->_rchild;

                     }

              }

       }

}

#endif //~_TREE_RETRIVE_H_
 
//main.cpp

 

#include "BinTreeNode.h"

#include "TreeRetrive.h"

 

#include <iostream>

using namespace std;

 

int main(int argc,char* argvp[])

{

       TreeNode root = BuildTree();

       PostRetrive(root);

       cout<<endl;

 

       PostTreeRetrive(root);

       cout<<endl;

 

       return 0;

}
 

    搜索一个目录及其子目录下所有的文件是比较常见的需求,而采用递归方式进行搜索则是
一个非常直观的算法。但是,由于目录中文件数量往往比较大,而每个文件名又往往占用许多
空间,目录嵌套比较深的情况下,这种递归算法对于程序的堆栈是一个严重的威胁。本文给出
一种非递归的算法进行目录下所有文件的检索和遍历。

typedef BOOL (*PROCESS_FILE_FUNCTION)(LPCTSTR filename);
上面的PROCESS_FILE_FUNCTION是一种函数指针,这个函数处理文件名为filename的文件,如果该函数返回
FALSE,则ProcessDirectory立刻退出,不再继续查找
void ProcessDirectory(LPCTSTR dirname,PROCESS_FILE_FUNCTION proc)
{
     CStringArray dirs;
     CString searchname;
     CFileFind find;
     dirs.Add(dirname);
     BOOL bRet;
     while(dirs.GetSize()>0)
     {
       
        searchname = dirs[0] +"\\*.*";
        dirs.RemoveAt(0);
        bRet = find.FindFile (searchname,0);
        if(!bRet)continue;
        do{
            bRet = find.FindNextFile ();
            if(find.IsDots ())
            {//忽略.和..文件
                continue;
            }
            if(find.IsDirectory ())
            {
                dirs.Add (find.GetFilePath());
                continue;
            }else{
                if(!proc(find.GetFilePath ()))
                {
                    return;
                }
            }
        }while(bRet);
     }
}

非常精彩加经典的时间换空间的算法。
(摘自VC知识库论坛精华,作者:罗恩)
以下是VC知识库论坛精华的评论,很好的解释了整个程序。
        很巧妙的方法。递归的方法好处是编程简单,代码容易理解,但缺点是效率不高,而且有深度限制,如果深度太深,则堆栈会满,
一致于程序出错,所以这一程序目的并不是用时间换空间,而是兼顾了各种因素,既提高效率,又不会有深度的限制

在JAVA中实现二叉树,程序如下(转载)

//********************************************************************
//filename: BinaryTreeTest.java
//purpose: test a binarytree with java
//date: 2002/12/18
//author: flyfan
//ver: 0.1
//********************************************************************

public class BinaryTreeTest
{
public static void main(String args[])
{
BinaryTreeTest b=new BinaryTreeTest();
int data[]={12,11,34,45,67,89,56,43,22,98};
BinaryTree root =new BinaryTree(data[0]);

System.out.print("二叉树的中的数据:  ");
for(int i=1;i<data.length;i++)
{
root.insertTree(root,data[i]);
System.out.print(data[i-1]+";");
}

System.out.println(data[data.length-1]);

int key=Integer.parseInt(args[0]);

if(b.searchkey(root,key))
{
System.out.println("找到了:"+key);
}
else
{
System.out.println("没有找到:"+key);
}
}

public boolean searchkey(BinaryTree root, int key)
{
boolean bl=false;
if(root==null)
{
bl=false;
return bl;
}
else if(root.data==key)
{
bl=true;
return bl;
}
else if(key>=root.data)
{
return searchkey(root.rightpoiter,key);
}
return searchkey(root.leftpoiter,key);
}
}

class BinaryTree
{
int data;
BinaryTree leftpoiter;
BinaryTree rightpoiter;

BinaryTree(int data)
{
this.data=data;
leftpoiter=null;
rightpoiter=null;
}

public void insertTree(BinaryTree root, int data)
{
if(data>=root.data)
{
if(root.rightpoiter==null)
{
root.rightpoiter=new BinaryTree(data);
}
else
{
insertTree(root.rightpoiter,data);
}
}
else
{
if(root.leftpoiter==null)
{
root.leftpoiter=new BinaryTree(data);
}
else
{
insertTree(root.leftpoiter,data);
}
}
}
}
//end

讲解:上述各序小,但层次分明,结构严谨,如果有数据库结构知识与C语文能力的JAVA初学者

一看就明白,二个方法如同C语文中的函数,一个寻找关键字--searchkey 另一个是插入一个

结点:insertTree 而class BinaryTree 如同一个C语言中的共同体.


另外这是一个完全的先序遍历二叉树的语法。先根结点,再左结点,如无再右结点,如些加归至

搜索完毕。

运行命令行:java BinaryTreeTest intNumber(一个整数)

#include <stack>
#include <iostream>
using namespace std;

template <class T>
class TreeNode
{
  public:
    T data;
    TreeNode<T> *left; //left child
    TreeNode<T> *right; //right child
 
    TreeNode():left(NULL),right(NULL)
    {
    }

    TreeNode(const T& t):data(t),left(NULL), right(NULL)
    {
    }

    TreeNode(const T& t, TreeNode<T*> left, TreeNode<T*> right):data(t),left(left), right(left)
    {
    }
};

/**purpose: 对二叉树进行后序遍历(非递归算法)
  *@param TreeNode<T> *root the root of the binary tree
  */
template <class T>
void postOrder(TreeNode<T> *root)
{
  stack<TreeNode<T>*> st;
  TreeNode<T> *p = root;
  TreeNode<T> *pre = NULL;//pre表示最近一次访问的结点
 
  while(p || st.size()!=0)
  {
    //沿着左孩子方向走到最左下 。
    while(p)
    {
      st.push(p);
      p = p->left;
    }
    //get the top element of the stack
    p = st.top();
    //如果p没有右孩子或者其右孩子刚刚被访问过
   if(p->right == NULL || p->right == pre)
    {
      //visit this element and then pop it
      cout << "visit: " << p->data << endl;
      st.pop();
      pre = p;
      p = NULL;
    
    }
   else
   {
     p = p->right;
   
   }
  }//end of while(p || st.size()!=0)

}

2006年04月18日

  1965IBM7044机上首次实现虚拟技术以来,这一名词对于计算机世界来说已经不是一个新名词。但当我们面对硬分区、软分区、逻辑分区、Solaris ContainerVMwareXenVirtual Server等名词的时候,真能准确分辨出它们的应用特性么?

  40多年来,虚拟技术已经发展到这样的一个阶段—我们每天享受着这一技术的好处,但并未意识到—例如当你打开Windows Server的命令行窗口时,便是在Windows上使用着虚拟机的功能。既然已经如此成熟,为什么我们还要关心虚拟技术?原因很简单,虚拟技术的概念虽然成熟,但它的重要性还尚未引起国内用户和读者的足够重视,尤其是在服务器和存储领域,虚拟技术将发挥决定性的作用。

何谓虚拟?

  当我们尝试着理解虚拟技术时,首先必须要理解“虚拟”的概念。“虚拟”这个词最早来源于光学,用于理解镜子里的物体。现在,“虚拟”这个词已经经过演化,用来描述任何真实物体的模拟了,例如分区、虚拟机、虚拟内存、虚拟磁盘和虚拟现实。在讨论虚拟技术的时候,使用“虚拟”这个词,是因为我们希望虚拟机看起来和工作起来都和真正的机器一模一样。这意味着,虚拟机并不是真正的机器,但是它能像真正的机器一模一样地工作。

  实际上,从原理上看,所有虚拟技术虚拟的是指令集。所有的IT设备,不管是PC、服务器还是存储,都有一个共同点:它们被设计用来完成一组特定的指令。这些指令组成一个指令集。对于虚拟技术而言,“虚拟”实际上就是指的这些指令集。虚拟机有许多不同的类型,但是它们有一个共同的主题就是模拟一个指令集的概念。每个虚拟机都有一个用户可以访问的指令集。虚拟机把这些虚拟指令“映射”到计算机的实际指令集。

  定义完“虚拟”的概念,我们可以清楚知道,目前所能看到的硬分区、软分区、逻辑分区、Solaris ContainerVMwareXen、微软Virtual Server2005这些虚拟技术,都是同样的原理,只是虚拟指令集所处的位置不同而已。

  按照虚拟层所处位置的不同,目前所有的虚拟技术大致可以分为硬件虚拟、逻辑虚拟、软件虚拟和应用虚拟四种类型。

虚拟原动力:服务器效率

  目前,一般企业内的服务器仅能达到15%30%的系统处理能力,绝大部分的服务器负载都低于40%,大部分的服务器处理能力并没有得到利用,IT投资回报率偏低。正如41年前IBM研发虚拟技术的出发点,让一台机器尽可能多地让更多用户和应用程序有效使用,一直都是虚拟技术发展的原动力。

  中科院物理所量子模拟科学中心(量子中心)的徐力方研究员对此感触颇深。2002年底,物理所定购了两台满配32Power4IBM p690服务器,一台用于后台作业运算,一台作为登录节点和交互作业运算。但到了20039月,由于研究所的科研项目和学生迅速增加,交互作业节点作业拥挤,导致整机效率下降。

  怎么办?徐力方咨询了IBM的技术人员,得到的答复是可以采用逻辑分区的技术,将登录节点机划分为8/24两个分区,8CPU的分区用于节点登录,另24CPU用于后台作业。但这样做仍然存在问题,因为8CPU又不够交互作业使用,徐力方介绍说,由于科研项目运行的并行程序众多, 其中有学生们自行编写或修改自开放源码,难以避免多数子作业运行完毕,而少数子作业还在运算的情况,这样就会出现计算能力的浪费。如果用逻辑分区把分区细分,又会出现某些项目在细分区上无法计算的情况——分区资源变更又浪费时间。

  这一问题最终得到了圆满解决,200310月,IBM发布了AIX 5L v5.2IBM的工程师随后以动态逻辑分区的方式配置了5个动态分区,高峰时每个研究组各占20%的资源,但闲暇时则每个分区都能调用所有的计算资源,这样,既做到资源的合理分配,又做到了资源的充分利用。

  量子中心的案例中,虽然使用了5个分区,但都采用的是AIX操作系统,那么多操作系统的虚拟应用情况如何?据中国惠普CSG企业服务器产品经理王镝介绍,国内已有实际用户实施了多操作系统虚拟。王镝介绍说,2005年,国内某用户采购了1台配置32颗安腾2处理器的(其中16颗为待激活状态)HP Integrity Superdome服务器,系统先以硬分区技术划分为两个物理分区,然后每个物理分区用vPARHP Virtual Machine技术划分为三类逻辑分区,分别运行社保交易服务器、BEA Weblogic应用服务器、Oracle数据库服务器,分别运行在HP-UXLinux平台上,统一以HP Workload Management管理。这样,当日常白天医保交易繁忙时,可将数据库服务器分区的计算资源调配到社保交易分区,晚上进行批处理业务的时候再调配;而在月末各分区的业务都繁忙时,以iCOD(按需扩容)或TiCOD(购买待激活CPU若干小时的点卡)的方式,将待激活CPU临时调配到各个分区。这样,用户既获得了足够的计算资源和安全性,但又只需较低的成本,保证了投资回报率。

24.jpg

25b.jpg

实施虚拟应注意什么?

  虚拟技术虽然成熟,但实施起来可不能想当然。那么在虚拟实施过程中应该注意些什么?

  中国人民银行清算总中心是IBM大型主机和p系列服务器的老用户,从1992年前后开始使用大型主机,算来也有14年的历史。清算中心开发部的副总经理贝劲松在谈起实施虚拟技术时,认为尤其应该关注两点。

  首先是实施前要对业务系统、对计算资源的需求有明确了解。贝劲松笑谈,清算总中心在这方面是摸索出来的经验。2002年时,清算总中心曾购买了两台配置81.1GHz Power4处理器的IBM eServer p690服务器,当时按照4/2/2的方式划分为了三个逻辑分区,但通过压力测试发现第一个分区负载较小,反而是第二个分区经常过载,于是将分区调整为2/4/2的配置,解决了这个问题。

  其次是实施前要有充分的测试期。贝劲松认为,像银行这样的自行开发业务系统的行业,相对比较容易了解业务系统对计算资源的压力,但即使这样,也需要进行充分测试,如不具备对等配置测试环境,也应在处理能力稍低的同类硬件平台上进行测试。例如,清算总中心2005年购买了8IBM eServer p570(用于生产)和两路的eServer p570 p550(用于测试),分别按照2/60.5/1.5的配置进行分区,在系统的开发期和测试期,p550上的测试数据有助于他们了解业务系统对计算资源的需求。如果是不自行开发业务系统的行业,就更有必要在近似系统上进行测试,毕竟生产系统的安全性是第一位的。

  除了老用户的经验之谈,笔者认为在实施虚拟技术之前,还应参照左表,决定采用哪种虚拟技术最合适。从表中容易得知,如果是简单的单机应用开发,那么采用应用虚拟技术最合适;如果需要开发Web应用,那么软件虚拟技术才能满足需求。

虚拟并非万能

  或许上面的内容会给读者一个错觉,即虚拟技术是如此优异,如果自己的企业还没有使用,将会在未来的竞争中出于劣势,实际上,仍有许多用户还不需要用到虚拟技术。

  国家气象中心是高性能计算机集群和IBM p系列的老用户,尤其是2004年采购的IBM eServer p655集群,以32001.7GHzPower4+处理器实现了10.31TeraFlops/sLinpack性能,牢牢占据着系统中国高性能计算机的头把交椅,如此高性能的系统,气象中心是如何看待分区或者虚拟机技术呢?

  答案出乎笔者先前的预料,据国家气象中心计算机室主任田浩的介绍,他们并没有采用任何分区和虚拟技术。为什么?原因就在于气象行业应用软件的特殊性上。田浩解释说,气象预报主要采用的短期和中期预报,大都采用将预测区域划分为近似正方的栅格(边长越小预报越精确),然后用各种计算模型(如MM5GRAPES等)进行运算。虽然气象中心集群的性能是国内目前最强大的,系统的满载率也接近60%(7×24×365),但如果是24小时的短期预报,目前一般在34小时内得出结果;而中期预报(10天,30公里)则大概需要56小时,这一速度虽然比以前快很多,但还算不上完美——也就是说,目前的整个系统跑生产系统没问题,但富余的资源并不多。田浩笑说,像气象中心这样对计算资源需求永无止境的行业,恐怕是很难体验到虚拟技术的好处的。不过,田浩同时认为,如果气象中心在业务系统的并行算法上能够取得突破,气象中心未来采用虚拟技术的可能性同样存在。

  除了气象预报和石油物探这样对计算资源需求永无止境的高端领域,低端应用当然也不需要用到虚拟技术,倒不是没这样的需求,而是因为通常前端应用的硬件平台性能还不够好,即使是一个勇于尝试的IT主管,也不会轻易在一台单路至强服务器上用VMware GSX Server虚拟出若干LinuxWindows操作系统,把公司的邮件、Web和文件服务整合到一台机器上。

25.jpg

25.jpg

25.jpg

25.jpg

  40多年来,虚拟技术已经发展到这样的一个阶段—我们每天享受着这一技术的好处,但并未意识到—例如当你打开Windows Server的命令行窗口时,便是在Windows上使用着虚拟机的功能。既然已经如此成熟,为什么我们还要关心虚拟技术?原因很简单,虚拟技术的概念虽然成熟,但它的重要性还尚未引起国内用户和读者的足够重视,尤其是在服务器和存储领域,虚拟技术将发挥决定性的作用。

何谓虚拟?

  当我们尝试着理解虚拟技术时,首先必须要理解“虚拟”的概念。“虚拟”这个词最早来源于光学,用于理解镜子里的物体。现在,“虚拟”这个词已经经过演化,用来描述任何真实物体的模拟了,例如分区、虚拟机、虚拟内存、虚拟磁盘和虚拟现实。在讨论虚拟技术的时候,使用“虚拟”这个词,是因为我们希望虚拟机看起来和工作起来都和真正的机器一模一样。这意味着,虚拟机并不是真正的机器,但是它能像真正的机器一模一样地工作。

  实际上,从原理上看,所有虚拟技术虚拟的是指令集。所有的IT设备,不管是PC、服务器还是存储,都有一个共同点:它们被设计用来完成一组特定的指令。这些指令组成一个指令集。对于虚拟技术而言,“虚拟”实际上就是指的这些指令集。虚拟机有许多不同的类型,但是它们有一个共同的主题就是模拟一个指令集的概念。每个虚拟机都有一个用户可以访问的指令集。虚拟机把这些虚拟指令“映射”到计算机的实际指令集。

  定义完“虚拟”的概念,我们可以清楚知道,目前所能看到的硬分区、软分区、逻辑分区、Solaris ContainerVMwareXen、微软Virtual Server2005这些虚拟技术,都是同样的原理,只是虚拟指令集所处的位置不同而已。

  按照虚拟层所处位置的不同,目前所有的虚拟技术大致可以分为硬件虚拟、逻辑虚拟、软件虚拟和应用虚拟四种类型。

虚拟原动力:服务器效率

  目前,一般企业内的服务器仅能达到15%30%的系统处理能力,绝大部分的服务器负载都低于40%,大部分的服务器处理能力并没有得到利用,IT投资回报率偏低。正如41年前IBM研发虚拟技术的出发点,让一台机器尽可能多地让更多用户和应用程序有效使用,一直都是虚拟技术发展的原动力。

  中科院物理所量子模拟科学中心(量子中心)的徐力方研究员对此感触颇深。2002年底,物理所定购了两台满配32Power4IBM p690服务器,一台用于后台作业运算,一台作为登录节点和交互作业运算。但到了20039月,由于研究所的科研项目和学生迅速增加,交互作业节点作业拥挤,导致整机效率下降。

  怎么办?徐力方咨询了IBM的技术人员,得到的答复是可以采用逻辑分区的技术,将登录节点机划分为8/24两个分区,8CPU的分区用于节点登录,另24CPU用于后台作业。但这样做仍然存在问题,因为8CPU又不够交互作业使用,徐力方介绍说,由于科研项目运行的并行程序众多, 其中有学生们自行编写或修改自开放源码,难以避免多数子作业运行完毕,而少数子作业还在运算的情况,这样就会出现计算能力的浪费。如果用逻辑分区把分区细分,又会出现某些项目在细分区上无法计算的情况——分区资源变更又浪费时间。

  这一问题最终得到了圆满解决,200310月,IBM发布了AIX 5L v5.2IBM的工程师随后以动态逻辑分区的方式配置了5个动态分区,高峰时每个研究组各占20%的资源,但闲暇时则每个分区都能调用所有的计算资源,这样,既做到资源的合理分配,又做到了资源的充分利用。

  量子中心的案例中,虽然使用了5个分区,但都采用的是AIX操作系统,那么多操作系统的虚拟应用情况如何?据中国惠普CSG企业服务器产品经理王镝介绍,国内已有实际用户实施了多操作系统虚拟。王镝介绍说,2005年,国内某用户采购了1台配置32颗安腾2处理器的(其中16颗为待激活状态)HP Integrity Superdome服务器,系统先以硬分区技术划分为两个物理分区,然后每个物理分区用vPARHP Virtual Machine技术划分为三类逻辑分区,分别运行社保交易服务器、BEA Weblogic应用服务器、Oracle数据库服务器,分别运行在HP-UXLinux平台上,统一以HP Workload Management管理。这样,当日常白天医保交易繁忙时,可将数据库服务器分区的计算资源调配到社保交易分区,晚上进行批处理业务的时候再调配;而在月末各分区的业务都繁忙时,以iCOD(按需扩容)或TiCOD(购买待激活CPU若干小时的点卡)的方式,将待激活CPU临时调配到各个分区。这样,用户既获得了足够的计算资源和安全性,但又只需较低的成本,保证了投资回报率。

24.jpg

25b.jpg

实施虚拟应注意什么?

  虚拟技术虽然成熟,但实施起来可不能想当然。那么在虚拟实施过程中应该注意些什么?

  中国人民银行清算总中心是IBM大型主机和p系列服务器的老用户,从1992年前后开始使用大型主机,算来也有14年的历史。清算中心开发部的副总经理贝劲松在谈起实施虚拟技术时,认为尤其应该关注两点。

  首先是实施前要对业务系统、对计算资源的需求有明确了解。贝劲松笑谈,清算总中心在这方面是摸索出来的经验。2002年时,清算总中心曾购买了两台配置81.1GHz Power4处理器的IBM eServer p690服务器,当时按照4/2/2的方式划分为了三个逻辑分区,但通过压力测试发现第一个分区负载较小,反而是第二个分区经常过载,于是将分区调整为2/4/2的配置,解决了这个问题。

  其次是实施前要有充分的测试期。贝劲松认为,像银行这样的自行开发业务系统的行业,相对比较容易了解业务系统对计算资源的压力,但即使这样,也需要进行充分测试,如不具备对等配置测试环境,也应在处理能力稍低的同类硬件平台上进行测试。例如,清算总中心2005年购买了8IBM eServer p570(用于生产)和两路的eServer p570 p550(用于测试),分别按照2/60.5/1.5的配置进行分区,在系统的开发期和测试期,p550上的测试数据有助于他们了解业务系统对计算资源的需求。如果是不自行开发业务系统的行业,就更有必要在近似系统上进行测试,毕竟生产系统的安全性是第一位的。

  除了老用户的经验之谈,笔者认为在实施虚拟技术之前,还应参照左表,决定采用哪种虚拟技术最合适。从表中容易得知,如果是简单的单机应用开发,那么采用应用虚拟技术最合适;如果需要开发Web应用,那么软件虚拟技术才能满足需求。

虚拟并非万能

  或许上面的内容会给读者一个错觉,即虚拟技术是如此优异,如果自己的企业还没有使用,将会在未来的竞争中出于劣势,实际上,仍有许多用户还不需要用到虚拟技术。

  国家气象中心是高性能计算机集群和IBM p系列的老用户,尤其是2004年采购的IBM eServer p655集群,以32001.7GHzPower4+处理器实现了10.31TeraFlops/sLinpack性能,牢牢占据着系统中国高性能计算机的头把交椅,如此高性能的系统,气象中心是如何看待分区或者虚拟机技术呢?

  答案出乎笔者先前的预料,据国家气象中心计算机室主任田浩的介绍,他们并没有采用任何分区和虚拟技术。为什么?原因就在于气象行业应用软件的特殊性上。田浩解释说,气象预报主要采用的短期和中期预报,大都采用将预测区域划分为近似正方的栅格(边长越小预报越精确),然后用各种计算模型(如MM5GRAPES等)进行运算。虽然气象中心集群的性能是国内目前最强大的,系统的满载率也接近60%(7×24×365),但如果是24小时的短期预报,目前一般在34小时内得出结果;而中期预报(10天,30公里)则大概需要56小时,这一速度虽然比以前快很多,但还算不上完美——也就是说,目前的整个系统跑生产系统没问题,但富余的资源并不多。田浩笑说,像气象中心这样对计算资源需求永无止境的行业,恐怕是很难体验到虚拟技术的好处的。不过,田浩同时认为,如果气象中心在业务系统的并行算法上能够取得突破,气象中心未来采用虚拟技术的可能性同样存在。

  除了气象预报和石油物探这样对计算资源需求永无止境的高端领域,低端应用当然也不需要用到虚拟技术,倒不是没这样的需求,而是因为通常前端应用的硬件平台性能还不够好,即使是一个勇于尝试的IT主管,也不会轻易在一台单路至强服务器上用VMware GSX Server虚拟出若干LinuxWindows操作系统,把公司的邮件、Web和文件服务整合到一台机器上。

25.jpg

25.jpg

25.jpg

25.jpg

归类于: Programming — freed @ 2:41 pm 评论(0)
2006年04月13日
自己动手写操作系统
作者:伊梅 本文选自:开放系统世界——赛迪网 2002年10月10日


自由软件社区是一个充满自由和梦想的地方,在10余年的时间里它创造了一个又一个奇迹。然而,这些奇迹的创造者不只是Stallman,也不只是Linus Torvalds,而是活跃在世界各地的不计其数的开发人员。

在使用各种功能强大的自由软件时,我总会对其开发者充满崇敬之情,期盼有朝一日自己也能成为他们中的一员。很多对自由社区充满向往之情的人,虽然也想努力融身于其中,但又不知该怎么做。那么,就请与我们一起从编写一个简单的操作系统开始吧!

我们要做的事情


有人可能担心自己既没有学过计算机原理,也没有学过操作系统原理,更不懂汇编语言,对C语言也一知半解,能写操作系统吗?答案是没问题。我将带大家一步一步完成自己的操作系统。当然如果学一学上述内容再好不过。

首先要明确处理器(也就是CPU)控制着计算机。对PC而言,启动的时候,CPU都处在实模式状态,相当于只是一个Intel 8086处理器。也就是说,即使你现在拥有一个奔腾处理器,它的功能也只能是8086级别。从这一点上来讲,可以使用一些软件把处理器转换到著名的保护模式。只有这样,我们才可以充分利用处理器的强大功能。

编写操作系统开始是对BIOS控制,取出存储在ROM里的程序。BIOS是用来执行POST(Power On Self Test,自检)的。自检是检查计算机的完整性(比如外设是否工作正常、键盘是否连接等)。这一切完成以后,你就会听到PC喇叭发出一声清脆的响声。如果一切正常,BIOS就会选择一个启动设备,并且读取该设备的第一扇区(即启动扇区),然后控制过程就会转移到指定位置。启动设备可能是一个软盘、光盘、硬盘,或者其它所选择的设备。在此我们把软盘作为启动设备。如果我们已经在软盘的启动扇区里写了一些代码,这时它就被执行。因此,我们的目的很明确,就是往软盘的启动扇区写一些程序。

首先使用8086汇编来写一个小程序,然后将其拷贝至软盘的启动扇区。为了实现拷贝,要写一个C程序。最后,使用软盘启动计算机。

需要的工具


● as86:这是一个汇编程序,它负责把写的代码转换成目标文件。

● ld86:这是一个连接器,as86产生的目标代码由它来转换成真正的机器语言。机器语言是8086能够解读的形式。

● GCC:著名的C编程器。因为我们需要写一个C程序将自己的OS转移到软盘中。

● 一张空软盘:它用于存储编写的操作系统,也是启动设备。

● 一台装有Linux的计算机:这台机器可以很旧,386、486都可以。

在大部分标准Linux发行版中都会带有as86和ld86。在我使用的Red Hat 7.3中就包含有这两个工具,并且在默认的情况下,它已经安装在机器里。如果使用的Linux没有这两个工具,可以从网上下载(http://www.cix.co.uk/~mayday/),这两个工具都包含在一个名为bin86的软件包中。此外,有关的文档也可以在网上获得(www.linux.org/docs/ldp/howto/Assembly-HOWTO/as86.html)。

开始工作


使用一个你喜欢的编辑器输入以下内容:


entry start
start:
      mov ax,#0xb800
      mov es,ax
      seg es
      mov [0],#0x41
      seg es
      mov [1],#0x1f
loop1: jmp loop1



这是as86可以读懂的一段汇编程序。第一个句子指明了程序的入口点,声明整个过程从start处开始。第二行指明了start的位置,说明整个程序要从start处开始执行。0xb800是显存的开始地址。#表明其后是一个立即数。执行语句:


mov ax,#oxb800



ax寄存器的值就变为0xb800,这就是显存的地址。下面再将这个值移至es寄存器,es是附加段寄存器。请记住8086有一个分段的体系结构。它的各段寄存器为代码段、数据段、堆栈段和附加段,对应的寄存器名称分别为cs、ds、ss和es。事实上,我们把显存地址送入了附加段,因此,任何送入附加段的东西都会被送到显存中。

要在屏幕上显示字符,就需要向显存中写两个字节。前一个是所要显示字符的ASCⅡ值,第二个字节表示该字符的属性。属性包括字符的前景色、背景色及是否闪烁等等。seg es指明下一个将要执行的指令是指向es段的。所以,我们把值0×41(在ASCⅡ中表示的字符是A)送到显存的第一个字节中。接下来要把字符的属性送到下一个字节当中。在此输入的是0×1f,该属性指的是在蓝色背景下显示白色的字符。因此,如果执行这个程序,就可以在屏幕上得到显示在蓝底上的一个白色的A。接着是一个循环。因为在执行完显示字符的任务后,要么让程序结束,要么使用一个循环使其永远运行下去。把该文件命名为boot.s,然后存盘。

此处显存的概念说得不是很清楚,有必要进一步解释一下。假设屏幕由80列×25行组成,那么第一行就需要160字节,其中一个字节用于表示字符,另外一个字节用于表示字符的属性。如果要在第三行显示某一字符的话,就要跳过显存的第0和1字节(它们是用于显示第1列的),第2和3字节(它们是用于显示第2列的),然后把需要显示字符的ASCⅡ码值入第4字节,把字符的属性写入第5字节。

把程序写至启动扇区


下面写一个C程序,把我的操作系统写入软盘第一扇区。程序内容如下:


#include  /* unistd.h 需要这个文件 */
#include     /* 包含有read和write函数 */
#include 
int main()
{
  char boot_buf[512];
   int floppy_desc, file_desc;
  file_desc = open("./boot", O_RDONLY);
  read(file_desc, boot_buf, 510);
  close(file_desc);
  boot_buf[510] = 0x55;
  boot_buf[511] = 0xaa;
  floppy_desc = open("/dev/fd0", O_RDWR);
  lseek(floppy_desc, 0, SEEK_CUR);
  write(floppy_desc, boot_buf, 512);
  close(floppy_desc);
}



首先,以只读模式打开boot文件,然后在打开文件时把文件描述符复制到file_desc变量中。从文件中读取510个字符,或者读取直到文件结束。在本例中由于文件很小,所以是读取至文件结束。然后关闭文件。

最后4行代码打开软盘驱动设备(一般来说是/dev/fd0)。使用lseek找到文件开始处,然后从缓冲中向软盘写512个字节。

在read、write、open和lseek的帮助页中,可以看到与函数所有有关的参数及其使用方法。程序中有两行比较难懂:


boot_buf[510] = 0x55;
boot_buf[511] = 0xaa;



该信息是用于BIOS的,如果它识别出该设备是一个可启动的设备,那么在第510和511的位置,该值就应该是0×55和0xaa。程序会把文件boot读至名为boot_buf的缓冲中。它要求改变第510和第511字节,然后把boot_buf写至软盘之上。如果执行代码,软盘上的前512字节就包含了启动代码。最后,把文件存为write.c。

编译运行


使用下面的命令把文件变为可执行文件:


as86 boot.s -o boot.o
ld86 -d boot.o -o boot
cc write.c -o write



首先将boot.s文件编译成目标文件boot.o,然后将该文件连接成最终的boot文件。最后C程序编译成可执行的write文件。

插入一个空白软盘,运行以下程序:


./write



重新启动电脑,进行BIOS的界面设置,并且把软盘设为第一个启动的设备。然后插入软盘,电脑从软盘上启动。

启动完成后,在屏幕上可以看到一个字母A(蓝底白字),启动速度很快,几乎是在瞬间完成。这就意味着系统已经从我们制作的软盘上启动了,并且执行了刚才写入启动扇区的程序。现在,它正处在一个无限循环的状态。所以,如果想进入Linux,必需拿掉软盘,并且重启机器。

至此,这个操作系统就算完成了,虽然它没有实现什么功能,但是它已经可以启动机器了。

下一期我将在这个启动扇区程序里加入一些代码,使它可以做一些比较复杂的事情(比如使用BIOS中断、保护模式切换等等)。

 

上一期,我讲述了如何在软盘的启动扇区写一些代码,然后再从软盘启动的过程。制作好一个启动扇区,在切换到保护模式之前,我们还应该知道如何使用BIOS中断。BIOS中断是一些由BIOS提供的、为了使操作系统的创建更容易的低级程序。在本文中,我们将学习处理BIOS的中断。

为什么要用BIOS


BIOS会把启动扇区拷贝至RAM中,并且执行这些代码。除此之外,BIOS还要做很多其它的事情。当一个操作系统刚开始启动时,系统中并没有显卡驱动、软盘驱动等任何驱动程序。因此,启动扇区中不可能包含任何一个驱动程序,我们要采取其它的途径。这个时候,BIOS就可以帮助我们了。BIOS中包含有各种可以使用的程序,包括检测安装的设备、控制打印机、计算内存大小等用于各种目的的程序。这些程序就是所说的BIOS中断。

如何调用BIOS中断


在一般的程序设计语言中,函数的调用是一件非常容易的事情。比如在C语言中,如果有一个名为display的程序,它带有两个参数,其中参数noofchar表示显示的字符数,参数attr表示显示字符的属性。那么要调用它,只需给出程序的名称即可。对于中断的调用,我们使用的是汇编语言中的int指令。

比如,在C语言中要显示一些东西时,使用的指令如下所示:


display(nofchar,attr);



而使用BIOS时,要实现相同功能使用的指令如下:


int 0x10



如何传递参数


在调用BIOS中断之前,我们需要先往寄存器中送一些特定的值。假设要使用BIOS的中断13h,该中断的功能是把数据从软盘传送至内存之中。在调用该中断之前,要先指定拷贝数据的段地址,指定驱动器号、磁道号、扇区号,以及要传送的扇区数等等。然后,就要往相应的寄存器送入相应的值。在进行下面的步骤前,读者有必要对这一点有比较明确地认识。

此外,一个比较重要的事实是同一个中断往往可以实现各种不同的功能。中断所实现的确切功能取决于所选择的功能号,功能号一般都存在ah寄存器之中。比如中断13h可以用于读磁盘、写磁盘等功能,如果把3送入ah寄存器中,那么中断选择的功能就是写磁盘;如果把2送入ah寄存器中,选择的功能则是读磁盘等。

我们要做的事情


这次我们的源代码由两个汇编语言程序和一个C程序组成。第一个汇编文件是引导扇区的代码。在引导扇区中,我们写的代码是要把软盘中第二扇区拷贝至内存段的0×500处(地址是0×5000,即偏移地址为0)。这时我们需要使用BIOS的中断13h。这时启动扇区的代码就会把控制权转移至0×500处。在第二个汇编文件中,代码会使用BIOS中断10h在屏幕上显示一个信息。C程序实现的功能则是把可执行的文件1拷贝至启动扇区,把可执行的文件2拷贝至软盘的第二扇区。

启动扇区代码


使用中断13h,启动扇区把软盘第二扇区里的内容加载至内存的0×5000处(段地址为0×500)。下面的代码是用于实现这一目的的代码,将其保存至文件sbect.s中。


LOC1=0x500
entry start
start:
  mov ax,#LOC1
  mov es,ax
  mov bx,#0
  mov dl,#0
  mov dh,#0
  mov ch,#0
  mov cl,#2
  mov al,#1
  mov ah,#2
  int 0x13
  jmpi 0,#LOC1



上面代码第一行类似于一个宏。接下去的两行则是把值0×500加载至es寄存器中,这是软盘上第二扇区代码将拷贝到的地方(第一扇区是启动扇区)。这时,把段内的偏移设为0。

接下来把驱动器号送入dl寄存器中,其中磁头号送入dl寄存器中,磁道号送入ch寄存器中,扇区号送入cl寄存器中,扇区数送入al寄存器之中。我们想要实现的功能是把扇区2、磁道号为0、驱动器号为0的内容送至段地址0×500处。所有这些参数都和1.44MB的软盘相对应。

把2送入ah寄存器中,是选择了由中断13h提供的相应功能,即实现从软驱转移数据的功能。

最后调用中断13h,并且转至偏移为0的段地址0×500处。

第二个扇区的代码


第二个扇区中的代码如下所示(把这些代码保存至文件sbect2.s之中):


entry start
start:
  mov     ah,#0x03
  xor     bh,bh
  int     0x10

  mov     cx,#26
  mov     bx,#0x0007
  mov     bp,#mymsg
  mov     ax,#0x1301
  int     0x10

loop1:  jmp     loop1
mymsg:
  .byte  13,10
  .ascii “Operating System is Loading......”



上面代码将被加载至段地址为0×500处,并且被执行。在这段代码中,使用了中断10h来获取目前的光标位置,然后显示信息。

从第3行到第5行用于得到目前光标的位置,在此中断10h选用的是功能3。然后,清除了bh寄存器的内容,并把字符串送至ch寄存器中。在bx中,我们送入了页码及显示的属性。此处,我们想要在黑背景上显示白色的字符。然后,把要显示字符的地址送到bp之中,信息由两个字节组成,其值分别为13的10,它们分别对应回车和LF(换行)的ASCⅡ值。接下来是一个由29个字符组成的串;在下面实现的功能是输出字符串然后移动光标;最后是调用中断,然后进入循环。

C程序代码


C程序的源代码如下所示,将其存储为write.c文件。


#include  /* unistd.h needs this */
#include     /* contains read/write */
#include 
int main()
{
  char boot_buf[512];
  int floppy_desc, file_desc;
  file_desc = open(“./bsect”, O_RDONLY);
  read(file_desc, boot_buf, 510);
  close(file_desc);
  boot_buf[510] = 0x55;
  boot_buf[511] = 0xaa;
  floppy_desc = open(“/dev/fd0”, O_RDWR);
  lseek(floppy_desc, 0, SEEK_SET);
  write(floppy_desc, boot_buf, 512);
  file_desc = open(“./sect2”, O_RDONLY);
  read(file_desc, boot_buf, 512);
  close(file_desc);
  lseek(floppy_desc, 512, SEEK_SET);
  write(floppy_desc, boot_buf, 512);
  close(floppy_desc);
}



在上一期中,我曾经介绍过如何操作能启动的软盘。现在这一个过程稍微有点不同,首先把由bsect.s编译出来的可执行文件bsect拷贝至软盘的启动扇区。然后再把由sect2.s产生的可执行文件sect2拷贝至软盘的第二个扇区。

把上述文件置于同一目录之下,然后分别对其进行编译,方法如下所示:


as86 bsect.s -o bsect.o
ld86 -d bsect.o -o bsect



对sect2.s文件重复以上的操作,得出可执行文件sect2。编译write.c,插入软盘后执行write文件,命令如下所示:


cc write.c -o write
./write



下一步我们要做的事情


从软盘启动以后,可以看到显示出来的字符串。这是使用了BIOS中断来完成的。下一期要做的事情是在这个操作系统中实现实模式向保护模式的转换。

发布时间:2005-1-25

 

在上两期中,我向大家讲述了如何使用Linux提供的开发工具在软盘的启动扇区写一些代码,以及如何调用BIOS的问题。现在,这个操作系统已经越来越接近当年Linus Torvalds的那个具有“历史意义”的Linux内核了。因此,要马上把这个系统切换到保护模式之下。

什么是保护模式


自从1969年推出第一个微处理器以来,Intel处理器就在不断地更新换代,从8086、8088、80286,到80386、80486、奔腾、奔腾Ⅱ、奔腾4等,其体系结构也在不断变化。80386以后,提供了一些新的功能,弥补了8086的一些缺陷。这其中包括内存保护、多任务及使用640KB以上的内存等,并仍然保持和8086家族的兼容性。也就是说80386仍然具备了8086和80286的所有功能,但是在功能上有了很大的增强。早期的处理器是工作在实模式之下的,80286以后引入了保护模式,而在80386以后保护模式又进行了很大的改进。在80386中,保护模式为程序员提供了更好的保护,提供了更多的内存。事实上,保护模式的目的不是为了保护程序,而是要保护程序以外的所有程序(包括操作系统)。

简言之,保护模式是处理器的一种最自然的模式。在这种模式下,处理器的所有指令及体系结构的所有特色都是可用的,并且能够达到最高的性能。

保护模式和实模式


从表面上看,保护模式和实模式并没有太大的区别,二者都使用了内存段、中断和设备驱动来处理硬件,但二者有很多不同之处。我们知道,在实模式中内存被划分成段,每个段的大小为64KB,而这样的段地址可以用16位来表示。内存段的处理是通过和段寄存器相关联的内部机制来处理的,这些段寄存器(CS、DS、SS和ES)的内容形成了物理地址的一部分。具体来说,最终的物理地址是由16位的段地址和16位的段内偏移地址组成的。用公式表示为:

物理地址=左移4位的段地址+偏移地址。

在保护模式下,段是通过一系列被称之为“描述符表”的表所定义的。段寄存器存储的是指向这些表的指针。用于定义内存段的表有两种:全局描述符表(GDT)和局部描述符表(LDT)。GDT是一个段描述符数组,其中包含所有应用程序都可以使用的基本描述符。在实模式中,段长是固定的(为64KB),而在保护模式中,段长是可变的,其最大可达4GB。LDT也是段描述符的一个数组。与GDT不同,LDT是一个段,其中存放的是局部的、不需要全局共享的段描述符。每一个操作系统都必须定义一个GDT,而每一个正在运行的任务都会有一个相应的LDT。每一个描述符的长度是8个字节,格式如图3所示。当段寄存器被加载的时候,段基地址就会从相应的表入口获得。描述符的内容会被存储在一个程序员不可见的影像寄存器(shadow register)之中,以便下一次同一个段可以使用该信息而不用每次都到表中提取。物理地址由16位或者32位的偏移加上影像寄存器中的基址组成。实模式和保护模式的不同可以从图1和图2中很清楚地看出来。

此外,还有一个中断描述符表(IDT)。这些中断描述符会告诉处理器到那里可以找到中断处理程序。和实模式一样,每一个中断都有一个入口,但是这些入口的格式却完全不同。因为在切换到保护模式的过程中没有使用到IDT,所以在此就不多做介绍了。

进入保护模式


80386有4个32位控制寄存器,名字分别为CR0、CR1、CR2和CR3。CR1是保留在未来处理器中使用的,在80386中没有定义。CR0包含系统的控制标志,用于控制处理器的操作模式和状态。CR2和CR3是用于控制分页机制的。在此,我们关注的是CR0寄存器的PE位控制,它负责实模式和保护模式之间的切换。当PE=1时,说明处理器运行于保护模式之下,其采用的段机制和前面所述的相应内容对应。如果PE=0,那么处理器就工作在实模式之下。

切换到保护模式,实际就是把PE位置为1。为了把系统切换到保护模式,还要做一些其它的事情。程序必须要对系统的段寄存器和控制寄存器进行初始化。把PE位置1后,还要执行跳转指令。过程简述如下:

1.创建GDT表;

2.通过置PE位为1进入保护模式;

3.执行跳转以清除在实模式下读取的任何指令。

下面使用代码来实现这个切换过程。

需要的东西


◆ 一张空白软盘

◆ NASM编译器

下面是整个程序的源代码:


org 0x07c00; 起始地址是0000:7c00
jmp short begin_boot   ; 跳过其它的数据,跳转到引导程序的开始处
bootmesg db "Our OS boot sector loading ......"
pm_mesg  db "Switching to protected mode ...."
dw 512  ; 每一扇区的字节数
db 1  ; 每一簇的扇区数
dw 1  ; 保留的扇区号
db 2
dw 0x00e0
dw 0x0b40
db 0x0f0
dw 9
dw 18
dw 2  ; 读写扇区号
dw 0  ; 隐藏扇区号
print_mesg :
   mov ah,0x13  ; 使用中断10h的功能13,在屏幕上写一个字符串
   mov al,0x00  ; 决定调用函数后光标所处的位置
   mov bx,0x0007    ; 设置显示属性
   mov cx,0x20  ; 在此字符串长度为32
   mov dx,0x0000    ; 光标的起始行和列
   int 0x10  ; 调用BIOS的中断10h
   ret  ; 返回调用程序
get_key :
   mov ah,0x00
   int 0x16  ; Get_key使用中断16h的功能0,读取下一个字符
   ret
clrscr :
   mov ax,0x0600  ; 使用中断10h的功能6,实现卷屏,如果al=0则清屏
   mov cx,0x0000  ; 清屏
   mov dx,0x174f  ; 卷屏至23,79
   mov bh,0  ; 使用颜色0来填充
   int 0x10  ; 调用10h中断
   ret
begin_boot :
   call clrscr   ; 先清屏
   mov bp,bootmesg  ; 提供串地址
   call print_mesg  ; 输出信息
   call get_key	  ; 等待用户按下任一键
bits 16
   call clrscr  ; 清屏
   mov ax,0xb800  ; 使gs指向显示内存
   mov gs,ax  ; 在实模式下显示一个棕色的A
   mov word [gs:0],0x641  ; 显示
   call get_key  ; 调用Get_key等待用户按下任一键
   mov bp,pm_mesg  ;  设置串指针
   call print_mesg  ; 调用print_mesg子程序
   call get_key  ; 等待按键
   call clrscr  ; 清屏
   cli  ; 关中断
   lgdt[gdtr]  ; 加载GDT
   mov eax,cr0
   or al,0x01  ; 设置保护模式位
   mov cr0,eax  ; 将更改后的字送至控制寄存器中
   jmp codesel:go_pm
bits 32
go_pm :
   mov ax,datasel
   mov ds,ax  ; 初始化ds和es,使其指向数据段
   mov es,ax
   mov ax,videosel  ; 初始化gs,使其指向显示内存
   mov gs,ax
   mov word [gs:0],0x741 ; 在保护模式下显示一个白色的字符A
spin : jmp spin  ; 循环
bits 16
gdtr :
   dw gdt_end-gdt-1  ; gdt的长度
   dd gdt  ; gdt的物理地址
gdt
nullsel equ $-gdt  ; $指向当前位置,所以nullsel = 0h
gdt0  ; 空描述符
   dd 0
   dd 0  ; 所有的段描述符都是64位的
codesel equ $-gdt  ; 这是8h也就是gdt的第二个描述符
code_gdt
   dw 0x0ffff  ; 段描述符的界限是4Gb
   dw 0x0000
   db 0x00
   db 0x09a
   db 0x0cf
   db 0x00
datasel equ $-gdt
data_gdt
   dw 0x0ffff
   dw 0x0000
   db 0x00
   db 0x092
   db 0x0cf
   db 0x00
videosel equ $-gdt
   dw 3999
   dw 0x8000  ; 基址是0xb8000
   db 0x0b
   db 0x92
   db 0x00
   db 0x00
gdt_end
times 510-($-$$)  db 0
dw 0x0aa55



把上面的代码存在一个名为abc.asm的文件之中,使用命令nasm abc.asm,将得出一个名为abc的文件。然后插入软盘,输入命令:dd if=abc of=/dev/fd0。该命令将把文件abc写入到软盘的第一扇区之中。然后重新启动系统,就会看到如下的信息:


*Our os booting................
* A (棕色)
* Switching to protected mode....
* A (白色)



对代码的解释


上面给出了所有的代码,下面我对上述代码做一些解释。

◆ 使用的函数

下面是代码中一些函数的说明:

print_mesg 该子程序使用了BIOS中断10h的功能13h,即向屏幕写一字符串。属性控制是通过向一些寄存器中送入不同的值来实现的。中断10h是用于各种字符串操作,我们把子功能号13h送到ah中,用于指明要打印一个字符串。al寄存器中的0说明了光标返回的起始位置,0表示调用函数后光标返回到下一行的行首。如果al为1则表示光标位于最后一个字符处。

显存被分成了几页,在同一时刻只能显示其中的一页。bh指明的是页号;bl则指明要显示字符的颜色;cx指明要显示字符串的长度;dx指明光标的位置(即起始的行和列)。所有相关寄存器初始化完成以后,就可以调用BIOS中断10h了。

get_key 使用中断16h的子功能00h,从屏幕得到下一个字符。

clrscr 该函数使用了中断10h的另外一个子功能06h,用于输出开始前清屏。初始化时给al中送入0。寄存器cx和dx指明要清屏的屏幕范围,在本例中是整个屏幕。寄存器bh指明屏幕填充的颜色,在本例中是黑色。

◆ 其它内容

程序一开始是一条短跳转指令,跳到begin_boot处。在实模式下,在此打印一个棕色的“A”,并且设置一个GDT。切换到保护模式,并且打印一个白色的“A”。这两种模式使用的都是自己的寻址方法。

在实模式下,使用段寄存器gs指示显存位置,我们使用的是CGA显卡(默认基址是0xb8000)。在代码中是不是漏了一个0呢?没有,因为实模式下会提供一个附加的0。这种方式也被80386继承下来了。A的ASCⅡ是0×41,0×06指明了需要一个棕色的字符。该显示会一直持续直至按下任意键。下面要在屏幕上显示一句话,告诉使用者下面马上要进入保护模式了。

启动到保护模式,在进行切换时不希望此时有中断的影响,故要关闭所有的中断(使用cli来实现)。然后对GDT初始化。在整个切换过程中,对4个描述符进行了初始化。这些描述符对代码段(code_gdt)、数据和堆栈段(data_gdt),以及为了访问显存而对显示段进行初始化。此外,还会对一个空描述符进行初始化。

GDT的基址要加载至GDTR系统寄存器之中。gdtr段的第一个字加载的是GDT的大小,在下一个双字中则加载的是基址。然后,lgdt指令把把gdt段加载至GDTR寄存器中。现在已经做好了切换到保护模式前的所有准备。最后一件事情就是把CR0寄存器的PE位置1。不过,即使这样还没有处于保护模式状态之下。

设置了PE位以后,还需要通过执行JMP指令来清除处理器指令预取队列。在80386中,使用指令前总是先将其从内存中取出,并且进行解码和寻址。然而,当进入保护模式以后,预取指令信息(它还处于实地址模式)就无效了。使用JMP指令的目的就是强迫处理器放弃无效的信息。

现在,已经在保护模式下了。那么,如何检测是在保护模式状态之下呢?让我们来看一看屏幕上这个白色的字母A。在这里,使用了数据段选择符(datase1)对数据段和附加段进行了初始化,使用显示段选择符(videose1)对gs进行了初始化。告示的字符“A”其ASCⅡ值和属性位于[gs:0000]处,也就是b8000:0000处。循环语句使得该字符一直在屏幕上显示,直至重新启动系统。

下一步要做的事


现在,这个操作系统已经工作在保护模式下了,但是实际上它并不实现什么具体的功能。你可以在这个基础上为它增加各种操作系统所具有的功能。我们自己动手写操作系统到此也就告一段落。

发布时间:2005-1-25
归类于: Programming — freed @ 3:48 pm 评论(0)
如何把图片存入到数据库(ZZ)

选择自 capsicum29 的 Blog

如果你不希望别人轻易地在其他站点引用你的图片,你可以把图片存入在数据库.下面介绍的是主要有二个:1如何把图片上传到数据库,2如何显示数据库并加上验证.
     首先我们先来熟悉一下将要使用的对象方法。我们用来获取上一个页面传递过来的数据一般是使用request对象。同样的,我们也可以使用request对象 来获取上传上来的文件数据,使用的方法是request.binaryread()。而我们要从 数据库中读出来图片的数据显示到网页上面要用到的方法是: 
request.binarywrite()。在我们得到了图片的数据,要保存到数据库中的时候, 不可以直接使用insert语句对数据库进行操作,而是要使用ado的 appendchunk方法,同样的,读出数据库中的图片数据,要使用getchunk方 法。各个方法的具体语法如下: 

1) request.binaryread语法: 
variant = request.binaryread(count) 
参数 :variant       返回值保存着从客户端读取到数据。 
        count       指明要从客户端读取的数据量大小,这个值小于或者等于使用方法request.totalbytes得到的数据量。 

2)request.binarywrite语法: 
request.binarywrite data 
参数: data         要写入到客户端浏览器中的数据包。 

3) request.totalbytes语法: 
variant = request.totalbytes 
参数 :variant       返回从客户端读取到数据量的字节数。 

4) appendchunk语法 
   将数据追加到大型文本、二进制数据 field 或 parameter 对象。 
object.appendchunk data 
参数: 
object field 或 parameter 对象
data 变体型,包含追加到对象中的数据。 
说明 
使用 field 或 parameter 对象的 appendchunk 方法可将长二进制或字符数据填写到对象中。在系统内存有限的情况下,可以使用 appendchunk 方法对长整型值进行部分而非全部的操作。 

5) getchunk语法 
返回大型文本或二进制数据 field 对象的全部或部分内容 。
variable = field.getchunk( size ) 
返回值 
返回变体型。 
参数 :size            长整型表达式,等于所要检索的字节或字符数。
说明:
  使用 field 对象的 getchunk 方法检索其部分或全部长二进制或字符数据。在系统内存有限的情况下,可使用 getchunk 方法处理部分而非全部的长整型值。 
getchunk 调用返回的数据将赋给“变量”。如果 size 大于剩余的数据,则 getchunk 仅返回剩余的数据而无需用空白填充“变量”。如果字段为空,则 
getchunk 方法返回 null。 每个后续的 getchunk 调用将检索从前一次 getchunk 调用停止处开始的数 据。但是,如果从一个字段检索数据然后在当前记录中设置或读取另一个字段的值,ado 将认为已从第一个字段中检索出数据。如果在第一个字段上再次调用 getchunk 方法,ado 将把调用解释为新的 getchunk 操作并从记录的起始处开始读取。如果其他 recordset 对象不是首个 recordset 对象的副本,则 访问其中的字段不会破坏 getchunk 操作。 如果 field 对象的 attributes 属性中的 adfldlong 位设置为 true,则可以对该字段使用 getchunk 方法。 
如果在 field 对象上使用 getchunk 方法时没有当前记录,将产生错误 3021 (无当前记录)。 

接下来,我们就要来设计我们的数据库了,作为测试我们的数据库结构如下(access): 
字段名称    类型    描述 
  id    自动编号   主键值
img ole对象   用来保存图片数据  
现在开始正式编写我们的纯asp代码上传部分了,首先,我们有一个提供给用户的上传界面,可以让用户选择要上传的图片。代码如下
(upload.htm): 
<html> 
<body> 
<center> 
   <form name="mainform" enctype="multipart/form-data" action="process.asp" method=post> 
    <input type=file name=upfile><br> 
   <input type=submit name=ok value="ok"> 
   </form> 
</center> 
</body> 
</html> 

  注意代码中<<enctype="multipart/form-data" >>,一定要在form中有这个属性,否则,将无法得到上传上来的数据。 
接下来,我们要在process.asp中对从浏览器中获取的数据进行必要的处 理,因为我们在process.asp中获取到的数据不仅仅包含了我们想要的上传上来的图片的数据,也包含了其他的无用的信息,我们需要剔除冗余数据,并将处理过的图片数据保存到数据库中,这里我们以access为例。具体代码如下(process.asp): 
<% 
response.buffer=true 
formsize=request.totalbytes 
formdata=request.binaryread(formsize) 
bncrlf=chrb(13) & chrb(10) 
divider=leftb(formdata,clng(instrb(formdata,bncrlf))-1) 
datastart=instrb(formdata,bncrlf & bncrlf)+4 
dataend=instrb(datastart+1,formdata,divider)-datastart 
mydata=midb(formdata,datastart,dataend) 
set conngraph=server.createobject("adodb.connection") 
conngraph.connectionstring="Provider=microsoft.jet.oledb.4.0;data source="& server.mappath("images.mdb") 
conngraph.open 
set rec=server.createobject("adodb.recordset") 
rec.open "select * from [images] where id is null",conngraph,1,3 
rec.addnew 
rec("img").appendchunk mydata 
rec.update 
rec.close 
set rec=nothing 
set conngraph=nothing 
%> 

  好了,这下我们就把上传来的图片保存到了名为images.mdb的数据库中 了,剩下的工作就是要将数据库中的图片数据显示到网页上面了。一般在html中,显示图片都是使用<img>标签,也就是<img src="图片路径">,但 是我们的图片是保存到了数据库中,“图片路径”是什么呢?呵呵,其实这个 src属性除了指定路径外,也可以这样使用哦: 
<img src="showimg.asp?id=xxx"> 

  所以,我们所要做的就是在showimg.asp中从数据库中读出来符合条件的 数据,并返回到src属性中就可以了,具体代码如下(showimg.asp):
<% 
fromwhere=lcase(request.ServerVariables("SERVER_NAME"))      
if Instr(fromwhere,"arhwen.com")=0 then                    ’判断是否从本站访问,如果不是就停止下面的执行.这样就可以防止盗链了.
response.end
end if           

set conngraph=server.createobject("adodb.connection") 
conngraph.connectionstring="provider=microsoft.jet.oledb.4.0;data source="& server.mappath("images.mdb")
conngraph.open 
set rec=server.createobject("adodb.recordset") 
strsql="select img from images where id=" & trim(request("id")) 
rec.open strsql,conngraph,1,1 
response.contenttype = "image/*" 
response.binarywrite rec("img").getchunk(7500000) 
rec.close 
set rec=nothing 
set conngraph=nothing 
%> 

  注意在输出到浏览器之前一定要指定response.contenttype = "image/*",以便正常显示图片。 
  通过这样的处理,相信要盗链你的图片就不容易了.

如果你不希望别人轻易地在其他站点引用你的图片,你可以把图片存入在数据库.下面介绍的是主要有二个:1如何把图片上传到数据库,2如何显示数据库并加上验证.
     首先我们先来熟悉一下将要使用的对象方法。我们用来获取上一个页面传递过来的数据一般是使用request对象。同样的,我们也可以使用request对象 来获取上传上来的文件数据,使用的方法是request.binaryread()。而我们要从 数据库中读出来图片的数据显示到网页上面要用到的方法是: 
request.binarywrite()。在我们得到了图片的数据,要保存到数据库中的时候, 不可以直接使用insert语句对数据库进行操作,而是要使用ado的 appendchunk方法,同样的,读出数据库中的图片数据,要使用getchunk方 法。各个方法的具体语法如下: 

1) request.binaryread语法: 
variant = request.binaryread(count) 
参数 :variant       返回值保存着从客户端读取到数据。 
        count       指明要从客户端读取的数据量大小,这个值小于或者等于使用方法request.totalbytes得到的数据量。 

2)request.binarywrite语法: 
request.binarywrite data 
参数: data         要写入到客户端浏览器中的数据包。 

3) request.totalbytes语法: 
variant = request.totalbytes 
参数 :variant       返回从客户端读取到数据量的字节数。 

4) appendchunk语法 
   将数据追加到大型文本、二进制数据 field 或 parameter 对象。 
object.appendchunk data 
参数: 
object field 或 parameter 对象
data 变体型,包含追加到对象中的数据。 
说明 
使用 field 或 parameter 对象的 appendchunk 方法可将长二进制或字符数据填写到对象中。在系统内存有限的情况下,可以使用 appendchunk 方法对长整型值进行部分而非全部的操作。 

5) getchunk语法 
返回大型文本或二进制数据 field 对象的全部或部分内容 。
variable = field.getchunk( size ) 
返回值 
返回变体型。 
参数 :size            长整型表达式,等于所要检索的字节或字符数。
说明:
  使用 field 对象的 getchunk 方法检索其部分或全部长二进制或字符数据。在系统内存有限的情况下,可使用 getchunk 方法处理部分而非全部的长整型值。 
getchunk 调用返回的数据将赋给“变量”。如果 size 大于剩余的数据,则 getchunk 仅返回剩余的数据而无需用空白填充“变量”。如果字段为空,则 
getchunk 方法返回 null。 每个后续的 getchunk 调用将检索从前一次 getchunk 调用停止处开始的数 据。但是,如果从一个字段检索数据然后在当前记录中设置或读取另一个字段的值,ado 将认为已从第一个字段中检索出数据。如果在第一个字段上再次调用 getchunk 方法,ado 将把调用解释为新的 getchunk 操作并从记录的起始处开始读取。如果其他 recordset 对象不是首个 recordset 对象的副本,则 访问其中的字段不会破坏 getchunk 操作。 如果 field 对象的 attributes 属性中的 adfldlong 位设置为 true,则可以对该字段使用 getchunk 方法。 
如果在 field 对象上使用 getchunk 方法时没有当前记录,将产生错误 3021 (无当前记录)。 

接下来,我们就要来设计我们的数据库了,作为测试我们的数据库结构如下(access): 
字段名称    类型    描述 
  id    自动编号   主键值
img ole对象   用来保存图片数据  
现在开始正式编写我们的纯asp代码上传部分了,首先,我们有一个提供给用户的上传界面,可以让用户选择要上传的图片。代码如下
(upload.htm): 
<html> 
<body> 
<center> 
   <form name="mainform" enctype="multipart/form-data" action="process.asp" method=post> 
    <input type=file name=upfile><br> 
   <input type=submit name=ok value="ok"> 
   </form> 
</center> 
</body> 
</html> 

  注意代码中<<enctype="multipart/form-data" >>,一定要在form中有这个属性,否则,将无法得到上传上来的数据。 
接下来,我们要在process.asp中对从浏览器中获取的数据进行必要的处 理,因为我们在process.asp中获取到的数据不仅仅包含了我们想要的上传上来的图片的数据,也包含了其他的无用的信息,我们需要剔除冗余数据,并将处理过的图片数据保存到数据库中,这里我们以access为例。具体代码如下(process.asp): 
<% 
response.buffer=true 
formsize=request.totalbytes 
formdata=request.binaryread(formsize) 
bncrlf=chrb(13) & chrb(10) 
divider=leftb(formdata,clng(instrb(formdata,bncrlf))-1) 
datastart=instrb(formdata,bncrlf & bncrlf)+4 
dataend=instrb(datastart+1,formdata,divider)-datastart 
mydata=midb(formdata,datastart,dataend) 
set conngraph=server.createobject("adodb.connection") 
conngraph.connectionstring="Provider=microsoft.jet.oledb.4.0;data source="& server.mappath("images.mdb") 
conngraph.open 
set rec=server.createobject("adodb.recordset") 
rec.open "select * from [images] where id is null",conngraph,1,3 
rec.addnew 
rec("img").appendchunk mydata 
rec.update 
rec.close 
set rec=nothing 
set conngraph=nothing 
%> 

  好了,这下我们就把上传来的图片保存到了名为images.mdb的数据库中 了,剩下的工作就是要将数据库中的图片数据显示到网页上面了。一般在html中,显示图片都是使用<img>标签,也就是<img src="图片路径">,但 是我们的图片是保存到了数据库中,“图片路径”是什么呢?呵呵,其实这个 src属性除了指定路径外,也可以这样使用哦: 
<img src="showimg.asp?id=xxx"> 

  所以,我们所要做的就是在showimg.asp中从数据库中读出来符合条件的 数据,并返回到src属性中就可以了,具体代码如下(showimg.asp):
<% 
fromwhere=lcase(request.ServerVariables("SERVER_NAME"))      
if Instr(fromwhere,"arhwen.com")=0 then                    ’判断是否从本站访问,如果不是就停止下面的执行.这样就可以防止盗链了.
response.end
end if           

set conngraph=server.createobject("adodb.connection") 
conngraph.connectionstring="provider=microsoft.jet.oledb.4.0;data source="& server.mappath("images.mdb")
conngraph.open 
set rec=server.createobject("adodb.recordset") 
strsql="select img from images where id=" & trim(request("id")) 
rec.open strsql,conngraph,1,1 
response.contenttype = "image/*" 
response.binarywrite rec("img").getchunk(7500000) 
rec.close 
set rec=nothing 
set conngraph=nothing 
%> 

  注意在输出到浏览器之前一定要指定response.contenttype = "image/*",以便正常显示图片。 
  通过这样的处理,相信要盗链你的图片就不容易了.

作者Blog:http://blog.csdn.net/capsicum29/
归类于: Programming — freed @ 2:38 pm 评论(0)
如何编写异常安全的C++代码(ZZ)

 选择自 wingfiring 的 Blog

    关于C++中异常的争论何其多也,但往往是一些不合事实的误解。异常曾经是一个难以用好的语言特性,幸运的是,随着C++社区经验的积累,今天我们已经有足够的知识轻松编写异常安全的代码了,而且编写异常安全的代码一般也不会对性能造成影响。

    使用异常还是返回错误码?这是个争论不休的话题。大家一定听说过这样的说法:只有在真正异常的时候,才使用异常。那什么是“真正异常的时候”?在回答这个问题以前,让我们先看一看程序设计中的不变式原理。
    对象就是属性聚合加方法,如何判定一个对象的属性聚合是不是处于逻辑上正确的状态呢?这可以通过一系列的断言,最后下一个结论说:这个对象的属性聚合逻辑上是正确的或者是有问题的。这些断言就是衡量对象属性聚合对错的不变式。
    我们通常在函数调用中,实施不变式的检查。不变式分为三类:前条件,后条件和不变式。前条件是指在函数调用之前,必须满足的逻辑条件,后条件是函数调用后必须满足的逻辑条件,不变式则是整个函数执行中都必须满足的条件。在我们的讨论中,不变式既是前条件又是后条件。前条件是必须满足的,如果不满足,那就是程序逻辑错误,后条件则不一定。现在,我们可以用不变式来严格定义异常状况了:满足前条件,但是无法满足后条件,即为异常状况。当且仅当发生异常状况时,才抛出异常。
    关于何时抛出异常的回答中,并不排斥返回值报告错误,而且这两者是正交的。然而,从我们经验上来说,完全可以在这两者中加以选择,这又是为什么呢?事实上,当我们做出这种选择时,必然意味着接口语意的改变,在不改变接口的情况下,其实是无法选择的(试试看,用返回值处理构造函数中的错误)。通过不变式区别出正常和异常状况,还可以更好地提炼接口。
    对于异常安全的评定,可分为三个级别:基本保证、强保证和不会失败。
基本保证:确保出现异常时程序(对象)处于未知但有效的状态。所谓有效,即对象的不变式检查全部通过。
强保证:确保操作的事务性,要么成功,程序处于目标状态,要么不发生改变。
不会失败:对于大多数函数来说,这是很难保证的。对于C++程序,至少析构函数、释放函数和swap函数要确保不会失败,这是编写异常安全代码的基础。
    首先从异常情况下资源管理的问题开始.很多人可能都这么干过:
    Type* obj = new Type;
    try{  do_something…}
    catch(…){ delete obj; throw;}

    不要这么做!这么做只会使你的代码看上去混乱,而且会降低效率,这也是一直以来异常名声不大好的原因之一. 请借助于RAII技术来完成这样的工作:
   
auto_ptr<Type> obj_ptr(new Type);
    do_something…

    这样的代码简洁、安全而且无损于效率。当你不关心或是无法处理异常时,请不要试图捕获它。并非使用try…catch才能编写异常安全的代码,大部分异常安全的代码都不需要try…catch。我承认,现实世界并非总是如上述的例子那样简单,但是这个例子确实可以代表很多异常安全代码的做法。在这个例子中,boost::scoped_ptr是auto_ptr一个更适合的替代品。
    现在来考虑这样一个构造函数:
    Type() : m_a(new TypeA), m_b(new TypeB){}
    假设成员变量m_a和m_b是原始的指针类型,并且和Type内的申明顺序一致。这样的代码是不安全的,它存在资源泄漏问题,构造函数的失败回滚机制无法应对这样的问题。如果new TypeB抛出异常,new TypeA返回的资源是得不到释放机会的.曾经,很多人用这样的方法避免异常:
   
Type() : m_a(NULL), m_b(NULL){
        auto_ptr<TypeA> tmp_a(new TypeA);
        auto_ptr<TypeB> tmp_b(new TypeB);
        m_a = tmp_a.release();
        m_b = tmp_b.release();
    }

当然,这样的方法确实是能够实现异常安全的代码的,而且其中实现思想将是非常重要的,在如何实现强保证的异常安全代码中会采用这种思想.然而这种做法不够彻底,至少析构函数还是要手动完成的。我们仍然可以借助RAII技术,把这件事做得更为彻底:shared_ptr<TypeA> m_a; shared_ptr<TypeB> m_b;这样,我们就可以轻而易举地写出异常安全的代码:
    Type() : m_a(new TypeA), m_b(new TypeB){}
如果你觉得shared_ptr的性能不能满足要求,可以编写一个接口类似scoped_ptr的智能指针类,在析构函数中释放资源即可。如果类设计成不可复制的,也可以直接用scoped_ptr。强烈建议不要把auto_ptr作为数据成员使用,scoped_ptr虽然名字不大好,但是至少很安全而且不会导致混乱。
    RAII技术并不仅仅用于上述例子中,所有必须成对出现的操作都可以通过这一技术完成而不必try…catch.下面的代码也是常见的:
   
a_lock.lock();
    try{ …} catch(…) {a_lock.unlock();throw;}
    a_lock.unlock();

可以这样解决,先提供一个成对操作的辅助类:
   
struct scoped_lock{
        explicit scoped_lock(Lock& lock) : m_l(lock){m_l.lock();}
        ~scoped_lock(){m_l.unlock();}
    private: 
        Lock& m_l;
    };

然后,代码只需这样写:
   
scoped_lock guard(a_lock);
    do_something…

清晰而优雅!继续考察这个例子,假设我们并不需要成对操作, 显然,修改scoped_lock构造函数即可解决问题。然而,往往方法名称和参数也不是那么固定的,怎么办?可以借助这样一个辅助类:
   
template<typename FEnd, typename FBegin>
    struct pair_guard{
        pair_guard(FEnd fe, FBegin fb) : m_fe(fe) {if (fb) fb();}
        ~pair_guard(){m_fe();}
    private:
        FEnd m_fe;
        …//禁止复制
    };
    typedef pair_guard<function<void () > , function<void()> > simple_pair_guard;

好了,借助boost库,我们可以这样来编写代码了:
   
simple_pair_guard guard(bind(&Lock::unlock, a_lock), bind(&Lock::lock, a_lock) );
    do_something…

我承认,这样的代码不如前面的简洁和容易理解,但是它更灵活,无论函数名称是什么,都可以拿来结对。我们可以加强对bind的运用,结合占位符和reference_wrapper,就可以处理函数参数、动态绑定变量。所有我们在catch内外的相同工作,交给pair_guard去完成即可。
    考察前面的几个例子,也许你已经发现了,所谓异常安全的代码,竟然就是如何避免try…catch的代码,这和直觉似乎是违背的。有些时候,事情就是如此违背直觉。异常是无处不在的,当你不需要关心异常或者无法处理异常的时候,就应该避免捕获异常。除非你打算捕获所有异常,否则,请务必把未处理的异常再次抛出。try…catch的方式固然能够写出异常安全的代码,但是那样的代码无论是清晰性和效率都是难以忍受的,而这正是很多人抨击C++异常的理由。在C++的世界,就应该按照C++的法则来行事。
    如果按照上述的原则行事,能够实现基本保证了吗?诚恳地说,基础设施有了,但技巧上还不够,让我们继续分析不够的部分。
    对于一个方法常规的执行过程,我们在方法内部可能需要多次修改对象状态,在方法执行的中途,对象是可能处于非法状态的(非法状态 != 未知状态),如果此时发生异常,对象将变得无效。利用前述的手段,在pair_guard的析构中修复对象是可行的,但缺乏效率,代码将变得复杂。最好的办法是……是避免这么作,这么说有点不厚道,但并非毫无道理。当对象处于非法状态时,意味着此时此刻对象不能安全重入、不能共享。现实一点的做法是:
    a.每一次修改对象,都确保对象处于合法状态
    b.或者当对象处于非法状态时,所有操作决不会失败。
在接下来的强保证的讨论中细述如何做到这两点。

    强保证是事务性的,这个事务性和数据库的事务性有区别,也有共通性。实现强保证的原则做法是:在可能失败的过程中计算出对象的目标状态,但是不修改对象,在决不失败的过程中,把对象替换到目标状态。考察一个不安全的字符串赋值方法:
string& operator=(const string& rsh){
    if (this != &rsh){
        myalloc locked_pool(m_data);
        locked_pool.deallocate(m_data);
        if (rsh.empty())
        m_data = NULL;
        else{
        m_data = locked_pool.allocate(rsh.size() + 1);
        never_failed_copy(m_data, rsh.m_data, rsh.size() + 1);
        }
    }
    return *this;
    }

locked_pool是为了锁定内存页。为了讨论的简单起见,我们假设只有locked_pool构造函数和allocate是可能抛出异常的,那么这段代码连基本保证也没有做到。若allocate失败,则m_data取值将是非法的。参考上面的b条目,我们可以这样修改代码:
myalloc locked_pool(m_data);
    locked_pool.deallocate(m_data);   //进入非法状态
    m_data = NULL;            //立刻再次回到合法状态,且不会失败
    if(!rsh.empty()){
    m_data = locked_pool.allocate(rsh.size() + 1);
    never_failed_memcopy(m_data, rsh.m_data, rsh.size() + 1);
    }

现在,如果locked_pool失败,对象不发生改变。如果allocate失败,对象是一个空字符串,这既不是初始状态,也不是我们预期的目标状态,但它是一个合法状态。我们阐明了实现基本保证所需要的技巧部分,结合前述的基础设施(RAII的运用),完全可以实现基本保证了…哦,其实还是有一点疏漏,不过,那就留到最后吧。
   继续,让上面的代码实现强保证:
myalloc locked_pool(m_data);
    char* tmp = NULL;
    if(!rsh.empty()){
    tmp = locked_pool.allocate(rsh.size() + 1);
    never_failed_memcopy(tmp, rsh.m_data, rsh.size() + 1); //先生成目标状态
    }
    swap(tmp, m_data);       //对象安全进入目标状态
    m_alloc.deallocate(tmp);    //释放原有资源

强保证的代码多使用了一个局部变量tmp,先计算出目标状态放在tmp中,然后在安全进入目标状态,这个过程我们并没有损失什么东西(代码清晰性,性能等等)。看上去,实现强保证并不比基本保证困难多少,一般而言,也确实如此。不过,别太自信,举一种典型的很难实现强保证的例子,对于区间操作的强保证:
   
for (itr = range.begin(); itr != range.end(); ++itr){
    itr->do_something();
    }

如果某个do_something失败了,range将处于什么状态?这段代码仍然做到了基本保证,但不是强保证的,根据实现强保证的基本原则,我们可以这么做:
   
tmp = range;
    for (itr = tmp.begin(); itr != tmp.end(); ++itr){
    itr->do_something();
    }
    swap(tmp, range);

似乎很简单啊!呵呵,这样的做法并非不可取,只是有时候行不通。因为我们额外付出了性能的代价,而且,这个代价可能很大。无论如何,我们阐述了实现强保证的方法,怎么取舍则由您决定了。

    接下来讨论最后一种异常安全保证:不会失败。
    通常,我们并不需要这么强的安全保证,但是我们至少必须保证三类过程不会失败:析构函数,释放类函数,swap。析构和释放函数不会失败,这是RAII技术有效的基石,swap不会失败,是为了“在决不失败的过程中,把对象替换到目标状态”。我们前面的所有讨论都是建立在这三类过程不会失败的基础上的,在这里,弥补了上面的那个疏漏。
    一般而言,语言内部类型的赋值、取地址等运算是不会发生异常的,上述三类过程逻辑上也是不会发生异常的。内部运算中,除法运算可能抛出异常。但是地址访问错通常是一种错误,而不是异常,我们本应该在前条件检查中就发现的这一点的。所有不会发生异常操作的简单累加,仍然不会导致异常。

好了,现在我们可以总结一下编写异常安全代码的几条准则了:
1.只在应该使用异常的地方抛出异常
2.如果不知道如何处理异常,请不要捕获(截留)异常。
3.充分使用RAII,旁路异常。
4.努力实现强保证,至少实现基本保证。
5.确保析构函数、释放类函数和swap不会失败。

另外,还有一些语言细节问题,因为和这个主题有关也一并列出:
1.不要这样抛出异常:throw new exception;这将导致内存泄漏。
2.自定义类型,应该捕获异常的引用类型:catch(exception& e)或catch(const exception& e)。
3.不要使用异常规范,即使是空异常规范。编译器并不保证只抛出异常规范允许的异常,更多内容请参考相关书籍。

作者Blog:http://blog.csdn.net/wingfiring/
归类于: Programming — freed @ 2:31 pm 评论(0)
2006年04月12日
怎样成为一个Flash Lite Developer (开发篇)–ZZ

作者:luar

原文链接:http://www.luar.com.hk/flashbook/archives/001228.php

要開發Flash Lite內容,大部分人頭痛是那Flash 4語法,對於資深Flash開發者來說,Flash 4不難寫,程式設計美麗的地方,就是同一個需要,有很有多不同寫法,窮則變,變則通。相反,在手機上跑的東西,效能和記憶體佔用才是最大困難所在,往往就是要開發者用智能去克服效能的問題。所以,Flash 4語法是門外的人看以為的問題,克服效能才是平日Flash Lite開發者奮鬥的目標。

好了,廢話說完。Flash Lite ActionScript是怎樣?就是Flash 4 ActionScript、編譯器幫助下一些Flash 5指令、手機屬性和FSCommand2。

Flash 4 ActionScript

包含以下東西:

注意:不支援startDrag, stopDrag, _dropTarget, soundBufTime, _url和String()轉換。

 

Flash 5 Object

在Compiler幫助下,有一些Flash 5指令可以用,它們在編譯時,轉為Flash 4語法,包括:

 

手機屬性和FSCommand2

這些都是一些取得手機資料,和控制手機(例如震動、發SMS等)的指令,如果平常用Flash Lite開發遊戲,比較常用的有:

 

由Flash 4 Port到Flash Lite

開始編程Flash Lite時,往往由以前的Flash 4東西開始,例如將以前的東西改為Flash Lite版,在這些轉移過程中,要注意的地方:

归类于: Programming — freed @ 5:00 pm 评论(0)
怎样成为一个Flash Lite Developer (工具篇)–ZZ

作者:luar

原文链接:http://www.luar.com.hk/flashbook/archives/001227.php

功欲善其事,必先利其器,首先要有一部支援Flash Lite手機,目前香港普遍流行的有Nokia 7610和新的6680,或者3G機Sony Ericsson Z800i,不過支援Flash Lite手機仍然偏貴,如果自己目前已經有一部手機,就沒有必要買一部新機。

買支援Flash Lite手機

因此,普遍Flash Lite Developer都會去找二手的Nokia N-Gage,它是支援Flash Lite手機中最便宜(因為不受歡迎,跌價太厲害吧)。要找二手的N-Gage,可以去深水埗鴨寮街,路邊小販檔,約HK$1050-1200,一手價目前是HK$1600-1700,所以這些手機收買佬食水太深,N-Gage Trade-in價約HK$600而己!所以我建議上Yahoo! Bid,一般HK$800可以買到。

N-Gage分舊版(Classic)和QD版,兩者在一二手價錢都分別不大,功能上有什麼分別?當然不是關心QD版抽了MP3和FM功能。最主要留意跟PC連接能力和方便程度。QD版沒有USB Data Cable,一定要用藍牙,所以又要買多一個藍牙收發器(HK$70-140),Classic版用USB Data Cable,一連上電腦,就像多了一個Harddisk,直接複製File到手機的MMC Card裡。如果有MMC讀卡器,也可以複製File,不過Classic版要拆殼拆電池才能拿出卡,QD版MMC卡在機旁,可以直接抽出。

藍牙收發器始終要買,因為有秘技用藍牙連PC上網,省了GPRS費用,如果開發跟數據連接的Flash內容,測試時可以放心一點,嘻。

找舊Simcard

手機要開啟,需要Simcard,沒有理由放棄目前手機插入N-Gage,N-Gage講電話時實在太異相,怎可以出街見人?!要找一張舊Simcard不難,因為攜號碼轉台很容易,應該找到舊Simcard吧。

下載Flash Lite Player

除了Sony Ericsson Z800i內置外,Nokia S60系列機都沒有,要到Macromedia網站買,購買時,地區選美國,Asia Pacific沒有Flash Lite賣,輸入手機IMEI(按*#06#看),信用卡或PayPal付款後,可以立即下載。

安裝CDK或等Flash Pro 8

Flash Lite 1.1 Content Development Kits(CDK)
是一些組件、範例、開發指引和ActionScript的教學PDF、Flash Lite PC上陽春模擬Player等。Flash Pro 8有增強Flash Lite開發內容的功能,等一下,所以目前下載CDK是必須。


附:

Flash Lite 2.0

 

[概念]Flash Lite 2.0 是Flash Player专为非个人电脑类电子设备推出的新版本,将为全球的手机产品和消费电子品提供更加丰富的用户界面。Flash Player SDK 7 是Flash Player专为消费电子产品优化的版本,它使消费电子产品制造商、系统集成商和浏览器提供商等,能够通过借助众多网站丰富的Flash内容,创造出更具影响力的,具有全面网络浏览能力的产品与服务。

 

[特点]Flash Lite 2.0完全基于Flash 7的标准,这意味着在Flash的PC开发平台上可以开发移动设备上的应用。它不仅可以支持动态的XML数据,能够加载和解析外部XML数据,而且在数据传递(Persistent Data)方面,它可以支持例如参数设置,最高分,用户名等等。

归类于: Programming — freed @ 4:59 pm 评论(0)