Android系统初始化开始位置及流程分析

Android系统初始化开始位置及流程分析
android系统的初始化过程是从那里开始呢?它在加载linux基本内核后,就开始运行一个初始化进程,叫做init进程,那么怎么样知道它是加载init进程的呢?难道上天就注定的吗?呵呵,不是的,原来是从android加载linux内核时,就设置了下面的参数:
Kernel command line: noinitrd root=/dev/nfs console=ttySAC0 init=/init nfsroot=192.168.1.103:/nfsboot ip=192.168.1.20:192.168.1.103:192.168.1.1:255.255.255.0::eth0:on
在这行命令里,就是告诉linux内核初始化完成后开始运行init进程,由于init进程就是放在系统根目录下面。而这个进程的代码,就是位于源码的目录system/core/init下面,现在就来仔细地分析这个进程到底做了什么事情,以便理解整个系统运行情况。在分析过程中,会学习很多有用知识,甚至linux编程知识。这么有用,还等什么呢?现在就开始,找到目录system/core/init/init.c代码,先从main函数开始,如下:
#001 int main(int argc, char **argv)
#002 {
#003 int device_fd = -1;
#004 int property_set_fd = -1;
#005 int signal_recv_fd = -1;
#006 int keychord_fd = -1;
#007 int fd_count;
#008 int s[2];
#009 int fd;
#010 struct sigaction act;
#011 char tmp[PROP_VALUE_MAX];
#012 struct pollfd ufds[4];
#013 char *tmpdev;
#014 char* debuggable;
#015
#016
#017 act.sa_handler = sigchld_handler;
#018 act.sa_flags = SA_NOCLDSTOP;
#019 act.sa_mask = 0;
#020 act.sa_restorer = NULL;
#021 sigaction(SIGCHLD, &act, 0);
在上面这段代码里,调用函数sigaction来设置处理子进程发送回来的关闭信号,其中SIGCHLD是设置子进程信号处理,SA_NOCLDSTOP是表示子进程结束时不要向父进程发送SIGCHLD,sigchld_handler是信号SIGCHLD的处理函数。这样做的作用,就是如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。因此需要对SIGCHLD信号做出处理,回收僵尸进程的资源,避免造成不必要的资源浪费。
#022
#023 /* clear the umask */
#024 umask(0);
在上面这段代码里,调用函数umask来设置屏蔽位为0值。这样的意思是什么呢?是告诉系统做了那些工作呢?要了解这个,就得深入查看一下linux函数大全了,因为它的作用就一目了然了,它的解释如下:
linux中的 umask 函数主要用于:在创建新文件或目录时 屏蔽掉新文件或目录不应有的访问允许权限。文件的访问允许权限共有9种,分别是:r w x r w x r w x(它们分别代表:用户读 用户写 用户执行 组读 组写 组执行 其它读 其它写 其它执行)。
其实这个函数的作用,就是设置允许当前进程创建文件或者目录最大可操作的权限,比如这里设置为0,它的

意思就是0取反再创建文件时权限相与,也就是:(~0) & mode 等于八进制的值0777 & mode了,这样就是给后面的代码调用函数mkdir给出最大的权限,避免了创建目录或文件的权限不确定性,指定明确的标志,可谓是开发人员对代码健壮性深刻的反映,高度明确性。
#025
#026 /* Get the basic filesystem setup we need put
#027 * together in the initramdisk on / and then we'll
#028 * let the rc file figure out the rest.
#029 */
#030 mkdir("/dev", 0755);
#031 mkdir("/proc", 0755);
#032 mkdir("/sys", 0755);
在上面这段代码里,主要就是在当前内存模拟磁盘里建立一个基本的文件系统,以便后面加载rc文件来做其它事情。其中就是创建设备目录dev,进程文件系统目录proc,系统目录sys。

/dev是devfs(设备文件系统)或者udev的挂在点所在。在使用devfs的内核里如果没有/dev,根本见不到Shell启动的信息,因为内核找不到/dev/console;在使用udev的系统里,也事先需要在/dev下建立console和null这两个节点。关于devfs和udev的区别,网上很多文章说。当然如果你的内核已经不支持devfs了(2.6.12以后),可以使用纯纯的静态节点。也就是用mknod人工生成。

/proc是用来挂载存放系统信息虚拟文件系统——“proc文件系统”,“proc文件系统”在内核里面可以选。如果没有“proc文件系统”,很多Shell自己的命令就没有办法运行,比如ifconfig。“proc文件系统”不像devfs可以自动挂载,它需要使用初始化脚本挂载。另外,udev也需要“proc文件系统”的支持。
/sys用于挂载“sysfs文件系统”,“sysfs文件系统”在内核里面可以选。
#033
#034 mount("tmpfs", "/dev", "tmpfs", 0, "mode=0755");
#035 mkdir("/dev/pts", 0755);
#036 mkdir("/dev/socket", 0755);
#037 mount("devpts", "/dev/pts", "devpts", 0, NULL);
#038 mount("proc", "/proc", "proc", 0, NULL);
#039 mount("sysfs", "/sys", "sysfs", 0, NULL);
在 Linux 中将一个文件系统与一个存储设备关联起来的过程称为挂装(mount)。使用 mount 命令将一个文件系统附着到当前文件系统层次结构中(根)。在执行挂装时,要提供文件系统类型、文件系统和一个挂装点。因此,这里就是把tmpfs文件系统加到目录/dev下面,文件系统的名称是tmpfs。tmpfs是一个虚拟内存文件系统,它不同于传统的用块设备形式来实现的Ramdisk,也不同于针对物理内存的Ramfs。Tmpfs可以使用物理内存,也可以使用交换分区。在Linux内核中,虚拟内存资源由物理内存(RAM)和交换分区组成,这些资源是由内核中的虚拟内存子系统来负责分配和管理。Tmpfs向虚拟内存子系统请求页来存储文件,它同Linux的其它请求页的部分一样,不知道

分配给自己的页是在内存中还是在交换分区中。同Ramfs一样,其大小也不是固定的,而是随着所需要的空间而动态的增减。接着创建pts和socket目录,在/dev/pts挂装devpts虚拟文件系统,在目录/proc挂装proc文件系统,在目录/sys挂装sysfs文件系统。
#040
#041 /* We must have some place other than / to create the
#042 * device nodes for kmsg and null, otherwise we won't
#043 * be able to remount / read-only later on.
#044 * Now that tmpfs is mounted on /dev, we can actually
#045 * talk to the outside world.
#046 */
#047 open_devnull_stdio();
这段代码是创建空的设备节点(/dev/null)。
#048 log_init();
这段代码是创建kmsg(/dev/kmsg)节点,主要用来输出LOG信息。比如把LOG信息输出到开发板的串口上,再在电脑上打印出来,方便跟踪和调试系统的功能。
#049
#050 //caijs add test. 2010-07-13
#051 ERROR("Init::main() '%s'\n", "xxxxxxxxxxx 2010-07-13");
这里是我测试系统引导输出的一行LOG代码。
#052
#053 INFO("reading config file\n");
#054 parse_config_file("/init.rc");
这段代码是分析根目录下面的init.rc配置文件,并且把里面的参数组成链表的方式,以便后面使用,后面再来仔细地分析init.rc文件的格式和内容。
#055
#056 /* pull the kernel commandline and ramdisk properties file in */
#057 qemu_init();
这里初始化qemu模拟器运行计数,这里是指模拟ARM指令的虚拟系统。
#058 import_kernel_cmdline(0);
这段代码是从linux内核里获取引导系统给内核的引导参数,并保存到全局变量,以便使用。
#059
#060 get_hardware_name();
这段代码是获取当前android系统运行的硬件信息,比如硬件的CPU名称。主要从/proc/cpuinfo里读到相关的信息。
#061 snprintf(tmp, sizeof(tmp), "/init.%s.rc", hardware);
#062 parse_config_file(tmp);
这段代码是从前面获取到的硬件名称,然后以硬件的名称(/init.硬件名称.rc)来获取相应硬件的配置文件,并且把配置文件参数加载到链表里。
#063
#064 action_for_each_trigger("early-init", action_add_queue_tail);
#065 drain_action_queue();
这段代码是先把有early-init标识的命令提前添加到一个命令队列,以便函数drain_action_queue一个一个命令地执行配置文件里的函数,这样可以在不同的配置文件里,只要标明是最先执行的函数,就可以优先地运行。
#066
#067 INFO("device init\n");
#068 device_fd = device_init();
这段代码是遍历为/sys,添加设备事件响应,创建设备节点。
#069
#070 property_init();
这段代码是进行属性初始化。每个属性都有一个名称和值,它们都是字符串格式。属性被大量使用在Android系统中,用来记

