Android AIDLipcAndroid开发

Android跨进程通信IPC之2——Bionic

2017-07-11  本文已影响1110人  隔壁老李头

Android跨进程通信IPC整体内容如下

一、为什么要学习Bionic

Bionic库是Android的基础库之一,也是连接Android系统和Linux系统内核的桥梁,Bionic中包含了很多基本的功能模块,这些功能模块基本上都是源于Linux,但是就像青出于蓝而胜于蓝,它和Linux还是有一些不一样的的地方。同时,为了更好的服务Android,Bionic中也增加了一些新的模块,由于本次的主题是Androdi的跨进程通信,所以了解Bionic对我们更好的学习Android的跨进行通信还是很有帮助的。

Android除了使用ARM版本的内核外和传统的x86有所不同,谷歌还自己开发了Bionic库,那么谷歌为什么要这样做那?

二、谷歌为什么使用Bionic库

谷歌使用Bionic库主要因为以下三点:

三、Bionic库简介

Bionic 音标为 bīˈänik,翻译为"仿生"

Bionic包含了系统中最基本的lib库,包括libc,libm,libdl,libstd++,libthread_db,以及Android特有的链接器linker。

四、Bionic库的特性

Bionic库的特性很多,受篇幅限制,我挑几个和大家平时接触到的说下

(一)、架构

Bionic 当前支持ARM、x86和MIPS执行集,理论上可以支持更多,但是需要做些工作,ARM相关的代码在目录arch-arm中,x86相关代码在arch-x86中,mips相关的代码在arch-mips中。

(二)、Linux核心头文件

Bionic自带一套经过清理的Linxu内核头文件,允许用户控件代码使用内核特有的声明(如iotcls,常量等)这些头文件位于目录:

bionic/libc/kernel/common
bionic/libc/kernel/arch-arm
bionic/libc/kernel/arch-x86
bionic/libc/kernel/arch-mips

(三)、DNS解析器

虽然Bionic 使用NetBSD-derived解析库,但是它也做了一些修改。

(四)、二进制兼容性

由于Bionic不与GNU C库、ucLibc,或者任何已知的Linux C相兼容。所以意味着不要期望使用GNU C库头文件编译出来的模块能够正常地动态链接到Bionic

(五)、Android特性

Bionict提供了少部分Android特有的功能

1、访问系统特性

Android 提供了一个简单的"共享键/值 对" 空间给系统的中的所有进程,用来存储一定数量的"属性"。每个属性由一个限制长度的字符串"键"和一个限制长度的字符串"值"组成。
头文件<sys/system_properties.h>中定义了读系统属性的函数,也定义了键/值对的最大长度。

2、Android用户/组管理

在Android中没有etc/password和etc/groups 文件。Android使用扩展的Linux用户/组管理特性,以确保进程根据权限来对不同的文件系统目录进行访问。
Android的策略是:

五、Bionic库的模块简介

Bionic目录下一共有5个库和一个linker程序
5个库分别是:

(一)、Libc库

Libc是C语言最基础的库文件,它提供了所有系统的基本功能,这些功能主要是对系统调用的封装,是Libc是应用和Linux内核交流的桥梁,主要功能如下:

(二)、Libm库

Libm 是数学函数库,提供了常见的数学函数和浮点运算功能,但是Android浮点运算时通过软件实现的,运行速度慢,不建议频繁使用。

(三)、libdl库

libdl库原本是用于动态库的装载。很多函数实现都是空壳,应用进程使用的一些函数,实际上是在linker模块中实现。

(四)、Libm库

libstd++ 是标准的C++的功能库,但是,Android的实现是非常简单的,只是new,delete等少数几个操作符的实现。

(五)、libthread_db库

libthread_db 用来支持对多线程的中动态库的调试。

(六)、Linker模块

Linux系统上其实有两种并不完全相同的可执行文件

静态可执行程序用在一些特殊场合,例如,系统初始化时,这时整个系统还没有准备好,动态链接的程序还无法使用。系统的启动程序Init就是一个静态链接的例子。在Android中,会给程序自动加上两个".o"文件,分别是"crtbegin_static.c"和"certtend_android.o",这两个".o"文件对应的源文件位于bionic/libc/arch-common/bionic目录下,文件分别是crtbegin.c和certtend.S。_start()函数就位于cerbegin.c中。

在动态链接时,execuve()函数会分析可执行文件的文件头来寻找链接器,Linux文件就是ld.so,而Android则是Linker。execuve()函数将会将Linker载入到可执行文件的空间,然后执行Linker的_start()函数。Linker完成动态库的装载和符号重定位后再去运行真正的可执行文件的代码。

