Linux IPC 3 之信號

信號介紹

信號是在軟件層次上對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一箇中斷請求可以說是一樣的。信號是異步的,一個進程不必通過任何操作來等待信號的到達,事實上,進程也不知道信號到底什麼時候到達。

信號的產生是隨機的,進程只需要註冊信號處理函數,在信號到來時執行信號處理函數即可。

信號的分類

可靠信號==實時信號, 不可靠信號==非實時信號

root@kali:~/桌面# kill -l
 1) SIGHUP   2) SIGINT   3) SIGQUIT  4) SIGILL   5) SIGTRAP
 6) SIGABRT  7) SIGBUS   8) SIGFPE   9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG  24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF 28) SIGWINCH    29) SIGIO   30) SIGPWR
31) SIGSYS  34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX    

上圖顯示所有信號
1. 不可靠信號
那些從UNIX系統繼承過來的信號都是非可靠信號, 表現在信號不支持排隊,信號可能會丟失, 比如發送多次相同的信號, 進程只能收到一次. 信號值小於SIGRTMIN的都是非可靠信號.
2. 可靠信號
後來, Linux改進了信號機制, 增加了32種新的信號, 這些信號都是可靠信號, 表現在信號支持排隊, 不會丟失, 發多少次, 就可以收到多少次. 信號值位於 [SIGRTMIN, SIGRTMAX] 區間的都是可靠信號

信號機制處理流程

對於一個完整的信號生命週期(從信號發送到相應的處理函數執行完畢)來說,可以分爲三個階段:

信號誕生
信號在進程中註冊
信號的執行和註銷

信號誕生

信號事件的發生有兩個來源:硬件來源(比如我們按下了鍵盤或者其它硬件故障);軟件來源,最常用發送信號的系統函數是kill, raise, alarm和setitimer以及sigqueue函數,軟件來源還包括一些非法運算等操作。

這裏按發出信號的原因簡單分類,以瞭解各種信號:

1) 與進程終止相關的信號。當進程退出,或者子進程終止時,發出這類信號。

(2) 與進程例外事件相關的信號。如進程越界,或企圖寫一個只讀的內存區域(如程序正文區),或執行一個特權指令及其他各種硬件錯誤。

(3) 與在系統調用期間遇到不可恢復條件相關的信號。如執行系統調用exec時,原有資源已經釋放,而目前系統資源又已經耗盡。

(4) 與執行系統調用時遇到非預測錯誤條件相關的信號。如執行一個並不存在的系統調用。

(5) 在用戶態下的進程發出的信號。如進程調用系統調用kill向其他進程發送信號。

(6) 與終端交互相關的信號。如用戶關閉一個終端,或按下break鍵等情況。

(7) 跟蹤進程執行的信號。

Linux支持的信號列表如下。很多信號是與機器的體系結構相關的

信號值 默認處理動作 發出信號的原因

SIGHUP 1 A 終端掛起或者控制進程終止

SIGINT 2 A 鍵盤中斷(如break鍵被按下)

SIGQUIT 3 C 鍵盤的退出鍵被按下

SIGILL 4 C 非法指令

SIGABRT 6 C 由abort(3)發出的退出指令

SIGFPE 8 C 浮點異常

SIGKILL 9 AEF Kill信號

SIGSEGV 11 C 無效的內存引用

SIGPIPE 13 A 管道破裂: 寫一個沒有讀端口的管道

SIGALRM 14 A 由alarm(2)發出的信號

SIGTERM 15 A 終止信號

SIGUSR1 30,10,16 A 用戶自定義信號1

SIGUSR2 31,12,17 A 用戶自定義信號2

SIGCHLD 20,17,18 B 子進程結束信號

SIGCONT 19,18,25 進程繼續(曾被停止的進程)

SIGSTOP 17,19,23 DEF 終止進程

SIGTSTP 18,20,24 D 控制終端(tty)上按下停止鍵

SIGTTIN 21,21,26 D 後臺進程企圖從控制終端讀

