linux驅動開發(fā)(二):Linux字符設備驅動程序(設備號元扔、cdev、設備節(jié)點旋膳、file_operations)

Linux系統(tǒng)將設備分成字符設備澎语、塊設備、網(wǎng)絡設備三類验懊。


用戶程序調用硬件的過程如下擅羞。

一、用戶級义图、內核級和系統(tǒng)調用

Linux/Unix系統(tǒng)下的進程運行分為用戶態(tài)進程態(tài)兩種狀態(tài)减俏。我們的應用程序通常僅在用戶態(tài)下運行,出于保護內核資源的需要碱工,用戶態(tài)下運行的程序在只能訪問有限的資源娃承,例如不能訪問內核的數(shù)據(jù)結構和程序。

內核的一個重要功能就是協(xié)調和管理硬件資源怕篷,包括CPU草慧、內存、I/O設備等匙头,從而為上層運行的諸多應用程序提供更好的運行環(huán)境。因此仔雷,驅動程序通常都是在內核態(tài)下運行蹂析。我們的進程大多數(shù)時間都是運行在用戶態(tài)下的,一旦它需要操作系統(tǒng)幫助它完成一些自己沒有權限和能力完成的工作碟婆,就會切換到內核態(tài)电抚。而系統(tǒng)調用就是進程主動申請從用戶態(tài)切換到內核態(tài)的方式。

除了系統(tǒng)調用以外竖共,發(fā)生異瞅眩或外圍設備的中斷也會讓進程從用戶態(tài)進入內核態(tài)。但是公给,系統(tǒng)調用是應用程序主動轉入內核態(tài)借帘,而異常和中斷是被動轉入內核態(tài)。
異常又稱內中斷淌铐,來源于操作系統(tǒng)的內部肺然,通常是進程執(zhí)行時引發(fā)了故障,如缺頁腿准、地址越界际起、算術溢出、除數(shù)為零等。異常產生后街望,操作系統(tǒng)會奪回內核的使用權校翔,對異常進行處理。
外圍設備的中斷是指在外圍設備在處理完用戶要求的操作后灾前,會向操作系統(tǒng)發(fā)出中斷信號防症,此時操作系統(tǒng)會暫停執(zhí)行當前正在執(zhí)行的指令,轉而處理中斷信號豫柬。這個過程需要在內核態(tài)下進行告希,因此會由用戶態(tài)進入內核態(tài)。

二烧给、虛擬文件系統(tǒng)(VFS)

1. 什么是虛擬文件系統(tǒng)

我們在Unix/Linux系統(tǒng)下用戶程序操作普通文件燕偶,即txt、pdf础嫡、mp4等指么,使用open函數(shù)打開文件,使用read榴鼎、write函數(shù)對文件進行讀寫伯诬,使用close函數(shù)關閉文件。但除了普通文件外巫财,Unix/Linux的創(chuàng)作者提出“一切皆文件”的思想盗似,希望將目錄、字符設備平项、塊設備赫舒、套接字等也被等同于文件對待,使用普通文件使用相同的文件操作接口闽瓢,如open接癌、read、write扣讼,對這些設備進行操作缺猛。

虛擬文件系統(tǒng)(Virtual File System,VFS)是Linux系統(tǒng)中的一個軟件抽象層椭符,它就是實現(xiàn)上述功能的關鍵荔燎。它為用戶空間的程序提供了文件系統(tǒng)接口,同時定義了所有文件系統(tǒng)都支持的基本的销钝、概念上的接口和數(shù)據(jù)結構湖雹。例如,讀寫普通的文本文件和讀寫I/O設備的具體實現(xiàn)方法必然是不同的曙搬,但VFS提供了統(tǒng)一的接口read和write摔吏,開發(fā)人員需要編寫這些接口的具體的不同的實現(xiàn)鸽嫂。因此,在VFS層和內核的其他部分看來征讲,所有文件都只需調用read和write函數(shù)就可以完成讀寫功能据某,具體的實現(xiàn)過程它們并不關心。

2. 文件接口結構體file_operation

Linux內核定義了結構體file_operation诗箍,作為提供給VFS的文件接口癣籽。該結構體的定義如下。

