shell編程范例之進(jìn)程的操作
2008-02-21
這一小節(jié)寫(xiě)了很久,到現(xiàn)在才寫(xiě)完。本來(lái)關(guān)注的內(nèi)容比較多,包括程序開(kāi)發(fā)過(guò)程的細(xì)節(jié)、ELF格式的分析、進(jìn)程的內(nèi)存映像等,后來(lái)搞得“雪球越滾越大”,甚至 脫離了shell編程關(guān)注的內(nèi)容。所以呢,想了個(gè)小辦法,“大事化小,小事化了”,把涉及到的內(nèi)容分成如下幾個(gè)部分:
1、把VIM打造成源代碼編輯器(源代碼編輯過(guò)程:用VIM編輯代碼的一些技巧)
2、GCC編譯的背后 第一部分:預(yù)處理和編譯 第二部分:匯編和鏈接(編譯過(guò)程:預(yù)處理、編譯、匯編、鏈接)
3、程序執(zhí)行的那一剎那 (執(zhí)行過(guò)程:當(dāng)我們從命令行輸入一個(gè)命令之后)
4、進(jìn)程的內(nèi)存映像 (進(jìn)程加載過(guò)程:程序在內(nèi)存里是個(gè)什么樣子)
5、動(dòng)態(tài)符號(hào)鏈接的細(xì)節(jié)(動(dòng)態(tài)鏈接過(guò)程:函數(shù)puts/printf的地址在哪里)
6、代碼測(cè)試、調(diào)試與優(yōu)化小結(jié)(程序開(kāi)發(fā)過(guò)后:內(nèi)存溢出了嗎?有緩沖區(qū)溢出?代碼覆蓋率如何測(cè)試呢?怎么調(diào)試匯編代碼?有哪些代碼優(yōu)化技巧和方法呢?)
7、 8、進(jìn)程和進(jìn)程的基本操作(本小節(jié))
呵呵,好多。終于可以一部分一部分地完成了,不會(huì)再有一種對(duì)著一個(gè)大蛋糕卻不知道如何下口的尷尬了。
進(jìn)程作為程序真正發(fā)揮作用時(shí)的“形態(tài)”,我們有必要對(duì)它的一些相關(guān)操作非常熟悉,這一節(jié)主要描述進(jìn)程相關(guān)的概念和操作,將介紹包括程序、進(jìn)程、作業(yè)等基本概念以及進(jìn)程狀態(tài)查詢、進(jìn)程通信等相關(guān)的基本操作等。
1、什么是程序,什么又是進(jìn)程
程序是指令的集合,而進(jìn)程則是程序執(zhí)行的基本單元。為了讓程序完成它的工作,我們必須讓程序運(yùn)行起來(lái)成為進(jìn)程,進(jìn)而利用處理器資源,內(nèi)存資源,進(jìn)行各種I/O操作,從而完成某項(xiàng)指定的工作。
在這個(gè)意思上說(shuō),程序是靜態(tài)的,而進(jìn)程則是動(dòng)態(tài)的。
而進(jìn)程有區(qū)別于程序的地方還有,進(jìn)程除了包含程序文件中的指令數(shù)據(jù)意外,還需要在內(nèi)核中有一個(gè)數(shù)據(jù)結(jié)構(gòu)用以存放特定進(jìn)程的相關(guān)屬性,以便內(nèi)核更好的管理和調(diào)度進(jìn)程,從而完成多進(jìn)程協(xié)作的任務(wù)。因此,從這個(gè)意義上可以說(shuō)“高于”程序,超出了程序指令本身。
如果進(jìn)行過(guò)多進(jìn)程程序的開(kāi)發(fā),你又會(huì)發(fā)現(xiàn),一個(gè)程序可能創(chuàng)建多個(gè)進(jìn)程,通過(guò)多個(gè)進(jìn)程的交互完成任務(wù)。在Linux下,多進(jìn)程的創(chuàng)建通常是通過(guò)fork系統(tǒng)調(diào)用實(shí)現(xiàn)的。從這個(gè)意義上來(lái)說(shuō)程序則”包含”了進(jìn)程。
另外一個(gè)需要明確的是,程序可以由多種不同的程序語(yǔ)言描述,包括C語(yǔ)言程序、匯編語(yǔ)言程序和最后編譯產(chǎn)生的機(jī)器指令等。
下面我們簡(jiǎn)單討論一下Linux下面如何通過(guò)shell進(jìn)行進(jìn)程的相關(guān)操作。
2、進(jìn)程的創(chuàng)建
通常在命令行鍵入某個(gè)程序文件名以后,一個(gè)進(jìn)程就被創(chuàng)建了。例如,
| Quote: |
|
$ sleep 100 & #讓sleep程序在后臺(tái)運(yùn)行 |
當(dāng)一個(gè)程序被執(zhí)行以后,程序被加載到內(nèi)存中,成為了一個(gè)進(jìn)程。上面顯示了該進(jìn)程的內(nèi)存映像(虛擬內(nèi)存),包括程序指令、數(shù)據(jù),以及一些用于存放程命令行參數(shù)、環(huán)境變量的棧空間,用于動(dòng)態(tài)內(nèi)存申請(qǐng)的堆空間都被分配好了。
關(guān)于程序在命令行執(zhí)行過(guò)程的細(xì)節(jié),請(qǐng)參考《Linux命令行下程序執(zhí)行的那一剎那》。
實(shí)際上,創(chuàng)建一個(gè)進(jìn)程,也就是說(shuō)讓程序運(yùn)行,還有其他的辦法,比如,通過(guò)一些配置讓系統(tǒng)啟動(dòng)時(shí)自動(dòng)啟動(dòng)我們的程序(具體參考"man init"),或者是通過(guò)配置crond(或者at)讓它定時(shí)啟動(dòng)我們的程序。除此之外,還有一個(gè)方式,那就是編寫(xiě)shell腳本,把程序?qū)懭胍粋€(gè)腳本文 件,當(dāng)執(zhí)行腳本文件時(shí),文件中的程序?qū)⒈粓?zhí)行而成為進(jìn)程。這些方式的細(xì)節(jié)就不介紹了,下面介紹如何查看進(jìn)程的屬性。
需要補(bǔ)充一點(diǎn)的是,在命令行下執(zhí)行程序時(shí),我們可以通過(guò)ulimit內(nèi)置命令來(lái)設(shè)置進(jìn)程可以利用的資源,比如進(jìn)程可以打開(kāi)的最大文件描述符個(gè)數(shù),最大的棧空間,虛擬內(nèi)存空間等。具體用法見(jiàn)"help ulimit"。
3、查看進(jìn)程的屬性和狀態(tài)
我們可以通過(guò)ps命令查看進(jìn)程的相關(guān)屬性和狀態(tài),這些信息包括進(jìn)程所屬用戶,進(jìn)程對(duì)應(yīng)的程序,進(jìn)程對(duì)cpu和內(nèi)存的使用情況等信息。熟悉如何查看它們有助于我們進(jìn)行相關(guān)的統(tǒng)計(jì)分析和進(jìn)一步的操作。
| Quote: |
|
$ ps -ef #查看系統(tǒng)所有當(dāng)前進(jìn)程的屬性 |
由于系統(tǒng)所有進(jìn)程之間都有“親緣”關(guān)系,所以可以通過(guò)pstree查看這種關(guān)系,
| Quote: |
|
$ pstree #打印系統(tǒng)進(jìn)程調(diào)用樹(shù),可以非常清楚地看到當(dāng)前系統(tǒng)中所有活動(dòng)進(jìn)程之間的調(diào)用關(guān)系 |
動(dòng)態(tài)查看進(jìn)程信息,
| Quote: |
|
$ top |
該命令最大的特點(diǎn)是可以動(dòng)態(tài)的查看進(jìn)程的信息,當(dāng)然,它還提供了一些有用的參數(shù),比如-S可以按照累計(jì)執(zhí)行時(shí)間的大小排序查看,也可以通過(guò)-u查看指定用戶啟動(dòng)的進(jìn)程等。
感覺(jué)有上面幾個(gè)命令來(lái)查看進(jìn)程的信息就差不多了,下面來(lái)討論一個(gè)有趣的問(wèn)題:如何讓一個(gè)程序在同一時(shí)間只有一個(gè)在運(yùn)行。
這意味著當(dāng)一個(gè)程序正在被執(zhí)行時(shí),它將不能再被啟動(dòng)。那該怎么做呢?
假如一份相同的程序被復(fù)制成了很多份,并且具有不同的文件名被放在不同的位置,這個(gè)將比較糟糕,所以我們考慮最簡(jiǎn)單的情況,那就是這份程序在整個(gè)系統(tǒng)上是唯一的,而且名字也是唯一的。這樣的話,我們有哪些辦法來(lái)回答上面的問(wèn)題呢?
總的機(jī)理是:在這個(gè)程序的開(kāi)頭檢查自己有沒(méi)有執(zhí)行,如果執(zhí)行了則停止否則繼續(xù)執(zhí)行后續(xù)代碼。
策略則是多樣的,由于前面的假設(shè)已經(jīng)保證程序文件名和代碼的唯一性,所以
Code:
[Ctrl+A Select All]
Code:
[Ctrl+A Select All]
更多實(shí)現(xiàn)策略自己盡情的發(fā)揮吧!
4、調(diào)整進(jìn)程的優(yōu)先級(jí)
在保證每個(gè)進(jìn)程都能夠順利執(zhí)行外,為了讓某些任務(wù)優(yōu)先完成,那么系統(tǒng)在進(jìn)行進(jìn)程調(diào)度時(shí)就會(huì)采用一定的調(diào)度辦法,比如常見(jiàn)的有按照優(yōu)先級(jí)的時(shí)間片輪轉(zhuǎn)的調(diào)度算法。這種情況下,我們可以通過(guò)renice調(diào)整正在運(yùn)行的程序的優(yōu)先級(jí),例如,
| Quote: |
|
$ ps -e -o "%p %c %n" | grep xfs #打印的信息分別是進(jìn)程ID,進(jìn)程對(duì)應(yīng)的程序名,優(yōu)先級(jí) |
5、結(jié)束進(jìn)程
既然可以通過(guò)命令行執(zhí)行程序,創(chuàng)建進(jìn)程,那么也有辦法結(jié)束它。我們可以通過(guò)kill命令給用戶自己?jiǎn)?dòng)的進(jìn)程發(fā)送一定信號(hào)讓進(jìn)程終止,當(dāng)然“萬(wàn)能”的root幾乎可以kill所有進(jìn)程(除了init之外)。例如,
| Quote: |
|
$ sleep 50 & #啟動(dòng)一個(gè)進(jìn)程 |
kill命令默認(rèn)會(huì)發(fā)送終止信號(hào)(SIGTERM)給程序,讓程序退出,但是kill還可以發(fā)送其他的信號(hào),這些信號(hào)的定義我們可以通過(guò)man 7 signal查看到,也可以通過(guò)kill -l列出來(lái)。
| Quote: |
|
$ man 7 signal |
例如,我們用kill命令發(fā)送SIGSTOP信號(hào)給某個(gè)程序,讓它暫停,然后發(fā)送SIGCONT信號(hào)讓它繼續(xù)運(yùn)行。
| Quote: |
|
$ sleep 50 & |
可見(jiàn)kill命令為我們提供了非常好的功能,不過(guò)kill命令只能根據(jù)進(jìn)程的ID或者作業(yè)來(lái)控制進(jìn)程,所以pkill和killall給我們提供了更多選擇,它們擴(kuò)展了通過(guò)程序名甚至是進(jìn)程的用戶名來(lái)控制進(jìn)程的方法。更多用法請(qǐng)參考它們的手冊(cè)。
當(dāng)一個(gè)程序退出以后,如何判斷這個(gè)程序是正常退出還是異常退出呢?還記得Linux下,那個(gè)經(jīng)典"hello,world"程序嗎?在代碼的最后總是有條 “return 0”語(yǔ)句。這個(gè)“return 0”實(shí)際上是為了讓程序員來(lái)檢查進(jìn)程是否正常退出的。如果進(jìn)程返回了一個(gè)其他的數(shù)值,那么我們可以肯定的說(shuō)這個(gè)進(jìn)程異常退出了,因?yàn)樗紱](méi)有執(zhí)行到 “return 0”這條語(yǔ)句就退出了。
那怎么檢查進(jìn)程退出的狀態(tài),即那個(gè)返回的數(shù)值呢?
在shell程序中,我們可以檢查這個(gè)特殊的變量$?,它存放了上一條命令執(zhí)行后的退出狀態(tài)。
| Quote: |
|
$ test1 |
貌似返回0成為了一個(gè)潛規(guī)則,雖然沒(méi)有標(biāo)準(zhǔn)明確規(guī)定,不過(guò)當(dāng)程序正常返回時(shí),我們總是可以從$?中檢測(cè)到0,但是異常時(shí),我們總是檢測(cè)到一個(gè)非0的值。這 就告訴我們?cè)诔绦虻淖詈笪覀冏詈檬歉弦粋€(gè)exit 0以便任何人都可以通過(guò)檢測(cè)$?確定你的程序是否正常結(jié)束。如果有一天,有人偶爾用到你的程序,試圖檢查你的程序的退出狀態(tài),而你卻在程序的末尾莫名的返 回了一個(gè)-1或者1,那么他將會(huì)很苦惱,會(huì)懷疑自己的程序哪個(gè)地方出了問(wèn)題,檢查半天卻不知所措,因?yàn)樗湃文懔耍谷粡念^至尾都沒(méi)有懷疑你的編程習(xí)慣 可能會(huì)與眾不同。
6、進(jìn)程通信
為了便于設(shè)計(jì)和實(shí)現(xiàn),通常一個(gè)大型的任務(wù)都被劃分成較小的模塊。不同模塊之間啟動(dòng)后成為進(jìn)程,它們之間如何通信以便交互數(shù)據(jù),協(xié)同工作呢?在《UNIX環(huán) 境高級(jí)編程》一書(shū)中提到很多方法,諸如管道(無(wú)名管道和有名管道)、信號(hào)(signal)、報(bào)文(Message)隊(duì)列(消息隊(duì)列)、共享內(nèi)存 (mmap/munmap)、信號(hào)量(semaphore,主要是同步用,進(jìn)程之間,進(jìn)程的不同線程之間)、套接口(Socket,支持不同機(jī)器之間的進(jìn) 程通信)等,而在shell編程里頭,我們通常直接用到的就有管道和信號(hào)等。下面主要介紹管道和信號(hào)機(jī)制在shell編程時(shí)候的一些用法。
在Linux下,你可以通過(guò)"|"連接兩個(gè)程序,這樣就可以用它來(lái)連接后一個(gè)程序的輸入和前一個(gè)程序的輸出,因此被形象地叫做個(gè)管道。在C語(yǔ)言里頭,創(chuàng)建 無(wú)名管道非常簡(jiǎn)單方便,用pipe函數(shù),傳入一個(gè)具有兩個(gè)元素的int型的數(shù)組就可以。這個(gè)數(shù)組實(shí)際上保存的是兩個(gè)文件描述符,父進(jìn)程往第一個(gè)文件描述符 里頭寫(xiě)入東西后,子進(jìn)程可以從第一個(gè)文件描述符中讀出來(lái)。
如果用多了命令行,這個(gè)管子"|"應(yīng)該會(huì)經(jīng)常用。比如我們?cè)谏厦娴难菔局邪裵s命令的輸出作為grep命令的輸入,從而可以過(guò)濾掉一些我們感興趣的信息:
| Quote: |
|
$ ps -ef | grep init |
也許你會(huì)覺(jué)得這個(gè)“管子”好有魔法,竟然真地能夠鏈接兩個(gè)程序的輸入和輸出,它們到底是怎么實(shí)現(xiàn)的呢?實(shí)際上當(dāng)我們輸入這樣一組命令的時(shí)候,當(dāng)前解釋程序 會(huì)進(jìn)行適當(dāng)?shù)慕馕觯亚懊嬉粋€(gè)進(jìn)程的輸出關(guān)聯(lián)到管道的輸出文件描述符,把后面一個(gè)進(jìn)程的輸入關(guān)聯(lián)到管道的輸入文件描述符,這個(gè)關(guān)聯(lián)過(guò)程通過(guò)輸入輸出重定向 函數(shù)dup(或者fcntl)來(lái)實(shí)現(xiàn)。
有名管道實(shí)際上是一個(gè)文件(無(wú)名管道也像一個(gè)文件,雖然關(guān)系到兩個(gè)文件描述符,不過(guò)只能一邊讀另外一邊寫(xiě)),不過(guò)這個(gè)文件比較特別,操作時(shí)要滿足先進(jìn)先 出,而且,如果試圖讀一個(gè)沒(méi)有內(nèi)容的有名管道,那么就會(huì)被阻塞,同樣地,如果試圖往一個(gè)有名管道里頭寫(xiě)東西,而當(dāng)前沒(méi)有程序試圖讀它,也會(huì)被阻塞。下面看 看效果。
| Quote: |
|
$ mkfifo fifo_test #通過(guò)mkfifo命令可以創(chuàng)建一個(gè)有名管道 |
在這里echo和cat是兩個(gè)不同的程序,在這種情況下,通過(guò)echo和cat啟動(dòng)的兩個(gè)進(jìn)程之間并沒(méi)有父子關(guān)系。不過(guò)它們依然可以通過(guò)有名管道通信。這 樣一種通信方式非常適合某些情況:例如有這樣一個(gè)架構(gòu),這個(gè)架構(gòu)由兩個(gè)應(yīng)用程序構(gòu)成,其中一個(gè)通過(guò)一個(gè)循環(huán)不斷讀取fifo_test中的內(nèi)容,以便判 斷,它下一步要做什么。如果這個(gè)管道沒(méi)有內(nèi)容,那么它就會(huì)被阻塞在那里,而不會(huì)死循環(huán)而耗費(fèi)資源,另外一個(gè)則作為一個(gè)控制程序不斷地往fifo_test 中寫(xiě)入一些控制信息,以便告訴之前的那個(gè)程序該做什么。下面寫(xiě)一個(gè)非常簡(jiǎn)單的例子。我們可以設(shè)計(jì)一些控制碼,然控制程序不斷的往fifo_test里頭寫(xiě) 入,然后應(yīng)用程序根據(jù)這些控制碼完成不同的動(dòng)作。當(dāng)然,也可以往fifo_test傳入除控制碼外的不同的數(shù)據(jù)。
| Quote: |
|
$ cat app.sh #應(yīng)用程序的代碼 |
這樣一種應(yīng)用架構(gòu)非常適合本地的多程序任務(wù)的設(shè)計(jì),如果結(jié)合web cgi,那么也將適合遠(yuǎn)程控制的要求。引入web cgi的唯一改變是,要把控制程序./control.sh放到web的cgi目錄下,并對(duì)它作一些修改,以使它符合CGI的規(guī)范,這些規(guī)范包括文檔輸出 格式的表示(在文件開(kāi)頭需要輸出content-tpye: text/html以及一個(gè)空白行)和輸入?yún)?shù)的獲取(web輸入?yún)?shù)都存放在QUERY_STRING環(huán)境變量里頭)。因此一個(gè)非常簡(jiǎn)單的CGI形式控 制程序?qū)㈩?lèi)似下面。
Code:
[Ctrl+A Select All]
在實(shí)際使用的時(shí)候,請(qǐng)確保control.sh能夠訪問(wèn)到fifo_test管道,并且有寫(xiě)權(quán)限。這樣我們?cè)跒g覽器上就可以這樣控制app.sh了。
http://ipaddress_or_dns/cgi-bin/control.sh?0
問(wèn)號(hào)(?)后面的內(nèi)容即QUERY_STRING,類(lèi)似之前的$1。
這樣一種應(yīng)用對(duì)于遠(yuǎn)程控制,特別是嵌入式系統(tǒng)的遠(yuǎn)程控制很有實(shí)際意義。在去年的暑期課程上,我們就通過(guò)這樣一種方式來(lái)實(shí)現(xiàn)馬達(dá)的遠(yuǎn)程控制。首先,我們實(shí)現(xiàn) 了一個(gè)簡(jiǎn)單的應(yīng)用程序以便控制馬達(dá)的轉(zhuǎn)動(dòng),包括轉(zhuǎn)速,方向等的控制。為了實(shí)現(xiàn)遠(yuǎn)程控制,我們?cè)O(shè)計(jì)了一些控制碼,以便控制馬達(dá)轉(zhuǎn)動(dòng)相關(guān)的不同屬性。
在C語(yǔ)言中,如果要用有名管道,和shell下的類(lèi)似,只不過(guò)在讀寫(xiě)數(shù)據(jù)的時(shí)候用read,write調(diào)用,在創(chuàng)建fifo的時(shí)候用mkfifo函數(shù)調(diào)用。
信號(hào)是軟件中斷,在Linux下面用戶可以通過(guò)kill命令給某個(gè)進(jìn)程發(fā)送一個(gè)特定的信號(hào),也可以通過(guò)鍵盤(pán)發(fā)送一些信號(hào),比如CTRL+C可能觸發(fā) CGIINT信號(hào),而CTRL+\可能觸發(fā)SGIQUIT信號(hào)等,除此之外,內(nèi)核在某些情況下也會(huì)給進(jìn)程發(fā)送信號(hào),比如在訪問(wèn)內(nèi)存越界時(shí)產(chǎn)生 SGISEGV信號(hào),當(dāng)然,進(jìn)程本身也可以通過(guò)kill,raise等函數(shù)給自己發(fā)送信號(hào)。對(duì)于Linux下支持的信號(hào)類(lèi)型,大家可以通過(guò)"man 7 signal"或者"kill -l"查看到相關(guān)列表和說(shuō)明。
對(duì)于有些信號(hào),進(jìn)程會(huì)有默認(rèn)的響應(yīng)動(dòng)作,而有些信號(hào),進(jìn)程可能直接會(huì)忽略,當(dāng)然,用戶還可以對(duì)某些信號(hào)設(shè)定專門(mén)的處理函數(shù)。在shell程序中,我們可以 通過(guò)trap命令(shell的內(nèi)置命令)來(lái)設(shè)定響應(yīng)某個(gè)信號(hào)的動(dòng)作(某個(gè)命令或者是你定義的某個(gè)函數(shù)),而在C語(yǔ)言里頭可以通過(guò)signal調(diào)用注冊(cè)某 個(gè)信號(hào)的處理函數(shù)。這里僅僅演示trap命令的用法。
| Quote: |
|
$ function signal_handler { #定一個(gè)signal_handler的函數(shù),>是按下?lián)Q行符號(hào)自動(dòng)出現(xiàn)的 |
類(lèi)似地,如果設(shè)定信號(hào)0的響應(yīng)動(dòng)作,那么就可以用trap來(lái)模擬C語(yǔ)言程序中的atexit程序終止函數(shù)的登記,即通過(guò)trap signal_handler SIGQUIT設(shè)定的signal_handler函數(shù)將在程序退出的時(shí)候被執(zhí)行。信號(hào)0是一個(gè)特別的信號(hào),在POSIX.1中把信號(hào)編號(hào)0定義為空信 號(hào),這將常被用來(lái)確定一個(gè)特定進(jìn)程是否仍舊存在。當(dāng)一個(gè)程序退出時(shí)會(huì)觸發(fā)該信號(hào)。
| Quote: |
|
$ cat sigexit.sh |
7、作業(yè)和作業(yè)控制
當(dāng)我們?yōu)橥瓿梢恍?fù)雜的任務(wù)而將多個(gè)命令通過(guò)|,>,<, ;, (, )等組合在一起的時(shí)候,通常這樣個(gè)命令序列會(huì)啟動(dòng)多個(gè)進(jìn)程,它們之間通過(guò)管道等進(jìn)行通信。而有些時(shí)候,我們?cè)趫?zhí)行一個(gè)任務(wù)的同時(shí),還有其他的任務(wù)需要處 理,那么就經(jīng)常會(huì)在命令序列的最后加上一個(gè)&,或者在執(zhí)行命令以后,按下CTRL+Z讓前一個(gè)命令暫停。以便做其他的任務(wù)。等做完其他一些任務(wù)以 后,再通過(guò)fg命令把后臺(tái)的任務(wù)切換到前臺(tái)。這樣一種控制過(guò)程通常被成為作業(yè)控制,而那些命令序列則被成為作業(yè),這個(gè)作業(yè)可能涉及一個(gè)或者多個(gè)程序,一個(gè) 或者多個(gè)進(jìn)程。下面演示一下幾個(gè)常用的作業(yè)控制操作。
| Quote: |
|
$ sleep 50 & #讓程序在后臺(tái)運(yùn)行,將打印[作業(yè)號(hào)]和進(jìn)程號(hào) |
不過(guò),要在命令行下使用作業(yè)控制,需要當(dāng)前shell,內(nèi)核終端驅(qū)動(dòng)等對(duì)作業(yè)控制支持才行。
參考資料
<UNIX環(huán)境高級(jí)編程>相關(guān)章節(jié)
posted on 2008-03-14 15:34 隨意門(mén) 閱讀(1021) 評(píng)論(0) 編輯 收藏 引用
