Posted: January 18, 2009 at 2:20 am | Tags: brew
我们PC的Windows操作系统功能是如此的强大,以至于我们可以获得任何一个我们想要的程序,任何一个人都可以为Windows开发应用程序,或者给自己,或者为他人。而这一切源于操作系统的开放性和硬件平台的不断发展,尤其是存储器的发展,使得我们在编写程序的时候不必在意需要多大的存储空间了。
然而,嵌入式系统可就没那么幸运了。至今为止,在嵌入式系统里仍然没有一个能够像Windows这样应用如此广泛的操作系统,也没有可以不考虑存储空间的硬件平台。在数以亿计的嵌入式设备使用者中,都还在用着一成不变的应用程序,单调同时也令人乏味。我们能不能也像在Windows下面一样,在嵌入式系统中可以安装应用程序呢?应该怎样克服嵌入式系统的限制而实现这个功能呢?
有梦想才会不断的追求!我们知道,在Windows中程序都是以文件的形式存储在文件系统中的,然后通过操作系统控制这些程序的运行,我们可以说它的程序是“分散式”的。而在嵌入式系统中通常是将程序烧录在一个Flash芯片中,文件系统在另一个Flash芯片中(也可以二者在同一个芯片中),CPU是直接从程序Flash芯片中读取指令执行的,没有经过文件系统,我们可以叫这种程序是“一体式”的。Windows的“分散式”程序体通过文件的形式存在,可以把程序的不同部分分割成不同的文件,当我们只需要更新一个模块内容的时候,只更新这个文件就可以了。熟悉它的朋友们可能已经知道了,这个文件就是在Windows操作系统中的DLL文件。这样的方式可以很容易的实现程序分发,这给了我们一个很好的启示:嵌入式系统中也有文件系统,把程序放在文件系统里不就可以了吗?真是个好主意!
在我们庆幸找到了好方法的时候,问题不偏不倚地出现了:系统如何运行文件系统中的程序,文件中的程序又如何调用平台中的函数?要实现“分散式”的程序运行,这两个问题是必须要解决的,而其中第二个问题就更为重要了。或许您现在还不是十分的明白这些问题的意义,不要着急,这一章里我会逐一的向您讲解如何理解并解决这两个问题。当然,现在我们知道BREW已经在嵌入式系统中解决了这两个问题,从现在开始就让我们沿着开发者的足迹去追寻BREW的本质吧。
1.平台的作用
如果想要清楚的了解我们在嵌入式系统中所面临的问题,那么我们就首先需要了解“分散式”系统的结构。一个“分散式”系统需要有三个部分组成:平台、软件开发工具包(SDK: Software Development Kit)和应用程序。“分散式”应用程序的运行需要平台的支持,就像是DLL文件只有在Windows操作系统平台下才有作用,而到了Linux平台则不起任何作用一样;应用程序则通过SDK进行开发,开发出的源程序经过编译之后可以运行在运行平台之上。平台又分为开发平台和运行平台,开发平台是SDK运行的平台,用来开发可以在运行平台上运行的应用程序,对于一些系统还会提供模拟运行平台的模拟器,以便于在没有显而易见的运行设备的时候也可以看到开发的效果;运行平台是应用程序运行的平台,它提供应用程序运行的环境,同时肩负着控制应用程序的作用。开发平台和运行平台可以是同一个平台,也可以两个不同的平台,比如现在的Winows平台的应用就可以使用VC等工具开发基于Windows的应用程序,而BREW SDK则是运行在Windows环境下,但应用程序却在嵌入式系统中运行。它们之间的关系如下图:

图1 分散式系统结构图
从这个图中我们可以看出,SDK需要使用运行平台的接口声明来开发应用程序,运行平台负责根据用户的输入启动应用程序,而应用程序则通过运行平台的接口调用运行平台的函数库来实现功能,我们的问题主要集中在应用程序和运行平台的互动关系上。
从前面的编译器基础一章中我们可以知道,在固定链接的模式下,各个函数的地址是固定的,我们可以在同一个映像文件中调用任何函数。而存储在文件系统中的程序就不一样了,文件系统中的程序没有固定的位置并且地址也不连续,我们该怎样实现应用程序的启动呢?可选的方案就是将应用程序复制到一个连续的内存块中去,然后在内存中执行程序。在这里需要特别的说明一下,在PC的Windows操作系统中,Windows将全部的程序载入内存中运行,并且其中包含了复杂的内存管理功能,但是在嵌入式系统中通常程序是在Flash芯片中运行的,只有可读写的数据是放在RAM中的,具体的细节可以参考编译器基础一章。BREW主要是应用程序在嵌入式系统中的,因此将程序复制到内存中执行是需要特殊处理的。这个特殊的处理就是我们所面临的第一个问题了——系统如何运行文件系统中的程序。
在我一开始理解BREW的时候,我曾经认为系统如何运行存在于文件系统中的程序是我们所面临的主要问题,但当深入BREW内部的时候发现根本不是这么回事。现在我们可以假设运行平台分配了一个足够大的内存,这块内存地址是已知的,可以想象的到我们可以从这个地址开始执行程序。现在我们暂时忽略那些特殊的处理,文件系统中的这个程序现在正在运行,就像在Flash中的程序一样的在运行。从理论上来讲这个是行得通的,因此系统如何运行文件系统中的程序的问题并不是我们所面临的难题。实际上BREW也是按照这个思路做的,只是实现时还有细节的东西在,关于这些细节我们将在Shell内幕一章进行详细的介绍。
进一步的,假设现在程序运行到了需要调用平台函数的时候了,问题就来了,由于当前的应用程序是开发者使用SDK开发的,就像平台不知道应用程序的地址一样,应用程序也不知道平台的函数地址,因此,我们现在面临的问题是怎么能够知道应用程序中所调用的平台函数的地址。SDK可以提供运行平台中的每个函数的地址吗?可以提供,但是行不通。因为平台是会经常升级的,导致每个函数的链接地址不固定,如果由SDK提供所有函数的地址带来的问题是,只要运行平台一升级,那么SDK和应用程序都需要同时升级。如果这样的话我们的分散式程序就不能实现“分散式”的升级了,这种程序的运行方式也就没有任何意义了。看来我们还要寻找更为高级的方法,这种方法要能够提供一种应用程序与运行平台之间无关的机制。我们现在所需要的这个“机制”就是第二个问题了——文件系统中的程序如何调用平台的函数。
从分散式系统结构图中我们可以看到,SDK使用的是运行平台的接口声明,应用程序调用真正的运行平台接口。或者换句话说开发过程中使用运行平台的接口声明,而在运行时应用程序使用真正的二进制接口,并且在二进制层面调用接口函数。那么,现在无论是SDK还是应用程序都与接口相关,那么,可以想象的到的解决第二个问题的方式就是让接口和接口实现之间分离。接口与实现间分离的方法就是BREW的核心,也是接下来我们主要阐述的议题。
2.软件分发和C语言
为了更好的理解“实现接口和实现分离”所面临的问题,让我们先来看看通常的C语言软件库是如何分发的,这对于我们的理解非常有用。为了能够更加清楚地理解问题,我们在接下来几节的论述中不会仅仅局限于嵌入式系统,因为同样的问题也存在于PC系统中,更为重要的是PC系统所面临的问题更加典型,并且这些问题在软件系统中是具有普遍性的。
想象现在有一个C语言库的开发厂商,它开发了一个算法,可以在O(1)时间效率内实现子字符串的搜索,O(1)时间效率的意思是指搜索时间为常数,与目标字符串的长度没有关系。为了实现这个功能,软件厂商生成了一个头文件FastString.h,包含如下内容:
|
FastString.h– 接口声明文件第一版
|
|
#ifndef FASTSTRING_H_
#define FASTSTRING_H_
// 要求使用者不能直接使用此结构体中的内容
typedef struct _IFastString {
char *m_pString; // 指向字符串的指针
} IFastString;
// 创建目标字符串对象
void IFastString_CreateObject(IFastString * pIFastString, char *pStr);
// 释放目标字符串对象
void IFastString_Release(IFastString *pIFastString);
// 获取目标字符串长度
int IFastString_GetLength(IFastString *pIFastString);
// 查找字符串,返回偏移量
int IFastString_Find(IFastString *pIFastString, char *pFindStr);
#endif // FASTSTRING_H_
|
除了这个头文件之外软件厂商还提供了接口的实现文件FastString.c
|
FastString.c– 接口实现文件第一版
|
|
#include ”FastString.h”
#include <string.h>
// 创建目标字符串对象
void IFastString_ CreateObject (IFastString * pIFastString, char *pStr)
{
IFastString *pMe = pIFastString;
if(pMe == NULL || pStr == NULL)
{
return;
}
pMe->m_pString = malloc(strlen(pStr) +1);
strcpy(pMe->m_pString,pStr);
}
// 释放目标字符串对象
void IFastString_Release(IFastString *pIFastString)
{
IFastString *pMe = pIFastString;
if(pMe == NULL)
{
return;
}
if(pMe->m_pString)
{
free(pMe->m_pString);
}
}
// 获取目标字符串长度
int IFastString_GetLength(IFastString *pIFastString)
{
IFastString *pMe = pIFastString;
if(pMe == NULL)
{
return;
}
return strlen(pMe->m_pString);
}
// 查找字符串,返回偏移量
int IFastString_Find(IFastString *pIFastString, char *pFindStr)
{
IFastString *pMe = pIFastString;
if(pMe == NULL)
{
return;
}
// 搜索算法省略,因为这里仅仅假设存在这样的一个搜索算法
}
|
这个接口总共有四个接口:CreateObject、Release、GetLength和Find。CreateObject用来创建IFastString接口,从实现中我们可以看到它构建了IFastString结构体的内容。Release用来释放由CreatObject分配的内存。GetLength获得字符串的长度。Find用来在目标字符串中查找指定的字符串。在这个接口的实现中,使用了一个初始化的技巧(CreateObject和Release),目的是为了再使用前构建IFastString结构体,使用后可以通过Release释放构建时分配的内存。
一般来讲这个库的使用者会将.lib库链接到自己的工程中,通过接口声明的头文件使用库中的函数,这是一个非常可行的做法。这样做带来的结果是库的可执行代码将成为客户应应用程序中不可分割的一部分。
现在假设对于FastString库文件占用了大约16M的代码空间(这里假设为了完成O(1)算法可能需要十分复杂的程序,并且采取了一些空间换时间的策略等,这可能占用大量的存储空间)。如图2所示,如果现在有三个应用程序都在使用FastString库,那么每一个应用程序都将使用16M的空间来存储这些代码,总共花费了48M的空间。如果一个用户安装了这三个程序,也就是说有32M的空间浪费了,去存储了同样的FastString.lib中的代码。
图2 多个应用程序使用FastString库
在这种情况下的另一个问题是,如果当前FastString库的厂商发现了程序中的缺陷,那么就没有任何办法可以替换已经存在的缺陷代码。一旦FastString的代码链接到了应用程序中,我们就不可能在用户的设备上替换这部分的代码。因此,库厂商不得不重新为每个应用程序的开发者广播发布新的库文件,并且希望他们能够重新的编译他们的应用程序,以便能够使用新的代码。很显然,这可真是一件麻烦的事情,一旦应用程序开发者链接了这个库文件,FastString便失去了模块化的特征。跟进一步说,这在嵌入式系统中是完全不可能的,在这里FastString的角色就是运行平台,我们总不能每一个应用程序都包含一个运行平台去啊。
3.动态链接
解决上面问题的一种技术是使用动态链接技术(Dynamic Link)将FastString包含起来,这种技术的典型应用是Windows操作系统中的动态链接库(DLL文件)。这种方法是将FastString源文件编译成特殊的独立的二进制文件,并强迫FastString将所有的接口从二进制文件中引用出去,建立相应的引出表,以便于在运行时把每个接口的名字映射到对应的二进制接口地址上。与此同时还需要为使用者生成相应的引入库,通过这个引入库FastString的使用者可以获得FastString中每个接口的符号。引入库中并没有包含实际的代码,它只是简单的包含FastString引出的符号名字。当客户链接引入库的时候,这些符号信息会加入到当前的可执行文件中,运行时动态的装载FastString二进制库文件,并在执行时调用相应的程序。当然这些需要一些辅助工具的支持,例如编译器的支持等。此时应用程序的结构如图3:
图3 动态链接示意图
上图就是FastString在动态链接方式下的运行模式,这里面的引入库非常的小,所以可以忽略它占用的空间。在这种情况下,FastString的代码库就只需要一份了。运行时所有的应用程序调用同一个库中的内容,理论上当发现FastString中有缺陷的时候,我们可以更新FastString二进制组件而不影响应用程序。可以看到,我们已经迈出了重要一步,不过还没有完全解决我们所面临的问题。
4.封装性
我们现在已经找到了一种可以实现动态链接的方法,那么下一个问题则与封装有关。考虑这样的情形:一个组织使用了FastString,同时希望能够在2个月内完成开发和测试。假设在这两个月中,某些具有特殊的怀疑精神的开发人员打算在他们的应用程序上测试一下FastString的性能,以便于测试O(1)时间效率的搜索算法。令人惊讶的是Find方法的搜索速度很快,并且与字符串的长度无关,但是GetLength方法的速度不是很理想,因为在GetLength方法中使用的是strlen计算字符串的长度,它查找字符串中的NULL结束符,它的算法需要遍历正个字符串的内容,因此它的执行效率是O(n),当字符串很长,而且调用次数很多的时候,执行的速度很慢。于是开发人员要求厂商提高GetLength操作的执行速度,使它在常数时间内完成。但是现在有一个问题,开发人员已经开发完成了他们的应用程序,他们不希望由于使用新的GetLength方法而更改任何现有的程序。而且其他的厂商可能已经发布了使用这个现有版本的基于FastString的产品,从任何方面将库厂商都不应该影响这些已经面世的产品。
这个时候我们要查看我们的FastString的实现,以便确定哪些可以改变,哪些不可以改变。幸运的是,我们已经要求使用者不能直接使用IFastString结构体中的内容,假设所有的使用者都遵循了这一约定,于是我们很快的修改了GetLength的方法,将头文件改成了如下的方式(未修改部分未列出):
|
FastString.h– 接口声明文件第二版
|
|
#ifndef FASTSTRING_H_
#define FASTSTRING_H_
// 要求使用者不能直接使用此结构体中的内容
typedef struct _IFastString {
char *m_pString; // 指向字符串的指针
int m_nLen; // 存储字符串的长度
} IFastString;
#endif // FASTSTRING_H_
|
将实现文件改成了如下方式(未修改部分未列出):
|
FastString.c– 接口实现文件第二版
|
|
// 创建目标字符串对象
void IFastString_ CreateObject (IFastString * pIFastString, char *pStr)
{
IFastString *pMe = pIFastString;
if(pMe == NULL || pStr == NULL)
{
return;
}
pMe->m_nLen = strlen(pStr);
pMe->m_pString = malloc(pMe->m_nLen +1);
strcpy(pMe->m_pString,pStr);
}
// 获取目标字符串长度
int IFastString_GetLength(IFastString *pIFastString)
{
IFastString *pMe = pIFastString;
if(pMe == NULL)
{
return;
}
return pMe->m_nLen;
}
|
很快的修改后重新发布了FastString的第二个版本。在这里一个显著的改进是在CreateObject时将字符串长度存储起来了,当用户调用GetLength方法时直接返回存储的长度。这样没有修改任何接口的内容,因此应用程序也就不需要修改了。
客户收到了第二版的FastString后,替换了FastString的动态链接库,重新编译链接全部的应用程序,测试后发现不但原始代码不需要任何修改,而且GetLength方法的速度也大大加快了。最终这个第二版的FastString会随着这个产品而发布到用户手中。在安装应用程序的时候,第二版的FastString会将第一版的替换掉。这通常不会有问题,因为修改并没有影响公开的接口,因该只会增强原先已经安装的使用FastString的应用程序的功能而以。
请想象这样的情形,当用户更新了第二版的FastString后,运行新版的应用程序,用户惊喜的发现程序运行的速度提高了。然后用户关闭了新的应用程序,而打开了另一个以前安装的使用旧版本FastSting的应用程序。现在的FastString已经替换成了第二版,因此用户发现这个应用程序的性能也增强了。然而不久异常出现了,系统出现了未知的错误。不过没关系,对于已经习惯了现代商业软件的人士来说,这不算什么问题,于是重新卸载并重新安装了两个应用程序,还是不起作用啊,异常依然发生!到底是怎么回事?
原因在于IFastString结构体的修改。在未修改前sizeof(IFastString) == 4,因为只有一个char *m_ pString变量(假设系统是32位的)。修改后sizeof(IFastString) == 8,增加了4个字节。新版本的软件已经重新编译了,因此相应的IFastString结构体已经增加了空间。但是对于使用第一版FastString编译的应用程序来说,此时在应用程序里面分配的IFastString的空间依然是4,于是当第一版的应用程序调用第二版FastString的时候,将本该属于其他用途的4个字节用作了m_nLen的区间,这是十分粗暴的,产生异常也就不足为奇了。
还记得前面的约定吗?我们要求FastString的使用者不可以直接对IFastString结构体中的数据进行直接操作,以达到应用程序与数据结构间的无关性。但实际上这样的约定基本上不可能被遵守,因为在实际中不管出于什么样的目的,总会有些开发者直接使用结构体中的内容(这些开发者使用的使FastString的C语言库文件形式,没有使用动态链接技术)。加上前面的异常,这一切的根源是我们没有一个实现二进制数据封装的方式。如果现在能够有一种可以将全部的数据结构封装在FastString内部的方法就好了。C语言是灵活的,只要我们找到了问题,我们就可以实现它。于是第三版的FastString新鲜出炉了,首先是头文件:
|
FastString.h– 接口声明文件第三版
|
|
#ifndef FASTSTRING_H_
#define FASTSTRING_H_
typedef void IFastString;
// 创建目标字符串对象
void IFastString_CreateObject(IFastString ** ppIFastString, char *pStr);
// 释放目标字符串对象
void IFastString_Release(IFastString *pIFastString);
// 获取目标字符串长度
int IFastString_GetLength(IFastString *pIFastString);
// 查找字符串,返回偏移量
int IFastString_Find(IFastString *pIFastString, char *pFindStr);
#endif // FASTSTRING_H_
|
接下来是实现的源文件:
|
FastString.c– 接口实现文件第三版
|
|
#include ”FastString.h”
#include <string.h>
typedef struct _CFastString {
char *m_pString; // 指向字符串的指针
int m_nLen; // 存储字符串的长度
} CFastString;
// 创建目标字符串对象
void IFastString_CreateObject (IFastString **ppIFastString, char *pStr)
{
CFastString *pMe = malloc(sizeof(CFastString));
if(pMe == NULL || pStr == NULL)
{
return;
}
pMe->m_nLen = strlen(pStr);
pMe->m_pString = malloc(pMe->m_nLen +1);
strcpy(pMe->m_pString,pStr);
* ppIFastString = (IFastString *)pMe;
}
// 释放目标字符串对象
void IFastString_Release(IFastString *pIFastString)
{
CFastString *pMe = (CFastString *)pIFastString;
if(pMe == NULL)
{
return;
}
if(pMe->m_pString)
{
free(pMe->m_pString);
}
free(pMe)
}
// 获取目标字符串长度
int IFastString_GetLength(IFastString *pIFastString)
{
CFastString *pMe = (CFastString *)pIFastString;
if(pMe == NULL)
{
return;
}
return strlen(pMe->m_pString);
}
// 查找字符串,返回偏移量
int IFastString_Find(IFastString *pIFastString, char *pFindStr)
{
CFastString *pMe = (CFastString *)pIFastString;
if(pMe == NULL)
{
return;
}
// 搜索算法省略,因为这里仅仅假设存在这样的一个搜索算法
}
|
在这个实现中,我们使用了CFastString做为FastString的内部数据结构,定义了void型的IFastString类型做为接口指针传递,还有重要的一条是通过CreateObject获得数据结构的存储空间。通过这样的实现方法,我们将全部的数据类型封装在了FastString库中,这样,无论新的还是老的应用程序,使用的都是统一的IFastString指针,真正的数据是在CreateObject中进行创建的,也就不会出现上面的两种情况了。对于FastString的客户来说,他们所能看到的就是IFastString的void类型和四个接口函数,内部的CFastString的结构已经被隐藏起来了。不过这个第三版的FastString修改了接口函数CreateObject,因此不能够与前两版兼容。不过不要紧,我们现在只是在说明一个更好的方法而已。
4.虚拟函数表
封装性的本质是实现了接口与实现之间在二进制层次的分离,第三版的FastString似乎已经解决了我们所面临的第二个问题。不过现在我们的接口仍然在使用着动态链接用的引入库文件,而且相应的动态链接库也需要提供由符号名到二进制函数地址映射的内容,为了支持这些特性,我们必须要修改相应的编译器才行。修改编译器,很复杂不是吗?
让我们再回到嵌入式系统上来吧,ARM CPU是在嵌入式系统中使用最广泛的CPU,因此相应的ARM编译器也是应用最广泛的,基本上成为了一种通用的编译器。我们怎么修改这个编译器呢?似乎难度有点大。更近一步的,嵌入式系统中大大小小的CPU有好多种,我们也不可能把所有的这些编译器都修改了啊,看来修改编译器的可能性不大。
又一次让我们体验到了理想与现实之间的差距!不过别灰心,看看我们现在的接口,它是二进制层面上的,我们能不能把这个引入库变成标准的C语言的头文件同时又能够实现接口与实现间的分离呢?如果真的能够实现这样的方法,那么将意味着我们可以通过通用编译器来实现动态链接的技术。这可真是令人兴奋,这个方法要比动态链接技术还要好。
按照这个思路进一步分析,如果将现在的第三版FastString使用的C头文件做为标准的接口文件,那么将意味着各个接口需要静态的链接到应用程序中,接口和实现之间还是没有实现分离,难道我们转了一圈又回到原点了?真的又回来了,不过,不同的是我们现在已经得到了第三版的FastString。现在我们已经知道了通过CreateObject来获得内部的空间,那么我们是否也可以通过一个函数来获得接口呢?可以,这个技术就是虚拟函数表(VTBL)。
这可真是“山重水复疑无路,柳暗花明又一村”啊,先看看我们这个第四版的FastString的头文件吧:
|
FastString.h– 接口声明文件第四版
|
|
#ifndef FASTSTRING_H_
#define FASTSTRING_H_
typedef struct _IFastString IFastString;
typedef struct _IFastStringVtbl IFastStringVtbl;
typedef void (*PFNCreateObject)( IFastString **ppIFastString, char *pStr);
struct _IFastString
{
struct IFastStringVtbl *pvt;
};
struct _IFastStringVtbl
{
void (*Release) (IFastString *pIFastString);
int (*GetLength) (IFastString *pIFastString);
int (*Find) (IFastString *pIFastString, char *pFindStr);
};
// 释放目标字符串对象
#define IFASTSTRING_Release(p) ((IFastString*)p->pvt)->Release(p)
// 获取目标字符串长度
#define IFASTSTRING_GetLength(p) ((IFastString*)p->pvt)->GetLength(p)
// 查找字符串,返回偏移量
#define IFASTSTRING_Find(p,s) ((IFastString*)p->pvt)->Find(p,s)
#endif // FASTSTRING_H_
|
接下来是C语言的源文件:
|
FastString.c– 接口实现文件第四版
|
|
#include ”FastString.h”
#include <string.h>
typedef struct _CFastString {
IFastStringVtbl *pvt; // 指向虚拟函数表的指针
char *m_pString; // 指向字符串的指针
int m_nLen; // 存储字符串的长度
} CFastString;
// 函数声明
static void IFastString_Release(IFastString *pIFastString);
static int IFastString_GetLength(IFastString *pIFastString)
static int IFastString_Find(IFastString *pIFastString, char *pFindStr);
IFastString_GetLength,
IFastString_Find
};
// 创建目标字符串对象
void IFastString_CreateObject (IFastString **ppIFastString, char *pStr)
{
CFastString *pMe = malloc(sizeof(CFastString));
if(pMe == NULL || pStr == NULL)
{
return;
}
pMe->pvt = &gvtFastString;
pMe->m_nLen = strlen(pStr);
pMe->m_pString = malloc(pMe->m_nLen +1);
strcpy(pMe->m_pString,pStr);
* ppIFastString = (IFastString *)pMe;
}
// 释放目标字符串对象
static void IFastString_Release(IFastString *pIFastString)
{
CFastString *pMe = (CFastString *)pIFastString;
if(pMe == NULL)
{
return;
}
if(pMe->m_pString)
{
free(pMe->m_pString);
}
free(pMe)
}
// 获取目标字符串长度
static int IFastString_GetLength(IFastString *pIFastString)
{
CFastString *pMe = (CFastString *)pIFastString;
if(pMe == NULL)
{
return;
}
return strlen(pMe->m_pString);
}
// 查找字符串,返回偏移量
static int IFastString_Find(IFastString *pIFastString, char *pFindStr)
{
CFastString *pMe = (CFastString *)pIFastString;
if(pMe == NULL)
{
return;
}
// 搜索算法省略,因为这里仅仅假设存在这样的一个搜索算法
}
|
首先对这个第四版的FastString程序做一个说明。在FastString头文件中我们定义了两个类型:IFastString和IFastStringVtbl。IFastStringVtbl类型是虚拟函数表的类型,IFastString中包含了指向虚拟函数表的指针。在接口定义的时候,我们使用了((IFastString*)p->pvt)来调用虚拟函数表中的函数指针,这也说明了如果要使用接口,必须要提供IFastString的指针类型。可以看出Release、GetLength和Find已经实现了在C语言定义的接口与实现函数之间的分离。
接着看一下源文件中的情况。CFastString结构体与第三版的FastString不同的是增加了一个IFastStringVtbl类型的指针,而且这个指针在结构体的最顶部,如果将IFastString和CFastString对比一下我们可以发现他们都在顶部包含了IFastStringVtbl的指针,这就意味着CFastString是IFastString的超集。这一点是很重要的,我们可以看见在CreateObject函数中返回的IFastString指针其实是指向CFastString的指针的,只有CFastString是IFastString的超集的时候我们才可以这么做。在这个源文件中还定义了一个IFastStringVtbl的变量gvtFastString,并为这个变量初始化成了各个对应的函数,这个变量就使我们的虚拟函数表。虚拟函数表的示意图如下:
图4 虚拟函数表
在这个第四版的FastString中,我们可以看到,除了CreateObject成员之外,其余的三个成员函数都已经添加到了虚拟函数表中,而且这个虚拟函数表还可以随着需求的增加而进行无限的扩大,我们只付出了一个函数CreateObject的代价就实现了无限多个接口和实现之间的分离了。
由于用户需要使用CreateObject来获得IFastString的指针,因此我们没有办法将其与实现分离开,怎么办?现在只有这一根“线”还在困扰着我们,我们难道要功败垂成了吗?当然不能了。开动脑筋,回到我们应用程序的启动过程,对于一个程序,不管是由main函数或者其他的什么函数做为启动函数,都允许启动的时候传递参数,可能您已经想到了吧,我们把这个CreateObject函数做为参数传递给应用程序不就可以了吗?恍然大悟!这也就是为什么我们将CreateObject函数定义成了一个PFNCreateObject的函数类型的原因了,目的就是为了让使用者可以在应用程序中定义这种类型的函数指针。
现在我们的应用程序、接口和实现之间已经分离了,中间使用了CreateObject这根细线连接了起来,只要接口不变,应用程序和实现之间就不会有任何的联系,包括二进制层面和C语言层面的。只不过这要求我们应用程序和接口的实现之间需要使用同一种编译器,或许这就叫做有得有失吧,不过对于嵌入式系统来说这是必须的,因为没有哪一种编译器可以支持全部的嵌入式CPU。
5.支持多个接口
到现在为止所展示的技术已经解决了我们所面临的问题,不过对于一个平台来说不可能只有一个FastString接口,可能还有诸如FastNumber的接口。我们总不能把FastString和FastNumber两个接口的CreateObject都做为参数传递给应用程序的启动函数吧?看来我们现有的FastString需要进行一些扩展,来实现只传递一个参数给应用程序就可以创建多个接口的功能。在这里我们将增加一个叫做Shell的接口来管理其他的接口,相关的代码如下:
|
shell.h– 接口声明文件
|
|
#ifndef SHELL_H_
#define SHELL_H_
#define CLASSID_FASTSTRING 0×00000001
#define CLASSID_FASTNUMBER 0×00000002
typedef struct _IShell IShell;
typedef struct _IShellVtbl IShellVtbl;
struct _IShell
{
struct IShellVtbl *pvt;
};
struct _IShellVtbl
{
void (*CreateInstance) (IShell *pIShell,
int nClassID,
void **ppObj,
unsigned int nUserData);
};
// 释放目标字符串对象
#define ISHELL_CreateInstance(p,c,pp,u) ((IShell*)p->pvt)->CreateInstance(p,c,pp,u)
#endif // SHELL_H_
|
源文件如下:
|
Shell.c– 接口实现文件
|
|
#include ”Shell.h”
typedef struct _CShell {
IShellVtbl *pvt; // 指向虚拟函数表的指针
} CShell;
// 函数声明
static void IShell_CreateInstance(IShell *pIShell,
int nClassID,
void **ppObj,
unsigned int nUserData);
IShellVtbl gvtShell = { IShell_CreateInstance };
// 创建Shell对象
void IShell_CreateObject (void**ppObj, unsigned int nUserData)
{
CShell *pMe = malloc(sizeof(CShell));
if(pMe == NULL)
{
return;
}
pMe->pvt = &gvtShell;
* ppObj = (void*)pMe;
}
// 创建由ClsID指定的
static void IShell_CreateInstance(IShell *pIShell,
int nClassID,
void **ppObj,
unsigned int nUserData)
{
CShell *pMe = (CShell *)pIShell;
if(pMe == NULL)
{
return;
}
switch(nClassID){
case CLASSID_FASTSTRING:
break;
case CLASSID_FASTNUMBER:
IFastNumber_CreateObject((IFastNumber **)ppObj, nUserData);
break;
default:
break;
}
}
|
在这个Shell接口中,我们定义了一个接口函数CreateInstance。它的作用是通过参数nClassID来创建指定的接口实例。IShell_CreateObject函数用来创建Shell接口本身,在使用的时候必须由系统直接调用IShell_CreateObject来产生Shell对象,然后再通过Shell接口来创建其他的接口(如FastString)。同时在这里包含了一个在本书中尚未实现的接口FastNumber,使用它仅仅是为了方便举例而已,因此有兴趣的读者可以仿照FastString接口实现FastNumber接口。
从shell.h和shell.c文件中可以看到,Shell接口的实现方式与第四版的FastString实现方式是相同的。更进一步的,从CreateInstance接口函数的内部实现我们可以知道,它使用了一个Class ID来识别用户创建的是哪一个接口,并通过switch语句实现相关接口的CreateObject函数的调用。通过这样的Shell管理,应用程序只需要知道一个Shell接口的指针就可以创建其他的接口了。换句话说,在启动应用程序的时候,我们先调用IShell_CreateObject函数创建一个Shell指针,并将这个Shell指针做为参数传递给应用程序的启动函数,那么理所当然的,我们就可以在应用程序中使用Shell的接口ISHELL_CreateInstance来创建其他的接口了。通过这样的方式,我们不但可以实现接口的管理工作,而且同时也为接口的扩展性提供了足够的灵活性。
6.接口的扩展性
到现在为止所展示的技术使得用户可以通过调用统一的C语言声明的接口,实现动态的二进制库的装载,这样可以无限制的升级库程序而不影响已有的应用程序,并且客户也不需要重新编译他们基于当前库文件所开发的应用程序,这对于创建一个复杂的运行平台来说是非常有用的。然而,这个接口却不能够随着时间而变化。这是因为客户在编译的时候需要有精确的接口定义,对接口的任何变化都需要客户重新编译他们的应用程序,以适应这种变化。更为糟糕的是,改变接口的定义完全违背了我们对接口封装性的要求。即便是最无伤大雅的变化,比如修改了接口的用途但是保留接口函数的原形不变,也会导致应用程序不再发生作用。这意味着接口的定义绝对不能改变,它既是语义上的约定,同时也是二进制层次上的约定。为了拥有一个稳定的,行为可预测性的运行时环境,接口的不变性这一要求是十分重要的。
尽管接口具有不变性的原则,但是我们通常需要在一个接口定义好之后,希望能够加入原先设计时没有预测到的新功能。此时我们可以利用对虚拟函数表布局结构的知识,只是简单的把新的方法追加到现有接口的底部,就可以实现对接口的扩展。考虑下面的FastString接口声明:
|
typedef struct _IFastString IFastString;
typedef struct _IFastStringVtbl IFastStringVtbl;
struct _IFastString
{
struct IFastStringVtbl *pvt;
};
struct _IFastStringVtbl
{
void (*Release) (IFastString *pIFastString);
int (*GetLength) (IFastString *pIFastString);
int (*Find) (IFastString *pIFastString, char *pFindStr);
};
|
简单的修改接口vtbl的声明,在现有的结构体内增加新的接口函数的类型声明,这样得到的二进制声明结构是原有的声明的超集,因为新的方法总是出现在旧版本方法之后。在针对新方法的接口实现中,我们可以填充在这个结构体中的函数指针:
|
typedef struct _IFastString IFastString;
typedef struct _IFastStringVtbl IFastStringVtbl;
struct _IFastString
{
struct IFastStringVtbl *pvt;
};
struct _IFastStringVtbl
{
// 第一版接口
void (*Release) (IFastString *pIFastString);
int (*GetLength) (IFastString *pIFastString);
int (*Find) (IFastString *pIFastString, char *pFindStr);
// 第二版接口
int (*FindN)( IFastString *pIFastString, char *pFindStr, int n);
};
|
这种方式完全可以正常工作。在第一版接口上开发的应用程序将忽略前三个接口之外的其他接口的信息。当老的应用程序使用第二版接口实现的二进制程序的时候,它仍然可以正常的工作。然而,新的客户总是希望可以使用新的方法FindN,以便于能够获得子字符串第N次出现的位置。如果此时应用程序的运行平台依然使用的是第一个版本的实现,那么不幸的,问题发生了。当调用一个未曾实现的接口时,显而易见的,程序崩溃了。
这项技术的问题是修改了公开的接口,从而影响了接口的封装性。就像是只修改了C语言函数的声明会产生编译错误一样,改变了二进制的接口结构也会引起运行时的代码错误。这意味着接口必须是不可改变的,一旦公开后就不能再变化。解决这个问题有两种方法:一是允许接口的实现暴露多个接口,或者换句话说就是如果需要扩展老的接口,那么就重新定义一组新的接口,这样的话就可以在不支持新接口的旧平台上运行的时候判断接口的有效性,当然,这要求应用程序要对创建接口时的异常进行处理;二是支持在运行时对接口版本的判断或者在应用程序和运行平台之间有一种版本的比较机制,比如通过提供一个可以获得接口版本的API来进行版本的比较工作。不管是使用哪一种方法,无疑都会增加开发的负担,包括接口的开发者和用户的开发者,因此最好的方法是尽可能的不要去修改已经定义好的接口。
7.资源管理
当我们在使用我们的接口的时候,我们还会遇到另外的一个问题,请看下面的代码:
|
IFastString *pFastString;
char TargetStr[] = “This is a test example only!”’
ISHELL_CreateInstance(pShell, CLASSID_FASTSTRING, (void **)&pFastString, (unsigned int)TargetStr);
(void)FastString_Test(pFastString);
|
相应的FastString_Test函数如下:
|
int FastString_Test(IFastString *pFastString)
{
int nOffset;
nOffset = IFASTSTRING_Find(pFastString, ”test”);
IFASTSTRING_Release(pFastString);
return nOffset;
}
|
在这个例子里,pFastString在主函数中创建,同时做为参数传递给了FastString_Test函数,然后释放pFastString,这很正常,没有什么问题。在FastString_Test函数中调用了Find方法,并且在使用函数退出的时候释放了pFastString指针,这很正常,也没有什么问题。然而,当我们将两者结合起来的时候,问题出现了:主函数中释放了一次接口指针,而在FastString_Test函数中也释放了一次同样的接口指针。换句话说,在这个程序中由于开发者的疏忽,对同一个接口释放了两次,这将导致不可预测的异常发生。这里面的问题是在增加了对接口指针的引用的时候,没有相应的处理机制来记录当前实例引用的次数,也就是当前接口实例的一个资源管理的问题。
或许您可以说这个问题可以通过使用者的细心来避免,那么我们再来看另外一个问题:如果当前的接口是可以共用的一些函数,比如这个接口中的方法全部是诸如STRLEN之类的重定义助手函数接口,并且在创建接口的时候需要分配一些公用的内存空间,那么,我们在每一次创建这个接口实例的时候,都必须分配不一样的存储空间吗?如果我们这样做了,实际上不会有什么问题,但是会浪费存储空间,因为他们本来是可以全部共用的。况且,如果我们全部都使用同一个指针在各个函数之间调用,那么对于这个指针来说使用起来会很危险的,因为我们不知道每一个函数是怎样处理这个指针的,这可真是太糟糕了。
为了解决这个问题,一个可行的做法是为每一个接口增加一个引用计数的管理机制。在增加了这个机制之后的第五版FastString实现如下:
|
FastString.h– 接口声明文件第五版
|
|
#ifndef FASTSTRING_H_
#define FASTSTRING_H_
typedef struct _IFastString IFastString;
typedef struct _IFastStringVtbl IFastStringVtbl;
struct _IFastString
{
struct IFastStringVtbl *pvt;
};
struct _IFastStringVtbl
{
int (*AddRef) (IFastString *pIFastString);
int (*Release) (IFastString *pIFastString);
int (*GetLength) (IFastString *pIFastString);
int (*Find) (IFastString *pIFastString, char *pFindStr);
};
// 增加接口指针的引用计数
#define IFASTSTRING_AddRef(p) ((IFastString*)p->pvt)->AddRef(p)
// 释放目标字符串对象
#define IFASTSTRING_Release(p) ((IFastString*)p->pvt)->Release(p)
// 获取目标字符串长度
#define IFASTSTRING_GetLength(p) ((IFastString*)p->pvt)->GetLength(p)
// 查找字符串,返回偏移量
#define IFASTSTRING_Find(p,s) ((IFastString*)p->pvt)->Find(p,s)
#endif // FASTSTRING_H_
|
相应的实现文件如下:
|
FastString.c– 接口实现文件第五版
|
|
#include ”FastString.h”
#include <string.h>
typedef struct _CFastString {
IFastStringVtbl *pvt; // 指向虚拟函数表的指针
int m_nRef;
char *m_pString; // 指向字符串的指针
int m_nLen; // 存储字符串的长度
} CFastString;
// 函数声明
static int IFastString_AddRef(IFastString *pIFastString);
static int IFastString_Release(IFastString *pIFastString);
static int IFastString_GetLength(IFastString *pIFastString)
static int IFastString_Find(IFastString *pIFastString, char *pFindStr);
IFastStringVtbl gvtFastString = { IFastString_AddRef,
IFastString_Release,
IFastString_GetLength,
IFastString_Find
};
// 创建目标字符串对象
void IFastString_CreateObject (IFastString **ppIFastString, unsigned int nUserData)
{
CFastString *pMe = malloc(sizeof(CFastString));
char *pStr = (char *)nUserData;
if(pMe == NULL || pStr == NULL)
{
return;
}
pMe->pvt = &gvtFastString;
pMe->m_nLen = strlen(pStr);
pMe->m_pString = malloc(pMe->m_nLen +1);
strcpy(pMe->m_pString,pStr);
pMe->m_nRef = 1;
* ppIFastString = (IFastString *)pMe;
}
// 增加接口指针的引用计数
static int IFastString_AddRef(IFastString *pIFastString)
{
CFastString *pMe = (CFastString *)pIFastString;
return (++pMe->m_nRef);
}
// 释放目标字符串对象
static int IFastString_Release(IFastString *pIFastString)
{
CFastString *pMe = (CFastString *)pIFastString;
if(–pMe->m_nRef >0)
{
return pMe->m_nRef;
}
if(pMe->m_pString)
{
free(pMe->m_pString);
}
free(pMe)
}
// 获取目标字符串长度
static int IFastString_GetLength(IFastString *pIFastString)
{
CFastString *pMe = (CFastString *)pIFastString;
return strlen(pMe->m_pString);
}
// 查找字符串,返回偏移量
static int IFastString_Find(IFastString *pIFastString, char *pFindStr)
{
CFastString *pMe = (CFastString *)pIFastString;
// 搜索算法省略,因为这里仅仅假设存在这样的一个搜索算法
}
|
在第五版的FastString中,我们主要是增加了接口AddRef。这个接口是在增加指针引用的时候调用的,它仅仅增加了接口内部变量m_nRef。同时为了实现相应的机制,在Release方法内增加了对引用计数的条件判断:如果当前的引用计数不为零,则直接返回引用计数的值,否则释放创建接口实例时所分配的内存。这样,当我们在使用上面的FastString_Test函数之前,调用IFASTSTRING_AddRef方法,就解决了资源管理的问题了。
增加这个AddRef方法还有一定的人为因素,因为只要程序员能够足够的注意,那么就不会存在资源管理的问题。但是,人生不如意十有八九,我们不能将全部的希望都寄托在程序员的身上,谁都有犯错误的时候。因此增加了AddRef的约定,它与Release方法相对应,形成了一种对称的编程“美感”,约定了是要增加了对指针的引用就调用AddRef,对应的在在适当的位置使用Release释放指针引用。
细心的读者可能还会发现第五版的FastString的实现中有两处不一样:一是IFastString_CreateObject函数的参数变化了,由原来的char *类型成了现在的unsigned int类型,这样做的目的是为了统一在Shell程序中CreateObject的形式;二是FastString接口函数中if(pMe == NULL)的判断去掉了,我们知道,在第一版的FastString中这句判断是很有必要的,它可以检测当前指针的有效性,那么想象一下对于我们的虚拟函数表NULL指针意味着什么?根据我们已有的虚拟函数表的知识,调用的接口是基于IFastString指针的相对偏移量,例如使用NULL指针来调用Release接口,那么实际上调用的是基于0地址的4字节偏移位置的函数,只有天知道这个地址中存储的是什么东西!因此,为接口传递空指针的时候会不可避免的发生异常,根本就不可能执行到接口函数,所以相应的判断是没有任何意义的。相应的示意图如下:
图5 接口的偏移量
最后,再让我们清楚地看一下这种虚拟函数表所实现的总体框图吧,看看应用程序、接口定义以及接口实现之间的关系:
图6 应用程序、接口定义以及接口实现的关系
至此,我们已经完成了全部需要解决的问题,最终我们实现的第五版FastString就是BREW接口的实现方法,只不过在实现的细节上有所不同。同时,我也相信各位在阅读了第二部分之后,一定会对这一部分的介绍颇有感触。接下来我们就趁热打铁,介绍BREW实现方法的一些高级特性,其中涉及了面向对象和COM组件的相关知识,对于没有接触过这两部分内容的读者来说,阅读可能会有一定困难。不过我会尽我最大的努力,争取用最容易理解的方式来阐述这些特性。
7.面向对象的特性
C语言本身没有面向对象的特性,但是我们使用C语言开发的BREW就具有了面向对象的特性了。面向对象的主要特征是:数据抽象、继承和多态。数据抽象指的是使用一组数据和方法描述一个我们要表达的内容(对象),它的关键点在于将方法和数据结合也叫做封装;继承是指一个对象可以通过某种方式使用另一个对象中的方法和数据,它包含了语法上的继承和二进制层次的继承;多态是指通过虚拟函数实现的成员函数晚捆绑的特性,其核心的特征是成员函数的晚捆绑。
BREW具有良好的数据抽象特性,它将全部的数据封装在了接口实现的内部,只将接口函数暴露给外部使用。这种方式是实现数据封装的理想方式,我想这一点可以从前面的“封装性”一节看出来。因此,BREW已经具有了面向对象的第一个特性。
在这个最好的封装实现的基础上,根本没有任何数据直接暴露出来,因此,继承性就体现在接口的继承上面了。由于BREW是使用C语言实现的,因此没有办法实现语法上的直接继承,但是它实现了二进制层次上的继承。继承在二进制上的表现就是在本数据结构中兼容另一个数据结构。例如,假设结构体A中包含了成员int i,现在有结构体B包含了同样的int i类型的成员,并且后面紧跟了int j成员,此时B结构是A结构的超集,也可以说B结构继承了A结构。现在我们已经知道BREW接口的二进制结构,如果需要实现二进制的BREW继承,我们就可以通过定义一个接口A的超集来实现另一个接口B,此时我们就可以说这个接口B是从接口A继承来的。我们可以通过接口A的定义使用接口B中的方法。因此说,BREW具有面向对象的继承的特性,只不过不是在语法上,而是在二进制的数据结构层面。
面向对象的第三个特性就是多态,可以说这个特性是BREW天生的。我们知道BREW一开始就是通过VTBL来实现的,可以实现运行时的接口函数绑定(也就是迟绑定)。关于这里点我们可以从五个版本的FastString的实现中发现这一过程。晚捆绑与早捆绑的区别在于早捆绑在使用接口的时候再编译链接的时候就已经确切的知道每个接口函数的地址,这就类似于使用一个C语言的库;而晚捆绑则是在运行时才知道每个接口函数的确切地址,因为虚拟函数表是在运行时才赋值给相应的接口的。使用这种技术的一个动机是我们可以在运行的时候控制使用一个接口中的多个实现中的哪一个实现。如果当前FastString接口已经公开了,那么可能会有第三方的厂家按照这个接口规范,来实现一个更好的FastString接口。与最原始的FastString接口布局一样,新的实现的接口函数布局与老的一样,那么此时仅仅需要更新一个Class ID我们就可以切换到新的接口了。使用晚捆绑的另一个动机是,应用程序可以检测到当前的运行平台是否已经实现了此接口,并给予相应的处理,这样可以避免在接口为实现的平台上运行时引起致命的错误。
综上所述,BREW具有了面向对象的三个主要特性,因此说他是具有面向对象性质的一种开发平台。
8.与COM的比较
COM是Windows平台上实现的一种跨语言的开发机制,目前在Windows平台的底层,许多功能都是通过COM机制来实现的。COM通过统一的、独立的接口定义语言(IDL:Interfase Definition Language)来定义统一的接口,并规定了相应的接口二进制规范,这样就可以按照这个二进制规范,通过各种不同的开发语言来实现COM程序的开发,而实现这种接口与实现之间完全分离的技术就是虚拟函数表(VTBL)。
熟悉COM开发的读者对BREW应该有一种似曾相识的感觉,没错,在我的定义中,BREW就是一个简单版本的COM。BREW与COM相比,它们的核心思想是十分一致的,都具有接口与实现分离的特性,都使用了VTBL的技术等等。它们的不同点主要表现在以下几个方面:
第一,它们的接口与实现间的分离程度不同。COM是接口和实现的完全分离,为此专门规范了统一的接口定义语言,因此而接口的实现可以采用任何一种开发语言,如C/C++和Java等。而BREW则为了简化开发,使用了C语言形式的接口定义,这样就使得BREW的应用程序和实现都需要基于同样规则的编译器。或者换句话说,COM实现是与开发语言无关的,而BREW的实现则是与语言相关的。
第二,它们的接口创建方式不同。COM通过Windows注册表,使用文本的名字寻找相应的Class ID(这个Class ID需要通过注册程序进行注册),例如FastString接口可以通过传递字符串“FastString”最为参数从而创建FastString接口,实现这个功能的基础是运行时的类型识别技术(RTTI,相关的内容可以参考VC的书籍)。而BREW则为了简便起见,仅仅通过Class ID来创建接口。
第三,它们的规范层次不同。COM是二进制层次的规范,只要符合COM的二件制规范,我们可以使用任何一种语言进行开发。而BREW则是一种开发语言上的规范(使用了统一的C语言接口定义)。当然,我们也可以把BREW做为一种二进制层次的规范,但是似乎这并不大适合于在嵌入式系统中应用程序,因为当前大部分嵌入式系统都是使用C语言来开发的。不过对于BREW应用程序来说,只要遵循BREW的调用约定,是可以使用其它语言开发的。这里就不详细的讨论这个问题了。
9.小结
本章介绍了BREW实现的来龙去脉,展示了BREW所需要解决的两个主要问题:如何启动程序以及如何调用平台中的函数。最终,我们通过五个版本的FastString事例解决了全部的问题。BREW的本质是通过虚拟函数表技术实现了接口定义与接口实现之间的分离,这样BERW应用程序就可以存储在文件系统中,并在运行的时候调用接口函数。可以这样说,BREW已经将C语言运用到了极至,理解了BREW的原理可以对程序的开发有更加深入的认识。
Posted: January 18, 2009 at 2:17 am | Tags: brew
转至Gemsea大人的BLOG:http://blog.csdn.net/Gemsea
硬件是软件的运行平台,没有硬件的支撑软件也将不复存在。您能想象没有显示器软件将如何显示图形,没有CPU软件将如何运行吗?反正我想象不到!但是如果把问题反过来问就问到本质了,软件运行需要哪些硬件支持呢?看图1.1:
图1.1 系统结构框图
我们抛开硬件的什么电器特性等等,去芜存菁,就是上面的这个图了。如果程序要运行没有CPU是不行的,CPU要快速的交换数据,没有RAM也是不行的。因此无论任何系统,CPU和RAM都是必不可少的。您一定会提醒我ROM不也是不变的吗?这种说法不完全对,因为在PC系统和嵌入式系统之间ROM的作用是不一样的。在PC系统中ROM就是那个BIOS芯片,是用来提供系统的启动代码和基本的输入输出功能的;而在嵌入式系统中,ROM存储了全部的代码,它已经将PC中的BIOS和硬盘的与代码相关的功能混合在一起了。
|
设备
|
PC系统典型硬件设备
|
嵌入式系统典型硬件设备
|
|
CPU
|
任何CPU
|
任何CPU
|
|
RAM
|
任何RAM
|
任何RAM
|
|
ROM
|
BIOS芯片
|
Flash芯片
|
|
存储设备
|
硬盘
|
Flash芯片
|
|
输入设备
|
键盘
|
键盘
|
|
输出设备
|
显示卡+显示器
|
LCD显示屏
|
PC的ROM ——BIOS芯片可以采用Flash芯片,在这里之所以不写成Flash芯片是因为BIOS的作用和嵌入式系统的Flash作用不大一样,使用BIOS以示区分。
从软件观点来讲,任何CPU和RAM都可以应用于各种系统中,不存在明显的区别,只要CPU可以执行指令控制设备就可以了。但是考虑到耗电以及体积(嵌入式设备通常要求耗电低、体积小)等问题,嵌入式系统就发展出了专用的CPU芯片。当前应用最广泛的是ARM CPU。ARM CPU是由英国的ARM公司设计的,由于其执行效率高,体积小,耗电少等特点被广泛应用于嵌入式系统。由于嵌入式系统要求高集成度,通常不会存在单独的CPU芯片,而是将CPU和很多的外围电路集成到一起,做成一块芯片,因此ARM采用授权的方式提供内核芯片设计,以便于使用者进行芯片的集成。
CPU按照次执行指令的数据带宽可以分为16位、32位、64位等。32位CPU一次只能处理32位,也就是4个字节的数据;而64位CPU一次就能处理64位即8个字节的数据。如果我们将总长128位的指令分别按照16位、32位、64位为单位进行编辑的话:旧的16位CPU(如Intel 80286 CPU)需要8个指令,32位的CPU需要4个指令,而64位CPU则只要两个指令。显然,在工作频率相同的情况下,64位CPU的处理速度比16位、32位的更快。
除了运算能力之外,与32位CPU相比,64位CPU的优势还体现在系统对内存的控制上。由于地址使用的是特殊的整数,而64位CPU的一个ALU(算术逻辑运算器)和寄存器可以处理更大的整数,也就是更大的地址。传统32位CPU的寻址空间最大为4GB,使得很多需要大容量内存的大规模的数据处理程序在这时都会显得捉襟见肘,形成了运行效率的瓶颈。而64位的处理器在理论上则可以达到1800万个TB(1TB=1024GB),将能够彻底解决32位计算系统所遇到的瓶颈现象。当然64位寻址空间也有一定的缺点:内存地址值随着位数的增加而变为原来的两倍,这样内存地址将在缓存中占用更多的空间,其他有用的数据就无法载入缓存,从而引起了整体性能一定程度的下降。
在进行系统设计时,会根据不同寻址能力的CPU来进行寻址空间的分配。由于CPU都是通过设备的寄存器(这个寄存器可以理解为设备本身带的RAM)来控制设备的,因此地址空间的划分就显得十分重要。例如,一个具有32位寻址能力的CPU不可能讲全部的地址空间都分配给RAM,好比PC系统需要为BIOS分配存储空间等。也就是说只要是需要CPU直接控制的外部设备都需要为其分配CPU地址空间。
RAM就是可以随机访问,快速读写的存储器。CPU可以直接从RAM中取得数据(CPU可以从所有分配了地址空间的设备寄存器中取得数据)或代码指令,因此RAM的访问速度将直接影响系统的性能。
ROM是每个计算机系统必不可少的,但是其实现的方式却不尽相同。在我们熟悉的PC系统中,ROM是一个称作BIOS(Base Input & Output System)的芯片。CPU上电时会从ROM中读取指令,因此没有ROM的系统是不能够运行的,因为如果没有ROM,CPU将无法获得起始的执行指令。在PC系统中BIOS的作用除了提供起始指令以外,还会扫描硬件设备并初始化主板(Main Board)上的硬件接口。由于PC上的接口都遵循着一组通用的协议,因此BIOS就可以实现所有硬件接口的驱动(如USB接口、显示卡、键盘和鼠标等)和硬件的数据输入输出功能,这也是BIOS(基本输入输出系统)名称的由来了。在BIOS控制的硬件接口中也包含了硬盘的控制接口,在BIOS初始化完成后就会到硬盘主分区上查找启动文件(注3),后面的事情就交给PC的操作系统了。这个过程请看图1.2。
通常,在硬盘内有一个主引导记录区,在安装操作系统的时候由操作系统写入Boot程序,BIOS就是取出这段程序然后执行。由于Boot程序是由操作系统写入的,因此从这个Boot程序开始,系统的运行权限就交由操作系统来控制了。以Windows2000操作系统为例,这段代码区域执行时会搜索名叫“NTLDR”的系统文件。之所以分成Boot程序和NTLDR文件的原因是硬盘的Boot Sector很小(只有466Bytes),不可能容纳全部的启动程序。
图1.2只是一个启动的示意图,在这里我并没有详细的列出每一个必须的步骤,因为我的目的只是让我们能够了解BIOS硬件芯片在系统中的作用。
对比PC的BIOS,嵌入式系统由于软件规模小,因此将引导代码和操作系统代码全部放到了系统的Flash芯片中了。正如我们所知道的,PC机上大部分的操作系统代码全部放在硬盘上,然后从硬盘上将程序载入内存执行。而嵌入式系统中目前大多数采用直接寻址的方式从Nor Flash芯片中读取代码并执行。因此,实际上嵌入式系统简化了PC系统的设计,将PC系统中的BIOS和硬盘代码全部集中到了一个Flash芯片上。因此BIOS虽然也可以使用Flash芯片,但是相对于嵌入式系统来说,他们的含义和作用却不同。
图1.2 PC开机流程
Flash芯片对于我们来说并不陌生,那些可以更新BIOS程序的BIOS芯片也是使用Flash芯片实现的,还有MP3用的SD卡等等也是Flash芯片。Flash芯片是一种可以多次擦写的存储芯片,广泛的应用于嵌入式系统。Flash的特点是耗电低,容量大(相对于嵌入式系统而言),写入之前需要先擦除(因为Flash芯片的存储单元只允许从1变到0)。当前流行的分为NOR Flash和NAND Flash。
NAND与NOR Flash的区别主要有:
1、NAND Flash的空间比Nor Flash大
2、NAND Flash的访问速度比Nor Flash快
3、NAND Flash只有Page访问模式,Nor Flash可以进行Page和直接地址访问(直接地址访问也就是CPU可以直接寻址,或者叫做随机访问)
4、NAND Flash允许有坏块,但是Nor Flash不能有坏块
5、NAND Flash比NOR Flash更加便宜
在嵌入式系统中,NOR 和NAND都可以做为代码区和文件系统区来使用。通常情况下NOR和NAND做为嵌入式文件系统区的时候都使用Page模式。Page模式允许一次读取多个字节,就像硬盘的最小读写单位是扇区一样,只不过Flash的最小读写单位叫做Page。Page模式下可以加快Flash的读写速度。由于NAND Flash只支持Page读写模式,因此使用NAND Flash做为代码区的时候需要外加控制电路。当前使用NAND做为代码区正在成为一种流行的趋势(因为NAND Flash成本更低),主要的实现方式有两种:一是添加仿真电路使得NAND Flash可以支持随机访问;二是增加一个类似硬盘的引导区(通常是第一个Page),系统启动的时候使用引导区的代码将全部NAND中的代码复制到RAM中执行。
当然,可以设想随着将来嵌入式操作系统的发展,动态载入内存的形式也许就会出现了,但是目前嵌入式系统仍然没有发展到这个地步。更进一步,当前应用于嵌入式系统的微型硬盘也已经出现了,或许更为复杂的操作系统也可以应用在嵌入式系统上了。
在这里我们主要介绍的ROM存储芯片是Flash,严格意义上说Flash并不能称作ROM,因为ROM是只读存储器(Read Only Memory),而Flash是一种可读可写的芯片。但是由于ROM“一次成型、终生不变”的特点,不便于升级换代,现在正逐渐的被Flash芯片所取代,但是其功能性的称谓“ROM”还在为大家所用。
在计算机系统中主要存在用户数据、程序数据和代码三种二进制内容。用户数据指用户文件,程序数据是指程序运行时需要修改或使用的非代码内容。下面将就PC系统和嵌入式系统中的这三种二进制内容做一个比较,请看下表:
|
二进制形态
|
PC系统
|
嵌入式系统
|
|
用户数据
|
存储在文件系统中,典型的设备是硬盘
|
存储在文件系统中,典型的设备是Flash存储芯片
|
|
程序数据
|
可读可写的数据存放在RAM中;只读数据存放在硬盘中,运行时与代码一起读入RAM
|
可读可写的数据存放在RAM中;只读数据存放在Flash中,与代码存储在同一个区域
|
|
代码
|
存储在文件系统中的文件里,运行时读入RAM由CPU执行
|
如果存储在NOR Flash等可随机访问的空间中则CPU直接在芯片中取指令运行;如果存储在NAND Flash等不能随机访问的空间中则需要读入RAM中运行
|
关于程序数据的详细情况将在编译器基础一节详细介绍。
输出设备有很多种,例如显示器、打印机,在这里我们主要讲一下显示设备。
任何显示设备都是点阵式的,至少目前是这样。还记得初次看见电视里显示的人物时内心的惊异,这个世界竟是如此神奇!后来知道了,如果我愿意,我可以买640*480个灯泡,组成一个640*480的方阵,然后控制每个灯泡的亮灭,我也可以显示一个人物。终于懂了,原来任何的显示设备都是基于这个原理实现的。我想关于这一点我没有必要再多说什么了,毕竟万变不离其宗嘛。
当前流行的显示设备有CRT显示器(也就是电脑上很大个头的那种显示器),LCD(液晶)等(虽然我在大学实验室里用的可以显示数字的LED灯也是显示设备,但是他太简单了)。CRT显示器经历了从球形到纯平的物理演变,LCD则经历了从黑白到彩色的变化。按照显示器每个点能够显示的颜色数目可以分为黑白两色、灰度、8位色、16位色、24位真彩色等显示设备。这个颜色数目就是所谓的色深(Color Depth),色深越大,每个点能够表达的颜色数就越多,这个点就是“象素”。
在嵌入式系统中主要使用LCD的显示设备,LCD会集成一个显示的存储空间,在这个空间中存储了对应的每个象素的值。例如16位色的LCD每个象素需要由2个字节来表示,如果显示屏幕的大小是100*100,那么就需要2*100*100 = 20000个字节的存储空间。程序正是通过更新这个存储空间中的内容来控制LCD的显示。
衡量LCD显示效果的还有象素间距,象素间距越小,画面就越细腻,显示效果越好。举个极端的例子,如果一个象素是整个LCD那么大,那就只能看见一个象素点的“灯泡”了,也就没法显示图像了。通过像素个数和每个点的大小就可以换算出显示屏的大小了,例如常用的xx英寸大小的屏幕等等。
输入设备也有很多种,最典型的是键盘和触摸屏。在这一节里,我将简单的介绍一下它们的实现原理。
键盘通常是一个矩阵式的电路,当按键按下的时候,接通电路产生信号,如图1.3:
图1.3 键盘原理
图1.3是一个简版的键盘原理图,图中任何交叉点的横向和纵向都未连通,并假设只要交叉点连通,则相应的行和列就可以连通并发生状态改变。其中1、2、3和A、B、C连接在控制芯片上,通过扫描行和列,确定行的A、B、C是否连通,再扫描列1、2、3是否连通,这样就可以唯一确定一个点是否按下。千万注意,这只是一个示意图,并不是真正的键盘原理图。
触摸屏是近来应用越来越多的输入器件。典型触摸屏的工作部分一般由三部分组成:两层透明的阻性导体层、两层导体之间的隔离层、电极。阻性导体层选用阻性材料,如铟锡氧化物(ITO)涂在衬底上构成,上层衬底用塑料,下层衬底用玻璃。隔离层为粘性绝缘液体材料,如聚脂薄膜。电极选用导电性能极好的材料(如银粉墨)构成。
触摸屏在工作时,上下导体层相当于电阻网络。当某一层电极加上电压时,会在该网络上形成电压梯度。如有外力使得上下两层在某一点接触,则在电极未加电压的另一层可以测得接触点处的电压,从而知道接触点处的坐标。比如,在顶层的电极(X+,X-)上加上电压,则在顶层导体层上形成电压梯度,当有外力使得上下两层在某一点接触,在底层就可以测得接触点处的电压,再根据该电压与电极(X+)之间的距离关系,知道该处的X坐标。然后,将电压切换到底层电极(Y+,Y-)上,并在顶层测量接触点处的电压,从而知道Y坐标。通常有专门的控制芯片。很显然,触摸屏的控制芯片要完成两件事情:一是完成电极电压的切换;二是采集接触点处的电压值(即A/D)。虽然还有其他的实现方法,我就不赘述了,因为本书的目的不是讲解硬件的原理,而仅仅是让我们能够基本了解它们。
在这一章里介绍了各种硬件的特性和原理,其中包括了最小系统各种组件的介绍,为的是防止我们见到这些东西的时候会一头雾水,不知来由。只要我们见到这些硬件不再感到神秘,那么这个基础就算打好了。
在PC计算机系统中硬盘、BIOS、RAM和嵌入式系统中的文件Flash、程序Flash、RAM之间有什么区别?它们在系统中的作用分别是什么?
Posted: January 13, 2009 at 1:26 pm | Tags: web
中国年轻学者呼吁抵制央视洗脑
中央电视台被指是官方控制的主要喉舌
22位中国学者、律师周一(12日)在互联网上联名发表一封抵制中央电视台( CCTV)洗脑的公开信。
这封名为《抵制央视,拒绝洗脑》的公开信的发起人凌沧州在接受本台BBC中文部的采访时介绍说,发表该公开信的目的是为了表明联合签署人对垄断性的信息传播和新闻传播的立场。
他说,如此多学者、律师联名对官控主要喉舌说不,在大陆几十年来属于首次,不仅反应了众多大陆公民、网民想表达而无处表达的心声,而且相信对人们鉴别谎言有一定作用。
央视劣行
公开信列举了中央电视台( CCTV)七大劣行,这包括CCTV在其节目中为三鹿鼓噪宣传,称其有1100道检测关;CCTV新闻节目对中国转型时期社会矛盾的事件报道采取的选择性失语策略,对许多突发性事件、群体事件不予以报道或者淡化处理。
公开信认为,CCTV的新闻联播节目几十年来风格、理念陈旧老套,在国内报道上经常报喜不报忧;在国际报道上经常报忧不报喜,与其称之为”新闻联播”不如正名为”宣传联播”更恰当。
如何看中国学者呼吁抵制央视洗脑?
它列举CCTV在伊拉克战争期间,其新闻节目中让所谓的军事专家为萨达姆吹嘘鼓噪,号称要打”人民战争 “,结果 “大漠穷秋公子毙,地洞衰颜独夫擒”–乌代、库赛双双被击毙,萨达姆逃亡躲藏被捉拿。
公开信还猛烈抨击了CCTV在黄金时段播出过大量的辫子戏,认为这些辫子戏不仅以其宫廷权谋、皇权专制、太监奴才的表演毒化了中国走向自由民主的氛围,而且灌输、打造许多人的奴才人格;更是为专制木乃伊文化美容,撕开历史的伤口,再一次伤害了被征服民族的感情。
四不策略
此外,公开信还批评了CCTV的所谓讲坛节目。认为节目上的专家信口雌黄,为文字狱屠夫康雍乾脸上贴金,扭曲历史真相,激起正直之士普遍的反感。
公开信说,”我们这群年轻的中国学人将集体抵制CCTV 的所谓”新闻节目”及其网络;将对CCTV的”新闻节目”和”宣传节目”及网络采取”不看、不上、不听、不说”的四不策略。”
去年,这几位年轻的中国学人曾经发表过《新春节文化宣言》, 提出抵制电视上庸俗而充满宣传说教的春节晚会,那些晚会越来越有把中国人传统温情的大年除夕夜或春节打造成愚民节的味道。
今年,他们再次表示:对垄断公共电视资源而污染视听的”新闻节目 “和”宣传节目”,有权保持抵制。

