Android系统启动——2init进程

本次系列的内容如下:

Android启动流程——1 序言、bootloader引导与Linux启动
Android系统启动——2 init进程
Android系统启动——3 init.rc解析
Android系统启动——4 zyogte进程
Android系统启动——5 zyogte进程(Java篇)
Android系统启动——6 SystemServer启动
Android系统启动——7 附录1:Android属性系统
Android系统启动——8 附录2:相关守护进程简介

本篇文章的主要内容如下:

  • 1、init进程简介
  • 2、Init.cpp的main()方法解析

一、init进程简介

通过上篇文章我们知道,Android设备启动要经过3个阶段,BootLoaderLinux Kernel和Android系统服务,一般情况下,他们都会相应的启动对动画对应。前面我们已经知道Andorid系统是如何启动的BootLoaderLinux Kernel的。

严格上讲,Android系统实际上是运行于Linux内核之上的一系列"服务进程",并不算一个完成意义上的"操作系统";而这一系列进程是维持Android设备正常工作的关键,所以它们肯定有一个"根进程",这个"根进程"衍生出了这一系列进程。这个"根进程"就是init进程。

init进程是Android系统启动的第一个进程。它通过解析init.rc脚本来构建出系统的初始形态。其他的"一系列"Android系统进程大部分也是通过"init.rc"来启动的。因为要兼容不同的开发商,所以init.rc脚本的语法很简单,并且采用的是纯文本编辑的,这样导致它可读性就会很高。

二、Init.cpp

init是Linux系统中用户空间的第一个进程(pid=1),Linux Kernel启动后,会调用/system/core/init/Init.cpp的main()方法

那我们就来看下init.cpp的main()里面的具体实现

代码在init.cpp989行

int main(int argc, char** argv) {

// ****************** 第一部分 ****************** 
// 检查启动程序的文件名

    if (!strcmp(basename(argv[0]), "ueventd")) {
        return ueventd_main(argc, argv);
    }

    if (!strcmp(basename(argv[0]), "watchdogd")) {
        return watchdogd_main(argc, argv);
    }

// ****************** 第二部分 ****************** 
// 设置文件属性为0777
    // Clear the umask.
    umask(0);

// ****************** 第三部分 ****************** 
// 设置环境变量
    add_environment("PATH", _PATH_DEFPATH);

// ****************** 第四部分 ****************** 
// 创建一些基本目录,并挂载

    //判断是否是第一次
    bool is_first_stage = (argc == 1) || (strcmp(argv[1], "--second-stage") != 0);

    // Get the basic filesystem setup we need put together in the initramdisk
    // on / and then we'll let the rc file figure out the rest
    //如果是第一次.
    if (is_first_stage) {
        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);
    }


// ****************** 第五部分 ****************** 
// 把标准输入、标准输出和标准错误重定向到空设备文件"/dev/_null_"

    // We must have some place other than / to create the device nodes for
    // kmsg and null, otherwise we won't be able to remount / read-only
    // later on. Now that tmpfs is mounted on /dev, we can actually talk
    // to the outside world.
    open_devnull_stdio();


// ****************** 第六部分 ****************** 
// 启动kernel log
    klog_init();
    klog_set_level(KLOG_NOTICE_LEVEL);

    // 输出init启动阶段的log      
    NOTICE("init%s started!\n", is_first_stage ? "" : " second stage");


// ****************** 第七部分 ****************** 
// 设置系统属性
    if (!is_first_stage) {
        // Indicate that booting is in progress to background fw loaders, etc.
// 7.1 创建初始化标志
        close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));

//7.2 初始化Android的属性系统
        property_init();

        // If arguments are passed both on the command line and in DT,
        // properties set in DT always have priority over the command-line ones.
//7.3  解析DT和命令行中的kernel启动参数
        process_kernel_dt();
        process_kernel_cmdline();

        // Propogate the kernel variables to internal variables
        // used by init as well as the current required properties.
//7.4  设置系统属性
        export_kernel_boot_props();
    }

// ****************** 第八部分 ****************** 

    // Set up SELinux, including loading the SELinux policy if we're in the kernel domain.
    // 调用selinux_initialize函数启动SELinux
    selinux_initialize(is_first_stage);

    // If we're in the kernel domain, re-exec init to transition to the init domain now
    // that the SELinux policy has been loaded.
    if (is_first_stage) {

        // 按照selinux policy要求,重新设置init文件属性
        if (restorecon("/init") == -1) {
            ERROR("restorecon failed: %s\n", strerror(errno));
            security_failure();
        }
        char* path = argv[0];

        // 设置参数  --second-stage
        char* args[] = { path, const_cast<char*>("--second-stage"), nullptr };

        // 执行init进程,重新进入main函数
        if (execv(path, args) == -1) {
            ERROR("execv(\"%s\") failed: %s\n", path, strerror(errno));
            security_failure();
        }
    }

    // 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.
    INFO("Running restorecon...\n");
    restorecon("/dev");
    restorecon("/dev/socket");
    restorecon("/dev/__properties__");
    restorecon_recursive("/sys");

// ****************** 第九部分 ****************** 
    epoll_fd = epoll_create1(EPOLL_CLOEXEC);
    if (epoll_fd == -1) {
        ERROR("epoll_create1 failed: %s\n", strerror(errno));
        exit(1);
    }
    signal_handler_init();

// ****************** 第十部分 ****************** 

    property_load_boot_defaults();
    start_property_service();

// ****************** 第十一部分 ****************** 
// 重点部分,我们后面用专门用一篇文章讲解
    init_parse_config_file("/init.rc");

