關(guān)于我們
書單推薦
新書推薦
|
Linux環(huán)境編程圖文指南(配視頻教程)
本書從零開始,循序漸進地攻破Linux環(huán)境編程所遇到的各級關(guān)卡,以圖文并茂的形式幫助讀者理解各個概念。本書內(nèi)容翔實,囊括了Linux系統(tǒng)操作細節(jié),Shell腳本編程精要,各種編程環(huán)境所需要解決的技術(shù)難點,以及在Linux環(huán)境下的C語言編程技術(shù)、并發(fā)編程技術(shù)和音/視頻編程等核心內(nèi)容。全書用400余幅圖表幫助讀者理解復雜概念,因此讀者不需要具備任何計算機編程經(jīng)驗,在本書的指導下就能進入編程的世界,并能在閱讀和實踐中享受編程的樂趣。同時,本書配套完整的視頻教程,給讀者以最直觀、最容易吸收知識的方式,融會貫通書中所有的知識點。不僅如此,讀者還能夠得到作者及其團隊的在線技術(shù)支援和答疑。 本書通俗易懂,適合從事Linux/UNIX編程開發(fā)、嵌入式開發(fā)、C環(huán)境開發(fā)的讀者,尤其適合計算機相關(guān)專業(yè)的高職院校的學生,以及希望轉(zhuǎn)向IT類就業(yè)方向的在職人士。
圖文結(jié)合視頻,全面精講Linux編程關(guān)鍵知識點; 只要你看,就能懂能會能用。
林世霖,國內(nèi)C編程專家,粵嵌最受歡迎培訓師和演講家,金牌講師,資深嵌入式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)驗。同時具有嫻熟的授課技巧和成體系化的教學經(jīng)驗,上課風格多樣化,善于并樂于傳播IT技術(shù),熱衷于教育行業(yè),常常與學生打成一片。
第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 復雜指針定義 147 2.3.5 指針運算 151 2.3.6 數(shù)組與指針 152 2.3.7 復雜數(shù)組剖析 155 2.3.8 const指針 158 2.3.9 char指針和char數(shù)組 160 2.4 內(nèi)存管理 162 2.4.1 進程內(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 復雜聲明 184 2.6.6 attribute機制 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 標準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進程線程 345 5.1 Linux進程入門 345 5.1.1 進程概念 345 5.1.2 進程組織方式 346 5.2 進程的“生老病死” 348 5.2.1 進程狀態(tài) 348 5.2.2 相關(guān)重要API 350 5.3 進程的語言 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 標準音頻接口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進程線程
5.1 Linux進程入門5.1.1 進程概念一個程序文件(Program),只是一堆待執(zhí)行的代碼和部分待處理的數(shù)據(jù),它們只有被加載到內(nèi)存中,然后讓CPU逐條執(zhí)行其代碼,根據(jù)代碼做出相應(yīng)的動作,才形成一個真正“活的”、動態(tài)的進程(Process)。因此,進程是一個動態(tài)變化的過程,是一出有始有終的戲,而程序文件只是這一系列動作的原始藍本,是一個靜態(tài)的劇本。 圖5-1更好地展示了程序和進程的關(guān)系。
圖5-1 ELF文件與進程虛擬內(nèi)存 圖5-1中的程序文件,是一個靜態(tài)的存儲于外部存儲器(如磁盤、flash等掉電非易失器件)之中的文件,里面包含了將來進程要運行的“劇本”,即執(zhí)行時會被復制到內(nèi)存的數(shù)據(jù)和代碼。除了這些部分,ELF格式中的大部分數(shù)據(jù)與程序本身的邏輯沒有關(guān)系,只是程序被加載到內(nèi)存中執(zhí)行時,系統(tǒng)需要處理的額外的輔助信息。另外注意.bss段,這里面放的是未初始化的靜態(tài)數(shù)據(jù),它們是不需要被復制的,具體解釋請參閱2.4.1節(jié)。 當這個ELF格式的程序被執(zhí)行時,內(nèi)核中實際上產(chǎn)生了一個名為task_struct{}的結(jié)構(gòu)體來表示這個進程。進程是一個“活動的實體”,這個活動的實體從一開始誕生就需要各種各樣的資源以便于生存下去,比如內(nèi)存資源、CPU資源、文件、信號、各種鎖資源等,所有這些東西都是動態(tài)變化的,這些信息都被事無巨細地一一記錄在結(jié)構(gòu)體task_struct之中,所以這個結(jié)構(gòu)體也常常稱為進程控制塊(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那么大,想想它包含了一個進程的所有信息,這么龐大也就不足為怪了。Linux內(nèi)核代碼紛繁復雜、千頭萬緒,這個結(jié)構(gòu)體是系統(tǒng)進程在執(zhí)行過程中所有涉及的方方面面的縮影,包括系統(tǒng)內(nèi)存管理子系統(tǒng)、進程調(diào)度子系統(tǒng)、虛擬文件系統(tǒng)等,以這個所謂的PCB為切入點,是一個很好的研究內(nèi)核的窗口。 總之,當一個程序文件被執(zhí)行時,內(nèi)核將會產(chǎn)生這么一個結(jié)構(gòu)體,來承載所有該活動實體日后運行時所需要的所有資源,隨著進程的運行,各種資源被分配和釋放,是一個動態(tài)的過程。 5.1.2 進程組織方式既然進程是一個動態(tài)的過程,有誕生的一刻,也就有死掉的一天,跟人類非常相似,人不可能無父無母,不可能突然從石頭中蹦出來,進程也一樣,每一個進程都必然有一個生它的父母(除了init),這個父母是一個被稱為“父進程”的進程。實際上可以用命令pstree來查看整個系統(tǒng)的進程關(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是一個用“樹狀”方式查看當前系統(tǒng)所有進程關(guān)系的命令,可以明顯看到它們的關(guān)系就像人類社會的族譜,大家都有一個共同的祖先init,每個人都可以生出幾個孩子(進程沒有性別,自己一個人就能生!)。其中祖先init是一個非常特別的進程,它沒有父進程!它是一個真正從石頭(操作系統(tǒng)啟動鏡像文件)中蹦出來的野孩子。 另外,每個進程都有自己的“身份證號碼”,即PID號,PID是重要的系統(tǒng)資源,它是用以區(qū)分各個進程的基本依據(jù),可以使用命令ps來查看進程的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列是每個進程的父進程的PID。既然進程有父子關(guān)系,進程可以生孩子,那么自然會有“生老病死”,欲知后事如何,且聽下節(jié)分解。 5.2 進程的“生老病死”5.2.1 進程狀態(tài)說進程是動態(tài)的活動的實體,指的是進程會有很多種運行狀態(tài),一會兒睡眠、一會兒暫停、一會兒又繼續(xù)執(zhí)行。如圖5-2所示為Linux進程從被創(chuàng)建(生)到被回收(死)的全部狀態(tài),以及這些狀態(tài)發(fā)生轉(zhuǎn)換時的條件。
圖5-2 Linux進程狀態(tài)轉(zhuǎn)換圖 結(jié)合圖5-2所示,一起看一下進程從生到死的過程。 (1)從“蛋生”可以看到,一個進程的誕生,是從其父進程調(diào)用fork( )開始的。 (2)進程剛被創(chuàng)建出來時,處于TASK_RUNNING狀態(tài),從圖5-2中可以看到,處于該狀態(tài)的進程可以是正在進程等待隊列中排隊,也可以占用CPU正在運行,我們習慣上稱前者為“就緒態(tài)”,后者為“執(zhí)行態(tài)”。當進程狀態(tài)為TASK_RUNNING并且占用CPU時才是真正運行。 (3)剛被創(chuàng)建的進程都處于“就緒”狀態(tài),等待系統(tǒng)調(diào)度,內(nèi)核中的函數(shù)sched( )稱為調(diào)度器,它會根據(jù)各種參數(shù)來選擇一個等待的進程去占用CPU。進程占用CPU之后就可以真正運行了,運行時間有個限定,比如20ms,這段時間稱為time slice,即“時間片”的概念。時間片耗光的情況下如果進程還沒有結(jié)束,那么會被系統(tǒng)重新放入等待隊列中等待。另外,正處于“執(zhí)行態(tài)”的進程即使時間片沒有耗光,也可能被別的更高優(yōu)先級的進程“搶占”CPU,被迫重新回到等待隊列中等待。 換句話說,進程跟人一樣,從來都沒有什么平等可言,有貴族就有屌絲,它們要處理的事情有不同的輕重緩急之分。 (4)進程處于“執(zhí)行態(tài)”時,可能會由于某些資源的不可得而被置為“睡眠態(tài)/掛起態(tài)”,比如進程要讀取一個管道文件數(shù)據(jù)而管道為空,或者進程要獲得一個鎖資源而當前鎖不可獲取,或者干脆進程自己調(diào)用sleep( )來強制自己掛起,這些情況下進程的狀態(tài)都會變成TASK_INTERRUPIBLE或TASK_UNINTERRUPIBLE,它們的區(qū)別是一般后者跟某些硬件設(shè)置相關(guān),在睡眠期間不能響應(yīng)信號,因此TASK_UNINTERRUPIBLE的狀態(tài)也稱為深度睡眠,相應(yīng)地TASK_INTERRUPIBLE期間進程是可以響應(yīng)信號的。當進程所等待的資源變得可獲取時,又會被系統(tǒng)置為TASK_RUNNING狀態(tài)重新就緒排隊。 (5)當進程收到SIGSTOP或SIGTSTP中的一個信號時,狀態(tài)會被置為TASK_STOPPED,此時稱為“暫停態(tài)”,該狀態(tài)下的進程不再參與調(diào)度,但系統(tǒng)資源不釋放,直到收到SIGCONT信號后被重新置為就緒態(tài)。當進程被追蹤時(典型情況是被調(diào)試器調(diào)試時),收到任何信號狀態(tài)都會被置為TASK_TRACED,該狀態(tài)與暫停態(tài)是一樣的,一直要等到SIGCONT才會重新參與系統(tǒng)進程調(diào)度。 (6)運行的進程跟人一樣,遲早都會死掉。進程的死亡可以有多種方式,可以是壽終正寢的正常退出,也可以是被異常殺死。比如圖5-2中,在main函數(shù)內(nèi)return或調(diào)用exit( ),包括在最后線程調(diào)用pthread_exit( )都是正常退出,而受到致命信號死掉的情況則是異常死亡,不管怎么死,最后內(nèi)核都會調(diào)用do_exit( )的函數(shù)來使得進程的狀態(tài)變成所謂的僵尸態(tài)EXIT_ZOMBIE,單詞ZOMBIE對于玩過“植物大戰(zhàn)僵尸”的讀者都不會陌生,這里的“僵尸”指的是進程的PCB(進程控制塊)。 為什么一個進程的死掉之后還要把尸體留下呢?因為進程在退出時,將其退出信息都封存在它的尸體里面了,比如如果它正常退出,那退出值是多少呢?如果被信號殺死,那么是哪個信號呢?這些“死亡信息”都被一一封存在該進程的PCB當中,好讓別人可以清楚地知道:我是怎么死的。 那誰會關(guān)心它是怎么死的呢?答案是它的父進程,它的父進程之所以要創(chuàng)建它,很大的原因是要讓這個孩子去干某一件事情,現(xiàn)在這個孩子已死,那事情辦得如何?孩子是否需要有個交代?但它又死掉了,所以之后將這些“死亡信息”封存在自己的尸體里面,等著父進程去查看。例如,父子進程可以約定:如果事情辦成了退出值為0;如果權(quán)限不足退出值為1;如果內(nèi)存不夠退出值為2;等等。父進程可以隨時查看一個已經(jīng)死去的孩子的PCB來確定事情究竟辦得如何?梢钥吹,在工業(yè)社會中,哪怕是進程間的協(xié)作,也充滿了契約精神。 (7)父進程調(diào)用wait( ) /waitpid( )來查看孩子的“死亡信息”,順便做一件非常重要的事情:將該孩子的狀態(tài)設(shè)置為EXIT_DEAD,即死亡態(tài),因為處于這個狀態(tài)的進程的PCB才能被系統(tǒng)回收。由此可見,父進程應(yīng)盡職盡責地及時調(diào)用wait( ) /waitpid( ),否則系統(tǒng)會充滿越來越多的“僵尸”! 問題是,如何保證父進程一定要及時地調(diào)用wait( ) /waitpid( )從而避免僵尸進程泛濫呢?答案是不能,因為父進程也許需要做別的事情沒空去幫那些死去的孩子收尸,甚至那些孩子在變成僵尸時,它的父進程已經(jīng)先它而去了! 后一種情況其實比較容易解決:如果一個進程的父進程退出,那么祖先進程init(該進程是系統(tǒng)第一個運行的進程,它的PCB是從內(nèi)核的啟動鏡像文件中直接加載的,不需要別的進程fork( )出來,因此它是無父無母的,系統(tǒng)中的所有其他進程都是它的后代)將會收養(yǎng)(adopt)這些孤兒進程。換句話說,Linux系統(tǒng)保證任何一個進程(除了init)都有父進程,也許是其真正的生父,也許是其祖先init。 而前一種情況是:父進程有別的事情要干,不能隨時執(zhí)行wait( ) /waitpid( )來確保回收僵尸資源。在這樣的情形下,我們可以考慮使用信號異步通知機制,讓一個孩子在變成僵尸時,給其父進程發(fā)一個信號,父進程接收到這個信號之后,對其進行處理,在此之前想干嘛就干嘛,異步操作。但即便是這樣也仍然存在問題:如果兩個以上的孩子同時退出變僵尸,那么它們就會同時給其父進程發(fā)送相同的信號,而相同的信號將會被淹沒。如何解決這個問題,請參閱5.3.2節(jié)。 5.2.2 相關(guān)重要API本節(jié)將詳細展示進程開發(fā)相關(guān)的API,第一個需要知道的接口函數(shù)當然是創(chuàng)建一個新的進程,如表5-1所示。 表5-1 函數(shù)fork( )的接口規(guī)范
這個函數(shù)接口本身非常簡單,簡單到連參數(shù)都沒有,但是這個函數(shù)有個與眾不同的地方:它會使得進程一分為二!就像細胞分裂一樣,如圖5-3所示。
圖5-3 細胞分裂 當一個進程調(diào)用fork( )成功后,fork( )將分別返回到兩個進程之中,換句話說,fork( )在父子兩個進程中都會返回,而它們所得到的返回值也不一樣,如圖5-4所示。 要著重注意如下幾點。 (1)fork( )會使得進程本身被復制(想想細胞分裂),因此被創(chuàng)建出來的子進程和父進程幾乎是一模一樣的,說“幾乎”意味著子進程并不是100%為一份父進程的復印件,它們的具體關(guān)系如下。
圖5-4 創(chuàng)建子進程的過程示意圖 父子進程的以下屬性在創(chuàng)建之初完全一樣,子進程相當于做了一份復制品。 l 實際UID和GID,以及有效UID和GID。 l 所有環(huán)境變量。 l 進程組ID和會話ID。 l 當前工作路徑。除非用chdir()加以修改。 l 打開的文件。 l 信號響應(yīng)函數(shù)。 l 整個內(nèi)存空間,包括棧、堆、數(shù)據(jù)段、代碼段、標準I/O的緩沖區(qū)等。 而以下屬性,父子進程是不一樣的。 l 進程號PID。PID是身份證號碼,哪怕親如父子,也要區(qū)分開。 l 記錄鎖。父進程對某文件加了把鎖,子進程不會繼承這把鎖。 l 掛起的信號。這些信號是所謂的“懸而未決”的信號,等待著進程的響應(yīng),子進程也不會繼承這些信號。 (2)子進程會從fork( )返回值后的下一條邏輯語句開始運行。這樣就避免了不斷調(diào)用fork( )而產(chǎn)生無限子孫的悖論。 (3)父子進程是相互平等的。它的執(zhí)行次序是隨機的,或者說它們是并發(fā)運行的,除非使用特殊機制來同步它們,否則不能判斷它們的運行究竟誰先誰后。 (4)父子進程是相互獨立的。由于子進程完整地復制了父進程的內(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( )展示了當前進程的PID,其中23900是父進程,23901是子進程。從執(zhí)行效果看還有一個很有意思的現(xiàn)象:子進程打印的信息被擠到Shell命令提示符(vincent@ubuntu:~/ch05/5.2$)之后!造成這個結(jié)果的原因是:Shell命令提示符默認會在父進程退出之后立即顯示出來,而父進程退出之時,子進程還沒來得及執(zhí)行完第11行。 由于父子進程的并發(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$ 接下來一個脫口而出的疑問是:好不容易生了個孩子,但是干的事情跟父進程是一樣的,那我們要這個孩子有何用呢?答案是:上述代碼確實沒有什么實際意義,事實上我們一般會讓孩子去執(zhí)行一個預先準備好的ELF文件或腳本,用以覆蓋從父進程復制過來的代碼,下面先介紹這個加載ELF文件或腳本的接口函數(shù),如表5-2所示。 表5-2 函數(shù)族exec( )的接口規(guī)范
上述代碼組成一個所謂的“exec函數(shù)簇”,因為它們都“長”得差不多,功能都是一樣的,彼此間有些許區(qū)別(詳見表5-2中的備注)。使用這些函數(shù)還要注意以下事實。 (1)被加載的文件的參數(shù)列表必須以自身名字為開始,以NULL為結(jié)尾。比如要加載執(zhí)行當前目錄下的一個名為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í)行的,它們也是無法返回的。 下面展示子進程被創(chuàng)建出來之后執(zhí)行的代碼,以及如何加載這個指定的程序。被子進程加載的示例代碼如下。 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來讓子進程加載上述代碼的示例。 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) //父進程 11 { 12 printf("[%d]: I am the parent\n", (int)getpid()); 13 exit(0); 14 } 15 16 if(x == 0) //子進程 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é)果看到,父進程比其子進程先執(zhí)行完代碼并退出,因此Shell命令提示行又被夾在中間了,那么怎么讓子進程先運行并退出之后,父進程再繼續(xù)呢?子進程的退出狀態(tài)又怎么傳遞給父進程呢?答案是:可以使用exit( )/_exit( )來退出并傳遞退出值,使用wait( )/waitpid( )來使父進程阻塞等待子進程,順便還可以幫子進程收尸,這幾個函數(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ù)輸送至標準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ù)”指的是進程使用exit( )退出時被自動執(zhí)行的函數(shù),需要使用atexit( )來注冊)都被相應(yīng)地處理了。而如果編譯時加了-D_EXIT的話,那么程序?qū)?zhí)行_exit(0),從執(zhí)行結(jié)果看,緩沖區(qū)數(shù)據(jù)沒有被沖洗,退出處理函數(shù)也沒有被執(zhí)行。 這兩個函數(shù)的參數(shù)status是該進程的退出值,進程退出后狀態(tài)切換為EXIT_ZOMBIE,相應(yīng)地,這個值將會被放在該進程的“尸體”(PCB)里面,等待父進程回收。在進程異常退出時,有時需要向父進程匯報異常情況,此時就用非零值來代表特定的異常情況,比如1代表權(quán)限不足、2代表內(nèi)存不夠等,具體情況只要父子進程商定好就可以了。 接下來,父進程如果需要,可以使用wait( )/waitpid( )來獲得子進程正常退出的退出值,當然,這兩個函數(shù)還可以使得父進程阻塞等待子進程的退出,以及將子進程狀態(tài)切換為EXIT_DEAD,以便于系統(tǒng)釋放子進程資源。表5-5所示是這兩個函數(shù)的接口。 表5-4 函數(shù)wait()和waitpid()的接口規(guī)范
續(xù)表
注意,所謂的退出狀態(tài)不是退出值,退出狀態(tài)包括了退出值。如果使用以上兩個函數(shù)成功獲取了子進程的退出狀態(tài),則可以使用以下宏來進一步解析,如表5-5所示。 表5-5 處理子進程退出狀態(tài)值的宏
注: ① 正常退出指的是調(diào)用exit( )/_exit( ),或者在主函數(shù)中調(diào)用return,或者在最后一個線程調(diào)用pthread_exit( )。 ② 由于沒有在POSXI.1—2001標準中定義,這個選項在某些UNIX系統(tǒng)中無效,比如AIX或者sunOS中。 以下示例代碼,綜合展示了如何正確使用fork( )/exec( )函數(shù)簇、exit( )/_exit( )和wait( )/waitpid( )。程序的功能是:父進程產(chǎn)生一個子進程讓它去程序child_elf,并且等待它的退出(可以用wait( )阻塞等待,也可以用waitpid( )非阻塞等待),子進程退出(可以正常退出,也可以異常退出)后,父進程獲取子進程的退出狀態(tài)后打印出來。詳細代碼如下。 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) //子進程,執(zhí)行指定程序child_elf 18 { 19 execl("./child_elf", "child_elf", NULL); 20 } 21 22 if(x > 0) //父進程,使用wait( )阻塞等待子進程的退出 23 { 24 int status; 25 wait(&status); 26 27 if(WIFEXITED(status)) //判斷子進程是否正常退出 28 { 29 printf("child exit normally, " 30 "exit value: %hhu\n", WEXITSTATUS(status)); 31 } 32 33 if(WIFSIGNALED(status)) //判斷子進程是否被信號殺死 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$ 可以看到,子進程不同的退出情形,父進程的確可以通過wait( )/waitpid( )和一些相應(yīng)的宏來獲取,這是協(xié)調(diào)父子進程工作的一個重要途徑。 至此,我們已經(jīng)知道如何創(chuàng)建多進程,以及掌握了它們的基本操作方法了,有一點是必須再提醒一次的:進程它們是相互獨立的,最重要體現(xiàn)在它們互不干擾的內(nèi)存空間上,它們的數(shù)據(jù)是不共享的,但如果多個進程需要協(xié)同合作,就必然會有數(shù)據(jù)共享的需求,就像人與人之間需要說話一樣,進程需要通過某樣東西來互相傳遞信息和數(shù)據(jù),這就是所謂的IPC(Inter-Process Comunication)機制,IPC有很多種,它們是如何使用的?有哪些特點?在什么場合適用?請看5.3節(jié)。
你還可能感興趣
我要評論
|