数据换机

news/2024/10/6 3:01:08/文章来源:https://www.cnblogs.com/1118zjg/p/18164253

图1为本系统的方法的流程示意图

图2为首次拷贝到X中断需要遍历的文件示意图

图3为非首次拷贝找到X中断点需要遍历的文件示意图

 

实现的功能

  1. 支持换机对SD卡的数据迁移
  2. 大文件按阈值切成切片文件
  3. 小文件和切片文件按阈值分段拷贝
  4. 切片文件的恢复
  5. 小文件和切片文件从换机cache下恢复到三方应用下时的同步删除
  6. installd的拷贝、切片、恢复过程的中断
  7. installd的拷贝、切片、恢复过程相关信号的重置、未恢复完全的应用数据的清除
  8. 系统侧对换机线程实时监控、并在换机应用异常退出时触发installd的换机中断流程
  9. 支持异步查询待换机应用数据的总大小
  10. 支持异步查询应用数据的恢复进度
 

优化的效果

  • 优化旧手机换机所需保留的空间大小:
最大应用拥有的数据大小空间 ——> 1个切片的大小空间
  • 优化新手机换机所需保留的空间大小:
两倍最大应用拥有的数据大小空间 ——> 最大应用拥有的数据+1个切片的大小空间
  • 优化拷贝过程:
无法中断、无法获知拷贝进度 ——> 可中断、可继续、可知进度
  • 优化恢复过程
无法中断、无法获知恢复进度 ——> 可中断、可继续、可知进度
  • 优化异常情况:
换机异常退出时无法发出中断指令 ——> 可智能中断后台任务
换机恢复时中断会导致当前应用数据不完整 ——> 可清除恢复不完整的应用数据
 

需拷贝的用户数据目录

  • 用户数据和缓存
/data/data/ + 包名 (即data/user/0)
/data/user_de/0/ + 包名
  • sdcard存储
/data/media/0/Android/data/ + 包名(/mnt/user/0/emulated/0/Android/data/ )
/data/media/0/ + 应用自定义名字
/data/media/0/Download/ + 应用自定义名字
(/sdcard = = /data/media/0 = = /storage/emulated/0 但访问权限有些许不同)
 
 
frameworks/base仓库:
frameworks/base/core/java/android/app/IActivityManager.aidl(暴露监控换机进程的接口)
frameworks/base/core/java/android/content/pm/ApplicationInfo.java(系统接口调试限制打开isAllowedToUseHiddenApis)
frameworks/base/core/java/android/content/pm/IPackageManager.aidl(暴露pms接口)
frameworks/base/core/java/android/os/IInstalld.aidl(暴露installd接口)
frameworks/base/services/core/java/android/os/IInstalld.aidl(暴露installd接口,和上面那个文件内容一模一样)
frameworks/base/services/core/java/com/android/server/pm/Installer.java(封装native层的真正功能函数)
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java(用于实现换机进程监控的功能)
frameworks/base/services/core/java/com/android/server/pm/IPackageManagerBase.java(继承IPackageManager.aidl,包装pms函数)
frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java(调用Installer.java的函数)frameworks/native仓库:
frameworks/native/cmds/installd/binder/android/os/IInstalld.aidl(暴露installd接口)
frameworks/native/cmds/installd/InstalldNativeService.h(用于声明的头文件)
frameworks/native/cmds/installd/InstalldNativeService.cpp(对接实现Installer.java的封装接口)
frameworks/native/cmds/installd/slice.h(用于声明的头文件)(utils.h)
frameworks/native/cmds/installd/slice.cpp(被InstalldNativeService.cpp调用的真正功能实现)(utils.cpp)

 

 

