对冗余挑拣重点,对重点深入补充,输出结构清晰的精简版
1.必备知识
设备驱动
文件描述符
页框
2.Binder 概述
3.binder 驱动
binder_open
binder_mmap
binder_ioctl
4.Service Manager
启动
注册与查询
5.最后
必备知识
设备驱动
Linux 把所有的硬件访问都抽象为对文件的读写、设置,这一"抽象"的具体实现就是驱动程序。驱动程序充当硬件和软件之间的枢纽,提供了一套标准化的调用,并将这些调用映射为实际硬件设备相关的操作,对应用程序来说隐藏了设备工作的细节。
Linux 设备分为三类,分别是字符设备、块设备和网络设备:
1.字符设备: 能够像字节流(类似文件)一样被访问的设备。对字符设备进行读/写操作时,实际硬件的 I/O 操作一般也紧接着发生。字符设备驱动程序通常都会实现 open、close、read 和 write 系统调用,比如触摸屏、键盘、串口、LCD、LED 等。
2.块设备: 指通过传输数据块(一般为 512 或 1k)来访问的设备,比如硬盘、SD卡、U盘、光盘等。
3.网络设备: 能够和其他主机交换数据的设备,比如网卡设备、蓝牙设备等。
通过 cat /proc/devices 命令可以查看字符设备和块设备:
Character devices:
1 mem
4 ttyS
10 misc
...
Block devices:
1 ramdisk
7 loop
8 sd
...
可以看到属于字符设备的 misc 杂项设备,设备号为 10。通过 ls /dev -l 命令可以查看具体的注册设备:
crw-rw-rw- 1 root root 10, 61 2020-03-16 16:52 ashmem
crw-rw-rw- 1 root root 10, 58 2020-03-16 16:52 binder
...
其中 Ashmem、Binder 的设备号是 10,都属于 misc 杂项设备,10 是 主设备号,61、58 叫做 从设备号,有了主、从设备号,就可以唯一标识一个设备。
文件描述符
Linux 中一切都可以看作文件,包括普通文件、链接文件、Socket 以及设备驱动等,对其进行相关操作时,都可能会创建对应的文件描述符。文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,用于指代被打开的文件,对文件所有 I/O 操作相关的系统调用都需要通过文件描述符。
文件描述符与文件是什么关系呢?下图 Linux 中的三张表可以体现:
-
进程级别的文件描述符表:内核为每个进程维护一个文件描述符表,该表记录了文件描述符的相关信息,包括文件描述符、指向打开文件表中记录的指针。
-
系统级别的打开文件表:内核对所有打开文件维护的一个进程共享的打开文件描述表,表中存储了处于打开状态文件的相关信息,包括文件类型、访问权限、文件操作函数(file_operations)等。
-
系统级别的 i-node 表:i-node 结构体记录了文件相关的信息,包括文件长度,文件所在设备,文件物理位置,创建、修改和更新时间等,“ls -i” 命令可以查看文件 i-node 节点
文件描述符是一种系统资源,可以通过以下命令来查看文件描述符的上限:
#查看所有进程允许打开的最大 fd 数量
126|generic_x86:/ # cat /proc/sys/fs/file-max
174139
#查看所有进程已经打开的 fd 数量以及允许的最大数量
generic_x86:/ # cat /proc/sys/fs/file-nr
11040 0 174139
#查看单个进程允许打开的最大 fd 数量.
generic_x86:/ # ulimit -n
32768
也可以查看某进程当前已使用的 fd :
#查看某进程(进程 id 为 15077)已经打开的 fd
generic_x86:/ # ls -l /proc/15077/fd/
total 0
lrwx------ 1 u0_a136 u0_a136 64 2020-04-15 23:04 0 -> /dev/null
lrwx------ 1 u0_a136 u0_a136 64 2020-04-15 23:04 1 -> /dev/null
lrwx------ 1 u0_a136 u0_a136 64 2020-04-15 23:04 35 -> /dev/binder
lrwx------ 1 u0_a136 u0_a136 64 2020-04-09 01:01 44 -> socket:[780404]
lrwx------ 1 u0_a136 u0_a136 64 2020-04-15 23:04 55 -> /dev/ashmem
lrwx------ 1 u0_a136 u0_a136 64 2020-04-15 23:04 60 -> /dev/ashmem
...
上面这个进程是一个 Android 应用进程,所以能看到 ashmem、binder 等 Android 特有设备文件相关的 fd 。再来看一个实际打开磁盘文件的例子:
File file = new File(getCacheDir(), "testFdFile");
FileOutputStream out = new FileOutputStream(file);
执行上面代码后会申请一个对应的 fd:
# ls -l /proc/{pid}/fd/
...
l-wx------ u0_a55 u0_a55 2020-04-16 00:24 995 -> /data/data/com.example.test/cache/testFdFile
...
实际开发中,可能会遇到 fd 资源超过上限导致的 “Too many open files” 之类的问题,一般都是因为没有及时释放掉 fd,比如上面代码中 FileOutputStream 没有关闭,若循环执行超过单个进程允许打开的最大 fd 数量,程序就会出现异常。
页框
页框(Page Frame)是指一块实际的物理内存块,页是指程序的一块内存数据单元。内存数据一定是存储在实际的物理内存上,即页必然对应于一个页框,页数据实际是存储在页框上的。
页框和页一样大,都是内核对内存的分块单位。一个页框可以映射给多个页,也就是说一块实际的物理存储空间可以映射给多个进程的多个虚拟内存空间,这也是 mmap 机制依赖的基础规则。
Binder 概述
不同进程处于不同的内存空间,具有不同的虚拟地址映射规则,所以不能直接通信。 Binder 是 Android 中使用最广泛的 IPC 机制,正因为有了 Binder,Android 系统中形形色色的进程与组件才能真正统一成有机的整体。Binder 通信机制与 TCP/IP 有共通之处,其组成元素可以这样来类比:
- binder 驱动 -> 路由器
- Service Manager -> DNS
- Binder Client -> 客户端
- Binder Server -> 服务器
Binder 的本质目标就是客户端要与服务器通信,但由于是不同的进程,必须通过 binder 驱动(路由器)把请求正确投递到对方进程中,所以通信的进程需要持有一个唯一的 Binder 标志(IP 地址)。
而 Binder 标志可能是会动态更新的 “IP 地址”,对通信进程来说获取难度较大且可读性差,这就需要一个 Service Manager(DNS)来解决这个问题。但 Service Manager 自身也是一个 Binder Server(服务器),怎么找到它的 "IP 地址"呢?Binder 机制对此做了特别规定:Service Manager 在 Binder 通信过程中的唯一标志永远是 0。
binder 驱动
binder 驱动运行在内核态,向上层提供 /dev/binder 设备节点,并不对应真实的硬件设备。binder 驱动的注册逻辑在 Binder.c 中:
//drivers/staging/android/Binder.c
static init __init binder_init(void){
...
ret = misc_register(&binder_miscdev); //注册为 misc 驱动
}
binder_miscdev 即 Binder 设备描述如下:
static struct miscdevice binder_miscdev = {
.minor = MISC_DYNAMIC_MINOR, //自动分配次设备号
.name = "binder", //驱动名称
.fops = &binder_fops //binder 驱动支持的文件操作
}
binder_fops 为 Binder 设备支持的操作函数,如下:
static const struct file_operations binder_fops = {
.owner = THIS_MODULE,
.poll = binder_poll,
.unlocked_ioctl = binder_ioctl,
.mmap = binder_mmap,
.open = binder_open,
.flush = binder_flush,
.release = binder_release,
};
与 Ashmem 设备类似,最关键的是 binder_open()、binder_mmap()、binder_ioctl(),下面分别介绍这三个函数。
binder_open
用户应用程序通过 Binder 通信时,需先调用 binder_open() 方法打开 binder 驱动,binder_open() 中主要做了两个工作,对应的分为两部分来看:
//binder.c
static int