当前位置: > 华清远见教育科技集团 > 嵌入式学习 > 讲师博文 > Android init.c简析
Android init.c简析
时间:2016-12-13作者:华清远见

在Android系统启动时,内核引导参数上一般都会设置"init=/init",这样的话,如果内核成功挂载了这个文件系统之后,首先运行的就是这个根目录下的init程序。这个程序所了什么呢? 我们只有RFSC(Read the Fucking Source code)!! init程序源码在Android官方源码的system/core/init中,main在init.c里。我们的分析就从main开始

init:(1)安装SIGCHLD信号。(如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。因此需要对SIGCHLD信号做出处理,回收僵尸进程的资源,避免造成不必要的资源浪费

static void sigchld_handler(int s)
        {
                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);
                struct sigaction 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,请看//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。

该命令的一般形式为:umask nnn
        其中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)
        {
                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;
                        }
                }

   &nnbsp;            exit(1);
        }

(4)创建/dev/null和/dev/kmsg节点。

init进程通过log_init函数,生成"/dev/__kmsg__"设备节点文件。__kmsg__设备调用内核信息输出函数printk(),init进程即是通过该函数输出log信息。

void log_init(void)
        {
                static const 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);
        static void 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, 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;
                        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;

  nbsp;              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 the property system is working as share memory.Logically it looks like a registry under windows 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 if ro.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 us started */
        //执行所有触发标志为init 的action
        action_for_each_trigger("init", action_add_queue_tail);
        drain_action_queue();

(18)開始property服務

/* read any property files on system or data and
        * fire up the property service. This must happen
        * after the ro.foo properties are set above so
        * that /data/local.prop cannot interfere with them.
        */
        /*

开始property 服务,读取一些property 文件,这一动作必须在前面那些ro.foo 设置后做,
        以便/data/local.prop 不能干预到他们
        - /system/build.prop
        - /system/default.prop
      &nnbsp; - /data/local.prop
        - 在读取认识的property 后读取presistent propertie,在/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 the sigchld 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 the pieces we need */
        /*

(21)这段代码是判断关键的几个组件是否成功初始化,主要就是设备文件系统是否成功初始化,属性服务是否成功初始化,信号通讯机制是否成功初始化。

if ((device_fd < 0) ||
        (property_set_fd < 0) ||
        (signal_recv_fd < 0)) {
        ERROR("init startup failure\n");
        return 1;
        }

(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 property triggers */
        // 标明属性触发器已经初始化完成。
        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 init failure\n");
        } else if (bootchart_count > 0) {
        NOTICE("bootcharting started (period=%d ms)\n",
        bootchart_count*BOOTCHART_POLLING_MS);
        } else {
        NOTICE("bootcharting ignored\n");
        }
        #endif
        /*

进入主进程循环:

- 查询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 as needed */
        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;
        }

发表评论
评论列表(网友评论仅供网友表达个人看法,并不表明本站同意其观点或证实其描述)