录系统设置或进程之间的信息交换。属性是在整个系统中全局可见的。每个进程可以get/set属性。在系统初始化时,Android将分配一个共享内存区来存储的属性,这里主要是从/default.prop属性文件读取属性。这个有点像Windows下的注册表的作用。
#071
#072 // only listen for keychords if ro.debuggable is true
#073 debuggable = property_get("ro.debuggable");
#074 if (debuggable && !strcmp(debuggable, "1")) {
#075 keychord_fd = open_keychord();
#076 }
这段代码是从属性里获取调试标志,如果是可以调试,就打开组合按键输入驱动程序。
#077
#078 if (console[0]) {
#079 snprintf(tmp, sizeof(tmp), "/dev/%s", console);
#080 console_name = strdup(tmp);
#081 }
#082
#083 fd = open(console_name, O_RDWR);
#084 if (fd >= 0)
#085 have_console = 1;
#086 close(fd);
这段代码是判断是否有控制台,如果没有,就尝试是否是可以打缺省的控制台。
#087
#088 if( load_565rle_image(INIT_IMAGE_FILE) ) {
#089 fd = open("/dev/tty0", O_WRONLY);
#090 if (fd >= 0) {
#091 const char *msg;
#092 msg = "\n"
#093 "\n"
#094 "\n"
#095 "\n"
#096 "\n"
#097 "\n"
#098 "\n" // console is 40 cols x 30 lines
#099 "\n"
#100 "\n"
#101 "\n"
#102 "\n"
#103 "\n"
#104 "\n"
#105 "\n"
#106 " A N D R O I D ";
#107 write(fd, msg, strlen(msg));
#108 close(fd);
#109 }
#110 }
这段代码是先调用load_565rle_image函数来尝试加载定制的显示的LOGO图片,如果不成功,就直接在屏幕上显示字符串android。通过这里可以定制不同厂家的LOGO图片显示,以便在系统初始化时,进行更人性化的等待,更加漂亮个性。
#111
#112 if (qemu[0])
#113 import_kernel_cmdline(1);
这段代码是用来判断是否使用模拟器运行,如果时,就加载内核命令行参数。

#114
#115 if (!strcmp(bootmode,"factory"))
#116 property_set("ro.factorytest", "1");
#117 else if (!strcmp(bootmode,"factory2"))
#118 property_set("ro.factorytest", "2");
#119 else
#120 property_set("ro.factorytest", "0");
这段代码是根据内核命令行参数来设置工厂模式测试,比如在工厂生产手机过程里需要自动化演示功能,就可以根据这个标志来进行特别处理。


#121
#122 property_set("ro.serialno", serialno[0] ? serialno : "");
这段代码是设置手机序列号到属性里保存,以便上层应用程序可以识别这台手机。

#123 property_set("ro.bootmode", bootmode[0] ? bootmode : "unknown");
这段代码是保存启动模式到属性里。


#124 property_set("ro.baseban

d", baseband[0] ? baseband : "unknown");
这段代码是保存手机基带频率到属性里。

#125 property_set("ro.carrier", carrier[0] ? carrier : "unknown");
这段代码是保存手机硬件载波的方式到属性里。

#126 property_set("ro.bootloader", bootloader[0] ? bootloader : "unknown");
这里是保存引导程序的版本号到属性里,以便系统知道引导程序有什么特性。

#127
#128 property_set("ro.hardware", hardware);
这里是保存硬件信息到属性里,其实就是获取CPU的信息。

#129 snprintf(tmp, PROP_VALUE_MAX, "%d", revision);
#130 property_set("ro.revision", tmp);
这里是保存硬件修订的版本号到属性里,这样可以方便应用程序区分不同的硬件版本。

#131
#132 /* execute all the boot actions to get us started */
#133 action_for_each_trigger("init", action_add_queue_tail);
#134 drain_action_queue();
这段代码是先把所有init命令添加队列,然后再执行。

#135
#136 /* read any property files on system or data and
#137 * fire up the property service. This must happen
#138 * after the ro.foo properties are set above so
#139 * that /data/local.prop cannot interfere with them.
#140 */
#141 property_set_fd = start_property_service();
这段代码是加载system和data目录下的属性,并启动属性监听服务。


#142
#143 /* create a signalling mechanism for the sigchld handler */
#144 if (socketpair(AF_UNIX, SOCK_STREAM, 0, s) == 0) {
#145 signal_fd = s[0];
#146 signal_recv_fd = s[1];
#147 fcntl(s[0], F_SETFD, FD_CLOEXEC);
#148 fcntl(s[0], F_SETFL, O_NONBLOCK);
#149 fcntl(s[1], F_SETFD, FD_CLOEXEC);
#150 fcntl(s[1], F_SETFL, O_NONBLOCK);
#151 }
这段代码是创建一个全双工的通讯机制的两个SOCKET,信号可以在signal_fd和signal_recv_fd双向通讯,从而建立起沟通的管道。其实这个信号管理,就是用来让init进程与它的子进程进行沟通的,子进程从signal_fd写入信息,init进程从signal_recv_fd收到信息,然后再做处理。


#152
#153 /* make sure we actually have all the pieces we need */
#154 if ((device_fd < 0) ||
#155 (property_set_fd < 0) ||
#156 (signal_recv_fd < 0)) {
#157 ERROR("init startup failure\n");
#158 return 1;
#159 }
这段代码是判断关键的几个组件是否成功初始化,主要就是设备文件系统是否成功初始化,属性服务是否成功初始化,信号通讯机制是否成功初始化。


#160
#161 /* execute all the boot actions to get us started */
#162 action_for_each_trigger("early-boot", action_add_queue_tail);
#163 action_for_each_trigger("boot", action_add_queue_tail);
#164 drain_action_queue();
这段代码是执行early-bo

ot和boot属性的命令。


#165
#166 /* run all property triggers based on current state of the properties */
#167 queue_all_property_triggers();
#168 drain_action_queue();
这段代码是根据当前属性,运行属性命令。

