整理的Linux面經嵌入式相關知識點

自己在找工作的過程中,整理的LINUX系統嵌入式相關的知識點以及參考的其他一些相關博客文章

  • 大小端判斷程序

首先,ARM存儲器格式分爲大端格式和小端格式;
 - 大端格式:字數據的高字節存儲在低地址中,低字節存儲在高地址中
 - 小端格式:字數據的低字節存放在低地址中,高字節存放在高地址中
判斷程序:
 **一.共用體**

 union test
    {
        int a;
        char b;
    }
    int main(void)
    {
        union test t1;
        t1.a=1;
        if(t1.b==1)
            printf("小端格式");
        else
            printf("大端格式");
        return 0;
    }


**二.指針法**

int main(void)
    {
        int a=1;
        char *ptr=(char*)(&a);   //char型指針指向的char一個字節的地址空間
        if((*ptr) == 1)
            printf("小端格式");
        else
            printf("大端格式");
        return 0;
    }

 

  • int型指針和char型指針的區別:
  1.  - 本質上都是計算機裏面的一個地址
  2.  - 默認指向空間佔用的大小不同:int*指向空間需要4個字節,char*指針只需要一個指針
  3.  - 使用時的取值範圍不同:用*取值時int*得到得值是int類型的範圍,char*可以取到的值是char的範圍
  4.  - 賦值時範圍不同,用*p形式賦值時,如果是int*型的,會按照int來截取,如果是char*型的,按照char範圍截取
  5. 比如int a, *pa=&a; char b, *pb = &b;

執行*pa = 0x12345678後,*pa的值就是0x12345678。
執行*pb =0x12345678後,*pb的值就會被截取,值爲0x78。

  • IIC協議

物理層:
只要求兩條總線線路,一條串行數據線SDA,一條串行時鐘線SCL
每個連線到總線的器件都可以通過唯一的地址與其他器件通信,主從機角色和地址可配置
傳輸速率在標準模式下可以達到100Kb/s,快速模式下能達到400kb/s, 高速模式下能達到3.4Mb/s

  •  linux內核驅動加載過程,具體實現

Linux的驅動加載分兩種情況:靜態加載和動態加載
**靜態加載:**就是把驅動程序直接編譯進內核,然後內核在啓動過程中由do_initcall()函數加載
在make menuconfig命令進行內核配置裁剪時,在窗口中可以選擇是否編譯進內核,還是放入/lib/modules/下的內核版本目錄中,還是不選
**動態加載:**將動態驅動模塊加載到內核中,加載驅動命令:insmod,modprobe
insmod與modprobe不同之處:insmod絕對路徑/xx.ko, 而modprobe xx即可,不用加.o或.ko後綴,也不用加路徑,重要的一點是modprobe同時會加載當前模塊所依賴的其他模塊
模塊加載的時候(insmod, modprobe),**sys_init_module()**系統調用會調用**module_init**指定的函數,在module_init指定的函數過程:
首先進行**申請設備號**,其中包括動態分配和靜態申請兩種:

  1.  - **靜態申請:** register_chrdev_region()函數
  2.  - **動態分配:** alloc_chrdev_region()函數

然後就是進行設備的註冊,字符設備使用struct cdev來描述,字符設備的註冊可分爲3個步驟:

 

  1.  - **分配cdev:** 使用 struct cdev *cdev_alloc(void) 函數
  2.  - **初始化cdev:** 使用 void cdev_init(struct cdev *dev, const struct file_operations *fops) 函數
  3.  - **添加cdev:** 使用 int cdev_add(struct cdev *p, dev_t dev, unsigned count) 函數

加載完驅動模塊後,可以在 cat /proc/devices 下面查看設備號
接着可以創建設備節點: mknod devicename c 2031 0(mknod 設備名稱 設備類型 主設備號 次設備號)
創建設別節點後,用戶進程可以通過 /dev/devicename 這個路徑就可以訪問到全局變量設備虛擬設備
也可以自動創建設備節點:
class_create爲該設備創建一個class,再爲每個設備調用device_create創建對應的設備

  • 主設備號與次設備號的區別

 - **主設備號:**用來表示與設備文件相連的驅動程序,反映的是設備類型
 - **次設備號:**被驅動程序用來辨別操作哪個設備,用來區分同類型的設備

  • 字符設備與塊設備的區別

