Android 进程间通信机制(二) mmap 原理

news2024/12/28 3:59:23

一. 前言

        Binder中一次拷贝的实现就是利用mmap(memory mapping)内存映射机制,我们来看看它的工作原理.

二. 参考文章

下面这几篇文章建议先好好阅读一下,都是总结的很好的文章, 每个人理解可能不一样

笔者也是看了好多博客文章和B站视频讲解,  然后加上自己的理解后 输出的一点总结

我保证写出来的都是经过思考后, 尽量输出的是对的, 不误导别人, 如果有错误,也请指出来,一起讨论

感谢!

 Linux下 给一个进程分配4G虚拟地址空间的数据结构图: Linux下的4G虚拟地址空间

关于mmap概念? 可以参阅这篇文章 :认真分析mmap:是什么 为什么 怎么用

mmap映射的原理,可以参阅这篇文章: Android 内存映射mmap浅谈

Binder进程间通信, mmap使用细节: Android-内存映射mmap

binder驱动层关于mmap实现, 可以参阅这篇文章: Binder驱动概述

binder驱动层计算映射内存大小,可以参阅这篇文章: binder驱动-------之内存映射篇_binder内存映射_xiaojsj111的博客-CSDN博客

三. 理解和总结

3.1  Linux下每个进程的4G虚拟地址空间的数据结构图

我们现在所写的源代码并不是我们所说的程序,从C代码(.c/.cpp)---->链接程序(.exe)是要经过以下几个过程才能真正的运行链接的;

C源程序--->预编译处理(.c/.cpp)-->编译,优化程序(.s)--->汇编程序(.o)--->链接(.exe)

在编译运行过程中,我们首先需要将我们的程序存储到内存中才能调取运行,但是内存是有限的,不可能将所有的进程都放在内存中去,所以都会给进程分配一个4G的虚拟地址空间存储数据,在进程运行时在映射到内存中去

在  windows下  4G的空间分布为 :   用户态:内核态=1:1

      Linux下         4G的空间分布为:    用户态:内核态=3:1   

4G都是虚拟地址空间.

3.2  理解用户空间和内核空间

用户空间与内核空间
Linux的进程是相互独立的,一个进程是不能直接操作或者访问别一个进程空间的。每个进程空间还分为用户空间和内核(Kernel)空间,相当于把Kernel和上层的应用程序抽像的隔离开。

用户空间和内核空间,用户空间是用户程序代码运行的地方,内核空间是内核代码运行的地方。为了安全,它们是隔离的,即使用户的程序崩溃了,内核也不受影响。

这里有两个隔离,一个进程间是相互隔离的,二是进程内有用户空间和内核空间的隔离。

1. 进程间,用户空间的数据不可共享,所以用户空间 = 不可共享空间
2. 进程间,内核空间的数据可共享,    所以内核空间 = 可共享空间
3. 每个进程的内核空间1G(地址: 0xC0000000--0xFFFFFFFF)都是共享的, 属于所有进程, 内核线性地址空间由所有进程共享,但只有运行在内核态的进程才能访问.

3.3  Binder进程间通信使用mmap

1. Linux内存映射(mmap)概念:Linux通过将一个虚拟内存区域与一个磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping)。实现映射关系后,就可以采用指针的方式读写操作这一段内存,而系统会自动回写到对应的文件磁盘上。

了解进程间通信的人都知道Android使用的是Binder进行进程间通信,它的效率高于Linux其他传统的进程间通信,因为它只要一次拷贝,而之所以只需要进行一次拷贝的原因就在于使用了mmap

一次完整的 Binder IPC 通信过程通常是这样:

1. Server端在启动之后,调用对/dev/binder设备调用mmap。

 service端进程在用户空间调用库函数mmap 是在

