在30天自制操作系统上编写网卡驱动[6]:用缓冲环和中断来接收数据
控制网卡接收数据
我们已经成功的把一个DHCP discover数组发送出去了,并且通过抓包软件wireshark也抓取到了我们发送出去的DHCP discover数组。
那么运行在路由器上的DHCP服务就会给我们返回一个数组回来,这个数组是回应我们发出去的DHCP discover数组的,这个数组是DHCP offer类型的。
那么问题来了,如何操作网卡,网卡才会接收到这个DHCP offer类型的数组呢?
前面几篇是控制网卡发送数据的内容:
如何在自制操作系统写网卡驱动程序(1)
在自制操作系统上写网卡驱动(2): 网卡的I/O配置
在30天自制操作系统上编写网卡驱动「3」:读取网卡的MAC地址
在30天自制操作系统上编写网卡驱动[4]:寄存器控制网卡收发数据
在30天自制操作系统上编写网卡驱动[5]:寄存器控制网卡收发数据
今天开始,就可以接收数据了。
其实网卡芯片会自动地接收网线上的一切网络数据,然后存储在网卡的内存里。随后就乖乖等着我们去读取这些数据了。
所以,我们直接从网卡的内存空间里读取数据,就相当于从网线上读取数据了。
FIFO 与接收缓冲环
那么问题来了:网线上只要有数据,就会进入网卡的内存,如果时间拉长为无限长,那么我们将得到一个无限长的有序数组,但是网卡的内存却是有限的,是无法存储这个无限长的有序数组的,对于此种情况,该怎么办呢?
这种情况就类似于咱们的鼠标的中断信号,键盘的中断信号。
我们采用了FIFO的思路,即First in First Out先入先出思路,建立了类似如下的一个FIFO32结构体。
struct FIFO32 {
int *buf;
int curr, bnry,size;
};
void fifo32_init(struct FIFO32 *fifo, int size, int *buf, struct TASK *task);
int fifo32_put(struct FIFO32 *fifo, int data);
int fifo32_get(struct FIFO32 *fifo);
int fifo32_status(struct FIFO32 *fifo);
这个结构体中有一个数组buf,它是有限长的。每次有新数据要存入buf的时候,就存入buf[cu rr],
然后把curr++. 当curr到达buf的末端时,就令curr=0,这样,要有新数据存入buf时,就存入buf[0]。
我们用fifo32_put函数实现往buf里放入新数据:
int fifo32_put(struct FIFO32 *fifo, int data)
{
...
fifo->buf[fifo->curr] = data;
fifo->curr++;
if (fifo->curr == fifo->size) {
fifo->curr = 0;
}
...
return 0;
}
因为每次把buf存满的时候,就会从buf[0]位置重新存储,这样的循环存储,就不怕要存入的数据有无限长了。
那有同学问:按重新存储,就会把原来buf[0]的位置覆盖掉了呢?
对,会被覆盖掉。所以,在buf[0]被覆盖掉之前,我们得把这个数据读出去。如果还没来得及读取,要设置一个标志bnry,让fifo32_put函数知道,当前这个位置的数据还没有被读出去,你还不能覆盖。
如果这个bnry=0,也就是说,如果fifo32_put函数,发现bnry=0,就不能把数据放在0位置了,因为bnry=0,就意味着,0位置的数据还没有被读出来。
bnry就是一个存放数据的边界了。
当我们把buf[0]读出来后,把bnry+1.这样,bnry=1就是数据存放的边界了,buf[0]的位置就可以存放新的数据了。
按照新的思路,一旦curr的值,与bury的值相等,就不能再存储数据了,所以要把fifo32_put函数改为如下形式:
int fifo32_put(struct FIFO32 *fifo, int data)
{
...
if(fifo->curr != fifo->bnry){
fifo->buf[fifo->curr] = data;
fifo->curr++;
}
if (fifo->curr == fifo->size) {
fifo->curr = 0;
}
...
return 0;
}
同理,一旦bnry的值与curr相等,就不能再读出数据了,所以读出数据的函数可以这样写:
int fifo32_get(struct FIFO32 *fifo)
{
...
int data;
if(fifo->bnry != fifo->curr){
data=fifo->buf[fifo->bnry];
fifo->bnry++;
}
if (fifo->bnry == fifo->size) {
fifo->bnry = 0;
}
...
return data;
}
不过,推演一下,发现这里有个bug:
拿一个具体的例子:比如现在bnry=3:
此时按照fifo32_put函数的逻辑,当curr=3时,就无法再往buff[3]放入数据,如果能从buff[3]读出一个数据,才能继续放入。如果不把buff[3]读出,就无法往buff[3]写入.
而如果此时没有新的数据要写入,所以我们就一直执行读出函数fifo32_get,按照fifo32_get函数的逻辑,读出函数也会一直执行到当bnry=curr=3时停止,此时,就无法再从buff[3]读出数据。如果能从buff[3]写入一个数据,才能够继续读出。如果不写入buff[3],就无法读出buff[3].
而如果此时,有一个新的数据要往buff中写入,就会发现写不进去。因为fifo32_put函数中,需要从buff[3]把数据读出。而fifo32_get函数不能满足其需求,返回fifo32_get函数需要往buff[3]写入一个数据,才能往外读出数据。
也就是说,当curr=bnry=3时,即不能读出buff[3],也不能写入buff[3].
这是一个隐藏的bug,这种情况发生的时候也不会报错,我们从表面上看:好像没有数据从网线上写入网卡内存,也没有数据从网卡内存读出。但,真实情况是:fifo32在逻辑上锁死了,虽然我们再用fifo32_put函数往里写,但写不进去。虽然我们在用fifoe32_get函数往外读,也读不出来。
该怎么办呢?其中一个办法就是: 新数据要存入时,我们不再往buf[curr]存入数据,而是往buf[curr-1]存入数据,如下:
int fifo32_put(struct FIFO32 *fifo, int data)
{
...
if(fifo->curr != fifo->bnry){
fifo->buf[fifo->curr-1] = data;
fifo->curr++;
}
if (fifo->curr == fifo->size) {
fifo->curr = 0;
}
...
return 0;
}
这样改变之后,当curr=bury=3时,就不能再往curr-1=2存放数据了,如果能从buf中读出一个数据,使bnry+1=4,才能往curr-1=2放入数据。此时,希望从bnry=curr=3读出一个数据。
既然curr-1=2处此时没有存入数据,那么从buf中往外读出数据时,就要判断bnry 与2是否相等了,如果相等,就不能读出数据了,将fifo32_get改为:
int fifo32_get(struct FIFO32 *fifo)
{
...
int data;
if(fifo->bnry != (fifo->curr-1)){
data=fifo->buf[fifo->bnry];
fifo->bnry++;
}
if (fifo->bnry == fifo->size) {
fifo->bnry = 0;
}
...
return data;
}
这样改过之后,就只能读取到buff[1], 此时,如果curr-1=2位置写入一个数据,才能读取bnry=curr-1=2处的数据.此时,希望往curr-1=2处写入一个数。
更改后,当无法再写入时,需要从bnry=curr=3读出一个数据就可以继续写入了
当无法再读出时,需要往curr-1=2处写入一个数据,就可以继续读出了。
这就没有矛盾了
这就不会再出现需要往curr=3处写入,又需要从curr=3处读出的情况发生了。
这样边在curr-1位置处存储,边在bnry位置处读出数据的过程,在解决了把无限长的数据按顺序的存入有限的数组内,同时保证了按照顺序的读出。从数据的角度来看,先进入数组的数据,会先被读出,我们称这样的数组为FIFO数组。
我们也形象的把这种用循环的方式来存储数据的结构,叫做环结构,ring struct
如果每次写入/读出的数据不是一个int,而是256个bytes,即一Page数据,那么它就是我们8029网卡地接收缓冲环了
这个基本结构,就是网卡驱动的资料上常说的接收缓冲区环,发送缓冲区环。
其实,在操作系统的鼠标数据,键盘数据的接收上,我们已经使用了这种环结构,不过那是,我们称其为FIFO结构体,我们每次存入一个int.
那么,网卡就在硬件内部实现了这个ring 结构,然后给了我们两个寄存器:BNRY,CURR,分别来代表fifo结构中的curr和bnry。
也就是说,网卡内部内存对应buff, 网卡内部已经实现了fifo32_put函数,即网卡可以自动的把网线上的数据存入网卡的内存中。
那么我们编写网卡驱动的人,想要读取网卡内存中的数据,只用自己实现fifo32_get函数就行了。
所以,我们只用读取网卡的BNRY寄存器的值,与CURR寄存器的值做比较,只要BNRY的值与CURR-1不想等,就可以从内存的BNRY位置往外读数据。一旦BURY=CURR-1,就意味着已经没有可以读出的数据了。
由此,从网卡内存中读取数据的程序可以这样写:
网卡会自动把数据从网线中读入网卡内存中,我们只用在合适的时候,从网卡内存中读出数据即可
uchar recv[0xffff];
page_select(0);
bnry=io_in8(IOADDR+3);// 读取BNRY寄存器的值,放入bnry,即要读取的数据的地址
page_select(1);
curr=io_in8(IOADDR+7);// 读取CURR寄存器的值,即要写入的地址+1
page_select(0);
if(bnry+1!=curr){
//此时表示收到了数据包了
io_out8(IOADDR+9,bnry);// 把要读取的地址的高位放入位置寄存器
io_out8(IOADDR+8,0x00);
io_out8(IOADDR+0xb,0x00);// 把读取数据个数的高8位放入数量寄存器
io_out8(IOADDR+0xa,18);// 读取数据个数的低8位
io_in8(IOADDR+0x0a);//read,启动remote dma 读
for(i=0;i<18;i++)
recv[i]=io_in8(IOADDR+0x10)
}
在以上代码的第3行,我们读取了bnry寄存器的值,这个值是网卡接收完成的数据在网卡内存中的地址。
在以上代码的第5行,我读取了curr寄存器的值,网卡会把从网络上接收到的数据存放在curr-1处。
可以看到,只要bnry+1不等于curr,我们就可以读取数据。
8-15行就是读取数据的,这里用到了几个新的寄存器,我们还是结合8029的datasheet来看:
io_out8(IOADDR+9,bnry);// 把要读取的地址的高位放入位置寄存器
io_out8(IOADDR+8,0x00);
操作的是Page0的RSAR1,RSAR0两个寄存器。
这两个寄存器是Remote Start Address Registers, 从远端DMS的这个内存地址处开始,把数据从网卡读入到CPU的内存中。这里DMA是指从网卡内存到CPU内存的数据传输使用了DMA技术,Remote是指网卡到GPU的距离相对于网卡到网线的距离比较远。其实网卡从网线接收数据也是用了DMA技术,只不过从网线获取技术的操作我们不用管。
io_out8(IOADDR+0xb,0x00);// 把读取数据个数的高8位放入数量寄存器
io_out8(IOADDR+0xa,18);// 读取数据个数的低8位
这两个寄存器是RBCR1,0.即Remote Byte Count Register ,设置用DMA技术接收的字节数。
这4个寄存器,2个构成开始地址,两个构成字节数,定义了要从网卡内存的bnry*256位置,读入18个字节到CPU的内存recv数组中。
13,14,15行启动DMA传送,然后使用for循环从DMA的端口上读取了18个 bytes的数据。
这里问什么只读取18个数据呢?因为ethernet协议头是14个字节,网卡在ethernet协议头之前,又加了4个字节的数据,所以一共18个字节。
ehternet协议头的14个字节为:
即,12个字节的MAC地址,2个字节的协议号
网卡在ethernet协议头之前加的4个字节为:
这4个字节分别为:
Receive Status:网卡RSR寄存器的值,即这条数据再接收过程中是否有错误?
Nest Packet Pointer: 下一个数据包的地址
Receive Byte Count0,Receive Byte Count1,数据的长度。
图中的21,62,4d,1就是前4个字节的值:
21,即RSR=21=0010 0001B,即PRX=1,表示接收数据没有错误发生。PHY=1,表示我们收到的数据里含有MAC地址。
62表示如果下一个接收得到的数据会存储在网卡内存的0x6200处。
4d,1表示称0x014d,表示这条数据的总长度。
第5-第17个字节分别是:6位目的地网卡的mac地址:0,1c,42,4f,d2,1f,6位发送这条数据的网卡的mac地址:0,1c,42,0,0,18,以及ether type : 8,0,即0x0800,表示后续跟的数据是按照IP协议整理出来的IP 头。
综合来收,当我们接收一条数据,它的前18位基本上告诉我们:这条数据是不是正常接收? 这条数据是否完整,如果不完整,那么它的下一页数据的地址在哪里? 以及这条数据所含有的字节数是多少? 这条数据的目的地网卡的MAC地址,发送这条数据的网卡的MAC地址,最后,可以通过ether type知道以太网头后面的数据是按照那种协议编写的?如果这个ether type=0x0800的话,以太网头后面的数据是按照IP协议整理的,说明从第19位开始的数据是IP头,如果ether type=0x0806,说明以太网头后面的数据是按照ARP协议整理的,从第19位开始的数据就是ARP头。
至此,我们接收到了数据,并且通过读取数据的前18个字节,来获取到非常多的信息。
通过对比目的网卡地址与我们自己的网卡地址,就知道这条数据是不是发送给我们的。
通过查看ether type,就知道从第19个字节开始的数据,所遵循的协议是哪个协议。
比如如果是DHCP数组,那以太网头后面应该是IP头,IP头后应该是UDP头,UDP头后就是DHCP消息。
所以,如果我们想接到DHCP offer数组,这里就可以只分析ether type=0x0800时的数据,因为enter type为其他值时,肯定不是DHCP offer数组。
我们现在收到了一条数据,并且也明白了这条数据的基本意义,是不是可以去接收18个字节之后的数据,然后进行更多地解析了呢?
当我们发出DHCP discover信息之后,在路由器上的DHCP服务器有没有给我们回复一个DHCP offer 信息呢?
可以的。
只要我们写一个for循环,在操作系统的无限循环里,去无限循环的调用我们的接收数据的代码即可。
虽然这样也可以,不过这显然是要把一大堆任务塞入for循环里。因为接收到数据后,通常还要对数据解析,解析之后要继续构造合适的数据包再发送出去。所以,把这个复杂的任务放在for循环里,显然是不合适的。
所以,我们使用中断来获取接收到了新数据的信号,而不再用for循环去无限查询。
使用中断机制来完成数据的接收
要想使用网卡的中断机制,还得去看PCI配置中,关于中断的配置信息:
上图的右上角就是PCI配置信息了,其中第15行,即0x0f行的0x0107就配置了中断号。
这里这个07就是interrupt line:当网卡接收到数据后,可以给CPU发送一个中断,这个中断号为0x20+interrupt line=0x20+0x07=0x27。
我们可以像接收鼠标,键盘的中断一样,去接收完卡的这个中断号为0x27的中断。
这个interrpt line 值是BIOS帮我们配置好的,我们直接使用就行。
不过,在8029寄存器中,是可以设置IMR寄存器,来屏蔽掉这个中断的。当然,我们这里使用中断,就要不能屏蔽。
那么8029中,与中断有关的寄存器有2个:IMR,ISR。ISR,即interrupt status register ,中断状态寄存器,表明因为什么原因发生的中断,比如发送完成可以产生中断,接收完成可以产生中断,接收缓冲环满,然生中断等。
通过查询8029的datasheet,我们发现,当CPU接收到一个0x27号中断时,可能是因为如上图所示的8个原因之一。具体那种原因造成的中断,只用查看ISR寄存器的值就行了。
IMR寄存器即interrupt mask register ,如果不希望发生中断,可以将其屏蔽掉。
IMR寄存器专门用来屏蔽8种中断中的每一种。比如设置IMR=0x0F,即允许PTR,RTX,PXE,TXE4种原因造成的中断,不允许OVW,CNT,RDC,RST造成的中断。IMR的第几位设置为1,就表示允许ISR的第几位所对应的中断发生。
所以,在代码上,我们要首先设置8029寄存器中的IMR寄存器,然后模仿鼠标中断,给操作系统添加一个新的中断函数。
首先在8029的初始化函数中:
void netcard_init()
{
...
page_select(0);
io_out8(IOADDR+0xf,0x3);// IMR:enable PTX,PRX两个中断
...
return;
}
然后就是给操作系统新添加一个专门处理向量号为0x27的中断函数。
首先IDT的初始化函数里,把0x27号向量对应的中断函数asm_inthandler27,写入IDT中,以下代码第7行。
IDT即Interrupt descriptor table,中断函数描述表,这个表里记录了CPU收到中断后,应该去执行的函数,比如收到0x0d号函数,去执行asm_inthandler0d函数。收到0x27号中断,就是执行asm_inthandler27函数。
void init_gdtidt(void)
{
...
set_gatedesc(idt + 0x0d, (int) asm_inthandler0d, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x20, (int) asm_inthandler20, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x27, (int) asm_inthandler27, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x2b, (int) asm_inthandler2b, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x40, (int) asm_hrb_api, 2 * 8, AR_INTGATE32 + 0x60);
return;
}
接着去完成asm_inthandler27函数:中断函数都要用汇编写,如果有复杂的功能需要实现,可以把复杂的部分写到一个函数里,然后再用汇编中去调用:
_asm_inthandler27:
PUSH ES
PUSH DS
PUSHAD
MOV EAX,ESP
PUSH EAX
MOV AX,SS
MOV DS,AX
MOV ES,AX
CALL _inthandler27
POP EAX
POPAD
POP DS
POP ES
IRETD
这里,我们CALL了一个函数,inthandler27, 在这个函数里,我们写了一些不方便再汇编里的功能:
struct FIFO32 *netfifo;
void inthandler27(int *esp)
{
io_out8(PIC0_OCW2, 0x67);
fifo32_put(netfifo, io_in8(IOADDR+0x07));
//一定要选择页,不然不能有效清除中断状态,芯片会一直报中断,造成非常多的中断涌入
page_select(0);
io_out8(IOADDR+0x07,0xFF);
return;
}
可以看到,在这个函数里,我们首先将中断已响应的消息通过端口PIC0_OCW2发送给中断控制器,然后就ISR寄存器的值放入了netfifo.
这里的netfifo是一个我们自己实现的先入先出数组,专门用来存储每次中断发生时,ISR寄存器的值。然后在操作系统的for循环里,我们就可以将从netfifo里读出ISR寄存器的值,确定是否时接收中断,如果是接收中断,就调用我们上述的接收代码。
这样,我们就完成了利用中断来知道已经接收到数据了,不用再去不停的查询了:
//操作系统的for循环
for(;;){
...
int fifo_length=fifo32_status(netfifo);
if(fifo_length>0){//判断netfifo里是否存储了ISR的值,如果有
io_out8(IOADDR+0x07,0xFF);
int ISR = fifo32_get(&fifo_net);//获取ISR的值
//bit-0 PRX:recive success
if((ISR&0x01)==0x01){//是否时接收成功的中断
unsigned char bnry,curr;
short length=0;
page_select(0);
bnry=io_in8(IOADDR+3);// 读取BNRY寄存器
page_select(1);
curr=io_in8(IOADDR+7);// 读取CURR寄存器
// 如果此时bnry+1!=curr/即还有数据没有从环中读取出来
if(bnry+1!=curr){
// 先读18个字节,看数据是否是我们要的0x0800类型的数据
int j=ReadFromDma(bnry<<8,18,recv);
// 利用结构体的protocal字段去取出enther type ,
if(recv_frame->header.protocal==hxl(0x0800)){
sprintf(s2,"ether type:%s",number2etherprototal(recv_frame->header.protocal));
strshow(s2);
}
}
}
...
}
}
第20行判断了ether type 的类型是不是0x0800,如果是,就表示是IP协议,可能是我们要接收的DHCP offer消息。
hxl函数将short类型数据的高8位和低8位进行了交换,因为数据传输时,顺序颠倒了,需要调换回来。
这里的ReadFromDma函数是从网卡内存的start_addr位置读取count个字节到CPU内存中的数组str里
// 使用remote dma技术从网卡的存储区读入若干个字符
int ReadFromDma(int start_addr,int count, unsigned char *str){
int i;
page_select(0);
WriteToNet(0x09,(start_addr&0x0000ff00)>>8);
WriteToNet(0x08,start_addr&0x000000ff);
WriteToNet(0x0b,(count&0x0000ff00)>>8); //DMA 写高字节
WriteToNet(0x0a,count&0x000000ff); //DMA写低字节
WriteToNet(0,0x0a); //开始读
for(i=0;i<count;i++)
str[i]=(ReadFromNet(0x10)); // 从dma读取数据出来
return i;
}
这样,我们就完成了对接收到的数据的初步解析。
至此,我们完成了使用中断机制去接收数据,并且对数据进行了初步的解析,过滤出了IP协议封装的数据。
我们可以再下一篇文章继续解析,看看路由器上的DHCP 服务器 有没有给我们发送一个DHCP offer数据。
开发后记
使用网卡接收数据还是比较繁琐的。
比使用网卡发送数据繁琐。
碰到的第一个难点就是:对接收缓冲环的理解
因为首先就是要理解CURR寄存器和BNRY寄存器的功能,理解接收缓冲环的机制,然后才能把CURR寄存器和BNRY寄存器正确的使用起来
接着之前的FIFO的基础,花了些时间,最终理解了CURR寄存器和BNRY寄存器的作用,完成了接收程序。
第二个难点就是:把中断机制用起来。
中断机制涉及到多个设备:PCI的配置信息,对网卡的ISR,IMR寄存器的设置,以及CPU的中断函数编写,还有把中断函数注册到IDT中。
如果有一个设备理解的不对,就无法产生中断。
在一个难点就是:虚拟网卡本身对中断的不支持。我花费了一周时间,才慢慢明白过来,可能qemu虚拟机中的ne2k网卡,对中断不支持。后来再paralles虚拟机中找到了对中断支持的8029AS 网卡,才顺利完成了实验。
请先 后发表评论~