免费视频淫片aa毛片_日韩高清在线亚洲专区vr_日韩大片免费观看视频播放_亚洲欧美国产精品完整版

打開APP
userphoto
未登錄

開通VIP,暢享免費(fèi)電子書等14項(xiàng)超值服

開通VIP
細(xì)說(shuō)程序運(yùn)行的環(huán)境和運(yùn)行過(guò)程再看不懂請(qǐng)自行面壁

(掌握底層技術(shù)能力的重要性如果你想進(jìn)階成為這個(gè)層次的工程師不能只學(xué)上層的語(yǔ)法而是要把計(jì)算機(jī)語(yǔ)言從上層的語(yǔ)法到底層的運(yùn)行機(jī)制都了解透徹)

https://m.toutiao.com/is/eRynJwa/ 

編譯器的任務(wù),是要生成能夠在計(jì)算機(jī)上運(yùn)行的代碼,但要生成代碼,我們必須對(duì)程序的運(yùn)行環(huán)境和運(yùn)行機(jī)制有比較透徹的了解。

你要知道,大型的、復(fù)雜一點(diǎn)兒的系統(tǒng),比如像淘寶一樣的電商系統(tǒng)、搜索引擎系統(tǒng)等等,都存在一些技術(shù)任務(wù),是需要你深入了解底層機(jī)制才能解決的。比如淘寶的基礎(chǔ)技術(shù)團(tuán)隊(duì)就曾經(jīng)貢獻(xiàn)過(guò),Java 虛擬機(jī)即時(shí)編譯功能中的一個(gè)補(bǔ)丁。

這反映出掌握底層技術(shù)能力的重要性,所以,如果你想進(jìn)階成為這個(gè)層次的工程師,不能只學(xué)學(xué)上層的語(yǔ)法,而是要把計(jì)算機(jī)語(yǔ)言從上層的語(yǔ)法到底層的運(yùn)行機(jī)制都了解透徹。

文本我會(huì)對(duì)計(jì)算機(jī)程序如何運(yùn)行,做一個(gè)解密,話題分成兩個(gè)部分:

  1. 了解程序運(yùn)行的環(huán)境,包括 CPU、內(nèi)存和操作系統(tǒng),探知它們跟程序到底有什么關(guān)系。

  2. 了解程序運(yùn)行的過(guò)程。比如,一個(gè)程序是怎么跑起來(lái)的,代碼是怎樣執(zhí)行和跳轉(zhuǎn)的,又是如何管理內(nèi)存的。

首先,我們先來(lái)了解一下程序運(yùn)行的環(huán)境。

程序運(yùn)行的環(huán)境

程序運(yùn)行的過(guò)程中,主要是跟兩個(gè)硬件(CPU 和內(nèi)存)以及一個(gè)軟件(操作系統(tǒng))打交道。

本質(zhì)上,我們的程序只關(guān)心 CPU 和內(nèi)存這兩個(gè)硬件。你可能說(shuō):“不對(duì)啊,計(jì)算機(jī)還有其他硬件,比如顯示器和硬盤啊?!钡珜?duì)我們的程序來(lái)說(shuō),操作這些硬件,也只是執(zhí)行某些特定的驅(qū)動(dòng)代碼,跟執(zhí)行其他代碼并沒(méi)有什么差異。

1. 關(guān)注CPU和內(nèi)存

CPU 的內(nèi)部有很多組成部分,對(duì)于本文來(lái)說(shuō),我們重點(diǎn)關(guān)注的是寄存器以及高速緩存,它們跟程序的執(zhí)行機(jī)制和優(yōu)化密切相關(guān)。

寄存器是 CPU 指令在進(jìn)行計(jì)算的時(shí)候,臨時(shí)數(shù)據(jù)存儲(chǔ)的地方。CPU 指令一般都會(huì)用到寄存器,比如,典型的一個(gè)加法計(jì)算(c=a+b)的過(guò)程是這樣的:

指令 1(mov):從內(nèi)取 a 的值放到寄存器中;指令 2(add):再把內(nèi)存中 b 的值取出來(lái)與這個(gè)寄存器中的值相加,仍然保存在寄存器中;指令 3(mov):最后再把寄存器中的數(shù)據(jù)寫回內(nèi)存中 c 的地址。

寄存器的速度也很快,所以能用寄存器就別用內(nèi)存。盡量充分利用寄存器,是編譯器做優(yōu)化的內(nèi)容之一。

而高速緩存可以彌補(bǔ) CPU 的處理速度和內(nèi)存訪問(wèn)速度之間的差距。所以,我們的指令在內(nèi)存讀一個(gè)數(shù)據(jù)的時(shí)候,它不是老老實(shí)實(shí)地只讀進(jìn)當(dāng)前指令所需要的數(shù)據(jù),而是把跟這個(gè)數(shù)據(jù)相鄰的一組數(shù)據(jù)都讀進(jìn)高速緩存了。這就相當(dāng)于外賣小哥送餐的時(shí)候,不會(huì)為每一單來(lái)回跑一趟,而是一次取一批,如果這一批外賣恰好都是同一個(gè)寫字樓里的,那小哥的送餐效率就會(huì)很高。

內(nèi)存和高速緩存的速度差異差不多是兩個(gè)數(shù)量級(jí),也就是一百倍。比如,高速緩存的讀取時(shí)間可能是 0.5ns,而內(nèi)存的訪問(wèn)時(shí)間可能是 50ns。不同硬件的參數(shù)可能有差異,但總體來(lái)說(shuō)是幾十倍到上百倍的差異。

你寫程序時(shí),盡量把某個(gè)操作所需的數(shù)據(jù)都放在內(nèi)存中的連續(xù)區(qū)域中,不要零零散散地到處放,這樣有利于充分利用高速緩存。這種優(yōu)化思路,叫做數(shù)據(jù)的局部性

這里提一句,在寫系統(tǒng)級(jí)的程序時(shí),你要對(duì)各種 IO 的時(shí)間有基本的概念,比如高速緩存、內(nèi)存、磁盤、網(wǎng)絡(luò)的 IO 大致都是什么數(shù)量級(jí)的。因?yàn)檫@都影響到系統(tǒng)的整體性能,也影響到你如何做程序優(yōu)化。如果你需要對(duì)程序做更多的優(yōu)化,還需要了解更多的 CPU 運(yùn)行機(jī)制,包括流水線機(jī)制、并行機(jī)制等等,這里就不展開了。

講完 CPU 之后,還有內(nèi)存這個(gè)硬件。

程序在運(yùn)行時(shí),操作系統(tǒng)會(huì)給它分配一塊虛擬的內(nèi)存空間,讓它在運(yùn)行期可以使用。我們目前使用的都是 64 位的機(jī)器,你可以用一個(gè) 64 位的長(zhǎng)整型來(lái)表示內(nèi)存地址,它能夠表示的所有地址,我們叫做尋址空間。

64 位機(jī)器的尋址空間就有 2 的 64 次方那么大,也就是有很多很多個(gè) T(Terabyte),大到你的程序根本用不完。不過(guò),操作系統(tǒng)一般會(huì)給予一定的限制,不會(huì)給你這么大的尋址空間,比如給到 100 來(lái)個(gè) G,這對(duì)一般的程序,也足夠用了。

在存在操作系統(tǒng)的情況下,程序邏輯上可使用的內(nèi)存一般大于實(shí)際的物理內(nèi)存。程序在使用內(nèi)存的時(shí)候,操作系統(tǒng)會(huì)把程序使用的邏輯地址映射到真實(shí)的物理內(nèi)存地址。有的物理內(nèi)存區(qū)域會(huì)映射進(jìn)多個(gè)進(jìn)程的地址空間。

