常见操作系统
CPU
All problems in computer science can be solved by another level of indirection
- 计算机科学中的所有问题都可以通过增加一个间接层来解决
cache分布
- 每个CPU有自己的cache1和cache2,但是cache3是共享的
写入cache流程
缓存一致性问题
出现原因
- 多个CPU同时更新同一个数据,出现不一致现象,类似缓存一致性
解决办法
- 需要同时满足写同步和串行化
写同步
- 某个 CPU 核心里的 Cache 数据更新时,必须要传播到其他核心的 Cache
- 通过总线嗅探(Bus Snooping)实现
串行化
- 所有核心都能看到相同顺序的数据变化
- 通过MESI 协议实现
cpu选择线程
- CFS公平算法
- CPU优先选择vruntime时间小的
- cpu维护一个运行队列(底层是按照vruntime排序的红黑树)
nice值
- priority(new) = priority(old) + nice
- 时间片的分配由nice值决定
启动过程
- BISO 开机自检
- MBR 查找启动分区的代码执行(现在可能没有了) >MBR存在于可启动磁盘的0磁道0扇区,占用512字节,它主要用来告诉计算机从选定的可启动设备的哪个分区来加载引导加载程序(Boot loader),MBR中存在如下内容: (1) Boot Loader 占用446字节,存储有操作系统(OS)相关信息,如操作系统名称,操作系统内核位置等,它的主要功能是加载内核到内存中运行。 (2) Partition Table 分区表,占用64字节,每个主分区占用16字节(这就是为啥一块硬盘只能有4个主分区啦 (3)分区表有效性标记占用2字节 CPU将MBR读取至内存,运行GRUB(Boot Loader常用的有GRUB和LILO两种,现在常用的是GRUB),GRUB会把内核加载到内存去执行。
- Loader唤醒操作系统,唤醒下一层,把kernal读入内存,将initrd.img(一个虚拟的临时的根文件系统,每台机器都不一样)读到内存
- Kernel,init 长期执行,使用只带驱动,替代biso提供的服务,然后后台运行
- Application Manager,接管和用户打交道的部分
- Applications,各种各样的应用运行
运行等级
runlevel
命令查看,Linux系统有7个运行级别(runlevel):- 0:系统停机状态,系统默认运行级别不能设为0,否则不能正常启动
- 1:单用户工作状态,root权限,用于系统维护,禁止远程登录
- 2:多用户状态(没有NFS)
- 3:完全的多用户状态(有NFS),登录后进入控制台命令行模式
- 4:系统未使用,保留
- 5:X11控制台,登录后进入图形GUI模式
- 6:系统正常关闭并重启,默认运行级别不能设为6,否则不能正常启动
并发和并行的区别
并发
并行
相较于顺序执行的优缺点
优点
- 提高程序执行效率:多线程可以利用多个CPU核心同时执行任务,从而提高程序的执行效率。在单核CPU上,多线程也可以使程序更加响应,因为线程可以在等待I/O操作(如读写文件、网络通信等)时让出CPU资源,从而避免了阻塞等待。
- 提高程序的并发性:多线程可以让程序同时执行多个任务,从而提高程序的并发性。这对于需要同时处理多个请求的Web服务器和数据库等应用程序来说尤其重要。
- 提高用户体验:多线程可以让程序更加快速地响应用户操作,从而提高用户体验。例如,在GUI程序中,一个线程可以负责处理用户界面的更新和事件响应,另一个线程可以负责执行耗时的计算任务,这样用户就不会感觉到界面的卡顿。
缺点
- 线程之间的通讯和同步麻烦
- 竞争现象,容易产生死锁
- 当python这种全局只有一个解释器,在单核的场景下,多线程比单线程还慢
僵尸进程和孤儿进程
- 启动了一个A进程,然后通过A进程再启动B进程,此时如果杀死A进程,B可能有两种情况,B被杀死或者B被作为孤儿进程被1号进程收养,如果是使用kill发出信号的形式,那么操作系统会将所有的子进程一起kill,但是如果是父进程自然结束,没有收到信号,那么子进程将会被挂到1号进程的进程树中
- 子比父先死,但是父没有回收讲座僵尸进程,损耗系统资源,父比子先死,值会成为孤儿进程被1号给接管
进程之间通讯方式
-
管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
-
命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
-
消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
-
共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
-
信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
互斥量是信号量的一种特例,互斥量的本质是一把锁 信号量一般用于进程同步,本质上只是一个atomic
-
套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
-
信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
优缺点
- 管道:速度慢,容量有限;
- Socket:任何进程间都能通讯,但速度慢
- 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题
- 信号量:不能传递复杂消息,只能用来同步
- 共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进 程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存。
进程状态
运行状态 | 等级表示 | 描述 |
---|---|---|
r | running | 进程正在运行或正在运行队列中等待 |
s | sleeping | 进程被挂起,并正在等待某个事件的发生 |
d | waiting | 进程正在等待 i/o 操作完成,例如等待磁盘输入/输出 |
z | zombie | 进程已经结束,但其父进程还没有将其完全释放 |
t | stopped | 进程被暂停或停止 |
408状态
- 运行态,占有CPU
- 就绪态,等待CPU,准备好其他的
- 阻塞态,因为等待事件发生
- 创建态,初始化PCB
- 终止态,回收PCB
进程的组成
- PCB,操作系统为进程分配的数据结构,描述进程信息,创建进程实质就是创建进程PCB
- 程序段,数据段
- 操作系统维护一个PCB链表统一管理进程
锁
自旋锁
- 只旋锁不能在单cpu下使用(会出现A拿到锁,被换下去,B等待锁,就会陷入漫长的等待)
死锁
- 解决死锁的办法就是获取资源的顺序保持相同
//死锁demo
go func() {
mu1.Lock()
// 在请求锁1后休眠一段时间,模拟复杂的执行过程
time.Sleep(1 * time.Second)
mu2.Lock()
mu2.Unlock()
mu1.Unlock()
wg.Done()
}()
go func() {
mu2.Lock()
// 在请求锁2后休眠一段时间,模拟复杂的执行过程
time.Sleep(1 * time.Second)
mu1.Lock()
mu1.Unlock()
mu2.Unlock()
wg.Done()
}()
//解决demo 核心在于获取临界资源的顺序一致性
go func() {
defer wg.Done()
mu1.Lock()
defer mu1.Unlock()
mu2.Lock()
defer mu2.Unlock()
// 真正的临界区代码
}()
go func() {
defer wg.Done()
mu1.Lock()
defer mu1.Unlock()
mu2.Lock()
defer mu2.Unlock()
// 真正的临界区代码
}()
中断
- 上半部,对应硬中断,由硬件触发中断,用来快速处理中断;
- 下半部,对应软中断,由内核触发中断,用来异步处理上半部未完成的工作;
- 当中断发生时候,CPU立刻进入核心态
- 中断发生后,进程停止运行,操作系统堆中断进行处理
- 用户态到核心态的切换是通过中断实现的,而且中断是唯一途径
- 只有下半部中断可以被打断,上半部执行完成后会加入下半部的队列中执行
数据存储
浮点数的表示
所以通常将
1000.101
这种二进制数,规格化表示成1.000101 x 2^3
,其中,最为关键的是 000101 和 3 这两个东西,它就可以包含了这个二进制小数的所有信息:
000101
称为尾数,即小数点后面的数字;3
称为指数,指定了小数点在数据中的位置;
内核类型
宏内核
- 宏内核的特征是系统内核的所有模块,比如进程调度、内存管理、文件系统、设备驱动等, 都运行在内核态。
优缺点
- 内核管理着CPU调度,内存管理,文件管理和系统调用等各模块的的工作,由于用户服务和内核服务被实现在同一空间中,这样在执行速度上要比微内核快
- 当内核中的某个服务崩溃了,整个内核也会崩溃。另一点,想要在内核中添加新的功能就意味着内核中的各个模块需要做相应的修改,因此其扩展性很弱。
微内核
- 微内核架构的内核只保留最基本的能力,比如进程调度、虚拟机 内存、中断等,把一些应用放到了用户空间,比如驱动程序、文件系统等
优缺点
- 用户服务是独立于内核服务的,因此任何用户服务崩溃都不会影响到内核服务,这就加强了操作系统的健壮性,这是微内核的优势所在。另一点,微内核的扩展性强,添加一个功能,只需要建立一个新的服务到用户空间当中,而内核空间不需要任何的修改。因此,微内核可移植性强、安全并且易于扩展。
- 内核中的某个服务负责管理缺页异常并保存新分配的页,只要有缺页异常发生,请求就经过内核通知页管理器。页管理器必须进入特权模式下来获取内存的访问,然后回到用户模式下。然后发送一个返回结果来触发进程,当然这个过程也是需要经过内核的。处理缺页异常或者保存新分配页的整个过程是繁复而耗时的。
混合类型内核
- 内核里面会有一个最小版本的内 核,然后其他模块会在这个基础上搭建,然后实现的时候会跟宏内核类似,也就是把整个内 核做成一个完整的程序,大部分服务都在内核中,这就像是宏内核的方式包裹着一个微内核。(windows使用)
用户态内核态
切换
- 当用户程序需要访问系统资源时,会触发一个系统调用(system call),该系统调用会在用户态产生一个异常(exception)请求。
- 操作系统捕获到异常请求后,会检查该请求是否合法。如果合法,操作系统会切换到内核态,并将程序执行的现场(如程序计数器、寄存器状态等)保存在内核态的堆栈中。
- 在内核态中,操作系统会根据请求类型,调用相应的内核服务例程(只是执行待代码片段,而不是一个进程),对系统资源执行所需操作并返回结果。
- 操作系统在完成了对系统资源的操作之后,会将结果返回给用户程序,并将现场(程序计数器、寄存器状态等)恢复为切换前的状态。
- 操作系统再次切换回用户态,用户程序可以继续执行。
区别
- 内核态是操作系统内核在运行,而用户态是普通应用程序在运行。
- 处于内核态执行时,则能访问所有的内存空间和对象,且所占有的处理器是不允许被抢占的。
- 执行指令的不同,特权指令必须在内核态执行
- 判断是通过程序中的PSW标识位判断当前的状态的
切换时机
- 系统调用:这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,类似print,fork
- 中断:当外围设备完成用户请求的操作后,会向 CPU 发出相应的中断信号,这时 CPU 会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
- 异常:当 CPU 在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常,比如除0异常
内存
OOM
- (Out Of Memory Killer),如果发现内存不够了,而且没开启swapfile,那么杀死占用内存最高的进程,以维护系统不崩溃
碎片
内部碎片
- 一个分区(某个进程中)内部出现的碎片(即被浪费的空间),不能被利用。如一个进程申请43KB的内存空间,某些处理器因为限制(比如其体系结构规定只能整除4、8、16),该进程被分配了44KB,就有1KB的内部碎片。
解决办法
- 内存交换,内存分页方法.⻚表是存储在内存里的,内存管理单元 (MMU)就做将虚拟内存地址转换成物理地址的工作。
外部碎片
- 动态分区法中,频繁进行分配回收后,会出现越来越多的小空闲块,由于太小了,无法装进小进程,就是外部碎片
解决办法
- 紧缩(利用动态重定位技术):移动某些已分配区的内容,是所有进程的分区紧挨在一起,把空闲区留在另一端
虚拟内存
- 使用页表使得虚拟内存的地址和实际内存的地址联系起来
意义
- 因为内存分页,如果不使用虚拟内存,那么应用的地址是不连续的,那么应用的开发体验会不好(无法确定地址的位置),虚拟地址是连续的
- 虚拟内存允许进程使用比物理内存更大的内存空间,这样可以扩展可用内存的大小。即使物理内存有限,虚拟内存可以为进程提供更大的地址空间。
- 内存隔离和保护:虚拟内存将进程的地址空间划分为多个页面,每个页面可以独立进行访问和管理。这样可以实现内存隔离,使得每个进程的内存空间相互隔离,互不影响。同时,虚拟内存还可以通过页面级别的权限设置,提供内存保护机制,防止进程之间相互干扰或访问非法内存
- 更好的支持内存交换机制
swap机制
- 系统的物理内存不够用的时候,就需要将物理内存中的一部分空间释放出来,以供当前运行的程序使用。那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间会被临时保存到磁盘,等到那些程序要运行时,再从磁盘中恢复保存的数据到内存中。当内存使用存在压力的时候,会开始触发内存回收行为,会把这些不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了
- 优点是应用程序实际可以使用的内存空间将远远超过系统的物理内存。由于硬盘空间的价格远比内存要低,因此这种是经济实惠的。当然,频繁地读写硬盘,会显著降低操作系统的运行速率,这也是 Swap 的弊端。
[!info] linux下swapfile的调整参考 如何在 Ubuntu 中创建、删除和调整 SWAP 空间 - 系统极客
内存空间
- 32 位系统的内核空间占用 1G ,位于最高处,剩下的 3G 是用户空间
- 64 位系统的内核空间和用户空间都是 128T ,分别占据整个内存空间的最高和最低处, 剩下的中间部分是未定义的。
程序内存布局
- 程序文件段,包括二进制可执行代码
- 已初始化数据段,包括静态常量
- 未初始化数据段,包括未初始化的静态变量
- 堆段,包括动态分配的内存,从低地址开始向上增⻓
- 文件映射段,包括动态库、共享内存等,从低地址开始向上增⻓部变量和函
- 调用的上下文等。栈的大小是固定的,一般是 8 MB
栈:由编译器管理分配和回收,存放局部变量和函数参数。 堆:由程序员管理,需要手动 new malloc delete free 进行分配和回收,空间较大,但可能会 出现内存泄漏和空闲碎片的情况。 全局/静态存储区:分为初始化和未初始化两个相邻区域,存储初始化和未初始化的全局变量 和静态变量。 常量存储区:存储常量,一般不允许修改。 代码区:存放程序的二进制代码。
静态库和动态库加载
- 动态库:动态库的加载是在程序运行时由操作系统进行的,通常是将动态库的代码和数据加载到进程的虚拟内存空间的共享库区域。这个共享库区域通常是由操作系统维护的,多个进程可以共享同一个动态库的实例,从而实现了动态库的代码重用和共享
- 静态库:静态库的加载则是在编译链接时进行的,在编译链接过程中,静态库的代码和数据会被直接嵌入到可执行文件中的相应区域。在程序运行时,这些静态库的代码和数据会随着可执行文件一起加载到进程的虚拟内存空间的代码段和数据段中。
如何malloc很大的内存如何分配
- malloc时候分配的是虚拟内存,并不是实际内存,当程序下如这部分内存的地址时候CPU访问这个内存,这时会发现这个虚拟内存没有映射到物理内存, CPU 就会产生缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的 Page Fault Handler (缺页中断函数)处理
- 如果内存足够,直接进行映射,如果不够系统启动内存回收流程
- 如果申请物理内存大小超过了空闲物理内存大小,就要看操作系统有没有开启 Swap 机制:
- 如果没有开启 Swap 机制,程序就会直接 OOM;
- 如果有开启 Swap 机制,程序可以正常运行。
进程和线程协程
- 线程与进程最大的区别在于:线程是调度的基本单位,而进程则是资源拥有的基本单位。 线程是程序执行流的最小单位
- 线程的调度是系统级的,资源消耗大,协程是用户级的,资源消耗少,最大区别就是管理调度这些实现一个在系统,一个在用户,协程适合大量IO操作(阻塞代价小),协程主打并发,线程主打并行,操作系统 > 并发和并行的区别
- 进程调度参考调度算法
- 协程适合大量的并发任务,因为创建代价小,适合大量IO操作(切换代价小)
进程切换步骤
- 保存进程上下文环境(程序计数器,寄存器内容,堆栈指针等),一个叫做PCB的结构,使用数组或者链表实现,系统层面的
- 切换到进程的上下文环境,将状态信息加载待CPU寄存器中
- 移动控制权
线程的实现方式
- 根据操作系统内核是否对线程可感知,可以把线程分为内核线程和用户线程
用户型线程
- 内核无法感知用户的进程,进程的创建销毁都是通过线程库实现
- 库调度器从进程的多个线程中选择一个线程,然后该线程和该进程允许的一个内核线程关联起来。内核线程将被操作系统调度器指派到处理器内核。用户级线程是一种”多对一”的线程映射。
- 实现简单,无需用户态内核态的切换,因此更加快速
- 缺点是无法发挥多核心优势
内核型线程
- 内核线程驻留在内核空间,它们是内核对象。有了内核线程,每个用户线程被映射或绑定到一个内核线程。用户线程在其生命期内都会绑定到该内核线程。一旦用户线程终止,两个线程都将离开系统。这被称作”一对一”线程映射,
- 优点是可靠性和安全性,可以利用多核的优势
- 缺点是需要用户态的频繁切换,性能较低
组合方式
- linux操所系统使用的方式
- 使用组合方式的多线程实现, 线程创建完全在用户空间中完成,线程的调度和同步也在应用程序中进行. 一个应用程序中的多个用户级线程被映射到一些(小于或等于用户级线程的数目)内核级线程上
内存置换算法
LRU算法
- 详情参考[[单调栈和链表双指针#146. LRU 缓存]|LRU]]
- 因为需要维护更新链表,使用较少
时钟⻚面置换算法
该算法的思路是,把所有的⻚面都保存在一个类似钟面的「环形链表」中,一个表针指向最 老的⻚面。 当发生缺⻚中断时,算法首先检查表针指向的⻚面: 如果它的访问位位是 0 就淘汰该⻚面,并把新的⻚面插入这个位置,然后把表针前移一 个位置; 如果访问位是 1 就清除访问位,并把表针前移一个位置,重复这个过程直到找到了一个 访问位为 0 的⻚面为止;
文件系统
DMA技术
- 通过磁盘只带的DMA,避免了CPU长期处于用于拷贝数据状态
零拷贝
传统文件传输(发送文件)
- 用户态到内核态
- DMA拷贝到内核缓冲区,CPU拷贝到用户缓冲区
- 内核态到用户态
- 用户态到内核态
- CPU拷贝到socket内核缓冲区,DMA拷贝到网卡
- 内核态到用户态
缺点
- 内核态和用户态切换频繁
- 拷贝太多且没有意义
零拷贝文件传输
- 用户态到内核态
- DMA拷贝到内存,CPU发送描述符和数据长度给socket(并非拷贝)
- socket通过DMA拷贝数据到网卡
- 内核态到用户态
优点
- 减少切换次数
- 在内核区域没有对数据进行拷贝,零拷贝,kafka和nginx都使用了
IO多路复用
单路socket
- 单线程阻塞
- 只能处理一个socket连接
socket+多进程
- 每个请求一个进程连接一旦太大扛不住
- 上下文切换和内存拷贝频繁,效率低下
socket+线程池
- 线程池因为共享内存,加锁频繁,但是减少了内存拷贝
- 但是还是出现了上下文拷贝频繁,还是扛不住大流量
- 可以充分发挥多核优势
Select和Poll多路复用
- select底层使用bitmap代表每个socket,如果有消息就置为1,没有就为0,bitmap限制了监听的最大socket值(默认为1024),遍历bitmap查询
- poll底层使用链表,因此没有最大描述符的限制,但是仍然需要遍历查找每个监听socket,查询
- poll 和 select 并没有太大的本质区别,都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合,这种方式随着并发数上来,性能的损耗会呈指数级增长。
Epoll多路复用
- epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字,把需要监控的 socket 通过
epoll_ctl()
函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删改一般时间复杂度是O(logn)
。而 select/poll 内核里没有类似 epoll 红黑树这种保存所有待检测的 socket 的数据结构,所以 select/poll 每次操作时都传入整个 socket 集合给内核,而 epoll 因为在内核维护了红黑树,可以保存所有待检测的 socket ,所以只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷贝和内存分配。 - epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用
epoll_wait()
函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。 - epoll 的方式即使监听的 Socket 数量越多的时候,效率不会大幅度降低,能够同时监听的 Socket 的数目也非常的多了,上限就为系统定义的进程打开的最大文件描述符个数。因而,epoll 被称为解决 C10K 问题的利器。
水平触发和边缘触发
- 类似于电平,水平触发时候电平处于高电位就会触发(有消息就会触发)
- 边缘触发只有电平突变才会触发(有消息触发之后即使高电位也不会继续触发)
- 水平触发简单但是效率低下
reactor模型
- 通过epoll IO复用,再从线程池拿出线程处理
- 综合两者的优点,大部分使用
proactor模型
- 通知程序的内容从socket事件变成了,从socket读到了
- Reactor 可以理解为「来了事件操作系统通知应用进程,让应用进程来处理」,而 Proactor 可以理解为「来了事件操作系统来处理,处理完再通知应用进程」
- 它是采用异步 I/O 实现的异步网络模型,感知的是已完成的读写事件,而不需要像 Reactor 感知到事件后,还需要调用 read 来从内核中获取数据。
eventfd
- #TODO 补全eventfd
linux文件描述符
- Linux中一个进程访问文件的唯一标识。
设计
- 进程级的文件描述符表:文件描述符表示进程级别的,即每一个进程都会有一张维护其占用FD的表。在进程内部,FD标识即第一列是唯一的。第二列是保存了指向系统级的打开文件描述符表的指针
- 系统级的打开文件描述符表:打开文件描述符表是系统级别的,即整个系统只会保留一份。每一个进程在创建FD之后,都会在这个表里面注册一下。所有的进程FD都可以在这里找到一条对应的记录。分为三列,第一列是FD的状态,即可读,可写还是读写。第二列是当前FD操作的文件索引到哪里了。第三列是一个指向node的指针。
- 文件系统的i-node表:i-node表是文件系统级别的。我们都知道,i-node是存储文件实际元数据的地方,即文件长度等属性。
常识性问题
- ==绝对不要带空格文件名,绝对不要用中文作为用户名,绝对不要在目录中出现中文文件夹,最好不要在文件名出现中文==,则只最基本的常识,否则任何软件都可能因为这些报错,记住,英文才是世界通用语言!
参考
- https://cloud.tencent.com/developer/article/1114481
- https://www.cnblogs.com/LUO77/p/5816326.html