Java教程

盈趣科技嵌入式面试总结

本文主要是介绍盈趣科技嵌入式面试总结,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

时间:2021年4月6日
公司:盈趣科技–嵌入式驱动开发工程师

面试流程
  • 前台填写应聘基本信息表 – 5分钟
  • 技术官机试考核并问答面试 – 30分钟
面试总结

失败原因:招聘岗位与自我掌握知识不符合,这家公司的驱动开发工作内容更偏向于Android开发。
面试问题多为Android的外围设备驱动开发以及与上层通讯交互流程的深究,在数据流程等问题须加深总结学习。

面试题分析

1、TP驱动开发流程?需要注意的点?以及如何调试?

Android/Linux TP驱动移植调试一般包括以下几个流程:
当手指接触到屏幕时,会在CPU产生一个中断,中断下半部通过IIC总线,从TP的IC中读取相关信息,在经过Input子系统再对这些数据进行分析,以决定调用哪个事件。

  • 硬件IO口配置
  • TP驱动移植
  • I2C通信调试
  • 中断触发
  • 数据上报

所以一个TP驱动的调试首先需要了解硬件接口配置,包括电源配置信息、I2C通信地址、reset脚等IO口。
然后根据硬件信息配置dts中的clock、intc等基本信息,修改驱动文件,查看驱动模块是否能正常加载。
调试查看I2C通讯是否成功?是否正常通信传输数据?如果通信失败需要使用逻辑分析仪等工具抓取波形分析原因。
查看中断触发以及中断处理函数是否正常,如果失败分析中断调度失败的原因。
数据上报属于input子系统中的数据传送到事件处理层的流程,在应用层调试触摸的坐标信息是否准确。
该问题需要总结的三大问题
1.1、中断机制?
//Todo:
1.2、IIC通讯与驱动开发?
//Todo:
1.3、Input子系统与数据上报流程?
//Todo:

2、linux内核空间与用户空间交互的方式?

//内核空间与用户空间
对32位操作系统而言,它的寻址空间(虚拟地址空间,或叫线性地址空间)为4G(2^32)。也就是说一个进程的最大地址空间为4G。
操作系统的核心是内核(kernel),它独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证内核的安全,现在的操作系统一般都强制用户进程不能直接操作内核。
具体的实现方式基本都是由操作系统将虚拟地址空间划分为两部分,一部分为内核空间,另一部分为用户空间。针对Linux操作系统而言,最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF)由内核使用。而较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF)由各个进程使用,称为用户空间。
对于上面的内容可以理解为:每个进程的4G空间中,最高1G都是一样的,及内核空间。只有剩余的3G才归进程自己使用。换句话就是,最高1G地址空间是被所有进程共享的。下图描述了每个进程4G地址空间的分配情况:
内核空间与用户空间地址分配
//为什么需要区分内核空间与用户空间?
在CPU的所有指令中,有些指令是非常危险的,如果用错,将导致系统奔溃,比如清内存、设置时钟等。如果允许所有的程序都可以使用这些指令,那么系统崩溃的概率将大大增加。
所以,CPU指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通应用程序只能使用那些不会造成灾难的指令。比如Intel的CPU将特权等级分为4个等级Ring0~Ring3。
其实Linux系统只使用了Ring0和Ring3两个运行级别(Windows系统也是一样的)。当进程运行在Ring3级别时被称为运行在用户态,而运行在Ring0级别时被称为运行在内核态。
CPU指令优先级

//内核态与用户态
当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。
在内核态下,进程运行在内核地址空间中,此时CPU可以执行任何指令。运行的代码也不受任何的限制,可以自由地访问任何有效地址,也可以直接进行端口的访问。
内核态
在用户态下,进程运行在用户地址空间中,被执行的代码要受到CPU的诸多检查,它们只能访问映射其地址空间的页表项中规定的在用户态下可访问页面的虚拟地址,且只能对任务状态段(TSS)中I/O许可位图(I/O Permission bitmap)中规定的可访问端口进行直接访问。否则操作系统会将程序代码kill掉,这就是著名的Segmentation fault、不能执行特权指令。
用户态
对于以前的DOS系统来说,是没有内核空间、用户空间以及内核态、用户态这些概念的。可以认为所有的代码都是运行在内核态的,因而用户编写的应用程序代码可以很容易的让操作系统崩溃。
对于Linux来说,通过区分内核空间和用户空间的设计,隔离了操作系统代码(操作系统的代码要比应用程序的代码健壮很多)与应用程序代码。即便是单个应用程序出现错误也不会影响到操作系统的稳定性,这样其它的程序还可以正常的运行(Linux系统是一个多任务系统)。所以,区分内核空间和用户空间本质上是要提高操作系统的稳定性和可用性。

