探索之路
zero
大学学计算机理论知识时,常常有种莫名的失败感,倒不是因为考试不及格;而是被前所未有的迷惑困扰。比如说,我不知道计算机是如何的去执行我写的代码,也不知道计算机是抱着什么样的心思去看我那些笨拙的代码的;我不知道我写的那些代码是如何的去与我所看到的界面打交道;不知道为什么能够上网浏览网页;我甚至不知道软件开发到底是怎么回事;我只知道,我一边圈圈点点一边写些似乎很合逻辑的代码,然后编译,然后我的电脑就莫名其妙地跑起来了,再然后我就什么也不知道了。迷惘,愤怒,生气都没有,唯有鼓起勇气,开始探索之路,方是解决之道。计算机虽显得神秘和不可思议,可是经过努力探索之后,还是能找到点眉目的;笔者如今把一些心得体会写出来,希望能对刚涉及计算机的师弟师妹有所帮助,同时也希望高手们批评指点。
一、计算机如何的执行代码
大家都知道,程序代码,实质上就是一系列指令,一些预先设计好的命令序列。比如说,用户想知道两数字相加的结果;那么作为程序员的我们,在windows下可能就需事先设计好一个界面,供用户输入相加的两个数,然后我们可能还需要提供一个执行按钮,待用户按下按钮后就把结果输出给用户看。从显示界面到输出结果,这些行为,都是我们利用代码去命令计算机去做的,这就算是小小的程序设计了。如果用C语言来描述两个数字相加的行为大概是这样子“Z=X+Y;”,只可惜计算机太笨了,不知道“Z=X+Y;”是什么意思,计算机能识别的只有机器语言 ,那何为机器语言呢?可以这样理解:我们知道,在数字电路里面可以控制电流的高低,如果我们把电流的高低有目的地串地来,并用二进制形式的符号串来表示高低电流串,比如1表示高0表示低,这样就可以形成二进制数据;现在,我们就可以根据二进制数据的不同(实际上是电流的变化况不同),来表示计算机不同的行为,比如可用"111111"来表示两个数字相加,“1111110”表示两个数字相减等;于是,计算机的制造者就可以用二进制数据(实际上是电流的变化)为计算机规定一系列的行为(命令);同样的,为我们要处理的数据也用两进制数据来表示,这就行成了机器语言;再进一步,我们用易于理解的字符串为二进制命令和二进制数据起个名字,使易于理解的字符串与二进制形式机器语言一一对应起来,这就形成了汇编语言;比如说用add表示"111111"两数相加。而现在我所用C语言属于更高级别的语言,他使我们命令计算机为我们办事变得更容易;他不像机器语言与汇编语言一样一一对应;而是利用编译软件把C语言描述的形为转化成用机器语言来描述(语义相等),好让计算机能识别。也就是说,无论是C/C++还是Pascal语言,对于同一台机器来说,最终生成的都是同一种机器(或汇编)语言,只是语法不同而已。这样一来,高级语言还有一个好处,就是机器无关性,这使我们写的程序能正确地运行于不同的产商生产的计算机上,而不必为每种机器重新写一套程序;当然,前提是计算机产商要为这种高级语言编写编译程序以便生成相应的机器语言。
上面提到,C++语言需依据语义编译形成机器语言(或汇编语言),才能为计算机所识别,那么C++的语法如何去表示语义呢?在这里,我简单举个例子介绍一下,有兴趣了解常情的朋友可以看,候捷译的《深度探索C++对象模型》。比如我定如下一个类:
Class Test
{
private:
long a;
long b;
public:
long GetAValue();
long GetBValue();
};
long Test::GetAValue()
{
return a;
};
long Test::GetBValue()
{
return b;
};
有如下代码:
Test* pTest;
pTest=new Test;
lgTemp=pTest->GetAValue();
那么Test* pTest,语义为,定义了一个Test类型的指针,如果这个指针指向数据的话,那么它就能合法访问它所指向的连续八个字节的数据(long型占四个字节内存,Test类中有两个long 型成员所以占用八个字节)。pTest=new Test,系统动态分配八个字节的内存,并使Test类指针pTest指向这片新分配的内存段(注:并没有为成员函数分配内存)。lgTemp=pTest->GetAValue()的语义即为:调用Test的成员函数GetAValue(),并把结果保存在lgTemp中。这个函数返回结果给lgTemp可能有两种内部实现方式(编译器会对我们的代码做些手脚,以方便转换为机器语言);一是:
pTest->GetAValue(lgTemp,this){ lgTemp=this->a; }
二是:long lgAdd_Temp;
pTest->GetAValue(lgAdd_Temp,this){ lgAdd_Temp=this->a; }
lgTemp=lgAdd_Temp;
这里还有一个问题,new Test时并没有生成GetAValue的函数地址,我们如何找到 GetAValue并调用他?答案是,编译器会生成一张与作用域相关的表,在调用GetAValue之前我们已经知道pTest是一个Test类对象,编译器会先在作用域表中找到Test类的位置再找到GetAValue函数的地址(函数名字与函数地址一一对应)。详细情况可看编译原理,笔者也记不太清楚了。
总结一下,计算机执行处理我们写的代码的过程大概是这样子:把我们写的代码编译成可执行文件;执行可执行文件,操作系统创建一个新进程,并为这个进程分配资源,如内存空间;把可执行文件中的代码调入进程内存空间中的代码段;找到程序的入口点,调用主函数开始执行程序;执行过程中需调用动态链接库时,在进程空间内动态为之分配内存,并把之调入,动态库自己管理新分配的内存释放;为程序中的局部变量或全局变量在栈上分配内存,但无需我们写代码去管理这部分内存;我们自己写代码在堆上动态分配的内存,则需自己写代码控制这部分内存的释放,否则内存泄漏;完成我们的任务,释放为进程分配的资源,退出程序。
二、数据管理的烦恼
CPU能做的工作其实非常的单纯,我们所写的程序最终都将变成顺序排好队的一条条机器指令;CPU每次只是读入一条指令,按指令中数据段的内容找到此条指令要处理的数据,然后按指令的要求处理之并得到结果。其中,一条指令要处理的数据可以来自己内存,硬盘,寄存器等等,指令处理后的数据也可以放在内存,硬盘,寄存器等处;一条指令能做的事非常有限,为了让机器能为我们做有意议的事,我们必需去控制与管理指令与指令之间关系(比如说我们使一条指令的执行结果做为下一条指令的处理对象),使之共同合作实现我们想要的功能。通常,当我们想计算机为我们服务时,就会去执行一个应用程序,一个应用程序就代表了一系列的命令的集合以及待处理的数据的集合;为了方便指令与指令之间密切合作,我们创建一个进程并为这个进程分配一个非常大的逻辑上的进程地址空间,CPU可以轻松地访问在这个地址范围内的数据,因为实际上内存并没有那样的巨大,所以实际分配了内存的物理地址空间只是进程地址空间中的一小部分;“一系列的命令的集合以及待处理的数据的集合”就是导入这片分配了内存的地址内存空间中,并且这片地址空间最好是连续和有序的,以方便CPU存取数据与指令。CPU的指令还好办,大小不会变,但CPU操作的数据往往是动态分配的,并且经常改变而难以捉摸,因此通常会遇到很多烦恼。比如说,在内存地址空间1000--1030处存放着一个字符串strA,在地址1032--1050处存放着另一字符串strB;CPU依据地址1000找到这符串strA,在程序的执行过程中,我们可能要增加strA的内容,比如增加10个字符,但因为地址1032处已存在另一字符串,如果把这些新增字符追加在1030后势必复盖掉strB的数据,这是我们不想看到的;有两个解决方案可提供,一是为strA重新分配一个足够大的内存并使操作strA的指针指向这新分配的地址,原先的1000--1030就得释放以供以后使用,二是在原先为strA分配内存时多分配一些,以便当要增长strA时可在后面追加,但这就浪费了些内存。当程序执行过程中,出现大量这种数据变动的行为,如何管理内存中数据就变得非常的头痛。可能就因为这个问题,进程地址空间中人为地分成的不会改变长度的代码段地址空间,和经常改变的堆栈段地址空间等区域,以方便管理数据。对程序员而言,最关注的就是堆栈段数据的变化情况,并用尽办法去管理去控制他,以保证编写的程序高效稳定。
对于管理磁盘中的数据也遇到了类似的问题。比如说,我有三个文本文件A,B和C,大小都为10M,B在A与C空间的中间,并且假设三个文件连续放在30M的磁盘空间中。我们经常会对文件进行的操作有,修改文件中的某一段数据(文件因此可能变长与变短),向文件头或文件尾添加数据等等;而我们对磁盘数据的操作是通过磁盘指针的读写数据来完成的,磁盘读写数据需要移动指针,磁盘指针的移动(特别是磁道间的移动)相对来说非常地慢,因此为提高读写数据的速度的最好的办法是把要读数据放在一片连续的数据磁盘空间中,或把要写的数据写在一片连续的地址空间中以避免磁盘指针频繁的移动。那么,当我修改文件B时,可能遇到三种情况,一是文件变小,二是文件变大,三是大小不变。对于文件变小,当我们只是删除(修改)文件尾时很好办,不会遇到什么麻烦事,可是当我们修改文件中某一小段时,甚至只是删除了一个字符,我们将遇到很多麻烦:因为文件中间少了一些数据,为了保证数据的连续性,我们可以把修改处以后的数据都往前移,但如修改处在离文件头近处的话,我们将需要移动近10M的数据(想想,我只是删除一个字符而已,你就要我移动10M的数据,太不公平了吧!),当然,我们不是非得要让数据连续不可的。对于文件变大,就更麻烦了,因为B文件前有A后有C,已经没有多余的空间让他变大了,所以,只能够在磁盘中另外分配一块足够大的连续空间来存放了(即使文件只增加了一个字符的大小),原来B处空间也就因没有用了而被回收。这里还有一个小小问题,当B文件的空间被回收后,在重用这片空间时,如果另一文件的大小为11M,那么这片空间存不下;如果为5M,就可能导致浪费了5M,当然,剩余5M还可以用来存放小于5M的文件。好了,管理磁盘数据时遇到的问题已列出一些来了,现在让我们发挥我们超常的想像力,想想我们平时对文件的所作所为,然后再想想为了满足我们的为所欲为计算机所要做的工作,很恐怖是吧?对于上面所遇到的问题,这里有一个折衷的方案,就是把文件切割成一个个小小的数据块,其中每个数据块内的数据是连续的,然后用一条链把这些数据块链起来;那么当我们修改文件中的数据时,通常只需修改一个数据块就能满足需求了,而不需对整个文件进行修改,之后把修改过后的数据块重新连接上数据文件的链表中就行了。在操作系统文件管理中,这样的一个数据块,听说好像是1K的大小,太大与太小都不好。现代数据库技术中,也有数据块概念的,不同的是,数据库做得更绝,数据块中除了有用的数据外,还在块中多留了一些空闲空间,以方便数据块内数据的增长;如oracle中有两个参pct_user, pct_free就是用来控制这个空闲空间的。当然,为了效率和简单,并非所有的操作系统都愿意把文件分割的,笔者知道有个叫Mach的网络操作系统,文件中的数据绝对是连续存放的,代价是,不允许你对文件进行修改,你如果非要修改,只能以先删除原文件再创建一个同名文件的方式实现。
计算机管理数据时所遇到的问题远不止上面这些。在这里再举一个例子:当我们要处理的数据量非常非常的大,数据将因此变得混乱,我们如何使这些数据有条有理起来?又如何快速地找到我们想要的数据呢?这时,操作系统的文件管理系统已不能满足需求了,我们需要一种新技术,数据库管理技术。数据库管理技术比文件管理系统更加灵活地对我们要处理的数据进行划分归类和更能体现要处理的数据相互之间的关系;如在MS SQLServer中就对要处理的数据划分为数据库,数据表,记录等等几个层次来管理,数据库中的表与表之间,表中的记录与记录之间都有一定的关系。当然,要使数据库这项伟大的技术发挥应有作用,还需我们用户配合使用才行。对于数据库管理数据时遇到的问题,在这里就不详说了,有兴趣的朋友推荐去看《数据库原理、编程与性能》,机械出版,里面对性能的考虑很到位。
三、程序与用户的交流
一台家用电脑,由CPU,内存,主板,硬盘,鼠标,键盘,显示器,光驱,软驱等组成。
我们通过键盘与鼠标向计算机传达命令,而计算机即通过显示器把我们想要的结果显示给我们看;一却似乎都是那么的顺其自然,只是不知大家有没有想过,计算机是怎么看待我们这些行为的?鼠标,键盘,显示器又是如何的扮演他们的角色的?其实,在计算机看来,我们的操作,都在他(正确来说是在编写程序的程序员)的预料当中;而当我们做了些出乎他预料之外的事时,他就无法正确运行,要么报个错,要么干脆死掉。而鼠标,键盘,显示器这些设备只是我们与计算机间的通信工具。现在,让我们看看鼠标,键盘,显示器,能够为我们提供一些什么样的服务,他们各自承担了什么样的责任以及怎么与计算机交流的。
鼠标,鼠标能够帮助计算机在显示器定位一个位置,并以这个位置为热点,通过热点的移动CPU就可精确定位显示器的每一个像素点;我们如果为显示器屏幕建一套直角坐标系统的话,鼠标的功能就表现为计算机提供一个坐标值(X,Y);鼠标移动时(X,Y)值也相应跟着变动,以表示热点在屏幕的移动。鼠标还能够为计算机提供左右键单击,双击等几个不同的信号;如果和屏幕上的热点的坐标值结合起来就可以让计算机知道我们在屏幕的那个位置上对鼠标进行了何种操作;这就是鼠标的责任,也是他所能做到的事。而键盘,可以为计算机提供一百多个不同的信号,并为这些信号规定不同的意思,通过这些信号不同的组合来与计算机交流。显示器呢,无它,只是显示一些漂亮的界面给我们看,图形化、形象地反应我们用鼠标与键盘对计算机的操作,以方便我们去控制计算机而已。在计算机与显示器的交互中,我们可以利用API函数在屏幕上画图,写字;无论是画图还是写字,目的只是为了方便用户与计算机交流,至于这些字,这些图的信息从那儿来的,怎么来的,显示器并没有兴趣知道。
我们还知道,Windows 是一个“基于事件的,消息驱动的”操作系统;也就说,我们只能通过向操作系统发送消息而使系统运行起来。那什么是事件,什么是消息呢?我们在Windows下执行一个程序,通常会在显示器中看到一个个的窗口,只要我们利用鼠标或键盘,对窗口有所动作(如改变窗口大小或移动、单击鼠标或按下键盘一个键等),该动作就是一个“事件”,系统每次检测到一个事件时,就会给程序发送一个“消息”,从而使程序执行相应的代码来处理该事件。于是我们的编程工作就变成了,为每一个事件,每一个消息,写一段代码去处理这个消息或这个事件,而把这些响应消息的代码有目的地组织起来,就形成了一个个应用程序。这里还有一个小小的问题,刚才提到过,改变窗口的大小会产生一个消息,而显示器只负责显示,其它的一概不管,那么消息是如何产生,又是何时产生的呢?当我们用鼠标左键单击窗口右上角的最大化按钮,窗口就变大了,消息就是在这个时候产生的了,就在我们按下鼠标的那一刻!在Windows下,为了方便我们编写程序,通常会对键盘,鼠标产生的消息进行一些包装处理;比如说,我们也把窗口大小改变当作一个消息来对代,这里我们假设用一个函数FunSize来封装处理这个事件;这样一来,当产生鼠标左击最大化按钮消息时,可以调用函数FunSize来处理(可看做一个消息激活另一个消息),在窗口边当产生鼠标按下并拖动窗口的消息时,也可以调用FunSize来处理,这样做的好处是显然的。
好了,现在总结一下,鼠标,显示器,CPU在窗口大小改变这件事上是怎么分工合作的:当鼠标移到一个窗口的最大化按钮上并左击时,鼠标马上告诉CPU两样信息,当前鼠标的位置(X,Y),当前鼠标的动作(左键单击);因为产生新的消息,CPU中断当前的工作;然后根据鼠标的位置(X,Y)在内存中查看反映屏幕的画面的那一部分数据,看看(X,Y)这个位置目前属于那个顶层窗口,然后向这个窗口发送消息;通过查看内存数据,这时,CPU还知道了,鼠标目前正好移到窗口的最大化按钮上,并且按下了左键,所以知道了他要做的事,最大化窗口;知道任务后,CPU马上工作,重新调整窗口数据,使之满足窗口最大化显示的需求;然后调用API画图函数,重画整个窗口;然后,我们就可以看屏幕上看到最大化后的窗口了。
四、通信
我在广州上大学时,当发现口袋里没钱,就会打电话回家,向爸爸伸手要钱;当然,我也可以选择写信回家,叫爸爸寄点钱过来;然后爸爸就把钱存在我的银行帐户里,然后我就可到银行取钱来用了。不知大家有没有想过,从没钱、叫爸爸寄钱、从银行取钱这一系列行为得以顺利进行的原因?为了叫爸爸寄钱过来,我必需事先知道家里的电话号码,或者家庭地址,这样我才有办法联系上爸爸;其次,我还得懂说粤语,懂听粤语,爸爸也得懂粤语,这样我打通电话后才能正常交流;或者,如果是写信的话,我得懂写汉字,爸爸得识汉字。这就是我们生活中的通信情况,那么计算机之间的通信又是怎么样子的呢?
其实,计算机之间的通信与我和爸爸之间通信的情况很相似。如果把寄钱这件事换作计算机来描述,可能是这个样子。我是客户机,爸爸是服务器,家里的电话号码和地址就是服务器的IP地址及端口号;银行的账号就是客户机的IP地址及端口号;打电话就是选择TCP/IP通信协议通信,写信就是选择UDP/IP通信协议通信;汉字与粤语就是服务器与客户机内部定义的通信协议,钱是服务器处理后的输出数据。我爸爸并非总是呆在家里的,他还得上班工作,可是,也不能不回家,如果他不回家的话,打电话没人听,写信没人接收,那么我就没法叫他寄钱了,所以,他必需得隔段时间回家一趟才得;相似的,服务器也得监听端口,每隔一段时间看看有没有新任务到。寄钱,我通常选择打电话这种通信方式,因为方便快捷;可是,如果想和爸爸说点真心话,描述下学校生活,打电话的话常常会语塞,如果写信的话,我就有充足的时间去组织语言了;由此可见,打电话与写信这两种通信方式各有所长。相似的,TCP/IP协议建立的是可靠的面向连接的通信协议,他保证了数据传输的可靠与正确,但服务器如果同时为N个客户服务的话,就需同时维护N个连接,当N很大时,这个代价就会很高;而UDP/IP通信协议对于同时为N个客户代价就比较轻(不需同时维护N个连接),可是不能保证数据的可靠与正确;因此,也是各有所长。由此可见,现实生活中的通信,与计算机间的通信,是何其的相似!在计算机通信中遇到的问题,现实中几乎都有相应的影子;如果我们的脑袋能够转过弯来的话,说不定还能把现实中解决问题的办法用来解决计算机间的通信难题呢。
通过上述的描述,细心的朋友,大概已察觉一个的问题;现实生活的通信交流,之所以能正常进行,是因为--知识!通信双方都懂的知识!想想,当我们刚出生时,不会说话,肚子饿了连做个手势指指肚子表示饿了都不懂,这时与妈妈唯一的通信方式,只能哭了,好让妈妈想起够时间喂奶了。后来,我们学会了说话,还学会了走路,打架,肚子饿时,还学会偷东西吃,被妈妈捉到了还会说谎为自己辩护。现在,我们全国推行普通话,如果只懂粤语的话,到了北京、西藏的话,和当地人无法交谈,日子定然不好过;幸好,我也学会了普通话,可以方便地同当地同胞交流,问路,买东西都不成问题。可是英语差劲的我,如果到美国去的话,大家大概就能猜到我将如到的麻烦了。所以,为了全世界人民能正常交流,在全世界范围内普及英语(天呀!为什么不是普通话!)显得是那么的重要。知识,是我们人类社会通信的基础;那么在计算机世界时,通信的基础又是什么呢?协议,通信协议!从电平的高低变化开始,创造出一套套世界范围认可的不同层次不同应用的协议,形成了机器语言,形成了数据;然后是汇编,C,C++,SQL等等,正是这一套套的协议的出现,一套套标准的制定,构造出现在多姿多彩的现代计算机世界。
结语
“路漫漫其修远兮,吾将上下而求索”,具然选择了走程序员这条路,就得做好充分的心理,接受层出不穷的技术的挑战,承担创新的责任;朋友一起上路,多一个朋友,少一分寂寞,多一份收获。
2003.6.13