// ****************** 第十二部分 ****************** 

    action_for_each_trigger("early-init", action_add_queue_tail);

    // Queue an action that waits for coldboot done so we know ueventd has set up all of /dev...
    queue_builtin_action(wait_for_coldboot_done_action, "wait_for_coldboot_done");
    // ... so that we can start queuing up actions that require stuff from /dev.
    queue_builtin_action(mix_hwrng_into_linux_rng_action, "mix_hwrng_into_linux_rng");
    queue_builtin_action(keychord_init_action, "keychord_init");
    queue_builtin_action(console_init_action, "console_init");

    // Trigger all the boot actions to get us started.
    action_for_each_trigger("init", action_add_queue_tail);

    // Repeat mix_hwrng_into_linux_rng in case /dev/hw_random or /dev/random
    // wasn't ready immediately after wait_for_coldboot_done
    queue_builtin_action(mix_hwrng_into_linux_rng_action, "mix_hwrng_into_linux_rng");

    // Don't mount filesystems or start core system services in charger mode.
    char bootmode[PROP_VALUE_MAX];
    if (property_get("ro.bootmode", bootmode) > 0 && strcmp(bootmode, "charger") == 0) {
        action_for_each_trigger("charger", action_add_queue_tail);
    } else {
        action_for_each_trigger("late-init", action_add_queue_tail);
    }

    // Run all property triggers based on current state of the properties.
    queue_builtin_action(queue_property_triggers_action, "queue_property_triggers");

// ****************** 第十三部分 ****************** 
    while (true) {
        if (!waiting_for_exec) {
            execute_one_command();
            restart_processes();
        }

        int timeout = -1;
        if (process_needs_restart) {
            timeout = (process_needs_restart - gettime()) * 1000;
            if (timeout < 0)
                timeout = 0;
        }

        if (!action_queue_empty() || cur_action) {
            timeout = 0;
        }

        bootchart_sample(&timeout);

        epoll_event ev;
        int nr = TEMP_FAILURE_RETRY(epoll_wait(epoll_fd, &ev, 1, timeout));
        if (nr == -1) {
            ERROR("epoll_wait failed: %s\n", strerror(errno));
        } else if (nr == 1) {
            ((void (*)()) ev.data.ptr)();
        }
    }
    return 0;
}

由于main()函数比较长,整个init进程的启动流程都在这个函数中,我们把main()切割成,然后我们就来挨个解释下

1、第一部分

主要是检查启动程序的文件名,这里面分成三种情况

  • 1 如果文件名是"ueventd",则执行守护进程ueventd的主函数ueventd_main()
  • 2 如果文件名是"watchdogd",则执行看门狗守护进程的主函数watchdogd_main()
  • 如果文件名既不是"ueventd"也不是"watchdogd",则往下执行

2、第二部分

 umask(0);

缺省情况下,一个进程创建出来的文件和文件夹的属性是022,使用umask()函数能设置文件属性的掩码。参数为0意味着进程创建的文件属性是0777

3、第3部分

设置环境变量地址

4、第4部分

创建一些基本的目录,包括/dev、/porc、/sysfc等。同时把一些文件系统,如tmpfs、devpt、proc、sysfs等mount到项目的目录。我们把上面的文件目录简单说一下

  • tmpfs
    是一种基于内存的文件系统,mount后就可以使用。tmpfs文件系统下的文件都存放在内存中,访问速度快,但是关机后所有内容偶读会丢失,因此tmpfs文件系统比较合适存放一些临时性的文件。tmpfs文件系统的大小是动态变化的,刚开始占用空间很小,随着文件的增多会随之变大,很节省空间。Android将tmpfs文件mount到/dev目录,dev目录用来存放系统创建的设备节点,正好符合tmpfsw文件系统的特点。
    -devpts
    是虚拟终端文件系统,它通常mount在目录dev/pts下
  • proc
    是一种基于内存的虚拟文件系统,它可以看作是内核内部数据结构的接口,通过它可以获得系统的信息,同时能够在运行时修改特定的内核参数
  • sysfs
    文件系统和proc文件系统类似,它是Linux2.6内核引入的,作用是把系统的设备和总线按层次组织起来,使得它们可以在用户空间存取,用来向用户空间导出内核的数据结构及它们的属性。

5、第5部分

调用open_devnull_stdio()函数把标准输入、标准输出和标准错误重定向到空设备文件"/dev/null",这是创建守护进程常用的手段

6、第6部分

调用klog_init()函数创建节点/dev/kmsg,这样init进程可以使用kernel的log系统来出书log了,同时调用klog_set_level函数来设置输出log的级别。

PS:

  • 1 这里补充下,为什么要使用kernel的log系统,因为此时Android系统的log还没有启动,所以需要使用kernel的log系统。
  • 2 log的输出级别是KLOG_NOTICE_LEVEL(5),当log级别小于5时,这回输出kernel log,默认值是3,关于log级别如下:
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_DEFUALT_LEVEL  3

默认为3

7、第7部分

如果不是第一次,则进行一些设置,我又将这里具体划分为4个部分

  • 7.1 创建初始化标志
  • 7.2 初始化Android的属性系统
  • 7.3 解析DT和命令行中的kernel启动参数
  • 7.4 设置系统属性
7.1 创建初始化标志
 close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));

在/dev目录下创建一个空文件".booting"表示初始化正在进行
is_booting()函数会依靠空文件".booting"来判断是否进程处于初始化中,初始化结束后,这个文件会被删除

