Android系统启动之配置文件解析

以下代码基于Android 7.0分析

简介

我们知道在Android系统启动的时候会创建Init进程,在Init进程的main()入口函数中会解析系统配置文件进行服务进程的创建和启动。

解析Init.rc配置文件

[->system/core/init/init.cpp]

int main(int argc, char** argv) {
  ...
  //这里将Action的function_map_替换为BuiltinFunctionMap
  //下文将通过BuiltinFuntionMap的map方法,获取keyword对应的处理函数
  const BuiltinFunctionMap function_map;
  Action::set_function_map(&function_map);

  //构造出解析文件用的parser对象
  Parser& parser = Parser::GetInstance();
  //为一些类型的关键字,创建特定的parser
  parser.AddSectionParser("service",std::make_unique<ServiceParser>());
  parser.AddSectionParser("on", std::make_unique<ActionParser>());
  parser.AddSectionParser("import", std::make_unique<ImportParser>());
  //开始实际的解析过程
  parser.ParseConfig("/init.rc");
  ...
}

在解析init.rc文件之前,我们先来简单的介绍一下init.rc文件。init.rc文件是在init进程启动后执行的启动脚本,文件中记录着init进程需执行的操作。在Android系统中,使用init.rc和init.{ hardware }.rc两个文件。

其中init.rc文件在Android系统运行过程中用于通用的环境设置与进程相关的定义,init.{hardware}.rc(例如,高通有init.qcom.rc,MTK有init.mediatek.rc)用于定义Android在不同平台下的特定进程和环境设置等。

此处的init.rc位于system/core/rootdir/init.rc中。
init.rc文件大致分为2部分,一部分是以"on"关键字开头的动作列表(action list):

on early-init
    # Set init and its forked children's oom_adj.
    write /proc/1/oom_score_adj -1000
    .........
    start ueventd

另一部分是以"service"关键字开头的服务列表(service list):

service ueventd /sbin/ueventd
    class core
    critical
    seclabel u:r:ueventd:s0

借助系统环境变量或Linux命令,动作列表用于创建所需目录,以及为某些特定文件指定权限,而服务列表用来记录init进程需要启动的一系列子进程。如上面代码所示,service关键字后第一个字符串表示服务(子进程)的名称,第二个字符串表示服务的执行路径。
接下来我们就从ParseConfig函数入手,逐步分析整个解析过程。
[->system/core/init/ init_parser.cpp]

bool Parser::ParseConfig(const std::string& path) {
    //path为/init.rc
    if (is_dir(path.c_str())) {
        //传入参数为目录地址
        return ParseConfigDir(path);
    }
    //传入参数为文件地址
    return ParseConfigFile(path);
}

bool Parser::ParseConfigDir(const std::string& path) {
    ...........
    std::unique_ptr<DIR, int(*)(DIR*)> config_dir(opendir(path.c_str()), closedir);
    ..........
    //看起来很复杂,其实就是递归目录
    while ((current_file = readdir(config_dir.get()))) {
        std::string current_path = android::base::StringPrintf("%s/%s", path.c_str(), current_file->d_name);
        if (current_file->d_type == DT_REG) {
            //最终还是靠ParseConfigFile来解析实际的文件
            if (!ParseConfigFile(current_path)) {
                .............
            }
        }
    }
}

bool Parser::ParseConfigFile(const std::string& path) {
    ........
    std::string data;
    //读取路径指定文件中的内容,保存为字符串形式
    if (!read_file(path.c_str(), &data)) {
        return false;
    }
    .........
    //解析获取的字符串
    ParseData(path, data);
    .........
}

