Bionic 简介

 

1、为何要学习Bionic

 

Bionic库是Android的基础库之一,也是链接Android系统和Linux系统内核的桥梁,Bionic中包含了不少基本的功能模块,这些功能模块基本上都是源于Linux,可是就像青出于蓝而胜于蓝,它和Linux仍是有一些不同的的地方。同时,为了更好的服务Android,Bionic中也增长了一些新的模块,因为本次的主题是Androdi的跨进程通讯,因此了解Bionic对咱们更好的学习Android的跨进行通讯仍是颇有帮助的。android

Android除了使用ARM版本的内核外和传统的x86有所不一样,谷歌还本身开发了Bionic库,那么谷歌为何要这样作那?算法

2、谷歌为何使用Bionic库

谷歌使用Bionic库主要由于如下三点:编程

 
  • 一、谷歌没有使用Linux的GUN Libc,很大一部分缘由是由于GNU Libc的受权方式是GPL 受权协议有限制,由于一旦软件中使用了GPL的受权协议,该系统全部代码必须开元。
  • 二、谷歌在BSD的C库上的基础上加入了一些Linux特性从而生成了Bionic。Bionic名字的来源就是BSD和Linux的混合。并且不受限制的开源方式,因此在现代的商业公司中比较受欢迎。
  • 三、还有就是由于性能的缘由,由于Bionic的核心设计思想就是"简单",因此Bionic中去掉了不少高级功能。这样Bionic库仅为200K左右,是GNU版本体积的一半,这意味着更高的效率和低内存的使用,同时配合通过优化的Java VM Dalvik才能够保证高的性能。

3、Bionic库简介

 

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

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

4、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解析库,可是它也作了一些修改。app

 
  • 一、不实现name-server-switch特性
  • 二、读取/system/etc/resolv.conf而不是/etc/resolv.config
  • 三、从系统属性中读取服务器地址列表,代码中会查找'net.dns1','net.dns2',等属性。每一个属性都应该包含一个DNS服务器的IP地址。这些属性能被Android系统的其它进程修改设置。在实现上,也支持进程单独的DNS服务器列表,使用属性'net.dns1.<pid>'、'net.dns2.<pid>'等,这里<pid> 表示当前进程的ID号。
  • 四、在执行查询时,使用一个合适的随机ID(而不是每次+1),以提高安全性。
  • 五、在执行查询时,给本地客户socket绑定一个随机端口号,以提升安全性。
  • 六、删除了一些源代码,这些源代码会形成了不少线程安全的问题

(四)、二进制兼容性

因为Bionic不与GNU C库、ucLibc,或者任何已知的Linux C相兼容。因此意味着不要指望使用GNU C库头文件编译出来的模块可以正常地动态连接到Bionic

(五)、Android特性

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

一、访问系统特性

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

二、Android用户/组管理

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

 
  • 一、每一个已经安装的的应用程序都有本身的用户ID和组ID。ID从10000(一万)开始,小于10000(一万)的ID留给系统的守护进程。
  • 二、tpwnam()能识别一些硬编码的子进程名(如"radio"),能将他们翻译为用户id值,它也能识别"app_1234",这样的组合名字,知道将后面的1234和10000(一万)相加,获得的ID值为11234.getgrname()也相似。
  • 三、getservent() Android中没有/etc/service,C库在执行文件中嵌入只读的服务列表做为代替,这个列表被须要它的函数所解析。所见文件bionic/libc/netbsd/net/getservent.c和bionic/libc/netbsd/net/service.h。
    这个内部定义的服务列表,将来可能有变化,这个功能是遗留的,实际不多使用。getservent()返回的是本地数据,getservbyport()和getservbyname()也按照一样的方式实现。
  • 四、getprotoent() 在Android中没有/etc/protocel,Bionic目前没有实现getprotocent()和相关函数。若是增长的话,极可能会以getervent()相同的方式。

5、Bionic库的模块简介

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

 
  • 一、libc
  • 二、libm
  • 三、libdl
  • 四、libstd++
  • 五、libthread_db

