Android启动流程之二 init.rc解析

一、init.rc

Android中利用rc文件配置一些需要提前执行的操作,在系统启动的时候解析并执行,为启动Android系统核心服务提供保障。可参考:http://androidxref.com/9.0.0_r3/xref/system/core/init/README.md

rc文件以行为单位,一行定义一个语句,使用#作为注释。rc语法核心包括

  • Action
  • Service
  • Command
  • Options

其中需要注意的是Action和Service的名称是唯一的。
rc文件是可以通过import语句来导入其他rc文件的,例如/init.rc就引入包含了其他文件


import /init.environ.rc
import /init.usb.rc
import /init.${ro.hardware}.rc
import /vendor/etc/init/hw/init.${ro.hardware}.rc
import /init.usb.configfs.rc
import /init.${ro.zygote}.rc

1.1、 Action

以on开头,通过触发器trigger来决定对应的service什么时候执行,执行的时机有

  • on early-init -> 初始化的早期触发
  • on init -> 初始化阶段触发
  • on late-init -> 初始化后期触发
  • on boot/charger -> 系统启动或者充电时触发
  • on property:<key>=<value> -> 满足条件时触发
    定义语法
on <trigger> #触发条件
    <command> #触发命令
    <command1>#第二个触发命令,可以执行多个命令

1.2、Service

Service,顾名思义就是需要启动的服务,以service开头,init启动后会由init进程启动它作为子进程。
定义:

service <name> <pathname> [<argument>]*  #可以有多个参数
    <option>
    <option> #可以有多个option

例子

service servicemanager /system/bin/servicemanager

定义的就是名称为servicemanager的服务,对应的执行文件为system/bin/servicemanager,因此在启动服务前需要判断服务对应的文件是否存在

1.3、Command

Command主要是一些常用的操作命令,例如:

  • class_start<service_class_name> -> 启动属于同一个class的所有服务
  • start<service_name> ->启动指定的服务
  • stop <service_name> ->停止正在运行的服务
  • setprop<name> <value> -> 设置系统属性值
  • symlink <target><sym_link> ->创建连接到target的sym_link符号连接
  • write<path><string> -> 向path路径下写入字符串
  • exec -> fork 并执行,init进程会被阻塞,知道执行完毕
  • export -> 设置环境变量

1.4、Options

Option是Service配合使用的可选操作项

  • disable -> 不跟随class自动启动,只有跟随service name才启动
  • oneshot -> service执行完毕退出后不会再重启
  • user/group -> 设置service的用户和群组,默认是root
  • class -> 设置所属的类名,service绑定的class启动或者退出时,service也会启动或者退出,默认是default
  • onrestart ->服务重启时执行
  • socket -> 创建名为/dev/socket/name的socket
  • critical -> 在规定的时间内service如果不断重启,系统会重启进入recovery模式

1.5 demo

#初始化早期执行
on early-init
    # Set init and its forked children's oom_adj.
    # 往/proc/1/oom_score_adj中写入 -1000
    write /proc/1/oom_score_adj -1000
    #启动ueventd服务
    start ueventd

#初始化时执行
on init
  mkdir /dev/stune
  write /proc/sys/kernel/sched_tunable_scaling 0

#当sys.boot_from_charger_mode为1时执行
on property:sys.boot_from_charger_mode=1
    class_stop charger
    trigger late-init

#初始化后期执行
on late-init
    trigger early-fs

# 定义一个名为flash_recovery,执行文件在/system/bin/install-recovery.sh的服务
# 该服务是oneshot的,执行完成后不再重启,且启动或者退出动作和main服务一起
service flash_recovery /system/bin/install-recovery.sh
    class main
    oneshot

二、init.rc解析

《Android启动流程之一 init进程启动》中有提到,init进程的main函数中有加载init.rc文件,对应的执行函数为:

2.1、LoadBootScripts

先判断是否自定义了rc文件,如果没有的话就读取默认的/init.rc

system/core/init/init.cpp