include/linux/fs.h
---------------------------------------------------------------------------------------------------
struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    int (*iopoll)(struct kiocb *kiocb, bool spin);
    int (*iterate) (struct file *, struct dir_context *);
    int (*iterate_shared) (struct file *, struct dir_context *);
    __poll_t (*poll) (struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    unsigned long mmap_supported_flags;
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **, void **);
    long (*fallocate)(struct file *file, int mode, loff_t offset,
        loff_t len);
    void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
    unsigned (*mmap_capabilities)(struct file *);
#endif
    ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,loff_t, size_t, unsigned int);
    loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,struct file *file_out, loff_t pos_out,loff_t len, unsigned int remap_flags);
    int (*fadvise)(struct file *, loff_t, loff_t, int);
}滤祖;

file_operations的成員變量基本上都是函數(shù)指針筷狼,驅動開發(fā)者需要根據(jù)不同設備的要求,為這些函數(shù)指針編寫具體的實現(xiàn)匠童。以open函數(shù)為例埂材,不同設備的打開方式不同,因此open函數(shù)的實現(xiàn)顯然應該是不同的汤求,但從VFS層面上看俏险,都是只需要調用open函數(shù)就可以滿足打開設備的需求。

三扬绪、設備號

1. 什么是設備號

Linux系統(tǒng)使用設備號來區(qū)分各個設備竖独。設備號是一個32位的無符號整型數(shù),它的高12位是主設備號挤牛,低20位是次設備號莹痢。

為什么要區(qū)分主設備號和次設備號呢?

舉個例子墓赴,在一個硬件上可能有多個串口竞膳,系統(tǒng)會將每個串口都作為一個設備處理。但很多時候竣蹦,這些串口發(fā)揮的作用是相同的,只有地址不同沧奴。我們希望這些串口都由同一個驅動管理痘括,而不是為每個串口都寫一個驅動程序,因此Linux將設備號分為主設備號和次設備號滔吠。我們將這些串口分配相同的主設備號和不同的次設備號纲菌,這樣只要使用同一個驅動程序即可。

/proc/devices文件下記錄著每個設備和它對應的設備號疮绷,注意翰舌,字符設備和塊設備是分開獨立編號的。(/proc目錄用來存放系統(tǒng)進程運行時使用的文件)

2. 與設備號有關的宏

Linux內核定義了一些與設備號有關的宏冬骚,常見的宏如下椅贱。

#define MKDEV(major, minor) //將主設備號和次設備號拼接成設備號
#define MAJOR(devno) //從設備號中提取主設備號
#define MINOR(devno) //從設備號中提取次設備號

3. 分配和釋放設備號的函數(shù)

register_chrdev_region函數(shù)用來將一系列字符設備號分配給字符設備懂算。這一系列字符設備有相同的主設備號,次設備號是連續(xù)的庇麦。

int register_chrdev_region(dev_t from, unsigned count, const char *name)

各個參數(shù)的含義计技。

參數(shù) 含義
from 要分配的這一系列設備號的第一個設備號
count 分配的設備號的個數(shù)
name 設備的名字

返回值。設備分配成功時返回0山橄,分配失敗時垮媒,例如要分配的設備號已經被占用了,則返回一個負值的錯誤碼航棱。

unregister_chrdev_region函數(shù)用來將設備號釋放掉睡雇。由于設備號是臨界資源,同一個設備號不能被多個設備共有饮醇,因此設備使用結束后一定要及時釋放它抱,以免不必要的資源浪費。

void unregister_chrdev_region(dev_t from, unsigned count)

該函數(shù)參數(shù)的含義與register_chrdev_region參數(shù)的含義相同驳阎。

4. 設備號分配的原理

在Linux內核中定義了一個全局指針數(shù)組chrdevs抗愁,用來管理分配出去的設備號。chrdevs的每一個元素都是一個指向char_device_struct結構體的指針呵晚。它的具體定義如下蜘腌。

fs/char_dev.c
-----------------------------------------------------------------------------------------------
#define CHRDEV_MAJOR_HASH_SIZE 255