//如何从用户空间进入内核空间
其实所有的系统资源管理都是在内核空间中完成的。比如读写磁盘文件,分配回收内存,从网络接口读写数据等。而这些操作是应用程序无法直接完成的,但是可以通过内核提供的接口来完成这样的任务。
比如应用程序要读取磁盘上的一个文件,它可以向内核发起一个“==系统调用 ==”告诉内核:“我要读取磁盘上的某某文件”。其实就是通过一个特殊的指令让进程从用户态进入到内核态(进入内核空间),在内核空间中,CPU可以执行任何的指令,当然也包括从磁盘上读取数据。具体过程是先把数据读取到内核空间中,然后再把数据拷贝到用户空间并从内核态切换回用户态。此时应用程序已经从系统调用中返回并且拿到了想要的数据,可以开开心心的往下执行了。简单的理解就是应用程序把高科技的事情(从磁盘读取文件)外包给了系统内核,系统内核做这些事情及专业又高效。
系统调用
对于一个进程来说,从用户空间进入内核空间并最终返回用户空间,这个过程十分复杂。例如:“堆栈”,其实进程在内核态和用户态各有一个堆栈。运行在用户空间进程使用的用户空间中的堆栈,而运行在内核空间中进程使用的是内核空间中的堆栈。所以,Linux中每个进程有两个栈,分别用于用户态和内核态。
【扩展】用户栈和内核栈有什么区别?
内核在创建进程的时候,在创建task_struct的同时,会为进程创建相应的堆栈。每个进程会有两个栈:一个用户栈,存在于用户空间。一个内核栈,存在于内核空间。当进程在用户空间运行时,CPU堆栈指针寄存器里面的内容是用户堆栈地址,使用用户栈;当进程在内核空间时,CPU堆栈指针寄存器里面的内容是内核栈空间地址,使用内核栈。
当进程因为中断或者系统调用而从用户态转为内核态时,进程所使用的的堆栈也要从用户栈转到内核栈。进程陷入内核态后,先把用户态堆栈的地址保存在内核栈之中,然后设置堆栈指针寄存器的内容为内核栈的地址,这样就完成了用户栈向内核栈的切换;当进程从内核态恢复到用户态时,把内核栈中保存的用户态的堆栈的地址恢复到堆栈指针寄存器即可。这样就实现了内核栈和用户栈的互转。
那么,当从内核态转到用户态时,由于用户栈的地址是在陷入内核的时候保存在内核栈里面的,所以可以很容易地找到这个地址;但是在陷入内核的时候,如何知道内核栈的地址?关键在进程从用户态转到内核态的时候,进程的内核栈总是空的。这是因为当进程在用户态运行时,使用的是用户栈,当进程陷入到内核态时,内核栈保存进程在内核态运行的相关信息,但是,一旦进程返回到用户态后,内核栈中保存的信息无效,会全部恢复,因此每次进程从用户态陷入内核时得到的内核栈都是空的,所以在进程陷入内核的时候,直接把内核栈的栈顶地址给堆栈指针寄存器就可以了。

既然用户态的进程必须切换到内核态才能使用系统的资源,那么进程从用户态进入内核态一共有几种方式?概括来说一共是三种:系统调用、软中断和硬中断。
下图简明的描述了用户态和内核态之间的转换:
用户态与内核态切换