static void LoadBootScripts(ActionManager& action_manager, ServiceList& service_list) {
    //创建解析器
    Parser parser = CreateParser(action_manager, service_list);
    //先判断是否自定义了rc文件,如果没有的话就读取默认的/init.rc
    std::string bootscript = GetProperty("ro.boot.init_rc", "");
    if (bootscript.empty()) {
        //解析/init.rc
        parser.ParseConfig("/init.rc");
        //以下这些rc文件如果第一次解析失败,就放到完一点的解析队列中
        if (!parser.ParseConfig("/system/etc/init")) {
            late_import_paths.emplace_back("/system/etc/init");
        }
        if (!parser.ParseConfig("/product/etc/init")) {
            late_import_paths.emplace_back("/product/etc/init");
        }
        if (!parser.ParseConfig("/odm/etc/init")) {
            late_import_paths.emplace_back("/odm/etc/init");
        }
        if (!parser.ParseConfig("/vendor/etc/init")) {
            late_import_paths.emplace_back("/vendor/etc/init");
        }
    } else {
        //解析自定义的rc文件
        parser.ParseConfig(bootscript);
    }
}

这个过程中最主要的一个操作是,调用CreateParser方法创建了rc文件的解析器,有三种解析器,分别是:

  • service解析器,用于解析service开头的rc语句,对应的类是service.cpp
  • on 解析器,用于解析on 开头的语句.对应的类是action_parser.cpp
  • import解析器,用于解析import开头的语句,该语句定义了让rc文件之间可以互相包含进来.对应的类是import_parser.cpp

类图为:


Parser解析器类图

Parser中主要有四个关键方法:

  • ParseSection 该方法主要解析主要的指令,例如service,on,import等
  • ParseLineSection 该方法主要是解析指令service,on后面跟随的Command和options
  • EndSection 解析每个命令配置完成后调用,这时解析完成的数据需要在这里进行保存
  • EndFile 解析完成一个文件时调用

2.2、ParseConfig & ParseData方法

ParseConfig会调用两个参数的ParseConfig,这里会判断传入的path是文件还是目录,如果是文件的话调用ParseConfigFile解析,如果是目录的话调用ParseConfigDir遍历目录下的rc文件全部解析,这个步骤可以忽略不用太关注,最终都会调用到ParseData方法真正解析文件,整个解析过程会扫描配置文件,解析指令存放到链表中,在init进程中执行。
ParseData函数

system/core/init/parser.cpp