對(duì)于不太常用的內(nèi)存數(shù)據(jù),操作系統(tǒng)會(huì)寫到磁盤上,以便騰出更多可用的物理內(nèi)存。

當(dāng)然,也存在沒(méi)有操作系統(tǒng)的情況,這個(gè)時(shí)候你的程序所使用的內(nèi)存就是物理內(nèi)存,我們必須自己做好內(nèi)存的管理。

對(duì)于這個(gè)內(nèi)存,該怎么用呢?

本質(zhì)上來(lái)說(shuō),你想怎么用就怎么用,并沒(méi)有什么特別的限制。一個(gè)編譯器的作者,可以決定在哪兒放代碼,在哪兒放數(shù)據(jù),當(dāng)然了,別的作者也可能采用其他的策略。實(shí)際上,C 語(yǔ)言和 Java 虛擬機(jī)對(duì)內(nèi)存的管理和使用策略就是不同的。

盡管如此,大多數(shù)語(yǔ)言還是會(huì)采用一些通用的內(nèi)存管理模式。以 C 語(yǔ)言為例,會(huì)把內(nèi)存劃分為代碼區(qū)、靜態(tài)數(shù)據(jù)區(qū)、棧和堆。

一般來(lái)講,代碼區(qū)是在最低的地址區(qū)域,然后是靜態(tài)數(shù)據(jù)區(qū),然后是堆。而棧傳統(tǒng)上是從高地址向低地址延伸,棧的最頂部有一塊區(qū)域,用來(lái)保存環(huán)境變量。

代碼區(qū)(也叫文本段)存放編譯完成以后的機(jī)器碼。這個(gè)內(nèi)存區(qū)域是只讀的,不會(huì)再修改,但也不絕對(duì)。現(xiàn)代語(yǔ)言的運(yùn)行時(shí)已經(jīng)越來(lái)越動(dòng)態(tài)化,除了保存機(jī)器碼,還可以存放中間代碼,并且還可以在運(yùn)行時(shí)把中間代碼編譯成機(jī)器碼,寫入代碼區(qū)。

靜態(tài)數(shù)據(jù)區(qū)保存程序中全局的變量和常量。它的地址在編譯期就是確定的,在生成的代碼里直接使用這個(gè)地址就可以訪問(wèn)它們,它們的生存期是從程序啟動(dòng)一直到程序結(jié)束。它又可以細(xì)分為 Data 和 BSS 兩個(gè)段。Data 段中的變量是在編譯期就初始化好的,直接從程序裝在進(jìn)內(nèi)存。BSS 段中是那些沒(méi)有聲明初始化值的變量,都會(huì)被初始化成 0。

適合管理生存期較長(zhǎng)的一些數(shù)據(jù),這些數(shù)據(jù)在退出作用域以后也不會(huì)消失。比如,我們?cè)谀硞€(gè)方法里創(chuàng)建了一個(gè)對(duì)象并返回,并希望代表這個(gè)對(duì)象的數(shù)據(jù)在退出函數(shù)后仍然可以訪問(wèn)。

而棧適合保存生存期比較短的數(shù)據(jù),比如函數(shù)和方法里的本地變量。它們?cè)谶M(jìn)入某個(gè)作用域的時(shí)候申請(qǐng)內(nèi)存,退出這個(gè)作用域的時(shí)候就可以釋放掉。

講完了 CPU 和內(nèi)存之后,我們?cè)賮?lái)看看跟程序打交道的操作系統(tǒng)。

2. 程序和操作系統(tǒng)的關(guān)系

程序跟操作系統(tǒng)的關(guān)系比較微妙:

一方面我們的程序可以編譯成不需要操作系統(tǒng)也能運(yùn)行,就像一些物聯(lián)網(wǎng)應(yīng)用那樣,完全跑在裸設(shè)備上。另一方面,有了操作系統(tǒng)的幫助,可以為程序提供便利,比如可以使用超過(guò)物理內(nèi)存的存儲(chǔ)空間,操作系統(tǒng)負(fù)責(zé)進(jìn)行虛擬內(nèi)存的管理。