frameworks/native/libs/binder/ProcessState.cpp文件中

 if (mDriverFD >= 0) {
        // mmap the binder, providing a chunk of virtual address space to receive transactions.
        mVMStart = mmap(nullptr, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
        if (mVMStart == MAP_FAILED) {
            // *sigh*
            ALOGE("Using %s failed: unable to mmap transaction memory.\n", mDriverName.c_str());
            close(mDriverFD);
            mDriverFD = -1;
            mDriverName.clear();
        }
    }
#define BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2)
#define DEFAULT_MAX_BINDER_THREADS 15
//binder默认驱动名称
const char* kDefaultDriver = "/dev/binder";
  • ProcessState的单例模式的惟一性,因此一个进程只打开binder设备一次,其中ProcessState的成员变量mDriverFD记录binder驱动的fd,用于访问binder设备。
  • BINDER_VM_SIZE = (1*1024*1024) - (4096 *2), binder分配的默认内存大小为1M-8k。 _SC_PAGE_SIZE 一个页的大小为4K
  • DEFAULT_MAX_BINDER_THREADS = 15,binder默认的最大可并发访问的线程数为16。

当一个进程启动后, 它就有属于自己的一块binder内存, 这块内存的大小就是在进程调用mmap函数时分配的大小 : 1M-8K

可以验证一下,链接上自己的手机,执行命令:

1. adb shell 

2.  ps  -A     查看所以运行的进程, 我们找几个比较常见的应用进程 

me@ubuntu:~$ adb shell
device:/ # ps -A
USER           PID  PPID     VSZ    RSS WCHAN            ADDR S NAME 

 PID号在第二列显示,比如 systemui的进程号为4568     launcher的进程号为5018

系统调用mmap所分配的地址空间,并可以通过cat proc/进程号/maps | grep dev/binder 命令查看到:

看下SystemUI:  

 地址范围为: 7b2b1df000---7b2b2dd000   通过计算器 16进制转化成十进制计算得出为:

1040384  /  1024  =  1016K  刚好为  1M-8K

再看下Launcher3:

 地址范围: 7b2b1cd000---7b2b2cb000    通过计算 转换成十进制

1040384  /  1024  =  1016K  刚好为  1M-8K

2. 内核中的binder_mmap函数进行对应的处理:申请一块物理内存,然后在Server端的用户空间和内核空间同时进行映射.

 路径:  bsp/kernel/kernel4.14/drivers/android/binder.c

我们再看/drivers/staging/android/binder.c驱动中static int binder_mmap(struct file *filp, struct vm_area_struct *vma)函数的含义:

vma->vm_start和vma->vm_end即为此次映射内核为我们分配的开始地址和结束地址,他们差值就是系统调用mmap中的length的值。而vma->vm_start的则是系统调用mmap调用的返回值。需要注意的是vma->vm_start和vma->vm_end都是调用进程的用户空间的虚拟地址,他们地址范围可以通过如下命令:cat /proc/pid/maps | grep "/dev/binder"看到,如上面两个图,针对Systemui.apk进程,他们vma->vm_start和vma->vm_end的值分别为:7b2b1df000和7b2b2dd000

static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
	int ret;
	struct vm_struct *area;
	struct binder_proc *proc = filp->private_data;
	const char *failure_string;
	struct binder_buffer *buffer;//内核进入这个函数时,就已经预先为此次映射分配好了调用进程在用户空间的虚拟地址范围(vma->vm_start,vma->vm_end)
 
  	if ((vma->vm_end - vma->vm_start) > SZ_4M)
		vma->vm_end = vma->vm_start + SZ_4M;
 
	binder_debug(BINDER_DEBUG_OPEN_CLOSE,
		     "binder_mmap: %d %lx-%lx (%ld K) vma %lx pagep %lx\n",
		     proc->pid, vma->vm_start, vma->vm_end,
		     (vma->vm_end - vma->vm_start) / SZ_1K, vma->vm_flags,
		     (unsigned long)pgprot_val(vma->vm_page_prot));
 
	if (vma->vm_flags & FORBIDDEN_MMAP_FLAGS) {
		ret = -EPERM;
		failure_string = "bad vm_flags";
		goto err_bad_arg;
	}
	vma->vm_flags = (vma->vm_flags | VM_DONTCOPY) & ~VM_MAYWRITE;
 
	mutex_lock(&binder_mmap_lock);
	if (proc->buffer) {
		ret = -EBUSY;
		failure_string = "already mapped";
		goto err_already_mapped;
	}
        //为进程所在的内核空间申请与用户空间同样长度的虚拟地址空间,这段空间用于内核来访问和管理binder内存区域
	area = get_vm_area(vma->vm_end - vma->vm_start, VM_IOREMAP);
	if (area == NULL) {
		ret = -ENOMEM;
		failure_string = "get_vm_area";
		goto err_get_vm_area_failed;
	}//对应内核虚拟地址的开始,即为binder内存的开始地址
	proc->buffer = area->addr;
	proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;//同一块物理内存,内核的虚拟地址同应用空间的虚拟地址的差
	mutex_unlock(&binder_mmap_lock);
 
#ifdef CONFIG_CPU_CACHE_VIPT
	if (cache_is_vipt_aliasing()) {
		while (CACHE_COLOUR((vma->vm_start ^ (uint32_t)proc->buffer))) {
			printk(KERN_INFO "binder_mmap: %d %lx-%lx maps %p bad alignment\n", proc->pid, vma->vm_start, vma->vm_end, proc->buffer);
			vma->vm_start += PAGE_SIZE;
		}
	}
#endif//用于存放内核分配的物理页的页描述指针:struct page,每个物理页对应这样一个struct page结构
 	proc->pages = kzalloc(sizeof(proc->pages[0]) * ((vma->vm_end - vma->vm_start) / PAGE_SIZE), GFP_KERNEL);
	if (proc->pages == NULL) {
		ret = -ENOMEM;
		failure_string = "alloc page array";
		goto err_alloc_pages_failed;
	}
	proc->buffer_size = vma->vm_end - vma->vm_start;//映射的长度即为binder内存的大小
 
	vma->vm_ops = &binder_vm_ops;//设定
	vma->vm_private_data = proc;//为binder内存的最开始的一个页的地址建立虚拟到物理页的映射,其他的虚拟地址都还没有建立相应的映射,没有映射也就还不能够被访问
	if (binder_update_page_range(proc, 1, proc->buffer, proc->buffer + PAGE_SIZE, vma)) {
		ret = -ENOMEM;
		failure_string = "alloc small buf";
		goto err_alloc_small_buf_failed;
	}
	buffer = proc->buffer;
	INIT_LIST_HEAD(&proc->buffers);//proc->buffers用来连接所有已建立映射的binder内存块
	list_add(&buffer->entry, &proc->buffers);
	buffer->free = 1;
	binder_insert_free_buffer(proc, buffer);//将刚分配的一个page大小的binder内存块插入到proc->free_buffers红黑树中
	proc->free_async_space = proc->buffer_size / 2;
	barrier();
	proc->files = get_files_struct(proc->tsk);
	proc->vma = vma;
	proc->vma_vm_mm = vma->vm_mm;
 
	/*printk(KERN_INFO "binder_mmap: %d %lx-%lx maps %p\n",
		 proc->pid, vma->vm_start, vma->vm_end, proc->buffer);*/
	return 0;
 
err_alloc_small_buf_failed:
	kfree(proc->pages);
	proc->pages = NULL;
err_alloc_pages_failed:
	mutex_lock(&binder_mmap_lock);
	vfree(proc->buffer);
	proc->buffer = NULL;
err_get_vm_area_failed:
err_already_mapped:
	mutex_unlock(&binder_mmap_lock);
err_bad_arg:
	printk(KERN_ERR "binder_mmap: %d %lx-%lx %s failed %d\n",
	       proc->pid, vma->vm_start, vma->vm_end, failure_string, ret);
	return ret;
}