//第一个参数:文件路径,第二个参数:文件内容,第三个参数:错误信息
void Parser::ParseData(const std::string& filename, const std::string& data, size_t* parse_errors) {
    // TODO: Use a parser with const input and remove this copy
    //开始之前为了安全考虑,先复制一份文件内容进行解析
    std::vector<char> data_copy(data.begin(), data.end());
    //在读取到的数据最后添加一个‘\0’
    data_copy.push_back('\0');

    parse_state state;
    //初始化从第0行
    state.line = 0;
    //开始指针指向0
    state.ptr = &data_copy[0];
    state.nexttoken = 0;

    SectionParser* section_parser = nullptr;
    int section_start_line = -1;
    //存放读取到的配置文本
    std::vector<std::string> args;

    auto end_section = [&] {
        if (section_parser == nullptr) return;
        //调用EndSection方法保存读取到的配置信息
        if (auto result = section_parser->EndSection(); !result) {
            (*parse_errors)++;
            LOG(ERROR) << filename << ": " << section_start_line << ": " << result.error();
        }

        section_parser = nullptr;
        section_start_line = -1;
    };

    for (;;) {
        switch (next_token(&state)) {
            case T_EOF: //字符串解析结束
                end_section();
                return;
            case T_NEWLINE://解析完一行,新开始一行的解析
                //开始读取新的一行,line加1
                state.line++;
                //如果始终没有读取到文本则进入下一个循环,读取下一行
                if (args.empty()) break;
                // If we have a line matching a prefix we recognize, call its callback and unset any
                // current section parsers.  This is meant for /sys/ and /dev/ line entries for
                // uevent.
                //这里主要是针对ueventd.rc文件的处理,识别 /sys/ /dev/开头的配置
                for (const auto& [prefix, callback] : line_callbacks_) {
                    if (android::base::StartsWith(args[0], prefix)) {
                        end_section();

                        if (auto result = callback(std::move(args)); !result) {
                            (*parse_errors)++;
                            LOG(ERROR) << filename << ": " << state.line << ": " << result.error();
                        }
                        break;
                    }
                }
                if (section_parsers_.count(args[0])) {
                    //切换parser之前先确保上一次的解析完成
                    end_section();
                    //args[0]表示是section的开头文本,也就是service,on,import三者之一
                    //依据这个来匹配到对应的Parser
                    section_parser = section_parsers_[args[0]].get();
                    section_start_line = state.line;
                    //解析Action关键字行
                    if (auto result =
                            section_parser->ParseSection(std::move(args), filename, state.line);
                        !result) {
                        (*parse_errors)++;
                        LOG(ERROR) << filename << ": " << state.line << ": " << result.error();
                        section_parser = nullptr;
                    }
                } else if (section_parser) {
                    //解析Action对应的命令行
                    if (auto result = section_parser->ParseLineSection(std::move(args), state.line);
                        !result) {
                        (*parse_errors)++;
                        LOG(ERROR) << filename << ": " << state.line << ": " << result.error();
                    }
                }
                args.clear();
                break;
            case T_TEXT://解析到字符串
                //读取到文本就添加到args中,一行保存一次
                //state是一个结构体
                //struct parse_state
                //{
                //  char *ptr;      // 要解析的字符串
                //  char *text;     // 解析到的字符串,可以理解为返回一行的数据
                //  int line;       // 解析到第行数
                //  int nexttoken;  // 解析状态,有 T_EOF、T_NEWLINE、T_TEXT 三种
                //};
                //
                //
                args.emplace_back(state.text);
                break;
        }
    }
}

ParseData函数中我们需要注意以下几点

  • arg[0] 是每一行读取到的第一个字符串,解析过程中会依据这个字符串来动态切换解析器,字符串和解析器的对应关系为:service -> ServiceParser, on -> ActionParser,import -> ImportParser
  • parse_state 是一个结构体
struct parse_state{
   char *ptr; //要解析的字符串
   char *text;//解析到的字符串,一般为一行字符串
   int line; //解析到第几行
   int nexttoken; //解析状态,有 T_EOF、T_NEWLINE、T_TEXT 三种
}

  • nexttoken,解析状态,T_EOF 表示字符串解析结束,T_NEWLINE 表示解析完一行的数据,T_TEXT 表示解析到一个单词

2.3、ActionParser

上面有提供Parser的类图大致结构,在解析过程中实际的解析会在对应的Parser中进行。ActionParser主要是解析以 on开头的一系列命令,Action决定对应的动作什么时候会执行,解析完成后Action会被添加到存放Action的队列中。Action的每个命令是按顺序排队执行,每个命令依次进入执行状态。
ActionParser实际上就是把配置解析为Action结构,对应的Action类定义在system/core/init/action.h中

    ......
    std::map<std::string, std::string> property_triggers_;
    std::string event_trigger_;
    std::vector<Command> commands_;
    ......

Commands存放Action中定义的一系列Command操作。

2.3.1 ParseSection

Result<Success> ActionParser::ParseSection(std::vector<std::string>&& args,
                                           const std::string& filename, int line) {
    //这里判断Action是否有定义对应的trigger触发器,如果没有的话就返回失败
    std::vector<std::string> triggers(args.begin() + 1, args.end());
    if (triggers.size() < 1) {
        return Error() << "Actions must have a trigger";
    }

   ......

    std::string event_trigger;
    std::map<std::string, std::string> property_triggers;
    //解析Action的trigger触发器,提取event_trigger和property_triggers
    if (auto result = ParseTriggers(triggers, action_subcontext, &event_trigger, &property_triggers);
        !result) {
        return Error() << "ParseTriggers() failed: " << result.error();
    }
    //创建Action对象
    auto action = std::make_unique<Action>(false, action_subcontext, filename, line, event_trigger,
                                           property_triggers);

    action_ = std::move(action);
    return Success();
}

