http://www.2cto.com/kf/201411/354953.html


功能概述

init进程是Android内核启动的第一个进程,其进程号(pid)为1,是Android系统所有进程的祖先,因此它肩负着系统启动的重要责任。Android的init源代码位于system/core/init/目录下,伴随Android系统多个版本的迭代,init源代码也几经重构。
目前Android4.4源代码中,init目录编译后生成如下Android系统的三个文件,分别是 /init/sbin/ueventd-->/init
/sbin/watchdogd-->/init 其中ueventd与wathdogd均是指向/init的软链接。(具体实现请 阅读 init/Android.mk)。
在Android系统早期版本(2.2之前)只有init进程,Android2.2中将创建设备驱动节点文件功能独立到ueventd进程完成,在Android4.1中则添加了watchdogd。

/init主要完成三大功能:
解析init.rc初始化Android属性系统,并维护属性服务初始化Android属性系统,并维护属性服务
处理子进程启动、停止、重启动 /ueventd用于创建设备驱动节点。 /watchdogd 是看门狗服务进程。

代码分析

分析代码当先抓住主干,了解其大致结构与流程,再逐块深入,分析其实现细节。这样先大局再细节的方法可以让我们在阅读代码时保持头脑的清醒,切忌不可在没有对整体流程了解的情况下深入细节,那很容易导致我们迷失在代码森林中。
接下来分析init.c的main函数。为了方便分析,将main函数代码做了精简,代码如下。 ?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 int main( int argc, char **argv) { //<part 1=""> if (!strcmp(basename(argv[ 0 ]), "ueventd" )) return ueventd_main(argc, argv); if (!strcmp(basename(argv[ 0 ]), "watchdogd" )) return watchdogd_main(argc, argv); //<part2> umask( 0 ); 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); .... open_devnull_stdio(); klog_init(); property_init(); .... //<part3> INFO( "reading config file\n" ); init_parse_config_file( "/init.rc" ); ... action_for_each_trigger( "early-init" , action_add_queue_tail); .... queue_builtin_action(queue_property_triggers_action, "queue_property_triggers" ); //<part4> for (;;) { ... execute_one_command(); restart_processes(); .... nr = poll(ufds, fd_count, timeout); if (nr <= 0 ) continue ; for (i = 0 ; i < fd_count; i++) { if (ufds[i].revents & POLLIN) { if (ufds[i].fd == get_property_set_fd()) handle_property_set_fd(); else if (ufds[i].fd == get_keychord_fd()) handle_keychord(); else if (ufds[i].fd == get_signal_fd()) handle_signal(); } } } return 0 ; }</part4></part3></part2></part>
将main函数分为上述4个部分,对应part1到part4,下面分别做具体说明。

代码

通过命令行判断argv[0]的字符串内容,来区分当前程序是init,ueventd或是watchdogd。
C程序的main函数原型为 main(int argc, char* argv[]), ueventd以及watchdogd的启动都在init.rc中描述,由init进程解析后执行fork、exec启动,因此其入口参数的构造在init代码中,将在init.rc解析时分析。此时我们只需要直到argv[0]中将存储可执行文件的名字。

代码

umaks(0)用于设定当前进程(即/init)的文件模型创建掩码(file mode creation mask),注意这里的文件是广泛意义上的文件,包括普通文件、目录、链接文件、设备节点等。
PS. 以上解释摘自umask的mannual,可在linux系统中执行man 3 umask查看。
Linux C库中mkdir与open的函数运行如下。
int mkdir(const char *pathname, mode_t mode);
int open(const char *pathname, int flags, mode_t mode);
Linux内核给每一个进程都设定了一个掩码,当程序调用open、mkdir等函数创建文件或目录时,传入open的mode会现在掩码做运算,得到的文件mode,才是文件真正的mode。
譬如要创建一个目录,并设定它的文件权限为0777,
mkdir("testdir", 0777)
但实际上写入的文件权限却未必是777,因为mkdir系统调用在创建testdir时,会将0777与当前进程的掩码(称为umask)运算,具体运算方法为 0777&~umask作为testdir的真正权限。因此上述init中首先调用umask(0)将进程掩码清0,这样调用open/mkdir等函数创建文件或目录时,传入的mode就会作为实际的值写入文件系统。

接下来创建目录,并挂载内核文件系统,它们是
tmpfs,虚拟内存文件系统,该文件系统被挂载到/dev目录下,主要存放设备节点文件,用户进程通过访问/dev目录下的设备节点文件可以与硬件驱动程序交互。devpts,一种虚拟终端文件系统
proc,虚拟文件系统,被挂载到/proc目录下,通过该文件系统可与内核数据结构交互,查看以及设定内核参数。sysfs,虚拟文件系统,被挂载到/sys目录下,它与proc类似,是2.6内核在吸收了proc文件系统的设计经验和教训的基础上所实现的一种较新的文件系统,为内核提供了统一的设备驱动模型。(引用:http://www.ibm.com/developerworks/cn/linux/l-cn-sysfs/index. html ) 代码随后的代码如下。
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 open_devnull_stdio(); klog_init(); property_init(); get_hardware_name(hardware, &revision); process_kernel_cmdline(); union selinux_callback cb; cb.func_log = log_callback; selinux_set_callback(SELINUX_CB_LOG, cb); cb.func_audit = audit_callback; selinux_set_callback(SELINUX_CB_AUDIT, cb); selinux_initialize(); /* These directories were necessarily created before initial policy load * and therefore need their security context restored to the proper value. * This must happen before /dev is populatedproperty_init(); by ueventd. */ restorecon( "/dev" ); restorecon( "/dev/socket" ); restorecon( "/dev/__properties__" ); restorecon_recursive( "/sys" ); is_charger = !strcmp(bootmode, "charger" ); INFO( "property init\n" ); property_load_boot_defaults();

open_devnull_stdio()

该函数名字暗示将init进程的stido,包括stdin(标准输入,文件描述符为0)、stdout(标准输出,文件描述符为1)以及stderr(标准错误,文件描述符号为2),全部重定向/dev/null设备,但是细心的读者可能会有疑问,在代码中虽然挂载了tmpfs文件系统到/dev目录下,但是并未创建任何设备节点文件,/dev/null此时并不存在啊,如何才能将stdio重定向到null设备中呢?带着疑问我们来分析该函数实现。
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 ); }
该函数中通过mknode函数创建/dev/__null__设备节点文件,随后打开该文件得到文件描述符fd,然后利用dup2系统调用将文件描述符0、1、2绑定到fd上。这个/dev/__null__看起来很奇怪,Linux系统中的null不是/dev/null么,这两者有什么关系么?
在Linux内核为设备节点文件分别了一个主、次设备号,内核实际以这两个设备号来标识某个设备驱动,而并不以文件名作为标识。mknod系统调用创建设备节点文件,其第三个参数的高8位为主设备号,低8位次设备号。可见/dev/__null__的主次设备号分别是1、3。它是否就是/dev/null呢?我们需要深入内核去确认这一点。
kernel/Documentation/devices.txt 中存在如下片段
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 1 char Memory devices 1 = /dev/mem Physical memory access 2 = /dev/kmem Kernel virtual memory access 3 = /dev/ null Null device 4 = /dev/port I/O port access 5 = /dev/zero Null byte source 6 = /dev/core OBSOLETE - replaced by /proc/kcore 7 = /dev/full Returns ENOSPC on write 8 = /dev/random Nondeterministic random number gen. 9 = /dev/urandom Faster, less secure random number gen. 10 = /dev/aio Asynchronous I/O notification interface 11 = /dev/kmsg Writes to this come out as printk's 12 = /dev/oldmem Used by crashdump kernels to access the memory of the kernel that crashed.
可见/dev/__null__与/dev/null的设备号完全相同,它就是/dev/null的马甲。那么为什么init进程不直接创建/dev/null呢? 当前我们还无法回答这个问题,要等到分析/sbin/uevnted的原理时才能明白。
还有一个疑问,为什么要将stdio重定向/dev/__null__设备呢?这是因为此时Anrdoid系统上处于启动的早期阶段,可用于接收init进程标准输出、标准错误的设备节点还不存在。因此init进程一不做二不休,直接把它们重定向到/dev/__nulll__了。
当我们学习C语言时,第一个helloworld程序是通过printf打印的,我们知道它通过标准输出打印到终端上。printf也是我们广大程序员最喜爱的调试方法之一。现在标准输出被重定向到null设备了,如果我们想在init中添加打印语句,怎么办呢?带着这样的担忧,我们继续分析代码。

