2020 6.s081——Lab1:Xv6 and Unix utilities梦开始的地方

一任宫长骁瘦

台高冰泪难流

锦书送罢蓦回首

无余岁可偷

——知否知否

完整代码见:6.s081/kernel at util · SnowLegend-star/6.s081 (github.com)

Lecture 01知识点总结

首先透彻理解Lecture01的知识很是重要

pid=wait((int *) 0);

“wait(int *status)”函数用于等待子进程的终止,它的参数是一个指向整数的指针(通常是 int * 类型),并且通常用来存储子进程的终止状态。这里“0”意思是将指针赋值为NULL,表示不关心子进程的退出状态信息,这意味着不打算获取子进程的终止状态。

如果传递一个非NULL的指针给wait(int *status)函数,它将用来存储子进程的退出状态信息,以便可以检查子进程的退出状态,例如子进程是正常终止还是出错等。在这种情况下,应该确保指针指向一个有效的内存位置,以便存储子进程的状态信息。而父进程可以利用status这个指针来获取子进程的退出状态。

exec(char *file, char *argv[])详解

当调用exec(char *file, char *argv[])函数时,xv6 将会卸载当前进程的代码和数据,并加载并执行指定路径的新程序。新程序会接管当前进程的上下文,并开始执行。这意味着 exec 调用后,当前进程的地址空间、堆栈、文件描述符等状态都会被新程序取代。

这对于实现进程的动态加载和替换,以及执行不同的程序非常重要。例如,当您在命令行中运行一个可执行程序时,实际上是通过 exec 系统调用来执行它,从而替换了当前的 shell 进程。

文件描述符

文件描述符fd其实就是代表了open操作对应的那个文件。例如

fd = open("example.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);

int write(int fd, char *buf, int n)

这里的fd就是指“example.txt”这文件;此外,write函数里面的fd不能替换为函数名。因为 write 函数需要一个文件描述符作为其第一个参数,而不是文件名称。文件描述符是一个整数,用于标识已打开文件或其他 I/O 资源。

每个进程都维护一个独立的文件描述符表,其中包含了该进程打开的所有文件和I/O资源的引用。

 

 

运行可执行文件的问题

对于一般的可执行文件a,要运行它的命名形式为“./a”,而不是直接在命令行输入“a”来运行。只有当一个可执行文件位于系统的 PATH 路径中时,可以直接输入其文件名来运行它,而不需要指定完整的文件路径。系统会在 PATH 中的各个目录中查找这个可执行文件,如果找到了匹配的文件,就会执行它。

管道

return 0与exit(0)

int main()函数其实不用return 0,如果main函数没有显式的return语句,编译器将会隐式地在函数末尾插入一个return 0语句,表示程序正常退出并返回0。但是,在xv6只用return 0则会出问题。

在 xv6 中,要正常终止一个进程,应该使用 exit(0) 系统调用而不是在 main 函数中使用 return 0。这是因为 xv6 通过系统调用来通知内核进程的结束,同时执行一些清理工作,以确保资源的正确释放。exit(0) 系统调用会触发这个行为,而简单的 return 0 并不会。

在你提到的错误信息中,"usertrap(): unexpected scause 0x000000000000000d" 是一个异常信息,表明出现了意外的异常类型。当你在 main 函数中使用 return 0 时,进程没有经过适当的清理,导致 xv6 报告了这个异常。

总结起来,为了正常终止 xv6 进程并避免异常错误,应该使用 exit(0) 而不是 return 0。这确保了进程的正确退出并执行必要的清理工作。

Boot xv6 (easy)

我奶奶都能过的lab。注意一点就是得在“xv6-labs-2020”这个文件夹底下运行“make qemu”,而不是在“xv6-labs-2020/user”这个文件夹底下运行它。

sleep

真正意义上梦开始的地方

自己编写的文件得放在“user”文件夹里面,然后在 “Makefile”的“UPROGS”里面添加这个文件名,最后打开qemu进行运行。当然,我们发现不进入qemu也能运行“sleep”这条命令,但是这里的“sleep”命令和我们自己编写的“sleep”函数有出入。那是因为“sleep”是系统的内置函数,只有打开qemu才能运行我们自己编写的“sleep”函数。

今天在linux环境下想直接测试讲义上的“fork”函数,结果总是报错。问了GPT才发现原来书上给的一些函数调用是只有xv6才具有的,在普通的linux系统上并不具备这些函数调用。

sleep()实现如下


#include "kernel/types.h"
#include "user/user.h"int main(int argc, char const *argv[]){if(argc!=2){printf("Error!The function should obtain two argument.\n");exit(1);}sleep(atoi(argv[1]));exit(0);
}

