關(guān)于我們
書單推薦
新書推薦
|
Linux環(huán)境編程圖文指南(配視頻教程)
本書從零開始,循序漸進(jìn)地攻破Linux環(huán)境編程所遇到的各級(jí)關(guān)卡,以圖文并茂的形式幫助讀者理解各個(gè)概念。本書內(nèi)容翔實(shí),囊括了Linux系統(tǒng)操作細(xì)節(jié),Shell腳本編程精要,各種編程環(huán)境所需要解決的技術(shù)難點(diǎn),以及在Linux環(huán)境下的C語(yǔ)言編程技術(shù)、并發(fā)編程技術(shù)和音/視頻編程等核心內(nèi)容。全書用400余幅圖表幫助讀者理解復(fù)雜概念,因此讀者不需要具備任何計(jì)算機(jī)編程經(jīng)驗(yàn),在本書的指導(dǎo)下就能進(jìn)入編程的世界,并能在閱讀和實(shí)踐中享受編程的樂趣。同時(shí),本書配套完整的視頻教程,給讀者以最直觀、最容易吸收知識(shí)的方式,融會(huì)貫通書中所有的知識(shí)點(diǎn)。不僅如此,讀者還能夠得到作者及其團(tuán)隊(duì)的在線技術(shù)支援和答疑。 本書通俗易懂,適合從事Linux/UNIX編程開發(fā)、嵌入式開發(fā)、C環(huán)境開發(fā)的讀者,尤其適合計(jì)算機(jī)相關(guān)專業(yè)的高職院校的學(xué)生,以及希望轉(zhuǎn)向IT類就業(yè)方向的在職人士。
圖文結(jié)合視頻,全面精講Linux編程關(guān)鍵知識(shí)點(diǎn); 只要你看,就能懂能會(huì)能用。
林世霖,國(guó)內(nèi)C編程專家,粵嵌最受歡迎培訓(xùn)師和演講家,金牌講師,資深嵌入式Linux研發(fā)工程師。目前主要致力于Linux應(yīng)用軟件及系統(tǒng)開發(fā)和研究,十余年嵌入式系統(tǒng)軟件開發(fā)經(jīng)驗(yàn)。而且精通數(shù)據(jù)結(jié)構(gòu)算法與實(shí)現(xiàn),以及SCO系統(tǒng)下shell編程與系統(tǒng)編程,有豐富的銀行交易系統(tǒng)開發(fā)經(jīng)驗(yàn)。同時(shí)具有嫻熟的授課技巧和成體系化的教學(xué)經(jīng)驗(yàn),上課風(fēng)格多樣化,善于并樂于傳播IT技術(shù),熱衷于教育行業(yè),常常與學(xué)生打成一片。
第1章 Linux編程環(huán)境 1 1.1 基本工具 1 1.1.1 免費(fèi)大餐: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 開場(chǎng)白 45 1.3.2 腳本格式 45 1.3.3 變量 46 1.3.4 特殊符號(hào)們 48 1.3.5 字符串處理 50 1.3.6 測(cè)試語(yǔ)句 51 1.3.7 腳本語(yǔ)法單元 52 1.4 編譯器:GCC 55 1.4.1 簡(jiǎn)述 55 1.4.2 編譯過程簡(jiǎn)介 55 1.4.3 實(shí)用的編譯選項(xiàng) 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 實(shí)用make選項(xiàng)集錦 85 1.6 GNU-autotools 86 1.6.1 autotools簡(jiǎn)介 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 運(yùn)算符 108 2.1.4 控制流 116 2.2 函數(shù) 124 2.2.1 函數(shù)初體驗(yàn) 125 2.2.2 函數(shù)調(diào)用內(nèi)幕 128 2.2.3 遞歸思維及其實(shí)現(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 指針運(yùn)算 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 高級(jí)議題 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 開場(chǎng)白 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 文明的社會(huì):隊(duì)列 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)簡(jiǎn)介 330 4.4.2 TSLIB庫(kù)詳解 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)程的語(yǔ)言 358 5.3.1 管道 358 5.3.2 信號(hào) 363 5.3.3 system-V IPC簡(jiǎn)介 380 5.3.4 消息隊(duì)列(MSG) 381 5.3.5 共享內(nèi)存(SHM) 387 5.3.6 信號(hào)量(SEM) 392 5.4 Linux線程入門 400 5.4.1 線程基本概念 400 5.4.2 線程API及特點(diǎn) 401 5.5 線程安全 410 5.5.1 POSIX信號(hào)量 410 5.5.2 互斥鎖與讀寫鎖 415 5.5.3 條件變量 418 5.5.4 可重入函數(shù) 421 5.6 線程池 422 5.6.1 實(shí)現(xiàn)原理 422 5.6.2 接口設(shè)計(jì) 423 5.6.3 實(shí)現(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簡(jiǎn)介 478 6.4.2 V4L2視頻采集流程 478 6.4.3 V4L2核心命令字和結(jié)構(gòu)體 481 6.4.4 編碼格式和媒體流 484 6.5 多媒體開發(fā)庫(kù)SDL 489 6.5.1 SDL簡(jiǎn)介 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 音/視頻編解碼庫(kù)FFmpeg 504 6.6.1 FFmpeg簡(jiǎn)介 504 6.6.2 核心結(jié)構(gòu)體與常用API 505 6.6.3 與SDL結(jié)合實(shí)現(xiàn)簡(jiǎn)單的播放器 511
第5章Linux進(jìn)程線程
5.1 Linux進(jìn)程入門5.1.1 進(jìn)程概念一個(gè)程序文件(Program),只是一堆待執(zhí)行的代碼和部分待處理的數(shù)據(jù),它們只有被加載到內(nèi)存中,然后讓CPU逐條執(zhí)行其代碼,根據(jù)代碼做出相應(yīng)的動(dòng)作,才形成一個(gè)真正“活的”、動(dòng)態(tài)的進(jìn)程(Process)。因此,進(jìn)程是一個(gè)動(dòng)態(tài)變化的過程,是一出有始有終的戲,而程序文件只是這一系列動(dòng)作的原始藍(lán)本,是一個(gè)靜態(tài)的劇本。 圖5-1更好地展示了程序和進(jìn)程的關(guān)系。
圖5-1 ELF文件與進(jìn)程虛擬內(nèi)存 圖5-1中的程序文件,是一個(gè)靜態(tài)的存儲(chǔ)于外部存儲(chǔ)器(如磁盤、flash等掉電非易失器件)之中的文件,里面包含了將來進(jìn)程要運(yùn)行的“劇本”,即執(zhí)行時(shí)會(huì)被復(fù)制到內(nèi)存的數(shù)據(jù)和代碼。除了這些部分,ELF格式中的大部分?jǐn)?shù)據(jù)與程序本身的邏輯沒有關(guān)系,只是程序被加載到內(nèi)存中執(zhí)行時(shí),系統(tǒng)需要處理的額外的輔助信息。另外注意.bss段,這里面放的是未初始化的靜態(tài)數(shù)據(jù),它們是不需要被復(fù)制的,具體解釋請(qǐng)參閱2.4.1節(jié)。 當(dāng)這個(gè)ELF格式的程序被執(zhí)行時(shí),內(nèi)核中實(shí)際上產(chǎn)生了一個(gè)名為task_struct{}的結(jié)構(gòu)體來表示這個(gè)進(jìn)程。進(jìn)程是一個(gè)“活動(dòng)的實(shí)體”,這個(gè)活動(dòng)的實(shí)體從一開始誕生就需要各種各樣的資源以便于生存下去,比如內(nèi)存資源、CPU資源、文件、信號(hào)、各種鎖資源等,所有這些東西都是動(dòng)態(tài)變化的,這些信息都被事無巨細(xì)地一一記錄在結(jié)構(gòu)體task_struct之中,所以這個(gè)結(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; …… 如果沒什么意外,這個(gè)結(jié)構(gòu)體可能是最大的單個(gè)變量了,一個(gè)結(jié)構(gòu)體就有好幾KB那么大,想想它包含了一個(gè)進(jìn)程的所有信息,這么龐大也就不足為怪了。Linux內(nèi)核代碼紛繁復(fù)雜、千頭萬(wàn)緒,這個(gè)結(jié)構(gòu)體是系統(tǒng)進(jìn)程在執(zhí)行過程中所有涉及的方方面面的縮影,包括系統(tǒng)內(nèi)存管理子系統(tǒng)、進(jìn)程調(diào)度子系統(tǒng)、虛擬文件系統(tǒng)等,以這個(gè)所謂的PCB為切入點(diǎn),是一個(gè)很好的研究?jī)?nèi)核的窗口。 總之,當(dāng)一個(gè)程序文件被執(zhí)行時(shí),內(nèi)核將會(huì)產(chǎn)生這么一個(gè)結(jié)構(gòu)體,來承載所有該活動(dòng)實(shí)體日后運(yùn)行時(shí)所需要的所有資源,隨著進(jìn)程的運(yùn)行,各種資源被分配和釋放,是一個(gè)動(dòng)態(tài)的過程。 5.1.2 進(jìn)程組織方式既然進(jìn)程是一個(gè)動(dòng)態(tài)的過程,有誕生的一刻,也就有死掉的一天,跟人類非常相似,人不可能無父無母,不可能突然從石頭中蹦出來,進(jìn)程也一樣,每一個(gè)進(jìn)程都必然有一個(gè)生它的父母(除了init),這個(gè)父母是一個(gè)被稱為“父進(jìn)程”的進(jìn)程。實(shí)際上可以用命令pstree來查看整個(gè)系統(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是一個(gè)用“樹狀”方式查看當(dāng)前系統(tǒng)所有進(jìn)程關(guān)系的命令,可以明顯看到它們的關(guān)系就像人類社會(huì)的族譜,大家都有一個(gè)共同的祖先init,每個(gè)人都可以生出幾個(gè)孩子(進(jìn)程沒有性別,自己一個(gè)人就能生!)。其中祖先init是一個(gè)非常特別的進(jìn)程,它沒有父進(jìn)程!它是一個(gè)真正從石頭(操作系統(tǒng)啟動(dòng)鏡像文件)中蹦出來的野孩子。 另外,每個(gè)進(jìn)程都有自己的“身份證號(hào)碼”,即PID號(hào),PID是重要的系統(tǒng)資源,它是用以區(qū)分各個(gè)進(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列是每個(gè)進(jìn)程的父進(jìn)程的PID。既然進(jìn)程有父子關(guān)系,進(jìn)程可以生孩子,那么自然會(huì)有“生老病死”,欲知后事如何,且聽下節(jié)分解。 5.2 進(jìn)程的“生老病死”5.2.1 進(jìn)程狀態(tài)說進(jìn)程是動(dòng)態(tài)的活動(dòng)的實(shí)體,指的是進(jìn)程會(huì)有很多種運(yùn)行狀態(tài),一會(huì)兒睡眠、一會(huì)兒暫停、一會(huì)兒又繼續(xù)執(zhí)行。如圖5-2所示為L(zhǎng)inux進(jìn)程從被創(chuàng)建(生)到被回收(死)的全部狀態(tài),以及這些狀態(tài)發(fā)生轉(zhuǎn)換時(shí)的條件。
圖5-2 Linux進(jìn)程狀態(tài)轉(zhuǎn)換圖 結(jié)合圖5-2所示,一起看一下進(jìn)程從生到死的過程。 (1)從“蛋生”可以看到,一個(gè)進(jìn)程的誕生,是從其父進(jìn)程調(diào)用fork( )開始的。 (2)進(jìn)程剛被創(chuàng)建出來時(shí),處于TASK_RUNNING狀態(tài),從圖5-2中可以看到,處于該狀態(tài)的進(jìn)程可以是正在進(jìn)程等待隊(duì)列中排隊(duì),也可以占用CPU正在運(yùn)行,我們習(xí)慣上稱前者為“就緒態(tài)”,后者為“執(zhí)行態(tài)”。當(dāng)進(jìn)程狀態(tài)為TASK_RUNNING并且占用CPU時(shí)才是真正運(yùn)行。 (3)剛被創(chuàng)建的進(jìn)程都處于“就緒”狀態(tài),等待系統(tǒng)調(diào)度,內(nèi)核中的函數(shù)sched( )稱為調(diào)度器,它會(huì)根據(jù)各種參數(shù)來選擇一個(gè)等待的進(jìn)程去占用CPU。進(jìn)程占用CPU之后就可以真正運(yùn)行了,運(yùn)行時(shí)間有個(gè)限定,比如20ms,這段時(shí)間稱為time slice,即“時(shí)間片”的概念。時(shí)間片耗光的情況下如果進(jìn)程還沒有結(jié)束,那么會(huì)被系統(tǒng)重新放入等待隊(duì)列中等待。另外,正處于“執(zhí)行態(tài)”的進(jìn)程即使時(shí)間片沒有耗光,也可能被別的更高優(yōu)先級(jí)的進(jìn)程“搶占”CPU,被迫重新回到等待隊(duì)列中等待。 換句話說,進(jìn)程跟人一樣,從來都沒有什么平等可言,有貴族就有屌絲,它們要處理的事情有不同的輕重緩急之分。 (4)進(jìn)程處于“執(zhí)行態(tài)”時(shí),可能會(huì)由于某些資源的不可得而被置為“睡眠態(tài)/掛起態(tài)”,比如進(jìn)程要讀取一個(gè)管道文件數(shù)據(jù)而管道為空,或者進(jìn)程要獲得一個(gè)鎖資源而當(dāng)前鎖不可獲取,或者干脆進(jìn)程自己調(diào)用sleep( )來強(qiáng)制自己掛起,這些情況下進(jìn)程的狀態(tài)都會(huì)變成TASK_INTERRUPIBLE或TASK_UNINTERRUPIBLE,它們的區(qū)別是一般后者跟某些硬件設(shè)置相關(guān),在睡眠期間不能響應(yīng)信號(hào),因此TASK_UNINTERRUPIBLE的狀態(tài)也稱為深度睡眠,相應(yīng)地TASK_INTERRUPIBLE期間進(jìn)程是可以響應(yīng)信號(hào)的。當(dāng)進(jìn)程所等待的資源變得可獲取時(shí),又會(huì)被系統(tǒng)置為TASK_RUNNING狀態(tài)重新就緒排隊(duì)。 (5)當(dāng)進(jìn)程收到SIGSTOP或SIGTSTP中的一個(gè)信號(hào)時(shí),狀態(tài)會(huì)被置為TASK_STOPPED,此時(shí)稱為“暫停態(tài)”,該狀態(tài)下的進(jìn)程不再參與調(diào)度,但系統(tǒng)資源不釋放,直到收到SIGCONT信號(hào)后被重新置為就緒態(tài)。當(dāng)進(jìn)程被追蹤時(shí)(典型情況是被調(diào)試器調(diào)試時(shí)),收到任何信號(hào)狀態(tài)都會(huì)被置為TASK_TRACED,該狀態(tài)與暫停態(tài)是一樣的,一直要等到SIGCONT才會(huì)重新參與系統(tǒng)進(jìn)程調(diào)度。 (6)運(yùn)行的進(jìn)程跟人一樣,遲早都會(huì)死掉。進(jìn)程的死亡可以有多種方式,可以是壽終正寢的正常退出,也可以是被異常殺死。比如圖5-2中,在main函數(shù)內(nèi)return或調(diào)用exit( ),包括在最后線程調(diào)用pthread_exit( )都是正常退出,而受到致命信號(hào)死掉的情況則是異常死亡,不管怎么死,最后內(nèi)核都會(huì)調(diào)用do_exit( )的函數(shù)來使得進(jìn)程的狀態(tài)變成所謂的僵尸態(tài)EXIT_ZOMBIE,單詞ZOMBIE對(duì)于玩過“植物大戰(zhàn)僵尸”的讀者都不會(huì)陌生,這里的“僵尸”指的是進(jìn)程的PCB(進(jìn)程控制塊)。 為什么一個(gè)進(jìn)程的死掉之后還要把尸體留下呢?因?yàn)檫M(jìn)程在退出時(shí),將其退出信息都封存在它的尸體里面了,比如如果它正常退出,那退出值是多少呢?如果被信號(hào)殺死,那么是哪個(gè)信號(hào)呢?這些“死亡信息”都被一一封存在該進(jìn)程的PCB當(dāng)中,好讓別人可以清楚地知道:我是怎么死的。 那誰(shuí)會(huì)關(guān)心它是怎么死的呢?答案是它的父進(jìn)程,它的父進(jìn)程之所以要?jiǎng)?chuàng)建它,很大的原因是要讓這個(gè)孩子去干某一件事情,現(xiàn)在這個(gè)孩子已死,那事情辦得如何?孩子是否需要有個(gè)交代?但它又死掉了,所以之后將這些“死亡信息”封存在自己的尸體里面,等著父進(jìn)程去查看。例如,父子進(jìn)程可以約定:如果事情辦成了退出值為0;如果權(quán)限不足退出值為1;如果內(nèi)存不夠退出值為2;等等。父進(jìn)程可以隨時(shí)查看一個(gè)已經(jīng)死去的孩子的PCB來確定事情究竟辦得如何?梢钥吹剑诠I(yè)社會(huì)中,哪怕是進(jìn)程間的協(xié)作,也充滿了契約精神。 (7)父進(jìn)程調(diào)用wait( ) /waitpid( )來查看孩子的“死亡信息”,順便做一件非常重要的事情:將該孩子的狀態(tài)設(shè)置為EXIT_DEAD,即死亡態(tài),因?yàn)樘幱谶@個(gè)狀態(tài)的進(jìn)程的PCB才能被系統(tǒng)回收。由此可見,父進(jìn)程應(yīng)盡職盡責(zé)地及時(shí)調(diào)用wait( ) /waitpid( ),否則系統(tǒng)會(huì)充滿越來越多的“僵尸”! 問題是,如何保證父進(jìn)程一定要及時(shí)地調(diào)用wait( ) /waitpid( )從而避免僵尸進(jìn)程泛濫呢?答案是不能,因?yàn)楦高M(jìn)程也許需要做別的事情沒空去幫那些死去的孩子收尸,甚至那些孩子在變成僵尸時(shí),它的父進(jìn)程已經(jīng)先它而去了! 后一種情況其實(shí)比較容易解決:如果一個(gè)進(jìn)程的父進(jìn)程退出,那么祖先進(jìn)程init(該進(jìn)程是系統(tǒng)第一個(gè)運(yùn)行的進(jìn)程,它的PCB是從內(nèi)核的啟動(dòng)鏡像文件中直接加載的,不需要?jiǎng)e的進(jìn)程fork( )出來,因此它是無父無母的,系統(tǒng)中的所有其他進(jìn)程都是它的后代)將會(huì)收養(yǎng)(adopt)這些孤兒進(jìn)程。換句話說,Linux系統(tǒng)保證任何一個(gè)進(jìn)程(除了init)都有父進(jìn)程,也許是其真正的生父,也許是其祖先init。 而前一種情況是:父進(jìn)程有別的事情要干,不能隨時(shí)執(zhí)行wait( ) /waitpid( )來確;厥战┦Y源。在這樣的情形下,我們可以考慮使用信號(hào)異步通知機(jī)制,讓一個(gè)孩子在變成僵尸時(shí),給其父進(jìn)程發(fā)一個(gè)信號(hào),父進(jìn)程接收到這個(gè)信號(hào)之后,對(duì)其進(jìn)行處理,在此之前想干嘛就干嘛,異步操作。但即便是這樣也仍然存在問題:如果兩個(gè)以上的孩子同時(shí)退出變僵尸,那么它們就會(huì)同時(shí)給其父進(jìn)程發(fā)送相同的信號(hào),而相同的信號(hào)將會(huì)被淹沒。如何解決這個(gè)問題,請(qǐng)參閱5.3.2節(jié)。 5.2.2 相關(guān)重要API本節(jié)將詳細(xì)展示進(jìn)程開發(fā)相關(guān)的API,第一個(gè)需要知道的接口函數(shù)當(dāng)然是創(chuàng)建一個(gè)新的進(jìn)程,如表5-1所示。 表5-1 函數(shù)fork( )的接口規(guī)范
這個(gè)函數(shù)接口本身非常簡(jiǎn)單,簡(jiǎn)單到連參數(shù)都沒有,但是這個(gè)函數(shù)有個(gè)與眾不同的地方:它會(huì)使得進(jìn)程一分為二!就像細(xì)胞分裂一樣,如圖5-3所示。
圖5-3 細(xì)胞分裂 當(dāng)一個(gè)進(jìn)程調(diào)用fork( )成功后,fork( )將分別返回到兩個(gè)進(jìn)程之中,換句話說,fork( )在父子兩個(gè)進(jìn)程中都會(huì)返回,而它們所得到的返回值也不一樣,如圖5-4所示。 要著重注意如下幾點(diǎn)。 (1)fork( )會(huì)使得進(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 實(shí)際UID和GID,以及有效UID和GID。 l 所有環(huán)境變量。 l 進(jìn)程組ID和會(huì)話ID。 l 當(dāng)前工作路徑。除非用chdir()加以修改。 l 打開的文件。 l 信號(hào)響應(yīng)函數(shù)。 l 整個(gè)內(nèi)存空間,包括棧、堆、數(shù)據(jù)段、代碼段、標(biāo)準(zhǔn)I/O的緩沖區(qū)等。 而以下屬性,父子進(jìn)程是不一樣的。 l 進(jìn)程號(hào)PID。PID是身份證號(hào)碼,哪怕親如父子,也要區(qū)分開。 l 記錄鎖。父進(jìn)程對(duì)某文件加了把鎖,子進(jìn)程不會(huì)繼承這把鎖。 l 掛起的信號(hào)。這些信號(hào)是所謂的“懸而未決”的信號(hào),等待著進(jìn)程的響應(yīng),子進(jìn)程也不會(huì)繼承這些信號(hào)。 (2)子進(jìn)程會(huì)從fork( )返回值后的下一條邏輯語(yǔ)句開始運(yùn)行。這樣就避免了不斷調(diào)用fork( )而產(chǎn)生無限子孫的悖論。 (3)父子進(jìn)程是相互平等的。它的執(zhí)行次序是隨機(jī)的,或者說它們是并發(fā)運(yùn)行的,除非使用特殊機(jī)制來同步它們,否則不能判斷它們的運(yùn)行究竟誰(shuí)先誰(shuí)后。 (4)父子進(jìn)程是相互獨(dú)立的。由于子進(jìn)程完整地復(fù)制了父進(jìn)程的內(nèi)存空間,因此從內(nèi)存空間的角度看它們是相互獨(dú)立、互不影響的。 以下代碼顯示了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(); //生個(gè)孩子 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í)行效果看還有一個(gè)很有意思的現(xiàn)象:子進(jìn)程打印的信息被擠到Shell命令提示符(vincent@ubuntu:~/ch05/5.2$)之后!造成這個(gè)結(jié)果的原因是:Shell命令提示符默認(rèn)會(huì)在父進(jìn)程退出之后立即顯示出來,而父進(jìn)程退出之時(shí),子進(jìn)程還沒來得及執(zhí)行完第11行。 由于父子進(jìn)程的并發(fā)性,以上程序的執(zhí)行效果是不一定的,換句話說,們?nèi)绻賵?zhí)行一遍代碼可能會(huì)得到這樣的效果: vincent@ubuntu:~/ch05/5.2$ ./fork [23900]: before fork() ... [23901]: after fork() ... [23900]: after fork() ... vincent@ubuntu:~/ch05/5.2$ 接下來一個(gè)脫口而出的疑問是:好不容易生了個(gè)孩子,但是干的事情跟父進(jìn)程是一樣的,那我們要這個(gè)孩子有何用呢?答案是:上述代碼確實(shí)沒有什么實(shí)際意義,事實(shí)上我們一般會(huì)讓孩子去執(zhí)行一個(gè)預(yù)先準(zhǔn)備好的ELF文件或腳本,用以覆蓋從父進(jìn)程復(fù)制過來的代碼,下面先介紹這個(gè)加載ELF文件或腳本的接口函數(shù),如表5-2所示。 表5-2 函數(shù)族e(cuò)xec( )的接口規(guī)范
上述代碼組成一個(gè)所謂的“exec函數(shù)簇”,因?yàn)樗鼈兌?ldquo;長(zhǎng)”得差不多,功能都是一樣的,彼此間有些許區(qū)別(詳見表5-2中的備注)。使用這些函數(shù)還要注意以下事實(shí)。 (1)被加載的文件的參數(shù)列表必須以自身名字為開始,以NULL為結(jié)尾。比如要加載執(zhí)行當(dāng)前目錄下的一個(gè)名為a.out的文件,需要一個(gè)參數(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í)行的代碼,以及如何加載這個(gè)指定的程序。被子進(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)程先運(yùn)行并退出之后,父進(jìn)程再繼續(xù)呢?子進(jìn)程的退出狀態(tài)又怎么傳遞給父進(jìn)程呢?答案是:可以使用exit( )/_exit( )來退出并傳遞退出值,使用wait( )/waitpid( )來使父進(jìn)程阻塞等待子進(jìn)程,順便還可以幫子進(jìn)程收尸,這幾個(gè)函數(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); //注冊(cè)退出處理函數(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$ 通過以上操作可見,如果編譯時(shí)不加-D_EXIT,那么程序?qū)?huì)執(zhí)行exit(0),那么字符串a(chǎn)bcdef和兩個(gè)退出處理函數(shù)(所謂的“退出處理函數(shù)”指的是進(jìn)程使用exit( )退出時(shí)被自動(dòng)執(zhí)行的函數(shù),需要使用atexit( )來注冊(cè))都被相應(yīng)地處理了。而如果編譯時(shí)加了-D_EXIT的話,那么程序?qū)?zhí)行_exit(0),從執(zhí)行結(jié)果看,緩沖區(qū)數(shù)據(jù)沒有被沖洗,退出處理函數(shù)也沒有被執(zhí)行。 這兩個(gè)函數(shù)的參數(shù)status是該進(jìn)程的退出值,進(jìn)程退出后狀態(tài)切換為EXIT_ZOMBIE,相應(yīng)地,這個(gè)值將會(huì)被放在該進(jìn)程的“尸體”(PCB)里面,等待父進(jìn)程回收。在進(jìn)程異常退出時(shí),有時(shí)需要向父進(jìn)程匯報(bào)異常情況,此時(shí)就用非零值來代表特定的異常情況,比如1代表權(quán)限不足、2代表內(nèi)存不夠等,具體情況只要父子進(jìn)程商定好就可以了。 接下來,父進(jìn)程如果需要,可以使用wait( )/waitpid( )來獲得子進(jìn)程正常退出的退出值,當(dāng)然,這兩個(gè)函數(shù)還可以使得父進(jìn)程阻塞等待子進(jìn)程的退出,以及將子進(jìn)程狀態(tài)切換為EXIT_DEAD,以便于系統(tǒng)釋放子進(jìn)程資源。表5-5所示是這兩個(gè)函數(shù)的接口。 表5-4 函數(shù)wait()和waitpid()的接口規(guī)范
續(xù)表
注意,所謂的退出狀態(tài)不是退出值,退出狀態(tài)包括了退出值。如果使用以上兩個(gè)函數(shù)成功獲取了子進(jìn)程的退出狀態(tài),則可以使用以下宏來進(jìn)一步解析,如表5-5所示。 表5-5 處理子進(jìn)程退出狀態(tài)值的宏
注: ① 正常退出指的是調(diào)用exit( )/_exit( ),或者在主函數(shù)中調(diào)用return,或者在最后一個(gè)線程調(diào)用pthread_exit( )。 ② 由于沒有在POSXI.1—2001標(biāo)準(zhǔn)中定義,這個(gè)選項(xiàng)在某些UNIX系統(tǒng)中無效,比如AIX或者sunOS中。 以下示例代碼,綜合展示了如何正確使用fork( )/exec( )函數(shù)簇、exit( )/_exit( )和wait( )/waitpid( )。程序的功能是:父進(jìn)程產(chǎn)生一個(gè)子進(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ā)送一個(gè)致命信號(hào)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)程是否被信號(hào)殺死 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)程工作的一個(gè)重要途徑。 至此,我們已經(jīng)知道如何創(chuàng)建多進(jìn)程,以及掌握了它們的基本操作方法了,有一點(diǎn)是必須再提醒一次的:進(jìn)程它們是相互獨(dú)立的,最重要體現(xiàn)在它們互不干擾的內(nèi)存空間上,它們的數(shù)據(jù)是不共享的,但如果多個(gè)進(jìn)程需要協(xié)同合作,就必然會(huì)有數(shù)據(jù)共享的需求,就像人與人之間需要說話一樣,進(jìn)程需要通過某樣?xùn)|西來互相傳遞信息和數(shù)據(jù),這就是所謂的IPC(Inter-Process Comunication)機(jī)制,IPC有很多種,它們是如何使用的?有哪些特點(diǎn)?在什么場(chǎng)合適用?請(qǐng)看5.3節(jié)。
你還可能感興趣
我要評(píng)論
|