在存在操作系統(tǒng)的情況下,因?yàn)楹芏噙M(jìn)程共享計(jì)算機(jī)資源,所以就要遵循一些約定。這就仿佛辦公室是所有同事共享的,那么大家就都要遵守一些約定,如果一個(gè)人大聲喧嘩,就會(huì)影響到其他人。

程序需要遵守的約定包括:程序文件的二進(jìn)制格式約定,這樣操作系統(tǒng)才能程序正確地加載進(jìn)來(lái),并為同一個(gè)程序的多個(gè)進(jìn)程共享代碼區(qū)。在使用寄存器和棧的時(shí)候也要遵守一些約定,便于操作系統(tǒng)在不同的進(jìn)程之間切換的時(shí)候、在做系統(tǒng)調(diào)用的時(shí)候,做好上下文的保護(hù)。

所以,我們編譯程序的時(shí)候,要知道需要遵守哪些約定。因?yàn)榫退闶鞘褂猛瑯拥?CPU,針對(duì)不同的操作系統(tǒng),編譯的結(jié)果也是非常不同的。

好了,我們了解了程序運(yùn)行時(shí)的硬件和操作系統(tǒng)環(huán)境。接下來(lái),我們看看程序運(yùn)行時(shí),是怎么跟它們互動(dòng)的。

程序運(yùn)行的過(guò)程

你天天運(yùn)行程序,可對(duì)于程序運(yùn)行的細(xì)節(jié),真的清楚嗎?

1. 程序運(yùn)行的細(xì)節(jié)

首先,可運(yùn)行的程序一般是由操作系統(tǒng)加載到內(nèi)存的,并且定位到代碼區(qū)里程序的入口開始執(zhí)行。比如,C 語(yǔ)言的 main 函數(shù)的第一行代碼。

每次加載一條代碼,程序都會(huì)順序執(zhí)行,碰到跳轉(zhuǎn)語(yǔ)句,才會(huì)跳到另一個(gè)地址執(zhí)行。CPU里有一個(gè)指令寄存器,里面保存了下一條指令的地址。

假設(shè)我們運(yùn)行這樣一段代碼編譯后形成的程序:

int main(){ int a = 1; foo(3); bar();}int foo(int c){ int b = 2; return b+c;}int bar(){ return foo(4) + 1;}

我們首先激活(Activate)main() 函數(shù),main() 函數(shù)又激活 foo() 函數(shù),然后又激活 bar()函數(shù),bar() 函數(shù)還會(huì)激活 foo() 函數(shù),其中 foo() 函數(shù)被兩次以不同的路徑激活。

我們把每次調(diào)用一個(gè)函數(shù)的過(guò)程,叫做一次活動(dòng)(Activation)。每個(gè)活動(dòng)都對(duì)應(yīng)一個(gè)活動(dòng)記錄(Activation Record),這個(gè)活動(dòng)記錄里有這個(gè)函數(shù)運(yùn)行所需要的信息,比如參數(shù)、返回值、本地變量等。