7.2 初始化Android的属性系统

主要是调用property_init()函数来初始化Android的属性系统,property_init()函数主要作用是创建一个共享区域来存储属性值

7.3 解析DT和命令行中的kernel启动参数

这里里面又分两个,由于DT的优先级高于cmdline,所以先调用process_kernel_dt()函数解析启动参数,然后调用process_kernel_cmdline解析启动参数。

为了让大家更好的理解这个两个方法,下面我们来看下这两个方法的具体实现

  • process_kernel_dt函数解析

代码在init.cpp816行

static void process_kernel_dt(void)
{
    static const char android_dir[] = "/proc/device-tree/firmware/android";

    std::string file_name = android::base::StringPrintf("%s/compatible", android_dir);

    std::string dt_file;
    android::base::ReadFileToString(file_name, &dt_file);

    // 判断compatible文件内容是否是android,firmware
    if (!dt_file.compare("android,firmware")) {
        ERROR("firmware/android is not compatible with 'android,firmware'\n");
        return;
    }

    std::unique_ptr<DIR, int(*)(DIR*)>dir(opendir(android_dir), closedir);
    if (!dir)
        return;

    struct dirent *dp;

   // 读取目录的每个文件
    while ((dp = readdir(dir.get())) != NULL) {
        if (dp->d_type != DT_REG || !strcmp(dp->d_name, "compatible"))
            continue;

        file_name = android::base::StringPrintf("%s/%s", android_dir, dp->d_name);

        android::base::ReadFileToString(file_name, &dt_file);
        std::replace(dt_file.begin(), dt_file.end(), ',', '.');

        // 每个文件名作为属性名,里面的内容作为属性值
        std::string property_name = android::base::StringPrintf("ro.boot.%s", dp->d_name);
        property_set(property_name.c_str(), dt_file.c_str());
    }
}

上面这个函数主要是在/proc/device-tree/firmware/android 这个目录下,先看compatiable文件内容是否是android,firmware。然后这个目录下每个文件名作为属性,文件里面的内容作为属性值。这里的话就是ro.boot.hareware ro.boot.name这两个属性值。

我们知道内核通常由bootloader(启动引导程序)加载启动,前面说过了,目前使用最广泛的是bootloader大都基于u-boot定制。内核允许bootloader启动自己时传递参数。在内核内核启动完毕后,启动参数可以通过/proc/cmdline查看。

例如android 4.4模拟器启动后,查看其内核启动参数,如下:
root@generic:/ # cat /proc/cmdline
qemu.gles=0 qemu=1 console=ttyS0 android.qemud=ttyS1 android.checkjni=1 ndns=1

  • process_kernel_cmdline函数解析

代码在init.cpp848行

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(false, import_kernel_nv);
    if (qemu[0])
        import_kernel_cmdline(true, import_kernel_nv);
}

这个函数内部有点小复杂,我将其分为2个步骤,解析如下:

  • 第一步:修改/proc/cmdline文件权限,0440即表明只有root用户或root组用户可以续写该文件,其他用户无法访问。
  • 第二步:调用import_kernel_cmdline函数,就是读取proc/cmdline中的内容。这个函数有两个有两个参数,第一个采纳数标识当前Android十倍是否是模拟器。第二个参数是一个函数指针;主要指的是通过调用import_kernel_nv函数来设置系统属性。

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次,依次传入上述字符串。函数实现如下:

代码在init.cpp763行

static void import_kernel_nv(char *name, bool 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] 

可验证我们的分析是正确的。

7.4 设置系统属性

代码在init.cpp796行

static void export_kernel_boot_props() {
    struct {
        const char *src_prop;
        const char *dst_prop;
        const char *default_value;
    } prop_map[] = {
        { "ro.boot.serialno",   "ro.serialno",   "", },
        { "ro.boot.mode",       "ro.bootmode",   "unknown", },
        { "ro.boot.baseband",   "ro.baseband",   "unknown", },
        { "ro.boot.bootloader", "ro.bootloader", "unknown", },
        { "ro.boot.hardware",   "ro.hardware",   "unknown", },
        { "ro.boot.revision",   "ro.revision",   "0", },
    };

 //  通过内核的属性设置应用层配置文件的属性
    for (size_t i = 0; i < ARRAY_SIZE(prop_map); i++) {
        char value[PROP_VALUE_MAX];
        int rc = property_get(prop_map[i].src_prop, value);
        property_set(prop_map[i].dst_prop, (rc > 0) ? value : prop_map[i].default_value);
    }
}

所以export_kernel_boot_props这个函数,它就是设置一些属性,设置ro属性根据之前的ro.boot这类的属性值,如果没有设置成unknown,像之前我们有ro.boot.hardware,那我们就可以设置root.hardware这样的属性。这样描述很有朋友看不懂,我把上面的语言转化为最直观的语言如下:

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.revision,若存在,若存在其值写入revision中,否则ro.revision写入"unkown"。
  • 读取ro.boot.hardware,若存在其值写入ro.hardware,否则ro.hardware写入"unkown"。

8、第8部分

这部分主要是selinux相关的,这部分代码是Android4.3之后添加的安全内核,随后伴随着Android系统更新不断迭代,这段代码主要是设计SELinux初始化。后面有时间咱们开个专题讲解SELinux。

SELinux带给Linux的主要价值是:提供了一个灵活的,可配置的MAC机制。同时SELinux是一个安全体系结构,它通过LSM(Linux Security Modules)框架被集成到Linux Kernel 2.6.x。它是NSA(United States National Security Agency)和SELinux社区的联合项目。

