ARM中斷處理過程

轉自:http://www.wowotech.net/irq_handler.htmlhtml

 

1、前言node

本文主要以ARM體系結構下的中斷處理爲例,講述整個中斷處理過程當中的硬件行爲和軟件動做。具體整個處理過程分紅三個步驟來描述:linux

一、第二章描述了中斷處理的準備過程算法

二、第三章描述了當發生中的時候,ARM硬件的行爲bootstrap

三、第四章描述了ARM的中斷進入過程數據結構

四、第五章描述了ARM的中斷退出過程app

2、中斷處理的準備過程dom

ARM處理器有多種processor mode,例如user mode(用戶空間的AP所處於的模式)、supervisor mode(即SVC mode,大部分的內核態代碼都處於這種mode)、IRQ mode(發生中斷後,處理器會切入到該mode)等。函數

對於linux kernel,其中斷處理處理過程當中,ARM 處理器大部分都是處於SVC mode。fetch

可是,實際上產生中斷的時候,ARM處理器其實是先進入IRQ mode,所以在進入真正的IRQ異常處理以前會有一小段IRQ mode的操做,以後會進入SVC mode進行真正的IRQ異常處理。因爲IRQ mode只是一個過分,所以IRQ mode的棧很小,只有12個字節,具體以下:

sprdroid9.0_trunk/kernel4.4/arch/arm/kernel/setup.c

132/*
133 * Cached cpu_architecture() result for use by assembler code.
134 * C code should use the cpu_architecture() function instead of accessing this
135 * variable directly.
136 */
137int __cpu_architecture __read_mostly = CPU_ARCH_UNKNOWN;
138
139struct stack {
140	u32 irq[3];
141	u32 abt[3];
142	u32 und[3];
143	u32 fiq[3];
144} ____cacheline_aligned;

除了irq mode,linux kernel在處理abt mode(當發生data abort exception或者prefetch abort exception的時候進入的模式)和und mode(處理器遇到一個未定義的指令的時候進入的異常模式)的時候也是採用了相同的策略。

也就是通過一個簡短的abt或者und mode以後,stack切換到svc mode的棧上,這個棧就是發生異常那個時間點current thread的內核棧

anyway,在irq mode和svc mode之間老是須要一個stack保存數據,這就是中斷模式的stack,系統初始化的時候,cpu_init函數中會進行中斷模式stack的設定:

/*
518 * cpu_init - initialise one CPU.
519 *
520 * cpu_init sets up the per-CPU stacks.
521 */
522void notrace cpu_init(void)
523{
524#ifndef CONFIG_CPU_V7M
525    unsigned int cpu = smp_processor_id();------獲取CPU ID
526    struct stack *stk = &stacks[cpu];---------獲取該CPU對於的irq abt和und的stack指針
527
528    if (cpu >= NR_CPUS) {
529        pr_crit("CPU%u: bad primary CPU number\n", cpu);
530        BUG();
531    }
532
533    /*
534     * This only works on resume and secondary cores. For booting on the
535     * boot cpu, smp_prepare_boot_cpu is called after percpu area setup.
536     */
537    set_my_cpu_offset(per_cpu_offset(cpu));
538
539    cpu_proc_init();
540
541    /*
542     * Define the placement constraint for the inline asm directive below.
543     * In Thumb-2, msr with an immediate value is not allowed.
544     */
545#ifdef CONFIG_THUMB2_KERNEL
546#define PLC    "r"------Thumb-2下,msr指令不容許使用當即數,只能使用寄存器。
547#else
548#define PLC    "I"
549#endif
550
551    /*
552     * setup stacks for re-entrant exception handlers
553     */
554    __asm__ (
555    "msr    cpsr_c, %1\n\t"------讓CPU進入IRQ mode 
556    "add    r14, %0, %2\n\t"------r14寄存器保存stk->irq 
557    "mov    sp, r14\n\t"--------設定IRQ mode的stack爲stk->irq 
558    "msr    cpsr_c, %3\n\t"
559    "add    r14, %0, %4\n\t"
560    "mov    sp, r14\n\t"--------設定abt mode的stack爲stk->abt 
561    "msr    cpsr_c, %5\n\t"
562    "add    r14, %0, %6\n\t"
563    "mov    sp, r14\n\t"--------設定und mode的stack爲stk->und 
564    "msr    cpsr_c, %7\n\t"
565    "add    r14, %0, %8\n\t"
566    "mov    sp, r14\n\t"--------設定fiq mode的stack爲stk->fiq 
567    "msr    cpsr_c, %9"--------回到SVC mode
568        :--------------------上面是code,下面的output部分是空的 
569        : "r" (stk),----------------------對應上面代碼中的%0 
570          PLC (PSR_F_BIT | PSR_I_BIT | IRQ_MODE),------對應上面代碼中的%1
571          "I" (offsetof(struct stack, irq[0])),------------對應上面代碼中的%2 
572          PLC (PSR_F_BIT | PSR_I_BIT | ABT_MODE),------以此類推,下面不贅述 
573          "I" (offsetof(struct stack, abt[0])),
574          PLC (PSR_F_BIT | PSR_I_BIT | UND_MODE),
575          "I" (offsetof(struct stack, und[0])),
576          PLC (PSR_F_BIT | PSR_I_BIT | FIQ_MODE),
577          "I" (offsetof(struct stack, fiq[0])),
578          PLC (PSR_F_BIT | PSR_I_BIT | SVC_MODE)
579        : "r14");--------上面是input操做數列表,r14是要clobbered register列表 
580#endif
581}