static struct char_device_struct {
    struct char_device_struct *next;
    unsigned int major;
    unsigned int baseminor;
    int minorct;
    char name[64];
    struct cdev *cdev;
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

每個成員的含義如下。

成員 含義
next 鏈表中指向下一個節(jié)點的指針
major 主設備號
baseminor 次設備號的第一個
minorct 申請的次設備號的數(shù)量
name 設備的名字
cdev 下文中會具體介紹cdev結構體

chrdevs實際上是一個哈希表饵隙,它的關鍵字為主設備號major撮珠,散列函數(shù)為index = major % 255。初始狀態(tài)下金矛,沒有設備號被分配出去芯急,chrdevs數(shù)組為空。一旦有設備號被分配驶俊,則首先為該設備創(chuàng)建一個char_device_struct結構體對象娶耍,然后根據(jù)主設備號計算得到散列結果,將結構體對象掛載在chrdevs中的對應位置上饼酿。如果該位置已經有節(jié)點榕酒,則根據(jù)次設備號由小到大的尋找到合適的位置,掛載在對應節(jié)點的next指針上故俐,構成有序列表想鹰。

下面舉個例子說明這個過程。

初始狀態(tài)下的的chrdevs表药版,實際上是255個char_device_struct *類型指針辑舷。


使用register_chrdev_region函數(shù),向系統(tǒng)注冊一個主設備號major = 257槽片、次設備號為0到3的設備何缓、名稱為“dev1”的設備肢础,則需要首先創(chuàng)建一個char_device_struct結構體,并對結構體中的成員進行初始化歌殃,然后計算散列值i = 257 % 255 = 2乔妈,并讓chrdevs[2]這個指針指向這個結構體。

如果我們再向系統(tǒng)注冊一個主設備號為2氓皱、次設備號為0到1路召、名稱為“dev2”的設備,則同樣創(chuàng)建一個char_device_struct結構體并初始化波材。這次我們計算得到的散列值i = 2 % 255依舊為2股淡,與上一個設備在哈希表上產生了沖突,這時結構體中的指針發(fā)揮了作用廷区,我們將新結構體掛在老結構體的前面或后面唯灵,形成一個鏈表即可。掛載的位置由主設備號的大小決定隙轻,我們要求形成的鏈表是一個major由小到大排序的有序鏈表埠帕。

四、字符設備的內核抽象

1. 字符設備結構體cdev

內核為字符設備抽象出一個結構體cdev玖绿,定義如下:

struct cdev {
    struct kobject      kobj;
    struct module       *owner;
    const struct file_operation *ops;
    struct list_head    list;
    dev_t               dev;
    unsigned int        count;
};

每個成員的含義如下敛瓷。

成員 含義
kobj 內嵌的內核對象,此處不展開討論
owner 字符設備驅動程序所在的內核模塊指針斑匪,此處不展開討論
ops 提供給VFS的文件接口結構體
list 將系統(tǒng)中的字符設備形成鏈表
dev 字符設備的設備號
count 隸屬于同一主設備號的次設備號的數(shù)量呐籽,表示當前設備驅動程序控制的設備的數(shù)量

2. 字符設備的初始化

Linux內核為初始化cdev對象提供了cdev_init函數(shù),定義如下蚀瘸。

fs/char_dev.c
-----------------------------------------------------------------------------------------------
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
    memset(cdev, 0, sizeof *cdev);
    INIT_LIST_HEAD(&cdev->list);
    kobject_init(&cdev->kobj, &ktype_cdev_default);
    cdev->ops = fops;
}

這個函數(shù)的作用就是針對一個cdev結構體里面的成員進行初始化狡蝶,其中最重要的一步就是把cdev和fops連接在一起,因為每一個設備都會有一個自己的文件操作邏輯贮勃,即需要實現(xiàn)一個自己的file_operations結構體贪惹。

3. 向系統(tǒng)添加或刪除字符設備

