Linux版本:v6.0
處理器架構:ARMv8
前言 這幾週花了不少時間在看Linux到底是怎麼處理中斷的,在閱讀網路上前輩們介紹Linux中斷的blogs時總覺得細節都講得不錯,但大方向好難掌握住,我也是真的下去trace一遍才比較有感覺。這篇文章希望用另一個角度,以最快的速度 帶過一遍中斷處理流程的建立與執行,達到初步理解整體架構的目的。我會先講解Linux處理中斷的一些關鍵概念,再使用一個真實的例子來說明這些概念怎麼對應到核心中真實的code。
這裡使用的例子是ARM64架構配合一個GICv2的中斷控制器建立IPI的情況,為了不讓整體描述過於發散&過長導致理解混亂,這篇用了很case specific的方式來講述,從而有些細節不免被省略,文章最後會放一些沒有提到的概念,供各位深入研究。
因為寫起來過長,文章計畫分成 中斷處理流程建立 與 中斷處理流程執行 兩篇
作業系統中斷極簡介紹 在說明Linux如何處理中斷之前,當然要知道中斷是個什麼東西。中斷基本上來說就是CPU核心的一個針腳被外界改變電位,內部電路感知之後開始相應的處理來響應這件事。在現代大部分的電腦系統中,有各式各樣的設備需要中斷CPU的執行,像是網卡收發封包、 鍵盤輸入、 甚至是其他的CPU核心想要求某個特定的CPU核心處理某些事情等等,中斷控制器(interrupt controller)就是替CPU核心們管理這些的一個硬體設備,CPU可以藉由和中斷控制器溝通來彈性的管理各種系統上的中斷:
開關各個中斷
調整各個中斷要送達哪一個CPU核心
決定各個中斷的優先級
觸發軟體中斷
等等
另外,在處理中斷時,CPU核心也要和中斷控制器進行溝通,以下是處理中斷時的一個典型步驟:
中斷控制器感知到外界第21號設備要中斷CPU核心
中斷控制器觀察到21號設備先前被設定成要送達0號CPU核心
中斷控制器把”21”這個數字存入代表目前中斷的暫存器中 (這個暫存器在中斷控制器中,不是CPU暫存器)
中斷控制器藉由改變與CPU0的中斷針腳的電位通知CPU0有中斷到達
CPU0放下手邊工作,讀取中斷控制器代表目前中斷的暫存器,讀到21,了解第21號設備需要服務
CPU0處理21號中斷
完成之後,CPU0寫入另一個中斷控制器的暫存器(EOI),來告知中斷控制器它已完成此次中斷的處理
Linux核心的中斷管理與資料結構 Linux IRQ number & Hardware IRQ number Linux在管理系統上的各式中斷時,會為每一個中斷分配一個屬於自己的IRQ號碼,許多人稱Linux IRQ number。該數字用於區分系統中所有的中斷,在/proc/interrupts
看到的中斷號碼就是Linux IRQ number。Hardware IRQ number則是另一個概念,中斷控制器在前面例子中的”21”號,就是hardware IRQ number,是CPU向中斷控制器詢問哪個中斷觸發了,所會得到的答案。
你可能會想問為什麼要有Linux IRQ number呢? 全部都用hardware IRQ number不好嗎?
答案是: 的確,如果能都使用hardware IRQ number的話最好,但是隨著電腦系統越來越複雜,有些系統會出現不只一個中斷控制器的情況,這樣子問題就很明顯,不同中斷控制器有可能有相同的hardware IRQ number,但是接到完全不同的設備。於是乎,Linux就設計了hardware IRQ number轉換到Linux IRQ number的機制: IRQ Domain。
IRQ Domain Domain在數學中的定義是:
Domain,定義域,函數自變數所有可取值的集合。 —wikipedia
而在這邊的意義就是一個hardware IRQ number → Linux IRQ number的轉換範疇,每一個中斷控制器會有一個屬於它的IRQ Domain,屬於該中斷控制器的hardware IRQ number就要使用該IRQ Domain來轉換成全局唯一的Linux IRQ number,藉此繼續處理該中斷。
實際轉換時,Linux提供兩種方式:
radix tree: 使用hardware IRQ number lookup radix tree來得到Linux IRQ number (對radix tree不熟目前只能這樣講 :P)
array lookup: 以hardware IRQ number作為array index取得Linux IRQ number
以下我會把轉換的資料結構叫做”revmap”。
中斷控制器的驅動可以從兩種方式自由選用其一,radix tree適合不連續的hardware IRQ numbers。
1 2 3 4 5 6 7 8 9 10 struct irq_domain { struct list_head link ; const char *name; const struct irq_domain_ops *ops ; [...] unsigned int revmap_size; struct radix_tree_root revmap_tree ; [...] struct irq_data __rcu *revmap []; };
IRQ Desc & IRQ Data & IRQ Action & 中斷處理函式們 在irq_domain
可以看到revmap[]
其實並不是單純存著Linux IRQ number (int),而是irq_data
的位址。每一個中斷初始化時都會分配一個irq_desc
儲存這個中斷的metadata,而irq_data
是irq_desc
的一部分,一個中斷對應到一個irq_desc
和irq_data
。Linux IRQ number → Hardware IRQ number的轉換(irq
, hwirq
),以及其中斷處理函式 (handle_irq
)均可以在這裡找到。irqaction
則是irq_desc
中的一個鏈表,每一個irqaction
都有一個handler(irqaction→handler
),存著該Linux IRQ number的中斷處理函式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 struct irq_data { u32 mask; unsigned int irq; unsigned long hwirq; struct irq_common_data *common ; struct irq_chip *chip ; struct irq_domain *domain ; #ifdef CONFIG_IRQ_DOMAIN_HIERARCHY struct irq_data *parent_data ; #endif void *chip_data; }; struct irq_desc { struct irq_common_data irq_common_data ; struct irq_data irq_data ; unsigned int __percpu *kstat_irqs; irq_flow_handler_t handle_irq; struct irqaction *action ; [...] } ____cacheline_internodealigned_in_smp; struct irqaction { irq_handler_t handler; void *dev_id; void __percpu *percpu_dev_id; struct irqaction *next ; irq_handler_t thread_fn; struct task_struct *thread ; struct irqaction *secondary ; unsigned int irq; unsigned int flags; [...] } ____cacheline_internodealigned_in_smp;
以下是這四個資料結構的連結方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ┌────────────────────┐ │ irq_domain │ │ │ │ revmap │ │ │ └─┬─┬────────────────┘ │ │ │ │ ┌───────────────┐ ┌─────────────┐ ┌─────────────┐ │ └──────────►│ irq_ desc │ ┌►│ irqaction ├──►│ irqaction │ │ │ │ │ └──────┬──────┘ └──────┬──────┘ │ ┌─┴────────────┐ │ │ │ │ │ │ irq_desc │ │ │ ▼ ▼ │ │ │ │ │ ┌─────────┐ ┌─────────┐ │ │ ┌──────────┐ │ │ │ │ handler │ │ handler │ └──────────►│ │ irq_ data │ │ ├─┘ └─────────┘ └─────────┘ │ └──────────┘ ├──┘ │ │ ┌─────────────┐ ┌─────────────┐ │ ├──►│ irqaction ├──►│ irqaction │ └──────────────┘ └──────┬──────┘ └──────┬──────┘ │ │ ▼ ▼ ┌─────────┐ ┌─────────┐ │ handler │ │ handler │ └─────────┘ └─────────┘
對於一個中斷來說,已經有irq_desc→handle_irq
了,為什麼還要有irqaction
鏈表中的handler?
irq_desc→handle_irq
主要負責該IRQ的flow control和中斷控制器互動,例如mask unmask, ACK(通知控制器該中斷已正被處理), EOI(通知控制器中斷已完成處理)等等操作,而irqaction→handler
則是device specific的中斷處理,例如和網卡、 硬碟互動等等。通常irqaction→handler
由各設備的驅動程式提供,irq_desc→handle_irq
則是由中斷控制器驅動設置,基本上是handle_*
家族函式,如
handle_level_irq
handle_edge_irq
handle_fasteoi_irq
handle_percpu_devid_irq
有人會把上述irq_desc→handle_irq
叫做這個IRQ的high level irq event handler。
除此之外,中斷控制器初始化時還須提供一個函式,來執行從中斷控制器讀取hardware IRQ number,利用irq domain定位正確irq_desc
來處理中斷等等更加底層的操作。在我們的ARM64例子中,設定的方式是GICv2初始化時會讓handle_arch_irq
這個函式指標指到gic_handle_irq
來處理中斷。
Linux中斷處理流程 換一個方式,利用簡化的Linux中斷處理流程來理解前面介紹的各個資料結構和各個中斷處理函式:
中斷發生,exception vector進行中斷現場保存,呼叫handle_arch_irq
(function pointer) 指向的函式
該函式讀取中斷控制器的暫存器得知hardware IRQ number,使用中斷控制器對應的irq_domain
進行hardware IRQ number → Linux IRQ number轉換,並得到irq_desc
呼叫irq_desc→handle_irq
(high level irq event handler)
high level irq event handler依次呼叫irq_desc->action
鏈表中的handler
Linux在能夠處理中斷之前,要把這3個不同層級的handlers function pointer設定好:
全局的handle_arch_irq
這個函式由最底層負責中斷現場保存的程式呼叫,通常由中斷控制器初始化程式設定,所有中斷處理都會呼叫(全局唯一)
irq_desc→handle_irq
此函式負責該中斷的flow control,ACK, EOI, etc. 一個中斷一個,也稱high level event handler
irqaction→handler
irq_desc→handle_irq
會依次呼叫irq_desc
中irqaction
鏈表中的各個handler
(一個中斷可以有多個)
實例: GICv2建立IPI處理流程 經過上面的描述之後我們來觀察GICv2本身和IPI(inter processor interrupt)中斷是如何初始化的,以下每個小節介紹中斷處理機制建立過程的一部分,各個小節按照執行順序編排,而因為有些函式做很多事情,所以會有些函式重複出現。
小提示: 以下列出的函式數量不少,實際把call graph畫出來會讓理解方便許多
1. 設置全局irq handler handle_arch_irq
start_kernel
→ init_IRQ
→ irqchip_init
→ of_irq_init
of_irq_init
呼叫GICv2初始化的callback gic_of_init
→ __gic_init_bases
→ set_irq_handler
來設置全局irq handler。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 static int __init __gic_init_bases(struct gic_chip_data *gic, struct fwnode_handle *handle) { [...] if (gic == &gic_data[0 ]) { for (i = 0 ; i < NR_GIC_CPU_IF; i++) gic_cpu_map[i] = 0xff ; set_handle_irq(gic_handle_irq); if (static_branch_likely(&supports_deactivate_key)) pr_info("GIC: Using split EOI/Deactivate mode\n" ); } ret = gic_init_bases(gic, handle); [...] } int __init set_handle_irq (void (*handle_irq)(struct pt_regs *)) { if (handle_arch_irq != default_handle_irq) return -EBUSY; handle_arch_irq = handle_irq; pr_info("Root IRQ handler: %ps\n" , handle_irq); return 0 ; }
2. allocate屬於此中斷控制器的irq_domain
與revmap 同樣在__gic_init_bases
,__gic_init_bases
→ gic_init_bases
→ irq_domain_create_linear
→ __irq_domain_add
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 static int gic_init_bases (struct gic_chip_data *gic, struct fwnode_handle *handle) { int gic_irqs, ret; [...] if (handle) { gic->domain = irq_domain_create_linear(handle, gic_irqs, &gic_irq_domain_hierarchy_ops, gic); } else { [...] } [...] return ret; } static inline struct irq_domain *irq_domain_create_linear (struct fwnode_handle *fwnode, unsigned int size, const struct irq_domain_ops *ops, void *host_data) { return __irq_domain_add(fwnode, size, size, 0 , ops, host_data); } struct irq_domain *__irq_domain_add (struct fwnode_handle *fwnode , unsigned int size , irq_hw_number_t hwirq_max , int direct_max , const struct irq_domain_ops *ops , void *host_data ) { struct irqchip_fwid *fwid ; struct irq_domain *domain ; static atomic_t unknown_domains; [...] domain = kzalloc_node(struct_size(domain, revmap, size), GFP_KERNEL, of_node_to_nid(to_of_node(fwnode))); if (!domain) return NULL ; [...] INIT_RADIX_TREE(&domain->revmap_tree, GFP_KERNEL); mutex_init(&domain->revmap_mutex); domain->ops = ops; domain->host_data = host_data; domain->hwirq_max = hwirq_max; if (direct_max) { domain->flags |= IRQ_DOMAIN_FLAG_NO_MAP; } domain->revmap_size = size; [...] mutex_lock(&irq_domain_mutex); debugfs_add_domain_dir(domain); list_add(&domain->link, &irq_domain_list); mutex_unlock(&irq_domain_mutex); [...] return domain; }
3. 利用IPI的hardware IRQ number來allocate對應數量的Linux IRQ number和 irq_desc
__gic_init_bases
→ gic_smp_init
→ __irq_domain_alloc_irqs
→ irq_domain_alloc_descs
→ __irq_alloc_descs
→ alloc_descs
→ alloc_desc
& irq_insert_desc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 static __init void gic_smp_init (void ) { struct irq_fwspec sgi_fwspec = { .fwnode = gic_data[0 ].domain->fwnode, .param_count = 1 , }; int base_sgi; cpuhp_setup_state_nocalls(CPUHP_AP_IRQ_GIC_STARTING, "irqchip/arm/gic:starting" , gic_starting_cpu, NULL ); base_sgi = __irq_domain_alloc_irqs(gic_data[0 ].domain, -1 , 8 , NUMA_NO_NODE, &sgi_fwspec, false , NULL ); if (WARN_ON(base_sgi <= 0 )) return ; set_smp_ipi_range(base_sgi, 8 ); } int __irq_domain_alloc_irqs(struct irq_domain *domain, int irq_base, unsigned int nr_irqs, int node, void *arg, bool realloc , const struct irq_affinity_desc *affinity) { int i, ret, virq; [...] if (realloc && irq_base >= 0 ) { virq = irq_base; } else { virq = irq_domain_alloc_descs(irq_base, nr_irqs, 0 , node, affinity); if (virq < 0 ) { pr_debug("cannot allocate IRQ(base %d, count %d)\n" , irq_base, nr_irqs); return virq; } } [...] return ret; } int irq_domain_alloc_descs (int virq, unsigned int cnt, irq_hw_number_t hwirq, int node, const struct irq_affinity_desc *affinity) { [...] virq = __irq_alloc_descs(virq, virq, cnt, node, THIS_MODULE, affinity); [...] } int __ref__irq_alloc_descs(int irq, unsigned int from, unsigned int cnt, int node, struct module *owner, const struct irq_affinity_desc *affinity) { int start, ret; if (!cnt) return -EINVAL; if (irq >= 0 ) { if (from > irq) return -EINVAL; from = irq; } else { from = arch_dynirq_lower_bound(from); } mutex_lock(&sparse_irq_lock); start = bitmap_find_next_zero_area(allocated_irqs, IRQ_BITMAP_BITS, from, cnt, 0 ); ret = -EEXIST; if (irq >=0 && start != irq) goto unlock; if (start + cnt > nr_irqs) { ret = irq_expand_nr_irqs(start + cnt); if (ret) goto unlock; } ret = alloc_descs(start, cnt, node, affinity, owner); unlock: mutex_unlock(&sparse_irq_lock); return ret; } static int alloc_descs (unsigned int start, unsigned int cnt, int node, const struct irq_affinity_desc *affinity, struct module *owner) { [...] for (i = 0 ; i < cnt; i++) { const struct cpumask *mask = NULL ; unsigned int flags = 0 ; [...] desc = alloc_desc(start + i, node, flags, mask, owner); if (!desc) goto err; irq_insert_desc(start + i, desc); [...] } bitmap_set(allocated_irqs, start, cnt); return start; [...] } static void irq_insert_desc (unsigned int irq, struct irq_desc *desc) { radix_tree_insert(&irq_desc_tree, irq, desc); }
4. 使用GICv2的callback設置IPI的irq_desc→handle_irq
剛才3.只是allocate,還沒設置irq_desc→handle_irq
中斷處理函式
__irq_domain_alloc_descs
→ irq_domain_alloc_irqs_hierarchy
→ gic_irq_domain_alloc
→ gic_irq_domain_map
→ irq_domain_set_info
→ __irq_set_handler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 int __irq_domain_alloc_irqs(struct irq_domain *domain, int irq_base, unsigned int nr_irqs, int node, void *arg, bool realloc , const struct irq_affinity_desc *affinity) { [...] if (realloc && irq_base >= 0 ) { virq = irq_base; } else { virq = irq_domain_alloc_descs(irq_base, nr_irqs, 0 , node, affinity); if (virq < 0 ) { pr_debug("cannot allocate IRQ(base %d, count %d)\n" , irq_base, nr_irqs); return virq; } } [...] mutex_lock(&irq_domain_mutex); ret = irq_domain_alloc_irqs_hierarchy(domain, virq, nr_irqs, arg); [...] return ret; } int irq_domain_alloc_irqs_hierarchy (struct irq_domain *domain, unsigned int irq_base, unsigned int nr_irqs, void *arg) { if (!domain->ops->alloc) { pr_debug("domain->ops->alloc() is NULL\n" ); return -ENOSYS; } return domain->ops->alloc(domain, irq_base, nr_irqs, arg); } static int gic_irq_domain_alloc (struct irq_domain *domain, unsigned int virq, unsigned int nr_irqs, void *arg) { int i, ret; irq_hw_number_t hwirq; unsigned int type = IRQ_TYPE_NONE; struct irq_fwspec *fwspec = arg; ret = gic_irq_domain_translate(domain, fwspec, &hwirq, &type); if (ret) return ret; for (i = 0 ; i < nr_irqs; i++) { ret = gic_irq_domain_map(domain, virq + i, hwirq + i); if (ret) return ret; } return 0 ; } static int gic_irq_domain_map (struct irq_domain *d, unsigned int irq, irq_hw_number_t hw) { struct gic_chip_data *gic = d->host_data; struct irq_data *irqd = irq_desc_get_irq_data(irq_to_desc(irq)); const struct irq_chip *chip ; chip = (static_branch_likely(&supports_deactivate_key) && gic == &gic_data[0 ]) ? &gic_chip_mode1 : &gic_chip; switch (hw) { case 0 ... 31 : irq_set_percpu_devid(irq); irq_domain_set_info(d, irq, hw, chip, d->host_data, handle_percpu_devid_irq, NULL , NULL ); break ; default : irq_domain_set_info(d, irq, hw, chip, d->host_data, handle_fasteoi_irq, NULL , NULL ); irq_set_probe(irq); irqd_set_single_target(irqd); break ; } irqd_set_handle_enforce_irqctx(irqd); return 0 ; } void irq_domain_set_info (struct irq_domain *domain, unsigned int virq, irq_hw_number_t hwirq, const struct irq_chip *chip, void *chip_data, irq_flow_handler_t handler, void *handler_data, const char *handler_name) { [...] __irq_set_handler(virq, handler, 0 , handler_name); [...] }
5. 建立hardware IRQ number → Linux IRQ number的轉換revmap __irq_domain_alloc_descs
→ irq_domain_insert_irq
→ irq_domain_set_mapping
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 int __irq_domain_alloc_irqs(struct irq_domain *domain, int irq_base, unsigned int nr_irqs, int node, void *arg, bool realloc , const struct irq_affinity_desc *affinity) { [...] if (realloc && irq_base >= 0 ) { virq = irq_base; } else { virq = irq_domain_alloc_descs(irq_base, nr_irqs, 0 , node, affinity); if (virq < 0 ) { pr_debug("cannot allocate IRQ(base %d, count %d)\n" , irq_base, nr_irqs); return virq; } } [...] mutex_lock(&irq_domain_mutex); ret = irq_domain_alloc_irqs_hierarchy(domain, virq, nr_irqs, arg); [...] for (i = 0 ; i < nr_irqs; i++) irq_domain_insert_irq(virq + i); [...] return ret; } static void irq_domain_insert_irq (int virq) { struct irq_data *data ; for (data = irq_get_irq_data(virq); data; data = data->parent_data) { struct irq_domain *domain = data->domain; domain->mapcount++; irq_domain_set_mapping(domain, data->hwirq, data); if (!domain->name && data->chip) domain->name = data->chip->name; } irq_clear_status_flags(virq, IRQ_NOREQUEST); } static void irq_domain_set_mapping (struct irq_domain *domain, irq_hw_number_t hwirq, struct irq_data *irq_data) { if (irq_domain_is_nomap(domain)) return ; mutex_lock(&domain->revmap_mutex); if (hwirq < domain->revmap_size) rcu_assign_pointer(domain->revmap[hwirq], irq_data); else radix_tree_insert(&domain->revmap_tree, hwirq, irq_data); mutex_unlock(&domain->revmap_mutex); }
6. 使用request_percpu_irq
allocate irqaction
並安裝到irq_desc→action
鏈表中 接著,gic_smp_init
→ set_smp_ipi_range
呼叫request_percpu_irq
來allocate irqaction
並安裝到irq_desc→action
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 void __init set_smp_ipi_range (int ipi_base, int n) { int i; WARN_ON(n < NR_IPI); nr_ipi = min(n, NR_IPI); for (i = 0 ; i < nr_ipi; i++) { int err; err = request_percpu_irq(ipi_base + i, ipi_handler, "IPI" , &cpu_number); WARN_ON(err); ipi_desc[i] = irq_to_desc(ipi_base + i); irq_set_status_flags(ipi_base + i, IRQ_HIDDEN); } [...] } static inline int __must_checkrequest_percpu_irq (unsigned int irq, irq_handler_t handler, const char *devname, void __percpu *percpu_dev_id) { return __request_percpu_irq(irq, handler, 0 , devname, percpu_dev_id); } int __request_percpu_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *devname, void __percpu *dev_id) { [...] desc = irq_to_desc(irq); [...] action = kzalloc(sizeof (struct irqaction), GFP_KERNEL); if (!action) return -ENOMEM; action->handler = handler; action->flags = flags | IRQF_PERCPU | IRQF_NO_SUSPEND; action->name = devname; action->percpu_dev_id = dev_id; [...] retval = __setup_irq(irq, desc, action); [...] return retval; }
未提到的中斷相關概念們
參考資料