主要接口功能简述

  提供两个主要接口,copy_files_staged、copy_files_restore,分别负责用户数据的分段拷贝和用户数据的恢复,参数是src、dst作为源路径和目标路径,还有单次拷贝阈值比如100Mb。
  • copy_files_staged:
  拷贝取换机应用的uid和gid作为拷贝文件的新uid和gid,使用广度优先遍历进入到上一次拷贝中断的路径位置,然后使用深度优先遍历的方式遍历二叉树目录结构进行拷贝,并不断累计当前拷贝文件的大小与单次拷贝阈值比较。一旦当前文件大小加上已拷贝文件大小总量遇到超过单次拷贝阈值后,记录当前文件的路径,同时判断当前文件是否为超过单次拷贝阈值的超大文件,是则使用二进制输入输出流的方式将其切割为n个单次拷贝阈值大小的切片文件并用后缀特殊标记。那么本次调用结束并return 返回值提示应用可再次调用此分段拷贝函数。
  • copy_files_restore:
   新应用安装好后,恢复先判断dst是/data/data下还是/sdcard下,因为安装完用户不打开应用就不会创建sdcard下的数据目录,dst就实际不存在。/data/data下取dst的uid和gid,/sdcard下取/sdcard/Download/的uid和gid,作为新恢复文件的uid和gid。通过深度遍历访问文件树,将正常文件恢复到对应目录下,将同一目录下属于同一个超大文件的切片文件取出以输入输出流的方式合并恢复为原文件。

 

主要流程描述

  请参阅图1,本系统提供了一种树形目录结构数据的分段拷贝方法,可以在所有上层为虚拟树形目录结构的系统中使用,此处以智能手机终端之间的数据传输详解实施方式。如图1所示本系统方法的流程示意图
1)在用户开始传输前,获取和计算出本次传输前拷贝所需的参数信息。
  在用户进入传输选择页面后,后台进行计算当前设备的剩余可用空间大小“freeSize”,选择待传输的数据后,计算出待拷贝数据大小“copySize”,然后根据自定义算法公式 OnceCopy = Min(Max(Min(copySize / 20, 2048), 300), freeSize)计算出在300Mb到2048Mb(2Gb)之间且低于用户剩余空间的合适的单次拷贝量“OnceCopy”的具体值。启动传输,此时“数据根目录”、“目标目录”、“单次拷贝量”作为3个参数传入底层接口。
2)开始传输先进行首次分段拷贝
  拷贝取“目标目录”的uid和gid作为拷贝文件新的uid和gid,使用深度优先遍历的方式遍历二叉树目录结构进行拷贝,并不断累计当前拷贝文件的大小与单次拷贝阈值比较。如图2所示的首次拷贝到X中断需要遍历的文件示意图。一旦当前文件大小加上已拷贝文件大小总量遇到超过单次拷贝阈值后,记录当前文件的路径和唯一的索引节点st_ino,同时判断当前文件是否为超过单次拷贝阈值的超大文件,是则将“遇到大文件”的信号量bigfileNow置为true,然后return并提示需再次调用此分段拷贝函数;否则直接return并提示需再次调用此分段拷贝函数。
3)分段数据产生后同时开始异步传输
  此时被首次拷贝出的分段树目录数据开始进行wifi-p2p传输到新的设备,并异步开始进行第二次分段拷贝来生成下一次数据传输所需的分段数据。经研究分段拷贝和wifi-p2p传输两者速度均可达到80Mb/s,两者异步进行,拷贝的时间消耗将被传输所掩盖,效果上形似连续传输,不会给用户带来割裂感。每次分段的数据被传输一部分就同时删除一部分,在下次拷贝开始前就会将前一次的临时拷贝数据完全清除,保证整个传输过程所需空间只有一个“单次拷贝量”OnceCopy的大小。
4)首次拷贝数据传输开始时同时开始下一段拷贝
  第n次(n>1)开始拷贝时,将使用贪心算法逐层匹配上一次记录下的断点文件所属文件夹,并逐级打开,使用广度优先遍历进入到上一次拷贝中断的路径位置,并通过上次记录下的断点文件的唯一的索引节点st_ino值进行确认。如图3所示的非首次拷贝找到X中断点需要遍历的文件示意图。之后开始从此处文件开始拷贝此文件。如果“遇到大文件”的信号量bigfileNow为true,则认定当前为大文件,使用二进制输入输出流的方式将其切割出1个单次拷贝阈值大小的切块文件命名为”原文件名字+特殊后缀+序号“来标记,并将此切块文件存放于待传输的已拷贝目录中,作为下一次传输的对象,并记录下此时属于第几个切块。