SIGTTOU 22,22,27 D 後臺進程企圖從控制終端寫


處理動作一項中的字母含義如下

A 缺省的動作是終止進程

B 缺省的動作是忽略此信號,將該信號丟棄,不做處理

C 缺省的動作是終止進程並進行內核映像轉儲(dump core),內核映像轉儲是指將進程數據在內存的映像和進程在內核結構中的部分內容以一定格式轉儲到文件系統,並且進程退出執行,這樣做的好處是爲程序員提供了方便,使得他們可以得到進程當時執行時的數據值,允許他們確定轉儲的原因,並且可以調試他們的程序。

D 缺省的動作是停止進程,進入停止狀況以後還能重新進行下去,一般是在調試的過程中(例如ptrace系統調用)

E 信號不能被捕獲

F 信號不能被忽略

信號在目標進程中註冊

在進程表的表項中有一個軟中斷信號域,該域中每一位對應一個信號。內核給一個進程發送軟中斷信號的方法,是在進程所在的進程表項的信號域設置對應於該信號的位。如果信號發送給一個正在睡眠的進程,如果進程睡眠在可被中斷的優先級上,則喚醒進程;否則僅設置進程表中信號域相應的位,而不喚醒進程。如果發送給一個處於可運行狀態的進程,則只置相應的域即可。

進程的task_struct結構中有關於本進程中未決信號的數據成員:

struct sigpending pending:
struct sigpending{
        struct sigqueue *head, *tail;
        sigset_t signal;
};

第三個成員是進程中所有未決信號集,第一、第二個成員分別指向一個sigqueue類型的結構鏈(稱之爲”未決信號信息鏈”)的首尾,信息鏈中的每個sigqueue結構刻畫一個特定信號所攜帶的信息,並指向下一個sigqueue結構:

struct sigqueue{
        struct sigqueue *next;
        siginfo_t info;
}

信號在進程中註冊指的就是信號值加入到進程的未決信號集sigset_t signal(每個信號佔用一位)中,並且信號所攜帶的信息被保留到未決信號信息鏈的某個sigqueue結構中。只要信號在進程的未決信號集中,表明進程已經知道這些信號的存在,但還沒來得及處理,或者該信號被進程阻塞。

當一個實時信號發送給一個進程時,不管該信號是否已經在進程中註冊,都會被再註冊一次,因此,信號不會丟失,因此,實時信號又叫做”可靠信號”。這意味着同一個實時信號可以在同一個進程的未決信號信息鏈中佔有多個sigqueue結構(進程每收到一個實時信號,都會爲它分配一個結構來登記該信號信息,並把該結構添加在未決信號鏈尾,即所有誕生的實時信號都會在目標進程中註冊)。

當一個非實時信號發送給一個進程時,如果該信號已經在進程中註冊(通過sigset_t signal指示),則該信號將被丟棄,造成信號丟失。因此,非實時信號又叫做”不可靠信號”。這意味着同一個非實時信號在進程的未決信號信息鏈中,至多佔有一個sigqueue結構。

總之信號註冊與否,與發送信號的函數(如kill()或sigqueue()等)以及信號安裝函數(signal()及sigaction())無關,只與信號值有關(信號值小於SIGRTMIN的信號最多隻註冊一次,信號值在SIGRTMIN及SIGRTMAX之間的信號,只要被進程接收到就被註冊)

信號的執行和註銷

內核處理一個進程收到的軟中斷信號是在該進程的上下文中,因此,進程必須處於運行狀態。當其由於被信號喚醒或者正常調度重新獲得CPU時,在其從內核空間返回到用戶空間時會檢測是否有信號等待處理。如果存在未決信號等待處理且該信號沒有被進程阻塞,則在運行相應的信號處理函數前,進程會把信號在未決信號鏈中佔有的結構卸掉。

