现在的位置: 首页 > 综合 > 正文

Android之init.c简析

2013年10月04日 ⁄ 综合 ⁄ 共 10916字 ⁄ 字号 评论关闭

在Android系统启动时,内核引导参数上一般都会设置“init=/init”,这样的话,如果内核成功挂载了这个文件系统之后,首先运行的就是这个根目录下的init程序。这个程序所了什么呢?我们只有RFSC(Read the Fucking Source code)!!
init程序源码在Android官方源码的system/core/init中,main在init.c里。我们的分析就从main开始
init:(1)init是一个守护进程,为了防止init的子进程成为僵尸进程(zombie process),需要init在子进程在结束时获取子进程的结束码,通过结束码将程序表中的子进程移除,防止成为僵尸进程的子进程占用程序表的空间,当程序表的空间达到上限时,则系统就不能再启动新的进程了,那么就会引起很严重的系统问题。
    在linux当中,父程序是通过捕捉SIGCHLD信号来得知子进程结束的情况的;由于系统默认在子进程暂停时也会发送信号SIGCHLD,init需要忽略子进程在暂停时发出的SIGCHLD信号,因此将act.sa_flags 置为SA_NOCLDSTOP,该标志位的含义是就是要求系统在子进程暂停时不发送SIGCHLD信号。
static void sigchld_handler(ints)
{
    write(signal_fd, &s, 1);
}
 
int main(int argc, char **argv)
{
act.sa_handler= sigchld_handler;
act.sa_flags= SA_NOCLDSTOP;
sigaction(SIGCHLD,&act, 0);
structsigaction act;
act.sa_handler= sigchld_handler;
act.sa_flags= SA_NOCLDSTOP;
       ………………………………………..
}
 
Linux进程通过互相发送接收消息来实现进程间的通信,这些消息被称为“信号”。每个进程在处理其他进程发送的信号时都需要注册程序,此程序被称为信号处理。当进程的运行状态改变或者终止时,就会产生某种信号,init进程是所有进程的父进程,当其子进程终止产生SIGCHLD信号时,init进程需要调用信号安装函数sigaction(),并通过参数传递至sigcation结构体中,已完成信号处理器的安装。
  Init进程通过上述代码注册与子进程相关的SIGCHLD信号处理器,并把sigcation结构体的sa_flags设置为SA_NOCLDSTOP,该值表示仅当进程终止时才接收SIGCHLD信号。
sigchld_handler函数用于通知全局变量signal_fd,SIGCHLD信号已发生。对于产生的信号的实际处理,在init进程的事件处理循环中进行。
(2)对umask进行清零。
    何为umask,请看http://www.szstudy.cn/showArticle/53978.shtml
 
umask是什么?
当我们登录系统之后创建一个文件总是有一个默认权限的,那么这个权限是怎么来的呢?这就是umask干的事情。umask设置了用户创建文件的默认权限,它与chmod的效果刚好相反,umask设置的是权限“补码”,而chmod设置的是文件权限码。一般在/etc/profile、$ [HOME]/.bash_profile或$[HOME]/.profile中设置umask值。
如何计算umask值?
umask命令允许你设定文件创建时的缺省模式,对应每一类用户(文件属主、同组用户、其他用户)存在一个相应的umask值中的数字。对于文件来说,这一数字的最大值分别是6。系统不允许你在创建一个文本文件时就赋予它执行权限,必须在创建后用chmod命令增加这一权限。目录则允许设置执行权限,这样针对目录来说,umask中各个数字最大可以到7。
该命令的一般形式为:umasknnn
其中nnn为umask置000 - 777。
如:umask值为022,则默认目录权限为755,默认文件权限为644。
(3)为rootfs建立必要的文件夹,并挂载适当的分区。
    /dev (tmpfs)
     /dev/pts (devpts)
     /dev/socket
    /proc (proc)
    /sys  (sysfs)
 
编译Android系统源码时,在生成的根文件系统中,不存在/dev,/proc/,/sys这类目录,他们是系统运行时的目录,有init进程在运行中生成,当系统终止时,他们就会消失。
  Init进程执行后,生成/dev目录,包含系统使用的设备,而后调用open_devnull_stdio();函数,创建运行日志输出设备。open_devnull_stdio()函数会在/dev目录下生成__null__设备节点文件,并将标准输入,标准输出,标准错误,标准错误输出全部重定向__null__设备中。
 
void open_devnull_stdio(void)
{
    intfd;
   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);
}
(4)创建/dev/null和/dev/kmsg节点。
init进程通过log_init函数,生成"/dev/__kmsg__"设备节点文件。__kmsg__设备调用内核信息输出函数printk(),init进程即是通过该函数输出log信息。
 