(一)、Libc库

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

 
  • 进程管理:包括进程的建立、调度策略和优先级的调整
  • 线程管理:包括线程的建立和销毁,线程的同步/互斥等
  • 内存管理:包括内存分配和释放等
  • 时间管理:包括获取和保存系统时间、获取当前系统运行时长等
  • 时区管理:包括时区的设置和调整等
  • 定时器管理:提供系统的定时服务
  • 文件系统管理:提供文件系统的挂载和移除功能
  • 文件管理:包括文件和目录的建立增删改
  • 网络套接字:建立和监听socket,发送和接受
  • DNS解析:帮助解析网络地址
  • 信号:用于进程间通讯
  • 环境变量:设置和获取系统的环境变量
  • Android Log:提供和Android Log驱动进行交互的功能
  • Android 属性:管理一个共享区域来设置和读取Android的属性
  • 标准输入/输出:提供格式化的输入/输出
  • 字符串:提供字符串的移动、复制和比较等功能
  • 宽字符:提供对宽字符的支持。

(二)、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完成动态库的装载和符号重定位后再去运行真正的可执行文件的代码。

6、Bionic库的内存管理函数

(一)内存管理函数

 

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

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

 

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

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

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

 
  • dlmalloc内部是以链表的形式将"堆"的空闲空间根据尺寸组织在一块儿。分配内存时经过这些链表能快速地找到合适大小的空闲内存。若是不能找到知足要求的空闲内存,dlmalloc会使用系统调用来扩大堆空间。
  • dlmalloc内存块被称为"trunk"。每块大小要求按地址对齐(默认8个字节),所以,trunk块的大小必须为8的倍数。
  • dlmalloc用3种不一样的的链表结构来组织不一样大小的空闲内存块。小于256字节的块使用malloc_chunk结构,按照大小组织在一块儿。因为尺寸小于的块一共有256/8=32,因此一共使用了32个malloc_chunk结构的环形链表来组织小于256的块。大小大于256字节的块由结构malloc_tree_chunk组成链表管理,这些块根据大小组成二叉树。而更大的尺寸则由系统经过mmap的方式单独分配一块空间,并经过malloc_segment组成的链表进行管理。
  • 当dlmalloc分配内存时,会经过查找这些链表来快速找到一块和要求的尺寸大小最匹配的空闲内存块(这样作事为了尽可能避免内存碎片)。若是没有合适大小的块,则将一块大的分红两块,一块分配出去,另外一块根据大小再加入对应的空闲链表中。
  • 当dlmalloc释放内存时,会将相邻的空闲块合并成一个大块来减小内存碎片。若是空闲块过多,超过了dlmaloc内存的阀值,dlmalloc就开始向系统返回内存。
  • dlmalloc除了能管理进程的"堆"空间,还能提供私有堆管理,就是在堆外单独分配一块地址空间,由dlmalloc按照一样的方式进行管理。dlmalloc中用来管理进程的"堆"空间的函数,都带有"dl"前缀,如"dlmalloc","dlfree"等,而私有堆的管理函数则带有前缀"msspace_",如"msspace_malloc"

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

7、线程

Bionic中的线程管理函数和通用的Linux版本的实现有不少差别,Android根据本身的须要作了不少裁剪工做。

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

一、pthread的实现基于Futext,同时尽可能使用简单的代码来实现通用操做,特征以下:

 
  • pthread_mutex_t,pthread_cond_t类型定义只有4字节。
  • 支持normal,recursive and error-check 互斥量。考虑到一般大多数的时候都使用normal,对normal分支下代码流程作了很细致的优化
  • 目前没有支持读写锁,互斥量的优先级和其余高级特征。在Android还不须要这些特征,可是在将来可能会添加进来。

