操作系統(tǒng)面試知識點總結1

技術交流QQ群:1027579432,歡迎你的加入!

歡迎關注我的微信公眾號:CurryCoder的程序人生

一.進程與線程的概念,以及為什么要有進程線程,其中有什么區(qū)別,它們各自又是怎么同步的?

  • 基本概念:
    • 進程是對運行時程序的封裝,是系統(tǒng)進行資源調度和分配的的基本單位,實現(xiàn)了操作系統(tǒng)的并發(fā);
    • 線程是進程的子任務,是CPU調度和分派的基本單位,用于保證程序的實時性,實現(xiàn)進程內部的并發(fā);線程是操作系統(tǒng)可識別的最小執(zhí)行和調度單位。每個線程都獨自占用一個虛擬處理器:獨自的寄存器組,指令計數(shù)器和處理器狀態(tài)。每個線程完成不同的任務,但是共享同一地址空間(也就是同樣的動態(tài)內存,映射文件,目標代碼等等),打開的文件隊列和其他內核資源。
  • 區(qū)別:
    • 1.一個線程只能屬于一個進程,而一個進程可以有多個線程,但至少有一個線程。線程依賴于進程而存在。
    • 2.進程在執(zhí)行過程中擁有獨立的內存單元,而多個線程共享進程的內存。(資源分配給進程,同一進程的所有線程共享該進程的所有資源。同一進程中的多個線程共享代碼段(代碼和常量),數(shù)據(jù)段(全局變量和靜態(tài)變量),擴展段(堆存儲)。但是每個線程擁有自己的棧段,棧段又叫運行時段,用來存放所有局部變量和臨時變量);
    • 3.進程是資源分配的最小單位,線程是CPU調度的最小單位;
    • 4.系統(tǒng)開銷: 由于在創(chuàng)建或撤消進程時,系統(tǒng)都要為之分配或回收資源,如內存空間、I/o設備等。因此,操作系統(tǒng)所付出的開銷將顯著地大于在創(chuàng)建或撤消線程時的開銷。類似地,在進行進程切換時,涉及到整個當前進程CPU環(huán)境的保存以及新被調度運行的進程的CPU環(huán)境的設置。而線程切換只須保存和設置少量寄存器的內容,并不涉及存儲器管理方面的操作。可見,進程切換的開銷也遠大于線程切換的開銷;
    • 5.通信:由于同一進程中的多個線程具有相同的地址空間,致使它們之間的同步和通信的實現(xiàn),也變得比較容易。進程間通信IPC,線程間可以直接讀寫進程數(shù)據(jù)段(如全局變量)來進行通信——需要進程同步和互斥手段的輔助,以保證數(shù)據(jù)的一致性。在有的系統(tǒng)中,線程的切換、同步和通信都無須操作系統(tǒng)內核的干預;
    • 6.進程編程調試簡單可靠性高,但是創(chuàng)建銷毀開銷大;線程正相反,開銷小,切換速度快,但是編程調試相對復雜。
    • 7.進程間不會相互影響 ;線程間相互影響,一個線程掛掉將導致整個進程掛掉
    • 8.進程適應于多核、多機分布;線程適用于多核
  • 進程間通信的方式:進程間通信主要包括管道、系統(tǒng)IPC(包括消息隊列、信號量、信號、共享內存等)、以及套接字socket。
    • 1.管道:管道主要包括無名管道和命名管道:管道可用于具有親緣關系的父子進程間的通信,有名管道除了具有管道所具有的功能外,它還允許無親緣關系進程間的通信
      • 1.1 普通管道PIPE:
        • 1)它是半雙工的(即數(shù)據(jù)只能在一個方向上流動),具有固定的讀端和寫端
        • 2)它只能用于具有親緣關系的進程之間的通信(也是父子進程或者兄弟進程之間)
        • 3)它可以看成是一種特殊的文件,對于它的讀寫也可以使用普通的read、write等函數(shù)。但是它不是普通的文件,并不屬于其他任何文件系統(tǒng),并且只存在于內存中。
      • 1.2 命名管道FIFO:
        • 1)FIFO可以在無關的進程之間交換數(shù)據(jù)
        • 2)FIFO有路徑名與之相關聯(lián),它以一種特殊設備文件形式存在于文件系統(tǒng)中。
    • 2.系統(tǒng)IPC(包括消息隊列、信號量、信號、共享內存等)
      • 2.1 消息隊列:消息隊列,是消息的鏈接表,存放在內核中。一個消息隊列由一個標識符(即隊列ID)來標記。 (消息隊列克服了信號傳遞信息少,管道只能承載無格式字節(jié)流以及緩沖區(qū)大小受限等特點)具有寫權限得進程可以按照一定得規(guī)則向消息隊列中添加新信息;對消息隊列有讀權限得進程則可以從消息隊列中讀取信息;
        • 1)消息隊列是面向記錄的,其中的消息具有特定的格式以及特定的優(yōu)先級。
        • 2)消息隊列獨立于發(fā)送與接收進程。進程終止時,消息隊列及其內容并不會被刪除。
        • 3)消息隊列可以實現(xiàn)消息的隨機查詢,消息不一定要以先進先出的次序讀取,也可以按消息的類型讀取。
      • 2.2 信號量semaphore:信號量(semaphore)與已經(jīng)介紹過的 IPC 結構不同,它是一個計數(shù)器,可以用來控制多個進程對共享資源的訪問。信號量用于實現(xiàn)進程間的互斥與同步,而不是用于存儲進程間通信數(shù)據(jù)。
        • 1)信號量用于進程間同步,若要在進程間傳遞數(shù)據(jù)需要結合共享內存。
        • 2)信號量基于操作系統(tǒng)的 PV 操作,程序對信號量的操作都是原子操作。
        • 3)每次對信號量的 PV 操作不僅限于對信號量值加 1 或減 1,而且可以加減任意正整數(shù)。
        • 4)支持信號量組。
      • 2.3 信號signal:信號是一種比較復雜的通信方式,用于通知接收進程某個事件已經(jīng)發(fā)生。
      • 2.4 共享內存(Shared Memory):它使得多個進程可以訪問同一塊內存空間,不同進程可以及時看到對方進程中對共享內存中數(shù)據(jù)得更新。這種方式需要依靠某種同步操作,如互斥鎖和信號量等
        • 1)共享內存是最快的一種IPC,因為進程是直接對內存進行存取;
        • 2)因為多個進程可以同時操作,所以需要進行同步;
        • 3)信號量+共享內存通常結合在一起使用,信號量用來同步對共享內存的訪問。
    • 3.套接字SOCKET:socket也是一種進程間通信機制,與其他通信機制不同的是,它可用于不同主機之間的進程通信。
  • 線程間通信的方式
    • 臨界區(qū):通過多線程的串行化來訪問公共資源或一段代碼,速度快,適合控制數(shù)據(jù)訪問;
    • 互斥量Synchronized/Lock:采用互斥對象機制,只有擁有互斥對象的線程才有訪問公共資源的權限。因為互斥對象只有一個,所以可以保證公共資源不會被多個線程同時訪問
    • 信號量Semphare:為控制具有有限數(shù)量的用戶資源而設計的,它允許多個線程在同一時刻去訪問同一個資源,但一般需要限制同一時刻訪問此資源的最大線程數(shù)目。
    • 事件(信號),Wait/Notify:通過通知操作的方式來保持多線程同步,還可以方便的實現(xiàn)多線程優(yōu)先級的比較操作。

