當前位置:首頁 » 操作系統 » linux驅動中斷

linux驅動中斷

發布時間: 2023-06-15 20:16:32

❶ 請教:linux 字元設備驅動IIC進不了中斷

如何編寫Linux設備驅動程序回想學習Linux操作系統已經有近一年的時間了,前前後後,零零碎碎的一路學習過來,也該試著寫的東西了。也算是給自己能留下一點記憶和回憶吧!由於完全是自學的,以下內容若有不當之處,還請大家多指教。Linux是Unix操作系統的一種變種,在Linux下編寫驅動程序的原理和思想完全類似於其他的Unix系統,但它dos或window環境下的驅動程序有很大的區別。在Linux環境下設計驅動程序,思想簡潔,操作方便,功能也很強大,但是支持函數少,只能依賴kernel中的函數,有些常用的操作要自己來編寫,而且調試也不方便。以下的一些文字主要來源於khg,johnsonm的Writelinuxdevicedriver,Brennan'sGuidetoInlineAssembly,TheLinuxA-Z,還有清華BBS上的有關devicedriver的一些資料。一、Linuxdevicedriver的概念系統調用是操作系統內核和應用程序之間的介面,設備驅動程序是操作系統內核和機器硬體之間的介面。設備驅動程序為應用程序屏蔽了硬體的細節,這樣在應用程序看來,硬體設備只是一個設備文件,應用程序可以象操作普通文件一樣對硬體設備進行操作。設備驅動程序是內核的一部分,它完成以下的功能:1、對設備初始化和釋放。2、把數據從內核傳送到硬體和從硬體讀取數據。3、讀取應用程序傳送給設備文件的數據和回送應用程序請求的數據。4、檢測和處理設備出現的錯誤。在Linux操作系統下有三類主要的設備文件類型,一是字元設備,二是塊設備,三是網路設備。字元設備和塊設備的主要區別是:在對字元設備發出讀/寫請求時,實際的硬體I/O一般就緊接著發生了,塊設備則不然,它利用一塊系統內存作緩沖區,當用戶進程對設備請求能滿足用戶的要求,就返回請求的數據,如果不能,就調用請求函數來進行實際的I/O操作。塊設備是主要針對磁碟等慢速設備設計的,以免耗費過多的CPU時間來等待。已經提到,用戶進程是通過設備文件來與實際的硬體打交道。每個設備文件都都有其文件屬性(c/b),表示是字元設備還是塊設備?另外每個文件都有兩個設備號,第一個是主設備號,標識驅動程序,第二個是從設備號,標識使用同一個設備驅動程序的不同的硬體設備,比如有兩個軟盤,就可以用從設備號來區分他們。設備文件的的主設備號必須與設備驅動程序在登記時申請的主設備號一致,否則用戶進程將無法訪問到驅動程序。最後必須提到的是,在用戶進程調用驅動程序時,系統進入核心態,這時不再是搶先式調度。也就是說,系統必須在你的驅動程序的子函數返回後才能進行其他的工作。如果你的驅動程序陷入死循環,不幸的是你只有重新啟動機器了,然後就是漫長的fsck。讀/寫時,它首先察看緩沖區的內容,如果緩沖區的數據未被處理,則先處理其中的內容。如何編寫Linux操作系統下的設備驅動程序二、實例剖析我們來寫一個最簡單的字元設備驅動程序。雖然它什麼也不做,但是通過它可以了解Linux的設備驅動程序的工作原理。把下面的C代碼輸入機器,你就會獲得一個真正的設備驅動程序。#define__NO_VERSION__#include#includecharkernel_version[]=UTS_RELEASE;這一段定義了一些版本信息,雖然用處不是很大,但也必不可少。Johnsonm說所有的驅動程序的開頭都要包含,一般來講最好使用。由於用戶進程是通過設備文件同硬體打交道,對設備文件的操作方式不外乎就是一些系統調用,如open,read,write,close…,注意,不是fopen,fread,但是如何把系統調用和驅動程序關聯起來呢?這需要了解一個非常關鍵的數據結構:structfile_operations{int(*seek)(structinode*,structfile*,off_t,int);int(*read)(structinode*,structfile*,char,int);int(*write)(structinode*,structfile*,off_t,int);int(*readdir)(structinode*,structfile*,structdirent*,int);int(*select)(structinode*,structfile*,int,select_table*);int(*ioctl)(structinode*,structfile*,unsinedint,unsignedlong);int(*mmap)(structinode*,structfile*,structvm_area_struct*);int(*open)(structinode*,structfile*);int(*release)(structinode*,structfile*);int(*fsync)(structinode*,structfile*);int(*fasync)(structinode*,structfile*,int);int(*check_media_change)(structinode*,structfile*);int(*revalidate)(dev_tdev);}這個結構的每一個成員的名字都對應著一個系統調用。用戶進程利用系統調用在對設備文件進行諸如read/write操作時,系統調用通過設備文件的主設備號找到相應的設備驅動程序,然後讀取這個數據結構相應的函數指針,接著把控制權交給該函數。這是linux的設備驅動程序工作的基本原理。既然是這樣,則編寫設備驅動程序的主要工作就是編寫子函數,並填充file_operations的各個域。下面就開始寫子程序。#include#include#include#include#include#includeunsignedinttest_major=0;staticintread_test(structinode*node,structfile*file,char*buf,intcount){intleft;if(verify_area(VERIFY_WRITE,buf,count)==-EFAULT)return-EFAULT;for(left=count;left>0;left--){__put_user(1,buf,1);buf++;}returncount;}這個函數是為read調用准備的。當調用read時,read_test()被調用,它把用戶的緩沖區全部寫1。buf是read調用的一個參數。它是用戶進程空間的一個地址。但是在read_test被調用時,系統進入核心態。所以不能使用buf這個地址,必須用__put_user(),這是kernel提供的一個函數,用於向用戶傳送數據。另外還有很多類似功能的函數。請參考Robert著的《Linux內核設計與實現》(第二版)。然而,在向用戶空間拷貝數據之前,必須驗證buf是否可用。這就用到函數verify_area。staticintwrite_tibet(structinode*inode,structfile*file,constchar*buf,intcount){returncount;}staticintopen_tibet(structinode*inode,structfile*file){MOD_INC_USE_COUNT;return0;}staticvoidrelease_tibet(structinode*inode,structfile*file){MOD_DEC_USE_COUNT;}這幾個函數都是空操作。實際調用發生時什麼也不做,他們僅僅為下面的結構提供函數指針。structfile_operationstest_fops={NULL,read_test,write_test,NULL,/*test_readdir*/NULL,NULL,/*test_ioctl*/NULL,/*test_mmap*/open_test,release_test,NULL,/*test_fsync*/NULL,/*test_fasync*//*nothingmore,fillwithNULLs*/};這樣,設備驅動程序的主體可以說是寫好了。現在要把驅動程序嵌入內核。驅動程序可以按照兩種方式編譯。一種是編譯進kernel,另一種是編譯成模塊(moles),如果編譯進內核的話,會增加內核的大小,還要改動內核的源文件,而且不能動態的卸載,不利於調試,所以推薦使用模塊方式。intinit_mole(void){intresult;result=register_chrdev(0,"test",&test_fops);if(result#include#include#includemain(){inttestdev;inti;charbuf[10];testdev=open("/dev/test",O_RDWR);if(testdev==-1){printf("Cann'topenfile\n");exit(0);}read(testdev,buf,10);for(i=0;i<10;i++)printf("%d\n",buf[i]);close(testdev);}編譯運行,看看是不是列印出全1?以上只是一個簡單的演示。真正實用的驅動程序要復雜的多,要處理如中斷,DMA,I/Oport等問題。這些才是真正的難點。請看下節,實際情況的處理。如何編寫Linux操作系統下的設備驅動程序三、設備驅動程序中的一些具體問題1。I/OPort。和硬體打交道離不開I/OPort,老的ISA設備經常是佔用實際的I/O埠,在linux下,操作系統沒有對I/O口屏蔽,也就是說,任何驅動程序都可對任意的I/O口操作,這樣就很容易引起混亂。每個驅動程序應該自己避免誤用埠。有兩個重要的kernel函數可以保證驅動程序做到這一點。1)check_region(intio_port,intoff_set)這個函數察看系統的I/O表,看是否有別的驅動程序佔用某一段I/O口。參數1:I/O埠的基地址,參數2:I/O埠佔用的范圍。返回值:0沒有佔用,非0,已經被佔用。2)request_region(intio_port,intoff_set,char*devname)如果這段I/O埠沒有被佔用,在我們的驅動程序中就可以使用它。在使用之前,必須向系統登記,以防止被其他程序佔用。登記後,在/proc/ioports文件中可以看到你登記的I/O口。參數1:io埠的基地址。參數2:io埠佔用的范圍。參數3:使用這段io地址的設備名。在對I/O口登記後,就可以放心地用inb(),outb()之類的函來訪問了。在一些pci設備中,I/O埠被映射到一段內存中去,要訪問這些埠就相當於訪問一段內存。經常性的,我們要獲得一塊內存的物理地址。2。內存操作在設備驅動程序中動態開辟內存,不是用malloc,而是kmalloc,或者用get_free_pages直接申請頁。釋放內存用的是kfree,或free_pages。請注意,kmalloc等函數返回的是物理地址!注意,kmalloc最大隻能開辟128k-16,16個位元組是被頁描述符結構佔用了。內存映射的I/O口,寄存器或者是硬體設備的RAM(如顯存)一般佔用F0000000以上的地址空間。在驅動程序中不能直接訪問,要通過kernel函數vremap獲得重新映射以後的地址。另外,很多硬體需要一塊比較大的連續內存用作DMA傳送。這塊程序需要一直駐留在內存,不能被交換到文件中去。但是kmalloc最多隻能開辟128k的內存。這可以通過犧牲一些系統內存的方法來解決。3。中斷處理同處理I/O埠一樣,要使用一個中斷,必須先向系統登記。intrequest_irq(unsignedintirq,void(*handle)(int,void*,structpt_regs*),unsignedintlongflags,constchar*device);irq:是要申請的中斷。handle:中斷處理函數指針。flags:SA_INTERRUPT請求一個快速中斷,0正常中斷。device:設備名。如果登記成功,返回0,這時在/proc/interrupts文件中可以看你請求的中斷。4。一些常見的問題。對硬體操作,有時時序很重要(關於時序的具體問題就要參考具體的設備晶元手冊啦!比如網卡晶元RTL8139)。但是如果用C語言寫一些低級的硬體操作的話,gcc往往會對你的程序進行優化,這樣時序會發生錯誤。如果用匯編寫呢,gcc同樣會對匯編代碼進行優化,除非用volatile關鍵字修飾。最保險的法是禁止優化。這當然只能對一部分你自己編寫的代碼。如果對所有的代碼都不優化,你會發現驅動程序根本無法裝載。這是因為在編譯驅動程序時要用到gcc的一些擴展特性,而這些擴展特性必須在加了優化選項之後才能體現出來。寫在後面:學習Linux確實不是一件容易的事情,因為要付出很多精力,也必須具備很好的C語言基礎;但是,學習Linux也是一件非常有趣的事情,它裡麵包含了許多高手的智慧和「幽默」,這些都需要自己親自動手才能體會到,O(∩_∩)O~哈哈!