void log_init(void)
{
    staticconst char *name = "/dev/__kmsg__";
    if(mknod(name, S_IFCHR | 0600, (1 << 8) | 11) == 0) {
       log_fd = open(name, O_WRONLY);
       fcntl(log_fd, F_SETFD, FD_CLOEXEC);
       unlink(name);
    }
}
 
Init进程通过__kmsg__设备定义用于输出信息的宏。关于宏输出信息,可以使用dmesg实用程序进行确认,dmesg用于显示内核信息。
#define ERROR(x...)   log_write(3, "<3>init: " x)
#define NOTICE(x...)  log_write(5, "<5>init: " x)
#define INFO(x...)    log_write(6, "<6>init: " x)
(5)解析/init.rc,将所有服务和操作信息加入链表
parse_config_file("/init.rc");
parse_config_file()函数用来分析*.rc配置文件,用来指定init.rc文件的路径。执行parse_config_file函数,读取并分析init.rc文件后,生成服务列表与动作列表。动作列表与服务列表全部会以链表的形式注册到service_list和action_list中,service_list和action_list是init进程中声明的全局结构体。
  
(6) 初始化qemu设备,设置模拟器环境;从/proc/cmdline中提取信息内核启动参数,并保存到全局变量。
qemu_init();
QEMU模拟器允许Android应用开发者在缺少Android实际设备的情况下运行处于开发中的应用程序。QEMU是面向PC的开源模拟器,能够模拟具有特定处理器设备,此外还提供虚拟网络,视频设备等。Android模拟器是虚拟的硬件平台,运行在模拟器的软件可以执行ARM命令集,LCD,相机,SD卡控制器等硬件设备都可以运行在Goldfish这种虚拟平台上。
import_kernel_cmdline(0);
staticvoid import_kernel_cmdline(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, ithappens */
        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;
        import_kernel_nv(ptr, in_qemu);
        ptr = x;
    }
(7)先从上一步获得的全局变量中获取信息硬件信息和版本号,如果没有则从/proc/cpuinfo中提取,并保存到全局变量。
(8)根据硬件信息选择一个/init.(硬件).rc,并解析,将服务和操作信息加入链表。
         在G1的ramdisk根目录下有两个/init.(硬件).rc:init.goldfish.rc和init.trout.rc,init程序会根据上一步获得的硬件信息选择一个解析。
(9)执行链表中带有“early-init”触发的的命令。                                    action_for_each_trigger("early-init",action_add_queue_tail);触发在init脚本文件中名字为early-init的action,并且执行其commands,其实是:on early-init,在我们的init.rc中是没有的。action_for_each_trigger函数会将第一个参数中的命令保存到action_add_queue_tail,而后通过drain_action_queue()函数将运行队列中的命令逐一取出执行。
(10)遍历/sys文件夹, 将这些目录下的uevent文件找出,并使kernel重新生成那些在init的设备管理器开始前的设备添加事件。 初始化动态设备管理,使内核产生设备添加事件(为了自动产生设备节点), 设备文件有变化时反应给内核。           
device_fd = device_init();
 
int device_init(void)
{
   suseconds_t t0, t1;
   int fd;
 
    fd = open_uevent_socket();
   if(fd < 0)
       return -1;
 
   fcntl(fd, F_SETFD, FD_CLOEXEC);
   fcntl(fd, F_SETFL, O_NONBLOCK);
 
   t0 = get_usecs();
   coldboot(fd, "/sys/class");
   coldboot(fd, "/sys/block");
   coldboot(fd, "/sys/devices");
   t1 = get_usecs();
 
   log_event_print("coldboot %ld uS\n", ((long) (t1 - t0)));
 
   make_device("/dev/pvrsrvkm", 0, 240, 0);
#if 0
   make_device("/dev/bc_example", 0, 242, 0);
#endif
 
#if 1
   make_device("/dev/fb0", 0, 29, 0);
   make_device("/dev/fb1", 0, 29, 1);
   make_device("/dev/fb2", 0, 29, 2);
   make_device("/dev/fb3", 0, 29, 3);
   make_device("/dev/fb4", 0, 29, 4);
#endif
   return fd;
}
(11)初始化属性系统,并导入初始化属性文件。  
初始化属性服务器,Actually theproperty system is working as share memory.Logically it looks like a registry underwindows system。
首先创建一个名字为system_properties的匿名共享内存区域,对并本init进程做mmap读写映射,其余共享它 的进程只有读的权限。然后将这个prop_area结构体通过全局变量__system_property_area__传递给property services。
接着调用函数load_properties_from_file(PROP_PATH_RAMDISK_DEFAULT)从/default.prop文件中加载编译时生成的属性。这个有点像Windows 下的注册表的作用。
  在Android系统中,所有的进程共享系统设置值,为此提供了一个名称为属性的保存空间。Init进程调用property_init()函数,在共享内存区域中,创建并初始化属性域。而后通过执行中的进程锁提供的API,访问属性中的设置值。但更改属性值只能在init进程中进行。当修改属性值时,要预先向init进程提交值变更申请,然后init进程处理该申请,并修改属性值。