klog_init()

随后klog_init()显然是在暗示我们,虽然标准输出没了,但是还有方法打印log的。带着欣喜又好奇的心情,让我们看看klog_init是如何实现的。
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void klog_init( void ) { static const char *name = "/dev/__kmsg__" ; if (klog_fd >= 0 ) return ; /* Already initialized */ if (mknod(name, S_IFCHR | 0600 , ( 1 << 8 ) | 11 ) == 0 ) { klog_fd = open(name, O_WRONLY); if (klog_fd < 0 ) return ; fcntl(klog_fd, F_SETFD, FD_CLOEXEC); unlink(name); } }
klog_init函数首先检查klog_fd是否已经初始化。首次执行时,调用mknod创建主设备号为1,从设备号为11的设备节点文件/dev/__kmsg__,然后打开该文件将文件描述符保存到变量klog_fd中,接着调用fcntl(klog_fd, F_SETFD, FD_CLOEXEC)句作用是设置当执行execv时,关闭该文件描述符。随后调用unlink来删除/dev/__kmsg__文件,这里比较特殊,具体解释下。
当open某个文件却还没有close它时,调用unlink并不能删除该文件,该文件将在调用close后被删除。对内核来说,当调用open打开一个文件,内核维护对应该文件的数据结构,其中存在一个变量维护当前文件的引用计数,该数据结构在用户空间即对应文件描述符。第一次open后,引用计数为1,调用open将使引用计数加1, 调用close将使得引用计数减1。当调用unlink系统调用时,若文件引用计数非0,则内核并不会立刻删除该文件,内核会在每次close该文件时检查引用计数,若为0时将真正删除文件。
?
1 2 P.S.根据unlink的mannul,(man 2 unlink),其中写道: If the name was the last link to a file but any processes still have the file open the file will remain in existence until the last file descriptor referring to it is closed.
/dev/__kmsg__文件与/dev/kmsg的设备节点完全相同,前者同样是后者的马甲。该设备驱动节点是内核日志文件,内核调用printk函数打印的log可以通过该设备节点访问,向该文件中写入则等同于执行内核printk。该文件的内容可通Linux系统标准程序dmesg读取, Android 系统也提供了dmesg命令。

