前言
在做Framework
之前呢,从事应用层开发的时候,有时候也会用到AIDL
,了解到底层是用Binder
实现,看了网上关于Binder
的文章之后,整个人陷入了一个懵圈的状态,Binder
到底是个啥?首先确定的一点,我们知道Binder
是用来做跨进程通信使用的.那么我们知道跨线程通信用Handle
,跨进程通信的特点是什么呢?
跨进程通信 (Inter Process Communication)
在Linux
中,是进程隔离的(如果对这个概念不理解,需要先了解一下进程隔离的相关知识),还需要了解内核空间和用户空间的相关概念,了解了这些特性,就理解了跨进程通信在Linux
系统中重要性.
场景设定
很多文章首先比较了其他跨进程通信方式与Binder
的优劣.这里就不描述了.直接描述一下Binder
要解决的问题, 我们根据系统的状况设定一个场景.
前提
XX酒店为一个简单的系统.
- 1 客房为用户空间.
- 2 公共空间是内核空间.
- 3 住在客房之中的游客就是我们的应用.(严谨一些就是每一个进程都住着一件客房),为了对应进程隔离,居住在不同客房的用户不能在各自的客房中直接交流.需要通过一些工具.
需求
为了解决上述交流的问题,酒店向市面征求解决方案,解决各个客房之间可能存在的沟通问题,但是也做了以下的限制:
- 1 为了保证用户之间的安全,用户之间不能在各自的客房之中进行交流,在公共空间进行交流.
- 2 为了保证便捷,给需要交流的用户尽可能的方便,不需要用户在客房和公共空间之间来回奔波.
- 3 为了保证用户的隐私,不需要的用户不能被打扰.
我们的方案
我们看到了酒店的需求,就提出了我们自己的一套方案,大致方案如下
- 1 在公共区域架设一个大的交换机.
- 2 有通话需求的客户只需要告诉客服一声,我们就会将就会记录下来用户的信息,只要其他客户报出了该用户的信息就可以进行通话请求了.
- 3 不需要的用户只要没有主动告知,我们将不对其进行被动沟通需求,只保留他的主动沟通需求.
酒店看了我们方案觉得还行,就让我们提供一个具体的技术方案,让酒店领导观看,下面我们就去写一下这个具体的需求.
放在公共区域的交换机
首先我们要确定方案并不是实时通话需求,那么就意味着我们需要一个空间来存储不同的客户之间交换的信息.
- 1:提供一个空间(
/dev/binder
). - 2:提供一个打开这个空间的方式
binder_open
. - 3:因为用户是需要将信息存储在这里,我们在空间中在分配一块区域去存储信息
binder_mmap
准备工作(binder_init)
static int __init binder_init(void)
{
int ret;
//首先需要一个组来运行整个工作,使用了一个单线程的工作组 Linux
binder_deferred_workqueue = create_singlethread_workqueue("binder");
if (!binder_deferred_workqueue)
return -ENOMEM;
//内核调试相关, 文件目录在/sys/kernel/debug/
binder_debugfs_dir_entry_root = debugfs_create_dir("binder", NULL);
if (binder_debugfs_dir_entry_root)
//如果/sys/kernel/debug/binder/目录创建成功,在其之下创建子目录proc
binder_debugfs_dir_entry_proc = debugfs_create_dir("proc",
binder_debugfs_dir_entry_root);
//注册binder驱动设备,misc是miscellaneous的意思,表示杂项设备注册.
ret = misc_register(&binder_miscdev);
//binder目录存在,在创建下面这些子目录用作调试
if (binder_debugfs_dir_entry_root) {
debugfs_create_file("state",
S_IRUGO,
binder_debugfs_dir_entry_root,
NULL,
&binder_state_fops);
debugfs_create_file("stats",
S_IRUGO,
binder_debugfs_dir_entry_root,
NULL,
&binder_stats_fops);
debugfs_create_file("transactions",
S_IRUGO,
binder_debugfs_dir_entry_root,
NULL,
&binder_transactions_fops);
debugfs_create_file("transaction_log",
S_IRUGO,
binder_debugfs_dir_entry_root,
&binder_transaction_log,
&binder_transaction_log_fops);
debugfs_create_file("failed_transaction_log",
S_IRUGO,
binder_debugfs_dir_entry_root,
&binder_transaction_log_failed,
&binder_transaction_log_fops);
}
return ret;
}
下面是注册binder
驱动设备时候传入的结构体.
//定义了不同文件操作,对应的函数
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,
};
static struct miscdevice binder_miscdev = {
.minor = MISC_DYNAMIC_MINOR,// 次设备号
.name = "binder", // 设备名称
.fops = &binder_fops //文件操作定义的集合,上面的自定义结构体
};
这一段我们可以理解为,我们设计了我们整个通信系统的最基本的设计,填写了一些报表,在酒店注册了一些信息和必须的准备.
申请空间(binder_open)
做好了上面一系列的准备工作,我们在酒店中也注册了我们的通信系统,也申请了一个工作组来做具体的维护工作,就提供了一个最进本
static int binder_open(struct inode *nodp, struct file *filp)
{
//创建一个binder进程 ,binder_proc 是一个自定义结构体
struct binder_proc *proc;
//宏定义函数binder_debug ,最终调用了pr_info去输出内核调试日志
binder_debug(BINDER_DEBUG_OPEN_CLOSE, "binder_open: %d:%d\n",
current->group_leader->pid, current->pid);
//给上面定义的binder进程申请一块内存,GFP_KERNEL是内核申请内存的一种方式.
proc = kzalloc(sizeof(*proc), GFP_KERNEL);
if (proc == NULL)
return -ENOMEM;
/*current这个变量是一个很突兀的变量,查了一下,这个是内核定义的一个宏函数get_current().
获取的是当前进程结构的指针.*/
//获取当前进程的task_struct ,Linux使用task_struct结构体中来管理进程,简单了解即可
get_task_struct(current);
//将当前线程的task_struct赋值给binder进程中的tsk
proc->tsk = current;
//内核宏函数,todo是一个list_head,初始化列表
INIT_LIST_HEAD(&proc->todo);
/*初始化等待队列,void init_waitqueue_head(wait_queue_head_t *q) 函数原型,&proc->wait的数据结
构是一个wait_queue_head_t,*/
init_waitqueue_head(&proc->wait);
//将当前进程的优先级设置为binder进程的优先级
proc->default_priority = task_nice(current);
//设置一个同步锁
binder_lock(__func__);
// BINDER_STAT_PROC类型+1,计数器,BINDER_STAT_PROC是一个枚举对象中的值
binder_stats_created(BINDER_STAT_PROC);
// 设置proc_node为链表的头部
hlist_add_head(&proc->proc_node, &binder_procs);
// 设置pid
proc->pid = current->group_leader->pid;
//初始化已经发送的死亡通知列表
INIT_LIST_HEAD(&proc->delivered_death);
//file文件的private_data指针指向binder进程.
filp->private_data = proc;
//释放锁
binder_unlock(__func__);
// 调试相关 sys/kernel/debug/binder目录存在
if (binder_debugfs_dir_entry_proc) {
char strbuf[11];
//打印信息
snprintf(strbuf, sizeof(strbuf), "%u", proc->pid);
//将binder进程的debugfs_entry指针指向建的测试文件
proc->debugfs_entry = debugfs_create_file(strbuf, S_IRUGO,
binder_debugfs_dir_entry_proc, proc, &binder_proc_fops);
}
return 0;
}
上面的代码我们可以理解为,我们正式在酒店的公共空间中申请了一块地盘来,在在里面我们做了基本的初始化工作.
给用户分配空间(binder_mmap)
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;
// current 前面介绍过,是当前进程的指针,判断传入的进程信息是否一致,不一致,就return
if (proc->tsk != current)
return -EINVAL;
//Linux内核使用vm_area_struct结构表示一个独立的虚拟内存区域, 虚拟内存大小小于4M,就设置为4M
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));
// 设置权限,禁止写入.下面会将vm_area_struct的结构代码贴出来,
if (vma->vm_flags & FORBIDDEN_MMAP_FLAGS) {
ret = -EPERM;
failure_string = "bad vm_flags";
goto err_bad_arg;
}
// 禁止copy,禁止写入
vma->vm_flags = (vma->vm_flags | VM_DONTCOPY) & ~VM_MAYWRITE;
// 同步锁
mutex_lock(&binder_mmap_lock);
// binder进程的buffer为null,就转错误处理
if (proc->buffer) {
ret = -EBUSY;
failure_string = "already mapped";
goto err_already_mapped;
}
// 申请一个与进程虚拟地址大小的内核虚拟空间,采用IOREMAP,可以映射
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);
// 内核配置,CPU CACHE是否支持VIPT
#ifdef CONFIG_CPU_CACHE_VIPT
if (cache_is_vipt_aliasing()) {
while (CACHE_COLOUR((vma->vm_start ^ (uint32_t)proc->buffer))) {
pr_info("binder_mmap: %d %lx-%lx maps %pK bad alignment\n", proc->pid, vma->vm_start, vma->vm_end, proc->buffer);
vma->vm_start += PAGE_SIZE;
}
}
#endif
// 内存虚拟空间大小是否小于 binder最小分配限制
if (vma->vm_end - vma->vm_start < BINDER_MIN_ALLOC) {
ret = -EINVAL;
failure_string = "VMA size < BINDER_MIN_ALLOC";
goto err_vma_too_small;
}
//分配物理页的指针数组,数组大小为vma的等效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;
}
//设置buffer长度
proc->buffer_size = vma->vm_end - vma->vm_start;
//设置文件映射函数合集 , binder_vm_ops 定义了open close fault
vma->vm_ops = & binder_vm_ops;
//将 vm_private_data指向 binder进程,vm_private_data,用于指向进程的 用户虚拟地址空间中的私有数据, 查到的内容,并不一定准确.
vma->vm_private_data = proc;
// 分配物理页面,1是申请,0是释放
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;
}
// 函数开头的binder_buffer 指向 proc中的buffer
buffer = proc->buffer;
// 前文有出现 ,初始化列表
INIT_LIST_HEAD(&proc->buffers);
//在buffers中 插入buffer
list_add(&buffer->entry, &proc->buffers);
//标记binder_buffer为空闲的
buffer->free = 1;
//将binder_buffer添加到proc
binder_insert_free_buffer(proc, buffer);
//异步可用空间大小为buffer总大小的一半。
proc->free_async_space = proc->buffer_size / 2;
//内存屏障,防止乱序
barrier();
//将binder_proc的文件结构指向当前进程的文件结构
proc->files = get_files_struct(current);
//设置binder_proc的虚拟地址指向
proc->vma = vma;
//设置binder_proc的虚拟内存结构
proc->vma_vm_mm = vma->vm_mm;
/*pr_info("binder_mmap: %d %lx-%lx maps %pK\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:
err_vma_too_small:
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:
pr_err("binder_mmap: %d %lx-%lx %s failed %d\n",
proc->pid, vma->vm_start, vma->vm_end, failure_string, ret);
return ret;
}
上面整个过程中出现了2个非常重要的结构体和一个非常重要的函数,binder_proc
,binder_buffer
和binder_update_page_range()
函数,binder_proc
在open
中就已经存在,只是binder_proc
和binder_buffer
有比较强的关联,我们将它们放在一起进行讲述.
binder_proc
从名称也能猜到,这个是用来描述binder
进程的,它的第一次出现是在binder_open
函数中,也就是当每一个进程去打开binder
驱动时,就会创建一个对应的binder_proc
来存储当前的进程的进程信息.
struct binder_proc {
//存储进程节点
struct hlist_node proc_node;
//关联binder_threads中的 rb_node中的红黑树节点
struct rb_root threads;
//关联binder_node中的rb_node中的红黑树节点
struct rb_root nodes;
//使用句柄来排序的红黑树
struct rb_root refs_by_desc;
//使用binder实体的地址来排序
struct rb_root refs_by_node;
// 进程ID
int pid;
// 进程的虚拟地址空间
struct vm_area_struct *vma;
// 进程的内存结构体
struct mm_struct *vma_vm_mm;
// 进程的task结构体
struct task_struct *tsk;
// 进程的文件结构体
struct files_struct *files;
struct hlist_node deferred_work_node;
int deferred_work;
//进程内核虚拟内存的起始地址,和进程的虚拟地址映射在驱动中的同一物理页
void *buffer;
//和进程虚拟地址的差值
ptrdiff_t user_buffer_offset;
//关联binder_buffer
struct list_head buffers;
//空闲内存
struct rb_root free_buffers;
//已分配内存
struct rb_root allocated_buffers;
//异步的可用空闲空间大小
size_t free_async_space;
// 指向物理内存页指针的指针
struct page **pages;
//映射内核内存的大小
size_t buffer_size;
// 可用内存大小
uint32_t buffer_free;
// 进程将要做的事
struct list_head todo;
// 等待队列
wait_queue_head_t wait;
// binder统计信息
struct binder_stats stats;
// 已分发的死亡通知
struct list_head delivered_death;
// 最大线程数
int max_threads;
// 请求的线程数
int requested_threads;
// 已启动的请求线程数
int requested_threads_started;
// 准备就绪的线程个数
int ready_threads;
// 默认优先级
long default_priority;
struct dentry *debugfs_entry;
};
binder_buffer
通信中binder
驱动中每一段的内存使用binder_buffer
用来描述.也就是binder_proc
中的buffer
关联.
struct binder_buffer {
struct list_head entry; /* free and allocated entries by address */
struct rb_node rb_node; /* free entry by size or allocated entry */
/* by address */
//是否空闲
unsigned free:1;
//是否允许释放
unsigned allow_user_free:1;
unsigned async_transaction:1;
unsigned debug_id:29;
struct binder_transaction *transaction;
//该缓存区所需处理的Binder实体
struct binder_node *target_node;
//数据大小
size_t data_size;
//数据偏移量
size_t offsets_size;
//数据地址
uint8_t data[0];
};
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,
"%d: %s pages %pK-%pK\n", proc->pid,
allocate ? "allocate" : "free", start, end);
// 申请的结束地址小于开始地址,直接return
if (end <= start)
return 0;
trace_binder_update_page_range(proc, allocate, start, end);
// binder_mmap vma 不为空
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("%d: vma mm and task mm mismatch\n",
proc->pid);
vma = NULL;
}
}
// 0代表释放,1代表申请
if (allocate == 0)
goto free_range;
if (vma == NULL) {
pr_err("%d: binder_alloc_buf failed to map pages in userspace, no vma\n",
proc->pid);
goto err_no_vma;
}
// 开始循环按照PAGE_SIZE 分配实际的物理页内存,每次分配一个
for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
int ret;
page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];
BUG_ON(*page);
// alloc_page() linux分配物理页
*page = alloc_page(GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO);
if (*page == NULL) {
pr_err("%d: binder_alloc_buf failed for page at %pK\n",
proc->pid, page_addr);
goto err_alloc_page_failed;
}
tmp_area.addr = page_addr;
tmp_area.size = PAGE_SIZE + PAGE_SIZE /* guard page? */;
//建立内核虚拟地址和物理页面的映射关系
ret = map_vm_area(&tmp_area, PAGE_KERNEL, page);
if (ret) {
pr_err("%d: binder_alloc_buf failed to map page at %pK 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);
}
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];
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;
}
小结
到这里,我们先分析上面所有的代码都做了什么,
- 1: 首先初始化了
binder
驱动设备.初始化一个名称为binder
的workqueue
.驱动设备中的fops
指向了binder_fops
// 定义了对应的操作的函数.其中open和mmap前面已经介绍过了.
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,
};
-
2: 假如当有外部进程使用
binder
进行通信,那么首先要调用open
函数,创建了一个binder_proc
实例.并将其加入到队列之中.前面也说过binder_proc
就是一个binder进程. -
3: 给对应的通信进程赋予一定的空间,这里mmap是内存映射.这里申请了一块物理内存,然后将内核虚拟地址和用户虚拟地址映射到同一块物理内存上(最大申请4M)
-
内存映射: 举个例子描述一下内存映射,酒店的每一个客房都是一个实际存在的实体.对应着计算机中存储空间中的实际物理内存,那么我们假如通过不同的网络平台定出去20个房间,对应的房间都有一个编码(对于着我们的常用的虚拟的内存地址),那么这时候一个虚拟的房间编码正常来说就对应这一个具体的房间.这个就是一个简单的内存映射.
-
4: 经过上面的全部准备,接下来就是使用我们的系统进行通信了,在驱动设备的
fops
中也看到了不同的ioctl
方式对于的函数都是binder_ioctl
.那么下一章节就来具体描述对于的通信细节. -
感受: 在读整个代码的时候有大量的linux知识,查找对应的细节花费的时间比看
binder
其本身的代码花费了更多的时间.不过也为了避免陷入细节无法自拔,基本上通过man
查看了对于的linux
函数之后,并没有继续深入linux
的实现细节.