(12)从属性系统中得到ro.debuggable,如果ro.debuggable为1,则初始化组合键(keychord )监听  这段代码是从属性里获取调试标志,如果是可以调试,就打开组合按键输入驱动程序,初始化keychord监听。
// only listen for keychords ifro.debuggable is true
debuggable =property_get("ro.debuggable");
if (debuggable &&!strcmp(debuggable, "1")) {
keychord_fd = open_keychord();
}
(13)打開console,如果cmdline中沒有指定console則打開默認的/dev/console。  
if (console[0]) {
snprintf(tmp, sizeof(tmp),"/dev/%s", console);
console_name = strdup(tmp);
}
//打开console,如果cmdline 中没有指定console 则打开默认的/dev/console
fd = open(console_name, O_RDWR);
if (fd >= 0)
have_console = 1;
close(fd);
(14)读取/initlogo.rle,是一张565 rle 压缩的位图,如果成功则在/dev/fb0显示Logo,如果失败则将/dev/tty0设为TEXT模式并打开/dev/tty0,输出文本的ANDROID字样。
load_565rle_image(INIT_IMAGE_FILE)函数将加载由参数传递过来的图像文件,而后将该文件显示在LCD屏幕上。如果想更改logo,只需修改INIT_IMAGE_FILE即可。由于函数只支持rle565格式图像的显示,再更改图像时,注意所选图像文件的格式。
(15) 这段代码是用来判断是否使用模拟器运行,如果是,就加载内核命令行参数。
if (qemu[0])
import_kernel_cmdline(1);
 
(16)这段代码是根据内核命令行参数来设置工厂模式测试,比如在工厂生产手机过程里需要自动化演示功能,就可以根据这个标志来进行特别处理。
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");
//这段代码是设置手机序列号到属性里保存,以便上层应用程序可以识别这台手机。
property_set("ro.serialno",serialno[0] ? serialno : "");
//这段代码是保存启动模式到属性里。
property_set("ro.bootmode",bootmode[0] ? bootmode : "unknown");
//这段代码是保存手机基带频率到属性里。
property_set("ro.baseband",baseband[0] ? baseband : "unknown");
//这段代码是保存手机硬件载波的方式到属性里。
property_set("ro.carrier",carrier[0] ? carrier : "unknown");
//保存引导程序的版本号到属性里,以便系统知道引导程序有什么特性。
property_set("ro.bootloader",bootloader[0] ? bootloader : "unknown");
//这里是保存硬件信息到属性里,其实就是获取CPU 的信息。
property_set("ro.hardware",hardware);
//这里是保存硬件修订的版本号到属性里,这样可以方便应用程序区分不同的硬件版本。
snprintf(tmp,PROP_VALUE_MAX, "%d", revision);
property_set("ro.revision",tmp);
/*
 
(17)執行所有触发标识为init的action。
 /* execute all the boot actions to get usstarted */
//执行所有触发标志为init的action
action_for_each_trigger("init",action_add_queue_tail);
drain_action_queue();
(18)開始property服務
/* read any property files on system or dataand
* fire up the property service. This musthappen
* after the ro.foo properties are set aboveso
* that /data/local.prop cannot interferewith them.
*/
/*
开始property 服务,读取一些property 文件,这一动作必须在前面那些ro.foo设置后做,
以便/data/local.prop 不能干预到他们
- /system/build.prop
- /system/default.prop
- /data/local.prop
- 在读取认识的property 后读取presistentpropertie,在/data/property 中
这段代码是加载system 和data 目录下的属性,并启动属性监听服务。
(19)為sigchld handler創建信號機制。
property_set_fd = start_property_service();
(20)创建一个全双工的通讯机制的两个SOCKET
/*
这段代码是创建一个全双工的通讯机制的两个SOCKET,信号可以在signal_fd
和signal_recv_fd 双向通讯,从而建立起沟通的管道。其实这个信号管理,就
是用来让init 进程与它的子进程进行沟通的,子进程从signal_fd 写入信息,init
进程从signal_recv_fd 收到信息,然后再做处理。
*/
/* create a signalling mechanism for thesigchld handler */
if (socketpair(AF_UNIX, SOCK_STREAM, 0, s)== 0) {
signal_fd = s[0];
signal_recv_fd = s[1];
fcntl(s[0], F_SETFD, FD_CLOEXEC);
fcntl(s[0], F_SETFL, O_NONBLOCK);
fcntl(s[1], F_SETFD, FD_CLOEXEC);
fcntl(s[1], F_SETFL, O_NONBLOCK);
}
/* make sure we actually have all thepieces we need */
/*
(21)这段代码是判断关键的几个组件是否成功初始化,主要就是设备文件系统是否成功初始化,属性服务是否成功初始化,信号通讯机制是否成功初始化。
if((device_fd < 0) ||
(property_set_fd< 0) ||
(signal_recv_fd< 0)) {
ERROR("initstartup failure\n");
return1;
}
(22)執行所有触发标识为early-boot的
action  action_for_each_trigger("early-boot",action_add_queue_tail);
//执行所有触发标志为boot的action
action_for_each_trigger("boot",action_add_queue_tail);
drain_action_queue();
(23)基于當前property狀態,執行所有触发标识为property的action 
//这段代码是根据当前属性,运行属性命令。
queue_all_property_triggers();
drain_action_queue();
/* enable propertytriggers */
// 标明属性触发器已经初始化完成。
property_triggers_enabled= 1;
 