转自:http://www.daynew.net/?p=1121
Posted: January 12, 2009 at 9:03 pm | Tags: blog, debug, html, 优化, 开发, 技术
先说一个身边真正发生过的故事,可能大家会对故事情节感觉非常熟悉。
一家小型电子商务公司,管理者就是公司的创始人,他有些技术开发背景。因为产品面向海外市场,他也比较关注产品设计与可用性。由于他对自己的技术、设计能力的自信,总是对开发部与设计部成员的能力持怀疑态度,经常插手他们的工作。
一次,开发人员就新产品需求、设计文档的要求,提出开发方案A。这位管理者看后,以其经验判断认为不理想,并给出方案B。开发人员觉得方案B有些问题,但一时又没有找到有说服力的例证,无奈只好以此方案进行开发。
果然,在开发过程中,由于管理者仓促间所提出方案,在设计上确实存在不合理的疏漏,导致项目出现了一些问题。当开发人员想方设法将问题一一解决后,突然发现,这个DEBUG后的开发方案B,已经变得与他们最初提出的方案A并无二致。
而此时,该项目已经严重超期,那位管理者还为此非常不满,认为是开发人员能力的问题导致的。此后,管理者对开发工作的介入变得更多,而开发人员也开始变
化,一部分对领导的这种超权行为越来越不满意,矛盾变得更加激化,后面工作中的沟通变得更加困难;另一部分则变得越来越没有积极性,领导要求怎么做就怎么
做。
刚好看到UCDChina.com中的一篇文章《管理者不应直接参与产品的开发与设计》,感觉说得太有道理了。上面这个故事,可以用来充实文章的论据。
其实团队管理者关注团队产品的成败,是非常正常的心态。他们插手设计、开发工作,无非是希望把产品更得更好,动机是无庸质疑的。尽管如此,还是不建议管理
者过多的介入具体的开发与设计工作,即使是领域专家型的管理者,过多的插手具体设计、开发工作,也得不到预期的结果,可能对于项目来说,往往事与愿违,由
此产生的恶性干扰多于良性的指导,这是得不偿失的。
为什么会这样?原因很多:
1、因为管理者与团队成员的权重不同,有管理者参与的项目,往往不能非常公平地对待其它项目涉众。尽管,管理者本意是以其对项目的理解和自身的经验,为设
计、开发团队提供一些指导和建议,至少也会增加一种思路。但往往事与愿违,管理者介入后,一般会导致所有项目涉众的思维模式,会被管理者所左右,就因为他
是团队的管理者。
大家想想,是否经常遇到这样的情况,设计、开发人员深思熟虑后形成的方案,经常因为管理者的一点儿质疑,而被否决;而管理者的灵光一闪,都可能形成一个决
策,如果其它团队成员想到推翻它,必须经过周密、详实的举证与论述,这本就不公平,对项目也绝对没有好处,这会影响到团队成员的思路,甚至导致他们逐渐推
动思考的动力。
2、因为技术背景与对项目的关注点不同,管理者与设计、开发人员会处在不同层面,看问题角度肯定是不同的。其实,能从多个不同角度看待产品,这本来是好事,可以把问题考虑得更全面一些。但如果以管理者的角度,去干扰设计、开发中的问题,那就是另外一码儿事了。
《论语•泰伯》:“子曰:不在其位,不谋其政。”
这句话说得非常好,这不是推卸责任意思,而恰恰是出于更负责的角度考虑。为了便于理解,这里把“位”理解为“职位”。
每个职位有不同的职责,职责会影响人的立场,不同立场、知识背景的人理解、处理同一个问题,其结果肯定是不一样的。也许管理者认为自己可以做到“换位思考
”,即便如此也是搁靴搔痒的状态,其结果绝对差强人意。比如,管理者明白不能让UI设计师去考虑代码优化的问题,那又为什么自己去犯同样的错误呢?
正所谓术业有专攻,专业的工作还是让专业的人去做,那才可能达到令人满意的结果。
3、有人说,在IT行业,许多管理者都是优秀程序员或策划、设计师出身,这种领域专家型的管理者插手做具体设计、开发工作,应该有对项目、对团队成员有帮助。
其实不然,前面提到的两点问题,对于这样的管理者也依然存在。而且,文章开头提到的那位管理者也曾经是开发人员,本身也是位不错的产品设计师。为什么团队成员在他的指导下,项目进展依然不顺利呢?
从事过产品设计、程序开发工作的朋友应该很清楚,这样的工作需要投入大量的时间与精力,这是一个反复尝试与迭代的过程。好的设计与优秀的编码,绝对不是通
过纸上谈兵或灵光一闪就可以得到的(当然,这个世界是有天才存在的,也许他们拍拍脑袋就可以把产品设计得非常完美,程序编码也可以没有任何BUG,但毕竟
这种天才少得没有说服力)。有的时候,即使写好伪代码,也不能百之百的确保业务逻辑没问题。
而团队的管理者,往往事务缠身,他们没有精力(也不应该)深入于其中任何一项具体工作,那样就会犯下过于关注细节的管理大忌。而浅尝辄止的心态,恰恰又是做好设计与开发工作的大忌。这样看来,管理者兼顾设计、开发工作,并取得好的结果,根本是一项不可能完成的任务。
当然,领域专家型的管理者是可遇而不可求的,他丰富的经验与思路都是对团队成员有帮助的,但仅此而已,具体的设计、开发工作还是应该让设计师和程序员来做。
基于上面分析,如果管理者仍执意要插手设计与开发工作,就必须保证自己的观点(或决策)是100%准确与全面的,这样才不会对项目有负面影响。
当然,这样的要求是不合理的,也没有人敢做此承诺,所以,管理者们还去做好本职工作吧,那里才是发挥你们特长的地方。好的管理者,要懂得放权,要对团队成员应该有足够的信任,相信他们的工作能力,也相信他们的工作态度。正所谓:用人不疑,疑人不用。
如果管理者实在想参与设计、开发工作,那就干脆转行吧。
转自:http://blog.sina.com.cn/s/blog_564cabe30100bxmb.html
Posted: January 8, 2009 at 6:10 pm | Tags: blog
最近,38岁的毛新宇在任正师职不到两年的情况下,升任军事科学院战略研究部副部长,这个位置是副军级,快要成为中国最年轻的将军。
但必须说明的是,这一切都与毛泽东本人无关,因为毛泽东已经逝世三十多年了,所以谩骂的、愤恨的、夸奖的、炒作的请都不要往毛泽东身上靠。邵华也好,毛新宇也好,他们是独立的自然人,他们就是他们自己,他们不等于毛泽东,也不能代表毛泽东,也希望我们的媒体应当坚持正确的导向。当然,我们还是要向年轻英俊、智力超常的毛新宇表示祝贺!
毛新宇,1970年1月生于北京,湖南湘潭人。中共党员。1992年毕业于中国人民大学历史系,1992年9月至1995年7月在中央党校理论部攻读硕士学
位,后在军事科学院攻读博士学位,2003年7月份获得博士学位。历任中国房地产总公司总经理助理,中央党史研究室、军事科学院研究员,军事科学院世军部研究员,军事科学院战争理论和战略研究部正师职研究员。2008年任军事科学院战争理论和战略研究部副部长。大校军衔。2005年加入中国作家协会。
让我们不要吐,而要向年轻英俊、智力超常的毛新宇表示祝贺!(说你呢,还吐!)