下面binder_update_page_range函数就是为进程的内核空间和进程的用户空间针对同一块物理内存建立映射,这样进程的用户空间和内核空间就可以共享该物理内存了。

static int binder_update_page_range(struct binder_proc *proc, int allocate,
				    void *start, void *end,
				    struct vm_area_struct *vma)
{
	void *page_addr;
	unsigned long user_page_addr;
	struct vm_struct tmp_area;
	struct page **page;
	struct mm_struct *mm;
 
	binder_debug(BINDER_DEBUG_BUFFER_ALLOC,
		     "binder: %d: %s pages %p-%p\n", proc->pid,
		     allocate ? "allocate" : "free", start, end);
 
	if (end <= start)//表示已经建立过映射,不需再映射
		return 0;
 
	trace_binder_update_page_range(proc, allocate, start, end);
 
	if (vma)
		mm = NULL;
	else
		mm = get_task_mm(proc->tsk);
 
	if (mm) {
		down_write(&mm->mmap_sem);
		vma = proc->vma;
		if (vma && mm != proc->vma_vm_mm) {
			pr_err("binder: %d: vma mm and task mm mismatch\n",
				proc->pid);
			vma = NULL;
		}
	}
 
	if (allocate == 0)//表示拆除已经建立的映射
		goto free_range;
 