klog.c文件代码较少,在此一并分析
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 static int klog_level = KLOG_DEFAULT_LEVEL; int klog_get_level( void ) { return klog_level; } void klog_set_level( int level) { klog_level = level; } #define LOG_BUF_MAX 512 void klog_vwrite( int level, const char *fmt, va_list ap) { char buf[LOG_BUF_MAX]; if (level > klog_level) return ; if (klog_fd < 0 ) klog_init(); if (klog_fd < 0 ) return ; vsnprintf(buf, LOG_BUF_MAX, fmt, ap); buf[LOG_BUF_MAX - 1 ] = 0 ; write(klog_fd, buf, strlen(buf)); } void klog_write( int level, const char *fmt, ...) { va_list ap; va_start(ap, fmt); klog_vwrite(level, fmt, ap); va_end(ap); }
klog_write调用klog_vwrite函数可用于向/dev/__kmesg__中写入日志,第一个参数是当前log的级别,如果当前level大于klog_leve则直接返回,即无法将log写入/dev/__kmesg__中。此外,提供了两个函数klog_set_level与klog_get_level分别用于设置和读取当前的klog_level,默认level为KLOG_DEFAULT_LEVEL,在klog.h中定义。

klog.h

?
1 2 3 4 5 6 7 8 9 10 11 12 13 #define KLOG_ERROR_LEVEL 3 #define KLOG_WARNING_LEVEL 4 #define KLOG_NOTICE_LEVEL 5 #define KLOG_INFO_LEVEL 6 #define KLOG_DEBUG_LEVEL 7 #define KLOG_ERROR(tag,x...) klog_write(KLOG_ERROR_LEVEL, "<3>" tag ": " x) #define KLOG_WARNING(tag,x...) klog_write(KLOG_WARNING_LEVEL, "<4>" tag ": " x) #define KLOG_NOTICE(tag,x...) klog_write(KLOG_NOTICE_LEVEL, "<5>" tag ": " x) #define KLOG_INFO(tag,x...) klog_write(KLOG_INFO_LEVEL, "<6>" tag ": " x) #define KLOG_DEBUG(tag,x...) klog_write(KLOG_DEBUG_LEVEL, "<7>" tag ": " x) #define KLOG_DEFAULT_LEVEL 3 /* messages <= this level are logged */
可见默认级别为3,即KLOG_ERROR_LEVEL,只有调用KLOG_ERROR才能被输出到/dev/__kmesg__中。