#169
#170 /* enable property triggers */
#171 property_triggers_enabled = 1;
这段代码是标明属性触发器已经初始化完成。

#172
#173 ufds[0].fd = device_fd;
#174 ufds[0].events = POLLIN;
#175 ufds[1].fd = property_set_fd;
#176 ufds[1].events = POLLIN;
#177 ufds[2].fd = signal_recv_fd;
#178 ufds[2].events = POLLIN;
#179 fd_count = 3;
这段代码是保存三个重要的服务socket,以便后面轮询使用。

#180
#181 if (keychord_fd > 0) {
#182 ufds[3].fd = keychord_fd;
#183 ufds[3].events = POLLIN;
#184 fd_count++;
#185 } else {
#186 ufds[3].events = 0;
#187 ufds[3].revents = 0;
#188 }
这段代码是判断是否处理组合键轮询。


#189
#190 #if BOOTCHART
#191 bootchart_count = bootchart_init();
#192 if (bootchart_count < 0) {
#193 ERROR("bootcharting init failure\n");
#194 } else if (bootchart_count > 0) {
#195 NOTICE("bootcharting started (period=%d ms)\n", bootchart_count*BOOTCHART_POLLING_MS);
#196 } else {
#197 NOTICE("bootcharting ignored\n");
#198 }
#199 #endif
这段代码是初始化linux程序启动速度的性能分析工具,这个工具有一个好处,就是图形化显示每个进程启动顺序和占用时间,如果想优化系统的启动速度,记得启用这个工具。
#200
#201 for(;;) {
#202 int nr, i, timeout = -1;
这段代码是进入死循环处理,以便这个init进程变成一个服务。

#203
#204 for (i = 0; i < fd_count; i++)
#205 ufds[i].revents = 0;
这段代码是清空每个socket的事件计数。

#206
#207 drain_action_queue();
这段代码是执行队列里的命令。

#208 restart_processes();
这句代码是用来判断那些服务需要重新启动。

#209
#210 if (process_needs_restart) {
#211 timeout = (process_needs_restart - gettime()) * 1000;
#212 if (timeout < 0)
#213 timeout = 0;
#214 }
这段代码是用来判断那些进程启动超时。


#215
#216 #if BOOTCHART
#217 if (bootchart_count > 0) {
#218 if (timeout < 0 || timeout > BOOTCHART_POLLING_MS)
#219 timeout = BOOTCHART_POLLING_MS;
#220 if (bootchart_step() < 0 || --bootchart_count == 0) {
#221 bootchart_finish();
#222 bootchart_count = 0;
#223 }
#224 }
#225 #endif
这段代码是用来计算运行性能。

#226 nr = poll(ufds, fd_count, timeout);
#22

7 if (nr <= 0)
#228 continue;
这段代码用来轮询几个socket是否有事件处理。

#229
#230 if (ufds[2].revents == POLLIN) {
#231 /* we got a SIGCHLD - reap and restart as needed */
#232 read(signal_recv_fd, tmp, sizeof(tmp));
#233 while (!wait_for_one_process(0))
#234 ;
#235 continue;
#236 }
这段代码是用来处理子进程的通讯,并且能删除任何已经退出或者杀死死进程,这样做可以保持系统更加健壮性,增强容错能力。


#237
#238 if (ufds[0].revents == POLLIN)
#239 handle_device_fd(device_fd);
这段代码是处理设备事件。

#240
#241 if (ufds[1].revents == POLLIN)
#242 handle_property_set_fd(property_set_fd);
这段代码是处理属性服务事件。

#243 if (ufds[3].revents == POLLIN)
#244 handle_keychord(keychord_fd);
这段代码是处理调试模式下的组合按键。

#245 }
#246
#247 return 0;
#248 }
#249

到这里已经分析完成init进程的主流程,后面再来详细地其它功能实现。
在主函数main里调用这个函数来做什么呢?而这个函数是怎么样实现的呢?下面就来了解这个函数的功能与产现,具代码如下:
#001 void open_devnull_stdio(void)
#002 {
#003 int fd;
#004 static const char *name = "/dev/__null__";
这段代码是指明创建设备节点的名称,这是空设备。

#005 if (mknod(name, S_IFCHR | 0600, (1 << 8) | 3) == 0) {
这行是调用函数mknod来创建设备节点/dev/__null__。空节点当作输出的黑洞,只进不出,写入它的字符永远不会满。

#006 fd = open(name, O_RDWR);
这行代码是打开空设备,进行读写。

#007 unlink(name);
这行代码是从文件系统中删除一个名称。如果名称是文件的最后一个连接,并且没有其它进程将文件打开,名称对应的文件会实际被删除。

#008 if (fd >= 0) {
#009 dup2(fd, 0);
#010 dup2(fd, 1);
#011 dup2(fd, 2);
#012 if (fd > 2) {
#013 close(fd);
#014 }
#015 return;
#016 }
这段代码是用来检查是否打开空设备成功,是否重定向设备成功,如果成功就返回。


#017 }
#018
#019 exit(1);
这行代码是当创建空设备节点出错时退出。

#020 }
#021

通过上面的代码,就可以创建/dev/__null__空设备,并测试是否创建成功。
parse_config_file函数是分析*.rc配置文件,并且把里面的参数组成链表的方式。下面来仔细地分析代码,如下:
#001 int parse_config_file(const char *fn)
#002 {
输入来的参数是文件名称的路径。

#003 char *data;
#004 data = read_file(f

n, 0);
#005 if (!data) return -1;
这段代码是从文件里读取数据,并保存数据缓冲区的指针在data里。

#006
#007 parse_config(fn, data);
这行代码是分析data数据里,然后把里面的参数组成链表的方式。

#008 DUMP();
这行代码是用来调试时输出配置文件里的内容。

#009 return 0;
#010 }


接着下来分析函数read_file的代码,如下:
#001 /* reads a file, making sure it is terminated with \n \0 */
#002 void *read_file(const char *fn, unsigned *_sz)
#003 {
#004 char *data;
#005 int sz;
#006 int fd;
#007
#008 data = 0;
#009 fd = open(fn, O_RDONLY);
#010 if(fd < 0) return 0;
这段代码是打开文件路径为fn的文件,使用只读的方式打开。

#011
#012 sz = lseek(fd, 0, SEEK_END);
#013 if(sz < 0) goto oops;
这段代码是移动文件指针到文件末尾,然后计算出文件的长度。

#014
#015 if(lseek(fd, 0, SEEK_SET) != 0) goto oops;
这段代码是移动到文件头开始位置。

#016
#017 data = (char*) malloc(sz + 2);
#018 if(data == 0) goto oops;
这段代码是分配文件所需内存大小。

#019
#020 if(read(fd, data, sz) != sz) goto oops;
#021 close(fd);
#022 data[sz] = '\n';
#023 data[sz+1] = 0;
这段代码是读取文件数据到缓冲区,并设置缓冲区最后的结束字符为换行符和0字符。

#024 if(_sz) *_sz = sz;
#025 return data;
这段代码是返回文件的长度和文件缓冲区的指针。

#026
#027 oops:
#028 close(fd);
#029 if(data != 0) free(data);
#030 return 0;
#031 }
这段代码是读取文件出错,删除缓冲区。