二. Linux虛擬地址空間

  • 虛擬地址空間出現(xiàn)的原因:為了防止不同進程同一時刻在物理內存中運行而對物理內存的爭奪和踐踏,采用了虛擬內存。
  • 虛擬內存技術使得不同進程在運行過程中,它所看到的是自己獨自占有了當前系統(tǒng)的4G內存。所有進程共享同一物理內存,每個進程只把自己目前需要的虛擬內存空間映射并存儲到物理內存上。 事實上,在每個進程創(chuàng)建加載時,內核只是為進程“創(chuàng)建”了虛擬內存的布局,具體就是初始化進程控制表中內存相關的鏈表,實際上并不立即就把虛擬內存對應位置的程序數(shù)據(jù)和代碼(比如.text .data段)拷貝到物理內存中,只是建立好虛擬內存和磁盤文件之間的映射就好(叫做存儲器映射),等到運行到對應的程序時,才會通過缺頁異常,來拷貝數(shù)據(jù)。還有進程運行過程中,要動態(tài)分配內存,比如malloc時,也只是分配了虛擬內存,即為這塊虛擬內存對應的頁表項做相應設置,當進程真正訪問到此數(shù)據(jù)時,才引發(fā)缺頁異常。請求分頁系統(tǒng)、請求分段系統(tǒng)和請求段頁式系統(tǒng)都是針對虛擬內存的,通過請求實現(xiàn)內存與外存的信息置換
  • 虛擬內存的好處
    • 1.擴大地址空間;
    • 2.內存保護:每個進程運行在各自的虛擬內存地址空間,互相不能干擾對方。虛存還對特定的內存地址提供寫保護,可以防止代碼或數(shù)據(jù)被惡意篡改。
    • 3.公平內存分配。采用了虛存之后,每個進程都相當于有同樣大小的虛存空間。
    • 4.當進程通信時,可采用虛存共享的方式實現(xiàn)。
    • 5.當不同的進程使用同樣的代碼時,比如庫文件中的代碼,物理內存中可以只存儲一份這樣的代碼,不同的進程只需要把自己的虛擬內存映射過去就可以了,節(jié)省內存
    • 6.虛擬內存很適合在多道程序設計系統(tǒng)中使用,許多程序的片段同時保存在內存中。當一個程序等待它的一部分讀入內存時,可以把CPU交給另一個進程使用。在內存中可以保留多個進程,系統(tǒng)并發(fā)度提高
    • 7.在程序需要分配連續(xù)的內存空間的時候,只需要在虛擬內存空間分配連續(xù)空間,而不需要實際物理內存的連續(xù)空間,可以利用碎片
  • 虛擬內存的代價
    • 1.虛存的管理需要建立很多數(shù)據(jù)結構,這些數(shù)據(jù)結構要占用額外的內存
    • 2.虛擬地址到物理地址的轉換,增加了指令的執(zhí)行時間。
    • 3.頁面的換入換出需要磁盤I/O,這是很耗時的
    • 4.如果一頁中只有一部分數(shù)據(jù),會浪費內存。

三.操作系統(tǒng)中程序的內存結構

操作系統(tǒng)中的內存結構
  • 一個程序本質上都是由BSS段、data段、text段三個組成的。可以看到一個可執(zhí)行程序在存儲(沒有調入內存)時分為代碼段、數(shù)據(jù)區(qū)和未初始化數(shù)據(jù)區(qū)三部分。
    • BSS段(未初始化數(shù)據(jù)區(qū)):通常用來存放程序中未初始化的全局變量和靜態(tài)變量的一塊內存區(qū)域。BSS段屬于靜態(tài)分配,程序結束后靜態(tài)變量資源由系統(tǒng)自動釋放。
    • 數(shù)據(jù)段:存放程序中已初始化的全局變量的一塊內存區(qū)域。數(shù)據(jù)段也屬于靜態(tài)內存分配
    • 代碼段:存放程序執(zhí)行代碼的一塊內存區(qū)域。這部分區(qū)域的大小在程序運行前就已經(jīng)確定,并且內存區(qū)域屬于只讀。在代碼段中,也有可能包含一些只讀的常數(shù)變量
    • text段和data段在編譯時已經(jīng)分配了空間,而BSS段并不占用可執(zhí)行文件的大小,它是由鏈接器來獲取內存的。
    • BSS段(未進行初始化的數(shù)據(jù))的內容并不存放在磁盤上的程序文件中。其原因是內核在程序開始運行前將它們設置為0。需要存放在程序文件中的只有正文段和初始化數(shù)據(jù)段。
    • data段(已經(jīng)初始化的數(shù)據(jù))則為數(shù)據(jù)分配空間,數(shù)據(jù)保存到目標文件中。
    • 數(shù)據(jù)段包含經(jīng)過初始化的全局變量以及它們的值。BSS段的大小從可執(zhí)行文件中得到,然后鏈接器得到這個大小的內存塊,緊跟在數(shù)據(jù)段的后面。當這個內存進入程序的地址空間后全部清零。包含數(shù)據(jù)段和BSS段的整個區(qū)段此時通常稱為數(shù)據(jù)區(qū)。
    • 可執(zhí)行程序在運行時又多出兩個區(qū)域:棧區(qū)和堆區(qū)
      • 棧區(qū):由編譯器自動釋放,存放函數(shù)的參數(shù)值、局部變量等。每當一個函數(shù)被調用時,該函數(shù)的返回類型和一些調用的信息被存放到棧中。然后這個被調用的函數(shù)再為他的自動變量和臨時變量在棧上分配空間。每調用一個函數(shù)一個新的棧就會被使用。棧區(qū)是從高地址位向低地址位增長的,是一塊連續(xù)的內存區(qū)域,最大容量是由系統(tǒng)預先定義好的,申請的棧空間超過這個界限時會提示溢出,用戶能從棧中獲取的空間較小。
      • 堆區(qū):用于動態(tài)分配內存,位于BSS和棧中間的地址區(qū)域。由程序員申請分配和釋放。堆是從低地址位向高地址位增長,采用鏈式存儲結構。頻繁的malloc/free造成內存空間的不連續(xù),產生碎片。當申請堆空間時庫函數(shù)是按照一定的算法搜索可用的足夠大的空間。因此堆的效率比棧要低的多。

