Binder的实现是比较复杂的,想要完全弄明白是怎么一回事,并不是一件容易的事情。

这里面牵涉到好几个层次,每一层都有一些模块和机制需要理解。这部分内容预计会分为三篇文章来讲解。本文是第一篇,首先会对整个Binder机制做一个架构性的讲解,然后会将大部分精力用来讲解Binder机制中最核心的部分:Binder驱动的实现。

Binder机制简介

Binder源自Be Inc公司开发的OpenBinder框架,后来该框架转移的Palm Inc,由Dianne Hackborn主导开发。OpenBinder的内核部分已经合入Linux Kernel 3.19。

Android Binder是在OpneBinder上的定制实现。原先的OpenBinder框架现在已经不再继续开发,可以说Android上的Binder让原先的OpneBinder得到了重生。

Binder是Android系统中大量使用的IPC(Inter-process communication,进程间通讯)机制。无论是应用程序对系统服务的请求,还是应用程序自身提供对外服务,都需要使用到Binder。

因此,Binder机制在Android系统中的地位非常重要,可以说,理解Binder是理解Android系统的绝对必要前提。

在Unix/Linux环境下,传统的IPC机制包括:

  • 管道
  • 消息队列
  • 共享内存
  • 信号量
  • Socket

等。

由于篇幅所限,本文不会对这些IPC机制做讲解,有兴趣的读者可以参阅《UNIX网络编程 卷2:进程间通信》。

Android系统中对于传统的IPC使用较少(但也有使用,例如:在请求Zygote fork进程的时候使用的是Socket IPC),大部分场景下使用的IPC都是Binder。

Binder相较于传统IPC来说更适合于Android系统,具体原因的包括如下三点:

  1. Binder本身是C/S架构的,这一点更符合Android系统的架构
  2. 性能上更有优势:管道,消息队列,Socket的通讯都需要两次数据拷贝,而Binder只需要一次。要知道,对于系统底层的IPC形式,少一次数据拷贝,对整体性能的影响是非常之大的
  3. 安全性更好:传统IPC形式,无法得到对方的身份标识(UID/GID),而在使用Binder IPC时,这些身份标示是跟随调用过程而自动传递的。Server端很容易就可以知道Client端的身份,非常便于做安全检查

整体架构

Binder整体架构如下所示:

从图中可以看出,Binder的实现分为这么几层:

  • Framework层
    • Java部分
    • JNI部分
    • C++部分
  • 驱动层

驱动层位于Linux内核中,它提供了最底层的数据传递,对象标识,线程管理,调用过程控制等功能。驱动层是整个Binder机制的核心

Framework层以驱动层为基础,提供了应用开发的基础设施。

Framework层既包含了C++部分的实现,也包含了Java部分的实现。为了能将C++的实现复用到Java端,中间通过JNI进行衔接。

开发者可以在Framework之上利用Binder提供的机制来进行具体的业务逻辑开发。其实不仅仅是第三方开发者,Android系统中本身也包含了很多系统服务都是基于Binder框架开发的。

既然是“进程间”通讯就至少牵涉到两个进程,Binder框架是典型的C/S架构。在下文中,我们把服务的请求方称之为Client,服务的实现方称之为Server。

Client对于Server的请求会经由Binder框架由上至下传递到内核的Binder驱动中,请求中包含了Client将要调用的命令和参数。请求到了Binder驱动之后,在确定了服务的提供方之后,会再从下至上将请求传递给具体的服务。整个调用过程如下图所示:

对网络协议有所了解的读者会发现,这个数据的传递过程和网络协议是如此的相似。

初识ServiceManager

前面已经提到,使用Binder框架的既包括系统服务,也包括第三方应用。因此,在同一时刻,系统中会有大量的Server同时存在。那么,Client在请求Server的时候,是如果确定请求发送给哪一个Server的呢?

这个问题,就和我们现实生活中如何找到一个公司/商场,如何确定一个人/一辆车一样,解决的方法就是:每个目标对象都需要一个唯一的标识。并且,需要有一个组织来管理这个唯一的标识。

而Binder框架中负责管理这个标识的就是ServiceManager。ServiceManager对于Binder Server的管理就好比车管所对于车牌号码的的管理,派出所对于身份证号码的管理:每个公开对外提供服务的Server都需要注册到ServiceManager中(通过addService),注册的时候需要指定一个唯一的id(这个id其实就是一个字符串)。

Client要对Server发出请求,就必须知道服务端的id。Client需要先根据Server的id通过ServerManager拿到Server的标示(通过getService),然后通过这个标示与Server进行通信。

整个过程如下图所示:

如果上面这些介绍已经让你一头雾水,请不要过分担心,下面会详细讲解这其中的细节。

下文会以自下而上的方式来讲解Binder框架。自下而上未必是最好的方法,每个人的思考方式不一样,如果你更喜欢自上而下的理解,你也按这样的顺序来阅读。

对于大部分人来说,我们可能需要反复的查阅才能完全理解。

驱动层

源码路径(这部分代码不在AOSP中,而是位于Linux内核代码中):

/kernel/drivers/android/binder.c
/kernel/include/uapi/linux/android/binder.h

或者

/kernel/drivers/staging/android/binder.c
/kernel/drivers/staging/android/uapi/binder.h

Binder机制的实现中,最核心的就是Binder驱动。 Binder是一个miscellaneous类型的驱动,本身不对应任何硬件,所有的操作都在软件层。 binder_init函数负责Binder驱动的初始化工作,该函数中大部分代码是在通过debugfs_create_dirdebugfs_create_file函数创建debugfs对应的文件。 如果内核在编译时打开了debugfs,则通过adb shell连上设备之后,可以在设备的这个路径找到debugfs对应的文件:/sys/kernel/debug。Binder驱动中创建的debug文件如下所示:

# ls -l /sys/kernel/debug/binder/                                     
total 0
-r--r--r-- 1 root root 0 1970-01-01 00:00 failed_transaction_log
drwxr-xr-x 2 root root 0 1970-05-09 01:19 proc
-r--r--r-- 1 root root 0 1970-01-01 00:00 state
-r--r--r-- 1 root root 0 1970-01-01 00:00 stats
-r--r--r-- 1 root root 0 1970-01-01 00:00 transaction_log
-r--r--r-- 1 root root 0 1970-01-01 00:00 transactions

这些文件其实都在内存中的,实时的反应了当前Binder的使用情况,在实际的开发过程中,这些信息可以帮忙分析问题。例如,可以通过查看/sys/kernel/debug/binder/proc目录来确定哪些进程正在使用Binder,通过查看transaction_logtransactions文件来确定Binder通信的数据。

binder_init函数中最主要的工作其实下面这行:

ret = misc_register(&binder_miscdev);

该行代码真正向内核中注册了Binder设备。binder_miscdev的定义如下:

static struct miscdevice binder_miscdev = {.minor = MISC_DYNAMIC_MINOR,.name = "binder",.fops = &binder_fops
};

这里指定了Binder设备的名称是“binder”。这样,在用户空间便可以通过对/dev/binder文件进行操作来使用Binder。

binder_miscdev同时也指定了该设备的fops。fops是另外一个结构体,这个结构中包含了一系列的函数指针,其定义如下:

static const struct file_operations binder_fops = {.owner = THIS_MODULE,.poll = binder_poll,.unlocked_ioctl = binder_ioctl,.compat_ioctl = binder_ioctl,.mmap = binder_mmap,.open = binder_open,.flush = binder_flush,.release = binder_release,
};

这里除了owner之外,每一个字段都是一个函数指针,这些函数指针对应了用户空间在使用Binder设备时的操作。例如:binder_poll对应了poll系统调用的处理,binder_mmap对应了mmap系统调用的处理,其他类同。

这其中,有三个函数尤为重要,它们是:binder_openbinder_mmapbinder_ioctl。 这是因为,需要使用Binder的进程,几乎总是先通过binder_open打开Binder设备,然后通过binder_mmap进行内存映射。

在这之后,通过binder_ioctl来进行实际的操作。Client对于Server端的请求,以及Server对于Client请求结果的返回,都是通过ioctl完成的。

这里提到的流程如下图所示:

主要结构

Binder驱动中包含了很多的结构体。为了便于下文讲解,这里我们先对这些结构体做一些介绍。

驱动中的结构体可以分为两类:

一类是与用户空间共用的,这些结构体在Binder通信协议过程中会用到。因此,这些结构体定义在binder.h中,包括:

结构体名称 说明
flat_binder_object 描述在Binder IPC中传递的对象,见下文
binder_write_read 存储一次读写操作的数据
binder_version 存储Binder的版本号
transaction_flags 描述事务的flag,例如是否是异步请求,是否支持fd
binder_transaction_data 存储一次事务的数据
binder_ptr_cookie 包含了一个指针和一个cookie
binder_handle_cookie 包含了一个句柄和一个cookie
binder_pri_desc 暂未用到
binder_pri_ptr_cookie 暂未用到

这其中,binder_write_readbinder_transaction_data这两个结构体最为重要,它们存储了IPC调用过程中的数据。关于这一点,我们在下文中会讲解。

Binder驱动中,还有一类结构体是仅仅Binder驱动内部实现过程中需要的,它们定义在binder.c中,包括:

结构体名称 说明
binder_node 描述Binder实体节点,即:对应了一个Server
binder_ref 描述对于Binder实体的引用
binder_buffer 描述Binder通信过程中存储数据的Buffer
binder_proc 描述使用Binder的进程
binder_thread 描述使用Binder的线程
binder_work 描述通信过程中的一项任务
binder_transaction 描述一次事务的相关信息
binder_deferred_state 描述延迟任务
binder_ref_death 描述Binder实体死亡的信息
binder_transaction_log debugfs日志
binder_transaction_log_entry debugfs日志条目

这里需要读者关注的结构体已经用加粗做了标注。

Binder协议

Binder协议可以分为控制协议和驱动协议两类。

控制协议是进程通过ioctl(“/dev/binder”) 与Binder设备进行通讯的协议,该协议包含以下几种命令:

命令 说明 参数类型
BINDER_WRITE_READ 读写操作,最常用的命令。IPC过程就是通过这个命令进行数据传递 binder_write_read
BINDER_SET_MAX_THREADS 设置进程支持的最大线程数量 size_t
BINDER_SET_CONTEXT_MGR 设置自身为ServiceManager
BINDER_THREAD_EXIT 通知驱动Binder线程退出
BINDER_VERSION 获取Binder驱动的版本号 binder_version
BINDER_SET_IDLE_PRIORITY 暂未用到 -
BINDER_SET_IDLE_TIMEOUT 暂未用到 -

Binder的驱动协议描述了对于Binder驱动的具体使用过程。驱动协议又可以分为两类:

  • 一类是binder_driver_command_protocol,描述了进程发送给Binder驱动的命令
  • 一类是binder_driver_return_protocol,描述了Binder驱动发送给进程的命令

binder_driver_command_protocol共包含17个命令,分别是:

命令 说明 参数类型
BC_TRANSACTION Binder事务,即:Client对于Server的请求 binder_transaction_data
BC_REPLY 事务的应答,即:Server对于Client的回复 binder_transaction_data
BC_FREE_BUFFER 通知驱动释放Buffer binder_uintptr_t
BC_ACQUIRE 强引用计数+1 __u32
BC_RELEASE 强引用计数-1 __u32
BC_INCREFS 弱引用计数+1 __u32
BC_DECREFS 弱引用计数-1 __u32
BC_ACQUIRE_DONE BR_ACQUIRE的回复 binder_ptr_cookie
BC_INCREFS_DONE BR_INCREFS的回复 binder_ptr_cookie
BC_ENTER_LOOPER 通知驱动主线程ready void
BC_REGISTER_LOOPER 通知驱动子线程ready void
BC_EXIT_LOOPER 通知驱动线程已经退出 void
BC_REQUEST_DEATH_NOTIFICATION 请求接收死亡通知 binder_handle_cookie
BC_CLEAR_DEATH_NOTIFICATION 去除接收死亡通知 binder_handle_cookie
BC_DEAD_BINDER_DONE 已经处理完死亡通知 binder_uintptr_t
BC_ATTEMPT_ACQUIRE 暂未实现 -
BC_ACQUIRE_RESULT 暂未实现 -

binder_driver_return_protocol共包含18个命令,分别是:

返回类型 说明 参数类型
BR_OK 操作完成 void
BR_NOOP 操作完成 void
BR_ERROR 发生错误 __s32
BR_TRANSACTION 通知进程收到一次Binder请求(Server端) binder_transaction_data
BR_REPLY 通知进程收到Binder请求的回复(Client) binder_transaction_data
BR_TRANSACTION_COMPLETE 驱动对于接受请求的确认回复 void
BR_FAILED_REPLY 告知发送方通信目标不存在 void
BR_SPAWN_LOOPER 通知Binder进程创建一个新的线程 void
BR_ACQUIRE 强引用计数+1请求 binder_ptr_cookie
BR_RELEASE 强引用计数-1请求 binder_ptr_cookie
BR_INCREFS 弱引用计数+1请求 binder_ptr_cookie
BR_DECREFS 若引用计数-1请求 binder_ptr_cookie
BR_DEAD_BINDER 发送死亡通知 binder_uintptr_t
BR_CLEAR_DEATH_NOTIFICATION_DONE 清理死亡通知完成 binder_uintptr_t
BR_DEAD_REPLY 告知发送方对方已经死亡 void
BR_ACQUIRE_RESULT 暂未实现 -
BR_ATTEMPT_ACQUIRE 暂未实现 -
BR_FINISHED 暂未实现 -

单独看上面的协议可能很难理解,这里我们以一次Binder请求过程来详细看一下Binder协议是如何通信的,就比较好理解了。

这幅图的说明如下:

  • Binder是C/S架构的,通信过程牵涉到:Client,Server以及Binder驱动三个角色
  • Client对于Server的请求以及Server对于Client回复都需要通过Binder驱动来中转数据
  • BC_XXX命令是进程发送给驱动的命令
  • BR_XXX命令是驱动发送给进程的命令
  • 整个通信过程由Binder驱动控制

这里再补充说明一下,通过上面的Binder协议的说明中我们看到,Binder协议的通信过程中,不仅仅是发送请求和接受数据这些命令。同时包括了对于引用计数的管理和对于死亡通知的管理(告知一方,通讯的另外一方已经死亡)等功能。

这些功能的通信过程和上面这幅图是类似的:一方发送BC_XXX,然后由驱动控制通信过程,接着发送对应的BR_XXX命令给通信的另外一方。因为这种相似性,对于这些内容就不再赘述了。

在有了上面这些背景知识介绍之后,我们就可以进入到Binder驱动的内部实现中来一探究竟了。

PS:上面介绍的这些结构体和协议,因为内容较多,初次看完记不住是很正常的,在下文详细讲解的时候,回过头来对照这些表格来理解是比较有帮助的。

打开Binder设备

任何进程在使用Binder之前,都需要先通过open("/dev/binder")打开Binder设备。上文已经提到,用户空间的open系统调用对应了驱动中的binder_open函数。在这个函数,Binder驱动会为调用的进程做一些初始化工作。binder_open函数代码如下所示:

static int binder_open(struct inode *nodp, struct file *filp)
{struct binder_proc *proc;// 创建进程对应的binder_proc对象
	proc = kzalloc(sizeof(*proc), GFP_KERNEL); if (proc == NULL)return -ENOMEM;get_task_struct(current);proc->tsk = current;// 初始化binder_proc
	INIT_LIST_HEAD(&proc->todo);init_waitqueue_head(&proc->wait);proc->default_priority = task_nice(current);// 锁保护
	binder_lock(__func__);binder_stats_created(BINDER_STAT_PROC);// 添加到全局列表binder_procs中
	hlist_add_head(&proc->proc_node, &binder_procs);proc->pid = current->group_leader->pid;INIT_LIST_HEAD(&proc->delivered_death);filp->private_data = proc;binder_unlock(__func__);return 0;
}

在Binder驱动中,通过binder_procs记录了所有使用Binder的进程。每个初次打开Binder设备的进程都会被添加到这个列表中的。

另外,请读者回顾一下上文介绍的Binder驱动中的几个关键结构体:

  • binder_proc
  • binder_node
  • binder_thread
  • binder_ref
  • binder_buffer

在实现过程中,为了便于查找,这些结构体互相之间都留有字段存储关联的结构。

下面这幅图描述了这里说到的这些内容:

内存映射(mmap)

在打开Binder设备之后,进程还会通过mmap进行内存映射。mmap的作用有如下两个:

  • 申请一块内存空间,用来接收Binder通信过程中的数据
  • 对这块内存进行地址映射,以便将来访问

