版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報或認(rèn)領(lǐng)
文檔簡介
3.3進(jìn)程控制3.3.1Linux下進(jìn)程概述(1)進(jìn)程相關(guān)基本概念<1>進(jìn)程的定義進(jìn)程的概念首先是在60年代初期由MIT的Multics系統(tǒng)和IBM的TSS/360系統(tǒng)引入的。經(jīng)過了40多年的發(fā)展,人們對進(jìn)程有過各種各樣的定義?,F(xiàn)列舉較為著名的幾種。進(jìn)程是一個獨立的可調(diào)度的活動(E.Cohen,D.Jofferson)進(jìn)程是一個抽象實體,當(dāng)它執(zhí)行某個任務(wù)時,將要分配和釋放各種資源(P.Denning)進(jìn)程是可以并行執(zhí)行的計算部分。(S.E.Madnick,J.T.Donovan)以上進(jìn)程的概念都不相同,但其本質(zhì)是一樣的。它指出了進(jìn)程是一個程序的一次執(zhí)行的過程。它和程序是有本質(zhì)區(qū)別的,程序是靜態(tài)的,它是一些保存在磁盤上的指令的有序集合,沒有任何執(zhí)行的概念;而進(jìn)程是一個動態(tài)的概念,它是程序執(zhí)行的過程,包括了動態(tài)創(chuàng)建、調(diào)度和消亡的整個過程。它是程序執(zhí)行和資源管理的最小單位。因此,對系統(tǒng)而言,當(dāng)用戶在系統(tǒng)中鍵入命令執(zhí)行一個程序的時候,它將啟動一個進(jìn)程。<2>進(jìn)程控制塊進(jìn)程是Linux系統(tǒng)的基本調(diào)度單位,那么從系統(tǒng)的角度看如何描述并表示它的變化呢?在這里,是通過進(jìn)程控制塊來描述的。進(jìn)程控制塊包含了進(jìn)程的描述信息、控制信息以及資源信息,它是進(jìn)程的一個靜態(tài)描述。在Linux中,進(jìn)程控制塊中的每一項都是一個task_struct結(jié)構(gòu),它是在include/linux/sched.h中定義的。<3>進(jìn)程的標(biāo)識在Linux中最主要的進(jìn)程標(biāo)識有進(jìn)程號(PID,ProcessIdenityNumber)和它的父進(jìn)程號(PPID,parentprocessID)。其中PID惟一地標(biāo)識一個進(jìn)程。PID和PPID都是非零的正整數(shù)。在Linux中獲得當(dāng)前進(jìn)程的PID和PPID的系統(tǒng)調(diào)用函數(shù)為getpid和getppid,通常程序獲得當(dāng)前進(jìn)程的PID和PPID可以將其寫入日志文件以做備份。getpid和getppid系統(tǒng)調(diào)用過程如下所示:/*process.c*/#include<stdio.h>#include<unistd.h>#include<stdlib.h>intmain(){/*獲得當(dāng)前進(jìn)程的進(jìn)程ID和其父進(jìn)程ID*/printf("ThePIDofthisprocessis%d\n",getpid());printf("ThePPIDofthisprocessis%d\n",getppid());}使用arm-linux-gcc進(jìn)行交叉編譯,再將其下載到目標(biāo)板上運行該程序,可以得到如下結(jié)果,該值在不同的系統(tǒng)上會有所不同:#./processThePIDofthisprocessis78THePPIDofthisprocessis36另外,進(jìn)程標(biāo)識還有用戶和用戶組標(biāo)識、進(jìn)程時間、資源利用情況等,這里就不做一一介紹,感興趣的讀者可以參見W.RichardStevens的《AdvancedProgrammingintheUNIXEnvironmen》。<4>進(jìn)程運行的狀態(tài)進(jìn)程是程序的執(zhí)行過程,根據(jù)它的生命期可以劃分成3種狀態(tài)。執(zhí)行態(tài):該進(jìn)程正在,即進(jìn)程正在占用CPU。就緒態(tài):進(jìn)程已經(jīng)具備執(zhí)行的一切條件,正在等待分配CPU的處理時間片。等待態(tài):進(jìn)程不能使用CPU,若等待事件發(fā)生則可將其喚醒。它們之間轉(zhuǎn)換的關(guān)系圖如圖所示。就緒就緒執(zhí)行等待處于睡眠狀態(tài)等待某個事件發(fā)生時間片到因等待事件發(fā)生而喚醒調(diào)度圖3.3進(jìn)程運行狀態(tài)轉(zhuǎn)換的關(guān)系圖(2)Linux下的進(jìn)程結(jié)構(gòu)Linux系統(tǒng)是一個多進(jìn)程的系統(tǒng),它的進(jìn)程之間具有并行性、互不干擾等特點。也就是說,進(jìn)程之間是分離的任務(wù),擁有各自的權(quán)利和責(zé)任。其中,每一個進(jìn)程都運行在各自獨立的虛擬地址空間,因此,即使一個進(jìn)程發(fā)生異常,它也不會影響到系統(tǒng)中的其他進(jìn)程。Linux中的進(jìn)程包含3個段,分別為“數(shù)據(jù)段”、“代碼段”和“堆棧段”?!皵?shù)據(jù)段”存放的是全局變量、常數(shù)以及動態(tài)數(shù)據(jù)分配的數(shù)據(jù)空間(如malloc函數(shù)取得的空間)等。“代碼段”存放的是程序代碼的數(shù)據(jù)?!岸褩6巍贝娣诺氖亲映绦虻姆祷氐刂贰⒆映绦虻膮?shù)以及程序的局部變量。(3)Linux下進(jìn)程的模式和類型在Linux系統(tǒng)中,進(jìn)程的執(zhí)行模式劃分為用戶模式和內(nèi)核模式。如果當(dāng)前運行的是用戶程序、應(yīng)用程序或者內(nèi)核之外的系統(tǒng)程序,那么對應(yīng)進(jìn)程就在用戶模式下運行;如果在用戶程序執(zhí)行過程中出現(xiàn)系統(tǒng)調(diào)用或者發(fā)生中斷事件,那么就要運行操作系統(tǒng)(即核心)程序,進(jìn)程模式就變成內(nèi)核模式。在內(nèi)核模式下運行的進(jìn)程可以執(zhí)行機(jī)器的特權(quán)指令,而且此時該進(jìn)程的運行不受用戶的干擾,即使是root用戶也不能干擾內(nèi)核模式下進(jìn)程的運行。用戶進(jìn)程既可以在用戶模式下運行,也可以在內(nèi)核模式下運行,如下圖所示。用戶進(jìn)程用戶進(jìn)程內(nèi)核進(jìn)程中斷或系統(tǒng)調(diào)用用戶態(tài)內(nèi)核態(tài)圖3.4用戶進(jìn)程的模式(4)Linux下的進(jìn)程管理Linux下的進(jìn)程管理包括啟動進(jìn)程和調(diào)度進(jìn)程,下面就分別對這兩方面進(jìn)行簡要講解。<1>啟動進(jìn)程Linux下啟動一個進(jìn)程有兩種主要途徑:手工啟動和調(diào)度啟動。手工啟動是由用戶輸入命令直接啟動進(jìn)程,而調(diào)度啟動是指系統(tǒng)根據(jù)用戶的設(shè)置自行啟動進(jìn)程。手工啟動手工啟動進(jìn)程又可分為前臺啟動和后臺啟動。前臺啟動是手工啟動一個進(jìn)程的最常用方式。一般地,當(dāng)用戶鍵入一個命令如“l(fā)s-l”時,就已經(jīng)啟動了一個進(jìn)程,并且是一個前臺的進(jìn)程。后臺啟動往往是在該進(jìn)程非常耗時,且用戶也不急著需要結(jié)果的時候啟動的。比如用戶要啟動一個需要長時間運行的格式化文本文件的進(jìn)程。為了不使整個shell在格式化過程中都處于“癱瘓”狀態(tài),從后臺啟動這個進(jìn)程是明智的選擇。調(diào)度啟動有時,系統(tǒng)需要進(jìn)行一些比較費時而且占用資源的維護(hù)工作,并且這些工作適合在深夜無人職守的時候進(jìn)行,這時用戶就可以事先進(jìn)行調(diào)度安排,指定任務(wù)運行的時間或者場合,到時候系統(tǒng)就會自動完成這一切工作。使用調(diào)度啟動進(jìn)程有幾個常用的命令,如at命令在指定時刻執(zhí)行相關(guān)進(jìn)程,cron命令可以自動周期性地執(zhí)行相關(guān)進(jìn)程,在需要使用時讀者可以查看相關(guān)幫助手冊。<2>調(diào)度進(jìn)程調(diào)度進(jìn)程包括對進(jìn)程的中斷操作、改變優(yōu)先級、查看進(jìn)程狀態(tài)等,在Linux下可以使用相關(guān)的系統(tǒng)命令實現(xiàn)其操作,下表列出了Linux中常見的調(diào)用進(jìn)程的系統(tǒng)命令,讀者在需要的時候可以自行查找其用法。表3.33Linux中常見的調(diào)用進(jìn)程的系統(tǒng)命令選項參數(shù)含義Ps查看系統(tǒng)中的進(jìn)程Top動態(tài)顯示系統(tǒng)中的進(jìn)程N(yùn)ice按用戶指定的優(yōu)先級運行Renice改變正在運行進(jìn)程的優(yōu)先級Kill終止進(jìn)程(包括后臺進(jìn)程)crontab用于安裝、刪除或者列出用于驅(qū)動cron后臺進(jìn)程的任務(wù)Bg將掛起的進(jìn)程放到后臺執(zhí)行3.3.2Linux進(jìn)程控制編程(1)進(jìn)程創(chuàng)建<1>fork()在Linux中創(chuàng)建一個新進(jìn)程的惟一方法是使用fork函數(shù)。fork函數(shù)是Linux中一個非常重要的函數(shù),和讀者以往遇到的函數(shù)也有很大的區(qū)別,它執(zhí)行一次卻返回兩個值。希望讀者能認(rèn)真地學(xué)習(xí)這一部分的內(nèi)容。fork函數(shù)說明fork函數(shù)用于從已存在進(jìn)程中創(chuàng)建一個新進(jìn)程。新進(jìn)程稱為子進(jìn)程,而原進(jìn)程稱為父進(jìn)程。這兩個分別帶回它們各自的返回值,其中父進(jìn)程的返回值是子進(jìn)程的進(jìn)程號,而子進(jìn)程則返回0。因此,可以通過返回值來判定該進(jìn)程是父進(jìn)程還是子進(jìn)程。使用fork函數(shù)得到的子進(jìn)程是父進(jìn)程的一個復(fù)制品,它從父進(jìn)程處繼承了整個進(jìn)程的地址空間,包括進(jìn)程上下文、進(jìn)程堆棧、內(nèi)存信息、打開的文件描述符、信號控制設(shè)定、進(jìn)程優(yōu)先級、進(jìn)程組號、當(dāng)前工作目錄、根目錄、資源限制、控制終端等,而子進(jìn)程所獨有的只有它的進(jìn)程號、資源使用和計時器等。因此可以看出,使用fork函數(shù)的代價是很大的,它復(fù)制了父進(jìn)程中的代碼段、數(shù)據(jù)段和堆棧段里的大部分內(nèi)容,使得fork函數(shù)的執(zhí)行速度并不很快。fork函數(shù)語法下表列出了fork函數(shù)的語法要點。表3.34fork函數(shù)的語法所需頭文件#include<sys/types.h>//提供類型pid_t的定義#include<unistd.h>函數(shù)原型pid_tfork(void)函數(shù)返回值0子進(jìn)程子進(jìn)程ID(大于0的整數(shù))父進(jìn)程-1出錯fork函數(shù)使用實例/*fork.c*/#include<sys/types.h>#include<unistd.h>#include<stdio.h>#include<stdlib.h>intmain(void){pid_tresult;/*調(diào)用fork函數(shù),其返回值為result*/result=fork();/*通過result的值來判斷fork函數(shù)的返回情況,首先進(jìn)行出錯處理*/if(result==-1){perror("fork");exit;}/*返回值為0代表子進(jìn)程*/elseif(result==0){printf("Thereturnvalueis%d\nInchildprocess!\nMyPIDis%d\n",result,getpid());}/*返回值大于0代表父進(jìn)程*/else{printf("Thereturnvalueis%d\nInfatherprocess!\nMyPIDis%d\n",result,getpid());}}編譯:#arm-linux-gccfork..c–ofork將可執(zhí)行程序下載到目標(biāo)板上,運行結(jié)果如下所示:Thereturnvaluds76Infatherprocess!!MyPIDis75Thereturnvalueis:0Inchildprocess!!MyPIDis76從該實例中可以看出,使用fork函數(shù)新建了一個子進(jìn)程,其中的父進(jìn)程返回子進(jìn)程的PID,而子進(jìn)程的返回值為0。函數(shù)使用注意點fork函數(shù)使用一次就創(chuàng)建一個進(jìn)程,所以若把fork函數(shù)放在了ifelse判斷語句中則要小心,不能多次使用fork函數(shù)。由于fork完整地拷貝了父進(jìn)程的整個地址空間,因此執(zhí)行速度是比較慢的。為了加快fork的執(zhí)行速度,有些UNIX系統(tǒng)設(shè)計者創(chuàng)建了vfork。vfork也能創(chuàng)建新進(jìn)程,但它不產(chǎn)生父進(jìn)程的副本。它是通過允許父子進(jìn)程可訪問相同物理內(nèi)存從而偽裝了對進(jìn)程地址空間的真實拷貝,當(dāng)子進(jìn)程需要改變內(nèi)存中數(shù)據(jù)時才拷貝父進(jìn)程。這就是著名的“寫操作時拷貝”(copy-on-write)技術(shù)?,F(xiàn)在很多嵌入式Linux系統(tǒng)的fork函數(shù)調(diào)用都采用vfork函數(shù)的實現(xiàn)方式,實際上uClinux所有的多進(jìn)程管理都通過vfork來實現(xiàn)。<2>exec函數(shù)族exec函數(shù)族說明fork函數(shù)是用于創(chuàng)建一個子進(jìn)程,該子進(jìn)程幾乎拷貝了父進(jìn)程的全部內(nèi)容,但是,這個新創(chuàng)建的進(jìn)程如何執(zhí)行呢?這個exec函數(shù)族就提供了一個在進(jìn)程中啟動另一個程序執(zhí)行的方法。它可以根據(jù)指定的文件名或目錄名找到可執(zhí)行文件,并用它來取代原調(diào)用進(jìn)程的數(shù)據(jù)段、代碼段和堆棧段,在執(zhí)行完之后,原調(diào)用進(jìn)程的內(nèi)容除了進(jìn)程號外,其他全部被新的進(jìn)程替換了。另外,這里的可執(zhí)行文件既可以是二進(jìn)制文件,也可以是Linux下任何可執(zhí)行的腳本文件。在Linux中使用exec函數(shù)族主要有兩種情況:當(dāng)進(jìn)程認(rèn)為自己不能再為系統(tǒng)和用戶做出任何貢獻(xiàn)時,就可以調(diào)用任何exec函數(shù)族讓自己重生;如果一個進(jìn)程想執(zhí)行另一個程序,那么它就可以調(diào)用fork函數(shù)新建一個進(jìn)程,然后調(diào)用任何一個exec,這樣看起來就好像通過執(zhí)行應(yīng)用程序而產(chǎn)生了一個新進(jìn)程。(這種情況非常普遍)exec函數(shù)族語法實際上,在Linux中并沒有exec()函數(shù),而是有6個以exec開頭的函數(shù)族,它們之間語法有細(xì)微差別。下表列舉了exec函數(shù)族的6個成員函數(shù)的語法。表3.35exec函數(shù)族的成員函數(shù)語法所需頭文件#include<unistd.h>函數(shù)原型intexecl(constchar*path,constchar*arg,...)intexecv(constchar*path,char*constargv[])intexecle(constchar*path,constchar*arg,...,char*constenvp[])intexecve(constchar*path,char*constargv[],char*constenvp[])intexeclp(constchar*file,constchar*arg,...)intexecvp(constchar*file,char*constargv[])函數(shù)返回值-1:出錯這6個函數(shù)在函數(shù)名和使用語法的規(guī)則上都有細(xì)微的區(qū)別,下面就可執(zhí)行文件查找方式、參數(shù)表傳遞方式及環(huán)境變量這幾個方面進(jìn)行比較。查找方式讀者可以注意到,上表中的前4個函數(shù)的查找方式都是完整的文件目錄路徑,而最后2個函數(shù)(也就是以p結(jié)尾的兩個函數(shù))可以只給出文件名,系統(tǒng)就會自動從環(huán)境變量“$PATH”所指出的路徑中進(jìn)行查找。參數(shù)傳遞方式exec函數(shù)族的參數(shù)傳遞有兩種方式:一種是逐個列舉的方式,而另一種則是將所有參數(shù)整體構(gòu)造指針數(shù)組傳遞。在這里是以函數(shù)名的第5位字母來區(qū)分的,字母為“l(fā)”(list)的表示逐個列舉的方式,其語法為char*arg;字母為“v”(vertor)的表示將所有參數(shù)整體構(gòu)造指針數(shù)組傳遞,其語法為*constargv[]。讀者可以觀察execl、execle、execlp的語法與execv、execve、execvp的區(qū)別。它們具體的用法在后面的實例講解中會舉例說明。這里的參數(shù)實際上就是用戶在使用這個可執(zhí)行文件時所需的全部命令選項字符串(包括該可執(zhí)行程序命令本身)。要注意的是,這些參數(shù)必須以NULL表示結(jié)束,如果使用逐個列舉方式,那么要把它強(qiáng)制轉(zhuǎn)化成一個字符指針,否則exec將會把它解釋為一個整型參數(shù),如果一個整型數(shù)的長度char*的長度不同,那么exec函數(shù)就會報錯。環(huán)境變量exec函數(shù)族可以默認(rèn)系統(tǒng)的環(huán)境變量,也可以傳入指定的環(huán)境變量。這里以“e”(Enviromen)結(jié)尾的兩個函數(shù)execle、execve就可以在envp[]中指定當(dāng)前進(jìn)程所使用的環(huán)境變量。下表再對這4個函數(shù)中函數(shù)名和對應(yīng)語法做一總結(jié),主要指出了函數(shù)名中每一位所表明的含義,希望讀者結(jié)合此表加以記憶。表3.36exec函數(shù)族中4個重要函數(shù)的語法前4位統(tǒng)一為:exec第5位l:參數(shù)傳遞為逐個列舉方式execl、execle、execlpv:參數(shù)傳遞為構(gòu)造指針數(shù)組方式execv、execve、execvp第6位e:可傳遞新進(jìn)程環(huán)境變量execle、execvep:可執(zhí)行文件查找方式為文件名execlp、execvpexec使用實例下面的第一個示例說明了如何使用文件名的方式來查找可執(zhí)行文件,同時使用參數(shù)列表的方式。這里用的函數(shù)是execlp。/*execlp.c*/#include<unistd.h>#include<stdio.h>#include<stdlib.h>intmain(){if(fork()==0){/*調(diào)用execlp函數(shù),這里相當(dāng)于調(diào)用了“ps-ef”命令*/if(execlp("ps","ps","-ef",NULL)<0)perror("execlperror!");}}在該程序中,首先使用fork函數(shù)新建一個子進(jìn)程,然后在子進(jìn)程里使用execlp函數(shù)。讀者可以看到,這里的參數(shù)列表就是在shell中使用的命令名和選項。并且當(dāng)使用文件名的方式進(jìn)行查找時,系統(tǒng)會在默認(rèn)的環(huán)境變量PATH中尋找該可執(zhí)行文件。讀者可將編譯后的結(jié)果下載到目標(biāo)板上,運行結(jié)果如下所示:#./execlpPIDTTYUidSizeStateCommand1root1832Sinit2root0S[keventd]3root0S[ksoftirqd_CPU0]4root0S[kswapd]5root0S[bdflush]6root0S[kupdated]7root0S[mtdblockd]8root0S[khubd]35root2104S/bin/bash/usr/etc/rc.local36root2324S/bin/bash41root1364S/sbin/inetd53root14260S/Qtopia/qtopia-free-1.7.0/bin/qpe-qws54root11672Squicklauncher65root0S[usb-storage-0]66root0S[scsi_eh_0]83root2020Rps-ef#envPATH=/Qtopia/qtopia-free-1.7.0/bin:/usr/bin:/bin:/usr/sbin:/sbin…此程序的運行結(jié)果與在Shell中直接鍵入命令“ps-ef”是一樣的,當(dāng)然,在不同的系統(tǒng)不同時刻都可能會有不同的結(jié)果。接下來的示例2使用完整的文件目錄來查找對應(yīng)的可執(zhí)行文件。注意目錄必須以“/”開頭,否則將其視為文件名。/*execl.c*/#include<unistd.h>#include<stdio.h>#include<stdlib.h>intmain(){if(fork()==0){/*調(diào)用execl函數(shù),注意這里要給出ps程序所在的完整路徑*/if(execl("/bin/ps","ps","-ef",NULL)<0)perror("execlerror!");}}同樣下載到目標(biāo)板上運行,運行結(jié)果同上例,如下所示:[davinci@davinci-desktop]#./execlPIDTTYUidSizeStateCommand1root1832Sinit2root0S[keventd]3root0S[ksoftirqd_CPU0]4root0S[kswapd]5root0S[bdflush]6root0S[kupdated]…示例3利用函數(shù)execle,將環(huán)境變量添加到新建的子進(jìn)程中去,這里的“env”是查看當(dāng)前進(jìn)程環(huán)境變量的命令,如下所示:/*execle*/#include<unistd.h>#include<stdio.h>#include<stdlib.h>intmain(){/*命令參數(shù)列表,必須以NULL結(jié)尾*/char*envp[]={"PATH=/tmp","USER=sunq",NULL};if(fork()==0){/*調(diào)用execle函數(shù),注意這里也要指出env的完整路徑*/if(execle("/bin/env","env",NULL,envp)<0)perror("execleerror!");}}下載到目標(biāo)板后的運行結(jié)果如下所示:#./execlePATH=/tmpUSER=sunq最后一個示例使用execve函數(shù),通過構(gòu)造指針數(shù)組的方式來傳遞參數(shù),注意參數(shù)列表一定要以NULL作為結(jié)尾標(biāo)識符。其代碼和運行結(jié)果如下所示:#include<unistd.h>#include<stdio.h>#include<stdlib.h>intmain(){/*命令參數(shù)列表,必須以NULL結(jié)尾*/char*arg[]={"env",NULL};char*envp[]={"PATH=/tmp","USER=sunq",NULL};if(fork()==0){if(execve("/bin/env",arg,,envp)<0)perror("execveerror!");}}下載到目標(biāo)板后的運行結(jié)果如下所示:#./execvePATH=/tmpUSER=sunqexec函數(shù)族使用注意點在使用exec函數(shù)族時,一定要加上錯誤判斷語句。因為exec很容易執(zhí)行失敗,其中最常見的原因有:找不到文件或路徑,此時errno被設(shè)置為ENOENT數(shù)組argv和envp忘記用NULL結(jié)束,此時errno被設(shè)置為EFAULT沒有對應(yīng)可執(zhí)行文件的運行權(quán)限,此時errno被設(shè)置為EACCES事實上,這6個函數(shù)中真正的系統(tǒng)調(diào)用只有execve,其他5個都是庫函數(shù),它們最終都會調(diào)用execve這個系統(tǒng)調(diào)用。<3>exit和_exitexit和_exit函數(shù)說明exit和_exit函數(shù)都是用來終止進(jìn)程的。當(dāng)程序執(zhí)行到exit或_exit時,進(jìn)程會無條件地停止剩下的所有操作,清除包括PCB在內(nèi)的各種數(shù)據(jù)結(jié)構(gòu),并終止本進(jìn)程的運行。但是,這兩個函數(shù)還是有區(qū)別的,這兩個函數(shù)的調(diào)用過程如圖所示。進(jìn)程運行進(jìn)程運行調(diào)用退出處理函數(shù)清理I/O緩沖exit系統(tǒng)調(diào)用進(jìn)程終止運行_exit()exit()圖3.5exit和_exit函數(shù)的調(diào)用過程從圖中可以看出,_exit()函數(shù)的作用是:直接使進(jìn)程停止運行,清除其使用的內(nèi)存空間,并清除其在內(nèi)核中的各種數(shù)據(jù)結(jié)構(gòu);exit()函數(shù)則在這些基礎(chǔ)上作了一些包裝,在執(zhí)行退出之前加了若干道工序。exit()函數(shù)與_exit()函數(shù)最大的區(qū)別就在于exit()函數(shù)在調(diào)用exit系統(tǒng)之前要檢查文件的打開情況,把文件緩沖區(qū)中的內(nèi)容寫回文件,就是圖中的“清理I/O緩沖”一項。由于在Linux的標(biāo)準(zhǔn)函數(shù)庫中,有一種被稱作“緩沖I/O(bufferedI/O)”操作,其特征就是對應(yīng)每一個打開的文件,在內(nèi)存中都有一片緩沖區(qū)。每次讀文件時,會連續(xù)讀出若干條記錄,這樣在下次讀文件時就可以直接從內(nèi)存的緩沖區(qū)中讀??;同樣,每次寫文件的時候,也僅僅是寫入內(nèi)存中的緩沖區(qū),等滿足了一定的條件(如達(dá)到一定數(shù)量或遇到特定字符等),再將緩沖區(qū)中的內(nèi)容一次性寫入文件。這種技術(shù)大大增加了文件讀寫的速度,但也為編程帶來了一點麻煩。比如有一些數(shù)據(jù),認(rèn)為已經(jīng)寫入了文件,實際上因為沒有滿足特定的條件,它們還只是保存在緩沖區(qū)內(nèi),這時用_exit()函數(shù)直接將進(jìn)程關(guān)閉,緩沖區(qū)中的數(shù)據(jù)就會丟失。因此,若想保證數(shù)據(jù)的完整性,就一定要使用exit()函數(shù)。exit和_exit函數(shù)語法下表列出了exit和_exit函數(shù)的語法規(guī)范。表3.37exit和_exit函數(shù)的語法規(guī)范所需頭文件exit#include<stdlib.h>_exit#include<unistd.h>函數(shù)原型exitvoidexit(intstatus)_exitvoid_exit(intstatus)函數(shù)傳入值status是一個整型的參數(shù),可以利用這個參數(shù)傳遞進(jìn)程結(jié)束時的狀態(tài)。一般來說,0表示正常結(jié)束;其他的數(shù)值表示出現(xiàn)了錯誤,進(jìn)程非正常結(jié)束。在實際編程時,可以用wait系統(tǒng)調(diào)用接收子進(jìn)程的返回值,從而針對不同的情況進(jìn)行不同的處理exit和_exit使用實例這兩個示例比較了exit和_exit兩個函數(shù)的區(qū)別。由于printf函數(shù)使用的是緩沖I/O方式,該函數(shù)在遇到“\n”換行符時自動從緩沖區(qū)中將記錄讀出。示例中就是利用這個性質(zhì)來進(jìn)行比較的。以下是示例1的代碼:/*exit.c*/#include<stdio.h>#include<stdlib.h>intmain(){printf("Usingexit...\n");printf("Thisisthecontentinbuffer");exit(0);}運行:#./exitUsingexit...Thisisthecontentinbuffer讀者從輸出的結(jié)果中可以看到,調(diào)用exit函數(shù)時,緩沖區(qū)中的記錄也能正常輸出。以下是示例2的代碼:/*_exit.c*/#include<stdio.h>#include<unistd.h>intmain(){printf("Using_exit...\n");printf("Thisisthecontentinbuffer");_exit(0);}運行:#./_exitUsing_exit...讀者從最后的結(jié)果中可以看到,調(diào)用_exit函數(shù)無法輸出緩沖區(qū)中的記錄。<4>wait和waitpidwait和waitpid函數(shù)說明wait函數(shù)用于使父進(jìn)程(調(diào)用wait的進(jìn)程)阻塞,直到一個子進(jìn)程結(jié)束或者該進(jìn)程接到了一個指定的信號為止。如果該父進(jìn)程沒有子進(jìn)程或者他的子進(jìn)程已經(jīng)結(jié)束,則wait就會立即返回。waitpid的作用和wait一樣,但它并不一定要等待第一個終止的子進(jìn)程,它還有若干選項,如可提供一個非阻塞版本的wait功能,也能支持作業(yè)控制。實際上wait函數(shù)只是waitpid函數(shù)的一個特例,在Linux內(nèi)部實現(xiàn)wait函數(shù)時直接調(diào)用的就是waitpid函數(shù)。wait和waitpid函數(shù)格式說明下面列出了wait函數(shù)的語法規(guī)范。表3.39wait函數(shù)的語法規(guī)范所需頭文件#include<sys/types.h>#include<sys/wait.h>函數(shù)原型pid_twait(int*status)函數(shù)傳入值這里的status是一個整型指針,是該子進(jìn)程退出時的狀態(tài)·status若為空,則代表任意狀態(tài)結(jié)束的子進(jìn)程·status若不為空,則代表指定狀態(tài)結(jié)束的子進(jìn)程另外,子進(jìn)程的結(jié)束狀態(tài)可由Linux中一些特定的宏來測定函數(shù)返回值成功:子進(jìn)程的進(jìn)程號失敗:-1下表列出了waitpid函數(shù)的語法規(guī)范。表3.40waitpid函數(shù)的語法規(guī)范所需頭文件#include<sys/types.h>#include<sys/wait.h>函數(shù)原型pid_twaitpid(pid_tpid,int*status,intoptions)函數(shù)傳入值pidpid>0只等待進(jìn)程ID等于pid的子進(jìn)程,不管已經(jīng)有其他子進(jìn)程運行結(jié)束退出了,只要指定的子進(jìn)程還沒有結(jié)束,waitpid就會一直等下去pid=-1等待任何一個子進(jìn)程退出,此時和wait作用一樣pid=0等待其組ID等于調(diào)用進(jìn)程的組ID的任一子進(jìn)程pid<-1等待其組ID等于pid的絕對值的任一子進(jìn)程status同waitoptionsWNOHANG若由pid指定的子進(jìn)程不立即可用,則waitpid不阻塞,此時返回值為0WUNTRACED若實現(xiàn)某支持作業(yè)控制,則由pid指定的任一子進(jìn)程狀態(tài)已暫停,且其狀態(tài)自暫停以來還未報告過,則返回其狀態(tài)0同wait,阻塞父進(jìn)程,等待子進(jìn)程退出函數(shù)返回值子進(jìn)程的進(jìn)程號正常0使用選項WNOHANG且沒有子進(jìn)程退出-1調(diào)用出錯waitpid使用實例由于wait函數(shù)的使用較為簡單,在此僅以waitpid為例進(jìn)行講解。本例中首先使用fork新建一子進(jìn)程,然后讓其子進(jìn)程暫停5s(使用了sleep函數(shù))。接下來對原有的父進(jìn)程使用waitpid函數(shù),并使用參數(shù)WNOHANG使該父進(jìn)程不會阻塞。若有子進(jìn)程退出,則waitpid返回子進(jìn)程號;若沒有子進(jìn)程退出,則waitpid返回0,并且父進(jìn)程每隔一秒循環(huán)判斷一次。該程序源代碼如下所示:/*waitpid.c*/#include<sys/types.h>#include<sys/wait.h>#include<unistd.h>#include<stdio.h>#include<stdlib.h>intmain(){pid_tpc,pr;pc=fork();if(pc<0) printf("Errorfork.\n");elseif(pc==0) /*子進(jìn)程*/{/*子進(jìn)程暫停5s*/sleep(5);/*子進(jìn)程正常退出*/exit(0);}else /*父進(jìn)程*/{/*循環(huán)測試子進(jìn)程是否退出*/do{/*調(diào)用waitpid,且父進(jìn)程不阻塞*/pr=waitpid(pc,NULL,WNOHANG);/*若子進(jìn)程還未退出,則父進(jìn)程暫停1s*/if(pr==0){printf("Thechildprocesshasnotexited\n");sleep(1);}}while(pr==0);/*若發(fā)現(xiàn)子進(jìn)程退出,打印出相應(yīng)情況*/if(pr==pc) printf("Getchild%d\n",pr);else printf("someerroroccured.\n");}}將該程序交叉編譯,下載到目標(biāo)板后的運行情況如下所示:#./waitpidThechildprocesshasnotexitedThechildprocesshasnotexitedThechildprocesshasnotexitedThechildprocesshasnotexitedThechildprocesshasnotexitedGetchild75可見,該程序在經(jīng)過5次循環(huán)之后,捕獲到了子進(jìn)程的退出信號,具體的子進(jìn)程號在不同的系統(tǒng)上會有所區(qū)別。
讀者還可以嘗試把“pr=waitpid(pc,NULL,WNOHANG);”這句改為“pr=waitpid(pc,NULL,0);”和“pr=wait(NULL);”,運行的結(jié)果為:#./waitpidGetchild76可見,在上述兩種情況下,父進(jìn)程在調(diào)用waitpid或wait之后就將自己阻塞,直到有子進(jìn)程退出為止。(2)Linux守護(hù)進(jìn)程<1>守護(hù)進(jìn)程概述守護(hù)進(jìn)程,也就是通常所說的Daemon進(jìn)程,是Linux中的后臺服務(wù)進(jìn)程。它是一個生存期較長的進(jìn)程,通常獨立于控制終端并且周期性地執(zhí)行某種任務(wù)或等待處理某些發(fā)生的事件。守護(hù)進(jìn)程常常在系統(tǒng)引導(dǎo)裝入時啟動,在系統(tǒng)關(guān)閉時終止。Linux系統(tǒng)有很多守護(hù)進(jìn)程,大多數(shù)服務(wù)都是通過守護(hù)進(jìn)程實現(xiàn)的。同時,守護(hù)進(jìn)程還能完成許多系統(tǒng)任務(wù),例如,作業(yè)規(guī)劃進(jìn)程crond、打印進(jìn)程lqd等(這里的結(jié)尾字母d就是Daemon的意思)。由于在Linux中,每一個系統(tǒng)與用戶進(jìn)行交流的界面稱為終端,每一個從此終端開始運行的進(jìn)程都會依附于這個終端,這個終端就稱為這些進(jìn)程的控制終端,當(dāng)控制終端被關(guān)閉時,相應(yīng)的進(jìn)程都會自動關(guān)閉。但是守護(hù)進(jìn)程卻能夠突破這種限制,它從被執(zhí)行開始運轉(zhuǎn),直到整個系統(tǒng)關(guān)閉時才會退出。如果想讓某個進(jìn)程不因為用戶或終端或其他的變化而受到影響,那么就必須把這個進(jìn)程變成一個守護(hù)進(jìn)程??梢姡刈o(hù)進(jìn)程是非常重要的。<2>編寫守護(hù)進(jìn)程編寫守護(hù)進(jìn)程看似復(fù)雜,但實際上也是遵循一個特定的流程。只要將此流程掌握了,就能很方便地編寫出用戶自己的守護(hù)進(jìn)程。下面就分4個步驟來講解怎樣創(chuàng)建一個簡單的守護(hù)進(jìn)程。在講解的同時,會配合介紹與創(chuàng)建守護(hù)進(jìn)程相關(guān)的幾個系統(tǒng)函數(shù),希望讀者能很好地掌握。創(chuàng)建子進(jìn)程,父進(jìn)程退出這是編寫守護(hù)進(jìn)程的第一步。由于守護(hù)進(jìn)程是脫離控制終端的,因此,完成第一步后就會在Shell終端里造成一程序已經(jīng)運行完畢的假象。之后的所有工作都在子進(jìn)程中完成,而用戶在Shell終端里則可以執(zhí)行其他的命令,從而在形式上做到了與控制終端的脫離。到這里,有心的讀者可能會問,父進(jìn)程創(chuàng)建了子進(jìn)程,而父進(jìn)程又退出之后,此時該子進(jìn)程不就沒有父進(jìn)程了嗎?守護(hù)進(jìn)程中確實會出現(xiàn)這么一個有趣的現(xiàn)象,由于父進(jìn)程已經(jīng)先于子進(jìn)程退出,會造成子進(jìn)程沒有父進(jìn)程,從而變成一個孤兒進(jìn)程。在Linux中,每當(dāng)系統(tǒng)發(fā)現(xiàn)一個孤兒進(jìn)程,就會自動由1號進(jìn)程(也就是init進(jìn)程)收養(yǎng)它,這樣,原先的子進(jìn)程就會變成init進(jìn)程的子進(jìn)程了。其關(guān)鍵代碼如下所示:/*父進(jìn)程退出*/pid=fork();if(pid>0) exit(0);在子進(jìn)程中創(chuàng)建新會話這個步驟是創(chuàng)建守護(hù)進(jìn)程中最重要的一步,雖然它的實現(xiàn)非常簡單,但它的意義卻非常重大。在這里使用的是系統(tǒng)函數(shù)setsid,在具體介紹setsid之前,讀者首先要了解兩個概念:進(jìn)程組和會話期。
進(jìn)程組進(jìn)程組是一個或多個進(jìn)程的集合。進(jìn)程組由進(jìn)程組ID來惟一標(biāo)識。除了進(jìn)程號(PID)之外,進(jìn)程組ID也一個進(jìn)程的必備屬性。每個進(jìn)程組都有一個組長進(jìn)程,其組長進(jìn)程的進(jìn)程號等于進(jìn)程組ID。且該進(jìn)程ID不會因組長進(jìn)程的退出而受到影響。會話期會話組是一個或多個進(jìn)程組的集合。通常,一個會話開始于用戶登錄,終止于用戶退出,在此期間該用戶運行的所有進(jìn)程都屬于這個會話期,它們之間的關(guān)系如下圖所示。圖3.7會話期中進(jìn)程之間的關(guān)系接下來就可以具體介紹setsid的相關(guān)內(nèi)容:setsid函數(shù)作用setsid函數(shù)用于創(chuàng)建一個新的會話,并擔(dān)任該會話組的組長。調(diào)用setsid有下面的3個作用。讓進(jìn)程擺脫原會話的控制。讓進(jìn)程擺脫原進(jìn)程組的控制。讓進(jìn)程擺脫原控制終端的控制。那么,在創(chuàng)建守護(hù)進(jìn)程時為什么要調(diào)用setsid函數(shù)呢?讀者可以回憶一下創(chuàng)建守護(hù)進(jìn)程的第一步,在那里調(diào)用了fork函數(shù)來創(chuàng)建子進(jìn)程再將父進(jìn)程退出。由于在調(diào)用fork函數(shù)時,子進(jìn)程全盤拷貝了父進(jìn)程的進(jìn)會話期、進(jìn)程組、控制終端等,雖然父進(jìn)程退出了,但原先的會話期、進(jìn)程組、控制終端等并沒有改變,因此,還不是真正意義上獨立開來,而setsid函數(shù)能夠使進(jìn)程完全獨立出來,從而脫離所有其他進(jìn)程的控制。setsid函數(shù)格式下表列出了setsid函數(shù)的語法規(guī)范:表3.41setsid函數(shù)的語法規(guī)范所需頭文件#include<sys/types.h>#include<unistd.h>函數(shù)原型pid_tsetsid(void)函數(shù)返回值成功:該進(jìn)程組ID出錯:-1改變當(dāng)前目錄為根目錄這一步也是必要的步驟。使用fork創(chuàng)建的子進(jìn)程繼承了父進(jìn)程的當(dāng)前工作目錄。由于在進(jìn)程運行過程中,當(dāng)前目錄所在的文件系統(tǒng)(比如“/mnt/usb”等)是不能卸載的,這對以后的使用會造成諸多的麻煩(比如系統(tǒng)由于某種原因要進(jìn)入單用戶模式)。因此,通常的做法是讓“/”作為守護(hù)進(jìn)程的當(dāng)前工作目錄,這樣就可以避免上述的問題,當(dāng)然,如有特殊需要,也可以把當(dāng)前工作目錄換成其他的路徑,如/tmp。改變工作目錄的常見函數(shù)是chdir。重設(shè)文件權(quán)限掩碼文件權(quán)限掩碼是指屏蔽掉文件權(quán)限中的對應(yīng)位。比如,有一個文件權(quán)限掩碼是050,它就屏蔽了文件組擁有者的可讀與可執(zhí)行權(quán)限。由于使用fork函數(shù)新建的子進(jìn)程繼承了父進(jìn)程的文件權(quán)限掩碼,這就給該子進(jìn)程使用文件帶來了諸多的麻煩。因此,把文件權(quán)限掩碼設(shè)置為0,可以大大增強(qiáng)該守護(hù)進(jìn)程的靈活性。設(shè)置文件權(quán)限掩碼的函數(shù)是umask。在這里,通常的使用方法為umask(0)。關(guān)閉文件描述符同文件權(quán)限掩碼一樣,用fork函數(shù)新建的子進(jìn)程會從父進(jìn)程那里繼承一些已經(jīng)打開了的文件。這些被打開的文件可能永遠(yuǎn)不會被守護(hù)進(jìn)程讀或?qū)?,但它們一樣消耗系統(tǒng)資源,而且可能導(dǎo)致所在的文件系統(tǒng)無法卸下。在上面的第二步之后,守護(hù)進(jìn)程已經(jīng)與所屬的控制終端失去了聯(lián)系。因此從終端輸入的字符不可能達(dá)到守護(hù)進(jìn)程,守護(hù)進(jìn)程中用常規(guī)方法(如printf)輸出的字符也不可能在終端上顯示出來。所以,文件描述符為0、1和2的3個文件(常說的輸入、輸出和報錯這3個文件)已經(jīng)失去了存在的價值,也應(yīng)被關(guān)閉。通常按如下方式關(guān)閉文件描述符:for(i=0;i<MAXFILE;i++) close(i);這樣,一個簡單的守護(hù)進(jìn)程就建立起來了,創(chuàng)建守護(hù)進(jìn)程的流程圖如圖所示。開始開始fork()exit()setsid()chdir("/")umash(0)close()結(jié)束圖3.8創(chuàng)建守護(hù)進(jìn)程的流程圖
下面是實現(xiàn)守護(hù)進(jìn)程的一個完整實例:此例首先建立了一個守護(hù)進(jìn)程,然后讓該守護(hù)進(jìn)程每隔10s在/tmp/dameon.log中寫入一句話。/*dameon.c創(chuàng)建守護(hù)進(jìn)程實例*/#include<stdio.h>#include<stdlib.h>#include<string.h>#include<fcntl.h>#include<sys/types.h>#include<unistd.h>#include<sys/wait.h>#defineMAXFILE65535intmain(){pid_tpc;inti,fd,len;char*buf="ThisisaDameon\n";len=strlen(buf);//第一步pc=fork();if(pc<0){printf("errorfork\n");exit(1);}elseif(pc>0)exit(0);/*第二步*/setsid();/*第三步*/chdir("/");/*第四步*/umask(0);/*第五步*/for(i=0;i<MAXFILE;i++) close(i);/*這時創(chuàng)建完守護(hù)進(jìn)程,以下開始正式進(jìn)入守護(hù)進(jìn)程工作*/while(1){if((fd=open("/tmp/dameon.log",O_CREAT|O_WRONLY|O_APPEND,0600))<0){perror("open");exit(1);}write(fd,buf,len+1);close(fd);sleep(10);}}
將該程序下載到開發(fā)板中,可以看到該程序每隔10s就會在對應(yīng)的文件中輸入相關(guān)內(nèi)容。并且使用ps可以看到該進(jìn)程在后臺運行。如下所示:#tail-f/tmp/dameon.logThisisaDameonThisisaDameonThisisaDameonThisisaDameon…#ps-ef|grepdaemon76root1272S./daemon85root1520Sgrepdaemon<3>守護(hù)進(jìn)程的出錯處理讀者在前面編寫守護(hù)進(jìn)程的具體調(diào)試過程中會發(fā)現(xiàn),由于守護(hù)進(jìn)程完全脫離了控制終端,因此,不能像其他進(jìn)程的程序一樣通過輸出錯誤信息到控制終端來通知程序員即使使用gdb也無法正常調(diào)試。那么,守護(hù)進(jìn)程的進(jìn)程要如何調(diào)試呢?一種通用的辦法是使用syslog服務(wù),將程序中的出錯信息輸入到“/var/log/messages”系統(tǒng)日志文件中,從而可以直觀地看到程序的問題所在。注意:“/var/log/message”系統(tǒng)日志文件只能由擁有root權(quán)限的超級用戶查看。Syslog是Linux中的系統(tǒng)日志管理服務(wù),通過守護(hù)進(jìn)程syslogd來維護(hù)。該守護(hù)進(jìn)程在啟動時會讀一個配置文件“/etc/syslog.conf”。該文件決定了不同種類的消息會發(fā)送向何處。例如,緊急消息可被送向系統(tǒng)管理員并在控制臺上顯示,而警告消息則可記錄到一個文件中。該機(jī)制提供了3個syslog函數(shù),分別為openlog、syslog和closelog。下面就分別介紹這3個函數(shù)。syslog函數(shù)說明通常,openlog函數(shù)用于打開系統(tǒng)日志服務(wù)的一個連接;syslog函數(shù)是用于向日志文件中寫入消息,在這里可以規(guī)定消息的優(yōu)先級、消息輸出格式等;closelog函數(shù)是用于關(guān)閉系統(tǒng)日志服務(wù)的連接。
syslog函數(shù)格式下表列出了openlog函數(shù)的語法規(guī)范:表3.42openlog函數(shù)的語法規(guī)范所需頭文件#include<syslog.h>函數(shù)原型voidopenlog(char*ident,intoption,intfacility)函數(shù)傳入值Ident要向每個消息加入的字符串,通常為程序的名稱Optionfacility:指定程序發(fā)送的消息類型LOG_CONS如果消息無法送到系統(tǒng)日志服務(wù),則直接輸出到系統(tǒng)控制終端LOG_NDELAY立即打開系統(tǒng)日志服務(wù)的連接。在正常情況下,直到發(fā)送到第一條消息時才打開連接LOG_PERROR將消息也同時送到stderr上LOG_PID在每條消息中包含進(jìn)程的PIDLOG_AUTHPRIV安全/授權(quán)訊息LOG_CRON時間守護(hù)進(jìn)程(cron及at)LOG_DAEMON其他系統(tǒng)守護(hù)進(jìn)程LOG_KERN內(nèi)核信息LOG_LOCAL[0~7]保留LOG_LPR行打印機(jī)子系統(tǒng)LOG_MAIL郵件子系統(tǒng)LOG_NEWS新聞子系統(tǒng)LOG_SYSLOGsyslogd內(nèi)部所產(chǎn)生的信息LOG_USER一般使用者等級訊息LOG_UUCPUUCP子系統(tǒng)下表列出了syslog函數(shù)的語法規(guī)范。表3.43syslog函數(shù)的語法規(guī)范所需頭文件#include<syslog.h>函數(shù)原型voidsyslog(intpriority,char*format,...)函數(shù)傳入值priority:指定消息的重要性LOG_EMERG系統(tǒng)無法使用LOG_ALERT需要立即采取措施LOG_CRIT有重要情況發(fā)生LOG_ERR有錯誤發(fā)生LOG_WARNING有警告發(fā)生LOG_NOTICE正常情況,但也是重要情況LOG_INFO信息消息LOG_DEBUG調(diào)試信息format以字符串指針的形式表示輸出的格式,類似printf中的格式下表列出了closelog函數(shù)的語法規(guī)范。表3.44closelog函數(shù)的語法規(guī)范所需頭文件#include<syslog.h>函數(shù)原型voidcloselog(void)使用實例這里將上一節(jié)中的示例程序用syslog服務(wù)進(jìn)行重寫,其中有區(qū)別的地方用加粗的字體表示,源代碼如下所示:/*syslog_dema.c利用syslog服務(wù)的守護(hù)進(jìn)程實例*/#include<stdio.h>#include<stdlib.h>#include<string.h>#include<fcntl.h>#include<sys/types.h>#include<unistd.h>#include<sys/wait.h>#include<syslog.h>#defineMAXFILE65535intmain(){pid_tpc,sid;inti,fd,len;char*buf="ThisisaDameon\n";len=strlen(buf);pc=fork();if(pc<0){printf("errorfork\n");exit(1);}else if(pc>0)exit(0);/*打開系統(tǒng)日志服務(wù),openlog*/openlog("demo_update",LOG_PID,LOG_DAEMON);if((sid=setsid())<0) {syslog(LOG_ERR,"%s\n","setsid");exit(1);}if((sid=chdir("/"))<0) {syslog(LOG_ERR,"%s\n","chdir");exit(1);}umask(0);for(i=0;i<MAXFILE;i++) close(i);while(1){/*打開守護(hù)進(jìn)程的日志文件,并寫入open的日志記錄*/if((fd=open("/tmp/dameon.log",O_CREAT|O_WRONLY|O_APPEND,0600))<0){syslog(LOG_ERR,"open");exit(1);}write(fd,buf,len+1);close(fd);sleep(10);}closelog();exit(0);}讀者可以嘗試用普通用戶的身份執(zhí)行此程序,由于這里的open函數(shù)必須具有root權(quán)限,因此,syslog就會將錯誤信息寫入到“/var/log/messages”中,如下所示:Jan3018:20:08localhostdemo_update[7996]:open3.4進(jìn)程間通信3.4.1Linux下進(jìn)程間通信概述在上一章中,讀者已經(jīng)知道了進(jìn)程是一個程序的一次執(zhí)行的過程。這里所說的進(jìn)程一般是指運行在用戶態(tài)的進(jìn)程,而由于處于用戶態(tài)的不同進(jìn)程之間是彼此隔離的,就像處于不同城市的人們,它們必須通過某種方式來提供通信,例如人們現(xiàn)在廣泛使用的手機(jī)等方式。本章就是講述如何建立這些不同的通話方式,就像人們有多種通信方式一樣。最初UNIX的進(jìn)程間通信基于SystemV進(jìn)程間通信基于Socket進(jìn)程間通信POSIX進(jìn)程間通信Linux進(jìn)程間通信Linux下的進(jìn)程通信手段基本上是從UNIX平臺上的進(jìn)程通信手段繼承而來的。而對UNIX發(fā)展做出重大貢獻(xiàn)的兩大主力AT&T的貝爾實驗室及BSD(加州大學(xué)伯克利分校的伯克利軟件發(fā)布中心)在進(jìn)程間的通信方面的側(cè)重點有所不同。前者是對UNIX早期的進(jìn)程間通信手段進(jìn)行了系統(tǒng)的改進(jìn)和擴(kuò)充,形成了“systemVIPC”,其通信進(jìn)程主要局限在單個計算機(jī)內(nèi);后者則跳過了該限制,形成了基于套接字(socket最初UNIX的進(jìn)程間通信基于SystemV進(jìn)程間通信基于Socket進(jìn)程間通信POSIX進(jìn)程間通信Linux進(jìn)程間通信圖3.9Linux進(jìn)程間通信UNIX進(jìn)程間通信(IPC)方式包括管道、FIFO、信號。SystemV進(jìn)程間通信(IPC)包括SystemV消息隊列、SystemV信號燈、SystemV共享內(nèi)存。Posix進(jìn)程間通信(IPC)包括Posix消息隊列、Posix信號燈、Posix共享內(nèi)存區(qū)?,F(xiàn)在在Linux中使用較多的進(jìn)程間通信方式主要有以下幾種:(1)管道(Pipe)及有名管道(namedpipe):管道可用于具有親緣關(guān)系進(jìn)程間的通信,有名管道,除具有管道所具有的功能外,它還允許無親緣關(guān)系進(jìn)程間的通信。(2)信號(Signal):信號是在軟件層次上對中斷機(jī)制的一種模擬,是比較復(fù)雜的通信方式,用于通知進(jìn)程有事件發(fā)生,進(jìn)程收到一個信號與處理器收到一個中斷請求效果上可以說是一樣的。(3)消息隊列:消息隊列是消息的鏈接表,包括Posix消息隊列systemV消息隊列。它克服了前兩種通信方式中信息量有限的缺點,具有寫權(quán)限的進(jìn)程可以向消息隊列中按照一定的規(guī)則添加新消息;對消息隊列有讀權(quán)限的進(jìn)程則可以從消息隊列中讀取消息。(4)共享內(nèi)存:可以說這是最有用的進(jìn)程間通信方式。它使得多個進(jìn)程可以訪問同一塊內(nèi)存空間,不同進(jìn)程可以及時看到對方進(jìn)程中對共享內(nèi)存中數(shù)據(jù)的更新。這種通信方式需要依靠某種同步機(jī)制,如互斥鎖和信號量等。(5)信號量:主要作為進(jìn)程間以及同一進(jìn)程不同線程之間的同步手段。(6)套接字(Socket):這是一種更為一般的進(jìn)程間通信機(jī)制,它可用于不同機(jī)器之間的進(jìn)程間通信,應(yīng)用非常廣泛。3.4.2管道通信(1)管道概述這里“ps–ef|grepntp”為例,描述管道的通信過程,如圖所示。進(jìn)程ps-ef進(jìn)程ps-ef進(jìn)程grepntp管道內(nèi)核圖3.10管道的通信過程管道是Linux中進(jìn)程間通信的一種方式。這里所說的管道主要指無名管道,它具有如下特點:它只能用于具有親緣關(guān)系的進(jìn)程之間的通信(也就是父子進(jìn)程或者兄弟進(jìn)程之間)。它是一個半雙工的通信模式,具有固定的讀端和寫端。管道也可以看成是一種特殊的文件,對于它的讀寫也可以使用普通的read、write等函數(shù),但是它不是普通的文件,并不屬于其他任何文件系統(tǒng),并且只存在于內(nèi)存中。(2)管道創(chuàng)建與關(guān)閉<1>管道創(chuàng)建與關(guān)閉說明管道是基于文件描述符的通信方式,當(dāng)一個管道建立時,它會創(chuàng)建兩個文件描述符fds[0]和fds[1],其中fds[0]固定用于讀管道,而fd[1]固定用于寫管道,這樣就構(gòu)成了一個半雙工的通道。管道關(guān)閉時只需將這兩個文件描述符關(guān)閉,可使用普通的close函數(shù)逐個關(guān)閉各文件描述符。注意:一個管道共享了多對文件描述符時,若將其中的一對讀寫文件描述符都刪除,則該管道就失效。<2>管道創(chuàng)建函數(shù)創(chuàng)建管道可以通過調(diào)用pipe來實現(xiàn),下表列出了pipe函數(shù)的語法要點。表3.45pipe函數(shù)的語法所需頭文件#include<unistd.h>函數(shù)原型intpipe(intfd[2])函數(shù)傳入值fd[2]:管道的兩個文件描述符,之后就可以直接操作這兩個文件描述符函數(shù)返回值成功:0出錯:-1<3>管道創(chuàng)建實例創(chuàng)建管道非常簡單,只需調(diào)用函數(shù)pipe即可,如下所示:/*pipe.c*/#include<unistd.h>#include<errno.h>#include<stdio.h>#include<stdlib.h>intmain(){intpipe_fd[2];/*創(chuàng)建一無名管道*/if(pipe(pipe_fd)<0){printf("pipecreateerror\n");return-1;}elseprintf("pipecreatesuccess\n");/*關(guān)閉管道描述符*/close(pipe_fd[0]);close(pipe_fd[1]);}程序運行后先成功創(chuàng)建一個無名管道,之后再將其關(guān)閉。(3)管道讀寫<1>管道讀寫說明父進(jìn)程fd[0]fd[1]子進(jìn)程fd[0]fd[1]內(nèi)核管道父進(jìn)程fd[0]fd[1]子進(jìn)程fd[0]fd[1]內(nèi)核管道圖3.11父子進(jìn)程管道的文件描述符對應(yīng)關(guān)系
這時的關(guān)系看似非常復(fù)雜,實際上卻已經(jīng)給不同進(jìn)程之間的讀寫創(chuàng)造了很好的條件。這時,父子進(jìn)程分別擁有自己的讀寫的通道,為了實現(xiàn)父子進(jìn)程之間的讀寫,只需把無關(guān)的讀端或?qū)懚说奈募枋龇P(guān)閉即可。例如在上圖中把父進(jìn)程的寫端fd[1]和子進(jìn)程的讀端fd[0]關(guān)閉。這時,父子進(jìn)程之間就建立起了一條“子進(jìn)程寫入父進(jìn)程讀”的通道。父進(jìn)程父進(jìn)程fd[0]fd[1]子進(jìn)程fd[0]fd[1]內(nèi)核管道圖3.12建立“子進(jìn)程寫入父進(jìn)程讀”的通道同樣,也可以關(guān)閉父進(jìn)程的fd[0]和子進(jìn)程的fd[1],這樣就可以建立一條“父進(jìn)程寫,子進(jìn)程讀”的通道。另外,父進(jìn)程還可以創(chuàng)建多個子進(jìn)程,各個子進(jìn)程都繼承了相應(yīng)的fd[0]和fd[1],這時,只需要關(guān)閉相應(yīng)端口就可以建立其各子進(jìn)程之間的通道。請思考:為什么無名管道只能建立具有親緣關(guān)系的進(jìn)程之間?<2>管道讀寫實例在本例中,首先創(chuàng)建管道,之后父進(jìn)程使用fork函數(shù)創(chuàng)建子進(jìn)程,之后通過關(guān)閉父進(jìn)程的讀描述符和子進(jìn)程的寫描述符,建立起它們之間的管道通信。/*pipe_rw.c*/#include<unistd.h>#include<sys/types.h>#include<errno.h>#include<stdio.h>#include<stdlib.h>intmain(){intpipe_fd[2];pid_tpid;charbuf_r[100];char*p_wbuf;intr_num;memset(buf_r,0,sizeof(buf_r));/*創(chuàng)建管道*/if(pipe(pipe_fd)<0){printf("pipecreateerror\n");return-1;}/*創(chuàng)建一子進(jìn)程*/if((pid=fork())==0){printf("\n");/*關(guān)閉子進(jìn)程寫描述符,并通過使父進(jìn)程暫停2秒確保父進(jìn)程已關(guān)閉相應(yīng)的讀描述符*/close(pipe_fd[1]);sleep(2);/*子進(jìn)程讀取管道內(nèi)容*/if((r_num=read(pipe_fd[0],buf_r,100))>0)printf("%dnumbersreadfromthepipeis%s\n",r_num,buf_r);/*關(guān)閉子進(jìn)程讀描述符*/close(pipe_fd[0]);exit(0);}elseif(pid>0){/*關(guān)閉父進(jìn)程讀描述符,并分兩次向管道中寫入HelloPipe*/close(pipe_fd[0]);if(write(pipe_fd[1],"Hello",5)!=-1)printf("parentwrite1success!\n");if(write(pipe_fd[1],"Pipe",5)!=-1)printf("parentwrite2success!\n");/*關(guān)閉父進(jìn)程寫描述符*/close(pipe_fd[1]);sleep(3);/*收集子進(jìn)程退出信息*/waitpid(pid,NULL,0);exit(0);}}將該程序交叉編譯,下載到開發(fā)板上的運行結(jié)果如下所示:#./pipe_rw2parentwrite1success!parentwrite2success!10numbersreadfromthepipeisHelloPipe<3>管道讀寫注意點只有在管道的讀端存在時向管道中寫入數(shù)據(jù)才有意義。否則,向管道中寫入數(shù)據(jù)的進(jìn)程將收到內(nèi)核傳來的SIFPIPE信號(通常Brokenpipe錯誤)。向管道中寫入數(shù)據(jù)時,linux將不保證寫入的原子性,管道緩沖區(qū)一有空閑區(qū)域,寫進(jìn)程就會試圖向管道寫入數(shù)據(jù)。如果讀進(jìn)程不讀取管道緩沖區(qū)中的數(shù)據(jù),那么寫操作將會一直阻塞。父子進(jìn)程在運行時,它們的先后次序并不能保證,因此,在這里為了保證父進(jìn)程已經(jīng)關(guān)閉了讀描述符,可在子進(jìn)程中調(diào)用sleep函數(shù)。(4)標(biāo)準(zhǔn)流管道<1>標(biāo)準(zhǔn)流管道函數(shù)說明與Linux中文件操作有基于文件流的標(biāo)準(zhǔn)I/O操作一樣,管道的操作也支持基于文件流的模式。這種基于文件流的管道主要是用來創(chuàng)建一個連接到另一個進(jìn)程的管道,這里的“另一個進(jìn)程”也就是一個可以進(jìn)行一定操作的可執(zhí)行文件,例如,用戶執(zhí)行“catpopen.c”或者自己編寫的程序“hello”等。由于這一類操作很常用,因此標(biāo)準(zhǔn)流管道就將一系列的創(chuàng)建過程合并到一個函數(shù)popen中完成。它所完成的工作有以下幾步:創(chuàng)建一個管道fork一個子進(jìn)程在父子進(jìn)程中關(guān)閉不需要的文件描述符執(zhí)行exec函數(shù)族調(diào)用執(zhí)行函數(shù)中所指定的命令這個函數(shù)的使用可以大大減少代碼的編寫量,但同時也有一些不利之處,例如,它沒有前面管道創(chuàng)建的函數(shù)靈活多樣,并且用popen創(chuàng)建的管道必須使用標(biāo)準(zhǔn)I/O函數(shù)進(jìn)行操作,但不能使用前面的read、write一類不帶緩沖的I/O函數(shù)。與之相對應(yīng),關(guān)閉用popen創(chuàng)建的流管道必須使用函數(shù)pclose來關(guān)閉該管道流。該函數(shù)關(guān)閉標(biāo)準(zhǔn)I/O流,并等待命令執(zhí)行結(jié)束。<2>函數(shù)格式popen和pclose函數(shù)格式如下表所示。表3.46popen和pclose函數(shù)格式所需頭文件#include<stdio.h>函數(shù)原型FILE*popen(constchar*command,constchar*type)函數(shù)傳入值Command指向的是一個以null結(jié)束符結(jié)尾的字符串,這個字符串包含一個shell命令,并被送到/bin/sh以-c參數(shù)執(zhí)行,即由shell來執(zhí)行type"r"文件指針連接到command的標(biāo)準(zhǔn)輸出,即該命令的結(jié)果產(chǎn)生輸出"w"文件指針連接到command的標(biāo)準(zhǔn)輸入,即該命令的結(jié)果產(chǎn)生輸入函數(shù)返回值成功:文件流指針出錯:-1所需頭文件#include<stdio.h>函數(shù)原型intpclose(FILE*stream)函數(shù)傳入值stream:要關(guān)閉的文件流函數(shù)返回值成功:返回popen中執(zhí)行命令的終止?fàn)顟B(tài)出錯:-1<3>函數(shù)使用實例在該實例中,使用popen來執(zhí)行“ps-ef”命令。可以看出,popen函數(shù)的使用能夠使程序變得短小精悍。/*popen.c*/#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<fcntl.h>#defineBUFSIZE1000intmain(){FILE*fp;char*cmd="ps-ef";charbuf[BUFSIZE];/*調(diào)用popen函數(shù)執(zhí)行相應(yīng)的命令*/if((fp=popen(cmd,"r"))==NULL) perror("popen");else{while((fgets(buf,BUFSIZE,fp))!=NULL)printf("%s",buf);pclose(fp);}exit(0);}下面是該程序在目標(biāo)板上的執(zhí)行結(jié)果。#./popenPIDTTYUidSizeStateCommand1root1832Sinit2root0S[keventd]3root0S[ksoftirqd_CPU0]4root0S[kswapd]5root0S[bdflush]6root0S[kupdated]7root0S[mtdblockd]8root0S[khubd]35root2104S/bin/bash/usr/etc/rc.local36root2324S/bin/bash41root1364S/sbin/inetd53root14260S/Qtopia/qtopia-free-1.7.0/bin/qpe-qws54root11672Squicklauncher55root0S[usb-storage-0]56root0S[scsi_eh_0]74root1284S./popen75root1836Ssh-cps-ef76root2020Rps–ef(5)FIFO<1>有名管道說明前面介紹的管道是無名管道,它只能用于具有親緣關(guān)系的進(jìn)程之間,這就大大地限制了管道的使用。有名管道的出現(xiàn)突破了這種限制,它可以使互不相關(guān)的兩個進(jìn)程實現(xiàn)彼此通信。該管道可以通過路徑名來指出,并且在文件系統(tǒng)中是可見的。在建立了管道之后,兩個進(jìn)程就可以把它當(dāng)作普通文件一樣進(jìn)行讀寫操作,使用非常方便。不過值得注意的是,F(xiàn)IFO是嚴(yán)格地遵循先進(jìn)先出規(guī)則的,對管道及FIFO的讀總是從開始處返回數(shù)據(jù),對它們的寫則把數(shù)據(jù)添加到末尾,它們不支持如lseek()等文件定位操作。有名管道的創(chuàng)建可以使用函數(shù)mkfifo(),該函數(shù)類似文件中的open()操作,可以指定管道的路徑和打開的模式。在創(chuàng)建管道成功之后,就可以使用open、read、write這些函數(shù)了。與普通文件的開發(fā)設(shè)置一樣,對于為讀而打開的管道可在open中設(shè)置O_RDONLY,對于為寫而打開的管道可在open中設(shè)置O_WRONLY,在這里與普通文件不同的是阻塞問題。由于普通文件的讀寫時不會出現(xiàn)阻塞問題,而在管道的讀寫中卻有阻塞的可能,這里的非阻塞標(biāo)志可以在open函數(shù)中設(shè)定為O_NONBLOCK。下面分別對阻塞打開和非阻塞打開的讀寫進(jìn)行一定的討論。對于讀進(jìn)程若該管道是阻塞打開,且當(dāng)前FIFO內(nèi)沒有數(shù)據(jù),則對讀進(jìn)程而言將一直阻塞直到有數(shù)據(jù)寫入。若該管道是非阻塞打開,則不論FIFO內(nèi)是否有數(shù)據(jù),讀進(jìn)程都會立即執(zhí)行讀操作。對于寫進(jìn)程若該管道是阻塞打開,則寫進(jìn)程而言將一直阻塞直到有讀進(jìn)程讀出數(shù)據(jù)。若該管道是非阻塞打開,則當(dāng)前FIFO內(nèi)沒有讀操作,寫進(jìn)程都會立即執(zhí)行讀操作。<2>mkfifo函數(shù)格式下表列出了mkfifo函數(shù)的語法要點。表3.47mkfifo函數(shù)的語法所需頭文件#include<sys/types.h>#include<sys/state.h>函數(shù)原型intmkfifo(constchar*filename,mode_tmode)函數(shù)傳入值filename要創(chuàng)建的管道m(xù)odeO_RDONLY讀管道O_WRONLY寫管道O_RDWR讀寫管道O_NONBLOCK非阻塞O_CREAT如果該文件不存在,那么就創(chuàng)建一個新的文件,并用第三的參數(shù)為其設(shè)置權(quán)限O_EXCL如果使用O_CREAT時文件存在,那么可返回錯誤消息。這一參數(shù)可測試文件是否存在函數(shù)返回值成功:0出錯:-1下表再對FIFO相關(guān)的出錯信息做一歸納,以方便用戶查錯。表3.48FIFO相關(guān)的出錯信息EACCESS參數(shù)filename所指定的目錄路徑無可執(zhí)行的權(quán)限EEXIST參數(shù)filename所指定的文件已存在ENAMETOOLONG參數(shù)filename的路徑名稱太長ENOENT參數(shù)filename包含的目錄不存在ENOSPC文件系統(tǒng)的剩余空間不足ENOTDIR參數(shù)filename路徑中的目錄存在但卻非真正的目錄EROFS參數(shù)filename指定的文件存在于只讀文件系統(tǒng)內(nèi)<3>使用實例下面的實例包含了兩個程序,一個用于讀管道,另一個用于寫管道。其中在寫管道的程序里創(chuàng)建管道,并且作為main函數(shù)里的參數(shù)由用戶輸入要寫入的內(nèi)容。讀管道讀出了用戶寫入管道的內(nèi)容,這兩個函數(shù)用的是非阻塞讀寫管道。/*fifo_write.c*/#include<sys/types.h>#include<sys/stat.h>#include<errno.h>#include<fcntl.h>#include<stdio.h>#include<stdlib.h>#include<string.h>#defineFIFO"/tmp/myfifo"main(intargc,char**argv)/*參數(shù)為即將寫入的字節(jié)數(shù)*/{intfd;charw_buf[100];intnwrite;if(fd==-1)if(errno==ENXIO)printf("openerror;noreadingprocess\n");/*打開FIFO管道,并設(shè)置非阻塞標(biāo)志*/fd=open(FIFO_SERVER,O_WRONLY|O_NONBLOCK,0);if(argc==1) printf("Pleasesendsomething\n");strcpy(w_buf,argv[1]);/*向管道中寫入字符串*/if((nwrite=write(fd,w_buf,100))==1){if(errno==EAGAIN) printf("TheFIFOhasnotbeenreadyet.Pleasetrylater\n");}else printf("write%stotheFIFO\n",w_buf);}/*fifl_read.c*/#include<sys/types.h>#include<sys/stat.h>#include<errno.h>#include<fcntl.h>#include<stdio.h>#include<stdlib.h>#include<string.h>#defineFIFO"/tmp/myfifo"main(intargc,char**argv){charbuf_r[100];intfd;intnread;/*創(chuàng)建有名管道,并設(shè)置相應(yīng)的權(quán)限*/if((mkfifo(FIFO,O_CREAT|O_EXCL)<0)&&(errno!=EEXIST))printf("cannotcreatefifoserver\n");printf("Preparingforreadingbytes...\n");memset(buf_r,0,sizeof(buf_r));/*打開有名管道,并設(shè)置非阻塞標(biāo)志*/fd=open(FIFO,O_RDONLY|O_NONBLOCK,0);if(fd==-1){perror("open");exit(1);}while(1){memset(buf_r,0,sizeof(buf_r));if((nread=read(fd,buf_r,100))==-1){if(errno==EAGAIN)printf("nodatayet\n");}printf("read%sfromFIFO\n",buf_r);sleep(1);}pause();unlink(FIFO);}為了能夠較好地觀察運行結(jié)果,需要把這兩個程序分別在兩個終端里運行,在這里首先啟動讀管道程序。由于這是非阻塞管道,因此在建立管道之后程序就開始循環(huán)從管道里讀出內(nèi)容。在啟動了寫管道程序后,讀進(jìn)程能夠從管道里讀出用戶的輸入內(nèi)容,程序運行結(jié)果如下所示。終端一:#./readPreparingforreadingbytes…readfromFIFOreadfromFIFOreadfromFIFOreadfromFIFOreadfromFIFOreadhellofromFIFOreadfromFIFOreadfromFIFOreadFIFOfromFIFOreadfromFIFOreadfromFIFO…終端二:#./writehellowritehellototheFIFO#./readFIFOwriteFIFOtotheFIFO3.4.3信號通信(1)信號概述信號是UNIX中所使用的進(jìn)程通信的一種最古老的方法。它是在軟件層次上對中斷機(jī)制的一種模擬,是一種異步通信方式。信號可以直接進(jìn)行用戶空間進(jìn)程和內(nèi)核進(jìn)程之間的交互,內(nèi)核進(jìn)程也可以利用它來通知用戶空間進(jìn)程發(fā)生了哪些系統(tǒng)事件。它可以在任何時候發(fā)給某一進(jìn)程,而無需知道該進(jìn)程的狀態(tài)。如果該進(jìn)程當(dāng)前并未處于執(zhí)行態(tài),則該信號就由內(nèi)核保存起來,直到該進(jìn)程恢復(fù)執(zhí)行再傳遞給它為止;如果一個信號被進(jìn)程設(shè)置為阻塞,則該信號的傳遞被延遲,直到其阻塞被取消時才被傳遞給進(jìn)程。kill命令中“?l
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 醫(yī)療機(jī)構(gòu)品牌建設(shè)與宣傳管理方案
- 家庭裝修施工安全技術(shù)方案
- 人力資源流程標(biāo)準(zhǔn)化操作方案
- 城市照明節(jié)能改造項目方案報告
- 互聯(lián)網(wǎng)營銷推廣方案及績效評估
- 房地產(chǎn)營銷策劃方案案例解析
- 酒運營管理方案
- 消費者運營融合方案
- 新媒體培訓(xùn)運營活動方案
- 用戶運營微信營銷方案
- 婚姻家庭繼承實務(wù)講座
- 湖南省長沙市中學(xué)雅培粹中學(xué)2026屆中考一模語文試題含解析
- 新內(nèi)瘺穿刺護(hù)理
- 鉗工個人實習(xí)總結(jié)
- 大健康養(yǎng)肝護(hù)肝針專題課件
- 物流公司托板管理制度
- 道路高程測量成果記錄表-自動計算
- 關(guān)于醫(yī)院“十五五”發(fā)展規(guī)劃(2026-2030)
- DB31-T 1587-2025 城市軌道交通智能化運營技術(shù)規(guī)范
- 醫(yī)療護(hù)理操作評分細(xì)則
- 自考-經(jīng)濟(jì)思想史知識點大全
評論
0/150
提交評論