四.操作系統(tǒng)中的缺頁中斷

  • malloc()和mmap()等內存分配函數(shù),在分配時只是建立了進程虛擬地址空間,并沒有分配虛擬內存對應的物理內存。當進程訪問這些沒有建立映射關系的虛擬內存時,處理器自動觸發(fā)一個缺頁異常。
  • 缺頁中斷:在請求分頁系統(tǒng)中,可以通過查詢頁表中的狀態(tài)位來確定所要訪問的頁面是否存在于內存中。每當所要訪問的頁面不在內存時,會產生一次缺頁中斷,此時操作系統(tǒng)會根據(jù)頁表中的外存地址在外存中找到所缺的一頁,將其調入內存。
  • 缺頁本身是一種中斷,與一般的中斷一樣,需要經(jīng)過4個處理步驟:
    • 1.保護CPU現(xiàn)場
    • 2.分析中斷原因
    • 3.轉入缺頁中斷處理程序進行處理
    • 4.恢復CPU現(xiàn)場,繼續(xù)執(zhí)行
  • 但是,如果缺頁中斷是由于所要訪問的頁面不存在于內存時,由硬件所產生的一種特殊的中斷時。因此,與一般的中斷存在區(qū)別:
    • 1.在指令執(zhí)行期間產生和處理缺頁中斷信號
    • 2.一條指令在執(zhí)行期間,可能產生多次缺頁中斷
    • 3.缺頁中斷返回是,執(zhí)行產生中斷的一條指令,而一般的中斷返回是,執(zhí)行下一條指令。

五.fork和vfork的區(qū)別

  • 1.fork的基礎知識
    • fork:創(chuàng)建一個和當前進程映像一樣的進程,可以通過fork( )系統(tǒng)調用
          #include <sys/types.h>
          #include <unistd.h>
          pid_t fork(void);
      
    • 成功調用fork( )會創(chuàng)建一個新的進程,它幾乎與調用fork( )的進程一模一樣,這兩個進程都會繼續(xù)運行。在子進程中,成功的fork( )調用會返回0。在父進程中fork( )返回子進程的pid。如果出現(xiàn)錯誤,fork( )返回一個負值。
    • 最常見的fork( )用法是創(chuàng)建一個新的進程,然后使用exec( )載入二進制映像,替換當前進程的映像。這種情況下,派生(fork)了新的進程,而這個子進程會執(zhí)行一個新的二進制可執(zhí)行文件的映像。這種“派生加執(zhí)行”的方式是很常見的。
  • **例題:下面程序一共會在屏幕上輸出多少個“-” **?
#include<iostream>
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
using namespace std;
int main( )
{
    int i;
    for(i = 0; i < 2; i++)
    {
        cout<<"-\n";
        fork( );
        cout <<"-\n";
    }
    cout << endl;
    return 1;
}
  • A:計算機程序設計中的分叉函數(shù)。返回值: 若成功調用一次則返回兩個值,子進程返回0,父進程返回子進程標記;否則,出錯返回-1。fork函數(shù)將運行著的程序分成2個(幾乎)完全一樣的進程,每個進程都啟動一個從代碼的同一位置開始執(zhí)行的線程。這兩個進程中的線程繼續(xù)執(zhí)行,就像是兩個用戶同時啟動了該應用程序的兩個副本。第一次第一個cout輸出一個"-"。然后,fork函數(shù)將運行的程序分成兩個(幾乎)完全一樣的進程。第一次第二個cout輸出兩個“-”。第二次第一個cout還是輸出兩個“-”。然后,fork函數(shù)將運行的兩個程序分成四個(幾乎)完全一樣的進程。第二次第二個cout輸出四個“-”。所以總共輸出的“-”個數(shù)為count=1+2+2+4=9!
  • 2.vfork的基礎知識
  • 在實現(xiàn)寫時復制之前,Unix的設計者們就一直很關注在fork后立刻執(zhí)行exec所造成的地址空間的浪費。BSD的開發(fā)者們在3.0的BSD系統(tǒng)中引入了vfork( )系統(tǒng)調用。
        #include <sys/types.h>
        #include <unistd.h>
        pid_t vfork(void);
    
  • 除了子進程必須要立刻執(zhí)行一次對exec的系統(tǒng)調用,或者調用_exit( )退出,對vfork( )的成功調用所產生的結果和fork( )是一樣的。vfork( )會掛起父進程直到子進程終止或者運行了一個新的可執(zhí)行文件的映像。通過這樣的方式,vfork( )避免了地址空間的按頁復制。在這個過程中,父進程和子進程共享相同的地址空間和頁表項。實際上vfork( )只完成了一件事:復制內部的內核數(shù)據(jù)結構。因此,子進程也就不能修改地址空間中的任何內存。
  • vfork( )是一個歷史遺留產物,Linux本不應該實現(xiàn)它。需要注意的是,即使增加了寫時復制,vfork( )也要比fork( )快,因為它沒有進行頁表項的復制。然而,寫時復制的出現(xiàn)減少了對于替換fork( )爭論。實際上,直到2.2.0內核,vfork( )只是一個封裝過的fork( )。因為對vfork( )的需求要小于fork( ),所以vfork( )的這種實現(xiàn)方式是可行的。
  • 寫時復制:是一種采取了惰性優(yōu)化方法來避免復制時的系統(tǒng)開銷。Linux采用了寫時復制的方法,以減少fork時對父進程空間進程整體復制帶來的開銷。它的前提很簡單:如果有多個進程要讀取它們自己的那部門資源的副本,那么復制是不必要的。每個進程只要保存一個指向這個資源的指針就可以了。只要沒有進程要去修改自己的“副本”,就存在著這樣的幻覺:每個進程好像獨占那個資源。從而就避免了復制帶來的負擔。如果一個進程要修改自己的那份資源“副本”,那么就會復制那份資源,并把復制的那份提供給進程。不過其中的復制對進程來說是透明的。這個進程就可以修改復制后的資源了,同時其他的進程仍然共享那份沒有修改過的資源。所以這就是名稱的由來:在寫入時進行復制。
  • fork和vfork的區(qū)別
    • 1.fork( )的子進程拷貝父進程的數(shù)據(jù)段和代碼段;vfork( )的子進程與父進程共享數(shù)據(jù)段
    • 2.fork( )的父子進程的執(zhí)行次序不確定;vfork( )保證子進程先運行,在調用exec或exit之前與父進程數(shù)據(jù)是共享的,在它調用exec或exit之后父進程才可能被調度運行。
    • 3.vfork( )保證子進程先運行,在它調用exec或exit之后父進程才可能被調度運行。如果在調用這兩個函數(shù)之前子進程依賴于父進程的進一步動作,則會導致死鎖。
    • 4.當需要改變共享數(shù)據(jù)段中變量的值,則拷貝父進程。

