简介

本项目设计电路并焊接红外线心率计(数电部分)

红外线心率计就是通过红外线传感器检测出手指中动脉血管的微弱波动,由计数器计算出每分钟波动的次数。

本项目中,模电部分不需要我们实际操作,我们只需要完成由门控电路控制3位计数器并进行译码、驱动、显示的部分

具体工作模块如下:

img

门控电路

门控电路用 555 接成单稳态触发器,作用是控制计数器的启停,并控制每次测量的时间

门控电路的电路图如下:

img

具体过程是,按下开关形成向下的边沿,LED灯亮起,2脚电压为0,低于1/3Vcc,3脚输出高电平,计数器开始计数,此时电解电容开始充电;当电压达到2/3Vcc时,3脚输出低电平,计数器停止计数,电容放电回到低电平,返回稳定状态,定时结束。

三位计数电路

由 MC14553 组成的 3 位计数电路对输入的方波进行计数,并把计数结果以 BCD 码(使用4位二进制表示一位十进制的方式,常见的有8421码、2421码、5421码,本项目采用8421码)的形式输出。引脚功能如下:

img

三位计数电路图如下:

img

译码、驱动、显示电路

3 位计数电路、译码、驱动、显示电路的作用是把计数器输出的计数结果显示在 3 位数码管上。其中译码器CD4543具体引脚功能如下:

img

数码管abcdefg对应的7个LED如下图所示:

img

PS:共阳极电路代表所有LED阳极被连接到一起,通过控制各自的阴极电平来点亮LED,低电平有效。共阴极电路代表所有LED阴极被连接到一起,通过控制各自的阳极电平来点亮LED,高电平有效。

整体电路图如下:

img

附加部分:NE555(不懂就跳过)

555定时器芯片一般不单独使用,而是构成单稳态触发器、多谐振荡器和施密特触发器等多种电路综合应用。

NE555内部电路图如下:
img

各个引脚的功能如下:

img

  • 1脚:接地。
  • 2脚:输入端Trigger,该脚会判断其电压是否小于1/3 Vcc。
  • 3脚:输出端Output。
  • 4脚:清零端Reset。正常工作时应接高电平。
  • 5脚:控制电压端。一般不使用,应通过一只0.01μF(103)瓷片电容接地,以防引入高频干扰。
  • 6脚:输入端Threshold,该脚会判断其电压是否大于2/3 Vcc。
  • 7脚:放电端Discharge。
  • 8脚:外接电源Vcc,范围为4.5V~16V,一般用5V。

输出端3脚的电平与输入的关系如下表所示:

img

如果不想了解具体的工作原理,只需要知道以下几点即可:

  • 1脚接地,8脚接电源,4脚大部分情况下也接电源
  • 5脚通过一个0.01μF电容接地,也可以悬空(不建议)
  • 2、6、7脚根据不同应用有不同接法
  • 3脚是输出

如果不想真正了解具体的工作原理,需要看懂以下电路图:

  • 555内部电路图
  • 555组成的单稳态触发器、施密特触发器、多谐振荡器等电路图

简介

导师制项目基于51单片机的智能循迹小车,包含黑线循迹、超声波避障、红外线遥控3大功能。

本文对该项目涉及外设以及具体的功能实现进行一个整合分析

硬件支持

主控芯片:STC89C52

小车车体:清翔电子的QX-A51两驱智能小车组件

电源:使用18650锂电池作为电源,负责提供电能给各个模块。LM7805三端稳压器提供稳定的5V直流电压,

电机驱动模块:使用L293D芯片作为电机驱动芯片,负责驱动两个TT减速电机,搭配两个车轮和一个定向轮,改变小车的速度和方向。

黑线循迹模块:使用两组RPR220一体化反射式光电传感器作为循迹传感器,使用LM324芯片用作电压比较模块芯片,负责检测地面上的黑线并输出高低电平信号。

超声波避障模块:使用HC-SR04超声波传感器作为避障传感器,负责检测前方是否有障碍物。

红外遥控模块:迷你红外遥控器发送红外光信号,使用HS0038红外接收探头接受红外光信号,并在单片机内部进行解码后输出指令或数据信号。

PWM(脉宽调制)实现小车变速

  • PWM(脉宽调制)

    利用微处理器的数字输出,来形成想要的波形。

    利用PWM实现小车变速,其实就是输出一段波形,高电平持续时间关闭电机,低电平时间开启电机,则占空比越大车速越慢。

  • 占空比

    脉冲信号中高电平持续时间与整个周期时间的比率

  • PWM信号频率

    PWM的信号频率通常取决于应用需求和电机特性。对于智能小车的电机控制,100Hz是一个常见且合理的选择。因此波形的周期是1/100Hz=10ms

  • 单片机晶振频率/时钟周期

    该单片机的晶振频率为11.0592MHz,则其时钟周期为1/11.0592MHz=0.0904us

  • 机器周期/计时器每变化一次所需时间

    51系列单片机的机器周期等于12个时钟周期,于是0.0904us*12=1.085us

  • 定时器初始值

    我们希望占空比变化范围在1/256~1之间,所以需要波形周期10ms分为256份,每份是39.0625us

    又知计时器每变化一次所需时间为1.085us,则39.0625us需要计数约36次

    设置定时器为8位重装模式(每逢计数到256溢出后重新设置为初值),则定时器初值应该设置为256-36=220

  • 实现小车变速

    这样一来,定时器每一次触发中断,就说明时间走完256份周期中的一份。

    设置一个计数标志位count,每一次中断count+1。

    当count达到阈值X(阈值X/256=占空比)时启动小车电机,当count达到255时关闭小车电机并置0,由此实现变速

  • 结果

    阈值X的设定应该在0~170之间较为合理,阈值过高则会导致小车电机输出时间过短,扭矩不够

红外循迹模块

  • 红外线在不同颜色的物体表面具有不同的反射性质,照射到黑线上时,会被黑线吸收,从而导致探测器接受红外光较弱;当红外线照射到白色地面上时,会被反射回来,探测器接受红外光较强

  • RPR220红外探照头以及LM324电压电路如下

img

智能小车一上电RPR220内部红外光发射,经由物体反射回光电三极管。当所接受到的红外光越强(即未接触黑线)则IN1与IN2两处电流越大。如图3所示,IN1与IN2将会接入LM324电压比较模块里与T1和T2处进行电压比较。如果T1>IN1则P3.2输出1,T2>IN2,则P3.3输出1;反之则都输出0。也就是说T1和T2的电压强度将会成为衡量是否接触黑线的标准,二者的电压强度可通过电位器RW3和RW4进行调节,调节到合适的强度使得小车可以准确识别黑线。

超声波避障

HC-SR04超声波测距模块是一种基于超声波反射原理的测距传感器,它由一个超声波发射器、一个超声波接收器和一个控制电路组成。

img

HC-SR04工作原理:当向TRIG引脚输入一个10us以上的高电平信号时,模块会自动发射8个40kHz的方波信号。当发射的超声波遇到障碍物时,会被反射回来,被接收器检测到。接收到反射回来的超声波时,模块会向ECHO引脚输出一个与超声波往返时间成正比的高电平信号。而通过测量ECHO引脚输出的高电平信号的持续时间,就可以根据声速计算出超声波从发射到返回的时间,进而得到距离障碍物的距离

红外遥控

NEC协议是一种常用的红外遥控通信协议,它采用脉冲位置调制(PPM),利用脉冲之间的时间间隔来区分“0”和“1”。

img

NEC协议的数据帧格式如图4所示,包括引导码、用户码、用户反码、数据码和数据反码。用户码和用户反码用来校验发送者的身份,数据码和数据反码用来传输按键信息。NEC协议的特点有以下几点:数据帧长度为32位,每个字节从最低位开始发送。引导码由9ms的低电平和4.5ms的高电平组成,用来标志数据帧的开始。逻辑“0”由560us的低电平和560us的高电平组成,逻辑“1”由560us的低电平和1680us的高电平组成。结束码由560us的低电平组成,用来标志数据帧的结束,因此在程序设计中可通过信号时间长短来解析数据帧。

外部中断0(INT0)被配置为下降沿触发模式,当HS0038接受到红外信号时会产生一个下降沿信号,从而触发外部中断0。

定时器0则设置为8位重装模式,初始值为0,也就是每256*1.085us=277.76us中断一次,并使IRtime++

使用外部中断(INT0)来捕获每一次红外信号下降沿、使用定时器0来记录每两次外部中断(INT0)之间的时间间隔IRtime、因此每一次外部中断(INT0)都可以通过访问IRtime来获取上一个红外信号方波持续时间(获取完数据后需给IRtime置0,保证下一次计数的正确性)

整体设计方案

本文件系统仿照Linux的ext2文件系统设计

img

文件的读写单位——块

硬盘的读写单位是扇区,但是由于硬盘读写速度较慢,所以当我们读写文件时,不是一个一个扇区进行读写,而是凑齐数个扇区一起读写,我们将其称为块。

因此 块是文件的读写单位, 一个块包含数个扇区(一定是扇区的整数倍)

文件的本质——inode

文件其实就是存储在硬盘上的数据,并且是以块为单位进行读写

我们关注文件,本质上是关注数据在硬盘上存储的块的组织方式(数据块在硬盘上是不连续的)

FAT文件系统曾采用链式结构进行组织(每个块的最后存储下一个块的地址),但由于访问文件中某一个块时都得从头遍历,最终放弃使用该文件系统

img

UINX操作系统采用索引结构inode进行组织,后来Linux的ext2文件系统也是模仿该组织形式,本项目同样采用索引结构inode

索引结构说简单点就是一个数组,每一个元素就是每一个块,比起链式结构来说,访问某个特定的块无需从头遍历

img

所以现在看来inode就是一个数组,数组元素就是块地址,我们根据数组的索引可以方便的寻找到对应块的地址