對於非實時信號來說,由於在未決信號信息鏈中最多隻佔用一個sigqueue結構,因此該結構被釋放後,應該把信號在進程未決信號集中刪除(信號註銷完畢);而對於實時信號來說,可能在未決信號信息鏈中佔用多個sigqueue結構,因此應該針對佔用sigqueue結構的數目區別對待:如果只佔用一個sigqueue結構(進程只收到該信號一次),則執行完相應的處理函數後應該把信號在進程的未決信號集中刪除(信號註銷完畢)。否則待該信號的所有sigqueue處理完畢後再在進程的未決信號集中刪除該信號。

當所有未被屏蔽的信號都處理完畢後,即可返回用戶空間。對於被屏蔽的信號,當取消屏蔽後,在返回到用戶空間時會再次執行上述檢查處理的一套流程。

內核處理一個進程收到的信號的時機是在一個進程從內核態返回用戶態時。所以,當一個進程在內核態下運行時,軟中斷信號並不立即起作用,要等到將返回用戶態時才處理。進程只有處理完信號纔會返回用戶態,進程在用戶態下不會有未處理完的信號。

處理信號有三種類型:進程接收到信號後退出;進程忽略該信號;進程收到信號後執行用戶設定用系統調用signal的函數。當進程接收到一個它忽略的信號時,進程丟棄該信號,就象沒有收到該信號似的繼續運行。如果進程收到一個要捕捉的信號,那麼進程從內核態返回用戶態時執行用戶定義的函數。而且執行用戶定義的函數的方法很巧妙,內核是在用戶棧上創建一個新的層,該層中將返回地址的值設置成用戶定義的處理函數的地址,這樣進程從內核返回彈出棧頂時就返回到用戶定義的函數處,從函數返回再彈出棧頂時,才返回原先進入內核的地方。這樣做的原因是用戶定義的處理函數不能且不允許在內核態下執行(如果用戶定義的函數在內核態下運行的話,用戶就可以獲得任何權限)。

信號的安裝

如果進程要處理某一信號,那麼就要在進程中安裝該信號。安裝信號主要用來確定信號值及進程針對該信號值的動作之間的映射關係,即進程將要處理哪個信號;該信號被傳遞給進程時,將執行何種操作。

linux主要有兩個函數實現信號的安裝:signal()、sigaction()。其中signal()只有兩個參數,不支持信號傳遞信息,主要是用於前32種非實時信號的安裝;而sigaction()是較新的函數(由兩個系統調用實現:sys_signal以及sys_rt_sigaction),有三個參數,支持信號傳遞信息,主要用來與 sigqueue() 系統調用配合使用,當然,sigaction()同樣支持非實時信號的安裝。sigaction()優於signal()主要體現在支持信號帶有參數。

signal
#include <signal.h>

void (*signal(int signum, void (*handler))(int)))(int);

如果該函數原型不容易理解的話,可以參考下面的分解方式來理解:

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler));
第一個參數指定信號的值,第二個參數指定針對前面信號值的處理,可以忽略該信號(參數設爲SIG_IGN);可以採用系統默認方式處理信號(參數設爲SIG_DFL);也可以自己實現處理方式(參數指定一個函數地址)。

如果signal()調用成功,返回最後一次爲安裝信號signum而調用signal()時的handler值;失敗則返回SIG_ERR。

傳遞給信號處理例程的整數參數是信號值,這樣可以使得一個信號處理例程處理多個信號。
sigaction
#include <signal.h>

int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));
sigaction函數用於改變進程接收到特定信號後的行爲。該函數的第一個參數爲信號的值,可以爲除SIGKILL及SIGSTOP外的任何一個特定有效的信號(爲這兩個信號定義自己的處理函數,將導致信號安裝錯誤)。第二個參數是指向結構sigaction的一個實例的指針,在結構sigaction的實例中,指定了對特定信號的處理,可以爲空,進程會以缺省方式對信號處理;第三個參數oldact指向的對象用來保存返回的原來對相應信號的處理,可指定oldact爲NULL。如果把第二、第三個參數都設爲NULL,那麼該函數可用於檢查信號的有效性。

