深入理解Android进程间通信机制

本文深入探讨了Android系统中的各种进程间通信(IPC)方式,包括管道、信号、消息队列、信号量、共享内存、Socket、mmap()和eventfd。详细介绍了每种通信方式的使用、原理及其在Android系统中的应用场景,如管道在Looper中的应用、信号用于进程杀掉、消息队列在Handler中的角色、共享内存用于大数据传输等。最后提到了Binder作为Android特有的IPC机制,强调了其设计背后的考虑,如效率、安全性和并发问题。
摘要由CSDN通过智能技术生成

​通信是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(int
  • 6
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值