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; }将main函数分为上述4个部分,对应part1到part4,下面分别做具体说明。
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();
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么,这两者有什么关系么?
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的原理时才能明白。
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__文件,这里比较特殊,具体解释下。
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命令。
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中定义。
#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__中。
这一句用来初始化Android的属性系统,将在init之属性系统中专门介绍。
get_hardware_name(hardware, &revision)通过读取/proc/cpuinfo文件获取硬件信息,以笔者的山寨机为例,该文件内容如下。
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 : 0000000000000000get_hardware_name函数读取该文件,将Hardware字段的值填入hardware数组中,将Revision字段的值转换为16进制数字填入revision变量中。
接下来init程序调用函数process_kernel_cmdline解析内核启动参数。内核通常由bootloader(启动引导程序)加载启动,目前广泛使用的bootloader大都基于u-boot定制。内核允许bootloader启动自己时传递参数。在内核启动完毕之后,启动参数可通过/proc/cmdline查看。
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的输出为例子,该字符串共可以拆分成以下几段
qemu.gles=0 qemu=1 console=ttyS0 android.qemud=ttyS1 android.checkjni=1 ndns=1因此在import_kernel_nv将会被连续调用6次,依次传入上述字符串。函数实现如下:
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函数,继续执行
if (qemu[0]) import_kernel_cmdline(1, import_kernel_nv);当系统为模拟器时,qemu[0]其值为'1',第二次执行import_kernel_cmdline,将再次调用6次import_kernel_nv,并且for_emulator为1,因此将生成6个属性,现在来确定以下我们的分析。
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]可验证我们的分析是正确的。
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函数<part2>继续分析
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中,其实现如下
#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函数之<part2>代码分析分析完毕。
接下来<part3>代码涉及init进程核心功能:init.rc解析。这部分代码逻辑我们将在独立文章《Android init源代码分析(2)init.rc解析》中介绍。