然而真正的inode并非这么简单,它还需要扩展两点

  • 文件过大怎么办?——引入间接块索引表指针

      假设我们一个indoe包含15个索引项,其中前面12个是直接块指针。当一个文件大小小于等于12块时就可以只使用前12个索引项解决问题。
    
      当文件大小超越12个块了怎么办?
    
      我们把第13个索引项作为一级间接块索引表指针,它指向一级间接块索引表(单独占一个块),而一级间接块索引表上面又存储着256个直接块地址。于是文件最大可达12+256=268个块
    
      当文件超越268个块了怎么办?
    
      我们把第14个索引项作为二级间接块索引表指针,它指向256个一级间接块索引表指针,于是文件最大可达12+256+256*256个块
    
      文件大小还是太大了怎么办?
    
      我们把第15个索引项作为三级间接块索引表指针,它指向256个二级间接块索引表指针,于是文件最大可达12+256+256*256+256*256*256个块
    
      一般的文件无法超越这个大小。再大的文件就只能呢使用mv命令分割成数个小文件
    
  • inode真的只是一个数组吗?

      其实不然,inode最核心的部分确实是块数组。但inode的本质其实是文件的元信息。除了包含块数组(描述文件存储位置),还包含一些其他的元信息,例如:i结点编号(inode的唯一编号)、文件大小、权限、创建时间、属主等等
    

结合上述两点扩展,现在真正的inode如下图所示:

img

所有文件的集合——inode数组

一个inode就是一个文件,我们将所有inode的编号作为索引,inode地址作为元素,构成一个inode数组。

现在所有的inode地址信息都集合在inode数组里了,只需要提供inode编号,我就可以去查找具体的inode

文件的分类

文件包含两大类:目录文件 和 普通文件

日常口语中的文件大多指的是普通文件,但本文讨论的文件既包括普通文件也包括目录文件

目录也是文件?对的,在ext2文件系统中,目录文件和普通文件一样都算文件,它们的真面目也都是inode

唯一的区别就是数据块存储的内容不同。目录文件的数据块存储的内容是目录项,而普通文件的数据块存储的内容是普通数据

二者对外表现都是inode,也就是说,在没有外来信息(上级目录)提供帮助的情况下,一个inode就是一个inode,你永远无法区分它到底是目录文化还是普通文件。

PS:目录项是什么?目录项是目录文件下的单位表项,它将inode编号、文件名、文件类型三者关联起来,如图:

img

完整的FCB——inode+目录项

FCB全称是文件控制块,只要是用于管理、控制文件相关信息的数据结构都称为FCB

ext2文件系统里的FCB包含inode和目录项两大数据结构,也就是说只凭借二者就可以完整的组织起所有的文件

inode和目录项的关系参照下图:

img

    我们假设inode1是根目录,是一切最开始的文件。由于根目录文件的位置是固定不动的,所以我们能确定它是目录文件而不会混淆,根目录文件里的数据块存储的理所当然是一系列目录项。
    
    而目录项上记载了文件的类型、文件名称、inode编号

    我们的inode2类型是普通文件,所以我们可以根据inode编号2在inode数组中查找到对应的inode地址。然后根据块指针找到硬盘上对应的数据块,并读取数据块上的普通数据

    我们的inode3类型是目录文件,所以我们可以根据inode编号3在inode数组中查找到对应的inode地址。然后根据块指针找到硬盘上的对应块,并读取数据块上的目录项

现在,假设我们有一串完整的文件地址,例如:D:\CodeSet\myBlog\source\img\SimpleOS-7-文件系统\img11.png
我们就可以根据上诉方法一步一步递归查找,最后寻找到正确的文件

文件系统的布局

一个文件系统就是一个分区,而每一个硬盘分区的空间大小是有限的。不论是inode还是用于存储数据的空闲块都需要占据同一个分区的空间。因此,一个分区所拥有的inode和空闲块都是有限的,而且需要使用位图数据结构进行管理。

综上,一个文件系统需要inode位图、空闲块位图、inode数组、固定的根目录位置,以及还需要一个用来描述该文件系统元信息的超级块

文件系统在磁盘上的布局如下图所示:

img

其中超级块的结构如下图所示:

img

至此,整个文件系统的布局已经完整,文件之间的组织方式也已经清晰,大体上如下图:

img

PS:一个操作系统具有多个分区,也就是多个文件系统,但初始化时往往只会挂载一个主要的文件系统,也就是当前文件系统

进程间的文件操作

众所周知,一个电脑允许多个进程,一个进程又允许多次打开同一个文件或者打开多个不同的文件。并对文件进行操作。

我们该如何处理 多进程 与 多文件 之间的关联问题?

为此,我们引入了如下概念:文件描述符、文件结构、文件表、inode缓冲队列

它们之间的逻辑关系如下图所示:

img

  • 文件结构file

      文件结构file用于记录文件操作(光标偏移量等),每次打开一个文件同时就会产生一个文件结构file,多次打开该文件就会产生多个文件结构。
    
      文件结构file如下图所示:
    

img

  • 文件表

      文件表就是文件结构file集成的数组。文件表用于记录系统打开的文件(不能超过规定的系统最大打开文件数)
    
      文件表存在于内存当中。每当系统初始化后(主文件系统已挂载),系统内存将会申请文件表所需的空间,并为数组的每一个元素置NULL,直到有文件被打开时,文件表会填写相应的文件结构file并返回下标(一般来说下表0被留给标准输入、1被留给标准输出、2被留给标准错误)。
    
  • inode缓冲队列

      每一个被挂载的分区都会初始化自己的indoe缓冲队列,它是该分区所有被打开的文件inode组成的缓冲队列(同一个文件被打开多次的情况下,也只会有唯一一个inode存在该队列,只不过该inode上记载着被打开的次数)
    
      inode缓冲队列也是存在于内存当中。
    
  • 文件描述符

      每一个PCB(进程控制块)都会有一个文件描述符数组,用于记录该进程打开的文件(不能超过规定的进程最大打开文件数)。
    
      该文件描述符数组存储的元素即是相对应的文件结构在文件表中的下标
    

当一个进程需要打开一个新文件时会经历如下步骤:

    1. 进程提供新文件的inode号作为参数调用函数

    2. 检测inode缓冲队列中是否存在该inode,如果存在则inode记录的文件打开次数加一,如果不存在则从硬盘里寻找该inode并加入缓冲队列
    
    3. 从文件表file_table中获取一个空闲位,并填入新构建的文件结构file(指向inode),然后返回该文件结构在文件表中的下标

    4. 进程PCB取得下标,存储在文件描述符数组中,并返回对应的文件描述符数组下标(即文件描述符)

    5. 调用函数结束,取得一个文件描述符

当一个进程需要修改一个已经打开的文件时会经历如下步骤:

    1. 进程提供 需要修改文件的 文件描述符 作为参数调用函数

    2. 根据文件描述符在PCB中寻找到对应文件结构在文件表里的下标

    3. 根据下标在文件表里取得对应的文件结构

    4. 根据文件结构在inode缓冲队列里找到对应的inode

    5. 根据inode的块指针修改磁盘上对应空间的数据

文件检索的关键 . 和 ..

任何一个目录下都需要两个子目录 . 和 ..

. 代表当前目录, .. 代表父目录,二者有助于文件系统的导航

具体的实现就是在当前目录下的目录项中将.和..添加进去

数据结构

/* 分区结构:一个分区就是一个文件系统 */
struct partition {
    uint32_t start_lba;		    // 起始扇区
    uint32_t sec_cnt;		    // 扇区数
    struct disk* my_disk;	    // 分区所属的硬盘
    struct list_elem part_tag;	// 用于队列中的标记,用于将分区形成链表进行管理
    char name[8];		        // 分区名称

    struct super_block* sb;	    // 本分区的超级块
    struct bitmap block_bitmap;	// 块位图
    struct bitmap inode_bitmap;	// inode位图
    struct list open_inodes;	// 本分区打开的inode缓冲队列
};

/* 超级块:文件系统元信息的配置文件 */
struct super_block {
    uint32_t magic;		            // 用来标识文件系统类型,支持多文件系统的操作系统通过此标志来识别文件系统类型

    uint32_t sec_cnt;		        // 本分区总共的扇区数
    uint32_t inode_cnt;		        // 本分区中inode数量
    uint32_t part_lba_base;	        // 本分区的起始lba地址

    uint32_t block_bitmap_lba;	    // 块位图本身起始扇区地址
    uint32_t block_bitmap_sects;    // 扇区位图本身占用的扇区数量

    uint32_t inode_bitmap_lba;	    // i结点位图起始扇区lba地址
    uint32_t inode_bitmap_sects;	// i结点位图占用的扇区数量

    uint32_t inode_table_lba;	    // i结点表起始扇区lba地址
    uint32_t inode_table_sects;	    // i结点表占用的扇区数量

    uint32_t data_start_lba;	    // 数据区开始的第一个扇区号
    uint32_t root_inode_no;	        // 根目录所在的I结点号
    uint32_t dir_entry_size;	    // 目录项大小

    uint8_t  pad[460];		        // 加上460字节,凑够512字节1扇区大小
    
} __attribute__ ((packed));

/* inode:文件的实质 */
struct inode {
    uint32_t i_no;                  // inode编号

    uint32_t i_size;                //当此inode是普通文件时,i_size是指普通文件大小
                                    //若此inode是目录,i_size是指该目录下所有目录项大小之和*/

    uint32_t i_open_cnts;           // 记录此文件被打开的次数
    bool write_deny;	            // 写文件不能并行,进程写文件前检查此标识
                        
    uint32_t i_sectors[13];         // i_sectors[0-11]是直接块,
                                    // i_sectors[12]用来存储一级间接块指针
    struct list_elem inode_tag;     //用于文件缓冲队列中
};

/* 文件结构 */
struct file
{
    uint32_t fd_pos;        // 记录当前文件操作的偏移地址,文件尾为-1
    uint32_t fd_flag;       // 文件操作标识符
    struct inode *fd_inode;
};

/* 目录结构 */
struct dir {
    struct inode* inode;   
    uint32_t dir_pos;	    // 记录在目录内的偏移
    uint8_t dir_buf[512];   // 目录的数据缓存
};

/* 目录项结构 */
struct dir_entry {
    char filename[MAX_FILE_NAME_LEN];   // 普通文件或目录名称
    uint32_t i_no;		                // 普通文件或目录对应的inode编号
    enum file_types f_type;	            // 文件类型
};

函数表

fs/fs.c

//------------------------文件系统初始化相关函数---------------------------------------

/*
@brief: 在磁盘上搜索文件系统,落没有则格式化分区创建文件系统
@detail:1.遍历整个磁盘,对每个已存在的分区创建文件系统
        2.挂载默认分区
        3.将当前分区的根目录打开
        4.初始化文件表
@param: 无
@retval:无
*/
void filesys_init();

