<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">我们在认识系统处理中断流程时,先要知道在硬件上,外部设备和cpu和中断控制器是如何连接的。</span>
当电平触发,或边沿触发一个外设产生一个硬件中断,这个电平信号会送到和这个外设相连的中断控制器,有可能中断控制器是级联的,
这个中断控制器会将中断信号送达上一级中断控制,最后由root中断控制器送到CPU,在这里又会遇到一个问题,一般系统不止一个CPU,
那么到底传给哪个CPU来处理呢,当然这里还有很多处理机制,还要结合gic的硬件结构,我也没有深入学习。
了解了中断硬件结构那么我们来看一下在高通代码中的软件流程
系统起来后,首先会初始化中断控制器,在高通8994代码中,有两个中断控制器
qcom,msm-qgic2 和qcom,msm-tlmm-gp,qcom,msm-tlmm-gp是第二级中断控制器,这个我们可以在device tree 中
grep -r "gpio-controller"
中断控制器在device tree 中都会有 gpio-controller 的标记。
而这些中断控制器会被编译进一个段中如:
IRQCHIP_DECLARE(msm_qgic2, "qcom,msm-qgic2", msm_gic_of_init);
IRQCHIP_DECLARE(tlmmv3_irq, "qcom,msm-tlmm-gp", irq_msm_gpio_init);
#define IRQCHIP_DECLARE(name,compstr,fn) \
static const struct of_device_id irqchip_of_match_##name \
__used __section(__irqchip_of_table) \
= { .compatible = compstr, .data = fn }
#ifdef CONFIG_IRQCHIP
#define IRQCHIP_OF_MATCH_TABLE() \
. = ALIGN(8); \
VMLINUX_SYMBOL(__irqchip_begin) = .; \
*(__irqchip_of_table) \
*(__irqchip_of_end)
void __init irqchip_init(void)
{
of_irq_init(__irqchip_begin);
}
系统起来后会初始化各个模块会调用
asmlinkage void __init start_kernel(void)
{
···
init_IRQ();
···
}
void __init init_IRQ(void)
{
irqchip_init();
if (!handle_arch_irq)
panic("No interrupt controller found.");
}
void __init irqchip_init(void)
{
of_irq_init(__irqchip_begin);
}
void __init of_irq_init(const struct of_device_id *matches)
{
····
for_each_matching_node(np, matches) { // 由于传入的np 为空,所以会遍历这个device tree
if (!of_find_property(np, "interrupt-controller", NULL)) //找到包含"interrupt-controller"属性的节点
continue;
desc = kzalloc(sizeof(*desc), GFP_KERNEL);
if (WARN_ON(!desc))
goto err;
desc->dev = np;
desc->interrupt_parent = of_irq_find_parent(np); //查找这个节点有没有父设备
if (desc->interrupt_parent == np)
desc->interrupt_parent = NULL;
list_add_tail(&desc->list, &intc_desc_list); //将找到的中断控制器信息添加到链表中
}
····
while (!list_empty(&intc_desc_list)) {
list_for_each_entry_safe(desc, temp_desc, &intc_desc_list, list) { //遍历保存中断控制器信息的链表
const struct of_device_id *match;
int ret;
of_irq_init_cb_t irq_init_cb;
if (desc->interrupt_parent != parent) //由于在此parent为空,意思就是找到最顶层的GIC
continue;
list_del(&desc->list);
match = of_match_node(matches, desc->dev);
if (WARN(!match->data,
"of_irq_init: no init function for %s\n",
match->compatible)) {
kfree(desc);
continue;
}
pr_debug("of_irq_init: init %s @ %p, parent %p\n",
match->compatible,
desc->dev, desc->interrupt_parent);
irq_init_cb = (of_irq_init_cb_t)match->data; //在此处,就把我们编译到段中的数据赋值给变量
ret = irq_init_cb(desc->dev, desc->interrupt_parent); //这里就调到了GIC的初始化函数了
if (ret) {
kfree(desc);
continue;
}
list_add_tail(&desc->list, &intc_parent_list);
}
desc = list_first_entry(&intc_parent_list, typeof(*desc), list);
if (list_empty(&intc_parent_list) || !desc) {
pr_err("of_irq_init: children remain, but no parents\n");
break;
}
list_del(&desc->list);
parent = desc->dev; //将最顶层的GIC 赋值给parent,接下来的步骤会依次执行它的子GIC的初始化代码
kfree(desc);
}
}
接下来我们就来看一下中断控制器的初始化。
在看gic的代码之前要知道什么是 irq_domain irq_number与HW interrupt ID,我这里就不再讲解,我这里是主要讲软件的流程。
int __init gic_of_init(struct device_node *node, struct device_node *parent)
{
···
dist_base = of_iomap(node, 0); //GIC Distributor的地址
WARN(!dist_base, "unable to map gic dist registers\n");
cpu_base = of_iomap(node, 1); //GIC CPU interface的地址
WARN(!cpu_base, "unable to map gic cpu registers\n");
if (of_property_read_u32(node, "cpu-offset", &percpu_offset))
percpu_offset = 0;
gic_init_bases(gic_cnt, -1, dist_base, cpu_base, percpu_offset, node);
···
if (parent) { //有可能这个GIC是级联的GIC不是最顶层的GIC,它还是要注册它自己的中断
irq = irq_of_parse_and_map(node, 0);
gic_cascade_irq(gic_cnt, irq);
}
}
void __init gic_init_bases(unsigned int gic_nr, int irq_start,
void __iomem *dist_base, void __iomem *cpu_base,
u32 percpu_offset, struct device_node *node)
{
···
gic_irqs = readl_relaxed(gic_data_dist_base(gic) + GIC_DIST_CTR) & 0x1f; //读取寄存器中的值
gic_irqs = (gic_irqs + 1) * 32; //计算出此GIC能支持多少个中断
if (gic_irqs > 1020)
gic_irqs = 1020; //最多支持1020个中断
gic_irqs -= hwirq_base; //减去16个,0-15的中断号是特殊的中断号
irq_base = irq_alloc_descs(irq_start, 16, gic_irqs, numa_node_id()); //开始分配中断号,从16号开始分配
if (IS_ERR_VALUE(irq_base)) {
WARN(1, "Cannot allocate irq_descs @ IRQ%d, assuming pre-allocated\n",
irq_start);
irq_base = irq_start;
}
gic->domain = irq_domain_add_legacy(node, gic_irqs, irq_base, //将HW interrupt id和irq number 映射起来
hwirq_base, &gic_irq_domain_ops, gic);
···
}
#define irq_alloc_descs(irq, from, cnt, node) \
__irq_alloc_descs(irq, from, cnt, node, THIS_MODULE)
int __ref __irq_alloc_descs(int irq, unsigned int from, unsigned int cnt, int node,
struct module *owner)
{
···
start = bitmap_find_next_zero_area(allocated_irqs, IRQ_BITMAP_BITS, //从from开始,在位图中找到第一个不为0的区域
from, cnt, 0);
ret = -EEXIST;
if (irq >=0 && start != irq)
goto err;
if (start + cnt > nr_irqs) {
ret = irq_expand_nr_irqs(start + cnt);
if (ret)
goto err;
}
bitmap_set(allocated_irqs, start, cnt); //将分配了的位,置位,也就是在allocated_irqs中,每一位对应了一个中断。
mutex_unlock(&sparse_irq_lock);
return alloc_descs(start, cnt, node, owner); //分配中断号,从start开始,一共分配cnt个
···
}
static int alloc_descs(unsigned int start, unsigned int cnt, int node,
struct module *owner)
{
struct irq_desc *desc; //中断描述符,每一个中断号,对应了一个中断描述符
int i;
for (i = 0; i < cnt; i++) {
desc = alloc_desc(start + i, node, owner); //依次分配中断描述符
if (!desc)
goto err;
mutex_lock(&sparse_irq_lock);
irq_insert_desc(start + i, desc); //将中断号与中断描述符建立关系,这里使用的是基数树,方便在以后,
//只要知道了中断号,就可以得到对应的中断描述符
mutex_unlock(&sparse_irq_lock);
}
return start;
···
}
static struct irq_desc *alloc_desc(int irq, int node, struct module *owner)
{
···
desc_set_defaults(irq, desc, node, owner); //将中断描述符赋值,这个只是一些缺省值,后面还会赋值
···
}
分配中断号就分析到这里,我们再回到gic_init_bases函数中。
现在已经为gic支持的所有中断,分配了中断号,并且每一个中断号都对应有一个中断描述符,接下来就要将硬件id和irq number
映射起来,当中断产生时,CPU会读GIC的寄存器,知道是哪个HW interrupt id,再通过这种映射好的关系,就能得到irq number
再通过irq number得到对象的中断描述符,然后执行handler 再执行注册的 action。
对应HW interrupt id 和 irq number的映射,可以有很多种,我们这里使用的是 IRQ_DOMAIN_MAP_LEGACY
#define IRQ_DOMAIN_MAP_LEGACY 0 /* driver allocated fixed range of irqs.
* ie. legacy 8259, gets irqs 1..15 */
#define IRQ_DOMAIN_MAP_NOMAP 1 /* no fast reverse mapping */
#define IRQ_DOMAIN_MAP_LINEAR 2 /* linear map of interrupts */
#define IRQ_DOMAIN_MAP_TREE 3 /* radix tree */
struct irq_domain *irq_domain_add_legacy(struct device_node *of_node,
unsigned int size,
unsigned int first_irq,
irq_hw_number_t first_hwirq,
const struct irq_domain_ops *ops,
void *host_data)
{
struct irq_domain *domain;
unsigned int i;
domain = irq_domain_alloc(of_node, IRQ_DOMAIN_MAP_LEGACY, ops, host_data); //分配irq_domain
if (!domain)
return NULL;
//其实这种映射类似于线性映射first_irq 对应 first_hwirq,first_irq+i对应first_hwirq+i
//当知道一个hw interrupt id时 hw_interrupt_id - first_hwirq + first_irq 就得到irq_number了
domain->revmap_data.legacy.first_irq = first_irq;
domain->revmap_data.legacy.first_hwirq = first_hwirq;
domain->revmap_data.legacy.size = size;
mutex_lock(&irq_domain_mutex);
/* Verify that all the irqs are available */
for (i = 0; i < size; i++) {
int irq = first_irq + i;
struct irq_data *irq_data = irq_get_irq_data(irq);
if (WARN_ON(!irq_data || irq_data->domain)) {
mutex_unlock(&irq_domain_mutex);
irq_domain_free(domain);
return NULL;
}
}
/* Claim all of the irqs before registering a legacy domain */
for (i = 0; i < size; i++) {
struct irq_data *irq_data = irq_get_irq_data(first_irq + i);
irq_data->hwirq = first_hwirq + i; //将相应的属性赋值
irq_data->domain = domain;
}
mutex_unlock(&irq_domain_mutex);
for (i = 0; i < size; i++) {
int irq = first_irq + i;
int hwirq = first_hwirq + i;
/* IRQ0 gets ignored */
if (!irq)
continue;
/* Legacy flags are left to default at this point,
* one can then use irq_create_mapping() to
* explicitly change them
*/
if (ops->map)
ops->map(domain, irq, hwirq); //这是一个回调函数,在gic_init_bases传进来的函数,主要是设置chip和handler
/* Clear norequest flags */
irq_clear_status_flags(irq, IRQ_NOREQUEST);
}
irq_domain_add(domain); //将注册好的domain 添加到系统的irq_domain_list中
return domain;
}
传入的映射函数:
static int gic_irq_domain_map(struct irq_domain *d, unsigned int irq,
irq_hw_number_t hw)
{
if (hw < 32) { //特殊的中断
irq_set_percpu_devid(irq);
irq_set_chip_and_handler(irq, &gic_chip,
handle_percpu_devid_irq);
set_irq_flags(irq, IRQF_VALID | IRQF_NOAUTOEN);
} else {
irq_set_chip_and_handler(irq, &gic_chip, //设置chip 和 handler
//chip: gic_chip
//handler:handle_fasteoi_irq
handle_fasteoi_irq);
set_irq_flags(irq, IRQF_VALID | IRQF_PROBE);
}
irq_set_chip_data(irq, d->host_data); //这里的host_data 是gic私有的,我们不能去修改它
return 0;
}
gic_chip 主要是一些操作GIC硬件的接口
static struct irq_chip gic_chip = {
.name = "GIC",
.irq_mask = gic_mask_irq,
.irq_unmask = gic_unmask_irq,
.irq_eoi = gic_eoi_irq,
.irq_set_type = gic_set_type,
.irq_retrigger = gic_retrigger,
#ifdef CONFIG_SMP
.irq_set_affinity = gic_set_affinity,
#endif
.irq_disable = gic_disable_irq,
.irq_set_wake = gic_set_wake,
};
handler 也就是 highlevel irq-events handler
我们这里的gic的highlevel handler用的是handle_fasteoi_irq
还有很多high level handler 比如:
handle_edge_irq
handle_level_irq
handle_nested_irq
···
至此gic的初始化基本完成,这里只介绍了一个大致的流程。
接下来就是申请中断。
内核的申请函数:
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id)
{
struct irqaction *action;
struct irq_desc *desc;
int retval;
/*
* Sanity-check: shared interrupts must pass in a real dev-ID,
* otherwise we'll have trouble later trying to figure out
* which interrupt is which (messes up the interrupt freeing
* logic etc).
*/
if ((irqflags & IRQF_SHARED) && !dev_id) //如果中断是共享的,但是没有dev_id那么直接返回错误
return -EINVAL;
desc = irq_to_desc(irq); //根据传入的要申请的中断号,找到此中断对应的中断描述符
if (!desc)
return -EINVAL;
if (!irq_settings_can_request(desc) || //有些中断号是不能被申请的,比如用于级联的中断号
WARN_ON(irq_settings_is_per_cpu_devid(desc))) //这个是针对percpu的另外一种情况,需要调用request_percpu_irq接口
return -EINVAL; //有兴趣可以学习一下
if (!handler) {
if (!thread_fn) //如果handler 和thread_fn都为空,是不行的。
return -EINVAL;
handler = irq_default_primary_handler; //这里设置缺省值,其实是唤醒我们的thread_fn
}
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;
//以下为将action 赋值
action->handler = handler;
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;
chip_bus_lock(desc);
retval = __setup_irq(irq, desc, action); //进行实际的注册
chip_bus_sync_unlock(desc);
if (retval)
kfree(action);
#ifdef CONFIG_DEBUG_SHIRQ_FIXME
if (!retval && (irqflags & IRQF_SHARED)) {
unsigned long flags;
disable_irq(irq);
local_irq_save(flags);
handler(irq, dev_id);
local_irq_restore(flags);
enable_irq(irq);
}
#endif
return retval;
}
再来看一下实际的注册:
static int __setup_irq(unsigned int irq, struct irq_desc *desc, struct irqaction *new)
{
struct irqaction *old, **old_ptr;
unsigned long flags, thread_mask = 0;
int ret, nested, shared = 0;
cpumask_var_t mask;
if (!desc)
return -EINVAL;
if (desc->irq_data.chip == &no_irq_chip)
return -ENOSYS;
if (!try_module_get(desc->owner))
return -ENODEV;
nested = irq_settings_is_nested_thread(desc); //判断此irq是不是嵌套中断,如果是嵌套的,那么它的执行是依赖于父中断的thread_fn
if (nested) {
if (!new->thread_fn) {
ret = -EINVAL;
goto out_mput;
}
new->handler = irq_nested_primary_handler; //打印调试信息,正常流程时不会调用的。
} else {
if (irq_settings_can_thread(desc)) //强制线程化,在我们的代码中,这个宏是没有打开的。
irq_setup_forced_threading(new);
}
if (new->thread_fn && !nested) { //如果thread_fn不为空,并且没有嵌套,那么内核将创建一个线程。
struct task_struct *t;
t = kthread_create(irq_thread, new, "irq/%d-%s", irq,
new->name);
if (IS_ERR(t)) {
ret = PTR_ERR(t);
goto out_mput;
}
get_task_struct(t); //为这个threaded handler的task struct增加一次reference count,这样,即便是
//该thread异常退出也可以保证它的task struct不会被释放掉
new->thread = t;
set_bit(IRQTF_AFFINITY, &new->thread_flags);
}
if (!alloc_cpumask_var(&mask, GFP_KERNEL)) {
ret = -ENOMEM;
goto out_thread;
}
if (desc->irq_data.chip->flags & IRQCHIP_ONESHOT_SAFE)
new->flags &= ~IRQF_ONESHOT;
raw_spin_lock_irqsave(&desc->lock, flags);
//一下是中断共享的代码
old_ptr = &desc->action;
old = *old_ptr;
if (old) {
if (!((old->flags & new->flags) & IRQF_SHARED) || //如果是中断共享,那么此中断要和之前的中断的特性一样
((old->flags ^ new->flags) & IRQF_TRIGGER_MASK) ||
((old->flags ^ new->flags) & IRQF_ONESHOT))
goto mismatch;
/* All handlers must agree on per-cpuness */
if ((old->flags & IRQF_PERCPU) !=
(new->flags & IRQF_PERCPU))
goto mismatch;
/* 将新的action加到队列的最后 */
do {
thread_mask |= old->thread_mask;
old_ptr = &old->next;
old = *old_ptr;
} while (old);
shared = 1;
}
if (new->flags & IRQF_ONESHOT) {
if (thread_mask == ~0UL) { //如果thread_mask 所有位都为1,表示这个中断号上已经挂了太多的共享中断了
ret = -EBUSY;
goto out_mask;
}
new->thread_mask = 1 << ffz(thread_mask); //找到为0的位,然后置1,其实就是thread_mask的每一位都代表着一个共享中断
} else if (new->handler == irq_default_primary_handler && //这里有一种情况,就是当中断没有oneshot标记,并且是电平触发,
//如果底层的irq chip 也不是oneshot,那就有可能出现,一直触发中断的情况,
// 因为这里没有清除中断位的操作
//
!(desc->irq_data.chip->flags & IRQCHIP_ONESHOT_SAFE)) {
pr_err("Threaded irq requested with handler=NULL and !ONESHOT for irq %d\n",
irq);
ret = -EINVAL;
goto out_mask;
}
if (!shared) {
init_waitqueue_head(&desc->wait_for_threads);
if (new->flags & IRQF_TRIGGER_MASK) {
ret = __irq_set_trigger(desc, irq, //这里就是设置中断的触发方式。
new->flags & IRQF_TRIGGER_MASK);
if (ret)
goto out_mask;
}
desc->istate &= ~(IRQS_AUTODETECT | IRQS_SPURIOUS_DISABLED | \
IRQS_ONESHOT | IRQS_WAITING);
irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);
if (new->flags & IRQF_PERCPU) {
irqd_set(&desc->irq_data, IRQD_PER_CPU);
irq_settings_set_per_cpu(desc);
}
if (new->flags & IRQF_ONESHOT)
desc->istate |= IRQS_ONESHOT;
if (irq_settings_can_autoenable(desc))
irq_startup(desc, true);
else
/* Undo nested disables: */
desc->depth = 1;
/* Exclude IRQ from balancing if requested */
if (new->flags & IRQF_NOBALANCING) {
irq_settings_set_no_balancing(desc);
irqd_set(&desc->irq_data, IRQD_NO_BALANCING);
}
/* Set default affinity mask once everything is setup */
setup_affinity(irq, desc, mask);
} else if (new->flags & IRQF_TRIGGER_MASK) {
unsigned int nmsk = new->flags & IRQF_TRIGGER_MASK;
unsigned int omsk = irq_settings_get_trigger_mask(desc);
if (nmsk != omsk)
/* hope the handler works with current trigger mode */
pr_warning("irq %d uses trigger mode %u; requested %u\n",
irq, nmsk, omsk);
}
new->irq = irq;
*old_ptr = new;
/* Reset broken irq detection when installing new handler */
desc->irq_count = 0;
desc->irqs_unhandled = 0;
if (shared && (desc->istate & IRQS_SPURIOUS_DISABLED)) {
desc->istate &= ~IRQS_SPURIOUS_DISABLED;
__enable_irq(desc, irq, false);
}
raw_spin_unlock_irqrestore(&desc->lock, flags);
if (new->thread)
wake_up_process(new->thread);
register_irq_proc(irq, desc);
new->dir = NULL;
register_handler_proc(irq, new);
free_cpumask_var(mask);
return 0;
···
}
整个中断的申请到已完成,接下来就等外部来触发它了。
我这里用gic的一个中断来大致看一下处理流程:
当一个连到GIC上的中断产生时,系统会进入IRQ mode,会找异常向量表,最后会调到函数
handle_arch_irq
我们在gic的初始化代码中看以看到
set_handle_irq(gic_handle_irq);
所以当会调到 gic_handle_irq
static asmlinkage void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
u32 irqstat, irqnr;
struct gic_chip_data *gic = &gic_data[0];
void __iomem *cpu_base = gic_data_cpu_base(gic);
do {
irqstat = readl_relaxed_no_log(cpu_base + GIC_CPU_INTACK); //读取gic的寄存器,可以得到是哪个硬件中断产生的中断
irqnr = irqstat & ~0x1c00;
if (likely(irqnr > 15 && irqnr < 1021)) {
irqnr = irq_find_mapping(gic->domain, irqnr); //以hwid 从irq domain中找到相应的irq number
handle_IRQ(irqnr, regs);
uncached_logk(LOGK_IRQ, (void *)(uintptr_t)irqnr);
continue;
}
if (irqnr < 16) { //特殊的中断
//ID0~ID15用于SGI,ID16~ID31用于PPI。
//PPI类型的中断会送到指定的process上,和其他的process无关。
//SGI是通过写GICD_SGIR寄存器而触发的中断
writel_relaxed_no_log(irqstat, cpu_base + GIC_CPU_EOI);
#ifdef CONFIG_SMP
handle_IPI(irqnr, regs);
#endif
uncached_logk(LOGK_IRQ, (void *)(uintptr_t)irqnr);
continue;
}
break;
} while (1);
}
void handle_IRQ(unsigned int irq, struct pt_regs *regs)
{
···
generic_handle_irq(irq);
···
}
int generic_handle_irq(unsigned int irq)
{
struct irq_desc *desc = irq_to_desc(irq); //通过irq number得到对应的中断描述符
if (!desc)
return -EINVAL;
generic_handle_irq_desc(irq, desc); //将中断描述符和中断号传入,进一步处理
return 0;
}
static inline void generic_handle_irq_desc(unsigned int irq, struct irq_desc *desc)
{
desc->handle_irq(irq, desc); //调用我们这个中断描述符中的handler
//还记得我们在gic初始化的时候,有一个map函数吗?里面的操作就是将handler_irq 赋值
//irq_set_chip_and_handler(irq, &gic_chip, handle_fasteoi_irq);
}
void handle_fasteoi_irq(unsigned int irq, struct irq_desc *desc)
{
raw_spin_lock(&desc->lock); //获得锁
if (unlikely(irqd_irq_inprogress(&desc->irq_data))) //如果此中断正在被其它的CPU处理,并且没有被轮询,那么直接退出。
if (!irq_check_poll(desc))
goto out;
desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
kstat_incr_irqs_this_cpu(irq, desc);
if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {
if (!irq_settings_is_level(desc))
desc->istate |= IRQS_PENDING;
mask_irq(desc);
goto out;
}
if (desc->istate & IRQS_ONESHOT) //如果是oneshot 那么立即mask住。
mask_irq(desc);
preflow_handler(desc);
handle_irq_event(desc);
if (desc->istate & IRQS_ONESHOT)
cond_unmask_irq(desc);
···
}
irqreturn_t handle_irq_event(struct irq_desc *desc)
{
struct irqaction *action = desc->action;
irqreturn_t ret;
desc->istate &= ~IRQS_PENDING; //清除PENGDING状态
irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS); //设置正杂处理的状态
raw_spin_unlock(&desc->lock); //在执行具体action时,是将锁释放了的。
ret = handle_irq_event_percpu(desc, action); //遍历action列表,依次执行
raw_spin_lock(&desc->lock);
irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);
return ret;
}
到此,整个中断的流程就结束了,其中还有很多细节没有介绍,我不理解的地方也有很多,这篇文章只是我的一个学习笔记而已。