- 是否了解Linux常用的跨进程通信方式
android很多底层实现,
都依赖于Linux的操作系统调用; - 是否研究过Android Framework并了解一些实现原理
- 是否了解Framework各组件之间的通信原理
很多组件基本都是要跨进程的,
跨进程通讯并不全是用Binder机制;
主要关注三个层面
- 列举用到哪些IPC方式
- 各个IPC方式的特点
- Framework中是怎么用到的
Android中主要用到的Linux IPC方式
- 管道
- Socket
- 共享内存
- 信号
管道通信
-
半双工的,单向的
管道的描述符
数据只能往一个方向流,要么读要么写,
如果需要既能读又能写,则需要给管道有两个描述符
;
不过Linux给了我们一个APIpipe(fds)
,
这个API可以生成一对描述符
,
一个用来写一个用来读; -
一般
无名管道
是在父子进程
之间使用的;
有名管道
只要两个进程都知道这个管道的名字
就可以通信了;
看一个例子
-
通过
pipe
调用,生成管道的一对描述符;
fd[1]是用来写的; fd[0]是用来读的;
通过fork()
调用创建一个子进程;
子进程会继承这对描述符; -
现在我们要父进程往子进程里面写东西,
首先,把子进程写描述符关闭
,把子进程写描述符关闭
;
接着,父进程往写描述符
里边写一个字符串
;
然后,子进程就可以从读描述符
里边把这个字符串
给读出来
;
if(pid == 0){
close(fd[1]);//把子进程写描述符关闭
//子进程就可以从读描述符里边把这个字符串给读出来
read(fd[0], buf, SIZE);
}else if(pid > 0){
close(fd[0]);//把子进程写描述符关闭
write(fd[1], "Hello", 5);//父进程往写描述符里边写一个字符串
}
概念图如下,
我们可以看到数据流的方向是
父进程写描述符fd[1]--管道--子进程读描述符fd[0]
,
即,我们刚刚所说的半双工设计
:
Framework中哪儿用到了管道
-
Android 4.4
中的MQ机制
中的重要元素Looper
,用到了管道
(更高的版本如Android 6.0
用的就不是管道了): - 代码大概实现:
前四行是为管道配置一对读写描述符
;
后半部分是注册一个监听事件
,
监听读描述符的读事件
,即eventItem.data.fd = mWakeReadPipeFd;
这个时候如果有另一个线程拿到写描述符
并往里面写东西的话,
读端就能收到通知了;
相关阅读
epoll机制有一套函数,共三个,如下
创建epoll句柄:
1. int epfd =epoll_create(intsize);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
将被监听的描述符(对应例程中mWakeReadPipeFd
)
添加
到epoll句柄
(对应例程中mEpollFd
)
或从epool句柄中删除
或者对监听事件进行修改
(添加、删除和修改通过op
位参数进行控制)
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
注意最后一句话,
该函数返回需要处理的事件数目,即几个事件被触发了,
第二个参数events列表用来接收存入触发的事件;
接着看epoll是怎么监听读端事件的
-
通过
epoll_wait
得到触发的事件列表及其数量; -
for循环中遍历
触发事件列表
,
遍历到事件的fd是刚刚设置的读描述符的(mWakeReadPipeFd
),
及其事件是刚刚设定的读事件的(EPOLLIN
),
则调用awoken()
,把管道中的东西读出来,
管道满了就写不进去了,所以二话不说先读出来; -
不管管道里面写了什么东西,
只要写了东西,这个线程
就能被唤醒,就能去处理消息; -
当别的线程 要往这个Looper线程里面写东西的时候,
就通过wake()函数往管道里边写东西:
小结
管道使用起来还是比较方便的,
它可以跟epoll相结合监听读写事件;
管道在进程自身中可以用,
跨进程也可以用;
在数据量不怎么大的跨进程通信的时候还是比较有用的;
Socket通信
- 这里说的不是网络上的socket,而是本地的;
- 全双工,既可读又可写;
- 可以用在两个无亲缘关系的进程之间的通信
- socket在创建的时候需要指定一个路径,
只要把路径公开给别的进程,
别的进程就可以过来通信;
Framework中哪里用到
-
在Zygote模块,
通过socket接收请求,然后启动应用进程;
- main函数是Zygote的logo函数,
registerZygoteSocket()
创建了一个本地的Socket,
socketName通过argv接收Socket的名字,
接着进入runSelectLoop()
,一个循环,
检测这个socket有没有新过来的连接或者是数据;
我们看runSelectLoop()
:
- 这里是一个循环,其中,
poll()
用来监测有没有我们关注的事件发生,
如果有的话,可能会有两种情况,
第一种,是可能会有新的连接
;
第二种,就是有新的数据
发过来,
这时候可以调用runOnce()
来处理数据(先从Socket中
把参数读
出来,
根据参数去执行响应的指令,主要是创建应用进程,
应用进程启动之后,通过Socket
把pid写
给对方)
所以我们可以看到Socket其实很方便,
它即可以读又可以写;
共享内存
-
很快,不需要多次拷贝;
拿到一个文件描述符(文件引用句柄),即把它映射到两个进程的内存空间,
这样,一个进程往里边写,另一个进程就能读到,运行速度非常快;
相对的,管道和Socket传输的数据量都不能太大,
如果太大的话性能会很糟,因为里边涉及到至少两次拷贝; -
进程之间不用存在亲缘关系;
只需要能拿到文件描述符就好了;
文件描述符可以跨进程
传递;
Android中哪里用到
- Android中涉及到进程之间大数据量传输的主要就是
图像相关的传输
; - 这里主要以
MemoryFile
为例,
这是Android的一个工具类,
封装了内存共享机制Ashmem,也就是匿名共享内存;
MemoryFile是android在最开始就引入的一套框架,其内部实际上是封装了android特有的内存共享机制Ashmem匿名共享内存,简单来说,Ashmem在Android内核中是被注册成一个特殊的字符设备,Ashmem驱动通过在内核的一个自定义slab缓冲区中初始化一段内存区域,然后通过mmap把申请的内存映射到用户的进程空间中(通过tmpfs),这样子就可以在用户进程中使用这里申请的内存了。
-
MemoryFile的实现,
通过下面这个方法首先创建了一块匿名共享内存: native_open方法会返回一个描述符(句柄);
首先是调用native_open方法,
这个方法在native层其实是
-
接下来调用native_mmap函数,
这个函数在native层调用下面这个方法 把描述符mFD映射到当前进程的内存空间,
内存空间的地址则返回过来,即例程中的mAddress;
- 下面是MemoryFile的读和写
-
读函数
就是把数据
从共享内存
读
到应用层的buff
中,
SetByteArrayRegion()
就是把native
的buff数据
拷到java数据流的; -
写函数
则与读函数
相反,
就是把数据
从应用层的buff
拷到共享内存
中
GetByteArrayRegion()
就是把java数据流中的数据
拷到native
的buff
的;
信号
- 单向的,只负责发出去,不接受回复的,
怎么处理,处理没有,处理结果怎么样都不管,是别人的事 - 只能带个信号,不能带别的参数
- 知道进程pid就能发信号,
可以一次给一群进程发信号 - 必须是root权限才能发信号,
或者本进程跟另一进程的Userid
相同,
本进程才能发信号;
Android中哪里用到
例1
- 有时候要kill应用进程,
就会调用Proces的killProces函数,
这个函数中其实就是给进程发送了一个SIGNAL_KILL
信号,
pid参数位就是要杀掉的进程pid;
当然不是想杀就能杀,同样这里是有权限控制
的,
比如说本进程跟另一进程的Userid
相同,
本进程才能发信号,杀掉另一个进程;
《开发艺术探索》中有一段类似的描述
- 虽然我们的应用进程都是从Zygote那fork出来的,
UID都是默认和Zygote相同的,
但是进程启动之后,
就会马上重新设置自己的UID的,
所以基于任意进程是不可以随便给别的进程发信号
的;
zygote翻译成中文是受精卵的意思,名字比较奇怪、但是很有意思,
zygote在android中主要有两个作用:
建立运行时环境并启动虚拟机,为应用程序创建DVM进程。
执行com.android.internal.os.ZygoteInit的main函数,
从而fork SystemService。
类似参考文章
例2
- 以上这个函数是Zygote用来关注信号的;
启动子进程之后,它需要关注子进程退出了没有,
如果子进程退出了,Zygote就要及时把子进程的资源给回收掉;
网友评论