重复以上操作到当前大文件切至最后一个切块后,将“遇到大文件”的信号量bigfileNow置为false。
重复传输和拷贝直到待拷贝数据完全被分段传输完成,return并提示拷贝成功。
5)当遇到拷贝失败或传输失败
  如果在拷贝和传输过程中因传输手机、发送手机、失败则会return并提示拷贝异常,调用方可选择尝试再次拷贝,并从已经记录到系统中的断点文件开始继续拷贝和传输,以模拟p2p形式的高速续传。(这里强调区分于现有技术的三方服务器和用户终端之间的基于数据网络的低速续传,此种续传受数据网络速度和三方服务器的限制,且断点是出现在传输过程中,但本实现的断点是产生于拷贝过程中)
6)数据在新设备的恢复
  取“目标目录”的uid和gid作为恢复文件新的uid和gid。通过深度遍历访问文件树,将正常文件恢复到对应目录下,将同一目录下属于同一个超大文件的切片文件相对路径取出存于vector容器,以输入输出流的方式按序合并恢复为原文件。此时用户即可在新应用权限下正常使用传输得到的新数据。
 
 

技术关键

大文件切片

int copy_bigfile_split(const char *src, char *dst, const struct stat64 *pSrcStat, uint options, int uid, int gid,long long splitSize) {/* open src/* .../* open dest/* ...*/int src_fd, dst_fd, stat_result;bool foundSlice = false;bool lastSlice = false;long long remainingBytes = fileSize - lastCopiedSize;long long bytesToCopy = std::min(splitSize, remainingBytes);std::vector<char> buffer(bytesToCopy);curBigFile.read(buffer.data(),bytesToCopy);std::streamsize bytesRead = curBigFile.gcount();outputFile.write(buffer.data(), bytesRead);outputFile.close();curBigFile.close();std::string rn = dst + std::string(".flyme.slice.");ALOGD("rename to rn :  %s", rn.c_str());rn.append(tarIndex + 1, '0');std::rename(dst ,rn.c_str());lastCopiedSize += bytesRead;tarIndex++; // 递增tarIndexif(remainingBytes <= splitSize){lastSlice = true;}else{foundSlice = true;}int mode = 0;if (strstr(dst, FLYME_BACKUP_POSTFIX)) {mode = 1;}if((uid > AID_APP && gid > AID_APP) || mode > 0) {if(strcmp(dst, FLYME_DATA_DATA) != 0){if(fchown(dst_fd, uid, gid) < 0 ) {ALOGE("slice copy big file split fchown fail");}}}(void) close(src_fd);(void) close(dst_fd);if(lastSlice){ALOGD("slice is lastSlice return 1");return 1;}else if (foundSlice){ALOGD("slice is foundSlice return 0");return 0;}return 0;
}

 

切片文件取出与合并

bool restore_split_data(const std::string& srcPath,const std::string& targetPath,const std::string& splitListFilePath, int uid, int gid){ALOGD("slice restore_split_data");std::regex sliceRegex(".flyme.slice.");std::vector<std::string> sliceFiles;std::string delim = "/";std::string saveSliceFile = "";std::string curSliceFile;bool mergeFileResult;// 获取本目录下属于同一个大文件的切片文件列表for (const auto& entry : std::filesystem::directory_iterator(srcPath)){if(std::regex_search(entry.path().string(), sliceRegex)){//找到.flyme.slice.字符串的位置int sliceIndex = entry.path().string().find("flyme.slice",SDCARD_DATAMIGRATION_LENGTH);//取出切片文件名int index_end = entry.path().string().find_last_of(delim);curSliceFile = entry.path().string().substr(index_end+1,sliceIndex-index_end-2);//判断当前entry为属于同一个大文件的切片文件,塞到vector容器sliceFiles中//if ...sliceFiles.push_back(entry.path().string());}}}mergeFileResult = mergeSplitFile(srcPath,targetPath,curSliceFile,saveSliceFile,sliceFiles,uid,gid)sliceFiles.clear();return true;
}bool mergeSplitFile(const std::string& srcPath,const std::string& targetPath,std::string curSliceFile, std::string saveSliceFile, std::vector<std::string> sliceFiles, int uid ,int gid){// 按照切片文件的序号排序std::sort(sliceFiles.begin(), sliceFiles.end());std::string mergedFilePath = targetPath + "/" + saveSliceFile;// 合并切片文件中的内容std::ofstream mergedFile(mergedFilePath, std::ios::binary);for (const std::string& sliceFile : sliceFiles) {//check mergedFile.is_open()// ...std::string tempSliceDir = srcPath + "/" + std::filesystem::path(sliceFile).filename().string();// 读取切片文件中的内容并写入合并文件std::ifstream sliceFileStream(tempSliceDir);mergedFile << sliceFileStream.rdbuf();sliceFileStream.close();// 删除临时切片目录ALOGD("slice mergedFilePath :'%s', restored Slicefiles and removefile is '%s',\n", mergedFilePath.c_str(), tempSliceDir.c_str());std::remove(tempSliceDir.c_str());}mergedFile.close();sliceFiles.clear();ALOGD("slice changeBigFileUgid uid : %d , gid : %d ,curSliceFile : %s",uid,gid,curSliceFile.c_str());//最后改变生成文件的uid、gidchown(mergedFilePath.c_str(), uid, gid);return true;
}

 

