Android系统启动过程分析

Android系统的框架架构图如下(来自网上):

 

Linux内核启动之后----->就到Android的Init进程 ----->进而启动Android相关的服务和应用。

整个的启动过程如下图所示:


以下针对Android 4.2内核代码的启动部分进行分析。

Init进程,是一个由内核启动的用户级进程。内核自行启动(已被载入内存,开始运行,并已初始化所有的设备驱动程序和数据结构等)之后,就通过启动用户级程序init的方式,完成引导进程。

Init进程始终是第一个进程。Init进程的对应的代码的main函数在目录~/my_android/system/core/init/init.c


整个Android系统的启动分为Linux kernel的启动和Android系统的启动。


Linux kernel启动起来后,然后就运行第一个用户程序,在Android中,就是init程序,在目录~/my_android/system/core/init/init.c,对其中的main()函数分段进行介绍

1. 首先声明一些局部变量,代码如下:

int main(int argc, char **argv)
{
int fd_count = 0;
struct pollfd ufds[4];
char *tmpdev;
char* debuggable;
char tmp[32];
int property_set_fd_init = 0;
int signal_fd_init = 0;
int keychord_fd_init = 0;
bool is_charger = false;
......
......
}

main函数中该段代码主要是声明了一些后续会使用的变量,其中涉及一个结构体pollfd,后续对其操作时再进行介绍


2.对传入的argv[0]进行判断,决定程序的执行分支,代码如下:

int main(int argc, char **argv)
{
...
... if (!strcmp(basename(argv[0]), "ueventd"))
return ueventd_main(argc, argv); if (!strcmp(basename(argv[0]), "watchdogd"))
return watchdogd_main(argc, argv); ...
...
}

说明了这里处理kernel执行会跳转到以外,还有其他地方会调用这个main函数。
其中的argv[0]就是表示要执行的函数名称。
从这里看,应该有三个地方会执行此处的main()函数:

  • 标准的android启动代码
  • ueventd_main()代码
  • watchdogd_main()代码


3. 创建并挂载Android系统启动所需要的文件系统

int main(int argc, char **argv)
{
...
... /* clear the umask */
umask(0); /* Get the basic filesystem setup we need put
* together in the initramdisk on / and then we'll
* let the rc file figure out the rest.
*/
/* Don't repeat the setup of these filesystems,
* it creates double mount points with an unknown effect
* on the system. This init file is for 2nd-init anyway.
*/
#ifndef NO_DEVFS_SETUP
mkdir("/dev", 0755);
mkdir("/proc", 0755);
mkdir("/sys", 0755); mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755");
mkdir("/dev/pts", 0755);
mkdir("/dev/socket", 0755);
mount("devpts", "/dev/pts", "devpts", 0, NULL);
mount("proc", "/proc", "proc", 0, NULL);
mount("sysfs", "/sys", "sysfs", 0, NULL); /* indicate that booting is in progress to background fw loaders, etc */
close(open("/dev/.booting", O_WRONLY | O_CREAT, 0000)); /* We must have some place other than / to create the
* device nodes for kmsg and null, otherwise we won't
* be able to remount / read-only later on.
* Now that tmpfs is mounted on /dev, we can actually
* talk to the outside world.
*/
...
...
}

4.生成log设备,以及一些属性设置

int main(int argc, char **argv)
{
...
...
/* We must have some place other than / to create the
* device nodes for kmsg and null, otherwise we won't
* be able to remount / read-only later on.
* Now that tmpfs is mounted on /dev, we can actually
* talk to the outside world.
*/
open_devnull_stdio();
klog_init();
#endif
property_init(); get_hardware_name(hardware, &revision); process_kernel_cmdline();
...
...
}

其中open_devnull_stdio()的定义在~/my_android/system/core/init/util.c中,代码如下:

void open_devnull_stdio(void)
{
int fd;
static const char *name = "/dev/__null__";
if (mknod(name, S_IFCHR | 0600, (1 << 8) | 3) == 0) {
fd = open(name, O_RDWR);
unlink(name);
if (fd >= 0) {
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
if (fd > 2) {
close(fd);
}
return;
}
} exit(1);
}

该函数首先创建一个设备节点"/dev/__null__"(在/dev目录下生产__null__设备节点文件),然后打开这个设备文件,并将打开的文件描述符保存在变量fd中,接着使用unlink函数删除该文件,虽然文件删除了,但是现在系统中借助这个fd还是能够找到该文件的内容的。if中的语句利用dup2函数将文件描述符fd的0,1,2信息重定向到这个fd文件描述符的文件中。即把标准输入、标准输出、标准错误输出重定向到一个设备文件中(0——标准输入,1——标准输出,2——标准错误输出)。重定向操作完成后,就关闭掉fd。示意图如下:


回到main()函数中,接着是执行klog_init()函数,其定义在:~/my_android/system/core/libcutils/klog.c中,实现代码如下:

void klog_init(void)
{
static const char *name = "/dev/__kmsg__";
if (mknod(name, S_IFCHR | 0600, (1 << 8) | 11) == 0) {
klog_fd = open(name, O_WRONLY);
fcntl(klog_fd, F_SETFD, FD_CLOEXEC);
unlink(name);
}
}

该函数和open_devnull_stdio的实现很相像,创建设备节点,打开,操作,然后删除文件。其中的fcntl(klog_fd, F_SETFD, FD_CLOEXEC); 表示当在子进程中使用exec执行其他程序时会把这个文件描述符关闭。

接着是执行main()函数中的property_init()函数,其定义在:~/my_android/system/core/init/property_service.c文件中。实现的代码如下:

void property_init(void)
{
init_property_area();
}

此处调用了另一函数init_property_area(),其定义也在~/my_android/system/core/init/property_service.c文件中,实现代码如下:

static int init_property_area(void)
{
prop_area *pa; if(pa_info_array)
return -1; if(init_workspace(&pa_workspace, PA_SIZE))
return -1; fcntl(pa_workspace.fd, F_SETFD, FD_CLOEXEC); pa_info_array = (void*) (((char*) pa_workspace.data) + PA_INFO_START); pa = pa_workspace.data;
memset(pa, 0, PA_SIZE);
pa->magic = PROP_AREA_MAGIC;
pa->version = PROP_AREA_VERSION; /* plug into the lib property services */
__system_property_area__ = pa;
property_area_inited = 1;
return 0;
}

在该函数中,涉及了几个结构体变量,首先看一下各结构体的定义

prop_area结构体的定义如下,其定义在:

struct prop_area{
nsigned volatile count;
unsigned volatile serial;
unsigned magic;
unsigned version;
unsigned reserved[4];
unsigned toc[1];
};

然后是pa_info_array的定义为:

static pro_info *pa_info_array;

所以在函数中,由于pa_info_array由于刚刚定义的,所以当if判断其是否为空,结果为空,所以继续往下执行。接着调用init_workspace(&pa_workspace, PA_SIZE)函数,其定义为:

static int init_workspace(workspace *w, size_t size)
{
void *data;
int fd; /* dev is a tmpfs that we can use to carve a shared workspace
* out of, so let's do that...
*/
fd = open("/dev/__properties__", O_RDWR | O_CREAT, 0600);
if (fd < 0)
return -1; if (ftruncate(fd, size) < 0)
goto out; data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(data == MAP_FAILED)
goto out; close(fd); fd = open("/dev/__properties__", O_RDONLY);
if (fd < 0)
return -1; unlink("/dev/__properties__"); w->data = data;
w->size = size;
w->fd = fd;
return 0; out:
close(fd);
return -1;
}

pa_workspace和PA_SIZE也在当前文件夹中定义,如下:

#define PA_SIZE 49152
static workspace pa_workspace;

该函数中有涉及另一个结构体workplace,就定义当函数所在的文件夹,其定义为:

typedef struct {
void *data;
size_t size;
int fd;
} workspace;

这个文件夹中存储了三个变量,数据、大小和文件描述符。init_workplace()这个函数里面就要初始化一个这样的结构体。

首先,打开一个设备文件/dev/__properties__", 通过ftruncate()函数调用将这个文件的大小改为size。size是通过调用函数时形参传递进来的。

然后,调用mmap()函数映射一段内存。返回映射区的地址保存在data中。

最后,将对应的data、size、fd分别给workplace结构体指针w赋值。

函数执行成功,返回0。回到init_property_area函数中。init_workplace()函数返回后进行if判断,若执行成功,返回0,所以接着往下执行。

调用fcntl()函数,关于fcntl函数的功能,

参考:http://www.cnblogs.com/andtt/articles/2178875.htmlhttp://blog.csdn.net/ustc_dylan/article/details/6930189

pa_workspace.data表示的是一段大小为PA_SIZE的内存地址,将这个地址加上PA_INFO_START赋值给pa_info_array。PA_INFO_START定义为:

#define PA_INFO_START 1536

然后将pa_workplace.data代表的那段大小为PA_SIZE的内存通过调用memset()函数将其内存清0。
记者就是一些简单的赋值操作了,现在回到main()函数中。

执行get_hardware_name(hardware, &revision);该函数定义在:~/my_android/system/core/init/util.c文件中,实现代码如下:

void get_hardware_name(char *hardware, unsigned int *revision)
{
char data[1024];
int fd, n;
char *x, *hw, *rev; /* Hardware string was provided on kernel command line */
if (hardware[0])
return; fd = open("/proc/cpuinfo", O_RDONLY);
if (fd < 0) return; n = read(fd, data, 1023);
close(fd);
if (n < 0) return; data[n] = 0;
hw = strstr(data, "\nHardware");
rev = strstr(data, "\nRevision"); if (hw) {
x = strstr(hw, ": ");
if (x) {
x += 2;
n = 0;
while (*x && *x != '\n') {
if (!isspace(*x))
hardware[n++] = tolower(*x);
x++;
if (n == 31) break;
}
hardware[n] = 0;
}
} if (rev) {
x = strstr(rev, ": ");
if (x) {
*revision = strtoul(x + 2, 0, 16);
}
}
}

这个函数的实参hardware是一32个元素的字符数组,revision为一个unsigned值,也定义在函数所在的文件中,定义如下:

static char hardware[32];
static unsigned revision = 0;

get_hardware_name()函数从"proc/cpuinfo"文件读取相应字符串到data中,然后通过调用strstr函数将data中"\nHardware"开始的字符保存到hw中,将“\nRevision”开始的字符保存到rev中。

strstr()函数的功能:就是在第一个参数中查找第二个参数第一次出现的地址,将地址赋值给一个字符指针,接着就可以利用这个字符指针找到从这个地址开始往后的字符。

"/proc/cpuinfo中"中的内容,可以通过adb shell登录模拟器来查看,其内容如下:


后面两个if语句对hw和rev进行处理,最终得到我们想要的数值。其中hw部分,提取Goldfish这几个字符,并将其大写转为小写。rev那部分数据转化为十六进制表示。

回到main()函数,接着执行process_kernel_cmdline();该函数和main()函数定义在同一文件夹中,实现代码如下:

static void process_kernel_cmdline(void)
{
/* don't expose the raw commandline to nonpriv processes */
chmod("/proc/cmdline", 0440); /* first pass does the common stuff, and finds if we are in qemu.
* second pass is only necessary for qemu to export all kernel params
* as props.
*/
import_kernel_cmdline(0, import_kernel_nv);
if (qemu[0])
import_kernel_cmdline(1, import_kernel_nv); /* now propogate the info given on command line to internal variables
* used by init as well as the current required properties
*/
export_kernel_boot_props();
}

除了使用import_kernel_cmdline函数导入内核变量外,主要的功能就是调用export_kernel_boot_props函数通过属性设置内核变量,主要实现的功能是处理内核命令行,以下从细节进行分析。


首先调用chmod()函数改变"/proc/cmdline"的文件属性。

import_kernel_cmdline(0, import_kernel_nv);的定义在~/my_android/system/core/init/util.c中,实现代码如下:

void import_kernel_cmdline(int in_qemu,
void (*import_kernel_nv)(char *name, int in_qemu))
{
char cmdline[1024];
char *ptr;
int fd; fd = open("/proc/cmdline", O_RDONLY);
if (fd >= 0) {
int n = read(fd, cmdline, 1023);
if (n < 0) n = 0; /* get rid of trailing newline, it happens */
if (n > 0 && cmdline[n-1] == '\n') n--; cmdline[n] = 0;
close(fd);
} else {
cmdline[0] = 0;
} ptr = cmdline;
while (ptr && *ptr) {
char *x = strchr(ptr, ' ');
if (x != 0) *x++ = 0; //可以拆分为*x = 0; x++;
import_kernel_nv(ptr, in_qemu);
ptr = x;
}
}

首先打开文件"/proc/cmdline",读取其内容到变量cmdline中,"/proc/cmdline"中内容如下:


其后对cmdline的字符数组处理非常简单。然后到while()循环,

strchr函数返回第二个变量在第一个变量中第一次出现的位置,具体用法可参考:http://blog.csdn.net/sky2098/article/details/1530433

import_kernel_nv(ptr, in_qemu)定义在~/my_android/system/core/init/init.c中,实现的代码如下:

static void import_kernel_nv(char *name, int for_emulator)
{
char *value = strchr(name, '=');
int name_len = strlen(name); if (value == 0) return;
*value++ = 0;
if (name_len == 0) return; #ifdef HAVE_SELINUX
if (!strcmp(name,"selinux")) {
selinux_enabled = atoi(value);
}
#endif if (for_emulator) {
/* in the emulator, export any kernel option with the
* ro.kernel. prefix */
char buff[PROP_NAME_MAX];
int len = snprintf( buff, sizeof(buff), "ro.kernel.%s", name ); if (len < (int)sizeof(buff))
property_set( buff, value );
return;
} if (!strcmp(name,"qemu")) {
strlcpy(qemu, value, sizeof(qemu));
#ifdef WANTS_EMMC_BOOT
} else if (!strcmp(name,"androidboot.emmc")) {
if (!strcmp(value,"true")) {
emmc_boot = 1;
}
#endif
} else if (!strcmp(name,BOARD_CHARGING_CMDLINE_NAME)) {
strlcpy(battchg_pause, value, sizeof(battchg_pause));
} else if (!strncmp(name, "androidboot.", 12) && name_len > 12) {
const char *boot_prop_name = name + 12;
char prop[PROP_NAME_MAX];
int cnt; cnt = snprintf(prop, sizeof(prop), "ro.boot.%s", boot_prop_name);
if (cnt < PROP_NAME_MAX)
property_set(prop, value);
}
}

然后,返回到process_kernel_cmdline()中,由于变量qemu的定义为:

static char qemu[32];

由于没有初始化,所以if(qemu[0])判断为否,所以接着执行export_kernel_boot_props()函数,其定义在~/my_android/system/core/init/init.c中,实现的代码如下:

static void export_kernel_boot_props(void)
{
char tmp[PROP_VALUE_MAX];
const char *pval;
unsigned i;
struct {
const char *src_prop;
const char *dest_prop;
const char *def_val;
} prop_map[] = {
{ "ro.boot.serialno", "ro.serialno", "", },
{ "ro.boot.mode", "ro.bootmode", "unknown", },
{ "ro.boot.baseband", "ro.baseband", "unknown", },
{ "ro.boot.bootloader", "ro.bootloader", "unknown", },
}; for (i = 0; i < ARRAY_SIZE(prop_map); i++) {
pval = property_get(prop_map[i].src_prop);
property_set(prop_map[i].dest_prop, pval ?: prop_map[i].def_val);
} pval = property_get("ro.boot.console");
if (pval)
strlcpy(console, pval, sizeof(console)); /* save a copy for init's usage during boot */
strlcpy(bootmode, property_get("ro.bootmode"), sizeof(bootmode)); /* if this was given on kernel command line, override what we read
* before (e.g. from /proc/cpuinfo), if anything */
pval = property_get("ro.boot.hardware");
if (pval)
strlcpy(hardware, pval, sizeof(hardware));
property_set("ro.hardware", hardware); snprintf(tmp, PROP_VALUE_MAX, "%d", revision);
property_set("ro.revision", tmp);
property_set("ro.emmc",emmc_boot ? "1" : "0");
property_set("ro.boot.emmc", emmc_boot ? "1" : "0"); /* TODO: these are obsolete. We should delete them */
if (!strcmp(bootmode,"factory"))
property_set("ro.factorytest", "1");
else if (!strcmp(bootmode,"factory2"))
property_set("ro.factorytest", "2");
else
property_set("ro.factorytest", "0");
}

从export_kernel_boot_props函数的代码可以看出,该函数实际上就是来回设置一些属性值,并且利用某些属性值修改console、hardware等变量。其中hardware变量(就是一个长度为32的字符数组)在get_hardware_name函数中已经从/proc/cpuinfo文件中获得过一次值了,在export_kernel_boot_props函数中又通过ro.boot.hardware属性设置了一次值。

接下来的#ifdef HAVE_SELINUX……#endif,是和Security-Enhanced Android相关的。

参考:

  1. http://blog.csdn.net/jiangbei_lengyu/article/details/8564144
  2. http://www.cnblogs.com/bastard/archive/2012/08/28/2660389.html
  3. http://www.cnblogs.com/nokiaguy/archive/2013/04/14/3020774.html