	if (vma == NULL) {
		printk(KERN_ERR "binder: %d: binder_alloc_buf failed to "
		       "map pages in userspace, no vma\n", proc->pid);
		goto err_no_vma;
	}
 
	for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
		int ret;
		struct page **page_array_ptr;
		page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];
 
		BUG_ON(*page);
		*page = alloc_page(GFP_KERNEL | __GFP_ZERO);//分配一个物理页,并将该物理页的struct page指针值存放在proc->pages二维数组中
		if (*page == NULL) {
			printk(KERN_ERR "binder: %d: binder_alloc_buf failed "
			       "for page at %p\n", proc->pid, page_addr);
			goto err_alloc_page_failed;
		}
		tmp_area.addr = page_addr;//映射对应的内核虚拟地址
		tmp_area.size = PAGE_SIZE + PAGE_SIZE /* guard page? */;//映射对应的大小
		page_array_ptr = page;//该物理页对应的struct page结构体,用这个结构体可以完全的描述这个物理页
		ret = map_vm_area(&tmp_area, PAGE_KERNEL, &page_array_ptr);//为内核的这段虚拟地址建立虚拟到物理页的映射
		if (ret) {
			printk(KERN_ERR "binder: %d: binder_alloc_buf failed "
			       "to map page at %p in kernel\n",
			       proc->pid, page_addr);
			goto err_map_kernel_failed;
		}
		user_page_addr =
			(uintptr_t)page_addr + proc->user_buffer_offset;//由内核的虚拟地址得到用户空间的虚拟地址
		ret = vm_insert_page(vma, user_page_addr, page[0]);//为应用空间的这段虚拟地址建立虚拟到物理的映射
		if (ret) {                     //至此应用空间和内核空间都映射到了同一块物理页内存
			printk(KERN_ERR "binder: %d: binder_alloc_buf failed "
			       "to map page at %lx in userspace\n",
			       proc->pid, user_page_addr);
			goto err_vm_insert_page_failed;
		}
		/* vm_insert_page does not seem to increment the refcount */
	}
	if (mm) {
		up_write(&mm->mmap_sem);
		mmput(mm);
	}
	return 0;
 
free_range://释放影射
	for (page_addr = end - PAGE_SIZE; page_addr >= start;
	     page_addr -= PAGE_SIZE) {
		page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];//得到该段虚拟地址对应的struct page结构体
		if (vma)
			zap_page_range(vma, (uintptr_t)page_addr +//拆除应用空间的映射表
				proc->user_buffer_offset, PAGE_SIZE, NULL);
err_vm_insert_page_failed://查出内核空间的映射表
		unmap_kernel_range((unsigned long)page_addr, PAGE_SIZE);
err_map_kernel_failed://释放对应的物理内存页,这样这块物理内存页又可以被系统其他地方使用了
		__free_page(*page);
		*page = NULL;
err_alloc_page_failed:
		;
	}
err_no_vma:
	if (mm) {
		up_write(&mm->mmap_sem);
		mmput(mm);
	}
	return -ENOMEM;
}

3. Client发送请求,这个请求将先放到驱动中,同时需要将数据从Client进程的用户空间拷贝(通过调用linux系统函数copy_from_user)到内核空间。

4. 驱动通过请求通知Server端有人发出请求,Server进行处理。由于内核空间和Server端进程的用户空间存在内存映射(它们都指向同一块物理内存),因此Server进程的代码可以直接访问。这样便完成了一次进程间的通信。

示例图:

四.  待更新

        由于笔者的工作的方向不是bsp层, 底层的源码看到比较蒙圈,   目前只能理解到这里了,