第一:字符設備是按字節來訪問設備,而塊設備只能以整塊數據爲單位來訪問設備
第二:字符設備是按照字節流的方式有序訪問,而塊設備可以隨機訪問固定大小的數據片

  •  自旋鎖底層實現機制

首先實現一個結構體用於自旋鎖的使用


    typedef struct spinlock{
        volatile unsigned int slock;
    }spinlock_t;

接口實現
(1) 初始化接口:
    
    ·#define spin_lock_init(lock) \
    do{ \
        ((spinlock_t*)lock)->slock = 0x0; \
        }while(0)

(2) 上鎖接口

    static inline void spin_lock(spinlock_t *lock)
    {
        raw_spin_lock(&lock->slock);
    }

(3) 釋放鎖

    static inline spin_unlock(spinlock_t *lock);
    {
        raw_spin_unlock(&lock->slock);
    }

更底層的彙編實現

    raw_spin_lock:     //完成自旋鎖的加鎖功能
    mov  r1,#1         @1-->r1

    DSB
    take_again:

            LDREX     r2,[r0]            @把r0的內容賦給r2,同時置全局標誌exclusive

            STREX     r3,r1,[r0]        @嘗試將r1寫入到鎖裏邊,首先檢查exclusive是否存在,如果存在則將r1-->r0,r3 = 0,並清除exclusive標誌,否則1--->r3,結束

            TEQ         r3,#0

            BNE       take_again

            TEQ        r2,#0
    
            BNE       take_again

            MOV       pc,lr                @返回



    raw_spin_unlock:

        DSB

        MOV r1,#0

        STR  r1,[r0,#0]                  @爲0,標示鎖已釋放

        DSB

        MOV  pc,lr

  • 有哪些線程同步機制


實現線程同步機制的方法:
 - **互斥鎖:**兩種狀態:鎖住狀態和不加鎖狀態
 - **讀寫鎖:**讀模式下加鎖狀態,寫模式下加鎖狀態,不加鎖狀態
 - **條件變量**條件變量是利用線程間共享的全局變量進行同步的一種機制,主要包括兩種動作:一個線程等待“條件變量的條件成立”而掛起;另一個線程使“條件成立”。爲了防止競爭,條件變量的使用總是和一個互斥鎖結合在一起
 - **臨界區:**臨界區的作用範圍僅限於本進程。其他進程無法獲取該鎖

  • spinlock特點,內部實現

**spin lock的特點**
 - spin lock是一種死等的鎖機制,當發生訪問資源衝突的時候,當前的執行thread會不斷的重新嘗試直到獲取進入臨界區
 - 只允許一個thread進入,一次只能有一個thread獲取並進入臨界區
 - 執行時間短,持有自旋鎖的時間一般不會超過兩次上下文切換的時間
 - 可以在中斷上下文執行

  • mutex和spinlock差別

如果代碼需要睡眠,這往往發生在和用戶空間同步時,使用信號量是唯一的選擇

**信號量:**是一種睡眠鎖,如果有一個任務想要獲得已經被佔用的信號量時,信號量會將這個進程放入一個等待隊列,然後讓其睡眠,當持有信號量的進程將信號釋放後,處於等待隊列中的任務將被喚醒,並讓其獲得信號量,信號量的初始值表示允許有幾個任務同時訪問該信號量保護的共享資源,初始值爲1就變成互斥鎖,如果釋放後的信號量的值爲非正數,表明有任務等待當前信號量,因此要喚醒等待該信號量的任務

**自旋鎖:**自旋鎖不會引起調用者睡眠,如果一個線程試圖獲得一個已經被持有的自旋鎖,線程就會一直進行忙循環

理想情況是所有的鎖都應該儘可能短的被持有,如果持有鎖的時間較長,使用信號量

即信號量適合保持時間較長的情況,而自旋鎖適合於保持時間非常短的情況而持有自旋鎖的時間一般不會超過兩次上下文切換的時間,一旦線程要進行切換,就至少花費切出切入兩次,自旋鎖的佔用時間如果遠遠長於兩次上下文切換,就應該選擇信號量

**自旋鎖與互斥鎖的區別在於不會導致睡眠,如果自旋鎖被其他執行單元持有了,那麼調用者就一直自旋在那循環的看持有者是否已經釋放,而不睡眠。在持有時間短的情況下使用比互斥鎖高效

/+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++/

 - 自旋鎖不會引起調用者睡眠,如果一個線程試圖獲得一個已經被持有的自旋鎖,線程就會一直進行忙循環;而信號量則引起調用者睡眠,如果有一個任務想要獲得已經被佔用的信號量時,信號量會將這個進程放入一個等待隊列,然後讓其睡眠,當持有信號量的進程將信號釋放後,處於等待隊列中的任務將被喚醒,並讓其獲得信號量
 - 自旋鎖適合於保持時間非常短的情況,可以在任何上下文使用,信號量適合於保持時間較長的情況,只能在進程
 - 上下文使用;如果被保護的共享資源只在上下文訪問,則可以以信號量來保護該進程資源,如果對共享資源的訪問時間非常短,選擇自旋鎖,但如果保護的共享資源需要在中斷上下文訪問,就必須使用自旋鎖

信號量根據計數值count的取值,可以將信號量分爲二值信號量和計數信號量

- 二值信號量強制二者同一時刻只有一個運行
- 計數信號量,其允許一個時刻有一個或者多個進程同時使用,取決於count值。

  • 互斥量和信號量的區別

 - 互斥量用於線程的互斥,信號量用於線程的同步,這是互斥量和信號量之間的根本區別
 - 互斥量值只能爲0/1,信號量值可以爲非負整數,也就是說,一個互斥量只能用於一個資源的互斥訪問,它不能實現多個資源的多線程互斥問題,信號量可以實現多個同類資源的多線程互斥和同步。當信號量爲單值信號量時,也可以完成一個資源的互斥訪問。
 - 互斥量的加鎖和解鎖必須由同一線程分別對應使用,信號量可以由一個線程釋放,另一個線程得到

  • 區分互斥和同步

**互斥:**是指某一個資源同時只允許一個訪問者對其進行訪問,具有唯一性和排他性,但互斥無法限制訪問者對資源的訪問順序,即訪問是無序的
**同步:**是指在互斥的基礎上,通過其他機制實現訪問者對資源的有序訪問,在大多數情況下,同步已經實現了互斥,特別是所有寫入自願的情況是互斥的,少數情況是指可以允許多個訪問者同時訪問資源

  •  數組指針和指針數組的區別

**指針數組:**array of pointers,即用於存儲指針的數組,也就是數組元素都是指針
**數組指針:**a pointer to an array, 即指向數組的指針

其注意用法上的區別:

int \*a[4]      指針數組,表示:數組a中的元素都爲int型指針  元素表示: \*a[i]---(其a[i]爲地址值)
int (\*a)[4]    數組指針,表示:指向數組a的指針,     元素表示:(\*a)[i]

    

#include <iostream>
    
    using namespace std;
    
    int main()
    {
    
        int c[4]={1,2,3,4};
        int *a[4]; //指針數組
        int (*b)[4]; //數組指針
        
        b=&c;
        
        //將數組c中元素賦給數組a
        for(int i=0;i<4;i++)
        {
            a[i]=&c[i];
        }
        
        //輸出看下結果
        cout<<*a[1]<<endl; //輸出2就對
        cout<<(*b)[2]<<endl; //輸出3就對
        
        return 0;
    }

 

  •  堆棧的區別

 - **棧:**由編譯器自動釋放內存,存放函數的參數值,局部變量,函數返回地址等
 - **堆:**一般由程序員分配釋放內存,屬於動態內存分配區域,malloc,realloc, calloc,並指明其大小

  • 函數調用參數傳遞時在棧中的實現

棧是從上到下生長(地址減小)的,壓棧的操作使棧頂地址減小,彈出的操作使棧頂地址增大

相關寄存器:
 - esp: 堆棧(Stack)指針寄存器,指向堆棧頂部
 - ebp:基址指針寄存器,指向當前堆棧底部
 - eip:指令寄存器,指向下一條指令的地址
 - eax:累加寄存器,常用於函數返回值
 - edx:數據(DATA)寄存器

Program Stack

-------函數參數3---------|ebp+16

-------函數參數2---------|ebp+12

-------函數參數1---------|ebp+8

-------函數返回地址-------|ebp+4

--------------------

ebp-----上一個函數的ebp----|ebp

--------局部變量1----------|ebp-4

--------局部變量2----------|ebp-8

esp-----局部變量3----------|ebp-12

對於函數返回值:

小於4個字節的返回值由eax寄存器返回,eax寄存器本身只有4個字節,5到8個字節的返回值由eax和edx寄存器聯合返回,eax寄存器返回低四個字節,edx返回高於4個字節的部分
對於大於8個字節的返回值:C語言會在函數返回時使用一個臨時的棧上內存區域作爲中轉,結果返回值對象會被拷貝兩次

 - 首先main函數在棧上開闢一片空間,將這塊空間的一部分作爲傳遞返回值的臨時對象,這裏稱爲temp
 - 將temp對象的地址作爲隱藏參數傳遞給return_test函數
 - return_test函數將數據拷貝給temp對象,並將temp對象的地址用eax傳出
 - return_test返回之後,main函數將eax指向的temp對象的內容拷貝給n

  •  ARM的工作模式
  1.  - **用戶模式:**ARM處理器正常的程序執行狀態
  2.  - **快速中斷模式(FIQ):**處理高速中斷,用於高速數據傳輸或通道處理
  3.  - **外部中斷模式(IRQ):**用於普通的中斷處理
  4.  - **管理模式:**操作系統使用的保護模式,系統復位後的默認模式
  5.  - **中止模式:**數據或指令預取中止時進入該模式
  6.  - **未定義模式:**處理未定義指令,用於支持硬件協處理器的軟件仿真
  7.  - **系統模式:**運行特權級的操作系統任務
  •  快速中斷(FIQ)和外部中斷(IRQ)的區別

快速中斷模式用於高速數據傳輸或通道處理,外部中斷模式用於普通的中斷處理

 - **執行速度FIQ比IRQ快**
 
ARM的FIQ模式提供了更多的banked寄存器,R8到R14還有SPSR,而IRQ模式就沒有那麼多,R8,R9,R10,R11,R12對應的banked寄存器就沒有,這就意味着在ARM的IRQ模式下,中斷處理程序自己要保存R8到R12這幾個寄存器,然後退出中斷處理程序要恢復這幾個寄存器,而FIQ模式由於這幾個寄存器都有banked寄存器,模式切換時CPU自動保存這些值到banked寄存器,退出FIQ模式自動恢復,所以這個過程FIQ和IRQ快

 - **FIQ比IRQ有更高的優先級,即IRQ可以被FIQ所中斷,但FIQ不能被IRQ所中斷,在處理FIQ時必須關閉中斷**

FIQ的中斷向量地址在0x0000001C,而IRQ的在0x00000018.(也有的在0xFFFF001C以及0xFFFF0018),18只能放一條指令,爲了不與1C處的FIQ衝突,這個地方只能跳轉,而FIQ不一樣,1C後就沒有任何中斷向量表了,這樣可以直接在1C處放FIQ的中斷處理程序,由於跳轉的範圍限制,至少少了一條跳轉指令

 - **IRQ和FIQ的響應延遲有區別**

IRQ的響應並不及時,從verilog仿真來看,IRQ會延遲幾個指令週期才跳轉到中斷向量處,看起來像是在等預取的指令執行完。

## 進程死鎖以及死鎖的必要條件和解決方法

**死鎖:**進程A佔有資源R1,等待進程B佔有的資源R2;進程B佔有資源R2,等待進程A佔有的資源R1,而且資源R1,R2只允許一個進程佔有,即:不允許兩個進程同時佔用,結果兩個進程都不能繼續執行。

**產生死鎖的必要條件**

 - **互斥條件**即某個資源在一段時間內只能由一個進程佔有,不能同時被兩個或兩個以上的進程佔有
 - **不可搶佔條件**進程所獲得的資源在未使用完畢之前,資源申請者不能強行從資源佔有者手中奪取資源,只能由該資源的佔有者進程自行釋放
 - **佔有且申請條件**進程至少已經佔有一個資源,但又申請新的資源;由於該資源已被另外進程佔有,此時該進程阻塞;但它在等待新資源之時,仍繼續佔用已佔有的資源
 - **循環等待條件**形成一個進程循環等待環

  • 驅動裏面爲什麼要有併發,互斥的控制?如何實現?

併發(concurrency)指的是多個執行單元同時,並行被執行,而併發的執行單元對共享資源(硬件資源和軟件上的全局變量,靜態變量等)的訪問則很容易導致競態(race conditions)。
解決競態問題的途徑是保證對共享資源的互斥訪問,所謂互斥訪問就是指一個執行單元在訪問共享資源的時候,其他的執行單元都被禁止訪問。
訪問共享資源的代碼區域被稱爲臨界區,臨界區需要以某種互斥機制加以保護,中斷屏蔽,原子操作,自旋鎖和信號量都是linux設備驅動中採用的互斥途徑

  •  malloc(), vmalloc()和kmalloc()的區別

 - kmalloc和vmalloc是分配內核的內存,malloc分配的是用戶的內存
 - kmalloc保證分配的內存在物理上是連續的,vmalloc保證的是在虛擬地址空間上的連續,物理地址上不連續,malloc不保證任何東西
 - kmalloc分配的內存大小有限,是**32字節到128KB**,vmalloc和malloc能分配的大小相對較大
 - 內存只有在要被DMA訪問的時候才需要物理上連續,此時用kmalloc
 - vmalloc比kmalloc要慢

因爲vmalloc函數會建立新的頁表,將不連續的物理內存映射成連續的虛擬內存,所以開銷比較大
-----------------------------------------
**kmalloc和vmalloc**
這兩個區別大體概括爲:

  1.  - vmalloc分配的一般爲高端內存,只有當內存不夠的時候才分配低端內存,kmalloc從低端分配內存
  2.  - vmalloc分配的物理地址一般不連續,kmalloc分配的物理地址連續,兩者分配的虛擬地址都是連續的
  3.  - vmalloc分配的一般爲大塊內存,kmalloc一般分配的是小塊內存

kmalloc分配的內存處於3GB~high_memory之間,vmalloc分配的內存在VMALLOC_START~4GB之間,非連續的內存區
kmalloc分配內存基於slab分配器,Linux中爲一些反覆分配和釋放的結構體預留了一些內存空間,使用內存池來管理,這種技術就是slab(後備高速緩存)

  • Linux系統中硬鏈接和軟鏈接的區別

 - 軟鏈接相當於WINDOWS中的快捷方式,如果打開並修改軟鏈接,相應的文件也會隨之改變,但是如果刪除軟鏈接,源文件並不會受到影響
 - 硬鏈接有點像引用和指針的結合,當打開並修改它時,相應的文件隨之改變,但是所有這個文件的硬件內容也隨之改變,這是因爲所有的硬鏈接都擁有唯一的一個inode號,它們指向的是同一文件。
 - 軟鏈接可以跨文件系統創建,也就是可以在某一個分區中創建到另外一個分區的軟鏈接
 - 硬鏈接則只能在本文件系統中創建使用,因爲inode是這個文件在當前分區中的索引值,是相對於這個分區的,不能跨越文件系統
 - 軟鏈接可以連接任何文件或者文件夾,而硬鏈接則只能在文件之間創建,並且不能對目錄進行創建

  • Linux設備驅動管理的中斷

Linux中斷分爲兩個半部:上半部(tophalf)和下半部(bottom half),上半部的功能是“登記中斷”,當一箇中斷髮生時,它進行相應的硬件讀寫後就把中斷例程的下半部掛到該設備的下半部執行隊列中去,上半部執行的速度會很快,上半部與下半部最大的不同就是上半部是不可中斷的,下半部是可中斷的,下半部幾乎做了中斷處理程序的所有事情。

下半部的實現機制主要有tasklet和工作隊列

  •  靜態鏈接庫和動態鏈接庫的區別

------------------------1靜態鏈接庫的優點--------------------------
 (1)代碼裝載速度快,執行速度略比動態鏈接庫快; 
 (2)只需保證在開發者的計算機中有正確的.LIB文件,在以二進制形式發佈程序時不需考慮在用戶的計算機上.LIB文件是否存在及版本問題,可避免DLL地獄等問題。 
--------------------------2動態鏈接庫的優點------------------ 
 (1)更加節省內存並減少頁面交換;
 (2) DLL文件與EXE文件獨立,只要輸出接口不變(即名稱、參數、返回值類型和調用約定不變),更換DLL文件不會對    EXE文件造成任何影響,因而極大地提高了可維護性和可擴展性;
 (3)不同編程語言編寫的程序只要按照函數調用約定就可以調用同一個DLL函數;
 (4)適用於大規模的軟件開發,使開發過程獨立、耦合度小,便於不同開發者和開發組織之間進行開發和測試。
--------------------------3不足之處--------------------------
 (1)使用靜態鏈接生成的可執行文件體積較大,包含相同的公共代碼,造成浪費;
 (2)使用動態鏈接庫的應用程序不是自完備的,它依賴的DLL模塊也要存在,如果使用載入時動態鏈接,程序啓動時發現DLL不存在,系統將終止程序並給出錯誤信息。而使用運行時動態鏈接,系統不會終止,但由於DLL中的導出函數不可用,程序會加載失敗;速度比靜態鏈接慢。當某個模塊更新後,如果新模塊與舊的模塊不兼容,那麼那些需要該模塊才能運行的軟件,統統撕掉。這在早期Windows中很常見。

  • 驅動模塊與模塊之間的通信(函數調用)

如模塊1調用模塊2的功能函數過程:

首先加載模塊2:

 - insmod 模塊2.ko
 - 內核爲模塊2分配空間,然後將模塊的代碼和數據裝入分配內存中
 - 內核發現符號表中有函數1,函數2可以導出,於是將其內存地址記錄在**內核符號表**中

導出:EXPORT_SYMBOL(add_intergar);
導出後可以在 /proc/kallsyms下查看
    
    root# cat /proc/kallsyms | grep add_integar

    __ksymtab_add_integar 表明內核符號add_integar已導出

輸出信息可以在 /var/log/syslog裏面查看

    root# tail -n 10 /var/log/syslog

加載模塊1:

 - insmod命令給模塊分配空間,然後將模塊的代碼和數據裝在內存中。
 - 內核在模塊1的符號表中發現一些未解析的函數,於是模塊1會通過內核符號表,查找相應的函數,並將函數地址填到模塊1的符號表中

  • 函數可重入

一個函數被重入,表示這個函數沒有執行完成,由於外部因素或內部調用,又一次進入該函數執行,一個函數要被重入,有兩種情況:

 - 多個線程同時執行這個函數
 - 函數自身調用自身

一個函數可重入,具有以下幾個特點:

  1.  - 不適用靜態或者全局的非const變量
  2.  - 不返回任何靜態或者全局的非const變量的指針
  3.  - 僅依賴於調用方提供的參數
  4.  - 不依賴任何單個資源的鎖
  5.  - 不調用任何不可重入的函數

 

  •  Linux系統中的系統調用

從用戶態切換到內核態的兩種方式:系統調用和中斷方式
實現系統調用一般是彙編指令:

    int $0x80

128號中斷引發軟中斷進入內核空間
在實際執行中斷向量表中的第0x80號元素所對應的函數之前,CPU首先還要進行棧的切換,用戶態和內核態使用的是不同的棧,兩者各自負責各自的函數調用,在應用程序調用0x80號中斷時,程序的執行流程是從用戶態切換到內核態,從中斷處理函數返回時,程序的當前棧還要從內核棧切換回用戶態
所謂當前棧,指的是ESP的值所在的棧空間,此外,寄存器SS的值還應該指向當前棧所在的頁
當前棧切換到內核棧:
 - 保存當前ESP, SS的值
 - 將ESP,SS的值設置爲內核棧的相應值

將當前棧由內核棧切換爲用戶棧:

 - 恢復原來ESP, SS的值
 - 用戶態的ESP和SS的值保存在內核棧上

先保存系統調用號的值,再SWI軟中斷進入內核系統,即Entry_common.S中的ENTRY<VECTORY_SWI>中取出系統調用號,再根據系統調用號去查找系統調用表(calls.S文件中的sys_call_table)中找到相應的系統調用的內核處理函數

參考的一些博客文章:

linux驅動面試題2018(面試題整理,含答案)

linux驅動工程面試必問知識點

Linux 驅動面試題總結

你知道底層自旋鎖是如何實現的嗎

Linux中的spinlock和mutex區別

gdb調試(線程和正在運行中的程序)

gdb調試當前運行的程序

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