binder_mmap函数对应了mmap系统调用的处理,这个函数也是Binder驱动的精华所在(这里说的binder_mmap函数也包括其内部调用的binder_update_page_range函数,见下文)。

前文我们说到,使用Binder机制,数据只需要经历一次拷贝就可以了,其原理就在这个函数中。

binder_mmap这个函数中,会申请一块物理内存,然后在用户空间和内核空间同时对应到这块内存上。在这之后,当有Client要发送数据给Server的时候,只需一次,将Client发送过来的数据拷贝到Server端的内核空间指定的内存地址即可,由于这个内存地址在服务端已经同时映射到用户空间,因此无需再做一次复制,Server即可直接访问,整个过程如下图所示:

这幅图的说明如下:

  1. Server在启动之后,调用对/dev/binder设备调用mmap
  2. 内核中的binder_mmap函数进行对应的处理:申请一块物理内存,然后在用户空间和内核空间同时进行映射
  3. Client通过BINDER_WRITE_READ命令发送请求,这个请求将先到驱动中,同时需要将数据从Client进程的用户空间拷贝到内核空间
  4. 驱动通过BR_TRANSACTION通知Server有人发出请求,Server进行处理。由于这块内存也在用户空间进行了映射,因此Server进程的代码可以直接访问

了解原理之后,我们再来看一下Binder驱动的相关源码。这段代码有两个函数:

  • binder_mmap函数对应了mmap的系统调用的处理
  • binder_update_page_range函数真正实现了内存分配和地址映射
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;...// 在内核空间获取一块地址范围
	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;}proc->buffer = area->addr;// 记录内核空间与用户空间的地址偏移
	proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;mutex_unlock(&binder_mmap_lock);...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;vma->vm_ops = &binder_vm_ops;vma->vm_private_data = proc;/* binder_update_page_range assumes preemption is disabled */preempt_disable();// 通过下面这个函数真正完成内存的申请和地址的映射
	// 初次使用,先申请一个PAGE_SIZE大小的内存
	ret = binder_update_page_range(proc, 1, proc->buffer, proc->buffer + PAGE_SIZE, vma);...
}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;...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_HIGHMEM | __GFP_ZERO);if (*page == NULL) {pr_err("%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;// 在内核空间进行内存映射
		ret = map_vm_area(&tmp_area, PAGE_KERNEL, &page_array_ptr);if (ret) {pr_err("%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) {pr_err("%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);}preempt_disable();return 0;
...	

在开发过程中,我们可以通过procfs看到进程映射的这块内存空间:

  1. 将Android设备连接到电脑上之后,通过adb shell进入到终端
  2. 然后选择一个使用了Binder的进程,例如system_server(这是系统中一个非常重要的进程,下一章我们会专门讲解),通过 ps | grep system_server来确定进程号,例如是1889
  3. 通过 cat /proc/[pid]/maps | grep "/dev/binder" 过滤出这块内存的地址

在我的Nexus 6P上,控制台输出如下:

angler:/ # ps  | grep system_server                                          
system    1889  526   2353404 140016 SyS_epoll_ 72972eeaf4 S system_server
angler:/ # cat /proc/1889/maps | grep "/dev/binder"                            
7294761000-729485f000 r--p 00000000 00:0c 12593                          /dev/binder

PS:grep是通过通配符进行匹配过滤的命令,“|”是Unix上的管道命令。即将前一个命令的输出给下一个命令作为输入。如果这里我们不加“ | grep xxx”,那么将看到前一个命令的完整输出。

内存的管理

上文中,我们看到binder_mmap的时候,会申请一个PAGE_SIZE(通常是4K)的内存。而实际使用过程中,一个PAGE_SIZE的大小通常是不够的。

在驱动中,会根据实际的使用情况进行内存的分配。有内存的分配,当然也需要内存的释放。这里我们就来看看Binder驱动中是如何进行内存的管理的。

首先,我们还是从一次IPC请求说起。

当一个Client想要对Server发出请求时,它首先将请求发送到Binder设备上,由Binder驱动根据请求的信息找到对应的目标节点,然后将请求数据传递过去。

进程通过ioctl系统调用来发出请求:ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr)

PS:这行代码来自于Framework层的IPCThreadState类。在后文中,我们将看到,IPCThreadState类专门负责与驱动进行通信

这里的mProcess->mDriverFD对应了打开Binder设备时的fd。BINDER_WRITE_READ对应了具体要做的操作码,这个操作码将由Binder驱动解析。bwr存储了请求数据,其类型是binder_write_read

binder_write_read其实是一个相对外层的数据结构,其内部会包含一个binder_transaction_data结构的数据。binder_transaction_data包含了发出请求者的标识,请求的目标对象以及请求所需要的参数。它们的关系如下图所示:

binder_ioctl函数对应了ioctl系统调用的处理。这个函数的逻辑比较简单,就是根据ioctl的命令来确定进一步处理的逻辑,具体如下:

  • 如果命令是BINDER_WRITE_READ,并且
    • 如果 bwr.write_size > 0,则调用binder_thread_write
    • 如果 bwr.read_size > 0,则调用binder_thread_read
  • 如果命令是BINDER_SET_MAX_THREADS,则设置进程的max_threads,即进程支持的最大线程数
  • 如果命令是BINDER_SET_CONTEXT_MGR,则设置当前进程为ServiceManager,见下文
  • 如果命令是BINDER_THREAD_EXIT,则调用binder_free_thread,释放binder_thread
  • 如果命令是BINDER_VERSION,则返回当前的Binder版本号

这其中,最关键的就是binder_thread_write方法。当Client请求Server的时候,便会发送一个BINDER_WRITE_READ命令,同时框架会将将实际的数据包装好。此时,binder_transaction_data中的code将是BC_TRANSACTION,由此便会调用到binder_transaction方法,这个方法是对一次Binder事务的处理,这其中会调用binder_alloc_buf函数为此次事务申请一个缓存。这里提到到调用关系如下:

binder_update_page_range这个函数在上文中,我们已经看到过了。其作用就是:进行内存分配并且完成内存的映射。而binder_alloc_buf函数,正如其名称那样的:完成缓存的分配。

在驱动中,通过binder_buffer结构体描述缓存。一次Binder事务就会对应一个binder_buffer,其结构如下所示:

struct binder_buffer {struct list_head entry;struct rb_node rb_node;unsigned free:1;unsigned allow_user_free:1;unsigned async_transaction:1;unsigned debug_id:29;struct binder_transaction *transaction;struct binder_node *target_node;size_t data_size;size_t offsets_size;uint8_t data[0];
};

而在binder_proc(描述了使用Binder的进程)中,包含了几个字段用来管理进程在Binder IPC过程中缓存,如下:

struct binder_proc {...struct list_head buffers; // 进程拥有的buffer列表
	struct rb_root free_buffers; // 空闲buffer列表
	struct rb_root allocated_buffers; // 已使用的buffer列表 
	size_t free_async_space; // 剩余的异步调用的空间
	size_t buffer_size; // 缓存的上限
  ...
};

进程在mmap时,会设定支持的总缓存大小的上限(下文会讲到)。而进程每当收到BC_TRANSACTION,就会判断已使用缓存加本次申请的和有没有超过上限。如果没有,就考虑进行内存的分配。

进程的空闲缓存记录在binder_proc的free_buffers中,这是一个以红黑树形式存储的结构。每次尝试分配缓存的时候,会从这里面按大小顺序进行查找,找到最接近需要的一块缓存。查找的逻辑如下:

while (n) {buffer = rb_entry(n, struct binder_buffer, rb_node);BUG_ON(!buffer->free);buffer_size = binder_buffer_size(proc, buffer);if (size < buffer_size) {best_fit = n;n = n->rb_left;} else if (size > buffer_size)n = n->rb_right;else {best_fit = n;break;}
}

找到之后,还需要对binder_proc中的字段进行相应的更新:

rb_erase(best_fit, &proc->free_buffers);
buffer->free = 0;
binder_insert_allocated_buffer(proc, buffer);
if (buffer_size != size) {struct binder_buffer *new_buffer = (void *)buffer->data + size;list_add(&new_buffer->entry, &buffer->entry);new_buffer->free = 1;binder_insert_free_buffer(proc, new_buffer);
}
binder_debug(BINDER_DEBUG_BUFFER_ALLOC,"%d: binder_alloc_buf size %zd got %p\n",proc->pid, size, buffer);
buffer->data_size = data_size;
buffer->offsets_size = offsets_size;
buffer->async_transaction = is_async;
if (is_async) {proc->free_async_space -= size + sizeof(struct binder_buffer);binder_debug(BINDER_DEBUG_BUFFER_ALLOC_ASYNC,"%d: binder_alloc_buf size %zd async free %zd\n",proc->pid, size, proc->free_async_space);
}

下面我们再来看看内存的释放。

BC_FREE_BUFFER命令是通知驱动进行内存的释放,binder_free_buf函数是真正实现的逻辑,这个函数与binder_alloc_buf是刚好对应的。在这个函数中,所做的事情包括:

  • 重新计算进程的空闲缓存大小
  • 通过binder_update_page_range释放内存
  • 更新binder_proc的buffers,free_buffers,allocated_buffers字段

Binder中的“面向对象”

Binder机制淡化了进程的边界,使得跨越进程也能够调用到指定服务的方法,其原因是因为Binder机制在底层处理了在进程间的“对象”传递。

在Binder驱动中,并不是真的将对象在进程间来回序列化,而是通过特定的标识来进行对象的传递。Binder驱动中,通过flat_binder_object来描述需要跨越进程传递的对象。其定义如下:

struct flat_binder_object {__u32		type;__u32		flags;union {binder_uintptr_t	binder; /* local object */__u32			handle;	/* remote object */};binder_uintptr_t	cookie;
};

这其中,type有如下5种类型。

enum {BINDER_TYPE_BINDER	= B_PACK_CHARS('s', 'b', '*', B_TYPE_LARGE),BINDER_TYPE_WEAK_BINDER	= B_PACK_CHARS('w', 'b', '*', B_TYPE_LARGE),BINDER_TYPE_HANDLE	= B_PACK_CHARS('s', 'h', '*', B_TYPE_LARGE),BINDER_TYPE_WEAK_HANDLE	= B_PACK_CHARS('w', 'h', '*', B_TYPE_LARGE),BINDER_TYPE_FD		= B_PACK_CHARS('f', 'd', '*', B_TYPE_LARGE),
};

当对象传递到Binder驱动中的时候,由驱动来进行翻译和解释,然后传递到接收的进程。

例如当Server把Binder实体传递给Client时,在发送数据流中,flat_binder_object中的type是BINDER_TYPE_BINDER,同时binder字段指向Server进程用户空间地址。但这个地址对于Client进程是没有意义的(Linux中,每个进程的地址空间是互相隔离的),驱动必须对数据流中的flat_binder_object做相应的翻译:将type该成BINDER_TYPE_HANDLE;为这个Binder在接收进程中创建位于内核中的引用并将引用号填入handle中。对于发生数据流中引用类型的Binder也要做同样转换。经过处理后接收进程从数据流中取得的Binder引用才是有效的,才可以将其填入数据包binder_transaction_data的target.handle域,向Binder实体发送请求。

由于每个请求和请求的返回都会经历内核的翻译,因此这个过程从进程的角度来看是完全透明的。进程完全不用感知这个过程,就好像对象真的在进程间来回传递一样。

驱动层的线程管理

上文多次提到,Binder本身是C/S架构。由Server提供服务,被Client使用。既然是C/S架构,就可能存在多个Client会同时访问Server的情况。 在这种情况下,如果Server只有一个线程处理响应,就会导致客户端的请求可能需要排队而导致响应过慢的现象发生。解决这个问题的方法就是引入多线程。

Binder机制的设计从最底层–驱动层,就考虑到了对于多线程的支持。具体内容如下:

  • 使用Binder的进程在启动之后,通过BINDER_SET_MAX_THREADS告知驱动其支持的最大线程数量
  • 驱动会对线程进行管理。在binder_proc结构中,这些字段记录了进程中线程的信息:max_threads,requested_threads,requested_threads_started,ready_threads
  • binder_thread结构对应了Binder进程中的线程
  • 驱动通过BR_SPAWN_LOOPER命令告知进程需要创建一个新的线程
  • 进程通过BC_ENTER_LOOPER命令告知驱动其主线程已经ready
  • 进程通过BC_REGISTER_LOOPER命令告知驱动其子线程(非主线程)已经ready
  • 进程通过BC_EXIT_LOOPER命令告知驱动其线程将要退出
  • 在线程退出之后,通过BINDER_THREAD_EXIT告知Binder驱动。驱动将对应的binder_thread对象销毁

再聊ServiceManager

上文已经说过,每一个Binder Server在驱动中会有一个binder_node进行对应。同时,Binder驱动会负责在进程间传递服务对象,并负责底层的转换。另外,我们也提到,每一个Binder服务都需要有一个唯一的名称。由ServiceManager来管理这些服务的注册和查找。

而实际上,为了便于使用,ServiceManager本身也实现为一个Server对象。任何进程在使用ServiceManager的时候,都需要先拿到指向它的标识。然后通过这个标识来使用ServiceManager。

这似乎形成了一个互相矛盾的现象:

  1. 通过ServiceManager我们才能拿到Server的标识
  2. ServiceManager本身也是一个Server

解决这个矛盾的办法其实也很简单:Binder机制为ServiceManager预留了一个特殊的位置。这个位置是预先定好的,任何想要使用ServiceManager的进程只要通过这个特定的位置就可以访问到ServiceManager了(而不用再通过ServiceManager的接口)。

在Binder驱动中,有一个全局的变量:

static struct binder_node *binder_context_mgr_node;

这个变量指向的就是ServiceManager。

当有进程通过ioctl并指定命令为BINDER_SET_CONTEXT_MGR的时候,驱动被认定这个进程是ServiceManager,binder_ioctl函数中对应的处理如下:

case BINDER_SET_CONTEXT_MGR:if (binder_context_mgr_node != NULL) {pr_err("BINDER_SET_CONTEXT_MGR already set\n");ret = -EBUSY;goto err;}ret = security_binder_set_context_mgr(proc->tsk);if (ret < 0)goto err;if (uid_valid(binder_context_mgr_uid)) {if (!uid_eq(binder_context_mgr_uid, current->cred->euid)) {pr_err("BINDER_SET_CONTEXT_MGR bad uid %d != %d\n",from_kuid(&init_user_ns, current->cred->euid),from_kuid(&init_user_ns, binder_context_mgr_uid));   ret = -EPERM;goto err;}} elsebinder_context_mgr_uid = current->cred->euid;binder_context_mgr_node = binder_new_node(proc, 0, 0);if (binder_context_mgr_node == NULL) {ret = -ENOMEM;goto err;}binder_context_mgr_node->local_weak_refs++;binder_context_mgr_node->local_strong_refs++;binder_context_mgr_node->has_strong_ref = 1;binder_context_mgr_node->has_weak_ref = 1;break;

ServiceManager应当要先于所有Binder Server之前启动。在它启动完成并告知Binder驱动之后,驱动便设定好了这个特定的节点。

在这之后,当有其他模块想要使用ServerManager的时候,只要将请求指向ServiceManager所在的位置即可。

在Binder驱动中,通过handle = 0这个位置来访问ServiceManager。例如,binder_transaction中,判断如果target.handler为0,则认为这个请求是发送给ServiceManager的,相关代码如下:

if (tr->target.handle) {struct binder_ref *ref;ref = binder_get_ref(proc, tr->target.handle, true);if (ref == NULL) {binder_user_error("%d:%d got transaction to invalid handle\n",proc->pid, thread->pid);return_error = BR_FAILED_REPLY;goto err_invalid_target_handle;}target_node = ref->node;
} else {target_node = binder_context_mgr_node;if (target_node == NULL) {return_error = BR_DEAD_REPLY;goto err_no_context_mgr_node;}
}

结束语

本篇文章中,我们对Binder机制做了整体架构和分层的介绍,也详细讲解了Binder机制中的驱动模块。对于驱动之上的模块,会在今后的文章中讲解。

下一篇文章中,我们会详细讲解Android Binder机制的Framework层,敬请期待。


原文:http://qiangbo.space/2017-01-15/AndroidAnatomy_Binder_Driver/

查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. 微信小程序,大多数人都搞错的八个问题

    声明:本文为CSDN原创投稿文章。 作者:王安,数字天堂DCloud公司创始人兼CEO 责编:陈秋歌,关注微信开发等领域,寻求报道或者投稿请发邮件chenqg#csdn.net。研发心得、项目实战、前沿技术、外文翻译……,只要是技术干货,十分欢迎投稿至chenqg#csdn.net。人人都是主编,这…...

    2024/4/17 4:52:42
  2. 张钹、高文、杨强同台论道“AI精度与隐私的博弈

    2020-06-23 22:08:21作者 | 陈大鑫、青暮编辑 | 丛 末AI时代,大众是不是真的就没有隐私了呢?以联邦学习为代表的新兴AI技术,能否实现AI协作,提升模型精度的同时实现数据隐私的保护。那么中国如何抢占人工智能安全发展的制高点?下一个10年中人工智能又将何去何从?就上述话…...

    2024/5/2 12:45:02
  3. ToolTip用HTML实现

    第一种方式:适合于全部HTML标签 <acronym title="World Wide Web"> WWW </acronym> 第二种方式:IMG之类的标签例如: <img src="#" alt="鼠标移动到上方会显示并且未加载图片时也会显示"> 第三种方式:input之类的标签例如: <…...

    2024/4/20 11:34:25
  4. socket 网络编程快速入门(二)教你编写基于UDP/TCP的服务端多线程通信

    在上一篇博文中,我们介绍了利用socket进行简单的UDP/TCP的服务端和客户端的通信。 (一) 在基于UDP的程序中,你有没有想过,如果我的这台主机在通讯的时候要求既能够收到别的主机发来的数据,又能够自己向目的主机发出数据,该怎样实现?也就是说需要两个while循环同时进行。…...

    2024/4/17 4:53:48
  5. Hive从入门到精通系列之--0.Hive概述和安装环境

    一 Hive概述Hive是由Facebook开源用于解决海量结构化日志的数据统计Hive是基于Hadoop框架的一个数据仓库分析工具,这里对于数据仓库,何为数据仓库?mysql,orical,sqlserver我们叫这些为数据库,所谓数据仓库就是在这个数据仓库中这些数据库都包含了进去。Hive既然是基于Had…...

    2024/4/17 4:53:36
  6. 微信小程序:动画效果集合

    Life is like riding a bicycle.To keep your balance you must keep moving.生活就像骑自行车。为了保持平衡,你必须不断前进。微信小程序:心跳动画 https://blog.csdn.net/wtdask/article/details/82734944文字跑马灯效果: http://www.wxapp-union.com/portal.php?mod=…...

    2024/3/31 21:03:44
  7. Android Binder机制浅析

    摘要Binder是android中一个很重要且很复杂的概念,它在系统的整体运作中发挥着极其重要的作用,不过本文并不打算从深层次分析Binder机制,有两点原因:1是目前网上已经有2篇很好的文章了,2是对Binder机制进行深入底层乃至驱动的分析这一过程相当困难且相当耗时,因此并不适合…...

    2024/5/2 14:59:29
  8. 书新编ASP .NET+SQL Server 2005从入门到精通(1CD)(双色印刷)的评论

    新编ASP .NET+SQL Server 2005从入门到精通(1CD)(双色印刷) 当当上购买 当当网全场免运费!!关于新编ASP .NET+SQL Server 2005从入门到精通(1CD)(双色印刷) 评论读后感:对初学者,这种树书你会不知道他在写什么,对于想提高的他又太肤浅了。文笔太生硬,通篇都是绕口的概念…...

    2024/4/20 8:11:12
  9. 【转载】2019 年,国内博士后的招聘要求和待遇是怎样的?

    笔者这大半年也在申请帝都各高校的博士后,虽然是文科吧,但是形势很严峻吧,简要谈一下。1.现在进入北京、上海的名校任教非常难,北京只要是211/985高校就必须得是博士后,而且从去年开始博士后也有了三个规定,第一必须是全职的,第二必须35岁以下(个别要求严苛的要求32岁以…...

    2024/5/2 12:47:48
  10. input 标签中text和button在同一行内不对齐解决方法

    1.问题css样式:<style>#keywords{height: 36px;line-height: 36px;width: 500px;border: 1px solid #b0b0b0;border-right:0px ;}#keywords:focus{border: 1px solid rgb(71,145,255);border-right:0px ;}input[type="button"]{height: 40px;line-height: 40p…...

    2024/4/20 11:14:18
  11. 微信小程序动画效果集合

    一期一期的整,假如以下内容中,有已经无法使用的部分,欢迎指出;假如你有你觉得可以加入特殊效果范围的文章或帖子,或是你有自己制作的特殊效果,欢迎回复分享; 文字跑马灯效果:http://www.wxapp-union.com/portal.php?mod=view&aid=1038 触摸水波涟漪效果:http://w…...

    2024/4/17 4:53:36
  12. 人工智能其实就是『八卦』

    1956年,几位计算机科学家在达特茅斯会议(Dartmouth Conferences)上提出了“人工智能”的概念,关于人工智能的研究和应用自此开始慢慢孵化。近几年深度学习的研究和发展把人工智能又推到了前所未有的高度。机器问答、智能客服、合同智能审阅等人工智能产品走近了企业和大众的…...

    2024/4/17 4:54:06
  13. 在flask_wtforms中自定义button标签

    在flask_wtforms没有原生的<input type=button>的标签参考:http://blog.csdn.net/tiwoo/article/details/46038249该博客我自己修改了下 可以解决在quick_form中会有div标签的问题;from wtforms.widgets import HTMLString, html_params from wtforms import Fieldclass …...

    2024/4/17 4:52:54
  14. Android Binder机制原理

    Android Binder机制原理说起Binder,其实在我们开发中总是没有感觉到它的存在,但是往往在很多地方都间接的在使用它,比如我们再startAcivity的时候,必定要去调用ActivityManagerService,因为我们都知道Activity并不是我们直接new出来的,而是通过这个service创建出来的,然…...

    2024/5/2 7:48:40
  15. 微服务架构从入门到精通(三)微服务架构模式

    用Scale Cube方法设计应用架构,将应用服务按功能拆分成一组相互协作的服务。每个服务负责一组特定、相关的功能。每个服务可以有自己独立的数据库,从而保证与其他服务解耦。1.1 聚合器微服务设计模式聚合器调用多个服务实现应用程序所需的功能。它可以是一个简单的Web页面,将…...

    2024/4/17 4:55:12
  16. 【Socket编程】篇六之IO多路复用——select、poll、epoll

    文章参考自:http://blog.csdn.net/tennysonsky/article/details/45745887(秋叶原 — Mike VS 麦克《Linux系统编程——I/O多路复用select、poll、epoll的区别使用》) 此外,还有一篇好文:epoll机制:epoll_create、epoll_ctl、epoll_wait、close(鱼思故渊的专栏)在上一篇中…...

    2024/4/27 22:57:16
  17. 介绍一款强大的框架-------一套代码生成两端应用【h5应用】--【小程序】

    Touch WX 是什么? Touch WX是一套完全免费的微信小程序开发框架,包含丰富的UI控件用于官方组件的补充。与WeTouch和Touch UI开发模式保持了很大的一致性。 与其他小程序框架最主要的区别在于:Touch WX完全是基于小程序官方的自定义组件机制实现,输出的是小程序原始代码,而…...

    2024/4/17 4:55:00
  18. Silverlight 2.5D RPG游戏技巧与特效处理:(十一)AI系统

    谈到人工智能(AI),这个话题就太大了;大学里有《人工智能教程》专门讲这方面的知识,什么大名鼎鼎的人工神经网络、遗传算法等等均可一窥究竟,这里如赘述似乎有些班门弄斧,我们暂且丢它一边去吧。 本节,我的主要目的是与大家共同探讨AI在RPG游戏中的应用。看过之前教程的朋…...

    2024/4/17 4:55:06
  19. 隐藏按钮显示

    if($.trim($("#feeType").val())=="zj"){$("#goonpay").css("display","inline");}$("feeType").val() 为一个input text 的值,...

    2024/4/17 4:55:00
  20. Binder机制原理分析(Java层)

    1、背景谈到Binder相信大家肯定都有所感触吧,我们平时肯定或多或少的接触一些,但是在分析Android源码之前我对其也是一头雾水,在网络上看到的关于Binder的文章也是似懂非懂,因为目前网络上关于Binder的文章大部分都是从C开始讲起的,对于我这个应用上层的人来说根本就没有心…...

    2024/4/20 13:38:06

最新文章

  1. Hive优化以及相关参数设置

    1.表层面设计优化 1.1 表分区 分区表实际上就是对应一个 HDFS 文件系统上的独立的文件夹&#xff0c;该文件夹下是该分区所有的数据文件。Hive 中的分区就是分目录&#xff0c;把一个大的数据集根据业务需要分割成小的数据集。在查询时通过 WHERE 子句中的表达式选择查询所需要…...

    2024/5/2 19:59:46
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. Linux中的错误处理艺术:理解错误编号与头文件引用

    引言 在Linux环境下开发程序时&#xff0c;准确且有效地处理错误是至关重要的。操作系统通过错误编号&#xff08;Error Number&#xff09;向程序反馈各种异常情况&#xff0c;帮助开发者定位和修复问题。本文将深入探讨Linux错误处理机制&#xff0c;并介绍如何查找特定错误…...

    2024/4/30 17:33:51
  4. 第十二届蓝桥杯省赛真题(C/C++大学B组)

    目录 #A 空间 #B 卡片 #C 直线 #D 货物摆放 #E 路径 #F 时间显示 #G 砝码称重 #H 杨辉三角形 #I 双向排序 #J 括号序列 #A 空间 #include <bits/stdc.h> using namespace std;int main() {cout<<256 * 1024 * 1024 / 4<<endl;return 0; } #B 卡片…...

    2024/5/2 10:47:22
  5. #QT项目实战(天气预报)

    1.IDE&#xff1a;QTCreator 2.实验&#xff1a; 3.记录&#xff1a; &#xff08;1&#xff09;调用API的Url a.调用API获取IP whois.pconline.com.cn/ipJson.jsp?iphttp://whois.pconline.com.cn/ipJson.jsp?ip if(window.IPCallBack) {IPCallBack({"ip":&quo…...

    2024/5/1 13:42:00
  6. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/5/1 17:30:59
  7. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/5/2 16:16:39
  8. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/4/29 2:29:43
  9. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/5/2 9:28:15
  10. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/4/27 17:58:04
  11. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/4/27 14:22:49
  12. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/4/28 1:28:33
  13. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/4/30 9:43:09
  14. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/4/27 17:59:30
  15. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/5/2 15:04:34
  16. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/4/28 1:34:08
  17. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/4/26 19:03:37
  18. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/4/29 20:46:55
  19. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/4/30 22:21:04
  20. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/5/1 4:32:01
  21. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/4/27 23:24:42
  22. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/4/28 5:48:52
  23. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/4/30 9:42:22
  24. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/5/2 9:07:46
  25. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/4/30 9:42:49
  26. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  27. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  28. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  29. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  30. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  31. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  32. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  33. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  34. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  35. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  36. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  37. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  38. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  39. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  40. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  41. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  42. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  43. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  44. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  45. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57