当前位置: 首页 > news >正文

内核内存回收关键隐藏变量之page引用计数

在分析内核内存回收源码时,page引用计数并不显眼,但是page引用计数对page的内存回收至关重要。本文基于linux-4.18.0-240版本内核源码,总结下文件页page的引用计数的相关细节。首先是get_page()和put_page()函数,分别令page引用计数加1和减1.

  1. //page引用计数加1
  2. static inline void get_page(struct page *page)
  3. {
  4.     page_ref_inc(page);
  5. }
  6. //page引用计数减1
  7. static inline void put_page(struct page *page)
  8. {
  9.     if (put_page_testzero(page))
  10.         __put_page(page);
  11. }

以read系统调用读文件为例,最后执行到generic_file_buffered_read函数,先page_cache_alloc()分配一个文件页page,此时的page引用计数是0。

  1. static ssize_t generic_file_buffered_read(struct kiocb *iocb,
  2.         struct iov_iter *iter, ssize_t written)
  3. {
  4.     page = page_cache_alloc(mapping);
  5.     error = add_to_page_cache_lru(page, mapping, index,mapping_gfp_constraint(mapping, GFP_KERNEL));
  6. }

然后执行add_to_page_cache_lru函数把page添加到radix/xrray tree,接着把page添加到lru缓存和lru链表

  1. int add_to_page_cache_lru(struct page *page, struct address_space *mapping,
  2.                 pgoff_t offset, gfp_t gfp_mask)
  3. {
  4.     //page添加到radix/xrray tree时令page引用计数加1
  5.     ret = __add_to_page_cache_locked(page, mapping, offset,gfp_mask, &shadow);
  6.     //page添加到lru缓存时令page引用计数加1,把pagelru缓存移动到lru链表时再令page引用计数减1
  7.     lru_cache_add(page);
  8. }

在把page添加到radix/xrray tree时令page引用计数加1

  1. static int __add_to_page_cache_locked(struct page *page,
  2.                       struct address_space *mapping,
  3.                       pgoff_t offset, gfp_t gfp_mask,
  4.                       void **shadowp)
  5. {
  6.     XA_STATE(xas, &mapping->i_pages, offset);
  7.     .........
  8.     //page引用计数加1
  9.     get_page(page);
  10.     page->mapping = mapping;
  11.     page->index = offset;
  12.     .........
  13.     old = xas_load(&xas);
  14.     xas_store(&xas, page);
  15.     mapping->nrpages++;
  16.     .........
  17. }

然后执行lru_cache_add函数page添加到lru缓存时令page引用计数加1pagelru缓存移动到lru链表时再令page引用计数减1,函数流程是lru_cache_add->__lru_cache_add->__pagevec_lru_add->release_pages,关键函数如下:

把page添加到lru缓存时令page引用计数加1

  1. static void __lru_cache_add(struct page *page)
  2. {
  3.     struct pagevec *pvec = &get_cpu_var(lru_add_pvec);
  4.     //page引用计数加1
  5.     get_page(page);
  6.     if (!pagevec_add(pvec, page) || PageCompound(page))
  7.         __pagevec_lru_add(pvec);
  8.     put_cpu_var(lru_add_pvec);
  9. }
  10. //page添加到lru缓存
  11. static inline unsigned pagevec_add(struct pagevec *pvec, struct page *page)
  12. {
  13.     pvec->pages[pvec->nr++] = page;
  14.     return pagevec_space(pvec);
  15. }

接着把page添加到lru链表时令page引用计数减1

  1. void __pagevec_lru_add(struct pagevec *pvec)
  2. {
  3.     pagevec_lru_move_fn(pvec, __pagevec_lru_add_fn, NULL);
  4. }
  5. static void __pagevec_lru_add_fn(struct page *page, struct lruvec *lruvec,
  6.                  void *arg)
  7. {
  8.     SetPageLRU(page);
  9.     //page添加到lru链表
  10.     add_page_to_lru_list(page, lruvec, lru);
  11. }
  12. void release_pages(struct page **pages, int nr)
  13. {
  14.     for (i = 0; i < nr; i++) {
  15.         struct page *page = pages[i];
  16.         //page引用计数减1。如果之后page引用计数是0说明没有进程使用该page了,然后执行free_unref_page_list()page释放回伙伴系统。
  17.         if (!put_page_testzero(page))
  18.             continue;
  19.         .........  
  20.         list_add(&page->lru, &pages_to_free);
  21.     }
  22.     free_unref_page_list(&pages_to_free);
  23. }

