Android技术内幕:系统卷
http://dl.iteye.com/upload/attachment/505065/7e53ff99-181f-3aea-8035-90e47a07d763.jpg书名:Android技术内幕:系统卷 作者:杨丰盛出版社:机械工业出版社
ISBN:9787111337270
出版日期:2011年6月20日
开本:16
页码:548
定价:69元
豆瓣网讨论地址:http://book.douban.com/subject/6047744
China-pub地址:http://product.china-pub.com/197947
图书内容 国内首本系统对Android的源代码进行深入分析的著作,5大专业社区联袂推荐,权威性毋庸置疑!
全书将Android系统从构架上依次分为应用层、应用框架层、系统运行库层、硬件抽象层和Linux内核层等5个层次,旨在通过对Android系统源代码的全面分析来帮助开发者加深对Android系统架构设计和实现原理的认识,从而帮助他们解决开发中遇到的更加复杂的问题。 《Android技术内幕:系统卷》分为两卷,系统卷主要分析了Linux内核层、硬件抽象层和系统运行库层的各个模块的底层原理和实现细节;应用卷主要分析了应用层和应用框架层的各个模块的底层原理和实现细节。具体而言,系统卷第1章首先从宏观上介绍了Android系统的架构以及各个层次之间的关系,然后介绍了如何获取Android源代码并搭建。Android源代码开发环境和阅读环境的方法;第2章有针对性地剖析了Android的内核机制和结构,以及Android对Linux内核的改动和增强;第3章分析了Binder的架构和工作机制,以及Binder驱动的实现原理;第4章分析了Android电源管理模块的机制与实现;第5章全面地剖析了Android硬件设备驱动(显示、视频、音频、MTD、Event、蓝牙、WLAN等)的工作原理和实现,掌握这部分内容即可修改和编写基于Android的设备驱动程序;第6章深刻阐述了Android原生库的原理及实现,涉及系统C库、功能库、扩展库和原生的Server等重要内容;第7章系统地讲解了硬件抽象层的原理与实现,掌握这部分内容即可编写适合特定硬件设备驱动的抽象层接口;第8章和第9章是对系统运行库层的分析,主要讲解了Dalvik虚拟机的架构、原理与实现,以及Android的核心库相关的知识,掌握这部分内容即可完成对Android运行库的移植和修改。
媒体推荐
《Android技术内幕:系统卷》是Android开发者不可不看的一本翔实、全面、深入的Android著作,对Android系统的源代码进行了全面的分析,不愧为“技术内幕”,强烈推荐!——机锋网
毫无疑问,移动开发是软件开发领域当下最热门的话题之一,其中Android开发又是移动开发领域的焦点。目前市面上关于Android开发的书很多,但是从源代码角度系统分析Android架构设计与实现原理的书却没有,本书填补了市场空白。本书有两大特点:宏观上足够有高度,Android系统的五层架构一览无余;微观上足够有深度,对Android系统的Linux内核层、硬件抽象层和系统运行库层得各个模块的实现原理进行了深入的分析。这是一本难得的经典之作,强烈推荐!——51CTO移动开发频道
无论是移动设备的数量、开发者的数量,还是是应用程序的数量,Android均已超过iOS,可谓势不可挡。目前绝大部分的Android应用都运行于手机和平板电脑等智能移动终端上,但是Android逐渐开始被工业领域的一些设备所采用,开发者将面临更加复杂的应用环境,这要求开发者对Android系统有更深入的理解。本书的出版可谓恰逢其时,是开发者系统学习Android系统原理的宝贵资料,也是目前唯一可参考的中文资料。不可错过! ——AOA(傲卓网)
如果你是一位高级Android应用开发工程师,或者是一位Android系统开发工程师,或者是一位Android移植工程师,再或者是一位Android应用架构师,本书对你来说将非常有价值。它对Android系统的Linux内核层、硬件抽象层和系统运行库层的各模块的进行了细致入微地分析,足以帮你解决平时开发过程中遇到的与系统底层相关度的难题。极力推荐 。——Andriod开发者社区
自Android在国内普及以来,市面上关于Android的图书如雨后春笋般涌现,但是大多数图书都是引领读者入门的,重在讲解Android上层的框架及其使用,真正从底层去分析和阐述Android架构设计与实现原理的书屈指可数,本书恰好是这方面的一本佳作。如果想了解Android的整体结构和底层原理,这本书请不要错过! ——姚尚朗(IceskYsl) eoeAndroid社区创始人兼CTO/畅销书作家
目 录 [ - ]
[*]《Android技术内幕:系统卷》前 言
[*]《Android技术内幕:系统卷》目录
[*]第1章 准备工作
[*]1.1深入认识Android
[*]1.1.1Android的系统构架
[*]1.1.2Android的初始化流程
[*]1.1.3各个层次之间的相互关系
[*]1.1.4Android系统开发(移植)和应用开发
[*]1.2获取和编译Android的源码
[*]1.2.1环境配置
[*]1.2.2获取Android源码
[*]1.2.3编译Android的源码及其工具包
[*]1.2.4运行Android系统
[*]1.3开发环境搭建
[*]1.3.1应用开发环境搭建
[*]1.3.2源码开发环境搭建
[*]1.4Android源码结构
[*]1.5小结
[*]第2章 Android的内核机制和结构剖析本章主要内容
[*]2.1Linux与Android的关系
[*]2.1.1为什么会选择Linux
[*]2.1.2Android不是Linux
[*]2.2Android对Linux内核的改动
[*]2.2.1Goldfish
[*]2.2.2YAFFS2
[*]2.2.3蓝牙
[*]2.2.4调度器(Scheduler)
[*]2.2.5Android新增的驱动
[*]2.2.6电源管理
[*]2.3Android对Linux内核的增强
[*]2.3.1Alarm(硬件时钟)
[*]2.3.2Ashmem(匿名内存共享)
[*]2.3.3Low Memory Killer(低内存管理)
[*]2.3.4Logger(日志设备)
《Android技术内幕:系统卷》前 言 http://www.iteye.com/images/wiki/top.gif?1324994303 经过三年的发展,Android已经从最初的智能电话领域逐渐进入教育、医疗、军事、汽车、家居等重要领域。它一路披荆斩棘,攻城拔寨,发展势头有目共睹,已经成为移动平台领域当之无愧的王者。目前,已有众多设备开始选择使用Android系统,比如智能手机、智能电视、平板电脑、上网本、MP3、MP4、智能相机等;相信在不久的未来,还将有更多采用Android系统的高科技产品进入我们的生活。这些设备将产生各种各样的应用需求,尤其是与Android系统底层相关的应用,这将给开发者带来大量的机会,尤其是系统级应用开发工程师。
Android基于Linux内核,但它并不是标准的Linux。因为Google为了让Android更适合移动手持设备,对Linux内核进行了各种优化和增强,这些增强的部分也正是从事Android系统开发的嵌入式系统工程师所急需了解的内容;同时Android的源代码不仅复杂,而且代码量巨大,各模块之间联系紧密。这让大多数Android应用开发者不知从何处入手,他们都希望能够有一本系统且全面的,对Android内核的构架和实现原理进行分析的书,而国内目前分析Android底层实现的书籍甚少。因此,笔者对自身的实战经验进行了总结和整理,编写了本书,希望能够帮助众多Android应用开发者更快、更深入地理解Android各个部分的具体实现,从而为开发各种系统级的应用做好准备。
本书分为两卷,系统卷和应用卷。系统卷主要分析Android系统层的实现,包括Android系统构架中的下面三层:Linux内核层、硬件抽象层、系统运行库层;应用卷介绍Android系统构架中的上面两层,重在分析Android应用层的实现,包括应用程序框架层和应用层。具体如图1所示:
http://dl.iteye.com/upload/attachment/504110/be1b7338-c79f-3dee-bdae-3267eb6b68c0.jpg
图中Linux内核(Kernel)部分是本书系统卷第一部分的内容,主要分析Android的核心驱动程序的实现,包括驱动程序的系统构架、原理和实现。掌握这部分内容后,读者将能够修改和编写Android的各个设备驱动程序。紧接着上面则是硬件抽象层,本书第7章通过大量篇幅深入分析了Android中各个模块的硬件抽象层实现,使读者在掌握Android中已有的硬件设备接口实现的同时,能够独立编写适合自己的硬件设备驱动的抽象层接口。图中的Libraries部分即本书的第6章,分析了Android的系统库、程序库和功能库的具体实现,它能让读者在理解Android的各种功能的底层实现的同时,还能按照功能需求进行扩展和优化。最后,图中的Android运行时(Runtime)部分又分为Dalvik虚拟机和核心库两部分,分别在本书的第8章和第9章介绍,剖析了Dalvik虚拟机的构架与实现,以及Android核心库和API的运作机制,使读者能够完成Android运行库的移植和修改。
本书面向的读者 本书(系统卷)主要分析了Android系统底层的构架与实现原理,从源代码的获取和系统开发环境的搭建,到Android Kernel的核心实现,再到硬件抽象层和Android运行库等各个模块的细节实现,让读者可以从更深的层次去理解Android的系统构架,并对Android系统进行移植和二次开发。阅读本书的一个必要条件是对Linux内核有一定了解,因此本书(系统卷)非常适合以下开发人员阅读:
* Android系统开发/移植工程师
* Android驱动开发/移植工程师
* Android系统构架师
* 嵌入式系统工程师
应用卷则重在分析Android的应用层和应用程序框架层的运作机制,从基础的应用程序剖析入手,到应用程序API的实现,再到各模块的原理,让读者对Android有更深入、更全面的认识,同时结合商业案例的分析,让读者不仅能使用API开发应用,更能对API功能进行扩展,从而满足开发中的各种需求。因此,应用卷非常适合以下人员阅读:
* Android应用开发/移植工程师
* Android游戏开发/移植工程师
* Android构架师
如何阅读本书 在编写本书之前,笔者收到很多《Android应用开发揭秘》一书的读者发来的邮件,他们都希望有一本能深入讲解Android实现原理的书籍,因此编写了本书,旨在帮助众多开发者晋级。本书分为两册,如果是进行系统级开发,建议阅读本卷;如果是进行应用开发,建议阅读应用卷。
本书是以Android源码为基础进行分析的,因为源码内容很多,不能全部列出来,因此笔者建议在阅读本书的同时,最好能对照查看Android的源码实现(本书的所有代码清单都指明了它在源码中的路径,以方便大家查看);另外,本书中有多处标记为“注意”、“扩展学习”的内容,都是一些实战经验。最后,虽然Android的各部分联系紧密,但各个部分的讲解都较为完整,大家仍然可以根据需要调整阅读顺序。
致谢
感谢所有在本书写作过程中给予过我指导、帮助和鼓励的朋友,尤其是本书的策划编辑杨福川,他不仅对本书提出了宝贵的写作建议,而且还和他的同事曾珊对书稿进行了仔细的审阅。
感谢一直以来信任、鼓励和支持我的父母和其他亲人。
最后还要感谢我的女友,正是你的爱与支持,才使我有了今天的收获。
虽然我热切地希望与广大读者朋友分享Android系统的底层实现技术,但由于时间有限,书中难免存在疏漏与错误,诚恳地希望各位读者批评和指正。如果发现书中有任何问题,或是想与我交流关于Android开发的相关话题,欢迎通过Android.Yarin@gmail.com与我联系。希望能结识更多的朋友,大家共同进步。
《Android技术内幕:系统卷》目录 http://www.iteye.com/images/wiki/top.gif?1324994303 前 言
第1章准备工作 /1
1.1深入认识Android /2
1.1.1Android的系统构架 /2
1.1.2Android的初始化流程 /5
1.1.3各个层次之间的相互关系 /8
1.1.4Android系统开发(移植)和应用开发 /11
1.2获取和编译Android的源码 /13
1.2.1环境配置 /13
1.2.2获取Android源码 /14
1.2.3编译Android的源码及其工具包 /16
1.2.4运行Android系统 /21
1.3开发环境搭建 /23
1.3.1应用开发环境搭建 /23
1.3.2源码开发环境搭建 /26
1.4Android源码结构 /32
1.5小结 /33
第2章Android的内核机制和结构剖析 /34
2.1Linux与Android的关系 /35
2.1.1为什么会选择Linux /35
2.1.2Android不是Linux /35
2.2Android对Linux内核的改动 /37
2.2.1Goldfish /37
2.2.2YAFFS2 /38
2.2.3蓝牙 /39
2.2.4调度器(Scheduler)/39
2.2.5Android新增的驱动 /40
2.2.6电源管理 /41
2.2.7杂项 /41
2.3Android对Linux内核的增强 /42
2.3.1Alarm(硬件时钟)/43
2.3.2Ashmem(匿名内存共享)/46
2.3.3Low Memory Killer(低内存管理)/52
2.3.4Logger(日志设备)/56
2.3.5Android PMEM /65
2.3.6switch /79
2.3.7Timed GPIO /88
2.3.8Android Ram Console /94
2.4小结 /99
第3章Android的IPC机制--Binder /100
3.1Binder概述 /101
3.1.1为什么选择Binder /101
3.1.2初识Binder /102
3.2Binder驱动的原理和实现 /102
3.2.1Binder驱动的原理 /102
3.2.2Binder驱动的实现 /103
3.3Binder的构架与实现 /132
3.3.1Binder的系统构架 /132
3.3.2Binder的机制和原理 /133
3.4小结 /150
第4章电源管理 /151
4.1电源管理概述 /152
4.2电源管理结构 /152
4.3Android的电源管理机制 /153
4.4Android电源管理机制的实现 /154
4.5小结 /187
第5章驱动的工作原理及实现机制 /188
5.1显示驱动(Framebuffer)/189
5.1.1Framebuffer的工作原理 /189
5.1.2Framebuffer的构架 /190
5.1.3Framebuffer驱动的实现机制 /190
5.2视频驱动(V4L和V4L2)/201
5.2.1V4L2介绍 /201
5.2.2V4L2的原理和构架 /201
5.2.3V4L2的实现 /202
5.3音频驱动(OSS和ALSA)/208
5.3.1OSS与ALSA介绍 /208
5.3.2OSS的构架与实现 /209
5.3.3ALSA的构架与实现 /213
5.4MTD驱动 /214
5.4.1MTD驱动的功能 /214
5.4.2MTD驱动的构架 /215
5.4.3MTD驱动的原理及实现 /215
5.5Event输入设备驱动 /223
5.5.1Input的系统构架 /223
5.5.2Event输入驱动的构架 /224
5.5.3Event输入驱动的原理 /224
5.5.4Event输入驱动的实现 /225
5.6蓝牙驱动(Bluetooth)/235
5.6.1Bluetooth驱动的构架 /235
5.6.2BlueZ的原理及实现 /237
5.7WLAN驱动(Wi-Fi)/244
5.7.1WLAN构架 /244
5.7.2Wi-Fi驱动的实现原理 /245
5.8小结 /245
第6章原生库的原理及实现 /246
6.1系统C库(Bionic Libc)/247
6.1.1Bionic Libc功能概述 /247
6.1.2Bionic Libc实现原理 /248
6.2功能库 /258
6.2.1WebKit构架与实现 /258
6.2.2多媒体框架与实现 /275
6.2.3Android SQLite框架及原理 /285
6.3扩展库 /289
6.3.1Skia底层库分析 /289
6.3.2OpenGL底层库分析 /299
6.3.3Android-OpenSSL实现及运用 /306
6.3.4FreeType及Font Engine Manager /317
6.3.5FreeType结构体系和渲染流程 /317
6.4原生服务 /328
6.4.1AudioFlinger实现 /328
6.4.2SurfaceFlinger实现 /341
6.5小结 /353
第7章硬件抽象层的原理与实现 /354
7.1硬件抽象层的实现原理 /355
7.1.1Android HAL构架 /355
7.1.2Android HAL的实现 /357
7.2Android Overlay构架与实现 /361
7.2.1Android Overlay系统构架 /361
7.2.2Overlay HAL框架与实现 /362
7.2.3Overlay与SurfaceFinger /369
7.3Android Camera 构架与实现 /375
7.3.1Android Camera系统构架 /375
7.3.2Camera HAL框架与实现 /377
7.3.3Camera本地实现 /385
7.4Android Audio HAL实现 /394
7.4.1Audio HAL框架 /395
7.4.2Android默认的Audio HAL实现 /398
7.4.3DUMP功能的Audio HAL实现 /400
7.4.4基于A2dp的蓝牙音频设备HAL实现 /402
7.4.5模拟器上的Audio HAL实现 /403
7.5Android RIL实现 /404
7.5.1Android RIL构架 /404
7.5.2radiooptiongs实现 /407
7.5.3libril库实现 /409
7.5.4reference-ril库实现 /415
7.5.5RILD守护进程实现 /418
7.5.6request流程分析 /423
7.5.7response流程分析 /427
7.6Android Sensor HAL实现 /434
7.6.1Android Sensor构建 /434
7.6.2Sensor HAL接口 /435
7.6.3Sensor HAL实现 /438
7.7Android WIFI HAL实现 /441
7.7.1Android WIFI系统构架 /441
7.7.2wpa_supplicant框架 /442
7.7.3WIFI HAL实现 /444
7.8Android蓝牙本地实现 /447
7.8.1Android蓝牙构架 /447
7.8.2BlueZ结构体系 /448
7.8.3BlueZ适配层 /452
7.9Android 定位实现 /453
7.9.1定位系统构架 /453
7.9.2GPS HAL实现 /454
7.10Android Power HAL实现 /459
7.11Android Vibrator HAL实现 /461
7.12小结 /462
第8章Dalvik虚拟机的构架、原理与实现 /463
8.1Dalvik虚拟机概述 /464
8.1.1什么是Dalvik虚拟机 /464
8.1.2Dalvik虚拟机的功能 /464
8.1.3Dalvik虚拟机与Java虚拟机的区别 /465
8.2Dalvik构架与实现 /466
8.2.1Dalvik系统构架 /466
8.2.2dx和dexdump工具 /468
8.2.3.dex文件格式解析 /470
8.2.4Dalvik内部机制 /487
8.2.5Dalvik进程管理 /492
8.2.6Dalvik内存管理 /501
8.2.7Dalvik加载器 /509
8.2.8Dalvik解释器 /517
8.2.9Dalvik JIT /519
8.3JNI的构架与实现 /523
8.3.1JNI构架 /523
8.3.2JNI实现 /524
8.4小结 /526
第9章Android 核心库 /527
9.1Android核心库简介 /528
9.2Android系统API /529
9.2.1android包 /529
9.2.2android资源包 /529
9.2.3ApiCheck机制 /529
9.3小结 /532
后记 /533
第1章 准备工作 http://www.iteye.com/images/wiki/top.gif?1324994303 本章主要内容
q您认识Android吗?
q开发者能基于Android做些什么?
q如何才能更好地利用Android这个开源项目?
q如何搭建Android源码开发环境?
自2007年11月发布以来,Android已经经历了数个版本的更新,市面上采用该系统的移动设置数量也在飞速增长,目前,它已经是一个强大而成熟的系统,但是Google并没有停止,也没有减慢研发速度,而是在更加努力地将它做得更好、更完美!让我们带着前面的问题重新审视Android,一起迎接移动互联网的未来。
1.1深入认识Android
首先,我们有必要对Android进行深入的认识。如果你或多或少对Android有一些了解,我仍然建议大家仔细地阅读本章的内容,因为你能从不同的侧面和深度重新认识Android,这些内容是开发者必须了解的。如果你之前对Android没有任何了解,也不必担心,我们将仔细地为你讲解Android的每一个知识点。
这一节我们将为大家解答本章开头列出的部分问题,首先,将分析Android的众多技术亮点,让你对Android的概念以及它的功能有一个全面的了解;其次,分析Android的系统构架、剖析Android的初始化流程以及各个层次之间的关系,让你能够看到Android更真实的一面;最后,介绍了Android系统开发和应用开发相关的基础概念。
内容为大家准备得够丰富吧!是不是有些迫不及待了,让我们一起开始吧。
1.1深入认识Android http://www.iteye.com/images/wiki/top.gif?1324994303 首先,我们有必要对Android进行深入的认识。如果你或多或少对Android有一些了解,我仍然建议大家仔细地阅读本章的内容,因为你能从不同的侧面和深度重新认识Android,这些内容是开发者必须了解的。如果你之前对Android没有任何了解,也不必担心,我们将仔细地为你讲解Android的每一个知识点。
这一节我们将为大家解答本章开头列出的部分问题,首先,将分析Android的众多技术亮点,让你对Android的概念以及它的功能有一个全面的了解;其次,分析Android的系统构架、剖析Android的初始化流程以及各个层次之间的关系,让你能够看到Android更真实的一面;最后,介绍了Android系统开发和应用开发相关的基础概念。
内容为大家准备得够丰富吧!是不是有些迫不及待了,让我们一起开始吧。
1.1.1Android的系统构架 http://www.iteye.com/images/wiki/top.gif?1324994303 要深入学习Android,首先需要学习Android的系统构架。Android的系统构架和其操作系统一样,采用了分层的构架,层次非常清晰,因此要掌握它的构架并不难。前言中的图1为Android的系统构架图,如果你对该图已经不陌生,并且理解图中所示的构架,那么你可以跳过这部分内容(或者快速浏览);如果你是第一次见到该图,建议你详细阅读该部分内容,因为整本书的内容都是以这幅图为基础的。我们会对图中的每一个模块进行详细地分析,让你真正掌握Android的技术内幕。 从前言中的图 1 可以看出,Android分为五层,从高层到低层分别是应用程序层(Applications)、应用程序框架层(Application Framework)、系统运行库层(Libraries和Android Runtime)和Linux内核层(Linux Kernel)。下面分别来看各个层次为我们提供了什么功能,以及如何来运用这些功能。
1.应用程序层
Android会与核心应用程序包一起发布,该应用程序包包括图1为大家展示的主屏(Home)、E-mail客户端、SMS/MMS短消息程序、日历、地图、浏览器、联系人管理程序等。所有的应用程序都是使用Java语言编写的,通过调用应用程序框架层(Application Framework)所提供的API来完成。当然,你也可以使用Java通过JNI的方式,配合Android NDK来开发原生的应用程序,这样可以提高应用程序的效率,但是难度也大大增加——你需要精通C和C++等语言,并且对Android NDK所提供的为数不多的功能有很深的认识。因为Android NDK提供的功能不是太多,为了避免你做了很久之后才发现——原来NDK不支持某项功能,大家可以根据自己的需求来选择是否采用NDK开发原生程序。
2.应用程序框架层
应用程序框架层为开发人员提供了可以完全访问核心应用程序所使用的API框架。该应用程序的构架设计简化了组件的重用,任何一个应用程序(以及任何其他应用程序)都可以发布自己的功能模块(在遵循框架的安全性限制的前提下)。同样,该应用程序重用机制也使用户可以方便地替换程序组件。下面来看一下该层的每一个模块为我们提供的组件,如表1-1所示。
http://dl.iteye.com/upload/attachment/504195/c81b142c-e523-3231-ab58-b87231abc681.jpg 3.系统运行库层
系统运行库层包括程序库和Android运行库两部分,下面分别来介绍这两个部分。
(1)程序库
Android 包含一些C/C++库,这些库能被Android系统中的不同组件使用,它们通过应用程序框架为开发者提供服务,表1-2给出了这些核心库的功能介绍。
http://dl.iteye.com/upload/attachment/504204/1be95020-e30f-3a38-baa5-a904503ea395.jpg
http://dl.iteye.com/upload/attachment/504206/5bc84581-2fa6-3b78-9b66-65f3d39955ae.jpg
(2)Android 运行时库
从图1可以看出,Android运行时库又分为核心库和Dalvik虚拟机两部分。核心库提供了Java语言核心库的大多数功能,这里主要通过JNI的方式向应用程序框架层提供调用底层程序库的接口。Dalvik虚拟机是为了能同时高效地运行多个VMs而实现的。Dalvik虚拟机执行.dex的Dalvik可执行文件,该格式的文件针对最小内存使用做了优化。Dalvik虚拟机是基于寄存器的,所有的类都经由Java汇编器编译,然后通过SDK中的dx工具转化成.dex格式并由虚拟机执行。Dalvik虚拟机依赖Linux的一些功能,比如线程机制和底层的内存管理机制。每一个Android应用程序都在它自己的进程中运行,都拥有一个独立的Dalvik虚拟机实例。关于这部分内容,先简单向大家介绍到这里,后面会给大家详细分析Android SDK工具包和Dalvik虚拟机。4.Linux内核层
Android依赖于Linux 2.6版内核提供的核心系统服务,例如安全、内存管理、进程管理、网络栈、驱动模块等。内核作为一个抽象层,存在于软件栈层和硬件层之间,Android对内核进行了增强,主要表现在以下几个方面:
* 硬件时钟 (Alarm)
* 内存分配与共享 (Ashmem)
* 低内存管理器 (Low Memory Killer)
* Kernel调试 (Kernel Debugger)
* 日志设备 (Logger)
* Android IPC机制 (Binder)
* 电源管理 (Power Management)
到这里,我们对Android系统构架的分析就告一段落了,你现在至少应该明白整个Android系统的构架和每个模块的功能,以便为后面的深入学习打下基础。Android系统是如何启动的?启动过程中需要初始化什么内容?如果你不知道(或不完全知道)这些问题的答案,不必担心,下一小节将剖析Android系统启动时的初始化操作。
1.1.2Android的初始化流程 http://www.iteye.com/images/wiki/top.gif?1324994303 上一小节分析了Android的系统构架,让大家对Android系统有了一个深入的认识;但是,Android系统本身非常庞大,在深入分析每个模块的具体功能之前,有必要对其启动过程进行分析。我们需要了解这么一个庞大的系统在启动的时候需要执行哪些初始化操作。
Android系统在启动时首先会启动Linux基础系统,然后引导加载Linux Kernel并启动初始化进程(Init),如图1-1所示。
http://dl.iteye.com/upload/attachment/504169/02591e64-3ae0-31e9-a0da-64ca7e8cae96.jpg
接着,启动Linux守护进程(daemons)。这个过程主要需要启动以下内容,如图1-2所示。
*启动USB守护进程(usbd)来管理USB连接。
*启动Android Debug Bridge守护进程(adbd)来管理ADB连接。
*启动Debug守护进程(debuggerd)来管理调试进程的请求(包括内存转换等)。
*启动无线接口守护进程(rild)来管理无线通信。
http://dl.iteye.com/upload/attachment/504214/21e5db01-7e1b-39f8-b9c8-5bd37a7596d2.jpg
在启动Linux守护进程的同时还需要启动Zygote进程,如图1-3所示。它主要包括以下需要启动和注册的内容:
*初始化一个Dalvik虚拟机实例。
*装载Socket请求所需的类和监听。
*创建虚拟机实例来管理应用程序的进程。
http://dl.iteye.com/upload/attachment/504218/4dc977f9-a939-3d00-a3aa-374bde6a9186.jpg
再接着,需要初始化runtime进程,初始化过程如图1-4所示。在这个过程中需要处理以下操作:
*初始化服务管理器。
*注册服务管理器,以它作为默认Binder服务的Context管理器。
http://dl.iteye.com/upload/attachment/504222/89f0f459-6cc2-3671-8bd8-a954da55618d.jpg
runtime进程初始化之后,runtime进程将发送一个请求到Zygote,开始启动系统服务,这时Zygote将为系统服务进程建立一个虚拟机实例,并启动系统服务,如图1-5所示。
http://dl.iteye.com/upload/attachment/504224/0a6becb8-2cea-344e-9f7e-ed00889d23bd.jpg
紧接着,系统服务将启动原生系统服务,主要包括Surface Flinger和Audio Flinger。这些本地系统服务将注册到服务管理器(Service Manager)作为IPC服务的目标,如图1-6所示。
http://dl.iteye.com/upload/attachment/504226/902b856e-4321-3405-896e-5be46731d5d6.jpg
系统服务将启动Android管理服务,Android管理服务将都被注册到服务管理器上,如图1-7所示。
http://dl.iteye.com/upload/attachment/504228/43330338-781e-3f0d-be07-d9343bdbc004.jpg
最后,当系统加载完所有的服务之后会处于等待状态,等待程序运行。但是,每一个应用程序都将启动一个单独的进程。如图1-8所示,系统启动了一个Home进程和一个Contacts进程。那么,各个进程之间如何进行交互呢?这就需要使用IPC机制了,后面会详细介绍。
http://dl.iteye.com/upload/attachment/504230/1b710851-776b-34e0-a1b4-338cb882120e.jpg
到这里,系统的整个启动过程就结束了,可以在上面运行应用程序了。你也应该对Android系统启动过程的每一步都有了深入的理解。实际上,这个启动过程就是从Android系统构架图中最底层的Linux内核层一步一步加载和注册到应用程序框架层,最终在应用程序层运行我们自己的应用程序。那么,各个层次之间又有什么样的关系呢?应用程序在最上层又是如何调用到最底层的核心服务的呢?下一小节将为大家分析各个层次之间的关系。
1.1.3各个层次之间的相互关系 http://www.iteye.com/images/wiki/top.gif?1324994303 上一小节介绍了Android系统的启动过程,本小节将介绍Android应用程序是如何按照层次关系来调用最底层的硬件和服务的。如果你对上一节的内容理解得还不够深入,那么可以回去再仔细地看一遍,这样才能更好地理解本小节的内容。好的,相信你已经准备好了,开始吧。
在Android中运行的应用程序都是通过以下三种方式来层层深入的:
* App→Runtime Service→Lib
* App→Runtime Service→Native Service→Lib
* App→Runtime Service→Native Daemon→Lib
下面就分别来分析这三种方式,我们还是采用流程图的方式来为大家展示。
App→Runtime Service→Lib方式对应的流程图如图1-9所示。
http://dl.iteye.com/upload/attachment/504233/f80f78b3-c5a4-3e5c-bfbd-26c9dae8b5a0.jpg
通过图1-9我们可以看出,在Android平台上,应用程序首先是在应用程序层通过Binder IPC调用应用程序框架层的Runtime Service,然后再通过JNI与运行库中的原生服务绑定,并动态地加载Hal库,进而调用Linux内核层的Kernel Driver。为了便于大家更好地理解,我们通过一个实例(Location Manager)来为分析该流程,如图1-10所示。
http://dl.iteye.com/upload/attachment/504253/a5346217-d353-3cef-b53b-d7e80fe4ccbe.jpg
以上就是第一种方式的调用过程,接下来我们再看一下第二种方式(App→Runtime Service→Native Service→Lib)是如何调用的。这种方式通常被Android原生服务所采用,同样先看一下调用流程图,如图1-11所示。
http://dl.iteye.com/upload/attachment/504237/cf5a8006-1378-3d6c-90ae-901cabb300a7.jpg
图1-11为我们展示了Android原生服务的调用流程,可以看出,与第一种方式相比,只多了一个通过IPC机制调用原生服务并进行动态装载的过程。所以,这里我们就不再重复介绍,但还是给出一个Audio的例子,如图1-12所示。
http://dl.iteye.com/upload/attachment/504239/e7d1a5e4-8d0c-3391-913b-5127cd056b25.jpg
从图1-12可以看出,应用程序调用了应用程序框架层的MediaPlayer,然后调用系统运行库层的MediaPlaye。这时MediaPlaye又分别调用了Media Framework和AudioFlinger,而后通过AudioFlinger调用指定的库(libaudio.so),最后才调用到Kernel Driver。
下面来看一下最后一种方式“App→Runtime Service→Native Daemon→Lib”,如图1-13所示。这种方式通常用于守护进程的连接。
http://dl.iteye.com/upload/attachment/504241/5619088e-ae7c-3127-9698-6be7b391f961.jpg
从图1-13可以看出,这种方式比原生服务的调用更简单,它直接通过JNI绑定原生服务,再通过sockets调用守护进程进行动态加载。下面就来看一个简单的例子,电话管理(Telephony Manager)的调用就是这样一个原生的守护进程调用,其流程如图1-14所示。
http://dl.iteye.com/upload/attachment/504247/afa3bd38-f568-30e2-8c5e-b302dd76c2ec.jpg
这个调用的过程非常简单,相信图1-14已经表述得足够清楚了。
1.1.4Android系统开发(移植)和应用开发 http://www.iteye.com/images/wiki/top.gif?1324994303 通过前面的学习,我们了解到Android是一个庞大且完善的系统,我们可以基于Android来进行哪些开发工作?每一种开发工作又有什么不同?不同的开发工作需要掌握的技术有什么差异?学完本小节的内容你就会知道应该如何利用Android来开发自己所需要的产品。
因为Android是一个开源的操作系统,所以可以选择的开发方式主要有以下两种:
* Android系统开发(移植)
* Android应用开发
1.Android系统开发(移植)
Android系统开发(移植)属于底层的开发工作,主要针对Android系统本身进行完善和将其移植到其他的硬件平台,因此需要掌握Android系统构架中的Linux内核层和系统运行库层,如图1-15所示。
Android系统开发主要涉及Libraries和Android Runtime这两部分内容,一般就是在本地编写C/C++代码,然后通过JNI向上层提供调用接口,主要是为应用开发提供服务等工作。
Android系统移植则主要涉及硬件抽象层和Linux 内核层,移植的主要工作是将驱动移植到不同的硬件上,使其能完美地运行Android系统。这些驱动又主要包括设备驱动和Android专用驱动。
http://dl.iteye.com/upload/attachment/504251/81977533-d8c2-3e72-9676-7abcf1db8d64.jpg
无论是系统开发还是系统移植,都是由底层直接操作Android的源代码,因此在开发之前,我们首先就需要准备Android的源码。下一节将向大家详细介绍如何获取和编译Andorid的源码,以及Android源码的结构。
2.Android应用开发
Android应用开发主要是根据Android系统所提供的API来完成各种应用程序的编写,所使用的API属于Android的应用框架层,如图1-16所示。如果Android系统没有提供某些API,那么只能通过在Android系统底层编写C/C++代码来开发这些API并向Android应用框架层提供接口。但是笔者不建议这样做,因为这很可能会导致你的应用程序与其他Android系统不兼容。
http://dl.iteye.com/upload/attachment/504249/540616da-554f-38d5-9b95-c7705286e677.jpg
我们自己所开发出来的应用程序与应用层的应用程序运行在同一个层次上,因此再次提醒大家,开发应用时一定要遵循Android API框架的规定,以避免开发的应用程序不兼容。
另外,应用开发属于上层开发,不需要接触底层的C/C++代码,所以,开发前我们只需要准备Android SDK和模拟器即可(当然,你可能也需要使用一款IDE,推荐使用Eclipse)。最后,Android SDK既可以通过编译源码获得,也可以在Android的官方网站下载编译好的SDK。下面将讲解如何通过编译源码的方式来取得Android SDK,以及SDK的结构和开发工具包。
1.2获取和编译Android的源码 http://www.iteye.com/images/wiki/top.gif?1324994303 这一节我们将从零开始讲解如何取得并编译Android的源码及其常用工具。在这个过程中,你可以和本书一起准备好更加深入学习Android所必需的环境和工具,同时也能对Android源码的结构有一个整体的认识。
1.2.1环境配置 http://www.iteye.com/images/wiki/top.gif?1324994303
由于我们需要对Android系统进行开发(移植),所以就必须下载Android的源代码。如果只是查看源代码,在线浏览即可,因为:第一,Android的源代码庞大,下载需要很长的时间;第二,官方所支持的Android源代码下载系统仅为Linux和Mac OS(当然,在Windows下也能下载,但是建议大家还是在Linux下下载)。这里我们将以Ubuntu 10.04为例,为大家详细介绍如何获取和编译Android的源代码。
我们从Android源码的官方网站(http://source.android.com)取得的关于配置需求的信息如下:
? 磁盘 需要6GB以上剩余空间
? Linux系统 Ubuntu 6.06以上版本
? Git工具 Git 1.5.4以上版本
? Java环境 JDK 5.0 update12以上版本
? Python Python 2.4以上版本
? 依赖的deb包 flex、bison、gperf、libsdl-dev、libesd0-dev、libwxgtk2.6-dev、
build-essential、zip、curl
? 调试工具 valgrind
首先,准备好所需要的Linux操作系统以及磁盘空间。由于Ubuntu10.04系统自带的源里找不到JDK1.5的安装包,因此我们要做的第一件事是增加两个软件源(如果你能找到SDK1.5的安装包则不需要),如下:
deb http://tw.archive.ubuntu.com/ubuntu/jaunty main restricted universe multiverse
deb-src http://tw.archive.ubuntu.com/ubuntu/ jaunty main restricted universe multiverse
然后,开始准备下载和编译Android源码的环境:
1)安装JDK1.5,在终端执行命令如下:
$ sudo apt-get install sun-java5-jdk
2)安装开发过程中需要的一些开发包,在终端执行以下命令:
$ sudo apt-get install git-core gnupg valgrind flex bison gperf libsdl-dev libesd0-dev libwxgtk2.6-dev build-essential zip curl libncurses5-dev zlib1g-dev
到这里,你已经准备好了获取Android源码所需要的环境,下面将开始获取Android源码。
1.2.2获取Android源码 http://www.iteye.com/images/wiki/top.gif?1324994303 上一节已经准备好了Android源码开发所需的环境(请确保你所准备的环境无误),这一节将从Android源码服务器获取Android源码。由于Android的源码非常庞大,所以需要分为两部分来获取,分别是Android源码和Android内核(Android Linux Kernel)。
1.获取Android源码的步骤
如果要获取Android的完整源码,则需要按照如下步骤操作:
1)在用户目录下新建一个文件夹用来存放我们的Android目录,在终端执行如下命令:
mkdir Android
cd Android
mkdir bin
cd bin
curl http://android.git.kernel.org/repo > repo
稍等片刻即可完成,如图1-17所示。
http://dl.iteye.com/upload/attachment/504265/7dd04b63-ba1e-3c56-82b2-22cd102584d9.jpg
2)建立存放Android源代码的目录source,并初始化版本,在终端执行如下命令:
sudo chmod a+x repo
cd
cd Android
mkdir source
cd source
../bin/repo init -u
git://android.git.kernel.org/platform/manifest.git
这次需要等的时间可能会稍微长一点,如图1-18所示。
http://dl.iteye.com/upload/attachment/504267/cd5e04b4-4953-35b3-b79c-f933cbf88db3.jpg
3)之后要求输入用户名和邮箱地址,可随机输入。如果你需要向Google递交Bug,那么就输入你的Google账户,如图1-19所示。完成之后如果出现“repo initialized in / home/yarin/Android”,则表示初始化完毕,可以开始下载了。
http://dl.iteye.com/upload/attachment/504269/c88d8407-ffdb-3b9b-9509-74f069765a50.jpg
注意在显示初始化完成时,出现了一个路径,其中包含一个“yarin”字符,熟悉Linux的朋友可能已经明白为什么了,但这里还是说明一下。笔者在编写本书时,所使用的Linux系统的用户名为yarin,而且所建立的用于存放Android源码的目录也在用户目录中,以后还会多次出现,所以我们就不再详细解释了。
4)在终端执行如下命令,开始下载,如图1-20所示。
http://dl.iteye.com/upload/attachment/504271/b2e59878-7a62-3841-9d4b-553f9df9fd49.jpg
注意以上操作下载的是全部的源码,完全下载需要很长的时间。你也可以在
../bin/repo init -u git://android.git.kernel.org/platform/manifest.git
之后加入参数来指定下载某一部分源码。请参见在第二步之后出现的帮助信息(图1-18)。下载过程中可能会出现连接超时或者服务器繁忙的错误。
下载完成之后,你就得到了完整的Android源码。前面说过需要分为两部分来下载,并且这两部分是可同时下载,彼此不会受到任何影响。因此,为了节约时间,我们马上开始下载第二部分源码。
2.下载Android内核
在下载之前,我们先来看看Android内核属于哪一部分。首先,打开Android的开源工程官方网站(http://android.git.kernel.org/),Android内核主要包括了如图1-21所示的内容,这部分内容在我们使用“../bin/repo syn”下载的时候并不会被一起下载。如果你不需要对Android内核进行修改和移植,可以不需要这部分内容,可以从Android模拟器中提取这部分内容的镜像。但是,本书需要关注这部分内容,并会对它进行详细分析,所以需要下载它。
http://dl.iteye.com/upload/attachment/504273/d117b299-6342-320e-abc8-5bd11a698796.jpg
下载Android内核的具体步骤如下:
首先,进入上面我们创建的存放Android源码的目录,新建一个存放Android内核的目录“kernel”,并进入该目录,在终端执行如下命令,开始下载,如图1-22所示。
http://dl.iteye.com/upload/attachment/504275/7c5c1def-d9ec-3f1b-863b-ed1e18ee64f9.jpg
下载Android内核时间会比下载Android源码的时间短很多。需要注意的是,Android内核的下载不能被中断,而Android源码支持断点下载。下一小节将为大家介绍编译的全过程。
下载完成之后在存放Android源码的目录中也许看不到任何内容,不必担心,那是因为下载的文件是隐藏的,你需要显示隐藏文件才能看见。下载完成之后,你可以在终端通过du -sk或du -sm命令检测文件夹的大小,以确保全部下载完成。如果你按本书的方式进行全部下载,那么总大小会接近4GB左右。另外,还需要编译和开发,所以请确保磁盘空间大于10GB。
1.2.3编译Android的源码及其工具包 http://www.iteye.com/images/wiki/top.gif?1324994303
功夫不负有心人!终于下载完了,下面我们就开始编译。接下来的内容并不难,但是需要十分仔细(比如:不能多一个空格,也不能少某个步骤),否则编译过程中就可能有意想不到的“惊喜”降临到你身上。当然,你也不必紧张,只要按照下面的步骤一步一步操作,你自己编译的Android系统最后肯定能够运行成功。
1.编译Android源码
只要确保源码下载是完整的,编译过程中一般不会出错。笔者编译过很多次,都没有遇到意外情况,因为Android文件系统已经自带了经过优化的交叉编译工具,并且为所有的源码都提供了经过验证的makefile,所以系统的整体编译相对简单。首先需要从终端进入存放源码的目录,然后在终端输入如下命令,如图1-23所示。
http://dl.iteye.com/upload/attachment/505095/78accf5a-cf14-3e7c-a2bd-26275a0a9673.jpg
在官方发布的版本中,基本功能都已经包含在makefile中。如果需要增加其他功能,可以到build/target/product/路径下修改相应的.mk文件。比如,如果要增加中文的输入法,可以按照下面的步骤进行:
cd /home/yarin/Android/source/build/target/product/
gedit generic.mk
打开这个文件后,在PRODUCT_PACKAGES中加入PinyinIME,然后保存并退出。这样,在整体编译的时候就会把中文输入法也编译进系统中。
编译的时间一般不会比下载的时间长,大约2~3小时就能完成。在默认情况下,编译完成之后,会在/home/yarin/Android/source目录下生成一个out文件夹,生成的所有文件都放在该文件夹下,如果你想更改生成文件的目录,请参考./build/buildspec.mk.default文件中的说明和选项(虽然很简单,但是笔者还是不建议大家去更改)。编译完成之后会在out/target/ product/generic/目录下生成一些.img镜像文件,比如system.img、userdata.img、ramdisk.img等,我们暂时不去测试生成的这些文件是否有效,因为稍后还需要编译SDK和其他工具,然后在模拟器上加载这些文件,这样才能测试其是否有效。
2.编译Android内核
我们在编译Android源码时并不会自动编译Android内核,因此需要手动来编译这部分内容,下面是编译内核的完整步骤。其实,操作与编译Linux内核一样,如果你非常熟悉,可以跳过这部分内容。
(1)确定内核版本
进入存放Android源码目录下的kernel目录,输入如下命令,查看kernel/common.git的默认主分支(本书所使用的源码显示为* android-2.6.35)。
git branch
然后,输入如下命令,显示所有head分支。
git branch -a
输入如下命令,选择当前最新的版本(本书选择了android-goldfish-2.6.35),其中goldfish是Android的模拟器模拟的CPU。
git checkout -b android-goldfish-2.6.35
origin/android-goldfish-2.6.35
此时,再次输入“git branch”命令,将显示我们所选择的最新分支,表示我们目前工作在这个被选择的分支上。
(2)设定环境变量
在编译Android内核时,需要使用交叉编译器。我们所下载的Android代码树中有一个prebuilt文件夹,它包含了我们编译内核所需的交叉编译工具。因此,我们将其设置到环境变量中去,以方便使用。打开用户目录下的.bashrc文件,在后面添加如下代码:
export
PATH=$PATH:~/Android/source/prebuilt/linux-x86/toolchain/arm-eabi-4.4.0/bin
export ARCH=arm
保存并退出,通过如下命令在用户目录下进行更新。因为有可能会因为添加之后没有及时更新而在编译时提示错误,如找不到交叉编译工具目录等(如果加入了环境变量仍在编译时提示找不到,可以尝试多更新几次)。
source ~/.bashrc
(3)设定交叉编译参数
在编译之前,我们还需要设定编译时所使用的交叉编译参数。具体步骤如下:
首先,打开kernel目录下的makefile文件,把CROSS_COMPILE指向Android源码提供的prebuilt中的arm-eabi编译器,如图1-24所示。
http://dl.iteye.com/upload/attachment/505097/b7507a66-983a-3db8-9aa3-533125ec5c4c.jpg
然后,找到如下代码行:
LDFLAGS_BUILD_ID = $(patsubst -Wl$(comma)%,%,\
$(call ld-option, -Wl$(comma)-build-id,))
将其删除(建议注释掉即可),并且添加一个空的LDFLAGS_BUILD_ID定义,如图1-25所示。
http://dl.iteye.com/upload/attachment/505099/19ef87cf-f4ae-3da3-99fc-aaf0169587dc.jpg
(4)编译kernel镜像
一切设置完成之后,编译就很简单了,进入kernel目录,输入以下命令即可,如图1-26所示。
$ cd ~/Android/source/kernel
$ make goldfish_defconfig
$ make
其中第2条命令是编译时默认的配置文件,如果不执行第二条命令,编译时可能会提示“找不到配置文件”。执行完第二条命令之后,会在kernel目录中多出一个.config文件。
http://dl.iteye.com/upload/attachment/505101/68800a41-5798-39bf-b053-075903f5c3cd.jpg
编译完成之后,会在“kernel/arch/arm/boot/”目录下生成名为zImage的文件;稍后再运行系统时,就可以不使用Android SDK为我们提供的镜像文件,而加载该镜像以及上一节编译的系统镜像。既然要测试,就需要SDK,所以我们一下步将编译SDK。
3.编译SDK
要测试以上所编译的镜像和使用SDK进行应用开发,就需要编译SDK(当然,如果你不想编译,也可以直接到官方网站下载SDK,本书所使用的全部是通过源码编译出来的工具)。由于编译Android源码和内核时并不会自动编译SDK,因此下面我们就来手动编译SDK。
注意如果需要,建议先编译ADT和单独模块,因为在编译ADT和单独模块时会清除编译SDK所生成的目录。
SDK的编译其实很简单,基本上不用配置,直接进入存放Android源码的目录,使用如下命令即可,如图1-27所示。
$ make PRODUCT-sdk-sdk
http://dl.iteye.com/upload/attachment/505103/700a5882-1580-3b04-b193-5891dc6c379b.jpg
注意为了避免大家在搭建环境时编译的SDK无效,建议第一次不要直接使用make sdk命令,而使用如上所示命令。
编译时间稍长,编译后生成的SDK存放在out/host/linux-x86/sdk/目录下。此目录下有android-sdk_eng.xxx_linux-x86.zip和android-sdk_eng.xxx_linux-x86两个目录,android-sdk_ eng.xxx_linux-x86就是我们需要使用的SDK。建议大家将SDK备份,因为我们马上会讲到的单独模块的编译可能会清除SDK目录。
4.编译单独模块
Android允许我们对应用程序进行单独编译,但是编译后需要重新生成system.img,下面就来学习如何单独编译应用程序。
首先,需要在源码存放目录中执行如下命令,注意:“.”后面有空格。
$ . build/envsetup.sh
完成之后就会多出以下命令,你可以加上-help查看其用法。
- croot: Changes directory to the top of the tree.
- m: Makes from the top of the tree.
- mm: Builds all of the modules in the current directory.
- mmm: Builds all of the modules in the supplied directories.
- cgrep: Greps on all local C/C++ files.
- jgrep: Greps on all local Java files.
- resgrep: Greps on all local res/*.xml files.
- godir: Go to the directory containing a file.
然后,可以使用mmm来编译指定目录的模块。比如,我们要编译联系人应用,可以输入如下命令:
$ mmm packages/apps/Contacts/
编完之后生成两个文件:
out/target/product/generic/data/app/ContactsTests.apk
out/target/product/generic/system/app/Contacts.apk
最后,在运行模拟器之前,我们可以使用如下命令来重新生成system.img(即系统镜像)。
$ make snod
5.编译ADT
编译ADT的目的是使用Eclipse来开发应用程序,如果你不想编译,可以在Android官方网站下载,或者直接在线更新。这里,我们主要是告诉大家如何使用源码来编译ADT。
(1)配置环境
要编译ADT,首先应该确保系统中安装了Eclipse。如果没有安装,那么在编译ADT时会下载安装(但这需要取得root权限,否则无法安装,也就无法编译);如果需要自己手动下载并安装Eclipse,建议下载Eclipse3.4.0 RCP版本,因为自动下载安装时,下载的是Eclipse3.4.0 RCP;如果高于这个版本,编译过程中可能会出现找不到某些jar文件的情况,使得编译无法继续。编译ADT的过程很简单,但是经常会因为环境的准备不够充分而出错。所以,笔者建议大家下载官方的ADT或者让ADT编译过程自己下载合适的Eclipse版本,以减少错误的发生。
安装好Eclipse之后需要设置ECLIPSE_HOME的环境变量,否则编译程序会认为你没有装Eclipse,然后又帮你重新下载,我们不希望出现这样的情况。比如,笔者将Eclipsse安装在“~/Develop/eclipse-android_src/eclipse-RCP-3.4-linux-gtk”目录中,就在.bashrc文件中加入如下代码(如果不知道如何添加环境变量,请查看“Android kernel编译”章节的环境配置,这里我们修改的当然是用户的环境变量,同样也可以修改整个系统的环境变量)。
export
ECLIPSE_HOME=$PATH:~/Develop/eclipse-android_src/eclipse-RCP-3.4-linux-gtk
(2)编译ADT
准备好环境之后,要编译ADT,进入Android源码存放目录,在终端输入如下命令即可,结果如图1-28所示。其中“/home/yarin/Android/source/adt”即我们存放编译的ADT的目录。
sdk/eclipse/scripts/build_server.sh /home/yarin/Android/source/adt
http://dl.iteye.com/upload/attachment/505105/d0a5c514-f7c3-3121-bf2c-3516dbd7392c.jpg
编译完成之后,就会在我们指定的存放ADT的目录下找到生成的ADT,然后就可以使用该ADT搭建应用开发环境。
现在我们就已经编译了Android源码和所有需要使用的工具包,那么下面将测试我们的编译是否成功,即运行编译的Android系统。
1.2.4运行Android系统 http://www.iteye.com/images/wiki/top.gif?1324994303 令人激动的时刻来临了,我们终于能看到自己所编译的Android系统了,下面就来一步一步对每个镜像进行测试。首先,需要成功地运行模拟器来加载镜像,模拟器在我们编译SDK的时候已经生成了。由于模拟器的运行需要创建AVD和sdcard卡(不是必需的),所以接下来首先进入SDK目录下的tools文件夹,然后在终端输入如下命令: ./mksdcard 512M sdcard.img ./android create avd --target 1 --name eyarin --sdcard sdcard.img ./android list avd 第一条命令用于创建一个512MB的sd卡。第二条命令用于创建一个AVD,并指定target为1,名字为eyarin,并且建在第一步创建的sd卡上。当提示是否需要自定义AVD时,我们这里选择“no”,即采用默认设置。第三条命令用于显示我们创建的AVD,用来检测是否创建成功,如图1-29所示。创建完成之后,会在当前用户目录中生成一个.android的目录,其中存放了我们所创建的AVD,你可以同时创建多个AVD。
http://dl.iteye.com/upload/attachment/505116/39e78cbb-f2c2-3a08-b2a9-97d3f2845e8b.jpg
现在你已经可以使用如下命令运行一下模拟器看看效果了。
./emulator -avd eyarin
但是,这里运行的效果只是检测我们编译的SDK是否正确,并没有加载我们编译的系统镜像等。下面就分模块来进行测试,首先运行如下代码,加载我们编译的Android Kernel镜像,运行效果如图1-30所示。
./emulator -kernel
/home/yarin/Android/source/kernel/arch/arm/boot/zImage
http://dl.iteye.com/upload/attachment/505118/f91ebe78-8ec4-3579-b936-2f73c8e42a88.jpg
该命令行参数-kernel指定了我们生成的Android Kernel镜像。确保无误之后,再来测试并加载系统镜像,在终端输入如下命令来加载我们编译的所有镜像,也包括上面测试的Android Kernel,运行效果如图1-31所示。
./emulator -system /home/yarin/Android/source/out/target/product/generic/system. img
-data /home/yarin/Android/source/out/target/product/generic/userdata.img
-ramdisk/home/yarin/Android/source/out/target/product/generic/ramdisk.img
-kernel /home/yarin/ Android/source/kernel/arch/arm/boot/zImage
http://dl.iteye.com/upload/attachment/505120/f0a340a6-f4b8-3018-98d6-59bf94bf6e54.jpg
心中颇有些喜悦,毕竟用上我们自己编译出来的Android操作系统了,继续学习,还会有更多令人愉快的事。你可以开发出优秀的应用程序,以及修改和移植该操作系统到其他的平台,但是在做这些事情需要首先完成开发环境的搭建,下一节就来学习如何搭建开发环境。
1.3开发环境搭建 http://www.iteye.com/images/wiki/top.gif?1324994303 我们可以使用Android来做很多事情,这一节主要介绍在使用源码编译出来的SDK的基础上搭建应用开发环境和源码开发环境。
1.3.1应用开发环境搭建 http://www.iteye.com/images/wiki/top.gif?1324994303 由于Android源码开发环境的搭建需要在应用开发环境的基础上进行,所以这里先介绍如何在Linux(ubuntn)下搭建应用开发环境,在Windows和Mac系统下的操作步骤也基本一致。
在Linux(ubuntn)下搭建应用开发环境时所需工具如下:
* Eclipse 3.4以上版本
* ADT
* Android SDK(Linux 版本)
ADT和SDK可以使用上一节我们自己编译出来的,也可以到其官方网站下载。准备好所需工具之后,按下面的步骤来搭建Android应用开发环境。
(1)安装Eclipse
下载Eclipse(你也可以使用之前编译ADT时使用的Eclipse,为了避免混淆,笔者重新使用一个Eclipse来开发应用程序),下载后解压到指定目录,按提示操作即可完成安装。然后启动Eclipse,指定工作空间,进入Eclipse工作窗口。
(2)安装ADT插件
运行Eclipse,进入“help->Install new software”菜单并选择ADD,将会弹出一个对话框,单击“ADD”按钮。为了取得最新的ADT(我们编译的源码可能并不是Android 所发布的最新源码,所以编译的ADT也就不是最新的,同样也可以使用我们自己编译的ADT),我们选择在线更新,输入“http://dl-ssl.google.com/Android/eclipse”,单击“OK”按钮,如图1-32所示。
http://dl.iteye.com/upload/attachment/505122/881e369b-422f-31be-9258-7a8aad75358b.jpg
更新完成后,选择要安装的软件,单击“Next”按钮,然后选择“接受协议”,直到安装成功。Eclipse会建议你重启,单击“Yes”重启即可。
(3)配置Android
重启Eclipse后,设置Android SDK,进入“Window->preferences”菜单,在SDK location中输入Android SDK tools的路径,可以使用自己编译的SDK,也可以使用官方网站下载的SDK。这里我们选择了自己编译的SDK,因为后期在修改和移植源码时可能需要测试,如图1-33所示,完成之后单击“OK”按钮。
http://dl.iteye.com/upload/attachment/505124/675d0777-bac6-3ee6-b5ee-dfddc97505fa.jpg
4)测试环境
现在该我们的“Hello World”出场了,程序虽然简单,但这是使用我们自己编译的SDK编写的,所以测试是需要的(笔者第一次编译之后就没有成功,原因是直接使用了make sdk命令,结果新建的工程不能生成R.java文件,出现“Unable to get buffer of resource asset file”错误)。这里建议大家新建一个工程进行测试,新建的工程结构如图1-34所示,Android2.3-updata1则是我们刚刚编译的。
http://dl.iteye.com/upload/attachment/505126/b6ded748-14c0-3aec-a528-a504eedb8b74.jpg
现在来运行“Hello World”程序,在配置时所选择的模拟器AVD也就是我们自己创建的Eyarin正确运行,效果如图1-35所示。
http://dl.iteye.com/upload/attachment/505128/00a04079-8d10-332c-a714-1b046dca2b67.jpg
到此,就完成了使用我们自己编译的SDK来搭建应用开发环境的工作,也说明我们之前所编译的内容都没有错误。如果你没有到达这一步,请仔细按照前面的步骤检查,找出错误的原因,最终会走到这里的。
1.3.2源码开发环境搭建 http://www.iteye.com/images/wiki/top.gif?1324994303 前面我们使用了Eclipse和ADT来开发Android应用程序,这是Google官方推荐的方式。因为Android的Eclipse插件ADT提供了很多功能,我们可以在Eclipse上轻松地开发和调试Android应用程序,大家逐渐也会体会到这种方式的优点。下面我们来搭建Android源码开发环境,这又该使用什么编辑器呢?有很多人在Linux下使用类似Windows记事本的简单文件编辑器来编写代码,对于新手而言,这样可能会严重影响效率。由于我们可能比较熟悉Eclipse开发工具,所以还是将源码的开发环境也搭建到Eclipse上。当然,在Linux下还有很多优秀的编辑工具,大家可以根据自己的喜好来选择,这里我们就以Eclipse为例来搭建一个Android源码开发环境。
开始之前需要说明:我们不推荐用Eclipse来编译Android源码,所以编译源码的任务还是在我们的终端来完成。我们的目的是能够使用Eclipse来方便地编写和管理代码以及进行必要的调试。如果按照以下步骤来操作,我们就能使用Eclipse来开发Android源码了。
1.将Eclipse工程配置文件复制到Android源码根目录
Android源码中为我们提供了Eclipse配置文件(可见Google官方还是推荐使用Eclipse来开发Android源码),它位于存放Android源码目录下的“development/ide/eclipse/.classpath”路径下,我们将其复制到Android源码的根目录中(笔者的机器对应的目录为“/home/yarin/Android/source”)。
2.修改Eclipse程序的配置
Eclipse程序的配置主要有3个方面需要修改,如下所示。
(1)修改Eclipse的缓存设置
把eclipse.ini文件(在Eclipse的安装目录下)的3个值改为下面的值,对应如图1-36所示(建议修改前进行备份)。
-Xms128m
- Xmx512m
-XX:MaxPermSize=256m
http://dl.iteye.com/upload/attachment/505130/563c129b-d9b3-31ad-80c0-f3b487a5d87c.jpg
2)把android-formatting.xml和android.importorder导入Eclipse
android-formatting.xml、.classpath和android.importorder文件被存放在Android目录的“development/ide/eclipse/”下,其中android-formatting.xml用来配置Eclipse编辑器的代码风格,android.importorder用来配置Eclipse 的导入顺序和结构。
首先导入android-formatting.xml,依次在Eclipse中选择“window->preferences-> Java->Code Style->Formatter”即可,如图1-37所示。
然后导入android.importorder文件,依次在Eclipse中选择“window->preferences-> Java->Code Style->Organize Imports”即可,如图1-38所示。
导入完成之后保存。由于我们需要编辑Java和C语言以及其他语言代码,因此这里为大家推荐一款Eclipse插件anyedit,可以很方便地帮我们管理和维护Android源码工程,请参考其官方网站http://andrei.gmxhome.de/anyedit/进行安装和设置。
(3)将Android源码导入Eclipse中
导入过程很可能会破坏Android源码,虽然我们每一步都进行了细心的测试,但是难免出现我们都不想见到的情况。因此,笔者建议导入前对Android源码进行备份,以防万一。导入源码的操作很简单,但是导入前先检查.classpath里的文件在Android源码中是否有相应的文件(文件夹),否则会破坏Android源码(一般是多添加文件/文件夹),.classpath里多余的路径可删除,然后开始导入。
http://dl.iteye.com/upload/attachment/505132/7c3f5802-1110-3049-96ee-67a054ef21be.jpg
http://dl.iteye.com/upload/attachment/505136/aaf8d1ee-0f75-3282-91fd-6ae505e4741a.jpg
新建Java Project(不是Android Project,否则会破坏Android源码),选择从已存在的工程导入,工程名可随机输入(这里我们设为android_src),如图1-39所示。
http://dl.iteye.com/upload/attachment/505138/b509c084-16e7-3381-950b-7952c0c0d465.jpg
单击“Next”按钮,开时导入;之后单击“Finish”按钮,完成导入。此时,Eclipse会build整个工程(速度比较慢,请耐心等待)。导入完成之后一般不会出现错误,工程结构如图1-40所示,其中包括了所有Android源码的目录(这里的截图没有显示完整),这样就方便我们阅读和编辑源码了。
(4)调试Android程序
要调试程序,首先需要运行模拟器,打开DDMS窗口。先从终端进入Android源码目录,可以通过如下命令来完成,完整命令运行后如图1-41所示。
. build/envsetup.sh
lunch 1
emulator &
ddms &
注意第一条命令的“.”后面有一个空格。
运行完第二条命令就应该启动一个模拟器了,模拟器启动完成后执行第三条命令,将得到一个用于调试的DDMS窗口(速度慢,稍等片刻),如图1-42所示。
http://dl.iteye.com/upload/attachment/505140/7eb05126-4e55-3af7-9dc4-8acc21a02f1b.jpg
http://dl.iteye.com/upload/attachment/505142/564babb7-0c7c-3ad7-b228-bffc5a2f69be.jpg
http://dl.iteye.com/upload/attachment/505143/212ac61c-eb35-37ab-bdab-a3c9fb8f0840.jpg
然后,需要在Eclipse中配置调试类型和端口。依次进入“Run->Debug Configurations-> Remote Java Application”菜单选项,在弹出的Debug Configurations窗口中进行相关设置,如图1-43所示。单击“Apply”按钮,完成配置,再点击“Debug”按钮,开始调试,同样支持断点不调试,很方便吧。
http://dl.iteye.com/upload/attachment/505146/f38307a3-75ba-3148-aefa-52afd4836eef.jpg
注意如果单击“Debug”按钮后出错,显示8700端口被占用,那么只需要把DDMS程序关掉并重新打开即可。如果提供连不到VM错误时,要先在DDMS中(图1-42所示)选中某一进程(对应某一应用程序),这样才能在Eclipse中进行调试。如果你不太明白,不要紧,后面我们在调试实际程序时还会继续讲解。
(5)编译Android程序
像前面这样将源码导入Eclipse,不能在Eclipse中编译程序,因此我们需要在Android源码目录中执行如下命令,然后根据提示编译不同的程序。详情大家可以参考1.2.3节关于单独模块编译的内容。
. build/envsetup.sh
关于编译,我们还可以直接在Android源码根目录下执行“make 模块名”命令来编译模块,具体的操作在后面的章节会详细介绍。
关于开发环境的搭建就介绍到这里,下一节将对Android源码及其应用开发所需要使用的SDK进行整体性的介绍,不要错过哦!
1.4Android源码结构 http://www.iteye.com/images/wiki/top.gif?1324994303 在对源码进行系统学习之前,首先来清点一下可供我们使用的资源,即分析源码的结构及其可用工具,了解Android为我们提供了哪些功能,以及我们需要关注的内容。本节分三个部分,首先分析Android源码的结构,为后期系统的学习打下基础;然后对Android SDK框架进行整体介绍,让我们做到心中有数;最后整理可供我们在开发过程中使用的各种工具,以提高后期的开发效率。
Android的源码本身就很庞大,因此我们有必要理清其结构。为了让大家能够理解得更透彻,我们仍然根据前言中所展示的Android系统构架图,对源码的分布结构进行剖析。
1.Linux内核层
我们知道,这一层主要包括了Android的Linux内核和驱动程序,因为Android本身就是基于Linux 2.6内核的。以下是Android的各版本与Linux内核版本之间的对应关系。
* Android1.0——> Linux2.6.25
* Android1.5——> Linux2.6.27
* Android1.6——> Linux2.6.29
* Android2.0——> Linux2.6.29
* Android2.1——> Linux2.6.29
* Android2.2——> Linux2.6.32
* Android2.3——> Linux2.6.35
Android的内核结构与Linux 2.6的内核结构基本一致,只是增加了部分Android专用的驱动,这也是我们后面会重点讲解的内容。以下为Linux内核层所对应的源码的分布情况。
bionic
bionic C库
kernel
Android Linux内核(其中包括一些Android专用驱动)
system
文件系统库、应用及组件等
hardware
主要包括硬件适配层HAL
当然,除了上面列出的,还有一些比较核心的,比如frameworks包括了Android的核心框架和配置策略等,后面我们会对这些内容进行详细的介绍。下面继续看系统运行库。
2.系统运行库层
系统运行库主要包括原生库、扩展库、Dalvik 虚拟机、Android 核心库几个部分。其中,原生库主要分布在bionic/libc和hardware/libhardware中(这里还包括大部分硬件抽象层的实现,本书第7章将会详细介绍);扩展库内容比较多,大多数都来源于开源项目,比如Webkit浏览器引擎、skia图形库、SQLite、OpenCore(多媒体框架)、qemu(系统模拟环境)等;Dalvik 虚拟机主要在dalvik目录下,它的主要作用包括对象生命周期管理、堆/栈管理、线程管理、安全和异常管理;Android核心库主要提供一些基本的Java类接口,包括数据结构、I/O工具和网络模块等。
3.应用程序框架层
应用程序框架层主要为开发人员开发Android应用程序开发提供一系列的接口和服务,同时,负责对应用程序生命周期和资源等进行管理。主要分布在frameworks/base中的Java部分。
4.应用层
应用层主要包括随同Android一起发布的一些通话时所必需的应用程序,包括Home(主屏)、联系人、短信等。大家可以参考Android的系统构架图来进行对比,我们开发的应用程序也将处于该层,在Android源码中位于packages/apps目录下。packages/providers是内容提供器,packages/inputmethods是输入法相关的内容,packages/wallpapers是Android系统墙纸设置相关的内容。
1.5小结 http://www.iteye.com/images/wiki/top.gif?1324994303 1.5小结
本章的内容比较多,但是这些都是深入学习Android之前需要掌握的。首先,介绍了Android的系统构架、初始化过程以及各层次之间的关系,旨在让大家对Android的认识更进一步;然后,讲解了Android的完整源码的获取方法,以及如何编译镜像及其工具包,能让大家清楚地认识Android系统镜像、Linux内核、硬件驱动和开发工具之间的关系;接着,讲解了如何搭建源码开发环境,为后期的系统开发做好准备;最后,对Android的源码结构等进行了介绍,因为后面有很多内容都是围绕Android的源码展开的。
通过对本章的学习,我们已经具备了深入了解Android系统的构架、原理和实现的基础。下面我们开始吧。
第2章 Android的内核机制和结构剖析本章主要内容 http://www.iteye.com/images/wiki/top.gif?1324994303 qAndroid为什么要采用Linux内核?
q设备在睡眠状态时如何确保系统时间保持运行,以及如何从睡眠状态被唤醒?
q如何管理内存和内存共享?
q当内存过低时怎么办?
q内核调试器的原理是什么?
q如何抓取Android系统的各种日志?
第1章在宏观上介绍了Android的结构和原理,同时,搭建了后面我们将要使用的开发环境。本章将开始对Android的内核进行剖析,主要介绍Android和Linux之间的关系,以及Android系统在Linux系统之上扩展的部分功能和驱动。难度可能比上一章大一点,但是不用担心,我们会带着上述问题详细讲解的。
2.1Linux与Android的关系 http://www.iteye.com/images/wiki/top.gif?1324994303 虽然Android基于Linux内核,但是它与Linux之间还是有很大的差别,比如Android在Linux内核的基础上添加了自己所特有的驱动程序。下面我们就来分析一下它们之间究竟有什么关系?
2.1.1为什么会选择Linux http://www.iteye.com/images/wiki/top.gif?1324994303 成熟的操作系统有很多,但是Android为什么选择采用Linux内核呢?这就与Linux的一些特性有关了,比如:
* 强大的内存管理和进程管理方案
* 基于权限的安全模式
* 支持共享库
* 经过认证的驱动模型
* Linux本身就是开源项目
更多关于上述特性的信息可以参考Linux 2.6版内核的官方文档,这便于我们在后面的学习中更好地理解Android所特有的功能特性。接下来分析Android与Linux的关系。
2.1.2Android不是Linux http://www.iteye.com/images/wiki/top.gif?1324994303 看到这个标题大家可能会有些迷惑,前面不是一直说Android是基于Linux内核的吗,怎么现在又不是Linux了?迷惑也是正常的,请先看下面几个要点,然后我们将对每一个要点进行分析,看完后你就会觉得Android不是Linux了。
* 它没有本地窗口系统
* 它没有glibc的支持
* 它并不包括一整套标准的Linux使用程序
* 它增强了Linux以支持其特有的驱动
1.它没有本地窗口系统
什么是本地窗口系统呢?本地窗口系统是指GNU/Linux上的X窗口系统,或者Mac OX X的Quartz等。不同的操作系统的窗口系统可能不一样,Android并没有使用(也不需要使用)Linux的X窗口系统,这是Android不是Linux的一个基本原因。
2.它没有glibc支持
由于Android最初用于一些便携的移动设备上,所以,可能出于效率等方面的考虑,Android并没有采用glibc作为C库,而是Google自己开发了一套Bionic Libc来代替glibc。
3.它并不包括一整套标准的Linux使用程序
Android并没有完全照搬Liunx系统的内核,除了修正部分Liunx的Bug之外,还增加了不少内容,比如:它基于ARM构架增加的Gold-Fish平台,以及yaffs2 FLASH文件系统等。
4.Android专有的驱动程序
除了上面这些不同点之外,Android还对Linux设备驱动进行了增强,主要如下所示。
1)Android Binder基于OpenBinder框架的一个驱动,用于提供 Android平台的进程间通信(InterProcess Communication,IPC)功能。源代码位于drivers/staging/android/binder.c。
2)Android电源管理(PM)一个基于标准Linux电源管理系统的轻量级Android电源管理驱动,针对嵌入式设备做了很多优化。源代码位于:
* kernel/power/earlysuspend.c
* kernel/power/consoleearlysuspend.c
* kernel/power/fbearlysuspend.c
* kernel/power/wakelock.c
* kernel/power/userwakelock.c
3)低内存管理器(Low Memory Killer)比Linux的标准的OOM(Out Of Memory)机制更加灵活,它可以根据需要杀死进程以释放需要的内存。源代码位于 drivers/staging/ android/lowmemorykiller.c。
4)匿名共享内存(Ashmem)为进程间提供大块共享内存,同时为内核提供回收和管理这个内存的机制。源代码位于mm/ashmem.c。
5)Android PMEM(Physical)PMEM用于向用户空间提供连续的物理内存区域,DSP和某些设备只能工作在连续的物理内存上。源代码位于drivers/misc/pmem.c。
6)Android Logger一个轻量级的日志设备,用于抓取Android系统的各种日志。源代码位于drivers/staging/android/logger.c。
7)Android Alarm提供了一个定时器,用于把设备从睡眠状态唤醒,同时它还提供了一个即使在设备睡眠时也会运行的时钟基准。源代码位于drivers/rtc/alarm.c。
8)USB Gadget驱动一个基于标准 Linux USB gadget驱动框架的设备驱动,Android的USB驱动是基于gaeget框架的。源代码位于drivers/usb/gadget/。
9)Android Ram Console为了提供调试功能,Android允许将调试日志信息写入一个被称为RAM Console的设备里,它是一个基于RAM的Buffer。源代码位于drivers/staging/android / ram_console.c。
10)Android timed device提供了对设备进行定时控制的功能,目前支持vibrator和LED设备。源代码位于drivers/staging/android /timed_output.c(timed_gpio.c)。
11)Yaffs2 文件系统Android采用Yaffs2作为MTD nand flash文件系统,源代码位于fs/yaffs2/目录下。Yaffs2是一个快速稳定的应用于NAND和NOR Flash的跨平台的嵌入式设备文件系统,同其他Flash文件系统相比,Yaffs2能使用更小的内存来保存其运行状态,因此它占用内存小。Yaffs2的垃圾回收非常简单而且快速,因此能表现出更好的性能。Yaffs2在大容量的NAND Flash上的性能表现尤为突出,非常适合大容量的Flash存储。
上面这些要点足以说明Android不是Linux。本书的主要内容将围绕Android的这些特有的部分展开,我们的讲解会尽量通俗易懂,但还是建议大家先复习一下Linux内核的基本知识。在具体学习之前,我们还是先来总体浏览一下Android对Linux内核进行了哪些改动,在移植时就需要对这些改动加以调整。
2.2Android对Linux内核的改动 http://www.iteye.com/images/wiki/top.gif?1324994303 Android从多个方面对Linux内核进行了改动与增强,下面将对此进行详细介绍和分析。
2.2.1Goldfish http://www.iteye.com/images/wiki/top.gif?1324994303 Android模拟器通过运行一个Goldfish的虚拟CPU.Goldfish来运行arm926t指令集(arm926t属于armv5构架),并且仿真了输入/输出,比如键盘输入和LCD 输出。这个模拟器其实是在qemu之上开发的,输入/输出是基于libSDL的。既然Goldfish是被模拟器运行的虚拟CPU,那么当Android在真实的硬件设备上运行时,我们就需要去掉它,因此,只有知道Google对Goldfish做了哪些具体改动之后才能正确地去掉。据统计,Android 内核对Goldfish的改动主要涉及44个文件,具体汇总如下。
说明本书中在被改动的文件前面加了Chg标记,在新增的文件前面加了New标记。
1
Chg
arch/arm/Makefile
添加CONFIG_ARCH_GOLDFISH
2
New
arch/arm/configs/goldfish_defconfig
默认配置文件
3
New
arch/arm/mach-goldfish/Kconfig
为Goldfish CPU添加Kernel配置文件
4
New
arch/arm/mach-goldfish/Makefile
添加board-goldfish.o
5
New
arch/arm/mach-goldfish/Makefile.boot
为Goldfish CPU进行启动配置
6
New
arch/arm/mach-goldfish/audio.c Audio
的输入/输出
7
New
arch/arm/mach-goldfish/board-goldfish.c
中断请求、输入/输出等
8
New
arch/arm/mach-goldfish/pdev_bus.c
设备总线
9
New
arch/arm/mach-goldfish/pm.c
电源管理
10
New
arch/arm/mach-goldfish/switch.c
Switch控制
11
New
arch/arm/mach-goldfish/timer.c
获取和设置时间
12
Chg
arch/arm/mm/Kconfig
添加ARCH_GOLDFISH到支持列表
13
Chg
drivers/char/Makefile
添加goldfish_tty
14
New
drivers/char/goldfish_tty.c
TTY驱动
15
Chg
drivers/input/keyboard/Kconfig
为Goldfish的键盘事件添加配置文件
16
Chg
drivers/input/keyboard/Makefile
添加goldfish_events事件
17
New
drivers/input/keyboard/goldfish_events.c
Goldfish键盘驱动
18
Chg
drivers/mmc/host/Kconfig
添加Kernel配置选项Goldfish MMC卡
19
Chg
drivers/mmc/host/Makefile
添加Goldfish MMC卡驱动
20
New
drivers/mmc/host/goldfish.c
多媒体驱动
21
Chg
drivers/mtd/devices/Kconfig
为Goldfish的NAND flash device添加Kernel配置选项
22
Chg
drivers/mtd/devices/Makefile
添加goldfish_nand
23
New
drivers/mtd/devices/goldfish_nand.c
NAND flash驱动
24
New
drivers/mtd/devices/goldfish_nand_reg.h
NAND flash驱动
25
Chg
drivers/power/Kconfig
为Goldfish的battery(电池)驱动添加kernel配置选项
26
Chg
drivers/power/Makefile
添加Goldfish电池
27
New
drivers/power/goldfish_battery.c
能源和电池状态驱动
28
Chg
drivers/rtc/Kconfig
为Goldfish的rtc(时钟)驱动添加Kernel配置选项
29
Chg
drivers/rtc/Makefile
添加rtc-goldfish
30
New
drivers/rtc/rtc-goldfish.c
实时时钟驱动
31
Chg
drivers/video/Kconfig
添加Goldfish的framebuffer
32
Chg
drivers/video/Makefile
添加Goldfish的framebuffer
33
New
drivers/video/goldfishfb.c
framebuffer驱动
34
New
include/asm-arm/arch-goldfish/dma.h
35
New
include/asm-arm/arch-goldfish/entry-macro.S
36
New
include/asm-arm/arch-goldfish/hardware.h
37
New
include/asm-arm/arch-goldfish/io.h
38
New
include/asm-arm/arch-goldfish/irqs.h
39
New
include/asm-arm/arch-goldfish/memory.h
40
New
include/asm-arm/arch-goldfish/system.h
41
New
include/asm-arm/arch-goldfish/timer.h
42
New
include/asm-arm/arch-goldfish/timex.h
43
New
include/asm-arm/arch-goldfish/uncompress.h
44
New
include/asm-arm/arch-goldfish/vmalloc.h
2.2.2YAFFS2 http://www.iteye.com/images/wiki/top.gif?1324994303 不同于PC机(文件是存储在硬盘上的),手机使用FLASH作为存储介质。HTC的G1使用的是NANDFLASH——这种存储目前已经相当普及了,而且种类也颇多(如SLC、MLC等),存储密度也越来越高(已经出现几十GB大小的NANDFLASH),价格也越来越低。
YAFFS2是专门用在FLASH上的文件系统,YAFFS2是“Yet Another Flash File System,2nd edition”的缩写。YAFFS2为Linux内核提供了一个高效访问NANDFLASH的接口。但是NANDFLASH的支持并不包含在标准的2.6.25内核中,所以Google在其中添加了对NANDFLASH的支持。据统计,为了支持YAFFS2,Google一共改动和增加了以下35个文件:
1
Chg
fs/Kconfig
添加YAFFS配置
2
Chg
fs/Makefile
添加YAFFS
以下为新增的YAFFS2:
1
New
fs/yaffs2/Kconfig
18
New
fs/yaffs2/yaffs_mtddif2.h
2
New
fs/yaffs2/Makefile
19
New
fs/yaffs2/yaffs_nand.c
3
New
fs/yaffs2/devextras.h
20
New
fs/yaffs2/yaffs_nand.h
4
New
fs/yaffs2/moduleconfig.h
21
New
fs/yaffs2/yaffs_nandemul2k.h
5
New
fs/yaffs2/yaffs_checkptrw.c
22
New
fs/yaffs2/yaffs_packedtags1.c
6
New
fs/yaffs2/yaffs_checkprtw.h
23
New
fs/yaffs2/yaffs_packedtags1.h
7
New
fs/yaffs2/yaffs_ecc.c
24
New
fs/yaffs2/yaffs_packedtags2.c
8
New
fs/yaffs2/yaffs_ecc.h
25
New
fs/yaffs2/yaffs_packedtags2.h
9
New
fs/yaffs2/yaffs_fs.c
26
New
fs/yaffs2/yaffs_qsort.c
10
New
fs/yaffs2/yaffs_getblockinfo.h
27
New
fs/yaffs2/yaffs_qsort.h
11
New
fs/yaffs2/yaffs_guts.c
28
New
fs/yaffs2/yaffs_tagscompat.c
12
New
fs/yaffs2/yaffs_guts.h
29
New
fs/yaffs2/yaffs_tagscompat.h
13
New
fs/yaffs2/yaffs_mtdif.c
30
New
fs/yaffs2/yaffs_tagsvaliditiy.c
14
New
fs/yaffs2/yaffs_mtdif.h
31
New
fs/yaffs2/yaffs_tagsvalidity.h
15
New
fs/yaffs2/yaffs_mtddif1.c
32
New
fs/yaffs2/yaffsinterface.h
16
New
fs/yaffs2/yaffs_mtddif1.h
33
New
fs/yaffs2/yportenv.h
17
New
fs/yaffs2/yaffs_mtddif2.c
2.2.3蓝牙 http://www.iteye.com/images/wiki/top.gif?1324994303 在蓝牙通信协议栈里Google修改了10个文件。这些改动修复了一些与蓝牙耳机相关的明显的Bug,以及一些与蓝牙调试和访问控制相关的函数,具体如下所示。
1
Chg
drivers/bluetooth/Kconfig
添加HCI UART Debug
2
Chg
drivers/bluetooth/hci_II.c
如果HCI UART Debug定义在Kernel配置中,则添加BT_DBG()宏
3
Chg
net/bluetooth/Kconfig
添加配置选项L2CAP, HCI_CORE, HCI_SOCK,以及通用接口和语音
4
Chg
net/bluetooth/af_bluetooth.c
如果CONFIG_ANDROID_PARANOID_NETWORK被定义,则添加蓝牙功能的安全检查
5
Chg
net/bluetooth/hci_event.c
修正蓝牙的加密Bug和增加语音的支持
6
Chg
net/bluetooth/rfcomm/core.c
修正Bug
7
Chg
net/bluetooth/rfcomm/sock.c
修复Bug
8
Chg
net/bluetooth/sco.c
禁用SCO链接
9
Chg
include/net/bluetooth/hci_core.h
禁用LMP_ESCO
10
Chg
include/net/bluetooth/rfcomm.h
在rfcomm_dlc中添加“out”参数
2.2.4调度器(Scheduler) http://www.iteye.com/images/wiki/top.gif?1324994303 Android内核还修改了与进程调度和时钟相关的策略。只改动了5个文件,如下:
1
Chg
kernel/sched.c
添加NORMALIZED_SLEEPER
2
Chg
kernel/sched_fair.c
修改内核的调度方式
3
Chg
kernel/softirq.c
修改为CPU调度
4
Chg
kernel/time/tick-sched.c
修改为CPU调度
5
Chg
include/linux/tick.h
如果CONFIG_NO_HZ被定义,则添加tick_nohz_ update_ stopped_ sched_tick()
2.2.5Android新增的驱动 http://www.iteye.com/images/wiki/top.gif?1324994303 Android在Linux的基础上新增了许多特有的驱动,如下所示。
1)IPC Binder一种IPC(进程间通信)机制。它的进程能够为其他进程提供服务——通过标准的Linux系统调用API。IPC Binder的概念起源于一家名为Be.Inc的公司,在Google之前就已经被Palm软件采用了。
2)Low Memory Killer其实内核里已经有一个类似的功能,名称为oom killer(out of memory killer)。当内存不够的时候,该策略会试图结束一个进程。
3)Ashmem匿名共享内存。该功能使得进程间能够共享大块的内存。比如说,系统可以使用Ashmem保存一些图标,多个应用程序可以访问这个共享内存来获取这些图标。Ashmem为内核提供了一种回收这些使用完的共享内存块的方法,如果某个进程试图访问这些已经被回收的内存块,它将会得到错误的返回值,以便它重新进行内存块分配和数据初始化。
4)RAM Console and Log Device为了调试方便,Android添加了一个功能,使调试信息可以输入到一个内存块中。此外,Android还添加了一个独立的日志模块,这样用户空间的进程就能够读写日志消息,以及调试打印信息等。
5)Android Debug Bridge嵌入式设备的调试的确比较麻烦,为了便于调试,Google设计了这个调试工具,可以简称为ADB,使用USB作为连接方式,ADB可以看做是连接Android设备和PC机的一套协议。
除了这些主要的功能之外,Android还增加了诸如 real-time clock、switch、timed GPIO等功能,所有这些改动和增加包含在以下28个文件之中。
1
Chg
drivers/Kconfig
进入配置文件
2
Chg
drivers/Makefile
添加switch,驱动等
3
New
drivers/android/Kconfig
添加BINDER_IPC、POWER、POWER_STAT、POWER_ ALARM、LOGGER、RAM_CONSOLE、TIMED_GPIO、PARANOID_NETWORK到配置中
4
New
drivers/android/Makefile
添加binder.o、power.o、alarm.o、logger.o、ram_console.o、timed_gpio
5
New
drivers/android/alarm.c
系统硬件时钟和实时时钟管理
6
New
drivers/android/binder.c
IPC机制(Binder)
7
New
drivers/android/logger.c
Google的日志API
8
New
drivers/android/ram_console.c
RAM控制台和日志设备方便调试①
9
New
drivers/android/timed_gpio.c
Google的GPIO定时驱动
10
New
drivers/switch/Kconfig
为GPIO添加配置选项
11
New
drivers/switch/Makefile
引入GPIO驱动
12
New
drivers/switch/switch_class.c
13
New
drivers/switch/switch_gpio.c
14
Chg
drivers/usb/gadget/Kconfig
添加ADB配置选项
15
Chg
drivers/usb/gadget/Makefile
编译ADB所需的配置选项
16
New
drivers/usb/gadget/android_adb.c
ADB驱动
17
New
include/linux/android_aid.h
添加AIDs、INET、networking
18
New
include/linux/android_alarm.h
时钟功能设置
19
New
include/linux/android_timed_gpio.h
GPIO结构体
20
New
include/linux/ashmem.h
Android共享内存
21
New
include/linux/binder.h
Binder IPC API定义
22
New
include/linux/logger.h
Logger定义
23
New
include/linux/switch.h
GPIO switch接口
24
Chg
mm/Makefile
添加ashmem.o
25
New
mm/ashmem.c
内存共享实现
26
Chg
drivers/misc/Kconfig
添加LOW_MEMORY_KILLER配置选项
27
Chg
drivers/misc/Makefile
添加lowmemorykiller.c
28
New
drivers/misc/lowmemorykiller.c
当内存过低时,选择并结束进程
2.2.6电源管理 http://www.iteye.com/images/wiki/top.gif?1324994303 电源管理(Power Management)对于移动设备来说相当重要,也是最为复杂和开发难度最高的一个功能。Google添加了一个新的电源管理系统,不包含原有的apm和dpm等。这项改动主要涉及以下5个文件:
1
New
include/linux/android_power.h
定义电源管理API
2
New
drivers/android/power.c
电源管理API实现
3
Chg
drivers/input/evdev.c
修改Android电源处理方式
4
Chg
fs/inotify_user.c
修改Android电源处理方式
5
Chg
kernel/power/process.c
修改Android电源处理方式
2.2.7杂项
除了上述改动之外,还有一些小改动,如新增的额外调试功能、键盘背光控制、TCP 网络管理等,共涉及36个文件,如下所示。
1
New
Documentation/vm/pagemap.txt
2
Chg
arch/arm/Kconfig
添加HAVE_LATENCYTOP_SUPPORT和ARCH_GOLDFISH
3
Chg
arch/arm/kernel/process.c
添加dump_task_regs方法
4
Chg
arch/arm/kernel/signal.c
解决系统无法重新启动的问题
5
Chg
arch/arm/kernel/stacktrace.c
改进调试栈跟踪
6
Chg
arch/arm/mm/abort-ev6.S
7
Chg
drivers/char/Kconfig
添加Memory device driver和Goldfish TTY driver
8
Chg
drivers/char/mem.c
使编译结果输出到/dev/kmem and /dev/mem
9
Chg
drivers/leds/Kconfig
当CPU运行时打开LEDS,但是屏幕是关闭的
10
Chg
drivers/leds/Makefile
添加编译ledtrig-sleep.o
11
New
drivers/leds/ledtrig-sleep.c
睡眠(当关闭屏幕后CPU仍然运行)
12
Chg
drivers/rtc/class.c
修正实时时钟误差的Bug
13
Chg
fs/fat/dir.c
添加VFAT_IOCTL_GET_VOLUME_ID到fat_dir_ioctl()
14
Chg
fs/fat/inode.c
15
Chg
fs/proc/base.c
当内存不足时调整/proc文件
16
Chg
fs/proc/proc_misc.c
修正kpagecount_read和kpageflags_read返回的一些错误
17
Chg
fs/proc/task_mmu.c
简化add_to_pagemap中的错误检查
18
Chg
include/asm-arm/elf.h
添加ELF_CORE_COPY_TASK_REGS()宏调用dump_task_ regs(...)
19
Chg
include/linux/mm.h
添加shmem_set_file(...)函数原型
20
Chg
include/linux/msdos_fs.h
添加VFAT_IOCTL_GET_VOLUME_ID宏
21
Chg
kernel/hrtimer.c
修复run_hrtimer_pending错误
22
Chg
init/Kconfig
添加PANIC_TIMEOUT默认为0
23
Chg
kernel/panic.c
设置默认的panic_timeout:从kernel配置到PANIC_TIMEOUT
24
Chg
kernel/power/console.c
修复虚拟控制台的错误信息
25
Chg
kernel/printk.c
修复printk错误
26
Chg
mm/filemap.c
修正filemap_fault
27
Chg
mm/shmmem.c
重构shmem_zero_setup
28
Chg
mm/tiny-shmem.c
重构shmem_zero_setup
29
Chg
include/linux/sockios.h
添加SIOCKILLADDR控制
30
Chg
include/net/tcp.h
添加tcp_v4_nuke_addr函数
31
Chg
net/ipv4/Makefile
如果设置CONFIG_SYSFS,编译sysfs_net_ipv4
32
Chg
net/ipv4/af_inet_c
如果定义CONFIG_ANDROID_PARANOID_NETWORK,则添加安全检查
33
Chg
net/ipv4/devinet.c
添加SIOCKILLADDR
34
Chg
net/ipv4/sysfs_net_ipv4.c
控制TCP窗口长度
35
Chg
net/ipv4/tcp_ipv4.c
添加tcp_v4_nuke_addr函数
36
Chg
net/ipv6/af_inet6.c
如果定义CONFIG_ANDROID_PARANOID_NETWORK,则添加安全检查
上面这些看似简单,但是非常重要,当大家进行系统级应用开发和程序移植时都需要研究这些文件。对于每个文件的具体改动方式和实现,我们需要进一步查看Android的内核源代码,这是后面将要详细讲解的内容。
2.3Android对Linux内核的增强 http://www.iteye.com/images/wiki/top.gif?1324994303 2.2节介绍了Android对Linux内核的改动,这一节将重点介绍Android对Linux内核的增强,主要包括Alarm(硬件时钟)、Ashmem(匿名内存共享)、Low Memory Killer(低内存管理)、Logger(日志设备),等等。
2.3.1Alarm(硬件时钟) http://www.iteye.com/images/wiki/top.gif?1324994303 Alarm就是一个硬件时钟,前面我们已经知道它提供了一个定时器,用于把设备从睡眠状态唤醒,同时它也提供了一个在设备睡眠时仍然会运行的时钟基准。在应用层上,有关时间的应用都需要Alarm的支持,源代码位于“drivers/rtc/alarm.c”。
Alarm的设备名为“/dev/alarm”。该设备的实现非常简单,我们首先打开源码,可以看到include <linux/android_alarm.h>,其中定义了一些Alarm的相关信息。Alarm的类型枚举如下:
enum android_alarm_type {
ANDROID_ALARM_RTC_WAKEUP,
ANDROID_ALARM_RTC,
ANDROID_ALARM_ELAPSED_REALTIME_WAKEUP,
ANDROID_ALARM_ELAPSED_REALTIME,
ANDROID_ALARM_SYSTEMTIME,
ANDROID_ALARM_TYPE_COUNT,
};
主要包括了5种类型的Alarm,_WAKEUP类型表示在触发Alarm时需要唤醒设备,反之则不需要唤醒设备;ANDROID_ALARM_RTC类型表示在指定的某一时刻出发Alarm;ANDROID_ALARM_ELAPSED_REALTIME表示在设备启动后,流逝的时间达到总时间之后触发Alarm;ANDROID_ALARM_SYSTEMTIME类型则表示系统时间;ANDROID_ALARM_ TYPE_COUNT则是Alram类型的计数。
注意流逝的时间也包括设备睡眠的时间,流逝时间的计算点从它最后一次启动算起。
Alarm返回标记的枚举类型如下:
enum android_alarm_return_flags {
ANDROID_ALARM_RTC_WAKEUP_MASK = 1U << ANDROID_ALARM_RTC_WAKEUP,
ANDROID_ALARM_RTC_MASK = 1U << ANDROID_ALARM_RTC,
ANDROID_ALARM_ELAPSED_REALTIME_WAKEUP_MASK =
1U << ANDROID_ALARM_ELAPSED_REALTIME_WAKEUP,
ANDROID_ALARM_ELAPSED_REALTIME_MASK =
1U << ANDROID_ALARM_ELAPSED_REALTIME,
ANDROID_ALARM_SYSTEMTIME_MASK = 1U << ANDROID_ALARM_SYSTEMTIME,
ANDROID_ALARM_TIME_CHANGE_MASK = 1U << 16
};
Alarm返回标记会随着Alarm的类型而改变。最后还定义了一些宏,主要包括禁用Alarm、Alarm等待、设置Alarm等。下面我们来分析Alarm驱动的具体实现。
首先,Alarm的初始化及退出由以下三个函数来完成:
* late_initcall(alarm_late_init);
* module_init(alarm_init);
* module_exit(alarm_exit);
其中alarm_init函数对Alarm执行初始化操作,alarm_late_init需要在初始化完成之后进行调用,最后退出时需要调用alarm_exit来销毁和卸载Alarm接口及驱动。
1.alarm_init
在初始化过程中,首先需要初始化系统时间,通过platform_driver_register函数来注册Alarm驱动的相关参数,具体如下所示:
static struct platform_driver alarm_driver = {
.suspend = alarm_suspend,
.resume = alarm_resume,
.driver = {
.name = "alarm"
}
};
该参数主要指定了当系统挂起(suspend)和唤醒(Desume)所需要实现的分别为alarm_suspend和alarm_resume,同时将Alarm设备驱动的名称设置为了“alarm”。
如果设置正确,那么继续通过如下代码来初始化SUSPEND lock,因为在使用它们之前必须执行初始化操作。
wake_lock_init(&alarm_wake_lock, WAKE_LOCK_SUSPEND, "alarm");
wake_lock_init(&alarm_rtc_wake_lock, WAKE_LOCK_SUSPEND, "alarm_rtc");
紧接着通过class_interface_register函数来注册Alarm接口信息,主要包括设备的添加和移除操作,内容如下:
static struct class_interface rtc_alarm_interface = {
.add_dev = &rtc_alarm_add_device,
.remove_dev = &rtc_alarm_remove_device,
};
如果在此过程中出现错误,那么需要销毁已经注册的SUSPEND lock,并且卸载Alarm驱动,代码如下:
wake_lock_destroy(&alarm_rtc_wake_lock);
wake_lock_destroy(&alarm_wake_lock);
platform_driver_unregister(&alarm_driver);
注意wake lock是一种锁机制,只要有用户持有该锁,系统就无法进入休眠状态,该锁可以被用户态程序和内核获得。这个锁可以是超时的或者是没有超时的,超时的锁会在时间过期以后自动解锁。如果没有锁或者超时了,内核就会启动休眠机制进入休眠状态,后面在讲电源管理时还会进一步讲解该机制。
2.alarm_late_init
当Alarm启动之后,我们需要读取当前的RCT和系统时间,由于需要确保在这个操作过程中不被中断,或者在中断之后能告诉其他进程该过程没有读取完成,不能被请求,因此这里需要通过spin_lock_irqsave和spin_unlock_irqrestore来对其执行锁定和解锁操作。实现代码如下:
static int __init alarm_late_init(void)
{
unsigned long flags;
struct timespec system_time;
spin_lock_irqsave(&alarm_slock, flags);
getnstimeofday(&elapsed_rtc_delta);
ktime_get_ts(&system_time);
elapsed_rtc_delta = timespec_sub(elapsed_rtc_delta, system_time);
spin_unlock_irqrestore(&alarm_slock, flags);
ANDROID_ALARM_DPRINTF(ANDROID_ALARM_PRINT_INFO,
"alarm_late_init: rtc to elapsed realtime delta %ld.%09ld\n",
elapsed_rtc_delta.tv_sec, elapsed_rtc_delta.tv_nsec);
return 0;
}
3.alarm_exit
当Alarm退出时,就需要通过class_interface_unregister函数来卸载在初始化时注册的Alarm接口,通过wake_lock_destroy函数来销毁SUSPEND lock,以及通过platform_driver_unregister函数来卸载Alarm驱动。实现代码如下:
static void__exit alarm_exit(void)
{
class_interface_unregister(&rtc_alarm_interface);
wake_lock_destroy(&alarm_rtc_wake_lock);
wake_lock_destroy(&alarm_wake_lock);
platform_driver_unregister(&alarm_driver);
}
4.添加和移除设备
接下来是rtc_alarm_add_device和rtc_alarm_remove_device函数的实现。添加设备时,首先将设备转换成rtc_device类型,然后,通过misc_register函数将自己注册成为一个Misc设备。其包括的主要特性如下面的代码所示:
static struct file_operations alarm_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = alarm_ioctl,
.open = alarm_open,
.release = alarm_release,
};
static struct miscdevice alarm_device = {
.minor = MISC_DYNAMIC_MINOR,
.name = "alarm",
.fops = &alarm_fops,
};
其中alarm_device中的“.name”表示设备文件名称,而alarm_fops则定义了Alarm的常用操作,包括打开、释放和I/O控制。这里还需要通过rtc_irq_register函数注册一个rtc_task,用来处理Alarm触发的方法,其定义如下:
static struct rtc_task alarm_rtc_task = {
.func = alarm_triggered_func
};
其中“alarm_triggered_func”则是Alarm需要触发的方法。
注意如果在添加设备的过程中出现错误,我们需要对已经执行的操作进行释放、销毁和卸载。但是,移除一个设备时同样需要判断设备是否是Alarm设备,然后再执行卸载等操作。另外,在处理挂起操作时,我们首先就需要对设备进行锁定,然后根据Alarm的类型执行不同的操作,同时要保存时间。
alarm_open和alarm_release的实现很简单。最后需要说明的是,对于I/O操作而言,主要需要实现:设置时间、设置RTC、获取时间、设置Alarm等待等。
本小节主要对Android中最简单的设备驱动——Alarm的实现流程进行了分析,大家应该可以自己绘制出一个流程图来了吧。对于Alarm的具体实现,大家可以参考源代码“drivers/rtc/alarm.c”中的实现方式。
2.3.2Ashmem(匿名内存共享) http://www.iteye.com/images/wiki/top.gif?1324994303 Ashmem是Android的内存分配与共享机制,它在dev目录下对应的设备文件为/dev/ashmem,其实现的源文件为:
* include/linux/ashmem.h
* kernel/mm/ashmem.c
相比于malloc和anonymous/named mmap等传统的内存分配机制,其优势是通过内核驱动提供了辅助内核的内存回收算法机制(pin/unpin)。什么是pin和unpin呢?具体来讲,就是当你使用Ashmem分配了一块内存,但是其中某些部分却不会被使用时,那么就可以将这块内存unpin掉。unpin后,内核可以将它对应的物理页面回收,以作他用。你也不用担心进程无法对unpin掉的内存进行再次访问,因为回收后的内存还可以再次被获得(通过缺页handler),因为unpin操作并不会改变已经 mmap的地址空间。下面就来分析Ashmem的内核驱动是如何完成这些功能。
首先,打开其头文件(ashmem.h),可以看到定义了以下一些宏和结构体:
//设备文件名称
#define ASHMEM_NAME_DEF "dev/ashmem"
//从ASHMEM_PIN返回的值,判断是否需要清楚
#define ASHMEM_NOT_PURGED 0
#define ASHMEM_WAS_PURGED 1
//从ASHMEM_GET_PIN_STATUS返回的值,判断是pin还是unpin
#define ASHMEM_IS_UNPINNED 0
#define ASHMEM_IS_PINNED 1
struct ashmem_pin {
__u32 offset; //在Ashmem区域的偏移量
__u32 len; //从偏移量开始的长度
};
另外一些宏用于设置Ashmem的名称和状态,以及pin和unpin等操作。接下来看一下Ashmem的具体实现,打开(ashmem.c)文件,首先大致预览一下它有哪些功能函数,如图2-1所示。
http://dl.iteye.com/upload/attachment/505360/2dbed4ee-daba-317d-abcc-99785858b81e.jpg
可以看到Ashmem是通过以下代码来管理其初始化和退出操作的,我们分别需要实现其初始化函数ashmem_init和退出函数ashmem_exit。
module_init(ashmem_init);
module_exit(ashmem_exit);
ashmem_init的实现很简单,首先,定义一个结构体ashmem_area代表匿名共享内存区;然后,定义一个结构体ashmem_range代表unpinned页面的区域,代码如下:
struct ashmem_area {
char name;/* 用于/proc/pid/maps中的一个标识名称 */
struct list_head unpinned_list; /* 所有的匿名共享内存区列表 */
struct file *file; /* Ashmem所支持的文件 */
size_t size; /* 字节数 */
unsigned long prot_mask; /* vm_flags */
};
struct ashmem_range {
struct list_head lru; /* LRU列表 */
struct list_head unpinned; /* unpinned列表 */
struct ashmem_area *asma; /* ashmem_area结构 */
size_t pgstart; /* 开始页面 */
size_t pgend; /* 结束页面 */
unsigned int purged; /* 是否需要清除(ASHMEM_NOT_PURGED 或者ASHMEM_WAS_PURGED) */
};
ashmem_area的生命周期为文件的open()和release()操作之间,而ashmem_range的生命周期则是从unpin到pin,初始化时首先通过kmem_cache_create创建一个高速缓存cache,所需参数如下:
* name 用于/proc/slabinfo文件中来识别这个cache
* size 在对应的cache中所创建的对象的长度
* align 对象对齐尺寸
* flags SLAB标志
* ctor 构造函数
如果创建成功,则返回指向cache的指针;如果创建失败,则返回NULL。当针对cache的新的页面分配成功时运行ctor构造函数,然后采用unlikely来对其创建结果进行判断。如果成功,就接着创建ashmem_range的cache(实现原理与ashmem_area一样)。创建完成之后,通过misc_register函数将Ashmem注册为misc设备。这里需要注意,我们对所创建的这些cache都需要进行回收,因此,再紧接着需调用register_shrinker注册回收函数ashmem_shrinker。而从图2-1可以看出,ashmem_shrinker实际上是一个结构体,真正的回收函数是在ashmem_shrinker中定义的ashmem_shrink。到这里,初始化操作则完成了,实现代码如下:
static int __init ashmem_init(void)
{
int ret;
ashmem_area_cachep = kmem_cache_create("ashmem_area_cache",
sizeof(struct ashmem_area),
0, 0, NULL);
if (unlikely(!ashmem_area_cachep)) {
printk(KERN_ERR "ashmem: failed to create slab cache\n");
return -ENOMEM;
}
ashmem_range_cachep = kmem_cache_create("ashmem_range_cache",
sizeof(struct ashmem_range),
0, 0, NULL);
if (unlikely(!ashmem_range_cachep)) {
printk(KERN_ERR "ashmem: failed to create slab cache\n");
return -ENOMEM;
}
ret = misc_register(&ashmem_misc);
if (unlikely(ret)) {
printk(KERN_ERR "ashmem: failed to register misc device!\n");
return ret;
}
/* 注册回收函数 */
register_shrinker(&ashmem_shrinker);
printk(KERN_INFO "ashmem: initialized\n");
return 0;
}
当Ashmem退出时,又该执行什么操作呢?下面是Ashmem退出时需要执行的ashmem_exit函数的具体实现:
static void __exit ashmem_exit(void)
{
int ret;
/* 卸载回收函数 */
unregister_shrinker(&ashmem_shrinker);
/* 卸载Ashmem设备 */
ret = misc_deregister(&ashmem_misc);
if (unlikely(ret))
printk(KERN_ERR "ashmem: failed to unregister misc device!\n");
/* 卸载cache */
kmem_cache_destroy(ashmem_range_cachep);
kmem_cache_destroy(ashmem_area_cachep);
printk(KERN_INFO "ashmem: unloaded\n");
}
现在我们已经很清楚Ashmem的初始化和退出操作了,接下来我们将分析使用Ashmem对内存进行分配、释放和回收等机制的实现过程。在了解这些实现之前,我们先看看Ashmem分配内存的流程:
1)打开“/dev/ashmem”文件。
2)通过ioctl来设置名称和尺寸等。
3)调用mmap将Ashmem分配的空间映射到进程空间。
由于Ashmem支持pin/unpin机制,所以还可以通过ioctl来pin和unpin某一段映射的空间。Ashmem的作用就是分配空间,打开多少次/dev/ashmem设备并mmap,就会获得多少个不同的空间。
下面来分析如何通过打开设备文件来分配空间,并对空间进行回收。我们在初始化Ashmem时注册了Ashmem设备,其中包含的相关方法及其作用如下面的代码所示。
static struct file_operations ashmem_fops = {
.owner = THIS_MODULE,
.open = ashmem_open, /* 打开Ashmem */
.release = ashmem_release, /* 释放Ashmem */
.mmap = ashmem_mmap, /* mmap函数 */
.unlocked_ioctl = ashmem_ioctl, /* ioctl */
.compat_ioctl = ashmem_ioctl,
};
static struct miscdevice ashmem_misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = "ashmem",
.fops = &ashmem_fops,
};
其中,ashmem_open方法主要是对unpinned列表进行初始化,并将Ashmem分配的地址空间赋给file结构的private_data,这就排除了进程间共享的可能性。ashmem_release方法用于将指定的节点的空间从链表中删除并释放掉。需要指出的是,当使用list_for_each_entry_safe(pos, n, head,member)函数时,需要调用者另外提供一个与pos同类型的指针n,在for循环中暂存pos节点的下一个节点的地址,避免因pos节点被释放而造成断链。ashmem_release函数的实现如下:
static int ashmem_release(struct inode *ignored, struct file *file)
{
struct ashmem_area *asma = file->private_data;
struct ashmem_range *range, *next;
mutex_lock(&ashmem_mutex);
list_for_each_entry_safe(range, next, &asma->unpinned_list, unpinned)
range_del(range);/* 删除 */
mutex_unlock(&ashmem_mutex);
if (asma->file)
fput(asma->file);
kmem_cache_free(ashmem_area_cachep, asma);
return 0;
}
接下来就是将分配的空间映射到进程空间。在ashmem_mmap函数中需要指出的是,它借助了Linux内核的shmem_file_setup(支撑文件)工具,使得我们不需要自己去实现这一复杂的过程。所以ashmem_mmap的整个实现过程很简单,大家可以参考它的源代码。最后,我们还将分析通过ioctl来pin和unpin某一段映射的空间的实现方式。ashmem_ioctl函数的功能很多,它可以通过其参数cmd来处理不同的操作,包括设置(获取)名称和尺寸、pin/unpin以及获取pin的一些状态。最终对pin/unpin的处理会通过下面这个函数来完成:
//pin/unpin处理函数
static int ashmem_pin_unpin(struct ashmem_area *asma, unsigned long cmd,void __user *p)
//如果页面是unpinned和ASHMEM_IS_PINNED,则返回ASHMEM_IS_UNPINNED状态
static int ashmem_get_pin_status(struct ashmem_area *asma, size_t pgstart,size_t pgend)
//unpin 指定区域页面,返回0表示成功
//调用者必须持有ashmem_mutex
static int ashmem_unpin(struct ashmem_area *asma, size_t pgstart, size_t pgend)
//pin ashmem指定的区域
//返回是否曾被清除过(即ASHMEM_WAS_PURGED或者ASHMEM_NOT_PURGED)
//调用者必须持有ashmem_mutex
static int ashmem_pin(struct ashmem_area *asma, size_t pgstart, size_t pgend)
最后需要说明:回收函数cache_shrinker同样也参考了Linux内核的slab分配算法用于页面回收的回调函数。具体实现如下:
static int ashmem_shrink(int nr_to_scan, gfp_t gfp_mask)
{
struct ashmem_range *range, *next;
if (nr_to_scan && !(gfp_mask & __GFP_FS))
return -1;
if (!nr_to_scan)
return lru_count;
mutex_lock(&ashmem_mutex);
list_for_each_entry_safe(range, next, &ashmem_lru_list, lru) {
struct inode *inode = range->asma->file->f_dentry->d_inode;
loff_t start = range->pgstart * PAGE_SIZE;
loff_t end = (range->pgend + 1) * PAGE_SIZE - 1;
vmtruncate_range(inode, start, end);
range->purged = ASHMEM_WAS_PURGED;
lru_del(range);
nr_to_scan -= range_size(range);
if (nr_to_scan <= 0)
break;
}
mutex_unlock(&ashmem_mutex);
return lru_count;
}
cache_shrinker同样先取得了ashmem_mutex,通过list_for_each_entry_safe来确保其被安全释放。该方法会被mm/vmscan.c :: shrink_slab调用,其中参数nr_to_scan表示有多少个页面对象。如果该参数为0,则表示查询所有的页面对象总数。而“gfp_mask”是一个配置,返回值为被回收之后剩下的页面数量;如果返回(1,则表示由于配置文件(gfp_mask)产生的问题,使得mutex_lock不能进行安全的死锁。
Ashmem的源代码实现很简单,注释和代码总共不到700行。主要因为它借助了Linux内核已经有的工具,例如shmem_file_setup(支撑文件)和cache_shrinker(slab分配算法用于页面回收的回调函数)等,实现了高效的内存使用和管理,但是用户需进行额外的ioctl调用来设置名字和大小,以及执行pin和unpin操作等。
到这里,对Ashmem驱动的分析已经结束了。因为我们讲述的是实现的原理和机制,所以没有将代码全部贴出来,建议大家参考源代码进行理解。
2.3.3Low Memory Killer(低内存管理) http://www.iteye.com/images/wiki/top.gif?1324994303 对于PC来说,内存是至关重要。如果某个程序发生了内存泄漏,那么一般情况下系统就会将其进程Kill掉。Linux中使用一种名称为OOM(Out Of Memory,内存不足)的机制来完成这个任务,该机制会在系统内存不足的情况下,选择一个进程并将其Kill掉。Android则使用了一个新的机制——Low Memory Killer来完成同样的任务。下面首先来看看Low Memory Killer机制的原理以及它是如何选择将被Kill的进程的。
1.Low Memory Killer的原理和机制
Low Memory Killer在用户空间中指定了一组内存临界值,当其中的某个值与进程描述中的oom_adj值在同一范围时,该进程将被Kill掉。通常,在“/sys/module/lowmemorykiller / parameters/adj”中指定oom_adj的最小值,在“/sys/module/lowmemorykiller/parameters/minfree”中储存空闲页面的数量,所有的值都用一个逗号将其隔开且以升序排列。比如:把“0,8”写入到/sys/module/lowmemorykiller/parameters/adj中,把“1024,4096”写入到/sys/module/lowmemory- killer/parameters/minfree中,就表示当一个进程的空闲存储空间下降到4096个页面时,oom_adj值为8或者更大的进程会被Kill掉。同理,当一个进程的空闲存储空间下降到1024个页面时,oom_adj值为0或者更大的进程会被Kill掉。我们发现在lowmemorykiller.c中就指定了这样的值,如下所示:
static int lowmem_adj = {
0,
1,
6,
12,
};
static int lowmem_adj_size = 4;
static size_t lowmem_minfree = {
3*512, // 6MB
2*1024, // 8MB
4*1024, // 16MB
16*1024, // 64MB
};
static int lowmem_minfree_size = 4;
这就说明,当一个进程的空闲空间下降到3(512个页面时,oom_adj值为0或者更大的进程会被Kill掉;当一个进程的空闲空间下降到2(1024个页面时,oom_adj值为10或者更大的进程会被Kill掉,依此类推。其实更简明的理解就是满足以下条件的进程将被优先Kill掉:
* task_struct->signal_struct->oom_adj越大的越优先被Kill。
* 占用物理内存最多的那个进程会被优先Kill。
进程描述符中的signal_struct->oom_adj表示当内存短缺时进程被选择并Kill的优先级,取值范围是(17~15。如果是(17,则表示不会被选中,值越大越可能被选中。当某个进程被选中后,内核会发送SIGKILL信号将其Kill掉。
实际上,Low Memory Killer驱动程序会认为被用于缓存的存储空间都要被释放,但是,如果很大一部分缓存存储空间处于被锁定的状态,那么这将是一个非常严重的错误,并且当正常的oom killer被触发之前,进程是不会被Kill掉的。
2.Low Memory Killer的具体实现
在了解了Low Memory Killer的原理之后,我们再来看如何实现这个驱动。Low Memory Killer驱动的实现位于drivers/misc/lowmemorykiller.c。
该驱动的实现非常简单,其初始化与退出操作也是我们到目前为止见过的最简单的,代码如下:
static int __init lowmem_init(void)
{
register_shrinker(&lowmem_shrinker);
return 0;
}
static void __exit lowmem_exit(void)
{
unregister_shrinker(&lowmem_shrinker);
}
module_init(lowmem_init);
module_exit(lowmem_exit);
在初始化函数lowmem_init中通过register_shrinker注册了一个shrinker为lowmem_shrinker;退出时又调用了函数lowmem_exit,通过unregister_shrinker来卸载被注册的lowmem_shrinker。其中lowmem_shrinker的定义如下:
static struct shrinker lowmem_shrinker = {
.shrink = lowmem_shrink,
.seeks = DEFAULT_SEEKS * 16
};
lowmem_shrink是这个驱动的核心实现,当内存不足时就会调用lowmem_shrink方法来Kill掉某些进程。下面来分析其具体实现,实现代码如下:
static int lowmem_shrink(int nr_to_scan, gfp_t gfp_mask)
{
struct task_struct *p;
struct task_struct *selected = NULL;
int rem = 0;
int tasksize;
int i;
int min_adj = OOM_ADJUST_MAX + 1;
int selected_tasksize = 0;
int array_size = ARRAY_SIZE(lowmem_adj);
int other_free = global_page_state(NR_FREE_PAGES);
int other_file = global_page_state(NR_FILE_PAGES);
if(lowmem_adj_size < array_size)
array_size = lowmem_adj_size;
if(lowmem_minfree_size < array_size)
array_size = lowmem_minfree_size;
for(i = 0; i < array_size; i++) {
if (other_free < lowmem_minfree &&
other_file < lowmem_minfree) {
min_adj = lowmem_adj;
break;
}
}
if(nr_to_scan > 0)
lowmem_print(3, "lowmem_shrink %d, %x, ofree %d %d, ma %d\n", nr_to_scan,
gfp_mask, other_free, other_file, min_adj);
rem = global_page_state(NR_ACTIVE_ANON) +
global_page_state(NR_ACTIVE_FILE) +
global_page_state(NR_INACTIVE_ANON) +
global_page_state(NR_INACTIVE_FILE);
if (nr_to_scan <= 0 || min_adj == OOM_ADJUST_MAX + 1) {
lowmem_print(5, "lowmem_shrink %d, %x, return %d\n", nr_to_scan, gfp_mask,
rem);
return rem;
}
read_lock(&tasklist_lock);
for_each_process(p) {
if (p->oomkilladj < min_adj || !p->mm)
continue;
tasksize = get_mm_rss(p->mm);
if (tasksize <= 0)
continue;
if (selected) {
if (p->oomkilladj < selected->oomkilladj)
continue;
if (p->oomkilladj == selected->oomkilladj &&
tasksize <= selected_tasksize)
continue;
}
selected = p;
selected_tasksize = tasksize;
lowmem_print(2, "select %d (%s), adj %d, size %d, to kill\n",
p->pid, p->comm, p->oomkilladj, tasksize);
}
if(selected != NULL) {
lowmem_print(1, "send sigkill to %d (%s), adj %d, size %d\n",
selected->pid, selected->comm,
selected->oomkilladj, selected_tasksize);
force_sig(SIGKILL, selected);
rem -= selected_tasksize;
}
lowmem_print(4, "lowmem_shrink %d, %x, return %d\n", nr_to_scan, gfp_mask, rem);
read_unlock(&tasklist_lock);
return rem;
}
可以看出,其中多处用到了global_page_state函数。有很多人找不到这个函数,其实它被定义在了linux/vmstat.h中,其参数使用zone_stat_item枚举,被定义在linux/mmzone.h中,具体代码如下:
enum zone_stat_item {
NR_FREE_PAGES,
NR_LRU_BASE,
NR_INACTIVE_ANON = NR_LRU_BASE,
NR_ACTIVE_ANON,
NR_INACTIVE_FILE,
NR_ACTIVE_FILE,
#ifdef CONFIG_UNEVICTABLE_LRU
NR_UNEVICTABLE,
NR_MLOCK,
#else
NR_UNEVICTABLE = NR_ACTIVE_FILE, /* 避免编译错误*/
NR_MLOCK = NR_ACTIVE_FILE,
#endif
NR_ANON_PAGES, /* 匿名映射页面*/
NR_FILE_MAPPED, /*映射页面*/
NR_FILE_PAGES,
NR_FILE_DIRTY,
NR_WRITEBACK,
NR_SLAB_RECLAIMABLE,
NR_SLAB_UNRECLAIMABLE,
NR_PAGETABLE,
NR_UNSTABLE_NFS,
NR_BOUNCE,
NR_VMSCAN_WRITE,
NR_WRITEBACK_TEMP, /* 使用临时缓冲区*/
#ifdef CONFIG_NUMA
NUMA_HIT, /* 在预定节点上分配*/
NUMA_MISS, /* 在非预定节点上分配*/
NUMA_FOREIGN,
NUMA_INTERLEAVE_HIT,
NUMA_LOCAL, /* 从本地页面分配*/
NUMA_OTHER, /* 从其他节点分配 */
#endif
NR_VM_ZONE_STAT_ITEMS };
再回过头来看owmem_shrink函数,首先确定我们所定义的lowmem_adj和lowmem_minfree数组的大小(元素个数)是否一致,如果不一致则以最小的为基准。因为我们需要通过比较lowmem_minfree中的空闲储存空间的值,以确定最小min_adj值(当满足其条件时,通过其数组索引来寻找lowmem_adj中对应元素的值);之后检测min_adj的值是否是初始值“OOM_ADJUST_MAX + 1”,如果是,则表示没有满足条件的min_adj值,否则进入下一步;然后使用循环对每一个进程块进行判断,通过min_adj来寻找满足条件的具体进程(主要包括对oomkilladj和task_struct进行判断);最后,对找到的进程进行NULL判断,通过“force_sig(SIGKILL, selected)”发送一条SIGKILL信号到内核,Kill掉被选中的“selected”进程。
关于Low Memory Killer的分析就到这里,在了解了其机制和原理之后,我们发现它的实现非常简单,与标准的Linux OOM机制类似,只是实现方式稍有不同。标准Linux的OOM Killer机制在mm/oom_kill.c中实现,且会被__alloc_pages_may_oom调用(在分配内存时,即mm/page_alloc.c中)。oom_kill.c最主要的一个函数是out_of_memory,它选择一个bad进程Kill,Kill的方法同样是通过发送SIGKILL信号。在out_of_memory中通过调用select_bad_process来选择一个进程Kill,选择的依据在badness函数中实现,基于多个标准来给每个进程评分,评分最高的被选中并Kill。一般而言,占用内存越多,oom_adj就越大,也就越有可能被选中。
2.3.4Logger(日志设备) http://www.iteye.com/images/wiki/top.gif?1324994303 我们在开发Android应用的过程中可以很方便地使用Log信息来调试程序,这都归功于Android的Logger驱动为用户层提供的Log支持。无论是底层的源代码还是上层的应用,我们都可以使用Logger这个日志设备来进行调试。Logger一共包括三个设备节点,它们分别是:
* /dev/log/main
* /dev/log/event
* /dev/log/radio
其驱动程序的实现源文件位于:
* include/linux/logger.h
* include/linux/logger.c
下面将对该驱动的实现进行分析,首先打开logger.h文件,我们可以看到如下所示的一个结构体logger_entry,它定义了每一条日志信息的属性。
struct logger_entry {
__u16 len;
__u16 __pad;
__s32 pid;
__s32 tid;
__s32 sec;
__s32 nsec;
char msg;
};
其中,len表示日志信息的有效长度;__pad目前没有什么实质作用,但是需要使用两个字节来占位;pid表示生成该日志信息的进程的pid;tid表示生成该日志信息的进程的tid;sec表示生成该日志的时间,单位是秒;nsec表示当生成该日志的时间不足1秒时,用纳秒来计算;msg储存着该日志的有效信息,即我们前面说的长度为len的日志信息属于有效信息。
此外,还定义了代表不同设备事件的宏,分别对应于Logger的三个不同的设备节点,如下所示:
#define LOGGER_LOG_RADIO "log_radio" /* 无线相关消息 */
#define LOGGER_LOG_EVENTS "log_events" /* 系统硬件事件 */
#define LOGGER_LOG_MAIN "log_main" /* 任何事件 */
接下来在logger.c中还定义了logger_log结构体,它定义每一个日志设备的相关信息。我们上面所说的radio、events和main都将使用logger_log结构体来表示,定义如下:
struct logger_log {
unsigned char * buffer;
struct miscdevice misc;
wait_queue_head_t wq;
struct list_head readers;
struct mutex mutex;
size_t w_off;
size_t head;
size_t size;
};
其中,buffer表示该设备储存日志的环形缓冲区,(为什么是环形缓冲区,后面将给大家解释);misc代表日志设备的miscdevice,在注册设备的时候需要使用;wq表示一个等待队列,等待在该设备上读取日志的进程readers;readers表示读取日志的readers链表;mutex则是用于多线程同步和保护该结构体的mutex;w_off代表当前写入日志的位置,即在环形缓冲区中(buffer)的偏移量;head是一个读取日志的新的readers,表示从这里开始读取,同样指在环形缓冲区中(buffer)的偏移量;size则代表该日志的大小,即环形缓冲区中(buffer)的大小。
根据上面这个日志设备结构logger_log可以得知,要读取日志还需要一个用于读取日志的readers。下面我们来分析一下readers的定义,其结构体位于logger.c中的logger_reader结构体中,代码如下:
struct logger_reader {
struct logger_log * log;
struct list_head list;
size_t r_off;
};
logger_reader结构体的实现就很简单,其中log代表相关的日志设备,即当前将要读取数据的日志设备(logger_log);list用于指向日志设备的读取进程(readers);r_off则表示开始读取日志的一个偏移量,即日志设备中将要被读取的buffer的偏移量。
了解了这些数据结构之后,我们来分析一下该驱动是如何工作的,即该驱动的工作流程。
1.logger_init
首先还是来看其初始化方式,如下所示:
static int __init logger_init(void)
{
int ret;
ret = init_log(&log_main);
if (unlikely(ret))
goto out;
ret = init_log(&log_events);
if (unlikely(ret))
goto out;
ret = init_log(&log_radio);
if (unlikely(ret))
goto out;
out:
return ret;
}
device_initcall(logger_init);
当系统内核启动后,在init过程中就会调用device_initcall所指向的logger_init来初始化日志设备。我们可以看到,在logger_init函数中正好调用了init_log函数来初始化前面所提到的日志系统的三个设备节点。下面我们来看看init_log函数中究竟是如何初始化这些设备节点的。init_log的实现如下:
static int __init init_log(struct logger_log *log)
{
int ret;
ret = misc_register(&log->misc);
if (unlikely(ret)) {
printk(KERN_ERR "logger: failed to register misc "
"device for log '%s'!\n", log->misc.name);
return ret;
}
printk(KERN_INFO "logger: created %luK log '%s'\n",
(unsigned long) log->size >> 10, log->misc.name);
return 0;
}
非常简单,通过调用misc_register来初始化每个日志设备的miscdevice(logger_log->misc)。我们并没有看到具体的初始化日志设备的操作,那是因为这些工作都由DEFINE_LOGGER_ DEVICE宏来完成了,DEFINE_LOGGER_DEVICE的实现如下:
#define DEFINE_LOGGER_DEVICE(VAR, NAME, SIZE)
static unsigned char _buf_ ## VAR;
static struct logger_log VAR = {
.buffer = _buf_ ## VAR,
.misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = NAME,
.fops = &logger_fops,
.parent = NULL,
},
.wq = __WAIT_QUEUE_HEAD_INITIALIZER(VAR .wq),
.readers = LIST_HEAD_INIT(VAR .readers),
.mutex = __MUTEX_INITIALIZER(VAR .mutex),
.w_off = 0,
.head = 0,
.size = SIZE,
};
DEFINE_LOGGER_DEVICE需要我们传入三个参数,其作用就是使用参数NAME作为名称和使用SIZE作为尺寸来创建一个日志设备。这里需要注意:SIZE的大小必须为2的幂,并且要大于LOGGER_ENTRY_MAX_LEN,小于LONG_MAX-LOGGER_ENTRY_ MAX_ LEN。该宏的定义如下(源代码在logger.h文件中),表示日志的最大长度,同时还定义了LOGGER_ ENTRY_MAX_PAYLOAD表示日志的最大有效长度。
#define LOGGER_ENTRY_MAX_LEN (4*1024)
#define LOGGER_ENTRY_MAX_PAYLOAD
(LOGGER_ENTRY_MAX_LEN - sizeof(struct logger_entry))
有了这些定义之后,现在要初始化一个日志设备就变得非常简单,以下代码初始化了三个不同的日志设备:
DEFINE_LOGGER_DEVICE(log_main, LOGGER_LOG_MAIN, 64*1024)
DEFINE_LOGGER_DEVICE(log_events, LOGGER_LOG_EVENTS, 256*1024)
DEFINE_LOGGER_DEVICE(log_radio, LOGGER_LOG_RADIO, 64*1024)
在初始化过程中,我们为设备指定了对应的file_operations,其定义如下:
static struct file_operations logger_fops = {
.owner = THIS_MODULE,
.read = logger_read,
.aio_write = logger_aio_write,
.poll = logger_poll,
.unlocked_ioctl = logger_ioctl,
.compat_ioctl = logger_ioctl,
.open = logger_open,
.release = logger_release,
};
其中主要包括了关于日志设备的各种操作函数和接口,比如:读取日志的logger_read、打开日志设备文件的logger_open读取数据的logger_read,等等。下面,我们将分别对这些函数的实现进行分析。
2.logger_open
该方法为打开日志设备文件的方法,具体实现如下:
static int logger_open(struct inode *inode, struct file *file)
{
struct logger_log *log;
int ret;
ret = nonseekable_open(inode, file);
if (ret)
return ret;
//判断类型
log = get_log_from_minor(MINOR(inode->i_rdev));
if (!log)
return -ENODEV;
//只读模式
if (file->f_mode & FMODE_READ) {
struct logger_reader *reader;
reader = kmalloc(sizeof(struct logger_reader), GFP_KERNEL);
if (!reader)
return -ENOMEM;
//指定日志设备
reader->log = log;
INIT_LIST_HEAD(&reader->list);
//指定mutex
mutex_lock(&log->mutex);
//指定读取偏移量
reader->r_off = log->head;
list_add_tail(&reader->list, &log->readers);
mutex_unlock(&log->mutex);
//保存数据到private_data
file->private_data = reader;
} else //读写模式
file->private_data = log;
return 0;
}
该函数首先调用get_log_from_minor函数来判断需要打开的日志设备的类型,判断方法非常简单,直接判断日志设备的misc.minor参数和minor参数即可,实现代码如下:
static struct logger_log * get_log_from_minor(int minor)
{
if (log_main.misc.minor == minor)
return &log_main;
if (log_events.misc.minor == minor)
return &log_events;
if (log_radio.misc.minor == minor)
return &log_radio;
return NULL;
}
再回过头来看logger_open函数,在取得了日志设备的类型之后,我们需要判断其读写模式。如果是只读模式,则将创建一个logger_reader,然后对其所需的数据进行初始化(指定日志设备、mutex、读取偏移量r_off),最后将该logger_reader保存到file->private_data中;如果是读写模式或者写模式,则直接将日志设备log保存到file->private_data中,这样做就方便我们在以后的读写过程中直接通过file->private_data来取得logger_reader和logger_log。
3.logger_release
在分析了打开操作之后,我们再来看一下释放操作,具体实现如下:
static int logger_release(struct inode *ignored, struct file *file)
{
if (file->f_mode & FMODE_READ) {
struct logger_reader *reader = file->private_data;
list_del(&reader->list);
kfree(reader);
}
return 0;
}
首先判断其是否为只读模式,如果是只读模式,则直接通过file->private_data取得其对应的logger_reader,然后删除其队列并释放即可。写操作则没有额外分配空间,所以不需要处理。
4.logger_read
接下来分析一下读数据的操作方法,其实现代码如下:
static ssize_t logger_read(struct file *file, char __user *buf,
size_t count, loff_t *pos)
{
//通过file->private_data获取logger_reader及其日志设备logger_log
struct logger_reader *reader = file->private_data;
struct logger_log *log = reader->log;
ssize_t ret;
DEFINE_WAIT(wait);
start:
while (1) {
//添加进程到等待队列
prepare_to_wait(&log->wq, &wait, TASK_INTERRUPTIBLE);
mutex_lock(&log->mutex);
ret = (log->w_off == reader->r_off);
mutex_unlock(&log->mutex);
if (!ret)
break;
if (file->f_flags & O_NONBLOCK) {
ret = -EAGAIN;
break;
}
if (signal_pending(current)) {
ret = -EINTR;
break;
}
schedule();
}
finish_wait(&log->wq, &wait);
if (ret) return ret;
mutex_lock(&log->mutex);
if (unlikely(log->w_off == reader->r_off)) {
mutex_unlock(&log->mutex);
goto start;
}
//读取下一条日志
ret = get_entry_len(log, reader->r_off);
if (count < ret) {
ret = -EINVAL;
goto out;
}
//复制到用户空间
ret = do_read_log_to_user(log, reader, buf, ret);
out:
mutex_unlock(&log->mutex);
return ret;
}
整体过程比较简单,但是这里需要注意:我们首先是通过prepare_to_wait函数将当前进程添加到等待队列log->wq之中,通过偏移量来判断当前日志的buffer是否为空。如果为空,则调度其他的进程运行,自己挂起;如果指定了非阻塞模式,则直接返回EAGAIN。然后,通过while循环来重复该过程,直到buffer中有可供读取的日志为止。最后,通过get_entry_len函数读取下一条日志,并通过do_read_log_to_user将其复制到用户空间,读取完毕。
5.logger_aio_write
分析了读操作,下面登场的应该是写操作了。在这里,我们终于可以清楚地向大家解释之前的疑问——为什么缓冲区是环形的。在写入日志时,当其日志缓冲区buffer被写满之后,我们就不能再执行写入操作了吗?答案是否定的,正因为buffer是环形的,在写满之后,新写入的数据就会覆盖最初的数据,所以我们需要采取一定的措施来避免原来的数据被覆盖,以免造成数据丢失。写操作的具体实现如下:
ssize_t logger_aio_write(struct kiocb *iocb, const struct iovec *iov,
unsigned long nr_segs, loff_t ppos)
{
//取得日志设备logger_log
struct logger_log *log = file_get_log(iocb->ki_filp);
size_t orig = log->w_off;
struct logger_entry header;
struct timespec now;
ssize_t ret = 0;
now = current_kernel_time();
//初始化日志数据logger_entry
header.pid = current->tgid;
header.tid = current->pid;
header.sec = now.tv_sec;
header.nsec = now.tv_nsec;
header.len = min_t(size_t, iocb->ki_left, LOGGER_ENTRY_MAX_PAYLOAD);
if (unlikely(!header.len))
return 0;
mutex_lock(&log->mutex);
//修正偏移量,避免被覆盖
fix_up_readers(log, sizeof(struct logger_entry) + header.len);
//写入操作
do_write_log(log, &header, sizeof(struct logger_entry));
while (nr_segs-- > 0) {
size_t len;
ssize_t nr;
len = min_t(size_t, iov->iov_len, header.len - ret);
//从用户空间写入日志
nr = do_write_log_from_user(log, iov->iov_base, len);
if (unlikely(nr < 0)) {
log->w_off = orig;
mutex_unlock(&log->mutex);
return nr;
}
iov++;
ret += nr;
}
mutex_unlock(&log->mutex);
wake_up_interruptible(&log->wq);
return ret;
}
与读操作一样,首先,需要取得日志设备logger_log,这里我们是通过file_get_log函数来获取日志设备;然后,对要写入的日志执行初始化操作(包括进程的pid、tid和时间等)。因为我们的写操作支持同步、异步以及scatter等方式(非常灵活),而且在进行写操作时读操作可能并没有发生,这样就会被覆盖,所以通过在写操作之前执行fix_up_readers函数来修正其偏移量(r_off),然后才执行真正的写入操作。
fix_up_readers函数真正能修正其偏移量而使其不被覆盖吗?下面我们先看看该函数的具体实现,如下所示:
static void fix_up_readers(struct logger_log *log, size_t len)
{
//当前写偏移量
size_t old = log->w_off;
//写入长度为len的数据后的偏移量
size_t new = logger_offset(old + len);
struct logger_reader *reader;
if (clock_interval(old, new, log->head))
//查询下一个
log->head = get_next_entry(log, log->head, len);
//遍历reader链表
list_for_each_entry(reader, &log->readers, list)
if (clock_interval(old, new, reader->r_off))
reader->r_off = get_next_entry(log, reader->r_off, len);
}
大家可以看到,在执行clock_interval进行new复制时,将会覆盖log->head,所以我们使用get_next_entry来查询下一个节点,并使其作为head节点。通常在执行查询时,我们使用的都是要被写入的整个数据的长度(len),因为是环形缓冲区,所以会出现覆盖数据的情况,因此这里传入的长度为最大长度(即要写入的数据长度);然后遍历reader链表,如果reader在覆盖范围内,那么调整当前reader位置到下一个log数据区。因此从这里我们可以看出,fix_up_readers函数只是起到一个缓解的作用,也不能最终解决数据覆盖问题,所以写入的数据如果不被及时读取,则会造成数据丢失。
6.logger_poll
该函数用来判断当前进程是否可以对日志设备进行操作,其具体实现代码如下:
static unsigned int logger_poll(struct file *file, poll_table *wait)
{
struct logger_reader *reader;
struct logger_log *log;
unsigned int ret = POLLOUT | POLLWRNORM;
if (!(file->f_mode & FMODE_READ))
return ret;
reader = file->private_data;
log = reader->log;
poll_wait(file, &log->wq, wait);
mutex_lock(&log->mutex);
//判断是否为空
if (log->w_off != reader->r_off)
ret |= POLLIN | POLLRDNORM;
mutex_unlock(&log->mutex);
return ret;
}
我们可以看出,POLLOUT总是成立的,即进程总是可以进行写入操作;读操作则不一样了,如果只是以FMODE_READ模式打开日志设备的进程,那么就需要判断当前日志缓冲区是否为空,只有不为空才能读取日志。
7.logger_ioctl
该函数主要用于对一些命令进行操作,它可以支持以下命令操作:
LOGGER_GET_LOG_BUF_SIZE
得到日志环形缓冲区的尺寸
LOGGER_GET_LOG_LEN
得到当前日志buffer中未被读出的日志长度
LOGGER_GET_NEXT_ENTRY_LEN
得到下一条日志长度
LOGGER_FLUSH_LOG
清空日志
它们分别对应于logger.h中所定义的下面这些宏:
#define LOGGER_GET_LOG_BUF_SIZE_IO(__LOGGERIO, 1)
#define LOGGER_GET_LOG_LEN_IO(__LOGGERIO, 2)
#define LOGGER_GET_NEXT_ENTRY_LEN_IO(__LOGGERIO, 3)
#define LOGGER_FLUSH_LOG_IO(__LOGGERIO, 4)
这些操作的具体实现很简单,大家可以参考logger.c中的logger_ioctl函数。以上就是我们对Logger驱动的分析,大家可以对应源码来阅读,这样会更容易理解。
楼主呀,,,您太有才了。。。
页:
[1]