property_init();

这一句用来初始化Android的属性系统,将在init之属性系统中专门介绍。

get_hardware_name

get_hardware_name(hardware, &revision)通过读取/proc/cpuinfo文件获取硬件信息,以笔者的山寨机为例,该文件内容如下。

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 shell @android :/ $ cat /proc/cpuinfo Processor : ARMv7 Processor rev 1 (v7l) processor : 0 BogoMIPS : 348.76 processor : 1 BogoMIPS : 348.76 processor : 2 BogoMIPS : 348.76 processor : 3 BogoMIPS : 348.76 Features : swp half thumb fastmult vfp edsp thumbee neon vfpv3 tls vfpv4 CPU implementer : 0x41 CPU architecture: 7 CPU variant : 0x0 CPU part : 0xc05 CPU revision : 1 Hardware : QRD MSM8625Q SKUD Revision : 0000 Serial : 0000000000000000
get_hardware_name函数读取该文件,将Hardware字段的值填入hardware数组中,将Revision字段的值转换为16进制数字填入revision变量中。

process_kernel_cmdline

接下来init程序调用函数process_kernel_cmdline解析内核启动参数。内核通常由bootloader(启动引导程序)加载启动,目前广泛使用的bootloader大都基于u-boot定制。内核允许bootloader启动自己时传递参数。在内核启动完毕之后,启动参数可通过/proc/cmdline查看。

例如android4.4模拟器启动后,查看其内核启动参数,如下
[email protected]:/ # cat /proc/cmdline
qemu.gles=0 qemu=1 console=ttyS0 android.qemud=ttyS1 android.checkjni=1 ndns=1

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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(); }
首先修改/proc/cmdline文件权限,0440即表明只有root用户或root组用户可以读写该文件,其他用户无法访问。随后连续调用import_kernel_cmdline函数,第一个参数标识当前Android设备是否是模拟器,第二个参数一个函数指针。

import_kernel_cmdline函数将/proc/cmdline内容读入到内部缓冲区中,并将cmdline内容的以空格拆分成小段字符串,依次传递给import_kernel_nv函数处理。以前面/proc/cmdline的输出为例子,该字符串共可以拆分成以下几段

?
1 2 3 4 5 6 qemu.gles= 0 qemu= 1 console=ttyS0 android.qemud=ttyS1 android.checkjni= 1 ndns= 1
因此在import_kernel_nv将会被连续调用6次,依次传入上述字符串。函数实现如下:

import_kernel_nv

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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 ; 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)); } 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); } }
import_kernel_cmdline第一次执行时,传入import_kernel_nv的形式参数for_emulator为 0,,因此将匹配name是否为qemu,如果是,将其值保存到qemu全局静态缓冲区中。对于android模拟器,存在/proc/cmdline中存在“qemu=1”字段。如果for_emulator为1,则将生成ro.kernel.{name}={value}属性写入Android的属性系统中。

此时回到process_kernel_cmdline函数,继续执行

?
1 2 if (qemu[ 0 ]) import_kernel_cmdline( 1 , import_kernel_nv);
当系统为模拟器时,qemu[0]其值为'1',第二次执行import_kernel_cmdline,将再次调用6次import_kernel_nv,并且for_emulator为1,因此将生成6个属性,现在来确定以下我们的分析。

?
1 2 3 4 5 6 7 root @generic :/ # getprop | grep ro.kernel. [ro.kernel.android.checkjni]: [ 1 ] [ro.kernel.android.qemud]: [ttyS1] [ro.kernel.console]: [ttyS0] [ro.kernel.ndns]: [ 1 ] [ro.kernel.qemu.gles]: [ 0 ] [ro.kernel.qemu]: [ 1 ]
可验证我们的分析是正确的。

