進程
創(chuàng)建
? ? 創(chuàng)建進程用fork()函數(shù)小作。fork()為子進程創(chuàng)建新的地址空間并且拷貝頁表。子進程的虛擬地址空間和父進程是相等的,子進程的物理內(nèi)存與父進程是共用的悬嗓。但是,此時物理內(nèi)存中的數(shù)據(jù)是只讀的裕坊。fork()函數(shù)使用寫時拷貝包竹,即一旦有其中一個人去寫數(shù)據(jù)x,那么發(fā)生缺頁異常籍凝,系統(tǒng)會新建一個物理頁來存儲獨立的數(shù)據(jù)x并修改這個人的頁表映企,指向新的物理頁。這時静浴,父子之間的數(shù)據(jù)x就不再共享了堰氓。
? ? 創(chuàng)建進程還有一個函數(shù),即vfork()苹享。vfork()不拷貝父進程的地址空間和頁表双絮,而是直接在父進程的地址空間里執(zhí)行浴麻。所以,對vfork()中執(zhí)行的數(shù)據(jù)進行更改的時候囤攀,則會修改父進程的數(shù)據(jù)软免,因為父子進程的地址空間是完全共享的。于此同時父進程被堵塞焚挠,直到vfork()的_exit()或者exec*()調(diào)用之后膏萧。
? ??exec*()創(chuàng)建新的地址空間并且把新的程序拷貝進來。
????進程通過exit()函數(shù)退出蝌衔,或者是有特殊情況而被動地終結(jié)榛泛,無論如何,都要調(diào)用do_exit()來執(zhí)行實際操作噩斟。do_exit()釋放大部分資源曹锨,執(zhí)行完do_exit()以后,進程處于ZOMBIE狀態(tài)剃允,即成為僵尸進程沛简,且還擁有三項資源:(內(nèi)核棧、task_struct斥废、thread_info)椒楣。父進程調(diào)用wait4()掛起并詢問子進程是否死亡,如果有孩子死了牡肉,那么調(diào)用release_task()回收上述三項資源撒顿。如果父進程在子進程死之前死了,那么子進程變成孤兒進程荚板,必須要在當前線程組找一個父親凤壁,如果找不到那么用init進程當父親。
????線程共享:(地址空間跪另、files_struct拧抖、fs_struct、信號處理函數(shù))免绿。最重要的是唧席,Linux中進程和線程的本質(zhì)區(qū)別就在于是否共享父進程的地址空間。內(nèi)核線程沒有獨立的地址空間嘲驾,而是在內(nèi)核空間里執(zhí)行淌哟。線程共享同一個files_struct,即線程共享進程描述符表辽故。而父子進程不共享進程描述符表徒仓,只是父子進程的進程描述符表在fork()之后是一樣的。父子進程共享進程描述符指向的struct file誊垢。
表示
? ? 進程用struct task_struct結(jié)構(gòu)體來描述掉弛,每一個進程(或線程)對應(yīng)一個task_struct對象症见。Linux通過slab來分配task_struct。為了找到task_struct殃饿,內(nèi)核還有一個結(jié)構(gòu)體struct thread_info谋作,thread_info里面有一個指針指向task_struct,同時也含有一些關(guān)于進程的信息乎芳。thread_info放在內(nèi)核棧的最高處遵蚜,由這個特性,我們就可以經(jīng)由thread_info快速找到進程的task_struct奈惑。我們討論在現(xiàn)在的語境下有必要介紹的task_struct的數(shù)據(jù)成員吭净。task_struct.state存儲進程的五種狀態(tài),分別是:RUNNING携取、INTERRUPTIBLE攒钳、UNINTERRUPTIBLE帮孔、TRACED雷滋、STOPPED。task_struct.parent指向父進程文兢,task_struct.children是子進程的鏈表晤斩。task.cpu_allowed來指定進程特定的cpu,這樣進程可以強制綁定到當前的cpu姆坚。
? ? current宏代表當前正在執(zhí)行的進程澳泵,current的存在是必要的,有些硬件體系結(jié)構(gòu)寄存器比較富裕兼呵,可以有一個專門的寄存器存放當前的task_struct的首地址兔辅,而有的硬件體系結(jié)構(gòu)則只能經(jīng)由thread_info的指針找到。由軟件工程基本定理知击喂,加入一層抽象可以解決任何問題维苔。所以current宏可以隱藏這種硬件的不一致,讓不同的硬件都發(fā)揮相應(yīng)的效能懂昂。當一個進程執(zhí)行系統(tǒng)調(diào)用或者觸發(fā)了異常的時候介时,進程陷入內(nèi)核空間,內(nèi)核代替進程執(zhí)行凌彬,此時內(nèi)核執(zhí)行于進程上下文中沸柔,current指向當前進程,尤為有用铲敛。而在中斷時褐澎,內(nèi)核代替硬件執(zhí)行,與被打斷的進程沒有任何關(guān)系伐蒋,處于中斷上下文中乱凿,current沒有用處顽素。
調(diào)度
? ? Linux有兩種不同的優(yōu)先級范圍,一種是nice徒蟆,-20~19胁出;一種是實時優(yōu)先級,0~99段审。任何實時優(yōu)先級都優(yōu)先于nice普通優(yōu)先級全蝶。進程的調(diào)度通過調(diào)度器類,一般進程用CFS(即完全公平調(diào)度)調(diào)度器類寺枉,實時進程用實時調(diào)度器類抑淫。
? ??CFS之所以叫完全公平調(diào)度,是因為它在一定的時間粒度上是完全公平的姥闪。在一個調(diào)度周期里面始苇,權(quán)重比就是運行時間的比,并且每個進程的加權(quán)增長速率都是相同的筐喳。CFS選取vruntime最小的進程來運行催式,所以根據(jù)vruntime為鍵,內(nèi)核把進程組織成一個紅黑樹來方便選取這個最小值避归。其中荣月,分配給進程的運行時間 = 調(diào)度周期 * 進程權(quán)重 / 所有進程權(quán)重之和。vruntime = 實際運行時間 * 1024 / 進程權(quán)重梳毙。每個CPU的運行隊列cfs_rq都維護一個min_vruntime字段哺窄,記錄該運行隊列中所有進程的vruntime最小值,新進程的初始vruntime值就以它所在運行隊列的min_vruntime為基礎(chǔ)來設(shè)置账锹,與老進程保持在合理的差距范圍內(nèi)萌业。內(nèi)核調(diào)用__pick_next_entity()來運行紅黑樹最小的節(jié)點。進程調(diào)度的函數(shù)接口是schedule()奸柬,它的調(diào)用層次為schedule() -> pick_next_task() -> pick_next_entity()生年。
????休眠的進程移出紅黑樹,通過add_wait_queue()加入到等待隊列鸟缕,當wake_up()調(diào)用以后晶框,進程調(diào)用try_to_wake_up()來嘗試蘇醒,如果蘇醒的進程優(yōu)先級高于正在運行的懂从,那么設(shè)置need_resched標志授段,指示schedule()重新調(diào)度。只要內(nèi)核返回用戶空間番甩,就需要檢查need_resched標志侵贵,如果設(shè)置了那么必須調(diào)用schedule()進行調(diào)度,這叫用戶搶占缘薛。而于此同時窍育,只要當前的任務(wù)不帶有鎖卡睦,即thread_info.preempt_count為0,那么這個任務(wù)也可以被內(nèi)核搶占漱抓。注意表锻,need_resched保存在thread_info中,這樣訪問比把need_resched當成全局變量要快乞娄。schedule()也負責進程的切換瞬逊,調(diào)用層次是schedule() -> context_switch()。其中context_switch()分別調(diào)用switch_mm()切換地址空間仪或,調(diào)用switch_to()切換進程的狀態(tài)并保存上一個進程的信息确镊。
? ??實時調(diào)度有兩種策略,一種是FIFO范删,高優(yōu)先級執(zhí)行完再執(zhí)行低優(yōu)先級蕾域,同優(yōu)先級之間保持先進先出;一種是RR到旦,高優(yōu)先級執(zhí)行完再執(zhí)行低優(yōu)先級旨巷,同優(yōu)先級之間保持時間片輪轉(zhuǎn)。進程還可以調(diào)用sched_yield()來轉(zhuǎn)讓給其他進程執(zhí)行厢绝,自己移動到過期隊列契沫。如果是實時進程調(diào)用sched_yield()的話带猴,則移動到優(yōu)先級隊列的最后面昔汉。
中斷
原理
? ? 中斷是硬件設(shè)備產(chǎn)生的一種特殊的電信號,通過總線發(fā)送給中斷控制器拴清,如果中斷線是激活的靶病,那么中斷控制器把中斷發(fā)送給cpu,如果cpu沒有禁止中斷口予,那么cpu立刻停止當前正在做的工作娄周,禁止中斷,并且執(zhí)行中斷處理程序沪停。執(zhí)行中斷處理程序最后總是要調(diào)用do_IRQ()煤辨,do_IRQ()首先禁止這個中斷線,然后調(diào)用handle_IRQ_event()運行這條中斷線上的所有處理程序木张。中斷是硬件產(chǎn)生的众辨,是異步的,不考慮系統(tǒng)時鐘舷礼,而異常則是cpu自己產(chǎn)生的鹃彻,和時鐘同步。每一個中斷都有唯一的中斷號妻献,稱為中斷線蛛株,中斷線可以共享团赁。一個設(shè)備的中斷處理程序是其驅(qū)動程序的一部分,中斷處理程序被內(nèi)核調(diào)用來響應(yīng)中斷谨履。驅(qū)動程序通過request_irq()注冊中斷處理程序欢摄,卸載中斷處理程序則通過free_irq(),如果中斷線不是共享的笋粟,那么刪除處理程序之后禁用中斷線剧浸,如果共享的話僅當刪除的是當前中斷線的最后一個處理程序時,才禁用中斷線矗钟。中斷處理程序運行在中斷上下文中唆香,代替硬件執(zhí)行硬件相關(guān)的程序,打斷了當前正在執(zhí)行的進程吨艇,所以要盡快且不能睡眠躬它。為了快速從中斷處理程序中退出,Linux把中斷分成了兩部分东涡,一部分是上半部冯吓,即中斷處理程序;一部分是下半部疮跑。
系統(tǒng)調(diào)用
? ? 有一種特殊的中斷是系統(tǒng)調(diào)用组贺,它不是硬件產(chǎn)生的,而是進程調(diào)用int 0x80觸發(fā)的祖娘。這條指令產(chǎn)生一個異常失尖,讓系統(tǒng)切換到內(nèi)核態(tài)去執(zhí)行0x80號(即8 * 16 = 128號)中斷處理函數(shù),這個函數(shù)叫做system_call()渐苏。所有的系統(tǒng)調(diào)用存放在一個表里掀潮,叫做sys_call_table,內(nèi)核通過表的索引值來查找應(yīng)該調(diào)用哪一個系統(tǒng)調(diào)用琼富,這個索引就叫做系統(tǒng)調(diào)用號仪吧。系統(tǒng)調(diào)用的調(diào)用約定是:系統(tǒng)調(diào)用號通過eax寄存器來傳遞。系統(tǒng)調(diào)用參數(shù)通過五個寄存器鞠眉,即ebx ecx edx esi edi來傳遞薯鼠。如果參數(shù)多于五個,那么ebx存放所有參數(shù)在內(nèi)存中的起始地址械蹋。系統(tǒng)調(diào)用的返回值存儲在eax中出皇。注意,系統(tǒng)調(diào)用執(zhí)行在進程上下文中朝蜘,它代替進程執(zhí)行恶迈,并且寄存器的初始值來源于進程,所以處在進程的環(huán)境里。current有效暇仲,并且可以休眠步做,可以被搶占。
中斷棧
? ? 一個進程的所有地址空間可以分為用戶空間(0~3G)和內(nèi)核空間(3G~4G)奈附,這兩個空間都有相應(yīng)的棧全度,分別叫用戶棧和內(nèi)核棧。內(nèi)核棧一般是兩頁的大小斥滤。中斷原來是執(zhí)行在內(nèi)核棧里面的将鸵,但是現(xiàn)在有一個可選的選項,即把內(nèi)核椨悠模縮小成一頁顶掉,并且存在一個中斷棧,也是一頁挑胸,每個處理器一個痒筒。
中斷狀態(tài)
? ? 想要得知中斷的狀態(tài)有三個函數(shù)接口,irq_disable()得知本地處理器中斷是否禁止茬贵,in_interrupt()得知內(nèi)核是否處于中斷簿透,in_irq()得知內(nèi)核是否在執(zhí)行中斷處理程序。
中斷處理程序
? ? 中斷處理程序(即上半部分)一般只處理必須要在中斷上下文中的解藻、和硬件關(guān)系非常緊密的操作老充,例如對中斷的確認和從硬件拷貝數(shù)據(jù)。做完就立刻返回螟左,而其他的操作交給下半部分做啡浊。
中斷下半部
????下半部分可以有多種實現(xiàn)方法,現(xiàn)存的有:(軟中斷路狮、tasklet虫啥、工作隊列)蔚约。
? ? 軟中斷是靜態(tài)定義的奄妨,有32個。實際上軟中斷就是一個struct softirq_action softirq_vec[32]數(shù)組苹祟,其中softirq_action就是每一個軟中斷砸抛,其中有一個函數(shù)指針。唯一可以打斷軟中斷執(zhí)行的就是中斷處理程序树枫。有一個位圖pending來表示軟中斷數(shù)組的每一位是否置位直焙。軟中斷的執(zhí)行的實際操作最后都依賴do_softirq()函數(shù),它遍歷軟中斷砂轻,根據(jù)pending是否置位來決定執(zhí)行哪些軟中斷奔誓。目前只有兩個系統(tǒng)使用軟中斷,即網(wǎng)絡(luò)和SCSI。
????使用軟中斷的話厨喂,首先要注冊和措,即調(diào)用open_softirq()函數(shù)。啟動軟中斷有兩個時機蜕煌。一派阱、在中斷處理程序中,可以調(diào)用raise_softirq()設(shè)置相應(yīng)的pending位斜纪,在硬中斷的中斷處理程序中的do_IRQ()里面會會調(diào)用irq_exit()贫母,這個函數(shù)會判定是否有pending的某一位置位,如果有那么調(diào)用invoke_softirq()盒刚,invoke_softirq()調(diào)用__invoke_softirq()腺劣,__invoke_softirq()調(diào)用__do_softirq()遍歷軟中斷數(shù)組執(zhí)行pending置位的位相應(yīng)的軟中斷。二因块、在非中斷上下文中調(diào)用rasie_softirq_irqoff()函數(shù)設(shè)置相應(yīng)的pending位誓酒,然后喚醒內(nèi)核線程ksoftirqd/n()執(zhí)行軟中斷(和tasklet),ksoftirqd/n優(yōu)先級最低贮聂,為19靠柑。
? ? tasklet是動態(tài)的,是用軟中斷來實現(xiàn)的吓懈。實際上tasklet就是一個struct tasklet_struct鏈表歼冰。tasklet用struct tasklet_struct來表示,其中有tasklet_struct.count來代表tasklet是否被禁止耻警。tasklet_struct.func指向處理函數(shù)隔嫡。tasklet_struct.data是傳給處理函數(shù)的參數(shù)。tasklet_struct.next用來聚合成鏈表甘穿。tasklet_schedule()或tasklet_hi_schedule()把tasklet_struct添加到兩個鏈表之一(即tasklet_vec或tasklet_hi_vec)并且分別置位TASKLET_SOFTIRQ(軟中斷號是5)或HI_SOFTIRQ(軟中斷號是0)兩個軟中斷腮恩。這兩個軟中斷的處理程序分別是tasklet_action()和tasklet_hi_action(),他們遍歷相應(yīng)的鏈表温兼,執(zhí)行鏈表上所有的tasklet秸滴。
????使用tasklet的話,可以調(diào)用DECLARE_TASKLET或DECLARE_TASKLET_DISABLED靜態(tài)創(chuàng)建tasklet募判,也可以調(diào)用tasklet_init()來動態(tài)創(chuàng)建tasklet荡含。然后就可以在中斷處理程序中調(diào)用tasklet_schedule()或tasklet_hi_schedule()來把tasklet添加到兩個鏈表其中一個并置位軟中斷。
? ? 工作隊列把推后的工作交給內(nèi)核線程去執(zhí)行届垫,也就是說释液,工作隊列是在進程上下文中執(zhí)行的,可以參與調(diào)度装处,可以睡眠误债。工作隊列實際上就是內(nèi)核線程。Linux有一個默認的工作隊列,用struct workqueue_struct來表示寝蹈,其中有一個struct cpu_workqueue_struct[N_CPU]數(shù)組糟袁,即每一個CPU都對應(yīng)一個struct cpu_workqueue,這個結(jié)構(gòu)體有一個worklist鏈表躺盛,鏈表的每一個節(jié)點都代表一個需要執(zhí)行的中斷下半部项戴。每一個處理器都有一個默認的工作內(nèi)核線程叫events/n,來執(zhí)行worklist中的所有中斷下半部函數(shù)槽惫。每一個內(nèi)核線程調(diào)用worker_thread()函數(shù)周叮,這個函數(shù)執(zhí)行一個死循環(huán)并且休眠,如果有新的函數(shù)加入到了worklist里面界斜,那么就喚醒執(zhí)行仿耽,沒有操作了就睡眠。
????使用工作隊列的話各薇,可以調(diào)用DECLARE_WORK()靜態(tài)創(chuàng)建work_struct项贺,也可以用INIT_WORK()動態(tài)創(chuàng)建。可以在硬中斷處理程序中調(diào)用schedule_work(&work)把工作交給events/n峭判,函數(shù)會立馬被執(zhí)行开缎。也可以通過schedule_delayed_work(&work, delay)來指定delay。flush_scheduled_work()來保證工作隊列的所有對象都被執(zhí)行完才會返回林螃,可以用于一些同步的情況奕删。
時鐘
時鐘
? ? 全局變量jiffies是記錄自系統(tǒng)啟動以來所產(chǎn)生的節(jié)拍總數(shù),是unsigned long volatile類型的變量疗认,volatile指示編譯器每次訪問變量都從內(nèi)存中獲得完残,而不是從寄存器中來訪問。jiffies = HZ * seconds横漏。每次執(zhí)行時鐘中斷處理程序都會增加jiffies谨设。內(nèi)核有兩個jiffies變量,一個叫jiffies缎浇,一個叫jiffies_64扎拣,其中jiffies = jiffies_64,即在32位機器华畏,jiffies是jiffies_64的后32位鹏秋,而64位機器上,這兩個值等價亡笑。32位jiffies在1000HZ情況下50天溢出,100HZ情況下500天溢出横朋。而64位不可能溢出仑乌。為了解決溢出帶來的問題,內(nèi)核使用四個宏解決比大小,即:time_after()晰甚,time_before()衙传,time_after_eq(),time_before_eq()厕九。
? ? 實時時鐘(RTC)是用來持久存放時間的設(shè)備蓖捶,即使設(shè)備關(guān)機后仍然可以依靠主板上的電池維持自己。一般RTC和CMOS是集成在一起的扁远。當系統(tǒng)啟動時俊鱼,內(nèi)核讀取RTC初始化墻上時間,該墻上時間存儲在xtime中畅买。xtime是struct timespec{ tv_sec, tv_nsec }類型的并闲,tv_sec是從1970.1.1至今的秒數(shù),tv_nsec是ns數(shù)谷羞。讀寫xtime要用xtime_lock()鎖帝火,這時一個seq鎖(即順序鎖)。用戶取得墻上時間用gettimeofday()湃缎。
定時器
? ? 定時器有兩種犀填,一種是系統(tǒng)定時器,一種是動態(tài)定時器嗓违。
????系統(tǒng)定時器是硬件提供的宏浩,以一定的頻率(即節(jié)拍率)自行觸發(fā)時鐘中斷,然后內(nèi)核去執(zhí)行時鐘中斷處理程序靠瞎。兩次時鐘中斷間隔時間即是節(jié)拍比庄,節(jié)拍等于節(jié)拍率分之一。x86默認時鐘節(jié)拍率是100HZ乏盐。而在2.5內(nèi)核中佳窑,時鐘節(jié)拍率提高到了1000HZ,提高節(jié)拍率的好處是更高的頻度和準確度父能,而壞處是系統(tǒng)負擔變重了神凑,時鐘中斷處理程序頻繁打斷進程并占用處理器,打亂了處理器的高速緩存并且增加了耗電何吝。時鐘處理程序需要做的事情非常多溉委,其中包括:設(shè)置各種時鐘值、調(diào)用體系結(jié)構(gòu)無關(guān)的tick_periodic()爱榕。tick_periodic()做了很多瓣喊,包括:jiffies_64加一、更新各種值黔酥、置位軟中斷的pending的第1位(動態(tài)定時器)藻三、執(zhí)行scheduler_tick()洪橘、更新xtime墻上時間、計算負載棵帽。其中scheduler_tick()減少進程的時間片熄求,并且在時間片用光的時候設(shè)置need_resched標志,以及平衡各個處理器上的運行隊列逗概。
????動態(tài)定時器不是周期執(zhí)行的弟晚,而是使得任務(wù)能夠在指定的時間執(zhí)行。動態(tài)定時器由struct timer_list表示逾苫,是一個鏈表卿城。使用動態(tài)定時器要先定義并初始化,即:struct timer_list my timer; init_timer(&my_timer)隶垮。注意藻雪,init_timer()只初始化系統(tǒng)內(nèi)部的變量,timer_list.expires狸吞,timer_list.data勉耀,timer_list.function都需要再手動設(shè)定。注意蹋偏,my_timer.expires是超時時刻便斥,是一個時間點捧请,所以一般這樣賦值:my_timer.expires = jiffies + delay囤锉。這樣定義和初始化階段就完成了荠耽。還需要激活timer鸳劳,調(diào)用add_timer(&my_timer)。還可以用mod_timer(&my_timer)來更改激活或者未激活(如果未激活歧强,mod_timer()會把它激活)的動態(tài)定時器窄驹。如果要刪除一個定時器要調(diào)用del_timer()缘揪,注意已經(jīng)超時的會自行刪除脓斩,所以這里有一個競爭條件木西,即調(diào)用del_timer()的時候已經(jīng)刪除,但是定時器中斷已經(jīng)在別的處理器上執(zhí)行了随静,del_timer()卻直接返回八千。而del_timer_sync()則等到當前的定時器中斷執(zhí)行完才返回,這個函數(shù)不能在中斷上下文使用燎猛。一般情況下應(yīng)該使用del_timer_sync()恋捆,很保險。動態(tài)定時器是依靠軟中斷來實現(xiàn)的重绷,軟中斷號是TIMER_SOFTIRQ(即是1)沸停。動態(tài)定時器的執(zhí)行是作為時鐘中斷處理函數(shù)的下半部分來執(zhí)行的。時鐘中斷處理程序會執(zhí)行update_process_times()论寨,該函數(shù)調(diào)用run_local_timers()星立,該函數(shù)調(diào)用raise_softirq()來設(shè)置pending的第1位爽茴。TIMER_SOFTIRQ對應(yīng)的軟中斷處理程序是run_timer_softirq()葬凳,這個函數(shù)在當前處理器上遍歷timer_list鏈表绰垂,運行所有的超時定時器。為了提高效率火焰,timer_list的鏈表分為5組劲装,當超時時間接近時,定時器隨著組一起下移昌简。
延遲執(zhí)行
? ? 延遲執(zhí)行除了動態(tài)定時器和下半部機制以外(實際上動態(tài)定時器就是時鐘中斷處理程序的下半部)占业,還有:忙等待、短延遲纯赎、schedule_timeout()谦疾。
內(nèi)存
物理頁
? ? 物理頁(也叫頁框、頁幀犬金、page frame)把內(nèi)存(DRAM)分為一頁一頁的大小來管理念恍,頁框是內(nèi)存管理的基本單位。大多數(shù)32位機器里面晚顷,頁框是4KB峰伙,而64位的頁框大多是8KB。內(nèi)核用struct page來表示物理頁该默。內(nèi)核用struct page來管理每一頁框的原因是瞳氓,內(nèi)核需要知道每個頁框的詳細信息。內(nèi)核用alloc_pages()來分配2^n個頁框栓袖,返回指向第一個頁框結(jié)構(gòu)體struct page的指針匣摘。內(nèi)核用page_address()把頁框轉(zhuǎn)換成邏輯地址。分配頁框和釋放頁框還有好多函數(shù)裹刮,不過都是基于alloc_pages()音榜。
內(nèi)存分區(qū)
? ? 內(nèi)核把頁框(即內(nèi)存)分為不同的區(qū),Linux主要有四種內(nèi)存分區(qū)必指,即:DMA囊咏、DMA32、NORMAL塔橡、HIGHEM梅割。每一個區(qū)域用struct zone來表示。分區(qū)的原因是葛家,每個進程有它獨立的地址空間户辞,地址空間在32位機器上是4GB,而內(nèi)存可以大于4GB癞谒,所以地址空間不能和內(nèi)存進行一一映射底燎。如果要想充分利用內(nèi)存刃榨,就一定要在地址空間和內(nèi)存兩個集合中分別預(yù)留出來一部分來做非永久的映射,這樣地址空間就能訪問到所有內(nèi)存了双仍。
? ??一般來說枢希,物理內(nèi)存中0~16M是DMA區(qū)域,16M~896M的是NORMAL區(qū)域朱沃,896M以上的內(nèi)存就都是HIGHEM的內(nèi)存區(qū)域了苞轿。
????而對于x86-64這種64位機器來說,地址空間高達無數(shù)逗物,基本上都可以保證內(nèi)存可以映射到地址空間里面去搬卒,所以就不需要分區(qū)了。
內(nèi)存分配
? ? 在內(nèi)核中翎卓,kmalloc()用來分配內(nèi)存空間契邀,釋放函數(shù)是kfree()。kmalloc()函數(shù)有一些標志失暴,GFP_KERNEL會睡眠坯门,GFP_ATOMIC不會睡眠,GFP_NOIO不啟動磁盤IO锐帜,GFP_NOFS不啟動文件系統(tǒng)田盈,GFP_DMA必須從DMA區(qū)分配。kmalloc()保證在地址空間和物理內(nèi)存空間都是連續(xù)分配的缴阎,所以允瞧,kmalloc()分配的是上述所說的一一映射的區(qū)域(DMA和NORMAL,一共896MB)蛮拔。而vmalloc()分配的內(nèi)存只是在地址空間連續(xù)述暂,而不在物理內(nèi)存空間連續(xù),所以vmalloc()映射的是HIGHEM區(qū)域中vmalloc區(qū)的內(nèi)存建炫。kmalloc()由于是直接一一映射畦韭,地址空間和物理內(nèi)存的轉(zhuǎn)換極為簡單,只相差一個PAGE_OFFSET(即內(nèi)核地址空間和用戶地址空間的分界肛跌,32位系統(tǒng)即3G)而且連續(xù)艺配,所以速度很快;但是vmalloc()就要通過內(nèi)核頁表的缺頁異常來映射了衍慎,動作很慢转唉,而且有可能會睡眠。但是vmalloc()可以分配很大塊的內(nèi)存稳捆。vmalloc()分配的物理內(nèi)存用vfree()釋放赠法。
? ??地址空間中的內(nèi)核空間的高端映射區(qū)域分為:(vmalloc區(qū)、可持久映射區(qū)乔夯、臨時映射區(qū))砖织。其中vmalloc()映射的是vmalloc區(qū)的內(nèi)存款侵。其中可持久映射區(qū)的使用方式是這樣的:先從alloc_pages()返回一頁的指針,然后調(diào)用kmap(struct page*)來在可持久映射區(qū)映射一頁的內(nèi)存區(qū)域侧纯。kmap()可以睡眠新锈,而且應(yīng)當在不使用時調(diào)用kunmap()解除映射∶荆可以調(diào)用kmap_atomic()不讓這個函數(shù)睡眠壕鹉。kamp_atomic()函數(shù)是映射在內(nèi)核空間的臨時映射區(qū)的剃幌。臨時映射區(qū)又叫原子映射區(qū)聋涨。當然解除的時候調(diào)用kunmap_atomic()。
? ? 分配和釋放數(shù)據(jù)結(jié)構(gòu)是使用物理內(nèi)存的最普遍的操作负乡。所以為了便于頻繁地分配和釋放同一個數(shù)據(jù)結(jié)構(gòu)牍白,可以采用高速緩存struct kmem_cache。每一個數(shù)據(jù)結(jié)構(gòu)都對應(yīng)于一個高速緩存抖棘。kmem_cache里面含有一個struct kmem_list3茂腥,這其中有3個slab鏈表,分別是:滿的切省、部分的最岗、空的。每一個slab都含有一個或者多個物理頁朝捆“愣桑可以用kmem_cache_create()創(chuàng)建新的高速緩存,用kmem_cache_destroy()刪除高速緩存芙盘,用kmem_getpages()分配新的slab驯用,用kmem_freepages()釋放slab。用kmem_cache_alloc()分配一個對象儒老,如果高速緩存中對象沒有可用的蝴乔,那么就先調(diào)用kmem_getpages()創(chuàng)建新的slab。實際上驮樊,slab就是一個或多個頁框的頭薇正,用來把這一個或多個頁框組成一個整體并且串起來。用kmem_cache_free()把一個對象還給高速緩存囚衔。而一般的內(nèi)存分配則通過伙伴系統(tǒng)來分配挖腰,伙伴系統(tǒng)通過每一個struct zone的zone.free_area數(shù)組來組織。
頁高速緩存
? ? 在物理頁struct page中佳魔,有一個struct address_space *mapping成員曙聂,這個成員代表一個IO緩存,即頁高速緩存鞠鲜。頁高速緩存是內(nèi)存對磁盤的緩存宁脊,來減少對磁盤IO的調(diào)用断国。頁高速緩存把磁盤的數(shù)據(jù)緩存到物理內(nèi)存中。
? ??當一個文件打開后榆苞,內(nèi)核在物理內(nèi)存中創(chuàng)建一個inode稳衬,其中inode.i_mapping指向的就是這個文件在內(nèi)存中的頁高速緩存,即struct address_space結(jié)構(gòu)坐漏。address_space.host指向?qū)?yīng)的inode薄疚。一個磁盤文件對應(yīng)一個struct inode,也對應(yīng)一個struct address_space赊琳,但是對應(yīng)多個struct file街夭。address.a_ops指向一個struct address_space_operations函數(shù)表,里面有具體的回寫躏筏,讀入內(nèi)存數(shù)據(jù)等函數(shù)板丽。
????對于read()調(diào)用,進程會先去inode.i_mapping頁高速緩存看看讀的東西是否存在趁尼,如果存在直接返回埃碱,如果不命中則產(chǎn)生缺頁異常,創(chuàng)建一個頁緩存頁酥泞,同時通過inode找到文件該頁的磁盤地址砚殿,讀取相應(yīng)的頁填充該緩存頁。
????如果是write()調(diào)用芝囤,進程會先去inode.i_mapping頁高速緩存看看讀的東西是否存在似炎,如果存在那么直接修改,如果不命中則產(chǎn)生缺頁異常凡人,創(chuàng)建一個頁緩存頁名党,同時通過inode找到文件該頁的磁盤地址,讀取相應(yīng)的頁填充該緩存頁挠轴,然后修改传睹。被寫入的頁框標記成臟的并且加入一個臟頁鏈表中,然后在合適的時機岸晦,由內(nèi)核線程來回寫到磁盤欧啤,然后刪除臟頁的標志。
? ??回寫的條件是:空閑內(nèi)存低于某個閾值启上、臟頁停留時間高于某個閾值邢隧、用戶進程調(diào)用sync()或fsync()。在2.6中冈在,內(nèi)核調(diào)用一組內(nèi)核線程flusher來進行回寫倒慧。內(nèi)核當:內(nèi)存低于閾值、顯式調(diào)用sync()或fsync()時,通過函數(shù)flusher_threads()調(diào)用一個或多個flusher線程纫谅,線程調(diào)用bdi_writeback_all()開始回寫炫贤,直到:指定的頁數(shù)寫完了、空閑內(nèi)存超過閾值付秕、所有臟頁都寫完了時兰珍,停止運行。當然询吴,flusher線程群也會周期運行掠河,將駐留時間過長的臟頁寫回。Linux還有一種回寫策略猛计,叫做膝上型計算機模式唠摹,以硬盤轉(zhuǎn)動最小為目標,不會專門為了回寫而主動調(diào)用磁盤IO有滑,而且上述的兩個閾值也非常大跃闹。多數(shù)Linux在電池供電時自動用這個,而交流電供電時用正常的毛好。
虛擬內(nèi)存
? ? Linux也有虛擬內(nèi)存。Linux對頁的換出采取了雙鏈策略苛秕,即維護兩個鏈表:活躍與非活躍肌访。非活躍的鏈表里面的頁面是可以換出的,兩個鏈表需要維持平衡艇劫,這種頁面置換算法叫做LRU/2吼驶。
地址空間
分類
? ? 地址空間是邏輯上的,即是虛擬的店煞。所有的進程都有它獨立的地址空間蟹演,一般來說32位機器上是4G,其中0G~3G為用戶空間顷蟀,3G~4G為內(nèi)核空間酒请。內(nèi)核空間的3G~3G+16M被內(nèi)存的DMA區(qū)域映射,3G+16M~3G+896M被內(nèi)存的NORMAL區(qū)域映射鸣个,3G+896M~4G被內(nèi)存的HIGHEM區(qū)域映射羞反。其中3G+896M~4G還分為:vmalloc區(qū)域、可持久映射區(qū)域囤萤、臨時映射區(qū)域昼窗。
分區(qū)
? ? 地址空間中能被進程訪問的部分叫做內(nèi)存區(qū)域。當一個進程訪問了它不能訪問的涛舍,或者是以錯誤的方式訪問的地址空間時澄惊,那么內(nèi)核終止該進程并返回一個段錯誤。內(nèi)存區(qū)域包含:數(shù)據(jù)段、代碼段掸驱、BSS段窘哈、用戶空間棧、映射的文件亭敢,等等滚婉。對于Linux,地址空間是平坦的帅刀、不分段的让腹。即所有進程所擁有的地址空間都是一塊大的連續(xù)的虛擬空間。虛擬地址是地址空間范圍內(nèi)的一個值扣溺。對于用戶角度骇窍,所有的指針的值、變量的地址锥余,只要是用戶看見的腹纳,都是虛擬的地址。
表示
? ? 內(nèi)核使用struct mm_struct來描述一個進程的地址空間驱犹。在task_struct中嘲恍,用mm指針指向這個進程的mm_struct。其中mm_users代表使用計數(shù)雄驹,mm_count代表主引用數(shù)佃牛。mmap和mm_rb都表示全部的內(nèi)存區(qū)域,只不過一個用鏈表聚合医舆,一個用紅黑樹聚合俘侠。所有的進程的mm_struct通過一個雙向鏈表mmlist相連。鏈表頭是init_mm蔬将,是init進程的地址空間爷速。
? ? 內(nèi)核線程沒有獨立的地址空間,也沒有相關(guān)的mm_struct和page table霞怀。即惫东,該線程的mm = NULL。但是當內(nèi)核線程想使用相關(guān)數(shù)據(jù)的時候里烦,就會使用前一個進程的mm_struct凿蒜。即,當一個內(nèi)核線程被調(diào)度時胁黑,內(nèi)核發(fā)現(xiàn)mm = NULL废封,所以就保留前一個進程的mm_struct,并用內(nèi)核線程的active_mm指向該mm_struct丧蘸。
? ? 內(nèi)存區(qū)域用struct vm_area_struct來描述漂洋。其中有vm_mm指向了它所在的地址空間mm_struct遥皂,有vm_start和vm_end來指明它在這個地址空間里面的范圍。同時還有一個鏈表節(jié)點next和紅黑樹節(jié)點vm_rb刽漂,就是mm_struct里面的鏈表和紅黑樹演训。vm_area_struct采用了面向?qū)ο蟮脑O(shè)計思路,有一個vm_ops指向了一個struct vm_operations函數(shù)表贝咙。
創(chuàng)建地址空間
????當我們用fork()創(chuàng)建進程的時候样悟,fork()調(diào)用allocate_mm()從slab中分配一個mm_struct,fork()還調(diào)用copy_mm()來復制父進程的mm_struct庭猩。然而如果在clone中指定了CLONE_VM標志窟她,那么fork()不調(diào)用allocate_mm(),而是直接在copy_mm()中讓子進程的mm指針指向父進程的mm_struct蔼水。當進程退出時震糖,調(diào)用exit_mm()函數(shù),這個函數(shù)會掉用mmput()減少mm_users趴腋,如果為0那么調(diào)用mmdrop()減少mm_count吊说,如果mm_count為0那么調(diào)用free_mm(),這個函數(shù)調(diào)用kmem_cache_free()將mm_歸還給slab优炬。
? ? exec*()也會創(chuàng)建新的地址空間颁井。
創(chuàng)建內(nèi)存區(qū)域
? ? 內(nèi)核用do_mmap()來為地址空間創(chuàng)建新的區(qū)域,這個函數(shù)把文件(struct file)和地址空間的區(qū)域(struct vm_area_struct)來進行映射穿剖。這樣的話蚤蔓,可以根本不用文件的相關(guān)的系統(tǒng)調(diào)用,就能像訪問內(nèi)存那樣去讀寫文件糊余,即使文件關(guān)閉,照樣可以使用這片映射來讀寫文件单寂。如果創(chuàng)建的區(qū)域和現(xiàn)有的相鄰贬芥,那么就合并加入到里面,如果不相鄰宣决,那么就從slab中分配一個vm_area_struct蘸劈,并且調(diào)用vma_link()來把vm_area_struct添加到鏈表和紅黑樹中。函數(shù)do_mmap()原型是unsigned long do_mmap(file, addr, len, prot, flag, offset)尊沸。如果調(diào)用的時候file = NULL且offset = 0威沫,這就叫匿名映射。可以創(chuàng)建匿名映射后再調(diào)用fork()洼专,這樣父子進程就可以實現(xiàn)對內(nèi)存的共享棒掠。函數(shù)do_munmap()從特定的地址空間中刪除一個vm_area_struct。
地址空間與物理內(nèi)存的轉(zhuǎn)換
? ? 應(yīng)用程序操作的都是虛擬的地址空間屁商,而cpu卻操作的都是真實的物理內(nèi)存烟很,所以需要一個轉(zhuǎn)換,這個轉(zhuǎn)換由MMU這個硬件來完成。MMU實現(xiàn)虛擬地址到物理內(nèi)存地址的轉(zhuǎn)換雾袱,并且檢查訪問權(quán)限恤筛。
????Linux使用三級頁表(PGD、PMD芹橡、PTE)毒坛,多級列表可以節(jié)省很多空間。每一個進程都有它自己的用戶頁表林说,而內(nèi)核空間里面也有一個內(nèi)核頁表煎殷,內(nèi)核頁表也可以產(chǎn)生缺頁異常,因為內(nèi)核的地址空間和物理內(nèi)存也有不是一一映射的時候述么,即3G+896M~4G蝌数。
????線程會共享地址空間和頁表。
????其中mm_struct.pgd就指向PGD度秘。為了加快轉(zhuǎn)化顶伞,Linux使用了TLB這個硬件來緩存,當cpu想知道一個虛擬地址空間的地址所對應(yīng)的物理內(nèi)存地址時剑梳,就先去檢查TLB唆貌,如果沒有,再通過頁表一級級索引垢乙。
虛擬文件系統(tǒng)
? ? 虛擬文件系統(tǒng)(VFS)是Linux為用戶空間的程序提供了操作文件的相關(guān)接口锨咙,VFS可以屏蔽掉(一定程度上屏蔽掉)各種類型的文件系統(tǒng)的不一致,并提供了所有文件系統(tǒng)都支持(或者說應(yīng)該支持)的數(shù)據(jù)結(jié)構(gòu)和操作追逮。
????例如用戶調(diào)用write()酪刀,先會進入sys_write()系統(tǒng)調(diào)用,然后調(diào)用vfs_write()钮孵,然后vfs_write()就會去調(diào)用f -> f_op -> write(f)骂倘,即和文件系統(tǒng)相關(guān)的,由文件系統(tǒng)完成的write()函數(shù)去寫文件巴席。Linux的VFS將文件和文件相關(guān)的信息分開存儲历涝。一般來說在磁盤里也是這樣的,如果不是這樣的漾唉,也可以使用VFS荧库,但是需要在轉(zhuǎn)化的過程中付出沉重的開銷。Linux的VFS設(shè)計參考了面向?qū)ο蟮囊恍┧季S赵刑,即把對對象的操作放在函數(shù)表中分衫,并用對象的指針指向這個表,例如上面舉的例子f -> f_op -> write(f)料睛,還要注意要把自己傳給write()函數(shù)丐箩,這樣才能操縱f里面的數(shù)據(jù)成員摇邦。這相當于面向?qū)ο罄锩娴膖his指針。
? ??Linux的VFS為了描述一個文件系統(tǒng)屎勘,提供了四個結(jié)構(gòu)施籍。分別是:struct super_block代表一個文件系統(tǒng)的信息。struct inode代表一個磁盤里面的文件元信息概漱。struct dentry代表一個目錄項丑慎,dentry存儲文件名。struct file代表一個進程打開的文件瓤摧,struct file里面存儲文件的打開時指定的標志竿裂、文件的offset指針。注意這里面的dentry不是一個具體的存在于磁盤里面的結(jié)構(gòu)照弥,它的存在是為了快速的定位一個文件的路徑腻异。dentry存儲在一個目錄項緩存dcache中,VFS會先去目錄項緩存搜索路徑名这揣,如果沒有的話再去文件樹遍歷悔常,然后把相關(guān)的目錄加入到dcache中。注意緩存目錄項的同時也會去緩存相應(yīng)的inode给赞。
? ? 在task_struct中机打,有一個files指針指向struct files_struct。files_struct里面有一個fd_array指針數(shù)組片迅,指向的是struct file残邀,即這個進程打開的文件。fd_array的索引就是fd柑蛇,即文件描述符芥挣。在64位系統(tǒng)中,這個數(shù)組大小是64個耻台,如果一個進程打開了超過64個的文件九秀,那么會再為多出來的文件分配一個文件指針數(shù)組,這個數(shù)組由files_struct的fdt指向粘我。也就是說如果訪問多于64個的文件的話,就要多通過一個指針痹换。
? ??父進程先open得到文件描述符fd之后再fork征字,子進程擁有父進程打開的文件描述符fd。這時娇豫,父子進程的兩個fd指向同一個struct file匙姜,也就是操作同一個文件,擁有同樣的文件的打開時指定的標志冯痢,擁有同樣的文件的offset指針氮昧。
????task_struct的fs指針指向struct fs_struct框杜,這是當前進程的工作目錄和根目錄。task_struct的mmt_namespace指向struct namespace袖肥,這是進程所在的命名空間咪辱。一般來說,每一個進程都有屬于它自己的椎组,獨立的一個files_struct和fs_struct油狂,但是所有進程都指向同一個namespace。當然寸癌,對于特殊的专筷,即調(diào)用clone的時候指明CLONE_FILES的進程則和父進程共享files_struct,指明CLONE_FS的進程和父進程共享fs_struct蒸苇,指明CLONE_NEWS的話磷蛹,會為這個進程創(chuàng)建一個屬于它的新的命名空間。
? ? 一個inode對應(yīng)一個磁盤文件溪烤。因為硬鏈接味咳,所以一個inode可以對應(yīng)多個dentry。因為多個進程可以打開同一個目錄項對應(yīng)的文件氛什,一個進程也可以多次打開同一個目錄項對應(yīng)的文件莺葫,所以一個dentry可以對應(yīng)多個file。
塊設(shè)備
簡述
????Linux一共有兩種硬件存儲設(shè)備枪眉,即字符設(shè)備和塊設(shè)備捺檬。塊設(shè)備是可以隨機訪問的設(shè)備,常見的塊設(shè)備是磁盤贸铜。字符設(shè)備是只能按照字符流來有序訪問的硬件設(shè)備堡纬,例如鍵盤。
? ??對于硬件來說蒿秦,塊設(shè)備最小的可尋址單元是扇區(qū)烤镐,一般是512B。扇區(qū)是一個設(shè)備的物理屬性棍鳖。而最小的邏輯可尋址單元是塊炮叶,內(nèi)核所有執(zhí)行塊設(shè)備的操作都是按照塊來的。塊倍數(shù)于扇區(qū)渡处,但是要小于頁框镜悉。通常塊是:512字節(jié)、1KB医瘫、4KB侣肄。
緩沖區(qū)表示
? ? 當塊調(diào)入內(nèi)存時,會有一個緩沖區(qū)和它對應(yīng)醇份,內(nèi)核用struct buffer_head來表示緩沖區(qū)的信息稼锅。buffer_head.b_bdev指明對應(yīng)的塊設(shè)備吼具,buffer_head.bblocknr指明塊設(shè)備的起始塊號,buffer_head.page指明用于存儲這個塊的頁框矩距,buffer_head.b_data用來指明塊在頁內(nèi)的起始地址拗盒,buffer_head.b_size指明塊的大小。所以塊在buffer_head.page的(b_data, b_data + b_size)區(qū)間處剩晴。一個buffer_head只能指明一個塊和一個物理頁的對應(yīng)關(guān)系锣咒,只能描述一個塊。
塊IO表示
? ? 在2.5中赞弥,引入了struct bio結(jié)構(gòu)體來表示一個正在進行的塊IO操作毅整。bio里面有一個struct bio_vec *bi_io_vec動態(tài)數(shù)組,這個動態(tài)數(shù)組包含了這個IO操作所需要的所有片段(即塊在內(nèi)存的緩沖區(qū))绽左。其中struct bio_vec是一個{ page, offset, len }結(jié)構(gòu)悼嫉,描述一個塊。bio.vcnt表示這個動態(tài)數(shù)組的長度拼窥。bio.bi_idx表示正在操作的IO片段戏蔑。所有的塊請求保存在一個請求隊列struct request_queue中。每一個請求用struct request表示鲁纠,一般來說一個bio代表一個請求总棵,但是因為有合并操作,所以一個請求也可以有多個bio改含。
塊IO調(diào)度
????在2.4版本中使用的調(diào)度程序是Linus電梯調(diào)度情龄,即:1.如果請求隊列中存在前相鄰或者后相鄰的,那么合并捍壤。2.如果隊列中存在駐留時間過長的請求骤视,直接加到隊列尾部。3.如果存在一個合適的插入位置鹃觉,那么插入专酗。4.如果不存在插入位置則加入尾部。在2.6中有新的IO調(diào)度算法盗扇,即最后期限IO調(diào)度祷肯,基礎(chǔ)還是Linus電梯,但是擁有三個鏈表疗隶,其中多出來的兩個是FIFO的讀和寫鏈表躬柬,分別有超時時間(默認為500ms和5s),如果超時了則優(yōu)先從這兩個鏈表里取請求抽减。預(yù)測IO調(diào)度和最后期限一樣,只是請求提交完會停留6ms橄碾,來給相鄰的請求提交的機會卵沉,預(yù)測IO會跟蹤并統(tǒng)計每個進程的習慣和行為颠锉。完全公平調(diào)度是每個提交了IO的進程都有自己的隊列,按照時間片輪轉(zhuǎn)執(zhí)行每個隊列的請求史汗。空操作IO調(diào)度除了合并以外什么也不干琼掠,這個調(diào)度程序是給真正可以隨機訪問的設(shè)備用的,例如閃存卡停撞。