OHOS3.0啟動流程分析丨init階段
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
init階段
內(nèi)核啟動完是init階段,源碼的路徑在 base\startup\init_lite\services\src\main.c,雖然文件夾命名為init_lite,但是init部分的代碼是小型系統(tǒng)(small system)和標(biāo)準(zhǔn)系統(tǒng)(standard system)通用的。相關(guān)碼倉啟動模塊init進程 (gitee.com)
接下來參考linux內(nèi)核梳理下啟動流程,
1.關(guān)閉輸入輸出
/dev/null,空設(shè)備,特殊的設(shè)備文件,丟棄一切寫入其中的數(shù)據(jù)(但報告寫入操作成功),讀取它則會立即得到一個EOF。其作用是對stdin/stdout/stderr進行保護,把文件描述符0,1,2分配出去,以后再分配的時候就不會將stdin/stdout/stderr打開,以達到保護目的。
- // base\startup\init_lite\services\src\device.c
 - void CloseStdio(void)
 - {
 - int fd = open("/dev/null", O_RDWR | O_CLOEXEC);
 - if (fd < 0) {
 - return;
 - }
 - dup2(fd, 0);
 - dup2(fd, 1);
 - dup2(fd, 2);
 - close(fd);
 - }
 
2.在串口打印調(diào)試信息
寫入/dev/kmsg的信息,可以在dmesg(開機信息)中查看。
- // base\startup\init_lite\services\log\init_log.c
 - void OpenLogDevice(void)
 - {
 - int fd = open("/dev/kmsg", O_WRONLY | O_CLOEXEC, S_IRUSR | S_IWUSR | S_IRGRP | S_IRGRP);
 - if (fd >= 0) {
 - g_fd = fd;
 - }
 - return;
 - }
 
標(biāo)準(zhǔn)系統(tǒng)空實現(xiàn),不深究了。
- // base\startup\init_lite\services\src\main.c
 - static void PrintSysInfo()
 - {
 - #ifdef OHOS_LITE
 - const char* sysInfo = GetVersionId();
 - if (sysInfo != NULL) {
 - INIT_LOGE("%s", sysInfo);
 - return;
 - }
 - INIT_LOGE("main, GetVersionId failed!");
 - #endif
 - }
 