文件分段拷贝

两函数互相调用对文件树深度遍历,单函数内循环进行广度遍历,拷贝达到阈值或碰到大文件后打断并记录断点,返回信号值提示应用端重新调用接口以继续拷贝下一段数据。
int copy_directory_staged(const char *src, const char *dst, uint options, int uid, int gid) {int ret_val = FLAG_SUCCEED_BACKUP;struct stat64 dst_stat;DIR *dir;int stat_result;//ALOGD("slice copy_directory_staged %d  %d \n", uid, gid);if (is_restore || first_call_copy || find_first_file) {//广度优先寻找断点stat_result = stat64(dst, &dst_stat);//对stat_result结果做一些error判断//...if (stat_result != 0) {int mkdir_result = mkdir(dst, 0755);}}//处理// Open the directory, and plow through its contents.dir = opendir(src);if (dir == nullptr) {ALOGD("slice acp: unable to open directory '%s': %s\n", src, strerror(errno));return FLAG_FAILED_BACKUP;}while (true) {//单函数内循环进行广度遍历struct dirent* ent;char* src_file;char* dst_file;int src_len, dst_len, name_len;ent = readdir(dir);if (ent == nullptr)break;if (strcmp(ent->d_name, "." ) == 0 || strcmp(ent->d_name, "..") == 0) {continue;}name_len = strlen(ent->d_name);src_len = strlen(src);dst_len = strlen(dst);src_file = (char*) malloc(src_len + 1 + name_len + 1);memcpy(src_file, src, src_len);src_file[src_len] = FSEP;memcpy(src_file + src_len + 1, ent->d_name, name_len + 1);dst_file = (char*)malloc(dst_len + 1 + name_len + 1);memcpy(dst_file, dst, dst_len);dst_file[dst_len] = FSEP;memcpy(dst_file + dst_len + 1, ent->d_name, name_len + 1);if (!is_find_file){//广度优先寻找断点if (is_pause_dir_contain(src,pause_st_dir) || first_call_copy){ret_val = copy_file_recursive_staged(src_file, dst_file, options, uid, gid);free(src_file);free(dst_file);break;}} else {continue;}} else {//两函数互相调用对文件树深度遍历ret_val = copy_file_recursive_staged(src_file, dst_file, options, uid, gid);logger.slice_fprintf("%s slice ready to pause_st_dir22 src: '%s' \n", timeToString().c_str(),src);ALOGD("slice ready to pause_st_dir22 src: '%s' \n", src);if (ret_val != FLAG_SUCCEED_BACKUP) {free(src_file);free(dst_file);logger.slice_fprintf("%s slice ready to jump outof22 src: '%s' \n", timeToString().c_str(),src);ALOGD("slice ready to jump outof22 src: '%s' \n", src);break;}}free(src_file);free(dst_file);}int mode = 0;if (strstr(dst, FLYME_BACKUP_POSTFIX)) {mode = 1;}if ((uid > AID_APP && gid > AID_APP) || mode > 0) {if (strcmp(dst, FLYME_DATA_DATA) != 0){if (chown(dst, uid, gid) < 0 ) {ALOGE("slice copy dir chown from '%s' to '%s' fail\n", src, dst);}}}closedir(dir);return ret_val;
}int copy_file_recursive_staged(const char* src, char* dst,unsigned int options, const int uid, const int gid) {//add isRestore for compile errorint ret_val = FLAG_SUCCEED_BACKUP;int split_val = 0;struct stat64 src_stat;int stat_result, stat_error;stat_result = lstat64(src, &src_stat);if (S_ISDIR(src_stat.st_mode)) {//两函数互相调用对文件树深度遍历ret_val = copy_directory_staged(src, dst, options, uid, gid);chown(dst, uid, gid);} else if (S_ISREG(src_stat.st_mode)) {if (is_restore){//处理是否是在走恢复流程//...}//同时处理遇到大文件的情况// ...if (pause_st_ino == 0 || pause_st_ino == src_stat.st_ino) {if (find_first_file == false) {//广度优先寻找断点则需要对目录进行最少的创建,不反复创建已经前面传输过的目录createDirectory(dst, uid, gid);find_first_file = true;}////区分继续切大文件还是走正常文件流程if (bfconflag == FLAG_BIG_FILE_SLICE_FINISH && pause_st_ino == src_stat.st_ino){bfconflag = FLAG_BIG_FILE_SLICE_SWITCH;} else {//When restart_copy is used to copy pause_st_ino files, the sequence copy can also enter the if logical segmentpause_st_ino = 0;//保证restart_copy拷贝时,拷贝完pause_st_ino文件后序拷贝也能正常走进此if逻辑段中current_size += src_stat.st_size;ALOGD("slice pause_st_ino is '%d', src is '%s', st_size is '%ld', st_ino is '%d', current_size is '%ld'\n", pause_st_ino, src, src_stat.st_size, src_stat.st_ino, current_size);if (MAX_ONCE_COPY < current_size) {ALOGD("slice now MAX_ONCE_COPY < current_size skipping copy '%s'\n",src);pause_st_ino = src_stat.st_ino;restartflag = FLAG_RESTART_BACKUP;ret_val = FLAG_RESTART_BACKUP;if (MAX_ONCE_COPY < src_stat.st_size) {//同时处理遇到大文件的情况// ...split_val = copy_bigfile_split(src, dst, &src_stat, options, uid, gid,MAX_ONCE_COPY);ret_val = FLAG_RESTART_BACKUP;}return ret_val;}ret_val = copy_regular64(src, dst, &src_stat, options, uid, gid);} else {//尚未循环到上次中断的点,不进行实际拷贝,默认已经拷贝成功ALOGD("slice ret_val is '%d', jump this file copy and set ret_val = FLAG_SUCCEED_BACKUP\n", ret_val);}} else {ALOGD("slice skipping unusual file '%s' (mode=0%o)\n", src, src_stat.st_mode);}return ret_val;
}bool is_pause_dir_contain(const char *src, std::string pause_st_dir){//1、src为传进来的递归目录,pause_st_dir为当前保存的切片包的最后文件//2、取src的/或者\ 分隔符后的目录格式个数,如a/b/c/ 为3个,再从pause_st_dir中截取相同路径个数进行比较//3、若不同,则返回false,给上面递归的地方作判断用,false则continue;若相同,则返回true,给上面递归的地方让其继续执行递归,相应的下次src会增加一级目录了,返回true前,执行截取路径个数count++count = c_in_str(src,FSEP);std::string cut_dir = cut_out_n_string(pause_st_dir,"/",count+1);int res = strcmp(cut_dir.c_str(),src);if (res == 0){count++;return true;}ALOGD("slice is_pause_dir_contain count is %d,cut_dir is %s,src is %s,pause_st_dir is %s",count,cut_dir.c_str(),src,pause_st_dir.c_str());return false;
}
 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/738363.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