pingpong

这个实验真正意义上把我搞麻了,初次pipe简直就是恶心至极。开始我一直有一个困扰,就是在父进程进行了pipe(p)之后进行fork(),此时父进程和子进程就都可以访问这个管道p了。但是我看到好多题解里面明明在一个进程里面关闭了管道的一端比如“close(p[0])”,但是又在另一个进程里面用到了“read(po[0],buf,sizeof(buf))”。我的想法是既然子进程和父进程都可以对管道进行有效操作,那上述的两个操作不就相悖了吗?这个疑惑困扰了我两天,那个时候四处寻找合适的解释结果都不能很好地解答我的疑惑。然后我又在讲义里看到了另一句话“在一个程序中创建子进程后,子进程将会继承与父进程相同的文件描述符表”,这句话又让我丈二和尚摸不着头脑。后来去群里一问才得知这句话的本质和建立软连接差不多。

       就拿在父进程里面创建的管道来说,子进程自己复制了一份管道的引用。然后close()操作只是作用于这份引用,也就是说子进程自己不能再使用某个端口的引用了,但是这并不影响父进程对两个端口的操作。领悟到这一层后,我又去测试了一番,果真是这样。这个困扰我几天的问题一解决,那pingpong的实现就变得有头绪了。

       对于实现过程,我最开始想不知道怎么保证父子进程之间的同步关系,在两个进程里面都是先write()然后接着read()。由于进程执行的异步性必然会导致两个进程的输出交杂在一起,就像这样“43:r:erceecieviev epo pnigng”。

后来想到的可能的解决办法是用wait()来保证子进程和父进程之间的同步关系。但是这个技巧在这题行不通,因为实验要求我们先打印子进程收到了数据,这就要求父进程自己得先写入,那把wait()放在父进程部分的开头就会导致类似死锁一样的结果。参考率其他人的结果,他们都是通过控制子进程的write()部分在read()部分后面来保证输出同步。用wait()一样可以实现,把wait()放在父进程的wirte()后但是read()之前即可解决。

pingpong()实现如下
 


#include"kernel/types.h"
#include"user/user.h"
#include "kernel/stat.h"int main(){int pipe_ptc[2];    //父进程用来给子进程写入信息的管道int pipe_ctp[2];    //子进程用来给父进程写入东西的管道char* ptc_msg="ping",*ctp_msg="pong",ptc[256],ctp[256];if(pipe(pipe_ctp)==-1){printf("There is something wrong with pipe()!");exit(1);}if(pipe(pipe_ptc)==-1){printf("There is something wrong with pipe()!");exit(1);}if(fork()!=0){//子进程的fork()返回0write(pipe_ptc[1],ptc_msg,sizeof(ptc_msg));close(pipe_ptc[1]);                                 //关闭父进程管道的写入端int parent_pid=getpid();wait((int *)0);if(read(pipe_ctp[0],ctp,256)!=-1){printf("%d: received pong\n",parent_pid);}close(pipe_ctp[0]);                                 //关闭子进程管道的读入端exit(0);}else{int child_pid=getpid();write(pipe_ctp[1],ctp_msg,sizeof(ctp_msg));close(pipe_ctp[1]);                                 //关闭子进程管道的写入端if(read(pipe_ptc[0],ptc,256)!=-1){printf("%d: received ping\n",child_pid);}close(pipe_ptc[0]);                                 //关闭父进程的读入端exit(0);                                            //很重要,要不然子进程不会退出}exit(0);
}

primes