❷ 如何關閉linux smp中斷

在多 CPU 的環境中,還有一個中斷平衡的問題,比如,網卡中斷會教給哪個 CPU 處理,這個參數控制哪些 CPU 可以綁定 IRQ 中斷。其中的 {number} 是對應設備的中斷編號,可以用下面的命令找出:

cat /proc/interrupt
比如,一般 eth0 的 IRQ 編號是 16,所以控制 eth0 中斷綁定的 /proc 文件名是 /proc/irq/16/smp_affinity。上面這個命令還可以看到某些中斷對應的CPU處理的次數,預設的時候肯定是不平衡的。

設置其值的方法很簡單,smp_affinity 自身是一個位掩碼(bitmask),特定的位對應特定的 CPU,這樣,01 就意味著只有第一個 CPU 可以處理對應的中斷,而 0f(0x1111)意味著四個 CPU 都會參與中斷處理。

幾乎所有外設都有這個參數設置,可以關注一下。

這個數值的推薦設置,其實在很大程度上,讓專門的CPU處理專門的中斷是效率最高的,比如,給磁碟IO一個CPU,給網卡一個CPU,這樣是比較合理的。

現在的伺服器一般都是多核了,但是中斷很多時候都是只用一個核,如果有些中斷要求比較高,可以把它獨立分配給一個cpu使用。