二、Bionic不支持pthread_cancel(),由于加入它会使得C库文件明显变大,不太值得,同时有如下几点考虑

 
  • 要正确实现pthread_cancel(),必须在C库的不少地方插入对终止线程的检测。
  • 一个好的实现,必须清理资源,例如释放内存,解锁互斥量,若是终止刚好发生在复杂的函数里面(好比gthosbyname()),这会使许多函数正常执行也变慢。
  • pthread_cancel()不能终止全部线程。好比无穷循环中的线程。
  • pthread_cancel()自己也有缺点,不太容易移植。
  • Bionic中实现了pthread_cleanup_push()和pthread_cleanup_pop()函数,在线程经过调用pthread_exit()退出或者从它的主函数中返回到时候,它们能够作些清理工做。

三、不要在pthread_once()的回调函数中调用fork(),这么作会致使下次调用pthread_once()的时候死锁。并且不能在回调函数中抛出一个C++的异常。

四、不能使用_thread关键词来定义线程本地存储区。

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

一、建立线程

函数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;
 
  • 参数thread是一个指针,pthread_create函数成功后,会将表明线程的值写入其指向的变量。
  • 参数 args 通常状况下为NULL,表示使用缺省属性。
  • 参数start_routine是线程的执行函数
  • 参数arg是传入线程执行函数的参数

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

二、线程的属性

结构 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;
}

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

 
  • 一、flag 用来表示线程的分离状态
    Linux线程有两种状态:分离(detch)状态和非分离(joinable)状态,若是线程是非分离状态(joinable)状态,当线程函数退出时或者调用pthread_exit()时都不会释放线程所占用的系统资源。只有当调用了pthread_join()以后这些资源才会释放。若是是分离(detach)状态的线程,这些资源在线程函数退出时调用pthread_exit()时会自动释放
  • 二、stack_base: 线程栈的基地址
  • 三、stack_size: 线程栈的大小。基地址和栈的大小。
  • 四、guard_size: 线程的栈溢出保护区大小。
  • 五、sched_policy:线程的调度方式。
    线程一共有3中调度方式:SCHED_NORMAL,SCHED_FIFO,SCHED_RR。其中SCHED_NORMAL表明分时调度策略,SCHED_FIFO表明实时调度策略,先到先服务,一旦占用CPU则一直运行,一直运行到有更高优先级的任务到达,或者本身放弃。SCHED_RR表明实时调度策略:时间片轮转,当前进程时间片用完,系统将从新分配时间片,并置于就绪队尾。
  • 六、sched_priority:线程的优先级。

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

三、退出线程的方法