后面有新的正确理解再更新进来,  文章中如果有写的不对的地方请在评论区更正, 也欢迎一起探讨,

谢谢!

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

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

相关文章

程序的编译和链接

程序的编译和链接程序的编译和链接程序的两种环境翻译环境详解编译和链接预处理编译汇编链接运行环境程序的编译和链接 程序的两种环境 在ANSI C的任何一种实现中&#xff0c;存在两个不同的环境。 第1种是翻译环境&#xff0c;在这个环境中源代码被转换为可执行的机器指令。 …

《数据分析-JiMuReport04》JiMuReport报表设计入门介绍-页面优化

报表设计 2 页面优化 如上图所示的报表&#xff0c;仅仅是展示数据&#xff0c;不过这样看起来似乎太草率了&#xff0c;所以再优化一下吧 保存报表后&#xff0c;在积木报表中就可以看到对应的报表文件 此时我们如果还需要编辑报表&#xff0c;就点击这个报表即可 2.1 居中…

如何设计企业节点的『工业互联网标识解析系统』

一、『星火 链网』体系架构 『星火 链网』以节点形式进行组织互联互通&#xff0c;其中包括三类节点&#xff1a;超级节点、骨干节点、业务节点。 其底层采用“1N”主从链群架构&#xff0c;支持同构和异构区块链接入主链。在全国重点区域部署『星火 链网』超级节点&#…

three.js的demo例子-STL加载对象组件

three.js的demo例子-STL加载对象组件 提示&#xff1a;demo示例中所涉及到的three.js安装插件方法这里就不单个说明了哈&#xff0c;有需要的网上有很多教程 文章目录three.js的demo例子-STL加载对象组件效果展示插件模型一、HTML部分二、script部分1.引入库2.初始化数据3.监听…

卷王都在偷偷准备金三银四了...

年终奖没发&#xff1b; 简历石沉大海&#xff1b; 发消息只读不回 打开某招聘&#xff0c;看了看岗位&#xff0c;这个厂还不错&#xff0c;可是要求好高&#xff0c;我啥都不会。 “哎&#xff0c;算了&#xff0c;我简历还没更新呢&#xff0c;我躺到6月份拿到年终奖再跑…

【动态规划】多重背包问题,分组背包问题

Halo&#xff0c;这里是Ppeua。平时主要更新C语言&#xff0c;C&#xff0c;数据结构算法......感兴趣就关注我吧&#xff01;你定不会失望。 &#x1f308;个人主页&#xff1a;主页链接 &#x1f308;算法专栏&#xff1a;专栏链接 我会一直往里填充内容哒&#xff01; &…

名创优品业绩狂飙,手握哪些王牌?

撰稿 | 多客 来源 | 贝多财经 2023年注定是名创优品(NYSE:MNSO、HKEX:9896)发展史上具有重要意义的一年&#xff0c;不仅是创立的10周年&#xff0c;也是全球品牌战略升级的开局之年。 2月28日&#xff0c;名创优品公布了2023财年第二财季未经审计财务报告。得益于全球化战略…

机器学习笔记之狄利克雷过程(六)预测任务求解

机器学习笔记之狄利克雷过程——预测任务求解引言回顾&#xff1a;基于狄利克雷过程的预测过程预测任务的求解过程引言 上一节引出了基于狄利克雷过程的预测任务&#xff0c;本节将对该预测任务进行求解。 回顾&#xff1a;基于狄利克雷过程的预测过程 在已知隐变量样本集合…

Windows 环境安装Scala详情

为了进一步学习Spark&#xff0c;必须先学习Scala 编程语言。首先开始Scala 环境搭建。温馨提示&#xff1a;本文是基于Windows 11 安装Scala 2.13.1 版本第一步&#xff1a;确保本机已经正确安装JDK1.8 环境第二步&#xff1a;Scala 官网下载我们所属scala版本文件。Scala 官网…

JavaScript(WebAPI)

目录 1.什么是Web API? 2.DOM和DOM树 3.获取元素 4.事件 5.操作元素 获取/修改元素内容 1.innerText 2. innerHTML 获取/修改元素属性 获取/修改表单元素属性 获取/修改样式属性 1.修改内联样式 2.修改元素应用的CSS类名 6.操作节点 新增节点 删除节点 7.案例…