❸ 《Linux設備驅動程序》(十六)-中斷處理

設備與處理器之間的工作通常來說是非同步,設備數據要傳遞給處理器通常來說有以下幾種方法:輪詢、等待和中斷。

讓CPU進行輪詢等待總是不能讓人滿意,所以通常都採用中斷的形式,讓設備來通知CPU讀取數據。

2.6內核的函數參數與現在的參數有所區別,這里都主要介紹概念,具體實現方法需要結合具體的內核版本。

request_irq函數申請中斷,返回0表示申請成功,其他返回值表示申請失敗,其具體參數解釋如下:

flags 掩碼可以使用以下幾個:

快速和慢速處理常式 :現代內核中基本沒有這兩個概念了,使用SA_INTERRUPT位後,當中斷被執行時,當前處理器的其他中斷都將被禁止。通常不要使用SA_INTERRUPT標志位,除非自己明確知道會發生什麼。

共享中斷 :使用共享中斷時,一方面要使用SA_SHIRQ位,另一個是request_irq中的dev_id必須是唯一的,不能為NULL。這個限制的原因是:內核為每個中斷維護了一個共享處理常式的列表,常式中的dev_id各不相同,就像設備簽名。如果dev_id相同,在卸載的時候引起混淆(卸載了另一個中斷),當中斷到達時會產生內核OOP消息。