再接着来分析函数parse_config,主要实现从缓冲区里分析配置数据,生成链表。它的代码如下:
#001 static void parse_config(const char *fn, char *s)
#002 {
#003 struct parse_state state;
#004 char *args[SVC_MAXARGS];
#005 int nargs;
#006
#007 nargs = 0;
#008 state.filename = fn;
#009 state.line = 1;
#010 state.ptr = s;
#011 state.nexttoken = 0;
#012 state.parse_line = parse_line_no_op;
这段代码是设置分析开始状态,其实state.filename指向文件名称;state.line是第一行;state.ptr是指向数据缓冲区;state.nexttoken是下一个词位置;state.parse_line是指向空操作行函数。

下面开始循环适别所有配置文件。
#013 for (;;) {

下面根据三种状态进行处理。
#014 switch (next_token(&state)) {
#015 case T_EOF:

已经到了文件结尾字符处理。
#016 state.parse_line(&state, 0, 0);
#017 return;

新一行配置文件处理。
#018 case T_NEWLINE:
#019 if (nargs) {

查找这一行里配置的关键字。
#020 int kw = lookup_keyword

(args[0]);
#021 if (kw_is(kw, SECTION)) {

是关键字处理,分近这一行字符。
#022 state.parse_line(&state, 0, 0);

调用函数parse_new_section来分析这一节配置文件的意思,比如服务或者动作,或者参数等等。
#023 parse_new_section(&state, kw, nargs, args);
#024 } else {

保存一行的参数。
#025 state.parse_line(&state, nargs, args);
#026 }
#027 nargs = 0;
#028 }
#029 break;

保存命令配置的参数。
#030 case T_TEXT:
#031 if (nargs < SVC_MAXARGS) {
#032 args[nargs++] = state.text;
#033 }
#034 break;
#035 }
#036 }
#037 }
在上面函数主要识别的关键字有:
copy capability chdir chroot class class_start class_stop console chown chmod critical disabled domainname device exec export group hostname
ifup insmod import keycodes loglevel mkdir mount on oneshot onrestart restart service setenv setkey setprop setrlimit socket start stop
symlink sysclktz trigger user write。
也就是配置文件只能使用上面的关键字,其它都是作为标识符的。这些关键的作用,其实是通过预先定义的操作来决定的,如下代码所示:
#001 #define KEYWORD(symbol, flags, nargs, func) K_##symbol,
#002 enum {
#003 K_UNKNOWN,
#004 #endif
#005 KEYWORD(capability, OPTION, 0, 0)
这个关键字是用来执行linux服务之前检查linux内核的兼容性,它是一个选项。

#006 KEYWORD(chdir, COMMAND, 1, do_chdir)
这个关键字是用来改变当前工作的目录,它是一个命令。

#007 KEYWORD(chroot, COMMAND, 1, do_chroot)
这个关键字是用来更改某个进程所能看到的根目录,即将某进程限制在指定目录中,保证该进程只能对该目录及其子目录的文件有所动作,从而保证整个服务器的安全,它是一个命令。

#008 KEYWORD(class, OPTION, 0, 0)
这个关键字是为一个服务指明一个类名称,它是一个选项。

#009 KEYWORD(class_start, COMMAND, 1, do_class_start)
这个关键字是启动所有指定服务类下的未运行服务,它是一个命令。

#010 KEYWORD(class_stop, COMMAND, 1, do_class_stop)
这个关键字是停止指定服务类下的所有已运行的服务,它是一个命令。

#011 KEYWORD(console, OPTION, 0, 0)
这个关键字是控制台选项,它是一个选项。

#012 KEYWORD(critical, OPTION, 0, 0)
这个关键字是说明这是一个对于设备关键的服务。如果他四分钟内退出大于四次,系统将会重启并进入recovery(恢复)模式。

#013 KEYWORD(disabled, OPTION, 0, 0)
这个关键字是说明这个服务不会同与他同trigger(触发器)下的服务

自动启动。他必须被明确的按名启动。。

#014 KEYWORD(domainname, COMMAND, 1, do_domainname)
这个关键字是设置域名,它是一个命令。

#015 KEYWORD(exec, COMMAND, 1, do_exec)
这个关键字是创建或执行一个程序,它是一个命令。

#016 KEYWORD(export, COMMAND, 2, do_export)
这个关键字是用来设置全局环境变量的值,它是一个命令。

#017 KEYWORD(group, OPTION, 0, 0)
这个关键字是用来改服务的组名,它是一个选项。

#018 KEYWORD(hostname, COMMAND, 1, do_hostname)
这个关键字是用来主机名称,它是一个命令。

#019 KEYWORD(ifup, COMMAND, 1, do_ifup)
这个关键字是用来启动网络接口,它是一个命令。

#020 KEYWORD(insmod, COMMAND, 1, do_insmod)
这个关键字是用来加载指定路径的模块,它是一个命令。

#021 KEYWORD(import, COMMAND, 1, do_import)
这个关键字是用来加载一个init能识别的rc文件,它是一个命令。

#022 KEYWORD(keycodes, OPTION, 0, 0)
这个关键字是用来定义按键码的选项。

#023 KEYWORD(mkdir, COMMAND, 1, do_mkdir)
这个关键字是用来建立一个目录,它是一个命令。

#024 KEYWORD(mount, COMMAND, 3, do_mount)
这个关键字是用来指定目录加载设备,它是一个命令。

#025 KEYWORD(on, SECTION, 0, 0)
这个关键字是用来设置一段命令按什么事件进行触发运行,它是一个段描述符。

#026 KEYWORD(oneshot, OPTION, 0, 0)
这个关键字是用来设置服务器只运行一次就关闭,它是一个选项。

#027 KEYWORD(onrestart, OPTION, 0, 0)
这个关键字是用来设置当服务重启动,执行一个命令,它是一个选项。

#028 KEYWORD(restart, COMMAND, 1, do_restart)
这个关键字是用来重新启动服务,它是一个命令。

#029 KEYWORD(service, SECTION, 0, 0)
这个关键字是用来设置一段服务的命令,往往一段服务里需要有多个选项组成。

#030 KEYWORD(setenv, OPTION, 2, 0)
这个关键字是用来设置环境变量,它是一个选项。

#031 KEYWORD(setkey, COMMAND, 0, do_setkey)
这个关键字是用来设置按键的索引和键值,它是一个命令。

#032 KEYWORD(setprop, COMMAND, 2, do_setprop)
这个关键字是用来设置系统属性名称为某个值,它是一个命令。

#033 KEYWORD(setrlimit, COMMAND, 3, do_setrlimit)
这个关键字是用来设置系统资源限制,它是一个命令。

#034 KEYWORD(socket, OPTION, 0, 0)
这个关键字是用来设置socket给一个应用程序,它是一个选项。

#035 KEYWORD(start, COMMAND, 1, do_start)
这个关键字是用来启动一个服务,它是一个命令。

#036 KEYWORD(stop, COMMAN

D, 1, do_stop)
这个关键字是用来停止一个服务,它是一个命令。

#037 KEYWORD(trigger, COMMAND, 1, do_trigger)
这个关键字是用来标志一个触发命令,它是一个命令。

#038 KEYWORD(symlink, COMMAND, 1, do_symlink)
这个关键字是用来设置一个路径的符号连接,它是一个命令。

#039 KEYWORD(sysclktz, COMMAND, 1, do_sysclktz)
这个关键字是用来设置系统时钟基准,它是一个命令。

