首页 文章

进程和线程有什么区别?

提问于
浏览
1316

进程和线程之间的技术差异是什么?

我感觉像'process'这样的词被过度使用,还有硬件和软件线程 . 如Erlang这样的语言轻量级进程怎么样?是否有明确的理由使用一个术语而不是另一个术语?

30 回答

  • 5

    Process :正在执行的程序称为进程

    Thread :线程是一个基于"one with other"的概念与程序的其他部分一起执行的功能,因此线程是进程的一部分 .

  • 2

    我到目前为止看到的最好的短定义来自Michael Kerrisk的'The Linux Programming Interface':

    在现代UNIX实现中,每个进程可以有多个执行线程 . 设想线程的一种方法是作为一组共享相同虚拟内存的进程,以及一系列其他属性 . 每个线程执行相同的程序代码并共享相同的数据区和堆 . 但是,每个线程都有自己的堆栈,包含局部变量和函数调用链接信息 . [LPI 2.12]

  • 1

    http://lkml.iu.edu/hypermail/linux/kernel/9608/0191.html

    Linus Torvalds(torvalds@cs.helsinki.fi)星期二,1996年8月6日12:47:31 0300(EET DST)消息排序:[日期] [主题] [主题] [作者]下一条消息:Bernd P. Ziller :“Re:在get_hash_table中哎呀”上一条消息:Linus Torvalds:“Re:I / O请求订购”1996年8月5日星期一,Peter P. Eiserloh写道:我们需要明确线程的概念 . 太多人似乎将一个线程与一个进程混淆了 . 以下讨论并未反映Linux的当前状态,而是试图保持高级别的讨论 . 没有!没有理由认为“线程”和“进程”是独立的实体 . 这就是它传统上的做法,但我个人认为这样做是一个很大的错误 . 这种方式的唯一理由是历史包袱 . 线程和进程实际上只是一件事:“执行的上下文” . 试图人为地区分不同的案例只是自我限制 . “执行的背景”,在此称为COE,只是该COE所有州的集团 . 该状态包括CPU状态(寄存器等),MMU状态(页面映射),权限状态(uid,gid)和各种“通信状态”(打开文件,信号处理程序等) . 传统上,“线程”和“进程”之间的区别主要在于线程具有CPU状态(可能是其他一些最小状态),而所有其他上下文都来自该进程 . 然而,这只是分割COE总状态的一种方式,没有任何东西可以说它是正确的方法 . 将自己限制在那种形象上只是简单的愚蠢 . Linux对此的思考方式(以及我希望工作的方式)是没有“进程”或“线程”这样的东西 . 只有COE的全部(Linux称为“任务”) . 不同的COE可以彼此共享其上下文的一部分,并且该共享的一个子集是传统的“线程”/“进程”设置,但这应该被视为仅仅是一个子集(它是一个重要的子集,但是这种重要性来自不是来自设计,而是来自标准:我们绝对希望在Linux之上运行符合标准的线程程序 . 简而言之:不要围绕线程/流程思维方式进行设计 . 内核应该围绕COE思维方式设计,然后pthreads库可以将有限的pthreads接口导出到想要使用这种方式查看COE的用户 . 正如您认为COE与线程/进程相对的可能性的示例:您可以执行外部“cd”程序,这在UNIX和/或进程/线程中是传统上不可能的(愚蠢的例子,但想法是您可以拥有这些不仅限于传统UNIX /线程设置的“模块” . 做一个:克隆(CLONE_VM | CLONE_FS); child:execve(“external-cd”); / “execve()”将取消关联VM,因此我们使用CLONE_VM的唯一原因是使克隆行为更快 /您可以自然地执行“vfork()”(它需要最小的内核支持,但是这种支持适合CUA的思维方式完美):克隆(CLONE_VM);孩子:继续跑,最后execve()妈妈:等待execve你可以做外部“IO deamons”:克隆(CLONE_FILES); child:打开文件描述符等妈妈:使用fd的子打开和vv . 所有上述工作都是因为你没有依赖于线程/进程的思维方式 . 以Web服务器为例,其中CGI脚本是作为“执行线程”完成的 . 你不能用传统的线程做到这一点,因为传统的线程总是必须共享整个地址空间,所以你必须链接你想要在Web服务器本身做的所有事情(一个“线程”无法运行)另一个可执行文件将此视为“执行上下文”问题,您的任务现在可以选择执行外部程序(=将地址空间与父项分开)等,或者他们可以例如与父项共享所有内容除外文件描述符(这样子“线程”可以打开大量文件,父母不需要担心它们:当子“线程”退出时它们自动关闭,并且它不会在父项中使用fd) . 例如,可以考虑使用线程“inetd” . 你想要低开销的fork exec,所以用Linux的方式你可以代替使用“fork()”来编写一个多线程的inetd,其中每个线程只用CLONE_VM创建(共享地址空间,但不共享文件描述符)等等) . 然后孩子可以执行,如果它是一个外部服务(例如rlogind),或者它可能是内部inetd服务之一(echo,timeofday),在这种情况下它只是做它的事情并退出 . 你不能用“线程”/“进程”来做到这一点 . 莱纳斯

  • 4

    进程是代码,内存,数据和其他资源的集合 . 线程是在进程范围内执行的一系列代码 . 您(通常)可以在同一进程中同时执行多个线程 .

  • 2

    进程是应用程序的执行实例 . 那是什么意思?嗯,例如,当您双击Microsoft Word图标时,您启动一个运行Word的进程 . 线程是进程内的执行路径 . 此外,进程可以包含多个线程 . 当您启动Word时,操作系统会创建一个进程并开始执行该进程的主线程 .

    重要的是要注意线程可以执行进程可以执行的任何操作 . 但由于进程可以包含多个线程,因此可以将线程视为“轻量级”进程 . 因此,线程和进程之间的本质区别是每个人用来完成的工作 . 线程用于小任务,而进程用于更“重量级”的任务 - 基本上是应用程序的执行 .

    线程和进程之间的另一个区别是同一进程中的线程共享相同的地址空间,而不同的进程则不共享 . 这允许线程读取和写入相同的数据结构和变量,并且还促进线程之间的通信 . 进程之间的通信(也称为IPC或进程间通信)非常困难且资源密集 .

  • 6

    首先,让我们看一下理论方面 . 您需要了解流程在概念上是什么,以理解流程和线程之间的差异以及它们之间共享的内容 .

    我们有以下内容来自Tanenbaum的第9.2.2节Modern Operating Systems 3e中的经典线程模型:

    流程模型基于两个独立的概念:资源分组和执行 . 有时将它们分开是有用的;这是线程进来的地方......

    他继续:

    查看流程的一种方法是将相关资源组合在一起 . 进程具有包含程序文本和数据以及其他资源的地址空间 . 这些资源可能包括打开文件,子进程,待处理警报,信号处理程序,记帐信息等 . 通过以流程的形式将它们组合在一起,可以更轻松地管理它们 . 进程的另一个概念是执行线程,通常简称为线程 . 该线程有一个程序计数器,用于跟踪下一个要执行的指令 . 它有寄存器,它保存当前的工作变量 . 它有一个堆栈,其中包含执行历史记录,每个过程调用一个帧但尚未返回 . 虽然线程必须在某个进程中执行,但线程及其进程是不同的概念,可以单独处理 . 流程用于将资源组合在一起;线程是计划在CPU上执行的实体 .

    再向下,他提供了下表:

    Per process items             | Per thread items
    ------------------------------|-----------------
    Address space                 | Program counter
    Global variables              | Registers
    Open files                    | Stack
    Child processes               | State
    Pending alarms                |
    Signals and signal handlers   |
    Accounting information        |
    

    我们来处理hardware multithreading问题 . 传统上,CPU会支持单个执行线程,维护线程's state via a single program counter, and set of registers. But what happens if there'是一个缓存未命中?从主内存获取数据需要很长时间,而在发生这种情况时,CPU只是闲置在那里 . 所以有人有想法基本上有两组线程状态(PC寄存器),以便另一个线程(可能在同一个进程中,可能在不同的进程中)可以在另一个线程在主内存上等待时完成工作 . 这个概念有多个名称和实现,例如HyperThreading和Simultaneous Multithreading(简称SMT) .

    现在让我们来看看软件方面 . 在软件方面,基本上有三种方法可以实现线程 .

    • 用户空间线程

    • 内核线程

    • 两者的结合

    实现线程所需要的只是能够保存CPU状态并维护多个堆栈,这在很多情况下可以在用户空间中完成 . 用户空间线程的优点是超快速的线程切换,因为您不必陷入内核并且能够以您喜欢的方式调度线程 . 最大的缺点是无法阻塞I / O(这将阻止整个过程和所有它的用户线程),这是我们首先使用线程的一个重要原因 . 在许多情况下,使用线程阻止I / O大大简化了程序设计 .

    除了将所有调度问题留给操作系统之外,内核线程还具有能够使用阻塞I / O的优点 . 但是每个线程切换都需要陷入内核,这可能相对较慢 . 但是,如果由于I / O阻塞而切换线程,这实际上不是问题,因为I / O操作可能已经将您困在内核中了 .

    另一种方法是将两者结合起来,多个内核线程各自具有多个用户线程 .

    回到你的术语问题,你可以看到一个过程和一个执行的线程是两个不同的概念,你选择使用哪个术语取决于你所谈论的内容 . 关于“轻量级过程”这个术语,我个人没有看到它的重点,因为它并没有真正传达正在发生的事情以及“执行线程”这个术语 .

  • 5
    • 基本上,线程是进程的一部分,没有进程线程无法工作 .

    • 一个线程是轻量级的,而这个过程是重量级的 .
      进程之间的

    • 通信需要一些时间,而线程需要更少的时间 .

    • 线程可以共享相同的内存区域,而进程可以分开存在 .

  • 17
    • 线程在共享内存空间中运行,但进程在单独的内存空间中运行

    • 一个线程是一个轻量级的过程,但一个过程是一个重量级的过程 .

    • 线程是进程的子类型 .

  • 665

    试图从Linux内核的操作系统视图中回答它

    程序在启动到内存时成为一个过程 . 进程有自己的地址空间,意味着在内存中有各种段,例如用于存储已编译代码的.text段,用于存储未初始化的静态或全局变量的.bss等 . 每个进程都有自己的程序计数器和用户spcae堆栈 . 在内核中,每个进程都有自己的内核堆栈(为了安全问题而与用户空间堆栈分开)和一个名为 task_struct 的结构,它通常被抽象为进程控制块,存储有关进程的所有信息,例如优先级,国家,(以及其他一大块) . 进程可以有多个执行线程 .

    进入线程,它们驻留在进程内并共享父进程的地址空间以及可在线程创建期间传递的其他资源,例如文件系统资源,共享挂起信号,共享数据(变量和指令),从而使线程轻量级因此允许更快的上下文切换在内核中,每个线程都有自己的内核堆栈以及定义线程的 task_struct 结构 . 因此,内核查看与不同实体相同的进程的线程,并且可以自行调度 . 同一进程中的线程共享一个称为线程组ID( tgid )的公共ID,它们也有一个唯一的id,称为进程ID( pid ) .

  • 9

    线程和进程都是OS资源分配的原子单元(即,存在描述CPU时间如何在它们之间划分的并发模型,以及拥有其他OS资源的模型) . 区别在于:

    • 共享资源(线程按定义共享内存,它们除了堆栈和局部变量之外没有任何其他内容;进程也可以共享内存,但是有一个单独的机制,由OS维护)

    • 分配空间(进程的内核空间与线程的用户空间)

    上面的Greg Hewgill关于"process"这个词的Erlang含义是正确的,并且here讨论了为什么Erlang可以做轻量级处理 .

  • 3

    Real world example for Process and Thread 这将为您提供有关线程和进程的基本概念

    我从Scott Langham的答案中借用了上述信息 - 谢谢

  • 18

    试图回答有关Java世界的这个问题 .

    进程是程序的执行,但是线程是进程内的单个执行序列 . 进程可以包含多个线程 . 线程有时称为 lightweight process .

    例如:

    示例1:JVM在单个进程中运行,JVM中的线程共享属于该进程的堆 . 这就是几个线程可以访问同一个对象的原因 . 线程共享堆并拥有自己的堆栈空间 . 这是一个线程调用方法及其局部变量的方式与其他线程保持线程安全的方式 . 但是堆不是线程安全的,必须同步才能保证线程安全 .

    示例2:程序可能无法通过读取击键来绘制图片 . 该程序必须充分注意键盘输入,并且缺乏一次处理多个事件的能力将导致麻烦 . 解决此问题的理想方法是同时无缝地执行程序的两个或多个部分 . 线程允许我们这样做 . 这里绘制图片是一个过程,阅读按键是子过程(线程) .

  • 17

    从访问者的角度来看,基本上只有3个主要内容我想听,除了明显的事情,比如一个进程可以有多个线程:

    • 线程共享相同的内存空间,这意味着线程可以从其他线程内存访问内存 . 流程通常不能 .

    • 资源 . 资源(内存,句柄,套接字等)在进程终止时释放,而不是线程终止 .

    • 安全 . 进程具有固定的安全令牌 . 另一方面,线程可以冒充不同的用户/令牌 .

    如果你想要更多,Scott Langham的回应几乎涵盖了一切 . 所有这些都是从操作系统的角度出发的 . 不同语言可以实现不同的概念,像任务,光线等等,但它们只是使用线程(Windows上的光纤)的方式 . 没有硬件和软件线程 . 有硬件和软件 exceptionsinterrupts ,或用户模式和内核 threads .

  • 24

    Difference between Thread and Process?

    进程是应用程序的执行实例,而线程是进程内的执行路径 . 此外,一个进程可以包含多个线程 . 重要的是要注意一个线程可以执行进程可以执行的任何操作 . 但由于进程可以包含多个线程,因此可以将线程视为“轻量级”进程 . 因此,线程和进程之间的本质区别是每个人用来完成的工作 . 线程用于小任务,而进程用于更“重量级”的任务 - 基本上是应用程序的执行 .

    线程和进程之间的另一个区别是同一进程中的线程共享相同的地址空间,而不同的进程则不共享 . 这允许线程读取和写入相同的数据结构和变量,并且还促进线程之间的通信 . 进程之间的通信(也称为IPC或进程间通信)非常困难且资源密集 .

    以下是线程和进程之间差异的摘要:

    • 线程比进程更容易创建,因为它们不需要单独的地址空间 .

    • 多线程需要仔细编程,因为线程共享数据结构,一次只能由一个线程修改 . 与线程不同,进程不共享相同的地址空间 .

    • 线程被认为是轻量级的,因为它们使用的资源远少于进程 .

    • 进程彼此独立 . 线程,因为它们共享相同的地址空间是相互依赖的,所以必须小心谨慎,以便不同的线程不会相互踩踏 .
      这是另一种说明#2的方式 .

    • 进程可以包含多个线程 .

  • 99

    Process:

    • 程序的执行实例称为进程 .

    • 某些操作系统使用术语“任务”来指代正在执行的程序 .

    • 进程始终存储在主存储器中,也称为主存储器或随机存取存储器 .

    • 因此,一个过程被称为活动实体 . 如果重新启动机器,它会消失 .

    • 多个进程可能与同一程序相关联 .

    • 在多处理器系统上,可以并行执行多个进程 .

    • 在单处理器系统上,虽然没有实现真正的并行性,但是应用了进程调度算法,并且处理器被安排为一次执行一个进程,产生并发错觉 .

    • Example: 执行'Calculator'程序的多个实例 . 每个实例都被称为一个过程 .

    Thread:

    • 线程是进程的子集 .

    • 它被称为“轻量级进程”,因为它类似于实际进程,但在进程的上下文中执行,并共享内核分配给进程的相同资源 .

    • 通常,进程只有一个控制线程 - 一次执行一组机器指令 .

    • 进程也可以由同时执行指令的多个执行线程组成 .

    • 多个控制线程可以利用多处理器系统上可能的真正并行性 .

    • 在单处理器系统上,应用线程调度算法,并且计划处理器一次运行一个线程 .

    • 进程内运行的所有线程共享相同的地址空间,文件描述符,堆栈和其他与进程相关的属性 .

    • 由于进程的线程共享相同的内存,因此在进程内同步对共享数据的访问会获得前所未有的重要性 .

    我从Knowledge Quest! blog借了上述信息 .

  • 44
    • 每个进程都是一个线程(主线程) .

    • 但每个线程都不是进程 . 它是流程的一部分(实体) .

  • 243

    在使用包含多线程的Python(解释语言)构建算法时,我惊讶地发现,与我之前构建的顺序算法相比,执行时间并没有更好 . 为了理解这个结果的原因,我做了一些阅读,并相信我学到的东西提供了一个有趣的背景,从中可以更好地理解多线程和多进程之间的差异 .

    多核系统可以运行多个执行线程,因此Python应该支持多线程 . 但Python不是一种编译语言,而是一种解释型语言1 . 这意味着必须解释程序才能运行,并且解释程序在开始执行之前不知道程序 . 然而,它所知道的是Python的规则,然后它动态地应用这些规则 . 然后,Python中的优化必须主要是解释器本身的优化,而不是要运行的代码 . 这与诸如C之类的编译语言形成对比,并且对Python中的多线程产生影响 . 具体来说,Python使用Global Interpreter Lock来管理多线程 .

    另一方面,编译语言已被编译 . 该程序“完全”处理,首先根据其语法定义进行解释,然后映射到语言不可知的中间表示,最后链接到可执行代码 . 此过程允许对代码进行高度优化,因为它在编译时全部可用 . 在创建可执行文件时定义各种程序交互和关系,并且可以做出关于优化的稳健决策 .

    在现代环境中,Python的解释器必须允许多线程,这必须既安全又有效 . 这就是解释语言与编译语言之间的区别 . 解释器不得干扰来自不同线程的内部共享数据,同时优化处理器的使用以进行计算 .

    正如之前的帖子中所指出的,进程和线程都是独立的顺序执行,主要区别在于内存在进程的多个线程之间共享,而进程隔离了它们的内存空间 .

    在Python中,全局解释器锁保护数据免受不同线程的同时访问 . 它要求在任何Python程序中只能随时执行一个线程 . 另一方面,可以运行多个进程,因为每个进程的内存与任何其他进程隔离,并且进程可以在多个核上运行 .


    1 Donald Knuth对“计算机编程艺术:基本算法”中的解释程序有很好的解释 .

  • 12

    它们几乎一样......但关键的区别在于线程是轻量级的,而且是一个进程在上下文切换,工作负载等方面的重量级 .

  • 9

    解释有关并发编程的更多信息

    • 进程具有自包含的执行环境 . 进程通常具有完整的私有基本运行时资源集;特别是,每个进程都有自己的内存空间 .

    • 线程存在于进程中 - 每个进程至少有一个进程 . 线程共享进程的资源,包括内存和打开文件 . 这使得有效但可能有问题的通信成为可能 .

    记住普通人,

    在您的计算机上,打开Microsoft Word和Web浏览器 . 我们称这两个过程 .

    在Microsoft Word中,您键入一些内容并自动保存 . 现在,你会发现编辑和保存并行发生 - 在一个线程上编辑并保存在另一个线程上 .

  • 1

    对于那些通过可视化学习更加舒适的人来说,这是我为解释过程和线程而创建的一个方便的图表 .
    我使用了MSDN的信息 - About Processes and Threads

  • 4

    将流程视为所有权单位或任务所需的资源 . 进程可以拥有内存空间,特定输入/输出,特定文件和优先级等资源 .

    线程是可分派的执行单元,或者简单地说是通过一系列指令的进展

  • 22

    应用程序由一个或多个进程组成 . 最简单的过程是一个执行程序 . 一个或多个线程在进程的上下文中运行 . 线程是操作系统分配处理器时间的基本单元 . 线程可以执行进程代码的任何部分,包括当前由另一个线程执行的部分 . 光纤是执行单元,必须由应用程序手动调度 . 光纤在调度它们的线程的上下文中运行 .

    被盗here .

  • 10

    以下是我从The Code Project上的一篇文章中得到的内容 . 我想这清楚地解释了所需的一切 .

    线程是将工作负载拆分为单独的执行流的另一种机制 . 线程比进程重量轻 . 这意味着,它提供的灵活性低于完整的流程,但可以更快地启动,因为操作系统的设置较少 . 当程序由两个或多个线程组成时,所有线程共享一个内存空间 . 进程被赋予单独的地址空间 . 所有线程共享一个堆 . 但每个线程都有自己的堆栈 .

  • 7

    同一进程中的线程共享内存,但每个线程都有自己的堆栈和寄存器,线程在堆中存储特定于线程的数据 . 线程永远不会独立执行,因此与进程间通信相比,线程间通信要快得多 .

    进程永远不会共享相同的内存 . 子进程创建时,它会复制父进程的内存位置 . 通过使用管道,共享内存和消息解析来完成进程通信 . 线程之间的上下文切换非常慢 .

  • 6

    示例1:JVM在单个进程中运行,JVM中的线程共享属于该进程的堆 . 这就是几个线程可以访问同一个对象的原因 . 线程共享堆并拥有自己的堆栈空间 . 这是一个线程调用方法及其局部变量的方式与其他线程保持线程安全的方式 . 但是堆不是线程安全的,必须同步才能保证线程安全 .

  • 7

    Process
    每个进程都提供执行程序所需的资源 . 进程具有虚拟地址空间,可执行代码,系统对象的打开句柄,安全上下文,唯一进程标识符,环境变量,优先级类,最小和最大工作集大小以及至少一个执行线程 . 每个进程都使用单个线程启动,通常称为主线程,但可以从其任何线程创建其他线程 .

    Thread
    线程是进程中可以调度执行的实体 . 进程的所有线程共享其虚拟地址空间和系统资源 . 此外,每个线程都维护异常处理程序,调度优先级,线程本地存储,唯一线程标识符以及系统将用于保存线程上下文直到调度的一组结构 . 线程上下文包括线程's set of machine registers, the kernel stack, a thread environment block, and a user stack in the address space of the thread'进程 . 线程也可以有自己的安全上下文,可用于模拟客户端 .


    在MSDN上找到这个:
    About Processes and Threads

    Microsoft Windows支持抢占式多任务处理,它可以创建从多个进程同时执行多个线程的效果 . 在多处理器计算机上,系统可以同时执行与计算机上的处理器一样多的线程 .

  • 5

    进程和线程都是独立的执行序列 . 典型的区别是(同一进程的)线程在共享内存中运行空间,而进程在不同的内存空间中运行 .

    我不确定你可能指的是什么“硬件”与“软件”线程 . 线程是一种操作环境功能,而不是CPU功能(尽管CPU通常具有使线程高效的操作) .

    Erlang使用术语“进程”,因为它不公开共享内存多道程序设计模型 . 将它们称为“线程”意味着它们共享内存 .

  • 4

    进程和线程都是独立的执行序列 . 典型的区别在于(同一进程的)线程在共享内存空间中运行,而进程在不同的内存空间中运行 .

    处理

    是一个正在执行的程序 . 它有文本部分,即程序代码,当前活动由程序计数器的值和处理器寄存器的内容表示 . 它还包括包含临时数据(例如函数参数,返回地址和局部变量)的进程堆栈,以及包含全局变量的数据部分 . 进程还可以包括堆,该堆是在进程运行时期间动态分配的内存 .

    线

    线程是CPU利用率的基本单位;它包括线程ID,程序计数器,寄存器组和堆栈 . 它与属于同一进程的其他线程共享其代码段,数据段和其他操作系统资源,如打开文件和信号 .

    • 来自Galvin的操作系统
  • 66

    来自嵌入式世界,我想补充说,进程的概念只存在于具有MMU(内存管理单元)的"big"处理器(桌面CPU,ARM Cortex A-9)和支持使用MMU的操作系统(例如Linux的) . 对于小/旧处理器和微控制器以及小型RTOS操作系统(实时操作系统),例如freeRTOS,没有MMU支持,因此没有进程而只有线程 .

    Threads 可以访问彼此的内存,它们是由OS以交错方式调度,因此它们似乎并行运行(或者与多核并行运行) .

    另一方面, Processes 住在由MMU提供和保护的虚拟内存私人沙箱中 . 这很方便,因为它可以:

    • 防止错误进程导致整个系统崩溃 .

    • 通过使其他进程数据不可见且无法访问来维护安全性 . 过程中的实际工作由一个或多个线程处理 .

  • 1189

    Process:

    • 过程是一个沉重的过程 .

    • Process是一个单独的程序,具有独立的内存,数据,资源等 .

    • 使用fork()方法创建进程 .

    • 在进程之间切换上下文非常耗时 .

    例:
    比如说,打开任何浏览器(mozilla,Chrome,IE) . 此时,新进程将开始执行 .

    Threads:

    • 线程是轻量级进程 . 线程是在进程内捆绑的 .

    • 线程有共享内存,数据,资源,文件等 .

    • 使用clone()方法创建线程 .

    • 线程之间的上下文切换不像Process那么耗时 .

    例:
    在浏览器中打开多个选项卡 .

相关问题