为了做好运维面试路上的助攻手,特整理了上百道 【运维技术栈面试题集锦】 ,让你面试不慌心不跳,高薪offer怀里抱!
这次整理的面试题,小到shell、MySQL,大到K8s等云原生技术栈,不仅适合运维新人入行面试需要,还适用于想提升进阶跳槽加薪的运维朋友。
本份面试集锦涵盖了
- 174 道运维工程师面试题
- 128道k8s面试题
- 108道shell脚本面试题
- 200道Linux面试题
- 51道docker面试题
- 35道Jenkis面试题
- 78道MongoDB面试题
- 17道ansible面试题
- 60道dubbo面试题
- 53道kafka面试
- 18道mysql面试题
- 40道nginx面试题
- 77道redis面试题
- 28道zookeeper
总计 1000+ 道面试题, 内容 又全含金量又高
- 174道运维工程师面试题
1、什么是运维?
2、在工作中,运维人员经常需要跟运营人员打交道,请问运营人员是做什么工作的?
3、现在给你三百台服务器,你怎么对他们进行管理?
4、简述raid0 raid1raid5二种工作模式的工作原理及特点
5、LVS、Nginx、HAproxy有什么区别?工作中你怎么选择?
6、Squid、Varinsh和Nginx有什么区别,工作中你怎么选择?
7、Tomcat和Resin有什么区别,工作中你怎么选择?
8、什么是中间件?什么是jdk?
9、讲述一下Tomcat8005、8009、8080三个端口的含义?
10、什么叫CDN?
11、什么叫网站灰度发布?
12、简述DNS进行域名解析的过程?
13、RabbitMQ是什么东西?
14、讲一下Keepalived的工作原理?
15、讲述一下LVS三种模式的工作过程?
16、mysql的innodb如何定位锁问题,mysql如何减少主从复制延迟?
17、如何重置mysql root密码?
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
#include <unistd.h>
int main()
{
int cnt = 0;
while(1)
{
int ret = fork();
if(ret < 0){
printf(“fork error!, cnt: %d\n”, cnt);
break;
}
else if(ret == 0){
//child
while(1) sleep(1);
}
//partent
cnt++;
}
return 0;
}
当运行后就会出现如下错误:
>
> -bash: fork: retry: No child processes
> -bash: fork: retry: No child processes
> -bash: fork: retry: No child processes
> -bash: fork: retry: No child processes
> -bash: fork: Resource temporarily unavailable
> -bash-4.2$
>
>
>
**解决方法:**
>
> 1.kill -9 -1 //将进程全部杀死
>
>
> 2.重新增加一个用户使用
>
>
>
###
### 进程终止
---
#### 进程退出场景
>
> 代码运行完毕,结果正确 --return 0;
>
>
> 代码运行完毕,结果不正确 --return !0;退出码
>
>
> 代码异常终止 --退出码无意义
>
>
>
#### **进程常见退出方法**
**正常终止(可以通过 echo $? 查看进程退出码)**:
1. 从main返回--return 返回
2. 调用exit --任意地方调用
3. \_exit
**异常退出:**
ctrl + c,信号终止
**return退出**
return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
我们通过return返回0为正确,返回其他为错误,代码如下:
#include <stdio.h>
int addToTarget(int from, int to)
{
int sum = 0;
int i=0;
for(i = from; i < to; i++)
{
sum += i;
}
return sum;
}
int main()
{
//进程退出的时候,对应的退出码
//标定进程执行结果是否正确
int num = addToTarget(1, 100);
if(num == 5050)
return 0;
else
return 1;
return 0;
}
>
> **运行结果:**
>
>
> [hongxin@VM-8-2-centos 12-14]$ ./mytest
> [hongxin@VM-8-2-centos 12-14]$ echo $?
> 1
> [hongxin@VM-8-2-centos 12-14]$ echo $?
> 0
>
>
>
>
> **代码解释:**
>
>
> ./mytest:运行一个进程
>
>
> echo:显示
>
>
> $?:永远记录最近的一个在命令行中执行完毕时对应的退出码(main->return ?:)
>
>
> 这里的1:标识错误,是mytest进程中代码不正确
>
>
> 这里的0:标识正确,因为echo $?本来都是一个进程
>
>
>
前面也说过退出码0标识成功,其他表示不正确,但是不同的数字可以描述不同错误,对于计算机来说很好识别数字,但对于程序员来说语言描述是更加友好。所以在学习c语言的时候,我们就学习过strerror,就是用字符串进行描述,它大概有134种标识;
>
> **运行代码:**
>
>
> for(int i=0;i<200;i++)
> {
> printf("%d:%s\n",i,strerror(i) );
> }
>
>
>
>
> **运行结果:**
>
>
> [hongxin@VM-8-2-centos 12-14]$ ./mytest
> 0:Success
>
>
> 1:Operation not permitted
> 2:No such file or directory
> 3:No such process
> 4:Interrupted system call
> 5:Input/output error
> 6:No such device or address
>
>
> ....................................
>
>
> 127:Key has expired
> 128:Key has been revoked
> 129:Key was rejected by service
> 130:Owner died
> 131:State not recoverable
> 132:Operation not possible due to RF-kill
> 133:Memory page has hardware error
> 134:Unknown error 134
>
>
>
**exit函数**
>
> #include <unistd.h>
>
>
> void exit(int status);
>
>
> 参数:status 定义了进程的终止状态,父进程通过wait来获取该值
>
>
> 说明:虽然status是int,但是仅有低8位可以被父进程所用。所以\_exit(-1)时,在终端执行$?发现返回值 是255。
>
>
>
当exit(-1)时,结果如下
>
> **运行结果:**
>
>
> [hongxin@VM-8-2-centos 12-14]$ ./mytest
> hello bit![hongxin@VM-8-2-centos 12-14]$ echo $?
> 255
>
>
>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
int addToTarget(int from, int to)
{
int sum = 0;
int i=0;
for(i = from; i < to; i++)
{
sum += i;
}
// return sum;
exit(12);
}
int main()
{
printf("hello world!\n");
int ret = addToTarget(0, 100);
printf("sum=%d\n", ret);
while(1) sleep(1);
}
>
> [hongxin@VM-8-2-centos 12-14]$ ./mytest
> hello world! //exit在调用addToTarget函数时直接退出
>
>
> [hongxin@VM-8-2-centos 12-14]$ echo $?
> 12
>
>
>
**\_exit函数与exit函数**
使用方法不变,exit是库函数,\_exit系统调用。他们本质就是上下层关系。
实例:
>
> int main()
>
>
> {
>
>
> printf("hello");
>
>
> exit(0);
>
>
> }
>
>
> 运行结果:
>
>
> [root@localhost linux]# ./a.out
>
>
> hello[root@localhost linux]#
>
>
> //前面两秒没有数据,后两秒显示数据
>
>
> ------------------------------------------------------------------------------------------------------
>
>
> int main()
>
>
> {
>
>
> printf("hello");
>
>
> \_exit(0);
>
>
> }
>
>
> 运行结果:
>
>
> [root@localhost linux]# ./a.out
>
>
> [root@localhost linux]#
>
>
> //前两秒没有数据,两秒后程序直接退出
>
>
>
**结论:**exit 终止进程,主动刷新缓冲区;\_exit终止进程,不会刷新缓冲区
那么缓存区再哪儿呢?
我们发现如果缓冲区在操作系统层,不管是exit还是\_eixt都会刷新缓冲区。实则是在用户级的缓存区,后面基础I/O会将。
**exit最后也会调用exit, 但在调用exit之前,还做了其他工作:**
1. 执行用户通过 atexit或on\_exit定义的清理函数。
2. 关闭所有打开的流,所有的缓存数据均被写入
3. 调用\_exit
![](https://img-blog.csdnimg.cn/e6bffcee986948978f603079411055ff.png)
###
### 进程等待
---
#### 进程等待必要性
之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法 杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
#### 进程等待的方法
**wait方法**
>
> #include <sys/types.h>
>
>
> #include <sys/wait.h>
>
>
> pid\_t wait(int\*status);
>
>
> 返回值:
>
>
> 成功返回被等待进程pid,失败返回-1。
>
>
> 参数:
>
>
> 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
>
>
>
通过fork创建进程, 每次打印时睡眠1秒,观察这5秒的状态。等子进程结束后,睡眠10秒,观察该进程的状态,最后wiat等待后,父进程接受到子进程后的状态。
>
> **脚本:**
>
>
> while :; do ps axj | head -1 && ps axj | grep mytest| grep -v grep ;sleep 1; done
>
>
>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程
int cnt = 5;
while(cnt)
{
printf(“我是子进程: %d, 父进程: %d, cnt: %d\n”, getpid(), getppid(), cnt–); sleep(1);
}
exit(0); //进程退出
}
sleep(10);
pid_t ret = wait(NULL);
if(id > 0)
{
printf("wait success: %d", ret);
}
sleep(5);
}
![](https://img-blog.csdnimg.cn/2a04bdcfc04641b0b84e1c1a7696c722.png)
通过观察我们发现最开始为S状态--等待状态,子进程结束后未被父进程接受变成Z状态--僵尸状态,最后通过等待后返回子进程信息,子进程结束,父进程运行。
**waitpid方法**
>
> pid\_ t waitpid(pid\_t pid, int \*status, int options);
>
>
> 返回值:
>
>
> 当正常返回的时候waitpid返回收集到的子进程的进程ID;
>
>
> 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
>
>
> 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
>
>
> 参数:
>
>
> pid:
>
>
> Pid=-1,等待任一个子进程。与wait等效。
>
>
> Pid>0.等待其进程ID与pid相等的子进程。
>
>
> status:
>
>
> WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
>
>
> WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
>
>
> options:
>
>
> WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
>
>
>
**获取子进程status**
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
![](https://img-blog.csdnimg.cn/08830237245749ae9c8b428c38cdf6ab.png)
整数的低 16 位,其中又可以分为 **最低八位** 和 **次低八位**
*其中重要的是:可以通过提取 status 的次低八位,就可以拿到子进程的退出码。*
core dump--核心转储
*它是操作系统在进程收到某些信号而终止运行时,将此时进程地址空间的内容以及有关进程状态的其他信息写出的一个磁盘文件,这种信息往往用于调试。**后面在信号阶段的时候会详细介绍。*
**代码测试如下:**
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程
int cnt = 5;
while(cnt)
{
printf(“我是子进程: %d, 父进程: %d, cnt: %d\n”, getpid(), getppid(), cnt–);
int *p = NULL;
*p = 100;
sleep(1);
}
// 运行完
// 1. 代码完,结果对
// 2. 代码完,结果不对
// 异常
// 3. 代码没跑完,出异常了
exit(12); //进程退出
//`` exit(0); //进程退出
}
int status = 0; // 不是被整体使用的,有自己的位图结构
pid_t ret = waitpid(id, &status, 0);
if(id > 0)
{
printf("wait success: %d, sig number: %d, child exit code: %d\n", ret, (status & 0x7F), (status>>8)&0xFF);
}
sleep(5);
}
关于下面代码,我们知道*status*本质是位图,位图可以对指定区域进行访问,那么我们只需要取其次低八位。所以我们可以用 **位操作**来完成,将*status* 右移八位再按位与上 ![\textrm{0xFF}](https://latex.csdn.net/eq?%5Ctextrm%7B0xFF%7D),即 (*status*>>8)&0xFF ,就可以提取到 *status*的次低八位了。
| |
| --- |
| printf("wait success: %d, sig number: %d, child exit code: %d\n", ret, (status & 0x7F), (status>>8)&0xFF); |
该代码其目的就是为了让父进程获取到子进程的退出码,如下实验结果也是证实了在子进程中获取的pid,ppid与我们通过提取 status 的次低八位的结果一样。就证实了可以通过*status* 拿到进程的退出码。
>
> **运行结果:**
>
>
> [hongxin@VM-8-2-centos 12-15]$ ./mytest
> 我是子进程: 6347, 父进程: 6346, cnt: 5
> wait success: 6347, sig number: 11, child exit code: 0
>
>
>
![](https://img-blog.csdnimg.cn/0538314aecbc4eeebd5e859499d9442c.png)
通过kill手册可以明确地得到是野指针的问题!
#### **阻塞与非阻塞**
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。
![](https://img-blog.csdnimg.cn/e272b13fdc214a01bb3b845fc6cc30b3.png)
用更直白的话就是:在子进程运行的时候,父进程一直处于等待状态,这个时候父进程一直检测子进程状态,这个时候父进程没有干其他事情,这个过程就叫做---阻塞。
相反的,如果在子进程运行的时候,父进程一直处于等待状态,这个时候父进程一直检测子进程状态,如果没有就绪,那么就直接返回,这个时候父进程能干一其他的事,这个过程就叫---非阻塞。每一次都是一次非阻塞等待;多次非阻塞等待就叫--轮询
那么非阻塞有什么好处呢!如果当父进程检查到子进程未就绪,那么父进程就可以做一些其他的任务。
非阻塞的好处就是:不会占用父进程的所有精力,可以在轮询期间,做其他任务。
**非阻塞**
代码测试:
#include <assert.h>
#define NUM 10
typedef void (*func_t)(); //函数指针
func_t handlerTask[NUM];
//样例任务
void task1()
{
printf(“handler task1\n”);
}
void task2()
{
printf(“handler task1\n”);
}
void task3()
{
printf(“handler task1\n”);
}
void loadTask()
{
memset(handlerTask, 0, sizeof(handlerTask));
handlerTask[0] = task1;
handlerTask[1] = task1;
handlerTask[2] = task1;
}
void addtask()
{}
int main()
{
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
//child
int cnt = 10;
while(cnt)
{
printf(“child running, pid: %d, ppid: %d, cnt: %d\n”, getpid(), getppid(), cnt–);
sleep(1);
// int *p = 0;
// *p = 100; //野指针问题
}
exit(10);
}
loadTask();
// parent
int status = 0;
while(1)
{
pid_t ret = waitpid(id, &status, WNOHANG); //WNOHANG: 非阻塞-> 子进程没有退出, 父进程检测时候,立即返回
if(ret == 0)
{
// waitpid调用成功 && 子进程没退出
//子进程没有退出,我的waitpid没有等待失败,仅仅是监测到了子进程没退出.
printf("wait done, but child is running...., parent running other things\n");
for(int i = 0; handlerTask[i] != NULL; i++)
{
handlerTask[i](); //采用回调的方式,执行我们想让父进程在空闲的时候做的事情
}
}
else if(ret > 0)
{
// 1.waitpid调用成功 && 子进程退出了
printf(“wait success, exit code: %d, sig: %d\n”, (status>>8)&0xFF, status & 0x7F);
break;
}
else
{
// waitpid调用失败
printf(“waitpid call failed\n”);
// break;
}
sleep(1);
}
return 0;
}
>
> **运行结果:**
>
>
> [hongxin@VM-8-2-centos 12-16]$ make
> gcc -o mychild mychild.c -std=c99
> [hongxin@VM-8-2-centos 12-16]$ ./mychild
> wait done, but child is running...., parent running other things
> handler task1
> handler task1
> handler task1
> child running, pid: 22206, ppid: 22205, cnt: 10
> wait done, but child is running...., parent running other things
> handler task1
> handler task1
> handler task1
>
>
> .......................................................................................................................
>
>
> wait done, but child is running...., parent running other things
> handler task1
> handler task1
> handler task1
> wait done, but child is running...., parent running other things
> handler task1
> handler task1
> handler task1
> wait success, exit code: 10, sig: 0
>
>
>
### 进程程序替换
---
#### **替换原理**
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
![](https://img-blog.csdnimg.cn/ce59db2bbb0c4d6d9cb01ef93152e3c2.png)
**代码测试**
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <unistd.h>
int main()
{
printf(“process is running…\n”);
execl("/usr/bin/ls","ls","-a",NULL);
printf("process is running..\n");
return 0;
}
>
> **测试结果:**
>
>
> [hongxin@VM-8-2-centos 12-16\_1]$ ./myexec
> process is running..
> . .. Makefile myexec myexec.c
>
>
>
通过测试结果我们发现没有打印最后的printf,这个原因就是因为printf在execl之后,当execl执行完之后,代码和数据已经完全被覆盖,开始执行新的代码了,所以printf就无法执行了!
当我们写错后,又会是怎样的结果呢?
>
> **测试代码:**
>
>
> execl("/usr/bin/djhalshl","ls","-a",NULL); **测试结果:**
>
>
> [hongxin@VM-8-2-centos 12-16\_1]$ ./myexec
> process is running..
> process is running..
>
>
>
因为我们输入时是错误的地址,检测不到,这个时候execl函数就会调用失败,那么代码和数据就没有被替换,下面代码就继续执行。
我们通过查man手册,我们又发现execl只有错误时返回值,而没有正确时的返回值。
>
> RETURE VALUE
> The exec() functions return only if an error has occurred. The return value is -1, and errno is set to indicate the error.
>
>
>
这是因为成功后代码和数据就被覆盖了,再对下面判断就毫无意义,只要返回就一定是错误!
**通常写法**
int main()
{
printf(“process is running…\n”);
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
execlp("ls", "ls", "-a", "-l", "--color=auto", NULL);
exit(1);
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret>0) printf("wait success: exit code: %d, sig: %d\n", (status>>8)&0xFF, status & 0x7F);
printf("process is running..\n");
return 0;
}
为了使execl不影响父进程的代码和数据,让子进程来执行来执行父进程的一部分代码。因为虚拟地址空间加页表保证了进程的独立性,一旦有执行流想要替换代码和数据,就会发生写时拷贝。
>
> **运行结果:**
>
>
> wait success: exit code: 0, sig: 0
> process is running..
>
>
>
**总结:创建子进程就是想让子进程执行一个全新的程序**
#### 替换函数
>
> #include <unistd.h>
>
>
>
> int exec**l**(const char \*path, const char \*arg, ...);
>
>
> l--list:将参数一个一个传入execl\*中
>
>
>
> int execl**p**(const char \*file, const char \*arg, ...);
>
>
> p--path:不用告诉execl程序的路径,只需要告诉是谁,就会自动在环境变量PATH,进行可执行程序的查找
>
>
>
> int execle , const char \*arg, ...,char \*const envp[]);
>
>
> e:环境变量
>
>
>
> int exec**v**(const char \*path, char \*const argv[]);
>
>
> v--vector:可以将所有的执行参数,放入数组中,统一传递,而不用进行使用可变参数方案
>
>
>
> int execvp(const char \*file, char \*const argv[]);
>
>
>
**函数解释**
>
> 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
>
>
> 如果调用出错则返回-1
>
>
> 所以exec函数只有出错的返回值而没有成功的返回值
>
>
>
**命名理解**
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
>
> l(list) : 表示参数采用列表
>
>
> v(vector) : 参数用数组
>
>
> p(path) : 有p自动搜索环境变量PATH
>
>
> e(env) : 表示自己维护环境变量
>
>
>
![](https://img-blog.csdnimg.cn/1469a893bd4642b4be34f725f4c39dc8.png)
**exec调用举例如下:**
>
>
> #include <unistd.h>
>
>
> int main()
>
>
> {
>
>
> char \*const argv[] = {"ps", "-ef", NULL};
>
>
> char \*const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
>
>
> execl("/bin/ps", "ps", "-ef", NULL); // 带p的,可以使用环境变量PATH,无需写全路径
>
>
> execlp("ps", "ps", "-ef", NULL); // 带e的,需要自己组装环境变量
>
>
> execle("ps", "ps", "-ef", NULL, envp);
>
>
> execv("/bin/ps", argv); // 带p的,可以使用环境变量PATH,无需写全路径
>
>
> execvp("ps", argv); // 带e的,需要自己组装环境变量
>
>
> execve("/bin/ps", argv, envp);
>
>
> exit(0);
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以点击这里获取!](https://bbs.csdn.net/topics/618542503)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
>
**命名理解**
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
>
> l(list) : 表示参数采用列表
>
>
> v(vector) : 参数用数组
>
>
> p(path) : 有p自动搜索环境变量PATH
>
>
> e(env) : 表示自己维护环境变量
>
>
>
![](https://img-blog.csdnimg.cn/1469a893bd4642b4be34f725f4c39dc8.png)
**exec调用举例如下:**
>
>
> #include <unistd.h>
>
>
> int main()
>
>
> {
>
>
> char \*const argv[] = {"ps", "-ef", NULL};
>
>
> char \*const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
>
>
> execl("/bin/ps", "ps", "-ef", NULL); // 带p的,可以使用环境变量PATH,无需写全路径
>
>
> execlp("ps", "ps", "-ef", NULL); // 带e的,需要自己组装环境变量
>
>
> execle("ps", "ps", "-ef", NULL, envp);
>
>
> execv("/bin/ps", argv); // 带p的,可以使用环境变量PATH,无需写全路径
>
>
> execvp("ps", argv); // 带e的,需要自己组装环境变量
>
>
> execve("/bin/ps", argv, envp);
>
>
> exit(0);
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以点击这里获取!](https://bbs.csdn.net/topics/618542503)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**