通信是Android开发必不可少的一部分,不管是我们做应用App开发,还是Android系统,都使用了大量的通信。通信又分为进程间通信和进程内通信,在这篇文章,我主要深入讲解Android系统所涉及到的所有进程间通信方式。
Android系统中有大量IPC(进程间通信)的场景,比如我们想要创建一个新的进程,需要通过Socket这种IPC方式去让Zygote Fork新进程;如果我们要杀掉一个进程,需要通过信号这种IPC方式去将SIGNAL_KILL信号传递到系统内核;如果我们想要唤醒主线程处于休眠中的Looper,需要管道这种IPC方式来唤醒;我们想要在应用开发中使用AIDL,广播或者Messager等方式来进行跨进程通信,其实底层都是使用了Binder这种IPC方式。
那么,Android到底有多少种进程间通信的方式呢?什么样的场景要选择什么样的通信方式呢?这些IPC通信方式怎么使用呢?这些IPC通信的底层原理又是什么呢?看完这篇文章,你就能回答这几个问题了。
我们先通过下面一览Android系统所具有的IPC通信方式
可以看到,Android所拥有的IPC总共有这些:
- 基于Unix系统的IPC的管道,FIFO,信号
- 基于SystemV和Posix系统的IPC的消息队列,信号量,共享内存
- 基于Socket的IPC
- Linux的内存映射函数mmap()
- Linux 2.6.22版本后才有的eventfd
- Android系统独有的Binder和匿名共享内存Ashmen
下面,我会详细介绍这些IPC的通信机制。
管道
PIPE和FIFO的使用及原理
PIPE和FIFO都是指管道,只是PIPE独指匿名管道,FIFO独指有名管道,我们先看一下管道的数据结构以及他们的使用方式:
//匿名管道(PIPE)
#include <unistd.h>
int pipe (int fd[2]); //创建pipe
ssize_t write(int fd, const void *buf, size_t count); //写数据
ssize_t read(int fd, void *buf, size_t count); //读数据
//有名管道(FIFO)
#include<sys/stat.h>
#include <unistd.h>
int mkfifo(const char *path, mode_t mode); //创建fifo文件
int open(const char *pathname, int flags); //打开fifo文件
ssize_t write(int fd, const void *buf, size_t count); //写数据
ssize_t read(int fd, void *buf, size_t count); //读数据
可以看到,匿名管道通过pipe函数创建,pipe函数通过在内核的虚拟文件系统中创建两个pipefs虚拟文件(不清楚虚拟文件的可以去了解下Linux的虚拟文件系统VFS),并返回这两个虚拟文件描述符,有了这两个文件描述,我们就能进行跨进程通信了。
匿名管道是单向半双工的通信方式,单向即意味着只能一端读另一端写,半双工意味着不能同时读和写,其中文件描述符fd[1]只能用来写,文件描述符f[0]只能用来读,pipe创建好后,我们就可以用Linux标准的文件读取函数read和写入函数write来对pipe进行读写了。
为什么pipe是匿名管道呢?因为pipefs文件是特殊的虚拟文件系统,并不会显示在VFS的目录中,所以用户不可见,既然用户不可见,那么又怎么能进行进程间通信呢?因为pipe是匿名的,所以它只支持父子和兄弟进程之间的通信。通过fork创建父子进程或者通过clone创建兄弟进程的时候,会共享内存拷贝,这时,子进程或兄弟进程就能在共享拷贝中拿到pipe的文件描述符进行通信了。我们通过下图看一下通信的流程。
接着说有名管道FIFO,FIFO是半双工双向通信,所以通过FIFO创建的管道既能读也能写,但是不能同时读和写,FIFO本质上是一个先进先出的队列数据结构,最早放入的数据被最先读出来,这样的数据结构能保证信息交流的顺序。
FIFO使用也很简单,通过mkfifo函数创建管道,它同样会在内核的虚拟文件系统中创建一个FIFO虚拟文件,FIFO文件在在VFS目录中可见,所以他是有名管道,FIFO创建后,我们需要调用open函数打开这个fifo文件,然后才能通过write和read函数进行读写
我们来总结一下pipe和fifo的异同点
相同点
- IPC的本质都是通过在内核创建虚拟文件,并且调用文件读写函数来进行数据通信
- 都只能接收字节流数据
- 都是半双工通信
不同点
- pipe是单向通信,fifo可以双向通信
- pipe只能在父子,兄弟进程间通信,fifo没有这个限制
那么管道的使用场景是什么呢?匿名管道只能用在亲属进程之间通信,而且传输的数量小,一般只支持4K,不适合大数据的交换数据和不同进程间的通信,但是使用简单方便,因为是单向通信,所以不存在并发问题。虽然FIFO能在任意两个进程间进行通信,但是因为FIFO是可以双向通信的,这样也不可避免的带来了并发的问题,我们需要花费比较大的精力用来控制并发问题。
管道在Android系统中的使用场景
下面说一下Android系统中具体使用到管道的场景:Looper。Looper不就是一个消息队列吗?怎么还使用到了管道呢?其实在Android 6.0 以下版本中,主线程Looper的唤醒就使用到了管道。
//文件-->/system/core/libutils/Looper.cpp
Looper::Looper(bool allowNonCallbacks) :
mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {
int wakeFds[2];
int result = pipe(wakeFds); //创建pipe
mWakeReadPipeFd = wakeFds[0];
mWakeWritePipeFd = wakeFds[1];
result = fcntl(mWakeReadPipeFd, F_SETFL, O_NONBLOCK);
LOG_ALWAYS_FATAL_IF(result != 0, "Could not make wake read pipe non-blocking. errno=%d",
errno);
result = fcntl(mWakeWritePipeFd, F_SETFL, O_NONBLOCK);
LOG_ALWAYS_FATAL_IF(result != 0, "Could not make wake write pipe non-blocking. errno=%d",
errno);
mIdling = false;
// Allocate the epoll instance and register the wake pipe.
mEpollFd = epoll_create(EPOLL_SIZE_HINT);
LOG_ALWAYS_FATAL_IF(mEpollFd < 0, "Could not create epoll instance. errno=%d", errno);
struct epoll_event eventItem;
memset(& eventItem, 0, sizeof(epoll_event)); // zero out unused members of data field union
eventItem.events = EPOLLIN;
eventItem.data.fd = mWakeReadPipeFd;
result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, & eventItem);
LOG_ALWAYS_FATAL_IF(result != 0, "Could not add wake read pipe to epoll instance. errno=%d",
errno);
}
从第上面代码可以看到,native层的Looper的构造函数就使用了pipe来创建管道,通过mWakeReadPipeFd,mWakeWritePipeFd这两个文件描述符的命名也看出,它是用来做唤醒的,我们就来看一下具体的唤醒的实现吧。
//文件-->/system/core/libutils/Looper.cpp
void Looper::wake() {
ssize_t nWrite;
do {
nWrite = write(mWakeWritePipeFd, "W", 1);
} while (nWrite == -1 && errno == EINTR);
if (nWrite != 1) {
if (errno != EAGAIN) {
ALOGW("Could not write wake signal, errno=%d", errno);
}
}
}
可以看到,唤醒函数其实就是往管道mWakeWritePipeFd里写入一个字母“W”,mWakeReadPipeFd接收到数据后,就会唤醒Looper。
信号
信号的使用及原理
信号实质上是一种软中断,既然是一种中断,就说明信号是异步的,信号接收函数不需要一直阻塞等待信号的到达。当信号发出后,如果有地方注册了这个信号,就会执行响应函数,如果没有地方注册这个信号,该信号就会被忽略。我们来看一下信号的使用方法。
#include <signal.h>
sighandler_t signal(int signum, sighandler_t handler); //信号注册函数
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); //信号注册函数
struct sigaction {
void (*sa_handler)(int); //信号处理程序,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作
void (*sa_sigaction)(int, siginfo_t *, void *); //信号处理程序,能够接受额外数据和sigqueue配合使用
sigset_t sa_mask;//阻塞关键字的信号集,可以再调用捕捉函数之前,把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先的值。
int sa_flags;//影响信号的行为SA_SIGINFO表示能够接受数据
};
int kill(pid_t pid, int sig); //信号发送函数
int sigqueue(pid_t pid, int sig, const union sigval value); //信号发送函数
//……
注册信号有两个方法
- **signal()**函数:signal不支持传递信息,signum入参为信号量,handler入参为信号处理函数
- **sigaction()**函数:sigaction支持传递信息,信息放在sigaction数据结构中
信号发送函数比较多,这里我列举一下。
- kill():用于向进程或进程组发送信号;
- sigqueue():只能向一个进程发送信号,不能向进程组发送信号;
- alarm():用于调用进程指定时间后发出SIGALARM信号;
- setitimer():设置定时器,计时达到后给进程发送SIGALRM信号,功能比alarm更强大;
- abort():向进程发送SIGABORT信号,默认进程会异常退出。
- raise():用于向进程自身发送信号;
通过kill -l指令可以查看Android手机支持的信号,从下图可以看到,总共有64个,前31个信号是普通信号,后33个信号是实时信号,实时信号支持队列,可以保证信号不会丢失。
我列举一下前几个信号的作用,其他的就不讲解了
1 | SIGHUP | 挂起 |
---|---|---|
2 | SIGINT | 中断 |
3 | SIGQUIT | 中断 |
3 | SIGQUIT | 退出 |
4 | SIGILL | 非法指令 |
5 | SIGTRAP | 断点或陷阱指令 |
6 | SIGABRT | abort发出的信号 |
7 | SIGBUS | 非法内存访问 |
8 | SIGFPE | 浮点异常 |
9 | SIGKILL | 杀进程信息 |
当我们调用信号发送函数后,信号是怎么传到注册的方法调用中去的呢?这里以kill()这个信号发送函数讲解一下这个流程。
kill()函数会经过系统调用方法sys_tkill()进入内核,sys_tkill是SYSCALL_DEFINE2这个方法来实现,这个方法的实现是一个宏定义。我会从这个方法一路往底追踪,这里我会忽略细节实现,只看关键部分的代码。
//文件-->syscalls.h
asmlinkage long sys_kill(int pid, int sig);
//文件-->kernel/signal.c
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
{
struct kernel_siginfo info;
clear_siginfo(&info);
info.si_signo = sig;
info.si_errno = 0;
info.si_code = SI_USER;
info.si_pid = task_tgid_vnr(current);
info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
return kill_something_info(sig, &info, pid);
}
static int kill_something_info(