//标准库
对于普通的应用程序来说,系统调用是个陌生的概念,因为在用户态进行程序编程时一般使用的是标准库函数实现系统调用对接。这是为什么呢?
虽然我们可以通过系统调用让操作系统帮我们完成特定的任务,但这些系统调用都是和操作系统强相关的,Linux和Windows的系统调用就是完全不同的。如果直接使用系统调用的话,那么Linux版本的程序就没有办法在Windows上运行,因此我们需要某种标准,该标准对程序员屏蔽底层差异,这样程序员写的程序就无需修改的能在不同的操作系统上运行。
在C语言中,这就是所谓的标准库。标准库代码也是运行在用户态,而不是像操作系统代码运行在内核态,一般来说,我们调用标准库去打开文件、网络通信等,标准库再根据操作系统选择对应的系统调用。
从分层的角度来说,我们的程序一般都是分成四层,最上层是应用程序,应用程序一般只和标准库打交道(当然,也可以绕过标准库),标准库通过系统调用和操作系统交互,操作系统管理底层硬件。这就是为什么在C语言中同样的open函数既能在Linux下打开文件也能在Windows下打开文件的原因。
程序分层
//整体结构
下面从内核空间和用户空间的角度看一看整个Linux系统的结构。它大体可以分为三个部分,从下往上依次为:硬件–>内核空间–>用户空间。如下图所示:
linux系统结构
从硬件到应用
在硬件之上,内核空间的代码控制了硬件资源的使用权,用户空间中的代码只有通过内核暴露的系统调用接口(System call Interface)才能使用到系统中的硬件资源。在这个设计理念上,Linux与Windows操作系统大同小异。
实际上每个处理器在任何指定时间点上的活动可以概括为下列三者之一:

  • 运行于用户空间,执行用户进程。
  • 运行于内核空间,处于进程上下文,代表某个特定的进程执行。
  • 运行于内核空间,处于中断上下文,与任何进程无关,处理某个特定的中断。

以上三点几乎包括所有的情况,比如当CPU空闲时,内核就运行一个空进程,处于进程上下文,但运行在内核空间。
说明:Linux系统的中断服务程序不能在进程的上下文中执行,它们在一个与所有进程都无关的、专门的中断上下文中执行。之所以存在一个专门的执行环境,就是为了保证中断服务程序能够在第一时间相应和处理中断请求,然后快速地退出。
总结:现代的操作系统大都通过内核空间和用户空间的设计来保护操作系统自身的安全性和稳定性。

//用户空间与内核通信方式
下面总结了7种方式,主要是对以前不是很熟悉的方式做了编程实现,以便加深印象。

  • 使用API:最常用的方式
    A.get_user(x,ptr):在内核中被调用,获取用户空间指定地址的数值并保存到内核变量x中。
    B.put_user(x,ptr):在内核中被调用,将内核空间的变量x的数值保存到用户空间指定地址处。
    C.copy_from_user()/copy_to_user():主要应用于设备驱动读写函数中,通过系统调用触发。

  • 使用proc文件系统:
    /proc文件系统是一种虚拟文件系统,通过他可以作为一种Linux内核空间与用户空间交互的手段。与普通文件不同,这里的虚拟文件的内容都是动态创建的。
    使用/proc文件系统的方式很简单,调用create_proc_entry,返回一个proc_dir_entry指针,然后去填充这个指针指向的结构就行了。

  • 使用sysfs+kobject:
    每个在内核中注册的kobject都对应着sysfs系统中的一个目录,可以通过读取根目录下的sys目录中的文件来获得相应的信息。除了sysfs文件系统和proc文件系统之外,一些其他的虚拟文件系统也能够能同样达到这个效果。

  • netlink:
    netlink socket提供了一组类似与BSD风格的API,用于用户态和内核态的IPC。相比于其他的用户态和内核态IPC机制,netlink有几个好处:1.使用自定义一种协议完成数据交换,不需要添加一个文件等。2.可以支持多点传送。3.支持内核先发起会话。4.异步通信,支持缓存机制。

  • 文件:
    这是一种相对比较笨拙的做法,不过确实是一种可行方法。当初与内核空间的时候,直接操作文件,将想要传递的信息写入文件,然后用户空间可以读取这个文件便可以得到想要的数据了。

  • 使用mmap系统调用:
    可以将内核空间的地址映射到用户空间。一方面可以在driver中修改Struct file_operations结构中的mmap函数指针来重新实现一个文件对应的映射操作。另一方面,也可以直接打开/dev/mem文件,把物理内存中的某一页映射到进程空间中的地址上。其实,除了重写file_operaitons中的mmap函数,还可以重写其他的方法如ioctl等,来达到驱动内核空间和用户空间通信的方式。

  • 信号:
    从内核空间想进程发送信号。用户程序出现重大错误时,内核发送信号杀死相应进程。

