C++手写操作系统学习笔记(五)—— 网络,硬盘和系统调用
说明:本系列为B站课程《使用c++编写操作系统》及其原版视频的学习笔记,环境为VScode连接本地虚拟机(Virtualbox)上Ubantu系统。
C++手写操作系统学习笔记(五)
1.网卡驱动
1.网络适配器AM79C973
网卡(网络适配器)是一块被设计用来允许计算机在计算机网络上进行通讯的计算机硬件。由于其拥有MAC地址,因此属于OSI模型的第1层和2层之间。它使得用户可以通过电缆或无线相互连接。
PCnet PCI II(AM79c973)是一个支持PCI总线的网络适配器。它内置了对CRC检查的支持,可以自动将短数据包填充到最小以太网长度。它支持PCI总线主控,可以在32位模式和传统的16位兼容模式下操作。最重要的是它是virtualbox所支持的虚拟网卡之一。我们的任务就是通过PCI获取到此网卡并编写相应的驱动程序。
所有有关am79c973的信息可以在这里找到https://www.alldatasheetcn.com/(只找到这个,过于复杂很难读懂)可以看到:它拥有相当多的功能和相当多的寄存器,这里只使用它最基础的功能。
2.网络功能实现
include/drivers/amd_am79c973.h
...(头)
class amd_am79c973 : public Driver, public hardwarecommunication::InterruptHandler
{//作为驱动程序,应继承driver和中断管理
struct InitializationBlock
{//定义初始化块,每次初始化都是读取这里的参数信息
common::uint16_t mode;
unsigned reserved1 : 4;//冒号表示该变量应占几位
unsigned numSendBuffers : 4;//8个缓冲区用3位
unsigned reserved2 : 4;
unsigned numRecvBuffers : 4;
common::uint64_t physicalAddress : 48;//48位MAC地址
common::uint16_t reserved3;
common::uint64_t logicalAddress;
common::uint32_t recvBufferDescrAddress;
common::uint32_t sendBufferDescrAddress;//记录发送和接收ip地址
} __attribute__((packed));
struct BufferDescriptor
{//定义缓冲区描述符:所有发送和接收的信息都要先写入缓冲区(各有8个)
common::uint32_t address;
common::uint32_t flags;
common::uint32_t flags2;
common::uint32_t avail;
} __attribute__((packed));
hardwarecommunication::Port16Bit MACAddress0Port;
hardwarecommunication::Port16Bit MACAddress2Port;
hardwarecommunication::Port16Bit MACAddress4Port;
hardwarecommunication::Port16Bit registerDataPort;
hardwarecommunication::Port16Bit registerAddressPort;//"经典的"成对出现的端口:一个控制端口一个传输数据端口
hardwarecommunication::Port16Bit resetPort;
hardwarecommunication::Port16Bit busControlRegisterDataPort;
//通过这6个端口控制am79c973
InitializationBlock initBlock;
BufferDescriptor* sendBufferDescr;
common::uint8_t sendBufferDescrMemory[2048+15];//申请内存,+15是为了对齐格式,
common::uint8_t sendBuffers[2*1024+15][8];
common::uint8_t currentSendBuffer;
//定义8个发送缓冲区(每次只能有一个活跃的)
BufferDescriptor* recvBufferDescr;
common::uint8_t recvBufferDescrMemory[2048+15];//申请内存
common::uint8_t recvBuffers[2*1024+15][8];
common::uint8_t currentRecvBuffer;
//定义8个接收缓冲区(每次只能有一个活跃的)
public:
amd_am79c973(myos::hardwarecommunication::PCI_DeviceDescriptor *dev,
myos::hardwarecommunication::InterruptManager* interrupts);
//am79c973作为PCI上设备,构造函数传入PCI设备描述符和中断管理
~amd_am79c973();
void Activate();//重写driver中的激活方法
int Reset();
common::uint32_t HandleInterrupt(common::uint32_t esp);
void Send(common::uint8_t* buffer, int count);
void Receive();
};
#endif
src/drivers/amd_am79c973.cpp
#include <drivers/amd_am79c973.h>
using namespace myos;
using namespace myos::common;
using namespace myos::drivers;
using namespace myos::hardwarecommunication;
void printf(char*);
void printfHex(uint8_t);
amd_am79c973::amd_am79c973(PCI_DeviceDescriptor *dev, InterruptManager* interrupts)
: Driver(),
InterruptHandler(interrupts, dev->interrupt + interrupts->HardwareInterruptOffset()),
MACAddress0Port(dev->portBase),
MACAddress2Port(dev->portBase + 0x02),
MACAddress4Port(dev->portBase + 0x04),
registerDataPort(dev->portBase + 0x10),
registerAddressPort(dev->portBase + 0x12),
resetPort(dev->portBase + 0x14),
busControlRegisterDataPort(dev->portBase + 0x16)
//构造函数,初始化操作am79c973的端口
{
this->handler = 0;
currentSendBuffer = 0;
currentRecvBuffer = 0;
uint64_t MAC0 = MACAddress0Port.Read() % 256;
uint64_t MAC1 = MACAddress0Port.Read() / 256;
uint64_t MAC2 = MACAddress2Port.Read() % 256;
uint64_t MAC3 = MACAddress2Port.Read() / 256;
uint64_t MAC4 = MACAddress4Port.Read() % 256;
uint64_t MAC5 = MACAddress4Port.Read() / 256;
//获取mac地址
uint64_t MAC = MAC5 << 40 | MAC4 << 32 | MAC3 << 24 | MAC2 << 16 | MAC1 << 8 | MAC0;
//拼接MAC地址
// 32 bit mode
registerAddressPort.Write(20);
busControlRegisterDataPort.Write(0x102);
// STOP reset
registerAddressPort.Write(0);
registerDataPort.Write(0x04);
// initBlock:初始化操作:在内存中读取initBlock以获取参数(来自https://www.alldatasheetcn.com/)
initBlock.mode = 0x0000; // promiscuous mode = false
initBlock.reserved1 = 0;
initBlock.numSendBuffers = 3;
initBlock.reserved2 = 0;
initBlock.numRecvBuffers = 3;
initBlock.physicalAddress = MAC;
initBlock.reserved3 = 0;
initBlock.logicalAddress = 0;
sendBufferDescr = (BufferDescriptor*)((((uint32_t)&sendBufferDescrMemory[0]) + 15) & ~((uint32_t)0xF));
initBlock.sendBufferDescrAddress = (uint32_t)sendBufferDescr;
recvBufferDescr = (BufferDescriptor*)((((uint32_t)&recvBufferDescrMemory[0]) + 15) & ~((uint32_t)0xF));
initBlock.recvBufferDescrAddress = (uint32_t)recvBufferDescr;
for(uint8_t i = 0; i < 8; i++)//设置所有8×2个缓冲区描述符
{
sendBufferDescr[i].address = (((uint32_t)&sendBuffers[i]) + 15 ) & ~(uint32_t)0xF;
sendBufferDescr[i].flags = 0x7FF | 0xF000;
sendBufferDescr[i].flags2 = 0;
sendBufferDescr[i].avail = 0;
recvBufferDescr[i].address = (((uint32_t)&recvBuffers[i]) + 15 ) & ~(uint32_t)0xF;
recvBufferDescr[i].flags = 0xF7FF | 0x80000000;
recvBufferDescr[i].flags2 = 0;
sendBufferDescr[i].avail = 0;
}
registerAddressPort.Write(1);
registerDataPort.Write( (uint32_t)(&initBlock) & 0xFFFF );
registerAddressPort.Write(2);
registerDataPort.Write( ((uint32_t)(&initBlock) >> 16) & 0xFFFF );//初始化操作
}
amd_am79c973::~amd_am79c973(){}
void amd_am79c973::Activate()
{//实际上是更多的初始化操作:向地址和数据端口写入特定信息
registerAddressPort.Write(0);
registerDataPort.Write(0x41);
registerAddressPort.Write(4);
uint32_t temp = registerDataPort.Read();
registerAddressPort.Write(4);
registerDataPort.Write(temp | 0xC00);
registerAddressPort.Write(0);
registerDataPort.Write(0x42);
}
int amd_am79c973::Reset(){
resetPort.Read();
resetPort.Write(0);
return 10;
}
uint32_t amd_am79c973::HandleInterrupt(common::uint32_t esp){
//中断信号是初始化操作后的反馈信息,不同位代表不同的信息
registerAddressPort.Write(0);
uint32_t temp = registerDataPort.Read();
if((temp & 0x8000) == 0x8000) printf("AMD am79c973 ERROR\n");
if((temp & 0x2000) == 0x2000) printf("AMD am79c973 COLLISION ERROR\n");
if((temp & 0x1000) == 0x1000) printf("AMD am79c973 MISSED FRAME\n");
if((temp & 0x0800) == 0x0800) printf("AMD am79c973 MEMORY ERROR\n");
if((temp & 0x0400) == 0x0400) Receive();
if((temp & 0x0200) == 0x0200) printf(" SENT");
// acknoledge
registerAddressPort.Write(0);
registerDataPort.Write(temp);
if((temp & 0x0100) == 0x0100) printf("AMD am79c973 INIT DONE\n");
return esp;
}
void amd_am79c973::Send(uint8_t* buffer, int size)
{//发送数据:传入第一个buffer的指针和数据大小
int sendDescriptor = currentSendBuffer;
currentSendBuffer = (currentSendBuffer + 1) % 8;//更新currentSendBuffer(递增)
if(size > 1518) size = 1518;//最大发送长度
for(uint8_t *src = buffer + size -1,
*dst = (uint8_t*)(sendBufferDescr[sendDescriptor].address + size -1);
src >= buffer; src--, dst--)
*dst = *src;
//src和dst是分别指向发送到数据的缓冲区末尾和开头的指针,通过for循环将要发送到信息写入发送缓冲区
printf("\nSEND: ");
for(int i = 14+20; i < (size>64?64:size); i++){
printfHex(buffer[i]);
printf(" ");
}
sendBufferDescr[sendDescriptor].avail = 0;
sendBufferDescr[sendDescriptor].flags2 = 0;
sendBufferDescr[sendDescriptor].flags = 0x8300F000 | ((uint16_t)((-size) & 0xFFF));
registerAddressPort.Write(0);
registerDataPort.Write(0x48);
}
void amd_am79c973::Receive()
{
printf("\nRECV: ");
for(; (recvBufferDescr[currentRecvBuffer].flags & 0x80000000) == 0;
currentRecvBuffer = (currentRecvBuffer + 1) % 8)
{//循环遍历所有有数据的(标志位为1)recvBuffer并更新currentbuffer
if(!(recvBufferDescr[currentRecvBuffer].flags & 0x40000000)
&& (recvBufferDescr[currentRecvBuffer].flags & 0x03000000) == 0x03000000){
uint32_t size = recvBufferDescr[currentRecvBuffer].flags & 0xFFF;//获取数据size
if(size > 64) size -= 4; // remove checksum
uint8_t* buffer = (uint8_t*)(recvBufferDescr[currentRecvBuffer].address);
for(int i = 14+20; i < (size>64?64:size); i++){
printfHex(buffer[i]);
printf(" ");
}//打印接收到的内容
if(handler != 0)
if(handler->OnRawDataReceived(buffer, size))
Send(buffer, size);
}
recvBufferDescr[currentRecvBuffer].flags2 = 0;
recvBufferDescr[currentRecvBuffer].flags = 0x8000F7FF;
}
}
现在我们有能力接收来自网络的信息,但是网络信息是通过不同的协议(以太网帧,ARP,ICMP,TCP等)组织起来的,我们还没有加入代码来解析这些协议,所以无法获取任何实质的信息。
2.硬盘驱动
1.ATA硬盘实现
ATA是广为使用的IDE和EIDE设备的相关标准。ATA是Advanced Technology Attachment的缩写,意思是高级技术附件规格。ATA可以使用户方便地在PC机上连接硬盘。ATA硬盘一般使用IDE接口,分为PATA硬盘(parallelATA,并行ATA硬盘接口规范,已经基本淘汰)和SATA硬盘(serialATA,串行ATA硬盘接口规范)。这里勉强实现了对硬盘的写入和读取,但一次必须写入或读取整个扇区。
ATA实现比较简单,省略库文件编写。
src/drivers/ata.cpp
#include <drivers/ata.h>
using namespace myos;
using namespace myos::common;
using namespace myos::drivers;
void printf(char* str);
void printfHex(uint8_t);
AdvancedTechnologyAttachment::AdvancedTechnologyAttachment(bool master, common::uint16_t portBase)
//初始化操作ATA的端口,master:确定主/从硬盘驱动器
: dataPort(portBase),
errorPort(portBase + 0x1),
sectorCountPort(portBase + 0x2),
lbaLowPort(portBase + 0x3),
lbaMidPort(portBase + 0x4),
lbaHiPort(portBase + 0x5),//lba地址的端口
devicePort(portBase + 0x6),
commandPort(portBase + 0x7),
controlPort(portBase + 0x206)
//LBA是非常单纯的一种定址模式,从0开始编号来定位区块,第一区块LBA=0,第二区块LBA=1,依此类推。这种定址模式取代了
//原先操作系统必须面对存储设备硬件(磁头磁道扇区)构造的方式。
{
this->master = master;
}
AdvancedTechnologyAttachment::~AdvancedTechnologyAttachment(){}
void AdvancedTechnologyAttachment::Identify()
{//确定地址
devicePort.Write(master ? 0xA0 : 0xB0);
controlPort.Write(0);
devicePort.Write(0xA0);
uint8_t status = commandPort.Read();//读取状态
if(status == 0xFF)
return;
devicePort.Write(master ? 0xA0 : 0xB0);
sectorCountPort.Write(0);
lbaLowPort.Write(0);
lbaMidPort.Write(0);
lbaHiPort.Write(0);
commandPort.Write(0xEC); // identify command
status = commandPort.Read();
if(status == 0x00)
return;
while(((status & 0x80) == 0x80) && ((status & 0x01) != 0x01))//通过while循环等待status更新
status = commandPort.Read();
if(status & 0x01){
printf("ERROR");
return;
}
for(int i = 0; i < 256; i++){
uint16_t data = dataPort.Read();
char *text = " \0";
text[0] = (data >> 8) & 0xFF;
text[1] = data & 0xFF;
printf(text);//每读取2B并打印
}
printf("\n");
}
void AdvancedTechnologyAttachment::Read28(common::uint32_t sectorNum, int count)
{//从硬盘读数据,指定硬盘号和读取大小
if(sectorNum > 0x0FFFFFFF) return;//设定读取大小上限
devicePort.Write( (master ? 0xE0 : 0xF0) | ((sectorNum & 0x0F000000) >> 24) );
errorPort.Write(0);
sectorCountPort.Write(1);
lbaLowPort.Write( sectorNum & 0x000000FF );
lbaMidPort.Write( (sectorNum & 0x0000FF00) >> 8);
lbaLowPort.Write( (sectorNum & 0x00FF0000) >> 16 );
commandPort.Write(0x20);
uint8_t status = commandPort.Read();
while(((status & 0x80) == 0x80) && ((status & 0x01) != 0x01))
status = commandPort.Read();
if(status & 0x01){
printf("ERROR");
return;
}
printf("Reading ATA Drive: ");
for(int i = 0; i < count; i += 2){
uint16_t wdata = dataPort.Read();
char *text = " \0";
text[0] = wdata & 0xFF;
if(i+1 < count)
text[1] = (wdata >> 8) & 0xFF;
else
text[1] = '\0';
printf(text);
}//输出读取内容
for(int i = count + (count%2); i < 512; i += 2)
dataPort.Read();//每次必须读取512B(一个扇区),不到512就读空
}
void AdvancedTechnologyAttachment::Write28(common::uint32_t sectorNum, common::uint8_t* data, common::uint32_t count)
{//向硬盘写数据,与读类似
if(sectorNum > 0x0FFFFFFF) return;
if(count > 512) return;
devicePort.Write( (master ? 0xE0 : 0xF0) | ((sectorNum & 0x0F000000) >> 24) );
errorPort.Write(0);
sectorCountPort.Write(1);
lbaLowPort.Write( sectorNum & 0x000000FF );
lbaMidPort.Write( (sectorNum & 0x0000FF00) >> 8);
lbaLowPort.Write( (sectorNum & 0x00FF0000) >> 16 );
commandPort.Write(0x30);
printf("Writing to ATA Drive: ");
for(int i = 0; i < count; i += 2){
uint16_t wdata = data[i];
if(i+1 < count)
wdata |= ((uint16_t)data[i+1]) << 8;
dataPort.Write(wdata);
char *text = " \0";
text[0] = (wdata >> 8) & 0xFF;
text[1] = wdata & 0xFF;
printf(text);
}
for(int i = count + (count%2); i < 512; i += 2)
dataPort.Write(0x0000);
}
void AdvancedTechnologyAttachment::Flush()
{//刷新:每次读写数据都要经过缓冲区,通过刷新清空缓冲
devicePort.Write( master ? 0xE0 : 0xF0 );
commandPort.Write(0xE7);
uint8_t status = commandPort.Read();
if(status == 0x00)
return;
while(((status & 0x80) == 0x80) && ((status & 0x01) != 0x01))
status = commandPort.Read();
if(status & 0x01){
printf("ERROR");
return;
}
}
2.硬盘功能测试
在ata.cpp中仅定义了硬盘的操作方法,在kernel.cpp中还要通过端口加入具体的(虚拟)硬盘。
同时,在虚拟机中为操作系统添加新的虚拟硬盘(设置->存储->添加硬盘->VDI->动态分配选择2G空间)
kernel.cpp
... ...
#ifdef GRAPHICSMODE
... ...
#endif
printf("\n primary master: ");
AdvancedTechnologyAttachment ata0m(true, 0x1F0);//通过端口找到硬盘地址
ata0m.Identify();
printf("\n primary slave: ");
AdvancedTechnologyAttachment ata0s(false, 0x1F0);
ata0s.Identify();
ata0s.Write28(0, (uint8_t*)"http://www.AlgorithMan.de", 25);
ata0s.Flush();
ata0s.Read28(0, 25);//写入与读写测试
printf("\nS-ATA secondary master: ");
AdvancedTechnologyAttachment ata1m(true, 0x170);
ata1m.Identify();
printf("\nS-ATA secondary slave: ");
AdvancedTechnologyAttachment ata1s(false, 0x170);
ata1s.Identify();
// third: 0x1E8
// fourth: 0x168
... ...
make run后发现出现了一些错误(作者选择忽视),但可以写入和读取我们的数据
3.系统调用
1.系统调用功能
到目前为止,我们主要编写的都属于操作系统内核内容,所谓系统调用,就是用户在程序中调用操作系统内核所提供的一些子功能,相当于用户可以使用的操作操作系统内核的接口(用户不能或不应该直接执行对系统影响非常大的操作)。是为了统一不同操作系统之间的系统调用而产生一种标准:POSIX: “可移植操作系统接口” Portable Operation System Interface 。
用户程序通过”陷入指令“(trap)来发起系统调用,Linux 系统调用(SCI,system call interface)的实现机制实际上是一个多路汇聚以及分解的过程,该汇聚点就是 0x80 中断这个入口点(X86 系统结构)。也就是说,所有系统调用都从用户空间中汇聚到 0x80 中断点,同时保存具体的系统调用号。当 0x80 中断处理程序运行时,将根据系统调用号(posix标准)对不同的系统调用分别处理(调用不同的内核函数处理)。这里我们只实现一个系统调用号作为范例。
同样省略库函数编写:
src/syscalls.cpp
#include <syscalls.h>
using namespace myos;
using namespace myos::common;
using namespace myos::hardwarecommunication;
SyscallHandler::SyscallHandler(InterruptManager* interruptManager, uint8_t InterruptNumber)
: InterruptHandler(interruptManager, InterruptNumber + interruptManager->HardwareInterruptOffset()){}
//系统调用同样通过中断来实现
SyscallHandler::~SyscallHandler(){}
void printf(char*);
uint32_t SyscallHandler::HandleInterrupt(uint32_t esp)
{
CPUState* cpu = (CPUState*)esp;
switch(cpu->eax)
{//按照POSIX编写对应的系统调用
case 4://系统调用号
printf((char*)cpu->ebx);
break;
default:
break;
}
return esp;
}
2.系统调用功能测试
在interrupstubs.s以及有关中断的程序中定义新的中断0x80(代码略)。
kernel.cpp
... ...
void sysprintf(char* str)
{
asm("int $0x80" : : "a" (4), "b" (str));//0x80执行4号系统调用,传入str参数
}
void taskA()
{
printf("A");
while(true)
sysprintf("A");
}
void taskB()
{
sysprintf("B");
while(true)
printf("B");
}
... ...
TaskManager taskManager;
Task task1(&gdt, taskA);
Task task2(&gdt, taskB);
taskManager.AddTask(&task1);
taskManager.AddTask(&task2);
InterruptManager interrupts(0x20, &gdt, &taskManager);
SyscallHandler syscalls(&interrupts, 0x80);//激活系统调用
... ...
make run后可以看到成功实现交替输出AB,即成功调用0x80中断发起系统调用。
4.总结
至此已经完成了本课程的主体内容(暂时不打算写附录有关网络的部分),本课程作为一门“操作系统实验课”,包含OS,微机原理,和一点点编译原理的内容,但由于是实际操作,总体来看不够全面也不够深入。实际操作起来对初学者(本人)来说也不太友好。适合初学C++且有一定OS基础的同学学习,对于初步了解C++工程结构,面向对象编程等相关知识有一定帮助。
更多推荐
所有评论(0)