探索之路
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的大小,太大與太小都不好?,F代數據庫技術中,也有數據塊概念的,不同的是,數據庫做得更絕,數據塊中除了有用的數據外,還在塊中多留了一些空閑空間,以方便數據塊內數據的增長;如oracle中有兩個參pct_user, pct_free就是用來控制這個空閑空間的。當然,為了效率和簡單,并非所有的操作系統都愿意把文件分割的,筆者知道有個叫Mach的網絡操作系統,文件中的數據絕對是連續存放的,代價是,不允許你對文件進行修改,你如果非要修改,只能以先刪除原文件再創建一個同名文件的方式實現。
計算機管理數據時所遇到的問題遠不止上面這些。在這里再舉一個例子:當我們要處理的數據量非常非常的大,數據將因此變得混亂,我們如何使這些數據有條有理起來?又如何快速地找到我們想要的數據呢?這時,操作系統的文件管理系統已不能滿足需求了,我們需要一種新技術,數據庫管理技術。數據庫管理技術比文件管理系統更加靈活地對我們要處理的數據進行劃分歸類和更能體現要處理的數據相互之間的關系;如在MS SQL Server中就對要處理的數據劃分為數據庫,數據表,記錄等等幾個層次來管理,數據庫中的表與表之間,表中的記錄與記錄之間都有一定的關系。當然,要使數據庫這項偉大的技術發揮應有作用,還需我們用戶配合使用才行。對于數據庫管理數據時遇到的問題,在這里就不詳說了,有興趣的朋友推薦去看《數據庫原理、編程與性能》,機械出版,里面對性能的考慮很到位。
三、程序與用戶的交流
一臺家用電腦,由CPU,內存,主板,硬盤,鼠標,鍵盤,顯示器,光驅,軟驅等組成。
我們通過鍵盤與鼠標向計算機傳達命令,而計算機即通過顯示器把我們想要的結果顯示給我們看;一卻似乎都是那么的順其自然,只是不知大家有沒有想過,計算機是怎么看待我們這些行為的?鼠標,鍵盤,顯示器又是如何的扮演他們的角色的?其實,在計算機看來,我們的操作,都在他(正確來說是在編寫程序的程序員)的預料當中;而當我們做了些出乎他預料之外的事時,他就無法正確運行,要么報個錯,要么干脆死掉。而鼠標,鍵盤,顯示器這些設備只是我們與計算機間的通信工具?,F在,讓我們看看鼠標,鍵盤,顯示器,能夠為我們提供一些什么樣的服務,他們各自承擔了什么樣的責任以及怎么與計算機交流的。
鼠標,鼠標能夠幫助計算機在顯示器定位一個位置,并以這個位置為熱點,通過熱點的移動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個連接),可是不能保證數據的可靠與正確;因此,也是各有所長。由此可見,現實生活中的通信,與計算機間的通信,是何其的相似!在計算機通信中遇到的問題,現實中幾乎都有相應的影子;如果我們的腦袋能夠轉過彎來的話,說不定還能把現實中解決問題的辦法用來解決計算機間的通信難題呢?!?BR> 通過上述的描述,細心的朋友,大概已察覺一個的問題;現實生活的通信交流,之所以能正常進行,是因為--知識!通信雙方都懂的知識!想想,當我們剛出生時,不會說話,肚子餓了連做個手勢指指肚子表示餓了都不懂,這時與媽媽唯一的通信方式,只能哭了,好讓媽媽想起夠時間喂奶了。后來,我們學會了說話,還學會了走路,打架,肚子餓時,還學會偷東西吃,被媽媽捉到了還會說謊為自己辯護?,F在,我們全國推行普通話,如果只懂粵語的話,到了北京、西藏的話,和當地人無法交談,日子定然不好過;幸好,我也學會了普通話,可以方便地同當地同胞交流,問路,買東西都不成問題??墒怯⒄Z差勁的我,如果到美國去的話,大家大概就能猜到我將如到的麻煩了。所以,為了全世界人民能正常交流,在全世界范圍內普及英語(天呀!為什么不是普通話?。╋@得是那么的重要。知識,是我們人類社會通信的基礎;那么在計算機世界時,通信的基礎又是什么呢?協議,通信協議!從電平的高低變化開始,創造出一套套世界范圍認可的不同層次不同應用的協議,形成了機器語言,形成了數據;然后是匯編,C,C++,SQL等等,正是這一套套的協議的出現,一套套標準的制定,構造出現在多姿多彩的現代計算機世界。
結語
“路漫漫其修遠兮,吾將上下而求索”,具然選擇了走程序員這條路,就得做好充分的心理,接受層出不窮的技術的挑戰,承擔創新的責任;朋友一起上路,多一個朋友,少一分寂寞,多一份收獲。
2003.6.13