3、container_of函数详解?

container_of在linux内核中是一个常见的宏,用于从包含在某个结构中的指针获得结构本身的指针,(给定结构体中某个成员的地址、该结构体类型和该成员的名字获取这个成员所在的机构提变量的首地址),通俗的讲就是通过结构体变量中某个成员的首地址进而获得整个结构体变量的首地址。
源码定义如下:

#define container_of(ptr, type, member) ({	    \
	const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
	(type *)( (char *)__mptr - offsetof(type,member) );})

其实它的语法很简单,只是一些指针的灵活应用,它分两步:

  • 第一步,首先定义一个临时的数据类型(通过typeof((type *)0)->member)获得 )与ptr相同的指针变量__mptr,然后用它来保存ptr的值。
  • 第二步,用(char *)__mptr减去member在结构体中的偏移量,得到的值就是整个结构体变量的首地址(整个宏的返回值就是这个首地址)。
  • =其中语法的难点就是如何得出成员相对结构体的偏移量?使用下面的例子进行理解
/* linux-2.6.38.8/include/linux/compiler-gcc4.h */
#define __compiler_offsetof(a,b) __builtin_offsetof(a,b)
 
/* linux-2.6.38.8/include/linux/stddef.h */
#undef offsetof
#ifdef __compiler_offsetof
#define offsetof(TYPE,MEMBER) __compiler_offsetof(TYPE,MEMBER)
#else
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#endif
 
#include <stdio.h>
 
struct test_struct {
    int num;
    char ch;
    float fl;
};
 
int main(void)
{
    printf("offsetof(struct test_struct, num) = %d\n", 
			offsetof(struct test_struct, num));
    
    printf("offsetof(struct test_struct,  ch) = %d\n", 
			offsetof(struct test_struct, ch));
    
    printf("offsetof(struct test_struct,  fl) = %d\n", 
			offsetof(struct test_struct, fl));
    
    return 0;
}
// 说明:__builtin_offsetof(a,b)是GCC的内置函数,可认为它的实现与((size_t) &((TYPE *)0)->MEMBER)这段代码是一致的。
//0
//4
//8

其中代码难以理解的地方就是它灵活地运用了0地址,如果觉得&((struct test_struct *)0)->ch)这样的代码不好理解,那么可以理解成:假设在0地址分配了一个结构体变量struct test_struct a,然后定义结构体指针变量p并指向a(struct test_struct *p = &a),如此我们可以通过&p->ch获得成员ch的地址。由于a的首地址为0x0,所以成员ch的首地址为0x4。
最后通过强制类型转换(size_t)把一个地址值转换为一个整数。
container_of

//使用示例:
/* linux-2.6.38.8/include/linux/compiler-gcc4.h */
#define __compiler_offsetof(a,b) __builtin_offsetof(a,b)
 
/* linux-2.6.38.8/include/linux/stddef.h */
#undef offsetof
#ifdef __compiler_offsetof
#define offsetof(TYPE,MEMBER) __compiler_offsetof(TYPE,MEMBER)
#else
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#endif
 
/* linux-2.6.38.8/include/linux/kernel.h *
 * container_of - cast a member of a structure out to the containing structure
 * @ptr: the pointer to the member.
 * @type:	the type of the container struct this is embedded in.
 * @member:    the name of the member within the struct.
 *
 */
#define container_of(ptr, type, member) ({	    \
	const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
	(type *)( (char *)__mptr - offsetof(type,member) );})
 
#include <stdio.h>
 
struct test_struct {
    int num;
    char ch;
    float fl;
};
 