void Parser::ParseData(const std::string& filename, const std::string& data) {
    .......
    parse_state state;
    .......
    std::vector<std::string> args;

    for (;;) {
        //next_token以行为单位分割参数传递过来的字符串
        //最先走到T_TEXT分支
        switch (next_token(&state)) {
        case T_EOF:
            if (section_parser) {
                //EOF,解析结束
                section_parser->EndSection();
            }
            return;
        case T_NEWLINE:
            state.line++;
            if (args.empty()) {
                break;
            }
            //在前文创建parser时,我们为service,on,import定义了对应的parser 
            //这里就是根据第一个参数,判断是否有对应的parser
            if (section_parsers_.count(args[0])) {
                if (section_parser) {
                    //结束上一个parser的工作,将构造出的对象加入到对应的service_list与action_list中
                    section_parser->EndSection();
                }
                //获取参数对应的parser
                section_parser = section_parsers_[args[0]].get();
                std::string ret_err;
                //调用实际parser的ParseSection函数(重点)
                if (!section_parser->ParseSection(args, &ret_err)) {
                    parse_error(&state, "%s\n", ret_err.c_str());
                    section_parser = nullptr;
                }
            } else if (section_parser) {
                std::string ret_err;
                //如果第一个参数不是service,on,import
                //则调用前一个parser的ParseLineSection函数
                //这里相当于解析一个参数块的子项
                if (!section_parser->ParseLineSection(args, state.filename, state.line, &ret_err)) {
                    parse_error(&state, "%s\n", ret_err.c_str());
                }
            }
            //清空本次解析的数据
            args.clear();
            break;
        case T_TEXT:
            //将本次解析的内容写入到args中
            args.emplace_back(state.text);
            break;
        }
    }
}

这里的解析看起来比较复杂,在6.0以前的版本中,整个解析是面向过程的。init进程统一调用一个函数来进行解析,然后在该函数中利用switch-case的形式,根据解析的内容进行相应的处理。
在Android 7.0中,为了更好地封装及面向对象,对于不同的关键字定义了不同的parser对象,每个对象通过多态实现自己的解析操作。
现在我们来回一下init进程main函数中创建的三个parse代码

    Parser& parser = Parser::GetInstance();
    parser.AddSectionParser("service",std::make_unique<ServiceParser>());
    parser.AddSectionParser("on", std::make_unique<ActionParser>());
    parser.AddSectionParser("import", std::make_unique<ImportParser>());

看下三个Parse的定义

class ServiceParser : public SectionParser {......}
class ActionParser : public SectionParser {......}
class ImportParser : public SectionParser {.......}

可以看到三个Parser均是继承SectionParser,具体的实现各有不同,我们以比较常用的ServiceParser和ActionParser为例,看看解析的结果如何处理。

ServiceParser

ServiceParser定义在system/core/init/service.cpp中。从前面的代码中我们知道,解析一个service块,首先需要调用ParseSection函数,接着你用ParseLineSection处理子块,在解析完数据后,调用EndSection。
因此,我们就着重来看下这三个函数。
[->system/core/init/service.cpp]

//根据参数来构造一个service对象
bool ServiceParser::ParseSection(.....) {
    .......
    const std::string& name = args[1];
    .......
    std::vector<std::string> str_args(args.begin() + 2, args.end());
    //主要根据参数,构造出一个service对象
    service_ = std::make_unique<Service>(name, "default", str_args);
    return true;
}

//注意这里已经在解析子项了
bool ServiceParser::ParseLineSection(......) const {
    //调用service对象的HandleLine
    return service_ ? service_->HandleLine(args, err) : false;
}

bool Service::HandleLine(.....) {
    ........
    //OptionHandlerMap继承自keywordMap<OptionHandler>
    static const OptionHandlerMap handler_map;
    //根据子项的内容,找到对应的handler函数
    //FindFunction定义与keyword模块中,FindFunction方法利用子类生成对应的map中,然后通过通用的查找方法,即比较键值找到对应的处理函数
    auto handler = handler_map.FindFunction(args[0], args.size() - 1, err);

    if (!handler) {
        return false;
    }
    //调用handler函数
    return (this->*handler)(args, err);
}