这里传递给ParseTriggers方法的triggers是去除了命令关键字on之外的触发器信息,ParseTriggers会判断是否是属性变化触发的触发器,如果是属性变化的触发器的话填充到property_triggers,否则填充到event_trigger。


Result<Success> ParseTriggers(const std::vector<std::string>& args, Subcontext* subcontext,
                              std::string* event_trigger,
                              std::map<std::string, std::string>* property_triggers) {
    const static std::string prop_str("property:");
    for (std::size_t i = 0; i < args.size(); ++i) {
        if (args[i].empty()) {
            return Error() << "empty trigger is not valid";
        }
        //trigger触发器需要通过 && 连接
        //例如:on zygote-start && property:ro.crypto.state=encrypted && property:ro.crypto.type=file
        if (i % 2) {
            if (args[i] != "&&") {
                return Error() << "&& is the only symbol allowed to concatenate actions";
            } else {
                continue;
            }
        }
        //判断是否是property:开头的配置条件,Action中满足条件时会被执行,这里主要是属性变化触发器的解析
        //这里会把属性变化的触发条件填充到property_triggers中
        if (!args[i].compare(0, prop_str.length(), prop_str)) {
            if (auto result = ParsePropertyTrigger(args[i], subcontext, property_triggers);
                !result) {
                return result;
            }
        } else {
            if (!event_trigger->empty()) {
                return Error() << "multiple event triggers are not allowed";
            }
            //如果不是属性变化的触发器,则填充到event_trigger中
            *event_trigger = args[i];
        }
    }

    return Success();
}

ParseTriggers做了两件事情

  • 触发器语法判断 && 连接符
  • 触发器依据是否是属性变化区分不同触发器提取到不同的数据结构里面

2.3.2、ParseLineSection

解析完成命令关键字部分后就会解析对应的Command子模块,ParseLineSection直接调用了Action的AddCommand函数


Result<Success> Action::AddCommand(const std::vector<std::string>& args, int line) {
    if (!function_map_) {
        return Error() << "no function map available";
    }
    //通过args调用FindFunction查找对应的处理命令,类似解析命令关键字行的做法
    //利用args[0]进行匹配,function_map在init进程启动的时候已经初始化了。
    auto function = function_map_->FindFunction(args);
    if (!function) return Error() << function.error();

    commands_.emplace_back(function->second, function->first, args, line);
    return Success();
}

