FRRouting学习笔记(一)——进程架构

本文详细解析了开源路由套件FRRouting的进程架构,包括进程中的线程、任务调度模型,从Quagga时代的event-loop到引入POSIX接口后的兼容模式。特别介绍了threadmaster结构和事件模型的运作机制,以及内核线程的使用和与事件系统的交互。
摘要由CSDN通过智能技术生成
FRRouting学习笔记(一)——进程架构

编者按:本系列文章记录学习开源路由套件——FRRrouting 8.5.2的过程,包括原生文档翻译/解读、原代码分析等工作,欢迎感兴趣的小伙伴一起讨论。

        第一篇文章来自于Frrouting 的原生文档翻译,原文档位于源代码中的doc/developer下,名为process-architecture.rst。此文档对FRRouting中进程、线程、任务/事件等概念进行了阐述,并解释了frrouting中多种调度模型并存的历史发展沿革。简而言之,在Quagga时代,任务调度采用的是event-loop模式,属于自己写的任务调度模型。后续加入posix接口后,新增了pthread线程接口,目前是二者兼容的模式。因此frr里面最上层的线程结构叫做frr_pthread,可以兼容二者。

——————————分割线——————————

        FRRouting(以下简写为FRR)  是一组提供不同功能的守护进程。本文档描述了守护进程的内部架构,  重点介绍了它们的一般设计模式, 特别是在使用它们的守护进程中如何使用线程。

1. 概述

        FRR     守 护 进 程 中 使 用 的 基 本 模 式 是 “ 事 件 循 环 <https://en.wikipedia.org/wiki/Event_loop> ”_ 。一些守护进程使用` 内核线程 <https://en.wikipedia.org/wiki/Thread_(computing)#Kernel_threads>`_。在这些守护 进程中,每个内核线程运行自己的事件循环。事件循环实现被构造为线程安全的, 并允许其所属线程以外的线程在其上调度事件。本文档的其余部分详细描述了这 两种设计。

2. 术语

        因为本文档描述了内核线程的架构以及Event(事件)系统, 所以在这里离题一下术语。

        历史上, Quagga   的循环系统被视为用户空间线程的一种实现。由于这种设 计选择, 事件系统内的各种数据结构的名称都是术语“线程”的变体。在这个系 统中,保存事件循环状态的主要数据结构被称为“threadmaster”。在事件循环 中安排的事件——今天在 libevent 这样的系统中被称为“事件”或“任务”—— 被称为“线程”,它们的数据结构是“结构线程”。更让人困惑的是, 这些“线 程”有各种各样的类型,其中一种是“事件”。为了避免一些混淆,本文档将这 些“线程”称为“任务”,除了数据结构被明确命名的地方。当它们被明确命名 时, 它们将被格式化为``这样`` ,以区别于概念名称。当谈到内核线程时,使用的 术语将是“pthread”,因为 FRR 的内核线程实现使用 POSIX 线程 API。