class Service::OptionHandlerMap : public KeywordMap<OptionHandler> {
    ...........
    Service::OptionHandlerMap::Map& Service::OptionHandlerMap::map() const {
    constexpr std::size_t kMax = std::numeric_limits<std::size_t>::max();
    static const Map option_handlers = {
        {"class",       {1,     1,    &Service::HandleClass}},
        {"console",     {0,     0,    &Service::HandleConsole}},
        {"critical",    {0,     0,    &Service::HandleCritical}},
        {"disabled",    {0,     0,    &Service::HandleDisabled}},
        {"group",       {1,     NR_SVC_SUPP_GIDS + 1, &Service::HandleGroup}},
        {"ioprio",      {2,     2,    &Service::HandleIoprio}},
        {"keycodes",    {1,     kMax, &Service::HandleKeycodes}},
        {"oneshot",     {0,     0,    &Service::HandleOneshot}},
        {"onrestart",   {1,     kMax, &Service::HandleOnrestart}},
        {"seclabel",    {1,     1,    &Service::HandleSeclabel}},
        {"setenv",      {2,     2,    &Service::HandleSetenv}},
        {"socket",      {3,     6,    &Service::HandleSocket}},
        {"user",        {1,     1,    &Service::HandleUser}},
        {"writepid",    {1,     kMax, &Service::HandleWritepid}},
    };
    return option_handlers;
}

//以class对应的处理函数为例,可以看出其实就是填充service对象对应的域
bool Service::HandleClass(const std::vector<std::string>& args, std::string* err) {
    classname_ = args[1];
    return true;
}

//注意此时service对象已经处理完毕
void ServiceParser::EndSection() {
    if (service_) {
        ServiceManager::GetInstance().AddService(std::move(service_));
    }
}

void ServiceManager::AddService(std::unique_ptr<Service> service) {
    Service* old_service = FindServiceByName(service->name());
    if (old_service) {
        ERROR("ignored duplicate definition of service '%s'",
              service->name().c_str());
        return;
    }
    //将service对象加入到services_里
    //7.0里,services_已经是个vector了
    services_.emplace_back(std::move(service));
}

从上面的一系列代码,我们可以看出ServiceParser其实就是:首先根据第一行的名字和参数创建出service对象,然后根据选项域的内容填充service对象,最后将创建出的service对象加入到vector类型的service链表中。

ActionParser

ActionParser定义于system/core/init/action.cpp中。Action的解析过程,其实与Service一样,也是先后调用ParseSection, ParseLineSection和EndSection。
[->system/core/init/action.cpp]

bool ActionParser::ParseSection(....) {
    ........
    //创建出新的action对象
    auto action = std::make_unique<Action>(false);
    //根据参数,填充action的trigger域,不详细分析了
    if (!action->InitTriggers(triggers, err)) {
        return false;
    }
    .........
}

bool ActionParser::ParseLineSection(.......) const {
    //构造Action对象的command域
    return action_ ? action_->AddCommand(args, filename, line, err) : false;
}

bool Action::AddCommand(.....) {
    ........
    //找出action对应的执行函数
    auto function = function_map_->FindFunction(args[0], args.size() - 1, err);
    ........
    //利用所有信息构造出command,加入到action对象中
    AddCommand(function, args, filename, line);
    return true;
}

void Action::AddCommand(......) {
    commands_.emplace_back(f, args, filename, line);
}

void ActionParser::EndSection() {
    if (action_ && action_->NumCommands() > 0) {
        ActionManager::GetInstance().AddAction(std::move(action_));
    }
}

void ActionManager::AddAction(.....) {
    ........
    auto old_action_it = std::find_if(actions_.begin(),
                     actions_.end(),
                     [&action] (std::unique_ptr<Action>& a) {
                         return action->TriggersEqual(*a);
                     });

    if (old_action_it != actions_.end()) {
        (*old_action_it)->CombineAction(*action);
    } else {
        //加入到action链表中,类型也是vector,其中装的是指针
        actions_.emplace_back(std::move(action));
    }
}

从上面的代码可以看出,加载action块的逻辑和service一样,不同的是需要填充trigger和command域。当然,最后解析出的action也需要加入到action链表中。
这里最后还剩下一个问题,那就是哪里定义了Action中command对应处理函数?
实际上,前文已经出现了过了,在init.cpp的main函数中:

const BuiltinFunctionMap function_map;
Action::set_function_map(&function_map);

因此,Action中调用function_map->FindFunction时,实际上调用的是BuiltinFunctionMap的FindFunction函数。我们已经知道FindFunction是keyword定义的通用函数,重点是重构的map函数。我们看看system/core/init/builtins.cpp:

BuiltinFunctionMap::Map& BuiltinFunctionMap::map() const {
    constexpr std::size_t kMax = std::numeric_limits<std::size_t>::max();
    static const Map builtin_functions = {
        {"bootchart_init",          {0,     0,    do_bootchart_init}},
        {"chmod",                   {2,     2,    do_chmod}},
        {"chown",                   {2,     3,    do_chown}},
        {"class_reset",             {1,     1,    do_class_reset}},
        {"class_start",             {1,     1,    do_class_start}},
        {"class_stop",              {1,     1,    do_class_stop}},
        {"copy",                    {2,     2,    do_copy}},
        {"domainname",              {1,     1,    do_domainname}},
        {"enable",                  {1,     1,    do_enable}},
        {"exec",                    {1,     kMax, do_exec}},
        {"export",                  {2,     2,    do_export}},
        {"hostname",                {1,     1,    do_hostname}},
        {"ifup",                    {1,     1,    do_ifup}},
        {"init_user0",              {0,     0,    do_init_user0}},
        {"insmod",                  {1,     kMax, do_insmod}},
        {"installkey",              {1,     1,    do_installkey}},
        {"load_persist_props",      {0,     0,    do_load_persist_props}},
        {"load_system_props",       {0,     0,    do_load_system_props}},
        {"loglevel",                {1,     1,    do_loglevel}},
        {"mkdir",                   {1,     4,    do_mkdir}},
        {"mount_all",               {1,     kMax, do_mount_all}},
        {"mount",                   {3,     kMax, do_mount}},
        {"powerctl",                {1,     1,    do_powerctl}},
        {"restart",                 {1,     1,    do_restart}},
        {"restorecon",              {1,     kMax, do_restorecon}},
        {"restorecon_recursive",    {1,     kMax, do_restorecon_recursive}},
        {"rm",                      {1,     1,    do_rm}},
        {"rmdir",                   {1,     1,    do_rmdir}},
        {"setprop",                 {2,     2,    do_setprop}},
        {"setrlimit",               {3,     3,    do_setrlimit}},
        {"start",                   {1,     1,    do_start}},
        {"stop",                    {1,     1,    do_stop}},
        {"swapon_all",              {1,     1,    do_swapon_all}},
        {"symlink",                 {2,     2,    do_symlink}},
        {"sysclktz",                {1,     1,    do_sysclktz}},
        {"trigger",                 {1,     1,    do_trigger}},
        {"verity_load_state",       {0,     0,    do_verity_load_state}},
        {"verity_update_state",     {0,     0,    do_verity_update_state}},
        {"wait",                    {1,     2,    do_wait}},
        {"write",                   {2,     2,    do_write}},
    };
    return builtin_functions;
}

上面代码的每项的最后一项就是Action中每个command所对应的执行函数。

向执行队列中添加其他action

介绍完init进程解析init.rc文件的过程后,我们继续将视角拉回到init进程的main函数:

ActionManager& am = ActionManager::GetInstance();

am.QueueEventTrigger("early-init");

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

// Trigger all the boot actions to get us started.
am.QueueEventTrigger("init");

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

// Don't mount filesystems or start core system services in charger mode.
std::string bootmode = property_get("ro.bootmode");
if (bootmode == "charger") {
    am.QueueEventTrigger("charger");
} else {
    am.QueueEventTrigger("late-init");
}

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

从上面的代码可以看出,接下来init进程中调用了大量的QueueEventTrigger和QueueBuiltinAction函数。

void ActionManager::QueueEventTrigger(const std::string& trigger) {
    trigger_queue_.push(std::make_unique<EventTrigger>(trigger));
}

void ActionManager::QueueBuiltinAction(BuiltinFunction func, const std::string& name) {
    //创建action
    auto action = std::make_unique<Action>(true);
    std::vector<std::string> name_vector{name};

    //保证唯一性
    if (!action->InitSingleTrigger(name)) {
        return;
    }

    //创建action的cmd,指定执行函数和参数
    action->AddCommand(func, name_vector);

    trigger_queue_.push(std::make_unique<BuiltinTrigger>(action.get()));
    actions_.emplace_back(std::move(action));
}

此处QueueEventTrigger函数就是利用参数构造EventTrigger,然后加入到trigger_queue_中。后续init进程处理trigger事件时,将会触发相应的操作。根据前文的分析,我们知道实际上就是将action_list中,对应trigger与第一个参数匹配的action,加入到运行队列action_queue中。

QueueBuiltinAction函数中构造新的action加入到actions_中,第一个参数作为新建action携带cmd的执行函数;第二个参数既作为action的trigger name,也作为action携带cmd的参数。
继续main函数主流程

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