这题主要就是理解文档给的那个“线程筛”图——即让父进程按从小到大的顺序把所有的数都传给子进程,然后子进程排除掉不能被第一个素数2整除的传递给孙进程,孙进程再排除掉不能被第二个素数3整除的传递给它的子进程,以此类推……很显然,每个进程传给它的子进程的那批数字里面第一个是最小的且是素数,递归得到的第i个进程可以得到第i-1个素数。

接下来详细描述下处理过程:

假设最初始的进程p0 对那些数不做任何处理,直接从小到大一股脑传给第一代子进程p1 。接下的一代代子孙进程对数据的处理就具有一般性了。首先,子进程pi-1 接收从它的父进程pi

传过来的数据,且pi 传过来的第一个数n1 为我们得到的第i-1个素数。pi-1 接收完数据后把那些不能被n1 整除的数据传递给它的子进程pi-2 ,而后进程pi-2

进行类似的处理。显然,我们如果要完成上述进程的两个通信过程,就势必得用到两个管道。同时,由于xv6的文件描述符有限,我们就要及时关闭用不到的管道端(即文件描述符)。这里提一句,务必养成及时关闭文件描述符的好习惯,不然最后总是会有各种输出卡住的问题。

还有些实现的细节需要注意。我遇到的问题是一开始习惯性地创建了个读缓冲区的数组buf[40],然后读取管道中的数据就是read(0,buf,sizeof(buf))。乍一看没问题,但是考虑到如果管道里面的数据没有40个,那这个进程就会被阻塞在这里,等着管道的写入端继续写入数据直到把40个数据读满。所以这题应当是一个整形一个整形地读入数据。

 

 primes()实现如下
 

