接上篇移植openharmony标准系统后,系统进入终端后,发现执行指令特别卡顿,太影响调试了。目前还不知道是什么问题导致的,不知道是不是cpu性能不够,但是感觉不太像是这个问题,卡顿如下图。基本一个操作需要卡半天。 为了解决下这个问题,也为了更熟悉openharmony代码启动流程。现在追踪下启动过程。记录如下。 首先可以知道的是openharmony系统启动后,执行的是init进程。可以查看源码目录下base/startup/init_lite/services/BUILD.gn文件,首先是不管小型还是标准系统都会参与编译的文件。 然后根据系统类型会选择编译不同的文件,我们是标准系统,那么我们具体编译的文件如下图所示。 很明显,我们找到了init的入口了,即为源码目录下的base/startup/init_lite/services/init/main.c文件,文件内容如下,
static const pid_t INIT_PROCESS_PID = 1;
int main(int argc, char * const argv[])
{
int isSecondStage = 0;
if (argc == 2 && (strcmp(argv[1], "--second-stage") == 0)) {
isSecondStage = 1;
}
if (getpid() != INIT_PROCESS_PID) {
INIT_LOGE("Process id error %d!", getpid());
return 0;
}
if (isSecondStage == 0) {
SystemPrepare();
} else {
LogInit();
}
SystemInit();
SystemExecuteRcs();
SystemConfig();
SystemRun();
return 0;
}
所以具体执行的函数为如下顺序:
- SystemPrepare();
- SystemInit();
- SystemExecuteRcs();
- SystemConfig();
- SystemRun();
那么我们就需要展开查看这五个函数的执行过程了。 1.1 首先执行的是SystemPrepare()函数,我们查找的话,是有如下图地方的地方存在改函数。 而我们是标准系统,那么我们执行的函数肯定就是base/startup/init_lite/services/init/standard/init.c文件中的SystemPrepare()函数了。这边我们可以添加打印函数进行验证。
void SystemPrepare(void)
{
INIT_LOGI("base/startup/init_lite/services/init/standard/init.c");
MountBasicFs();
LogInit();
EnableDevKmsg();
CreateDeviceNode();
INIT_LOGI("DISABLE_INIT_TWO_STAGES not defined");
if (InUpdaterMode() == 0) {
StartInitSecondStage();
}
}
void MountBasicFs(void)
{
if (mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755") != 0) {
INIT_LOGE("Mount tmpfs failed. %s", strerror(errno));
}
if (mount("tmpfs", "/mnt", "tmpfs", MS_NOSUID, "mode=0755") != 0) {
INIT_LOGE("Mount tmpfs failed. %s", strerror(errno));
}
if (mkdir("/dev/pts", S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH) != 0) {
INIT_LOGE("mkdir /dev/pts failed. %s", strerror(errno));
}
if (mount("devpts", "/dev/pts", "devpts", 0, NULL) != 0) {
INIT_LOGE("Mount devpts failed. %s", strerror(errno));
}
if (mount("proc", "/proc", "proc", 0, "hidepid=2") != 0) {
INIT_LOGE("Mount procfs failed. %s", strerror(errno));
}
if (mount("sysfs", "/sys", "sysfs", 0, NULL) != 0) {
INIT_LOGE("Mount sysfs failed. %s", strerror(errno));
}
if (mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL) != 0) {
INIT_LOGE("Mount selinuxfs failed. %s", strerror(errno));
}
}
其中mount函数说明如下:
int mount( const char* source, const char* target, const char* filesystemtype, unsigned long mountflags, const void * data);
source : 待挂载的文件系统,通常是一个设备名
target : 挂载点
filesystemtype : 文件系统的类型,例如:"ext2","ext3","msdos","proc","nfs4","iso9660"
mountflags : 指定文件系统的读写访问标志.
值通常如下 :
MS_BIND : 执行bind挂载,使文件或者子目录树在文件系统内的另一个点上可视。
MS_DIRSYNC : 同步目录的更新。
MS_MANDLOCK : 允许在文件上执行强制锁。
MS_MOVE : 移动子目录树。
MS_NOATIME : 不要更新文件上的访问时间。
MS_NODEV : 不允许访问设备文件。
MS_NODIRATIME : 不允许更新目录上的访问时间。
MS_NOEXEC : 不允许在挂上的文件系统上执行程序。
MS_NOSUID : 执行程序时,不遵照set-user-ID 和 set-group-ID位。
MS_RDONLY : 指定文件系统为只读。
MS_REMOUNT : 重新加载文件系统。允许改变现存文件系统的mountflag和数据而无需先卸载再挂上文件系统
MS_SYNCHRONOUS : 同步文件的更新。
MNT_FORCE : 强制卸载,即使文件系统处于忙状态。
MNT_EXPIRE : 将挂载点标志为过时。
data : 文件系统特有的参数。
函数成功执行时返回0。失败返回-1,errno被设为以下的某个值
EACCES : 权能不足,可能原因是:路径的一部分不可搜索或者挂载只读的文件系统时,没有指定 MS_RDONLY 标志。
EAGAIN : 成功地将不处于忙状态的文件系统标志为过时。
EBUSY : 1.源文件系统已被挂上或者不可以以只读的方式重新挂载,因为它还拥有以写方式打开的文件。2.目标处于忙状态。
EFAULT : 内存空间访问出错。
EINVAL : 操作无效,可能是源文件系统超级块无效。
ELOOP : 路径解析的过程中存在太多的符号连接。
EMFILE : 无需块设备要求的情况下,无用设备表已满。
ENAMETOOLONG : 路径名超出可允许的长度。
ENODEV : 内核不支持某中文件系统。
ENOENT : 路径名部分内容表示的目录不存在。
ENOMEM : 核心内存不足。
ENOTBLK : source不是块设备。
ENOTDIR : 路径名的部分内容不是目录。
EPERM : 调用者权能不足。
ENXIO : 块主设备号超出所允许的范围。
然后执行LogInit函数,依然在base/startup/init_lite/services/init/standard/init.c文件中。
void LogInit(void)
{
CloseStdio();
int ret = mknod("/dev/kmsg", S_IFCHR | S_IWUSR | S_IRUSR,
makedev(MEM_MAJOR, DEV_KMSG_MINOR));
if (ret == 0) {
OpenLogDevice();
}
}
然后是函数CreateDeviceNode(),在文件base/startup/init_lite/services/init/standard/device.c中。
void CreateDeviceNode(void)
{
if (mknod("/dev/null", S_IFCHR | DEFAULT_RW_MODE, makedev(MEM_MAJOR, DEV_NULL_MINOR)) != 0) {
INIT_LOGE("Create /dev/null device node failed. %s", strerror(errno));
}
if (mknod("/dev/random", S_IFCHR | DEFAULT_RW_MODE, makedev(MEM_MAJOR, DEV_RANDOM_MINOR)) != 0) {
INIT_LOGE("Create /dev/random device node failed. %s", strerror(errno));
}
if (mknod("/dev/urandom", S_IFCHR | DEFAULT_RW_MODE, makedev(MEM_MAJOR, DEV_URANDOM_MINOR)) != 0) {
INIT_LOGE("Create /dev/urandom device node failed. %s", strerror(errno));
}
}
然后进行判断是不是在升级模式 ,函数为InUpdaterMode(),在base/startup/init_lite/services/utils/init_utils.c文件中,
int InUpdaterMode(void)
{
const char * const updaterExecutabeFile = "/bin/updater";
if (access(updaterExecutabeFile, X_OK) == 0) {
return 1;
} else {
return 0;
}
}
access函数 : 确定文件或文件夹的访问权限。如果指定的存取方式有效,则函数返回0,否则函数返回-1。
R_OK 只判断是否有读权限
W_OK 只判断是否有写权限
X_OK 判断是否有执行权限
F_OK 只判断是否存在
然后执行StartInitSecondStage()函数,依然是在base/startup/init_lite/services/init/standard/init.c文件中
static void StartInitSecondStage(void)
{
const char *fstabFile = "/etc/fstab.required";
Fstab *fstab = NULL;
if (access(fstabFile, F_OK) != 0) {
fstabFile = "/system/etc/fstab.required";
}
INIT_ERROR_CHECK(access(fstabFile, F_OK) == 0, abort(), "Failed get fstab.required");
fstab = ReadFstabFromFile(fstabFile, false);
INIT_ERROR_CHECK(fstab != NULL, abort(), "Read fstab file \" %s \" failed\n", fstabFile);
int requiredNum = 0;
char **devices = GetRequiredDevices(*fstab, &requiredNum);
if (devices != NULL && requiredNum > 0) {
int ret = StartUeventd(devices, requiredNum);
if (ret == 0) {
ret = MountRequriedPartitions(fstab);
}
FreeStringVector(devices, requiredNum);
devices = NULL;
ReleaseFstab(fstab);
fstab = NULL;
if (ret < 0) {
INIT_LOGE("Mount requried partitions failed; please check fstab file");
execv("/bin/sh", NULL);
abort();
}
}
#ifndef DISABLE_INIT_TWO_STAGES
SwitchRoot("/usr");
char * const args[] = {
"/bin/init",
"--second-stage",
NULL,
};
if (execv("/bin/init", args) != 0) {
INIT_LOGE("Failed to exec \"/bin/init\", err = %d", errno);
exit(-1);
}
#endif
}
如果没有fstab.required文件,系统进入init后,会发生如下报错信息。
Freeing unused kernel memory: 1024K
Run /init as init process
[pid=1][INIT][INFO] [init.c:225)] DISABLE_INIT_TWO_STAGES not defined
[pid=1][INIT][ERROR] [init.c:175)] Failed get fstab.required
Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000004
CPU: 1 PID: 1 Comm: init Not tainted 5.10.79
Hardware name: Generic DT based system
[<c011105c>] (unwind_backtrace) from [<c010c770>] (show_stack+0x10/0x14)
[<c010c770>] (show_stack) from [<c05d0a14>] (dump_stack+0x90/0xc4)
[<c05d0a14>] (dump_stack) from [<c0120d78>] (panic+0x114/0x330)
[<c0120d78>] (panic) from [<c0125f90>] (complete_and_exit+0x0/0x1c)
[<c0125f90>] (complete_and_exit) from [<00000000>] (0x0)
如果有则会执行如下正确挂载的信息。
[ 7.169987] [pid=1][INIT][INFO] [init.c:227)] DISABLE_INIT_TWO_STAGES not defined
[ 7.184059] [pid=1][INIT][ERROR] [ueventd.c:297)] Failed get default_boot_device value from cmdline
[ 7.339797] [pid=1][INIT][INFO] [ueventd.c:144)] Handle block device partitionName rootfs
[ 7.355829] [pid=1][INIT][INFO] [ueventd.c:134)] Match with /dev/block/platform/soc@3000000/4020000.sdmmc/by-name/rootfs for /devices/platform/soc@3000000/4020000.sdmmc/mmc_host/mmc0/mmc0:1234/block/mmcblk0
[ 7.376373] [pid=1][INIT][INFO] [ueventd.c:140)] uevent->syspath /devices/platform/soc@3000000/4020000.sdmmc/mmc_host/mmc0/mmc0:1234/block/mmcblk0 not match deviceName /platform/soc@3000000/4020000.sdmmc/by-name/rootfs
[ 7.406339] [pid=1][INIT][INFO] [ueventd.c:144)] Handle block device partitionName vendor
[ 8.064424] [pid=1][INIT][INFO] [init_mount.c:24)] Mount required partitions
[ 8.957499] BEGET[pid=1][INIT][INFO] [fstab_mount.c:331)] Mount /dev/block/platform/soc@3000000/4020000.sdmmc/by-name/rootfs to /usr successful
[ 9.478341] EXT4-fs (mmcblk0p6): mounted filesystem without journal. Opts: barrier=1
[ 9.487070] BEGET[pid=1][INIT][INFO] [fstab_mount.c:331)] Mount /dev/block/platform/soc@3000000/4020000.sdmmc/by-name/vendor to /vendor successful
[ 9.501871] [pid=1][INIT][INFO] [init_hashmap.c:49)] Create hash map success 0
[ 9.509974] LoopEvent[pid=1][INIT][INFO] [le_signal.c:76)] LE_AddSignal 17 0
[ 9.517879] LoopEvent[pid=1][INIT][INFO] [le_signal.c:76)] LE_AddSignal 15 1
[ 9.526092] [pid=1][INIT][INFO] [init.c:87)] Init fd holder socket done
[ 9.533586] [pid=1][INIT][INFO] [init_hashmap.c:49)] Create hash map success 0
[ 9.541677] [pid=1][INIT][INFO] [init_hashmap.c:49)] Create hash map success 0
[ 9.549751] [pid=1][INIT][INFO] [init_hashmap.c:49)] Create hash map success 0
[ 9.557826] [pid=1][INIT][INFO] [init_hashmap.c:49)] Create hash map success 0
[ 9.565969] [pid=1][INIT][ERROR] [init_group_manager.c:189)] Failed to get boot group
所以再来回顾下SystemPrepare()函数,其实就是做一些准备工作。
void SystemPrepare(void)
{
MountBasicFs();
LogInit();
EnableDevKmsg();
CreateDeviceNode();
INIT_LOGI("DISABLE_INIT_TWO_STAGES not defined");
if (InUpdaterMode() == 0) {
StartInitSecondStage();
}
}
接着分析SystemInit()函数,实现在base/startup/init_lite/services/init/standard/init.c中,具体函数内容如下.
void SystemInit(void)
{
SignalInit();
(void)umask(DEFAULT_UMASK_INIT);
MakeDirRecursive("/dev/unix/socket", S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
int sock = FdHolderSockInit();
if (sock >= 0) {
RegisterFdHoldWatcher(sock);
}
}
static int FdHolderSockInit(void)
{
int sock = -1;
int on = 1;
int fdHolderBufferSize = FD_HOLDER_BUFFER_SIZE;
sock = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
if (sock < 0) {
INIT_LOGE("Failed to create fd holder socket, err = %d", errno);
return -1;
}
setsockopt(sock, SOL_SOCKET, SO_RCVBUFFORCE, &fdHolderBufferSize, sizeof(fdHolderBufferSize));
setsockopt(sock, SOL_SOCKET, SO_PASSCRED, &on, sizeof(on));
if (access(INIT_HOLDER_SOCKET_PATH, F_OK) == 0) {
INIT_LOGI("%s exist, remove it", INIT_HOLDER_SOCKET_PATH);
unlink(INIT_HOLDER_SOCKET_PATH);
}
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
if (strncpy_s(addr.sun_path, sizeof(addr.sun_path),
INIT_HOLDER_SOCKET_PATH, strlen(INIT_HOLDER_SOCKET_PATH)) != 0) {
INIT_LOGE("Faild to copy fd hoder socket path");
close(sock);
return -1;
}
socklen_t len = (socklen_t)(offsetof(struct sockaddr_un, sun_path) + strlen(addr.sun_path) + 1);
if (bind(sock, (struct sockaddr *)&addr, len) < 0) {
INIT_LOGE("Failed to binder fd folder socket %d", errno);
close(sock);
return -1;
}
if (lchown(addr.sun_path, 0, 0)) {
INIT_LOGW("Failed to change owner of fd holder socket, err = %d", errno);
}
mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
if (fchmodat(AT_FDCWD, addr.sun_path, mode, AT_SYMLINK_NOFOLLOW)) {
INIT_LOGW("Failed to change mode of fd holder socket, err = %d", errno);
}
INIT_LOGI("Init fd holder socket done");
return sock;
}
接着分析SystemExecuteRcs(),在文件base/startup/init_lite/services/init/adapter/init_adapter.c中,具体函数内容如下
void SystemExecuteRcs(void)
{
#if (defined __LINUX__) && (defined NEED_EXEC_RCS_LINUX)
pid_t retPid = fork();
if (retPid < 0) {
INIT_LOGE("ExecuteRcs, fork failed! err %d.", errno);
return;
}
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);
}
sem_t sem;
if (sem_init(&sem, 0, 0) != 0) {
INIT_LOGE("ExecuteRcs, sem_init failed, err %d.", errno);
return;
}
SignalRegWaitSem(retPid, &sem);
if (sem_wait(&sem) != 0) {
INIT_LOGE("ExecuteRcs, sem_wait failed, err %d.", errno);
}
#endif
}
然后是SystemConfig()函数,在base/startup/init_lite/services/init/standard/init.c中,可以看出内容还是有点多的。
void SystemConfig(void)
{
InitServiceSpace();
InitParseGroupCfg();
PluginManagerInit();
InitParamService();
RegisterBootStateChange(BootStateChange);
SystemLoadSelinux();
LoadDefaultParams("/system/etc/param/ohos_const", LOAD_PARAM_NORMAL);
LoadDefaultParams("/vendor/etc/param", LOAD_PARAM_NORMAL);
LoadDefaultParams("/system/etc/param", LOAD_PARAM_ONLY_ADD);
ReadConfig();
INIT_LOGI("Parse init config file done.");
#if defined(OHOS_SERVICE_DUMP)
AddCmdExecutor("display", SystemDump);
(void)AddCompleteJob("param:ohos.servicectrl.display", "ohos.servicectrl.display=*", "display system");
#endif
PostTrigger(EVENT_TRIGGER_BOOT, "pre-init", strlen("pre-init"));
PostTrigger(EVENT_TRIGGER_BOOT, "init", strlen("init"));
PostTrigger(EVENT_TRIGGER_BOOT, "post-init", strlen("post-init"));
}
void InitServiceSpace(void)
{
if (g_initWorkspace.initFlags != 0) {
return;
}
HashInfo info = {
GroupNodeNodeCompare,
GroupNodeKeyCompare,
GroupNodeGetNodeHashCode,
GroupNodeGetKeyHashCode,
GroupNodeFree,
GROUP_HASHMAP_BUCKET
};
for (size_t i = 0; i < ARRAY_LENGTH(g_initWorkspace.hashMap); i++) {
int ret = HashMapCreate(&g_initWorkspace.hashMap[i], &info);
if (ret != 0) {
INIT_LOGE("%s", "Failed to create hash map");
}
}
for (int i = 0; i < NODE_TYPE_MAX; i++) {
g_initWorkspace.groupNodes[i] = NULL;
}
char *data = ReadFileData(BOOT_CMD_LINE);
if (data != NULL) {
int ret = GetProcCmdlineValue(BOOT_GROUP_NAME, data,
g_initWorkspace.groupModeStr, sizeof(g_initWorkspace.groupModeStr));
if (ret != 0) {
INIT_LOGE("%s", "Failed to get boot group");
#ifdef INIT_TEST
if (GetBootModeFromMisc() == GROUP_CHARING) {
strcpy_s(g_initWorkspace.groupModeStr, sizeof(g_initWorkspace.groupModeStr), "device.charing.group");
} else {
strcpy_s(g_initWorkspace.groupModeStr, sizeof(g_initWorkspace.groupModeStr), BOOT_GROUP_DEFAULT);
}
#else
strcpy_s(g_initWorkspace.groupModeStr, sizeof(g_initWorkspace.groupModeStr), BOOT_GROUP_DEFAULT);
#endif
}
free(data);
}
INIT_LOGI("boot start %s", g_initWorkspace.groupModeStr);
g_initWorkspace.groupMode = GetBootGroupMode();
g_initWorkspace.initFlags = 1;
}
然后是函数InitParseGroupCfg(),
int InitParseGroupCfg(void)
{
char buffer[128] = {0};
char *realPath = GetAbsolutePath(GROUP_DEFAULT_PATH,
g_initWorkspace.groupModeStr, buffer, sizeof(buffer));
INIT_ERROR_CHECK(realPath != NULL, return -1,
"Failed to get path for %s", g_initWorkspace.groupModeStr);
InitParseGroupCfg_(realPath);
InitGroupNode *groupRoot = g_initWorkspace.groupNodes[NODE_TYPE_GROUPS];
int level = 0;
while ((groupRoot != NULL) && (level < GROUP_IMPORT_MAX_LEVEL)) {
g_initWorkspace.groupNodes[NODE_TYPE_GROUPS] = NULL;
InitImportGroupCfg_(groupRoot);
groupRoot = g_initWorkspace.groupNodes[NODE_TYPE_GROUPS];
level++;
}
InitFreeGroupNodes_(g_initWorkspace.groupNodes[NODE_TYPE_GROUPS]);
g_initWorkspace.groupNodes[NODE_TYPE_GROUPS] = NULL;
return 0;
}
void InitParamService(void)
{
PARAM_LOGI("InitParamService pipe: %s.", PIPE_NAME);
CheckAndCreateDir(PIPE_NAME);
int ret = InitParamWorkSpace(&g_paramWorkSpace, 0);
PARAM_CHECK(ret == 0, return, "Init parameter workspace fail");
ret = InitPersistParamWorkSpace(&g_paramWorkSpace);
PARAM_CHECK(ret == 0, return, "Init persist parameter workspace fail");
if (g_paramWorkSpace.serverTask == NULL) {
ParamStreamInfo info = {};
info.server = PIPE_NAME;
info.close = NULL;
info.recvMessage = NULL;
info.incomingConnect = OnIncomingConnect;
ret = ParamServerCreate(&g_paramWorkSpace.serverTask, &info);
PARAM_CHECK(ret == 0, return, "Failed to create server");
}
ret = InitTriggerWorkSpace();
PARAM_CHECK(ret == 0, return, "Failed to init trigger");
RegisterTriggerExec(TRIGGER_PARAM_WAIT, ExecuteWatchTrigger_);
RegisterTriggerExec(TRIGGER_PARAM_WATCH, ExecuteWatchTrigger_);
ParamAuditData auditData = {};
auditData.name = "#";
auditData.label = NULL;
auditData.dacData.gid = getegid();
auditData.dacData.uid = geteuid();
auditData.dacData.mode = DAC_ALL_PERMISSION;
ret = AddSecurityLabel(&auditData, (void *)&g_paramWorkSpace);
PARAM_CHECK(ret == 0, return, "Failed to add default dac label");
LoadParamFromCmdLine();
}
去掉多余的判断后函数,然后我们是定义了DISABLE_INIT_TWO_STAGES
#ifndef DISABLE_INIT_TWO_STAGES
#define INIT_CONFIGURATION_FILE "/etc/init.cfg"
#else
#define INIT_CONFIGURATION_FILE "/etc/init.without_two_stages.cfg"
#endif
#define OTHER_CFG_PATH "/system/etc/init"
#define MAX_PATH_ARGS_CNT 20
void ReadConfig(void)
{
ParseInitCfg(INIT_CONFIGURATION_FILE, NULL);
ReadFileInDir(OTHER_CFG_PATH, ".cfg", ParseInitCfg, NULL);
ReadFileInDir("/vendor/etc/init", ".cfg", ParseInitCfg, NULL);
}
所以再回过头来看SystemConfig()函数功能
void SystemConfig(void)
{
InitServiceSpace();
InitParseGroupCfg();
PluginManagerInit();
InitParamService();
RegisterBootStateChange(BootStateChange);
SystemLoadSelinux();
LoadDefaultParams("/system/etc/param/ohos_const", LOAD_PARAM_NORMAL);
LoadDefaultParams("/vendor/etc/param", LOAD_PARAM_NORMAL);
LoadDefaultParams("/system/etc/param", LOAD_PARAM_ONLY_ADD);
ReadConfig();
INIT_LOGI("Parse init config file done.");
#if defined(OHOS_SERVICE_DUMP)
AddCmdExecutor("display", SystemDump);
(void)AddCompleteJob("param:ohos.servicectrl.display", "ohos.servicectrl.display=*", "display system");
#endif
PostTrigger(EVENT_TRIGGER_BOOT, "pre-init", strlen("pre-init"));
PostTrigger(EVENT_TRIGGER_BOOT, "init", strlen("init"));
PostTrigger(EVENT_TRIGGER_BOOT, "post-init", strlen("post-init"));
}
我修改这里让它不执行读取其他目录的cfg文件,可以进入终端不卡端了,方便调试HDC功能了,所以就不追踪下去了。 等发现其他问题再继续追。
|