这里重点分析下selinux_initialize函数
代码在init.cpp955行

static void selinux_initialize(bool in_kernel_domain) {

    // 使用Timer计时,计算selinux初始化耗时
    Timer t;

    selinux_callback cb;

    // 用于打印Log的回调函数
    cb.func_log = selinux_klog_callback;
    selinux_set_callback(SELINUX_CB_LOG, cb);

    // 用于检查权限的回调函数
    cb.func_audit = audit_callback;
    selinux_set_callback(SELINUX_CB_AUDIT, cb);

    // 如果不支持,则直接返回
    if (selinux_is_disabled()) {
        return;
    }

    // 内核态处理流程,第一阶段in_kernel_domain为true
    if (in_kernel_domain) {

        // 这个log打印不出,因为是INFO级别
        INFO("Loading SELinux policy...\n");

        // 用于加载sepolicy文件,该函数最终将sepolicy文件传递给kernel,这样kernel就有了安全策略的配置文件
        if (selinux_android_load_policy() < 0) {
            ERROR("failed to load policy: %s\n", strerror(errno));
            security_failure();
        }

        // 命令行中得到的信息
        bool is_enforcing = selinux_is_enforcing();

       // 这里补充下 用于设置selinux工作模式。selinux有两种工作模式:
       // 1、"permissive",所有的操作都被允许(即没有MAC),但是如果违反权限的话,会记录日志。
       // 2、"enforcing",所有操作都会进行权限检查。在一般的中断中,应有工作于enforcing模式
        security_setenforce(is_enforcing);

        if (write_file("/sys/fs/selinux/checkreqprot", "0") == -1) {
            security_failure();
        }

        //输出selinux的模式,与初始化耗时
        NOTICE("(Initializing SELinux %s took %.2fs.)\n",
               is_enforcing ? "enforcing" : "non-enforcing", t.duration());
    } else {
        // 如果启动第二阶段,调用该函数
        selinux_init_all_handles();
    }
}

9、第9部分

这部分分为上下按两个阶段
第一阶段主要是调用epoll_create1创建epoll句柄,如果创建失败,则退出。
第二阶段是调用signal_handler_init()函数,主要是装载进程信号处理器。

signal_handler_init()函数主要是当子进程被kill之后,会在父进程接受一个信号。处理这个信号的时候往sockpair一段写数据,而另一端的fd是加入epoll中

init是一个守护进程,为了防止init的子进程称为僵尸进程(zombie process),需要init在子进程结束时获取子进程的结束码,通过结束码将程序表中的子进程移除,防止称为僵尸进程的子进程占用程序表的空间(程序表的空间达到上线时,系统就不能再启动新的进城了,会引起严重的系统问题)。

现在我们来看下signal_handler_init()函数的内容
signal_handler.cpp955行

void signal_handler_init() {

    // 在Linux中,父进程是通过捕捉SIGCHILD信号来得知子进程运行结束的情况
    // Create a signalling mechanism for SIGCHLD.
    int s[2];

    // 利用socketpair创建出已经连接的两个socket,分别作为信号的读、写端
    if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, s) == -1) {
        ERROR("socketpair failed: %s\n", strerror(errno));
        exit(1);
    }

    signal_write_fd = s[0];
    signal_read_fd = s[1];

    // Write to signal_write_fd if we catch SIGCHLD.
    struct sigaction act;
    memset(&act, 0, sizeof(act));

    // 信号处理器为SIGCHLD_handler,其被存在sigaction结构体重,负责处理SIGCHLD消息
    
    // 信号处理器
    act.sa_handler = SIGCHLD_handler;

     // 仅当进程终止时才接受
    act.sa_flags = SA_NOCLDSTOP;

     // 调用信号安装函数sigaction,将监听的信号及对应的信号处理器注册到内核中
    sigaction(SIGCHLD, &act, 0);

    reap_any_outstanding_children();

    // 定义在system/core/init/init.cpp中,注册epoll handler,当signal_read_fd 有数据可读时,调用handle_signal
    register_epoll_handler(signal_read_fd, handle_signal);
}

9.1信号

这里我们简单介绍下信号:
Linux进程通过相互发送接收消息来实现进程间通信,这些消息被称为"信号"。每个进程在处理它进程发送的信号时,都要注册处理者,处理者被称为信号处理器。

每个进程在处理其他进程发送的signal信号时都需要先注册,当进程的运行状态改变或终止时会产生某种signal信号,init进程是所有用户空间进程的父进程,当其子进程终止时产生SIGCHLD信号,init进程调用信号安装函数sigaction(),传递参数给sigaction结构体,便完成信号处理的过程。

这里有两个重要的函数:SIGCHLD_handler和handle_signal,他俩是对应的

代码在signal_handler.cpp 158行

static void SIGCHLD_handler(int) {
    //向signal_write_fd写入1,知道成功为止
    if (TEMP_FAILURE_RETRY(write(signal_write_fd, "1", 1)) == -1) {
        ERROR("write(signal_write_fd) failed: %s\n", strerror(errno));
    }
}

SIGCHLD_handler()函数负责写入

代码在signal_handler.cpp 150行

static void handle_signal() {
    // Clear outstanding requests.
    char buf[32];
    // 读取signal_read_fd数据,放入buf
    read(signal_read_fd, buf, sizeof(buf));

    reap_any_outstanding_children();
}

handle_signal()函数负责读取数据,上面调用了reap_any_outstanding_children()函数,我们来看下

代码在signal_handler.cpp 145行