/*
@brief: 初始化part分区的元信息,创建文件系统
@detail:初始化超级块、空闲块位图、inode位图、inode数组、根目录并全部写入磁盘
@param: 略
@retval:无
*/
static void partition_format(struct partition* part);

/*
@brief: 挂载名为part_name(arg)的分区
@detail:将硬盘中的超级块、空闲块位图、inode位图全部读取到内存中
        给cur_part赋值
@param: 略
@retval:无
*/
static bool mount_partition(struct list_elem *pelem, int arg);

//------------------------文件系统初始化相关函数---------------------------------------


//------------------------路径解析相关函数-----------------------

/*
@brief: 将最上层路径名称解析出来,存储到name_store,并返回子路径
@param: 略
@retval:无
*/
static char *path_parse(char *pathname, char *name_store);

/*
@brief: 返回路径深度
@param: 略
@retval:无
*/
int32_t path_depth_cnt(char *pathname);

/*
@brief: 搜索文件路径pathname,找到则返回其inode号,否则返回-1
@param: search_record:记录搜索过程中的父路径
@retval:无
@PS:   调用该函数后,会打开目标文件的父目录,并不会关闭,需要调用者关闭该目录
*/
static int search_file(const char *pathname, struct path_search_record *searched_record);

//------------------------路径解析相关函数-----------------------


//-------------------------系统调用-普通文件相关-------------------------

/*
@brief: 打开或创建普通文件
@detail:   1.先搜索该普通文件是否存在
            2.存在则打开,不存在则创建并打开
@param: flags:文件操作标识符
@retval:成功后,返回文件描述符,否则返回-1
*/
int32_t sys_open(const char *pathname, uint8_t flags);

/*
@brief: 关闭文件描述符fd指向的文件
@detail:   1.调用file_close()关闭普通文件;
            2.PCB->fd_table[fd]=-1;令文件描述符对应的数组可用
@param: flags:文件操作标识符
@retval:成功返回0,否则返回-1
*/
int32_t sys_close(int32_t fd)

/*
@brief: 将buf中连续count个字节写入文件描述符fd
@param: 略
@retval:成功则返回写入的字节数,失败返回-1
*/
int32_t sys_write(int32_t fd, const void *buf, uint32_t count);

/*
@brief: 从文件描述符fd指向的文件中读取count个字节到buf
@param: 略
@retval:若成功则返回读出的字节数,到文件尾则返回-1
*/
int32_t sys_read(int32_t fd, void *buf, uint32_t count);

/*
@brief: 重置用于文件读写操作的偏移指针(重置为:offset+文件指针位置)
@param: whence:文件指针位置标识符
        offset:相对于文件指针位置的偏移量
@retval:成功时返回新的偏移量,出错时返回-1
*/
int32_t sys_lseek(int32_t fd, int32_t offset, uint8_t whence);

/*
@brief: 删除普通文件(普通文件已打开则删除失败)
@param: 略
@retval:成功返回0,失败返回-1 
*/
int32_t sys_unlink(const char *pathname);
//-------------------------系统调用-普通文件相关-------------------------


//-------------------------系统调用-目录文件相关-------------------------

/*
@brief: 创建目录文件(并不打开)
@detail:1.申请inode位图,并同步到磁盘
        2.申请block位图,并同步到磁盘(先分配一个块就够用)
        3.往inode指向的数据块写入目录项'.'和'..'
        4.要将本目录的inode初始化,并同步到磁盘(无需申请内存空间)
        5.将关于本目录的目录项写入父目录数据块(写入磁盘)
        6.父目录inode更新大小并同步到磁盘
@param: pathname:目录文件路径
@retval:成功返回0,失败返回-1 
*/
int32_t sys_mkdir(const char *pathname);

/*
@brief: 打开目录文件,并返回目录指针
@detail:调用dir_open()来打开目录
        (目录打开只涉及part->open_inodes不涉及文件表、文件描述符数组等)
@param: 略
@retval:成功返回目录指针,失败返回-1 
*/
struct dir *sys_opendir(const char *name);

/*
@brief: 关闭目录
@detail:调用dir_close()来关闭目录
@param: 略
@retval:成功返回0,失败返回-1
*/
int32_t sys_closedir(struct dir *dir);

/*
@brief: 根据dir当前偏移位置,读取一个目录项,并更新偏移位置(调用dir_read()实现)
@param: 略
@retval:成功后返回其目录项地址,到目录尾时或出错时返回NULL
*/
struct dir_entry *sys_readdir(struct dir *dir);

/*
@brief: 把目录dir的指针dir_pos置0
@param: 略
@retval:无
*/
void sys_rewinddir(struct dir *dir);

/*
@brief: 删除空目录(调用dir_remove()实现)
@param: 略
@retval:成功时返回0,失败时返回-1
*/
int32_t sys_rmdir(const char *pathname);
//-------------------------系统调用-目录文件相关-------------------------


//-------------------------系统调用-cwd相关-------------------------

/*
@brief: 获得父目录的inode编号(利用目录项目'..')
@param: 略
@retval:无
*/
static uint32_t get_parent_dir_inode_nr(uint32_t child_inode_nr, void *io_buf);

/*
@brief: 在inode编号为p_inode_nr的目录中查找inode编号为c_inode_nr的子目录的名字,将名字存入缓冲区path. 
@param: 略
@retval:成功返回0,失败返-1
*/
static int get_child_dir_name(uint32_t p_inode_nr, uint32_t c_inode_nr, char *path, void *io_buf);

/*
@brief: 把当前工作目录绝对路径写入buf, size是buf的大小. 
@detail:根据PCB->cwd_inode_nr一层层向上追溯求得当前工作目录路径
@param: 略
@retval:成功返回buf,失败返NULL
*/
char *sys_getcwd(char *buf, uint32_t size);

/*
@brief: 更改当前工作目录为绝对路径path 
@detail:实质是修改PCB->cwd_inode_nr
@param: 略
@retval:成功则返回0,失败返回-1
*/
int32_t sys_chdir(const char *path);
//-------------------------系统调用-cwd相关-------------------------


//-------------------------系统调用-文件属性相关-------------------------
/*
@brief: 在buf中填充文件结构相关信息 
@param: 略
@retval:成功时返回0,失败返回-1
*/
int32_t sys_stat(const char *path, struct stat *buf);

//-------------------------系统调用-文件属性相关-------------------------


//------------------------转换函数----------------------
/*
@brief: 将文件描述符转化为文件表的下标
@param: 略
@retval:无
*/
static uint32_t fd_local2global(uint32_t local_fd);

//------------------------转换函数----------------------

fs/dir.c

/*
@brief: 打开根目录
@detail:1.利用inode_open()打开根目录
        2.并给root_dir赋值
@param: 略
@retval:无
*/
void open_root_dir(struct partition *part);

/*
@brief: 在分区part上打开节点号为inode_no的目录并返回目录指针 
@detail:1.利用inode_open()打开目录文件
        2.给目录结构pdir申请空间,初始化并返回
        (目录打开只涉及part->open_inodes不涉及文件表、文件描述符数组等)
@param: 略
@retval:无
*/
struct dir *dir_open(struct partition *part, uint32_t inode_no);

/*
@brief: 关闭目录
@detaili:1.利用inode_close()关闭目录,根目录不能被关闭
        2.释放目录结构dir的空间
@param: 略
@retval:无
*/
void dir_close(struct dir *dir);

/*
@brief: 在目录中寻找指定目录项
@detail:在part分区内的pdir目录内寻找包含name文件或目录的目录项
@param: 略
@retval:找到后返回true并将其目录项存入dir_e,否则返回false
*/
bool search_dir_entry(struct partition *part, struct dir *pdir, const char *name, struct dir_entry *dir_e);

/*
@brief: 在内存中初始化目录项p_de
@param: 略
@retval:无
*/
void create_dir_entry(char *filename, uint32_t inode_no, uint8_t file_type, struct dir_entry *p_de);

/*
@brief:     将目录项p_de写入父目录parent_dir中(直接写入磁盘)
@param:     io_buf:主调函数提供的缓冲区
@retval:    成功返回true,失败返回false
@PS:       父目录的inode.size更改过了,但并没有同步到磁盘的inode_table里!!
            调用者要负责把父目录的inode同步到磁盘
*/
bool sync_dir_entry(struct dir *parent_dir, struct dir_entry *p_de, void *io_buf);

/*
@brief: 把分区part目录pdir中关于inode_no的目录项删除(会将更新过的父目录inode写入磁盘)
@param: 略
@retval:成功返回true,失败返回false
*/
bool delete_dir_entry(struct partition *part, struct dir *pdir, uint32_t inode_no, void *io_buf);

/*
@brief: 根据dir当前偏移位置,读取一个目录项,并更新偏移位置
@param: 略
@retval:成功返回目录项,失败返回NULL
*/
struct dir_entry *dir_read(struct dir *dir);

/*
@brief: 判断目录是否为空
@detail:目录为空则代表目录中只含有.和..两个目录项
@param: 略
@retval:空返回true,非空返回false
*/
bool dir_is_empty(struct dir *dir);

/*
@brief: 移除目录child_dir
@detail:1.调用delete_dir_entry在父目录中移除本目录的目录项
        2.调用inode_release()回收本目录的inode
@param: 略
@retval:成功返回目录项,失败返回NULL
*/
int32_t dir_remove(struct dir *parent_dir, struct dir *child_dir);

fs/inode.c

/*
@brief: 打开并返回part分区里目标inode节点
        两个重要的功能:一个是打开inode、一个是返回inode节点
@detail:1.现在inode缓冲区队列中寻找该inode,若已打开则增加inode->i_open_cnts并返回inode
        2.没找到就去磁盘中寻找该inode读取到内存里,并放到inode缓冲区队列,返回inode
@param: part:目标分区
        inode_no:要打开的inode节点的i_no号
@retval:无
*/
struct inode *inode_open(struct partition *part, uint32_t inode_no);

/*
@brief: 关闭inode
@detail:1.减少inode->i_open_cnts
        2.inode->i_pen_cnts减少后若为0,则将从inode缓冲队列里去除,并释放为inode分配的空间
@param: 略
@retval:无
*/
void inode_close(struct inode *inode)

