關(guān)于我們
書單推薦
新書推薦
|
Linux環(huán)境編程圖文指南(配視頻教程)
本書從零開始,循序漸進(jìn)地攻破Linux環(huán)境編程所遇到的各級關(guān)卡,以圖文并茂的形式幫助讀者理解各個概念。本書內(nèi)容翔實,囊括了Linux系統(tǒng)操作細(xì)節(jié),Shell腳本編程精要,各種編程環(huán)境所需要解決的技術(shù)難點,以及在Linux環(huán)境下的C語言編程技術(shù)、并發(fā)編程技術(shù)和音/視頻編程等核心內(nèi)容。全書用400余幅圖表幫助讀者理解復(fù)雜概念,因此讀者不需要具備任何計算機(jī)編程經(jīng)驗,在本書的指導(dǎo)下就能進(jìn)入編程的世界,并能在閱讀和實踐中享受編程的樂趣。同時,本書配套完整的視頻教程,給讀者以最直觀、最容易吸收知識的方式,融會貫通書中所有的知識點。不僅如此,讀者還能夠得到作者及其團(tuán)隊的在線技術(shù)支援和答疑。 本書通俗易懂,適合從事Linux/UNIX編程開發(fā)、嵌入式開發(fā)、C環(huán)境開發(fā)的讀者,尤其適合計算機(jī)相關(guān)專業(yè)的高職院校的學(xué)生,以及希望轉(zhuǎn)向IT類就業(yè)方向的在職人士。
圖文結(jié)合視頻,全面精講Linux編程關(guān)鍵知識點; 只要你看,就能懂能會能用。
林世霖,國內(nèi)C編程專家,粵嵌最受歡迎培訓(xùn)師和演講家,金牌講師,資深嵌入式Linux研發(fā)工程師。目前主要致力于Linux應(yīng)用軟件及系統(tǒng)開發(fā)和研究,十余年嵌入式系統(tǒng)軟件開發(fā)經(jīng)驗。而且精通數(shù)據(jù)結(jié)構(gòu)算法與實現(xiàn),以及SCO系統(tǒng)下shell編程與系統(tǒng)編程,有豐富的銀行交易系統(tǒng)開發(fā)經(jīng)驗。同時具有嫻熟的授課技巧和成體系化的教學(xué)經(jīng)驗,上課風(fēng)格多樣化,善于并樂于傳播IT技術(shù),熱衷于教育行業(yè),常常與學(xué)生打成一片。
第1章 Linux編程環(huán)境 1 1.1 基本工具 1 1.1.1 免費大餐:Ubuntu 1 1.1.2 桌面系統(tǒng):gnome 6 1.1.3 網(wǎng)絡(luò)配置:純手工打造 6 1.1.4 軟件集散地:APT 8 1.1.5 無敵板斧:vi 10 1.1.6 開發(fā)圣典:man 13 1.1.7 配置共享目錄 15 1.2 Shell命令 17 1.2.1 概念掃盲 17 1.2.2 命令詳解 19 1.2.3 上古神器 38 1.3 Shell腳本編程 45 1.3.1 開場白 45 1.3.2 腳本格式 45 1.3.3 變量 46 1.3.4 特殊符號們 48 1.3.5 字符串處理 50 1.3.6 測試語句 51 1.3.7 腳本語法單元 52 1.4 編譯器:GCC 55 1.4.1 簡述 55 1.4.2 編譯過程簡介 55 1.4.3 實用的編譯選項 58 1.5 解剖Makefile 59 1.5.1 工程管理器make 59 1.5.2 概覽性示例 60 1.5.3 書寫格式 60 1.5.4 變量詳解 62 1.5.5 各種規(guī)則 71 1.5.6 條件判斷 75 1.5.7 函數(shù) 77 1.5.8 實用make選項集錦 85 1.6 GNU-autotools 86 1.6.1 autotools簡介 86 1.6.2 文件組織 87 1.6.3 configure.ac編寫規(guī)則 88 第2章 深度Linux-C 92 2.1 基本要素 92 2.1.1 Linux下C代碼規(guī)范 93 2.1.2 基本數(shù)據(jù)類型 97 2.1.3 運算符 108 2.1.4 控制流 116 2.2 函數(shù) 124 2.2.1 函數(shù)初體驗 125 2.2.2 函數(shù)調(diào)用內(nèi)幕 128 2.2.3 遞歸思維及其實現(xiàn) 130 2.2.4 變參函數(shù) 133 2.2.5 回調(diào)函數(shù) 137 2.2.6 內(nèi)聯(lián)函數(shù) 140 2.3 數(shù)組與指針 142 2.3.1 數(shù)組初階 142 2.3.2 內(nèi)存地址 144 2.3.3 指針初階 145 2.3.4 復(fù)雜指針定義 147 2.3.5 指針運算 151 2.3.6 數(shù)組與指針 152 2.3.7 復(fù)雜數(shù)組剖析 155 2.3.8 const指針 158 2.3.9 char指針和char數(shù)組 160 2.4 內(nèi)存管理 162 2.4.1 進(jìn)程內(nèi)存布局 162 2.4.2 堆(Heap) 164 2.5 組合數(shù)據(jù)類型 167 2.5.1 結(jié)構(gòu)體 167 2.5.2 共用體 171 2.5.3 枚舉 172 2.6 高級議題 173 2.6.1 工程代碼組織 173 2.6.2 頭文件 175 2.6.3 宏(macro) 176 2.6.4 條件編譯 182 2.6.5 復(fù)雜聲明 184 2.6.6 attribute機(jī)制 185 第3章 Linux的數(shù)據(jù)組織 188 3.1 無所不在的鏈表 188 3.1.1 開場白 188 3.1.2 單向鏈表 190 3.1.3 單向循環(huán)鏈表 198 3.1.4 雙向循環(huán)鏈表 200 3.1.5 Linux內(nèi)核鏈表 210 3.2 線性表變異體 227 3.2.1 堆疊的盤子:棧 227 3.2.2 文明的社會:隊列 236 3.3 小白慎入:非線性結(jié)構(gòu) 243 3.3.1 基本概念 243 3.3.2 玩轉(zhuǎn)BST 247 3.3.3 各種的遍歷算法 260 3.3.4 自平衡AVL樹 263 3.3.5 自平衡Linux紅黑樹 273 第4章 I/O編程技術(shù) 289 4.1 一切皆文件 289 4.1.1 文件的概念 289 4.1.2 各類文件 290 4.2 文件操作 290 4.2.1 系統(tǒng)I/O 291 4.2.2 標(biāo)準(zhǔn)I/O 306 4.2.3 文件屬性 320 4.3 目錄檢索 327 4.3.1 基本概念 327 4.3.2 相關(guān)API 328 4.4 觸控屏應(yīng)用接口 330 4.4.1 輸入子系統(tǒng)簡介 330 4.4.2 TSLIB庫詳解 333 4.4.3 劃屏算法 338 第5章 Linux進(jìn)程線程 345 5.1 Linux進(jìn)程入門 345 5.1.1 進(jìn)程概念 345 5.1.2 進(jìn)程組織方式 346 5.2 進(jìn)程的“生老病死” 348 5.2.1 進(jìn)程狀態(tài) 348 5.2.2 相關(guān)重要API 350 5.3 進(jìn)程的語言 358 5.3.1 管道 358 5.3.2 信號 363 5.3.3 system-V IPC簡介 380 5.3.4 消息隊列(MSG) 381 5.3.5 共享內(nèi)存(SHM) 387 5.3.6 信號量(SEM) 392 5.4 Linux線程入門 400 5.4.1 線程基本概念 400 5.4.2 線程API及特點 401 5.5 線程安全 410 5.5.1 POSIX信號量 410 5.5.2 互斥鎖與讀寫鎖 415 5.5.3 條件變量 418 5.5.4 可重入函數(shù) 421 5.6 線程池 422 5.6.1 實現(xiàn)原理 422 5.6.2 接口設(shè)計 423 5.6.3 實現(xiàn)源碼 425 第6章 Linux音頻、視頻編程 433 6.1 基本背景 433 6.2 Linux音頻 433 6.2.1 音頻概念 433 6.2.2 標(biāo)準(zhǔn)音頻接口ALSA 436 6.3 Linux視頻輸出 450 6.3.1 基本概念 450 6.3.2 framebuffer 452 6.3.3 在LCD上畫圖 462 6.3.4 效果算法 469 6.4 Linux視頻輸入 478 6.4.1 V4L2簡介 478 6.4.2 V4L2視頻采集流程 478 6.4.3 V4L2核心命令字和結(jié)構(gòu)體 481 6.4.4 編碼格式和媒體流 484 6.5 多媒體開發(fā)庫SDL 489 6.5.1 SDL簡介 489 6.5.2 編譯和移植 489 6.5.3 視頻子系統(tǒng) 490 6.5.4 音頻子系統(tǒng) 494 6.5.5 事件子系統(tǒng) 498 6.5.6 處理YUV視頻源 502 6.6 音/視頻編解碼庫FFmpeg 504 6.6.1 FFmpeg簡介 504 6.6.2 核心結(jié)構(gòu)體與常用API 505 6.6.3 與SDL結(jié)合實現(xiàn)簡單的播放器 511
第5章Linux進(jìn)程線程
5.1 Linux進(jìn)程入門5.1.1 進(jìn)程概念一個程序文件(Program),只是一堆待執(zhí)行的代碼和部分待處理的數(shù)據(jù),它們只有被加載到內(nèi)存中,然后讓CPU逐條執(zhí)行其代碼,根據(jù)代碼做出相應(yīng)的動作,才形成一個真正“活的”、動態(tài)的進(jìn)程(Process)。因此,進(jìn)程是一個動態(tài)變化的過程,是一出有始有終的戲,而程序文件只是這一系列動作的原始藍(lán)本,是一個靜態(tài)的劇本。 圖5-1更好地展示了程序和進(jìn)程的關(guān)系。
圖5-1 ELF文件與進(jìn)程虛擬內(nèi)存 圖5-1中的程序文件,是一個靜態(tài)的存儲于外部存儲器(如磁盤、flash等掉電非易失器件)之中的文件,里面包含了將來進(jìn)程要運行的“劇本”,即執(zhí)行時會被復(fù)制到內(nèi)存的數(shù)據(jù)和代碼。除了這些部分,ELF格式中的大部分?jǐn)?shù)據(jù)與程序本身的邏輯沒有關(guān)系,只是程序被加載到內(nèi)存中執(zhí)行時,系統(tǒng)需要處理的額外的輔助信息。另外注意.bss段,這里面放的是未初始化的靜態(tài)數(shù)據(jù),它們是不需要被復(fù)制的,具體解釋請參閱2.4.1節(jié)。 當(dāng)這個ELF格式的程序被執(zhí)行時,內(nèi)核中實際上產(chǎn)生了一個名為task_struct{}的結(jié)構(gòu)體來表示這個進(jìn)程。進(jìn)程是一個“活動的實體”,這個活動的實體從一開始誕生就需要各種各樣的資源以便于生存下去,比如內(nèi)存資源、CPU資源、文件、信號、各種鎖資源等,所有這些東西都是動態(tài)變化的,這些信息都被事無巨細(xì)地一一記錄在結(jié)構(gòu)體task_struct之中,所以這個結(jié)構(gòu)體也常常稱為進(jìn)程控制塊(Process Control Block,PCB)。 下面是該結(jié)構(gòu)體的掠影。 vincent@ubuntu:~/Linux-2.6.35.7/include/Linux$ cat sched.h -n …… 1168 struct task_struct { 1169 volatile long state; 1170 void *stack; 1171 atomic_t usage; 1172 unsigned int flags; /* per process flags, defined below */ 1173 unsigned int ptrace; 1174 1175 int lock_depth; /* BKL lock depth */ 1176 1177 #ifdef CONFIG_SMP 1178 #ifdef __ARCH_WANT_UNLOCKED_CTXSW 1179 int oncpu; 1180 #endif 1181 #endif 1182 1183 int prio, static_prio, normal_prio; 1184 unsigned int rt_priority; 1185 const struct sched_class *sched_class; 1186 struct sched_entity se; 1187 struct sched_rt_entity rt; …… 如果沒什么意外,這個結(jié)構(gòu)體可能是最大的單個變量了,一個結(jié)構(gòu)體就有好幾KB那么大,想想它包含了一個進(jìn)程的所有信息,這么龐大也就不足為怪了。Linux內(nèi)核代碼紛繁復(fù)雜、千頭萬緒,這個結(jié)構(gòu)體是系統(tǒng)進(jìn)程在執(zhí)行過程中所有涉及的方方面面的縮影,包括系統(tǒng)內(nèi)存管理子系統(tǒng)、進(jìn)程調(diào)度子系統(tǒng)、虛擬文件系統(tǒng)等,以這個所謂的PCB為切入點,是一個很好的研究內(nèi)核的窗口。 總之,當(dāng)一個程序文件被執(zhí)行時,內(nèi)核將會產(chǎn)生這么一個結(jié)構(gòu)體,來承載所有該活動實體日后運行時所需要的所有資源,隨著進(jìn)程的運行,各種資源被分配和釋放,是一個動態(tài)的過程。 5.1.2 進(jìn)程組織方式既然進(jìn)程是一個動態(tài)的過程,有誕生的一刻,也就有死掉的一天,跟人類非常相似,人不可能無父無母,不可能突然從石頭中蹦出來,進(jìn)程也一樣,每一個進(jìn)程都必然有一個生它的父母(除了init),這個父母是一個被稱為“父進(jìn)程”的進(jìn)程。實際上可以用命令pstree來查看整個系統(tǒng)的進(jìn)程關(guān)系。 vincent@ubuntu:~$ pstree init─┬─NetworkManager───{NetworkManager} ├─accounts-daemon───{accounts-daemon} ├─acpid ├─at-spi-bus-laun───2*[{at-spi-bus-laun}] ├─atd ├─avahi-daemon───avahi-daemon ├─bluetoothd ├─colord───2*[{colord}] ├─console-kit-dae───64*[{console-kit-dae}] ├─cron ├─cupsd ├─3*[dbus-daemon] ├─2*[dbus-launch] ├─dconf-service───2*[{dconf-service}] ├─gconfd-2 ├─geoclue-master ├─6*[getty] ├─gnome-keyring-d───6*[{gnome-keyring-d}] ├─gnome-terminal─┬─3*[bash] │ ├─bash───pstree │ ├─gnome-pty-helpe │ └─3*[{gnome-terminal}] ├─goa-daemon───{goa-daemon} ├─gsd-printer───{gsd-printer} ├─gvfs-afc-volume───{gvfs-afc-volume} ├─gvfs-fuse-daemo───3*[{gvfs-fuse-daemo}] ├─gvfs-gdu-volume ├─gvfs-gphoto2-vo ├─gvfsd ├─gvfsd-burn ├─gvfsd-metadata ├─gvfsd-trash …… pstree是一個用“樹狀”方式查看當(dāng)前系統(tǒng)所有進(jìn)程關(guān)系的命令,可以明顯看到它們的關(guān)系就像人類社會的族譜,大家都有一個共同的祖先init,每個人都可以生出幾個孩子(進(jìn)程沒有性別,自己一個人就能生。。其中祖先init是一個非常特別的進(jìn)程,它沒有父進(jìn)程!它是一個真正從石頭(操作系統(tǒng)啟動鏡像文件)中蹦出來的野孩子。 另外,每個進(jìn)程都有自己的“身份證號碼”,即PID號,PID是重要的系統(tǒng)資源,它是用以區(qū)分各個進(jìn)程的基本依據(jù),可以使用命令ps來查看進(jìn)程的PID。 vincent@ubuntu:~$ ps -ef | more UID PID PPID C STIME TTY TIME CMD root 1 0 0 Jul22 ? 00:00:03 /sbin/init root 2 0 0 Jul22 ? 00:00:00 [kthreadd] root 3 2 0 Jul22 ? 00:00:06 [ksoftirqd/0] root 6 2 0 Jul22 ? 00:00:01 [migration/0] root 7 2 0 Jul22 ? 00:00:01 [watchdog/0] root 8 2 0 Jul22 ? 00:00:00 [migration/1] root 10 2 0 Jul22 ? 00:00:05 [ksoftirqd/1] root 11 2 0 Jul22 ? 00:00:02 [watchdog/1] root 12 2 0 Jul22 ? 00:00:00 [cpuset] root 15 2 0 Jul22 ? 00:00:00 [netns] root 17 2 0 Jul22 ? 00:00:01 [sync_supers] …… 上述信息中的第2列就是PID,而第3列是每個進(jìn)程的父進(jìn)程的PID。既然進(jìn)程有父子關(guān)系,進(jìn)程可以生孩子,那么自然會有“生老病死”,欲知后事如何,且聽下節(jié)分解。 5.2 進(jìn)程的“生老病死”5.2.1 進(jìn)程狀態(tài)說進(jìn)程是動態(tài)的活動的實體,指的是進(jìn)程會有很多種運行狀態(tài),一會兒睡眠、一會兒暫停、一會兒又繼續(xù)執(zhí)行。如圖5-2所示為Linux進(jìn)程從被創(chuàng)建(生)到被回收(死)的全部狀態(tài),以及這些狀態(tài)發(fā)生轉(zhuǎn)換時的條件。
圖5-2 Linux進(jìn)程狀態(tài)轉(zhuǎn)換圖 結(jié)合圖5-2所示,一起看一下進(jìn)程從生到死的過程。 (1)從“蛋生”可以看到,一個進(jìn)程的誕生,是從其父進(jìn)程調(diào)用fork( )開始的。 (2)進(jìn)程剛被創(chuàng)建出來時,處于TASK_RUNNING狀態(tài),從圖5-2中可以看到,處于該狀態(tài)的進(jìn)程可以是正在進(jìn)程等待隊列中排隊,也可以占用CPU正在運行,我們習(xí)慣上稱前者為“就緒態(tài)”,后者為“執(zhí)行態(tài)”。當(dāng)進(jìn)程狀態(tài)為TASK_RUNNING并且占用CPU時才是真正運行。 (3)剛被創(chuàng)建的進(jìn)程都處于“就緒”狀態(tài),等待系統(tǒng)調(diào)度,內(nèi)核中的函數(shù)sched( )稱為調(diào)度器,它會根據(jù)各種參數(shù)來選擇一個等待的進(jìn)程去占用CPU。進(jìn)程占用CPU之后就可以真正運行了,運行時間有個限定,比如20ms,這段時間稱為time slice,即“時間片”的概念。時間片耗光的情況下如果進(jìn)程還沒有結(jié)束,那么會被系統(tǒng)重新放入等待隊列中等待。另外,正處于“執(zhí)行態(tài)”的進(jìn)程即使時間片沒有耗光,也可能被別的更高優(yōu)先級的進(jìn)程“搶占”CPU,被迫重新回到等待隊列中等待。 換句話說,進(jìn)程跟人一樣,從來都沒有什么平等可言,有貴族就有屌絲,它們要處理的事情有不同的輕重緩急之分。 (4)進(jìn)程處于“執(zhí)行態(tài)”時,可能會由于某些資源的不可得而被置為“睡眠態(tài)/掛起態(tài)”,比如進(jìn)程要讀取一個管道文件數(shù)據(jù)而管道為空,或者進(jìn)程要獲得一個鎖資源而當(dāng)前鎖不可獲取,或者干脆進(jìn)程自己調(diào)用sleep( )來強(qiáng)制自己掛起,這些情況下進(jìn)程的狀態(tài)都會變成TASK_INTERRUPIBLE或TASK_UNINTERRUPIBLE,它們的區(qū)別是一般后者跟某些硬件設(shè)置相關(guān),在睡眠期間不能響應(yīng)信號,因此TASK_UNINTERRUPIBLE的狀態(tài)也稱為深度睡眠,相應(yīng)地TASK_INTERRUPIBLE期間進(jìn)程是可以響應(yīng)信號的。當(dāng)進(jìn)程所等待的資源變得可獲取時,又會被系統(tǒng)置為TASK_RUNNING狀態(tài)重新就緒排隊。 (5)當(dāng)進(jìn)程收到SIGSTOP或SIGTSTP中的一個信號時,狀態(tài)會被置為TASK_STOPPED,此時稱為“暫停態(tài)”,該狀態(tài)下的進(jìn)程不再參與調(diào)度,但系統(tǒng)資源不釋放,直到收到SIGCONT信號后被重新置為就緒態(tài)。當(dāng)進(jìn)程被追蹤時(典型情況是被調(diào)試器調(diào)試時),收到任何信號狀態(tài)都會被置為TASK_TRACED,該狀態(tài)與暫停態(tài)是一樣的,一直要等到SIGCONT才會重新參與系統(tǒng)進(jìn)程調(diào)度。 (6)運行的進(jìn)程跟人一樣,遲早都會死掉。進(jìn)程的死亡可以有多種方式,可以是壽終正寢的正常退出,也可以是被異常殺死。比如圖5-2中,在main函數(shù)內(nèi)return或調(diào)用exit( ),包括在最后線程調(diào)用pthread_exit( )都是正常退出,而受到致命信號死掉的情況則是異常死亡,不管怎么死,最后內(nèi)核都會調(diào)用do_exit( )的函數(shù)來使得進(jìn)程的狀態(tài)變成所謂的僵尸態(tài)EXIT_ZOMBIE,單詞ZOMBIE對于玩過“植物大戰(zhàn)僵尸”的讀者都不會陌生,這里的“僵尸”指的是進(jìn)程的PCB(進(jìn)程控制塊)。 為什么一個進(jìn)程的死掉之后還要把尸體留下呢?因為進(jìn)程在退出時,將其退出信息都封存在它的尸體里面了,比如如果它正常退出,那退出值是多少呢?如果被信號殺死,那么是哪個信號呢?這些“死亡信息”都被一一封存在該進(jìn)程的PCB當(dāng)中,好讓別人可以清楚地知道:我是怎么死的。 那誰會關(guān)心它是怎么死的呢?答案是它的父進(jìn)程,它的父進(jìn)程之所以要創(chuàng)建它,很大的原因是要讓這個孩子去干某一件事情,現(xiàn)在這個孩子已死,那事情辦得如何?孩子是否需要有個交代?但它又死掉了,所以之后將這些“死亡信息”封存在自己的尸體里面,等著父進(jìn)程去查看。例如,父子進(jìn)程可以約定:如果事情辦成了退出值為0;如果權(quán)限不足退出值為1;如果內(nèi)存不夠退出值為2;等等。父進(jìn)程可以隨時查看一個已經(jīng)死去的孩子的PCB來確定事情究竟辦得如何?梢钥吹剑诠I(yè)社會中,哪怕是進(jìn)程間的協(xié)作,也充滿了契約精神。 (7)父進(jìn)程調(diào)用wait( ) /waitpid( )來查看孩子的“死亡信息”,順便做一件非常重要的事情:將該孩子的狀態(tài)設(shè)置為EXIT_DEAD,即死亡態(tài),因為處于這個狀態(tài)的進(jìn)程的PCB才能被系統(tǒng)回收。由此可見,父進(jìn)程應(yīng)盡職盡責(zé)地及時調(diào)用wait( ) /waitpid( ),否則系統(tǒng)會充滿越來越多的“僵尸”! 問題是,如何保證父進(jìn)程一定要及時地調(diào)用wait( ) /waitpid( )從而避免僵尸進(jìn)程泛濫呢?答案是不能,因為父進(jìn)程也許需要做別的事情沒空去幫那些死去的孩子收尸,甚至那些孩子在變成僵尸時,它的父進(jìn)程已經(jīng)先它而去了! 后一種情況其實比較容易解決:如果一個進(jìn)程的父進(jìn)程退出,那么祖先進(jìn)程init(該進(jìn)程是系統(tǒng)第一個運行的進(jìn)程,它的PCB是從內(nèi)核的啟動鏡像文件中直接加載的,不需要別的進(jìn)程fork( )出來,因此它是無父無母的,系統(tǒng)中的所有其他進(jìn)程都是它的后代)將會收養(yǎng)(adopt)這些孤兒進(jìn)程。換句話說,Linux系統(tǒng)保證任何一個進(jìn)程(除了init)都有父進(jìn)程,也許是其真正的生父,也許是其祖先init。 而前一種情況是:父進(jìn)程有別的事情要干,不能隨時執(zhí)行wait( ) /waitpid( )來確;厥战┦Y源。在這樣的情形下,我們可以考慮使用信號異步通知機(jī)制,讓一個孩子在變成僵尸時,給其父進(jìn)程發(fā)一個信號,父進(jìn)程接收到這個信號之后,對其進(jìn)行處理,在此之前想干嘛就干嘛,異步操作。但即便是這樣也仍然存在問題:如果兩個以上的孩子同時退出變僵尸,那么它們就會同時給其父進(jìn)程發(fā)送相同的信號,而相同的信號將會被淹沒。如何解決這個問題,請參閱5.3.2節(jié)。 5.2.2 相關(guān)重要API本節(jié)將詳細(xì)展示進(jìn)程開發(fā)相關(guān)的API,第一個需要知道的接口函數(shù)當(dāng)然是創(chuàng)建一個新的進(jìn)程,如表5-1所示。 表5-1 函數(shù)fork( )的接口規(guī)范
這個函數(shù)接口本身非常簡單,簡單到連參數(shù)都沒有,但是這個函數(shù)有個與眾不同的地方:它會使得進(jìn)程一分為二!就像細(xì)胞分裂一樣,如圖5-3所示。
圖5-3 細(xì)胞分裂 當(dāng)一個進(jìn)程調(diào)用fork( )成功后,fork( )將分別返回到兩個進(jìn)程之中,換句話說,fork( )在父子兩個進(jìn)程中都會返回,而它們所得到的返回值也不一樣,如圖5-4所示。 要著重注意如下幾點。 (1)fork( )會使得進(jìn)程本身被復(fù)制(想想細(xì)胞分裂),因此被創(chuàng)建出來的子進(jìn)程和父進(jìn)程幾乎是一模一樣的,說“幾乎”意味著子進(jìn)程并不是100%為一份父進(jìn)程的復(fù)印件,它們的具體關(guān)系如下。
圖5-4 創(chuàng)建子進(jìn)程的過程示意圖 父子進(jìn)程的以下屬性在創(chuàng)建之初完全一樣,子進(jìn)程相當(dāng)于做了一份復(fù)制品。 l 實際UID和GID,以及有效UID和GID。 l 所有環(huán)境變量。 l 進(jìn)程組ID和會話ID。 l 當(dāng)前工作路徑。除非用chdir()加以修改。 l 打開的文件。 l 信號響應(yīng)函數(shù)。 l 整個內(nèi)存空間,包括棧、堆、數(shù)據(jù)段、代碼段、標(biāo)準(zhǔn)I/O的緩沖區(qū)等。 而以下屬性,父子進(jìn)程是不一樣的。 l 進(jìn)程號PID。PID是身份證號碼,哪怕親如父子,也要區(qū)分開。 l 記錄鎖。父進(jìn)程對某文件加了把鎖,子進(jìn)程不會繼承這把鎖。 l 掛起的信號。這些信號是所謂的“懸而未決”的信號,等待著進(jìn)程的響應(yīng),子進(jìn)程也不會繼承這些信號。 (2)子進(jìn)程會從fork( )返回值后的下一條邏輯語句開始運行。這樣就避免了不斷調(diào)用fork( )而產(chǎn)生無限子孫的悖論。 (3)父子進(jìn)程是相互平等的。它的執(zhí)行次序是隨機(jī)的,或者說它們是并發(fā)運行的,除非使用特殊機(jī)制來同步它們,否則不能判斷它們的運行究竟誰先誰后。 (4)父子進(jìn)程是相互獨立的。由于子進(jìn)程完整地復(fù)制了父進(jìn)程的內(nèi)存空間,因此從內(nèi)存空間的角度看它們是相互獨立、互不影響的。 以下代碼顯示了fork( )的作用。 vincent@ubuntu:~/ch05/5.2$ cat fork.c -n 1 #include <stdio.h> 2 #include <unistd.h> 3 4 int main(void) 5 { 6 printf("[%d]: before fork() ... \n", (int)getpid()); 7 8 pid_t x; 9 x = fork(); //生個孩子 10 11 printf("[%d]: after fork() ...\n", (int)getpid()); 12 return 0; 13 } 執(zhí)行效果如下。 vincent@ubuntu:~/ch05/5.2$ ./fork [23900]: before fork() ... [23900]: after fork() ... vincent@ubuntu:~/ch05/5.2$ [23901]: after fork() ... 可以看到,第11行代碼被執(zhí)行了兩遍,函數(shù)getpid( )展示了當(dāng)前進(jìn)程的PID,其中23900是父進(jìn)程,23901是子進(jìn)程。從執(zhí)行效果看還有一個很有意思的現(xiàn)象:子進(jìn)程打印的信息被擠到Shell命令提示符(vincent@ubuntu:~/ch05/5.2$)之后!造成這個結(jié)果的原因是:Shell命令提示符默認(rèn)會在父進(jìn)程退出之后立即顯示出來,而父進(jìn)程退出之時,子進(jìn)程還沒來得及執(zhí)行完第11行。 由于父子進(jìn)程的并發(fā)性,以上程序的執(zhí)行效果是不一定的,換句話說,們?nèi)绻賵?zhí)行一遍代碼可能會得到這樣的效果: vincent@ubuntu:~/ch05/5.2$ ./fork [23900]: before fork() ... [23901]: after fork() ... [23900]: after fork() ... vincent@ubuntu:~/ch05/5.2$ 接下來一個脫口而出的疑問是:好不容易生了個孩子,但是干的事情跟父進(jìn)程是一樣的,那我們要這個孩子有何用呢?答案是:上述代碼確實沒有什么實際意義,事實上我們一般會讓孩子去執(zhí)行一個預(yù)先準(zhǔn)備好的ELF文件或腳本,用以覆蓋從父進(jìn)程復(fù)制過來的代碼,下面先介紹這個加載ELF文件或腳本的接口函數(shù),如表5-2所示。 表5-2 函數(shù)族exec( )的接口規(guī)范
上述代碼組成一個所謂的“exec函數(shù)簇”,因為它們都“長”得差不多,功能都是一樣的,彼此間有些許區(qū)別(詳見表5-2中的備注)。使用這些函數(shù)還要注意以下事實。 (1)被加載的文件的參數(shù)列表必須以自身名字為開始,以NULL為結(jié)尾。比如要加載執(zhí)行當(dāng)前目錄下的一個名為a.out的文件,需要一個參數(shù)“abcd”,那么正確的調(diào)用應(yīng)該是: execl("./a.out", "a.out", "abcd", NULL); 或者: const char *argv[3] = {"a.out", "abcd", NULL}; execv("./a.out", argv); (2)exec函數(shù)簇成功執(zhí)行后,原有的程序代碼都將被指定的文件或腳本覆蓋,因此這些函數(shù)一旦成功,后面的代碼是無法執(zhí)行的,它們也是無法返回的。 下面展示子進(jìn)程被創(chuàng)建出來之后執(zhí)行的代碼,以及如何加載這個指定的程序。被子進(jìn)程加載的示例代碼如下。 vincent@ubuntu:~/ch05/5.2$ cat child_elf.c -n 1 #include <stdio.h> 2 #include <stdlib.h> 3 4 int main(void) 5 { 6 printf("[%d]: yep, I am the child\n", (int)getpid()); 7 exit(0); 8 } 下面是使用exec函數(shù)簇中的execl來讓子進(jìn)程加載上述代碼的示例。 vincent@ubuntu:~/ch05/5.2$ cat exec.c -n 1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 5 int main(int argc, char **argv) 6 { 7 pid_t x; 8 x = fork(); 9 10 if(x > 0) //父進(jìn)程 11 { 12 printf("[%d]: I am the parent\n", (int)getpid()); 13 exit(0); 14 } 15 16 if(x == 0) //子進(jìn)程 17 { 18 printf("[%d]: I am the child\n", (int)getpid()); 19 execl("./child_elf", "child_elf", NULL); //執(zhí)行child_elf程序 20 21 printf("NEVER be printed\n"); //這是一條將被覆蓋的代碼 22 } 23 24 return 0; 25 } 下面是執(zhí)行結(jié)果: vincent@ubuntu:~/ch05/5.2$ ./exec [24585]: I am the parent vincent@ubuntu:~/ch05/5.2$ [24586]: I am the child [24586]: yep, I am the child 從以上執(zhí)行結(jié)果看到,父進(jìn)程比其子進(jìn)程先執(zhí)行完代碼并退出,因此Shell命令提示行又被夾在中間了,那么怎么讓子進(jìn)程先運行并退出之后,父進(jìn)程再繼續(xù)呢?子進(jìn)程的退出狀態(tài)又怎么傳遞給父進(jìn)程呢?答案是:可以使用exit( )/_exit( )來退出并傳遞退出值,使用wait( )/waitpid( )來使父進(jìn)程阻塞等待子進(jìn)程,順便還可以幫子進(jìn)程收尸,這幾個函數(shù)的接口如表5-3所示。 表5-3 函數(shù)exit()和_exit()的接口規(guī)范
以下代碼展示了exit( )和_exit( )的用法和區(qū)別。 vincent@ubuntu:~/ch05/5.2$ cat exit.c -n 1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 5 void routine1(void) //退出處理函數(shù) 6 { 7 printf("routine1 is called.\n"); 8 } 9 10 void routine2(void) //退出處理函數(shù) 11 { 12 printf("routine2 is called.\n"); 13 } 14 15 int main(int argc, char **argv) 16 { 17 atexit(routine1); //注冊退出處理函數(shù) 18 atexit(routine2); 19 20 fprintf(stdout, "abcdef"); //將數(shù)據(jù)輸送至標(biāo)準(zhǔn)IO緩沖區(qū) 21 22 #ifdef _EXIT 23 _exit(0); //直接退出 24 #else 25 exit(0); //沖洗緩沖區(qū)數(shù)據(jù),并執(zhí)行退出處理函數(shù) 26 #endif 27 } vincent@ubuntu:~/ch05/5.2$ gcc exit.c -o exit vincent@ubuntu:~/ch05/5.2$ ./exit abcdefroutine2 is called. routine1 is called. vincent@ubuntu:~/ch05/5.2$ gcc exit.c -o exit -D_EXIT vincent@ubuntu:~/ch05/5.2$ ./exit vincent@ubuntu:~/ch05/5.2$ 通過以上操作可見,如果編譯時不加-D_EXIT,那么程序?qū)䦂?zhí)行exit(0),那么字符串a(chǎn)bcdef和兩個退出處理函數(shù)(所謂的“退出處理函數(shù)”指的是進(jìn)程使用exit( )退出時被自動執(zhí)行的函數(shù),需要使用atexit( )來注冊)都被相應(yīng)地處理了。而如果編譯時加了-D_EXIT的話,那么程序?qū)?zhí)行_exit(0),從執(zhí)行結(jié)果看,緩沖區(qū)數(shù)據(jù)沒有被沖洗,退出處理函數(shù)也沒有被執(zhí)行。 這兩個函數(shù)的參數(shù)status是該進(jìn)程的退出值,進(jìn)程退出后狀態(tài)切換為EXIT_ZOMBIE,相應(yīng)地,這個值將會被放在該進(jìn)程的“尸體”(PCB)里面,等待父進(jìn)程回收。在進(jìn)程異常退出時,有時需要向父進(jìn)程匯報異常情況,此時就用非零值來代表特定的異常情況,比如1代表權(quán)限不足、2代表內(nèi)存不夠等,具體情況只要父子進(jìn)程商定好就可以了。 接下來,父進(jìn)程如果需要,可以使用wait( )/waitpid( )來獲得子進(jìn)程正常退出的退出值,當(dāng)然,這兩個函數(shù)還可以使得父進(jìn)程阻塞等待子進(jìn)程的退出,以及將子進(jìn)程狀態(tài)切換為EXIT_DEAD,以便于系統(tǒng)釋放子進(jìn)程資源。表5-5所示是這兩個函數(shù)的接口。 表5-4 函數(shù)wait()和waitpid()的接口規(guī)范
續(xù)表
注意,所謂的退出狀態(tài)不是退出值,退出狀態(tài)包括了退出值。如果使用以上兩個函數(shù)成功獲取了子進(jìn)程的退出狀態(tài),則可以使用以下宏來進(jìn)一步解析,如表5-5所示。 表5-5 處理子進(jìn)程退出狀態(tài)值的宏
注: ① 正常退出指的是調(diào)用exit( )/_exit( ),或者在主函數(shù)中調(diào)用return,或者在最后一個線程調(diào)用pthread_exit( )。 ② 由于沒有在POSXI.1—2001標(biāo)準(zhǔn)中定義,這個選項在某些UNIX系統(tǒng)中無效,比如AIX或者sunOS中。 以下示例代碼,綜合展示了如何正確使用fork( )/exec( )函數(shù)簇、exit( )/_exit( )和wait( )/waitpid( )。程序的功能是:父進(jìn)程產(chǎn)生一個子進(jìn)程讓它去程序child_elf,并且等待它的退出(可以用wait( )阻塞等待,也可以用waitpid( )非阻塞等待),子進(jìn)程退出(可以正常退出,也可以異常退出)后,父進(jìn)程獲取子進(jìn)程的退出狀態(tài)后打印出來。詳細(xì)代碼如下。 vincent@ubuntu:~/ch05/5.2$ cat child_elf.c -n 1 #include <stdio.h> 2 #include <stdlib.h> 3 4 int main(void) 5 { 6 printf("[%d]: yep, I am the child\n", (int)getpid()); 7 8 #ifdef ABORT 9 abort(); //自己給自己發(fā)送一個致命信號SIGABRT,自殺 10 #else 11 exit(7); //正常退出,且退出值為7 12 #endif 13 }
vincent@ubuntu:~/ch05/5.2$ cat wait.c -n 1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <stdbool.h> 4 #include <unistd.h> 5 #include <string.h> 6 #include <strings.h> 7 #include <errno.h> 8 9 #include <sys/stat.h> 10 #include <sys/types.h> 11 #include <fcntl.h> 12 13 int main(int argc, char **argv) 14 { 15 pid_t x = fork(); 16 17 if(x == 0) //子進(jìn)程,執(zhí)行指定程序child_elf 18 { 19 execl("./child_elf", "child_elf", NULL); 20 } 21 22 if(x > 0) //父進(jìn)程,使用wait( )阻塞等待子進(jìn)程的退出 23 { 24 int status; 25 wait(&status); 26 27 if(WIFEXITED(status)) //判斷子進(jìn)程是否正常退出 28 { 29 printf("child exit normally, " 30 "exit value: %hhu\n", WEXITSTATUS(status)); 31 } 32 33 if(WIFSIGNALED(status)) //判斷子進(jìn)程是否被信號殺死 34 { 35 printf("child killed by signal: %u\n", 36 WTERMSIG(status)); 37 } 38 } 39 40 return 0; 41 } 執(zhí)行效果如下: vincent@ubuntu:~/ch05/5.2$ gcc child_elf.c -o child_elf vincent@ubuntu:~/ch05/5.2$ ./wait [26259]: yep, I am the child child exit normally, exit value: 7 vincent@ubuntu:~/ch05/5.2$ gcc child_elf.c -o child_elf -DABORT vincent@ubuntu:~/ch05/5.2$ ./wait [26266]: yep, I am the child child killed by signal: 6 vincent@ubuntu:~/ch05/5.2$ 可以看到,子進(jìn)程不同的退出情形,父進(jìn)程的確可以通過wait( )/waitpid( )和一些相應(yīng)的宏來獲取,這是協(xié)調(diào)父子進(jìn)程工作的一個重要途徑。 至此,我們已經(jīng)知道如何創(chuàng)建多進(jìn)程,以及掌握了它們的基本操作方法了,有一點是必須再提醒一次的:進(jìn)程它們是相互獨立的,最重要體現(xiàn)在它們互不干擾的內(nèi)存空間上,它們的數(shù)據(jù)是不共享的,但如果多個進(jìn)程需要協(xié)同合作,就必然會有數(shù)據(jù)共享的需求,就像人與人之間需要說話一樣,進(jìn)程需要通過某樣?xùn)|西來互相傳遞信息和數(shù)據(jù),這就是所謂的IPC(Inter-Process Comunication)機(jī)制,IPC有很多種,它們是如何使用的?有哪些特點?在什么場合適用?請看5.3節(jié)。
你還可能感興趣
我要評論
|