static void reap_any_outstanding_children() {
    while (wait_for_one_process()) {
    }
}

我们看到reap_any_outstanding_children函数就是调用while循环来调用wait_for_one_process()函数,下面我们来看下wait_for_one_process()函数的具体执行

代码在signal_handler.cpp 53行

static bool wait_for_one_process() {
    int status;

    // 等待任意子进程,如果子进程没有退出则返回0,否则则返回该子进程的pid
    pid_t pid = TEMP_FAILURE_RETRY(waitpid(-1, &status, WNOHANG));
    if (pid == 0) {
        return false;
    } else if (pid == -1) {
        ERROR("waitpid failed: %s\n", strerror(errno));
        return false;
    }

    // 根据pid 查找相应的service
    service* svc = service_find_by_pid(pid);

    std::string name;
    if (svc) {
        name = android::base::StringPrintf("Service '%s' (pid %d)", svc->name, pid);
    } else {
        name = android::base::StringPrintf("Untracked pid %d", pid);
    }

    NOTICE("%s %s\n", name.c_str(), DescribeStatus(status).c_str());

    if (!svc) {
        return true;
    }

    // TODO: all the code from here down should be a member function on service.

    // 当flag为RESTART,且不是ONESHOT时,先kill进程组内所有子进程或子线程
    if (!(svc->flags & SVC_ONESHOT) || (svc->flags & SVC_RESTART)) {
        NOTICE("Service '%s' (pid %d) killing any children in process group\n", svc->name, pid);
        kill(-pid, SIGKILL);
    }

    // Remove any sockets we may have created.
    // 移除当前服务svc中所有创建过的socket
    for (socketinfo* si = svc->sockets; si; si = si->next) {
        char tmp[128];
        snprintf(tmp, sizeof(tmp), ANDROID_SOCKET_DIR"/%s", si->name);
        unlink(tmp);
    }

    // 当flags为EXEC时,释放相应的服务
    if (svc->flags & SVC_EXEC) {
        INFO("SVC_EXEC pid %d finished...\n", svc->pid);
        waiting_for_exec = false;
        list_remove(&svc->slist);
        free(svc->name);
        free(svc);
        return true;
    }

    svc->pid = 0;
    svc->flags &= (~SVC_RUNNING);

    // Oneshot processes go into the disabled state on exit,
    // except when manually restarted.
    // 对于ONESHOT服务,使其进入disabled状态
    if ((svc->flags & SVC_ONESHOT) && !(svc->flags & SVC_RESTART)) {
        svc->flags |= SVC_DISABLED;
    }

    // Disabled and reset processes do not get restarted automatically.
    // 禁用和重置的服务,都不再自动重启
    if (svc->flags & (SVC_DISABLED | SVC_RESET))  {
        // 设置相应的service状态为stopped
        svc->NotifyStateChange("stopped");
        return true;
    }


    // 服务在4分钟内重启次数超过4次,则重启手机进入recovery模式
    time_t now = gettime();
    if ((svc->flags & SVC_CRITICAL) && !(svc->flags & SVC_RESTART)) {
        if (svc->time_crashed + CRITICAL_CRASH_WINDOW >= now) {
            if (++svc->nr_crashed > CRITICAL_CRASH_THRESHOLD) {
                ERROR("critical process '%s' exited %d times in %d minutes; "
                      "rebooting into recovery mode\n", svc->name,
                      CRITICAL_CRASH_THRESHOLD, CRITICAL_CRASH_WINDOW / 60);
                android_reboot(ANDROID_RB_RESTART2, 0, "recovery");
                return true;
            }
        } else {
            svc->time_crashed = now;
            svc->nr_crashed = 1;
        }
    }

    svc->flags &= (~SVC_RESTART);
    svc->flags |= SVC_RESTARTING;

    // Execute all onrestart commands for this service.
    // 执行当前service中所有onrestart命令
    struct listnode* node;
    list_for_each(node, &svc->onrestart.commands) {
        command* cmd = node_to_item(node, struct command, clist);
        cmd->func(cmd->nargs, cmd->args);
    }

    // 设置相应的service状态未restarting
    svc->NotifyStateChange("restarting");
    return true;
}

总结一下:
当init进程调用signal_handler_init后,一旦受到子进程终止带来的SIGCHLD消息后,将利用信号处理者SIGCHLD_handler向signal_write_fd写入信息;epoll句柄监听到signal_read_fd收到消息后,将调用handle_signal进行处理。如下图


image.png

10、第10部分

这部分主要分为两部分,上半部分是调用property_load_boot_defaults()函数解析根目录的default.prop的属性,设置默认属性配置的相关工作。下半部分是调用start_prperty_service()函数,启动属性服务,并接受属性的socket的fd加入到epoll中,也定义了处理函数。那我们依次来看下

10.1、property_load_boot_defaults()函数解析

代码在property_service.cpp 494行

void property_load_boot_defaults() {
    load_properties_from_file(PROP_PATH_RAMDISK_DEFAULT, NULL);
}

我们看到他调用load_properties_from_file函数,那我们来看下load_properties_from_file函数。

代码在property_service.cpp 426行

/*
 * Filter is used to decide which properties to load: NULL loads all keys,
 * "ro.foo.*" is a prefix match, and "ro.foo.bar" is an exact match.
 */
static void load_properties_from_file(const char* filename, const char* filter) {
    Timer t;
    std::string data;
    if (read_file(filename, &data)) {
        data.push_back('\n');
        load_properties(&data[0], filter);
    }
    NOTICE("(Loading properties from %s took %.2fs.)\n", filename, t.duration());
}

