找合作項(xiàng)目app平臺(tái)濟(jì)南做seo外包
參考引用
- UNIX 環(huán)境高級(jí)編程 (第3版)
- 黑馬程序員-Linux 系統(tǒng)編程
1. UNIX 基礎(chǔ)知識(shí)
1.1 UNIX 體系結(jié)構(gòu)(下圖所示)
- 從嚴(yán)格意義上說(shuō),可將操作系統(tǒng)定義為一種軟件,它控制計(jì)算機(jī)硬件資源,提供程序運(yùn)行環(huán)境,通常將這種軟件稱(chēng)為內(nèi)核 (kernel),因?yàn)樗鄬?duì)較小,而且位于環(huán)境的核心
- 內(nèi)核的接口被稱(chēng)為系統(tǒng)調(diào)用 (system call,下圖中的陰影部分)
- 公用函數(shù)庫(kù)構(gòu)建在系統(tǒng)調(diào)用接口之上,應(yīng)用程序既可使用公用函數(shù)庫(kù),也可使用系統(tǒng)調(diào)用
- shell 是一個(gè)特殊的應(yīng)用程序,為運(yùn)行其他應(yīng)用程序提供了一個(gè)接口
1.2 文件和目錄
1.2.1 文件系統(tǒng)
- UNIX 文件系統(tǒng)是目錄和文件的一種層次結(jié)構(gòu),所有東西的起點(diǎn)是稱(chēng)為根 (root) 的目錄,這個(gè)目錄的名稱(chēng)是一個(gè)字符 “/”
- 目錄 (directory) 是一個(gè)包含目錄項(xiàng)的文件。在邏輯上,可認(rèn)為每個(gè)目錄項(xiàng)都包含一個(gè)文件名以及說(shuō)明該文件屬性的信息
- 文件屬性是指文件類(lèi)型 (是普通文件還是目錄等) 、文件大小、文件所有者、文件權(quán)限 (其他用戶(hù)能否訪問(wèn)該文件) 以及文件最后的修改時(shí)間等
- stat 和 fstat 函數(shù)返回包含所有文件屬性的一個(gè)信息結(jié)構(gòu)
1.2.2 文件名
- 目錄中的各個(gè)名字稱(chēng)為文件名 (flename)
- 只有斜線(xiàn) (/) 和空字符這兩個(gè)字符不能出現(xiàn)在文件名中
- 斜線(xiàn)用來(lái)分隔構(gòu)成路徑名的各文件名,空字符則用來(lái)終止一個(gè)路徑名
- 為了可移植性,POSIX.1 推薦將文件名限制在以下字符集之內(nèi): 字母 (a~z、A~Z)、數(shù)字 (0~9)、句點(diǎn) (.)、短橫線(xiàn) (-) 和下劃線(xiàn) (_)
- 創(chuàng)建新目錄時(shí)會(huì)自動(dòng)創(chuàng)建了兩個(gè)文件名:. (稱(chēng)為點(diǎn)) 和 …(稱(chēng)為點(diǎn)點(diǎn))
- 點(diǎn)指向當(dāng)前目錄,點(diǎn)點(diǎn)指向父目錄
- 在最高層次的根目錄中,點(diǎn)點(diǎn)與點(diǎn)相同
1.2.3 路徑名
- 由斜線(xiàn)分隔的一個(gè)或多個(gè)文件名組成的序列 (也可以斜線(xiàn)開(kāi)頭) 成路徑名 (pathmamme)
- 以斜線(xiàn)開(kāi)頭的路徑名稱(chēng)為絕對(duì)路徑名,否則稱(chēng)為相對(duì)路徑名,相對(duì)路徑名指向相對(duì)于當(dāng)前目錄的文件
- 文件系統(tǒng)根的名字 (/) 是一個(gè)特殊的絕對(duì)路徑名,它不包含文件名
1.2.4 工作目錄
- 每個(gè)進(jìn)程都有一個(gè)工作目錄 (working directory),有時(shí)稱(chēng)其為當(dāng)前工作目錄 (curent working directory),所有相對(duì)路徑名都從工作目錄開(kāi)始解釋,進(jìn)程可以用 chdir 函數(shù)更改其工作目錄
- 相對(duì)路徑名 doc/memo/joe 指的是當(dāng)前工作目錄中的 doc 目錄中的 memo 目錄中的文件 (或目錄) joe
- 從該路徑名可以看出,doc 和 memo 都應(yīng)當(dāng)是目錄,但是卻不能分辨 joe 是文件還是目錄
- 路徑名 /urs/lib/lint 是一個(gè)絕對(duì)路徑名,它指的是根目錄中的 usr 目錄中的 lib 目錄中的文件 (或目錄) lint
1.3 輸入和輸出
1.3.1 文件描述符
- 文件描述符 (file descriptor) 通常是一個(gè)小的非負(fù)整數(shù),內(nèi)核用以標(biāo)識(shí)一個(gè)特定進(jìn)程正在訪問(wèn)的文件。當(dāng)內(nèi)核打開(kāi)一個(gè)現(xiàn)有文件或創(chuàng)建一個(gè)新文件時(shí),它都返回一個(gè)文件描述符
1.3.2 標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤
- 每當(dāng)運(yùn)行一個(gè)新程序時(shí),所有的 shell 都為其打開(kāi) 3 個(gè)文件描述符,即標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出以及標(biāo)準(zhǔn)錯(cuò)誤
1.3.3 不帶緩沖的 I/O
- 函數(shù) open、read、write、lseek 以及 close 提供了不帶緩沖的 I/O,這些函數(shù)都使用文件描述符
1.3.4 標(biāo)準(zhǔn) I/O
- 標(biāo)準(zhǔn) I/O 函數(shù)為那些不帶緩沖的 I/O 函數(shù)提供了一個(gè)帶緩沖的接口,最熟悉的標(biāo)準(zhǔn) I/O 函數(shù)是 printf
1.4 程序和進(jìn)程
1.4.1 程序
- 程序 (program) 是一個(gè)存儲(chǔ)在磁盤(pán)上某個(gè)目錄中的可執(zhí)行文件。內(nèi)核使用 exec 函數(shù),將程序讀入內(nèi)存,并執(zhí)行程序
1.4.2 進(jìn)程和進(jìn)程 ID
- 程序的執(zhí)行實(shí)例被稱(chēng)為進(jìn)程 (process),某些操作系統(tǒng)用任務(wù) (task) 表示正在被執(zhí)行的程序
- UNIX 系統(tǒng)確保每個(gè)進(jìn)程都有一個(gè)唯一的數(shù)字標(biāo)識(shí)符,稱(chēng)為進(jìn)程 (process ID)。進(jìn)程 ID 總是一個(gè)非負(fù)整數(shù)
1.4.3 進(jìn)程控制
- 有 3 個(gè)用于進(jìn)程控制的主要函數(shù):fork、exec 和 waitpid(exec 函數(shù)有 7 種變體,但經(jīng)常把它們統(tǒng)稱(chēng)為 exec 函數(shù))
1.4.4 線(xiàn)程和線(xiàn)程 ID
- 通常,一個(gè)進(jìn)程 (process) 只有一個(gè)控制線(xiàn)程 (thread):某一時(shí)刻執(zhí)行的一組機(jī)器指令。對(duì)于某些問(wèn)題,如果有多個(gè)控制線(xiàn)程分別作用于它的不同部分,那么解決起來(lái)就容易得多。另外,多個(gè)控制線(xiàn)程也可以充分利用多處理器系統(tǒng)的并行能力
- 一個(gè)進(jìn)程內(nèi)的所有線(xiàn)程共享同一地址空間、文件描述符、棧以及與進(jìn)程相關(guān)的屬性。因?yàn)樗鼈兡茉L問(wèn)同一存儲(chǔ)區(qū),所以各線(xiàn)程在訪問(wèn)共享數(shù)據(jù)時(shí)需要采取同步措施以避免不一致性
- 與進(jìn)程相同,線(xiàn)程也用 ID 標(biāo)識(shí)。但是,線(xiàn)程只在它所屬的進(jìn)程內(nèi)起作用。一個(gè)進(jìn)程中的線(xiàn)程 ID 在另一個(gè)進(jìn)程中沒(méi)有意義。當(dāng)在一進(jìn)程中對(duì)某個(gè)特定線(xiàn)程進(jìn)行處理時(shí),可以使用該線(xiàn)程的 ID 引用它
1.5 出錯(cuò)處理
- 當(dāng) UNIX 系統(tǒng)函數(shù)出錯(cuò)時(shí),通常會(huì)返回一個(gè)負(fù)值,而且整型變量 errno 通常被設(shè)置為具有特定信息的值。而有些函數(shù)對(duì)于出錯(cuò)則使用另一種約定而不是返回負(fù)值。例如,大多數(shù)返回指向?qū)ο笾羔樀暮瘮?shù),在出錯(cuò)時(shí)會(huì)返回一個(gè) null 指針
- POSIX.1 和 ISO C 將 errno 定義為一個(gè)符號(hào),它擴(kuò)展成為一個(gè)可修改的整形左值
- 它可以是一個(gè)包含出錯(cuò)編號(hào)的整數(shù),也可以是一個(gè)返回出錯(cuò)編號(hào)指針的函數(shù)
- 在支持線(xiàn)程的環(huán)境中,多個(gè)線(xiàn)程共享進(jìn)程地址空間,每個(gè)線(xiàn)程都有屬于它自己的局部 errno 以避免一個(gè)線(xiàn)程干擾另一個(gè)線(xiàn)程
- 對(duì)于 errno 應(yīng)當(dāng)注意兩條規(guī)則
- 第一:如果沒(méi)有出錯(cuò),其值不會(huì)被例程清除。因此,僅當(dāng)函數(shù)的返回值指明出錯(cuò)時(shí),才檢驗(yàn)其值
- 第二:任何函數(shù)都不會(huì)將 errno 值設(shè)置為 0,而且在 <errno.h> 中定義的所有常量都不為 0
1.6 用戶(hù)標(biāo)識(shí)
1.6.1 用戶(hù) ID
- 口令文件登錄項(xiàng)中的用戶(hù) ID (user ID) 是一個(gè)數(shù)值,它向系統(tǒng)標(biāo)識(shí)各個(gè)不同的用戶(hù)。系統(tǒng)管理員在確定一個(gè)用戶(hù)的登錄名的同時(shí),確定其用戶(hù) ID。用戶(hù)不能更改其用戶(hù) ID,通常每個(gè)用戶(hù)有一個(gè)唯一的用戶(hù) ID
- 用戶(hù) ID 為 0 的用戶(hù)為根用戶(hù) (root) 或超級(jí)用戶(hù) (superuser)。在口令文件中,通常有一個(gè)登錄項(xiàng),其登錄名為 root,稱(chēng)這種用戶(hù)的特權(quán)為超級(jí)用戶(hù)特權(quán)。某些操作系統(tǒng)功能只向超級(jí)用戶(hù)提供,超級(jí)用戶(hù)對(duì)系統(tǒng)有自由的支配權(quán)
1.6.2 組 ID
- 口令文件登錄項(xiàng)也包括用戶(hù)的組 ID (group ID),它是一個(gè)數(shù)值。組 ID 也是由系統(tǒng)管理員在指定用戶(hù)登錄名時(shí)分配的。一般來(lái)說(shuō),在口令文件中有多個(gè)登錄項(xiàng)具有相同的組 ID。組被用于將若干用戶(hù)集合到項(xiàng)目或部門(mén)中去。這種機(jī)制允許同組的各個(gè)成員之間共享資源
- 組文件將組名映射為數(shù)值的組 ID,組文件通常是 /etc/group
- 對(duì)于磁盤(pán)上的每個(gè)文件,文件系統(tǒng)都存儲(chǔ)該文件所有者的用戶(hù) ID 和組 ID。存儲(chǔ)這兩個(gè)值只需 4 個(gè)字節(jié) (假定每個(gè)都以雙字節(jié)的整型值存放)。在檢驗(yàn)權(quán)限期間,比較字符串較之比較整型數(shù)更消耗時(shí)間
- 但是對(duì)于用戶(hù)而言,使用名字比使用數(shù)值方便,所以口令文件包含了登錄名和用戶(hù) ID 之間的映射關(guān)系,而組文件則包含了組名和組 D 之間的映射關(guān)系
1.7 信號(hào)
-
信號(hào) (signal) 用于通知進(jìn)程發(fā)生了某種情況。例如,若某一進(jìn)程執(zhí)行除法操作,其除數(shù)為 0,則將名為 SIGEPE (浮點(diǎn)異常) 的信號(hào)發(fā)送給該進(jìn)程。進(jìn)程有以下 3 種處理信號(hào)的方式
- (1) 忽略信號(hào)。有些信號(hào)表示硬件異常,例如,除以 0 或訪問(wèn)進(jìn)程地址空間以外的存儲(chǔ)單元等,因?yàn)檫@些異常產(chǎn)生的后果不確定,所以不推薦使用這種處理方式
- (2) 按系統(tǒng)默認(rèn)方式處理。對(duì)于除數(shù)為 0,系統(tǒng)默認(rèn)方式是終止該進(jìn)程
- (3) 提供一個(gè)函數(shù),信號(hào)發(fā)生時(shí)調(diào)用該函數(shù),這被稱(chēng)為捕捉該信號(hào)。通過(guò)提供自編的函數(shù)就能知道什么時(shí)候產(chǎn)生了信號(hào),并按期望的方式處理它
-
很多情況都會(huì)產(chǎn)生信號(hào)。終端鍵盤(pán)上有兩種產(chǎn)生信號(hào)的方法
- 中斷鍵 (通常是 Delete 鍵或 Crl+C) 和退出鍵 (通常是 Ctrl+\),它們被用于中斷當(dāng)前運(yùn)行的進(jìn)程
- 調(diào)用 kill 函數(shù)。在一個(gè)進(jìn)程中調(diào)用此函數(shù)就可向另一個(gè)進(jìn)程發(fā)送一個(gè)信號(hào)。當(dāng)然這樣做也有些限制:當(dāng)向一個(gè)進(jìn)程發(fā)送信號(hào)時(shí),必須是那個(gè)進(jìn)程的所有者或者是超級(jí)用戶(hù)
1.8 時(shí)間值
- UNIX 系統(tǒng)使用過(guò)兩種不同的時(shí)間值
- (1) 日歷時(shí)間。該值是自協(xié)調(diào)世界時(shí) (Coordinated Universal Time,UTC) 1970 年 1 月 1 日 00:00:00 這個(gè)特定時(shí)間以來(lái)所經(jīng)過(guò)的秒數(shù)累計(jì)值 (早期的手冊(cè)稱(chēng)UTC 為格林尼治標(biāo)準(zhǔn)時(shí)間)。這些時(shí)間值可用于記錄文件最近一次的修改時(shí)間等
- 系統(tǒng)基本數(shù)據(jù)類(lèi)型 time_t 用于保存這種時(shí)間值
- (2) 進(jìn)程時(shí)間。也被稱(chēng)為 CPU 時(shí)間,用以度量進(jìn)程使用的中央處理器資源。進(jìn)程時(shí)間以時(shí)鐘滴答計(jì)算。每秒鐘曾經(jīng)取為 50、60 或 100 個(gè)時(shí)鐘滴答
- 系統(tǒng)基本數(shù)據(jù)類(lèi)型 clock_t 保存這種時(shí)間值
- (1) 日歷時(shí)間。該值是自協(xié)調(diào)世界時(shí) (Coordinated Universal Time,UTC) 1970 年 1 月 1 日 00:00:00 這個(gè)特定時(shí)間以來(lái)所經(jīng)過(guò)的秒數(shù)累計(jì)值 (早期的手冊(cè)稱(chēng)UTC 為格林尼治標(biāo)準(zhǔn)時(shí)間)。這些時(shí)間值可用于記錄文件最近一次的修改時(shí)間等
- 當(dāng)度量一個(gè)進(jìn)程的執(zhí)行時(shí)間時(shí),UNIX 系統(tǒng)為一個(gè)進(jìn)程維護(hù)了 3 個(gè)進(jìn)程時(shí)間值
- 時(shí)鐘時(shí)間
- 時(shí)鐘時(shí)間又稱(chēng)為墻上時(shí)鐘時(shí)間 (wall clock time),它是進(jìn)程運(yùn)行的時(shí)間總量,其值與系統(tǒng)中同時(shí)運(yùn)行的進(jìn)程數(shù)有關(guān)
- 用戶(hù) CPU 時(shí)間
- 用戶(hù) CPU 時(shí)間是執(zhí)行用戶(hù)指今所用的時(shí)間量
- 系統(tǒng) CPU 時(shí)間
- 系統(tǒng) CPU 時(shí)間是為該進(jìn)程執(zhí)行內(nèi)核程序所經(jīng)歷的時(shí)間
- 用戶(hù) CPU 時(shí)間和系統(tǒng) CPU 時(shí)間之和常被稱(chēng)為 CPU 時(shí)間
- 時(shí)鐘時(shí)間
1.9 系統(tǒng)調(diào)用和庫(kù)函數(shù)
-
什么是系統(tǒng)調(diào)用?
- 由操作系統(tǒng)實(shí)現(xiàn)并提供給外部應(yīng)用程序的編程接口 (Application Programming Interface,API),是應(yīng)用程序同系統(tǒng)之間數(shù)據(jù)交互的橋梁
- 所有的操作系統(tǒng)都提供多種服務(wù)的入口點(diǎn),由此程序向內(nèi)核請(qǐng)求服務(wù)。各種版本的 UNIX 實(shí)現(xiàn)都提供良好定義、數(shù)量有限、直接進(jìn)入內(nèi)核的入口點(diǎn),這些入口點(diǎn)被稱(chēng)為系統(tǒng)調(diào)用 (system call)
-
通用庫(kù)函數(shù)可能會(huì)調(diào)用一個(gè)或多個(gè)內(nèi)核的系統(tǒng)調(diào)用,但是它們并不是內(nèi)核的入口點(diǎn)
- 例如,printf 函數(shù)會(huì)調(diào)用 write 系統(tǒng)調(diào)用以輸出一個(gè)字符串
- 但函數(shù) strcpy (復(fù)制一個(gè)字符串) 和 atoi (將 ASCII 轉(zhuǎn)換為整數(shù)) 并不使用任何內(nèi)核的系統(tǒng)調(diào)用
-
系統(tǒng)調(diào)用和庫(kù)函數(shù)都以 C 函數(shù)的形式出現(xiàn),兩者都為應(yīng)用程序提供服務(wù)
- 可以替換庫(kù)函數(shù),但系統(tǒng)調(diào)用通常是不能被替換的
- 系統(tǒng)調(diào)用通常提供一種最小接口,而庫(kù)函數(shù)通常提供比較復(fù)雜的功能
-
C 標(biāo)準(zhǔn)庫(kù)函數(shù)和系統(tǒng)函數(shù)/調(diào)用關(guān)系:一個(gè) “hello” 如何打印到屏幕的案例
- 其中系統(tǒng)調(diào)用相當(dāng)于對(duì)系統(tǒng)函數(shù)(man page 中的函數(shù))進(jìn)行了一個(gè)淺封裝
2. UNIX 標(biāo)準(zhǔn)及實(shí)現(xiàn)
2.1 UNIX 標(biāo)準(zhǔn)化
2.1.1 IOS C
- ISO C 標(biāo)準(zhǔn)現(xiàn)在由 ISO/TEC 的 C 程序設(shè)計(jì)語(yǔ)言國(guó)際標(biāo)準(zhǔn)工作組維護(hù)和開(kāi)發(fā)該工作組稱(chēng)為 ISO/IEC JTC1/SC22/WG14,簡(jiǎn)稱(chēng) WG14。ISO C 標(biāo)準(zhǔn)的意圖是提供 C 程序的可移植性,使其能適合于大量不同的操作系統(tǒng),而不只是適合 UNIX 系統(tǒng)
- ISO C 標(biāo)準(zhǔn)定義的頭文件
2.1.2 IEEE POSIX.1
- POSIX.1 是一個(gè)最初由 IEEE(Institute of Electricaland Electronics Engineers,電氣和電子工程師學(xué)會(huì)) 制訂的標(biāo)準(zhǔn)族。POSIX.1 指的是可移植操作系統(tǒng)接口 (Portable Operating System Interface)。它原來(lái)指的只是 IEEE 標(biāo)準(zhǔn) 1003.1-1988 (操作系統(tǒng)接口),后來(lái)則擴(kuò)展成包括很多記為 1003 的標(biāo)準(zhǔn)及標(biāo)準(zhǔn)草案,如 shell 和實(shí)用程序 (1003.2,本教程使用 1003.1)
- 由于 1003.1 標(biāo)準(zhǔn)說(shuō)明了一個(gè)接口而不是一種實(shí)現(xiàn),所以并不區(qū)分系統(tǒng)調(diào)用和庫(kù)函數(shù),所有在標(biāo)準(zhǔn)中的例程都被稱(chēng)為函數(shù)
- POSIX.1 標(biāo)準(zhǔn)定義的必需的頭文件
2.2 UNIX 系統(tǒng)實(shí)現(xiàn)
2.2.1 4.4 BSD
- BSD (Berkeley Sofware Distibution) 是由加州大學(xué)伯克利分校的計(jì)算機(jī)系統(tǒng)研究組研究開(kāi)發(fā)和分發(fā)的,4.2BSD 于 1983 年問(wèn)世,4.3BSD 則于 1986 年發(fā)布,4.4BSD 于 1994 年發(fā)布
2.2.2 FreeBSD
- FreeBSD 基于 4.4BSD-Lite 操作系統(tǒng)。在加州大學(xué)伯克分校的計(jì)算機(jī)系統(tǒng)研究組決定終止其在 UNIX 操作系統(tǒng)的 BSD 版本的研發(fā)工作,而且 386BSD 項(xiàng)目被忽視很長(zhǎng)時(shí)間之后,為了繼續(xù)堅(jiān)持 BSD 系列,形成了 FreeBSD 項(xiàng)目
2.2.3 Linux
- Linux 是由 Linus Torvalds 在 1991 年為替代 MNIX 而研發(fā)的
- Linux 是一種提供類(lèi)似于UNIX 的豐富編程環(huán)境的操作系統(tǒng),在 GNU 公用許可證的指導(dǎo)下 Linux 是免費(fèi)使用的
2.2.4 Mac OS X
- 與其以前的版本相比,Mac OS X 使用了完全不同的技術(shù)。其核心操作系統(tǒng)稱(chēng)為 “Darwin”,基于 Mach 內(nèi)核、FreeBSD 操作系統(tǒng)以及具有面向?qū)ο罂蚣艿尿?qū)動(dòng)和其他內(nèi)核擴(kuò)展的結(jié)合
2.2.5 Solaris
- Solaris 是由 Sun Microsystems (現(xiàn)為 Oracle) 開(kāi)發(fā)的 UNIX 系統(tǒng)版本
2.3 基本系統(tǒng)數(shù)據(jù)類(lèi)型
-
頭文件 <sys/types.h> 中定義了某些與實(shí)現(xiàn)有關(guān)的數(shù)據(jù)類(lèi)型,它們被稱(chēng)為基本系統(tǒng)數(shù)據(jù)類(lèi)型
-
一些常用的基本系統(tǒng)數(shù)據(jù)類(lèi)型
3. 文件 I/O
3.1 引言
- 可用的文件 I/O 函數(shù):打開(kāi)(open)文件、讀(read)文件、寫(xiě)(write)文件等
- UNIX 系統(tǒng)中的大多數(shù)文件 I/O 只需用到 5 個(gè)函數(shù):open、read、write、lseek 以及close
本章描述的函數(shù)經(jīng)常被稱(chēng)為不帶緩沖的 I/O (unbuffered I/O,與標(biāo)準(zhǔn) I/O 函數(shù)相對(duì)照)
- 不帶緩沖指的是每個(gè) read 和 write 都調(diào)用內(nèi)核中的一個(gè)系統(tǒng)調(diào)用
- 這些不帶緩沖的 I/O 函數(shù)不是 ISO C 的組成部分,但它們是 POSIX1 的組成部分
3.2 文件描述符
-
對(duì)內(nèi)核而言,所有打開(kāi)的文件都通過(guò)文件描述符引用
- 文件描述符是一個(gè)非負(fù)整數(shù)
- 當(dāng)打開(kāi)一個(gè)現(xiàn)有文件或創(chuàng)建一個(gè)新文件時(shí),內(nèi)核向進(jìn)程返回一個(gè)文件描述符
- 當(dāng)讀、寫(xiě)一個(gè)文件時(shí)使用 open 或 creat 返回的文件描述符標(biāo)識(shí)該文件,將其作為參數(shù)傳送給 read 或 write
-
按照慣例,UNIX 系統(tǒng) shell 把
- 文件描述符 0 與進(jìn)程的標(biāo)準(zhǔn)輸入關(guān)聯(lián)
- 文件描述符 1 與進(jìn)程的標(biāo)準(zhǔn)輸出關(guān)聯(lián)
- 文件描述符 2 與進(jìn)程的標(biāo)準(zhǔn)錯(cuò)誤關(guān)聯(lián)
-
在符合 POSIX.1 的應(yīng)用程序中,幻數(shù) 0、1、2 雖然已被標(biāo)準(zhǔn)化,但應(yīng)當(dāng)把它們替換成符號(hào)常量 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO 以提高可讀性。這些常量都在頭文件 <unistd.h> 中定義
文件描述符是指向一個(gè)文件結(jié)構(gòu)體的指針
PCB 進(jìn)程控制塊:本質(zhì)是結(jié)構(gòu)體,成員是文件描述符表
3.3 函數(shù) open 和 openat(打開(kāi)或創(chuàng)建一個(gè)文件)
3.3.1 函數(shù) open 和 openat 參數(shù)解析
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h> // 定義 flags 參數(shù)int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode); // 僅當(dāng)創(chuàng)建新文件時(shí)才使用第三個(gè)參數(shù),表明文件權(quán)限int openat(int dirfd, const char *pathname, int flags);
int openat(int dirfd, const char *pathname, int flags, mode_t mode);
- pathname:要打開(kāi)或創(chuàng)建文件的路徑名
- flags:用來(lái)說(shuō)明此函數(shù)的多個(gè)選項(xiàng),用以下一個(gè)或多個(gè)常量進(jìn)行 “或” 運(yùn)算構(gòu)成 flags 參數(shù)
- O_RDONLY(只讀打開(kāi))、O_WRONLY(只寫(xiě)打開(kāi))、O_RDWR(讀、寫(xiě)打開(kāi))、O_EXEC(只執(zhí)行打開(kāi))、O_SEARCH(只搜索打開(kāi),用于目錄)
- O_APPEND(每次寫(xiě)時(shí)都追加到文件末尾)
- O_CREAT(若此文件不存在則創(chuàng)建它,與第三個(gè)參數(shù) mode 同時(shí)使用)
- O_EXCL(如果同時(shí)指定了 O_CREAT,而文件已經(jīng)存在,則出錯(cuò))
- O_NONBLOCK(為文件的本次打開(kāi)操作和后續(xù)的 I/O 操作設(shè)置非阻塞方式)
- O_TRUNC(如果此文件存在,而且為只寫(xiě)或讀-寫(xiě)成功打開(kāi),則將其長(zhǎng)度截?cái)酁?0)
- 函數(shù)返回值
- 若成功,返回文件描述符
- 若出錯(cuò),返回 -1
- dirfd 參數(shù)把 open 和 openat 函數(shù)區(qū)分開(kāi),共有 3 種可能性
- path 參數(shù)指定的是絕對(duì)路徑名,在這種情況下,dirfd 參數(shù)被忽略,openat 函數(shù)就相當(dāng)于 open 函數(shù)
- path 參數(shù)指定的是相對(duì)路徑名,dirfd 參數(shù)指出了相對(duì)路徑名在文件系統(tǒng)中的開(kāi)始地址,dirfd 參數(shù)是通過(guò)打開(kāi)相對(duì)路徑名所在的目錄來(lái)獲取
- path 參數(shù)指定了相對(duì)路徑名,dirfd 參數(shù)具有特殊值 AT_FDCWD。在這種情況下,路徑名在當(dāng)前工作目錄中獲取,openat 函數(shù)在操作上與 open 函數(shù)類(lèi)似
- openat 函數(shù)是 POSIX.1 最新版本中新增的一類(lèi)函數(shù)之一,希望解決兩個(gè)問(wèn)題
- 第一,讓線(xiàn)程可以使用相對(duì)路徑名打開(kāi)目錄中的文件,而不再只能打開(kāi)當(dāng)前工作目錄
- 同一進(jìn)程中的所有線(xiàn)程共享相同的當(dāng)前工作目錄,因此很難讓同一進(jìn)程的多個(gè)不同線(xiàn)程在同一時(shí)間工作在不同的目錄中
- 第二,可以避免 time-of-check-to-time-of-use (TOCTTOU) 錯(cuò)誤
- TOCTTOU 錯(cuò)誤的基本思想是:如果有兩個(gè)基于文件的函數(shù)調(diào)用,其中第二個(gè)調(diào)用依賴(lài)于第一個(gè)調(diào)用的結(jié)果,那么程序是脆弱的。因?yàn)閮蓚€(gè)調(diào)用并不是原子操作,在兩個(gè)函數(shù)調(diào)用之間文件可能改變了,這樣也就造成了第一個(gè)調(diào)用的結(jié)果就不再有效,使得程序最終的結(jié)果是錯(cuò)誤的
- 第一,讓線(xiàn)程可以使用相對(duì)路徑名打開(kāi)目錄中的文件,而不再只能打開(kāi)當(dāng)前工作目錄
3.3.2 文件名和路徑名截?cái)?/h4>
- 在 POSIX.1 中常量 _POSIX_NO_TRUNC 決定是要截?cái)噙^(guò)長(zhǎng)的文件名或路徑名,還是返回一個(gè)出錯(cuò)。根據(jù)文件系統(tǒng)的類(lèi)型,此值可以變化??梢杂?fpathconf 或 pathconf 來(lái)查詢(xún)目錄具體支持何種行為,到底是截?cái)噙^(guò)長(zhǎng)的文件名還是返回出錯(cuò)
- 若 _POSIX_NO_TRUNC 有效,則在整個(gè)路徑名超過(guò) PATH_MAX,或路徑名中的任一文件名超過(guò) NAME_MAX 時(shí),出錯(cuò)返回,并將 errno 設(shè)置為 ENAMETOOLONG
3.4 函數(shù) close(關(guān)閉一個(gè)打開(kāi)文件)
#include <unistd.h>int close(int fd);
-
函數(shù)返回值
- 若成功,返回 0
- 若出錯(cuò),返回 -1
-
關(guān)閉一個(gè)文件時(shí)還會(huì)釋放該進(jìn)程加在該文件上的所有記錄鎖
-
當(dāng)一個(gè)進(jìn)程終止時(shí),內(nèi)核自動(dòng)關(guān)閉它所有的打開(kāi)文件。很多程序都利用了這一功能而不顯式地用 close 關(guān)閉打開(kāi)文件
3.5 函數(shù) creat(創(chuàng)建一個(gè)新文件)
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int creat(const char *pathname, mode_t mode);
-
函數(shù)返回值
- 若成功,返回為只寫(xiě)打開(kāi)的文件描述符
- 若出錯(cuò),返回 -1
-
此函數(shù)等效于
open(path, O_WRONLY | O_CREAT | O_TRUNC, mode)
creat 的一個(gè)不足之處是它以只寫(xiě)方式打開(kāi)所創(chuàng)建的文件。在提供 open 的新版本之前,如果要?jiǎng)?chuàng)建一個(gè)臨時(shí)文件,并要先寫(xiě)該文件,然后又讀該文件,則必須先調(diào)用 creat、close,然后再調(diào)用 open。現(xiàn)在則可用上述方式調(diào)用 open 實(shí)現(xiàn)
3.3-3.5 案例
案例 1
// open.c
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>int main(int argc, char *argv[]) {int fd;fd = open("./AUTHORS.txt", O_RDONLY);printf("fd = %d\n", fd);close(fd); return 0;
}
$ gcc open.c -o open$ ./open
# 輸出如下,表示文件存在并正確打開(kāi)
fd = 3
案例 2
// open2.c
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>int main(int argc, char *argv[]) {int fd;fd = open("./AUTHORS.cp", O_RDONLY | O_CREAT, 0644); // rw-r--r--printf("fd = %d\n", fd);close(fd);return 0;
}
$ gcc open2.c -o open2$ ./open2
fd = 3$ ll
# 創(chuàng)建了一個(gè)新文件 AUTHORS.cp,且文件權(quán)限對(duì)應(yīng)于 0644
-rw-r--r-- 1 yue yue 0 9月 10 22:19 AUTHORS.cp
案例 3
// open3.c
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>int main(int argc, char *argv[]) {int fd;// 如果文件存在,以只讀方式打開(kāi)并且截?cái)酁?0// 如果文件不存在,則把這個(gè)文件創(chuàng)建出來(lái)并指定權(quán)限為 0644fd = open("./AUTHORS.cp", O_RDONLY | O_CREAT | O_TRUNC, 0644); // rw-r--r--printf("fd = %d\n", fd);close(fd);return 0;
}
$ gcc open3.c -o open3$ ./open3
# 輸出如下,表示文件存在并正確打開(kāi)
fd = 3$ ll
# 首先在 AUTHORS.cp 文件中輸入內(nèi)容,然后經(jīng)過(guò) O_TRUNC 截?cái)嗪鬄?0
-rw-r--r-- 1 yue yue 0 9月 10 22:19 AUTHORS.cp
案例 4
- 創(chuàng)建文件時(shí),指定文件訪問(wèn)權(quán)限 mode,權(quán)限同時(shí)受 umask 影響。結(jié)論為
- 文件權(quán)限 = mode & ~umask
$ umask
0002 # 表明默認(rèn)創(chuàng)建文件權(quán)限為 ~umask = 775(第一個(gè) 0 表示八進(jìn)制)
// open4.c
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>int main(int argc, char *argv[]) {int fd;fd = open("./AUTHORS.cp2", O_RDONLY | O_CREAT | O_TRUNC, 0777); // rwxrwxrwxprintf("fd = %d\n", fd);close(fd);return 0;
}
$ gcc open4.c -o open4$ ./open4
fd = 3$ ll
# 創(chuàng)建了一個(gè)新文件 AUTHORS.cp2,且文件權(quán)限為 mode & ~umask = 775(rwxrwxr-x)
-rwxrwxr-x 1 yue yue 0 9月 10 22:38 AUTHORS.cp2*
案例 5
- open 函數(shù)常見(jiàn)錯(cuò)誤
- 打開(kāi)文件不存在
// open5.c
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>int main(int argc, char *argv[]) {int fd;fd = open("./AUTHORS.cp4", O_RDONLY);printf("fd = %d, errno = %d : %s\n", fd, errno, strerror(errno));close(fd);return 0;
}
$ gcc open5.c -o open5$ ./open5
fd = -1, errno = 2 : No such file or directory
- 以寫(xiě)方式打開(kāi)只讀文件(打開(kāi)文件沒(méi)有對(duì)應(yīng)權(quán)限)
// open6.c
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>int main(int argc, char *argv[]) {int fd;fd = open("./AUTHORS.cp3", O_WRONLY); // AUTHORS.cp3 文件權(quán)限為只讀printf("fd = %d, errno = %d : %s\n", fd, errno, strerror(errno));close(fd);return 0;
}
$ gcc open6.c -o open6$ ./open6
fd = -1, errno = 13 : Permission denied
- 以只寫(xiě)方式打開(kāi)目錄
$ mkdir mydir # 首先創(chuàng)建一個(gè)目錄
// open7.c
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>int main(int argc, char *argv[]) {int fd;fd = open("mydir", O_WRONLY);printf("fd = %d, errno = %d : %s\n", fd, errno, strerror(errno));close(fd);return 0;
}
$ gcc open7.c -o open7$ ./open7
fd = -1, errno = 21 : Is a directory
3.6 函數(shù) lseek(顯式的為一個(gè)打開(kāi)文件設(shè)置偏移量)
#include <sys/types.h>
#include <unistd.h>off_t lseek(int fd, off_t offset, int whence);
-
每個(gè)打開(kāi)文件都有一個(gè)與其相關(guān)聯(lián)的 “當(dāng)前文件偏移量”,通常是一個(gè)非負(fù)數(shù),用以度量從文件開(kāi)始處計(jì)算的字節(jié)數(shù)
-
lseek 中的 l 表示長(zhǎng)整型
-
函數(shù)返回值
- 若成功,返回新的文件偏移量
- 若出錯(cuò),返回 -1
-
按系統(tǒng)默認(rèn)的情況,當(dāng)打開(kāi)一個(gè)文件時(shí),除非指定 O_APPEND 選項(xiàng),否則該偏移量被設(shè)置為 0
-
對(duì)參數(shù) offset 的解釋與參數(shù) whence 的值有關(guān)
- 若 whence 是 SEEK_SET,則將該文件的偏移量設(shè)置為距文件開(kāi)始處 offset 個(gè)字節(jié)
- SEEK_SET(0) 絕對(duì)偏移量
- 若 whence 是 SEEK_CUR,則將該文件的偏移量設(shè)置為其當(dāng)前值加 offset,offset 可正可負(fù)
- SEEK_CUR(1) 相對(duì)于當(dāng)前位置的偏移量
- 若 whence 是 SEEK_END,則將該文件的偏移量設(shè)置為文件長(zhǎng)度加 offset,offset 可正可負(fù)
- SEEK_END(2) 相對(duì)文件尾端的偏移量
-
lseek 僅將當(dāng)前的文件偏移量記錄在內(nèi)核中,它并不引起任何 I/O 操作。然后,該偏移量用于下一個(gè)讀或?qū)懖僮?/p>
-
文件偏移量可以大于文件的當(dāng)前長(zhǎng)度,在這種情況下,對(duì)該文件的下一次寫(xiě)將加長(zhǎng)該文件,并在文件中構(gòu)成一個(gè)空洞,這一點(diǎn)是允許的。位于文件中但沒(méi)有寫(xiě)過(guò)的字節(jié)都被讀為 0
案例 1
- 文件的讀和寫(xiě)使用同一偏移位置
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>int main(void) {int fd, n;char msg[] = "It's a test for lseek\n";char ch;fd = open("lseek.txt", O_RDWR | O_CREAT, 0644);if (fd < 0) {perror("open lseek.txt error");exit(1);}// 使用 fd 對(duì)打開(kāi)的文件進(jìn)行寫(xiě)操作,讀寫(xiě)位置位于文件結(jié)尾處write(fd, msg, strlen(msg));// 若注釋下行代碼,由于文件寫(xiě)完之后未關(guān)閉,讀、寫(xiě)指針在文件末尾,所以不調(diào)節(jié)指針,直接讀取不到內(nèi)容lseek(fd, 0, SEEK_SET); // 修改文件讀寫(xiě)指針位置,位于文件開(kāi)頭while ((n = read(fd, &ch, 1))) {if (n < 0) {perror("read error");exit(1);} write(STDOUT_FILENO, &ch, n); // 將文件內(nèi)容按字節(jié)讀出,寫(xiě)出到屏幕}close(fd);return 0;
}
案例 2
- 使用 lseek 獲取文件大小
// lseek_size.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>int main(int argc, char *argv[]) {int fd = open(argv[1], O_RDWR);if (fd == -1) {perror("open error");exit(1);}int length = lseek(fd, 0, SEEK_END);printf("file size: %d\n", length);close(fd);return 0;
}
$ gcc lseek_size.c -o lseek_size
$ ./lseek_size fcntl.c # fcntl.c 文件大小為 678
678
案例 3
- 使用 lseek 擴(kuò)展文件大小
- 要想使文件大小真正擴(kuò)展,必須引起 IO 操作
// 修改案例 2 中下行代碼(擴(kuò)展 111 大小)
// 這樣并不能真正擴(kuò)展,使用 cat 命令查看文件大小未變化
int length = lseek(fd, 111, SEEK_END);// 在 printf 函數(shù)下行寫(xiě)如下代碼(引起 IO 操作)
write(fd, "\0", 1); // 結(jié)果便是在擴(kuò)展的文件尾部追加文件空洞
- 可使用 truncate 函數(shù)直接擴(kuò)展文件
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>int main(int argc, char*argv[]) {int ret = truncate("dict.cp", 250);printf("ret = %d\n", ret);return 0;
}
lseek 讀取的文件大小總是相對(duì)文件頭部而言。用 lseek 讀取文件大小實(shí)際用的是讀寫(xiě)指針初、末位置的偏移差,一個(gè)新開(kāi)文件,讀、寫(xiě)指針初位置都在文件開(kāi)頭。如果用這個(gè)來(lái)擴(kuò)展文件大小,必須引起 IO 才行,于是就至少要寫(xiě)入一個(gè)字符
3.7 函數(shù) read(從打開(kāi)文件中讀數(shù)據(jù))
#include <unistd.h>// ssize_t 表示帶符號(hào)整型;void* 表示通用指針
// 參數(shù)1:文件描述符;參數(shù)2:存數(shù)據(jù)的緩沖區(qū);參數(shù)3:緩沖區(qū)大小
ssize_t read(int fd, void *buf, size_t count);
- 函數(shù)返回值
- 若 read 成功,則返回讀到的字節(jié)數(shù),若已到文件尾,返回 0
- 若出錯(cuò),返回 -1
- 若返回 -1,并且 errno = EAGIN 或 EWOULDBLOCK,說(shuō)明不是 read 失敗,而是 read 在以非阻塞方式讀一個(gè)設(shè)備文件/網(wǎng)絡(luò)文件,并且文件無(wú)數(shù)據(jù)
- 有多種情況可使實(shí)際讀到的字節(jié)數(shù)少于要求讀的字節(jié)數(shù)
- 1、讀普通文件時(shí),在讀到要求字節(jié)數(shù)之前已到達(dá)了文件尾端
- 例如,若在到達(dá)文件尾端之前有 30 個(gè)字節(jié),而要求讀 100 個(gè)字節(jié),則 read 返 30。下一次再調(diào)用 read 時(shí),它將返回 0 (文件尾端)
- 2、當(dāng)從終端設(shè)備讀時(shí),通常一次最多讀一行
- 3、當(dāng)從網(wǎng)絡(luò)讀時(shí),網(wǎng)絡(luò)中的緩沖機(jī)制可能造成返回值小于所要求讀的字節(jié)數(shù)
- 4、當(dāng)從管道或 FIFO 讀時(shí),如若管道包含的字節(jié)少于所需的數(shù)量,那么 read 將只返回實(shí)際可用的字節(jié)數(shù)
- 5、當(dāng)從某些面向記錄的設(shè)備 (如磁帶) 讀時(shí),一次最多返回一個(gè)記錄
- 6、當(dāng)一信號(hào)造成中斷,而已經(jīng)讀了部分?jǐn)?shù)據(jù)量時(shí)
3.8 函數(shù) write(向打開(kāi)文件寫(xiě)數(shù)據(jù))
#include <unistd.h>// 參數(shù)1:文件描述符;參數(shù)2:待寫(xiě)出數(shù)據(jù)的緩沖區(qū);參數(shù)3:數(shù)據(jù)大小
ssize_t write(int fd, const void *buf, size_t count);
-
函數(shù)返回值
- 若 write 成功,則返回已寫(xiě)的字節(jié)數(shù)(返回值通常與參數(shù) count 值相同,否則表示出錯(cuò))
- 若出錯(cuò),返回 -1
-
write 出錯(cuò)的一個(gè)常見(jiàn)原因是:磁盤(pán)已寫(xiě)滿(mǎn),或者超過(guò)了一個(gè)給定進(jìn)程的文件長(zhǎng)度限制
-
對(duì)于普通文件,寫(xiě)操作從文件的當(dāng)前偏移量處開(kāi)始。如果在打開(kāi)該文件時(shí),指定了 O_APPEND 選項(xiàng),則在每次寫(xiě)操作之前,將文件偏移量設(shè)置在文件的當(dāng)前結(jié)尾處。在一次成功寫(xiě)之后,該文件偏移量增加實(shí)際寫(xiě)的字節(jié)數(shù)
阻塞和非阻塞
-
阻塞 (Block):當(dāng)進(jìn)程調(diào)用一個(gè)阻塞的系統(tǒng)函數(shù)時(shí),該進(jìn)程被置于睡眠 (Sleep) 狀態(tài),這時(shí)內(nèi)核調(diào)度其它進(jìn)程運(yùn)行,直到該進(jìn)程等待的事件發(fā)生了 (比如網(wǎng)絡(luò)上接收到數(shù)據(jù)包,或者調(diào)用 sleep 指定的睡眠時(shí)間到了) 它才有可能繼續(xù)運(yùn)行。與睡眠狀態(tài)相對(duì)的是運(yùn)行 (Running) 狀態(tài),在 Linux 內(nèi)核中,處于運(yùn)行狀態(tài)的進(jìn)程分為兩種情況
- 正在被調(diào)度執(zhí)行。CPU 處于該進(jìn)程的上下文環(huán)境中,程序計(jì)數(shù)器里保存著該進(jìn)程的指令地址,通用寄存器里保存著該進(jìn)程運(yùn)算過(guò)程的中間結(jié)果,正在執(zhí)行該進(jìn)程的指令,正在讀寫(xiě)該進(jìn)程的地址空間
- 就緒狀態(tài)。該進(jìn)程不需要等待什么事件發(fā)生,隨時(shí)都可以執(zhí)行,但 CPU 暫時(shí)還在執(zhí)行另一個(gè)進(jìn)程,所以該進(jìn)程在一個(gè)就緒隊(duì)列中等待被內(nèi)核調(diào)度
-
讀常規(guī)文件是不會(huì)阻塞的,不管讀多少字節(jié),read 一定會(huì)在有限的時(shí)間內(nèi)返回。從終端設(shè)備或網(wǎng)絡(luò)讀則不一定,如果從終端輸入的數(shù)據(jù)沒(méi)有換行符,調(diào)用 read 讀終端設(shè)備就會(huì)阻塞,如果網(wǎng)絡(luò)上沒(méi)有接收到數(shù)據(jù)包,調(diào)用 read 從網(wǎng)絡(luò)讀就會(huì)阻塞,至于會(huì)阻塞多長(zhǎng)時(shí)間也是不確定的,如果一直沒(méi)有數(shù)據(jù)到達(dá)就一直阻塞在那里。同樣,寫(xiě)常規(guī)文件是不會(huì)阻塞的,而向終端設(shè)備或網(wǎng)絡(luò)寫(xiě)則不一定
- /dev/tty – 終端文件
阻塞讀終端
// block_readtty.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>int main(void) {char buf[10];int n;n = read(STDIN_FILENO, buf, 10);if (n < 0){perror("read STDIN_FILENO");exit(1);}write(STDOUT_FILENO, buf, n);return 0;
}
$ gcc block_readtty.c -o block
$ ./block # 此時(shí)程序在阻塞等待輸入,下面輸入 hello 后回車(chē)即結(jié)束
hello
hello
非阻塞讀終端
// nonblock_readtty.c
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "time out\n"int main(void) {char buf[10];int fd, n, i;// 設(shè)置 /dev/tty 非阻塞狀態(tài)(默認(rèn)為阻塞狀態(tài))fd = open("/dev/tty", O_RDONLY | O_NONBLOCK); if(fd < 0) {perror("open /dev/tty");exit(1);}printf("open /dev/tty ok... %d\n", fd);for (i = 0; i < 5; i++) {n = read(fd, buf, 10);if (n > 0) { // 說(shuō)明讀到了東西break;}if (errno != EAGAIN) { perror("read /dev/tty");exit(1);} else {write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));sleep(2);}}if (i == 5) {write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));} else {write(STDOUT_FILENO, buf, n);}close(fd);return 0;
}
$ gcc block_readtty.c -o block
$ ./block # 此時(shí)程序在阻塞等待輸入,下面輸入 hello 后回車(chē)即結(jié)束
hello
hello
3.9 I/O 的效率
- 使用 read/write 函數(shù)實(shí)現(xiàn)文件拷貝
// 將一個(gè)文件的內(nèi)容復(fù)制到另一個(gè)文件中:通過(guò)打開(kāi)兩個(gè)文件,循環(huán)讀取第一個(gè)文件的內(nèi)容并寫(xiě)入到第二個(gè)文件中
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>int main(int argc, char* argv[]) {char buf[1]; // 定義一個(gè)大小為 1 的字符數(shù)組,用于存儲(chǔ)讀取或?qū)懭氲臄?shù)據(jù)int n = 0;// 打開(kāi)第一個(gè)參數(shù)所表示的文件,以只讀方式打開(kāi)int fd1 = open(argv[1], O_RDONLY);if (fd1 == -1) {perror("open argv1 error");exit(1);}// 打開(kāi)第二個(gè)參數(shù)所表示的文件,以可讀寫(xiě)方式打開(kāi),如果文件不存在則創(chuàng)建,如果文件存在則將其清空int fd2 = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0664);if (fd2 == -1) {perror("open argv2 error");exit(1);}// 循環(huán)讀取第一個(gè)文件的內(nèi)容,每次最多讀取 1024 字節(jié)// 將返回的實(shí)際讀取字節(jié)數(shù)賦值給變量 nwhile ((n = read(fd1, buf, 1024)) != 0) {if (n < 0) {perror("read error");break;}// 將存儲(chǔ)在 buf 數(shù)組中的數(shù)據(jù)寫(xiě)入文件描述符為 fd2 的文件write(fd2, buf, n);}close(fd1);close(fd2);return 0;
}
- 使用 fputc/fgetc 函數(shù)實(shí)現(xiàn)文件拷貝
// 使用了 C 標(biāo)準(zhǔn)庫(kù)中的文件操作函數(shù) fopen()、fgetc() 和 fputc() 來(lái)實(shí)現(xiàn)文件的讀取和寫(xiě)入
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>int main(int argc, char* argv[]) {FILE *fp, *fp_out;int n = 0;fp = fopen("hello.c", "r");if (fp == NULL) {perror("fopen error");exit(1);}fp_out = fopen("hello.cp", "w");if (fp_out == NULL) {perror("fopen error");exit(1);}// 判斷是否讀取到文件結(jié)束符 EOFwhile ((n = fgetc(fp)) != EOF) {fputc(n, fp_out); // 將讀取的字符寫(xiě)入輸出文件}fclose(fp);fclose(fp_out);return 0;
}
- read/write:每次寫(xiě)一個(gè)字節(jié),會(huì)不斷的進(jìn)行內(nèi)核態(tài)和用戶(hù)態(tài)的切換,所以非常耗時(shí)
- fgetc/fputc:有個(gè) 4096 緩沖區(qū),所以不是一個(gè)字節(jié)一個(gè)字節(jié)地寫(xiě),內(nèi)核和用戶(hù)切換就比較少(預(yù)讀入緩輸出機(jī)制)
系統(tǒng)函數(shù)并不一定比庫(kù)函數(shù)快,能使用庫(kù)函數(shù)的地方就使用庫(kù)函數(shù)
標(biāo)準(zhǔn) I/O 函數(shù)自帶用戶(hù)緩沖區(qū),系統(tǒng)調(diào)用無(wú)用戶(hù)級(jí)緩沖,系統(tǒng)緩沖區(qū)是都有的
- Linux 上用不同緩沖長(zhǎng)度進(jìn)行讀操作的時(shí)間結(jié)果
- 大多數(shù)文件系統(tǒng)為改善性能都采用某種預(yù)讀入 (read ahead) 緩輸出技術(shù)。當(dāng)檢測(cè)到正進(jìn)行順序讀取時(shí),系統(tǒng)就試圖讀入比應(yīng)用所要求的更多數(shù)據(jù),并假想應(yīng)用很快就會(huì)讀這些數(shù)據(jù)。預(yù)讀的效果可以從下圖看出:緩沖區(qū)長(zhǎng)度小至 32 字節(jié)時(shí)的時(shí)鐘時(shí)間與擁有較大緩沖區(qū)長(zhǎng)度時(shí)的時(shí)鐘時(shí)間幾乎一樣
#include <unistd.h>int close(int fd);
函數(shù)返回值
- 若成功,返回 0
- 若出錯(cuò),返回 -1
關(guān)閉一個(gè)文件時(shí)還會(huì)釋放該進(jìn)程加在該文件上的所有記錄鎖
當(dāng)一個(gè)進(jìn)程終止時(shí),內(nèi)核自動(dòng)關(guān)閉它所有的打開(kāi)文件。很多程序都利用了這一功能而不顯式地用 close 關(guān)閉打開(kāi)文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int creat(const char *pathname, mode_t mode);
函數(shù)返回值
- 若成功,返回為只寫(xiě)打開(kāi)的文件描述符
- 若出錯(cuò),返回 -1
此函數(shù)等效于
open(path, O_WRONLY | O_CREAT | O_TRUNC, mode)
creat 的一個(gè)不足之處是它以只寫(xiě)方式打開(kāi)所創(chuàng)建的文件。在提供 open 的新版本之前,如果要?jiǎng)?chuàng)建一個(gè)臨時(shí)文件,并要先寫(xiě)該文件,然后又讀該文件,則必須先調(diào)用 creat、close,然后再調(diào)用 open。現(xiàn)在則可用上述方式調(diào)用 open 實(shí)現(xiàn)
// open.c
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>int main(int argc, char *argv[]) {int fd;fd = open("./AUTHORS.txt", O_RDONLY);printf("fd = %d\n", fd);close(fd); return 0;
}
$ gcc open.c -o open$ ./open
# 輸出如下,表示文件存在并正確打開(kāi)
fd = 3
// open2.c
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>int main(int argc, char *argv[]) {int fd;fd = open("./AUTHORS.cp", O_RDONLY | O_CREAT, 0644); // rw-r--r--printf("fd = %d\n", fd);close(fd);return 0;
}
$ gcc open2.c -o open2$ ./open2
fd = 3$ ll
# 創(chuàng)建了一個(gè)新文件 AUTHORS.cp,且文件權(quán)限對(duì)應(yīng)于 0644
-rw-r--r-- 1 yue yue 0 9月 10 22:19 AUTHORS.cp
// open3.c
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>int main(int argc, char *argv[]) {int fd;// 如果文件存在,以只讀方式打開(kāi)并且截?cái)酁?0// 如果文件不存在,則把這個(gè)文件創(chuàng)建出來(lái)并指定權(quán)限為 0644fd = open("./AUTHORS.cp", O_RDONLY | O_CREAT | O_TRUNC, 0644); // rw-r--r--printf("fd = %d\n", fd);close(fd);return 0;
}
$ gcc open3.c -o open3$ ./open3
# 輸出如下,表示文件存在并正確打開(kāi)
fd = 3$ ll
# 首先在 AUTHORS.cp 文件中輸入內(nèi)容,然后經(jīng)過(guò) O_TRUNC 截?cái)嗪鬄?0
-rw-r--r-- 1 yue yue 0 9月 10 22:19 AUTHORS.cp
- 文件權(quán)限 = mode & ~umask
$ umask
0002 # 表明默認(rèn)創(chuàng)建文件權(quán)限為 ~umask = 775(第一個(gè) 0 表示八進(jìn)制)
// open4.c
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>int main(int argc, char *argv[]) {int fd;fd = open("./AUTHORS.cp2", O_RDONLY | O_CREAT | O_TRUNC, 0777); // rwxrwxrwxprintf("fd = %d\n", fd);close(fd);return 0;
}
$ gcc open4.c -o open4$ ./open4
fd = 3$ ll
# 創(chuàng)建了一個(gè)新文件 AUTHORS.cp2,且文件權(quán)限為 mode & ~umask = 775(rwxrwxr-x)
-rwxrwxr-x 1 yue yue 0 9月 10 22:38 AUTHORS.cp2*
- 打開(kāi)文件不存在
// open5.c
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>int main(int argc, char *argv[]) {int fd;fd = open("./AUTHORS.cp4", O_RDONLY);printf("fd = %d, errno = %d : %s\n", fd, errno, strerror(errno));close(fd);return 0;
}
$ gcc open5.c -o open5$ ./open5
fd = -1, errno = 2 : No such file or directory
- 以寫(xiě)方式打開(kāi)只讀文件(打開(kāi)文件沒(méi)有對(duì)應(yīng)權(quán)限)
// open6.c
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>int main(int argc, char *argv[]) {int fd;fd = open("./AUTHORS.cp3", O_WRONLY); // AUTHORS.cp3 文件權(quán)限為只讀printf("fd = %d, errno = %d : %s\n", fd, errno, strerror(errno));close(fd);return 0;
}
$ gcc open6.c -o open6$ ./open6
fd = -1, errno = 13 : Permission denied
- 以只寫(xiě)方式打開(kāi)目錄
$ mkdir mydir # 首先創(chuàng)建一個(gè)目錄
// open7.c
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>int main(int argc, char *argv[]) {int fd;fd = open("mydir", O_WRONLY);printf("fd = %d, errno = %d : %s\n", fd, errno, strerror(errno));close(fd);return 0;
}
$ gcc open7.c -o open7$ ./open7
fd = -1, errno = 21 : Is a directory
#include <sys/types.h>
#include <unistd.h>off_t lseek(int fd, off_t offset, int whence);
每個(gè)打開(kāi)文件都有一個(gè)與其相關(guān)聯(lián)的 “當(dāng)前文件偏移量”,通常是一個(gè)非負(fù)數(shù),用以度量從文件開(kāi)始處計(jì)算的字節(jié)數(shù)
lseek 中的 l 表示長(zhǎng)整型
函數(shù)返回值
- 若成功,返回新的文件偏移量
- 若出錯(cuò),返回 -1
按系統(tǒng)默認(rèn)的情況,當(dāng)打開(kāi)一個(gè)文件時(shí),除非指定 O_APPEND 選項(xiàng),否則該偏移量被設(shè)置為 0
對(duì)參數(shù) offset 的解釋與參數(shù) whence 的值有關(guān)
- 若 whence 是 SEEK_SET,則將該文件的偏移量設(shè)置為距文件開(kāi)始處 offset 個(gè)字節(jié)
- SEEK_SET(0) 絕對(duì)偏移量
- 若 whence 是 SEEK_CUR,則將該文件的偏移量設(shè)置為其當(dāng)前值加 offset,offset 可正可負(fù)
- SEEK_CUR(1) 相對(duì)于當(dāng)前位置的偏移量
- 若 whence 是 SEEK_END,則將該文件的偏移量設(shè)置為文件長(zhǎng)度加 offset,offset 可正可負(fù)
- SEEK_END(2) 相對(duì)文件尾端的偏移量
lseek 僅將當(dāng)前的文件偏移量記錄在內(nèi)核中,它并不引起任何 I/O 操作。然后,該偏移量用于下一個(gè)讀或?qū)懖僮?/p>
文件偏移量可以大于文件的當(dāng)前長(zhǎng)度,在這種情況下,對(duì)該文件的下一次寫(xiě)將加長(zhǎng)該文件,并在文件中構(gòu)成一個(gè)空洞,這一點(diǎn)是允許的。位于文件中但沒(méi)有寫(xiě)過(guò)的字節(jié)都被讀為 0
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>int main(void) {int fd, n;char msg[] = "It's a test for lseek\n";char ch;fd = open("lseek.txt", O_RDWR | O_CREAT, 0644);if (fd < 0) {perror("open lseek.txt error");exit(1);}// 使用 fd 對(duì)打開(kāi)的文件進(jìn)行寫(xiě)操作,讀寫(xiě)位置位于文件結(jié)尾處write(fd, msg, strlen(msg));// 若注釋下行代碼,由于文件寫(xiě)完之后未關(guān)閉,讀、寫(xiě)指針在文件末尾,所以不調(diào)節(jié)指針,直接讀取不到內(nèi)容lseek(fd, 0, SEEK_SET); // 修改文件讀寫(xiě)指針位置,位于文件開(kāi)頭while ((n = read(fd, &ch, 1))) {if (n < 0) {perror("read error");exit(1);} write(STDOUT_FILENO, &ch, n); // 將文件內(nèi)容按字節(jié)讀出,寫(xiě)出到屏幕}close(fd);return 0;
}
// lseek_size.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>int main(int argc, char *argv[]) {int fd = open(argv[1], O_RDWR);if (fd == -1) {perror("open error");exit(1);}int length = lseek(fd, 0, SEEK_END);printf("file size: %d\n", length);close(fd);return 0;
}
$ gcc lseek_size.c -o lseek_size
$ ./lseek_size fcntl.c # fcntl.c 文件大小為 678
678
- 要想使文件大小真正擴(kuò)展,必須引起 IO 操作
// 修改案例 2 中下行代碼(擴(kuò)展 111 大小)
// 這樣并不能真正擴(kuò)展,使用 cat 命令查看文件大小未變化
int length = lseek(fd, 111, SEEK_END);// 在 printf 函數(shù)下行寫(xiě)如下代碼(引起 IO 操作)
write(fd, "\0", 1); // 結(jié)果便是在擴(kuò)展的文件尾部追加文件空洞
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>int main(int argc, char*argv[]) {int ret = truncate("dict.cp", 250);printf("ret = %d\n", ret);return 0;
}
lseek 讀取的文件大小總是相對(duì)文件頭部而言。用 lseek 讀取文件大小實(shí)際用的是讀寫(xiě)指針初、末位置的偏移差,一個(gè)新開(kāi)文件,讀、寫(xiě)指針初位置都在文件開(kāi)頭。如果用這個(gè)來(lái)擴(kuò)展文件大小,必須引起 IO 才行,于是就至少要寫(xiě)入一個(gè)字符
#include <unistd.h>// ssize_t 表示帶符號(hào)整型;void* 表示通用指針
// 參數(shù)1:文件描述符;參數(shù)2:存數(shù)據(jù)的緩沖區(qū);參數(shù)3:緩沖區(qū)大小
ssize_t read(int fd, void *buf, size_t count);
- 若 read 成功,則返回讀到的字節(jié)數(shù),若已到文件尾,返回 0
- 若出錯(cuò),返回 -1
- 若返回 -1,并且 errno = EAGIN 或 EWOULDBLOCK,說(shuō)明不是 read 失敗,而是 read 在以非阻塞方式讀一個(gè)設(shè)備文件/網(wǎng)絡(luò)文件,并且文件無(wú)數(shù)據(jù)
- 1、讀普通文件時(shí),在讀到要求字節(jié)數(shù)之前已到達(dá)了文件尾端
- 例如,若在到達(dá)文件尾端之前有 30 個(gè)字節(jié),而要求讀 100 個(gè)字節(jié),則 read 返 30。下一次再調(diào)用 read 時(shí),它將返回 0 (文件尾端)
- 2、當(dāng)從終端設(shè)備讀時(shí),通常一次最多讀一行
- 3、當(dāng)從網(wǎng)絡(luò)讀時(shí),網(wǎng)絡(luò)中的緩沖機(jī)制可能造成返回值小于所要求讀的字節(jié)數(shù)
- 4、當(dāng)從管道或 FIFO 讀時(shí),如若管道包含的字節(jié)少于所需的數(shù)量,那么 read 將只返回實(shí)際可用的字節(jié)數(shù)
- 5、當(dāng)從某些面向記錄的設(shè)備 (如磁帶) 讀時(shí),一次最多返回一個(gè)記錄
- 6、當(dāng)一信號(hào)造成中斷,而已經(jīng)讀了部分?jǐn)?shù)據(jù)量時(shí)
#include <unistd.h>// 參數(shù)1:文件描述符;參數(shù)2:待寫(xiě)出數(shù)據(jù)的緩沖區(qū);參數(shù)3:數(shù)據(jù)大小
ssize_t write(int fd, const void *buf, size_t count);
函數(shù)返回值
- 若 write 成功,則返回已寫(xiě)的字節(jié)數(shù)(返回值通常與參數(shù) count 值相同,否則表示出錯(cuò))
- 若出錯(cuò),返回 -1
write 出錯(cuò)的一個(gè)常見(jiàn)原因是:磁盤(pán)已寫(xiě)滿(mǎn),或者超過(guò)了一個(gè)給定進(jìn)程的文件長(zhǎng)度限制
對(duì)于普通文件,寫(xiě)操作從文件的當(dāng)前偏移量處開(kāi)始。如果在打開(kāi)該文件時(shí),指定了 O_APPEND 選項(xiàng),則在每次寫(xiě)操作之前,將文件偏移量設(shè)置在文件的當(dāng)前結(jié)尾處。在一次成功寫(xiě)之后,該文件偏移量增加實(shí)際寫(xiě)的字節(jié)數(shù)
阻塞 (Block):當(dāng)進(jìn)程調(diào)用一個(gè)阻塞的系統(tǒng)函數(shù)時(shí),該進(jìn)程被置于睡眠 (Sleep) 狀態(tài),這時(shí)內(nèi)核調(diào)度其它進(jìn)程運(yùn)行,直到該進(jìn)程等待的事件發(fā)生了 (比如網(wǎng)絡(luò)上接收到數(shù)據(jù)包,或者調(diào)用 sleep 指定的睡眠時(shí)間到了) 它才有可能繼續(xù)運(yùn)行。與睡眠狀態(tài)相對(duì)的是運(yùn)行 (Running) 狀態(tài),在 Linux 內(nèi)核中,處于運(yùn)行狀態(tài)的進(jìn)程分為兩種情況
- 正在被調(diào)度執(zhí)行。CPU 處于該進(jìn)程的上下文環(huán)境中,程序計(jì)數(shù)器里保存著該進(jìn)程的指令地址,通用寄存器里保存著該進(jìn)程運(yùn)算過(guò)程的中間結(jié)果,正在執(zhí)行該進(jìn)程的指令,正在讀寫(xiě)該進(jìn)程的地址空間
- 就緒狀態(tài)。該進(jìn)程不需要等待什么事件發(fā)生,隨時(shí)都可以執(zhí)行,但 CPU 暫時(shí)還在執(zhí)行另一個(gè)進(jìn)程,所以該進(jìn)程在一個(gè)就緒隊(duì)列中等待被內(nèi)核調(diào)度
讀常規(guī)文件是不會(huì)阻塞的,不管讀多少字節(jié),read 一定會(huì)在有限的時(shí)間內(nèi)返回。從終端設(shè)備或網(wǎng)絡(luò)讀則不一定,如果從終端輸入的數(shù)據(jù)沒(méi)有換行符,調(diào)用 read 讀終端設(shè)備就會(huì)阻塞,如果網(wǎng)絡(luò)上沒(méi)有接收到數(shù)據(jù)包,調(diào)用 read 從網(wǎng)絡(luò)讀就會(huì)阻塞,至于會(huì)阻塞多長(zhǎng)時(shí)間也是不確定的,如果一直沒(méi)有數(shù)據(jù)到達(dá)就一直阻塞在那里。同樣,寫(xiě)常規(guī)文件是不會(huì)阻塞的,而向終端設(shè)備或網(wǎng)絡(luò)寫(xiě)則不一定
- /dev/tty – 終端文件
// block_readtty.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>int main(void) {char buf[10];int n;n = read(STDIN_FILENO, buf, 10);if (n < 0){perror("read STDIN_FILENO");exit(1);}write(STDOUT_FILENO, buf, n);return 0;
}
$ gcc block_readtty.c -o block
$ ./block # 此時(shí)程序在阻塞等待輸入,下面輸入 hello 后回車(chē)即結(jié)束
hello
hello
// nonblock_readtty.c
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "time out\n"int main(void) {char buf[10];int fd, n, i;// 設(shè)置 /dev/tty 非阻塞狀態(tài)(默認(rèn)為阻塞狀態(tài))fd = open("/dev/tty", O_RDONLY | O_NONBLOCK); if(fd < 0) {perror("open /dev/tty");exit(1);}printf("open /dev/tty ok... %d\n", fd);for (i = 0; i < 5; i++) {n = read(fd, buf, 10);if (n > 0) { // 說(shuō)明讀到了東西break;}if (errno != EAGAIN) { perror("read /dev/tty");exit(1);} else {write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));sleep(2);}}if (i == 5) {write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));} else {write(STDOUT_FILENO, buf, n);}close(fd);return 0;
}
$ gcc block_readtty.c -o block
$ ./block # 此時(shí)程序在阻塞等待輸入,下面輸入 hello 后回車(chē)即結(jié)束
hello
hello
// 將一個(gè)文件的內(nèi)容復(fù)制到另一個(gè)文件中:通過(guò)打開(kāi)兩個(gè)文件,循環(huán)讀取第一個(gè)文件的內(nèi)容并寫(xiě)入到第二個(gè)文件中
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>int main(int argc, char* argv[]) {char buf[1]; // 定義一個(gè)大小為 1 的字符數(shù)組,用于存儲(chǔ)讀取或?qū)懭氲臄?shù)據(jù)int n = 0;// 打開(kāi)第一個(gè)參數(shù)所表示的文件,以只讀方式打開(kāi)int fd1 = open(argv[1], O_RDONLY);if (fd1 == -1) {perror("open argv1 error");exit(1);}// 打開(kāi)第二個(gè)參數(shù)所表示的文件,以可讀寫(xiě)方式打開(kāi),如果文件不存在則創(chuàng)建,如果文件存在則將其清空int fd2 = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0664);if (fd2 == -1) {perror("open argv2 error");exit(1);}// 循環(huán)讀取第一個(gè)文件的內(nèi)容,每次最多讀取 1024 字節(jié)// 將返回的實(shí)際讀取字節(jié)數(shù)賦值給變量 nwhile ((n = read(fd1, buf, 1024)) != 0) {if (n < 0) {perror("read error");break;}// 將存儲(chǔ)在 buf 數(shù)組中的數(shù)據(jù)寫(xiě)入文件描述符為 fd2 的文件write(fd2, buf, n);}close(fd1);close(fd2);return 0;
}
// 使用了 C 標(biāo)準(zhǔn)庫(kù)中的文件操作函數(shù) fopen()、fgetc() 和 fputc() 來(lái)實(shí)現(xiàn)文件的讀取和寫(xiě)入
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>int main(int argc, char* argv[]) {FILE *fp, *fp_out;int n = 0;fp = fopen("hello.c", "r");if (fp == NULL) {perror("fopen error");exit(1);}fp_out = fopen("hello.cp", "w");if (fp_out == NULL) {perror("fopen error");exit(1);}// 判斷是否讀取到文件結(jié)束符 EOFwhile ((n = fgetc(fp)) != EOF) {fputc(n, fp_out); // 將讀取的字符寫(xiě)入輸出文件}fclose(fp);fclose(fp_out);return 0;
}
系統(tǒng)函數(shù)并不一定比庫(kù)函數(shù)快,能使用庫(kù)函數(shù)的地方就使用庫(kù)函數(shù)
標(biāo)準(zhǔn) I/O 函數(shù)自帶用戶(hù)緩沖區(qū),系統(tǒng)調(diào)用無(wú)用戶(hù)級(jí)緩沖,系統(tǒng)緩沖區(qū)是都有的
- 大多數(shù)文件系統(tǒng)為改善性能都采用某種預(yù)讀入 (read ahead) 緩輸出技術(shù)。當(dāng)檢測(cè)到正進(jìn)行順序讀取時(shí),系統(tǒng)就試圖讀入比應(yīng)用所要求的更多數(shù)據(jù),并假想應(yīng)用很快就會(huì)讀這些數(shù)據(jù)。預(yù)讀的效果可以從下圖看出:緩沖區(qū)長(zhǎng)度小至 32 字節(jié)時(shí)的時(shí)鐘時(shí)間與擁有較大緩沖區(qū)長(zhǎng)度時(shí)的時(shí)鐘時(shí)間幾乎一樣
3.10 文件共享
-
UNIX 系統(tǒng)支持在不同進(jìn)程間共享打開(kāi)文件
-
內(nèi)核使用 3 種數(shù)據(jù)結(jié)構(gòu)表示打開(kāi)文件,它們之間的關(guān)系決定了在文件共享方面一個(gè)進(jìn)程對(duì)另一個(gè)進(jìn)程可能產(chǎn)生的影響
- (1) 每個(gè)進(jìn)程在進(jìn)程表中都有一個(gè)記錄項(xiàng),記錄項(xiàng)中包含一張打開(kāi)文件描述符表,可將其視為一個(gè)矢量,每個(gè)描述符占用一項(xiàng)。與每個(gè)文件描述符相關(guān)聯(lián)的是:
- 文件描述符標(biāo)志
- 指向一個(gè)文件表項(xiàng)的指針
- (2) 內(nèi)核為所有打開(kāi)文件維持一張文件表。每個(gè)文件表項(xiàng)包含
- 文件狀態(tài)標(biāo)志 (讀、寫(xiě)、添寫(xiě)、同步和非阻塞等)
- 當(dāng)前文件偏移量
- 指向該文件 v 節(jié)點(diǎn)表項(xiàng)的指針
- (3) 每個(gè)打開(kāi)文件 (或設(shè)備) 都有一個(gè) v 節(jié)點(diǎn) (v-node) 結(jié)構(gòu)。v 節(jié)點(diǎn)包含了文件類(lèi)型和對(duì)此文件進(jìn)行各種操作函數(shù)的指針。對(duì)于大多數(shù)文件,v 節(jié)點(diǎn)還包含了該文件的 i 節(jié)點(diǎn) (i-node,索引節(jié)點(diǎn))。這些信息是在打開(kāi)文件時(shí)從磁盤(pán)上讀入內(nèi)存的,所以,文件的所有相關(guān)信息都是隨時(shí)可用的
- (1) 每個(gè)進(jìn)程在進(jìn)程表中都有一個(gè)記錄項(xiàng),記錄項(xiàng)中包含一張打開(kāi)文件描述符表,可將其視為一個(gè)矢量,每個(gè)描述符占用一項(xiàng)。與每個(gè)文件描述符相關(guān)聯(lián)的是:
-
打開(kāi)文件的內(nèi)核數(shù)據(jù)結(jié)構(gòu)
文件描述符標(biāo)志和文件狀態(tài)標(biāo)志在作用范圍方面的區(qū)別:前者只用于一個(gè)進(jìn)程的一個(gè)描述符,而后者則應(yīng)用于指向該給定文件表項(xiàng)的任何進(jìn)程中的所有描述符
3.11 原子操作
一般而言,原子操作 (atomic operation) 指的是由多步組成的一個(gè)操作。如果該操作原子地執(zhí)行,則要么執(zhí)行完所有步驟,要么一步也不執(zhí)行,不可能只執(zhí)行所有步驟的一個(gè)子集
3.11.1 追加到一個(gè)文件
-
考慮一個(gè)進(jìn)程,它要將數(shù)據(jù)追加到一個(gè)文件尾端
- 對(duì)單個(gè)進(jìn)程,這段程序能正常工作,但若有多個(gè)進(jìn)程同時(shí)使用這種方法將數(shù)據(jù)追加寫(xiě)到同一文件,則會(huì)產(chǎn)生問(wèn)題
if(lseek(fd, OL, 2) < 0)err_sys("lseek error"); if(write(fd, buf, 100) != 100)err_sys("write error");
-
假定有兩個(gè)獨(dú)立的進(jìn)程 A 和 B 都對(duì)同一文件進(jìn)行追加寫(xiě)操作,每個(gè)進(jìn)程都已打開(kāi)該文件但未使用 O_APPEND 標(biāo)志
- 此時(shí),每個(gè)進(jìn)程都有它自己的文件表項(xiàng),但是共享一個(gè) v 節(jié)點(diǎn)表項(xiàng)
- 假定進(jìn)程 A 調(diào)用了 lseek,它將進(jìn)程 A 的該文件當(dāng)前偏移量設(shè)置為 1500 字節(jié) (當(dāng)前文件尾端處)
- 然后內(nèi)核切換進(jìn)程,進(jìn)程 B 執(zhí)行 lseek,也將其對(duì)該文件的當(dāng)前偏移量設(shè)置為 1500 字節(jié) (當(dāng)前文件尾端處)
- 然后 B 調(diào)用 write,它將 B 的該文件當(dāng)前文件偏移量增加至 1600。因?yàn)樵撐募拈L(zhǎng)度已經(jīng)增加了,所以?xún)?nèi)核將 v 節(jié)點(diǎn)中的當(dāng)前文件長(zhǎng)度更新為 1600
- 然后,內(nèi)核又進(jìn)行進(jìn)程切換,使進(jìn)程 A 恢復(fù)運(yùn)行。當(dāng) A 用 write 時(shí)就從其當(dāng)前文件偏移量 (1500) 處開(kāi)始將數(shù)據(jù)寫(xiě)入到文件,這樣也就覆蓋了進(jìn)程 B 剛才寫(xiě)入到該文件中的數(shù)據(jù)
問(wèn)題出在邏輯操作 “先定位到文件尾端,然后寫(xiě)”,它使用了兩個(gè)分開(kāi)的函數(shù)調(diào)用
- 解決方法:使這兩個(gè)操作對(duì)于其他進(jìn)程而言成為一個(gè)原子操作。任何要求多于一個(gè)函數(shù)調(diào)用的操作都不是原子操作,因?yàn)樵趦蓚€(gè)函數(shù)調(diào)用之間,內(nèi)核有可能會(huì)臨時(shí)掛起進(jìn)程
- UNIX 系統(tǒng)為這樣的操作提供了一種原子操作方法,即在打開(kāi)文件時(shí)設(shè)置 O_APPEND 標(biāo)志。這樣做使得內(nèi)核在每次寫(xiě)操作之前,都將進(jìn)程的當(dāng)前偏移量設(shè)置到該文件的尾端處,于是在每次寫(xiě)之前就不再需要調(diào)用 lseek
3.11.2 函數(shù) pread 和 pwrite
#include <unistd.h>ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
-
pread 函數(shù)返回值
- 若成功則返回讀到的字節(jié)數(shù),若已讀到文件尾,返回 0
- 若出錯(cuò),返回 -1
-
pwrite 函數(shù)返回值
- 若成功則返回已寫(xiě)的字節(jié)數(shù)
- 若出錯(cuò),返回 -1
-
調(diào)用 pread 相當(dāng)于調(diào)用 lseek 后調(diào)用 read,但是 pread 又與這種順序調(diào)用有下列重要區(qū)別
- 調(diào)用 pread 時(shí),無(wú)法中斷其定位和讀操作
- 不更新當(dāng)前文件偏移量
3.12 函數(shù) dup 和 dup2(復(fù)制一個(gè)現(xiàn)有的文件描述符)
#include <unistd.h>int dup(int fd);
int dup2(int fd, int fd2);
-
函數(shù)返回值
- 若成功,返回新的文件描述符
- 若出錯(cuò),返回 -1
-
由 dup 返回的新文件描述符一定是當(dāng)前可用文件描述符中的最小數(shù)值
-
對(duì)于 dup2,可以用 fd2 參數(shù)指定新描述符的值
- 如果 fd2 已經(jīng)打開(kāi),則先將其關(guān)閉
- 如果 fd = fd2,則 dup2 返回 fd2,而不關(guān)閉它
- 否則,fd2 的 FD_CLOEXEC 文描述符標(biāo)志就被清除,這樣 fd2 在進(jìn)程調(diào)用 exec 時(shí)是打開(kāi)狀態(tài)
-
復(fù)制一個(gè)描述符的另一種方法是使用 fcntl 函數(shù),以下函數(shù)調(diào)用等價(jià)
dup(fd); fcntl(fd, F_DUPFD, 0);// 以下情況并不完全等價(jià) // (1) dup2 是一個(gè)原子操作,而 close 和 fcnt1 包括兩個(gè)函數(shù)調(diào)用// 有可能在 close 和 fcntl 之間調(diào)用了信號(hào)捕獲函數(shù),它可能修改文件描述符 // (2) dup2 和 fcntl 有一些不同的 errno dup2(fd, fd2);close(fd2); fcntl(fd, F_DUPFD, fd2);
3.13 函數(shù) sync、fsync 和 fdatasync
- 傳統(tǒng)的 UNIX 系統(tǒng)實(shí)現(xiàn)在內(nèi)核中設(shè)有緩沖區(qū)高速緩存或頁(yè)高速緩存,大多數(shù)磁盤(pán) I/O 都通過(guò)緩沖區(qū)進(jìn)行
- 向文件寫(xiě)入數(shù)據(jù)時(shí),內(nèi)核通常先將數(shù)據(jù)復(fù)制到緩沖區(qū),然后排入隊(duì)列,晚些時(shí)候再寫(xiě)入磁盤(pán),這種方式被稱(chēng)為延寫(xiě)
- 通常,當(dāng)內(nèi)核需要重用緩沖區(qū)來(lái)存放其他磁盤(pán)塊數(shù)據(jù)時(shí),它會(huì)把所有延遲寫(xiě)數(shù)據(jù)塊寫(xiě)入磁盤(pán)
- 為了保證磁盤(pán)上實(shí)際文件系統(tǒng)與緩沖區(qū)中內(nèi)容的一致性,UNIX 系統(tǒng)提供了 sync、fsync 和 fdatasync 三個(gè)函數(shù)
#include <unistd.h>int fsync(int fd);
int fdatasync(int fd);void sync(void);
- 函數(shù)返回值
- 若成功,返回 0
- 若出錯(cuò),返回 -1
- sync 只是將所有修改過(guò)的塊緩沖區(qū)排入寫(xiě)隊(duì)列,然后就返回,它并不等待實(shí)際寫(xiě)磁盤(pán)操作結(jié)束
- 稱(chēng)為 update 的系統(tǒng)守護(hù)進(jìn)程周期性地調(diào)用 (一般每隔30秒) sync 函數(shù),這就保證了定期沖洗內(nèi)核的塊緩沖區(qū)
- fsync 函數(shù)只對(duì)由文件描述符 fd 指定的一個(gè)文件起作用,并且等待寫(xiě)磁盤(pán)操作結(jié)束才返回
- fsync 可用于數(shù)據(jù)庫(kù)這樣的應(yīng)用程序,這種應(yīng)用程序需要確保修改過(guò)的塊立即寫(xiě)到磁盤(pán)上
- fdatasync 函數(shù)類(lèi)似于 fsync,但它只影響文件的數(shù)據(jù)部分
- 除數(shù)據(jù)外,fsync 還會(huì)同步更新文件的屬性
3.14 函數(shù) fcntl (改變已打開(kāi)文件的屬性)
#include <unistd.h>
#include <fcntl.h>// 參數(shù) 3 可以是整數(shù)或指向一個(gè)結(jié)構(gòu)的指針
int fcntl(int fd, int cmd, ... /* int arg */ );
- 函數(shù)返回值
- 若成功,則依賴(lài)于 cmd
- 復(fù)制一個(gè)已有的描述符:F_DUPFD 或 F_DUPFD_CLOEXEC,返回新的文件描述符
- 獲取/設(shè)置文件描述符標(biāo)志:F_GETFD 或 F_SETFD,返回相應(yīng)的標(biāo)志
- 獲取/設(shè)置文件狀態(tài)標(biāo)志:F_GETFL 或 F_SETFL,返回相應(yīng)的標(biāo)志
- 獲取/設(shè)置異步 I/O 所有權(quán):F_GETOWN 或 F_SETOWN,返回一個(gè)正的進(jìn)程 ID 或負(fù)的進(jìn)程組 ID
- 獲取/設(shè)置記錄鎖:F_GETLK、F_SETLK 或 F_SETLKW
- 若出錯(cuò),返回 -1
- 若成功,則依賴(lài)于 cmd
案例
// 終端文件默認(rèn)是阻塞讀的,這里用 fcntl 將其更改為非阻塞讀
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#define MSG_TRY "try again\n"int main(void) {char buf[10];int flags, n;flags = fcntl(STDIN_FILENO, F_GETFL);if (flags == -1) {perror("fcntl error");exit(1);}flags |= O_NONBLOCK; // 與或操作,打開(kāi) flagsint ret = fcntl(STDIN_FILENO, F_SETFL, flags);if (ret == -1) {perror("fcntl error");exit(1);}tryagain:n = read(STDIN_FILENO, buf, 10);if (n < 0) {if (errno != EAGAIN) {perror("read /dev/tty");exit(1);}sleep(3);write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));goto tryagain;}write(STDOUT_FILENO, buf, n);return 0;
}
3.15 函數(shù) ioctl
#include <sys/ioctl.h>int ioctl(int fd, unsigned long request, ...);
- 函數(shù)返回值
- 若出錯(cuò),返回 -1
- 若成功,返回其他值
- 對(duì)設(shè)備的 I/O 通道進(jìn)行管理,控制設(shè)備特性(主要應(yīng)用于設(shè)備驅(qū)動(dòng)程序中)
- 通常用來(lái)獲取文件的物理特性 (該特性,不同文件類(lèi)型所含有的值各不相同)
3.16 傳入、傳出參數(shù)
#include <string.h>char* strcpy(char* dest, const char* src);
char* strcpy(char* dest, const char* src, size_t n);
-
傳入?yún)?shù):src
- 指針作為函數(shù)參數(shù)
- 通常有 const 關(guān)鍵字修飾
- 指針指向有效區(qū)域,在函數(shù)內(nèi)部做讀操作
-
傳出參數(shù):dest
- 指針作為函數(shù)參數(shù)
- 在函數(shù)調(diào)用之前,指針指向的空間可以無(wú)意義,但必須有效
- 在函數(shù)內(nèi)部做寫(xiě)操作
- 函數(shù)調(diào)用結(jié)束后,充當(dāng)函數(shù)返回值
#include <string.h>char* strtok(char* str, const char* delim);
char* strtok_r(char* str, const char* delim, char** saveptr);
- 傳入傳出參數(shù):saveptr
- 指針作為函數(shù)參數(shù)
- 在函數(shù)調(diào)用之前,指針指向的空間有實(shí)際意義
- 在函數(shù)內(nèi)部先做讀、后做寫(xiě)操作
- 函數(shù)調(diào)用結(jié)束后,充當(dāng)函數(shù)返回值