將字符設備加入到系統(tǒng)中,簡單地說寂嘉,就是將上文中我們初始化的cdev結構體添加到Linux系統(tǒng)維護的一個全局哈希鏈表cdev_map中奏瞬,這樣其他模塊才能使用通過cdev_map找到它。Linux為了完成這個操作垫释,提供了cdev_add函數(shù)丝格,實現(xiàn)如下撑瞧。

fs/char_dev.c
-----------------------------------------------------------------------------------------------
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
    p->dev = dev;
    p->count = count;
    return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}

函數(shù)參數(shù)的含義如下棵譬。

參數(shù) 含義
p 要加入系統(tǒng)的字符設備對象的指針
dev 該設備的設備號
count 被注冊的設備數(shù)量

該函數(shù)的核心功能在kobj_map函數(shù)中實現(xiàn),該函數(shù)的實現(xiàn)略微有些復雜预伺,此處不再贅述订咸,有興趣的可以閱讀附錄曼尊。

相應的,刪除設備需要使用cdev_del函數(shù)脏嚷。該函數(shù)的作用就是將對應的設備從系統(tǒng)中移除骆撇,即從cdev_map鏈表中刪除節(jié)點并釋放內存空間。其定義如下父叙。

fs/char_dev.c
-----------------------------------------------------------------------------------------------
void cdev_del(struct cdev *p)
{
    cdev_unmap(p->dev, p->count);
    kobject_put(&p-> kobj);
}

五神郊、設備節(jié)點

1. 什么是設備節(jié)點

設備節(jié)點也可以稱為設備文件,是一種特殊類型的文件趾唱,其作用是溝通用戶空間的應用程序和內核空間的驅動程序的節(jié)點涌乳。應用程序如果想使用驅動程序提供的服務,則必須經過設備節(jié)點實現(xiàn)甜癞。

2. 設備節(jié)點的創(chuàng)建

設備節(jié)點有靜態(tài)創(chuàng)建和動態(tài)創(chuàng)建兩種方式夕晓,我們這里只介紹靜態(tài)創(chuàng)建方法。Linux系統(tǒng)使用mknod命令靜態(tài)創(chuàng)建設備節(jié)點悠咱,該命令需要列出設備節(jié)點的設備節(jié)點名蒸辆、主設備號和次設備號。通常情況下析既,Linux系統(tǒng)將所有設備節(jié)點都放在/dev目錄下躬贡,因此我們也將設備節(jié)點創(chuàng)建在/dev目錄下。該命令具體格式如下渡贾。

mknod /dev/<設備節(jié)點名> c <主設備號> <次設備號>

參數(shù)c表示節(jié)點的類型為字符設備逗宜。

mknod命令是通過調用mknod函數(shù)實現(xiàn),它會通過系統(tǒng)調用sys_mknod進入內核空間空骚,并生成一個inode纺讲。

inode是Linux文件管理系統(tǒng)維護的一個結構體,每一個文件(不限于設備節(jié)點)都會有一個自己inode囤屹,用來存儲文件的靜態(tài)信息熬甚,如文件訪問權限、屬主肋坚、組乡括、大小、生成時間智厌、訪問時間诲泌、最后修改時間等。

由于inode結構體中的成員非常多铣鹏,我們此處不再一一列舉敷扫,只列舉幾個常用的。

struct inode {
    kuid_t      i_uid;   //屬主的ID(UID)
    kgid_t      i_gid;   //屬主的組ID(GID)
    loff_t      i_size;  //文件大小
    dev_t       i_rdev;
    struct cdev *i_cdev;
    ......
};

這里我們主要用到i_rdev和i_cdev兩個成員诚卸,i_rdev是該設備的設備號葵第,i_cdev是該設備的cdev結構體绘迁。

3. 設備節(jié)點的打開

在創(chuàng)建了設備節(jié)點和它的inode后,應用程序就可以像打開普通文件一樣使用open函數(shù)打開設備節(jié)點卒密。用戶空間下的應用程序使用open系統(tǒng)調用缀台,其函數(shù)原型如下。

int open(const char *filename, int flags, mode_t mode);

參數(shù)的含義

參數(shù) 含義
filename 要打開的文件的文件名
flags 該文件的打開模式或創(chuàng)建模式
mode 僅在創(chuàng)建一個新文件時使用哮奇,用于指定新建文件的訪問權限
返回值 成功時返回該文件的文件描述符膛腐,失敗時返回-1