六.如何修改文件最大句柄數(shù)?

  • linux默認最大文件句柄數(shù)是1024個,在Linux服務器文件并發(fā)量比較大的情況下,系統(tǒng)會報"too many open files"的錯誤。故在Linux服務器高并發(fā)調優(yōu)時,往往需要預先調優(yōu)Linux參數(shù),修改Linux最大文件句柄數(shù)。有兩種方法可以實現(xiàn):
    • 1.ulimit -n <可以同時打開的文件數(shù)>,將當前進程的最大句柄數(shù)修改為指定的參數(shù)(注:該方法只針對當前進程有效,重新打開一個shell或者重新開啟一個進程,參數(shù)還是之前的值)
    • 2.對所有進程都有效的方法,修改Linux系統(tǒng)參數(shù)

七.并發(fā)(concurrency)和并行(parallelism)的區(qū)別?

  • 并發(fā)(concurrency):指宏觀上看起來兩個程序在同時運行,比如說在單核cpu上的多任務。但是從微觀上看兩個程序的指令是交織著運行的,你的指令之間穿插著我的指令,我的指令之間穿插著你的,在單個周期內只運行了一個指令。這種并發(fā)并不能提高計算機的性能,只能提高效率。
  • 并行(parallelism):指嚴格物理意義上的同時運行,比如多核cpu,兩個程序分別運行在兩個核上,兩者之間互不影響,單個周期內每個程序都運行了自己的指令,也就是運行了兩條指令。這樣說來并行的確提高了計算機的效率。所以現(xiàn)在的cpu都是往多核方面發(fā)展。

八.MySQL的端口號是多少,如何修改這個端口號

  • 查看端口號:使用命令show global variables like 'port';查看端口號 ,mysql的默認端口是3306。(補充:sqlserver默認端口號為:1433;oracle默認端口號為:1521;DB2默認端口號為:5000;PostgreSQL默認端口號為:5432)
  • 修改端口號:編輯/etc/my.cnf文件,早期版本有可能是my.conf文件名,增加端口參數(shù),并且設定端口,注意該端口未被使用,保存退出。

九.操作系統(tǒng)中的頁表尋址

  • 頁式內存管理,內存分成固定長度的一個個頁片。操作系統(tǒng)為每一個進程維護了一個從虛擬地址到物理地址的映射關系的數(shù)據(jù)結構,叫頁表。頁表的內容就是該進程的虛擬地址到物理地址的一個映射。頁表中的每一項都記錄了這個頁的基地址。通過頁表,由邏輯地址的高位部分先找到邏輯地址對應的頁基地址,再由頁基地址偏移一定長度就得到最后的物理地址,偏移的長度由邏輯地址的低位部分決定。一般情況下,這個過程都可以由硬件完成,所以效率還是比較高的。頁式內存管理的優(yōu)點就是比較靈活,內存管理以較小的頁為單位,方便內存換入換出和擴充地址空間。
  • Linux最初的兩級頁表機制:兩級分頁機制將32位的虛擬空間分成三段,低十二位表示頁內偏移,高20分成兩段分別表示兩級頁表的偏移。
    • PGD(Page Global Directory): 最高10位,全局頁目錄表索引
    • PTE(Page Table Entry):中間10位,頁表入口索引
  • 當在進行地址轉換時,結合在CR3寄存器中存放的頁目錄(page directory, PGD)的這一頁的物理地址,再加上從虛擬地址中抽出高10位叫做頁目錄表項(內核也稱這為pgd)的部分作為偏移, 即定位到可以描述該地址的pgd;從該pgd中可以獲取可以描述該地址的頁表的物理地址,再加上從虛擬地址中抽取中間10位作為偏移, 即定位到可以描述該地址的pte;在這個pte中即可獲取該地址對應的頁的物理地址, 加上從虛擬地址中抽取的最后12位,即形成該頁的頁內偏移, 即可最終完成從虛擬地址到物理地址的轉換。從上述過程中,可以看出,對虛擬地址的分級解析過程,實際上就是不斷深入頁表層次,逐漸定位到最終地址的過程,所以這一過程被叫做page talbe walk。
  • Linux的三級頁表機制:當X86引入物理地址擴展(Pisycal Addrress Extension, PAE)后,可以支持大于4G的物理內存(36位),但虛擬地址依然是32位,原先的頁表項不適用,它實際多4 bytes被擴充到8 bytes,這意味著,每一頁現(xiàn)在能存放的pte數(shù)目從1024變成512了(4k/8)。相應地,頁表層級發(fā)生了變化,Linus新增加了一個層級,叫做頁中間目錄(page middle directory, PMD), 變成:
    Linux的三級頁表機制
  • 現(xiàn)在就同時存在2級頁表和3級頁表,在代碼管理上肯定不方便。巧妙的是,Linux采取了一種抽象方法:所有架構全部使用3級頁表: 即PGD -> PMD -> PTE。那只使用2級頁表(如非PAE的X86)怎么辦?辦法是針對使用2級頁表的架構,把PMD抽象掉,即虛設一個PMD表項。這樣在page table walk過程中,PGD本直接指向PTE的,現(xiàn)在不了,指向一個虛擬的PMD,然后再由PMD指向PTE。這種抽象保持了代碼結構的統(tǒng)一
  • Linux的四級頁表機制:硬件在發(fā)展,3級頁表很快又捉襟見肘了,原因是64位CPU出現(xiàn)了, 比如X86_64, 它的硬件是實實在在支持4級頁表的。它支持48位的虛擬地址空間1。如下:
    Linux的四級頁表機制
  • Linux內核針為使用原來的3級列表(PGD->PMD->PTE),做了折衷。即采用一個唯一的,共享的頂級層次,叫PML4。這個PML4沒有編碼在地址中,這樣就能套用原來的3級列表方案了。不過代價就是,由于只有唯一的PML4, 尋址空間被局限在(239=)512G, 而本來PML4段有9位, 可以支持512個PML4表項的。現(xiàn)在為了使用3級列表方案,只能限制使用一個, 512G的空間很快就又不夠用了,解決方案呼之欲出。