嵌入式彙編的語法格式是:

asm(code

: output operand list

: input operand list

: clobber list);

你們對着上面的code就能夠分開各段內容了。在input operand list中,有兩種限制符(constraint),"r"或者"I","I"表示當即數(Immediate operands),"r"表示用通用寄存器傳遞參數。clobber list中有一個r14,表示在彙編代碼中修改了r14的值,這些信息是編譯器須要的內容。

 

對於SMP,bootstrap CPU會在系統初始化的時候執行cpu_init函數,進行本CPU的irq、abt和und三種模式的內核棧的設定,具體調用序列是:start_kernel--->setup_arch--->setup_processor--->cpu_init。

對於系統中其餘的CPU,bootstrap CPU會在系統初始化的最後,對每個online的CPU進行初始化,具體的調用序列是:start_kernel--->rest_init--->kernel_init--->kernel_init_freeable--->kernel_init_freeable--->smp_init--->cpu_up--->_cpu_up--->__cpu_up。__cpu_up函數是和CPU architecture相關的。

對於ARM,其調用序列是__cpu_up--->boot_secondary--->smp_ops.smp_boot_secondary(SOC相關代碼)--->secondary_startup--->__secondary_switched--->secondary_start_kernel--->cpu_init。

除了初始化,系統電源管理也須要irq、abt和und stack的設定。若是咱們設定的電源管理狀態在進入sleep的時候,CPU會丟失irq、abt和und stack point寄存器的值,那麼在CPU resume的過程當中,要調用cpu_init來從新設定這些值。

二、SVC模式的stack準備

咱們常常說進程的用戶空間和內核空間,對於一個應用程序而言,能夠運行在用戶空間,也能夠經過系統調用進入內核空間。在用戶空間,使用的是用戶棧,也就是咱們軟件工程師編寫用戶空間程序的時候,保存局部變量的stack。陷入內核後,固然不能用用戶棧了,這時候就須要使用到內核棧。所謂內核棧其實就是處於SVC mode時候使用的棧。

在linux最開始啓動的時候,系統只有一個進程(更準確的說是kernel thread),就是PID等於0的那個進程,叫作swapper進程(或者叫作idle進程)。該進程的內核棧是靜態定義的,以下:  

/sprdroid9.0_trunk/kernel4.4/init/init_task.c

21/*
22 * Initial thread structure. Alignment of this is handled by a special
23 * linker map entry.
24 */
25union thread_union init_thread_union __init_task_data = {
26#ifndef CONFIG_THREAD_INFO_IN_TASK
27    INIT_THREAD_INFO(init_task)
28#endif
29};

2633union thread_union {
2634#ifndef CONFIG_THREAD_INFO_IN_TASK
2635    struct thread_info thread_info;
2636#endif
2637    unsigned long stack[THREAD_SIZE/sizeof(long)];
2638};

對於ARM平臺,THREAD_SIZE是8192個byte,所以佔據兩個page frame。