PS:所谓充电模式是指充电器开机时设备进入的状态。这是kernel和init进程会启动,但是大部分服务都不会启动

10.2、start_property_service()函数解析

代码在property_service.cpp 570行

void start_property_service() {
    
    property_set_fd = create_socket(PROP_SERVICE_NAME, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK,
                                    0666, 0, 0, NULL);
    if (property_set_fd == -1) {
        ERROR("start_property_service socket creation failed: %s\n", strerror(errno));
        exit(1);
    }

    listen(property_set_fd, 8);
    
    register_epoll_handler(property_set_fd, handle_property_set_fd);
}

start_property_service()函数创建了socket,然后监听,并且调用register_epoll_handler()函数把socket的fd放入epoll中
函数创建了socket,然后监听,并且调用register_epoll_handler函数把socket的fd放入了epoll中

11、第11部分

这部分主要是解析init.rc文件,我们将会用单独一篇文章来讲解。这里先略过

12、第12部分

本部分是将把Action加入执行队列中

代码在init.cpp 570行

    // 执行init.rc中触发器为 on early-init的语句,即将early-init的Action添加到链表action_queue中
    action_for_each_trigger("early-init", action_add_queue_tail);

    // Queue an action that waits for coldboot done so we know ueventd has set up all of /dev...

    // 等冷插拔设备初始化完成,即创建wait_for_coldboot_done Action并添加到action_queue和action_list中
    queue_builtin_action(wait_for_coldboot_done_action, "wait_for_coldboot_done");
    // ... so that we can start queuing up actions that require stuff from /dev.
    queue_builtin_action(mix_hwrng_into_linux_rng_action, "mix_hwrng_into_linux_rng");

    // 设备组合键的初始化操作,创建keychord_init Action 并添加到链表action_queue和action_list中 
    queue_builtin_action(keychord_init_action, "keychord_init");
    // 创建console_init动作并添加到链表action_queue和action_list中
    queue_builtin_action(console_init_action, "console_init");

    // Trigger all the boot actions to get us started.
    // 执行init.rc文件中触发器为 on init 的语句,将init动作添加到链表action_queue中
    action_for_each_trigger("init", action_add_queue_tail);

    // Repeat mix_hwrng_into_linux_rng in case /dev/hw_random or /dev/random
    // wasn't ready immediately after wait_for_coldboot_done
    queue_builtin_action(mix_hwrng_into_linux_rng_action, "mix_hwrng_into_linux_rng");

    // Don't mount filesystems or start core system services in charger mode.
    char bootmode[PROP_VALUE_MAX];

    // 当处于充电模式,则charger加入执行队列;否则late-init加入队列。
    if (property_get("ro.bootmode", bootmode) > 0 && strcmp(bootmode, "charger") == 0) {
        action_for_each_trigger("charger", action_add_queue_tail);
    } else {
        action_for_each_trigger("late-init", action_add_queue_tail);
    }

    // Run all property triggers based on current state of the properties.
    // 触发器为属性是否设置
    queue_builtin_action(queue_property_triggers_action, "queue_property_triggers");

上面大量的调用了action_for_each_trigger函数、action_add_queue_tai函数和queue_builtin_action,那我们就依次研究下这两个函数

12.1 action_for_each_trigger()函数解析

代码在init_parser.cpp 546行

void action_for_each_trigger(const char *trigger,
                             void (*func)(struct action *act))
{
    struct listnode *node, *node2;
    struct action *act;
    struct trigger *cur_trigger;

    list_for_each(node, &action_list) {
        // 遍历每个action
        act = node_to_item(node, struct action, alist);
        list_for_each(node2, &act->triggers) {
            // 遍历每个action的triggers
            cur_trigger = node_to_item(node2, struct trigger, nlist);
            // 判断是否与传入的trigger名字匹配
            if (!strcmp(cur_trigger->name, trigger)) {
                // 调用回调函数
                func(act);
            }
        }
    }
}
12.2 action_add_queue_tail()函数解析

代码在init_parser.cpp 643行

void action_add_queue_tail(struct action *act)
{
    if (list_empty(&act->qlist)) {
        list_add_tail(&action_queue, &act->qlist);
    }
}

这个函数就是把action加入到执行列表中

12.3 queue_builtin_action()函数解析

代码在init_parser.cpp 623行

void queue_builtin_action(int (*func)(int nargs, char **args), const char *name)
{
    action* act = (action*) calloc(1, sizeof(*act));
    trigger* cur_trigger = (trigger*) calloc(1, sizeof(*cur_trigger));
    cur_trigger->name = name;
    list_init(&act->triggers);
    list_add_tail(&act->triggers, &cur_trigger->nlist);
    list_init(&act->commands);
    list_init(&act->qlist);

    command* cmd = (command*) calloc(1, sizeof(*cmd));
    cmd->func = func;
    cmd->args[0] = const_cast<char*>(name);
    cmd->nargs = 1;
    list_add_tail(&act->commands, &cmd->clist);

    list_add_tail(&action_list, &act->alist);
    action_add_queue_tail(act);
}

这个函数的话,就是直接创建一个action,然后新建command,关键是func会调用函数设置好,最后把action加入执行队列中。

queue_builtin_action函数用来动态生成一个Action并插入到执行列表"action_queue中"。插入的Action由一个函数指针和一个表示名字的字符串组成。Android在以前版本中直接调用这些函数来完成某些初始化的工作,但是,这些函数可能会依赖init.rc里面定义的一些命令和服务的执行情况。现在把这些初始化函数也通过Action的形式插入到执行列表中,这样就能控制他们的执行顺序了。