Android 4.2启动代码分析(一)的更多相关文章

  1. STM32启动代码分析 IAR 比较好

    stm32启动代码分析 (2012-06-12 09:43:31) 转载▼     最近开始使用ST的stm32w108芯片(也是一款zigbee芯片).开始看他的启动代码看的晕晕呼呼呼的. 还好在c ...

  2. Linux内核启动代码分析二之开发板相关驱动程序加载分析

    Linux内核启动代码分析二之开发板相关驱动程序加载分析 1 从linux开始启动的函数start_kernel开始分析,该函数位于linux-2.6.22/init/main.c  start_ke ...

  3. Cocos2d-x3.3RC0的Android编译Activity启动流程分析

    本文将从引擎源代码Jni分析Cocos2d-x3.3RC0的Android Activity的启动流程,以下是具体分析. 1.引擎源代码Jni.部分Java层和C++层代码分析 watermark/2 ...

  4. Cortex-M0(NXP LPC11C14)启动代码分析

    作者:刘老师,华清远见嵌入式学院讲师. 启动代码的一般作用 1.堆和栈的初始化: 2.向量表定义: 3.地址重映射及中断向量表的转移: 4.初始化有特殊要求的断口: 5.处理器模式: 6.进入C应用程 ...

  5. ARM Linux启动代码分析

    前言 在学习.分析之前首先要弄明白一个问题:为什么要分析启动代码? 因为启动代码绝大部分都是用汇编语言写的,对于没学过或者不熟悉汇编语言的同学确实有一定难度,但是如果你想真正深入地学习Linux,那么 ...

  6. STM32启动代码分析

    STM32启动文件简单分析(STM32F10x.s适用范围)定时器, 型号, 名字在<<STM32不完全手册里面>>,我们所有的例程都采用了一个叫STM32F10x.s的启动文 ...

  7. S3C6410的启动代码分析&nbsp;一

    本文开始第一篇,启动代码的编写,注意,仅仅是启动代码,并不是bootloader,因为只有boot,没有loader. 第一要明确:CPU上电之后,会从某个固定地址执行指令.ARM结构的CPU从地址0 ...

  8. STM32启动代码分析及其汇编学习-ARM

    STM32 启动代码 Author By YuCloud 边看启动文件边学汇编 汇编 see ARM: Assembler User Guide see: https://blog.csdn.net/ ...

  9. android recovery 主系统代码分析

    阅读完上一篇文章: http://blog.csdn.net/andyhuabing/article/details/9226569 我们已经清楚了如何进入正常模式和Recovery模式已有深刻理解了 ...

随机推荐

  1. VB生成xml

    Dim text As XmlText Dim doc As New XmlDocument '加入XML的声明段落 Dim node As XmlNode = doc.CreateXmlDeclar ...

  2. Go语言AST尝试

    Go语言有很多工具, goimports用于package的自动导入或者删除, golint用于检查源码中不符合Go coding style的地方, 比如全名,注释等. 还有其它工具如gorenam ...

  3. SQL Server IO系统问题解决

    方法 1. 查询是不是真的要返回这么多的数据. 方法 2. 查询是不是系统的内存不足. 方法 3. 检查查询要访问的数据是不是不常用.如果这个数据不常用,它没有在内存中也就不奇怪了. 方法 4. 是不 ...

  4. Realview MDK 中不用手动开中断的原因

    startup.s启动代码文件: ; Enter Supervisor Mode and set its Stack Pointer MSR CPSR_c, #Mode_SVC:OR:I_Bit:OR ...

  5. JD-GUI on Ubuntu 13.04 64-bit

    Java Decompiler (jd-gui) is a cute little tool I like using when working in Java. Unfortunately it o ...

  6. .NET(C#):使用反射来获取枚举的名称、值和特性【转】

    首先需要从内部了解一下枚举(Enumeration),相信许多人已经知道了,当我们声明一个这样的枚举类型: enumMyEnum { AAA, BBB, CCC } 背后的IL是这样的: .class ...

  7. POJ1325 Machine Schedule 【二分图最小顶点覆盖】

    Machine Schedule Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 11958   Accepted: 5094 ...

  8. EF5 通用数据层 增删改查操作,泛型类(转)

    using System; using System.Collections.Generic; using System.Data.Entity.Infrastructure; using Syste ...

  9. SharePoint 2013 强制安装解决方案

    Add-SPSolution Install-SPSolution -Identity DemonstrationZone.wsp -GACDeployment -CompatibilityLevel ...

  10. CRM setValue方法日期类型字段赋值

    setvalue datetime 赋值 得到“/Date(14000023232323)/”  这样的值 需要把 /去掉 var dd = Result.yt_purchase_date.subst ...