隨着初始化的進行,Linux kernel會建立若干的內核線程,而在進入用戶空間後,user space的進程也會建立進程或者線程。

Linux kernel在建立進程(包括用戶進程和內核線程)的時候都會分配一個(或者兩個,和配置相關)page frame,具體代碼以下:

static struct task_struct *dup_task_struct(struct task_struct *orig) 
{ 
    ...... 

    ti = alloc_thread_info_node(tsk, node); 
    if (!ti) 
        goto free_tsk; 

    ...... 
}

底部是struct thread_info數據結構,頂部(高地址)就是該進程的內核棧。當進程切換的時候,整個硬件和軟件的上下文都會進行切換,這裏就包括了svc mode的sp寄存器的值被切換到調度算法選定的新的進程的內核棧上來。

 

三、異常向量表的準備

對於ARM處理器而言,當發生異常的時候,處理器會暫停當前指令的執行,保存現場,轉而去執行對應的異常向量處的指令,當處理完該異常的時候,
恢復現場,回到原來的那點去繼續執行程序。系統全部的異常向量(共計8個)組成了異常向量表。向量表(vector table)的代碼以下:
/sprdroid9.0_trunk/kernel4.4/arch/arm/kernel/entry-armv.S

208    .section .vectors, "ax", %progbits
1209__vectors_start:
1210    W(b)    vector_rst
1211    W(b)    vector_und
1212    W(ldr)    pc, __vectors_start + 0x1000
1213    W(b)    vector_pabt
1214    W(b)    vector_dabt
1215    W(b)    vector_addrexcptn
1216    W(b)    vector_irq---------------------------IRQ Vector
1217    W(b)    vector_fiq
1218

對於本文而言,咱們重點關注vector_irq這個exception vector。異常向量表可能被安放在兩個位置上:

(1)異常向量表位於0x0的地址。這種設置叫作Normal vectors或者Low vectors。

(2)異常向量表位於0xffff0000的地址。這種設置叫作high vectors

具體是low vectors仍是high vectors是由ARM的一個叫作的SCTLR寄存器的第13個bit (vector bit)控制的。對於啓用MMU的ARM Linux而言,系統使用了high vectors。爲何不用low vector呢?對於linux而言,0~3G的空間是用戶空間,若是使用low vector,那麼異常向量表在0地址,那麼則是用戶空間的位置,所以linux選用high vector。固然,使用Low vector也能夠,這樣Low vector所在的空間則屬於kernel space了(也就是說,3G~4G的空間加上Low vector所佔的空間屬於kernel space),不過這時候要注意一點,由於全部的進程共享kernel space,而用戶空間的程序常常會發生空指針訪問,這時候,內存保護機制應該能夠捕獲這種錯誤(大部分的MMU均可以作到,例如:禁止userspace訪問kernel space的地址空間),防止vector table被訪問到。對於內核中因爲程序錯誤致使的空指針訪問,內存保護機制也須要控制vector table被修改,所以vector table所在的空間被設置成read only的。在使用了MMU以後,具體異常向量表放在那個物理地址已經不重要了,重要的是把它映射到0xffff0000的虛擬地址就OK了,具體代碼以下:

/sprdroid9.0_trunk/kernel4.4/arch/arm/mm/mmu.c
static void __init devicemaps_init(const struct machine_desc *mdesc) 
{ 
    …… 
    vectors = early_alloc(PAGE_SIZE * 2); -----分配兩個page的物理頁幀

    early_trap_init(vectors); -------copy向量表以及相關help function到該區域

    …… 
    map.pfn = __phys_to_pfn(virt_to_phys(vectors)); 
    map.virtual = 0xffff0000; 
    map.length = PAGE_SIZE; 
#ifdef CONFIG_KUSER_HELPERS 
    map.type = MT_HIGH_VECTORS; 
#else 
    map.type = MT_LOW_VECTORS; 
#endif 
    create_mapping(&map); ----------映射0xffff0000的那個page frame

    if (!vectors_high()) {---若是SCTLR.V的值設定爲low vectors,那麼還要映射0地址開始的memory 
        map.virtual = 0; 
        map.length = PAGE_SIZE * 2; 
        map.type = MT_LOW_VECTORS; 
        create_mapping(&map); 
    }


    map.pfn += 1; 
    map.virtual = 0xffff0000 + PAGE_SIZE; 
    map.length = PAGE_SIZE; 
    map.type = MT_LOW_VECTORS; 
    create_mapping(&map); ----------映射high vecotr開始的第二個page frame

…… 
}