(注意军服上的扣子…)
来源:苹果论坛
Posted: January 8, 2009 at 5:28 pm | Tags: html, 开发, 技术, 测试
这个文章是对产品经理比较合理的解释了,写得很务实。
——- 淫荡的分隔线 ———————————————
很多论坛(含此处)都在探讨何为产品经理,产品经理该干什么?
n多人也处于不明白产品经理为何物的蛮荒时代。
我本人从市场研究做起,后来是可用性测试,然后是产品设计师,再后来是产品经理,我自认为我对产品经理的理解强于大多数人,可以为你们解答疑惑。
背景:很多人的title都是产品经理,但是我要说的是真正意义上的产品经理,这种产品经理责任重大,能力超强,待遇超高。就算你目前不是这种产品经理,那么这也应该是你努力的方向。我下面说的产品经理指的都是这种产品经理
一、判断一下此人是不是产品经理
定义:产品经理,顾名思义,该人能够对产品负全责。
判断方法: 看指标、 看责任、看工作方式
1.看指标:以用户数(极个别时候用PV)作为考核指标,否则一定不是产品经理!产品的意义就在于留住用户,所以用户数是评价产品的最核心标准。你对产品所做的一切努力都会体现在用户数上。
2.看责任:产品经理需要对产品负全责。我举个例子,如果产品出现技术问题,比如奥运期间访问量大增,造成服务器负载过大,以至于当机,影响了用户访问,领导第一个骂谁?骂你?恭喜,你是一名真正的产品经理。产品经理显然应该了解服务器的最大载荷以及在中国各地的分布情况,网通和电信、铁通、校园网等链路的具体情况也理所当然的应该是产品经理的职责范畴。
3. 看工作方式:产品经理会一直运营一款(最多两款)产品,如果你看到一个人以项目的方式参与产品的某个阶段,工作完后就去做另一款产品 ,那么毫无疑问,此人非产品经理也。
二、产品经理的职责
对产品负全责,谁都会说。但是怎么才叫负全责呢?所谓的负全责是对“整个产品生命周期负责”。从市场调查、产品规划、概念设计、功能设计、产品逻辑设计、原型设计、交互设计、界面设计、技术环节的沟通、项目管理、产品上线、上线后的运营管理、产品推广、对外合作、产品改版升级……总之,从产品诞生开始一直到产品推出市场,再到市场运营,再到改版,知道产品退出市场。这一切都应该是由一名产品经理全权负责。他对产品的方方面面都很了解。一个PM在一款产品上做3、5年是很平常的。
三、产品经理的典型工作
我随便罗列一些我的日常工作吧,尽量按照产品生命周期写。
1. 规划阶段:竞品分析、产品整体规划
2. 设计阶段:产品一期概念设计、功能、交互、原型设计、技术可行性分析、可用性测试、形成需求文档
3. 开发阶段:项目排期、项目跟进、产品一期单元测试、产品一期上线
4.运营阶段:产品一期运营(内容运营、技术运营、运营人员工作安排,周末值班人员安排)、市场营销与推广(寻找合作机会,参与合作谈判——通常与市场部合作,签署合作法律文件——通常与公司法务部合作,监督合作推广的执行,分析推广效果ROI等等)、运营数据分析、一期改版意见、产品二期概念设计、产品二期需求文档草案
5. 产品二期设计阶段:产品二期需求文档、产品二期技术论证
6. 产品二期开发阶段 …….
…………
……..
…….
四、哪些职位被误认为是产品经理:
1. UE设计:这个东西最害人。产品设计师绝不是产品经理,请大家务必记住这一点。产品设计师管理的是设计过程,而产品经理管理的是整个产品的所有环节。目前有很多UE从业者——多是设计师——最容易将二者混淆
2. 项目经理:项目经理的职责是保证项目顺利按需求上线,别的不管。而产品经理要自始至终的管理一款产品。
3. 产品运营:运营是产品经理最主要的只能,但不是全部
五、产品经理的核心技能
“控制”是产品经理的核心技能。
要对产品的一切细节了然于胸,要对产品涉及到的方方面面有所了解,要能够控制产品团队(设计、技术、运营等一切环节),要有高超的沟通能力和技巧,要有极强的成功欲望和非常主动的做事态度。
六、总结
所以真正意义上的产品经理是很难得的,压力是很大的,待遇是很高的,人才是很稀有的:)
头顶产品经理title的人99%不具备产品经理的资质
也许你认为我写的这些要求太高了,但事实上一名合格的产品经理的确应该具备的基本素质。
希望大家共同努力,朝这个目标发展。
压力是巨大的,困难时巨大的,成就是巨大的:0
重申一遍,产品经理管理的是产品,不是人!
产品经理没有直接管理业务支持方(包括美工、技术、客服、市场)的权利,这是产品经理制度的管理学基础,这个是不能动摇的。如果产品经理有权利支配支持方,那是事业部制度。而事业部制度的核心是独立核算,自己是一个独立的利润中心 + 成本中心。但是产品经理制不是这样的,产品经理只是利润中心,不是成本中心。即便是有了产品管理团队,他们一般那也是和其他业务共用基础性资源:如美工、技术开发、市场合作等。但是不排除个别产品使用一些成本性资源,但是那不是常态。比如我们有一个产品招了个专职BD,但是大家都知道这个BD是临时归产品经理管,他最终还是会被市场部招安的。
原文地址:http://www.5gme.com/space-19213-do-thread-id-6631-page-1.html