/*
@brief: 初始化new_inode,为其赋值
@param: 略
@retval:无
*/
void inode_init(uint32_t inode_no, struct inode *new_inode)


/*
@brief: 定位inode在扇区的位置
@detail:在part里获取inode所在的扇区和扇区内的偏移量并存到inode_pos中
@param: 略
@retval:无
*/
static void inode_locate(struct partition *part, uint32_t inode_no, struct inode_position *inode_pos);

/*
@brief: 将inode写入到磁盘的分区part中
@param: 略
@retval:无
*/
void inode_sync(struct partition *part, struct inode *inode, void *io_buf);


/*
@brief: 回收inode
@detail:1.回收分配给inode的所有block(修改block_bitmap)
        2.回收inode(修改inode_bitmap)
        3.调用inode_delete删除硬盘上的inode数据(可以不用这步)
        4.确保该inode完全关闭
@param: 略
@retval:无
*/
void inode_release(struct partition *part, uint32_t inode_no);

/*
@brief: 将硬盘分区part上的对应的inode清除(修改磁盘上的inode_table)
@param: inode_no:要清楚的inode号
@retval:无
*/
void inode_delete(struct partition *part, uint32_t inode_no, void *io_buf);

fs/file.c

//------------------------文件表 文件描述符数组 相关操作-----------------------

/*
@brief: 从文件表file_table中获取一个空闲位
@param: 无
@retval:成功返回文件表下标,失败返回-1 
@PS:如果file_table[fd_idx].fd_inode == NULL,则判断文件结构可用
*/
int32_t get_free_slot_in_global(void);

/*
@brief: 将全局描述符(文件表下标)安装到进程或线程自己的文件描述符数组fd_table中 
@param: globa_fd_idx:文件表的下标(全局文件描述符)
@retval:成功返回文件描述符(文件描述符数组的下标),失败返回-1
*/
int32_t pcb_fd_install(int32_t globa_fd_idx);

//------------------------文件表 文件描述符数组 相关操作-----------------------


//---------------------------位图操作-------------------------
/*
@brief: 从inode位图里分配一个inode结点 (内存操作)
@param: 略
@retval:返回节点号i_no(就是inode在位图中的位置)
*/
int32_t inode_bitmap_alloc(struct partition *part);

/*
@brief: 从block位图里分配1个空闲块(1扇区),返回其扇区地址(内存操作)
@param: 略
@retval:略
*/
int32_t block_bitmap_alloc(struct partition *part);

/*
@brief: 同步内存里的位图结构到磁盘上
@detail:将内存中bitmap第bit_idx位所在的512字节同步到硬盘
@param: btmp_type:标识是inode位图还是block位图
@retval:略
*/
void bitmap_sync(struct partition *part, uint32_t bit_idx, uint8_t btmp_type);
//---------------------------位图操作-------------------------


//-------------------------------普通文件相关操作--------------------------------
/*
@brief: 创建并打开普通文件(磁盘操作)
@detail:1.创建new_inode(修改inode位图、inode分配内存、inode初始化)
        2.修改父目录(对应目录项写入父目录(写入磁盘),修改父目录inode.size)
        3.同步 父目录inode、new_inode、inode位图
        4.打开该文件(添加到file_table、PCB中的fd_table、Part中的open_inodes)
@param: flag:文件操作标识符
@retval:成功则返回文件描述符,否则返回-1
*/
int32_t file_create(struct dir *parent_dir, char *filename, uint8_t flag);

/*
@brief: 打开编号为inode_no的inode对应的普通文件
@detail:1.修改file_table
        2.修改PCB->fd_table
        3.调用inode_open()修改part->open_inodes
@param: 略
@retval:成功则返回文件描述符,否则返回-1
*/
int32_t file_open(uint32_t inode_no, uint8_t flag);

/*
@brief: 关闭文件结构
@detail:1.调用inode_close()修改part->open_inodes
        2. file->fd_inode = NULL; 使文件结构可用
@param: file:要关闭的文件结构
@retval:失败返回-1,成功返回0
@PS:没有将PCB中的文件描述符数组对应位置-1!!!
    调用者要负责将PCB->fd_table[]置-1
*/
int32_t file_close(struct file *file);

/*
@brief: 把buf中的count个字节写入file
@param: 略
@retval:成功则返回写入的字节数,失败则返回-1
*/
int32_t file_write(struct file *file, const void *buf, uint32_t count);

/*
@brief: 从文件file中读取count个字节写入buf
@param: 略
@retval:返回读出的字节数,若到文件尾则返回-1
*/
int32_t file_read(struct file *file, void *buf, uint32_t count);
//-------------------------------普通文件相关操作--------------------------------

关键函数说明

文件系统初始化函数

img

文件操作相关函数

img

工作路径相关函数

img

fs/file.c/file_create()

img

fs/file.c/file_write()

img

fs/file.c/file_read()

img

整体设计方案

内存管理模块整体方案如下:

img

物理内存池划分如下:

img

虚拟内存池划分如下:

img

数据结构

//物理内存池
struct pool{
    struct bitmap pool_bitmap;  //物理内存池位图
    uint32_t phy_addr_start;    //管理空间起始地址
    uint32_t pool_size;         //管理空间长度

    struct lock lock;           //进程申请物理内存时需要上锁
};

//虚拟内存池
struct virtual_addr{
    struct bitmap vaddr_bitmap; //虚拟内存池位图
    uint32_t vaddr_start;       //管理空间起始地址
};

/* 内存块 */
struct mem_block {
    struct list_elem free_elem;
};

/* 内存块描述符 */
struct mem_block_desc {
    uint32_t block_size;		 // 内存块大小
    uint32_t blocks_per_arena;	 // 本arena中可容纳此mem_block的数量.
    struct list free_list;	 // 目前可用的mem_block链表
};

/* 内存仓库arena元信息 */
struct arena {
    struct mem_block_desc* desc;	 // 此arena关联的mem_block_desc
    uint32_t cnt;
    bool large;		                // large为ture时,cnt表示的是页框数。否则cnt表示空闲mem_block数量
};

img

内存池提供以页为单位的内存空间
对于每一页内存空间都由元信息arena来组织,并将剩余空间切分成小块
每一个元信息arena组织的内存仓库 都按照块的大小有着相应的mem_block_desc
由mem_block_desc中的free_list来串起所有同一大小的空闲内存块

函数表

  • kernel/memory.c

      //-------------------------------------内存系统初始化相关函数---------------------------
    
      /*
      @brief: 初始化内存相关数据结构(内存池、内存仓库、内存块描述符)
      @param: 无
      @retval:无
      */
      void mem_init();
    
      /*
      @brief: 内存池初始化(物理内核内存池、物理用户内存池、虚拟内核内存池)
      @param: all_mem:物理内存总量
      @retval:无
      */
      static void mem_pool_init(uint32_t all_mem);
    
      /*
      @brief: 初始化内存块描述符数组(管理7种不同的内存块描述符(16、32、64、128、256、512、1024))
      @param: desc_array:要初始化的内存块描述符数组
      @retval:无
      */
      void block_desc_init(struct mem_block_desc* desc_array);
    
      //-------------------------------------内存系统初始化相关函数---------------------------
    
      //-------------------------------------内存分配相关函数---------------------------
    
      /*
      @brief: 分配pg_cnt页的内存空间
      @param: pf:内存池类型标识符(内核/用户)
              pg_nct:申请分配的页数
      @retval:成功返回起始虚拟地址,失败返回NULL
      */
      void* malloc_page(enum pool_flags pf,uint32_t pg_cnt);
    
      /*
      @brief: 向虚拟内存池申请pg_cnt页的空间
      @param: pf:内存池类型标识符(内核/用户)
              pg_nct:申请分配的页数
      @retval:成功返回起始虚拟地址,失败返回NULL
      */
      static void* vaddr_get(enum pool_flags pf,uint32_t pg_cnt);
    
      /*
      @brief: 向物理内核/用户内存池申请1页空间,
      @param: m_pool:申请的物理内存池(用户/内核)
      @retval:成功返回地址起点,失败返回-1
      */
      static void* palloc(struct pool* m_pool);
    
      /*
      @brief: 建立从虚拟地址到物理地址的映射(以页为单位)(建立相应的页表/页目录)
      @param: _vaddr:虚拟地址
              _page_phyaddr:物理地址
      @retval:无
      */
      static void page_table_add(void* _vaddr,void* _page_phyaddr);
    
      /*
      @brief: 给内核分配pg_cnt页内存,
      @param: 略
      @retval:成功则返回虚拟地址,失败返回NULL
      */
      void* get_kernel_pages(uint32_t pg_cnt);
    
      /*
      @brief: 给用户分配pg_cnt页内存,
      @param: 略
      @retval:成功则返回虚拟地址,失败返回NULL
      */
      void* get_user_pages(uint32_t pg_cnt);
    
      /*
      @brief: 在堆(即内存池)中申请size字节内存(灵活申请)
      @param: 略
      @retval:无
      */
      void* sys_malloc(uint32_t size);
    
      /*
      @brief: 给指定虚拟地址分配一页内存
      @param: pf:内存池表示符
              vaddr:指定虚拟地址
      @retval:成功则返回虚拟地址,失败返回NULL
      */
      void* get_a_page(enum pool_flags pf,uint32_t vaddr);
      //-------------------------------------内存分配相关函数---------------------------
                  
      //-------------------------------------内存回收相关函数---------------------------
    
      /*
      @brief: 释放 内核/用户 内存池种以虚拟地址vaddr为起点的cnt个物理页框 
      @param: pf:内存池标识符
              _vaddr:要回收的虚拟地址
              pg_cnt:要回收的页数
      @retval:无
      */
      void mfree_page(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt);
    
    
      /*
      @brief: 将1张物理页回收到物理内存池,实质就是清除物理内存池中位图的位
      @param: pg_pyh_addr:要回收页的物理地址
      @retval:无
      */
      void pfree(uint32_t pg_phy_addr);
    
      /*
      @brief: 将以_vaddr起始的连续pg_cnt个虚拟页回收到虚拟内存池,实质就是清除虚拟内存池中位图的位
      @param: pf:内存池标识符
              _vaddr:要回收的虚拟地址
              pg_cnt:要回收的页数
      @retval:无
      */
      static void vaddr_remove(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt)
    
      /*
      @brief: 解除页表中虚拟地址vaddr的映射,实质是将vaddr对应的pte存在位置0
      @param: 略
      @retval:无
      */
      static void page_table_pte_remove(uint32_t vaddr);
    
      /*
      @brief: 回收ptr指向的内存块(内存块大小由arena指出)
      @param: 略
      @retval:无
      */
      void sys_free(void* ptr);
    
      //-------------------------------------内存回收相关函数---------------------------
    
      //------------------------------工具函数-----------------------------------------
    
      /*
      @brief: 返回虚拟地址映射的物理地址 
      @param: 略
      @retval:略
      */
      uint32_t addr_v2p(uint32_t vaddr);
    
      /*
      @brief: 返回arena中第idx个内存块的地址
      @param: 略
      @retval:略
      */
      static struct mem_block* arena2block(struct arena* a, uint32_t idx);
    
      /*
      @brief: 返回内存块b所在的arena地址
      @param: 略
      @retval:略
      */
      static struct arena* block2arena(struct mem_block* b);
    
      /*
      @brief: 得到虚拟地址vaddr对应的pte指针
      @param: 略
      @retval:略
      */
      uint32_t* pte_ptr(uint32_t vaddr);
    
      /*
      @brief: 得到虚拟地址vaddr对应的pde指针
      @param: 略
      @retval:略
      */
      uint32_t* pde_ptr(uint32_t vaddr)
    
      //------------------------------工具函数-----------------------------------------
    