如果是write系统调用对文件页page有写操作,则还要为page分配buffer_head(即bh),然后建立文件页page和bh的联系,令page引用计数加1。源码流程如下(以ext4文件系统为例):vfs_write->new_sync_write->ext4_file_write_iter->__generic_file_write_iter->generic_perform_write->ext4_da_write_begin->__block_write_begin_int->create_page_buffers->create_empty_buffers->attach_page_buffers,

  1. static inline void attach_page_buffers(struct page *page,
  2.         struct buffer_head *head)
  3. {
  4.     //page引用计数加1
  5.     get_page(page);
  6.     //标记pageprivate属性
  7.     SetPagePrivate(page);
  8.     //建立pagebh的联系,本质是page->private=bh
  9.     set_page_private(page, (unsigned long)head);
  10. }

OK,此时page的引用计数是2。接着来到page的内存回收,执行shrink_inactive_list()函数,这里把该函数的关键源码列下:

  1. static unsigned long shrink_inactive_list(unsigned long nr_to_scan, struct lruvec *lruvec,
  2.              struct scan_control *sc, enum lru_list lru)
  3. {
  4.     spin_lock_irq(&pgdat->lru_lock);
  5.     //根据nr_to_scan数目从inactive lru链表隔离page符合条件的pagepage_list链表,同时都令这些page的引用计数加1
  6.     nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &page_list,&nr_scanned, sc, isolate_mode, lru);
  7.     spin_unlock_irq(&pgdat->lru_lock);
  8.     ..........
  9.     nr_reclaimed = shrink_page_list(&page_list, pgdat, sc, 0,&stat, false);
  10.     ..........
  11.     spin_lock_irq(&pgdat->lru_lock);
  12.     //没有成功内存回收的page再移动回 active/inactive lru链表,page引用计数减1。如果page引用计数是0说明没人用了,再移动回page_list
  13.     putback_inactive_pages(lruvec, &page_list);
  14.     spin_unlock_irq(&pgdat->lru_lock);
  15.     ..........
  16.     //释放page_list上引用计数是0page
  17.     free_unref_page_list(&page_list);
  18. }

看下隔离page执行的isolate_lru_pages函数,主要是page引用计数加1

  1. static unsigned long isolate_lru_pages(unsigned long nr_to_scan,
  2.         struct lruvec *lruvec, struct list_head *dst,
  3.         unsigned long *nr_scanned, struct scan_control *sc,
  4.         isolate_mode_t mode, enum lru_list lru)
  5. {
  6.         //page符合内存回收条件则清理pagePageLRU属性,并令page引用计数加1,返回0,否则返回负数
  7.         switch (__isolate_lru_page(page, mode)) {
  8.         case 0:
  9.             ........
  10.             //把符合内存回收条件的pagelru链表移动到dst临时链表
  11.             list_move(&page->lru, dst);
  12.             break;
  13.         case -EBUSY:
  14.             list_move(&page->lru, src);
  15.             continue;
  16.         }
  17. }
  18. int __isolate_lru_page(struct page *page, isolate_mode_t mode)
  19. {
  20.     int ret = -EINVAL;
  21.     //关键点,如果page已经从lru链条剔除,page隔离失败
  22.     if (!PageLRU(page))
  23.         return ret;
  24.     ret = -EBUSY;
  25.     //page引用计数不是0则加1并返回true。否则说明page应用计数是0,返回false,这种page已经没进程在使用了,已经不在LRU链表了
  26.     if (likely(get_page_unless_zero(page))){
  27.         //page将要从activeinactive lru链表移除,于是清理pagePageLRU属性
  28.         ClearPageLRU(page);
  29.         ret = 0;
  30.     }
  31.     return ret;
  32. }