#include"kernel/types.h"
#include"user/user.h"
#include "kernel/stat.h"void child(int pipe_p2c[]){// int buf[40];                           //buf[0]里面的元素应该是最小的int elem,min;                          int pipe_s2g[2];                       //子进程给孙进程通信的管道close(pipe_p2c[1]);// for(i=0;i<40;i++)//     buf[i]=0;// if(read(pipe_p2c[0],buf,40)==0){   //如果没东西可以读了就可以开始退出了       不能一下子读入sizeof(buf),因为管道里面的元素没有这么多if(read(pipe_p2c[0],&min,sizeof(int))==0){close(pipe_p2c[0]);exit(0);}printf("prime %d\n",min);pipe(pipe_s2g);if(fork()!=0){//子进程准备给孙子进程写东西了close(pipe_s2g[0]);                        //把管道的读入端关掉再说// for(i=1;buf[i]!=0;i++){//     if(buf[i]%buf[0]!=0)                //不能相除的才传给下一辈//         write(pipe_s2g[1],&buf[i],1);   //一个字节一个字节地写入管道// }while(read(pipe_p2c[0],&elem,sizeof(int))!=0){if(elem%min!=0)write(pipe_s2g[1],&elem,sizeof(int));}close(pipe_p2c[0]);close(pipe_s2g[1]);wait(0);exit(0);}else{//孙子进程child(pipe_s2g);}
}int main(){int i;int pipe_p2c[2];pipe(pipe_p2c);if(fork()!=0){close(pipe_p2c[0]);for(i=2;i<=35;i++){//把这些数字依次写入管道里面准备让子进程读write(pipe_p2c[1],&i,sizeof(int));}close(pipe_p2c[1]);//及时关闭文件描述符,不然输出会在这里卡住wait(0);exit(0);        }else{child(pipe_p2c);}exit(0);
}

find

这题主要是对题目的要求思考了很久,一开始没理解find命令在类unix机器上是怎么用的。查阅了下资料,发现这里的find命令大概格式如下“find path filename”,这里的path既可以是一个目录如“./a/b”,也可以是一个具体的文件路径“./a/c.txt”。

然后第二个困扰了我很久的点是题目的hint3:Don't recurse into "." and ".."我心想平时在windows系统或者是linux系统里面查看文件夹的内容时从来就没有看到过“.”和“..”这两个特殊的文件啊?后来问GPT说是用ls打开文件目录就可以看到这两个特殊的目录项,尝试了一番依然没有发现。直到后面进入qemu的时候再调用ls发现上来就把这两个特殊的目录项给列出来了。对于命令中要含有“.”和“..”我也不理解是个怎样的形式,问了GPT半天才发现形式可以如下“ls -l /path/to/some/directory/./../another/directory”。解决了这两个疑惑,就可以着手完成find了。

根据hint1先看一遍lc.c很容易搞得自己一头雾水,我看了两遍之后还有些不得要领,遂直接开始照着lc.c来实现find的功能,边写边理解。其实两个最后先出来大同小异,就是find传过去的参数不仅有“path”,还有“filename”。在lc.c原有的基础上适当加上对“filename”的匹配处理即可。

从ls.c中我们可以看到先是用open()打开传过去的“path”,看用户给出的“path”是否能够被访问。然后用fstat()把这个文件的信息存入stat结构体中。处理完这个“path”后,如果path是个目录,就开始对该目录底下的目录项进行遍历,目录项有文件和目录两种类型。对于目录项是目录的情况,我们又可以把这个子目录信息存入dirent这个结构体中进行递归遍历。

在完成实验要求的find功能之后,我心血来潮统计了下qemu当前存在的目录项。一个有趣的结果是“.”和“..”并没有被计入在目录项里面。这我就有一个猜测了,“.”和“..”本质就是软连接。又发现了个很奇怪的问题,把“dir_ItemNum”放在if(de.inum)前面就会导致输出结果总是为64,而且就算在xv6内部继续添加文件也还是如此,这是为什么呢?但是如果再在当前目录创建./a/b这个子目录,统计结果又正常了。这时在使用命令“find . a”会发现dir_ItemNum的值增加了3。难不成xv6的当前目录内置了文件上限是64且都已经被创建好了,只不过没有内容导致这些目录项的inum是0,但是遍历当前文件夹的时候还是会遍历之?

find()如下
 

#include"kernel/types.h"
#include"kernel/stat.h"
#include"user/user.h"
#include"kernel/fs.h"char *fmtname(char *path){static char buf[DIRSIZ+1];char *p;//find first character after last slashfor(p=path+strlen(path);p>path&&*p!='/';p--);p++;//return blank-padded nameif(strlen(p)>=DIRSIZ)return p;memmove(buf,p,strlen(p));memset(buf+strlen(p),' ',DIRSIZ-strlen(p));return buf;
}int flag=0;
int dir_ItemNum=0;    //看一下当前目录底下有多少条信息 “.”和“..”没被统计进去void find(char *directory,char *filename){char buf[512],*p;       //p作为定位指针int fd;struct dirent de;struct stat st;if((fd=open(directory,0))<0){printf("find: cannot open %s\n",directory);exit(1);}if(fstat(fd,&st)<0){printf("find: cannot stat %s\n",directory);close(fd);exit(1);}struct stat stat_temp;  //这句话不能定义在case内部吗?    switch(st.type){case T_DEVICE:case T_FILE:if(strcmp(fmtname(directory),filename)==0){printf("%s\n",directory);flag=1;}exit(0);case T_DIR:if(strlen(directory)+1+DIRSIZ+1>sizeof(buf)){printf("find: path too long\n");break;}strcpy(buf,directory);              //buf用来存当前正在访问目录的路径p=buf+strlen(buf);*p++='/';                           //把p挪到buf的最后一个元素上while(read(fd,&de,sizeof(de))==sizeof(de)){// dir_ItemNum++;  //怎么把这句放在if(d.inum)前面只能输出64呢?                if(de.inum==0)                  //如果 inum 字段等于 0,通常表示该目录项无效或未使用continue ;memmove(p,de.name,DIRSIZ);      //把文件名都存在buf刚添上的“/”后面p[DIRSIZ]=0;if(stat(buf,&stat_temp)<0){     //这不是多此一举吗  有用的,可以判断当前目录下打开的文件是什么类型printf("find: cannot stat %s\n",buf);continue;}if(stat_temp.type==T_FILE)      //如果是文件类型if(strcmp(de.name,filename)==0){printf("%s\n",buf);flag=1;                        }if(stat_temp.type==T_DIR){if((strcmp(de.name,".")==0)||(strcmp(de.name,"..")==0))     //得排除掉“.”和“..”这两个目录防止无限递归continue ;find(buf,filename);         //递归访问这个目录 开始把buf写成de.name了,有点小丑                   }}}return ;
}int main(int argc,char *argv[]){if(argc!=3){printf("Usage: find <directory> <filename>\n");exit(1);}find(argv[1],argv[2]);printf("%d\n",dir_ItemNum);if(flag==0)printf("Fail to find the file '%s'!\n",argv[2]);exit(0);
}