第二個參數最爲重要,其中包含了對指定信號的處理、信號所傳遞的信息、信號處理函數執行過程中應屏蔽掉哪些信號等等。

sigaction結構定義如下:

struct sigaction {
                 union{
                       __sighandler_t _sa_handler;
                       void (*_sa_sigaction)(int,struct siginfo *, void *);   
                    }_u

            sigset_t sa_mask;
            unsigned long sa_flags;
}
1、聯合數據結構中的兩個元素_sa_handler以及*_sa_sigaction指定信號關聯函數,即用戶指定的信號處理函數。除了可以是用戶自定義的處理函數外,還可以爲SIG_DFL(採用缺省的處理方式),也可以爲SIG_IGN(忽略信號)。

2、由_sa_sigaction是指定的信號處理函數帶有三個參數,是爲實時信號而設的(當然同樣支持非實時信號),它指定一個3參數信號處理函數。第一個參數爲信號值,第三個參數沒有使用,第二個參數是指向siginfo_t結構的指針,結構中包含信號攜帶的數據值,參數所指向的結構如下:
siginfo_t {
                  int      si_signo;  /* 信號值,對所有信號有意義*/
                  int      si_errno;  /* errno值,對所有信號有意義*/
                  int      si_code;   /* 信號產生的原因,對所有信號有意義*/
                  union{/* 聯合數據結構,不同成員適應不同信號 */

                                       //確保分配足夠大的存儲空間

                                       int _pad[SI_PAD_SIZE];

                                       //對SIGKILL有意義的結構

                                       struct{

                                                      ...

                                                 }...

                                               ... ...

                                               ... ...                               

                                       //對SIGILL, SIGFPE, SIGSEGV, SIGBUS有意義的結構

                                  struct{

                                                      ...

                                                 }...

                                               ... ...

                                         }

}

前面在討論系統調用sigqueue發送信號時,sigqueue的第三個參數就是sigval聯合數據結構,當調用sigqueue時,該數據結構中的數據就將拷貝到信號處理函數的第二個參數中。這樣,在發送信號同時,就可以讓信號傳遞一些附加信息。信號可以傳遞信息對程序開發是非常有意義的。

3、sa_mask指定在信號處理程序執行過程中,哪些信號應當被阻塞。缺省情況下當前信號本身被阻塞,防止信號的嵌套發送,除非指定SA_NODEFER或者SA_NOMASK標誌位。

注:請注意sa_mask指定的信號阻塞的前提條件,是在由sigaction()安裝信號的處理函數執行過程中由sa_mask指定的信號才被阻塞。

4、sa_flags中包含了許多標誌位,包括剛剛提到的SA_NODEFER及SA_NOMASK標誌位。另一個比較重要的標誌位是SA_SIGINFO,當設定了該標誌位時,表示信號附帶的參數可以被傳遞到信號處理函數中,因此,應該爲sigaction結構中的sa_sigaction指定處理函數,而不應該爲sa_handler指定信號處理函數,否則,設置該標誌變得毫無意義。即使爲sa_sigaction指定了信號處理函數,如果不設置SA_SIGINFO,信號處理函數同樣不能得到信號傳遞過來的數據,在信號處理函數中對這些信息的訪問都將導致段錯誤(Segmentation fault)。

信號的發送

發送信號的主要函數有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。

kill

#include <signal.h>

int kill(pid_t pid,int signo)



該系統調用可以用來向任何進程或進程組發送任何信號。參數pid的值爲信號的接收進程

pid>0 進程ID爲pid的進程

pid=0 同一個進程組的進程

pid<0 pid!=-1 進程組ID爲 -pid的所有進程

pid=-1 除發送進程自身外,所有進程ID大於1的進程



Sinno是信號值,當爲0時(即空信號),實際不發送任何信號,但照常進行錯誤檢查,因此,可用於檢查目標進程是否存在,以及當前進程是否具有向目標發送信號的權限(root權限的進程可以向任何進程發送信號,非root權限的進程只能向屬於同一個session或者同一個用戶的進程發送信號)。