目前我們用棧來(lái)管理內(nèi)存,所以可以把活動(dòng)記錄等價(jià)于棧楨。棧楨是活動(dòng)記錄的實(shí)現(xiàn)方式,我們可以自由設(shè)計(jì)活動(dòng)記錄或棧楨的結(jié)構(gòu),下圖是一個(gè)常見的設(shè)計(jì):

  • 返回值:一般放在最頂上,這樣它的地址是固定的。foo() 函數(shù)返回以后,它的調(diào)用者可以到這里來(lái)取到返回值。在實(shí)際情況中,我們會(huì)優(yōu)先通過(guò)寄存器來(lái)傳遞返回值,比通過(guò)內(nèi)存?zhèn)鬟f性能更高。

  • 參數(shù):在調(diào)用 foo 函數(shù)時(shí),把參數(shù)寫到這個(gè)地址里。同樣,我們也可以通過(guò)寄存器來(lái)傳遞,而不是內(nèi)存。

  • 控制鏈接:就是上一級(jí)棧楨的地址。如果用到了上一級(jí)作用域中的變量,就可以順著這個(gè)鏈接找到上一級(jí)棧楨,并找到變量的值。

  • 返回地址:foo 函數(shù)執(zhí)行完畢以后,繼續(xù)執(zhí)行哪條指令。同樣,我們可以用寄存器來(lái)保存這個(gè)信息。

  • 本地變量:foo 函數(shù)的本地變量 b 的存儲(chǔ)空間。

  • 寄存器信息:我們還經(jīng)常在棧楨里保存寄存器的數(shù)據(jù)。如果在 foo 函數(shù)里要使用某個(gè)寄存器,可能需要先把它的值保存下來(lái),防止破壞了別的代碼保存在這里的數(shù)據(jù)。這種約定叫做被調(diào)用者責(zé)任,也就是使用寄存器的人要保護(hù)好寄存器里原有的信息。某個(gè)函數(shù)如果使用了某個(gè)寄存器,但它又要調(diào)用別的函數(shù),為了防止別的函數(shù)把自己放在寄存器中的數(shù)據(jù)覆蓋掉,要自己保存在棧楨中。這種約定叫做調(diào)用者責(zé)任。

你可以看到,每個(gè)棧楨的長(zhǎng)度是不一樣的。

用到的參數(shù)和本地變量多,棧楨就要長(zhǎng)一點(diǎn)。但是,棧楨的長(zhǎng)度和結(jié)構(gòu)是在編譯期就能完全確定的。這樣就便于我們計(jì)算地址的偏移量,獲取棧楨里某個(gè)數(shù)據(jù)。

總的來(lái)說(shuō),棧楨的設(shè)計(jì)很自由。但是,你要考慮不同語(yǔ)言編譯形成的模塊要能夠鏈接在一起,所以還是要遵守一些公共的約定的,否則,你寫的函數(shù),別人就沒(méi)辦法調(diào)用了。

在之前的文章中我提到過(guò)棧楨,這次我們用了更加貼近具體實(shí)現(xiàn)的描述:棧楨就是一塊確定的內(nèi)存,變量就是這塊內(nèi)存里的地址。在下一講,我會(huì)帶你動(dòng)手實(shí)現(xiàn)我們的棧楨。

2.從全局角度看整個(gè)運(yùn)行過(guò)程

了解了棧楨的實(shí)現(xiàn)之后,我們?cè)賮?lái)看一個(gè)更大的場(chǎng)景,從全局的角度看看整個(gè)運(yùn)行過(guò)程中都發(fā)生了什么。

代碼區(qū)里存儲(chǔ)了一些代碼,main 函數(shù)、bar 函數(shù)和 foo 函數(shù)各自有一段連續(xù)的區(qū)域來(lái)存儲(chǔ)代碼,我用了一些匯編指令來(lái)表示這些代碼(實(shí)際運(yùn)行時(shí)這里其實(shí)是機(jī)器碼)。

假設(shè)我們執(zhí)行到 foo 函數(shù)中的一段指令,來(lái)計(jì)算“b+c”的值,并返回。這里用到了mov、add、jmp 這三個(gè)指令。mov 是把某個(gè)值從一個(gè)地方拷貝到另一個(gè)地方,add 是往

某個(gè)地方加一個(gè)值,jmp 是改變代碼執(zhí)行的順序,跳轉(zhuǎn)到另一個(gè)地方去執(zhí)行(匯編命令的細(xì)節(jié),我們下節(jié)再講,你現(xiàn)在簡(jiǎn)單了解一下就行了)。

mov b的地址寄存器1add c的地址寄存器1mov寄存器1 foo的返回值地址jmp返回地址//或ret指令