#040 KEYWORD(user, OPTION, 0, 0)
这个关键字是用来设置服务、文件或目录所属的用户,它是一个选项。

#041 KEYWORD(write, COMMAND, 2, do_write)
这个关键字是用来打开一个文件写多个字符串,它是一个命令。

#042 KEYWORD(copy, COMMAND, 2, do_copy)
这个关键字是用来拷贝文件,它是一个命令。

#043 KEYWORD(chown, COMMAND, 2, do_chown)
这个关键字是用来改变文件或目录的属某个用户的属性,它是一个命令。

#044 KEYWORD(chmod, COMMAND, 2, do_chmod)
这个关键字是用来改变文件或目录的访问权限,它是一个命令。

#045 KEYWORD(loglevel, COMMAND, 1, do_loglevel)
这个关键字是用来设置log输出的级别,它是一个命令。

#046 KEYWORD(device, COMMAND, 4, do_device)
这个关键字是用来设置设备的名称,它是一个命令。

#047 #ifdef __MAKE_KEYWORD_ENUM__
#048 KEYWORD_COUNT,
#049 };

Android初始化语言包含了四种类型的声明:Actions(行动)、Commands(命令)、Services(服务)和Options(选 项)。
通上面的函数就可以把服务和事件触发的命令添加队列里。其实是在文件parser.c头部,就声明了下面三个链表,如下:

static list_declare(service_list);
static list_declare(action_list);
static list_declare(action_queue);

service_list是定义添加分析所有资源文件里的服务,action_list是定义添加分析所有资源文件里的事件触发的命令列表,action_queue是定义添加当前需要执行操作的命令动作或者服务等。
那么又有下面一个问题了,init进程是在那里添加这些服务或命令到运行队列呢?在init.c文件看到下面的代码:
action_for_each_trigger("early-init", action_add_queue_tail);
drain_action_queue();

原来是通过函数action_for_each_trigger来把所有需要按事件触发运行的命令添加到队列里,函数action_add_queue_tail把需要执行命令放到队列,函数drain_action_queue把添加到队列里的命令进行运行。

#001 void action_for_each_trigger(const char *trigger,
#002 void (*func)(struct action *act))
#003 {
#004 struct listnode *node;
#005 struct action *act;
#006 list_for_each(node, &action_list) {
这行代码是遍历链表所有项。

#007 act =

node_to_item(node, struct action, alist);
这行代码是把动作节点转换为动作项。

#008 if (!strcmp(act->name, trigger)) {
#009 func(act);
这段代码是当找到与用户输入相同事件触发的动作,比如上面是查找“early-init”事件。这时就把动作项添加到将要运行的队列里。

#010 }
#011 }
#012 }
在init初始化进程里,设备初始化是怎么进行的呢?如果要了解这方面,就需要仔细分析下面的代码,如下:
#001 int device_init(void)
#002 {
#003 suseconds_t t0, t1;
#004 int fd;
#005
#006 fd = open_uevent_socket();
#007 if(fd < 0)
#008 return -1;
这段代码是调用函数open_uevent_socket来创建一个用户事件空间的socket。

#009
#010 fcntl(fd, F_SETFD, FD_CLOEXEC);
#011 fcntl(fd, F_SETFL, O_NONBLOCK);
这段代码是设置socket的属性,FD_CLOEXEC用来设置文件的close-on-exec状态标准。在exec()调用后,close-on-exec标志为0的情况,此文件不被关闭。非零则在exec()后被关闭。默认close-on-exec状态为0,需要通过FD_CLOEXEC设置。O_NONBLOCK设置这个socket是异步的通讯方式。

#012
#013 t0 = get_usecs();
这行代码是用来获取当前用户的时间。

#014 coldboot(fd, "/sys/class");
#015 coldboot(fd, "/sys/block");
#016 coldboot(fd, "/sys/devices");
这段代码是用来遍历所有在init进程前已经创建的设备,并找到设备的事件文件发送事件通知。

#017 t1 = get_usecs();
这行代码是用来获取当前用户的时间。

#018
#019 log_event_print("coldboot %ld uS\n", ((long) (t1 - t0)));
这行代码是用来打印输出冷启动的时间是多少秒。

#020
#021 return fd;
#022 }
#023
在android系统里,设计有一种系统叫做属性系统,它是用来做什么呢?这样设计有什么优势呢?其实这个属性系统主要是用来保存系统配置,或者用来交换不同进程的信息。这样的系统最大的优势是统一了系统配置的方式,统一了信息交换方式,通过共享内存的方式提高系统的性能。
下面就来分析属性系统的初始化函数,代码如下:
#001 void property_init(void)
#002 {
#003 init_property_area();
这行代码是调用函数init_property_area来设置属性内存的区域。

#004 load_properties_from_file(PROP_PATH_RAMDISK_DEFAULT);
这行代码是从ramdisk盘里加载属性文件。

#005 }

在这个函数里,需要查看一下宏定义,如下:
#define PROP_PATH_RAMDISK_DEFAULT "/default.prop"
也就是从内存盘里加载属性文件/default.prop,并把这些属性放到属性系统里。


接着来分析函数init_property_area是怎么创建共享内存,并把属性放到里面给所有进程共享使用的,代码如下:
#001 static int init_property_area(void

)
#002 {
#003 prop_area *pa;
#004
#005 if(pa_info_array)
#006 return -1;
这段代码是判断当属性信息数组已经初始化,就直接返回。

#007
#008 if(init_workspace(&pa_workspace, PA_SIZE))
#009 return -1;
这段代码是调用函数init_workspace创建共享内存。

#010
#011 fcntl(pa_workspace.fd, F_SETFD, FD_CLOEXEC);
这行代码是设置共享内存的执行结束后关闭。

#012
#013 pa_info_array = (void*) (((char*) pa_workspace.data) + PA_INFO_START);
这行代码是保存创建共享内存指针。

#014
#015 pa = pa_workspace.data;
#016 memset(pa, 0, PA_SIZE);
这段代码是清空属性共享的内存。

#017 pa->magic = PROP_AREA_MAGIC;
#018 pa->version = PROP_AREA_VERSION;
这段代码是设置属性共享内存的版本号。

#019
#020 /* plug into the lib property services */
#021 __system_property_area__ = pa;
这行代码是设置属性共享内存可以给库的属性共享服务使用。

#022
#023 return 0;
#024 }
#025

从上面的函数里可以看到一个创建共享内存的函数,它是怎么样实现创建共享内存的呢?现在来分析它的代码
前面学习了属性系统的初始化和加载,还有保存到属性文件等功能,下面来学习属性服务的方面,它主要用来提供一种服务的方式给java虚拟机上层使用,或者java应用程序使用。start_property_service函数的代码如下:
#001 int start_property_service(void)
#002 {
#003 int fd;
#004
#005 load_properties_from_file(PROP_PATH_SYSTEM_BUILD);
#006 load_properties_from_file(PROP_PATH_SYSTEM_DEFAULT);
#007 load_properties_from_file(PROP_PATH_LOCAL_OVERRIDE);
这段代码是用来加载下面几个缺省的属性文件:
/system/build.prop
/system/default.prop
/data/local.prop

#008 /* Read persistent properties after all default values have been loaded. */
#009 load_persistent_properties();
这行代码是用来加载保存为属性文件的属性。