注意,隔离page时,在对spin_locklru_lock加锁后,要令page引用计数加1,这个非常重要。此时其他进程就无法释放这个page了!如果在隔离page前,这个page可能被其他进程释放回伙伴系统,那page将没有LRU属性,此时__isolate_lru_page函数里的if (!PageLRU(page))将起到作用,导致隔离page失败。如果隔离page时没有对page引用计数加1,那page将可能并发被其他进程释放回伙伴系统,或者被释放回伙伴系统并且被新的进程分配并加入新的lru链表。这种情况下,page->mapping将发生变化,与原始的mapping就不一样了,可以据此判断出这种异常。

好的,page引用计数此时是3,接着来到shrink_page_list()函数对page进行真正的内存回收。

  1. static unsigned long shrink_page_list(struct list_head *page_list,
  2.                       struct pglist_data *pgdat,
  3.                       struct scan_control *sc,
  4.                       enum ttu_flags ttu_flags,
  5.                       struct reclaim_stat *stat,
  6.                       bool force_reclaim)
  7. {
  8.     while (!list_empty(page_list)) {
  9.         ............
  10.         //page有映射的bh
  11.         if (page_has_private(page)) {
  12.             //pagebh解除联系,并且令page引用计数减1
  13.             if (!try_to_release_page(page, sc->gfp_mask))
  14.                 goto activate_locked;
  15.         }
  16.         ...........
  17.         //pageradix treeaddress_space 剔除,如果page引用计数是2则清0,返回1page可以释放。否则page还再被其他进程使用,返回0,不能释放
  18.         else if (!mapping || !__remove_mapping(mapping, page, true))
  19.             goto keep_locked;
  20. free_it:
  21.         nr_reclaimed++;
  22.         list_add(&page->lru, &free_pages);
  23.         continue;
  24. activate_locked:
  25.         //重新设置page active
  26.         SetPageActive(page);
  27. keep_locked:
  28.         unlock_page(page);
  29. keep:
  30.         //到这里,page本轮不能回收,暂存ret_pages链表然后再移回activeinactive lru链表
  31.         list_add(&page->lru, &ret_pages);
  32.     }
  33.     //释放free_pages上的page到伙伴系统
  34.     free_unref_page_list(&free_pages);
  35.     ...............
  36. }

如果page有bh则if (page_has_private(page))成立,然后执行try_to_release_page解除page和bh的联系,并page引用计数减1,源码流程是try_to_release_page->ext4_releasepage->try_to_free_buffers->drop_buffers->__clear_page_buffers,

  1. static void __clear_page_buffers(struct page *page)
  2. {
  3.     //解除pagebh的联系
  4.     ClearPagePrivate(page);
  5.     set_page_private(page, 0);
  6.     //page引用计数减1
  7.     put_page(page);
  8. }

此时page的引用计数是2,然后执行到__remove_mapping()函数。

  1. static int __remove_mapping(struct address_space *mapping, struct page *page,
  2.                 bool reclaimed)
  3. {
  4.     refcount = 2;
  5.     //page引用计数是2则对page引用计数清0,并返回true,这个page可以释放了。否则page引用计数不是2则保持引用计数并返回false,这个page不能释放
  6.     if (!page_ref_freeze(page, refcount))
  7.         goto cannot_free;
  8.     .............
  9.     //pageradix tree 剔除
  10.     __delete_from_page_cache(page, shadow);
  11.     return 1;
  12. cannot_free:
  13.     return 0;  
  14. }

主要作用是:把page从radix tree、address_space 剔除,如果page引用计数是2则清0,返回1page可以释放。否则page还再被其他进程使用,返回0,不能释放