十.有了進程,為什么還要有線程?

  • 線程產生的原因:進程可以使多個程序能并發(fā)執(zhí)行,以提高資源的利用率和系統(tǒng)的吞吐量;但是其具有一些缺點:進程在同一時間只能干一件事、進程在執(zhí)行的過程中如果阻塞,整個進程就會掛起,即使進程中有些工作不依賴于等待的資源,仍然不會執(zhí)行。
  • 因此,操作系統(tǒng)引入了比進程粒度更小的線程,作為并發(fā)執(zhí)行的基本單位,從而減少程序在并發(fā)執(zhí)行時所付出的時空開銷,提高并發(fā)性。和進程相比,線程的優(yōu)勢如下
    • 從資源上來講,線程是一種非常"節(jié)儉"的多任務操作方式。在Linux系統(tǒng)下,啟動一個新的進程必須分配給它獨立的地址空間,建立眾多的數(shù)據(jù)表來維護它的代碼段、堆棧段和數(shù)據(jù)段,這是一種"昂貴"的多任務工作方式。
    • 從切換效率上來講,運行于一個進程中的多個線程,它們之間使用相同的地址空間,而且線程間彼此切換所需時間也遠遠小于進程間切換所需要的時間。據(jù)統(tǒng)計,一個進程的開銷大約是一個線程開銷的30倍左右。
    • 從通信機制上來講,線程間方便的通信機制。對不同進程來說,它們具有獨立的數(shù)據(jù)空間,要進行數(shù)據(jù)的傳遞只能通過進程間通信的方式進行,這種方式不僅費時,而且很不方便。線程則不然,由于同一進城下的線程之間貢獻數(shù)據(jù)空間,所以一個線程的數(shù)據(jù)可以直接為其他線程所用,這不僅快捷,而且方便。
    • 使多CPU系統(tǒng)更加有效。操作系統(tǒng)會保證當線程數(shù)不大于CPU數(shù)目時,不同的線程運行于不同的CPU上。
    • 改善程序結構。一個既長又復雜的進程可以考慮分為多個線程,成為幾個獨立或半獨立的運行部分,這樣的程序才會利于理解和修改。

十一.多線程和多進程的不同?

  • 進程是資源分配的最小單位,而線程時CPU調度的最小單位。多線程之間共享同一個進程的地址空間,線程間通信簡單,同步復雜,線程創(chuàng)建、銷毀和切換簡單,速度快,占用內存少,適用于多核分布式系統(tǒng),但是線程間會相互影響,一個線程意外終止會導致同一個進程的其他線程也終止,程序可靠性弱。而多進程間擁有各自獨立的運行地址空間,進程間不會相互影響,程序可靠性強,但是進程創(chuàng)建、銷毀和切換復雜,速度慢,占用內存多,進程間通信復雜,但是同步簡單,適用于多核、多機分布

十二.操作系統(tǒng)中的缺頁置換算法

  • 當訪問一個內存中不存在的頁,并且內存已滿,則需要從內存中調出一個頁或將數(shù)據(jù)送至磁盤對換區(qū),替換一個頁,這種現(xiàn)象叫做缺頁置換。當前操作系統(tǒng)最常采用的缺頁置換算法如下:
    • 先進先出(FIFO)算法:置換最先調入內存的頁面,即置換在內存中駐留時間最久的頁面。按照進入內存的先后次序排列成隊列,從隊尾進入,從隊首刪除。
    • 最近最少使用(LRU)算法: 置換最近一段時間以來最長時間未訪問過的頁面。根據(jù)程序局部性原理,剛被訪問的頁面,可能馬上又要被訪問;而較長時間內沒有被訪問的頁面,可能最近不會被訪問。當前最常采用的就是LRU算法。

十三.多進程和多線程的使用場景

  • 多線程模型主要優(yōu)勢為線程間切換代價較小,因此適用于I/O密集型的工作場景,因此I/O密集型的工作場景經(jīng)常會由于I/O阻塞導致頻繁的切換線程。同時,多線程模型也適用于單機多核分布式場景。
  • 多進程模型的優(yōu)勢是CPU,適用于CPU密集型。同時,多進程模型也適用于多機分布式場景中,易于多機擴展。

十四.死鎖發(fā)生的條件以及如何解決死鎖

  • 死鎖:是指兩個或兩個以上進程在執(zhí)行過程中,因爭奪資源而造成的下相互等待的現(xiàn)象。
  • 發(fā)生條件
    • 互斥條件:進程對所分配到的資源不允許其他進程訪問,若其他進程訪問該資源,只能等待,直至占有該資源的進程使用完成后釋放該資源;
    • 請求和保持條件:進程獲得一定的資源后,又對其他資源發(fā)出請求,但是該資源可能被其他進程占有,此時請求阻塞,但該進程不會釋放自己已經(jīng)占有的資源
    • 不可剝奪條件:進程已獲得的資源,在未完成使用之前,不可被剝奪,只能在使用后自己釋放
    • 環(huán)路等待條件:進程發(fā)生死鎖后,必然存在一個進程-資源之間的環(huán)形鏈
  • 解決方法
    • 資源一次性分配,從而剝奪請求和保持條件
    • 可剝奪資源:即當進程新的資源未得到滿足時,釋放已占有的資源,從而破壞不可剝奪的條件
    • 資源有序分配法:系統(tǒng)給每類資源賦予一個序號,每個進程按編號遞增的請求資源,釋放則相反,從而破壞環(huán)路等待的條件