文件描述符(File Discriptor, fd)本質是一個int型變量(非負整數(shù)),可以看作Linux系統(tǒng)為每個打開的文件分配的一個索引值鼎俘。在后續(xù)對該文件的read依疼、write、close等操作時而芥,都要通過這個索引值律罢,來找到這個打開的文件。

系統(tǒng)調用open通過層層復雜的機制棍丐,最終會調用到我們?yōu)樵撛O備編寫的file_operations結構體中的.open函數(shù)中误辑,最終實現(xiàn)該設備的打開操作。

Linux會為每一個打開的文件維護一個file結構體歌逢,它在文件打開時被創(chuàng)建巾钉,直到該文件關閉時被釋放。與inode結構體不同秘案,一個文件只能有一個inode結構體砰苍,但如果它被同時打開很多次,那么Linux會為它創(chuàng)建多個file結構體阱高,且file結構體最終指向同一個inode赚导。

file結構體中的成員較多,下面列舉一些常用的成員赤惊。

struct file {
    struct inode        *f_inode;
    const struct file_operations    *f_op;
    atomic_long_t       f_count;
    unsigned int        f_flags;
    fmode_t             f_mode;
    loff_t              f_pos;
    void                *private_data;
    ......
};

幾個常用的成員及其含義如下吼旧。

成員 含義
f_op 與文件關聯(lián)的各種操作file_operations
f_count 記錄該文件對象被引用的次數(shù),也就是有多少個進程正在使用該文件
f_flags 文件打開時指定的標志未舟,驅動程序中常用O_NONBLOCK來檢查是否是非阻塞請求
f_mode 文件的讀寫模式圈暗,通過置位FMODE_READ和FMODE_WRITE,來確定文件是可讀裕膀、可寫或者既可讀又可寫的
f_pos 文件當前的讀寫位置员串,本質是一個long long類型的值
private_data 用來保存自定義設備結構體的地址

以上就是字符設備中常用的數(shù)據(jù)結構和算法愈腾,在下一篇博客中伤柄,我會舉一個實例膘掰,來使用上述內容完成一個應用程序與驅動程序交互的實例肄扎。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末达吞,一起剝皮案震驚了整個濱河市游昼,隨后出現(xiàn)的幾起案子撵幽,更是在濱河造成了極大的恐慌伴榔,老刑警劉巖斯稳,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件海铆,死亡現(xiàn)場離奇詭異,居然都是意外死亡挣惰,警方通過查閱死者的電腦和手機卧斟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來憎茂,“玉大人珍语,你說我怎么就攤上這事∈#” “怎么了板乙?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長拳氢。 經常有香客問我募逞,道長,這世上最難降的妖魔是什么馋评? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任放接,我火速辦了婚禮,結果婚禮上留特,老公的妹妹穿的比我還像新娘纠脾。我一直安慰自己,他們只是感情好蜕青,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布苟蹈。 她就那樣靜靜地躺著,像睡著了一般右核。 火紅的嫁衣襯著肌膚如雪汉操。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天蒙兰,我揣著相機與錄音磷瘤,去河邊找鬼。 笑死搜变,一個胖子當著我的面吹牛采缚,可吹牛的內容都是我干的。 我是一名探鬼主播挠他,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼扳抽,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起贸呢,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤镰烧,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后楞陷,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體怔鳖,經...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年固蛾,在試婚紗的時候發(fā)現(xiàn)自己被綠了结执。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡艾凯,死狀恐怖献幔,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情趾诗,我是刑警寧澤蜡感,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站恃泪,受9級特大地震影響铸敏,放射性物質發(fā)生泄漏。R本人自食惡果不足惜悟泵,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一杈笔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧糕非,春花似錦蒙具、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至衡招,卻和暖如春篱昔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背始腾。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工州刽, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人浪箭。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓穗椅,卻偏偏與公主長得像,于是被迫代替她去往敵國和親奶栖。 傳聞我的和親對象是個殘疾皇子匹表,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內容