xargs

这个这个函数的实验要求十分怪异,我看半天没理解到底要实现哪些功能,还以为得实现到“find xx xx | xargs echo xxx”这种程度。谁知它只需要实现和echo的组合功能即可,这就简单不少了。

下面来简单介绍下管道命令“|”和“xargs”在类unix系统里的用法。用

$ echo “hello” | grep root

管道命令的作用,是将左侧命令(cat “hello)的标准输出转换为标准输入,提供给右侧命令(grep root)作为参数。但是,大多数命令都不接受标准输入作为参数,只能直接在命令行输入参数,这导致无法用管道命令传递参数。举例来说,echo命令就不接受管道传参。而xargs命令的作用,是将标准输入转为命令行参数。所以一般情况下“|”和“xargs”是穿一条裤子的。

实验给出的hint是用fork和exec两个调用来实现这个功能。我们得注意一点,我们正在实现的函数是“xarg.c”!所以agrcargc[]两个参数是以命令行中的xagrs命令以及它后面跟着的参数为操作对象的,千万不要以为这两个参数读取的是整个命令行的全部命令和参数!我开始就因为弄混了又搞半天才发现问题所在。如果想要读取整个命令行的数据,我们可以利用“read(0,p,1)”来一个字符一个字符地读取所有内容,直到读取到“\0”为止。因为在许多操作系统中,标准输入(stdin)的文件描述符通常默认已经打开,因此可以直接调用 read(0, buf, sizeof(buf)) 来从标准输入读取数据。

这里又有个小坑,开始问GPT“假如char p=' ',那while(p)会直接跳出吗?”GPT回答说是,我开始信以为真,直到去dev c++上去测试了下并不然。只有p=‘\0’时while(p)才能直接退出。

踩完以上两个坑后,思路就明了了起来。当前进程是实现xargs,那就得再fork()一个子进程,子进程调用exec()来实现整个命令行的命令。需要注意的是题目中要求为每一行执行一个命令,所以用数组char** arguments来存储命令行的命令的时候是一行一行地读取,即一行读一个命令或者参数。这里用不用管道都可以实现父进程给子进程传递xargs跟着的相应参数。

xargs代码如下

#include"kernel/types.h"
#include"kernel/stat.h"
#include"user/user.h"
#include"kernel/fs.h"
#include"kernel/param.h"int main(int argc,char *argv[]){int i;char buf[128]={0};int len;char* arguments[32];//用来读入命令行的输入if(argc==1){printf("Usage: xagrs [Command] [para1] ...[para n]");exit(1);}for(i=1;i<argc;i++){len=sizeof(argv[i])+1;arguments[i-1]=(char *)malloc(sizeof(char)*len);strcpy(arguments[i-1],argv[i]);    //不能简单地用“=”来赋值,两者的类型是不一样的}i--;    //因为这里i已经到了agrc那么大,但实际上arguments数组的下标才记到i-1char *p=buf;read(0,p,1);while(*p){          //苦也,GPT误我!if(*p=='\n'){ //如果读到一行末尾*p='\0';    //加上'\0'从而构成字符串len=sizeof(buf)+1;arguments[i]=(char*)malloc(sizeof(char)*len);strcpy(arguments[i],buf);i++;memset(buf,0,128);  //将buf置为初始状态p=buf;  //将p指针重新定位到buf开头read(0,p,1);continue;}p++;read(0,p,1);}arguments[i]=0;if(fork()==0){if(exec(argv[1],arguments)==-1){printf("xargs: exec failed.\n");exit(1);}}wait(0);    //回收子进程exit(0);
}

If you fail a test, make sure you understand why your code fails the test. Insert print statements until you understand what is going on.