int main(void)
{
    struct test_struct init_test_struct = { 99, 'C', 59.12 };
 
    char *char_ptr = &init_test_struct.ch;
 
    struct test_struct *test_struct = container_of(char_ptr, struct test_struct, ch);
    
    printf(" test_struct->num = %d\n test_struct->ch = %c\n test_struct->fl = %f\n", 
	    test_struct->num, test_struct->ch, test_struct->fl);
    
    return 0;
}
4、Linux文件系统概述

Linux文件系统简介
Linux系统的理念是:一切资源皆文件。linux文件系统是用于管理和组织文件的一种机制。
Linux文件系统的基本功能:为非易失性数据提供存储空间,为所有文件提供一种命名和组织方法,定义一个磁盘上数据的逻辑结构
Linux文件系统中的文件是以字节序列组成的信息载体,而文件系统不仅包含着文件中的数据,而且还有文件系统的结构。
Linux用户和应用程序看到的文件、目录、软链接以及文件保护信息都存储在linux文件系统中。
文件系统功能的实现是基于两层软件的方式来提高系统和程序员的效率。

  • 第一层是Linux虚拟文件系统(VFS)。虚拟文件系统提供了内核和开发者访问所有类型文件系统的单一命令集;对不同的文件系统抽象,为上层应用提供统一的API接口。虚拟文件系统软件通过调用特殊设备驱动和不同类型的文件系统进行交互。
  • 特定文件系统的设备驱动是第二层实现。设备驱动程序将文件系统命令的标准集解释为在分区或逻辑卷上的特定类型文件系统命令,为内核提供统一的IO操作接口。
    文件系统结构
    两层文件系统软件实现

Linux文件系统的目录结构
linux文件系统
linux文件系统采用树状结构,方便文件的查找与管理。从用户的观点来看,文件被组织在一个树结构的命名空间中,除了叶节点之外,树的所有节点都表示目录名,目录节点包含它下面文件及目录的所有信息。与树的根相对应的目录被称为根目录。
根目录(/)的内容与意义
根目录是整个文件系统中最重要的一个目录,位于Linux文件系统目录结构的顶层。因为不但都有的目录都是由根目录衍生出来的,同时根目录也与操作系统的开机、还原、系统修复等行为有关。(系统开机所需要的特定文件数据有:核心文件、开机程序、函数库等。如果系统出现错误,根目录也必须要包含有能够修复文件系统的程序才能。因此FHS标准建议:根目录所在分区应该越小越好,且应用程序所安装的软件最好不要与根目录放在同一个分区内。如此不但效能较佳,根目录所在的文件系统也不容易发生问题)。
因为根目录与开机有关,开机过程中仅有根目录会被挂载,其他分区则是在开机完成之后才持续的进行挂载行为。因此根目录下与开机过程有关的目录,就应该与根目录放在同一个分区中。这些目录分别是/etc, /bin, /dev, /lib, /sbin。
根文件系统
根文件系统首先是一种文件系统,该文件不仅具有普通文件系统的存储数据文件的功能,但是相对于比普通的文件系统,它的特殊之处在于,它是内核启动时所挂载(mount)的第一个文件系统,内核代码的映像文件保存在根文件系统中,系统引导启动程序会在根文件系统挂载之后从中把一些初始化脚本(如rcS,inittab)和服务加载到内存中去运行,里面包含了Linux系统能够运行所必须的应用程序、库等,比如可以给用户提供操作Linux的控制界面的shell程序、动态链接的程序运行时所必需的glibc库等。文件系统和内核是完全独立的两个部分。在嵌入式中移植的内核下载到开发板上,是没有办法真正的启动Linux操作系统的,会出现无法加载文件系统的错误。
根文件系统之所以在前面加一个“根”,说明它是加载其他文件系统的“根”,那么如果没有这个根,其它的文件系统也就没有办法进行加载。根文件系统包含系统启动时所必须的目录和关键性的文件,以及使其它文件系统得以挂载(mount)所必要的文件,例如:

  • init进程的应用程序必须运行在根文件系统上
  • 根文件系统提供了根目录‘/’
  • linux挂载分区时所依赖的信息存放于根文件系统/etc/fstab这个文件中
  • shell命令程序必须运行在根文件系统上,比如ls、cd命令等