十五.虛擬內存和物理內存怎么對應?

  • 1.物理地址(physical address):用于內存芯片級的單元尋址,與處理器和CPU連接的地址總線相對應;
  • 雖然可以直接把物理地址理解成插在機器上那根內存本身,把內存看成一個從0字節(jié)一直到最大空量逐字節(jié)的編號的大數(shù)組,然后把這個數(shù)組叫做物理地址,但是事實上,這只是一個硬件提供給軟件的抽像,內存的尋址方式并不是這樣。所以,說它是“與地址總線相對應”,是更貼切一些,不過拋開對物理內存尋址方式的考慮,直接把物理地址與物理的內存一一對應,也是可以接受的。也許錯誤的理解更利于形而上的抽像。
  • 2.虛擬地址(virtual memory):這是對整個內存(不要與機器上插那條對上號)的抽像描述。它是相對于物理內存來講的,可以直接理解成“不直實的”,“假的”內存,例如,一個0x08000000內存地址,它并不對就物理地址上那個大數(shù)組中0x08000000 - 1那個地址元素;之所以是這樣,是因為現(xiàn)代操作系統(tǒng)都提供了一種內存管理的抽像,即虛擬內存(virtual memory)。進程使用虛擬內存中的地址,由操作系統(tǒng)協(xié)助相關硬件,把它“轉換”成真正的物理地址。這個“轉換”,是所有問題討論的關鍵。有了這樣的抽像,一個程序,就可以使用比真實物理地址大得多的地址空間。甚至多個進程可以使用相同的地址。不奇怪,因為轉換后的物理地址并非相同的。可以把連接后的程序反編譯看一下,發(fā)現(xiàn)連接器已經(jīng)為程序分配了一個地址,例如,要調用某個函數(shù)A,代碼不是call A,而是call 0x0811111111 。也就是說,函數(shù)A的地址已經(jīng)被定下來了。沒有這樣的“轉換”,沒有虛擬地址的概念,這樣做是根本行不通的。
  • 3.地址轉換
    • 1.CPU段式管理:邏輯地址轉線性地址
      • CPU要利用其段式內存管理單元,先將為個邏輯地址轉換成一個線程地址。 一個邏輯地址由兩部份組成:段標識符和段內偏移量。段標識符是由一個16位長的字段組成,稱為段選擇符。其中前13位是一個索引號。后面3位包含一些硬件細節(jié),如圖:


        段標識符
      • 通過段標識符中的索引號從GDT或者LDT找到該段的段描述符,段描述符中的base字段是段的起始地址。段描述符:Base字段,它描述了一個段的開始位置的線性地址。一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每個進程自己的,就放在所謂的“局部段描述符表(LDT)”中。GDT在內存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT則在ldtr寄存器中。段起始地址+ 段內偏移量 = 線性地址
        描述符
      • 首先,給定一個完整的邏輯地址[段選擇符:段內偏移地址]:
        • a.看段選擇符的T1=0還是1,知道當前要轉換是GDT中的段,還是LDT中的段,再根據(jù)相應寄存器,得到其地址和大小,我們就有了一個數(shù)組了。
        • b.拿出段選擇符中前13位,可以在這個數(shù)組中,查找到對應的段描述符,這樣,它了Base,即基地址就知道了
        • c.把Base + offset,就是要轉換的線性地址了
    • 2.頁式管理:線性地址轉物理地址
      • 利用其頁式內存管理單元,轉換為最終物理地址。
  • 4.Linux假的段式管理
    • Intel要求兩次轉換,這樣雖說是兼容了,但是卻是很冗余,但是這是intel硬件的要求。 其它某些硬件平臺,沒有二次轉換的概念,Linux也需要提供一個高層抽像,來提供一個統(tǒng)一的界面。所以,Linux的段式管理,事實上只是“哄騙”了一下硬件而已。按照Intel的本意,全局的用GDT,每個進程自己的用LDT——不過Linux則對所有的進程都使用了相同的段來對指令和數(shù)據(jù)尋址。用戶數(shù)據(jù)段、用戶代碼段對應內核中的是內核數(shù)據(jù)段和內核代碼段。在Linux下,邏輯地址與線性地址總是一致的,即邏輯地址的偏移量字段的值與線性地址的值總是相同的。
  • 5.Linux頁式管理
    • CPU的頁式內存管理單元,負責把一個線性地址,最終翻譯為一個物理地址。線性地址被分為以固定長度為單位的組,稱為頁(page),例如一個32位的機器,線性地址最大可為4G,可以用4KB為一個頁來劃分,這頁,整個線性地址就被劃分為一個tatol_page[2^20]的大數(shù)組,共有2的20個次方個頁。另一類“頁”,我們稱之為物理頁,或者是頁框、頁楨的。是分頁單元把所有的物理內存也劃分為固定長度的管理單位,它的長度一般與內存頁是一一對應的。


      線性地址
    • 每個進程都有自己的頁目錄,當進程處于運行態(tài)的時候,其頁目錄地址存放在cr3寄存器中。每一個32位的線性地址被劃分為三部份,[頁目錄索引(10位):頁表索引(10位):頁內偏移(12位)]。依據(jù)以下步驟進行轉換:
      • 從cr3中取出進程的頁目錄地址(操作系統(tǒng)負責在調度進程的時候,把這個地址裝入對應寄存器);
      • 根據(jù)線性地址前十位,在數(shù)組中,找到對應的索引項,因為引入了二級管理模式,頁目錄中的項,不再是頁的地址,而是一個頁表的地址。(又引入了一個數(shù)組),頁的地址被放到頁表中去了;
      • 根據(jù)線性地址的中間十位,在頁表(也是數(shù)組)中找到頁的起始地址;將頁的起始地址與線性地址中最后12位相加。
    • 目的:內存節(jié)約:如果一級頁表中的一個頁表條目為空,那么那所指的二級頁表就根本不會存在。這表現(xiàn)出一種巨大的潛在節(jié)約,因為對于一個典型的程序,4GB虛擬地址空間的大部份都會是未分配的;32位,PGD = 10bit,PUD = PMD = 0,table = 10bit,offset = 12bit ; 64位,PUD和PMD ≠ 0


      線性地址轉換