关键函数说明

  • kernel/memory.c/内存分配相关函数

    img

  • kernel/memory.c/内存回收相关函数

    img

  • kernel/memory.c/page_table_add()

    img

  • kernel/memory.c/sys_malloc()

    img

背景知识

pool-virtual_addr

arena-mem_block_desc

工具图表

整体设计方案

img

如图电脑的启动后接力棒的第一棒从BIOS开始,第二棒MBR负责把硬盘上的loader加载到内存里,第三棒loader处理完5个子功能后把接力棒正式交给内核。

我们在本模块所做的事,就是构建具备上述功能的MBR以及loader。

在了解MBR和loader的设计之前请先了解实模式下低端物理内存1MB的布局

MBR设计

MBR只需要负责加载loader到相应位置即可

MBR的程序代码分为三个部分:

  1. 寄存器初始化(包括栈顶指针初始化)

  2. 调用函数loader_ready_proc()(寄存器传参)

    PS:请先了解LBA28相关知识

  3. loader_ready_proc()的具体实现(功能是装载loader,也就是把loader写入磁盘相应位置)

    PS:请先了解磁盘写入相关知识

  4. 保证MBR一共512字节,并最后两字节必须是0x55和0xaa,使得BIOS能够检测并识别

loader设计

loader要负责做的事情可多了,大致可分为六个部分

  1. 数据段

    loader程序的数据段里存放着GDT等重要数据结构,安排如下图所示

    img

  2. 计算内存大小并存储到0xb00(也就是total_men_bytes标号处)

    我们模仿Linux获取内存的方法,调用BIOS中断0x15的三个子功能(0xe820、0xe801、0x88)去获取内存(一种失败了就接着使用另外一种,直到成功)

    0xb00则是我们安排在loader.S数据段的一个固定位置,当然如果你喜欢也可以存放在其他位置。

    注意:我们使用BIOS中断0x15时,该中断会以ARDS数据结构(描述内存段大小的信息)的形式,返回数个ARDS,所以我们需要在loader.S中划分一块缓冲区用于临时存放返回的ARDS

  3. 从实模式切换到保护模式

    PS:请先了解保护模式相关知识点,以及如何从实模式进入保护模式

  4. 构建内核页表页目录,开启分页机制

    PS:请先了解分页机制

    我们所要建立的满足可以自举证的分页模型如下图所示

    img

    1. 物理空间中低端1MB用于存放内核代码,紧接着0x100000~0x200000这1MB空间用于存放255个页表+1个页目录,每个页表/页目录都刚好是一个4KB自然页大小,每个页表项/页目录项则占4字节大小

    2. 虚拟空间0x00~0x1000000xc0000000~0xc0100000两个区间都被映射到物理空间的低端1MB内核代码区间

    3. PD[1023]指向页目录本身,为的是实现在开启分页机制后还能正确访问页表和页目录

      如果虚地址高10位全为1、虚地址中10位全为0,就把PD[0]当成自己的页表项,最终指向物理页地址0x101000
      如果虚地址高10位全为1、虚地址中10位全为1,就把PD[1023]当成自己的页表项,最终指向物理页地址0x100000
      如果虚地址高10位全为1、虚地址中10位处于一定范围内,就把PD[768]~PD[1022]当成自己的页表项目,最终指向物理地址0x101000及以上空间

      总结出不变的规律:

      • 要获取页目录表物理地址:让虚位高20位地址全为1,低12位全为0,即0xfffff000。这就是页目录自身的起始物理地址
      • 要访问页目录中的页目录项,即获取页表物理地址:使虚拟地址为0xfffffxxx,其中xxx是页目录项的索引*4
      • 访问页表中的页表项:虚拟地址公式为 0x3ff<<22+中间10位<<12+低12位(中间10位是页表的索引,低12位为页表内的偏移地址)
  5. 加载kernel到内存中

    将硬盘从0x9开始占据200扇区的kernel代码读取到内存0x70000起始处

  6. 初始化kernel

    PS:请先了解加载并初始化内核相关知识以及elf文件格式

数据结构

函数表

  • boot/mbr.S

      /*
      @brief: 该函数负责把磁盘上的loader装载到内存里(汇编函数/寄存器传参)
      @param: loader_start_sector是loader的LBA28扇区地址
              loader_base_addr是内存起始地址
              sector_cnt是移动的扇区数目
      @retval:无
      */
      void loader_ready_proc(loader_start_sector,loader_base_addr,sector_cnt);
    
  • boot/loader.S

      /*
      @brief: 该函数负责5件事分布如下:(汇编函数)
              1. 计算内存并存储到0xb00
              2. 从实模式到保护模式
              3. 构建内核页表页目录,开启分页机制
              4. 加载kernel到内核中
              5. 初始化kernel
      @param: 无
      @retval:无
      */
      void loader_start();
    

关键函数说明

背景知识/工具图表

实模式下低端物理内存1MB布局

img

  1. 我们整个SimpleOS的代码实际上只会装载到0x500~0x9FBFF这块内存区间(包括两块空闲的可用区域,和一块由BIOS确定的MBR区域)

  2. 512字节的MBR将会被BIOS强制装载到0x7C00~0x7DFF,(MBR只负责加载loader,运行过一次就没用了,之后可以被其他代码覆盖)

  3. 2048字节的loader规划在可用区间0x900~0x1100(loader是内核的起点,安排在离0x500近一点的地方,为之后的内核文件腾出足够的空间。至于和0x500之间存在的一点间隔存储个人决策,可以忽略)

  4. 200扇区kernel.bin,将其装载在0x70000~0x89000可用区域,(内核代码应该装载在可用空间的尽可能高位,为内核映像文件腾出位置)

  5. 保护模式下一些虚地址分配

    • 0xc0001500(虚地址)作为内核代码的入口

    • 一般来说可用空间的上界限0xc009fc00是最好的栈顶,但是为了让内存的每一块都形成4KB的自然页,所以栈顶最好取4KB的整数倍,因此栈顶设置为0xc009f000

    • 0xc009e000~0xc009f0004KB空间分配给内核主线程PCB

    • 0xc009a000~0xc009e000这四个页的空间(可管理一共512MB空间)大小全给位图(物理内核内存池位图、物理用户内存池位图、虚拟内核内存池位图)

LBA28相关知识

LBA28是用28位比特来描述一个扇区的地址的一种方式

其中前24位分别写在3个8位寄存器LBAlow、LBAmid、LBAhigh,最后4位写在device寄存器里

磁盘写入相关知识

硬盘并行接口-PATA

PATA接口的线缆也称IDE线

一个主盘提供了两个IDE插槽,这两个插槽称为两个通道,IDE0叫Primary通道,IDE1叫Secondary通道

每一个IDE线都可以挂载两块硬盘,一个主盘(master),一个从盘(slave)

硬盘操作方法

当我们要读取硬盘时,我们要先在控制寄存器里写入 读取命令字,然后才能从相关寄存器里读取到所需要的数据

而当我们需要写入硬盘时,我们要先在相关寄存器里写入数据,然后再向控制寄存器里写入 写入命令字,即完成写入

硬盘控制器主要的端口寄存器

img

Command Block registers用于向硬盘驱动器写入命令字或者从硬盘控制器里活得硬盘状态

Control Block registers用于控制硬盘状态

  1. data寄存器用于管理数据

  2. Error寄存器用于记录失败时的错误信息/Feature寄存器用于部分命令需要指定额外参数

  3. Sector count寄存器用来指定带读取/写入的扇区数目

  4. 3个8位的LBA寄存器用于记录LBA28地址的低24位(高4位记录在device寄存器)

  5. Command寄存器用于写入操作时存放命令字,可使用命令字如下:

    identify:0xEC (硬盘识别)
    read sector:0x20 (读扇区)
    write sector:0x30(写扇区)

  6. device寄存器是杂项,status寄存器用于给出硬盘状态信息,具体信息见下图

    img

与端口交互的in/out指令

  1. in指令用于从端口中读取数据,格式如下:

     in al,dx
     in ax,dx
    

    只要使用in指令,源操作数必须是dx(存放端口号),而目的操作数是用al,还是ax取决于dx端口指代的寄存器是8位宽还是16位宽

  2. out 指令用于往端口中写数据,格式如下:

     out dx,al
     out dx,ax
     out 立即数,al
     out 立即数,ax
    

    out指令的源操作数是ax还是al取决于目标端口指代的寄存器是8位宽还是16位宽,源操作数可以是立即数直接给出端口号,也可以用dx(存放端口号)

硬盘操作约定顺序

  1. 先选择通道,往该通道的sector cout寄存器写入待操作的扇区数
  2. 往通道上的三个LBA写入扇区地址LBA28的低24位
  3. 往device写入LBA28的高4位,指定主从盘,并选择LBA寻址模式
  4. 第四步往该通道的command寄存器写入命令(一旦写入立即执行)
  5. 读取status寄存器,判断硬盘工作是否完成
  6. 将硬盘数据读出(如果是写硬盘则无需这步)

