把書讀薄之《Advanced Programming in the Unix Environment》(一)

前言

既然讀書,就把它讀薄。

在學(xué)習(xí)C語言的時(shí)候我讀到過一句話,說C語言并不是個(gè)復(fù)雜的語言,因此介紹C語言的書沒必要太長。我覺得很合心意,然后就喜歡上這個(gè)語言——誰會不喜歡簡潔的設(shè)計(jì)呢?

但并非所有的設(shè)計(jì)都能做到這樣,比如操作系統(tǒng),它本身就是個(gè)龐大復(fù)雜的家伙,是許多人協(xié)作勞動的成果,充滿了繁多的細(xì)節(jié)。幸運(yùn)的是有一種人有提煉總結(jié)的特異功能,能像煉金術(shù)士一樣把知識歸納總結(jié)重新濃縮提煉,再次呈現(xiàn)出來的時(shí)候不失原味又井井有條,所有的學(xué)習(xí)者都因?yàn)樗麄兊闹腔凼芤?。W. Richard Stevens就是這樣的煉金術(shù)士,他的《Advanced Programming in the Unix Environment》和《UNIX Network Programming》都是了不起的創(chuàng)造和作品。我崇拜這樣的人。

可即便如此,《Advanced Programming in the Unix Environment》也有1000多頁,這是因?yàn)楸举|(zhì)上手冊類的書籍需要面面俱到,容納各種細(xì)節(jié)和特例。但作為學(xué)習(xí)者的普通人類顯然是無法將所有知識都記憶的,當(dāng)然也沒有必要。好的學(xué)習(xí)者閱讀一本書的過程應(yīng)該是自己構(gòu)建起知識索引,能夠快速的定位、查閱并應(yīng)用,所謂消化知識就是這個(gè)意思。

這里的把書讀薄系列就是我閱讀過程中試圖建立的大腦索引,它抽取了一部書(或一個(gè)領(lǐng)域)中最重要的方面,將每一個(gè)方面通過一幅圖或者文字脈絡(luò)將重要的知識串起來,而舍棄掉細(xì)節(jié)和特例。當(dāng)然既然你已經(jīng)構(gòu)建起了知識的全景,你自然知道去哪里找到這些細(xì)節(jié)和特例。

總而言之,這是個(gè)人的讀書筆記。只不過我努力將它寫的對閱讀者友好,而不論閱讀者是別人還是自己——就像程序注釋那樣。

提綱

簡單說來,Unix環(huán)境給程序員提供的就是一系列的系統(tǒng)調(diào)用(System Call)和C庫函數(shù)(C Library Function),程序員通過在程序中調(diào)用這兩者來使用操作系統(tǒng)的服務(wù)。因此本質(zhì)上學(xué)習(xí)《Advanced Programming in the Unix Environment》就是學(xué)習(xí)這些系統(tǒng)調(diào)用和C庫函數(shù)。但如果認(rèn)為《Advanced Programming in the Unix Environment》僅僅是一本接口手冊,就有點(diǎn)兒買櫝還珠了。這本書的經(jīng)典之處在于它不僅把這些接口函數(shù)的講解分門別類,還描繪了接口背后Unix系統(tǒng)的相關(guān)設(shè)計(jì)和原理,再輔以豐富的實(shí)例代碼來展示怎樣使用這些接口,與一般的手冊可謂是云泥之別。

我認(rèn)為Unix環(huán)境編程的知識最重要的就5個(gè)方面,這5個(gè)方面占了Unix環(huán)境編程知識中的80%以上,掌握之后基本認(rèn)為對Unix系統(tǒng)胸有成竹了。它們是:

  • 文件系統(tǒng) (File System)
  • 權(quán)限控制 (Permission Control)
  • I/O (包括Unbuffered I/O 和 Buffered I/O)
  • 進(jìn)程和線程 (Process & Thread,包括進(jìn)程控制,線程控制,進(jìn)程間通信)
  • 內(nèi)存分配和管理 (Memory Allocation and Management)

除去上面5個(gè)方面,剩下的內(nèi)容不多,可以很快掃尾,我統(tǒng)稱為“其他”:

  • 信號 (Signal)
  • 時(shí)間 (Time)
  • 錯(cuò)誤處理 (Error Handle)

第1部分 : I/O

I/O 分為Unbuffered I/O和Buffered I/O兩種。