十六.互斥鎖(mutex)機制,以及互斥鎖和讀寫鎖的區(qū)別

  • 1.概念:
    • 互斥鎖:mutex,用于保證在任何時刻,都只能有一個線程訪問該對象。當獲取鎖操作失敗時,線程會進入睡眠,等待鎖釋放時被喚醒
    • 讀寫鎖:rwlock,分為讀鎖和寫鎖。處于讀操作時,可以允許多個線程同時獲得讀操作。但是同一時刻只能有一個線程可以獲得寫鎖。其它獲取寫鎖失敗的線程都會進入睡眠狀態(tài),直到寫鎖釋放時被喚醒。 注意:寫鎖會阻塞其它讀寫鎖。當有一個線程獲得寫鎖在寫時,讀鎖也不能被其它線程獲取;寫者優(yōu)先于讀者(一旦有寫者,則后續(xù)讀者必須等待,喚醒時優(yōu)先考慮寫者)。適用于讀取數(shù)據(jù)的頻率遠遠大于寫數(shù)據(jù)的頻率的場合。
    • 自旋鎖:spinlock,在任何時刻同樣只能有一個線程訪問對象。但是當獲取鎖操作失敗時,不會進入睡眠,而是會在原地自旋,直到鎖被釋放。這樣節(jié)省了線程從睡眠狀態(tài)到被喚醒期間的消耗,在加鎖時間短暫的環(huán)境下會極大的提高效率。但如果加鎖時間過長,則會非常浪費CPU資源。
    • RCU:即read-copy-update,在修改數(shù)據(jù)時,首先需要讀取數(shù)據(jù),然后生成一個副本,對副本進行修改。修改完成后,再將老數(shù)據(jù)update成新的數(shù)據(jù)。使用RCU時,讀者幾乎不需要同步開銷,既不需要獲得鎖,也不使用原子指令,不會導致鎖競爭,因此就不用考慮死鎖問題了。而對于寫者的同步開銷較大,它需要復制被修改的數(shù)據(jù),還必須使用鎖機制同步并行其它寫者的修改操作。在有大量讀操作,少量寫操作的情況下效率非常高。
  • 2.互斥鎖和讀寫鎖區(qū)別:
    • 1)讀寫鎖區(qū)分讀者和寫者,而互斥鎖不區(qū)分
    • 2)互斥鎖同一時間只允許一個線程訪問該對象,無論讀寫;讀寫鎖同一時間內只允許一個寫者,但是允許多個讀者同時讀對象。

十七.進程狀態(tài)轉換圖,動態(tài)就緒,靜態(tài)就緒,動態(tài)阻塞,靜態(tài)阻塞

  • 1.進程的五種基本狀態(tài):
    進程的五種基本狀態(tài)
    • 1.創(chuàng)建狀態(tài):進程正在被創(chuàng)建
    • 2.就緒狀態(tài):進程被加入到就緒隊列中等待CPU調度運行
    • 3.執(zhí)行狀態(tài):進程正在被運行
    • 4.等待阻塞狀態(tài):進程因為某種原因,比如等待I/O,等待設備,而暫時不能運
    • 5.終止狀態(tài):進程運行完畢
  • 2.交換技術
  • 當多個進程競爭內存資源時,會造成內存資源緊張,并且,如果此時沒有就緒進程,處理機會空閑,I/0速度比處理機速度慢得多,可能出現(xiàn)全部進程阻塞等待I/O。針對以上問題,提出了兩種解決方法:
    • 1.交換技術:換出一部分進程到外存,騰出內存空間;
    • 2.虛擬存儲技術:每個進程只能裝入一部分程序和數(shù)據(jù)。
  • 在交換技術上,將內存暫時不能運行的進程,或者暫時不用的數(shù)據(jù)和程序,換出到外存,來騰出足夠的內存空間,把已經(jīng)具備運行條件的進程,或進程所需的數(shù)據(jù)和程序換入到內存。從而出現(xiàn)了進程的掛起狀態(tài):進程被交換到外存,進程狀態(tài)就成為了掛起狀態(tài)。
  • 3.活動阻塞,靜止阻塞,活動就緒,靜止就緒
    • 1.活動阻塞:進程在內存,但是由于某種原因被阻塞了。
    • 2.靜止阻塞:進程在外存,同時被某種原因阻塞了。
    • 3.活動就緒:進程在內存,處于就緒狀態(tài),只要給CPU和調度就可以直接運行。
    • 4.靜止就緒:進程在外存,處于就緒狀態(tài),只要調度到內存,給CPU和調度就可以運行。

十八.軟鏈接和硬鏈接區(qū)別

  • 為了解決文件共享問題,Linux引入了軟鏈接和硬鏈接。除了為Linux解決文件共享使用,還帶來了隱藏文件路徑、增加權限安全及節(jié)省存儲等好處。若1個inode號對應多個文件名,則為硬鏈接,即硬鏈接就是同一個文件使用了不同的別名,使用ln創(chuàng)建。若文件用戶數(shù)據(jù)塊中存放的內容是另一個文件的路徑名指向,則該文件是軟連接。軟連接是一個普通文件,有自己獨立的inode,但是其數(shù)據(jù)塊內容比較特殊

十九.多線程的同步,鎖的機制

  • 同步的時候用一個互斥量,在訪問共享資源前對互斥量進行加鎖,在訪問完成后釋放互斥量上的鎖。對互斥量進行加鎖以后,任何其他試圖再次對互斥量加鎖的線程將會被阻塞直到當前線程釋放該互斥鎖。如果釋放互斥鎖時有多個線程阻塞,所有在該互斥鎖上的阻塞線程都會變成可運行狀態(tài),第一個變?yōu)檫\行狀態(tài)的線程可以對互斥量加鎖,其他線程將會看到互斥鎖依然被鎖住,只能回去再次等待它重新變?yōu)榭捎谩T谶@種方式下,每次只有一個線程可以向前執(zhí)行。

