這些作業系統的概念,保你沒聽過!
作業系統概念
大部分作業系統提供了特定的基礎概念和抽象,例如程序、地址空間、檔案等,它們是需要理解的核心內容。下面我們會簡要介紹一些基本概念,為了說明這些概念,我們會不時的從 UNIX
中提出示例,相同的示例也會存在於其他系統中,我們後面會進行介紹。
程序
作業系統一個很關鍵的概念就是 程序(Process)
。程序的本質就是作業系統執行的一個程式。與每個程序相關的是地址空間(address space)
,這是從某個最小值的儲存位置(通常是零)到某個最大值的儲存位置的列表。在這個地址空間中,程序可以進行讀寫操作。地址空間中存放有可執行程式,程式所需要的資料和它的棧。與每個程序相關的還有資源集,通常包括暫存器(registers)
程式計數器(program counter)
和堆疊指標(stack pointer)
)、開啟檔案的清單、突發的報警、有關的程序清單和其他需要執行程式的資訊。你可以把程序看作是容納執行一個程式所有資訊的一個容器。
對程序建立一種直觀感覺的方式是考慮建立一種多程式的系統。考慮下面這種情況:使用者啟動一個視訊編輯程式,指示它按照某種格式轉換視訊,然後再去瀏覽網頁。同時,一個檢查電子郵件的後臺程序被喚醒並開始執行,這樣,我們目前就會有三個活動程序:視訊編輯器、Web 瀏覽器和電子郵件接收程式。作業系統週期性的掛起一個程序然後啟動執行另一個程序,這可能是由於過去一兩秒鐘程式用完了 CPU 分配的時間片,而 CPU 轉而執行另外的程式。
像這樣暫時中斷程序後,下次應用程式在此啟動時,必須要恢復到與中斷時刻相同的狀態,這在我們使用者看起來是習以為常的事情,但是作業系統內部卻做了巨大的事情。這就像和足球比賽一樣,一場完美精彩的比賽是可以忽略裁判的存在的。這也意味著在掛起時該程序的所有資訊都要被儲存下來。例如,程序可能打開了多個檔案進行讀取。與每個檔案相關聯的是提供當前位置的指標(即下一個需要讀取的位元組或記錄的編號)。當程序被掛起時,必須要儲存這些指標,以便在重新啟動程序後執行的 read
呼叫將能夠正確的讀取資料。在許多作業系統中,與一個程序有關的所有資訊,除了該程序自身地址空間的內容以外,均存放在作業系統的一張表中,稱為 程序表(process table)
所以,一個掛起的程序包括:程序的地址空間(往往稱作磁芯映像
, core image,紀念過去的磁芯儲存器),以及對應的程序表項(其中包括暫存器以及稍後啟動該程序所需要的許多其他資訊)。
與程序管理有關的最關鍵的系統呼叫往往是決定著程序的建立和終止的系統呼叫。考慮一個典型的例子,有一個稱為 命令直譯器(command interpreter)
或 shell
的程序從終端上讀取命令。此時,使用者剛鍵入一條命令要求編譯一個程式。shell 必須先建立一個新程序來執行編譯程式,當編譯程式結束時,它執行一個系統呼叫來終止自己的程序。
如果一個程序能夠建立一個或多個程序(稱為子程序
),而且這些程序又可以建立子程序,則很容易找到程序數,如下所示
上圖表示一個程序樹的示意圖,程序 A 建立了兩個子程序 B 和程序 C,子程序 B 又建立了三個子程序 D、E、F。
合作完成某些作業的相關程序經常需要彼此通訊來完成作業,這種通訊稱為程序間通訊(interprocess communication)
。我們在後面會探討程序間通訊。
其他可用的程序系統呼叫包括:申請更多的記憶體(或釋放不再需要的記憶體),等待一個子程序結束,用另一個程式覆蓋該程式。
有時,需要向一個正在執行的程序傳遞資訊,而該程序並沒有等待接收資訊。例如,一個程序通過網路向另一臺機器上的程序傳送訊息進行通訊。為了保證一條訊息或訊息的應答不丟失。傳送者要求它所在的作業系統在指定的若干秒後傳送一個通知,這樣如果對方尚未收到確認訊息就可以進行重新發送。在設定該定時器後,程式可以繼續做其他工作。
在限定的時間到達後,作業系統會向程序傳送一個 警告訊號(alarm signal)
。這個訊號引起該程序暫時掛起,無論該程序正在做什麼,系統將其暫存器的值儲存到堆疊中,並開始重新啟動一個特殊的訊號處理程,比如重新發送可能丟失的訊息。這些訊號是軟體模擬的硬體中斷,除了定時器到期之外,該訊號可以通過各種原因產生。許多由硬體檢測出來的陷阱,如執行了非法指令或使用了無效地址等,也被轉換成該訊號並交給這個程序。
系統管理器授權每個程序使用一個給定的 UID(User IDentification)
。每個啟動的程序都會有一個作業系統賦予的 UID,子程序擁有與父程序一樣的 UID。使用者可以是某個組的成員,每個組也有一個 GID(Group IDentification)
。
在 UNIX 作業系統中,有一個 UID 是 超級使用者(superuser)
,或者 Windows 中的管理員(administrator)
,它具有特殊的權利,可以違背一些保護規則。在大型系統中,只有系統管理員掌握著那些使用者可以稱為超級使用者。
地址空間
每臺計算機都有一些主存用來儲存正在執行的程式。在一個非常簡單的作業系統中,僅僅有一個應用程式執行在記憶體中。為了執行第二個應用程式,需要把第一個應用程式移除才能把第二個程式裝入記憶體。
複雜一些的作業系統會允許多個應用程式同時裝入記憶體中執行。為了防止應用程式之間相互干擾(包括作業系統),需要有某種保護機制。雖然此機制是在硬體中實現,但卻是由作業系統控制的。
上述觀點涉及對計算機主存的管理和保護。另一種同等重要並與儲存器有關的內容是管理程序的地址空間。通常,每個程序有一些可以使用的地址集合,典型值從 0 開始直到某個最大值。一個程序可擁有的最大地址空間小於主存。在這種情況下,即使程序用完其地址空間,記憶體也會有足夠的記憶體執行該程序。
但是,在許多 32 位或 64 位地址的計算機中,分別有 2^32 或 2^64 位元組的地址空間。如果一個程序有比計算機擁有的主存還大的地址空間,而且該程序希望使用全部的記憶體,那該怎麼處理?在早期的計算機中是無法處理的。但是現在有了一種虛擬記憶體
的技術,正如前面講到過的,作業系統可以把部分地址空間裝入主存,部分留在磁碟上,並且在需要時來回交換它們。
檔案
幾乎所有作業系統都支援的另一個關鍵概念就是檔案系統。如前所述,作業系統的一項主要功能是遮蔽磁碟和其他 I/O 裝置的細節特性,給程式設計師提供一個良好、清晰的獨立於裝置的抽象檔案模型。建立檔案、刪除檔案、讀檔案和寫檔案 都需要系統呼叫。在檔案可以讀取之前,必須先在磁碟上定位和開啟檔案,在檔案讀過之後應該關閉該檔案,有關的系統呼叫則用於完成這類操作。
為了提供儲存檔案的地方,大多數個人計算機作業系統都有目錄(directory)
的概念,從而可以把檔案分組。比如,學生可以給每個課程都建立一個目錄,用於儲存該學科的資源,另一個目錄可以存放電子郵件,再有一個目錄可以存放全球資訊網主頁。這就需要系統呼叫建立和刪除目錄、將已有檔案放入目錄中,從目錄中刪除檔案等。目錄項可以是檔案或者目錄,目錄和目錄之間也可以巢狀,這樣就產生了檔案系統
程序和檔案層次都是以樹狀的結構組織,但這兩種樹狀結構有不少不同之處。一般程序的樹狀結構層次不深(很少超過三層),而檔案系統的樹狀結構要深一些,通常會到四層甚至五層。程序樹層次結構是暫時的,通常最多存在幾分鐘,而目錄層次則可能存在很長時間。程序和檔案在許可權保護方面也是有區別的。一般來說,父程序能控制和訪問子程序,而在檔案和目錄中通常存在一種機制,使檔案所有者之外的其他使用者也能訪問該檔案。
目錄層結構中的每一個檔案都可以通過從目錄的頂部即 根目錄(Root directory)
開始的路徑名(path name)
來確定。絕對路徑名包含了從根目錄到該檔案的所有目錄清單,它們之間用斜槓分隔符分開,在上面的大學院系檔案系統中,檔案 CS101 的路徑名是 /Faculty/Prof.Brown/Courses/CS101
。最開始的斜槓分隔符代表的是根目錄 /
,也就是檔案系統的絕對路徑。
出於歷史原因,Windows 下面的檔案系統以
\
來作為分隔符,但是 Linux 會以/
作為分隔符。
在上面的系統中,每個程序會有一個 工作目錄(working directory)
,對於沒有以斜線開頭給出絕對地址的路徑,將在這個工作目錄下尋找。如果 /Faculty/Prof.Brown
是工作目錄,那麼 /Courses/CS101
與上面給定的絕對路徑名錶示的是同一個檔案。程序可以通過使用系統呼叫指定新的工作目錄,從而變更其工作目錄。
在讀寫檔案之前,首先需要開啟檔案,檢查其訪問許可權。若許可權許可,系統將返回一個小整數,稱作檔案描述符(file descriptor)
,供後續操作使用。若禁止訪問,系統則返回一個錯誤碼。
在 UNIX 中,另一個重要的概念是 特殊檔案(special file)
。提供特殊檔案是為了使 I/O 裝置看起來像檔案一般。這樣,就像使用系統呼叫讀寫檔案一樣,I/O 裝置也可以通過同樣的系統呼叫進行讀寫。特殊檔案有兩種,一種是塊兒特殊檔案(block special file)
和 字元特殊檔案(character special file)
。塊特殊檔案指那些由可隨機存取的塊組成的裝置,如磁碟等。比如開啟一個塊特殊檔案,然後讀取第4塊,程式可以直接訪問裝置的第4塊而不必考慮存放在該檔案的檔案系統結構。類似的,字元特殊檔案用於印表機、調製解調起和其他接受或輸出字元流的裝置。按照慣例,特殊檔案儲存在 /dev
目錄中。例如,/devv/lp 是印表機。
還有一種與程序和檔案相關的特性是管道,管道(pipe)
是一種虛檔案,他可以連線兩個程序
如果 A 和 B 希望通過管道對話,他們必須提前設定管道。當程序 A 相對程序 B 傳送資料時,它把資料寫到管道上,相當於管道就是輸出檔案。這樣,在 UNIX 中兩個程序之間的通訊就非常類似於普通檔案的讀寫了。
保護
計算機中含有大量的資訊,使用者希望能夠對這些資訊中有用而且重要的資訊加以保護,這些資訊包括電子郵件、商業計劃等,管理這些資訊的安全性完全依靠作業系統來保證。例如,檔案提供授權使用者訪問。
比如 UNIX 作業系統,UNIX 作業系統通過對每個檔案賦予一個 9 位二進位制保護程式碼,對 UNIX 中的檔案實現保護。該保護程式碼有三個位子段,一個用於所有者,一個用於與所有者同組(使用者被系統管理員劃分成組)的其他成員,一個用於其他人。每個欄位中有一位用於讀訪問,一位用於寫訪問,一位用於執行訪問。這些位就是著名的 rwx位
。例如,保護程式碼 rwxr-x--x
的含義是所有者可以讀、寫或執行該檔案,其他的組成員可以讀或執行(但不能寫)此檔案、而其他人可以執行(但不能讀和寫)該檔案。
shell
作業系統是執行系統呼叫的程式碼。編輯器、編譯器、彙編程式、連結程式、使用程式以及命令解釋符等,儘管非常重要,非常有用,但是它們確實不是作業系統的組成部分。下面我們著重介紹一下 UNIX 下的命令提示符,也就是 shell
,shell 雖然有用,但它也不是作業系統的一部分,然而它卻能很好的說明作業系統很多特性,下面我們就來探討一下。
shell 有許多種,例如 sh、csh、ksh 以及 bash等,它們都支援下面這些功能,最早起的 shell 可以追溯到 sh
使用者登入時,會同時啟動一個 shell,它以終端作為標準輸入和標準輸出。首先顯示提示符(prompt)
,它可能是一個美元符號($)
,提示使用者 shell 正在等待接收命令,假如使用者輸入
date
shell 會建立一個子程序,並執行 date 做為子程序。在該子程序執行期間,shell 將等待它結束。在子程序完成時,shell 會顯示提示符並等待下一行輸入。
使用者可以將標準輸出重定向到一個檔案中,例如
date > file
同樣的,也可以將標準輸入作為重定向
sort <file1> file2
這會呼叫 sort 程式來接收 file1 的內容並把結果輸出到 file2。
可以將一個應用程式的輸出通過管道作為另一個程式的輸入,因此有
cat file1 file2 file3 | sort > /dev/lp
這會呼叫 cat 應用程式來合併三個檔案,將其結果輸送到 sort 程式中並按照字典進行排序。sort 應用程式又被重定向到 /dev/lp ,顯然這是一個列印操作。
系統呼叫
我們已經可以看到作業系統提供了兩種功能:為使用者提供應用程式抽象和管理計算機資源。對於大部分在應用程式和作業系統之間的互動主要是應用程式的抽象,例如建立、寫入、讀取和刪除檔案。計算機的資源管理對使用者來說基本上是透明的。因此,使用者程式和作業系統之間的介面主要是處理抽象。為了真正理解作業系統的行為,我們必須仔細的分析這個介面。
多數現代作業系統都有功能相同但是細節不同的系統呼叫,引發作業系統的呼叫依賴於計算機自身的機制,而且必須用匯編程式碼表達。任何單 CPU 計算機一次執行執行一條指令。如果一個程序在使用者態下執行使用者程式,例如從檔案中讀取資料。那麼如果想要把控制權交給作業系統控制,那麼必須執行一個異常指令或者系統呼叫指令。作業系統緊接著需要引數檢查找出所需要的呼叫程序。作業系統緊接著進行引數檢查找出所需要的呼叫程序。然後執行系統呼叫,把控制權移交給系統呼叫下面的指令。大致來說,系統呼叫就像是執行了一個特殊的過程呼叫,但是隻有系統呼叫能夠進入核心態而過程呼叫則不能進入核心態。
為了能夠了解具體的呼叫過程,下面我們以 read
方法為例來看一下呼叫過程。像上面提到的那樣,會有三個引數,第一個引數是指定檔案、第二個是指向緩衝區、第三個引數是給定需要讀取的位元組數。就像幾乎所有系統呼叫一樣,它通過使用與系統呼叫相同的名稱來呼叫一個函式庫,從而從C程式中呼叫:read。
count = read(fd,buffer,nbytes);
系統呼叫在 count 中返回實際讀出的位元組數。這個值通常與 nbytes 相同,但也可能更小。比如在讀過程中遇到了檔案尾的情況。
如果系統呼叫不能執行,不管是因為無效的引數還是磁碟錯誤,count 的值都會被置成 -1,然後在全域性變數 errno
中放入錯誤訊號。程式應該進場檢查系統呼叫的結果以瞭解是否出錯。
系統呼叫是通過一系列的步驟實現的,為了更清楚的說明這個概念,我們還以 read 呼叫為例,在準備系統呼叫前,首先會把引數壓入堆疊,如下所示
C 和 C++ 編譯器使用逆序(必須把第一個引數賦值給 printf(格式字串),放在堆疊的頂部)。第一個引數和第三個引數都是值呼叫,但是第二個引數通過引用傳遞,即傳遞的是緩衝區的地址(由 & 指示),而不是緩衝的內容。然後是 C 呼叫系統庫的 read 函式,這也是第四步。
在由組合語言寫成的庫過程中,一般把系統呼叫的編號放在作業系統所期望的地方,如暫存器(第五步)。然後執行一個 TRAP
指令,將使用者態切換到核心態,並在核心中的一個固定地址開始執行第六步。TRAP 指令實際上與過程呼叫指令非常相似,它們後面都跟隨一個來自遠處位置的指令,以及供以後使用的一個儲存在棧中的返回地址。
TRAP 指令與過程呼叫指令存在兩個方面的不同
- TRAP 指令會改變作業系統的狀態,由使用者態切換到核心態,而過程呼叫不改變模式
- 其次,TRAP 指令不能跳轉到任意地址上。根據機器的體系結構,要麼跳轉到一個單固定地址上,或者指令中有一 8 位長的欄位,它給定了記憶體中一張表格的索引,這張表格中含有跳轉地址,然後跳轉到指定地址上。
跟隨在 TRAP 指令後的核心程式碼開始檢查系統呼叫編號,然後dispatch
給正確的系統呼叫處理器,這通常是通過一張由系統呼叫編號所引用的、指向系統呼叫處理器的指標表來完成第七步。此時,系統呼叫處理器執行第八步,一旦系統呼叫處理器完成工作,控制權會根據 TRAP 指令後面的指令中返回給函式呼叫庫第九步。這個過程接著以通常的過程呼叫返回的方式,返回到客戶應用程式,這是第十步。然後呼叫完成後,作業系統還必須清除使用者堆疊,然後增加堆疊指標(increment stackpointer)
,用來清除呼叫 read 之前壓入的引數。從而完成整個 read 呼叫過程。
在上面的第九步中我們說道,控制可能返回 TRAP 指令後面的指令,把控制權再移交給呼叫者這個過程中,系統呼叫會發生阻塞,從而避免應用程式繼續執行。這麼做是有原因的。例如,如果試圖讀鍵盤,此時並沒有任何輸入,那麼呼叫者就必須被阻塞。在這種情形下,作業系統會檢查是否有其他可以執行的程序。這樣,當有使用者輸入 時候,程序會提醒作業系統,然後返回第 9 步繼續執行。
下面,我們會列出一些常用的 POSIX
系統呼叫,POSIX 系統呼叫大概有 100 多個,它們之中最重要的一些呼叫見下表
程序管理
呼叫 | 說明 |
---|---|
pid = fork() | 建立與父程序相同的子程序 |
pid = waitpid(pid, &statloc,options) | 等待一個子程序終止 |
s = execve(name,argv,environp) | 替換一個程序的核心映像 |
exit(status) | 終止程序執行並返回狀態 |
檔案管理
呼叫 | 說明 |
---|---|
fd = open(file, how,...) | 開啟一個檔案使用讀、寫 |
s = close(fd) | 關閉一個開啟的檔案 |
n = read(fd,buffer,nbytes) | 把資料從一個檔案讀到緩衝區中 |
n = write(fd,buffer,nbytes) | 把資料從緩衝區寫到一個檔案中 |
position = iseek(fd,offset,whence) | 移動檔案指標 |
s = stat(name,&buf) | 取得檔案狀態資訊 |
目錄和檔案系統管理
呼叫 | 說明 |
---|---|
s = mkdir(nname,mode) | 建立一個新目錄 |
s = rmdir(name) | 刪去一個空目錄 |
s = link(name1,name2) | 建立一個新目錄項 name2,並指向 name1 |
s = unlink(name) | 刪去一個目錄項 |
s = mount(special,name,flag) | 安裝一個檔案系統 |
s = umount(special) | 解除安裝一個檔案系統 |
其他
呼叫 | 說明 |
---|---|
s = chdir(dirname) | 改變工作目錄 |
s = chmod(name,mode) | 修改一個檔案的保護位 |
s = kill(pid, signal) | 傳送訊號給程序 |
seconds = time(&seconds) | 獲取從 1970 年1月1日至今的時間 |
上面的系統呼叫引數中有一些公共部分,例如 pid 系統程序 id,fd 是檔案描述符,n 是位元組數,position 是在檔案中的偏移量、seconds 是流逝時間。
從巨集觀角度上看,這些系統調所提供的服務確定了多數作業系統應該具有的功能,下面分別來對不同的系統呼叫進行解釋
用於程序管理的系統呼叫
在 UNIX 中,fork
是唯一可以在 POSIX 中建立程序的途徑,它建立一個原有程序的副本,包括所有的檔案描述符、暫存器等內容。在 fork 之後,原有程序以及副本(父與子)就分開了。在 fork 過程中,所有的變數都有相同的值,雖然父程序的資料通過複製給子程序,但是後續對其中任何一個程序的修改不會影響到另外一個。fork 呼叫會返回一個值,在子程序中該值為 0 ,並且在父程序中等於子程序的 程序識別符號(Process IDentified,PID)
。使用返回的 PID,就可以看出來哪個是父程序和子程序。
在多數情況下, 在 fork 之後,子程序需要執行和父程序不一樣的程式碼。從終端讀取命令,建立一個子程序,等待子程序執行命令,當子程序結束後再讀取下一個輸入的指令。為了等待子程序完成,父程序需要執行 waitpid
系統呼叫,父程序會等待直至子程序終止(若有多個子程序的話,則直至任何一個子程序終止)。waitpid 可以等待一個特定的子程序,或者通過將第一個引數設為 -1 的方式,等待任何一個比較老的子程序。當 waitpid 完成後,會將第二個引數 statloc
所指向的地址設定為子程序的退出狀態(正常或異常終止以及退出值)。有各種可使用的選項,它們由第三個引數確定。例如,如果沒有已經退出的子程序則立刻返回。
那麼 shell 該如何使用 fork 呢?在鍵入一條命令後,shell 會呼叫 fork 命令建立一個新的程序。這個子程序會執行使用者的指令。通過使用 execve
系統呼叫可以實現系統執行,這個系統呼叫會引起整個核心映像被一個檔案所替代,該檔案由第一個引數給定。下面是一個簡化版的例子說明 fork、waitpid 和 execve 的使用
#define TRUE 1
while(TRUE){ /* 一直迴圈下去 */
type_prompt(); /* 在螢幕上顯示提示符 */
read_command(command,parameters) /* 從終端讀取輸入 */
if(fork() != 0){ /* fork 子程序 */
/* 父程式碼 */
waitpid(-1, &status, 0); /* 等待子程序執行完畢 */
}else{
/* 子程式碼 */
execve(command,parameters,0) /* 執行命令 */
}
}
一般情況下,execve 有三個引數:將要執行的檔名稱,一個指向變數陣列的指標,以及一個指向環境陣列的指標。這裡對這些引數做一個簡要的說明。
先看一個 shell 指令
cp file1 file2
此命令把 file1 複製到 file2 檔案中,在 shell 執行 fork 之後,子程序定位並執行檔案拷貝,並將原始檔和目標檔案的名稱傳遞給它。
cp 的主程式(以及包含其他大多數 C 程式的主程式)包含宣告
main(argc,argv,envp)
其中 argc 是命令列中引數數目的計數,包括程式名稱。對於上面的例子,argc
是3。第二個引數argv
是陣列的指標。該陣列的元素 i 是指向該命令列第 i 個字串的指標。在上面的例子中,argv[0] 指向字串 cp,argv[1] 指向字串 file1,argv[2] 指向字串 file2。main 的第三個引數是指向環境的指標,該環境是一個數組,含有 name = value
的賦值形式,用以將諸如終端型別以及根目錄等資訊傳送給程式。這些變數通常用來確定使用者希望如何完成特定的任務(例如,使用預設印表機)。在上面的例子中,沒有環境引數傳遞給 execve ,所以環境變數是 0 ,所以 execve 的第三個引數為 0 。
可能你覺得 execve 過於複雜,這時候我要鼓勵一下你,execve 可能是 POSIX 的全部系統呼叫中最複雜的一個了,其他都比較簡單。作為一個簡單的例子,我們再來看一下 exit
,這是程序在執行完成後應執行的系統呼叫。這個系統呼叫有一個引數,它的退出狀態是 0 - 255 之間,它通過 waitpid 系統呼叫中的 statloc 返回給父級。
UNIX 中的程序將記憶體劃分成三個部分:text segment,文字區
,例如程式程式碼,data segment,資料區
,例如變數,stack segment
,棧區域。資料向上增長而堆疊向下增長,如下圖所示
上圖能說明三個部分的記憶體分配情況,夾在中間的是空閒區,也就是未分配的區域,堆疊在需要時自動的擠壓空閒區域,不過資料段的擴充套件是顯示地通過系統呼叫 brk
進行的,在資料段擴充後,該系統呼叫指向一個新地址。但是,這個呼叫不是 POSIX 標準中定義的,對於儲存器的動態分配,鼓勵程式設計師使用 malloc
函式,而 malloc 的內部實現則不是一個適合標準化的主題,因為幾乎沒有程式設計師直接使用它。
用於檔案管理的系統呼叫
許多系統呼叫都與檔案系統有關,要讀寫一個檔案,必須先將其開啟。這個系統呼叫通過絕對路徑名或指向工作目錄的相對路徑名指定要開啟檔案的名稱,而程式碼 O_RDONLY
、 O_WRONLY
或 O_RDWR
的含義分別是隻讀、只寫或者兩者都可以,為了建立一個新檔案,使用 O_CREATE
引數。然後可使用返回的檔案描述符進行讀寫操作。接著,可以使用 close 關閉檔案,這個呼叫使得檔案描述符在後續的 open 中被再次使用。
最常用的呼叫還是 read
和 write
,我們再前面探討過 read 呼叫,write 具有與 read 相同的引數。
儘管多數程式頻繁的讀寫檔案,但是仍有一些應用程式需要能夠隨機訪問一個檔案的任意部分。與每個檔案相關的是一個指向檔案當前位置的指標。在順序讀寫時,該指標通常指向要讀出(寫入)的下一個位元組。Iseek
呼叫可以改變該位置指標的值,這樣後續的 read 或 write 呼叫就可以在檔案的任何地方開始。
Iseek 有三個引數,position = iseek(fd,offset,whence)
,第一個是檔案描述符,第二個是檔案位置,第三個是說明該檔案位置是相對於檔案起始位置,當前位置還是檔案的結尾。在修改了指標之後,Iseek 所返回的值是檔案中的絕對位置。
UNIX 為每個檔案儲存了該檔案的型別(普通檔案、特殊檔案、目錄等)、大小,最後修改時間以及其他資訊,程式可以通過 stat
系統呼叫檢視這些資訊。s = stat(name,&buf)
,第一個引數指定了被檢查的檔案;第二個引數是一個指標,該指標指向存放這些資訊的結構。對於一個開啟的檔案而言,fstat 呼叫完成同樣的工作。
用於目錄管理的系統呼叫
下面我們探討目錄和整個檔案系統的系統呼叫,上面探討的是和某個檔案有關的系統呼叫。 mkdir
和 rmdir
分別用於建立s = mkdir(nname,mode)
和刪除 s = rmdir(name)
空目錄,下一個呼叫是 s = link(name1,name2)
它的作用是允許同一個檔案以兩個或者多個名稱出現,多數情況下是在不同的目錄中使用 link ,下面我們探討一下 link 是如何工作的
圖中有兩個使用者 ast
和 jim
,每個使用者都有他自己的一個目錄和一些檔案,如果 ast 要執行一個包含下面系統呼叫的應用程式
link("/usr/jim/memo", "/usr/ast/note");
jim 中的 memo 檔案現在會進入到 ast 的目錄中,在 note 名稱下。此後,/usr/jim/memo
和 /usr/ast/note
會有相同的名稱。
使用者目錄是儲存在 /usr,/user,/home 還是其他位置,都是由本地系統管理員決定的。
要理解 link 是如何工作的需要清楚 link 做了什麼操作。UNIX 中的每個檔案都有一個獨一無二的版本,也稱作 i - number,i-編號
,它標示著不同檔案的版本。這個 i - 編號是 i-nodes,i-節點
表的索引。每個檔案都會表明誰擁有這個檔案,這個磁碟塊的位置在哪,等等。目錄只是一個包含一組(i編號,ASCII名稱)對應的檔案。UNIX 中的第一個版本中,每個目錄項都會有 16 個位元組,2 個位元組對應 i - 編號和 14 個位元組對應其名稱。現在需要一個更復雜的結構需要支援長檔名,但是從概念上講一個目錄仍是一系列(i-編號,ASCII 名稱)的集合。在上圖中,mail
的 i-編號為 16,依此類推。link 只是利用某個已有檔案的 i-編號,建立一個新目錄項(也許用一個新名稱)。在上圖 b 中,你會發現有兩個相同的 70 i-編號的檔案,因此它們需要有相同的檔案。如果其中一個使用了 unlink
系統呼叫的話,其中一個會被移除,另一個將保留。如果兩個檔案都移除了,則 UNIX 會發現該檔案不存在任何沒有目錄項(i-節點中的一個域記錄著指向該檔案的目錄項),就會把該檔案從磁碟中移除。
就像我們上面提到過的那樣,mount
系統 s = mount(special,name,flag)
呼叫會將兩個檔案系統合併為一個。通常的情況是將根檔案系統分佈在硬碟(子)分割槽上,並將使用者檔案分佈在另一個(子)分割槽上,該根檔案系統包含常用命令的二進位制(可執行)版本和其他使用頻繁的檔案。然後,使用者就會插入可讀取的 USB 硬碟。
通過執行 mount 系統呼叫,USB 檔案系統可以被新增到根檔案系統中,
如果用 C 語言來執行那就是
mount("/dev/sdb0","/mnt",0)
這裡,第一個引數是 USB 驅動器 0 的塊特殊檔名稱,第二個引數是被安裝在樹中的位置,第三個引數說明將要安裝的檔案系統是可讀寫的還是隻讀的。
當不再需要一個檔案系統時,可以使用 umount 移除之。
其他系統呼叫
除了程序、檔案、目錄系統呼叫,也存在其他系統呼叫的情況,下面我們來探討一下。我們可以看到上面其他系統呼叫只有四種,首先來看第一個 chdir,chdir 呼叫更改當前工作目錄,在呼叫
chdir("/usr/ast/test");
後,開啟 xyz 檔案,會開啟 /usr/ast/test/xyz
檔案,工作目錄的概念消除了總是需要輸入長檔名的需要。
在 UNIX 系統中,每個檔案都會有保護模式,這個模式會有一個讀-寫-執行
位,它用來區分所有者、組和其他成員。chmod
系統呼叫提供改變檔案模式的操作。例如,要使一個檔案除了對所有者之外的使用者可讀,你可以執行
chmod("file",0644);
kill
系統呼叫是使用者和使用者程序傳送訊號的方式,如果一個程序準備好捕捉一個特定的訊號,那麼在訊號捕捉之前,會執行一個訊號處理程式。如果程序沒有準備好捕捉特定的訊號,那麼訊號的到來會殺掉該程序(此名字的由來)。
POSIX 定義了若干時間處理的程序。例如,time
以秒為單位返回當前時間,0 對應著 1970 年 1月 1日。在一臺 32 位字的計算機中,time 的最大值是 (2^32) - 1秒,這個數字對應 136 年多一點。所以在 2106 年,32 位的 UNIX 系統會發飆。如果讀者現在有 32 位 UNIX 系統,建議在 2106 年更換位 64 位作業系統(偷笑~)。
Win 32 API
上面我們提到的都是 UNIX 系統呼叫,現在我們來聊聊 Win 32 中的系統呼叫。Windows 和 UNIX 在各自的程式設計方式上有著根本的不同。UNIX 程式由執行某些操作或執行其他操作的程式碼組成,進行系統呼叫以執行某些服務。Windows 系統則不同,Windows 應用程式通常是由事件驅動的。主程式會等待一些事件發生,然後呼叫程式去處理。最簡單的事件處理是鍵盤敲擊和滑鼠滑過,或者是滑鼠點選,或者是插入 USB 驅動,然後作業系統呼叫處理器去處理事件,更新螢幕和更新程式內部狀態。這是與 UNIX 不同的設計風格。
當然,Windows 也有系統呼叫。在 UNIX 中,系統呼叫(比如 read)和系統呼叫所使用的呼叫庫(例如 read)幾乎是一對一的關係。而在 Windows 中,情況則大不相同。首先,函式庫的呼叫和實際的系統呼叫幾乎是不對應的。微軟定義了一系列過程,稱為 Win32應用程式設計介面(Application Programming Interface)
,程式設計師通過這套標準的介面來實現系統呼叫。這個介面支援從 Windows 95 版本以來所有的 Windows 版本。
Win32 API 呼叫的數量是非常巨大的,有數千個多。但這些呼叫並不都是在核心態的模式下執行時,有一些是在使用者態的模型下執行。Win32 API 有大量的呼叫,用來管理視窗、幾何圖形、文字、字型、滾動條、對話方塊、選單以及 GUI 的其他功能。為了使圖形子系統在核心態下執行,需要系統呼叫,否則就只有函式庫呼叫。
我們把關注點放在和 Win32 系統呼叫中來,我們可以簡單看一下 Win32 API 中的系統呼叫和 UNIX 中有什麼不同(並不是所有的系統呼叫)
UNIX | Win32 | 說明 |
---|---|---|
fork | CreateProcess | 建立一個新程序 |
waitpid | WaitForSingleObject | 等待一個程序退出 |
execve | none | CraeteProcess = fork + servvice |
exit | ExitProcess | 終止執行 |
open | CreateFile | 建立一個檔案或開啟一個已有的檔案 |
close | CloseHandle | 關閉檔案 |
read | ReadFile | 從單個檔案中讀取資料 |
write | WriteFile | 向單個檔案寫資料 |
lseek | SetFilePointer | 移動檔案指標 |
stat | GetFileAttributesEx | 獲得不同的檔案屬性 |
mkdir | CreateDirectory | 建立一個新的目錄 |
rmdir | RemoveDirectory | 移除一個空的目錄 |
link | none | Win32 不支援 link |
unlink | DeleteFile | 銷燬一個已有的檔案 |
mount | none | Win32 不支援 mount |
umount | none | Win32 不支援 mount,所以也不支援mount |
chdir | SetCurrentDirectory | 切換當前工作目錄 |
chmod | none | Win32 不支援安全 |
kill | none | Win32 不支援訊號 |
time | GetLocalTime | 獲取當前時間 |
上表中是 UNIX 呼叫大致對應的 Win32 API 系統呼叫,簡述一下上表。CreateProcess
用於建立一個新程序,它把 UNIX 中的 fork 和 execve 兩個指令合成一個,一起執行。它有許多引數用來指定新建立程序的性質。Windows 中沒有類似 UNIX 中的程序層次,所以不存在父程序和子程序的概念。在程序建立之後,建立者和被建立者是平等的。WaitForSingleObject
用於等待一個事件,等待的事件可以是多種可能的事件。如果有引數指定了某個程序,那麼呼叫者將等待指定的程序退出,這通過 ExitProcess
來完成。
然後是6個檔案操作,在功能上和 UNIX 的呼叫類似,然而在引數和細節上是不同的。和 UNIX 中一樣,檔案可以開啟,讀取,寫入,關閉。SetFilePointer
和 GetFileAttributesEx
設定檔案的位置並取得檔案的屬性。
Windows 中有目錄,目錄分別用 CreateDirectory
以及 RemoveDirectory
API 呼叫建立和刪除。也有對當前的目錄的標記,這可以通過 SetCurrentDirectory
來設定。使用GetLocalTime
可獲得當前時間。
Win32 介面中沒有檔案的連結、檔案系統的 mount、umount 和 stat ,當然, Win32 中也有大量 UNIX 中沒有的系統呼叫,特別是對 GUI 的管理和呼叫。
作業系統結構
下面我們會探討作業系統的幾種結構,主要包括單體結構、分層系統、微核心、客戶-服務端系統、虛擬機器和外核等。下面以此來探討一下
單體系統
到目前為止,在大多數系統中,整個系統在核心態以單一程式的方式執行。整個作業系統是以程式集合來編寫的,連結在一塊形成一個大的二進位制可執行程式。使用此技術時,如果系統中的每個過程都提供了前者所需的一些有用的計算,則它可以自由呼叫任何其他過程。在單體系統中,呼叫任何一個所需要的程式都非常高效,但是上千個不受限制的彼此呼叫往往非常臃腫和笨拙,而且單體系統必然存在單體問題,那就是隻要系統發生故障,那麼任何系統和應用程式將不可用,這往往是災難性的。
在單體系統中構造實際目標程式時,會首先編譯所有單個過程(或包含這些過程的檔案),然後使用系統連結器將它們全部繫結到一個可執行檔案中
對於單體系統,往往有下面幾種建議
- 需要有一個主程式,用來呼叫請求服務程式
- 需要一套服務過程,用來執行系統呼叫
- 需要一套服務程式,用來輔助服務過程呼叫
在單體系統中,對於每個系統呼叫都會有一個服務程式來保障和執行。需要一組實用程式來彌補服務程式需要的功能,例如從使用者程式中獲取資料。可將各種過程劃分為一個三層模型
除了在計算機初啟動時所裝載的核心作業系統外,許多作業系統還支援額外的擴充套件。比如 I/O 裝置驅動和檔案系統。這些部件可以按需裝載。在 UNIX 中把它們叫做 共享庫(shared library)
,在 Windows 中則被稱為 動態連結庫(Dynamic Link Library,DLL)
。他們的副檔名為 .dll
,在 C:\Windows\system32
目錄下存在 1000 多個 DLL 檔案,所以不要輕易刪除 C 盤檔案,否則可能就炸了哦。
分層系統
分層系統使用層來分隔不同的功能單元。每一層只與該層的上層和下層通訊。每一層都使用下面的層來執行其功能。層之間的通訊通過預定義的固定介面通訊。
分層系統是由 E.W.Dijkstar
和他的學生在荷蘭技術學院所開發的 THE 系統。
把上面單體系統進一步通用化,就變為了一個層次式結構的作業系統,它的上層軟體都是在下層軟體的基礎之上構建的。該系統分為六層,如下所示
層號 | 功能 |
---|---|
5 | 操作員 |
4 | 使用者程式 |
3 | 輸入/輸出管理 |
2 | 操作員-程序通訊 |
1 | 儲存器和磁鼓管理 |
0 | 處理器分配和多道程式程式設計 |
處理器在 0 層執行,當中斷髮生或定時器到期時,由該層完成程序切換;在第 0 層之上,系統由一些連續的程序組成,編寫這些程序時不用再考慮在單處理器上多程序執行的細節。記憶體管理在第 1 層,它分配程序的主存空間。第 1 層軟體保證一旦需要訪問某一頁面,該頁面必定已經在記憶體中,並且在頁面不需要的時候將其移出。
第 2 層處理程序與操作員控制檯(即使用者)之間的通訊。第 3 層管理 I/O 裝置和相關的資訊流緩衝區。第 4 層是使用者程式層,使用者程式不用考慮程序、記憶體、控制檯或 I/O 裝置管理等細節。系統操作員在第 5 層。
微核心
在分層方式中,設計者要確定在哪裡劃分 核心-使用者
的邊界。傳統上,所有的層都在核心中,但是這樣做沒有必要。事實上,儘可能減少核心態中功能可能是更好的做法。因為核心中的錯誤很難處理,一旦核心態中出錯誤會拖累整個系統。
所以,為了實現高可靠性,將作業系統劃分成小的、層級之間能夠更好定義的模組是很有必要的,只有一個模組 --- 微核心 --- 執行在核心態,其餘模組可以作為普通使用者程序執行。由於把每個裝置驅動和檔案系統分別作為普通使用者程序,這些模組中的錯誤雖然會使這些模組崩潰,但是不會使整個系統宕機。
MINIX 3
是微核心的代表作,它的具體結構如下
在核心的外部,系統的構造有三層,它們都在使用者態下執行,最底層是裝置驅動器。由於它們都在使用者態下執行,所以不能物理的訪問 I/O 埠空間,也不能直接發出 I/O 命令。相反,為了能夠對 I/O 裝置程式設計,驅動器構建一個結構,指明哪個引數值寫到哪個 I/O 埠,並聲稱一個核心呼叫,這樣就完成了一次呼叫過程。
位於使用者態的驅動程式上面是伺服器
層,包含有伺服器,它們完成作業系統的多數工作。由一個或多個檔案伺服器管理著檔案系統,程序管理器建立、銷燬和管理程序。伺服器中有一個特殊的伺服器稱為 再生伺服器(reincarnation server)
,它的任務就是檢查伺服器和驅動程式的功能是否正確,一旦檢查出來錯誤,它就會補上去,無需使用者干預。這種方式使得系統具有可恢復性,並具有較高的可靠性。
微核心中的核心還具有一種 機制
與 策略
分離的思想。比如系統排程,一個比較簡單的排程演算法是,對每個程序賦予一個優先順序,並讓核心執行具有最高優先順序的程序。這裡,核心機制就是尋找最高的優先順序程序並執行。而策略(賦予程序優先順序)可以在使用者態中的程序完成。在這種模式中,策略和機制是分離的,從而使核心變得更小。
客戶-伺服器模式
微核心思想的策略是把程序劃分為兩類:伺服器
,每個伺服器用來提供服務;客戶端
,使用這些服務。這個模式就是所謂的 客戶-伺服器
模式。
客戶-伺服器模式會有兩種載體,一種情況是一臺計算機既是客戶又是伺服器,在這種方式下,作業系統會有某種優化;但是普遍情況下是客戶端和伺服器在不同的機器上,它們通過區域網或廣域網連線。
客戶通過傳送訊息與伺服器通訊,客戶端並不需要知道這些訊息是在本地機器上處理,還是通過網路被送到遠端機器上處理。對於客戶端而言,這兩種情形是一樣的:都是傳送請求並得到迴應。
越來越多的系統,包括家裡的 PC,都成為客戶端,而在某地執行的大型機器則成為伺服器。許多 web 就是以這種方式執行的。一臺 PC 向某個伺服器請求一個 Web 頁面,伺服器把 Web 頁面返回給客戶端,這就是典型的客服-伺服器模式
文章參考:
《現代作業系統》第四版
https://baike.baidu.com/item/作業系統/192?fr=aladdin
《Modern Operating System》forth edition
http://faculty.cs.niu.edu/~hutchins/csci360/hchnotes/psw.htm
https://www.computerhope.com/jargon/c/clockcyc.htm
《B站-作業系統》
https://www.bilibili.com/video/av9555596?from=search&seid=8107077283516919308
https://en.wikipedia.org/wiki/System_call
http://c.biancheng.net/cpp/html/238.html
http://www.dossier-andreas.net/software_architecture/layers.html
相關推薦
這些作業系統的概念,保你沒聽過!
作業系統概念 大部分作業系統提供了特定的基礎概念和抽象,例如程序、地址空間、檔案等,它們是需要理解的核心內容。下面我們會簡要介紹一些基本概念,為了說明這些概念,我們會不時的從 UNIX 中提出示例,相同的示例也會存在於其他系統中,我們後面會進行介紹。 程序 作業系統一個很關鍵的概念就是 程序(Process)
老祖宗的絕招對治腰疼,別說你沒聽過
現代人深受腰痛之苦,還有頸椎病,滑鼠手,等等。科技的發達讓人們減少了勞累,更多的時間躺在沙發上,坐在椅子上,卻疏於鍛鍊。 作為程式設計師,長時間加班工作,滑鼠不離手,椅子不離身,高強度的工作讓很
不記住這些作業系統知識,拿什麼備戰校招!
這些知識點是我看了不少面經,以及查了不少資料總結的,目前我也正在每天牢記,考試只要考到作業系統,必然會考這些,希望這些能幫助到您,也可以提出寶貴意見!1、執行緒和程序的基本概念 程序:程序是具有一定獨立功能的程式關於某個資料集合上的一次執行活動,是系統進行資源分配和排程的一個
你沒聽過的梅森旋轉演算法
(標準開頭) 如果單獨提梅森旋轉演算法可能大家都很陌生,但如果說到C++11的random可能大家就都熟悉多了。事實上,C++,python等多種計算機語言的隨機數都是通過梅森旋轉演算法產生的。(也有一個稱呼是梅森纏繞演算法) 那,本文就著重介紹這個梅森螺旋旋轉演算法 (演算法本身挺學術的,我努力寫得輕鬆點)
你可能沒聽過“智慧製造”,但它肯定改變了你的生活
“智慧製造”這詞兒有點新鮮,一聽感覺和咱們沒什麼關係嘛,平時也很少接觸到相關的行業。可能學工科的同學、從事工業生產的朋友們會熟悉一些,那為啥說它改變了我們的生活呢?咱們慢慢道來。 從智慧製造的核心技術上來說,可以歸結為——大資料、物聯網、雲端計算。這個嘛,你絕對聽說過了,你看電視機裡頭天天講這些,那個男人經
這些奇葩的排序演算法,你沒見過動畫吧?
如果有人問你哪種排序演算法是你最喜歡的,可能你會偏愛簡單的氣泡排序,也有可能會選擇具備分治思想的快速排序或者歸併排序。 但如果有人問你你所見過哪些奇葩的演算法時,你的回答會是什麼? 下面,我就將網上腦洞大開的一些奇葩演算法畫出來,以饗讀者。 睡眠排序 根據CPU的排程演算法實現的,對一組資料進行排序,
這幾個軟體你可能沒聽過,但真的好用到爆!
1、Listary 第一款神器叫Listary,這是一款檔案搜尋的工具。 電腦上檔案一多,總是有些檔案不知道放哪了,雖然電腦也自帶檔案搜尋,但很慢;提起檔案搜尋工具,好多人應該還知道一個軟體,叫everything,搜尋的速度也很快。 與Every
Open Infrastructure丹佛峰會即將召開,這些邊緣計算議題等你來聽
決心 駕駛 使用 同方 人工智能 ont nfv lin 機器 導言:首屆Open Infrastructure峰會將落戶美國科羅拉多州丹佛市,九州雲技術團隊積極參與其中,並將分享三大與“邊緣計算”相關的議題,2019年4月29-5月1日,丹佛峰會現場等你來聽! 當地時間
用了這麽多年Linux,這些命令使用技巧也許你還不知道!
shadow AC line 換行符 4.3 star exec HA 轉發 在Unix/Linux下,高效工作方式不是操作圖形頁面,而是命令行操作,命令行意味著更容易自動化。使用過Linux系統的朋友應該都知道它的命令行強大之處。話說回來了,以下這些命令使用技巧你又知道多
中國最牛的7家人工智能企業,2個幾乎沒聽過,1個由微軟投資
androi 集成 高通 int 電子商務 社交網絡 平臺服 發展 研究院 中國最牛的7家人工智能企業,2個幾乎沒聽過,1個由微軟投資 註:本文非官方發布,部分數據不準確,僅供參考! 數月前,有關研究院推出AI First2017-2018中國人工智能先行企業榜在北京揭曉。
程式設計師:其實我覺得吧,壓力也沒那麼大!網友:那你下過班嗎?
很多年前人說男怕入錯行,女怕嫁錯郎。一入IT就是豬狗不如的生活,嫁人千萬別嫁IT程式猿,現在過了才十年左右,沒想到程式猿已經大翻身了,不信你看看BAT的工資,高的嚇人,夠我搬好久磚了。 如果有正在學java的程式設計師,可來我們的java技術學習扣qun哦:72340,3928,小編花了近一個月
理解五個基本概念,讓你更像機器學習專家
大多數人可能對機器學習有點恐懼或困惑。 腦子中會有它到底是什麼,它有什麼發展方向,我現在可以通過它掙錢嗎等等這樣的問題。 這些問題的提出都是有依據的。事實上,你可能沒有意識到自己其實多年來一直在訓練機器學習模型。你看過蘋果手機或者是Facebook上的照片吧? 你知道它如何向你展示一組面孔並要求你識別它
每天用兩小時看這些Python資料,讓你從小白到大神!
想必有很多人想接觸Python這門程式設計,但資料找的都是相對的零散,並不系統。這裡我向大家提供一個系統的資料方便大家學習。在分享之前。我先來分享一下什麼是Python。畢竟我們想學一門語言,首先我們肯定是先要了解它是不是。有很多想入門的小白想學但又不知道Python是什麼,這真的是很尷尬,拿到了資
JS的原型鏈,這個圖你沒見過
想到Objective-C有個isa指標,物件的isa指向類,類的isa指向元類,元類的isa指向自己。正是有了isa指標,才有了強大的runtime功能。 那麼,前端技術js也有自己的指向關係,這裡借用《JavaScript高階程式設計》中的描述: 每個建構函式都有一個
不是ESD保護二極體難買,是你沒找到對的方法
不是ESD靜電二極體難採購,是您沒找到對的方法 一位ESD二極體採購商的真實心聲:“開開心心當了採購,才發現,國內電子元器件行業是在是太亂了,比如說電路保護元器件ESD靜電二極體、TVS二極體、壓敏電阻等產品,次品太多了,稂莠不齊,沒有一雙火眼金睛,根本不敢下單
Android EventBus實戰 沒聽過你就out了
轉載請表明出處:http://blog.csdn.net/lmj623565791/article/details/40794879,本文出自:【張鴻洋的部落格】1、概述最近大家面試說經常被問到EventBus,github上果斷down了一份,地址:https://githu
不是你無法入門自然語言處理(NLP),而是你沒找到正確的開啟方式
AI研習社按:本文作者 Mr.Scofield,原文載於作者個人部落格,雷鋒網已獲授權。 〇、序 之前一段時間,在結合深度學習做 NLP 的時候一直有思考一些問題,其中有一個問題算是最核心一個:究竟深度網路是怎麼做到讓各種 NLP 任務解決地如何完美呢?到底我的資料在
你沒中過勒索病毒,不知道備份有多重要
今天是春節放假前的最後一天,照例對自己一些資料開始進行了備份。突然想到關於資料備份有些心得想要分享下,於是寫了這篇文章。 點此進入公眾號檢視。 為什麼備份很重要 你沒吃過虧,可能永遠不明白資料有多珍貴。我在去年8月的時候中過臭名昭著的勒索病毒zep
(轉)Julia: 函式手冊,有哪些函式你沒見過?
http://www.jlhub.com/julia/manual/en/ Julia Manual - Function List and Reference View by functional groups Functions : :, :@a
話題 | 手機充電越充越少,90%的人都遇過這些囧事,有你嗎?
作為一個手機控最害怕的事情是什麼? 手機沒電! 一個標準手機控的特徵之一是什麼? 不是在充電就是在充電的路上…… 就算你不是一個手機控,手機沒電總是讓人惶恐的,為了給手機充電你都發生過哪些囧事?看了很多網友的回答,感覺以下這6種情況是最多人遇到過的了,有你