openharmony中hdf框架的驱动消息机制的实现原理

news2025/2/24 11:54:52

openharmony中hdf框架的驱动消息机制的实现原理

在分析hdf框架时发现绕来绕去的,整体梳理画了一遍流程图,发现还是有点模糊甚至不清楚如何使用的,详细的每个点都去剖析细节又过于消耗时间,所以有时间便从功能应用的角度一块块的去梳理。

此文为参考官方源码(oh5.0版本)中的docs\zh-cn\device-dev\driver\driver-hdf-manage.md驱动开发手册,将驱动消息机制这个小章节拿出来,单独做剖析的。官方手册中只涉及了如何使用,未涉及具体的原理。本文会先整体说下实现原理,然后结合驱动开发手册将涉及使用hdf接口函数的部分再往下剖了一下,目的是了解具体的实现逻辑。

概述

HDF框架提供统一的驱动消息机制,支持用户态应用向内核态驱动发送消息,也支持内核态驱动向用户态应用发送消息,用于当用户态应用和内核态驱动需要交互的场景。

使用分析

由原理可知消息机制的功能主要有以下两种:

  • 用户态应用发送消息到驱动。
  • 用户态应用接收驱动主动上报事件。

表2 消息机制接口

方法描述
struct HdfIoService *HdfIoServiceBind(const char *serviceName);用户态获取驱动的服务,获取该服务之后通过服务中的Dispatch方法向驱动发送消息。
void HdfIoServiceRecycle(struct HdfIoService *service);用户态释放驱动的服务,与HdfIoServiceBind对应
int HdfDeviceRegisterEventListener(struct HdfIoService *target, struct HdfDevEventlistener *listener);用户态程序注册接收驱动上报事件的操作方法。
int32_t HdfDeviceSendEvent(const struct HdfDeviceObject *deviceObject, uint32_t id, const struct HdfSBuf *data)驱动主动上报事件接口。当驱动服务调用此函数发送消息时,所有通过 HdfDeviceRegisterEventListener注册了监听器的用户级应用程序都将收到该消息。

HdfIoServiceBind

用户态获取驱动的服务获取该服务之后通过服务中的Dispatch方法向驱动发送消息(见示例),函数的声明在接口文件中hdf_core\interfaces\inner_api\core\hdf_io_service_if.h,驱动开发者可以直接包含调用。

struct HdfIoService *HdfIoServiceBind(const char *serviceName)
    |-->HdfIoServiceAdapterObtain(serviceName) //获取设备服务接口的适配器函数
    	|-->svcMgr = DevSvcManagerClntGetInstance();//获取设备服务管理实例
		|-->HdfDeviceObject *deviceObject = svcMgr->devSvcMgrIf->GetObject(svcMgr->devSvcMgrIf, serviceName);//通过设备服务管理器获取设备对象
			|-->DevSvcManagerGetObject //通过获取设备服务管理实例的创建过程可知上述函数的回调为此函数
                |-->serviceRecord = DevSvcManagerSearchServiceLocked(inst, serviceKey)//从设备服务管理器中搜索该名称的服务
                |-->return serviceRecord->value //返回设备对象
		|-->HdfIoServiceKClient *kClient = HdfHdfIoServiceKClientInstance(deviceObject)//通过设备对象获取设备服务的客户端实例
                |-->kDispatcher = {.Dispatch = HdfKIoServiceDispatch,};
			   |-->struct HdfIoServiceKClient *client = OsalMemCalloc(sizeof(struct HdfIoServiceKClient))//分配内存
                |-->if (deviceObject->service->Open(&client->client) != HDF_SUCCESS)//回调驱动层的open函数
                |-->client->ioService.dispatcher = &kDispatcher//绑定具体的接口,
         |-->return &kClient->ioService//返回设备服务接口

HdfIoServiceRecycle

用户态释放驱动的服务,与HdfIoServiceBind对应,声明在接口文件中hdf_core\interfaces\inner_api\core\hdf_io_service_if.h,驱动开发者可以直接包含调用。