总之:一套linux体系,只有内核本身是不能工作的,必须要rootfs(上的etc目录下的配置文件、/bin/sbin等目录下的shell命令、还有/lib目录下的库文件等)相配合才能工作。
linux启动时,第一个必须挂载的是根文件系统。若系统不能从指定设备上挂载根文件系统,则系统会出错而退出启动。成功之后可以自动或手动挂载其他的文件系统。因此,一个系统中可以同时存在不同的文件系统。在linux中将一个文件系统与一个存储设备关联起来的过程称为挂载(mount)。使用mount命令将一个文件系统附着在当前文件系统层次结构中(根)。在执行挂载时,要提供文件系统类型、文件系统和一个挂载点。根文件系统被挂载到根目录下“/”上后,在根目录下就有根文件系统的各个目录,文件:/bin /sbin /mmt等,再将其他分区挂载到/mnt目录下,/mnt目录下就有这个分区的各个目录和文件。

inode与文件描述符
inode或i节点是指对文件的索引。如一个系统,所有文件是放在磁盘或flash上,就要编个目录来说明每个文件放在什么位置,有什么属性,及大小等。就像书本的目录一样,便于查找和管理。这目录是操作系统需要的,用来找文件或者管理文件,许多操作系统都用到这个概念,如linux,某些嵌入式文件系统等。当然,对某个系统来说,有许多i节点。所以对节点本身也是要进行管理的。
在linux中,内核通过inode来找到每个文件,但一个文件可以被许多用户同时打开或一个用户同时打开多次。这就有个问题,如何管理文件的当前位移量,因为可能每个用户打开文件后进行的操作都不一样,这样文件位移量也不同,当然还有其他的一些问题。所以linux搞了一个文件描述符(file descriptor)这个东西,来分别为一个用户服务。每个用户每次打开一个文件,就产生一个文件描述符,多次打开就产生多个文件描述符,一一对应,不管是同一个用户还是多个用户,该文件描述符就记录了当前打开的文件的偏移量数据。所以一个i节点可以有0个或多个文件描述符。多个文件描述符可以对应一个i节点。

Linux文件系统支持的文件类型
查看文件类型的三种方式:ls -l或ll、file命令、stat命令
-(f):普通文件
d:目录文件
b:块设备。就是一些存储数据,以提供系统存取的接口设备。例如一号硬盘的代码是/dev/hda1
c:字符设备。一些串行端口的接口设备,例如键盘、鼠标等!
l:符号链接文件,分为软链接和硬链接。类似于windows的快捷方式
p:管道文件,FIFO也是一种特殊的文件类型,它主要的目的是,解决多个程序同时存取一个文件所造成的的错误。
s:套接字文件,socket。如启动一个MySql服务器时产生的一个mysql.sock文件。

文件系统的类型
Linux系统支持大约100中分区类型的读取,但是只能对很少的一些进行创建和写操作。但是,可以挂载不同类型的文件系统在同一个root文件系统上,并且是很常见的。在这样的背景下,我们所说的文件系统一词是指在硬盘驱动器或逻辑卷上的一个分区中存储和管理用户数据所需要的结构和元数据。
Linux支持读取众多类型的分区系统的主要目的是为了提高兼容性,从而至少能够与其他计算机系统的文件系统进行交互。
常见文件系统类型:
ext2
ext3
ext4
fat
msdos
vfat
ntfs
...

挂载
在linux系统中使用mount命令进行文件系统的挂载。
一个挂载点简单的来说就是一个目录,就像任何其它目录一样,是作为root文件系统的一部分创建的。在Linux系统启动阶段的最初阶段,root文件系统就会被挂载到root目录下(/)。
使用 mount 命令可以把文件系统挂载到一个已有的目录/挂载点上。通常情况下,任何作为挂载点的目录都应该是空的且不包含任何其他文件。Linux 系统不会阻止用户挂载一个已被挂载了文件系统的目录或将文件系统挂载到一个包含文件的目录上。如果你将文件系统挂载到一个已有的目录或文件系统上,那么其原始内容将会被隐藏,只有新挂载的文件系统的内容是可见的。

这篇关于盈趣科技嵌入式面试总结的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!