二十.協(xié)程

  • 協(xié)程:又稱微線程,纖程,英文名Coroutine。協(xié)程看上去也是子程序,但執(zhí)行過程中,在子程序內部可中斷,然后轉而執(zhí)行別的子程序,在適當?shù)臅r候再返回來接著執(zhí)行。例如:
        def A() :
            print '1'
            print '2'
            print '3'
        def B() :
            print 'x'
            print 'y'
            print 'z'
    
    • 由協(xié)程運行結果可能是12x3yz。在執(zhí)行A的過程中,可以隨時中斷,去執(zhí)行B,B也可能在執(zhí)行過程中中斷再去執(zhí)行A。但協(xié)程的特點在于是一個線程執(zhí)行。
  • 協(xié)程和線程區(qū)別
    • 協(xié)程最大的優(yōu)勢就是協(xié)程極高的執(zhí)行效率。因為子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷,和多線程比,線程數(shù)量越多,協(xié)程的性能優(yōu)勢就越明顯。
    • 不需要多線程的鎖機制,因為只有一個線程,也不存在同時寫變量沖突,在協(xié)程中控制共享資源不加鎖,只需要判斷狀態(tài)就好了,所以執(zhí)行效率比多線程高很多。

二十一.用戶態(tài)到內核態(tài)的轉化原理

  • 1.用戶態(tài)切換到內核態(tài)的3種方式
    • 系統(tǒng)調用
    • 異常
    • 外圍設備的中斷
  • 2.用戶態(tài)切換到內核態(tài)的步驟主要包括
    • 從當前進程的描述符中提取其內核棧的ss0及esp0信息
    • 使用ss0和esp0指向的內核棧將當前進程的cs,eip,eflags,ss,esp信息保存起來,這個過程也完成了由用戶棧找到內核棧的切換過程,同時保存了被暫停執(zhí)行的程序的下一條指令
    • 將先前由中斷向量檢索得到的中斷處理程序的cs,eip信息裝入相應的寄存器,開始執(zhí)行中斷處理程序,這時就轉到了內核態(tài)的程序執(zhí)行了

二十二.微內核與宏內核

  • 1.宏內核:除了最基本的進程、線程管理、內存管理外,將文件系統(tǒng),驅動,網(wǎng)絡協(xié)議等等都集成在內核里面,例如Linux內核。
    • 優(yōu)點:效率高
    • 缺點:穩(wěn)定性差,開發(fā)過程中的bug經(jīng)常會導致整個系統(tǒng)掛掉
  • 2.微內核:內核中只有最基本的調度、內存管理。驅動、文件系統(tǒng)等都是用戶態(tài)的守護進程去實現(xiàn)的。
    • 優(yōu)點:穩(wěn)定,驅動等的錯誤只會導致相應進程死掉,不會導致整個系統(tǒng)都崩潰
    • 缺點:效率低。典型代表QNX,QNX的文件系統(tǒng)是跑在用戶態(tài)的進程,稱為resmgr的東西,是訂閱發(fā)布機制,文件系統(tǒng)的錯誤只會導致這個守護進程掛掉。不過數(shù)據(jù)吞吐量就比較不樂觀了。

二十三.正常進程、孤兒進程、僵尸進程

  • 1.正常進程:正常情況下,子進程是通過父進程創(chuàng)建的,子進程再創(chuàng)建新的進程。子進程的結束和父進程的運行是一個異步過程,即父進程永遠無法預測子進程到底什么時候結束。 當一個進程完成它的工作終止之后,它的父進程需要調用wait()或者waitpid()系統(tǒng)調用取得子進程的終止狀態(tài)。unix提供了一種機制可以保證只要父進程想知道子進程結束時的狀態(tài)信息, 就可以得到:在每個進程退出的時候,內核釋放該進程所有的資源,包括打開的文件,占用的內存等。 但是仍然為其保留一定的信息,直到父進程通過wait / waitpid來取時才釋放。保存信息包括:
    • 進程號the process ID
    • 退出狀態(tài)the termination status of the process
    • 運行時間the amount of CPU time taken by the process等
  • 2.孤兒進程:一個父進程退出,而它的一個或多個子進程還在運行,那么那些子進程將成為孤兒進程。孤兒進程將被init進程(進程號為1)所收養(yǎng),并由init進程對它們完成狀態(tài)收集工作。
  • 3.僵尸進程:一個進程使用fork創(chuàng)建子進程,如果子進程退出,而父進程并沒有調用wait或waitpid獲取子進程的狀態(tài)信息,那么子進程的進程描述符仍然保存在系統(tǒng)中。這種進程稱之為僵尸進程。僵尸進程是一個進程必然會經(jīng)過的過程:這是每個子進程在結束時都要經(jīng)過的階段。如果子進程在exit()之后,父進程沒有來得及處理,這時用ps命令就能看到子進程的狀態(tài)是“Z”。如果父進程能及時 處理,可能用ps命令就來不及看到子進程的僵尸狀態(tài),但這并不等于子進程不經(jīng)過僵尸狀態(tài)。如果父進程在子進程結束之前退出,則子進程將由init接管。init將會以父進程的身份對僵尸狀態(tài)的子進程進行處理。
    • 危害: 如果進程不調用wait / waitpid的話, 那么保留的那段信息就不會釋放,其進程號就會一直被占用,但是系統(tǒng)所能使用的進程號是有限的,如果大量的產生僵死進程,將因為沒有可用的進程號而導致系統(tǒng)不能產生新的進程。
    • 解決方法:
      • 外部消滅:通過kill發(fā)送SIGTERM或者SIGKILL信號消滅產生僵尸進程的進程,它產生的僵死進程就變成了孤兒進程,這些孤兒進程會被init進程接管,init進程會wait()這些孤兒進程,釋放它們占用的系統(tǒng)進程表中的資源。
      • 內部解決:子進程退出時向父進程發(fā)送SIGCHILD信號,父進程處理SIGCHILD信號。在信號處理函數(shù)中調用wait進行處理僵尸進程;fork兩次,原理是將子進程成為孤兒進程,從而其的父進程變?yōu)閕nit進程,通過init進程可以處理僵尸進程

二十四.Linux下怎么得到一個文件的100到200行

    sed -n '100,200p' inputfile
    awk 'NR>=100&&NR<=200{print}' inputfile
    head -200 inputfile|tail -100
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容