void HdfIoServiceRecycle(struct HdfIoService *service)
	|-->HdfIoServiceAdapterRecycle(service)
		|-->HdfIoServiceKClient *kClient = CONTAINER_OF(ioService, struct HdfIoServiceKClient, ioService)//根据ioService反推HdfIoServiceKClient对象①
		|-->kClient->client.device->service->Release(&kClient->client)//客户端设备的释放②
		|-->OsalMemFree(kClient)//对应HdfIoServiceBind函数中的HdfHdfIoServiceKClientInstance

①:关于反推函数CONTAINER_OF需要详细的了解的可以参考这篇文章

②:客户端设备的释放是在设备构建过程中构建的io服务的设备接口(IDeviceIoService),从整体的流程图中方便看,但太大了没法放上来,后续有时间整理了这部分再重新放上链接。✒️

HdfDeviceRegisterEventListener

用户态程序注册接收驱动上报事件的操作方法

int HdfDeviceRegisterEventListener(struct HdfIoService *target, struct HdfDevEventlistener *listener)
	|-->return HdfDeviceRegisterEventListenerWithSchedPolicy(target, listener, SCHED_OTHER)//为设备服务注册事件监听器,并指定事件处理线程的调度策略
    	|-->struct HdfSyscallAdapter *adapter = CONTAINER_OF(target, struct HdfSyscallAdapter, super)//从HdfIoService结构体中获取其所属的HdfSyscallAdapter实例
		|-->if (!AddListenerToAdapterLocked(adapter, listener)) //将事件监听器添加到适配器中
         |--> ret = HdfIoServiceGroupThreadStart(adapter->group, policy) //如果适配器属于一个服务组(adapter->group不为空)便启动服务组的线程,并指定调度策略(policy即传入的SCHED_OTHER)
            |--> if (HdfIoServiceGroupThreadInit(group) != HDF_SUCCESS) //对线程的初始化
                |-->HdfDevListenerThreadDoInit(thread) 
                	|-->int32_t ret = OsalThreadCreate(&thread->thread, HdfDevEventListenTask, thread)//创建监听线程
                		|-->//....路径较多省略了
                		|-->int32_t HdfDevEventDispatchLocked(
    const struct HdfDevListenerThread *thread, struct HdfSyscallAdapter *adapter, const struct HdfWriteReadBuf *bwr)
                		|-->(void)listener->callBack(listener->priv, bwr->cmdCode, sbuf)//此处进行监听事件的回调①
            |--> int32_t ret = HdfDevListenerThreadStart(group->thread)//初始化设备监听线程,并启动一个线程来处理设备事件
                |-->int32_t ret = HdfListenThreadInitPollFds(thread)//初始化监听的文件描述符列表
                |-->if (HdfAdapterStartListenIoctl(thread->pfds[i].fd)) {//启动监听
                |-->if (OsalThreadStart(&thread->thread, &config) != HDF_SUCCESS) {//启动线程
         	return ret;
         |-->if (HdfIoServiceStartListen(adapter, policy) != HDF_SUCCESS)//如果适配器不属于服务组,启动事件处理线程,并指定调度策略
             |-->return HdfDevListenerThreadStart(adapter->thread)//初始化设备监听线程,并启动一个线程来处理设备事件
  • SCHED_OTHER是linux系统中默认的进程调度策略,想详细了解进程调度策略可以参考这篇文章

  • 在本文后续的使用示例中可见此函数的使用的目的,主要是为了将事件进行回调,其中回调部分可参看本段代码的①处。

static struct HdfDevEventlistener listener = {
    .callBack = OnDevEventReceived,
    .priv ="Service0"
};
if (HdfDeviceRegisterEventListener(serv, &listener) != 0) {
    HDF_LOGE("fail to register event listener");
    return HDF_FAILURE;
}
  • 在此段代码中还可见有文件描述符的处理,即应用中我们用的selectpollepoll等。

HdfDeviceSendEvent

驱动主动上报事件接口,驱动服务调用此函数发送消息时,所有通过 HdfDeviceRegisterEventListener注册了监听器的用户级应用程序都将收到该消息。声明的头文件在hdf_core\interfaces\inner_api\host\shared\hdf_device_desc.h,意味着用户态可以直接包含调用。

int32_t HdfDeviceSendEvent(const struct HdfDeviceObject *deviceObject, uint32_t id, const struct HdfSBuf *data)
	|-->struct HdfDeviceNode *deviceNode = CONTAINER_OF(deviceObject, struct HdfDeviceNode, deviceObject)
    |-->adapter = (struct HdfVNodeAdapter *)(((struct DeviceNodeExt *)deviceNode)->ioService)
    |-->return HdfVNodeAdapterSendDevEvent(adapter, NULL, id, data)
        |-->ret = VNodeAdapterSendDevEventToClient(client, id, data)//将设备事件从驱动程序发送到客户端(通常是用户态应用程序或服务)
        	|-->event = OsalMemAlloc(sizeof(struct HdfDevEvent));//分配事件对象
			|--> event->data = HdfSbufCopy(data);//给事件对象赋值
			|-->DListInsertTail(&event->listNode, &vnodeClient->eventQueue) //将事件对象插入到客户端的事件队列
             |-->wake_up_interruptible(&vnodeClient->pollWait)//唤醒等待事件的客户端线程
  • 唤醒等待事件的客户端线程的wake_up_interruptible函数为linux内核的api函数,有需要详细了解的可以直接百度,想了解简单使用方法的可以参考这篇文章。

使用示例

驱动消息机制管理开发

  1. 将驱动配置信息(device_info.hcs)中服务策略policy字段设置为2(SERVICE_POLICY_CAPACITY,驱动对内核态和用户态都发布服务)。

    device_sample :: Device {
        policy = 2;
        permission = 0644;
        ...
    }
    
  2. 配置驱动信息中的服务设备节点权限(permission字段)是框架给驱动创建设备节点的权限,默认是0666,驱动开发者根据驱动的实际使用场景配置驱动设备节点的权限。

    权限值含义
    0666所有用户都可以读写该设备节点
    0644所有者可以读写,所属组和其他用户可以读取
    0640所有者可以读写,所属组可以读取,其他用户无法访问
    0600只有所有者可以读写,其他用户无法访问
  3. 在服务实现过程中,实现服务基类成员IDeviceIoService中的Dispatch方法

    #include <hdf_log.h>
    #include <hdf_device_io.h>
    #include <hdf_device_desc.h>
    #include <hdf_sbuf.h>
    
    // 假设的其他服务函数
    int32_t SampleDriverServiceA(struct HdfDeviceIoClient *client, struct HdfSBuf *data, struct HdfSBuf *reply) {
        HDF_LOGI("SampleDriverServiceA called");
        return HDF_SUCCESS;
    }
    
    int32_t SampleDriverServiceB(struct HdfDeviceIoClient *client, struct HdfSBuf *data, struct HdfSBuf *reply) {
        HDF_LOGI("SampleDriverServiceB called");
        return HDF_SUCCESS;
    }
    
    // I/O 请求处理函数
    int32_t SampleDriverDispatch(struct HdfDeviceIoClient *client, int cmdCode, struct HdfSBuf *data, struct HdfSBuf *reply) {
        HDF_LOGI("SampleDriverDispatch called with cmdCode: %d", cmdCode);
    
        // 根据 cmdCode 处理不同的命令
        switch (cmdCode) {
            case 1:
                HDF_LOGI("Handling command 1");
                // 处理命令 1 的逻辑
                const char *msg = "Hello from driver";
                struct HdfSBuf *eventData = HdfSbufObtainDefaultSize();  // 创建事件数据缓冲区
                if (eventData == NULL) {
                    HDF_LOGE("fail to obtain sbuf for event data");
                    return HDF_DEV_ERR_NO_MEMORY;
                }
    
                if (!HdfSbufWriteString(eventData, msg)) {  // 写入事件数据
                    HDF_LOGE("fail to write event data");
                    HdfSbufRecycle(eventData);
                    return HDF_FAILURE;
                }
                break;
            case 2:
                HDF_LOGI("Handling command 2");
                // 处理命令 2 的逻辑
                break;
            default:
                HDF_LOGE("Unknown command code: %d", cmdCode);
                return HDF_ERR_INVALID_PARAM;
        }
    	 // 上报事件
         int ret = HdfDeviceSendEvent(client->device, cmdCode, eventData);
         HdfSbufRecycle(eventData);  // 释放事件数据缓冲区
        return HDF_SUCCESS;
    }
    
    // 驱动绑定函数
    int32_t SampleDriverBind(struct HdfDeviceObject *device) {
        HDF_LOGI("SampleDriverBind called");
    
        if (device == NULL) {
            HDF_LOGE("Invalid device object");
            return HDF_FAILURE;
        }
    
        // 定义服务接口
        static struct ISampleDriverService sampleDriverA = {
            .ioService.Dispatch = SampleDriverDispatch,
            .ServiceA = SampleDriverServiceA,
            .ServiceB = SampleDriverServiceB,
        };
    
        // 将服务接口绑定到设备对象
        device->service = (struct IDeviceIoService *)&sampleDriverA;
    
        return HDF_SUCCESS;
    }
    
    // 驱动卸载函数
    void SampleDriverUnload(struct HdfDeviceObject *device) {
        HDF_LOGI("SampleDriverUnload called");
        // 在这里释放资源或执行清理操作
    }
    
    // 驱动描述符
    struct HdfDriverEntry g_sampleDriver = {
        .moduleVersion = 1,
        .Bind = SampleDriverBind,
        .Unload = SampleDriverUnload,
        .moduleName = "sample_driver",
    };
    
    // 驱动模块入口
    SYSEXPORT_DRIVER(g_sampleDriver);
    
  4. 驱动定义消息处理函数中的cmd类型

    #define SAMPLE_WRITE_READ 1    // 读写操作码1
    
  5. 用户态获取服务接口并发送消息到驱动

    int SendMsg(const char *testMsg)
    {
        if (testMsg == NULL) {
            HDF_LOGE("test msg is null");
            return HDF_FAILURE;
        }
        struct HdfIoService *serv = HdfIoServiceBind("sample_driver");// 绑定到驱动服务
        if (serv == NULL) {
            HDF_LOGE("fail to get service");
            return HDF_FAILURE;
        }
        struct HdfSBuf *data = HdfSbufObtainDefaultSize();// 分配数据和响应缓冲区
        if (data == NULL) {
            HDF_LOGE("fail to obtain sbuf data");
            return HDF_FAILURE;
        }
        struct HdfSBuf *reply = HdfSbufObtainDefaultSize();
        if (reply == NULL) {
            HDF_LOGE("fail to obtain sbuf reply");
            ret = HDF_DEV_ERR_NO_MEMORY;
            goto out;
        }
        if (!HdfSbufWriteString(data, testMsg)) {// 写入数据到数据缓冲区
            HDF_LOGE("fail to write sbuf");
            ret = HDF_FAILURE;
            goto out;
        }
        int ret = serv->dispatcher->Dispatch(&serv->object, SAMPLE_WRITE_READ, data, reply);// 调用服务的 Dispatch 函数发送请求
        if (ret != HDF_SUCCESS) {
            HDF_LOGE("fail to send service call");
            goto out;
        }
    out:
        HdfSbufRecycle(data);
        HdfSbbufRecycle(reply);
        HdfIoServiceRecycle(serv);
        return ret;
    }
    
  6. 用户态接收该驱动上报的消息

    //用户态编写驱动上报消息的处理函数。
    static int OnDevEventReceived(void *priv,  uint32_t id, struct HdfSBuf *data)
    {
        OsalTimespec time;
        OsalGetTime(&time);
        HDF_LOGI("%{public}s received event at %{public}llu.%{public}llu", (char *)priv, time.sec, time.usec);
    
        const char *string = HdfSbufReadString(data);
        if (string == NULL) {
            HDF_LOGE("fail to read string in event data");
            return HDF_FAILURE;
        }
        HDF_LOGI("%{public}s: dev event received: %{public}d %{public}s",  (char *)priv, id, string);
        return HDF_SUCCESS;
    }
    //用户态注册接收驱动上报消息的操作方法。
    int RegisterListen()
    {
        struct HdfIoService *serv = HdfIoServiceBind("sample_driver");
        if (serv == NULL) {
            HDF_LOGE("fail to get service");
            return HDF_FAILURE;
        }
        static struct HdfDevEventlistener listener = {
            .callBack = OnDevEventReceived,
            .priv ="Service0"
        };
        if (HdfDeviceRegisterEventListener(serv, &listener) != 0) {
            HDF_LOGE("fail to register event listener");
            return HDF_FAILURE;
        }
        ......
        HdfDeviceUnregisterEventListener(serv, &listener);
        HdfIoServiceRecycle(serv);
        return HDF_SUCCESS;
    }
    

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2304379.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

HTTP SSE 实现

参考&#xff1a; SSE协议 SSE技术详解&#xff1a;使用 HTTP 做服务端数据推送应用的技术 一句概扩 SSE可理解为&#xff1a;服务端和客户端建立连接之后双方均保持连接&#xff0c;但仅支持服务端向客户端推送数据。推送完毕之后关闭连接&#xff0c;无状态行。 下面是基于…

二分图检测算法以及最大匹配算法(C++)

上一节我们学习了有向图中的最大连通分量. 本节我们来学习二分图. 二分图是一种特殊的图结构, 能够帮助我们高效地解决这些匹配和分配问题. 本文将带你了解二分图的基本概念, 判定方法, 最大匹配算法以及实际应用场景. 环境要求 本文所用样例在Windows 11以及Ubuntu 24.04上面…

Keepalive基础

一。简介和功能 vrrp协议的软件实现&#xff0c;原生设计目的是为了高可用ipvs服务 功能&#xff1a; 1.基于vrrp协议完成地址流动 2.为vip地址所在的节点生成ipvs规则&#xff08;在配置文件中预先定义&#xff09; 3.为ipvs集群的各RS做健康状况检测 4.基于脚本调用接口…

计算机毕业设计SpringBoot+Vue.jst0图书馆管理系统(源码+LW文档+PPT+讲解)

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

【Java消息队列】应对消息丢失、重复、顺序与积压的全面策略

应对消息丢失、重复、顺序与积压的全面策略 引言kafka消息丢失生产者消费者重复消费顺序消费消息积压生产者消费者其他RabbitMQ消息丢失生产者事务机制,保证生产者发送消息到 RabbitMQ Server发送方确认机制,保证消息能从交换机路由到指定队列保证消息在 RabbitMQ Server 中的…

【论文解读】TransMLA: Multi-Head Latent Attention Is All You Need

论文链接 1. 论文背景与问题动机 现代大规模语言模型&#xff08;LLM&#xff09;在推理时往往遇到通信瓶颈&#xff0c;主要原因在于自注意力机制中需要缓存大量的 Key-Value&#xff08;KV&#xff09;对。例如&#xff0c;对于 LLaMA‑65B 这种模型&#xff0c;即使采用 8…

登录-06.JWT令牌-生成和校验

一.JWT令牌的生成和校验 JWT令牌生成 想要生成JWT令牌&#xff0c;那么就要首先引入JWT令牌的相关依赖&#xff0c; <dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.2</version>…

【Git】多人协作

文章目录 完成准备工作多人协作场景一场景二远程分支删除后&#xff0c;本地 git branch -a 依然能看到的解决办法 完成准备工作 在之前&#xff0c;我们所完成的工作如下&#xff1a; 基本完成 Git 的所有本地库的相关操作&#xff0c;git基本操作&#xff0c;分支理解&#…

邮件安全之发件人伪造

电子邮件工作原理 电子邮件传输过程中主要涉及到SMTP、IMAP、POP3三种协议&#xff0c;具体功能如下&#xff1a; SMTP:全称Simple Mail Transfer Protocol&#xff0c;即简单邮件传输协议&#xff0c;主要用于发送邮件&#xff0c;使用端口号25。 IMAP:全称Internet Mail Acce…

使用 AIStor 和 OpenSearch 增强搜索功能

在这篇文章中&#xff0c;我们将探讨搜索&#xff0c;特别是 OpenSearch 如何帮助我们识别模式或查看不断增长的数据中的趋势。例如&#xff0c;如果您正在查看运营数据&#xff0c;如果您的服务似乎是随机的&#xff0c;那么您需要尽可能回溯以识别模式并找出原因。这不仅适用…

【LLM】R1复现项目(SimpleRL、OpenR1、LogitRL、TinyZero)持续更新

note &#xff08;1&#xff09;未来的工作需亟待解决&#xff1a; 支持大规模 RL 训练&#xff08;PPO、GRPO 等&#xff09;的开源基础框架用于稳定训练的 GRPO 训练超参的自动化调优RL 训练数据的配比&#xff08;难度、领域、任务等&#xff09;基于 Instruct 模型训练 R…

买股票的最佳时机 - 2

买卖股票的最佳时机 III 题目描述&#xff1a; 提示&#xff1a; 1 < prices.length < 1050 < prices[i] < 105 分析过程&#xff1a; 写动态规划&#xff0c;我们需要考虑一下问题&#xff1a; 定义状态状态转移方程初始条件 遍历顺序 4种状态&#xff1a; …

Python基于flask的智慧交通可视化,大数据智慧交通数据可视化系统

博主介绍&#xff1a;✌程序员徐师兄、8年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战*✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447…

【Unity】鱼群效果模拟

鱼群效果模拟 文章目录 鱼群效果模拟Boid算法实现方式version1_CPUversion2_GPUversion3_Multilaterationversion4_Bitonic_Sorting &#xff08;GPU友好&#xff09;version5_Skinning &#xff08;TODO&#xff09; 细节项优化项参考链接 Boid算法 Boid算法是一种模拟群体行…

云图库平台(五)——后端图片模块开发

目录 一、需求分析二、库表设计三、图片的处理如何实现图片的上传和下载创建图片的业务流程如何对图片进行解析 四、创建并使用对象存储五、后端操作对象存储初始化客户端通用能力类文档上传文件下载 一、需求分析 管理员功能&#xff1a; 图片的上传和创建&#xff1a;仅管理…

postman调用ollama的api

按照如下设置&#xff0c;不需要设置key 保持长会话的方法 # 首次请求 curl http://localhost:11434/api/generate -d {"model": "deepseek-r1:32b","prompt": "请永久记住&#xff1a;110&#xff0c;1-12&#xff0c;之后所有数学计算必…

十、OSG学习笔记-多线程(OpenThreads)

上一节内容&#xff1a; 九、OSG学习笔记-NodeVisitor节点遍历器-CSDN博客https://blog.csdn.net/weixin_36323170/article/details/145742756?spm1001.2014.3001.5501 本章节代码&#xff1a; OsgStudy/Openthreads CuiQingCheng/OsgStudy - 码云 - 开源中国https://gite…

DeepSeek 助力 Vue 开发:打造丝滑的单选按钮(Radio Button)

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 Deep…

【行业解决方案篇十八】【DeepSeek航空航天:故障诊断专家系统 】

引言:为什么说这是“航天故障终结者”? 2025年春节刚过,航天宏图突然官宣"DeepSeek已在天权智能体上线",这个搭载在卫星和空间站上的神秘系统,号称能提前48小时预判99.97%的航天器故障。这不禁让人想起年初NASA禁用DeepSeek引发的轩然大波,更让人好奇:这套系…

谷歌浏览器更新后导致的刷新数据无法显示

这几天突然出现的问题&#xff0c;就是我做了一个网站&#xff0c;一直用Google展示&#xff0c;前两天突然就是刷新会丢失数据&#xff0c;然后再刷新几次吧又有了&#xff0c;之前一直好好的&#xff0c;后端也做了一些配置添加了CrossOrigin注解&#xff0c;然而换了edge浏览…