#010
#011 fd = create_socket(PROP_SERVICE_NAME, SOCK_STREAM, 0666, 0, 0);
#012 if(fd < 0) return -1;
#013 fcntl(fd, F_SETFD, FD_CLOEXEC);
#014 fcntl(fd, F_SETFL, O_NONBLOCK);
这段代码是用来创建服务的socket,并设置相应的属性。

#015
#016 listen(fd, 8);
这行代码是让属性服务进入监听状态。

#017 return fd;
#018 }
在初始化过程里,会显示一个LOGO图片,那么它是怎么实现显示这个图片的呢?它的代码如下:
#001 /* 565RLE image format: [count(2 bytes), rle(2 bytes)] */
#002
#003 int load_565rle_image(char *fn)
#004 {
这段代码是加载565RLE格式的LOGO图片,函数的参数是图片路径。

#005 struct FB fb;
#006 struct stat s;
#007 unsigned short *data, *bits, *ptr;
#008 unsigned count, max;
#009 in

t fd;
#010
#011 if (vt_set_mode(1))
#012 return -1;
这段代码是设置显示输出为图片模式。

#013
#014 fd = open(fn, O_RDONLY);
#015 if (fd < 0) {
#016 ERROR("cannot open '%s'\n", fn);
#017 goto fail_restore_text;
#018 }
这段代码是打开要显示的图片。

#019
#020 if (fstat(fd, &s) < 0) {
#021 goto fail_close_file;
#022 }
这段代码是获取打开文件的信息,比如文件的大小。

#023
#024 data = mmap(0, s.st_size, PROT_READ, MAP_SHARED, fd, 0);
#025 if (data == MAP_FAILED)
#026 goto fail_close_file;
这段代码是映射图片文件到内存。

#027
#028 if (fb_open(&fb))
#029 goto fail_unmap_data;
这段代码是打开显示缓存内存,以便把图片数据放到显示缓存里。

#030
#031 max = fb_width(&fb) * fb_height(&fb);
#032 ptr = data;
#033 count = s.st_size;
#034 bits = fb.bits;
#035 while (count > 3) {
#036 unsigned n = ptr[0];
#037 if (n > max)
#038 break;
#039 android_memset16(bits, ptr[1], n << 1);
#040 bits += n;
#041 max -= n;
#042 ptr += 2;
#043 count -= 4;
#044 }
这段代码是把图片数据填入显示缓存。

#045
#046 munmap(data, s.st_size);
#047 fb_update(&fb);
这段代码是更新显示缓存,并把LOGO显示出来。

#048 fb_close(&fb);
#049 close(fd);
#050 unlink(fn);
这段代码关闭上面打开的资源,并删除LOGO图片文件,由于android把LOGO图片加载到内存里,并且LOGO显示完成后,再没有作用了,及时回收资源。如果想不删除这个图片,就需要把这行代码删除掉,或者每次显示前动态地拷贝到这里。

#051 return 0;
#052
#053 fail_unmap_data:
#054 munmap(data, s.st_size);
#055 fail_close_file:
#056 close(fd);
#057 fail_restore_text:
#058 vt_set_mode(0);
#059 return -1;
这段代码是处理失败情况。

#060 }
通过上面的分析,可以知道LOGO图片是RLE编码的565格式的图片,也就是采用行程编码的方式,颜色位数采用16位的方式(红色5位,绿色6位,蓝色5位)。因此,所有其它图片的格式都需要转换为这种标准的格式,初始化进程才可以显示出来。下面就来详细地介绍定制一个LOGO图片的显示过程,比如要显示的图片如下:

这个图片的像素大小为480X272,显示屏的大小为4.3寸。直接在Windows平台里使用画笔就可以制作出来,然后保存为png文件,比如保存为xiyang.png。然后在linux里打开windows共享的文件夹,再把这个文件拷贝到linux的目录里,进行如下操作:
1. 下载imgageMagick工具,通过命令进行:sudo apt-get install imagemagick,这样就安装好图片文件转换工具,它

是用来把png文件转换为rgb原始格式的文件。
2. 把png格式的logo图片转换为raw原始格式图片:
convert -depth 8 xiyang.png rgb: xiyang.raw
3. 把raw原始格式图片转换为rle格式图片:
Android-2.0/out/host/linux-x86/bin/rgb2565 –rle < xiyang.raw > initlogo.rle
当转换成功时,就会输出多少个像素的提示。
把文件initlogo.rle拷贝到nfsboot的目录。
其中Android.mk是工程管理文件,to565.c是源程序,就是实现原始图片生成565格式,或者从565格式的图片生成原始格式。那么什么样的格式叫做原始格式呢?什么样的格式叫做565格式呢?为什么需要使用565格式呢?在我们表达图片的颜色空间里,主要使用红、绿、蓝三种颜色,在数字化后,每种颜色采用8位表达,那么每个点占用的大小,就是24位,这样的方式保存到文件里,就是原始格式,因为没有任何的变换。不过采用这种格式时,文件会比较大,特别在嵌入系统里空间特别有限,因此需要转换为一种在颜色和空间大小占用都比较合理的方式。而这种方式,就是565的格式,当然这种方式是以损失颜色精度为条件的。在565格式里,就是红色占5位,绿色占6位,蓝色占5位,刚好16位,采用两个字节就可以表达了,这样比原来每个点少占用一个字节。
接着来分析这个程序的代码,先从main函数开始:
int main(int argc, char **argv)
{
if ((argc == 2) && (!strcmp(argv[1],"-rle"))) {
在这里判断输入的命令行参数是否等于三个,一个命令行参数-rle,这个参数是表示使用游程编码压缩。后面两个参数,一个是表示文件输入,一个是表示文件输出。

to_565_rle();
在这里调用函数 to_565_rle把原始格式转换为565的格式,并且使用游程编码压缩后,再输出到文件里。

} else {
if (argc > 2 && (!strcmp(argv[1], "-w"))) {
如果超过三个参数,并且有参数为-w,那么就执行下面的代码。

to_565_raw_dither(atoi(argv[2]));
这里调用函数 to_565_raw_dither实现带抖动算法的565编码格式输出。

} else {
to_565_raw();
这里调用函数to_565_raw实现原始的565格式输出。
}
}
return 0;
}
接着来分析函数 to_565_rle,这个函数主要实现从24位颜色变换为565的16位颜色表示,并且进行行程压缩编码,代码如下:
void to_565_rle(void)
{
unsigned char in[3];
unsigned short last, color, count;
unsigned total = 0;
count = 0;
while(read(0, in, 3) == 3) {
从标准输入的文件,每次读取三个字节,如果读取不够三个字节,就退出处理。
color = to565(in[0],in[1],in[2]);
每三个字节作为一个像素点,然后通过宏 to565变换为565的16位表示。
下面这段代码进行游程编码,并写到标

准输出文件里。
if (count) {
if ((color == last) && (count != 65535)) {
count++;
continue;
} else {
write(1, &count, 2);
write(1, &last, 2);
total += count;
}
}
last = color;
count = 1;
}
下面这段代码写入最后一个像素的数据。
if (count) {
write(1, &count, 2);
write(1, &last, 2);
total += count;
}
这里提示总共处理多少个像素数。
fprintf(stderr,"%d pixels\n",total);
}
初看这段代码时,感觉非常奇怪,没有看到任何打开文件的函数,也没有任何关闭文件的函数,就可以直接操作读取和写入。难道文件是可以自动打开的吗?从linux系统调用API里知道是不可能啊!这时,反复查看输入的命令行,如下:
rgb2565 –rle initlogo.rle
从这个命令行里,看到一些眉目了吧?两个文件为什么要添加像个括号的东西呢?是啊,这两个括号是有特别用途的,对于我刚从WINDOWS转过的程序员来说,是比较奇怪,后来仔细地查看shell调用,详细介绍如下:
标准输入
标准输入是文件描述符0.它是命令的输入,缺省是键盘,也可以是文件或其它的命令输出
标准输出
标准输出是文件描述符1.它是命令的输出,缺省是屏幕,也可以是文件
标准错误
标准错误是文件件描述符2。它是命令错误码率的输出,缺省是屏幕,同样也可以是文件.
重定向操作符描述
> 将命令输出写入到文件或设备(如打印机),而不是命令提示符窗口或句柄。
< 从文件而不是从键盘或句柄读入命令输入。
>> 将命令输出添加到文件末尾而不删除文件中已有的信息。
>& 将一个句柄的输出写入到另一个句柄的输入中。
<& 从一个句柄读取输入并将其写入到另一个句柄输出中。
| 从一个命令中读取输出并将其写入另一个命令的输入中。也称作管道。
常用文件重定向命令
Shell代码
1.
command >filename 把标准输出重定向到一个新文件中
2.
command >>filename 把标准输出重定向到一个文件中(追加)
3.
command 1>fielname 把标准输出重定向到一个文件中
4.
command >filename 2>&1 把标准输出和标准错误一起重定向到一个文件中
5.
command 2 > filename 把标准错误重定向到一个文件中
6.
command 2 >> filename 把标准输出重定向到一个文件中(追加)
7.
command >> filename 2>&1 把标准输出和标准错误一起重定向到一个文件中(追加)
8.
command < filename >filename2 把command命令以filename文件作为标准输入,以filename文件作为标准输出
9.
command < filename 把command命令以filename文件作为标准输入
10.
command << delimiter 把从标准输