Unbuffered I/O,即無緩存I/O,是通過一系列的系統(tǒng)調(diào)用提供的。這里的無緩存是相對于C標(biāo)準(zhǔn)庫的I/O函數(shù)而言,文件的讀寫都是直接用的系統(tǒng)調(diào)用。而C庫函數(shù)提供的I/O操作則是在系統(tǒng)調(diào)用之上又作了一層封裝,加入了緩存邏輯,故稱為Buffered I/O。

Unbuffered I/O

Unbuffered I/O的接口設(shè)計(jì)非常簡潔容易理解,一共就7+3個(gè)系統(tǒng)調(diào)用。

  • 7個(gè)基本I/O操作包括:open,close,lseek,分別用于打開文件,關(guān)閉文件,重定位文件讀寫偏移。還有2對讀寫函數(shù),read和pread,write和pwrite。pread和pwrite是原子操作版的讀寫函數(shù)。
  • 3個(gè)其他I/O操作是dup,sync,和fcntl。

而這7+3個(gè)系統(tǒng)調(diào)用都緊密的圍繞在Unix系統(tǒng)維護(hù)的進(jìn)程空間和內(nèi)核空間的邏輯結(jié)構(gòu)之上,可以用一幅圖全部串聯(lián)起來。

Fig 1.1 文件打開后進(jìn)程空間和內(nèi)核空間的邏輯結(jié)構(gòu)


Fig 1.1
Fig 1.1

如Fig 1.1所示,Unbuffered I/O的系統(tǒng)調(diào)用背后,進(jìn)程空間和內(nèi)核空間維護(hù)著這樣一幅邏輯結(jié)構(gòu)——由Preocess Table(進(jìn)程表),F(xiàn)ile Table(文件表),v-node Table(v節(jié)點(diǎn)表)三個(gè)數(shù)據(jù)結(jié)構(gòu)組成。

我們先看圖中的黑色部分:
假設(shè)進(jìn)程a.out是通過shell命令a.out <input.txt >output.txt啟動得,相當(dāng)于shell幫進(jìn)程a.out在文件描述符0(標(biāo)準(zhǔn)輸入)位置以打開了input.txt,在文件描述符1的位置打開了文件output.txt。那么此時(shí)

  1. 如圖左側(cè)所示,進(jìn)程a.out在Process Table中擁有自己的條目,記錄著它當(dāng)前打開的所有文件描述符的相關(guān)信息——即它在文件描述符號0的位置打開了某個(gè)文件作為標(biāo)準(zhǔn)輸入,在文件描述符1的位置打開了某文件作為標(biāo)準(zhǔn)輸出。但具體文件打開的方式是什么,文件存在磁盤的什么位置,則要順著Process Table中的file pointer字段去到File Table。
  2. 如圖中部所示,F(xiàn)ile Table記錄的是 (1)file status flag記錄著文件的打開方式——比如input.txt就是以只讀方式打開的,相應(yīng)的file status flag字段即為O_RDONLY。output的file status flag字段應(yīng)為(O_WRONLY | O_APPEND | O_CREAT) (2)當(dāng)前文件偏移量current file offset,對于該文件的讀寫操作都會在current file offset指示的位置進(jìn)行 (3)如果要得到具體文件的一些信息,比如文件的擁有者是誰,文件存儲在磁盤的什么位置,則要順著File Table中的v-node指針找到v-node Table
  3. 如圖右側(cè)所示,v-node Table記錄著文件類型,文件的擁有者,文件大小,文件內(nèi)容存儲的磁盤block地址等信息。

值得注意的是,同一時(shí)刻無論有多少個(gè)進(jìn)程打開了同一個(gè)文件,v-node Table都只有一個(gè)表項(xiàng)。而File Table則不同,若某個(gè)進(jìn)程多次打開同一文件,或者多個(gè)進(jìn)程打開了同一文件,則File Table中會有多項(xiàng)。這非常好理解,因?yàn)槊看未蜷_的方式,還有偏移量可能不同。比如如圖Fig 1.1紅色部分所示,此時(shí)有另外一個(gè)進(jìn)程也打開了output.txt(在文件描述符4的位置),則該進(jìn)程會在File Table中有單獨(dú)的一個(gè)表項(xiàng)記錄該進(jìn)程的文件打開方式和偏移量,但v-node指針指向的v-node表項(xiàng)與a.out進(jìn)程指向的是同一個(gè)。