插入的函数有:

  • wait_for_coldboot_done_action()函数,等待冷插拔设备初始化完成
  • mix_hwrng_into_linux_rng_action()函数,从硬件PNG的设备文件/dev/hw_random中读取512字节并写到Linux RNG的设备文件/dev/urandom中
  • keychord_init_action()函数:初始化组合键监听模块
  • console_init_action()函数:在屏幕个上显示Android字样的Logo
  • property_service_init_action()函数:初始化属性服务,读取系统预制的属性值
  • singal_init_action()函数:初始化信号处理模块。
  • check_startup_action()函数:检查是否已经完成init进程初始化,如果完成则删除.booting文件。
  • queue_property_triggers_action()函数:检查Action列表中通过修改属性来触发的Action,查看相关属性值是否已经设置,如果已经设置,则将Action加入到执行列表中。

执行流程如下图:

13、第13部分

代码在init.cp 1109行

    while (true) {
        // 判断是否还有事件需要处理
        if (!waiting_for_exec) {
             //依次执行每个action中携带的command对应的执行函数
            execute_one_command();
             // 重启一些挂掉的进程
            restart_processes();
        }

        // 决定timeout的时间,将影响while循环的间隔
        int timeout = -1;

       // 有进程需要重启是,等待进程重启
        if (process_needs_restart) {
            timeout = (process_needs_restart - gettime()) * 1000;
            if (timeout < 0)
                timeout = 0;
        }

        if (!action_queue_empty() || cur_action) {
            timeout = 0;
        }

        // 进行性能数据采样
        bootchart_sample(&timeout);

        epoll_event ev;
        // 没有事件来的话,最多阻塞timeout时间
        int nr = TEMP_FAILURE_RETRY(epoll_wait(epoll_fd, &ev, 1, timeout));
        if (nr == -1) {
            ERROR("epoll_wait failed: %s\n", strerror(errno));
        } else if (nr == 1) {
             // 根据上下文知道,epoll句柄(即epoll_fd)主要监听子进程结束,及其他进程设置系统属性的请求 ,根据事件的到来,执行对应处理函数
            ((void (*)()) ev.data.ptr)();
        }
    }

最后init进程会进入到一个无线循环中去,在这个无线循环中,init进程会做以下五件事:

  • 第一件事:调用函数execute_one_command来检查action_queue列表是否为空。如果不为空的话,那么init进程就将保存在列表头部中的action移除,并且执行这个被移除的action。由于前面我们将一个名称为"console_init"的action添加到action_queue列表中,因此,在这个无线循环中,这个action就会被执行,即console_init_action函数会被调用。

  • 第二件事:调用函数restart_processes来检查系统中是否有进程需要重启。在启动脚本init.rc中,我们可以指定一个进程在退出之后会自动重启。在这种情况下,函数restart_processes就会检查是否存在需要重新启动的进程,如果存在的话,那么就将它重新启动起来。

  • 第三件事:处理系统属性变化事件。当我们调用函数property_set来改变一个系统属性时,系统就会通过一个socket(通过调用函数get_property_set_fd可以获得它的文件描述符)来向init进程发送一个属性值改变事件通知。init进程接受到这个属性值改变事件之后,就会调用函数handle_property_set_fd来进行相应的处理。后面在分析第三个开机画面显示过程时,我们就会看到,SurfaceFlinger服务就是通过修改“ctl.start”和“ctl.stop”属性来启动和停止三个开机画面的。

  • 第四件事:处理一种被称为"chorded keyboard"的键盘输入时间。这种类型为"chorded keyboard"的键盘设备通过不同的按键组合来描述不同的命令或者操作,它对应的设备为/dev/keychord。我们可以通过调用函数get_keychord_fd()来获的这个设备的文件描述符,以便可以监控它的输入事件,并且调用函数handle_keychord来对这些输入事件进行处理。

  • 第五件事:回收僵尸进程。我们知道,在Linux内核中,如果父进程不等待子进程结束就退出,那么当子进程结束的时候,就会变成一个僵尸进程,从而占用系统的资源。为了回收这些僵尸进程,init进程会安装一个SIGCHLD信号接收器。当这些父进程已经退出了子进程退出的时候,内核就会发出一个SIGCHLD信号,给init进程,init进程就可以通过一个socket(通过调用函数get_signal_fd可以获得它的文件描述符)来将接受到的SIGCHLD信号读取回来,并且调用函数handle_signal来对接收到的SIGCHLD信号来进行处理,即回收哪些已经变成僵尸进程的子进程。

PS:后面三件事都是可以通过文件描述符来描述的,因此,initJ进程的入口函数main使用poll机制来同时轮训它们,以便可以提高效率。

下面 这里我们来看一下execute_one_command()函数和restart_processes()函数的具体执行

13.1 execute_one_command函数的执行

代码在init.cpp 584行