爲何要分配兩個page frame呢?這裏vectors table和kuser helper函數(內核空間提供的函數,可是用戶空間使用)佔用了一個page frame,另外異常處理的stub函數佔用了另一個page frame。爲何會有stub函數呢?稍後會講到。

在early_trap_init函數中會初始化異常向量表,具體代碼以下:

void __init early_trap_init(void *vectors_base) 
{ 
    unsigned long vectors = (unsigned long)vectors_base; 
    extern char __stubs_start[], __stubs_end[]; 
    extern char __vectors_start[], __vectors_end[]; 
    unsigned i;

    vectors_page = vectors_base;

    將整個vector table那個page frame填充成未定義的指令。起始vector table加上kuser helper函數並不能徹底的充滿這個page,有些縫隙。若是不這麼處理,當極端狀況下(程序錯誤或者HW的issue),CPU可能從這些縫隙中取指執行,從而致使不可知的後果。若是將這些縫隙填充未定義指令,那麼CPU能夠捕獲這種異常。 
    for (i = 0; i < PAGE_SIZE / sizeof(u32); i++) 
        ((u32 *)vectors_base)[i] = 0xe7fddef1;

  拷貝vector table,拷貝stub function 
    memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start); 
    memcpy((void *)vectors + 0x1000, __stubs_start, __stubs_end - __stubs_start);

    kuser_init(vectors_base); ----copy kuser helper function

    flush_icache_range(vectors, vectors + PAGE_SIZE * 2); 
    modify_domain(DOMAIN_USER, DOMAIN_CLIENT); 

}