共享中斷需要滿足以下一個條件才能申請成功:

當不需要使用該中斷時,需要使用free_irq釋放中斷。

通常我們會在模塊載入的時候申請安裝中斷處理常式,但書中建議:在設備第一次打開的時候安裝,在設備最後一次關閉的時候卸載。

如果要查看中斷觸發的次數,可以查看 /proc/interrupts 和 /proc/stat。

書中講述了如何自動檢測中斷號,在嵌入式開發中通常都是查看原理圖和datasheet來直接確定。

自動檢測的原理如下:驅動程序通知設備產生中斷,然後查看哪些中斷信號線被觸發了。Linux提供了以下方法來進行探測:

探測工作耗時較長,建議在模塊載入的時候做。

中斷處理函數和普通函數其實差不多,唯一的區別是其運行的中斷上下文中,在這個上下文中有以下注意事項:

中斷處理函數典型用法如下:

中斷處理函數的參數和返回值含義如下:

返回值主要有兩個:IRQ_NONE和IRQ_HANDLED。

對於中斷我們是可以進行開啟和關閉的,Linux中提供了以下函數操作單個中斷的開關:

該方法可以在所有處理器上禁止或啟用中斷。

需要注意的是:

如果要關閉當前處理器上所有的中斷,則可以調用以下方法:

local_irq_save 會將中斷狀態保持到flags中,然後禁用處理器上的中斷;如果明確知道中斷沒有在其他地方被禁用,則可以使用local_irq_disable,否則請使用local_irq_save。

locat_irq_restore 會根據上面獲取到flags來恢復中斷;local_irq_enable 會無條件打開所有中斷。

在中斷中需要做一些工作,如果工作內容太多,必然導致中斷處理所需的時間過長;而中斷處理又要求能夠盡快完成,這樣才不會影響正常的系統調度,這兩個之間就產生了矛盾。

現在很多操作系統將中斷分為兩個部分來處理上面的矛盾:頂半部和底半部。

頂半部就是我們用request_irq來注冊的中斷處理函數,這個函數要求能夠盡快結束,同時在其中調度底半部,讓底半部在之後來進行後續的耗時工作。

頂半部就不再說明了,就是上面的中斷處理函數,只是要求能夠盡快處理完成並返回,不要處理耗時工作。

底半部通常使用tasklet或者工作隊列來實現。

tasklet的特點和注意事項:

工作隊列的特點和注意事項:

❹ 初學linux觸摸屏驅動,請求IRQ_ADC和IRQ_TC中斷總是返回EBUSY,請問怎麼解決啊

是該中斷線被佔用了,可能是其他設備佔用的,把那個地方找到,把中斷線釋放掉就行了

❺ 怎樣看linux串口驅動中斷 dma

查詢就是一直在查看標志位,是不是被置1了,如果是就去讀或者其他操作
中斷就是平時不用管,一單有東西來就會進入中斷服務程序,你再去操作
DMA是你初始化的時候把串口地址和需要傳輸的地址寫上,來東西他就自己把數據存到你初始化的地址上