实战篇——文件上传漏洞upload-labs-master靶场实战一

实战篇——文件上传漏洞upload-labs-master靶场实战(1) 前端验证绕过 (1) 篡改js代码 直接上传一句话木马失败:查看页面源代码:可见前端通过checkFile函数对上传文件的后缀名进行了验证。 使用Tampermonkey自定义js脚本,用于删除form表单的onsubmit属性:启用脚本,刷新页面…

国产数据库人大金仓Kingbase数据迁移工具

转载:国产数据库人大金仓Kingbase数据迁移工具-CSDN博客 注意:Kingbase自带的迁移工具,只能从其它数据库迁移到Kingbase数据库 适配数据库安装的时候建议使用完全安装(数据库可以不启动),后续也可以全部默认(如安装数据库,相关 参数设置可参考人大金仓(Kingbase)部署…

selenium操作

selenium介绍开发使用有头浏览器,部署使用无界面浏览器 selenium工作原理利用浏览器原生的API,封装成一套更加面向对象的Selenium WebDriver API,直接操作浏览器页面里的元素,甚至操作浏览器本身(截屏,窗口大小,启动,关闭,安装插件,配置证书之类的) selenium模块与d…

《Deep Learning》(深度学习)中英文双版PDF免费下载

“深度学习”经典著作《Deep Learning》中文版pdf免费下载。 《Deep Learning》(深度学习)是一本皆在帮助学生和从业人员进入机器学习领域的教科书,以开源的形式免费在网络上提供,这本书是由学界领军人物 Ian Goodfellow、Yoshua Bengio 和 Aaron Courville 合力打造。中文…

微服务异常问题

多服务间,被依赖服务代码更新,依赖服务不生效问题被依赖服务代码更新后,jar包会更新 依赖服务并不是通过http直接调用依赖服务的代码,而是通过依赖jar包的方式,把被依赖服务更新后的jar包更新到依赖服务中即可。 举例: 有A服务,B服务;其中A服务依赖B服务。 当A服务有代…

深度学习--强化学习--基本概念Q V--94

目录1. 强化学习2. 马尔科夫链3. Q值和V值 1. 强化学习 首先我们需要明确,强化学习的任务是什么? 这用大白话说:就是我们希望用强化学习的方式,使智能体获得独立自主地完成某种任务的能力。 智能体学习和工作的地方,我们就称为环境。 注意!所谓独立自主,就是智能体一旦启…

京东毫秒级热key探测框架设计与实践,已实战于618大促

在拥有大量并发用户的系统中,热key一直以来都是一个不可避免的问题。或许是突然某些商品成了爆款,或许是海量用户突然涌入某个店铺,或许是秒杀时瞬间大量开启的爬虫用户, 这些突发的无法预先感知的热key都是系统潜在的巨大风险。 风险是什么呢?主要是数据层,其次是服务层…

刷题Phuck2--data协议差异

刷题Phuck2 使用arjun扫出hl参数,获取到源码 ​​ 源码: <?phpstream_wrapper_unregister(php);if(isset($_GET[hl])) highlight_file(__FILE__);$mkdir = function($dir) {system(mkdir -- .escapeshellarg($dir));};$randFolder = bin2hex(random_bytes(16));$mkdir(us…

常见Linux命令

1、查看目录:ls 常用用法: ls -l :以列表的形式展示;简写ll效果展示:2、终端清屏:clear 常用用法: ctr+L:清空屏幕当前的内容,不会重置终端效果展示: 使用前使用后3、切换目录:cd 常用用法:cd /:切换到根目录cd /xx(目录名) :切换到根目录下的xx目录cd ..:切换…

R语言大学城咖啡店消费问卷调查报告:信度分析、主成分分析可视化

全文链接:https://tecdat.cn/?p=34656 原文出处:拓端数据部落公众号 本次调查旨在了解文汇路咖啡店的市场状况,以便为学校周边咖啡店的经营发展提供积极的引导意义。我们通过问卷调查的方式,收集了大量的数据,通过r软件对数据进行了基本情况分析、信度分析、问卷调查数据…

R语言武汉流动人口趋势预测:灰色模型GM(1,1)、ARIMA时间序列、logistic逻辑回归模型|附代码数据

全文链接:http://tecdat.cn/?p=32496 原文出处:拓端数据部落公众号 人口流动与迁移,作为人类产生以来就存在的一种社会现象,伴随着人类文明的不断进步从未间断。 人力资源是社会文明进步、人民富裕幸福、国家繁荣昌盛的核心推动力量。当前,我国经济正处于从以政府主导的投…

1panel搭建halo+alist+兰空图床

由于服务器更新过后,ssh连接不上,机商vnc延迟卡的很,输个命令都不行,刚好centos断更了,换ubuntu系统了。也顺便从MySQL转到PostgreSQL。这个算是个记录,给有基础的人看的,你连ssh都不会连,那你得自己一步一步慢慢百度来。其实还是比较无脑的,除了1panel的反代,感觉设…