保护模式概述

为什么要有保护模式(实模式的缺点)

  1. 实模式下用户程序和操作系统同一等级,而且逻辑地址就是物理地址,用户程序可以随意修改段基址访问所有内存,不安全

  2. 实模式16位寄存器决定访问超过64KB的内存区域要切换段基址、麻烦

  3. 一次只能运行一个程序,无法充分利用计算机资源

  4. 只有20条地址线,最大可用内存的寻址范围只有1MB,不够用

保护模式的特点

  1. 应用程序只能访问虚拟地址,虚拟地址由处理器和操作系统协作转换后才显示真正的物理地址

  2. 保护模式的运行环境是32位,寄存器、数据线、地址线也相应都被扩展到32位,指令格式也有了相应的扩展(允许32位源操作数)

  3. 保护模式不再使用中断向量表、段基址寄存器这些概念。取而代之的是段选择子寄存器、全局描述符、中断描述符表、各种门结构

  4. 保护模式引入了特权级的概念,应用程序不再和操作系统拥有同一特权级

保护模式的扩展

  1. 寄存器扩展:

    img

    保护模式下寄存器、地址线和数据总线都扩展到32位,内存寻址空间可达4GB,段内寻址空间也可达4GB。也就是说对内存的访问甚至可以让段基址=0,只由一个记录偏移量的寄存器来访问内存,这也就是所谓的平坦模型

    另外一提:保护模式抛弃基址这个概念,而是在内存里放入一个全局描述符表,每一个表项都是一个段描述符,用来描述各个内存段的起始地址、大小、权限等信息。段寄存器保护的也不再是段基址了,而是“选择子”,选择子本质上就是全局描述符表中的索引,就像是数组下标一样的东西。

  2. 寻址扩展:

    img

    如图所示保护模式的寻址方式更加灵活多变,不仅在基址寄存器(所有通用寄存器都可)和变址寄存器(处理esp外的所有通用寄存器都可)有了更多选择外,还引入了比例因子

  3. 运行模式反转:

    由于32位CPU兼容保护模式和实模式,所以如果你在保护模式下使用实模式的命令,或者在实模式下使用保护模式的命令,都会触发运行模式反转,将会在二进制机器码前加上相应的反转前缀。

    注意:模式反转前缀只对单条指令有效,效果并非是全局的

     [bit 16] ;告诉编译器接下来的代码是实模式
     [bit 32] ;告诉编译器接下来的代码是保护模式
    
    • 操作数反转前缀 0x66

      img

      如图上半部分是代码,下半部分是编译后的机器指令

      第三行在[bit 16]实模式下使用了eax,触发了保护模式转换,因此机器码前加了前缀0x66

      第五行在[bit 32]保护模式下使用了ax,触发了实模式转换,因此机器码前加了前缀0x66

    • 寻址方式反转前缀 0x67

      img

      第四行在[bit 16]实模式下同时使用了保护模式的32位源操作数和更加灵活的寻址方式,触发模式转换,机器码添加了前缀0x66、0x67

  4. 指令扩展

    指令扩展后允许32位寄存器和32位源操作数

从实模式到保护模式

从实模式到保护模式我们要执行四个步骤:

  1. 打开A20地址线

  2. 加载GDT

  3. 将CR0的PE位置1

  4. 使用jump指令更新流水线,避免指令出错

对于这三个步骤的讲解请看下文

段描述符

到了保护模式下,内存段不再是简单用寄存器加载即可用,而是需要提前把段定义好才可使用。全局描述符就是用来存储对每个段描述的表,全局描述符中的每一个表项包含段描述符,段描述符就是对一个段的描述,64位段描述符格式如下:

img

  1. 段基址:

    每个段都有32位的段基址,在段描述符中被拆分成三块存储。

    为什么被拆分成三块?为的是兼容,实模式下段基址是16位,80286有关短暂的24位段基址,而现在则是32位段基址,为了兼容原本应该连续存放的段基址被拆分为16-8-8的形式。

    当需要查看段基址时,硬件会把三个分散的段基址取出来并拼接在一起得到一个完整的32位段基址。

    PS:现在知道为什么有那么多屎山代码了,为了兼容旧时代的程序,屎山代码将成为每一个持续发展产品的最终归宿!

  2. 段界限:

    段界限表示段边界的扩展最值,20位段界限被拆分为两部分(当然又是为了兼容)。

    段界限是一个单位量,单位要么是1字节,要么是4KB(单位由G段决定)。也就是说段的最大寻址范围要么是1*2^20=1MB;要么是2*12*2^20=4GB。(注意寻址范围!=空间)

    实际的段界限边界值=(描述符中段界限+1)*(段界限的粒度大小:4KB/1Byte)-1

  3. S字段和type字段:

    img

    S字段只有1位:S=0 则说明是系统段(凡是硬件允许需要用到的东西,程序入口、调用门之类);S=1 则说明是非系统段(凡是软件运行需要的东西,数据、代码、栈都是数据段)

    type字段有4位:type字段只有在S确认后才有意义,X区分代码段和数据段,R代表是否可读,W代表是否可写,C代表是否一致,E代表向上扩展(E=0,低地址到高地址)或向下扩展(E=1,高地址到低地址),A代表是否被CPU读过(CPU访问过则置1)

  4. DPL(Descriptor Privilege Level)

    2位的DPL字段表示特权级,特权级从0~3,数字越低特权级越高,操作系统是0级,一般应用程序是3级

  5. P字段(Present):

    1位P表示段是否存在,有时候内存不够时,保护模式下CPU可能会按页(4KB)的单位将内存换到磁盘里,此时相当于该段不存在,即P=0;

  6. AVL字段(Avaliable):

    1位AVL字段代表该段是否可用,是否可用是对用户来说,对操作系统来说可随意访问此位

  7. L字段:

    1位L字段,L=1表示代码段是64位,L=0表示代码段是32位,我们在32位地址下编程将其设置为0即可

  8. D/B字段:

    1位D/B字段指定有效地址及操作数大小,对不同段的意义不同

    • 如果针对代码段,D=0时指令中有效地址和操作数是16位,指令有效地址用IP寄存器;D=1时指令中有效地址和操作数是32位,指令有效地址用EIP寄存器

    • 如果针对栈段,B=0时栈使用SP寄存器,栈最大寻址范围为2^16;B=1时栈使用ESP寄存器,栈最大寻址范围为2^32

  9. G段:

    1位G段用来指定段界限的单位大小,G=0时,段界限的单位时1字节;G=1时,段界限的单位是4KB

全局描述符号GDT、选择子以及GDTR寄存器

  1. GDT(Global Descriptor Table)相当于是段描述符的数组,每一表项都是一个段描述符

  2. 选择子是什么?选择子由三部分组成,如下图:

    img

    0~1位用来存储RPL,即特权级;第2位是TI(Table Indicator),用来表示选择子是GDT还是LDT的索引;3~5位是描述符的索引值,就是数组下表

    PS:我们注意到索引一共是13位,也就是说一个GDT最多有2^13=8192个表项

  3. LDT(Local Descriptor Table)是局部描述符,一个任务对应一个LDT,但它在现实中应用很少,我们的系统中也未用到LDT

  4. GDTR(Global Descriptor Table Register)是用来指向GDT的寄存器,GDT存储在内存中,GDTR存储的则是GDT的地址。

    img

    如图所示是GDTR的结构,48位寄存器前16位是GDT以字节为单位的界限,后32位是GDT在内存中的起始地址

    GDT界限范围有16位,也就是占有2^16个字节,而一个表项占有8字节,一个GDT一共可以存储2^16/8=8192个表项,和上面结论相符合

  5. ldgt(load Gloabal Descriptor Table)指令用来加载GPT,一般情况下从实模式进入保护模式我们需要使用命令ldgt来初始化GPTR,不仅如此,在保护模式中我们也可以使用ldgt命令来修改GPTR的值。ldgt的指令格式是:lgdt 48位内存数据

  6. 段描述符与内存的关系

    img

    如图可知,段描述符指向内存的各个地方。但是GDT的第0个段描述符是不可用的,因为GDT是用选择子来索引的,如果选择子忘记初始化就默认为0,这样选择子相当于索引到不可用的段描述符,而不会索引到其他内存空间。

打开A20地址线

  1. 实模式下的地址回绕

    实模式下有20根地址线,也就是说最多可以索引1MB空间。实模式下我们用16位段基址:16位偏移量的形式来计算物理地址,我们发现假设16位段基址是0xFFFF,16位地址量是0xFFFF,最终计算得到的物理地址应该是:0xFFFF*16+0xFFFF=0x10FFEF,我们发现这个地址已经超出了20位地址线所能传输的最大范围0xFFFFF。那当我们在实模式下访问超出0xFFFFF物理地址范围的空间时会发生什么事吗?其实并不会发生太糟糕的事,由于硬件原因,超出20位地址线的位将被舍弃,当你访问超过0x100000时就相当于访问0x00000,访问0x10FFEF时就相当于访问0x0FFEF。这个特点就叫做地址回绕。

  2. 32位CPU也要兼容地址回绕

    实模式下地址回绕的特性被许多程序员视为优点加以利用编程,但是保护模式却没有地址回绕这个问题。所以为了满足32位CPU必须兼容保护模式和实模式的特点,我们必须让32位CPU也要具备可以自由使用地址回绕的特点。

    我们知道32位CPU有32位的地址线,IBM在键盘控制器上的一些输出线来控制第21根地址线(A20)的有效性,成为A20Gate。

    如果A20Gate=1,当访问0x100000~0x10FFEF之间的地址将会正常访问

    如果A20Gate=0,当访问0x100000~0x10FFEF之间的地址将会触发地址回绕特性

  3. 打开A20地址线

    因此,当我们想从实模式进入保护模式时,我们必须打开A20Gate才能让保护模式的程序正常运行,打开A20地址总线的方式是将端口0x92的第一位置1,代码如下:

     in al,0x92
     or al,0000_0010B
     out 0x92,al
    

保护模式的开关,CRO寄存器的PE位

想从实模式进入保护模式,我们还差最后一步。控制寄存器CRx是CPU的窗口,既可以用来展示CPU内部状态,又可以用来控制CPU运行机制。这次我们要用到CR0寄存器的PE(Protection Eanble)位,CR0寄存器构造如下图所示:

img

右上方是CR0格式位,下方则是对每个位的描述,我们目前只需要关注PE位就行了,将PE位置1,让CPU知道我们要进入保护模式了,代码如下:

mov eax,cr0
or eax,0x00000001
mov cr0,eax

为什么使用远跳转指令来清空流水线

我们使用jmp dword SELECTOR_CODE:p_mode_start来更新流水线,究竟是为什么?

  1. 段描述缓冲寄存器未更新

    32位CPU兼容保护模式和实模式,段缓存寄存器在实模式下和保护模式下都有用。实模式下:段描述缓冲寄存器用于缓存段基址,保护模式下:段描述缓冲寄存器缓存段描述符。只有当CPU重新引用一个段后,段描述缓冲寄存器才会更新。

    当我们从实模式到保护模式后,我们的段描述缓存寄存器存在的还是实模式下用的20位段基址,这当然是不行的。所以我们指令跳转到SELECTOR_CODE:p_mode_start相当于重新引用一个段,让它更新。

  2. 流水线中指令译码错误

    从实模式到保护模式,一开始我们是16位指令,后来是32位指令。因为CPU的流水线技术提前被加载进流水线的32位指令可能会被译码错误成16位指令。因此我们使用无条件跳转指令jmp,跳转过后会自动清空流水线,避免译码错误。

  3. dword

    dword则是让编译器将p_mode_start当成32位操作数处理保证得到正确的地址

分页机制

为什么要分页?

我们只有4GB的内存空间,但我们想让每一个程序都拥有(或者以为自己拥有)4GB的内存空间,于是有了分页机制。

  1. 分页机制是在内存分段的基础上进行的

  2. 分页机制的核心思想是:通过映射,可以使连续的线性地址与任意物理内存地址相关联,逻辑上连续的线性地址其对应的物理地址可以不连续

  3. 一个程序它申请4GB的内存空间,实际上它并不是每时每刻都需要全部的4GB内存空间,大部分时候它都只在使用其中一两小部分的内存空间。我们将该4GB的内存空间分成好多个等大小的块(页),然后根据一个映射规则将当前有用到的块映射到物理内存中,这样4GB的物理内存就可以同时被接受多个程序享用。

一级页表

  1. 分页

    内存分段机制下的内存访问示意图如下:

    img

    我们在实模式下提供段基址,或者是在保护模式下提供的选择子加上另外提供的偏移量,在段部件的处理下形成了线性地址。在还没开启分页机制的情况下,这个线性地址就是真实的物理地址

    分页机制下的内存访问示意图如下:

    img

    如果打开了分页机制,线性地址还要经过页部件(负责检索页表的部件)的处理,然后才变成了真正的物理地址。我们把没经过页部件处理的线性地址叫做虚拟地址

    分页机制的作用在于:

    • 将线性地址转换成物理地址

    • 用大小相等的页代替大小不相等的段

    如下图所示:

    img

    在分段的基础上,将虚拟空间中的段划分为一块块大小相等的页然后映射到任意物理地址空间里

  2. 映射

    我们把存储映射关系的数据结构叫做页表(页表也是存储在内存中),页表中的每一项叫做页表项(记录着页对应的物理地址),一个页表项需要4字节的大小来描述,页表与物理内存之间的关系如下图所示:

    img

    线性地址和物理地址之间的映射有多种可选择的方案

    比如最简单的是逐字节映射,虚拟空间中的每一个字节对应到物理空间地址上的每一个字节,那么4GB的虚拟空间对应的页表就得有4G个页表项,每个页表项需要4字节,则一共需要16GB空间大小的页表。为了扩展4GB的内存空间而使用了16GB内存空间这明显是不合适的,所以我们要找到一个合适的映射关系,使得分页机机制即能实现,也不会占用太大的额外内存空间。

    最终决定的合适的映射方案是:每4KB大小的空间作为一页。也就是说4GB的内存空间一共可以划分成4GB/4KB=1M个页,一张页表就得含有1M个页表项,总大小为4MB(就空间耗费而言可以接受)

  3. 从线性地址到物理地址

    现在我们如何从线性地址定位到物理地址呢?

    • 首先页表是存在内存中的,页表的起始物理地址我们会放置在CR3控制器中,这样CPU就知道页表的位置了

    • 然后我们要定位到具体的页表项,取出线性地址的高20位作为索引*4(因为每个页表项占据4字节)+CR3中页表的起始物理地址=目标页表项的地址。找到了页表项也就相当于找到了该页对应的物理地址

    • 最后我们把线性地址低12位作为偏移量+页物理地址=线性地址对应的真正物理地址

    线性地址到物理地址转换的全过程如图所示:

    img

二级页表

一级页表的大小有4MB,这个大小虽然可以接受但不够灵活,我们需要保证内存里有一整块连续的4MB空间。而且每一个进程对应一个页表,当电脑同时运行多个进程的同时页表就会占据很大的空间。我们希望能更节约空间,于是有了二级页表机制

二级页表将原本一共有1M个页表项的大页表分成1k个每个包含1K个页表项的小页表。小页表的空间是1K*4Byte=4KB,刚好小页表的大小也是一个页。这样这些1K个小页表就可以灵活得分散到内存空间各个地方里了。但是为了找到这些小页表,我们需要一张页目录(页表的页表),页目录的每一项叫做页目录项(一个页目录项大小也是4字节,一共是1K项),每一项记录着对应小页表的物理地址。真巧!页目录的大小刚刚好也是1K*4Byte=4KB(就是一个页的大小)。

二级页表内存分布如下图所示:

img

这样做有什么好处吗?我们发现二级页表并没有让真正的页表所占用空间变少(只是把它们拆散了),反而多出了一个4KB大小的页目录。但实际上,这样做以后,小页表不仅不需要连续的大空间,而且也可以像普通的页一样在使用频率少的情况下被从内存换到磁盘上,只在需要用的时候才取回来。用4KB空间换取的灵活能带来更多好处。

如何从线性地址定位到物理地址(二级页表)?

  • 同样也是放在内存中的页目录变成了起点,页目录的起始物理地址我们会放置在CR3控制器中,这样CPU就知道页目录的位置了

  • 我们先取线性地址的高10位*4(页目录项也是4字节)定位到页目录中相对应的页目录项,找到了页目录项就相当于找到了对应页表的物理地址

  • 将线性地址的中间10位*4+对应页表的物理地址找到了页表项,找到了页表项就相当于找到了页的物理地址

  • 将线性地址的最后12位+页的物理地址=线性地址对应的真正物理地址

线性地址到物理地址(二级页表)转换的全过程如图所示:

img

页目录项、页表项以及CR3格式

页目录项和页表项的格式如下:

img

页目录基址寄存器(CR3)格式如下:

img

  1. 为什么页目录项的页表物理地址只有20位而不是32位?因为内存是以4KB每页为单位划分的,因此只要20位地址就可以找到对应的页表了

  2. 为什么页表的物理页地址也只有20位?这20位足够索引到内存中的对应页了,剩下的12位是段内偏移量由线性地址的最后12位组成

  3. AVL是Available位,表示可用,是给软件看的。操作系统可以不管该位

  4. G,全局位。G=1,则代表缓存在TLB(页表缓冲寄存器)中了,可以不用经过地址转换,直接通过TLB取值

  5. PAT(Page Attribute Table)此位比较复杂,直接置0即可

  6. D(Dirty)脏位,CPU对一个页进行写操作时,对应的页表项D位置1,表示该页已被修改过

  7. A(Accessd)访问位,每当CPU访问过该页时,对应的A位置1。过一段时间后由操作系统同一置0,操作系统可以通过置0的频率来判断该页是否被经常使用

  8. PCD(Page-level Cache Disable)页表高速缓冲禁止位,别管那么多,置0就行

  9. PWT(Page-level Write-Through)页级通写位,别管那么多,置0就行

  10. US(User/Supervisor)普通用户/超级用户位,为1表示User级,任意特权程序可访问。为0表示Supervisor级,特权级别3的程序不可访问

  11. RW(Read/Write)1表示可读可写,0表示可读不可写

  12. P(Present) 存在位,P=0表示该表不在物理内存中

启用分页机制的步骤

启用分页机制要做三件事:

  1. 准备好页目录以及页表

  2. 将页目录地址写入控制寄存器cr3

  3. 寄存器cr0的PG位置1

什么是可以自举的分页模型?

当我们想要访问一个物理地址时,我们给出的线性地址将会经过页部件的转换(页目录和页表的查询)后指向真实的物理地址。

现在有一个问题,如果我想要在开启分页机制的情况下修改现有的分表/页目录,我该怎么做?

你可能已经发现问题所在了,我们给出的线性地址都是经过页表/页目录的映射后才指向真实的物理地址。但是如果我想访问页表和页目录,我给出的地址也是会经过页表/页目录的映射后指向其他地方。所以我们需要可以自举的分页模型,也就是说给出的线性地址经过经过页部件转换后可以真正指向目标页表/页目录的物理地址。

接下来我们为loader构建的分页模型就是一个可以自举的分页模型

加载内核并初始化

加载内核并初始化的步骤

我们将告别汇编,用C编写内核文件kernel.bin,用C编写将会和之前有以下区别:

img

加载内核要做的事如下:

  1. 用C编写并使用gcc编译链接得到kernel.bin文件,然后用dd指令将kernel.bin文件放到磁盘里

  2. 修改loader.S,负责把kernel.bin文件加载到合适的位置(执行完第三步kernel.bin就没用了)

  3. 修改loader.S,负责初始化内核,即通过elf头文件信息 将kernel.bin文件里的每个段分别放置在elf头文件指定位置(elf中包含头文件,我们总不能把头文件里的元信息也放置到CPU上执行,所以需要拆解)

  4. 跳转到kernel的程序入口地址,loader.S交出最后一棒接力棒

内核文件的内存布局

我们要讲内核加载到内存的哪里?请看下图低端1MB内存布局里三个打勾的位置:

img

三个打勾的位置将会是我们内核存放的地方(加载在0x7c00的MBR的工作已经做完了,可以被覆盖。加载在0x900的loader里面包含gdt设置,不能被覆盖),从上述加载内核的步骤看我们需要两个地方来存储内核。

第一个地方存储kernel.bin(对应第2步)

第二个地方存储被loader.S处理后的真正的内核映像文件(对应第三步)