執(zhí)行完這幾個(gè)指令以后,foo 的返回值位置就寫入了 6,并跳轉(zhuǎn)到 bar 函數(shù)中執(zhí)行 foo 之后的代碼。

這時(shí),foo 的棧楨就沒(méi)用了,新的棧頂是 bar 的棧楨的頂部。理論上講,操作系統(tǒng)這時(shí)可以把 foo 的棧楨所占的內(nèi)存收回了。比如,可以映射到另一個(gè)程序的尋址空間,讓另一個(gè)程序使用。但是在這個(gè)例子中你會(huì)看到,即使返回了 bar 函數(shù),我們?nèi)砸L問(wèn)棧頂之外的一個(gè)內(nèi)存地址,也就是返回值的地址。

所以,目前的調(diào)用約定都規(guī)定,程序的棧頂之外,仍然會(huì)有一小塊內(nèi)存(比如 128K)是可以由程序訪問(wèn)的,比如我們可以拿來(lái)存儲(chǔ)返回值。這一小段內(nèi)存操作系統(tǒng)并不會(huì)回收。

我們目前只講了棧,堆的使用也類似,只不過(guò)是要手工進(jìn)行申請(qǐng)和釋放,比棧要多一些維護(hù)工作。

總結(jié)

本文帶你了解了程序運(yùn)行的環(huán)境和過(guò)程,我們的程序主要跟 CPU、內(nèi)存,以及操作系統(tǒng)打交道。你需要了解的重點(diǎn)如下:

  • CPU 上運(yùn)行程序的指令,運(yùn)行過(guò)程中要用到寄存器、高速緩存來(lái)提高指令和數(shù)據(jù)的存取效率。

  • 內(nèi)存可以劃分成不同的區(qū)域保存代碼、靜態(tài)數(shù)據(jù),并用棧和堆來(lái)存放運(yùn)行時(shí)產(chǎn)生的動(dòng)態(tài)數(shù)據(jù)。

  • 操作系統(tǒng)會(huì)把物理的內(nèi)存映射成進(jìn)程的尋址空間,同一份代碼會(huì)被映射進(jìn)多個(gè)進(jìn)程的內(nèi)存空間,操作系統(tǒng)的公共庫(kù)也會(huì)被映射進(jìn)進(jìn)程的內(nèi)存空間,操作系統(tǒng)還會(huì)自動(dòng)維護(hù)棧。

  • 程序在運(yùn)行時(shí)順序執(zhí)行代碼,可以根據(jù)跳轉(zhuǎn)指令來(lái)跳轉(zhuǎn);棧被劃分成棧楨,棧楨的設(shè)計(jì)有一定的自由度,但通常也要遵守一些約定;棧楨的大小和結(jié)構(gòu)在編譯時(shí)就能決定;在運(yùn)行時(shí),棧楨作為活動(dòng)記錄,不停地被動(dòng)態(tài)創(chuàng)建和釋放。

以上這些內(nèi)容就是一個(gè)程序運(yùn)行時(shí)的秘密。你再面對(duì)代碼時(shí),腦海里就會(huì)想象出它是怎樣跟CPU、內(nèi)存和操作系統(tǒng)打交道的了。而且有了這些背景知識(shí),你也可以讓編譯器生成代碼,按照本文所說(shuō)的模式運(yùn)行了!

本站僅提供存儲(chǔ)服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊舉報(bào)。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
我們按下電腦開機(jī)鍵的背后發(fā)生了什么?
操作系統(tǒng)內(nèi)存
操作系統(tǒng)中任務(wù)是怎么切換的
程序中的代碼段、數(shù)據(jù)段、堆、棧是怎么回事
沒(méi)有操作系統(tǒng)時(shí),人們是如何操控計(jì)算機(jī)的?
c/c++語(yǔ)言學(xué)習(xí)痛苦的根源指針(一句話就搞懂)
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長(zhǎng)圖 關(guān)注 下載文章
綁定賬號(hào)成功
后續(xù)可登錄賬號(hào)暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服