MIT 6.S081 Lab File System
前言
打开自己的blog一看,居然三个月没更新了...回想一下前几个月,开题 + 实验室杂活貌似也没占非常多的时间,还是自己太懈怠了吧,掉线城和文明6真的是时间刹手(
不过好消息是把15445的所有lab都锤完了,最近一个月应该没啥活干。立个flag,这个月更它个10篇blog,把15445的知识点、lab,以及6.S081想写的东西都写完。今天先做个复健,码一下刚做完的lab8,以及xv6的file system的学习笔记。
坏消息是leetcode一道没动,甚至主力啥语言啥框架还没定下来,开学找实习可能要炸了orz...
这个lab不算难,总代码量也就几十行,绝大多数时间拿来读文件系统相关的代码了(两三天吧),不过收获也挺大,理清楚xv6文件系统的逻辑后,lab一个晚上就搞定了。因此这篇blog我会写的简单一些,大头部分放在file system分析的blog中。
强烈推荐在做这个lab前把《xv6 book》中关于file system的章节全部看完,并详细分析相关的代码。我的这篇关于xv6的file system的学习笔记可能对你有帮助:
TODO:(在码了在码了,但愿今天能码完)
Lab链接:https://pdos.csail.mit.edu/6.828/2019/labs/fs.html
都2021年了还在做2019的lab,这真的好嘛(
Part1 Large Files
xv6选择的文件存储介质是磁盘,通过 struct dinode 来描述一个文件在磁盘上存储,并用 struct inode作为其对一个的struct dinode的拷贝,存储在内存中:
// kernel/fs.h
struct dinode {
short type; // File type
short major; // T_DEVICE only
short minor; // T_DEVICE only
short nlink; // Number of links to inode in file system
uint size; // Size of file (bytes)
uint addrs[NDIRECT+1]; // Data block addresses
};
// kernel/file.h
// in-memory copy of an inode
struct inode {
uint dev; // Device number
uint inum; // Inode number
int ref; // Reference count
struct sleeplock lock; // protects everything below here
int valid; // inode has been read from disk?
short type; // copy of disk inode
short major;
short minor;
short nlink;
uint size;
uint addrs[NDIRECT + 1];
};
我们只关注其中的addrs。addrs记录着文件所在磁盘的盘块号,这个数组的容量为13,前12个地址是直接地址,即该文件的第 0 ~ 11 号盘块,第13个地址是一个一级索引,用于索引文件的第 12 ~ 12 + 256 号盘块,如下图所示:

这样,一个文件最多可以占用 268 个盘块。
bmap是xv6中非常重要的api,其返回文件偏移量(bn * BSIZE)所对应的的磁盘盘块号:
// kernel/fs.c
static uint
bmap(struct inode *ip, uint bn)
{
uint addr, *a;
struct buf *bp;
if(bn < NDIRECT){ // offset in in NDIRECT range
if((addr = ip->addrs[bn]) == 0)
ip->addrs[bn] = addr = balloc(ip->dev);
return addr;
}
bn -= NDIRECT;
if(bn < NINDIRECT){
// Load indirect block. If indirect block is not exist, allocate it.
if((addr = ip->addrs[NDIRECT]) == 0)
ip->addrs[NDIRECT] = addr = balloc(ip->dev);
bp = bread(ip->dev, addr);
a = (uint*)bp->data;
if((addr = a[bn]) == 0){
a[bn] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
return addr;
}
当xv6删除文件时,要将文件所对应的盘块一一释放。如果这个文件存在一级索引块,那么除了释放间接索引块的表项所对应的盘块之外,还要将这块一级索引块一并释放:
static void
itrunc(struct inode *ip)
{
int i, j;
struct buf *bp;
uint *a;
for(i = 0; i < NDIRECT; i++){ // free direct block
if(ip->addrs[i]){
bfree(ip->dev, ip->addrs[i]);
ip->addrs[i] = 0;
}
}
if(ip->addrs[NDIRECT]){
bp = bread(ip->dev, ip->addrs[NDIRECT]);
a = (uint*)bp->data;
for(j = 0; j < NINDIRECT; j++){ // free block indexed by indirect-block
if(a[j])
bfree(ip->dev, a[j]);
}
brelse(bp);
bfree(ip->dev, ip->addrs[NDIRECT]); // free indirect-block
ip->addrs[NDIRECT] = 0;
}
}
Part1要求我们为xv6的文件系统增添一个二级索引。一个二级索引占据一个盘块,共计有256个表项,每个表项都指向一个一级索引块。由于addr的容量仍然是13,因此需要牺牲一个一级索引项,一个文件的盘块索引项也变成了 11个直接索引 + 1个一级索引 + 1个二级索引,共计可以索引 11 + 256 + 256 *256 = 65803个盘块,由此实现了最大文件大小的扩展。一个二级索引块的例子如下:

我们首先要修改一下宏 NINDIRECT,将其值改为11,并修改struct dinode和struct inode中addr的定义部分,以及有关文件大小的宏:
// kernel/fs.h
#define NDIRECT 11
#define NINDIRECT (BSIZE / sizeof(uint))
#define NDINDIRECT NINDIRECT * NINDIRECT
#define MAXFILE (NDIRECT + NINDIRECT + NDINDIRECT)
还有一些宏(NBLOCKS等)需要修改,在相应的实验手册中已经指出,因此不再赘述了。
随后需要修改两个api:bmap和itrunc。在bmap中添加计算二级索引的相关代码,以及对应的盘块的分配代码:
static uint
bmap(struct inode *ip, uint bn)
{
uint addr, *a;
struct buf *bp;
// offset in direct-block range
if(bn < NDIRECT){
if((addr = ip->addrs[bn]) == 0)
ip->addrs[bn] = addr = balloc(ip->dev);
return addr;
}
bn -= NDIRECT;
// offset in primary-index range
if(bn < NINDIRECT){
// Load indirect block, allocating if necessary.
if((addr = ip->addrs[NDIRECT]) == 0)
ip->addrs[NDIRECT] = addr = balloc(ip->dev); // allocate a block on disk
bp = bread(ip->dev, addr);
a = (uint*)bp->data;
if((addr = a[bn]) == 0){
a[bn] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
return addr;
}
bn -= NINDIRECT;
struct buf *dindbuf, *indbuf; // double-indirect block buffer, indirect block buffer
// offset in secondary-index range
if (bn < NDINDIRECT) {
// get the DIRECT index block. if it's not exist, allocate it.
if ((addr = ip->addrs[NDIRECT + 1]) == 0)
ip->addrs[NDIRECT + 1] = addr = balloc(ip->dev);
dindbuf = bread(ip->dev, addr); // map it into buffer
uint *dind = (uint *)dindbuf->data;
if(dind[bn / NINDIRECT] == 0) { // allocate a indirect block for double-indirect index and log it.
dind[bn / NINDIRECT] = balloc(ip->dev);
log_write(dindbuf);
}
indbuf = bread(ip->dev, dind[bn / NINDIRECT]);
uint *ind = (uint *)indbuf->data;
if (ind[bn % NINDIRECT] == 0) { // allocate file block if it's not exist.
ind[bn % NINDIRECT] = balloc(ip->dev);
log_write(indbuf);
}
brelse(dindbuf);
brelse(indbuf);
return ind[bn % NINDIRECT];
}
// out of range
panic("bmap: out of range");
}
此外,还要修改itrunc这个方法,添加从二级索引中释放盘块的支持代码:
static void
itrunc(struct inode *ip)
{
int i, j;
struct buf *bp;
uint *a;
// release DIRECT block
for(i = 0; i < NDIRECT; i++){
if(ip->addrs[i]){
bfree(ip->dev, ip->addrs[i]);
ip->addrs[i] = 0;
}
}
// release primary-index block
if(ip->addrs[NDIRECT]){
bp = bread(ip->dev, ip->addrs[NDIRECT]);
a = (uint*)bp->data;
for(j = 0; j < NINDIRECT; j++){
if(a[j])
bfree(ip->dev, a[j]);
}
brelse(bp);
bfree(ip->dev, ip->addrs[NDIRECT]);
ip->addrs[NDIRECT] = 0;
}
// release secondary-index block
if (ip->addrs[NDIRECT + 1]) {
struct buf *dinddbuf, *indbuf;
uint *dind, *ind;
dinddbuf = bread(ip->dev, ip->addrs[NDIRECT + 1]);
dind = (uint *)dinddbuf->data;
for (int k = 0; k < NINDIRECT; k++) {
if (dind[k]) {
indbuf = bread(ip->dev, dind[k]);
ind = (uint *)indbuf->data;
for (int l = 0; l < NINDIRECT; l++) {
bfree(ip->dev, ind[l]);
ind[l] = 0;
}
brelse(indbuf);
bfree(ip->dev, dind[k]);
}
}
brelse(dinddbuf);
bfree(ip->dev, ip->addrs[NINDIRECT + 1]);
}
ip->size = 0;
iupdate(ip);
}
这段代码里有两个值得注意的地方:
1)一级索引块、二级索引块刚开始并没有被分配,只有在需要使用的时候才会被分配
2)在xv6中使用了缓冲区来减少I/O次数,一切对盘块的读写操作都首先在缓冲块上进行,并且为了维持磁盘中元数据(super block)的一致性,一切写盘块的操作都需要调用log_write将写操作添加到日志中。在bmap中涉及到了两种对盘块的写操作:对二级索引块的写操作(向其中添加表项,一个表项对应一个一级索引块)和对一级索引块的写操作(向其中添加表项,一个表项对应一个文件内容盘块)。虽然bmap也会分配指向文件内容的盘块,但对这个盘块的写操作并不是在bmap中进行的,而是在其他api中进行的,因此无需将这个块录入到日志中。
虽然struct dinode和struct inode中也包含了type、major、nlink、size等成员,但读过相关的代码后你会发现,这些成员都无需修改,尤其是size这个成员很具有迷惑性,我们分配了盘块后这个成员看起来应该要增加的,但实际上这是一个没被用到的成员。毕竟xv6只是一个教学系统,不应对其苛求太多。这部分的分析都在我的关于file system的blog中有详细的说明。
Part1的相关代码也就这么多了,只要认真读下来file system相关的代码,并认真按照Hint来做,这就算是个白送的实验。不过比较要命的是usertests中的writebig测试,它会写一个MAXFILE大小的文件(添加大文件支持后,这个值从原来的2K变成了20W),会花费更长的测试时间。
xv6 kernel is booting
virtio disk init 0
init: starting sh
$ bigfile
..................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................
wrote 65803 blocks
bigfile done; ok
Part2 symlink
这部分实验要求我们为文件系统增添对符号链接(symbolic link)(也被称为软链接)的支持。为此我们首先简要了解一下xv6中的硬链接和软链接的含义。
在xv6中,每个文件拥有唯一的struct dinode和struct inode,更好的一种理解方法是,struct inode是一个文件控制块,存储着这个文件相关的元数据,一切对文件的读、写、打开、关闭、删除等操作都需要通过inode来完成。而很多时候,我们需要一个文件可以出现在多个目录下的支持(例如说多用户操作系统,每个用户都拥有一个自己的目录,它们希望共享某一文件)。如下图所示,这些文件的路径(链接)都导向了同一个inode:

这种可以直接索引到inode的链接即是硬链接,本质上它是目录文件中的一个条目,根据这个条目,可以(准确的说是必定)获得该文件的inode。在xv6的api中,通过sys_link创建一个文件的硬链接,其中的核心api是 dirlink:
// Write a new directory entry (name, inum) into the directory dp.
int
dirlink(struct inode *dp, char *name, uint inum)
{
int off;
struct dirent de;
struct inode *ip;
// Check that name is not present.
if((ip = dirlookup(dp, name, 0)) != 0){
iput(ip);
return -1;
}
// Look for an empty dirent.
for(off = 0; off < dp->size; off += sizeof(de)){
if(readi(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
panic("dirlink read");
if(de.inum == 0)
break;
}
strncpy(de.name, name, DIRSIZ);
de.inum = inum;
if(writei(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
panic("dirlink");
return 0;
}
在sys_link中调用dirlink时,传入的参数inum为目标文件的inode编号,该编号被添加到目录表项中;这样我们也可以理解为什么调用ll时,会发现同一个文件的硬链接对应的inode号是相同的了;硬链接如果存在,那么其对应的inode必须存在。inode通过维护一个nlink记录着硬链接的数量。当该inode新增一个硬链接时,其值便增1;当某个路径下的硬链接被删除时,nlink要减1,当值减至0时,说明该文件已经无法通过路径访问到,这时才能释放相应的文件。
软链接的定义则不同,我们可以看一下symlink的man page中的部分内容:
symlink() creates a symbolic link named linkpath which contains the string target.
Symbolic links are interpreted at run time as if the contents of the link had been substituted into the path being followed to find a file or directory.
A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent one; the latter case is known as a dangling link.
软链接并不会增加nlink的数量,且可以指向一个不存在的路径。软链接也可以指向一个软链接,遇到这种情况时将会递归,直至找到相应的硬链接(或者达到最大递归深度后返回错误)。
在本Part中,要求我们实现以下内容:
1)实现一个新的系统调用 sys_symlink,通过该系统调用可以创建一个软链接;
2)为sys_open提供软链接的打开支持(通过软链接打开文件、O_NOFOLLOW);
只要理解了软链接的作用后,实现这个lab的思路也比较容易了,这个Part实现的思路也很多,我使用的是最简单最容易想到的:
1)将软链接本身也看做是一个文件,即软链接本身也有自己的dinode和inode,其文件内容也只是一行字符串,表征着软链接指向的文件的路径。创建软链接时,要为其分配一个inode
2)将软链接的inode添加到目录条目中,注意要避免命名冲突
uint64
sys_symlink(void)
{
char target[MAXPATH + 1], path[MAXPATH + 1], name[MAXPATH + 1];
memset((void *)target, 0, MAXPATH + 1);
memset((void *)path, 0, MAXPATH + 1);
if (argstr(0, target, MAXPATH) < 0 || argstr(1, path, MAXPATH) < 0) {
return -1;
}
begin_op(ROOTDEV);
// get parent inode
struct inode *iparent, *isym;
if ((iparent = nameiparent((char *)path, (char *)name)) == 0) {
end_op(ROOTDEV);
return -1;
}
// avoid name conflict
// do not hold ilock for iparent
uint off;
if ((isym = dirlookup(iparent, name, &off)) != 0) {
// printf("symlink name conflict with an existing file\n");
// iunlockput(iparent);
iput(iparent);
end_op(ROOTDEV);
return -1;
}
// allocate a dinode for symlink. isym is locked when it func create return
// after this operation, symlink entry is added under corresponding path
if ((isym = create(path, T_SYMLINK, 0, 0)) == 0) {
panic("create inode for symlink error"); // panic is not suitable, but simplify our situations.
}
// fill symlink file content with targetpath
int retval = 0;
uint pathlen = strlen((char *)target);
uint r, total;
r = total = 0;
while (total != pathlen) {
if ((r = writei(isym, 0, (uint64)(target + total), total, pathlen - total)) > 0) {
total += r;
} else {
retval = -1;
break;
}
}
// release
iunlockput(isym);
iput(iparent);
end_op(ROOTDEV);
return retval;
}
这样我们也可以理解为什么在一些文件系统上,同一个文件的软链接和硬链接的inode可能不同,因为软链接本身也是一个文件,有自己的inode,而硬链接只是一个目录中的条目,该条目索引到了对应的inode。
然后修改sys_open,当发现打开的path对应的是一个软链接时,调用divesymlink,获得该软链接指向的文件的inode。如果使用了O_NOFOLLOW,说明我们希望打开的是软链接这个文件本身,一般fstate会使用这个flag:
uint64
sys_open(void)
{
char path[MAXPATH];
int fd, omode;
struct file *f;
struct inode *ip;
int n;
if((n = argstr(0, path, MAXPATH)) < 0 || argint(1, &omode) < 0)
return -1;
begin_op(ROOTDEV);
if(omode & O_CREATE){
ip = create(path, T_FILE, 0, 0);
if(ip == 0){
end_op(ROOTDEV);
return -1;
}
} else {
if((ip = namei(path)) == 0){
end_op(ROOTDEV);
return -1;
}
ilock(ip);
if(ip->type == T_DIR && omode != O_RDONLY){
iunlockput(ip);
end_op(ROOTDEV);
return -1;
}
}
/**************************
* SYMLINK *
*************************/
if (ip->type == T_SYMLINK && (omode & O_NOFOLLOW) == 0) {
ip = divesymlink(ip);
if (ip == 0) { // link target not exist
end_op(ROOTDEV);
return -1;
}
}
if(ip->type == T_DEVICE && (ip->major < 0 || ip->major >= NDEV)){
iunlockput(ip);
end_op(ROOTDEV);
return -1;
}
if((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0){
if(f)
fileclose(f);
iunlockput(ip);
end_op(ROOTDEV);
return -1;
}
if(ip->type == T_DEVICE){
f->type = FD_DEVICE;
f->major = ip->major;
f->minor = ip->minor;
} else {
f->type = FD_INODE;
}
f->ip = ip; // ref is here, so this function doesn't call iput
f->off = 0;
f->readable = !(omode & O_WRONLY);
f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
iunlock(ip);
end_op(ROOTDEV);
return fd;
}
divesymlink的实现如下,其的参数为软链接文件的inode,返回软链接最终指向的文件的inode,如果这个文件不存在则返回0。根据Hint,divesymlink还要解决软链接之间的环路引用问题,这里我们简单的通过递归深度来判断,当深度大于10时,认为软链接最终指向的文件不存在:
static struct inode *
divesymlink(struct inode *isym)
{
struct inode *ip = isym;
char path[MAXPATH + 1];
uint len;
int deep = 0;
do {
// get linked target
// we don't know how long the file str is, so we expect once read could get fullpath.
len = readi(ip, 0, (uint64)path, 0, MAXPATH);
if (readi(ip, 0, (uint64)(path + len), len, MAXPATH + 1) != 0)
panic("divesymlink : short read");
iunlockput(ip);
if (++deep > 10) { // may cycle link
return 0;
}
if ((ip = namei((char *)path)) == 0) { // link target not exist
return 0;
}
ilock(ip);
} while (ip->type == T_SYMLINK);
return ip;
}
这样Part2也解决了。比较棘手的是api的锁获取和锁释放,以及iput、iunlockput、iunlock这三个api的选择。我在这里简单介绍一下:
1)iput会使ip->ref的数量减1(注意与ip->nlink区分开)。这个值记录着外部所持有struct inode指针的总数量。当这个值减小到0时,说明没有进程在访问这个文件,此时要将icache中对应的struct inode释放掉,但不会释放对应的文件。
2)iunlock会释放掉struct inode的锁,这把锁用来实现并发环境下的struct inode的原子访问,即我们在查看struct inode中的成员(type、data等)时,必须持有这把锁
3)iunlockput同时调用上述两个函数;
即如果我们不需要访问struct inode的成员时,应调用iunlock释放掉inode的锁,等需要访问时再拿起这把锁;当我们不再需要使用struct inode的指针时,要调用iput,弃用掉这个指针;
这样symlinktest也可以Pass了,对应的usertests也可以全pass:
xv6 kernel is booting
virtio disk init 0
init: starting sh
$ symlinktest
Start: test symlinks
test symlinks: ok
Start: test concurrent symlinks
test concurrent symlinks: ok
$ usertests
usertests starting
test reparent2: OK
test pgbug: OK
test sbrkbugs: usertrap(): unexpected scause 0x000000000000000c pid=3210
sepc=0x00000000000044b2 stval=0x00000000000044b2
usertrap(): unexpected scause 0x000000000000000c pid=3211
sepc=0x00000000000044b2 stval=0x00000000000044b2
OK
test badarg: OK
test reparent: OK
test twochildren: OK
test forkfork: OK
test forkforkfork: OK
test argptest: OK
......
......
......
test opentest: OK
test writetest: OK
test writebig: OK
test createtest: OK
test openiput: OK
test exitiput: OK
test iput: OK
test mem: OK
test pipe1: OK
test preempt: kill... wait... OK
test exitwait: OK
test rmdot: OK
test fourteen: OK
test bigfile: OK
test dirfile: OK
test iref: OK
test forktest: OK
test bigdir: OK
ALL TESTS PASSED
后记
这个Lab不算难,绝大多数时间要花费在读代码上。我这篇xv6 file system的学习笔记可能会比较帮助:
TODO: (在码了在码了,但愿今天能码完):
MIT 6.S081 Lab File System的更多相关文章
- 9. Lab: file system
https://pdos.csail.mit.edu/6.S081/2021/labs/fs.html 1. Large files (moderate) 1.1 要求 Modify bmap() s ...
- MIT 6.S081 xv6调试不完全指北
前言 今晚在实验室摸鱼做6.S081的Lab3 Allocator,并立下flag,改掉一个bug就拍死一只在身边飞的蚊子.在击杀8只蚊子拿到Legendary后仍然没能通过usertest,人已原地 ...
- MIT 6.S081 Lab5 Copy-On-Write Fork
前言 最近绝大多数的空闲时间都拿来锤15-445了,很久没动6.S081.前几天回头看了一下一个月前锤完的Lazy Allocation,自己写的代码几乎都不认识了.......看来总结之类的东西最好 ...
- RH133读书 笔记(4) - Lab 4 System Services
Lab 4 System Services Goal: Develop skills using system administration tools and setting up and admi ...
- RH253读书笔记(1)-Lab 1 System Monitoring
Lab 1 System Monitoring Goal: To build skills to better assess system resources, performance and sec ...
- RH133读书笔记(11)-Lab 11 System Rescue and Troubleshooting
Lab 11 System Rescue and Troubleshooting Goal: To build skills in system rescue procedures. Estimate ...
- MIT-6.828-JOS-lab5:File system, Spawn and Shell
Lab 5: File system, Spawn and Shell tags: mit-6.828 os 概述 本lab将实现JOS的文件系统,只要包括如下四部分: 引入一个文件系统进程(FS进程 ...
- Storage System and File System Courses
I researched a lot about storage system classes given at good universities this year. This had two r ...
- Can Microsoft’s exFAT file system bridge the gap between OSes?
转自:http://arstechnica.com/information-technology/2013/06/review-is-microsofts-new-data-sharing-syste ...
随机推荐
- 看了CopyOnWriteArrayList后自己实现了一个CopyOnWriteHashMap
引言 面试官: 小伙子你有点眼熟啊,是不是去年来这面试过啊. 二胖: 啊,没有啊我这是第一次来这. 面试官: 行,那我们开始今天的面试吧,刚开始我们先来点简单的吧,java里面的容器你知道哪些啊,跟我 ...
- [linux]makefile多目录
在使用makefile多目录编写前需要掌握几个函数及符号 自定义变量 target=edit 引用的时候直接使用 $(target) 有点像C语言中的#define,这里的 $(target)会被替换 ...
- VIM操作快捷键
i:插入光标前一个字符I:插入行首a:插入光标后一个字符A:插入行末o:向下新开一行,插入行首O:向上新开一行,插入行首M:光标移到中间行L:光标移动到屏幕最后一行行首G:移动到指定行{:按段移动,上 ...
- .NET C#中处理Url中文编码问题
近些日子在做一个用C#访问webservise的程序,由于需要传递中文参数去请求网站,所以碰到了中文编码问题.我们知道像百度这种搜索引擎中,当用户输入中文关键字后,它会把中文转码,以确保在Url中不会 ...
- 探讨EFCore如何优雅的实现读写分离
前言 我们都知道当单库系统遇到性能瓶颈时,读写分离是首要优化手段之一.因为绝大多数系统读的比例远高于写的比例,并且大量耗时的读操作容易引起锁表导致无发写入数据,这时读写分离就更加重要了. ...
- 基础篇:JAVA原子组件和同步组件
前言 在使用多线程并发编程的时,经常会遇到对共享变量修改操作.此时我们可以选择ConcurrentHashMap,ConcurrentLinkedQueue来进行安全地存储数据.但如果单单是涉及状态的 ...
- 解决github下载速度慢问题
众所周知,GitHub是一个巨大的开源宝库,以及程序员和编程爱好者的聚集地,包括我之前推荐的诸多优秀的开源项目全部都是位于GitHub上.但是每当我们看到优秀的开源项目,准备去 下(bai)载(pia ...
- keycloak集成微信登陆~解决国内微信集成的问题
之前看了国内写的微信集成keycloak的文章,然后拿来就用了,但我的是jboss部署的keycloak,然后使用他的包之后,会出现类无法找到的问题,之后找了很多资料,多数都是国外的,在今天终于找到了 ...
- LeetCode117 每个节点的右向指针 II
给定一个二叉树 struct TreeLinkNode { TreeLinkNode *left; TreeLinkNode *right; TreeLinkNode *next; } 填充它的每个 ...
- SpringBoot对静态资源的映射规则
在WebMvcAutoConfiguration类中有相对应的方法addResourceHandlers public void addResourceHandlers(ResourceHandler ...