好的,正常情况page引用计数此时就是0了,然后就可以释放掉这个page了。如果page因为是脏页、writeback页等导致page回收失败,page就要暂存在page_list链表。shrink_page_list()函数执行后,再执行putback_inactive_pages()函数把page移动回lru链表,源码如下:

  1. static void putback_inactive_pages(struct lruvec *lruvec, struct list_head *page_list)
  2. {
  3.     struct zone_reclaim_stat *reclaim_stat = &lruvec->reclaim_stat;
  4.     struct pglist_data *pgdat = lruvec_pgdat(lruvec);
  5.     LIST_HEAD(pages_to_free);
  6.     while (!list_empty(page_list)) {
  7.         struct page *page = lru_to_page(page_list);
  8.         int lru;
  9.         lruvec = mem_cgroup_page_lruvec(page, pgdat);
  10.         //page要添加到inactive lru,设置LRU属性
  11.         SetPageLRU(page);
  12.         lru = page_lru(page);
  13.         //page添加到lru链表,并增加lru链表page
  14.         add_page_to_lru_list(page, lruvec, lru);
  15.         //page引用计数减1,减1后如果是0就说明page没人用了,可以释放了
  16.         if (put_page_testzero(page)) {
  17.             //清理pagelRUactive属性
  18.             __ClearPageLRU(page);
  19.             __ClearPageActive(page);
  20.             //pagelru链表剔除,并减少lru链表的page
  21.             del_page_from_lru_list(page, lruvec, lru);
  22.             //page再移动到pages_to_free链表,之后就直接释放掉
  23.             list_add(&page->lru, &pages_to_free);
  24.     }
  25.     list_splice(&pages_to_free, page_list);
  26. }

这里对page引用计数减1,因为之前隔离收该page时令page引用计数加1了,二者对冲掉。

OK,本文到这里基本就结束了。page引用计数可能有点复杂,简单说,当page要启用一个新功能时,就要对page引用计数加1,而回收page时要一一对应对page引用计数减1。水平有限,如有错误请指出。

相关文章:

内核内存回收关键隐藏变量之page引用计数

在分析内核内存回收源码时&#xff0c;page引用计数并不显眼&#xff0c;但是page引用计数对page的内存回收至关重要。本文基于linux-4.18.0-240版本内核源码&#xff0c;总结下文件页page的引用计数的相关细节。首先是get_page()和put_page()函数&#xff0c;分别令page引用计…...

数据结构---链表的基本操作

头插法遍历链表尾插法头删法尾删法按位置插入数据按位置删除数据直接插入排序 链表翻转快慢指针 linklist.c #include <stdio.h> #include <stdlib.h> #include "./linklist.h"linklist* create_linklist(void) {linklist* head (linklist*)malloc(siz…...

异步框架Celery在Django中的运用

参考博客&#xff1a;https://www.cnblogs.com/pyedu/p/12461819.html 参考视频&#xff1a;01 celery的工作机制_哔哩哔哩_bilibili 定义&#xff1a;简单灵活、处理大量消息的分布式系统&#xff0c;专注于实时处理异步队列&#xff0c;支持任务调度 主要架构&#xff1a; …...

YOLOv5代码解读[02] models/yolov5l.yaml文件解析

文章目录 YOLOv5代码解读[02] models/yolov5l.yaml文件解析yolov5l.yaml文件检测头1--->耦合头检测头2--->解耦头检测头3--->ASFF检测头Model类解析parse_model函数 YOLOv5代码解读[02] models/yolov5l.yaml文件解析 yolov5l.yaml文件 # YOLOv5 &#x1f680; by Ult…...

智能搬运机器人|海格里斯将如何持续推进工业和物流的智能化升级与发展?

存取、搬运、分拣是物流行业中的通用功能&#xff0c;但具体到每个行业又十分不同&#xff0c;例如&#xff1a;新能源电池领域&#xff0c;它所搬运的东西是电池&#xff0c;50KG~200KG&#xff1b;快递行业领域&#xff0c;所要处理的物料是那种扁平件和信封等等&#xff0c;…...

linux之前后端项目部署与发布

目录 前言 简介 一、安装Nginx 二、后端部署 2.1多个tomcat负载均衡 2.2 负载均衡 2.3 后端项目部署 三、前端部署 1.解压前端 2.Nginx配置文件修改 3.IP域名映射 4.重启Nginx服务 前言 上篇博主已经讲解过了单机项目的部署linux之JAVA环境配置JDK&Tomcat&a…...

Python 高级语法:一切皆对象

1 “一切皆对象”是一种核心设计哲学 在编程领域&#xff0c;特别是面向对象编程&#xff08;OOP&#xff09;中&#xff0c;“一切皆对象”是一种核心设计哲学。这种哲学主张&#xff0c;无论是数据、函数、还是更复杂的结构&#xff0c;都可以被视为对象&#xff0c;并赋予…...

python jupyter notebook打开页面方便使用

如果没安装jupyter, 请安装&#xff1a; pip install jupyter notebook 运行jupyter notebook jupyter-notebook...

音视频开发之旅(69)-SD图生图

目录 1. 效果展示 2. ControlNet介绍 3. 图生图流程浅析 4. SDWebui图生图代码流程 5. 参考资料 一、效果展示 图生图的应用场景非常多&#xff0c;比较典型的应用场景有风格转化&#xff08;真人与二次元&#xff09;、线稿上色、换装和对图片进行扩图等&#xff0c;下面…...

數據集成平台:datax將hive數據步到mysql(全部列和指定列)

數據集成平台&#xff1a;datax將hive數據步到mysql&#xff08;全部列和指定列&#xff09; 1.py腳本 傳入參數&#xff1a; target_database&#xff1a;數據庫 target_table&#xff1a;表 target_columns&#xff1a;列 target_positions&#xff1a;hive列的下標&#x…...

pikachu靶场-File Inclusion

介绍&#xff1a; File Inclusion(文件包含漏洞)概述 文件包含&#xff0c;是一个功能。在各种开发语言中都提供了内置的文件包含函数&#xff0c;其可以使开发人员在一个代码文件中直接包含&#xff08;引入&#xff09;另外一个代码文件。 比如 在PHP中&#xff0c;提供了&…...

[今天跟AI聊聊职场] ~你能接受你的直接领导能力不如你,年纪还比你小很多吗?

知乎问题&#xff1a; 弟弟今年35岁&#xff0c;刚换了一份工作&#xff0c;直接领导小A比他小5岁&#xff0c;各方面经验没有他成熟。难的工作都是弟弟在做&#xff0c;功劳都被直接领导小A抢走了&#xff0c;有时候还要被直接领导小A打压。弟弟感觉升职加薪无望。现在找工作不…...

网络原理TCP之“三次握手“

TCP内核中的建立连接 众所周知,TCP是有连接的. 当我们在客户端敲出socket new Socket(serverIp,severPort)时,就在系统内核就在建立连接 真正建立连接是在系统内核中建立的,我们程序员只是调用相关的api. 在此处,我们把TCP的建立连接称为三次握手. 系统在内核建立连接时如上…...

990-03产品经理与程序员:什么是 IT 与业务协调以及为什么它很重要?

What is IT-business alignment and why is it important? 什么是IT-业务一致性&#xff1f;为什么它很重要&#xff1f; It’s more important than ever that IT and the business operate from the same playbook(剧本). So why do so many organizations struggle to ach…...

Java Web(七)__Tomcat(二)

Tomcat工作模式 Tomcat作为Servlet容器&#xff0c;有以下三种工作模式。 1&#xff09;独立的Servlet容器&#xff0c;由Java虚拟机进程来运行 Tomcat作为独立的Web服务器来单独运行&#xff0c;Servlet容器组件作为Web服务器中的一部分而存在。这是Tomcat的默认工作模式。…...

【项目实战】帮美女老师做一个点名小程序(Python tkinter)

前言 博主有一个非常漂亮的老师朋友&#x1f60d;。最近&#xff0c;她急需一个能够实现随机点名的小程序&#xff0c;而博主正好擅长这方面的技术&#x1f90f;。所以&#xff0c;今天博主决定为她制作一个专门用于点名的小程序&#x1f4aa;。 博主在美女老师面前吹完牛皮之…...

Elasticsearch 去重后求和

标题的要求可以用如下 SQL 表示 select sum(column2) from (select distinct(column1),column2 from table)t 要如何用 DSL 实现呢&#xff0c;先准备下索引和数据 PUT test_index {"mappings": {"properties": {"column1": {"type"…...

考研数学——高数:函数与极限(3)

函数的连续性与间断点 函数的连续性 左连续 右连续 区间上的连续性 在xo处连续 函数的间断点 第一类间断点(左右极限都存在) 可去间断点: f(xo-0)= f(xo+0) 跳跃间断点: f(xo-0)≠ f(xo+0) 第二类间断点(震荡间断点、无穷间断点)...

LeetCode49 字母异位词分组

LeetCode49 字母异位词分组 在这篇博客中&#xff0c;我们将探讨 LeetCode 上的一道经典算法问题&#xff1a;字母异位词分组。这个问题要求将给定的字符串数组中的字母异位词组合在一起&#xff0c;并以任意顺序返回结果列表。 问题描述 给定一个字符串数组 strs&#xff0…...

【Python】Windows本地映射远程Linux服务器上的端口(解决jupyter notebook无法启动问题)

创作日志&#xff1a; 学习深度学习不想在本地破电脑上再安装各种软件&#xff0c;我就用实验室的服务器配置环境&#xff0c;启动jupyter notebook时脑子又瓦特了&#xff0c;在自己Windows电脑上打开服务器提供的网址&#xff0c;那肯定打不开啊&#xff0c;以前在其它电脑上…...

【Oracle APEX开发小技巧12】

有如下需求&#xff1a; 有一个问题反馈页面&#xff0c;要实现在apex页面展示能直观看到反馈时间超过7天未处理的数据&#xff0c;方便管理员及时处理反馈。 我的方法&#xff1a;直接将逻辑写在SQL中&#xff0c;这样可以直接在页面展示 完整代码&#xff1a; SELECTSF.FE…...

在鸿蒙HarmonyOS 5中实现抖音风格的点赞功能

下面我将详细介绍如何使用HarmonyOS SDK在HarmonyOS 5中实现类似抖音的点赞功能&#xff0c;包括动画效果、数据同步和交互优化。 1. 基础点赞功能实现 1.1 创建数据模型 // VideoModel.ets export class VideoModel {id: string "";title: string ""…...

python如何将word的doc另存为docx

将 DOCX 文件另存为 DOCX 格式&#xff08;Python 实现&#xff09; 在 Python 中&#xff0c;你可以使用 python-docx 库来操作 Word 文档。不过需要注意的是&#xff0c;.doc 是旧的 Word 格式&#xff0c;而 .docx 是新的基于 XML 的格式。python-docx 只能处理 .docx 格式…...

CMake 从 GitHub 下载第三方库并使用

有时我们希望直接使用 GitHub 上的开源库,而不想手动下载、编译和安装。 可以利用 CMake 提供的 FetchContent 模块来实现自动下载、构建和链接第三方库。 FetchContent 命令官方文档✅ 示例代码 我们将以 fmt 这个流行的格式化库为例,演示如何: 使用 FetchContent 从 GitH…...

全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比

目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec&#xff1f; IPsec VPN 5.1 IPsec传输模式&#xff08;Transport Mode&#xff09; 5.2 IPsec隧道模式&#xff08;Tunne…...

AI书签管理工具开发全记录(十九):嵌入资源处理

1.前言 &#x1f4dd; 在上一篇文章中&#xff0c;我们完成了书签的导入导出功能。本篇文章我们研究如何处理嵌入资源&#xff0c;方便后续将资源打包到一个可执行文件中。 2.embed介绍 &#x1f3af; Go 1.16 引入了革命性的 embed 包&#xff0c;彻底改变了静态资源管理的…...

稳定币的深度剖析与展望

一、引言 在当今数字化浪潮席卷全球的时代&#xff0c;加密货币作为一种新兴的金融现象&#xff0c;正以前所未有的速度改变着我们对传统货币和金融体系的认知。然而&#xff0c;加密货币市场的高度波动性却成为了其广泛应用和普及的一大障碍。在这样的背景下&#xff0c;稳定…...

MySQL账号权限管理指南:安全创建账户与精细授权技巧

在MySQL数据库管理中&#xff0c;合理创建用户账号并分配精确权限是保障数据安全的核心环节。直接使用root账号进行所有操作不仅危险且难以审计操作行为。今天我们来全面解析MySQL账号创建与权限分配的专业方法。 一、为何需要创建独立账号&#xff1f; 最小权限原则&#xf…...

android13 app的触摸问题定位分析流程

一、知识点 一般来说,触摸问题都是app层面出问题,我们可以在ViewRootImpl.java添加log的方式定位;如果是touchableRegion的计算问题,就会相对比较麻烦了,需要通过adb shell dumpsys input > input.log指令,且通过打印堆栈的方式,逐步定位问题,并找到修改方案。 问题…...

离线语音识别方案分析

随着人工智能技术的不断发展&#xff0c;语音识别技术也得到了广泛的应用&#xff0c;从智能家居到车载系统&#xff0c;语音识别正在改变我们与设备的交互方式。尤其是离线语音识别&#xff0c;由于其在没有网络连接的情况下仍然能提供稳定、准确的语音处理能力&#xff0c;广…...