kernel.bin应尽量位于高地址,给不断增长的kernel映像文件腾出空间。预计kernel.bin不会超过100kb,计划存储在0x70000(0x70000~0x9fbff有190KB)。

kernel被处理后的映像文件应该尽量放在低地址同时不能覆盖loader。预计loader大小不会超过2000字节,0x900+2000=0x10d0,取一个整数为kernel的映像文件地址0x1500。

上述我们说的都是物理地址,由于我们开启了分页机制后,写代码时里要将物理地址转化为虚拟地址,相应的两个虚拟地址分别是0xc0070000和0xc0001500

在加载完内核后,我们还需要选择一个新的地方作为内核代码的栈顶,可用空间的顶部0x9fc00作为栈顶是最合适的。但是由于pcb(后面章节讲)要求4KB对齐,所以栈顶既要接近0x9fc00又要是4KB的整数倍,所以我们选择了0x9f000作为内核代码的栈顶,转化为虚拟地址即是0xc009f000

elf文件格式

elf文件布局

elf文件=二进制可执行文件+头文件(存储元信息)

一个elf文件的逻辑布局如下图:

img

物理布局如下图:

img

关于这两图我们要讲几点:

  1. Section和Segment的区别:

    Section是写代码时为了更清楚的逻辑划分,程序员将代码主动划分为一节一节。(汇编语言中的section、segment关键字本质上划分的都是节)

    Segment是编译器将相同类型的Section集合在一起形成了段,如代码段、数据段。(经过编译器链接后,我们才称为段)

  2. 我们关注的重点:

    大部分的Section经过编译器链接后成为了Segment,我们关注的重点在Segment,我们所要做的就是根据elf头文件的指示,将每一个Segment放到它该去的地方

elf header结构

elf格式的数据类型(它们就和int、double一样,只关注字节大小就好了)

img

elf header的数据结构(该数据结构的布局是重点,我们关注每个字段的字节偏移,这样loader.S就可以读取它需要的字段了)

img

elf header具体数据成员意义描述(重在会查表应用,而且大部分时候我们只使用其中关键的几项:e_phoff、e_phentisize、e_phnum):

  1. e_ident

    img

  2. e_type

    img

  3. e_machine

    img

  4. others

    img

program table header结构

program table header的数据结构(该数据结构的布局是重点,我们关注每个字段的字节偏移,这样loader.S就可以读取它需要的字段了)

img

program table header的成员描述(重在会查表应用,而且大部分时候我们只使用其中关键的几项:p_offset、p_vaddr、p_mensz):

  1. p_type
    img

  2. p_flags
    img

  3. others
    img

实例:请参照操作系统真相象还原P218-5.3.4;我们可以使用命令readelf -e '文件名'来查看一个elf文件的头的具体数据,也可以使用hd '文件名'来查看一个elf文件的十六进制形式

完整源代码

见连接如下:

本文描述并解决在VSCode里遇到的控制台编码与文件编码不一致导致的乱码问题

VSCode控制台介绍

VSCode里不止只有1个控制台,如下图所示:

img

图中一共有四个控制台,可通过终端窗口右上角的+进行调整

  • PowerShell:VSCode默认采用PowerShell,功能上比cmd更为强大,兼容cmd命令的同时有自己扩展的指令集,用来管理Windows系统和应用程序,执行复杂的脚本和自动化任务。

  • Git Bash:Git Bash是Git自带的一个终端模拟器,兼容cmd命令的同时扩展了Linux命令和git命令,它可以在Windows上模拟Bash环境(Linux的控制台)。适合习惯使用Linux的用户。

  • JavaScript调试终端:可以让你在VSCode中直接运行和调试JavaScript代码,而不需要额外的配置或者浏览器。

  • Command Prompt:基于Dos的传统的cmd命令行,cmd不兼容上述控制台,只能用来执行一些基本的命令和批处理文件。

除了上述四个控制台外,还有在调试代码时跳出的针对不同语言不同的控制台,例如:用于调试C++/C代码的cppdbg;用于调试python代码的python Debug Console等等,可以在调试代码时,查看终端窗口右上角的小标题来确定你现在使用的是哪一个控制台

注意:不同的控制台使用的编码是独立不相互影响的,千万不要使用A控制台却去调整B控制台的编码

VSCode文件编码介绍

VSCode当前文件编码格式可以查看右下角

img

如图,当前文件编码是UTF-8

如果想要修改该文件编码,可以点击图中的UTF-8

img

先选择通过目标编码方式保存,再通过目标编码方式打开,这样就成功修改了当前文件的编码格式

常见的编码介绍

  • GBK:针对中文的编码(国内cmd默认的编码方式),在国家标准GB2312的基础上扩展的,向下兼容GB2312,但在国外并不常用。代码是936

  • UTF-8:UTF-8是一种针对多语言的编码,它包含了全世界所有国家需要用到的字符,基于Unicode字符集的,向下兼容ASCII,在国际上通行。代码是65001

如何查看并改变控制台编码

在控制台里输入以下命令可以查看当前控制台编码格式:

chcp

在控制台里输入以下命令可临时转化当前控制台编码格式:

chcp '编码代码'

综上,只要保证文件和控制台编码格式相同,就不会出现乱码问题

本文尚未解决的问题

本文只提供了临时更改控制台编码的方法,并没有提供更改各个控制台默认编码的方法

  这篇文章记录我第一次创建个人博客的过程,使用了Github个人账户域名,开源免费的Hexo博客框架以及Next主题。

安装Git和Node.js

Git 是一种分布式版本控制系统,即,代码的本地克隆就是一个完整的版本控制存储库。 通过这些功能齐全的本地存储库,无论脱机还是远程都能轻松工作。 开发人员会在本地提交其工作,然后再将存储库的副本与服务器上的副本进行同步。(Github就是搭配Git使用的用于存储代码的克隆库)

Node.js可以让JavaScript脱离浏览器运行,它是一个开源、跨平台的JavaScript运行时环境,可以用来开发高性能的 Web 服务器和网络应用。

搭建个人博客为何需要安装Git和Node.js?我们基于Hexo搭建博客,Hexo必须依赖Node.js提供的环境运行。而Git并非是搭建个人博客的必备,但我们仍然推荐下载Git,使用Git的相关命令来从Hexo下载Next主题。

  1. 从官网上安装Git并配置相关环境变量(PS:安装完Git后你可以使用Git Bash作为命令行窗口调用命令,Git Bash上可以使用Linux格式的命令,但由于Git Bash上安装下载无法看到进度条的问题,我个人更推荐用cmd来进行操作,本文后续无特殊声明命令行全采用cmd)

  2. 绑定Git和Github账号,在cmd里输入以下命令:

     git config --global user.name “Your Name”
     git config --global user.email email@example.com
     :: 其中Your Name和email@example.com替换成上面注册时的账户名和邮箱
    
  3. 从官网上安装Node.js并配置相关环境变量

  4. 执行完上述步骤可以用以下命令测试是否安装成功:

     git version
     node -v
     npm -v
    

    img

安装Hexo

hexo是一个基于Node.js的静态博客框架,它可以让您使用Markdown(或其他渲染引擎)编写文章,并在几秒内生成静态网页。

npm是Node.js 的默认程序包管理器,它可以让您从 npm 服务器下载、安装、上传和管理 Node.js 的模块或包。模块或包是一些可以重用的代码,可以实现一些特定的功能或提供一些特定的服务

使用npm安装Hexo,命令如下:

npm install -g hexo-cli

创建博客网站

所有的准备工作都做好了后,现在需要生成一个文件夹作为个人博客网站的根目录,在你希望放置个人博客文件夹的地方使用如下命令:

hexo init myBlog
:: 其中myBlog就是你的个人博客网站根目录,可以取自己喜欢的名字
cd myBlog
npm install

如果上面工作都没有出错的化,现在你的个人博客已经搭建成功了,你可以在个人博客根目录下输入以下命令在本地预览效果:

hexo s

img

并且我们可以在根目录下的_config.yml里对个人博客的初始设置进行配置,例如:姓名、标题等个性化设置

选择你喜欢的主题

大家可以去Hexo官网去寻找喜欢的主题下载下来,每个主题都可以点击预览,并且可以点击查看使用说明文档

我使用的Next主题是较为受欢迎的一款,风格简约大气

下载主题有两种方法,这边只介绍使用一种,在个人博客文件根目录下使用命令:

git clone https://github.com/next-theme/hexo-theme-next themes/next

如果下载成功则根目录下会出现该文件夹 /themes/next

我们就可以在NexT文件夹里的_config.yml里对该主题的一些设定进行配置,但这种方法存在弊端,官方推荐的配置方法以及具体的操作可以查看NexT官方说明文档

将个人博客部署到Github上

Github能且仅能使用一个同名仓库的代码托管一个静态站点.

  1. 在Github上创建一个名为:用户名.github.io的仓库

  2. 使用以下命令配置SSH钥匙:

     git config --global user.name "用户名"
     git config --global user.email "邮箱地址"
     ::之前已经配置过可直接输入第三条命令 
     ssh-keygen -t rsa -C '上面的邮箱'
    

    可在C:\Users\用户名\.ssh\id_rsa.pub文件里查看SSH公钥

  3. 首次使用还须使用以下命令确认并添加主机到本机SSH可信列表:

     ssh -T git@github.com
     ::若返回 Hi xxx! You've successfully authenticated, but GitHub does not provide shell access. 内容,则证实添加成功
    
  4. 登陆Github添加刚刚生成的SSH key,在下图中Key部分处放入SSH公钥内容
    img

  5. 在根目录底下_config.xml文件拉到最底部添加如下配置:

     deploy :
     type: git
     repo: https://github.com/1478540/1478540.github.io.git
     # repo是你的仓库地址
     branch: master
    
  6. 安装一个部署插件:

     npm install hexo-deployer-git --save
    
  7. 生成相应的博客文件并部署:

    hexo g 
    ::g是generate,生成相应文件
    hexo d
    ::d则是部署,部署完以后就可以通过Github账户域名访问个人博客了
    

结语

文章发布可以通过命令hexo new '文章标题',也可以直接在/sourse/_posts文件夹下创建.md文件,文件的具体的编写可以使用makedown语法

本文学习于B站教程[教程]Hexo & Github搭建自己的专属博客

0%