(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()进行监听,没有数据则挂起线程,经过管道的"写端"写入数据,就能唤起线程,从而释放资源,主动退出。

四、线程的本地存储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注意事项

  • TLS变量的数量有限,使用前要申请一个key,这个key和内部的slot关联一块儿,使用完须要释放。
    申请一个key的函数原型:
int  pthread_key_create(pthread_key_t *key,void (*destructor_function) (void *) );

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

  • 释放 TLS key的函数原型是:
int  pthread_key_delete ( pthread_key_t) ;

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

  • 利用TLS保存数据中函数原型:
int pthread_setspecific(pthread_key_t key,const void *value) ;
  • 读取TLS保存数据中的函数原型:
void * pthread_getsepcific (pthread_key_t key);

五、线程的互斥量(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种

 
  • PTHREAD_MUTEX_NORMAL:该类型的的互斥量不会检测死锁。若是线程没有解锁(unlock)互斥量的状况下再次锁定该互斥量,会产生死锁。若是线程尝试解锁由其余线程锁定的互斥量会产生不肯定的行为。若是尝试解锁未锁定的互斥量,也会产生不肯定的行为。** 这是Android目前惟一支持的类型 **。
  • PTHREAD_MUTEX_ERRORCHECK:此类型的互斥量可提供错误检查。若是线程在没有解锁互斥量的状况下尝试从新锁定该互斥量,或者线程尝试解锁的互斥量由其余线程锁定。** Android目前不支持这种类型 ** 。
  • PTHREAD_MUTEX_RECURSIVE。若是线程没有解锁互斥量的状况下从新锁定该互斥量,可成功锁定该互斥量,不会产生死锁状况,可是屡次锁定该互斥量须要进行相同次数的解锁才能释放锁,而后其余线程才能获取该互斥量。若是线程尝试解锁的互斥量已经由其余线程锁定,则会返回错误。若是线程尝试解锁还未锁定的互斥量,也会返回错误。** Android目前不支持这种类型 ** 。

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

 
  • PTHREAD_PROCESS_PRIVATE:互斥量的做用范围是进程内,这是缺省属性。
  • PTHREAD_PROCESS_SHARED:互斥量能够用于进程间线程的同步。Android文档中说不支持这种属性,可是实际上支持,在audiofliger和surfacefliger都有用到,只不过在持有锁的进程意外死亡的状况下,互斥量(Mutex)不能释放掉,这是目前实现的一个缺陷。

六、线程的条件量(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)" 属性的值有两种

  • PTHREAD_PROCESS_PRIVATE:条件量的做用范围是进程内,这是缺省的属性。
  • PTHREAD_PROCESS_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同步机制

 
  • Futex 是 fast userspace mutext的缩写,意思是快速用户控件互斥体。这里讨论Futex是由于在Android中不但线程函数使用了Futex,甚至一些模块中也直接使用了Futex做为进程间同步手段,了解Futex的原理有助于咱们理解这些模块的运行机制。
  • Linux从2.5.7开始支持Futex。在类Unix系统开发中,传统的进程同步机制都是经过对内核对象进行操做来完成,这个内核对象在须要同步的进程中都是可见的。这种同步方法由于涉及用户态和内核态的切换,效率比较低。使用了传统的同步机制时,进入临界区即便没有其余进程竞争也会切到内核态检查内核同步对象的状态,这种没必要要的切换明显下降了程序的执行效率。
  • Futex就是为了解决这个问题而设计的。Futex是一种用户态和内核态混合的同步机制,使用Futex同步机制,若是用于进程间同步,须要先调用mmap()建立一块共享内存,Futex变量就位于共享区。同时对Futex变量的操做必须是原子的,当进程驶入进入临界区或者退出临界区的时候,首先检查共享内存中的Futex变量,若是没有其余进程也申请了使用临界区,则只修改Futex变量而再也不执行系统调用。若是同时有其余进程也申请使用临界区,仍是须要经过系统调用去执行等待或唤醒操做。这样经过用户态的Futex变量的控制,减小了进程在用户态和内核态之间切换的次数,从而最大程度的下降了系统同步的开销。

一、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);
 
  • uaddr是Futex变量,一个共享的整数计数器。
  • op表示操做类型,有5中预约义的值,可是在Bionic中只使用了下面两种:① FUTEX_WAIT,内核将检查uaddr中家眷器的值是否等于val,若是等于则挂起进程,直到uaddr到达了FUTEX_WAKE调用或者超时时间到。②FUTEXT_WAKE:内核唤醒val个等待在uaddr上的进程。
  • val存放与操做op相关的值
  • timeout用于操做FUTEX_WAIT中,表示等待超时时间。
  • uaddr2和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的组合。

二、Futex的用户态操做

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

 
  • 首先建立一个全局的整数变量做为Futex变量,若是用于进程间的同步,这个变量必须位于共享内存。Futex变量的初始值为0。
  • 当进程或线程尝试持有锁的时候,检查Futex变量的值是否为0,若是为0,则将Futex变量的值设为1,而后继续执行;若是不为0,将Futex的值设为2之后,执行FUTEX_WAIT 系统调用进入挂起等待状态。
  • Futex变量值为0表示无锁状态,1表示有锁无竞争的状态,2表示有竞争的状态。
  • 当进程或线程释放锁的时候,若是Futex变量的值为1,说明没有其余线程在等待锁,这样讲Futex变量的值设为0就结束了;若是Futex变量的值2,说明还有线程等待锁,将Futex变量值设为0,同时执行FUTEX_WAKE()系统调用来唤醒等待的进程。

对Futex变量操做时,比较和赋值操做必须是原

做者:隔壁老李头 连接:https://www.jianshu.com/p/25a908c7eefa 來源:简书 著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。

相关文章
相关标签/搜索