3. 事件结构

        本节简要介绍当前在 FRR 中实现的事件模型。这个文档应该被扩展并分解成 它自己的部分。就目前而言, 它提供了理解事件系统和内核线程之间的相互作用 所需的基本信息。

        核 心 事 件 系 统 在 :file:  '   lib/thread.[ch]   '  中 实 现 。 主 要 结 构 是 `struct  thread_master``,以下简称为`threadmaster``。``threadmaster``是一个全局状态对 象或上下文,它保存了所有当前待执行的任务以及已经执行的任务的统计信息。 事件系统是通过将任务添加到这个数据结构, 然后调用一个函数来检索下一个要 执行的任务来驱动的。在初始化时,守护进程通常会创建一个`threadmaster`` , 添加一小部分初始任务,然后运行一个循环来获取每个任务并执行它。

        这些任务有不同的类型,与它们的一般操作相对应。类型由:file: ' thread.h ' 中的整数宏给出,它们是:

“THREAD_READ ' ':等待文件描述符准备读取,然后执行的任务。

“THREAD_WRITE ' ':等待文件描述符准备写入,然后执行的任务。

“THREAD_TIMER ' ':自从它被调度后,在一定时间后执行的任务。

“THREAD_EVENT ' ':以高优先级执行的通用任务,携带任意整数表示其处理程序的事件类型。 这些通常用于实现路由协议中典型的有限状态机。

“THREAD_READY ' ':用于内部就绪队列上的任务的类型。

“THREAD_UNUSED ' ':`struct thread`内部未使用的对象的类型。事件系统池化`struct  thread`以避 免堆分配;这是它们在池中的类型。

“THREAD_EXECUTE ' ':就在任务运行之前,它的类型被更改为 this。这是用来显示`X`作为输出的 类型:clicmd:`show thread cpu`。

        程序员永远不需要显式地使用这些类型。每一种类型的任务都是通过特定类 型的专用函数(实际上是宏, 但目前不相关)创建和排队的。例如, 要添加一个 THREAD_READ 任务,你可以调用::

Thread_add_read (struct thread_master *master, int (*handler)(struct thread *) , void *arg, int fd, struct thread  **ref);

        然后创建``struct thread``,并将其添加到``threadmaster``中适当的内部数据结 构中。请注意, ' ' READ ' '和' ' WRITE ' '任务是独立的——例如, ' '  READ ' '任务只 测试可读性。

3.1 事件循环

        要使用事件系统,在创建``threadmaster``之后,程序会添加一组初始任务。 随着这些任务的执行, 它们会添加更多在未来某个时间点执行的任务。这一系列 的任务驱动着程序的生命周期。当没有更多的任务可用时, 程序就会死亡。通常 在启动时添加的第一个任务是一个 I/O  任务为 VTYSH  以及任何网络套接字所需 的 peersions 或 IPC。

        为了获取下一个要运行的任务, 程序调用`thread_fetch()`。`thread_fetch()` 在 内部基于基本的优先级逻辑计算下 一 步要执行的任务。事件 ( 类型 '  'THREAD_EVENT ' ')以最高优先级执行, 其次是过期的计时器和最后的 I/O 任务(类 型' ' THREAD_READ ' ' 和' THREAD_WRITE ' ')。当调度一个任务时, 一个函数和一个 任意的参数被提供。从`thread_fetch()`返回的任务然后使用`thread_call()`执行。

下图展示了这个基础结构的简化版本。

使用单个 threadmaster 的程序的生命周期

        这一系列的“任务”框代表当前就绪的任务队列。其他类型的各种其他队列 没有显示。取-执行循环在底部展示。

将图中使用的通用名称映射到具体的 FRR 函数:

- ``task``就是``struct thread *``

- `fetch`是`thread_fetch()`

- `exec()``是`thread_call``

- `cancel()`是`thread_cancel()`

- `schedule()``是各种特定于任务的`thread_add_*` 函数

        添加任务是通过各种特定于任务的类似于函数的宏完成的。这些宏将底层函 数包装在:file:`thread.c`中, 以提供在编译时添加的额外信息,例如计划任务的行 号, 可以在运行时访问以进行调试, 记录和信息的目的。每个任务类型都有自己 特定的调度函数,遵循命名约定`thread_add_<type>``;详情见:file: ' thread.h '。

        这里有一些陷阱需要记住:

  1. - I/O 任务从与 I/O 操作相关的文件描述符中断开。这意味着对于任何给定的 文件描述符,每种类型的 I/O 任务(' ' THREAD_READ ' ' 和' ' THREAD_WRITE ' ')只能调度一个。例如,一个接一个地调度两个写任务, 会用第二个任务覆盖第一个任 务,导致第一个任务的全部丢失和困难的 bug。
  1. -计时器任务的精度仅与底层操作系统提供的单调时钟相同。
  2. -调度调用中传递的任意处理程序参数的内存管理是调用者的责任。

4.  内核线程体系结构

        已经开始在 FRR 中引入内核线程以提高性能和稳定性。自然, 内核线程架构 一直被视为与事件驱动架构正交, 两者在设计选择方面确实有显著的重叠。由于 事件模型被紧密地集成到 FRR  中,因此对如何引入 pthread、它们扮演什么角色 以及它们如何与事件模型互操作进行了仔细的考虑。

4.1 设计概述

        每个内核线程都表现为 FRR 中的轻量级进程, 共享相同的进程内存空间。另 一方面, 事件系统被设计为在单个进程中运行,并驱动一组任务的串行执行。考 虑到这一点, 自然的选择是在每个内核线程内实现事件系统。这允许我们利用事 件驱动的执行模型与当前存在的任务和上下文原语。通过这种方式, 熟悉的 FRR 执行模型获得了同时执行任务的能力,同时保留了现有的并发模型。

下图说明了具有多个 pthread 的体系结构,每个 pthread 都运行自己的基于 `threadmaster`的事件循环。

使用多个 pthreads 的程序生命周期,每个线程运行自己的`threadmaster` 。

        每个 roundrect 代表一个单独的pthread,运行下面描述的同一个事件循环:ref: ' event-architecture '。注意从右边的``exec()``框到中间 pthread ``schedule()``框的箭 头。这说明了在一个 pthread   中运行的将任务调度到另一个 pthread   的 threadmaster  上的代码。每个`threadmaster``都有一个全局锁来同步这些操作。 pthread 名称是例子。