一旦涉及代碼的拷貝,咱們就須要關心其編譯鏈接時地址(link-time address)和運行時地址(run-time address)。在kernel完成連接後,__vectors_start有了其link-time address,若是link-time address和run-time address一致,那麼這段代碼運行時毫無壓力。可是,目前對於vector table而言,其被copy到其餘的地址上(對於High vector,這是地址就是0xffff00000),也就是說,link-time address和run-time address不同了,若是仍然想要這些代碼能夠正確運行,那麼須要這些代碼是位置無關的代碼。對於vector table而言,必需要位置無關。B這個branch instruction自己就是位置無關的,它能夠跳轉到一個當前位置的offset。不過並不是全部的vector都是使用了branch instruction,對於軟中斷,其vector地址上指令是「W(ldr)    pc, __vectors_start + 0x1000 」,這條指令被編譯器編譯成ldr     pc, [pc, #4080],這種狀況下,該指令也是位置無關的,可是有個限制,offset必須在4K的範圍內,這也是爲什麼存在stub section的緣由了。

四、中斷控制器的初始化

3、ARM HW對中斷事件的處理

當一切準備好以後,一旦打開處理器的全局中斷就能夠處理來自外設的各類中斷事件了。

當外設(SOC內部或者外部均可以)檢測到了中斷事件,就會經過interrupt requestion line上的電平或者邊沿(上升沿或者降低沿或者both)通知到該外設鏈接到的那個中斷控制器,而中斷控制器就會在多個處理器中選擇一個,並把該中斷經過IRQ(或者FIQ,本文不討論FIQ的狀況)分發給該processor。ARM處理器感知到了中斷事件後,會進行下面一系列的動做:

一、修改CPSR(Current Program Status Register)寄存器中的M[4:0]。M[4:0]表示了ARM處理器當前處於的模式( processor modes)。ARM定義的mode包括:

處理器模式 縮寫 對應的M[4:0]編碼 Privilege level
User usr 10000 PL0
FIQ fiq 10001 PL1
IRQ irq 10010 PL1
Supervisor svc 10011 PL1
Monitor mon 10110 PL1
Abort abt 10111 PL1
Hyp hyp 11010 PL2
Undefined und 11011 PL1
System sys 11111 PL1

一旦設定了CPSR.M,ARM處理器就會將processor mode切換到IRQ mode。

二、保存發生中斷那一點的CPSR值(step 1以前的狀態)和PC值

ARM處理器支持9種processor mode,每種mode看到的ARM core register(R0~R15,共計16個)都是不一樣的。每種mode都是從一個包括全部的Banked ARM core register中選取。所有Banked ARM core register包括:

Usr System Hyp Supervisor abort undefined Monitor IRQ FIQ
R0_usr                
R1_usr                
R2_usr                
R3_usr                
R4_usr                
R5_usr                
R6_usr                
R7_usr                
R8_usr               R8_fiq
R9_usr               R9_fiq
R10_usr               R10_fiq
R11_usr               R11_fiq
R12_usr               R12_fiq
SP_usr   SP_hyp SP_svc SP_abt SP_und SP_mon SP_irq SP_fiq
LR_usr     LR_svc LR_abt LR_und LR_mon LR_irq LR_fiq
PC                
CPSR                
    SPSR_hyp SPSR_svc SPSR_abt SPSR_und SPSR_mon SPSR_irq SPSR_fiq
    ELR_hyp            

 

在IRQ mode下,CPU看到的R0~R12寄存器、PC以及CPSR是和usr mode(userspace)或者svc mode(kernel space)是同樣的。不一樣的是IRQ mode下,有本身的R13(SP,stack pointer)、R14(LR,link register)和SPSR(Saved Program Status Register)。

CPSR是共用的,雖然中斷可能發生在usr mode(用戶空間),也多是svc mode(內核空間),不過這些信息都是體如今CPSR寄存器中。硬件會將發生中斷那一刻的CPSR保存在SPSR寄存器中(因爲不一樣的mode下有不一樣的SPSR寄存器,所以更準確的說應該是SPSR-irq,也就是IRQ mode中的SPSR寄存器)。

PC也是共用的,因爲後續PC會被修改成irq exception vector,所以有必要保存PC值。固然,與其說保存PC值,不如說是保存返回執行的地址。對於IRQ而言,咱們指望返回地址是發生中斷那一點執行指令的下一條指令。具體的返回地址保存在lr寄存器中(注意:這個lr寄存器是IRQ mode的lr寄存器,能夠表示爲lr_irq):

(1)對於thumb state,lr_irq = PC

(2)對於ARM state,lr_irq = PC - 4

爲什麼要減去4?個人理解是這樣的(不必定對)。因爲ARM採用流水線結構,當CPU正在執行某一條指令的時候,其實取指的動做早就執行了,這時候PC值=正在執行的指令地址 + 8,以下所示:

----> 發生中斷的指令

               發生中斷的指令+4

-PC-->發生中斷的指令+8

               發生中斷的指令+12

一旦發生了中斷,當前正在執行的指令固然要執行完畢,可是已經完成取指、譯碼的指令則終止執行。當發生中斷的指令執行完畢以後,原來指向(發生中斷的指令+8)的PC會繼續增長4,所以發生中斷後,ARM core的硬件着手處理該中斷的時候,硬件現場以下圖所示:

 

----> 發生中斷的指令

               發生中斷的指令+4 <-------中斷返回的指令是這條指令

              發生中斷的指令+8

-PC-->發生中斷的指令+12

 

這時候的PC值實際上是比發生中斷時候的指令超前12。減去4以後,lr_irq中保存了(發生中斷的指令+8)的地址。爲何HW不幫忙直接減去8呢?這樣,後續軟件不就不用再減去4了。這裏咱們不能孤立的看待問題,實際上ARM的異常處理的硬件邏輯不只僅處理IRQ的exception,還要處理各類exception,很遺憾,不一樣的exception指望的返回地址不統一,所以,硬件只是幫忙減去4,剩下的交給軟件去調整。

三、mask IRQ exception。也就是設定CPSR.I = 1

四、設定PC值爲IRQ exception vector。基本上,ARM處理器的硬件就只能幫你幫到這裏了,一旦設定PC值,ARM處理器就會跳轉到IRQ的exception vector地址了,後續的動做都是軟件行爲了。

 

4、如何進入ARM中斷處理

一、IRQ mode中的處理

IRQ mode的處理都在vector_irq中,vector_stub是一個宏,定義以下:

.macro    vector_stub, name, mode, correction=0 
    .align    5

vector_\name: 
    .if \correction 
    sub    lr, lr, #\correction-------------(1) 
    .endif

    @ 
    @ Save r0, lr_ (parent PC) and spsr_ 
    @ (parent CPSR) 
    @ 
    stmia    sp, {r0, lr}        @ save r0, lr--------(2) 
    mrs    lr, spsr 
    str    lr, [sp, #8]        @ save spsr

    @ 
    @ Prepare for SVC32 mode.  IRQs remain disabled. 
    @ 
    mrs    r0, cpsr-----------------------(3) 
    eor    r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE) 
    msr    spsr_cxsf, r0

    @ 
    @ the branch table must immediately follow this code 
    @ 
    and    lr, lr, #0x0f---lr保存了發生IRQ時候的CPSR,經過and操做,能夠獲取CPSR.M[3:0]的值

                            這時候,若是中斷髮生在用戶空間,lr=0,若是是內核空間,lr=3 
THUMB( adr    r0, 1f            )----根據當前PC值,獲取lable 1的地址 
THUMB( ldr    lr, [r0, lr, lsl #2]  )-lr根據當前mode,要麼是__irq_usr的地址 ,要麼是__irq_svc的地址 
    mov    r0, sp------將irq mode的stack point經過r0傳遞給即將跳轉的函數 
ARM(    ldr    lr, [pc, lr, lsl #2]    )---根據mode,給lr賦值,__irq_usr或者__irq_svc 
    movs    pc, lr            @ branch to handler in SVC mode-----(4) 
ENDPROC(vector_\name)

    .align    2 
    @ handler addresses follow this label 
1: 
    .endm

(1)咱們指望在棧上保存發生中斷時候的硬件現場(HW context),這裏就包括ARM的core register。上一章咱們已經瞭解到,當發生IRQ中斷的時候,lr中保存了發生中斷的PC+4,若是減去4的話,獲得的就是發生中斷那一點的PC值。

(2)當前是IRQ mode,SP_irq在初始化的時候已經設定(12個字節)。在irq mode的stack上,依次保存了發生中斷那一點的r0值、PC值以及CPSR值(具體操做是經過spsr進行的,其實硬件已經幫咱們保存了CPSR到SPSR中了)。爲什麼要保存r0值?由於隨後的代碼要使用r0寄存器,所以咱們要把r0放到棧上,只有這樣才能完徹底全恢復硬件現場。

(3)可憐的IRQ mode稍縱即逝,這段代碼就是準備將ARM推送到SVC mode。如何準備?其實就是修改SPSR的值,SPSR不是CPSR,不會引發processor mode的切換(畢竟這一步只是準備而已)。

(4)不少異常處理的代碼返回的時候都是使用了stack相關的操做,這裏沒有。「movs    pc, lr 」指令除了字面上意思(把lr的值付給pc),還有一個隱含的操做(movs中‘s’的含義):把SPSR copy到CPSR,從而實現了模式的切換。

二、當發生中斷的時候,代碼運行在用戶空間

Interrupt dispatcher的代碼以下:

vector_stub    irq, IRQ_MODE, 4 -----減去4,確保返回發生中斷以後的那條指令

.long    __irq_usr            @  0  (USR_26 / USR_32)   <---------------------> base address + 0 
.long    __irq_invalid            @  1  (FIQ_26 / FIQ_32) 
.long    __irq_invalid            @  2  (IRQ_26 / IRQ_32) 
.long    __irq_svc            @  3  (SVC_26 / SVC_32)<---------------------> base address + 12 
.long    __irq_invalid            @  4 
.long    __irq_invalid            @  5 
.long    __irq_invalid            @  6 
.long    __irq_invalid            @  7 
.long    __irq_invalid            @  8 
.long    __irq_invalid            @  9 
.long    __irq_invalid            @  a 
.long    __irq_invalid            @  b 
.long    __irq_invalid            @  c 
.long    __irq_invalid            @  d 
.long    __irq_invalid            @  e 
.long    __irq_invalid            @  f

這其實就是一個lookup table,根據CPSR.M[3:0]的值進行跳轉(參考上一節的代碼:and    lr, lr, #0x0f)。所以,該lookup table共設定了16個入口,固然只有兩項有效,分別對應user mode和svc mode的跳轉地址。其餘入口的__irq_invalid也是很是關鍵的,這保證了在其模式下發生了中斷,系統能夠捕獲到這樣的錯誤,爲debug提供有用的信息。

相關文章
相關標籤/搜索