export_kernel_boot_props()

接下来继续执行process_kernel_cmdline函数的最后一句export_kernel_boot_props。由于该函数实现非常直观,其代码不在详细描述。该函数用于设置几个系统属性,具体包括如下:
读取ro.boot.serialno,若存在其值写入ro.serialno,否则ro.serialno写入空。 读取ro.boot.mode,若存在其值写入ro.bootmode,否则ro.bootmode写入"unkown" 读取ro.boot.baseband,若存在其值写入ro.baseband,否则ro.baseband写入"unkown"
读取ro.boot.bootloader,若存在其值写入ro.bootloader,否则ro.bootloader写入"unkown" 读取ro.boot.console,若存在,其值写入全局缓冲区console中 读取ro.bootmode,若存在,其值保存到全局缓冲区bootmode中
读取ro.boot.hardware,若存在其值写入ro.hardware,否则将/proc/cmdline中解析出来的hardware写入ro.hardware中。

SELinux

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 union selinux_callback cb; cb.func_log = log_callback; selinux_set_callback(SELINUX_CB_LOG, cb); cb.func_audit = audit_callback; selinux_set_callback(SELINUX_CB_AUDIT, cb); selinux_initialize(); /* These directories were necessarily created before initial policy load * and therefore need their security context restored to the proper value. * This must happen before /dev is populated by ueventd. */ restorecon( "/dev" ); restorecon( "/dev/socket" ); restorecon( "/dev/__properties__" ); restorecon_recursive( "/sys" );
这部分代码是在Android4.1之后添加的,随后伴随Android系统更新不停迭代。这段代码主要涉及SELinux初始化。由于SELinux与Android系统启动关闭不大,暂不分析。

回到init函数继续分析

?
1 2 3 4 is_charger = !strcmp(bootmode, "charger" ); INFO( "property init\n" ); property_load_boot_defaults();
第一句将利用bootmode与字符串"charger"将其保存到is_charger变量中,is_charger非0表明但前Android是以充电模式启动,否则为正常模式。正常启动模式与充电模式需要启动的进程不同的,这两种模式启动具体启动的程序差别将在init.rc解析时介绍。

接下来调用INFO宏打印一条log语句,此宏定义在init/log.h中,其实现如下

?
1 2 3 #define ERROR(x...) KLOG_ERROR( "init" , x) #define NOTICE(x...) KLOG_NOTICE( "init" , x) #define INFO(x...) KLOG_INFO( "init" , x)
显然这是一条level为KLOG_INFO_LEVEL的log语句。它是否能输出到/dev/__kmesg__中跟当前klog level的值有关。默认情况下,klog level为3,这条语句将不会输出到/dev/__kmsg__中。

到这里init.c main函数之代码分析分析完毕。

接下来代码涉及init进程核心功能:init.rc解析。这部分代码逻辑我们将在独立文章《Android init源代码分析(2)init.rc解析》中介绍。


更多相关文章

  1. C语言函数的递归(上)
  2. Android中自带的SQLite数据库
  3. 使用AndroidStudio生成打有系统签名的apk
  4. ubuntu 编译 Android(安卓)出现的若干错误及解决方法
  5. Android(安卓)NDK开发之旅(5):Android(安卓)Studio中使用CMake进
  6. Android(安卓)Studio查看Android(安卓)5.x源码的步骤详解
  7. Android文件相关:RandomAccessFile介绍与使用demo
  8. Android(安卓)ClassLoader
  9. android 数据存储之 SharedPreference

随机推荐

  1. Android(安卓)Studio中mergeDebugResourc
  2. Android之ListView原理学习与优化总结
  3. Android(安卓)侧滑关闭Activity的实例
  4. 滤镜开发(一) 开篇:关于滤镜
  5. android listView 到上下边界 蓝色或黄色
  6. 【已解决】Android(安卓)Studio中不能使
  7. Android倒计时控件的实现
  8. Android(安卓)= Java
  9. Android(安卓)Studio 多版本共存
  10. Android模拟开关按钮点击打开动画(属性动