❻ linux驅動中斷,程序運行幾個小時後系統崩潰

中斷與定時器:
中斷的概念:指CPU在執行過程中,出現某些突發事件急待處理,CPU暫停執行當前程序,轉去處理突發事件
,處理完後CPU又返回原程序被中斷的位置繼續執行
中斷的分類:內部中斷和外部中斷
內部中斷:中斷源來自CPU內部(軟體中斷指令、溢出、觸發錯誤等)
外部中斷:中斷源來自CPU外部,由外設提出請求

屏蔽中斷和不可屏蔽中斷:
可屏蔽中斷:可以通過屏蔽字被屏蔽,屏蔽後,該中斷不再得到響應
不可平布中斷:不能被屏蔽

向量中斷和非向量中斷:
向量中斷:CPU通常為不同的中斷分配不同的中斷號,當檢測到某中斷號的中斷到來後,就自動跳轉到與該中斷號對應的地址執行
非向量中斷:多個中斷共享一個入口地址。進入該入口地址後再通過軟體判斷中斷標志來識別具體哪個是中斷
也就是說向量中斷由軟體提供中斷服務程序入口地址,非向量中斷由軟體提供中斷入口地址

/*典型的非向量中斷首先會判斷中斷源,然後調用不同中斷源的中斷處理程序*/
irq_handler()
{
...
int int_src = read_int_status();/*讀硬體的中斷相關寄存器*/
switch(int_src){//判斷中斷標志
case DEV_A:
dev_a_handler();
break;
case DEV_B:
dev_b_handler();
break;
...
default:
break;
}
...
}

定時器中斷原理:
定時器在硬體上也以來中斷,PIT(可編程間隔定時器)接收一個時鍾輸入,
當時鍾脈沖到來時,將目前計數值增1並與已經設置的計數值比較,若相等,證明計數周期滿,產生定時器中斷,並
復位計數值。

如下圖所示:

Linux中斷處理程序架構:
Linux將中斷分為:頂半部(top half)和底半部(bottom half)
頂板部:完成盡可能少的比較緊急的功能,它往往只是簡單的讀取寄存器中的中斷狀態並清除中斷標志後就進行
「登記中斷」(也就是將底半部處理程序掛在到設備的底半部執行隊列中)的工作
特點:響應速度快

底半部:中斷處理的大部分工作都在底半部,它幾乎做了中斷處理程序的所有事情。
特點:處理相對來說不是非常緊急的事件

小知識:Linux中查看/proc/interrupts文件可以獲得系統中斷的統計信息。

如下圖所示:

第一列是中斷號 第二列是向CPU產生該中斷的次數

介紹完相關基礎概念後,讓我們一起來探討一下Linux中斷編程

Linux中斷編程:
1.申請和釋放中斷
申請中斷:
int request_irq(unsigned int irq,irq_handler_t handler,
unsigned long irqflags,const char *devname,void *dev_id)
參數介紹:irq是要申請的硬體中斷號
handler是向系統登記的中斷處理程序(頂半部),是一個回調函數,中斷發生時,系統調用它,將
dev_id參數傳遞給它
irqflags:是中斷處理的屬性,可以指定中斷的觸發方式和處理方式:
觸發方式:IRQF_TRIGGER_RISING、IRQF_TRIGGER_FALLING、IRQF_TRIGGER_HIGH、IRQF_TRIGGER_LOW
處理方式:IRQF_DISABLE表明中斷處理程序是快速處理程序,快速處理程序被調用時屏蔽所有中斷
IRQF_SHARED表示多個設備共享中斷,dev_id在中斷共享時會用到,一般設置為NULL

返回值:為0表示成功,返回-EINVAL表示中斷號無效,返回-EBUSY表示中斷已經被佔用,且不能共享
頂半部的handler的類型irq_handler_t定義為
typedef irqreturn_t (*irq_handler_t)(int,void*);
typedef int irqreturn_t;

2.釋放IRQ
有請求當然就有釋放了
void free_irq(unsigned int irq,void *dev_id);
參數定義與request_irq類似

3.使能和屏蔽中斷
void disable_irq(int irq);//等待目前中斷處理完成(最好別在頂板部使用,你懂得)
void disable_irq_nosync(int irq);//立即返回
void enable_irq(int irq);//