入中读入,直至遇到delimiter分界符
11.
command <&m 把把文件描述符m作为标准输入
12.
command >&m 把把标准输出重定向到文件描述符m中
从上面详细说明中,就可以知道这个命令行特别地方了。第二个参数第三个参数>initlogo.rle,就是作为标准输出文件打开了。

通过这段程序,可以了解写程序的人对linux很熟悉,并且利用到了极致,连文件的打开和关闭的事情都省了,极其高效。

接着分析下面这段代码:
void to_565_raw(void)
{
unsigned char in[3];
unsigned short out;
由于这个函数不做任何压缩等处理,只是把24位颜色变换为16位,因此很简单的一个循环就行了,读取数据写到文件里:
while(read(0, in, 3) == 3) {
out = to565(in[0],in[1],in[2]);
write(1, &out, 2);
}
return;
}
接着来分析带抖动转换的565算法:
void to_565_raw_dither(int width)
{
unsigned char in[3];
unsigned short out;
int i = 0;
int e;
创建两个点的误差保存数组。
int* error = malloc((width+2) * 3 * sizeof(int));
int* next_error = malloc((width+2) * 3 * sizeof(int));
清空误差数组。
memset(error, 0, (width+2) * 3 * sizeof(int));
memset(next_error, 0, (width+2) * 3 * sizeof(int));
计算数组是使用三个点误差来计算,所以从-3开始。
error += 3; // array goes from [-3..((width+1)*3+2)]
next_error += 3;
读取原文件里的数据。
while(read(0, in, 3) == 3) {
计算当前点的值,当然这里已经使用以前点值进行扩散。
int r = in[0] + error[i*3+0];
int rb = (r < 0) ? 0 : ((r > 255) ? 255 : r);
int g = in[1] + error[i*3+1];
int gb = (g < 0) ? 0 : ((g > 255) ? 255 : g);
int b = in[2] + error[i*3+2];
int bb = (b < 0) ? 0 : ((b > 255) ? 255 : b);
out = to565(rb, gb, bb);
write(1, &out, 2);
计算误差值,以便下一个点值进行颜色调整。
#define apply_error(ch) { \
next_error[(i-1)*3+ch] += e * 3 / 16; \
next_error[(i)*3+ch] += e * 5 / 16; \
next_error[(i+1)*3+ch] += e * 1 / 16; \
error[(i+1)*3+ch] += e - ((e*1/16) + (e*3/16) + (e*5/16)); \
}
当前计算出来的颜色,与565里恢复出来的误差值。
e = r - from565_r(out);
apply_error(0);
e = g - from565_g(out);
apply_error(1);
e = b - from565_b(out);
apply_error(2);
#undef apply_error
++i;
到最后,清空误差值。
if (i == width) {
// error <- next_error; next_error <- 0
int* temp = error; error = next_error; next_error = temp;
memset(next_error, 0, (width+1) * 3 * sizeof(int));
i = 0;
}
}
删除分配的空间。
free(error-3);
free(next_error-3);
return;
}
到这里就把这个简短的代码分析完成了,别看这么一段程序,它其实还是包括很多技术在里面的,比

如shell的了解、565颜色编码、游程编码、色差抖动算法。因此,要写出上面这个程序,要懂得比较的广博的知识,谁说知识无用呢?
在init.rc文件里第一个初始化的服务是sh服务,如下:
## Daemon processes to be run by init.
##
service console /system/bin/sh
console

sh服务是控制台服务,其实它是从NetBSD移植过来的,因此它的命令也是比较有限的,不过作为嵌入式系统,使用shell的机会不多。
sh服务的代码在目录:Android-2.0/system/core/sh
sh服务使用flex工具生成词法分析代码,使用bison生成语法分析代码。

下面来分析这个服务主要代码,先从main函数开始,如下:
int
main(int argc, char **argv)
{
struct jmploc jmploc;
struct stackmark smark;
volatile int state;
char *shinit;

下面这行代码,就是我为了加入调试使用的。
/* caijs add debug */
out2str("caijs add debug\n");

#if PROFILE
monitor(4, etext, profile_buf, sizeof profile_buf, 50);
#endif
state = 0;

下面这段代码设置执行命令异常的处理。
if (setjmp(jmploc.loc)) {
/*
* When a shell procedure is executed, we raise the
* exception EXSHELLPROC to clean up before executing
* the shell procedure.
*/
switch (exception) {
case EXSHELLPROC:
rootpid = getpid();
rootshell = 1;
minusc = NULL;
state = 3;
break;

case EXEXEC:
exitstatus = exerrno;
break;

case EXERROR:
exitstatus = 2;
break;

default:
break;
}

if (exception != EXSHELLPROC) {
if (state == 0 || iflag == 0 || ! rootshell)
exitshell(exitstatus);
}
reset();
if (exception == EXINT
#if ATTY
&& (! attyset() || equal(termval(), "emacs"))
#endif
) {
out2c('\n');
flushout(&errout);
}
popstackmark(&smark);
FORCEINTON; /* enable interrupts */
if (state == 1)
goto state1;
else if (state == 2)
goto state2;
else if (state == 3)
goto state3;
else
goto state4;
}
handler = &jmploc;
#ifdef DEBUG
#if DEBUG == 2
debug = 1;
#endif
opentrace();
trputs("Shell args: "); trargs(argv);
#endif


rootpid = getpid();
这行代码通过调用函数getpid 获取进程标识。

rootshell = 1;
init();
初始化内部支持的命令。

setstackmark(&smark);


procargs(argc, argv);
这行代码根据命令行进行处理。


下面代码读取配置参数。
if (argv[0] && argv[0][0] == '-') {
state = 1;
read_profile("/etc/profile");
state1:
state = 2;
read_profile(".profile");
}
state2:
state = 3;
if (getuid() == geteuid() && getgid() == getegid()) {
if ((shinit = lookupvar("ENV")) != NULL && *shinit != '\0') {
state = 3;
read_profile(shinit);
}
}
state3:
state = 4;
if (sflag == 0 || minusc) {
static int sigs[] = {
SIGINT, SIGQUIT, SIGHUP,
#ifdef SIGTSTP
SIGTSTP,
#endif
SIGPIPE
};
#define SIGSSIZE (sizeof(sigs)/sizeof(sigs[0]))
int i;

for (i = 0; i < SIGSSIZE; i++)
setsignal(sigs[i], 0);
}

if (minusc)
evalstring(minusc, 0);

下面的代码调用函数cmdloop 来进入shell的命令处理。
if (sflag || minusc == NULL) {
state4: /* XXX ??? - why isn't this before the "if" statement */
cmdloop(1);
}
#if PROFILE
monitor(0);
#endif

最后退出shell的执行。
exitshell(exitstatus);
/* NOTREACHED */
}
在init.rc文件里,可以看到下面的服务加载并运行:
# adbd is controlled by the persist.service.adb.enable system property
service adbd /sbin/adbd
disabled