int open(const char *pathname, int oflag, ... /* mode_t mode */ );
open函數(shù)是Unbuffered I/O的第一步,它以指定的方式打開指定文件,成功打開之后返回相應(yīng)的文件描述符,其他I/O操作都需要這個(gè)文件描述符來指定當(dāng)前是對哪個(gè)文件作操作。open打開文件前會作相應(yīng)的權(quán)限檢查,然后在當(dāng)前未被使用的最小的文件描述符處打開文件并返回該描述符。

int close(int filedes);
close函數(shù)關(guān)閉某文件描述符。若某進(jìn)程沒有顯式關(guān)閉它所打開的文件,則當(dāng)進(jìn)程結(jié)束時(shí)它所打開的所有文件都會被自動被關(guān)閉。

off_t lseek(int filedes, off_t offset, int whence);
lseek函數(shù)重定位File Table的current file offset字段——read或write會在current file offset處進(jìn)行。

ssize_t read(int filedes, void *buf, size_t nbytes);
ssize_t pread(int filedes, void *buf, size_t nbytes, off_t offset);
read函數(shù)從一個(gè)打開的文件的current file offset位置讀取數(shù)據(jù)。成功讀取nbyte的數(shù)據(jù)后current file offset會加上nbyte。
pread是多個(gè)進(jìn)程共享某文件時(shí)應(yīng)該采用的讀函數(shù),它的作用時(shí)每次lseek到指定位置然后read文件,且這兩個(gè)操作是不可中斷的原子操作(atomic operation)。

ssize_t write(int filedes, const void *buf, size_t nbytes);
ssize_t pwrite(int filedes, const void *buf, size_t nbytes, off_t offset);
write函數(shù)從一個(gè)打開的文件的current file offset位置寫入數(shù)據(jù),除非文件是以O(shè)_APPEND方式打開的,則每次寫前都會重置current file offset到文件尾。成功寫入nbyte后current file offset也會加上nbyte。
pwrite函數(shù)是多個(gè)進(jìn)程共享某文件時(shí)應(yīng)該采用的寫函數(shù),它的作用時(shí)每次lseek到指定位置然后write文件,且這兩個(gè)操作是不可中斷的原子操作(atomic operation)。

int dup(int filedes);
int dup2(int filedes, int filedes2);
dup, dup2函數(shù)的作用是復(fù)制文件描述符。
dup函數(shù)會自動在當(dāng)前最小的空閑描述符處復(fù)制描述符號filedes。比如a.out的程序中若有語句dup(1);,而當(dāng)前沒有被使用的描述符最小的是3(這里假定文件描述符2已被標(biāo)準(zhǔn)錯(cuò)誤占用),那么就會在文件描述符3的位置復(fù)制文件描述符1,結(jié)果就會如圖Fig 1.1的藍(lán)線所示,文件描述符1和3都指向同一個(gè)File Table表項(xiàng)。
dup2則是可以指定在文件描述符filedes2處復(fù)制文件描述符filedes,如果filedes2已經(jīng)被打開了,則dup2會先關(guān)閉它然后進(jìn)行復(fù)制。(dup2函數(shù)可以用于輸入輸出重定向,比如某個(gè)進(jìn)程在描述符3的位置打開了一個(gè)文件,然后調(diào)用dup2(3, STDOUT_FILENO),就相當(dāng)于將標(biāo)準(zhǔn)輸出重定向到了該文件)

void sync(void);
int fsync(int filedes);
int fdatasync(int filedes);
sync函數(shù):Unix系統(tǒng)中,當(dāng)對文件進(jìn)行寫操作時(shí)采用了“延遲寫”策略,即等夠一定的時(shí)間或者積攢一定數(shù)量的寫操作之后再真正將數(shù)據(jù)寫到磁盤上,這樣可以減少磁盤I/O次數(shù)。調(diào)用sync函數(shù)相當(dāng)于告訴操作系統(tǒng)立即將“延遲寫”的數(shù)據(jù)寫到磁盤。但它不等待磁盤寫操作完成即返。fsync函數(shù)與sync函數(shù)類似,只不過是對某個(gè)對特定的文件操作并會等待真正的磁盤寫操作完成才返回。fdatasy與fsync類似,但不像fsync函數(shù)那樣寫的時(shí)候也同時(shí)更新文件的屬性。

int fcntl(int filedes, int cmd, ... /* int arg */ );
fcntl函數(shù)是一個(gè)多功能的函數(shù),可以用來包括復(fù)制文件描述符,獲取或修改Process Table中的fd flags,獲取或修改File Table中的file status flags等,一共9種功能通過參數(shù)來控制。

(如果你覺得這篇文章有幫助,敬請期待Buffered I/O部分)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容