3.掛載目錄,建立索引節(jié)點
- // base\startup\init_lite\services\src\device.c
 - void MountBasicFs(void)
 - {
 - mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755"
 - mkdir("/dev/pts", S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH)
 - mount("devpts", "/dev/pts", "devpts", 0, NULL)
 - mount("proc", "/proc", "proc", 0, "hidepid=2")
 - mount("sysfs", "/sys", "sysfs", 0, NULL)
 - mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL)
 - }
 - void CreateDeviceNode(void)
 - {
 - mknod("/dev/kmsg", S_IFCHR | DEFAULT_NO_AUTHORITY_MODE, makedev(MEM_MAJOR, DEV_KMSG_MINOR)
 - mknod("/dev/null", S_IFCHR | DEFAULT_RW_MODE, makedev(MEM_MAJOR, DEV_NULL_MINOR)
 - mknod("/dev/random", S_IFCHR | DEFAULT_RW_MODE, makedev(MEM_MAJOR, DEV_RANDOM_MINOR)
 - mknod("/dev/urandom", S_IFCHR | DEFAULT_RW_MODE, makedev(MEM_MAJOR, DEV_URANDOM_MINOR)
 - }
 
4.開啟DevKmsg
注釋很清楚了,printk_devkmsg默認是流控的,設(shè)置為on取消流控。
- // base\startup\init_lite\services\log\init_log.c
 - void EnableDevKmsg(void)
 - {
 - /* printk_devkmsg default value is ratelimit, We need to set "on" and remove the restrictions */
 - int fd = open("/proc/sys/kernel/printk_devkmsg", O_WRONLY | O_CLOEXEC, S_IRUSR | S_IWUSR | S_IRGRP | S_IRGRP);
 - ...
 - write(fd, "on", strlen("on") + 1);
 - close(fd);
 - fd = -1;
 - return;
 - }
 
5.建立Socket文件夾
/dev/unix/socket/不過這個干啥用的,還沒搞清楚。
- MakeSocketDir("/dev/unix/socket/", S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
 - // base\startup\init_lite\services\src\device.c
 - int MakeSocketDir(const char *path, mode_t mode)
 - {
 - int rc = mkdir("/dev/unix/", mode);
 - ...
 - rc = mkdir("/dev/unix/socket/", mode);
 - ...
 - }
 
6.Singnal初始化
- // base\startup\init_lite\services\src\init_signal_handler.c
 - void SignalInitModule()
 - {
 - int ret = uv_signal_init(uv_default_loop(), &g_sigchldHandler);
 - int ret1 = uv_signal_init(uv_default_loop(), &g_sigtermHandler);
 - if (ret != 0 && ret1 != 0) {
 - INIT_LOGW("initialize signal handler failed");
 - return;
 - }
 - if (uv_signal_start(&g_sigchldHandler, UVSignalHandler, SIGCHLD) != 0) {
 - INIT_LOGW("start SIGCHLD handler failed");
 - }
 - if (uv_signal_start(&g_sigtermHandler, UVSignalHandler, SIGTERM) != 0) {
 - INIT_LOGW("start SIGTERM handler failed");
 - }
 - }
 
7.執(zhí)行命令腳本文件
兼容常規(guī)的*.rc文件,是執(zhí)行l(wèi)inux運行命令的腳本文件。
- // base\startup\init_lite\services\src\init_adapter.c
 - void ExecuteRcs()
 - {
 - #if (defined __LINUX__) && (defined NEED_EXEC_RCS_LINUX)
 - pid_t retPid = fork();
 - if (retPid < 0) {
 - INIT_LOGE("ExecuteRcs, fork failed! err %d.", errno);
 - return;
 - }
 - // child process
 - if (retPid == 0) {
 - INIT_LOGI("ExecuteRcs, child process id %d.", getpid());
 - if (execle("/bin/sh", "sh", "/etc/init.d/rcS", NULL, NULL) != 0) {
 - INIT_LOGE("ExecuteRcs, execle failed! err %d.", errno);
 - }
 - _exit(0x7f); // 0x7f: user specified
 - }
 - // init process
 - sem_t sem;
 - if (sem_init(&sem, 0, 0) != 0) {
 - INIT_LOGE("ExecuteRcs, sem_init failed, err %d.", errno);
 - return;
 - }
 - SignalRegWaitSem(retPid, &sem);
 - // wait until rcs process exited
 - if (sem_wait(&sem) != 0) {
 - INIT_LOGE("ExecuteRcs, sem_wait failed, err %d.", errno);
 - }
 - #endif
 - }
 
8.【重要】解析并執(zhí)行*.cfg文件。
OHOS3.0中的命令腳本文件是*.cfg,采用JSON格式,存儲的信息更多一些。前面都是啟動系統(tǒng)的一些準(zhǔn)備工作,而接下來才是重要部分。還是用個表格來分析。
base\startup\init_lite\services\src\init_read_cfg.c
a.初始化服務(wù)參數(shù)的工作區(qū)
- // base\startup\init_lite\services\param\service\param_service.c
 - void InitParamService()
 - {
 - int ret = InitParamWorkSpace(&g_paramWorkSpace, 0, g_initContext);
 - PARAM_CHECK(ret == 0, return, "Init parameter workspace fail");
 - }
 
b.解析init.cfg文件
- ./base/startup/init_lite/services/etc/init.cfg
 - ./base/update/updater/services/etc/init.cfg
 - ./out/ohos-arm-release/packages/phone/system/etc/init.cfg
 - ./out/ohos-arm-release/packages/phone/updater/etc/init.cfg
 - ./out/ohos-arm-release/obj/base/startup/init_lite/services/base/startup/init_lite/services/etc/init.cfg
 - ./out/ohos-arm-release/obj/base/update/updater/services/base/update/updater/services/etc/init.cfg
 - ./device/hisilicon/hi3516dv300/updater/init.cfg
 
編譯框架使用的哪個init.cfg還有待確定。
- // base\startup\init_lite\services\src\init_read_cfg.c
 - void ParseInitCfg(const char *configFile) // 文件路徑"/etc/init.cfg"
 - {
 - ...
 - char *fileBuf = ReadFileToBuf(configFile); // 讀取*.cfg文件
 - cJSON* fileRoot = cJSON_Parse(fileBuf); // 解析成JSON
 - ...
 - ParseInitCfgContents(fileRoot);
 - ...
 - }
 - ----------------------------------------------------------------------
 - static void ParseInitCfgContents(const cJSON *root) // JSON格式的init.cfg
 - {
 - ...
 - ParseAllServices(root); // ①.解析"services"部分,并執(zhí)行
 - ...
 - ParseAllJobs(root); // Liteos走這邊
 - ParseTriggerConfig(root); // Linux走這邊,②.解析"jobs"部分,并執(zhí)行
 - ...
 - ParseAllImports(root); // ③.提取init.cfg中"import"的部分,并執(zhí)行
 - }
 
①.解析"services"部分,并執(zhí)行
- // base\startup\init_lite\services\src\init_service_manager.c
 - void ParseAllServices(const cJSON* fileRoot)
 - {
 - int servArrSize = 0;
 - cJSON* serviceArr = GetArrItem(fileRoot, &servArrSize, "services");
 - ... // 截取 init.cfg 中的"services"部分
 - ///////////////////////////////// 參考格式
 - "services" : [{
 - "name" : "ueventd",
 - "path" : ["/system/bin/ueventd"],
 - "critical" : 1
 - }, {
 - "name" : "console",
 - "path" : ["/system/bin/sh"],
 - "disabled" : 1,
 - "console" : 1,
 - "uid" : "root",
 - "gid" : ["shell", "log", "readproc"]
 - }]
 - ///////////////////////////////// 參考格式
 - ... // 默認服務(wù)數(shù)量不能超過100
 - Service* retServices = (Service*)realloc(g_services, sizeof(Service) * (g_servicesCnt + servArrSize));
 - ...
 - // Skip already saved services,
 - Service* tmp = retServices + g_servicesCnt;
 - if (memset_s(tmp, sizeof(Service) * servArrSize, 0, sizeof(Service) * servArrSize) != EOK) {
 - free(retServices);
 - retServices = NULL;
 - return;
 - }
 - // 【重要】然后使用一個for循環(huán)遍歷服務(wù)數(shù)組
 - for (int i = 0; i < servArrSize; ++i) {
 - cJSON* curItem = cJSON_GetArrayItem(serviceArr, i);
 - if (CheckServiceKeyName(curItem) != SERVICE_SUCCESS) {
 - ReleaseServiceMem(&tmp[i]);
 - tmp[i].attribute |= SERVICE_ATTR_INVALID;
 - continue;
 - }
 - int ret = ParseOneService(curItem, &tmp[i]);
 - if (ret != SERVICE_SUCCESS) { // 如果服務(wù)啟動失敗
 - // release resources if it fails
 - ReleaseServiceMem(&tmp[i]);
 - tmp[i].attribute |= SERVICE_ATTR_INVALID;
 - INIT_LOGE("Parse information for service %s failed. ", tmp[i].name);
 - continue;
 - } else { // 如果服務(wù)啟動成功
 - INIT_LOGD("service[%d] name=%s, uid=%d, critical=%d, disabled=%d",
 - i, tmp[i].name, tmp[i].servPerm.uID, (tmp[i].attribute & SERVICE_ATTR_CRITICAL) ? 1 : 0,
 - (tmp[i].attribute & SERVICE_ATTR_DISABLED) ? 1 : 0);
 - }
 - if (GetServiceSocket(curItem, &tmp[i]) != SERVICE_SUCCESS) {
 - if (tmp[i].socketCfg != NULL) {
 - FreeServiceSocket(tmp[i].socketCfg);
 - tmp[i].socketCfg = NULL;
 - }
 - }
 - if (GetServiceOnRestart(curItem, &tmp[i]) == SERVICE_FAILURE) {
 - INIT_LOGE("Failed Get Service OnRestart service");
 - }
 - }
 - // Increase service counter.
 - RegisterServices(retServices, servArrSize); // 最后注冊服務(wù)
 - }
 - ----------------------------------------------------------------------
 - void RegisterServices(Service* services, int servicesCnt)
 - {
 - if (services == NULL) {
 - return;
 - }
 - g_services = services;
 - g_servicesCnt += servicesCnt;
 - // 到這里init.cfg中的"services"部分就已經(jīng)解析并執(zhí)行完畢了。
 - }
 
②.解析"jobs"部分,并執(zhí)行
- // base\startup\init_lite\services\param\trigger\trigger_processor.c
 - int ParseTriggerConfig(const cJSON *fileRoot)
 - {
 - ...
 - int ret = InitTriggerWorkSpace(&g_triggerWorkSpace); // 初始化觸發(fā)器的工作空間
 - ...
 - cJSON *triggers = cJSON_GetObjectItemCaseSensitive(fileRoot, "jobs"); // 提取init.cfg中"jobs"的部分
 - ...
 - int size = cJSON_GetArraySize(triggers);
 - ...
 - for (int i = 0; i < size; ++i) {
 - cJSON *item = cJSON_GetArrayItem(triggers, i);
 - ParseTrigger(&g_triggerWorkSpace, item);
 - }
 - return 0;
 - }
 - ----------------------------------------------------------------------
 - // 截取init.cfg中jobs段的部分代碼,格式如下
 - "jobs" : [{
 - "name" : "pre-init",
 - "cmds" : [
 - "write /proc/sys/kernel/sysrq 0",
 - ...
 - "mkdir /data",
 - ]
 - }, {
 - "name" : "init",
 - "cmds" : [
 - "copy /proc/cmdline /dev/urandom",
 - ...
 - "domainname localdomain"
 - ]
 - }, {
 - "name" : "param:sys.boot_from_charger_mode=1",
 - "condition" : "sys.boot_from_charger_mode=1",
 - "cmds" : [
 - "trigger post-init"
 - ]
 - },
 - ...
 - ],
 - // base\startup\init_lite\services\param\trigger\trigger_manager.c
 - int ParseTrigger(TriggerWorkSpace *workSpace, const cJSON *triggerItem)
 - {
 - ... // 提取init.cfg中jobs段的"name"的部分
 - char *name = cJSON_GetStringValue(cJSON_GetObjectItem(triggerItem, "name"));
 - ... // 提取init.cfg中jobs段的"condition"的部分
 - char *condition = cJSON_GetStringValue(cJSON_GetObjectItem(triggerItem, "condition"));
 - int index = GetTriggerIndex(name);
 - ...
 - u_int32_t offset = 0;
 - TriggerNode *trigger = GetTriggerByName(workSpace, name, &offset);
 - if (trigger == NULL) {
 - offset = AddTrigger(workSpace, index, name, condition);
 - PARAM_CHECK(offset > 0, return -1, "Failed to create trigger %s", name);
 - trigger = GetTriggerByIndex(workSpace, offset);
 - } else {
 - if (condition != NULL) {
 - PARAM_LOGE("Warning parseTrigger %s %s", name, condition);
 - }
 - }
 - PARAM_LOGD("ParseTrigger %s %u", name, offset);
 - // 添加命令行
 - cJSON* cmdItems = cJSON_GetObjectItem(triggerItem, "cmds"); // 提取init.cfg中jobs段的"cmds"的部分
 - ...
 - int cmdLinesCnt = cJSON_GetArraySize(cmdItems); // 獲取命令數(shù)量
 - ...
 - for (int i = 0; i < cmdLinesCnt; ++i) { // 循環(huán)執(zhí)行
 - char *cmdLineStr = cJSON_GetStringValue(cJSON_GetArrayItem(cmdItems, i));
 - ...
 - size_t cmdLineLen = strlen(cmdLineStr);
 - const char *matchCmd = GetMatchCmd(cmdLineStr);
 - if (matchCmd == NULL && strncmp(cmdLineStr, "trigger ", strlen("trigger ")) == 0) {
 - matchCmd = "trigger ";
 - }
 - ...
 - size_t matchLen = strlen(matchCmd);
 - if (matchLen == cmdLineLen) {
 - offset = AddCommand(workSpace, trigger, matchCmd, NULL);
 - } else {
 - offset = AddCommand(workSpace, trigger, matchCmd, cmdLineStr + matchLen);
 - }
 - PARAM_CHECK(offset > 0, continue, "Failed to add command %s", cmdLineStr);
 - }
 - return 0;
 - }
 
③.提取init.cfg中"import"的部分,并執(zhí)行
- // base\startup\init_lite\services\src\init_import.c
 - void ParseAllImports(const cJSON *root)
 - {
 - ///////////////////////////////// 參考格式
 - "import" : [
 - "/etc/init.usb.cfg",
 - "/etc/init.usb.configfs.cfg",
 - "/etc/init.usb.cfg",
 - "/etc/init.Hi3516DV300.usb.cfg",
 - "/etc/init.Hi3516DV300.cfg"
 - ],
 - /////////////////////////////////
 - cJSON *importAttr = cJSON_GetObjectItemCaseSensitive(root, "import"); // 提取init.cfg中"import"的部分
 - ...
 - int importAttrSize = cJSON_GetArraySize(importAttr);
 - for (int i = 0; i < importAttrSize; i++) { // 循環(huán)取出每一項
 - cJSON *importItem = cJSON_GetArrayItem(importAttr, i);
 - ...
 - char *importContent = cJSON_GetStringValue(importItem);
 - ...
 - // Only OHOS L2 support parameter.
 - #ifndef OHOS_LITE //這里有啥意義,前面都已經(jīng)判斷過了
 - if (ExtractCfgFile(&cfgFile, importContent) < 0) {
 - INIT_LOGW("Failed to import from %s", importContent);
 - if (cfgFile != NULL) {
 - free(cfgFile);
 - cfgFile = NULL;
 - }
 - continue;
 - }
 - #else
 - cfgFile = importContent;
 - #endif
 - INIT_LOGI("Import %s...", cfgFile);
 - ParseInitCfg(cfgFile); // 取出"import"中的路徑,解析方法和init.cfg解析方式一致。
 - ...
 - }
 - INIT_LOGD("parse import file done");
 - return;
 - }
 
到這里init.cfg就解析并執(zhí)行完畢了,需要注意的是,import導(dǎo)入的cfg文件是最后才執(zhí)行的。
c.解析執(zhí)行/system/etc/init/*.cfg文件
和import的原理類似,遍歷system/etc/init文件夾下的*.cfg文件,并執(zhí)行。我感覺寫在init.cfg的import中應(yīng)該也是可以的。
- // base\startup\init_lite\services\src\init_read_cfg.c
 - static void ParseOtherCfgs()
 - {
 - ReadCfgs("/system/etc/init");
 - return;
 - }
 - ------------------------------------
 - static void ReadCfgs(const char *dirPath)
 - {
 - DIR *pDir = opendir(dirPath);
 - ...
 - struct dirent *dp;
 - while ((dp = readdir(pDir)) != NULL) {
 - char fileName[FILE_NAME_MAX_SIZE];
 - if (snprintf_s(fileName, FILE_NAME_MAX_SIZE, FILE_NAME_MAX_SIZE - 1, "%s/%s", dirPath, dp->d_name) == -1) {
 - INIT_LOGE("ParseCfgs snprintf_s failed.");
 - closedir(pDir);
 - return;
 - }
 - struct stat st;
 - if (stat(fileName, &st) == 0) {
 - if (strstr(dp->d_name, ".cfg") == NULL) {
 - continue;
 - }
 - INIT_LOGI("ReadCfgs :%s from %s success.", fileName, dirPath);
 - ParseInitCfg(fileName); // 和init.cfg同樣的解析方式
 - }
 - }
 - closedir(pDir);
 - return;
 - }
 
d.PostTrigger
PostTrigger(EVENT_BOOT, “pre-init”, strlen(“pre-init”));
- // base\startup\init_lite\services\param\trigger\trigger_processor.c
 - void PostTrigger(EventType type, const char *content, u_int32_t contentLen)
 - {
 - ///////////////////////////////////// 參考TriggerDataEvent
 - typedef struct {
 - uv_work_t request;
 - EventType type;
 - u_int32_t contentSize;
 - char content[0];
 - } TriggerDataEvent;
 - /////////////////////////////////////
 - ...
 - TriggerDataEvent *event = (TriggerDataEvent *)malloc(sizeof(TriggerDataEvent) + contentLen + 1);
 - ...
 - event->type = type; // = EVENT_BOOT
 - event->request.data = (void *)((char*)event + sizeof(uv_work_t));
 - event->contentSize = contentLen; // = strlen("pre-init")
 - event->content[contentLen] = '\0'; // "pre-init"[strlen("pre-init")],設(shè)置結(jié)束符'\0'
 - SendTriggerEvent(event);
 - ...
 - }
 - ----------------------------------------------------------------------------
 - static void SendTriggerEvent(TriggerDataEvent *event)
 - {
 - ....
 - int ctrlSize = strlen("sys.powerctrl=");
 - if (strncmp(event->content, "sys.powerctrl=", ctrlSize) == 0) { // 如果event->content為"sys.powerctrl="
 - char *cmdParam = NULL;
 - const char *matchCmd = GetCmdInfo(event->content + ctrlSize, event->contentSize - ctrlSize, &cmdParam);
 - if (matchCmd != NULL) {
 - DoCmdByName(matchCmd, cmdParam);
 - } else {
 - PARAM_LOGE("SendTriggerEvent cmd %s not found", event->content);
 - }
 - } else if (strncmp(event->content, "ohos.ctl.start=", strlen("ohos.ctl.start=")) == 0) { // 如果是"ohos.ctl.start="
 - DoCmdByName("start ", event->content + strlen("ohos.ctl.start="));
 - } else if (strncmp(event->content, "ohos.ctl.stop=", strlen("ohos.ctl.stop=")) == 0) { // 如果是"ohos.ctl.stop="
 - DoCmdByName("stop ", event->content + strlen("ohos.ctl.stop="));
 - } else { // 否則執(zhí)行uv_queue_work(),uv_queue_work是將ProcessEvent提交給子線程執(zhí)行,完成后通知主線程,防止阻塞
 - uv_queue_work(uv_default_loop(), &event->request, ProcessEvent, ProcessAfterEvent);
 - event = NULL;
 - }
 - ...
 - }
 - -----------------------------------------------------------------------
 - // base\startup\init_lite\services\src\init_cmds.c
 - void DoCmdByName(const char *name, const char *.cmdContent)
 - {
 - ...
 - size_t cmdCnt = sizeof(CMD_TABLE) / sizeof(CMD_TABLE[0]);
 - unsigned int i = 0;
 - for (; i < cmdCnt; ++i) {
 - if (strncmp(name, CMD_TABLE[i].name, strlen(CMD_TABLE[i].name)) == 0) {
 - CMD_TABLE[i].DoFuncion(cmdContent, CMD_TABLE[i].maxArg);
 - break;
 - }
 - }
 - if (i == cmdCnt) {
 - INIT_LOGE("DoCmd, unknown cmd name %s.", name);
 - }
 - }
 
9.啟動參數(shù)服務(wù)
使用Libuv庫,官網(wǎng)地址。作為Nodejs的底層。
相關(guān)的API可以參考網(wǎng)址,uv_run。
- // base\startup\init_lite\services\param\service\param_service.c
 - int StartParamService()
 - {
 - PARAM_LOGI("StartParamService.");
 - uv_fs_t req;
 - uv_fs_unlink(uv_default_loop(), &req, PIPE_NAME, NULL);
 - uv_pipe_t pipeServer;
 - int ret = uv_pipe_init(uv_default_loop(), &pipeServer, 0);
 - ...
 - ret = uv_pipe_bind(&pipeServer, PIPE_NAME);
 - ...
 - ret = chmod(PIPE_NAME, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
 - ...
 - ret = uv_listen((uv_stream_t*)&pipeServer, SOMAXCONN, OnConnection);
 - ...
 - uv_run(uv_default_loop(), UV_RUN_DEFAULT); // 運行事件循環(huán),直到不再有活動和引用的句柄或請求。
 - ...
 - }
 
至此,init啟動結(jié)束,進入pause()??偨Y(jié)下來init首先會執(zhí)行一些通用的準(zhǔn)備操作,同時兼容常規(guī)linux內(nèi)核啟動腳本,之后在執(zhí)行鴻蒙init.cfg和單板相關(guān)*.cfg。以上分析都是我個人見解,如有錯誤歡迎指正。
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)


















 
 
 





 
 
 
 