    //以下决定timeout的时间,将影响while循环的间隔
    int timeout = -1;
    //有进程需要重启时,等待该进程重启
    if (process_needs_restart) {
        timeout = (process_needs_restart - gettime()) * 1000;
        if (timeout < 0)
            timeout = 0;
    }

    //有action待处理,不等待
    if (am.HasMoreCommands()) {
        timeout = 0;
    }

    //bootchart_sample应该是进行性能数据采样
    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进程将所有需要操作的action加入运行队列后, 进入无限循环过程,不断处理运行队列中的事件,同时进行重启service等操作。

ExecuteOneCommand中的主要部分如下图所示:

void ActionManager::ExecuteOneCommand() {
    // Loop through the trigger queue until we have an action to execute
    //当有可执行action或trigger queue为空时结束
    while (current_executing_actions_.empty() && !trigger_queue_.empty()) {
        //轮询actions链表
        for (const auto& action : actions_) {
            //依次查找trigger表
            if (trigger_queue_.front()->CheckTriggers(*action)) {
                //当action与trigger对应时,就可以执行当前action
                //一个trigger可以对应多个action,均加入current_executing_actions_
                current_executing_actions_.emplace(action.get());
            }
        }
        //trigger event出队
        trigger_queue_.pop();
    }

    if (current_executing_actions_.empty()) {
        return;
    }

    //每次只执行一个action,下次init进程while循环时,跳过上面的while循环,接着执行
    auto action = current_executing_actions_.front();

    if (current_command_ == 0) {
        std::string trigger_name = action->BuildTriggersString();
        INFO("processing action (%s)\n", trigger_name.c_str());
    }

    //实际的执行过程,此处仅处理当前action中的一个cmd
    action->ExecuteOneCommand(current_command_);

    //适当地清理工作,注意只有当前action中所有的command均执行完毕后,才会将该action从current_executing_actions_移除
    // If this was the last command in the current action, then remove
    // the action from the executing list.
    // If this action was oneshot, then also remove it from actions_.
    ++current_command_;
    if (current_command_ == action->NumCommands()) {
        current_executing_actions_.pop();
        current_command_ = 0;
        if (action->oneshot()) {
            auto eraser = [&action] (std::unique_ptr<Action>& a) {
                return a.get() == action;
            };
            actions_.erase(std::remove_if(actions_.begin(), actions_.end(), eraser));
        }
    }
}

void Action::ExecuteCommand(const Command& command) const {
    Timer t;
    //执行该command对应的处理函数
    int result = command.InvokeFunc();
    ........
}

从代码可以看出,当while循环不断调用ExecuteOneCommand函数时,将按照trigger表的顺序,依次取出action链表中与trigger匹配的action。
每次均执行一个action中的一个command对应函数(一个action可能携带多个command)。
当一个action所有的command均执行完毕后,再执行下一个action。
当一个trigger对应的action均执行完毕后,再执行下一个trigger对应action。

restart_processes函数负责按需重启service,代码如下所示:

static void restart_processes() {
    process_needs_restart = 0;
    ServiceManager::GetInstance().ForEachServiceWithFlags(
        SVC_RESTARTING,
        [] (Service* s) {
            s->RestartIfNeeded(process_needs_restart);
        });
}

从上面可以看出,该函数轮询service对应的链表,对于有SVC_RESTARING标志的service执行RestartIfNeeded(如上文所述,当子进程终止时,init进程会将可被重启进程的服务标志位置为SVC_RESTARTING)。
如下代码所示,restart_service_if_needed可以重新启动服务:

void Service::RestartIfNeeded(time_t& process_needs_restart)(struct service *svc)
{
    time_t next_start_time = svc->time_started + 5;

    //两次服务启动时间的间隔要大于5s
    if (next_start_time <= gettime()) {
        svc->flags &= (~SVC_RESTARTING);
        //满足时间间隔的要求后,重启服务
        //Start将会重新fork服务进程,并做相应的配置
        Start(svc, NULL);
        return;
    }

    //更新main函数中,while循环需要等待的时间
    if ((next_start_time < process_needs_restart) ||
        (process_needs_restart == 0)) {
        process_needs_restart = next_start_time;
    }
}

总结

关于配置文件的解析Android6.0和7.0整体的流程几乎不变,但7.0为了更好的封装性,修改了解析文件后存储用的数据结构,同时引入了一些多态相关的内容。
虽然人要往后看,但6.0中还是有很多有意思的地方,比如keywords.h的宏替换及利用listnode来组织通用的链表等。这部分内容,可以参阅一下《深入理解Android 卷I》,虽然书对应的代码比较老,但大体思想还是相似的。

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

推荐阅读更多精彩内容