adbd服务的代码在目录:Android-2.0/system/core/adb
adbd服务使用c语言实现,它不但可以在虚拟机里运行,也可以在实际的设备里运行。adbd服务是adb调试系统中的一部分,整个adb调试系统包括有三部分:手机运行的adbd服务、PC运行的服务器、PC运行的客户端。当android启动时,就运行adbd服务,创建一个调试端口,这样就可以让开发机器上的服务器连接过来,通过这个连接就可以发送调试信息给服务器,也可以接收到外面发送过来的调试命令。

先来分析编译文件Android.mk,adbd相关内容如下:
# adbd device daemon
# =========================================================

# build adbd in all non-simulator builds
BUILD_ADBD := false
当设置为BUILD_ADBD为true时,就是编译运行在模拟器里的调试服务,否则就是运行到真实机器里的调试服务。

ifneq ($(TARGET_SIMULATOR),true)
BUILD_ADBD := true
endif


如果运行在linux里模拟器,就需要使用下面的判断。
# build adbd for the Linux simulator build
# so we can use it to test the adb USB gadget driver on x86
#ifeq ($(HOST_OS),linux)
# BUILD_ADBD := true
#endif


ifeq ($(BUILD_ADBD),true)
include $(CLEAR_VARS)

LOCAL_SRC_FILES := \
adb.c \
fdevent.c \
transport.c \
transport_local.c \
transport_usb.c \
sockets.c \
services.c \
file_sync_service.c \
jdwp_service.c \
framebuffer_service.c \
remount_service.c \
usb_linux_client.c \
log_service.c \
utils.c

LOCAL_CFLAGS := -O2 -g -DADB_HOST=0 -Wall -Wno-unused-parameter
LOCAL_CFLAGS += -D_XOPEN_SOURCE -D_GNU_SOURCE

# TODO: This should probably be board specific, whether or not the kernel has
# the gadget driver; rather than relying on the architecture type.
ifeq ($(TARGET_ARCH),arm)
LOCAL_CFLAGS += -DANDROID_GADGET=1
endif

LOCAL_MODULE := adbd

LOCAL_FORCE_STATIC_EXECUTABLE := true
LOCAL_MODULE_PATH := $(TARGET_ROOT_OUT_SBIN)
LOCAL_UNSTRIPPED_PATH := $(TARGET_ROOT_OUT_SBIN_UNSTRIPPED)

如果在模拟器里运行,就需要编译为线程模式,否则就需要连接静态库的方式。
ifeq ($(TARGET_SIMULATOR),true)
LOCAL_STATIC_LIBRARIES := libcutils
LOCAL_LDLIBS += -lpthread
include $(BUILD_HOST_EXECUTABLE)
else
LOCAL_STATIC_LIBRARIES := libcutils libc
include $(BUILD_EXECUTABLE)
endif

接着来分析程序入口函数main处理过程,它的代码如下:
int main(int argc, char **argv)
{


adb_trace_init();
这行代码主要获取环境变量,用来判断到底输出什么样信息。

下面这段代码根据编译选项来决定编译为PC机里的服务,还是设备运行的服务。
#if ADB_HOST
adb_sysdeps_init();
return adb_commandline(argc - 1, argv + 1);
这里是选择编译为PC里的服务器。

#else

这段是选择为设备里的服务进程。
if((argc > 1) && (!strcmp(argv[1],"recovery"))) {
adb_device_banner = "recovery";
recovery_mode = 1;
}
上面这段用来判断是否恢复模式。


start_device_log();
这行代码开始输出调试LOG。

return adb_main(0);
这行代码是进入adb服务进程处理。

#endif
}
在init.rc文件里,可以看到加载下面的服务:
service servicemanager /system/bin/servicemanager
user system
critical
onrestart restart zygote
onrestart restart media

servicemanager服务的代码在目录:
Android-2.0/frameworks/base/cmds/servicemanager
servicemanager服务的作用主要是服务管理,所谓的服务管理其实就是获取服务、检查服务、添加服务、枚举所有服务。服务管理器是一个容器管理器,方便服务添加、调用和删除。在应用层的程序,都向这个服务管理器获取需要使用的服务,而所有提供服务的程序,都向这个服务器管理器注册自己的服务。服务管理器是应用程序与服务沟通的桥梁。

下面来分析一下main函数的代码如下:
int main(int argc, char **argv)
{
struct binder_state *bs;
void *svcmgr = BINDER_SERVICE_MANAGER;
这行代码是设置管理器从0开始。
bs = binder_open(128*1024);
这行代码是调用binder_open函数打开binder设备,并分配内存空间。
if (binder_become_context_manager(bs)) {
LOGE("cannot become context manager (%s)\n", strerror(errno));
return -1;
}
调用binder_become_context_manager函数设置本服务作为服务管理器。
svcmgr_handle = svcmgr;
这行代码是设置服务管理器处理函数。
binder_loop(bs, svcmgr_handler);
这行代码是让服务管理器进入循环地处理服务相关的命令。
return 0;
}
在init.rc文件里,可以看到加载下面的服务:
service vold /system/bin/vold
socket vold stream 0660 root mount
vold服务的代码在目录:
Android-2.0/system/core/vold
vold服务的作用主要是负责完成系统的动态卷管理,比如CDROM、U盘、MMC卡等外存储的管理。当有这外存储设备插入时,就需要监视这种变化,并加载相应的驱动程序,然后报告给系统和应用程序有新存储设备可以使用。
Vold处理过程大致分为三步:
1.创建链接:
在vold作为一个守护进程,一方面接受驱动的信息,并把信息传给应用层;另一方面接受上层的命令并完成相应。所以这里的链接一共有两条:
(1)vold socket: 负责vold与应用层的信息传递;
(2)访问

相关文档
最新文档