今天刚在群里看到说相比于gdb,直接用printf检查代码的执行情况会更为简单方便。经历了CSAPP的拷打之后,我对此深以为然。还记得proxy lab就是printf大法立功。今天第一次打开s081的Lab guidance,发现了上面那句话,看来printf的好用之处已经广为人知了。无知时诋毁printf,成长时理解printf,成熟时加入printf

最后为了先把项目上传到github的仓库里面,也是折腾了一晚上,索性终有所得。

 

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

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

相关文章

记一次服务器数据库被攻击勒索

如图&#xff0c;早上一起来就发现&#xff0c;我的MongoDB数据库里面的信息全部没有了&#xff0c;只留下一段话。 大致意思就是&#xff1a;我的数据库的数据被他们备份然后全部删掉了&#xff0c;我必须要支付0.0059的bitcoin&#xff08;折合400美刀&#xff09;来赎回我的…

不是,你不会还在用双层遍历循环来做新旧数组对比,寻找新增元素吧?

目录 一、双层循环遍历 1.1、双循环错误示范 1.2、正确的做法 ①使用array.includes() ②使用set 二、array.includes()的使用与技巧 2.1、基本语法 2.2、返回值 2.3、使用技巧 2.3.1、用户输入验证 2.3.2、权限检查 2.4、兼容问题 三、总结 一、双层循环遍历 1.…

OpenCV学习 基础图像操作(十六):图像距离变换

基础原理 顾名思义&#xff0c;我们可以利用像素之间的距离作为对该像素的一种刻画&#xff0c;并将其运用到相应的计算之中。然而&#xff0c;在一幅图像之中&#xff0c;某种类型的像素并不是唯一的&#xff0c;因此我门常计算的是一类像素到另一类的最小距离&#xff0c;并…

香橙派KunPengPro评测