Vue3的composition API—setup函数, ref函数,reactive函数

1、Setup 函数 1.setup 是vue3中的一个配置项 2、setup是所有组件所需要的数据和方法都需要配置到setup中的 3、setup两种返回值&#xff1a; 若返回一个对象 若返回一个渲染函数 mian.js文件 注意&#xff1a;尽量不与Vue2混用 setup中无法访问vue2中的配置 不能是async函数…

Optional--Java8新特性最佳实践

Optional是在 Java8中引入的新特性之一。使用Optional类包装数据&#xff0c;可以避免经典的空检查和一些try-catch代码块。也能够通过链式方法调用&#xff0c;写出更流畅的函数式编程的代码。另一方面&#xff0c;滥用Optional也会导致性能低下和代码混乱。过往项目业务中有大…

【Linux】旋转锁 | 读写锁

在之前的线程学习中&#xff0c;用到的锁都是挂起等待锁&#xff0c;如果申请不到锁&#xff0c;那就会在锁中等待&#xff1b; 自旋锁则不大相似 文章目录1.自旋锁1.1 概念1.2 接口1.2.1 pthread_spin_init/destroy1.2.2 pthread_spin_lock1.2.3 pthread_spin_unlock2.读写锁…

VIO优化中不客观自由度 (gauge freedom) 的处理 (gauge handle)

文章目录1. 不可观的解释2. 几种不同的gauge handle处理方式2.1. free gauge方式2.2. fix gauge方式2.3. prior gauge方式2.4. g2o tutorial方式3.不同方式的协方差矩阵1. 不可观的解释 这篇论文 中对VIO的4-DOF不可观的定义如下&#xff0c;可以看到这种不可观就是如果对最后…

gerrit操作和jinkens编译合入代码

gerrit 先 查看自己的push 找到后添加reviewer 填写邮箱开头就可以出来 记得1 然后send 让人review 编译不过&#xff0c;gerrit上查看 1.是不是checkstyle问题 2.编译不过&#xff0c;去jinkens查看 先retrigger重新编译 如果发现多次编译失败 则要看下console output 查…

【ONE·Data || 顺序表】

总言 数据结构基础&#xff1a;顺序表模拟实现。    文章目录总言1、顺序表各接口功能实现描述1.1、如何创建一个顺序表&#xff1f;1.2、如何初始化顺序表&#xff1a;SLInit1.3、顺序表的尾插、头插1.3.1、顺序表尾插1.0&#xff1a;SLPushBack1.3.2、顺序表头插1.0&#x…

网络连接的三种模式

文章目录前言一、三种连接模式介绍二、三种网络连接模式的区别前言 在进行虚拟机配置时&#xff0c;网络连接分为三种模式&#xff1a;桥接模式&#xff0c;NAT模式&#xff0c;主机模式 一、三种连接模式介绍 张三、李四、王五在同一个网段&#xff0c;所以他们之间可以相互…

数据结构---双链表

专栏&#xff1a;数据结构 个人主页&#xff1a;HaiFan. 专栏简介&#xff1a;从零开始&#xff0c;数据结构&#xff01;&#xff01; 双链表前言双链表各接口的实现为要插入的值开辟一块空间BuyLN初始化LNInit和销毁LNDestory打印链表中的值LNPrint尾插LNPushBack和尾删LNPop…

vue2+elementUI完成添加学生删除学生案列

效果图&#xff1a; 点击添加学生按钮&#xff0c;弹出Dialog,收集用户信息&#xff1a; el-table中自定义复选框&#xff0c;选中一行&#xff0c;可以点击删除 代码区域&#xff1a;就一个HTML文件 <!DOCTYPE html> <html lang"en"> <head>&…

Flume基操

Flume概述 Flume 定义 Flume 是 Cloudera 提供的一个高可用的&#xff0c;高可靠的&#xff0c;分布式的海量日志采集、聚合和传输的系统。Flume 基于流式架构&#xff0c;灵活简单。 Flume最主要的作用就是&#xff0c;实时读取服务器本地磁盘的数据&#xff0c;将数据写入到…