<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          csapp 第八章 異??刂屏?讀書筆記

          共 27226字,需瀏覽 55分鐘

           ·

          2023-07-29 09:55

          相關(guān)文章:csapp 第九章 虛擬內(nèi)存 讀書筆記

          第八章 異常控制流

          異??刂屏?/h2>

          處理器加電后和斷電前,程序計(jì)數(shù)器會(huì)假設(shè)一個(gè)值的序列:其中,每個(gè) 是某個(gè)相應(yīng)的指令 的地址,每次從 的過渡稱為控制轉(zhuǎn)移(control transfer)。這樣的控制轉(zhuǎn)移序列叫做處理器的控制流(flow of control or control flow) control flow的突變( )的不連續(xù),通常由類似跳轉(zhuǎn)、調(diào)用、和返回等程序指令造成。突變是現(xiàn)代系統(tǒng)應(yīng)對系統(tǒng)狀態(tài)變化的機(jī)制,這些突變稱為異??刂屏鳎‥xceptional Control Flow, ECF)。

          理解ECF的目的

          • 理解重要的系統(tǒng)概念:ECF是實(shí)現(xiàn)I/O、進(jìn)程和虛擬內(nèi)存的基本機(jī)制
          • 幫助理解應(yīng)用程序和操作系統(tǒng)的交互
          • 幫助實(shí)現(xiàn)新應(yīng)用
          • 幫助理解并發(fā)
          • 幫助理解軟件異常的工作模式

          8.1 異常

          異常(exception)是控制流中的突變,一部分由硬件實(shí)現(xiàn),一部分由操作系統(tǒng)實(shí)現(xiàn)。當(dāng)處理器檢測有事件發(fā)生時(shí),會(huì)通過一個(gè)叫做異常表(exception table)的跳轉(zhuǎn)表,進(jìn)行一個(gè)間接過程調(diào)用(異常),到一個(gè)專門設(shè)計(jì)用來處理這列事件的操作系統(tǒng)子程序(異常處理程序(exception handler))。當(dāng)異常處理程序完成處理后,根據(jù)引起異常的時(shí)間的類型,會(huì)發(fā)生以下三種情況:

          • 處理程序?qū)⒖刂品祷亟o當(dāng)前指令 ,即當(dāng)事件發(fā)生時(shí)正在執(zhí)行的指令;
          • 處理程序?qū)⒖刂品颠€給 ,如果沒有發(fā)生異常將會(huì)執(zhí)行的下一條指令;
          • 處理程序終止被中斷的程序

          8.1.1 異常處理

          系統(tǒng)中可能的每種類型的異常都分配了一個(gè)唯一的非負(fù)整數(shù)的異常號(hào)(exception number)。其中一些號(hào)碼是由處理器的設(shè)計(jì)者分配的,其他號(hào)碼是由操作系統(tǒng)內(nèi)核(操作系統(tǒng)常駐內(nèi)存部分)的設(shè)計(jì)者分配的,前者的示例包括被零除、缺頁、內(nèi)存訪問違例、斷點(diǎn)以及算術(shù)運(yùn)算溢出,后者包括系統(tǒng)調(diào)用和來自外部I/O設(shè)備的信號(hào)。系統(tǒng)啟動(dòng)時(shí),操作系統(tǒng)分配和初始化一張稱為異常表的跳轉(zhuǎn)表,使得表目k包含異常k的處理程序的地址,如下圖:

          在運(yùn)行時(shí),處理器檢測到發(fā)生了一個(gè)事件,并且確定了相應(yīng)的異常號(hào)k。隨后處理器觸發(fā)異常,方法是執(zhí)行間接過程調(diào)用,通過異常表的表目k,轉(zhuǎn)到相應(yīng)的處理程序,如下圖。異常表的起始地址放在一個(gè)叫做異常表基址寄存器(exception table base register)的特殊CPU寄存器里。異常類似于過程調(diào)用,但是有一些區(qū)別:

          • 過程調(diào)用時(shí),在跳轉(zhuǎn)到處理程序之前,處理器將返回地址壓入棧中,然而,根據(jù)異常的類型,返回地址要么是當(dāng)前指令(當(dāng)事件發(fā)生時(shí)正在執(zhí)行的指令),要么是下一條指令(如果事件不發(fā)生,將會(huì)在當(dāng)前指令后執(zhí)行的指令)
          • 處理器也會(huì)把一些額外的處理器狀態(tài)壓倒棧里,在處理程序返回時(shí),重新開始執(zhí)行被中斷的程序會(huì)需要這些狀態(tài)。
          • 如果控制從用戶程序轉(zhuǎn)移到內(nèi)核,所有這些項(xiàng)目都被壓到內(nèi)核棧中,而不是壓倒用戶棧中
          • 異常處理程序運(yùn)行在內(nèi)核模式下,這意味著它們對所有的系統(tǒng)資源都有完全的訪問權(quán)限

          8.1.2 異常的類型

          1.中斷

          中斷是異步發(fā)生的,是來自處理器外部的I/O設(shè)備的信號(hào)的結(jié)果。硬件中斷不是由任何一條專門的指令造成的,從這個(gè)意義上來說它是異步的。圖8-5概述了一個(gè)中斷的處理流程。在當(dāng)前指令完成執(zhí)行之后,處理器注意到中斷引腳的電壓變高了,就從系統(tǒng)總線讀取異常號(hào),然后調(diào)用適當(dāng)?shù)闹袛嗵幚沓绦颍?dāng)處理程序返回時(shí),它就將控制返回給下一條指令(即 如果沒有發(fā)生中斷,在控制流中會(huì)在當(dāng)前指令之后的那條指令),結(jié)果是程序繼續(xù)執(zhí)行,就好像沒有發(fā)生過中斷一樣。

          2.陷阱和系統(tǒng)調(diào)用

          陷阱是有意的異常,是執(zhí)行一條指令的結(jié)果。就像中斷處理程序一樣,陷阱處理程序?qū)⒖刂品祷氐较乱粭l指令。陷阱最重要的用途是在用戶程序在內(nèi)核之間提供一個(gè)像過程一樣的接口,叫做系統(tǒng)調(diào)用。用戶程序經(jīng)常需要向內(nèi)核請求服務(wù),比如讀一個(gè)文件(read)、創(chuàng)建一個(gè)新的進(jìn)程(fork)、加載一個(gè)新的程序(execve),或者終止當(dāng)前進(jìn)程(exit)。為了允許對這些內(nèi)核服務(wù)的受控的訪問,處理器提供了一條特殊的“syscall n”指令,當(dāng)用戶程序想要請求服務(wù)n時(shí),可移植性這條指令。執(zhí)行syscall指令會(huì)導(dǎo)致一個(gè)到異常處理程序的陷阱,這個(gè)處理程序解析參數(shù),并調(diào)用適當(dāng)?shù)膬?nèi)核程序,如圖8-6所示。普通函數(shù)調(diào)用是在用戶模式下,系統(tǒng)調(diào)用是在內(nèi)核模式下。

          3.故障

          故障是由錯(cuò)誤情況引起的,它可能能被故障處理程序修正。當(dāng)故障發(fā)生時(shí),處理器將控制轉(zhuǎn)移給故障處理程序。如果處理程序能夠修正這個(gè)錯(cuò)誤情況,它就將控制返回給引起故障的指令,從而重新執(zhí)行它。否則,處理程序返回到內(nèi)核中的abort例程,abort例程會(huì)終止引起故障的應(yīng)用程序,如圖8-7.eg:缺頁異常,當(dāng)指令引用一個(gè)虛擬地址,而與該地址相對應(yīng)的物理頁面不在內(nèi)存中,因此必須從磁盤中取出時(shí),就會(huì)發(fā)生故障。詳見第九章補(bǔ)上缺頁處理程序從磁盤加載適當(dāng)?shù)捻撁妫缓髮⒖刂品祷亟o引起故障的指令,當(dāng)指令再次執(zhí)行時(shí),相應(yīng)的物理頁面已經(jīng)駐留在內(nèi)存中了,指令就可以沒有故障地運(yùn)行完成了。

          4.終止

          終止是不可恢復(fù)的致命錯(cuò)誤造成的結(jié)果,通常是一些硬件錯(cuò)誤,終止程序?qū)⒖刂品颠€給一個(gè)abort例程,該例程會(huì)終止這個(gè)應(yīng)用程序。

          8.2 進(jìn)程

          異常是允許操作系統(tǒng)內(nèi)核提供進(jìn)程(process)概念的基本構(gòu)造塊。進(jìn)程的經(jīng)典定義:一個(gè)執(zhí)行中的程序的實(shí)例。系統(tǒng)中的每個(gè)程序都運(yùn)行在某個(gè)進(jìn)程的上下文(context)中,上下文是由程序正確運(yùn)行所需的狀態(tài)組成,狀態(tài)包括:存放在內(nèi)存中的程序的代碼和數(shù)據(jù),它的棧、通用目的寄存器的內(nèi)容,程序計(jì)數(shù)器、環(huán)境變量以及打開文件描述符的集合。每次用戶通過向shell輸入一個(gè)可執(zhí)行目標(biāo)文件的名字,運(yùn)行程序時(shí),shell就會(huì)創(chuàng)建一個(gè)新的進(jìn)程,然后在這個(gè)新進(jìn)程的上下文中運(yùn)行這個(gè)可執(zhí)行目標(biāo)文件。應(yīng)用程序也能夠創(chuàng)建新進(jìn)程,并且在這個(gè)新進(jìn)程的上下文中運(yùn)行它們自己的代碼或者其它應(yīng)用程序。

          8.2.1 邏輯控制流

          單步執(zhí)行程序,看到的一系列程序計(jì)數(shù)器(PC)的值(這些值唯一地對應(yīng)于包含在程序的可執(zhí)行目標(biāo)文件中的指令,或者包含在運(yùn)行時(shí)動(dòng)態(tài)鏈接到程序的共享對象中的指令),這個(gè)PC值的序列叫做邏輯控制流,或者簡稱控制流。如下圖,進(jìn)程是輪流使用處理器的,每個(gè)進(jìn)程執(zhí)行它的流的一部分,然后被搶占(preempted)(暫時(shí)掛起),然后輪到其它進(jìn)程。

          8.2.2 并發(fā)流

          一個(gè)邏輯流的執(zhí)行在時(shí)間上與另一個(gè)流重疊,稱為并發(fā)流(concurrent flow),這兩個(gè)流被稱為并發(fā)地運(yùn)行。更準(zhǔn)確地說,流X和流Y互相并發(fā),當(dāng)且僅當(dāng)X在Y開始之后和Y結(jié)束之前開始開始,或者Y在X開始之后和X結(jié)束之前開始。8-12中,A和B是并發(fā),A和C是并發(fā),B和C不是并發(fā)。多個(gè)流兵法的執(zhí)行的一般現(xiàn)象被稱為并發(fā)(concurrency)。一個(gè)進(jìn)程和其他進(jìn)程輪流運(yùn)行的概念稱為多任務(wù)(multitasking)。一個(gè)進(jìn)程執(zhí)行它的控制流的一部分的每一時(shí)間段叫做時(shí)間片(time slice)。如果兩個(gè)流并發(fā)地運(yùn)行在不同的處理器核或者計(jì)算機(jī)上,稱為并行流(parallel flow)

          8.2.3 私有地址空間

          進(jìn)程為每個(gè)程序提供它自己的私有地址空間,一般來說和這個(gè)空間中某個(gè)地址相關(guān)聯(lián)的那個(gè)內(nèi)存字節(jié)是不能被其它進(jìn)程讀或者寫的,從這個(gè)意義上說,這個(gè)地質(zhì)空間是私有的。盡管和每個(gè)私有地址空間相關(guān)聯(lián)的內(nèi)存的內(nèi)容一般是不同的,但是每個(gè)這樣的空間都有相同的通用結(jié)構(gòu),如8-13。

          8.2.4 用戶模式和內(nèi)核模式

          為了使操作系統(tǒng)內(nèi)核提供一個(gè)無懈可擊的進(jìn)程抽象,處理器必須提供一種機(jī)制,限制一個(gè)應(yīng)用可以執(zhí)行的指令以及它可以訪問的地址空間范圍。處理器通常是用某個(gè)控制寄存器中的一個(gè)模式位(mode bit)來提供這種功能,該寄存器描述了進(jìn)程當(dāng)前享有的特權(quán)。當(dāng)設(shè)置了模式位,進(jìn)程就運(yùn)行在內(nèi)核模式中(有時(shí)也叫作超級用戶模式)。沒有設(shè)置模式位時(shí),進(jìn)程就運(yùn)行在用戶模式中。用戶模式中的進(jìn)程不允許執(zhí)行特權(quán)指令(privileged instruction),比如停止處理器,改變模式位,或者發(fā)起一個(gè)I/O操作。也不允許用戶模式中的進(jìn)程直接引用地址空間中內(nèi)核區(qū)內(nèi)的代碼和數(shù)據(jù)。運(yùn)行應(yīng)用程序代碼的進(jìn)程初始時(shí)是在用戶模式中。進(jìn)程從用戶模式變?yōu)閮?nèi)核模式的唯一方法是通過注入中斷、故障或者陷入系統(tǒng)調(diào)用這樣的異常。Linux提供了一種叫做/proc文件系統(tǒng)的機(jī)制,允許用戶模式進(jìn)程訪問內(nèi)核數(shù)據(jù)結(jié)構(gòu)的內(nèi)容。/proc文件系統(tǒng)將許多內(nèi)核數(shù)據(jù)結(jié)構(gòu)的內(nèi)容輸出為一個(gè)用戶程序可以讀的文本文件的層次結(jié)構(gòu)。比如,可以使用文件系統(tǒng)找出一般的系統(tǒng)屬性,比如CPU類型(、proc/cpuinfo)?;蛘吣硞€(gè)特殊的進(jìn)程使用的內(nèi)存段。chatgpt對/proc的介紹如下:

          `/proc` 是一個(gè)虛擬文件系統(tǒng),也被稱為進(jìn)程文件系統(tǒng),它存在于內(nèi)存中而不是硬盤上。在 Linux 系統(tǒng)中,`/proc` 目錄包含了大量關(guān)于系統(tǒng)和正在運(yùn)行的進(jìn)程的實(shí)時(shí)信息。它是一個(gè)接口,通過這個(gè)接口,內(nèi)核可以向用戶空間程序提供信息。

          以下是 `/proc` 中的一些常見文件和目錄:

          - `/proc/cpuinfo`:包含了處理器的相關(guān)信息,如型號(hào)、MHz、緩存大小等。

          - `/proc/meminfo`:提供了關(guān)于系統(tǒng)內(nèi)存使用情況的信息,包括物理內(nèi)存、交換空間等。

          - `/proc/version`:顯示了系統(tǒng)的版本信息。

          - `/proc/pid`:每一個(gè)正在運(yùn)行的進(jìn)程都會(huì)在 `/proc` 下有一個(gè)以其進(jìn)程 ID 命名的目錄。這個(gè)目錄中包含了該進(jìn)程的相關(guān)信息。

          - `/proc/filesystems`:列出了系統(tǒng)支持的文件系統(tǒng)類型。

          - `/proc/mounts`:顯示了當(dāng)前系統(tǒng)掛載的所有文件系統(tǒng)。

          - `/proc/net`:包含了網(wǎng)絡(luò)協(xié)議的統(tǒng)計(jì)信息。

          通過閱讀和分析 `/proc` 中的文件,我們可以了解到系統(tǒng)和進(jìn)程的許多信息。這對于系統(tǒng)監(jiān)控、調(diào)試和性能調(diào)優(yōu)等任務(wù)非常有用。

          8.2.5 上下文切換

          • 內(nèi)核使用上下文切換(context switch)的較高形式的異??刂屏鱽韺?shí)現(xiàn)多任務(wù)。上下文切換機(jī)制建立在8.1的低層異常機(jī)制上。
          • 內(nèi)核為每個(gè)進(jìn)程維持一個(gè)上下文(context)。上下文就是內(nèi)核重新啟動(dòng)一個(gè)被搶占的進(jìn)程所需的狀態(tài)。它由一些對象的值組成,這些對象包括通用目的寄存器、浮點(diǎn)寄存器、程序計(jì)數(shù)器、用戶棧、狀態(tài)寄存器、內(nèi)核棧和各種內(nèi)核數(shù)據(jù)集,比如描述地址空間的頁表,包含有關(guān)當(dāng)前進(jìn)程信息的進(jìn)程表,以及包含進(jìn)程已打開文件的信息的文件表。
          • 在進(jìn)程執(zhí)行的某些時(shí)刻,內(nèi)核可以決定搶占當(dāng)前進(jìn)程,并重新開始一個(gè)先前被搶占了的進(jìn)程。這種決策就叫調(diào)度(scheduling),是由內(nèi)核中被稱為調(diào)度器(scheduler)的代碼處理的。當(dāng)內(nèi)核選擇一個(gè)新的進(jìn)程運(yùn)行時(shí),即內(nèi)核調(diào)度了這個(gè)進(jìn)程。在內(nèi)核調(diào)度了一個(gè)新的進(jìn)程運(yùn)行后,它就搶占當(dāng)前進(jìn)程,并使用一種稱為上下文切換的機(jī)制來將控制轉(zhuǎn)移到新的進(jìn)程
          • 上下文切換:
            • 1)保存當(dāng)前進(jìn)程的上下文
            • 2)恢復(fù)某個(gè)先前被搶占的進(jìn)程被保存的上下文
            • 3)將控制傳遞給這個(gè)新恢復(fù)的進(jìn)程
          • 當(dāng)內(nèi)核代表用戶執(zhí)行系統(tǒng)調(diào)度時(shí),如果系統(tǒng)調(diào)度因?yàn)榈却硞€(gè)事件而發(fā)生阻塞(eg:磁盤讀取數(shù)據(jù)),那么內(nèi)核可以讓當(dāng)前進(jìn)程休眠,切換到另一個(gè)進(jìn)程。
          • 中斷也可能發(fā)生上下文切換
          • 進(jìn)程切換示例:8-14

          8.3 系統(tǒng)調(diào)用錯(cuò)誤處理

          • 當(dāng)Unix系統(tǒng)級函數(shù)遇到錯(cuò)誤時(shí),它們通常會(huì)返回-1,并設(shè)置全局整數(shù)變量errno來表示什么出錯(cuò)了。如下:

          8.4 進(jìn)程控制

          8.4.1 獲取進(jìn)程ID

          每個(gè)進(jìn)程都有一個(gè)唯一的正數(shù)(非零)進(jìn)程ID(PID)。getpid函數(shù)返回調(diào)用進(jìn)程的PID,getppid函數(shù)返回它的父進(jìn)程的PID(創(chuàng)建調(diào)用進(jìn)程的進(jìn)程)

          8.4.2 創(chuàng)建和終止進(jìn)程

          • 程序員眼中進(jìn)程的三種狀態(tài)
            • 運(yùn)行:進(jìn)程要么在CPU上執(zhí)行,要么在等待被執(zhí)行且最終會(huì)被內(nèi)核調(diào)度
            • 停止:進(jìn)程的執(zhí)行被掛起(suspended),且不會(huì)被調(diào)度。當(dāng)收到SIGSTOP、SIGTSTP、SIGTTIN或者SIGTTOUT信號(hào)時(shí),進(jìn)程就停止,,并且保持停止直到它收到一個(gè)SIGCONT信號(hào),在這個(gè)時(shí)刻,進(jìn)程再次開始運(yùn)行。(信號(hào)是一種軟件終端的形式)
            • 終止:進(jìn)程永遠(yuǎn)地停止了。進(jìn)程會(huì)因?yàn)槿N原因終止:1)收到一個(gè)信號(hào),該信號(hào)的默認(rèn)行為是終止進(jìn)程;2)從主程序返回;3)調(diào)用exit函數(shù) 這些都是 Unix 或者類 Unix 系統(tǒng)(如 Linux)中的信號(hào)(Signals)。信號(hào)是一種在 Unix 系統(tǒng)中進(jìn)程間通信的方式,也用于通知進(jìn)程某些系統(tǒng)事件。當(dāng)一個(gè)信號(hào)發(fā)送給一個(gè)進(jìn)程時(shí),操作系統(tǒng)會(huì)中斷進(jìn)程的正常控制流程,然后傳遞這個(gè)信號(hào),進(jìn)程在接收到信號(hào)后會(huì)做出相應(yīng)的響應(yīng)。exit 函數(shù)以status退出狀態(tài)來終止進(jìn)程(另一種設(shè)置退出狀態(tài)的方法是從主程序中返回一個(gè)整數(shù)值)。
          SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU都是什么:

          以下是這些信號(hào)的解釋:

          - SIGSTOP:這個(gè)信號(hào)會(huì)讓接收它的進(jìn)程停止運(yùn)行。這是一個(gè)不能被阻塞、處理或者忽略的信號(hào)。
          - SIGTSTP:這個(gè)信號(hào)通常由用戶在終端中按下 Ctrl+Z 發(fā)送,導(dǎo)致進(jìn)程停止運(yùn)行,但是與 SIGSTOP 不同的是,這個(gè)信號(hào)是可以被捕獲和忽略的。

          - SIGTTIN:當(dāng)一個(gè)后臺(tái)進(jìn)程試圖讀取終端輸入時(shí),這個(gè)信號(hào)會(huì)被發(fā)送到該進(jìn)程。默認(rèn)情況下,這會(huì)導(dǎo)致進(jìn)程停止運(yùn)行。

          - SIGTTOU:當(dāng)一個(gè)后臺(tái)進(jìn)程試圖寫入它的控制終端或者改變終端的模式時(shí),這個(gè)信號(hào)會(huì)被發(fā)送到該進(jìn)程。默認(rèn)情況下,這會(huì)導(dǎo)致進(jìn)程停止運(yùn)行。

          這些信號(hào)通常用于實(shí)現(xiàn) Unix 系統(tǒng)的工作控制,例如將一個(gè)正在運(yùn)行的進(jìn)程暫停并放到后臺(tái),或者將一個(gè)在后臺(tái)暫停的進(jìn)程恢復(fù)運(yùn)行并放到前臺(tái)。

          創(chuàng)建

          • 新創(chuàng)建的子進(jìn)程幾乎但不完全與父進(jìn)程相同。子進(jìn)程得到與父進(jìn)程用戶級虛擬內(nèi)存地址空間相同的(但是獨(dú)立的)一份副本,包括代碼和數(shù)據(jù)段、堆、共享庫以及用戶棧。子進(jìn)程還獲得與附近成人和打開文件描述符相同的副本,這就意味著當(dāng)父進(jìn)程調(diào)用fork時(shí),子進(jìn)程還可以讀寫父進(jìn)程中打開的任何文件。父進(jìn)程和心創(chuàng)建的子進(jìn)程之間最大的區(qū)別在于它們有不同的PID。
          • fork函數(shù):調(diào)用一次,返回兩次:一次在調(diào)用進(jìn)程中,一次在新創(chuàng)建的子進(jìn)程中。在父進(jìn)程中,fork函數(shù)返回子進(jìn)程的PID,在子進(jìn)程中,fork返回0.因?yàn)樽辖堑腜ID總是非零,返回值就提供一個(gè)明確的方法來判斷程序是在父進(jìn)程還是在子進(jìn)程中執(zhí)行。
          • 父進(jìn)程調(diào)用子進(jìn)程示例:8-15
            • 調(diào)用一次,返回兩次: fork函數(shù)被父進(jìn)程調(diào)用一次,但是返回兩次:1)返回到父進(jìn)程,2)返回到新創(chuàng)建的子進(jìn)程。
            • 并發(fā)執(zhí)行:父進(jìn)程和子進(jìn)程是并發(fā)運(yùn)行的獨(dú)立進(jìn)程。內(nèi)核能夠以任意方式交替執(zhí)行它們的邏輯控制流中的指令。
            • 相同但是獨(dú)立的地址空間:如果能夠在fork函數(shù)在父進(jìn)程和子進(jìn)程中返回后立即暫停這兩個(gè)進(jìn)程,我們會(huì)看到兩個(gè)進(jìn)程的地址空間是相同的。每個(gè)進(jìn)程由相同的用戶棧、相同的本地變量值、相同的堆、相同的全局變量值,以及相同的代碼。
            • 共享文件:子進(jìn)程繼承了父進(jìn)程所有的打開文件

          8.4.3 回收子進(jìn)程

          當(dāng)一個(gè)進(jìn)程由于某種原因終止時(shí),內(nèi)核并不是立即把它從系統(tǒng)中清除。相反,進(jìn)程被保持在一種已終止的狀態(tài)中,直到被它的父進(jìn)程回收(reaped)。當(dāng)父進(jìn)程回收已終止的子進(jìn)程時(shí),內(nèi)核將紫禁城的退出狀態(tài)傳遞給父進(jìn)程,然后拋棄已終止的進(jìn)程,從此時(shí)開始,該進(jìn)程就不存在了。一個(gè)終止了但還未被回收的進(jìn)程稱為僵死進(jìn)程(zombie) 如果一個(gè)父進(jìn)程終止了,內(nèi)核會(huì)安排init進(jìn)程成為它的孤兒進(jìn)程的養(yǎng)父。init進(jìn)程的PID=1,是在系統(tǒng)啟動(dòng)時(shí)由內(nèi)核創(chuàng)建的,它不會(huì)終止,是所有進(jìn)程的祖先。一個(gè)進(jìn)程可以通過調(diào)用waitpid函數(shù)來等待它的子進(jìn)程終止或者停止。默認(rèn)情況下(option=0),waitpid掛起調(diào)用進(jìn)程的執(zhí)行,直到它的等待集合(wait set)中的一個(gè)子進(jìn)程終止。如果等待集合中的一個(gè)進(jìn)程在剛調(diào)用的時(shí)刻就已經(jīng)終止了,那么waitpid就立即返回。在這兩種情況中,waitpid返回導(dǎo)致waitpid返回的已終止子進(jìn)程的PID,此時(shí),已終止的子進(jìn)程已經(jīng)被回收,內(nèi)核會(huì)從系統(tǒng)中刪除掉它的所有痕跡。參數(shù)說明:

          • 1.判定等待集合的成員: 等待集合的成員是由參數(shù)pid來確定的
            • 如果pid > 0,那么等待集合就是一個(gè)單獨(dú)的子進(jìn)程,它的進(jìn)程ID等于pid
            • 如果pid = -1,那么等待集合就是由父進(jìn)程的所有子進(jìn)程組成
          • 2.修改默認(rèn)行為: 可以通過將options設(shè)置為常量WNOHANG、WUNTRACED、WCONTINUED的各種組合來修改默認(rèn)行為:
            • WNOHANG:如果等待集合中的任何子進(jìn)程都還沒有終止,那么就立即返回(返回的值為0)。默認(rèn)的行為是掛起調(diào)用進(jìn)程,直到有子進(jìn)程終止。在等待子進(jìn)程終止的同時(shí),如果還想做些有用的工作,這個(gè)選項(xiàng)會(huì)有用。
            • WUNTRACED:掛起調(diào)用進(jìn)程的執(zhí)行,直到等待集合中的一個(gè)進(jìn)程變成已終止或者被停止。返回的PID為導(dǎo)致返回的已終止或者被停止子進(jìn)程的PID,默認(rèn)的行為是只返回已終止的子進(jìn)程。當(dāng)你想要檢查已終止和被停止的子進(jìn)程時(shí),這個(gè)會(huì)比較管用。
            • WCONTINUED:掛起調(diào)用進(jìn)程的執(zhí)行,直到等待集合中一個(gè)正在運(yùn)行的進(jìn)程終止或等待集合中一個(gè)被停止的進(jìn)程收到SIGCONT信號(hào)重新開始執(zhí)行。
            • WNOHANG | WUNTRACED:立即返回,如果等待集合中的子進(jìn)程都沒有被停止或終止,則返回值為0;如果有一個(gè)停止或終止,則返回值為該子進(jìn)程的PID。
          • 3.檢查已回收子進(jìn)程的退出狀態(tài):如果statusp參數(shù)是非空的,那么waitpid就會(huì)在status中放上關(guān)于導(dǎo)致返回的子進(jìn)程的狀態(tài)信息,status是statusp指向的值。wait.h頭文件中定義了解釋status參數(shù)的幾個(gè)宏:
            • WIFEXITED(status):如果子進(jìn)程通過調(diào)用exit或者一個(gè)返回(return)正常終止,就返回真
            • WEXITSTATUS(status):返回一個(gè)正常終止的紫禁城的退出狀態(tài),只有在WIFEXITED(status)返回為真時(shí),才會(huì)定義這個(gè)狀態(tài)
            • WIFSIGNALED(status):如果子進(jìn)程是因?yàn)橐粋€(gè)未被捕獲的信號(hào)終止的,那么就返回真
            • WTERMSIG(status):返回導(dǎo)致子進(jìn)程終止的信號(hào)的編號(hào),只有在WIFSIGNALED()返回為真時(shí),才定義這個(gè)變量
            • WIFSTOPPED(status):如果引起返回的子進(jìn)程當(dāng)前是停止的,那么就返回真。
            • WSTOPSIG(status):返回引起子進(jìn)程停止的信號(hào)的編號(hào),只有在WIFSTOPPED()返回為真時(shí),才定義這個(gè)狀態(tài)。
            • WIFCONTINUED(status):如果子進(jìn)程收到SIGCONT信號(hào)重新啟動(dòng),則返回真
          • 4.錯(cuò)誤條件 如果調(diào)用進(jìn)程沒有子進(jìn)程,那么waitpid就返回-1,并且設(shè)置errno為ECHILD。如果waitpid函數(shù)被一個(gè)信號(hào)中斷,那么它返回-1,并設(shè)置errno為EINTR。
          • 6.waitpid示例

          8.4.4 讓進(jìn)程休眠

          • sleep:讓進(jìn)程掛起一段指定的時(shí)間
          • pause:讓調(diào)用函數(shù)休眠,直到該進(jìn)程收到一個(gè)信號(hào)

          8.4.5 加載并運(yùn)行程序

          • execve函數(shù)在當(dāng)前進(jìn)程的上下文中加載并運(yùn)行一個(gè)新程序。execve函數(shù)加載并運(yùn)行可執(zhí)行目標(biāo)文件filename,且?guī)?shù)列表argv和環(huán)境變量列表envp。只有當(dāng)出現(xiàn)錯(cuò)誤時(shí),才返回。參數(shù)列表和環(huán)境變量列表分別是指向一個(gè)以null結(jié)尾的指針數(shù)組,其中每個(gè)指針都指向一個(gè)參數(shù)字符串或者一個(gè)環(huán)境變量字符串。
          • 當(dāng)main開始時(shí),用戶棧的組織結(jié)構(gòu)如圖8-22。main函數(shù)有三個(gè)參數(shù),1)argc:給出argv[]數(shù)組中非空指針的數(shù)量,2)argv:指向argv[]的第一條目,3)envp:指向envp[]的第一個(gè)條目。

          8.4.6 利用fork和execve運(yùn)行程序

          8.5 信號(hào)

          一個(gè)信號(hào)就是一條小消息,它通知進(jìn)程系統(tǒng)中發(fā)生了一個(gè)某種類型的事件,允許進(jìn)程和內(nèi)核中斷其他進(jìn)程。每種信號(hào)類型都對應(yīng)于某種系統(tǒng)事件,低層的硬件異常是由內(nèi)核異常處理程序處理的,正常情況下,對用戶進(jìn)程而言是不可見的。信號(hào)提供了一種機(jī)制,通知用戶進(jìn)程發(fā)生了這些異常,異常表如下

          8.5.1 信號(hào)術(shù)語

          傳送一個(gè)信號(hào)到目的進(jìn)程是由兩個(gè)不同的步驟組成的:

          • 發(fā)送信號(hào):內(nèi)核通過更新目的進(jìn)程上下文中的某個(gè)狀態(tài),發(fā)送(遞送)一個(gè)信號(hào)給目的進(jìn)程。發(fā)送進(jìn)程可以有如下兩種原因:1)內(nèi)核檢測到一個(gè)系統(tǒng)事件,比如除零錯(cuò)誤或者子進(jìn)程終止;2)一個(gè)進(jìn)程調(diào)用了kill函數(shù),顯式地要求內(nèi)核發(fā)送一個(gè)信號(hào)給目的進(jìn)程,幾個(gè)進(jìn)程可以發(fā)送信號(hào)給它自己。
          • 接收信號(hào) 當(dāng)目的進(jìn)程被內(nèi)核強(qiáng)迫以某種方式對信號(hào)的發(fā)送做出反應(yīng)時(shí),它就接收了信號(hào)。進(jìn)程可以忽略這個(gè)信號(hào),終止或者通過執(zhí)行一個(gè)稱為信號(hào)處理程序(signal handler)的用戶層函數(shù)捕獲這個(gè)信號(hào),下面是信號(hào)處理程序捕獲信號(hào)的基本思想。

          一個(gè)發(fā)出而沒有被接收的信號(hào)叫做待處理信號(hào)(pending singal)。在任何時(shí)刻,一種類型至多只會(huì)有一個(gè)待處理信號(hào)。如果一個(gè)進(jìn)程有一個(gè)類型為k的待處理信號(hào),那么任何接下來發(fā)送到這個(gè)進(jìn)程的類型為k的信號(hào)都不會(huì)排隊(duì)等待;他們只是被簡單地丟棄。一個(gè)進(jìn)程可以選擇性地阻塞接收某種信號(hào)。當(dāng)一種信號(hào)被阻塞時(shí),它仍可以被發(fā)送,但是產(chǎn)生的待處理信號(hào)不會(huì)被接受,直到進(jìn)程取消對這種信號(hào)的阻塞。一個(gè)待處理信號(hào)最多只能被接受一次,內(nèi)核為每個(gè)進(jìn)程在pending位向量中維護(hù)著待處理信號(hào)的集合,而在blockerd位向量中維護(hù)著被阻塞的信號(hào)集合。只要傳送了一個(gè)類型為k的信號(hào),內(nèi)核就會(huì)設(shè)置pending中的第k位,而只要接收了一個(gè)類型為k的信號(hào),內(nèi)核就會(huì)清除pending中的第k位。

          8.5.2 發(fā)送信號(hào)

          1.進(jìn)程組 每個(gè)進(jìn)程都只屬于一個(gè)進(jìn)程組,進(jìn)程組是由一個(gè)正整數(shù)進(jìn)程組ID來標(biāo)識(shí)的。getpgrp函數(shù)返回當(dāng)前進(jìn)程的進(jìn)程組ID。默認(rèn)的,一個(gè)子進(jìn)程和它的父進(jìn)程同屬于一個(gè)進(jìn)程組。一個(gè)進(jìn)程可以通過使用setpgid函數(shù)來改變自己或者其他進(jìn)程的進(jìn)程組。setpgid 函數(shù)是在 Linux/UNIX 系統(tǒng)下用于設(shè)置某個(gè)進(jìn)程的進(jìn)程組 ID 的,它的函數(shù)原型如下:

          #include <unistd.h>
          int setpgid(pid_t pid, pid_t pgid);

          其中,pid 表示需要設(shè)置的進(jìn)程 ID,pgid 表示需要設(shè)置的進(jìn)程組 ID。setpgid 函數(shù)可以將一個(gè)進(jìn)程設(shè)置為所指定的進(jìn)程組中的一個(gè)成員,同時(shí)可以創(chuàng)建新的進(jìn)程組。使用 setpgid 函數(shù)創(chuàng)建新的進(jìn)程組時(shí),若 pid 參數(shù)所指的進(jìn)程尚未加入任何進(jìn)程組,則可將其作為新進(jìn)程組的組長進(jìn)程(即進(jìn)程組 ID 與該進(jìn)程 ID 相同),成功時(shí)返回 0,失敗時(shí)返回 -1。使用 setpgid 函數(shù)還可以實(shí)現(xiàn)進(jìn)程的前后臺(tái)切換。在 Linux/UNIX 系統(tǒng)中,每個(gè)終端都有一個(gè)唯一的進(jìn)程組 ID,在某個(gè)終端上運(yùn)行著的進(jìn)程都屬于該終端的進(jìn)程組。一個(gè)進(jìn)程組可以擁有多個(gè)進(jìn)程。在前臺(tái)運(yùn)行的進(jìn)程接收鍵盤輸入信號(hào),處于后臺(tái)運(yùn)行的進(jìn)程則接收不到鍵盤輸入信號(hào)。因此通過將進(jìn)程的進(jìn)程組 ID 設(shè)置為當(dāng)前終端的進(jìn)程組 ID,可以將其放到前臺(tái)運(yùn)行,在當(dāng)前終端接受鍵盤輸入信號(hào)。使用 setpgid 函數(shù)還可以實(shí)現(xiàn)進(jìn)程的作業(yè)控制,例如將多個(gè)進(jìn)程放在同一作業(yè)中,并對該作業(yè)進(jìn)行統(tǒng)一管理。2.用/bin/kill 程序發(fā)送信號(hào) /bin/kill程序可以向另外的進(jìn)程發(fā)送任意的信號(hào)。3.從鍵盤發(fā)送信號(hào) Unix shell 使用作業(yè)(job)這個(gè)抽象概念來表示對一條命令行求值而創(chuàng)建的進(jìn)程。在任何時(shí)刻,至多只有一個(gè)前臺(tái)作業(yè)和0個(gè)或多個(gè)后臺(tái)作業(yè)。比如鍵入:

          linux > ls | sort

          會(huì)創(chuàng)建一個(gè)一個(gè)由兩個(gè)進(jìn)程組成的前臺(tái)作業(yè)。這兩個(gè)進(jìn)程通過Unix管道連接:一個(gè)進(jìn)程運(yùn)行l(wèi)s程序,一個(gè)進(jìn)程運(yùn)行sort程序。shell為每個(gè)作業(yè)創(chuàng)建一個(gè)獨(dú)立的進(jìn)程組。進(jìn)程ID通常取自作業(yè)中父進(jìn)程中的一個(gè)。比如,下面展示了有一個(gè)前臺(tái)作業(yè)和兩個(gè)后臺(tái)作業(yè)的shell。前臺(tái)作業(yè)中的父進(jìn)程PID為20,進(jìn)程組ID也是20.父進(jìn)程創(chuàng)建兩個(gè)子進(jìn)程,每個(gè)也都是進(jìn)程組20的成員。在鍵盤上輸入Ctrl+C會(huì)導(dǎo)致內(nèi)核發(fā)送一個(gè)SIGINT信號(hào)到前臺(tái)進(jìn)程組中的每個(gè)進(jìn)程。默認(rèn)情況下,結(jié)果是終止前臺(tái)作業(yè)。類似的,輸入Ctrl+Z會(huì)發(fā)送一個(gè)SIGTSTO信號(hào)到前臺(tái)進(jìn)程組中的每個(gè)進(jìn)程。默認(rèn)情況下,結(jié)果是停止(掛起)前臺(tái)作業(yè)。4.用kill函數(shù)發(fā)送信號(hào) 進(jìn)程通過調(diào)用kill函數(shù)發(fā)送信號(hào)給其他進(jìn)程(包括自己) 下面展示了父進(jìn)程用kill函數(shù)發(fā)送SIGKILL信號(hào)給它的子進(jìn)程。5.用alarm函數(shù)發(fā)送信號(hào) 進(jìn)程可以通過調(diào)用alarm函數(shù)向他自己發(fā)送SIGALRM信號(hào)。alarm函數(shù)安排內(nèi)核在secs后發(fā)送一個(gè)SIGALRM信號(hào)給調(diào)用進(jìn)程,如果secs=0,則不會(huì)調(diào)度安排新的鬧鐘(alarm)。

          8.5.3 接收信號(hào)

          當(dāng)內(nèi)核把進(jìn)程p從內(nèi)核模式切換到用戶模式時(shí)(eg:從系統(tǒng)調(diào)用返回或是完成了一次上下文切換),它會(huì)檢查進(jìn)程p的未被阻塞的待處理信號(hào)的集合(pending & ~blocked)。如果這個(gè)集合為空(通常情況下),那么內(nèi)核將控制傳遞到p的邏輯控制流中的下一條指令。如果集合是非空的,那么內(nèi)核將控制傳遞到p的邏輯控制流中的下一條指令,并且強(qiáng)制p接受信號(hào)k。收到這個(gè)信號(hào)會(huì)觸發(fā)進(jìn)程采取某種行為。一旦進(jìn)程完成了這個(gè)行為,那么控制就傳遞回p的邏輯控制流中的下一條指令( )。每個(gè)信號(hào)類型都有一個(gè)預(yù)定義的默認(rèn)行為,是下面的一種:

          • 進(jìn)程終止
          • 進(jìn)程終止并轉(zhuǎn)儲(chǔ)內(nèi)存
          • 進(jìn)程停止(掛起)直到被SIGCONT信號(hào)重啟
          • 進(jìn)程忽略該信號(hào)

          進(jìn)程可以通過signal函數(shù)修改和信號(hào)相關(guān)聯(lián)的默認(rèn)行為,唯一的例外是SIGSTOP和SIGKILL,其默認(rèn)行為不能修改 signal函數(shù)可以通過下列三種方法之一來改變和信號(hào)signum相關(guān)聯(lián)的行為:

          • 如果handler是SIG_IGN,那么忽略類型為signum的信號(hào)
          • 如果handler是SIG——DFL,那么類型為signum的信號(hào)行為恢復(fù)為默認(rèn)行為
          • 否則,handler就是用用戶定義的函數(shù)的地址,這個(gè)函數(shù)被稱為信號(hào)處理程序,只要進(jìn)城接收到signal函數(shù)從而改變默認(rèn)行為,這個(gè)叫做設(shè)置信號(hào)處理程序(installing the handler)。調(diào)用信號(hào)處理程序被稱為捕獲信號(hào),執(zhí)行信號(hào)處理程序被稱為處理信號(hào)。

          當(dāng)一個(gè)進(jìn)程捕獲了一個(gè)類型為k的信號(hào)時(shí),會(huì)調(diào)用為信號(hào)k設(shè)置的處理程序,一個(gè)整數(shù)參數(shù)被設(shè)置為k,這個(gè)參數(shù)允許同一個(gè)處理函數(shù)捕獲不同類型的信號(hào)。當(dāng)處理程序執(zhí)行它的return語句時(shí),控制(通常)傳遞回控制流中進(jìn)程被信號(hào)接收中斷位置處的指令。eg:8-30,修改了Ctrl-Z的默認(rèn)行為信號(hào)處理程序可以被其他信號(hào)處理程序中斷,如下。

          8.5.4 阻塞和解除阻塞信號(hào)

          Linux提供阻塞信號(hào)的隱式和顯式機(jī)制

          • 隱式阻塞機(jī)制:內(nèi)核默認(rèn)阻塞任何當(dāng)前處理程序正在處理信號(hào)類型的待處理的信號(hào)。eg:如上圖中,假設(shè)程序捕獲了信號(hào)s,當(dāng)前正在運(yùn)行處理程序S。如果發(fā)送給該進(jìn)程另一個(gè)信號(hào)s,那么直到處理程序S返回,s會(huì)變成待處理而沒有被接收。
          • 顯式阻塞機(jī)制:應(yīng)用程序可以使用sigprocmask函數(shù)和它的輔助函數(shù),明確地阻塞和解除阻塞選定的信號(hào)。sigprocmask 是一個(gè) POSIX 標(biāo)準(zhǔn)定義的系統(tǒng)調(diào)用函數(shù),用來設(shè)置和獲取進(jìn)程的信號(hào)屏蔽字??梢酝ㄟ^ sigprocmask 函數(shù)調(diào)用設(shè)置進(jìn)程阻塞某些信號(hào)或解除信號(hào)阻塞。

          sigprocmask 函數(shù)的標(biāo)準(zhǔn)簽名如下:

          #include <signal.h>

          int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

          其中,how 參數(shù)指定了操作方式,可以是以下幾個(gè)常量中的一個(gè):

          • SIG_BLOCK :將 set 中的所有信號(hào)添加到當(dāng)前進(jìn)程的信號(hào)屏蔽字中。

          • SIG_UNBLOCK :將 set 中的所有信號(hào)從當(dāng)前進(jìn)程的信號(hào)屏蔽字中刪除。

          • SIG_SETMASK :將當(dāng)前信號(hào)屏蔽字設(shè)置為 set 中的值。

          set參數(shù)是一個(gè)指向 sigset_t 類型的指針,該類型是一個(gè)位向量表示信號(hào)集。如果設(shè)置信號(hào)屏蔽字,set指向的實(shí)際位向量中包含的是屏蔽信號(hào)的位,未屏蔽的信號(hào)的位則被清除。如果您想阻止信號(hào),則需要修改該信號(hào)的位狀態(tài)。另外,oldset 參數(shù)是一個(gè)輸出參數(shù),它記錄了之前當(dāng)前進(jìn)程的信號(hào)屏蔽字狀態(tài)。例如,下面的代碼存儲(chǔ)了當(dāng)前進(jìn)程的信號(hào)屏蔽字并禁用 SIGINT 信號(hào):

          #include <signal.h>

          int main() {
              sigset_t newmask, oldmask;

              // 初始化屏蔽字
              sigemptyset(&newmask);
              sigaddset(&newmask, SIGINT);

              // 設(shè)置屏蔽字,保存舊值
              sigprocmask(SIG_BLOCK, &newmask, &oldmask);

              // 執(zhí)行某些需要屏蔽信號(hào)的操作

              // 恢復(fù)舊屏蔽字
              sigprocmask(SIG_SETMASK, &oldmask, NULL);

              return 0;
          }

          上面的代碼首先創(chuàng)建一個(gè)新信號(hào)屏蔽字 newmask,它將 SIGINT 信號(hào)阻止。然后調(diào)用 sigprocmask 函數(shù)阻止了該信號(hào),并存儲(chǔ)了舊的屏蔽字。在此之后,執(zhí)行需要被屏蔽信號(hào)的操作,然后恢復(fù)舊的信號(hào)屏蔽字,讓 SIGINT 信號(hào)回到原始的可被處理狀態(tài)。

          8.5.5 編寫信號(hào)處理程序

          處理程序難以推理分析的原因:

          • 處理程序與主程序并發(fā)運(yùn)行,共享同樣的全局變量,因此可能與主程序和其他處理程序互相干擾
          • 如何以及何時(shí)接收信號(hào)的規(guī)則常常有違人的直覺
          • 不同系統(tǒng)有不同信號(hào)處理語義 基本規(guī)則
          • 1.安全的信號(hào)處理
            • 處理程序要盡可能簡單
            • 處理程序終止調(diào)用異步信號(hào)安全的函數(shù):所謂異步信號(hào)安全的函數(shù)能夠被信號(hào)處理程序安全地調(diào)用,原因有2:1)要么它是可重入的(可重入(reentrant)是計(jì)算機(jī)科學(xué)中的一個(gè)概念,指的是一個(gè)程序、代碼段或者是共享資源,可以被多個(gè)并發(fā)的線程或者進(jìn)程同時(shí)使用,而不會(huì)引起不一致或崩潰等問題。具體來說,可重入的代碼必須滿足以下條件:可重入的代碼自身不跟蹤任何狀態(tài),或者任何狀態(tài)信息必須由調(diào)用者或環(huán)境傳遞進(jìn)來??芍厝氲拇a必須保證它不擁有任何全局或靜態(tài)的狀態(tài)變量??芍厝氲拇a必須使用局部變量而非靜態(tài)變量,因?yàn)榫植孔兞吭跅V蟹峙?,每個(gè)線程或進(jìn)程都有自己獨(dú)立的棧空間??芍厝氲拇a必須使用互斥鎖或信號(hào)量等線程同步機(jī)制,以防止多個(gè)線程或進(jìn)程同時(shí)訪問它們的臨界區(qū)資源。使用可重入的代碼可以提高程序的性能和可維護(hù)性,并避免競爭條件和死鎖等問題。可重入代碼常見的應(yīng)用包括操作系統(tǒng)內(nèi)核、庫函數(shù)等),2)要么它不能被信號(hào)處理程序中斷。圖8-33列出了Linux保證安全的系統(tǒng)級函數(shù)。(信號(hào)處理程序產(chǎn)生輸出唯一安全的方法是使用write函數(shù),使用printf或者sprintf是不安全的)
            • 保存和恢復(fù)errno:進(jìn)入處理程序時(shí)把出錯(cuò)返回時(shí)設(shè)置的errno保存在一個(gè)局部變量中
            • 阻塞所有的信號(hào),保護(hù)對共享全局?jǐn)?shù)據(jù)結(jié)構(gòu)的訪問。如果處理程序和主程序或其他處理程序共享一個(gè)全局?jǐn)?shù)據(jù)結(jié)構(gòu),那么在訪問該數(shù)據(jù)結(jié)構(gòu)時(shí),你的處理程序和主程序應(yīng)該暫時(shí)阻塞所有的信號(hào)。這條規(guī)則的原因是:從主程序訪問一個(gè)數(shù)據(jù)結(jié)構(gòu)d通常需要一系列的指令,如果指令序列被訪問d的處理程序中斷,那么處理程序可能會(huì)發(fā)現(xiàn)d的狀態(tài)不一致,得到不可預(yù)知的結(jié)果。
            • 用volatile聲明全局變量:使用關(guān)鍵字“volatile”可以聲明一個(gè)變量為“易變的(volatile)”,使得該變量的值可能被意外更改,即該變量的值可能被程序外部的因素更改,例如操作系統(tǒng)或硬件。通常,這種情況發(fā)生在多線程/多進(jìn)程程序或者嵌入式系統(tǒng)中。當(dāng)我們在使用全局變量時(shí),如果其值會(huì)被通過其他方式修改(比如中斷處理函數(shù)),我們就應(yīng)該使用volatile關(guān)鍵字來修飾該變量,保證在程序中使用該變量時(shí)總是讀取最新的值。在多線程/多進(jìn)程程序中,當(dāng)一個(gè)線程或進(jìn)程修改了某個(gè)全局變量時(shí),其他線程或進(jìn)程會(huì)在不知情的情況下讀取過期的或者不正確的值,這可能會(huì)導(dǎo)致不可預(yù)期的結(jié)果,為了避免這種情況,我們可以使用volatile關(guān)鍵字來使得該變量的值隨時(shí)保持最新。需要注意的是,使用volatile關(guān)鍵字會(huì)影響編譯器的優(yōu)化過程,因?yàn)榫幾g器不會(huì)對該變量做過多的優(yōu)化,以保證程序的正確性。
            • 用sig_atomic_t聲明標(biāo)志:保證信號(hào)的讀寫是原子的。
          • 2.正確的信號(hào)處理 未處理的信號(hào)是不排隊(duì)的,因?yàn)閜ending位想兩種每種類型的信號(hào)只對應(yīng)有一位,所以每種類型最多只能有一個(gè)未處理信號(hào),多余的信號(hào)都會(huì)被丟棄。
          • 3.可移植的信號(hào)處理 Unix信號(hào)處理的另一個(gè)缺陷在于不同的系統(tǒng)有不同的信號(hào)處理語義:
          • signal函數(shù)的語義各有不同
          • 系統(tǒng)調(diào)用可以被中斷:像read、write和accpet這樣的系統(tǒng)調(diào)用潛在地會(huì)阻塞進(jìn)程一段較長的時(shí)間,稱為慢速系統(tǒng)調(diào)用。為了解決以上問題,Posix標(biāo)準(zhǔn)定義了sigaction函數(shù),允許用戶在設(shè)置信號(hào)處理時(shí),明確指定他們想要的信號(hào)處理含義。sigaction的替代方案是定義一個(gè)叫做signal的包裝函數(shù)調(diào)用sigaction

          8.5.6 同步流以避免討厭的并發(fā)錯(cuò)誤

          eg:考慮圖8-39的程序,它總結(jié)了一個(gè)典型的Unix shell的結(jié)構(gòu)。父進(jìn)程在一個(gè)全局作業(yè)列表中記錄著它的當(dāng)前子進(jìn)程,每一個(gè)作業(yè)一個(gè)條目。addjob和deletejob函數(shù)分別向這個(gè)作業(yè)列表添加和從中刪除作業(yè)。當(dāng)父進(jìn)程創(chuàng)建一個(gè)新的子進(jìn)程后,它就把這個(gè)子進(jìn)程添加到作業(yè)列表中。當(dāng)父進(jìn)程在SIGCHLD處理程序中回收一個(gè)終止的子進(jìn)程時(shí),它就從作業(yè)列表中刪除這個(gè)子進(jìn)程。問題是,可能會(huì)發(fā)生這樣的事情:

          • 1)父進(jìn)程執(zhí)行fork函數(shù),內(nèi)核調(diào)度新創(chuàng)建的子進(jìn)程運(yùn)行,而不是父進(jìn)程;
          • 2)在父進(jìn)程能夠再次運(yùn)行之前,子進(jìn)程就終止,并且變成一個(gè)僵死進(jìn)程,使得內(nèi)核傳遞一個(gè)SIGCHLD信號(hào)給父進(jìn)程;
          • 3)后來,當(dāng)父進(jìn)程再次變成可運(yùn)行但又在它執(zhí)行之前,內(nèi)核注意到有未處理的SIGCHLD信號(hào),并通過在父進(jìn)程中運(yùn)行處理程序接受這個(gè)信號(hào);
          • 4)信號(hào)處理程序回收終止的子進(jìn)程,并調(diào)用deletejob,這個(gè)函數(shù)什么也不做,因?yàn)楦高M(jìn)程還沒有把該子進(jìn)程添加到列表中;
          • 5)在處理程序執(zhí)行完畢之后,內(nèi)核運(yùn)行父進(jìn)程,父進(jìn)程從fork返回,通過調(diào)用addjob錯(cuò)誤地把不存在的子進(jìn)程添加到作業(yè)列表中

          因此,對于父進(jìn)程的main程序和信號(hào)處理流的某些交錯(cuò),可能會(huì)在addjob之前調(diào)用deletejob。這會(huì)導(dǎo)致作業(yè)列表出現(xiàn)一個(gè)不正確的條目,對應(yīng)于一個(gè)不再存在而且永遠(yuǎn)也不會(huì)被刪除的作業(yè)。另一方面,也有一些交錯(cuò),事件按照正確的順序發(fā)生。eg:如果在fork調(diào)用返回時(shí),內(nèi)核剛好調(diào)度父進(jìn)程而不是子進(jìn)程運(yùn)行,那么父進(jìn)程就會(huì)正確地把子進(jìn)程添加到作業(yè)列表中,然后子進(jìn)程終止,信號(hào)處理函數(shù)把該作業(yè)從列表中刪除。

          這是一個(gè)稱為競爭(race)的經(jīng)典同步錯(cuò)誤的示例。在這個(gè)情況下,main函數(shù)中調(diào)用addjob和處理程序中調(diào)用deletejob之間存在競爭。如果addjob贏,則結(jié)果是正確的,反之則出錯(cuò)。圖8-40展示了消除圖8-39中競爭的一種方法,通過在調(diào)用fork之前,阻塞SIGCHLD信號(hào),然后在調(diào)用addjob之后,取消阻塞。

          8.5.7 顯式地等待信號(hào)

          圖8-41給了一個(gè)基本思路。父進(jìn)程設(shè)置SIGINT和SIGCHLD的處理程序,然后進(jìn)入一個(gè)無限循環(huán)。它阻塞SIGCHLD信號(hào),避免父進(jìn)程和子進(jìn)程之間的競爭。創(chuàng)建了子進(jìn)程之后,把pid重置為0,取消阻塞SIGCHLD,然后以循環(huán)的方式等待pid變?yōu)榉橇?。子進(jìn)程終止后,處理程序回收它,把它非零的PID賦值給全局pid變量,終止循環(huán)。上訴代碼的問題是循環(huán)浪費(fèi)資源,有以下解決辦法:

          • 使用pause,會(huì)有競爭
          • 使用sleep,無法確定合適的事件
          • 使用sigsuspend,闊以
          sigsuspend 是一個(gè) UNIX 系統(tǒng)調(diào)用,用于暫停進(jìn)程并等待信號(hào)。當(dāng)進(jìn)程執(zhí)行 sigsuspend 時(shí),它會(huì)阻塞,直到收到指定信號(hào)之一為止。這個(gè)系統(tǒng)調(diào)用通常用于等待異步事件,比如定時(shí)器或 Socket 等待連接或數(shù)據(jù)。

          sigprocmask 和 sigaction 是 sigsuspend 的前置條件,這些系統(tǒng)調(diào)用可用于管理信號(hào)處理程序和控制信號(hào)的接收。在一些情況下,進(jìn)程可能需要暫停其信號(hào)處理器以等待特定信號(hào),這時(shí)就需要使用 sigsuspend。

          圖8-42是使用sigsuspend的例子。

          8.6 非本地跳轉(zhuǎn)

          C提供了一種用戶級異常控制流形式,稱為非本地跳轉(zhuǎn)(nonlocal jump),它將控制直接從一個(gè)函數(shù)轉(zhuǎn)移到另一個(gè)當(dāng)前正在執(zhí)行的函數(shù),而不需要經(jīng)過正常的調(diào)用-返回序列,非本地跳轉(zhuǎn)是通過setjmp和longjmp函數(shù)來提供的。

          setjmp 是一個(gè) C 語言標(biāo)準(zhǔn)庫函數(shù),它允許程序在任意位置保存當(dāng)前上下文,并在后續(xù)恢復(fù)該上下文。它通常用于實(shí)現(xiàn)非局部跳轉(zhuǎn)(longjmp)。


          當(dāng)調(diào)用 setjmp 時(shí),它會(huì)保存當(dāng)前的 CPU 上下文,包括棧指針、程序計(jì)數(shù)器和寄存器等信息。然后它會(huì)返回 0。此后,如果調(diào)用 longjmp 并傳入相同的 jmp_buf 參數(shù),程序?qū)?huì)回到 setjmp 調(diào)用的位置,并且 setjmp 的返回值將為非零值。


          setjmp 的返回值和傳遞給 longjmp 的非零值之間不存在顯式的聯(lián)系。通常,開發(fā)者會(huì)使用 setjmp 的返回值作為一個(gè)狀態(tài)指示器,用來判斷程序是從 setjmp 返回的還是從 longjmp 跳轉(zhuǎn)回來的。


          一個(gè)常見的用途是處理異常。當(dāng)程序發(fā)生錯(cuò)誤需要跳轉(zhuǎn)出去時(shí),可以使用 setjmp 保存當(dāng)前環(huán)境,然后在異常處理函數(shù)中使用 longjmp 返回到之前保存的環(huán)境。這樣就可以避免使用一些過于復(fù)雜的結(jié)構(gòu)來處理異常。
          longjmp 是一個(gè) C 語言標(biāo)準(zhǔn)庫函數(shù),用于進(jìn)行非局部跳轉(zhuǎn)。與 goto 不同的是,longjmp 可以跳出多層函數(shù)調(diào)用,返回到調(diào)用 setjmp 時(shí)保存的上下文狀態(tài)。


          當(dāng)調(diào)用 longjmp 時(shí),會(huì)使程序跳轉(zhuǎn)到之前調(diào)用過 setjmp 保存的上下文狀態(tài)。longjmp 的第二個(gè)參數(shù)指定跳轉(zhuǎn)時(shí)的返回值。


          longjmp 的使用必須在 setjmp 的作用域范圍內(nèi)。如果 longjmp 跳轉(zhuǎn)到了不在 setjmp 的作用域內(nèi)的位置,程序的行為將是未定義的。


          由于 longjmp 的跳轉(zhuǎn)是非常強(qiáng)大和危險(xiǎn)的,因此它通常被用于處理異常情況,當(dāng)程序遇到意外錯(cuò)誤時(shí),使用 setjmp 在當(dāng)前位置進(jìn)行狀態(tài)保存,然后在異常處理函數(shù)中使用 longjmp 跳轉(zhuǎn)到保存的狀態(tài)進(jìn)行處理。 但是,使用 setjmp 和 longjmp 時(shí)需要非常小心,確保它們被正確使用,否則會(huì)導(dǎo)致程序崩潰或產(chǎn)生其他嚴(yán)重問題。

          圖8.43展示了一個(gè)非本地跳轉(zhuǎn)的例子。非本地跳轉(zhuǎn)的一個(gè)重要應(yīng)用是允許從一個(gè)深層嵌套的函數(shù)調(diào)用中立即返回,通常是由檢測到某個(gè)錯(cuò)誤情況引起的。如果在一個(gè)深層嵌套的函數(shù)調(diào)用中發(fā)現(xiàn)了一個(gè)錯(cuò)誤情況,我們可以使用非本地跳轉(zhuǎn)直接返回到一個(gè)普通的本地化的錯(cuò)誤處理程序,而不是費(fèi)力地解開調(diào)用棧。longjmp允許它跳過所有中間調(diào)用的特性可能產(chǎn)生意外的后果。eg:如果中間函數(shù)調(diào)用中分配了某些數(shù)據(jù)結(jié)構(gòu),本來預(yù)期在函數(shù)結(jié)尾處釋放它們,那么這些釋放代碼會(huì)被跳過,從而產(chǎn)生內(nèi)存泄漏。

          非本地跳轉(zhuǎn)的另一個(gè)重要應(yīng)用是使一個(gè)信號(hào)處理程序分支到一個(gè)特殊的代碼位置,而不是返回到被信號(hào)到達(dá)中斷了的指令的位置。圖8-44展示了一個(gè)簡單的技術(shù),說明了這種基本技術(shù):當(dāng)用戶在鍵盤上鍵入Ctrl + C時(shí),這個(gè)程序用信號(hào)和非本地跳轉(zhuǎn)實(shí)現(xiàn)軟重啟。sigsetimp和siglongjmp是setjmp和longjmp的可以被信號(hào)處理程序使用的版本。在程序第一次啟動(dòng)時(shí),對sigsetimp函數(shù)的初始調(diào)用保存調(diào)用環(huán)境和信號(hào)的上下文(包括待處理的和被阻塞的信號(hào)向量)。隨后,主函數(shù)進(jìn)入一個(gè)無限循環(huán)。當(dāng)用戶鍵入Ctril + C時(shí),內(nèi)核發(fā)送一個(gè)SIGINT信號(hào)給這個(gè)進(jìn)程,該進(jìn)程捕獲這個(gè)信號(hào)。不是從信號(hào)處理程序返回,而是實(shí)現(xiàn)一個(gè)非本地跳轉(zhuǎn),回到main函數(shù)的開始處。兩個(gè)注意點(diǎn):

          • 必須在調(diào)用了sigsetimp之后再設(shè)置處理程序。否則就會(huì)冒在初始調(diào)用sigsetimp為siglongjmp設(shè)置調(diào)用環(huán)境之前運(yùn)行處理程序的風(fēng)險(xiǎn)
          • sigsetjmp和siglongjmp函數(shù)不在8-33異步信號(hào)安全的函數(shù)之列,因?yàn)槠淇梢蕴饺我獯a,所以最好只在siglongjmp可達(dá)的代碼中調(diào)用安全的函數(shù)。

          8.7 操作進(jìn)程的工具

          • STRACE:
          strace 是一種 Linux 系統(tǒng)級調(diào)試工具,它可以跟蹤和記錄應(yīng)用程序執(zhí)行時(shí)所有系統(tǒng)調(diào)用和信號(hào),以便分析和調(diào)試程序。它可以用來解決一些問題,例如程序運(yùn)行慢、程序崩潰或掛起等。


          使用 strace 可以獲取程序在執(zhí)行時(shí)調(diào)用系統(tǒng)庫的詳細(xì)信息和調(diào)用順序,例如文件操作、網(wǎng)絡(luò)操作、進(jìn)程管理等,可以顯示系統(tǒng)調(diào)用的返回值、參數(shù)以及調(diào)用時(shí)間等詳細(xì)信息。通過查看 strace 輸出可以判斷程序是否正常執(zhí)行,是否有權(quán)限問題或者其他異常。


          strace 的使用非常簡單,只需要在命令前加上 strace 即可,例如:


          strace ls -l

          這條命令會(huì)跟蹤 ls -l 命令執(zhí)行時(shí)所有的系統(tǒng)調(diào)用和信號(hào),并將結(jié)果輸出到標(biāo)準(zhǔn)輸出。


          除了常見的選項(xiàng),例如 -o 參數(shù)可以將輸出保存到文件中,-p 參數(shù)可以指定要跟蹤的進(jìn)程 ID,-f 參數(shù)可以跟蹤子進(jìn)程等。strace 是 Linux 系統(tǒng)中非常有用的調(diào)試工具之一。
          • PS
          ps 是一個(gè)常用的 Linux 命令,用于查看系統(tǒng)進(jìn)程信息。ps 命令可以顯示與當(dāng)前登錄用戶有關(guān)的所有進(jìn)程,或者指定進(jìn)程的信息。你可以使用 ps 命令來監(jiān)控系統(tǒng)運(yùn)行狀態(tài),或者找到某個(gè)進(jìn)程的 PID(進(jìn)程ID)。


          ps 命令的常用選項(xiàng)包括:



          ps aux 列出所有進(jìn)程的詳細(xì)信息,包括進(jìn)程的 PID、占用 CPU 的百分比、占用內(nèi)存的百分比、啟動(dòng)時(shí)間、命令及其參數(shù)等。

          ps -f 以完整格式顯示進(jìn)程信息。

          ps -ef 以完整格式顯示所有進(jìn)程信息,包括命令行參數(shù),使用 UID 和 GID 顯示所有者和組。

          ps -e 列出所有正在運(yùn)行的進(jìn)程。

          ps -C command_name 顯示指定命令的進(jìn)程信息。

          ps -p pid 顯示指定進(jìn)程的信息。

          ps -t terminal 僅顯示運(yùn)行在指定終端上的進(jìn)程。


          其中 aux 和 ef 是最常用的選項(xiàng),可以顯示最詳細(xì)的進(jìn)程信息。例如:


          ps aux

          該命令會(huì)列出當(dāng)前系統(tǒng)上所有的進(jìn)程信息,并展示詳細(xì)的配置信息,讓你很容易找到正在運(yùn)行的進(jìn)程信息和相應(yīng)的 PID。

          ps -h 是 ps 命令的一個(gè)選項(xiàng),它的作用是隱藏 ps 命令輸出中的標(biāo)題行。這意味著當(dāng)你使用 ps -h 命令時(shí),你只會(huì)看到進(jìn)程信息,而不會(huì)看到默認(rèn)的標(biāo)題行。

          • TOP
          top 是一個(gè)命令行實(shí)用程序,它可以在類 Unix 系統(tǒng)中可視化地顯示進(jìn)程活動(dòng)和系統(tǒng)資源的使用情況。它具有實(shí)時(shí)監(jiān)視的功能,可以幫助管理員了解系統(tǒng)的性能,并能夠及時(shí)響應(yīng)問題。


          當(dāng)你運(yùn)行 top 命令時(shí),它會(huì)向你展示一個(gè)實(shí)時(shí)更新的進(jìn)程列表,列表按照 CPU 使用率或內(nèi)存消耗來排序。默認(rèn)情況下,列表按照 CPU 使用率排序,最先顯示最消耗 CPU 的進(jìn)程。你可以按下不同的鍵來切換列表的排序方式。


          top 命令還會(huì)顯示系統(tǒng)的負(fù)載情況(即 CPU 利用率)、內(nèi)存使用情況、交換空間使用情況、進(jìn)程數(shù)量等信息。它還會(huì)提供一些基本的交互式功能,例如在進(jìn)程列表中選擇進(jìn)程來終止它們,修改進(jìn)程的優(yōu)先級等。
          • PMAP
          pmap 是一個(gè)命令行實(shí)用程序,它可以顯示指定進(jìn)程或進(jìn)程 ID 的內(nèi)存映射情況。它通常用于診斷和調(diào)試進(jìn)程的內(nèi)存使用情況,包括進(jìn)程占用的內(nèi)存大小、內(nèi)存區(qū)域的地址范圍、內(nèi)存映射文件、共享庫等信息。


          當(dāng)你運(yùn)行 pmap 命令時(shí),可以看到如下輸出:


          $ pmap 1234
          1234:   /usr/bin/python3 myscript.py
          0000555555554000     48K r-x-- myscript.py
          0000555555571000   2048K ----- myscript.py
          00007ffff7a38000   1408K r---- libc-2.27.so
          00007ffff7bd5000   1048K r-x-- libc-2.27.so
          00007ffff7cf3000     24K r---- libc-2.27.so
          00007ffff7cf9000      8K rw--- libc-2.27.so
          00007ffff7cfb000     16K rw---   [ anon ]
          00007ffff7cff000    144K r-x-- ld-2.27.so
          00007ffff7ee4000     12K rw---   [ anon ]
          00007ffff7eee000      8K rw---   [ anon ]
          00007ffff7ef0000      4K r---- ld-2.27.so
          00007ffff7ef1000      4K rw--- ld-2.27.so
          00007ffff7ef2000      4K rw---   [ anon ]
          00007ffff7ef3000      4K r---- myscript.py
          00007ffff7ef4000      4K rw--- myscript.py
          00007ffffffde000    132K rw---   [ stack ]
          ffffffffff600000      4K r-x--   [ anon ]
          ----------------  ------ 
          total            3896K

          輸出中分別列出了進(jìn)程中的內(nèi)存區(qū)域,并顯示了每個(gè)區(qū)域的地址范圍、權(quán)限、來源以及占用的內(nèi)存大小。在這個(gè)例子中,pmap 命令顯示了進(jìn)程 ID 為 1234 的 Python 進(jìn)程的內(nèi)存映射情況,其中還包括 Python 解釋器使用的一些共享庫和內(nèi)存區(qū)域。你可以使用 pmap 命令來確定內(nèi)存使用情況、查找內(nèi)存泄漏或者優(yōu)化進(jìn)程占用的內(nèi)存等。
          • /proc
          通過讀取 /proc 目錄中的文件,可以獲得有關(guān)系統(tǒng)和進(jìn)程狀態(tài)的各種信息。


          例如,讀取 /proc/cpuinfo 文件可以獲得有關(guān) CPU 型號(hào)、頻率、核心數(shù)和緩存等信息。讀取 /proc/meminfo 文件可以獲得有關(guān)系統(tǒng)內(nèi)存使用情況的信息。讀取 /proc/[pid]/status 文件可以獲得特定進(jìn)程的狀態(tài)信息,如進(jìn)程 ID、用戶名、運(yùn)行狀態(tài)、內(nèi)存使用情況等。


          /proc 目錄下的文件和目錄通常都是只讀的,但在某些情況下也可以進(jìn)行寫入。例如,向 /proc/sys/kernel/hostname 文件中寫入一個(gè)新的主機(jī)名可以更改系統(tǒng)的名稱。此外,通過在 /proc/sys 目錄中進(jìn)行寫入,可以更改系統(tǒng)內(nèi)核的一些參數(shù)和配置。


          總之,/proc 為我們提供了一種查看和更改系統(tǒng)狀態(tài)、進(jìn)程狀態(tài)和內(nèi)核參數(shù)的方法。但是需要注意的是,在讀取和寫入 /proc 目錄下的文件時(shí),需要有足夠的權(quán)限才可以進(jìn)行操作。

          shlab hw:

          • op:

            編譯:make 測試某一個(gè):make test{num} 查看標(biāo)準(zhǔn)答案:make rtest{num}

          • 要修改的函數(shù):如上

            • eval: 解析判斷cmdline,查看是否為builtin_cmd,如果不是,如果是后臺(tái)運(yùn)行,則阻塞,起子進(jìn)程,子進(jìn)程中解阻塞,運(yùn)行,主進(jìn)程addjob,解阻塞;如果是,進(jìn)入builtin_cmd。
            • builtin_cmd: 給quit、&、jobs、fg、bg做結(jié)構(gòu)判斷,如果是bg/fg,則進(jìn)入do_bgfg.
            • do_bgfg:解析cmdline,并操作
            • waitfg:等待停止
            • sigchld_handler:等待對應(yīng)進(jìn)程狀態(tài)變化,并根據(jù)變化時(shí)信號(hào)狀態(tài)進(jìn)程進(jìn)行deletejob(EXITED/SIGNQALED)/getjobpid(STOPPED)
            • sigint_hanler :捕獲ctrl-c
            • sigtstp_handler:捕獲ctrl-z
          • test:

            • 02:退出
            • 03:前臺(tái)進(jìn)程,退出
            • 04:run后臺(tái)臺(tái)進(jìn)程
            • 05: 運(yùn)行后臺(tái)進(jìn)程并列出
            • 06:運(yùn)行前臺(tái)進(jìn)程,并給出ctrl_z信號(hào)
            • 07: 同時(shí)運(yùn)行前后臺(tái)進(jìn)程并列出
            • 08: 同時(shí)運(yùn)行前后臺(tái)進(jìn)程給信號(hào)到前臺(tái)并列出
            • 09: 同時(shí)運(yùn)行前后臺(tái)進(jìn)程 將前臺(tái)進(jìn)程變成后臺(tái),并列出
            • 10: 運(yùn)行后臺(tái)進(jìn)程 將后臺(tái)進(jìn)程變成前臺(tái),并列出
            • 11: 運(yùn)行后臺(tái)進(jìn)程 將后臺(tái)進(jìn)程變成前臺(tái),并列出
            • 12 - 13: ps -a
            • 14: 錯(cuò)誤處理
            • 15: 綜合
            • 16: 測試其他進(jìn)程給的SIGTSTP 和 SIGINT

          github:

          https://github.com/liuxubit/csapp_labs/tree/shlab

          reference:

          https://gitee.com/sun-hongwei8011/csapp-lab


          瀏覽 797
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  大香蕉操逼片 | 日本操B 操B 操B | 亚洲视频中文字幕在线观看 | 五月丁香影视 | 午夜成人免费视频 |