Kill()最常用於pid>0時的信號發送。該調用執行成功時,返回值爲0;錯誤時,返回-1,並設置相應的錯誤代碼errno。下面是一些可能返回的錯誤代碼:

EINVAL:指定的信號sig無效。

ESRCH:參數pid指定的進程或進程組不存在。注意,在進程表項中存在的進程,可能是一個還沒有被wait收回,但已經終止執行的僵死進程。

EPERM: 進程沒有權力將這個信號發送到指定接收信號的進程。因爲,一個進程被允許將信號發送到進程pid時,必須擁有root權力,或者是發出調用的進程的UID 或EUID與指定接收的進程的UID或保存用戶ID(savedset-user-ID)相同。如果參數pid小於-1,即該信號發送給一個組,則該錯誤表示組中有成員進程不能接收該信號

sigqueue

#include <sys/types.h>
#include <signal.h>

int sigqueue(pid_t pid, int sig, const union sigval val)

調用成功返回 0;否則,返回 -1。



sigqueue()是比較新的發送信號系統調用,主要是針對實時信號提出的(當然也支持前32種),支持信號帶有參數,與函數sigaction()配合使用。

sigqueue的第一個參數是指定接收信號的進程ID,第二個參數確定即將發送的信號,第三個參數是一個聯合數據結構union sigval,指定了信號傳遞的參數,即通常所說的4字節值。

typedef union sigval {

               int  sival_int;

               void *sival_ptr;

}sigval_t;

sigqueue()比kill()傳遞了更多的附加信息,但sigqueue()只能向一個進程發送信號,而不能發送信號給一個進程組。如果signo=0,將會執行錯誤檢查,但實際上不發送任何信號,0值信號可用於檢查pid的有效性以及當前進程是否有權限向目標進程發送信號。

在調用sigqueue時,sigval_t指定的信息會拷貝到對應sig 註冊的3參數信號處理函數的siginfo_t結構中,這樣信號處理函數就可以處理這些信息了。由於sigqueue系統調用支持發送帶參數信號,所以比kill()系統調用的功能要靈活和強大得多。

alarm

#include <unistd.h>
unsigned int alarm(unsigned int seconds)

系統調用alarm安排內核爲調用進程在指定的seconds秒後發出一個SIGALRM的信號。如果指定的參數seconds爲0,則不再發送 SIGALRM信號。後一次設定將取消前一次的設定。該調用返回值爲上次定時調用到發送之間剩餘的時間,或者因爲沒有前一次定時調用而返回0。

注意,在使用時,alarm只設定爲發送一次信號,如果要多次發送,就要多次使用alarm調用。

setitimer

現在的系統中很多程序不再使用alarm調用,而是使用setitimer調用來設置定時器,用getitimer來得到定時器的狀態,這兩個調用的聲明格式如下:

int getitimer(int which, struct itimerval *value);

int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);

在使用這兩個調用的進程中加入以下頭文件:

#include <sys/time.h>

該系統調用給進程提供了三個定時器,它們各自有其獨有的計時域,當其中任何一個到達,就發送一個相應的信號給進程,並使得計時器重新開始。三個計時器由參數which指定,如下所示:

TIMER_REAL:按實際時間計時,計時到達將給進程發送SIGALRM信號。
ITIMER_VIRTUAL:僅當進程執行時才進行計時。計時到達將發送SIGVTALRM信號給進程。
ITIMER_PROF:當進程執行時和系統爲該進程執行動作時都計時。與ITIMER_VIR-TUAL是一對,該定時器經常用來統計進程在用戶態和內核態花費的時間。計時到達將發送SIGPROF信號給進程。

定時器中的參數value用來指明定時器的時間,其結構如下:

struct itimerval {

        struct timeval it_interval; /* 下一次的取值 */

        struct timeval it_value; /* 本次的設定值 */

};

該結構中timeval結構定義如下:

struct timeval {

        long tv_sec; /* 秒 */