void execute_one_command() {
    Timer t;

    char cmd_str[256] = "";
    char name_str[256] = "";

    //如果是第一次启动,所以都是NULL,所以肯定可以进入这个判断
    //如果不是第一次启动,因为得到cur_action或者cur_command都是null,并且如果这个command是当前action的最后一个command的话,会进入下面的这个判断
    if (!cur_action || !cur_command || is_last_command(cur_action, cur_command)) {

        // 依次从action_queue获取action
        cur_action = action_remove_queue_head();
        cur_command = NULL;

        if (!cur_action) {
            // 如果没有action了,则返回
            return;
        }

        build_triggers_string(name_str, sizeof(name_str), cur_action);

        INFO("processing action %p (%s)\n", cur_action, name_str);
        // 如果是一个新的action的话,会执行到这一步去获得first command
        cur_command = get_first_command(cur_action);
    } else {
         // 如果还在action的内部链表中,如果仍然存在没有获取到的command的话,则会去获取一下一个command
        cur_command = get_next_command(cur_action, cur_command);
    }

    if (!cur_command) {
        //如果可以获取到command为空的话,会返回,反之,继续
        return;
    }

    // 会调用这个command的func去执行,执行的参数个数为nargs,命令为args
    int result = cur_command->func(cur_command->nargs, cur_command->args);
    if (klog_get_level() >= KLOG_INFO_LEVEL) {
        for (int i = 0; i < cur_command->nargs; i++) {
            strlcat(cmd_str, cur_command->args[i], sizeof(cmd_str));
            if (i < cur_command->nargs - 1) {
                strlcat(cmd_str, " ", sizeof(cmd_str));
            }
        }
        char source[256];
        if (cur_command->filename) {
            snprintf(source, sizeof(source), " (%s:%d)", cur_command->filename, cur_command->line);
        } else {
            *source = '\0';
        }
        INFO("Command '%s' action=%s%s returned %d took %.2fs\n",
             cmd_str, cur_action ? name_str : "", source, result, t.duration());
    }
}

其实这个函数就是执行一个command。大体流程如下:

  • 首选从action_queue取下struct action *act赋值给cur_action
  • 其次从cur_action获得struct command * 赋值给curcommand
  • 最后执行cur_command->func(cur_command->nargs, cur_command->args)
13.1.1 action_remove_queue_head函数的执行

上面调用了action_remove_queue_head()函数,我们来看下
代码在init_parser.cpp 650行

struct action *action_remove_queue_head(void)
{
    // 先做非空判断
    if (list_empty(&action_queue)) {
        return 0;
    } else {

        // 如果还有未被执行的队列的话,就将node指向现在的action_queue的头指针
        struct listnode *node = list_head(&action_queue);

        // 取出action
        struct action *act = node_to_item(node, struct action, qlist);

         // 删将这个节点从整个action_queue的列表中删除
        list_remove(node);
   
        // 删除节点后,为了安全起见,将node自己指向自己,以避免出现野指针。
        list_init(node);

         // 返回 已经找到的action
        return act;
    }
}

我们知道,其实是从action_queue中拿每一个结构体的,拿到action之后,就从action里面去取对应的command了。

13.1.2 action_remove_queue_head函数的执行

代码在init.cpp 543行

// 由于这个函数主要是从一个action里面找到一个command,所以在传递的时候只传递action即可 
static struct command *get_first_command(struct action *act)
{
    struct listnode *node;

    // 将node 指向action的commands结构体
    node = list_head(&act->commands);
    if (!node || list_empty(&act->commands))
        // 如果这话节点不存在,或者这个action的commands结构体为空,则返回null
        return NULL;

    // 返回 第一个节点
    return node_to_item(node, struct command, clist);
}
13.1.3 get_next_command函数的执行
{  
static struct command *get_next_command(struct action *act, struct command *cmd)
{
    struct listnode *node;
    node = cmd->clist.next;

    // 如果不存在,则返回null
    if (!node)
        return NULL;

    // 如果这个节点已经是头节点了,则返回null
    if (node == &act->commands)
        return NULL;

    // 返回 next节点
    return node_to_item(node, struct command, clist);
}

这个函数 主要是返回当前commands的下一个command

13.2 restart_processes()函数的执行

当内存不足时,Android系统会自动杀死一些进程来释放空间,所以当某些重要服务被杀,同时该服务进程并未设置为oneshot,则必须重新启动该服务进程。

代码在init.cpp 473行

static void restart_processes()
{
    process_needs_restart = 0;
    service_for_each_flags(SVC_RESTARTING,
                           restart_service_if_needed);
}

我们看到这个函数什么都做,就是调用了service_for_each_flags函数,那我们再来研究下这个函数

代码在init_parser.cpp 533行

void service_for_each_flags(unsigned matchflags,
                            void (*func)(struct service *svc))
{
    struct listnode *node;
    struct service *svc;
    list_for_each(node, &service_list) {
        svc = node_to_item(node, struct service, slist);
        if (svc->flags & matchflags) {
            func(svc);
        }
    }
}

通过代码我们知道 函数service_for_each_flags主要是循环遍历服务链表,查找标志位为SVC_RESTARTING的服务,当该服务进程死亡的时候,init进程监控进程死亡事件,在处理该事件的时候会为该进程设置SVC_RESTARTING标志,并调用restart_service_if_needed函数重启服务。

我们来看下restart_service_if_needed函数
代码在init.cpp 457行

static void restart_service_if_needed(struct service *svc)
{
    time_t next_start_time = svc->time_started + 5;

    if (next_start_time <= gettime()) {
        svc->flags &= (~SVC_RESTARTING);
        service_start(svc, NULL);
        return;
    }

    if ((next_start_time < process_needs_restart) ||
        (process_needs_restart == 0)) {
        process_needs_restart = next_start_time;
    }
}

只有当前时间大于服务启动时间时,清除服务重启标志并启动该服务,

上一篇文章 1序言、bootloader引导与Linux启动
下一篇文章 Android系统启动——3init.rc解析

官人[飞吻],你都把臣妾从头看到尾了,喜欢就点个赞呗(眉眼)!!!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容