【山大智云】SeafileServer源码分析之CDC(基于内容长度可变分块)
2021SC@SDUSC
基于内容长度可变分块
滚动哈希与拉宾指纹
滚动哈希
考虑解决字符串匹配问题。一种方法是利用字符串哈希值进行匹配。已知模式串长度为nnn,那么我们可以依次截取匹配串中长为nnn的子串计算哈希,然后与模式串的哈希进行比对,若相等则得到一次匹配。在极大概率下,哈希碰撞到的子串与模式串相同。
然后用算法实现。在已经计算过si…i+n\small s_{i\dots i+n}si…i+n哈希的前提下,如果要计算si+1…i+n+1\small s_{i+1\dots i+n+1}si+1…i+n+1的哈希,若采用暴力方法,则需要扫描整个子串,其复杂度与暴力匹配相同,显然需要继续优化。
拉宾-卡普算法:https://zh.wikipedia.org/wiki/拉宾-卡普算法
考虑使用长为nnn的滑动窗口解决此问题:每扫过一个字符,从原哈希中删去旧字符的影响,然后加上新字符的影响,得到新哈希:
若采用此方法,则需要设计一个哈希函数支持动态地增删首尾字符的影响。可以考虑使用素域M\small MM上的多项式映射,其中n\small nn为窗口长度:(注意必须是素数,这样值域上各个值的概率才几乎相等)
hash(si…i+n−1)=(sian−1+si+1an−2+⋯+si+n−1a+si+n−1)(modM)hash(s_{i\dots i+n-1})=(s_{i}a^{n-1}+s_{i+1}a^{n-2}+\dots +s_{i+n-1}a+s_{i+n-1})_{\pmod{M}}hash(si…i+n−1)=(sian−1+si+1an−2+⋯+si+n−1a+si+n−1)(modM)
那么易推知:
hash(si+1…i+n)=(si+1an−1+si+2an−2+⋯+si+n−1a+si+n)(modM)hash(s_{i+1\dots i+n})=(s_{i+1}a^{n-1}+s_{i+2}a^{n-2}+\dots +s_{i+n-1}a+s_{i+n})_{\pmod{M}}hash(si+1…i+n)=(si+1an−1+si+2an−2+⋯+si+n−1a+si+n)(modM)
两式的递推关系如下:
hash(si+1…i+n)=(a⋅hash(si…i+n−1)(modM)−sian+si+n)(modM)hash(s_{i+1\dots i+n})=(a·hash(s_{i\dots i+n-1})_{\pmod{M}}-s_ia^{n}+s_{i+n})_{\pmod{M}}hash(si+1…i+n)=(a⋅hash(si…i+n−1)(modM)−sian+si+n)(modM)
因此就可以以O(1)\small O(1)O(1)的复杂度得到下一个窗口的哈希值。
# https://www.infoarena.ro/blog/rolling-hash
# a is a constant
an = 1 # a^n
rolling_hash = 0
for i in range(0, n):rolling_hash = (rolling_hash * a + S[i]) % MODan = (an * a) % MOD
if rolling_hash == hash_p:# match
for i in range(1, m - n + 1):rolling_hash = (rolling_hash * a + S[i + n - 1] - an * S[i - 1]) % MODif rolling_hash == hash_p:# match
其中an\small a^nan是常数,可以进行预处理。
拉宾指纹
拉宾指纹:https://zh.wikipedia.org/wiki/拉宾指纹
拉宾指纹也是一个多项式哈希映射,但它并非在素域M\small MM上,且映射结果不是一个值。拉宾指纹使用的是有限域GF(2)\small GF(2)GF(2)上的多项式,例如:f(x)=x3+x2+1f(x)=x^3+x^2+1f(x)=x3+x2+1。这个多项式可以使用二进制表示为1101\small 11011101。
之所以使用这样的多项式表示,是因为相比传统的值运算,GF(2)\small GF(2)GF(2)多项式运算更简单:加减都是异或,这样就完全不需要考虑进位的问题。并且多项式的乘除性质与整数相似。不过就算不需要考虑进位,乘法和除法(求余)也只能以k\small kk的复杂度完成(k\small kk为多项式的最高次幂)。
拉宾指纹的哈希函数如下:(和素域类似,模需要是个不可约多项式)
hash(si…i+n)=(sia(x)n+si+1a(x)n−1+⋯+si+n−1a(x)+si+n)(modM(x))hash(s_{i\dots i+n})=(s_{i}a(x)^n+s_{i+1}a(x)^{n-1}+\dots +s_{i+n-1}a(x)+s_{i+n})_{\pmod{M(x)}}hash(si…i+n)=(sia(x)n+si+1a(x)n−1+⋯+si+n−1a(x)+si+n)(modM(x))
递推式如下:
hash(si+1…i+n)=((a(x)⋅hash(si…i+n−1))(modM(x))−sian(x)+si+n)(modM(x))hash(s_{i+1\dots i+n})=((a(x)·hash(s_{i\dots i+n-1}))_{\pmod{M(x)}}-s_ia^{n}(x)+s_{i+n})_{\pmod{M(x)}}hash(si+1…i+n)=((a(x)⋅hash(si…i+n−1))(modM(x))−sian(x)+si+n)(modM(x))
实现
首先选择有限域GF(2)\small GF(2)GF(2)多项式的模p(x)p(x)p(x),要求是个不可约多项式。这里选取了一个k=64\small k=64k=64的多项式。
static u_int64_t poly = 0xbfe6b8a5bf378d83LL;
考虑滑动窗口每次滑动一个字节,则可以令a(x)=x8a(x)=x^8a(x)=x8。
根据拉宾指纹递推式,若直接运算,则需要进行2\small 22次乘法和1\small 11次求余,其复杂度为O(k)\small O(k)O(k)。虽说k\small kk是个常数,但如果优化了kkk,再结合GF(2)\small GF(2)GF(2)多项式无需进位的性质,拉宾指纹的性能甚至会超过传统哈希。
将递推式分为三个部分:不可预处理的乘法部分hash(si…i+n)⋅a(x)\small hash(s_{i\dots i+n})·a(x)hash(si…i+n)⋅a(x)、可预处理的乘法部分sian+1s_ia^{n+1}sian+1、加法部分si+n+1\small s_{i+n+1}si+n+1。第一个部分不可预处理,因为哈希值是不可预知的;第二个部分可预处理,因为sis_isi作为一个字节只有28\small 2^828种取值,且an(x)a^n(x)an(x)也是一个常量;最后一个部分只需一个异或运算,可忽略。
首先考虑第二部分。先实现多项式乘除法,然后易求得an(x)a^n(x)an(x),接着枚举sis_isi的值并求得sian(x)s_ia^n(x)sian(x),最后将结果缓存到U\small UU表中。对幂运算部分继续优化:假如已知处理第一部分的算法为MUL(p, a)
,其复杂度为O(1)\small O(1)O(1),那么幂运算就等价于p=MUL(p, a)
进行nnn次,这样就优化掉了常数kkk。
然后考虑优化第一部分,我们需要计算的是(p(x)⋅a(x))(modM(x))(\small p(x)·a(x))_{\pmod{M(x)}}(p(x)⋅a(x))(modM(x))。优化过程如下:
对乘法优化
发现每次都是乘以a(x)=x8\small a(x)=x^8a(x)=x8,若以二进制表示,就是左移八位。左移复杂度为O(1)\small O(1)O(1)。
对求余优化
令g(x)\small g(x)g(x)等于p(x)\small p(x)p(x)的最高次项xshiftxx^{shiftx}xshiftx,则必有g(x)≤p(x)\small g(x)\le p(x)g(x)≤p(x)。原式可改写为:
((p−p(modg/a)+p(modg/a))⋅a)(modM)((p-p_{\pmod{g/a}}+p_{\pmod{g/a}})·a)_{\pmod{M}}((p−p(modg/a)+p(modg/a))⋅a)(modM)
对其进行变换:
=(p(modg/a)⋅a)(modg/a)+((p−p(modg/a))⋅a)(modM)=((p−(p−p(modg/a)))⋅a)(modg/a)+((p−p(modg/a))⋅a)(modM)\begin{array}{ll}=(p_{\pmod{g/a}}·a)_{\pmod{g/a}}+((p-p_{\pmod{g/a}})·a)_{\pmod{M}}\\=((p-(p-p_{\pmod{g/a}}))·a)_{\pmod{g/a}}+((p-p_{\pmod{g/a}})·a)_{\pmod{M}}\end{array}=(p(modg/a)⋅a)(modg/a)+((p−p(modg/a))⋅a)(modM)=((p−(p−p(modg/a)))⋅a)(modg/a)+((p−p(modg/a))⋅a)(modM)
注意到第一个子式一定小于ggg,因此一定小于aaa。令j⋅(ga)=p−p(modg/a)\small j·(\frac{g}{a})=p-p_{\pmod{g/a}}j⋅(ag)=p−p(modg/a),则上式可改写为:
((p−j⋅(ga))⋅a)+(g⋅j)(modM)((p-j·(\frac{g}{a}))·a)+(g·j)_{\pmod{M}}((p−j⋅(ag))⋅a)+(g⋅j)(modM)
其中g/a=xshiftx−8\small g/a=x^{shiftx-8}g/a=xshiftx−8,因此p−p(modg/a)p-p_{\pmod{g/a}}p−p(modg/a)就是保留ppp的高八位其余位填零;而jjj就是ppp的高八位。将各个式子改写为二进制形式,并使用位运算,则上式等价于:
(p^j)<<8+g*j%(xshift-8)
继续改写,将ppp提出来:
(p<<8) ^ (g*j%(xshift-8)|j<<(xshift+8))
所以预处理g*j%(xshift-8)|j<<(xshift+8)
,就可以以O(1)\small O(1)O(1)计算第一部分。将前式缓存至T\small TT,并将其与第三部分结合,得到最终代码:
// (p * a + m) % M
(p<<8|m)^T[p>>xshift-8]
相关源码
int xshift, shift static inline char fls64(u_int64_t v); // 获取最高位的1在哪一位 u_int64_t polymod(u_int64_t nh, u_int64_t nl, u_int64_t d); // (nh<<64|nl) % d void polymult(u_int64_t *php, u_int64_t *plp, u_int64_t x, u_int64_t y); // x * y = (php<<64|plp) static u_int64_t append8(u_int64_t p, u_char m) {return ((p << 8) | m) ^ T[p >> shift]; } static void calcT (u_int64_t poly) { // Tint j = 0;xshift = fls64(poly) - 1;shift = xshift - 8;u_int64_t T1 = polymod(0, INT64 (1) << xshift, poly);for (j = 0; j < 256; j++) {T[j] = polymmult(j, T1, poly) | ((u_int64_t) j << xshift);} } static void calcU(int size) // U {int i;u_int64_t sizeshift = 1;for (i = 1; i < size; i++)sizeshift = append8(sizeshift, 0);for (i = 0; i < 256; i++)U[i] = polymmult(i, sizeshift, poly); } void rabin_init(int len) { // 初始化calcT(poly);calcU(len); } unsigned int rabin_checksum(char *buf, int len) { // 首次计算,窗口长度为lenint i;unsigned int sum = 0;for (i = 0; i < len; ++i) {sum = rabin_rolling_checksum (sum, len, 0, buf[i]);}return sum; } unsigned int rabin_rolling_checksum(unsigned int csum, int len,char c1, char c2) { // 滚动计算return append8(csum ^ U[(unsigned char)c1], c2); // (csum*a+c2-a^len*c1)%poly }
在git中也用到了此技术,源码位于:git/diff-delta,可作参考学习
CDC
问题的引入
为什么要分块
对于本地文件系统,分块用于解决孔问题、方便操作系统管理空间、降低扫描次数等。而对于网络文件系统更是如此,分块后只需同步那些被修改的块,比重新上传整个文件更有效率。
定长分块
基于内容可变长度的分块
假如我们基于内容进行分块,以
d
作为断点,在d
后产生断点,那么此时分块就变成了0abcd|efg
。发现这样分块与之前的分块仅一个块不一致,也就是说只需重新上传这个不一致的块,相比定长分块效率大大提高。缺点
滚动哈希与断点
显然不能总是以相同的内容作为断点,例如若文件内容为dd...d
,则分块后为d|d|...|d|
。每个块仅包含一个字符,非常浪费空间,更不方便管理,违背了分块的初衷。
我们希望以某种概率分布选择断点使得各个块有一个平均大小,同时又保证断点的某种属性相同。发现哈希恰好能满足这两个性质。
哈希是伪随机的,对于b\small bb位的二进制哈希值,其出现的概率都是2−b2^{-b}2−b。这时候考虑此前的滚动哈希,假设滑动窗口每滑动一次产生的哈希都是随机的,那么上一次碰撞到下一次碰撞的长度即块长恰好呈几何分布:
pdf(l)=(1−2−b)l−1⋅2−b\mathit{pdf}(l)=(1-2^{-b})^{l-1}·2^{-b}pdf(l)=(1−2−b)l−1⋅2−b
那么期望的块长就是2b2^b2b。
上下限
虽然哈希使得块长有预期的平均,但这是基于随机的数据,仍然存在特殊的数据使得特殊的情况出现,而一旦这种情况出现,将大大降低分块的效率。因此采用了一个折中的方案,限制块长的最小值与最大值。当以最小块长或最大块长分块时(非变长分块),块的性质类似于定长分块。
若平均块长为2202^{20}220字节(1MB),最小块长为2182^{18}218字节(256KB),最大块长为2222^{22}222字节(4MB),可以通过几何分布计算非变长分块出现的概率:
P(l≤218)+P(l≥222)=∑i=1218pdf(i)+∑i=222∞pdf(i)=1−∑i=218222pdf(i)≈0.239515P(l\le 2^{18})+P(l\ge 2^{22})=\sum_{i=1}^{2^{18}}\mathit{pdf}(i)+\sum_{i=2^{22}}^{\infty}\mathit{pdf}(i)=1-\sum_{i=2^{18}}^{2^{22}}\mathit{pdf}(i)\approx 0.239515P(l≤218)+P(l≥222)=i=1∑218pdf(i)+i=222∑∞pdf(i)=1−i=218∑222pdf(i)≈0.239515
也就是说断点的命中率约为p′=76%p'=\small 76\%p′=76%。假设数据中一个子序列sss在修改前后不变,其中包含了kkk个断点,那么可以通过几何分布算出CDC在第iii个断点首次命中的概率为:
pdf(i)=p′(1−p′)i−1\mathit{pdf}(i)=p'(1-p')^{i-1}pdf(i)=p′(1−p′)i−1
因此期望在第1p′≈1.314950\frac{1}{p'}\approx 1.314950p′1≈1.314950个断点命中。由于断点间隔的期望为2202^{20}220,那么对应的,未命中的数据长度期望则是2bp′≈1.3\frac{2^b}{p'}\approx 1.3p′2b≈1.3MB。一旦命中断点,后面的分块情况完全相同。也就是说在这个子序列中平均只有约1.3MB的数据被重新以最大长度或最小长度分块了,其余的块仍然不变。
(实际项目中定义平均块长为8MB,最小块长为6MB,最大块长为10MB。由此求得命中率约为0.185862\small 0.1858620.185862;其中块长为6MB的概率约为0.527633\small 0.5276330.527633,为10MB的概率约为0.286505\small 0.2865050.286505。虽然命中率低,但重分块的期望也仅2bp′=80.185862≈43\frac{2^b}{p'}=\frac{8}{0.185862}\approx 43p′2b=0.1858628≈43MB,这对于G甚至T级别的文件来讲微不足道)
实现
/*最小块长略与文件缓冲区略,主要看以下分块部分
*/
if (cur < block_min_sz - 1) // 最小块长cur = block_min_sz - 1;
while (cur < tail) { // 一直扫描直到达到tailfingerprint = (cur == block_min_sz - 1) ? // 计算指纹finger(buf + cur - BLOCK_WIN_SZ + 1, BLOCK_WIN_SZ) : // 首次计算rolling_finger (fingerprint, BLOCK_WIN_SZ, *(buf+cur-BLOCK_WIN_SZ), *(buf + cur)); // 滚动计算if (((fingerprint & block_mask) == ((BREAK_VALUE & block_mask)))|| cur + 1 >= file_descr->block_max_sz) // 碰撞,找到断点{if (file_descr->block_nr == file_descr->max_block_nr) {seaf_warning ("Block id array is not large enough, bail out.\n");free (buf);return -1;}gint64 idx_size = cur + 1;WRITE_CDC_BLOCK (cur + 1, write_data); // 将buf[0..cur]写入块if (indexed) // 记录已处理的长度*indexed += idx_size;break;} else { // 继续寻找cur ++;}
}
相关函数释义
具体注释详见:https://github.com/poi0qwe/seafile-server-learn/tree/main/common/cdc
将块写入文件 / WriteblockFunc
static int default_write_chunk (CDCDescriptor *chunk_descr) // 默认写块文件的方法 {char filename[NAME_MAX_SZ];char chksum_str[CHECKSUM_LENGTH *2 + 1];int fd_chunk, ret;memset(chksum_str, 0, sizeof(chksum_str));rawdata_to_hex (chunk_descr->checksum, chksum_str, CHECKSUM_LENGTH); // 将checksum转为HEX串snprintf (filename, NAME_MAX_SZ, "./%s", chksum_str); // 设置文件名为checksum的HEX串,并限定其最大长度为`NAME_MAX_SZ-1`fd_chunk = g_open (filename, O_RDWR | O_CREAT | O_BINARY, 0644); // 创建文件,并写文件if (fd_chunk < 0) // 打开文件失败return -1; ret = writen (fd_chunk, chunk_descr->block_buf, chunk_descr->len); // 将缓冲写入文件,写n个close (fd_chunk); // 关闭文件return ret; // 返回写的结果 }
初始化
// 给定文件,初始化它的文件分块信息(只用到了文件大小) static int init_cdc_file_descriptor (int fd,uint64_t file_size,CDCFileDescriptor *file_descr) {int max_block_nr = 0;int block_min_sz = 0;file_descr->block_nr = 0; // 实际块数// 若为空,则设置默认值if (file_descr->block_min_sz <= 0)file_descr->block_min_sz = BLOCK_MIN_SZ;if (file_descr->block_max_sz <= 0)file_descr->block_max_sz = BLOCK_MAX_SZ;if (file_descr->block_sz <= 0)file_descr->block_sz = BLOCK_SZ;if (file_descr->write_block == NULL)file_descr->write_block = (WriteblockFunc)default_write_chunk; // 默认写块文件的方法block_min_sz = file_descr->block_min_sz; // 块的最小大小max_block_nr = ((file_size + block_min_sz - 1) / block_min_sz); // 计算最大块数(极值)file_descr->blk_sha1s = (uint8_t *)calloc (sizeof(uint8_t),max_block_nr * CHECKSUM_LENGTH); // 按照最大块数申请空间file_descr->max_block_nr = max_block_nr;return 0; }
写数据与校验和
// 写一个块(block_sz是块的大小,write_data表示是否写入硬盘) #define WRITE_CDC_BLOCK(block_sz, write_data) \ do { \int _block_sz = (block_sz); \chunk_descr.len = _block_sz; \ // 设置缓冲长度chunk_descr.offset = offset; \ // 设置偏移ret = file_descr->write_block (file_descr->repo_id, \ // 写文件方法file_descr->version, \&chunk_descr, \crypt, \chunk_descr.checksum, \(write_data)); \if (ret < 0) { \ // 写失败free (buf); \g_warning ("CDC: failed to write chunk.\n"); \return -1; \} \memcpy (file_descr->blk_sha1s + \file_descr->block_nr * CHECKSUM_LENGTH, \chunk_descr.checksum, CHECKSUM_LENGTH); \ // 记录块的SHA1SHA1_Update (&file_ctx, chunk_descr.checksum, 20); \ // 更新SHA1file_descr->block_nr++; \ // 记录块数offset += _block_sz; \ // 记录偏移\memmove (buf, buf + _block_sz, tail - _block_sz); \ // 更新buftail = tail - _block_sz; \cur = 0; \ // 移动指针 }while(0); // 表示执行一次
分块主函数
int file_chunk_cdc(int fd_src, // 文件标识符CDCFileDescriptor *file_descr, // 文件分块信息,新建或更新SeafileCrypt *crypt, // 加密信息gboolean write_data, // 是否写入硬盘gint64 *indexed) // 块索引 // 注释详见:https://github.com/poi0qwe/seafile-server-learn/blob/main/common/cdc/cdc.c
【山大智云】SeafileServer源码分析之CDC(基于内容长度可变分块)相关推荐
- 【山大智云开发日志】项目安装与部署
2021SC@SDUSC 目录 简介 操作系统 前置操作 安装中文语言 安装前置组件 准备数据库 建表 创建master用户 下载项目源代码 编译并安装项目 创建配置文件 seafile-server ...
- 阿里云image-syncer源码分析
阿里云image-syncer源码分析 欢迎关注"云原生手记"微信公众号 背景 大家在公司中都会使用到容器镜像私有仓库,一般都用harbor,也有会用registry搭建一个简陋的 ...
- 大数据之Oozie——源码分析(一)程序入口
工作中发现在oozie中使用sqoop与在shell中直接调度sqoop性能上有很大的差异.为了更深入的探索其中的缘由,开始了oozie的源码分析之路.今天第一天阅读源码,由于没有编译成功,不能运行测 ...
- TreeMap源码分析——深入分析(基于JDK1.6)
TreeMap有Values.EntrySet.KeySet.PrivateEntryIterator.EntryIterator.ValueIterator.KeyIterator.Descendi ...
- OkHttpClient 源码分析 1(基于3.9.0的源码)
OkHttpClient是目前开发 android 应用使用最广泛的网络框架,最近看了阿里的 httpdns 里面对于 dns 的处理,我们团队就想调研一下在项目中有什么利弊,并且框架中是否对 soc ...
- Python wordcloud词云:源码分析及简单使用
Python版本的词云生成模块从2015年的v1.0到现在,已经更新到了v1.7. 下载请移步至:https://pypi.org/project/wordcloud/ wordcloud简单应用: ...
- 从源码分析常见的基于Array的数据结构动态扩容机制
本文的写作冲动来源于今晚看到的老赵的一则微博"大家知道System.Collections.Generic.List<T>是一种什么样的数据结构?内部的元素是怎么存放的?还有Di ...
- ZedGraph5.1.5源码分析去掉鼠标悬浮内容闪烁问题(附源码下载)
场景 在使用ZedGraph绘制曲线图时,将鼠标悬浮时内容闪烁,且频率很高. 找到其源码,发现不论鼠标移动的范围大小,甚至乎不论鼠标是否移动,都要刷新一次Tooltip. 注: 博客主页: https ...
- Struts 源码分析笔记1(尚无内容-请跳过,省得浪费时间)
只是单纯的 学习笔记..可能会毫无章法.目前对Struts的整个架构设计尚不不清晰 有错误敬请指出,谢谢.今天只开个头.. 随便写个大概的计划: 先把Struts框架所提供的基本功能 ,如:处理请求过 ...
- docker 源码分析 三(基于1.8.2版本),NewDaemon启动
本文来分析一下New Daemon的启动过程:在daemon/daemon.go文件中: func NewDaemon(config *Config, registryService *registr ...
最新文章
- shell (check return of each line)(PIPESTATUS[@])and sudoer
- 学习笔记之12个月提升计划
- 一入前端深似海,从此红尘是路人系列第七弹之孤独的剑客-单例模式
- 小米11 Pro概念图曝光:曲面挖孔屏+后置五摄相机模组
- angular监听输入框值的变化_angular 实时监听input框value值的变化触发函数方法
- [已解决]Tomcat启动报 java.net.BindException: Address already in use: JVM_Bind
- 券商IT的建设一定要有全局观、前瞻性,要走在业务前面,而不是被动响应
- GY-53红外激光测距模块的使用以及pwm模式代码的实现
- 免费、正版、最新的Idea(教育免费版)获取流程!!
- Hibernate表间映射时HHH000142异常
- 深度学习入门笔记(李沐)(一)
- Python 3,一行代码处理各种时间转换,从此跟datetime,time模块说拜拜 ~ ~ 不收藏算我输!!!
- Tomcat多实例与负载均衡
- Springer投稿流程——Multimedia Tools and Applications
- c语言分支编程改错题,二级C语言改错 二级C语言编程题 汇总整理篇.doc
- 制作 macOS U盘USB启动安装盘方法
- python调用ansys
- mysql 关联查询连接条件
- “世上唯一的后悔药”:冻卵?
- 计算机信息技术导论知识点,《信息技术导论》复习资料(ldst).doc
热门文章
- 同一包(package)下,两个不同类的调用操作详解
- css3复习知识点概括1(根据W3S顺序)
- 百思不得其姐学习笔记
- 爬取百思不得姐段子图片
- Blender_1_移动、旋转、缩放
- 《大数据之路:阿里巴巴大数据实践》第一篇 数据技术篇-读书笔记
- 计算机需要权限来执行此操作 win7,Win7系统下“文件夹访问被拒绝 您需要权限来执行操作”解决方法...
- Java获取今天是星期几
- java calendar星期_作业-用Calendar获取今天是星期几
- photoshop-photoshop记录