一、引言 二、开箱 2.1、主要包含说明 1、充电器(赠typec-c线) 2、香橙派kunpengpro(已经带装好带散热器) 3、SD卡(32G)(已经带装好系统openEuler 22.03 (LTS-SP3)) (注意&#xff1a;上电接HDMI线可直接用&#xff0c;账号&#xff1a;openEuler 密码&#xff1a;openEuler)…

vue使用tailwindcss

安装依赖 pnpm add -D tailwindcss postcss autoprefixer创建配置文件tailwind.config.js npx tailwindcss init在配置文件content中添加所有模板文件的路径 /** type {import(tailwindcss).Config} */ export default {content: [./index.html, ./src/**/*.{vue,js,ts,jsx,…

【Linux】开发工具入门指南,轻松掌握你的开发利器

开发工具 1. 软件包管理器yum1.1 软件包安装方式1.2 yum的"三板斧"1.3 yum的周边 2. 开发工具3. 编辑器vim4. 编译器gcc、g5. 项目自动化构建工具make、Makefile6. 进度条小程序7. 调试器gdb 1. 软件包管理器yum 1.1 软件包安装方式 源代码安装&#xff1a;用户手动…

微信小程序 npm构建+vant-weaap安装

微信小程序&#xff1a;工具-npm构建 报错 解决&#xff1a; 1、新建miniprogram文件后&#xff0c;直接进入到miniprogram目录&#xff0c;再次执行下面两个命令&#xff0c;然后再构建npm成功 npm init -y npm install express&#xff08;Node js后端Express开发&#xff…

智慧校园的机遇与挑战

随着5G、物联网、大数据等技能的日渐老练&#xff0c;数字化正在渗透到各行各业中&#xff0c;为事务立异和价值增加供给支撑。在教育职业&#xff0c;运用智能化体系赋能教育办理越来越受欢迎&#xff0c;教育信息化方针一再出台&#xff0c;进一步加快了智慧校园落地的脚步。…

Linux - 文件管理高级 sed

3.处理字符 sed ① sed 默认情况下不会修改原文件内容 ② sed 是一种非交互式的编辑器 3.1 工作原理 将原文件一行一行的进行处理&#xff0c;取出一行&#xff0c;放入“模式空间进行处理”&#xff0c;处理完成之后将结果输出到屏幕上&#xff0c;然后读取下一行&#xf…

彭涛 | 2024年5月小结

5月份还是蛮有刺激的&#xff0c;做了蛮多的事情&#xff0c;但是没赚到钱&#xff0c;真是一屯操作猛如虎&#xff0c;一看账户0.5。 就喜欢创业这种一天天累死累活还不赚钱的感觉&#xff0c;哈哈哈哈 老规矩简单说下这个月的情况&#xff0c;如果对你有收获就最好了。 游学丹…

【Tlias智能学习辅助系统】04 部门管理 删除 和 新增

Tlias智能学习辅助系统 04 部门管理 删除 和 新增 删除部门APIDeptController.javaDeptService.javaDeptServiceImpl.javaDeptMapper.java前端联调 新增部门API有一步简化DeptController.javaDeptService.javaDeptServiceImpl.javaDeptMapper.java前端联调 删除部门API 请求路径…

Selenium+Java 环境搭建

selenium 介绍 Selenium 是 web 应用中基于 UI 的自动化测试框架&#xff0c;支持多平台、多浏览器、多语言。 早期的 selenium RC 已经被现在的 webDriver 所替代&#xff0c;可以简单的理解为selenium1.0webdriver 构成 现在的 Selenium2.0 。现在我们说起 selenium &#xf…

适合学生写作业的台灯有哪些?台灯怎么选详细攻略!

在数字化飞速发展的今天&#xff0c;孩子们的学习和生活越来越离不开电子屏幕。然而&#xff0c;长时间盯着屏幕&#xff0c;不仅容易让眼睛感到疲劳&#xff0c;更是近视问题日益严重的元凶之一。每一位家长都希望孩子能拥有健康的视力&#xff0c;因此会为孩子挑选一台护眼灯…

MySQL十部曲之九:MySQL优化理论

文章目录 前言概述查询优化查询执行计划EXPLAIN获取表结构信息获取执行计划信息 EXPLAIN 输出格式如何使用EXPLAIN进行优化 范围访问优化单列索引的范围访问多列索引的范围访问 索引合并优化索引合并交叉访问算法索引合并联合访问算法索引合并排序联合访问算法 索引下推优化连接…

豆包浏览器插件会造成code标签内容无法正常显示

启用状态&#xff1a;页面的代码会显示不正常 禁用后&#xff0c;正常显示 害得我重置浏览器设置&#xff0c;一个个测试

spring mvc 中怎样定位到请求调用的controller

前言 在java web开发过程中&#xff0c;正常情况下controller都是我们自己写的&#xff0c;我们可以很方便的定位到controller的位置。但是有些时候我们引入的其他依赖中可能也有controller&#xff0c;为了找到并方便的调试jar包中的controller&#xff0c;我们一般会进行全局…

【CPP】双端队列简介(deque)

简介&#xff1a;双端队列(deque) 目录 1.概述2.特点3.底层原理 1.概述 双端队列&#xff1a;是一种顺序表和顺序表的结合数据结构&#xff0c;不是队列。 它提供顺序表的[]下标访问和链表的中间头部的较高效率插入删除操作。 2.特点 顺序表的优缺点&#xff1a; 优点&…

linux之docker- image.tar 的导出和导入

一、情况 docker 镜像有时无法从外网访问&#xff0c;需要把docker 打包导出到本地&#xff0c;然后以文件的形式&#xff0c;发送给其他人&#xff0c;再然后其他人把docker 镜像文件导入到自己的服务器本地镜像仓库&#xff0c;方可使用。也可把镜像上传到公司内网。下面就开…

Verilog HDL基础知识(二)

引言&#xff1a;本文继续介绍Verilog HDL基础知识&#xff0c;重点介绍赋值语句、阻塞与非阻塞、循环语句、同步与异步、函数与任务语法知识。 1. 赋值语句 在Verilog中&#xff0c;有两种进行赋值的方法&#xff0c;即连续赋值语句和过程赋值语句&#xff08;块&#xff09…

Java数据结构-二叉搜索树

目录 1. 概念2. 二叉搜索树的操作2.1 查找2.2 插入2.3 删除 3. 全部代码 1. 概念 二叉搜索树是特殊的二叉树,也叫二叉排序树,它的特点是:每个结点的左子树上的所有结点的值都小于这个结点的值,右子树上的所有结点的值都大于这个结点的值,另外所有的左子树和右子树也分别为二叉…