/*
这段代码是保存三个重要的服务socket,以便后面轮询使用。
*/
ufds[0].fd =device_fd;
ufds[0].events =POLLIN;
ufds[1].fd =property_set_fd;
ufds[1].events =POLLIN;
ufds[2].fd =signal_recv_fd;
ufds[2].events =POLLIN;
fd_count = 3;
 
//这段代码是判断是否处理组合键轮询。
ufds[3].events = POLLIN;
fd_count++;
} else {
ufds[3].events = 0;
ufds[3].revents = 0;
}
//如果支持BOOTCHART,则初始化BOOTCHART
#if BOOTCHART
/*
这段代码是初始化linux 程序启动速度的性能分析工具,这个工具有一个好处,就是图形化显示每个进程启动顺序和占用时间,如果想优化系统的启动速度,记得启用这个工具。
*/
bootchart_count = bootchart_init();
if (bootchart_count < 0) {
ERROR("bootcharting initfailure\n");
} else if (bootchart_count > 0) {
NOTICE("bootcharting started(period=%d ms)\n",
bootchart_count*BOOTCHART_POLLING_MS);
} else {
NOTICE("bootcharting ignored\n");
}
#endif
/*
进入主进程循环:
- 重置轮询事件的接受状态,revents 为0
- 查询action 队列,并执行。
- 重启需要重启的服务
- 轮询注册的事件
- 如果signal_recv_fd 的revents 为POLLIN,则得到一个信号,获取并处理
- 如果device_fd 的revents 为POLLIN,调用handle_device_fd
- 如果property_fd 的revents 为POLLIN,调用handle_property_set_fd
- 如果keychord_fd 的revents 为POLLIN,调用handle_keychord
这段代码是进入死循环处理,以便这个init 进程变成一个服务。
*/
for(;;) {
int nr, i, timeout = -1;
// 清空每个socket 的事件计数。
for (i = 0; i < fd_count; i++)
ufds[i].revents = 0;
// 这段代码是执行队列里的命令。
drain_action_queue();
// 这句代码是用来判断那些服务需要重新启动。
restart_processes();
// 这段代码是用来判断哪些进程启动超时。
if (process_needs_restart) {
timeout = (process_needs_restart -gettime()) * 1000;
if (timeout < 0)
timeout = 0;
}
#if BOOTCHART
//这段代码是用来计算运行性能。
if (bootchart_count > 0) {
if (timeout < 0 || timeout >BOOTCHART_POLLING_MS)
timeout = BOOTCHART_POLLING_MS;
if (bootchart_step() < 0 ||--bootchart_count == 0) {
bootchart_finish();
bootchart_count = 0;
}
}
#endif
// 这段代码用来轮询几个socket 是否有事件处理。
nr = poll(ufds, fd_count, timeout);
if (nr <= 0)
continue;
/*
这段代码是用来处理子进程的通讯,并且能删除任何已经退出或者杀死进程,这样做可以保持系统更加健壮性,增强容错能力。
*/
if (ufds[2].revents == POLLIN) {
/* we got a SIGCHLD - reap and restart asneeded */
read(signal_recv_fd, tmp, sizeof(tmp));
while (!wait_for_one_process(0))
;
continue;
}
// 这段代码是处理设备事件。
if (ufds[0].revents == POLLIN)
handle_device_fd(device_fd);
// 这段代码是处理属性服务事件。
if (ufds[1].revents == POLLIN)
handle_property_set_fd(property_set_fd);
// 这段代码是处理调试模式下的组合按键。
if (ufds[3].revents == POLLIN)
handle_keychord(keychord_fd);
}
return 0;
}

【上篇】
【下篇】

抱歉!评论已关闭.