4.屏蔽本CPU內所有中斷:
#define local_irq_save(flags)...//禁止中斷並保存狀態
void local_irq_disable(void);//禁止中斷,不保存狀態

下面來分別介紹一下頂半部和底半部的實現機制

底半部機制:
簡介:底半部機制主要有tasklet、工作隊列和軟中斷
1.底半部是想方法之一tasklet
(1)我們需要定義tasklet機器處理器並將兩者關聯
例如:
void my_tasklet_func(unsigned long);/*定義一個處理函數*/
DECLARE_TASKLET(my_tasklet,my_tasklet_func,data);
/*上述代碼定義了名為my_tasklet的tasklet並將其餘
my_tasklet_func()函數綁定,傳入的參數為data*/
(2)調度
tasklet_schele(&my_tasklet);
//使用此函數就能在是當的時候進行調度運行

tasklet使用模板:
/*定義tasklet和底半部函數並關聯*/
void xxx_do_tasklet(unsigned long);
DECLARE_TASKLET(xxx_tasklet,xxx_do_tasklet,0);

/*中斷處理底半部*/
void xxx_do_tasklet(unsigned long)
{
...
}

/*中斷處理頂半部*/
irqreturn_t xxx_interrupt(int irq,void *dev_id)
{
...
tasklet_schele(&xxx_tasklet);//調度地板部
...
}

/*設備驅動模塊載入函數*/
int __init xxx_init(void)
{
...
/*申請中斷*/
result = request_irq(xxx_irq,xxx_interrupt,
IRQF_DISABLED,"xxx",NULL);
...

return IRQ_HANDLED;
}

/*設備驅動模塊卸載函數*/
void __exit xxx_exit(void)
{
...
/*釋放中斷*/
free_irq(xxx_irq,xxx_interrupt);
...
}

2.底半部實現方法之二---工作隊列
使用方法和tasklet類似
相關操作:
struct work_struct my_wq;/*定義一個工作隊列*/
void my_wq_func(unsigned long);/*定義一個處理函數*/
通過INIT_WORK()可以初始化這個工作隊列並將工作隊列與處理函數綁定
INIT_WORK(&my_wq,(void (*)(void *))my_wq_func,NULL);
/*初始化工作隊列並將其與處理函數綁定*/
schele_work(&my_wq);/*調度工作隊列執行*/

/*工作隊列使用模板*/

/*定義工作隊列和關聯函數*/
struct work_struct(unsigned long);
void xxx_do_work(unsigned long);

/*中斷處理底半部*/
void xxx_do_work(unsigned long)
{
...
}

/*中斷處理頂半部*/
/*中斷處理頂半部*/
irqreturn_t xxx_interrupt(int irq,void *dev_id)
{
...
schele_work(&my_wq);//調度底半部
...
return IRQ_HANDLED;
}

/*設備驅動模塊載入函數*/
int xxx_init(void)
{
...
/*申請中斷*/
result = request_irq(xxx_irq,xxx_interrupt,
IRQF_DISABLED,"xxx",NULL);
...
/*初始化工作隊列*/
INIT_WORK(&my_wq,(void (*)(void *))xxx_do_work,NULL);
}

/*設備驅動模塊卸載函數*/
void xxx_exit(void)
{
...
/*釋放中斷*/
free_irq(xxx_irq,xxx_interrupt);
...
}

熱點內容
oraclelinux監聽配置 發布:2025-03-23 08:07:48 瀏覽:965
鎖頻密碼忘了怎麼辦 發布:2025-03-23 08:05:30 瀏覽:739
如何查詢公會伺服器 發布:2025-03-23 07:50:24 瀏覽:768
老電腦卡頓如何升級配置 發布:2025-03-23 07:48:08 瀏覽:979
伺服器tnt禁了怎麼炸區塊 發布:2025-03-23 07:43:48 瀏覽:687
線上兒童編程哪家好 發布:2025-03-23 07:43:42 瀏覽:923
如何配置多肉的圖片 發布:2025-03-23 07:38:37 瀏覽:805
千尋位置手簿哪裡輸入賬號密碼 發布:2025-03-23 07:34:50 瀏覽:419
荒野求生游戲需要哪些配置 發布:2025-03-23 07:34:49 瀏覽:233
mac自帶解壓縮軟體 發布:2025-03-23 07:33:51 瀏覽:23