        long tv_usec; /* 微秒,1秒 = 1000000 微秒*/

};

在setitimer 調用中,參數ovalue如果不爲空,則其中保留的是上次調用設定的值。定時器將it_value遞減到0時,產生一個信號,並將it_value的值設定爲it_interval的值,然後重新開始計時,如此往復。當it_value設定爲0時,計時器停止,或者當它計時到期,而it_interval 爲0時停止。調用成功時,返回0;錯誤時,返回-1,並設置相應的錯誤代碼errno:

EFAULT:參數value或ovalue是無效的指針。

EINVAL:參數which不是ITIMER_REAL、ITIMER_VIRT或ITIMER_PROF中的一個。

abort

#include <stdlib.h>

void abort(void);

向進程發送SIGABORT信號,默認情況下進程會異常退出,當然可定義自己的信號處理函數。即使SIGABORT被進程設置爲阻塞信號,調用abort()後,SIGABORT仍然能被進程接收。該函數無返回值。

raise

#include <signal.h>
int raise(int signo)
向進程本身發送信號,參數爲即將發送的信號值。調用成功返回 0;否則,返回 -1

信號集及信號集操作函數:

信號集被定義爲一種數據類型:

typedef struct {

                       unsigned long sig[_NSIG_WORDS];

} sigset_t

信號集用來描述信號的集合,每個信號佔用一位。Linux所支持的所有信號可以全部或部分的出現在信號集中,主要與信號阻塞相關函數配合使用。下面是爲信號集操作定義的相關函數:



#include <signal.h>

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signum)

int sigdelset(sigset_t *set, int signum);

int sigismember(const sigset_t *set, int signum);

sigemptyset(sigset_t *set)初始化由set指定的信號集,信號集裏面的所有信號被清空;

sigfillset(sigset_t *set)調用該函數後,set指向的信號集中將包含linux支持的64種信號;

sigaddset(sigset_t *set, int signum)在set指向的信號集中加入signum信號;

sigdelset(sigset_t *set, int signum)在set指向的信號集中刪除signum信號;

sigismember(const sigset_t *set, int signum)判定信號signum是否在set指向的信號集中。

信號阻塞與信號未決:

每個進程都有一個用來描述哪些信號遞送到進程時將被阻塞的信號集,該信號集中的所有信號在遞送到進程後都將被阻塞。下面是與信號阻塞相關的幾個函數:

#include <signal.h>
int  sigprocmask(int  how,  const  sigset_t *set, sigset_t *oldset));

int sigpending(sigset_t *set));

int sigsuspend(const sigset_t *mask));


sigprocmask()函數能夠根據參數how來實現對信號集的操作,操作主要有三種:

SIG_BLOCK 在進程當前阻塞信號集中添加set指向信號集中的信號

SIG_UNBLOCK 如果進程阻塞信號集中包含set指向信號集中的信號,則解除對該信號的阻塞

SIG_SETMASK 更新進程阻塞信號集爲set指向的信號集

sigpending(sigset_t *set))獲得當前已遞送到進程,卻被阻塞的所有信號,在set指向的信號集中返回結果。

sigsuspend(const sigset_t *mask))用於在接收到某個信號之前, 臨時用mask替換進程的信號掩碼, 並暫停進程執行,直到收到信號爲止。sigsuspend 返回後將恢復調用之前的信號掩碼。信號處理函數完成後,進程將繼續執行。該系統調用始終返回-1,並將errno設置爲EINTR。

信號處理方式

進程收到信號處理方式有三種:

  1. 忽略信號;大多數信號可以忽略,但兩種信號除外:SIGKILl和SIGSTOP。
  2. 執行系統默認操作;大部分信號的默認操作是終止該進程。
  3. 捕捉信號並執行信號處理函數;即在收到信號時,執行用戶自己定義的函數。

[1] http://blog.csdn.net/qq_26819579/article/details/52942205
[2] http://blog.csdn.net/shallnet/article/details/41451601
[3] http://www.cnblogs.com/yangang92/p/5679641.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章