4.2 内核线程包装器

        pthreads   和事件系统集成的基础是两个系统的轻量级包装器 :file: ' lib/frr_pthread.[ch] ' 。头文件提供了一个核心数据结构' ' struct frr_pthread ' ' ,它 封装了来自 POSIX 线程和:file: ' thread.[ch] '的结构。特别是,这个数据结构有一 个指向运行在 pthread 中的`threadmaster``的指针。它还具有用于名称的字段以及具有类似于' ' pthread_create() ' ' 的 POSIX 参数 的启动和停止函数。

        调用`frr_pthread_new()``创建并注册一个新的`frr_pthread``。返回的结构有一 个预先初始化的``threadmaster``,它的``start``和``stop``函数被初始化为默认值, 这会在给定的 threadmaster 上运行一个基本的事件循环。调用`frr_pthread_run``  会通过`start`函数启动线程。从那里开始,模型与常规事件模型相同。要在特定 的 pthread 上调度任务,只需像往常一样使用常规的:file:`thread.c`函数,并提供 从`frr_pthread`指向的`threadmaster`。作为实现包装器的一部分, :file:`thread.c`函 数是线程安全的。因此, 在`threadmaster`上调度事件是安全的, 它既属于调用线 程也属于* *任何其他 pthread* *。这作为线程间通信的基础, 并归结为一种稍微 复杂的消息传递方法, 其中消息是事件驱动模型中使用的常规任务事件。唯一的 区别是线程取消,它需要调用``thread_cancel_async()``而不是``thread_cancel`` 来 取消当前在属于不同 pthread 的``threadmaster``上调度的任务。

        这是必要的, 以避免特定情况下的竞争条件, 其中一个 pthread 想要保证另 一个 pthread 上的任务在继续之前被取消。

        此外,现有的用于显示事件驱动模型内任务的统计信息和其他信息的命令已 经被扩展为处理多个 pthread;Running:clicmd:  ' show thread  cpu '将显示通常的事 件分解,但它会对程序中运行的每个 pthread 进行显示。例如, :ref:  ' bgpd '运行 一个专用的 I/O pthread,并显示以下输出:clicmd: ' show thread cpu ':

        细心的读者会注意到还有第三个线程, keepalive 线程。这个帖子负责——惊  喜——为同行生成 keep - alive 消息。然而,没有统计数据显示这个帖子。虽然  pthread 使用``frr_pthread``包装器,但它选择不使用嵌入式的``threadmaster``设施。 相反,它用自定义函数替换``start``和``stop``函数。这样做是因为相对于 pthread     的任务, ``threadmaster`` 工具引入了少量但重要的开销。在这种情况下,由于  pthread 不需要事件驱动模型,也不需要从其他 pthread 接收任务,因此在提供   的事件设施之外实现它会更简单、更高效。从这个例子中得到的要点是, 虽然在  FRR  中方便使用 pthread 的工具已经实现,但是包装器是灵活的,允许使用其他  模型,同时仍然与 FRR 核心基础结构的其余部分集成。启动和停止这个 pthread     的工作原理与其他任何``frr_pthread``相同;唯一的区别是不会为它收集事件统计  信息,因为没有事件。

5. 关于设计和文档的注意事项

        由于选择将现有的事件系统嵌入到 FRR 中的每个 pthread 中, 因此此时没有 对其他 pthread 使用模型(如分治法)的集成支持。同样,也没有显式支持线程池化或类似的更高层次的构造。目前现有的基础设施是围绕着在每个守护进程中负 责特定任务的长时间运行的工作线程的概念设计的。这并不是说分而治之、线程 池化等未来就不能实现。但是, 在这个方向上的设计, 必须非常小心地考虑到现 有的代码库。在假定单线程执行的情况下编写的程序中引入内核线程必须非常小 心,以避免潜在的错误,并确保程序保持可理解和可维护。

        为了实现这些目标,未来关于内核线程的工作应该在这里广泛地记录下来, FRR 开发人员应该非常小心他们的设计选择, 因为紧密集成的糟糕选择可能会对 未来的开发工作造成灾难性的影响。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值