FindFunction函数在keyword_map.h中会通过args[0]以及具体的字符内容匹配到最终的命令参数,最后填充到commands_中
function_map_的内容如下(system/core/init/builtins.cpp)

 static const Map builtin_functions = {
        {"bootchart",               {1,     1,    {false,  do_bootchart}}},
        {"chmod",                   {2,     2,    {true,   do_chmod}}},
        {"chown",                   {2,     3,    {true,   do_chown}}},
        {"class_reset",             {1,     1,    {false,  do_class_reset}}},
        {"class_restart",           {1,     1,    {false,  do_class_restart}}},
        {"class_start",             {1,     1,    {false,  do_class_start}}},
        {"class_stop",              {1,     1,    {false,  do_class_stop}}},
        {"copy",                    {2,     2,    {true,   do_copy}}},
        {"domainname",              {1,     1,    {true,   do_domainname}}},
        {"enable",                  {1,     1,    {false,  do_enable}}},
        {"exec",                    {1,     kMax, {false,  do_exec}}},
        {"exec_background",         {1,     kMax, {false,  do_exec_background}}},
        {"exec_start",              {1,     1,    {false,  do_exec_start}}},
        {"export",                  {2,     2,    {false,  do_export}}},
        {"hostname",                {1,     1,    {true,   do_hostname}}},
        {"ifup",                    {1,     1,    {true,   do_ifup}}},
        {"init_user0",              {0,     0,    {false,  do_init_user0}}},
        {"insmod",                  {1,     kMax, {true,   do_insmod}}},
        {"installkey",              {1,     1,    {false,  do_installkey}}},
        {"load_persist_props",      {0,     0,    {false,  do_load_persist_props}}},
        {"load_system_props",       {0,     0,    {false,  do_load_system_props}}},
        {"loglevel",                {1,     1,    {false,  do_loglevel}}},
        {"mkdir",                   {1,     4,    {true,   do_mkdir}}},
        // TODO: Do mount operations in vendor_init.
        // mount_all is currently too complex to run in vendor_init as it queues action triggers,
        // imports rc scripts, etc.  It should be simplified and run in vendor_init context.
        // mount and umount are run in the same context as mount_all for symmetry.
        {"mount_all",               {1,     kMax, {false,  do_mount_all}}},
        {"mount",                   {3,     kMax, {false,  do_mount}}},
        {"umount",                  {1,     1,    {false,  do_umount}}},
        {"readahead",               {1,     2,    {true,   do_readahead}}},
        {"restart",                 {1,     1,    {false,  do_restart}}},
        {"restorecon",              {1,     kMax, {true,   do_restorecon}}},
        {"restorecon_recursive",    {1,     kMax, {true,   do_restorecon_recursive}}},
        {"rm",                      {1,     1,    {true,   do_rm}}},
        {"rmdir",                   {1,     1,    {true,   do_rmdir}}},
        {"setprop",                 {2,     2,    {true,   do_setprop}}},
        {"setrlimit",               {3,     3,    {false,  do_setrlimit}}},
        {"start",                   {1,     1,    {false,  do_start}}},
        {"stop",                    {1,     1,    {false,  do_stop}}},
        {"swapon_all",              {1,     1,    {false,  do_swapon_all}}},
        {"symlink",                 {2,     2,    {true,   do_symlink}}},
        {"sysclktz",                {1,     1,    {false,  do_sysclktz}}},
        {"trigger",                 {1,     1,    {false,  do_trigger}}},
        {"verity_load_state",       {0,     0,    {false,  do_verity_load_state}}},
        {"verity_update_state",     {0,     0,    {false,  do_verity_update_state}}},
        {"wait",                    {1,     2,    {true,   do_wait}}},
        {"wait_for_prop",           {2,     2,    {false,  do_wait_for_prop}}},
        {"write",                   {2,     2,    {true,   do_write}}},
        #ifdef VENDOR_EDIT
        {"reload_policy",           {0,     0,    {true,   do_reload_policy}}},
        {"md",                      {1,     1,    {true,   do_md}}},
        {"copyall",                 {2,     2,    {true,   do_copyall}}},
        #if OP_FEATURE_UPDATE_RESERVE == 1
        {"mount_reserve",           {3,     kMax, {false,  do_mount_reserve}}},
        #endif
        #endif
    };

map对应的各字段,从左到右

  • 函数名
  • 最小参数个数
  • 最大参数个数
  • 处理的函数地址

2.3.3、EndSection

这里主要是将最终解析出来的Action保存到ActionManager的actions_中


Result<Success> ActionParser::EndSection() {
    if (action_ && action_->NumCommands() > 0) {
        action_manager_->AddAction(std::move(action_));
    }

    return Success();
}

void ActionManager::AddAction(std::unique_ptr<Action> action) {
    actions_.emplace_back(std::move(action));
}

2.4、ServiceParser

Service结构比较复杂,感兴趣的话可以阅读system/core/init/service.h文件。但是主要还是定义了Service的一些操作函数,rc文件中定义的一些命令和Options

2.4.1、ParseSection

Result<Success> ServiceParser::ParseSection(std::vector<std::string>&& args,
                                            const std::string& filename, int line) {
    //首先需要验证service配置是否正确,service配置必须包含
    //服务名和启动参数
    if (args.size() < 3) {
        return Error() << "services must have a name and a program";
    }

    const std::string& name = args[1];
    //验证服务名是否合法
    if (!IsValidName(name)) {
        return Error() << "invalid service name '" << name << "'";
    }

   ......
    //服务启动参数
    std::vector<std::string> str_args(args.begin() + 2, args.end());
    service_ = std::make_unique<Service>(name, restart_action_subcontext, str_args);
    return Success();
}