六、Bionic库的内存管理函数

(一)内存管理函数

对于32位的操作系统,能使用的最大地址空间是4GB,其中地址空间03GB分配给用户进程使用,地址空间3GB4GB由内核使用,但是用户进程并不是在启动时就获取了所有的0~3GB地址空间的访问权利,而是需要事先向内核申请对模块地址空间的读写权利。而且申请的只是地址空间而已,此时并没有分配真是的物理地址。只有当进程访问某个地址时,如果该地址对应的物理页面不存在,则由内核产生缺页中断,在中断中才会分配物理内存并建立页表。如果用户进程不需要某块空间了,可以通过内核释放掉它们,对应的物理内存也释放掉。

但是由于缺页中断会导致运行缓慢,如果频繁的地由内核来分配和释放内存将会降低整个体统的性能,因此,一般操作系统都会在用户进程中提供地址空间的分配和回收机制。用户进程中的内存管理会预先向内核申请一块打的地址空间,称为堆。当用户进程需要分配内存时,由内存管理器从堆中寻找一块空闲的内存分配给用户进程使用。当用户进程释放某块内存时,内存管理器并不会立刻将它们交给内核释放,而是放入空闲列表中,留待下次分配使用。

内存管理器会动态的调整堆的大小,如果堆的空间使用完了,内存管理器会向堆内存申请更多的地址空间,如果堆中空闲太多,内存管理器也会将一部分空间返给内核。

(二) Bionic的内存管理器——dlmalloc

dlmalloc是一个十分流行的内存分配器。dlmalloc位于bionic/libc/upstream-dlmalloc下,只有一个C文件malloc.c。由于本次主题是跨进程通信,后续有时间就Android的内存回收单独作为一个课题去讲解,今天就不详细说了,就简单的说下原理。
dlmalloc的原理:

Dalvk虚拟机中使用了dlmalloc进行私有堆管理。

七、线程

Bionic中的线程管理函数和通用的Linux版本的实现有很多差异,Android根据自己的需要做了很多裁剪工作。

(一)、Bionic线程函数的特性

1、pthread的实现基于Futext,同时尽量使用简单的代码来实现通用操作,特征如下:
2、Bionic不支持pthread_cancel(),因为加入它会使得C库文件明显变大,不太值得,同时有以下几点考虑
3、不要在pthread_once()的回调函数中调用fork(),这么做会导致下次调用pthread_once()的时候死锁。而且不能在回调函数中抛出一个C++的异常。
4、不能使用_thread关键词来定义线程本地存储区。

(二)、创建线程和线程的属性

1、创建线程

函数pthread_create()用来创建线程,原型是:

int  pthread_create((pthread_t  *thread,  pthread_attr_t  *attr,  void  *(*start_routine)(void  *),  void  *arg)

其中,pthread_t在android中等同于long

typedef long pthread_t;

若线程创建成功,则返回0,若线程创建失败,则返回出错编号。
PS:要注意的是,pthread_create调用成功后线程已经创建完成,但是不会立刻发生线程切换。除非调用线程主动放弃执行,否则只能等待线程调度。

2、线程的属性

结构 pthread_atrr_t用来设置线程的一些属性,定义如下:

typedef struct
{
    uint32_t        flags;                
    void *    stack_base;              //指定栈的起始地址
    size_t    stack_size;               //指定栈的大小
    size_t    guard_size;  
    int32_t   sched_policy;           //线程的调度方式
    int32_t   sched_priority;          //线程的优先级
}

使用属性时要先初始化,函数原型是:

int  pthread_attr_init(pthread_attr_t*  attr)

通过pthread_attr_init()函数设置的缺省属性值如下:

int pthread_attr_init(pthread_attr_t* attr){
     attr->flag=0;
     attr->stack_base=null;
     attr->stack_szie=DEFAULT_THREAD_STACK_SIZE;  //缺省栈的尺寸是1MB
     attr0->quard_size=PAGE_SIZE;                                    //大小是4096
     attr0->sched_policy=SCHED_NORMAL;                      //普通调度方式
     attr0->sched_priority=0;                                                 //中等优先级
     return 0;
}

下面介绍每项属性的含义。

Bionic虽然也实现了pthread_attr_setscope()函数,但是只支持PTHREAD_SCOP_SYSTEM属性,也就意味着Android线程将在全系统的范围内竞争CPU资源。

3、退出线程的方法
(1)、调用pthread_exit函数退出

一般情况下,线程运行函数结束时线程才退出。但是如果需要,也可以在线程运行函数中调用pthread_exit()函数来主动退出线程运行。函数原型如下:

 void pthread_exit( void * retval) ;

其中参数retval用来设置返回值

(2)、设备布尔的全局变量

但是如果希望在其它线程中结束某个线程?前面介绍了Android不支持pthread_cancel()函数,因此,不能在Android中使用这个函数来结束线程。通俗的方法是,如果线程在一个循环中不停的运行,可以在每次循环中检查一个初始值为false的全局变量,一旦这个变量的值为ture,则主动退出,这样其它线程就可以铜鼓改变这个全局变量的值来控制线程的退出,示例如下:

bool g_force_exit =false;
void * thread_func(void *){
         for(;;){
             if(g_force_exit){
                    break;
             }
             .....
         }
         return NULL;
}
int main(){
     .....
     q_force_exit=true;       //青坡线程退出
}

这种方法实现起来简单可靠,在编程中经常使用。但它的缺点是:如果线程处于挂起等待状态,这种方法就不适用了。
另外一种方式是使用pthread_kill()函数。pthread_kill()函数的作用不是"杀死"一个线程,而是给线程发送信号。函数如下:

  int pthread_kill(pthread tid,int sig);

即使线程处于挂起状态,也可以使用pthead_kill()函数来给线程发送消息并使得线程执行处理函数,使用pthread_kill()函数的问题是:线程如果在信号处理函数中退出,不方便释放在线程的运行函数中分配的资源。

(3)、通过管道

更复杂的方法是:创建一个管道,在线程运行函数中对管道"读端"用select()或epoll()进行监听,没有数据则挂起线程,通过管道的"写端"写入数据,就能唤起线程,从而释放资源,主动退出。

4、线程的本地存储TLS

线程本地存储(TLS)用来保存、传递和线程有关的数据。例如在前面说道的使用pthread_kill()函数关闭线程的例子中,需要释放的资源可以使用TLS传递给信号处理函数。

(1)、TLS介绍

TLS在线程实例中是全局可见的,对某个线程实例而言TLS是这个线程实例的私有全局变量。同一个线程运行函数的不同运行实例,他们的TLS是不同的。在这个点上TLS和线程的关系有点类似栈变量和函数的关系。栈变量在函数退出时会消失,TLS也会在线程结束时释放。Android实现了TLS的方式是在线程栈的顶开辟了一块区域来存放TLS项,当然这块区域不再受线程栈的控制。

TLS内存区域按数组方式管理,每个数组元素称为一个slot。Android 4.4中的TLS一共有128 slot,这和Posix中的要求一致(Android 4.2是64个)

(2)、TLS注意事项
int  pthread_key_create(pthread_key_t *key,void (*destructor_function) (void *) );

pthread_key_create()函数成功返回0,参数key中是分配的slot,如果将来放入slot中的对象需要在线程结束的时候由系统释放,则需要提供一个释放函数,通过第二个函数destructor_function传入。

 int  pthread_key_delete ( pthread_key_t) ;

pthread_key_delete()函数并不检查当前是否还有线程正在使用这个slot,也不会调用清理函数,只是将slot释放以供下次调用pthread_key_create()使用。

  int pthread_setspecific(pthread_key_t key,const void *value) ;
 void * pthread_getsepcific (pthread_key_t key);
5、线程的互斥量(Mutex)函数

Linux线程提供了一组函数用于线程间的互斥访问,Android中的Mutex类实质上是对Linux互斥函数的封装,互斥量可以理解为一把锁,在进入某个保护区域前要先检查是否已经上锁了。如果没有上锁就可以进入,否则就必须等待,进入后现将锁锁上,这样别的线程就无法再进入了,退出保护区后腰解锁,其它线程才可以继续使用

(1)、Mutex在使用前需要初始化

初始化函数是:

int pthread_mutex_init(pthread_mutext_t *mutex, const pthread_mutexattr_t *attr);

成功后函数返回0,metex被初始化成未锁定的状态。如果参数attr为NULL,则使用缺省的属性MUTEX_TYPE-BITS_NORMAL。
互斥量的属性主要有两种,类型type和范围scope,设置和获取属性的函数如下:

int  pthread_mutexattr_settype (pthread_mutexattr_t * attr, type);
int  pthread_mutexattr_gettype (const pthread_mutexattr_t * attr, int *type);
int  pthread_mutexattr_getpshared(pthread_mutexattr_t *attr, int *pshared );
int  pthread_mutexattrattr_ setpshared (pthread_mutexattr_t *attr,int  pshared);

互斥量Mutex的类型(type) 有3种

互斥量Mutex的作用范围(scope) 有2种

6、线程的条件量(Condition)函数
(1)为什么需要条件量Condition函数

条件量Condition是为了解决一些更复杂的同步问题而设计的。考虑这样的一种情况,A和B线程不但需要互斥访问某个区域,而且线程A还必须等待线程B的运行结果。如果仅使用互斥量进行保护,在线程B先运行的的情况下没有问题。但是如果线程A先运行,拿到互斥量的锁,往下忘无法进行。

条件量就是解决这类问题的。在使用条件量的情况下,如果线程A先运行,得到锁以后,可以使用条件量的等待函数解锁并等待,这样线程B得到了运行的机会。线程B运行完以后通过条件量的信号函数唤醒等待的线程A,这样线程A的条件也满足了,程序就能继续执行力额。

(2)Condition函数

1️⃣ 条件量在使用前需要先初始化,函数原型是:

int  pthread_cond_init(pthread_cond_t *cond, const pthread_condattr *attr);

使用完需要销毁,函数原型是:

int  pthread_cond_destroy(pthread_cond_t *cond);

条件量的属性只有 "共享(share)" 一种,下面是属性相关函数原型,下面是属性相关的函数原型:

int  pthread_condattr_init(pthread_condattr_t *attr);
int  pthread_condattr_getpshared(pthread_condattr_t *attr,int *pshared);
int pthread_condattr_setpshared(pthread_condattr_t *attr,int pshared) 
int pthread_condattr_destroy (pthread_condattr_t *__attr);

"共享(shared)" 属性的值有两种

2️⃣条件量的等待函数的原型如下:

 int pthread_cond_wait (pthread_cond_t *__restrict __cond,pthread_mutex_t *__restrict __mutex);

 int pthread_cond_timedwait (pthread_cond_t *__restrict __cond,pthread_mutex_t *__restrict __mutex, __const struct timespec *__restrict __abstime);

条件量的等待函数会先解锁互斥量,因此,使用前一定要确保mutex已经上锁。锁上后线程将挂起。pthread_cond_timedwait()用在希望线程等待一段时间的情况下,如果时间到了线程就会恢复运行。

3️⃣ 可以使用函数pthread_cond_signal()来唤醒等待队列中的一个线程,原型如下:

 int pthread_cond_signal (pthread_cond_t *__cond);

也可以通过pthread_cond_broadcast()唤醒所有等待的线程

 int pthread_cond_broadcast (pthread_cond_t *__cond);

(三)、Futex同步机制

1、Futex的系统调用

在Linux中,Futex系统调用的定义如下:

#define _NR_futex    240

(1) Fetex系统调用的原型是:

int  futex(int *uaddr, int cp, int val, const struct timespec *timeout, int *uaddr2, int val3);
(1) 在Bionic中,提供了两个函数来包装Futex系统调用:
extern int  _futex_wait(volatile void *ftx,int val, const struct timespec *timespec );
extern int _futex_wake(volatile void *ftx, int count);
(2) Bionic还有两个类似的函数,它们的原型如下:
extern int  _futex_wake_ex(volatile void *ftx,int pshared,int val);
extern int  _futex_wait_ex(volatile void *fex,int pshared,int val, const stuct timespec *timeout);

这两个函数多了一个参数pshared,pshared的值为true 表示wake和wait操作是用于进程间的挂起和唤醒;值为false表示操作于进程内线程的挂起和唤醒。当pshare的值为false时,执行Futex系统调用的操作码为

FUTEX_WAIT|FUTEX_PRIVATE_FLAG

内核如何检测到操作有FUTEX_PRIVATE_FLAG标记,能以更快的速度执行七挂起和唤醒操作。
_futex_wait 和_futex_wake函数相当于pshared等于true的情况。

(3) 在Bionic中,提供了两个函数来包装Futex系统调用:
extern int  _futex_syscall3(volatile void *ftx,int pshared,int val);
extern int  _futex_syscall4(volatile void *ftx,int pshared,int val, const struct timespec *timeout);

_futex_syscall3()相当于 _futex_wake(),而 _futex_system4()相当于 _futex_wait()。这两个函数与前面的区别是能指定操作码op作为参数。操作码可以是FUTEX_WAIT_FUTEX_WAKE或者它们和FUTEX_PRIVATE_FLAG的组合。

2、Futex的用户态操作

Futex的系统调用FUTEX_WAIT和FUTEX_WAKE只是用来挂起或者唤醒进程,Futex的同步机制还包括用户态下的判断操作。用户态下的操作没有固定的函数调用,只是一种检测共享变量的方法。Futex用于临界区的算法如下:

对Futex变量操作时,比较和赋值操作必须是原子的。

上一篇下一篇

猜你喜欢

热点阅读