2.4.2、ParseLineSection

ParseLineSection主要是解析service命令快接下来Options的指令块。


Result<Success> ServiceParser::ParseLineSection(std::vector<std::string>&& args, int line) {
    return service_ ? service_->ParseLine(std::move(args)) : Success();
}


Result<Success> Service::ParseLine(const std::vector<std::string>& args) {
    static const OptionParserMap parser_map;
    auto parser = parser_map.FindFunction(args);

    if (!parser) return parser.error();

    return std::invoke(*parser, this, args);
}

parser_map是固定的,内容如下,参数分别代表:处理关键字、最小参数个数、最大参数个数、处理函数地址。:

static const Map option_parsers = {
        {"capabilities",
                        {1,     kMax, &Service::ParseCapabilities}},
        {"class",       {1,     kMax, &Service::ParseClass}},
        {"console",     {0,     1,    &Service::ParseConsole}},
        {"critical",    {0,     0,    &Service::ParseCritical}},
        {"disabled",    {0,     0,    &Service::ParseDisabled}},
        {"enter_namespace",
                        {2,     2,    &Service::ParseEnterNamespace}},
        {"group",       {1,     NR_SVC_SUPP_GIDS + 1, &Service::ParseGroup}},
        {"interface",   {2,     2,    &Service::ParseInterface}},
        {"ioprio",      {2,     2,    &Service::ParseIoprio}},
        {"priority",    {1,     1,    &Service::ParsePriority}},
        {"keycodes",    {1,     kMax, &Service::ParseKeycodes}},
        {"oneshot",     {0,     0,    &Service::ParseOneshot}},
        {"onrestart",   {1,     kMax, &Service::ParseOnrestart}},
        {"override",    {0,     0,    &Service::ParseOverride}},
        {"oom_score_adjust",
                        {1,     1,    &Service::ParseOomScoreAdjust}},
        {"memcg.swappiness",
                        {1,     1,    &Service::ParseMemcgSwappiness}},
        {"memcg.soft_limit_in_bytes",
                        {1,     1,    &Service::ParseMemcgSoftLimitInBytes}},
        {"memcg.limit_in_bytes",
                        {1,     1,    &Service::ParseMemcgLimitInBytes}},
        {"namespace",   {1,     2,    &Service::ParseNamespace}},
        {"rlimit",      {3,     3,    &Service::ParseProcessRlimit}},
        {"seclabel",    {1,     1,    &Service::ParseSeclabel}},
        {"setenv",      {2,     2,    &Service::ParseSetenv}},
        {"shutdown",    {1,     1,    &Service::ParseShutdown}},
        {"socket",      {3,     6,    &Service::ParseSocket}},
        {"file",        {2,     2,    &Service::ParseFile}},
        {"user",        {1,     1,    &Service::ParseUser}},
        {"writepid",    {1,     kMax, &Service::ParseWritepid}},
    };

3.4.3 EndSection

EndSection 将创建并填充完成的 Sevice 对象加入到 services_ 链表中,首先会先依据service name在链表中查找是否已经有了,如果有的话需要先移除,再重新创建新的服务保存到链表中


Result<Success> ServiceParser::EndSection() {
    if (service_) {
        //先找到旧的service并移除
        Service* old_service = service_list_->FindService(service_->name());
        if (old_service) {
            if (!service_->is_override()) {
                return Error() << "ignored duplicate definition of service '" << service_->name()
                               << "'";
            }

            service_list_->RemoveService(*old_service);
            old_service = nullptr;
        }
        //添加service到链表中
        service_list_->AddService(std::move(service_));
    }

    return Success();
}

三、总结

总结来看,init.rc文件会依据不同的命令切换不同的Parser进行解析,解析过程中主要是执行ParseSection,ParseLineSection和EndSection来进行解析。此过程中会包含语法的检查,数据储存等。大致的过程如下:


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

推荐阅读更多精彩内容