v2.0.0版本的项目地址:
github.com地址:
https://github.com/zenglong/zenglOX (只包含git提交上来的源代码)
Dropbox地址:
点此进入Dropbox网盘 该版本位于zenglOX_v2.0.0的文件夹中,文件夹里的zip压缩包为源代码,readme.txt为版本的简单说明,pdf电子档为intel的e1000系列网卡的开发手册。
Google Drive地址:
点此进入Google Drive云端硬盘 对应也是zenglOX_v2.0.0的文件夹。
sourceforge地址:
https://sourceforge.net/projects/zenglox/files 对应也是zenglOX_v2.0.0的文件夹。
Dropbox与Google Drive如果正常途径访问不了,则需要使用代理访问。另外,Dropbox目前需要登录才能下载到数据。
和zenglOX编译调试相关的内容,请参考v0.0.1版本对应的文章,另外,当前版本要想在bochs下正常运行,还必须对bochs进行重新编译,下面会进行详细说明。
由于本章内容较多,因此,下面将分为三节来进行介绍,第一节(或者说第一页)介绍bochs编译运行以及 PCI 驱动相关的内容。第二节介绍e1000网卡驱动相关的内容。第三节介绍网络模块,新增的各种网络工具,以及如何在VirtualBox与VMware下设置和使用e1000网卡等内容。
与bochs编译运行相关的内容介绍:
由于默认情况下,bochs并没有安装e1000相关的插件,因此,使用常规的make和make iso命令后,再运行startBochs脚本时,就会提示 plugin 'e1000' not found 的错误。要让bochs安装e1000插件,需要重新编译bochs ,步骤如下(下面的bochs-2.6.tar.gz文件可以从v0.0.1版本对应的网盘中下载到,编译安装过程与v0.0.1版本相关文章里的过程差不多,只是configure多了两个参数而已):
root@zengl:/mnt/zenglOX# tar xvf bochs-2.6.tar.gz
root@zengl:/mnt/zenglOX# mkdir bochs-build
root@zengl:/mnt/zenglOX# cd bochs-build
root@zengl:/mnt/zenglOX/bochs-build# ../bochs-2.6/configure --enable-gdb-stub --enable-pci --enable-e1000
root@zengl:/mnt/zenglOX/bochs-build# make
root@zengl:/mnt/zenglOX/bochs-build# make install
|
以上都是需要root权限的,可以看到configure在配置时,比原来多了
--enable-pci和
--enable-e1000的参数,其中
--enable-pci参数表示让bochs支持pci总线,要使用e1000系列的网卡,就必须首先开启PCI的支持,另一个
--enable-e1000参数表示让bochs可以使用e1000的插件来模拟e1000系列的网卡,目前bochs的e1000只模拟了intel 82540EM的网卡。
bochs重新编译安装好后,要在模拟器里使用e1000网卡,还必须在bochsrc.txt文件里进行配置。默认情况下,作者已经在当前版本的bochsrc.txt里添加了如下一行配置信息:
e1000: enabled=1, mac=52:54:00:12:34:56, ethmod=linux, ethdev=eth0 |
上面一条配置表示在模拟器里开启e1000网卡,其中ethmod=linux, ethdev=eth0这条配置表示直接使用linux上的eth0网络接口来收发模拟器里的网络数据包。因此,你必须确保linux主机上有eth0接口,可以在linux命令行下通过ifconfig -a命令来查看,如果你的主机上的网络接口名为eth2的话, 就必须将上面的ethdev=eth0修改为ethdev=eth2,否则startBochs脚本运行时就会弹出类似如下的错误提示:
eth_linux: could not get index for interface 'eth0' |
最后一点需要注意的是,必须使用root权限来运行startBochs脚本,因为上面提到的ethmod=linux配置表示要使用linux的内置网络模块来收发数据,也就是要用到linux内核驱动层面的东东,因此,需要root权限来运行,在Ubuntu里可以使用
sudo ./startBochs命令来运行。
在
http://bochs.sourceforge.net/doc/docbook/user/bochsrc.html 该链接的4-5表格里,可以看到上面e1000配置中,ethmod可以使用的模块名(下表中ethdev列表示对应模块是否需要ethdev参数,script列表示对应模块是否需要script参数,Bochs version列表示对应模块是从哪个版本开始,被加入进来的):
Module
模块名 |
Description
模块描述 |
ethdev |
script |
Bochs
version |
fbsd |
FreeBSD / OpenBSD packetmover.
使用FreeBSD或OpenBSD系统的网络模块 |
Yes |
No |
1.0 |
linux |
Linux packetmover - 'root' privileges required, no connection to the host machine.
使用Linux系统的网络模块(直接使用Linux的eth0之类的网络接口来收发数据包),需要"root"权限,模拟器内的虚拟机不能和主机进行通信,但是可以和主机一样与外部进行通信 |
Yes |
No |
1.3 |
null |
Null packetmover. All packets are discarded, but logged to a few files.
null模块,所有的数据包都会被丢弃,但是数据包信息会被记录到文件中,该接口无法访问到实际的物理网络。 |
No |
No |
1.0 |
tap |
TAP packetmover.
使用tap即虚拟的以太网设备来收发数据包。 |
Yes |
Yes |
1.4 |
tuntap |
TUN/TAP packetmover - see Configuring and using a tuntap network interface.
通过tuntap网络接口来收发数据包,该接口的好处在于,可以让虚拟机与主机进行通信,如果主机对虚拟机传过来的数据进行route(路由转发)的话,那么虚拟机还可以访问到外部网络。不过要使用该接口,首先要在Linux内核里开启tuntap接口。 |
Yes |
Yes |
2.0 |
vde |
Virtual Distributed Ethernet packetmover.
使用VDE模块来收发数据包,VDE即虚拟的以太网,在这个虚拟的以太网里,有虚拟的交换机,虚拟的电缆等,构成一个虚拟的以太网络,多个虚拟机可以接入这个网络,适合进行一些网络试验,(不过vde模块无法访问到实际的物理网络),vde的详情请参考:
http://en.wikipedia.org/wiki/Virtual_Distributed_Ethernet
以及
http://wiki.virtualsquare.org/wiki/index.php/VDE_Basic_Networking |
Yes |
Yes |
2.2 |
vnet |
ARP, ping (ICMP-echo), DHCP and read/write TFTP simulation. The virtual host uses 192.168.10.1. DHCP assigns 192.168.10.2 to the guest. The TFTP server uses the 'ethdev' value for the root directory and doesn't overwrite files.
模拟器虚拟的网络,在该虚拟网络里可以测试arp协议,ICMP协议,及DHCP协议等,vnet模块无法访问到实际的物理网络。 |
Yes, for TFTP |
No |
2.2 |
slirp |
Built-in Slirp support with DHCP / TFTP servers. Adds user mode networking to Bochs - see Using the 'slirp' networking module. The 'script' parameter can be used to set up an alternative IP configuration or additional features. The TFTP server uses the 'ethdev' value for the root directory and doesn't overwrite files.
通过slirp网络模块来收发数据包,要使用slirp,还需要在系统里安装slirp程式。 |
Yes, for TFTP |
Yes, for Slirp config |
2.6.5 |
win32 |
Win32 packetmover - WinPCap driver required.
通过windows系统的网络接口来收发数据包,需要安装WinPCap驱动。 |
Yes |
No |
1.3 |
zenglOX当前版本的bochsrc.txt的配置文件里,ethmod使用的是linux模块,而bochs对于e1000网卡的官方标准配置,使用的是slirp模块,不过如果你想使用该模块的话,就必须手动再安装一个slirp程式。作者的slackware系统不是很方便安装这个程式,因此就采用了linux模块, 该linux模块是从bochs 1.3的版本开始就已经支持了的,而官方配置里的slirp模块则是从bochs 2.6.5版本才加入进来的。(qemu模拟器默认集成了slirp程式, 而bochs则没有)
在使用startBochs脚本运行模拟器后, 如果一切正常的话, 在zenglOX启动时就会检测到PCI信息以及e1000系列的网卡信息, 比如网卡的mac地址等。
PCI驱动讲解:
之所以先从PCI驱动开始说起,是因为e1000系列的网卡是连接在PCI总线上的设备,只有先将PCI总线上的设备信息检测出来,才能再用这些信息对网卡进行初始化操作。
在
http://wiki.osdev.org/PCI 该链接对应的文章里,已经对PCI的结构及编程方法都进行了详细的介绍,下面作者将结合zenglOX的源代码对PCI进行讲解。
PCI总线被设计为建立一个高性能,低消耗的本地总线,在PCI标准的升级过程中,信号传输速率由132MB/s(33MHz * 32bit / 8 = 132MB/s)提升到了528MB/s(66MHz * 64bit / 8 = 528MB/s),该总线具有5V和3.3V两种信号环境,既可以用于低端的桌面PC系统,又可以用于高端的服务器产品。PCI总线的相关组件及其附加的设备接口都是与处理器无关的,因此可以很容易的过渡到未来的处理器产品,还可以用于多处理器架构。PCI总线的不足,在于它可以驱动的电子负载的数目是有限的,单个PCI总线最多只能驱动10个负载。
PCI总线为总线上的每个设备都提供了一个256字节的Configuration Space(配置空间),通过读取配置空间里的信息,就可以利用这些信息来对设备进行初始化操作。另外,向配置空间里的某些位置写入数据,还可以对设备进行相关配置。
Configuration Space(配置空间)的结构如下图所示:
图1
上图所显示的是当Header type字段的值为0时的结构,当Header type字段的值为0x01或0x02时,结构会有所不同,由于我们只需要图1的结构,因此,这里就不显示另外两种结构了,这三种结构中,前16个字节都是通用的,每个字段的具体含义与其他两种配置结构,请参考
http://wiki.osdev.org/PCI 链接对应的文章。
根据上面的图1,我们就在zlox_pci.h头文件里定义出了ZLOX_PCI_CONF_HDR结构:
struct _ZLOX_PCI_CONF_HDR {
ZLOX_UINT16 vend_id;
ZLOX_UINT16 dev_id;
ZLOX_UINT16 command;
ZLOX_UINT16 status;
ZLOX_UINT8 rev;
ZLOX_UINT8 prog_if;
ZLOX_UINT8 sub_class;
ZLOX_UINT8 class_code;
ZLOX_UINT8 cache_line_size;
ZLOX_UINT8 latency_timer;
ZLOX_UINT8 header_type;
ZLOX_UINT8 bist;
ZLOX_UINT32 bars[6];
ZLOX_UINT32 reserved[2];
ZLOX_UINT32 romaddr;
ZLOX_UINT32 reserved2[2];
ZLOX_UINT8 int_line;
ZLOX_UINT8 int_pin;
ZLOX_UINT8 min_grant;
ZLOX_UINT8 max_latency;
ZLOX_UINT8 data[192];
} __attribute__((packed));
typedef struct _ZLOX_PCI_CONF_HDR ZLOX_PCI_CONF_HDR;
|
我们可以通过ZLOX_PCI_CONF_HDR结构中的vend_id与dev_id字段,来检测出PCI设备的类型,后面的e1000驱动里就将用到这两个字段来检测网卡的类型。
那么该如何将这些配置信息给读取出来呢,PCI提供了两个32位的I/O端口,一个是值为0xCF8的被称作CONFIG_ADDRESS的配置地址端口,另一个是值为0xCFC的被称作CONFIG_DATA的配置数据端口。因此,在zlox_pci.c文件的开头就有了这两个端口号的宏定义:
// zlox_pci.c 和PCI相关的函数定义
#include "zlox_pci.h"
#include "zlox_monitor.h"
#include "zlox_kheap.h"
#define ZLOX_PCI_ADDRESS 0xCF8
#define ZLOX_PCI_DATA 0xCFC
......................................
|
向CONFIG_ADDRESS端口写入的地址数据,必须符合如下所示的32位结构:
图2
上图显示的结构中,位0与位1必须是0,位2到位7用于存储寄存器号,在上面
图1所示的结构里,最左侧的
register列就是对应的寄存器号,通过寄存器号,就可以将所需寄存器中的4个字节的数据给读取出来。上图中Bus Number字段表示需要访问的PCI总线号,Device Number字段表示该总线上的设备号,Function Number字段表示设备里的功能模块号(某些PCI设备具有功能模块,一个功能模块可以看成一个设备,具有自己独立的PCI配置空间)。最高位Enable Bit用于表示对CONFIG_DATA数据端口的读写操作是否指向配置空间,因此,当访问PCI设备的配置空间时,需要将该位设置为1 。
根据上面的
图2,就有了zlox_pci.h头文件中的ZLOX_UNI_PCI_CFG_ADDR结构:
union _ZLOX_UNI_PCI_CFG_ADDR {
struct{
ZLOX_UINT32 type:2;
ZLOX_UINT32 reg:6;
ZLOX_UINT32 function:3;
ZLOX_UINT32 device:5;
ZLOX_UINT32 bus:8;
ZLOX_UINT32 reserved:7;
ZLOX_UINT32 enable:1;
};
ZLOX_UINT32 val;
} __attribute__((packed));
typedef union _ZLOX_UNI_PCI_CFG_ADDR ZLOX_UNI_PCI_CFG_ADDR;
|
由于上面是个union联合体,这样当向val成员写入0时,就可以一次将所有的字段都清为0 ,上面的type字段对应为
图2中的位0到位1的部分,reg字段对应
图2里的Register Number部分,function字段对应图中的Function Number部分,device字段对应图中的Device Number部分,bus对应图中的Bus Number部分,enable字段对应图中的Enable Bit。
zlox_pci.c文件中的zlox_pci_device_read_config函数,就是根据ZLOX_UNI_PCI_CFG_ADDR结构,再加上CONFIG_ADDRESS与CONFIG_DATA端口来循环读取出设备的配置空间里的数据的:
ZLOX_UNI_PCI_CFG_ADDR zlox_pci_device_read_config(ZLOX_PCI_CONF_HDR * hdr, ZLOX_UINT32 bus,
ZLOX_UINT32 device, ZLOX_UINT32 function)
{
ZLOX_UNI_PCI_CFG_ADDR saddr;
saddr.val = 0;
saddr.bus = bus;
saddr.device = device;
saddr.function = function;
saddr.enable = 1;
// if you set saddr.reg < 0x40 then you will enter Infinite loop
// Because the reg only has six bit!
for(saddr.reg = 0; saddr.reg < 0x3f; saddr.reg++)
{
zlox_outl(ZLOX_PCI_ADDRESS, saddr.val);
((ZLOX_UINT32 *)hdr)[saddr.reg] = zlox_inl(ZLOX_PCI_DATA);
}
saddr.reg = 0;
return saddr;
}
|
上面函数在设置好saddr地址数据里的各个字段后,就可以将该地址数据通过zlox_outl函数,写入到ZLOX_PCI_ADDRESS即0xCF8的地址端口里,接着,再通过zlox_inl函数就可以从ZLOX_PCI_DATA即0xCFC的数据端口中读取出配置空间里的数据了。不过这里需要注意的是,上面的for循环里不能使用 saddr.reg < 0x40 作为条件判断语句,因为当saddr.reg的值达到0x3f后,再进行加加运算时,saddr.reg的值就会溢出变为0 (因为reg字段只有6位),也就会陷入无限循环。
有了上面的zlox_pci_device_read_config函数,就可以循环将PCI的各个总线上的所有设备的配置空间数据都给读出来了:
// zlox_pci.c 和PCI相关的函数定义
.................................................
ZLOX_PCI_DEVCONF_LST pci_devconf_lst = {0};
.................................................
ZLOX_VOID zlox_pci_bus_scan(ZLOX_UINT32 bus)
{
ZLOX_UNI_PCI_CFG_ADDR ret_addr;
ZLOX_PCI_CONF_HDR * hdr = (ZLOX_PCI_CONF_HDR *)zlox_kmalloc(sizeof(ZLOX_PCI_CONF_HDR));
for(ZLOX_UINT32 i = 0; i < 0x20; i++)
{
ret_addr = zlox_pci_device_read_config(hdr, bus, i, 0);
// 如果读出来的vend_id等于0xFFFF
// 则表示对应的设备不存在,
// 就continue跳到下一个设备
if(hdr->vend_id == 0xFFFF)
{
continue;
}
// 如果设备存在,则将读取出来的hdr即配置空间数据
// 通过zlox_pci_devconf_lst_add函数
// 添加到pci_devconf_lst的动态数组中
zlox_pci_devconf_lst_add(ret_addr, hdr);
// 如果配置空间的header_type字段的最高位
// 为1,则表示该设备存在功能模块,
// 则循环将设备的功能模块的配置空间数据
// 也读取并添加到pci_devconf_lst的
// 动态数组中
if((hdr->header_type & 0x80) != 0)
{
for(ZLOX_UINT32 j = 1; j < 8; j++)
{
ret_addr = zlox_pci_device_read_config(hdr, bus, i, j);
if(hdr->vend_id == 0xFFFF)
continue;
zlox_pci_devconf_lst_add(ret_addr, hdr);
}
}
}
zlox_kfree(hdr);
}
.................................................
ZLOX_VOID zlox_pci_init()
{
ZLOX_UNI_PCI_CFG_ADDR saddr, saddr2;
saddr.val = 0;
saddr.enable = 1;
// saddr地址数据的bus字段有8位
// 因此,可以对0到254的bus执行
// zlox_pci_bus_scan函数来扫描出
// 对应总线上的所有的设备信息。
// 这里不可以使用 i < 0x100 的判断语句
// 因为当bus为0xFF即255时,再进行加加
// 运算就会溢出变为0,而陷入死循环
for(ZLOX_UINT32 i = 0; i < 0xFF; i++)
{
saddr.bus = i;
zlox_outl(ZLOX_PCI_ADDRESS, saddr.val);
saddr2.val = zlox_inl(ZLOX_PCI_DATA);
if(saddr2.val == 0xFFFFFFFF)
continue;
zlox_pci_bus_scan(i);
}
}
.................................................
|
上面棕色的注释是此处额外添加的,在源文件里暂时没有。
以上就是和PCI总线相关的内容介绍及其相关的代码,zlox_pci.c即PCI的驱动程式里,还有些如zlox_pci_get_bar之类的函数,这些函数都是要在设备具体的初始化过程中才会用到的,例如,下面会介绍的E1000系列网卡的初始化部分,因此,这里就先不去讲解了。
Intel的E1000系列网卡驱动:
[zengl pagebreak]
Intel的E1000系列网卡驱动:
下面先看下如何从PCI总线中检测出e1000网卡的相关设备信息。
前面的PCI驱动部分,已经将PCI总线上所有设备的配置空间里的数据都添加到了pci_devconf_lst的动态数组中了,在zlox_pci.c文件里,有一个zlox_pci_get_devconf函数,该函数可以从pci_devconf_lst动态数组中检测出设备的类型,同时返回一个和设备相关的数据结构:
ZLOX_PCI_DEVCONF * zlox_pci_get_devconf(ZLOX_UINT16 vend_id, ZLOX_UINT16 dev_id)
{
for(ZLOX_UINT32 i = 0; i < pci_devconf_lst.count ;i++)
{
if(pci_devconf_lst.ptr[i].cfg_hdr.vend_id == vend_id &&
pci_devconf_lst.ptr[i].cfg_hdr.dev_id == dev_id)
return &pci_devconf_lst.ptr[i];
}
return ZLOX_NULL;
}
|
每个PCI设备都有一个vend id(用于表示生产厂家)和一个dev id(即厂家分配的设备ID),因此我们只要将需要检测的e1000网卡的vend id与dev id提供给该函数,该函数就可以从pci_devconf_lst动态数组里通过循环比较id值来进行查找,如果没找到符合条件的id值,则返回空指针,找到了则返回ZLOX_PCI_DEVCONF结构的指针,该结构定义在zlox_pci.h的头文件里:
typedef struct _ZLOX_PCI_DEVCONF {
ZLOX_UNI_PCI_CFG_ADDR cfg_addr;
ZLOX_PCI_CONF_HDR cfg_hdr;
}ZLOX_PCI_DEVCONF;
|
该结构体中的ZLOX_UNI_PCI_CFG_ADDR结构是设备对应的总线地址数据,ZLOX_PCI_CONF_HDR结构则是设备对应的配置空间数据,这两种结构在前面PCI驱动部分已经讲解过了。
zlox_e1000.c文件的zlox_e1000_init初始化函数部分,就是通过上面的zlox_pci_get_devconf函数来检测出网卡设备的:
// zlox_e1000.c -- 和Intel Pro/1000 (e1000)网卡驱动相关的函数定义
.................................................
#define ZLOX_E1000_VEND_ID 0x8086
#define ZLOX_E1000_82540EM_A 0x100E
#define ZLOX_E1000_82545EM_A 0x100F
#define ZLOX_E1000_82543GC_COPPER 0x1004
#define ZLOX_E1000_82541GI_LF 0x107C
#define ZLOX_E1000_ICH10_R_BM_LF 0x10CD
#define ZLOX_E1000_82574L 0x10D3
#define ZLOX_E1000_ICH10_D_BM_LM 0x10DE
#define ZLOX_E1000_82571EB_COPPER 0x105E
.................................................
ZLOX_VOID zlox_e1000_init()
{
if(e1000_dev.isInit == ZLOX_TRUE)
return ;
// 82540EM can be used in bochs and VirtualBox
e1000_dev.pci_devconf = zlox_pci_get_devconf(ZLOX_E1000_VEND_ID, ZLOX_E1000_82540EM_A);
// 82545EM can be used in VirtualBox and Vmware
if(e1000_dev.pci_devconf == ZLOX_NULL)
e1000_dev.pci_devconf = zlox_pci_get_devconf(ZLOX_E1000_VEND_ID, ZLOX_E1000_82545EM_A);
// 82543GC can be used in VirtualBox
if(e1000_dev.pci_devconf == ZLOX_NULL)
e1000_dev.pci_devconf = zlox_pci_get_devconf(ZLOX_E1000_VEND_ID, ZLOX_E1000_82543GC_COPPER);
// the follow card is from Minix3's e1000.conf
if(e1000_dev.pci_devconf == ZLOX_NULL)
e1000_dev.pci_devconf = zlox_pci_get_devconf(ZLOX_E1000_VEND_ID, ZLOX_E1000_82541GI_LF);
if(e1000_dev.pci_devconf == ZLOX_NULL)
e1000_dev.pci_devconf = zlox_pci_get_devconf(ZLOX_E1000_VEND_ID, ZLOX_E1000_ICH10_R_BM_LF);
if(e1000_dev.pci_devconf == ZLOX_NULL)
e1000_dev.pci_devconf = zlox_pci_get_devconf(ZLOX_E1000_VEND_ID, ZLOX_E1000_82574L);
if(e1000_dev.pci_devconf == ZLOX_NULL)
e1000_dev.pci_devconf = zlox_pci_get_devconf(ZLOX_E1000_VEND_ID, ZLOX_E1000_ICH10_D_BM_LM);
if(e1000_dev.pci_devconf == ZLOX_NULL)
e1000_dev.pci_devconf = zlox_pci_get_devconf(ZLOX_E1000_VEND_ID, ZLOX_E1000_82571EB_COPPER);
if(e1000_dev.pci_devconf == ZLOX_NULL)
return ;
.................................................
}
|
上面的ZLOX_E1000_VEND_ID对应的宏值为0x8086,这是Intel厂商的vend id 。其余的,如ZLOX_E1000_82540EM_A对应的宏值为0x100E,就表示e1000系列的intel 82540EM网卡的设备id为0x100e ,这些id值可以在e1000网卡的开发手册的第108页找到(在当前版本对应的网盘里,有开发手册对应的pdf电子档,这里的页数是pdf电子档顶部,分页输入框中的页数):
图3(pdf的第108页)
上面只显示了一部分id值,其余的id值与对应的网卡类型请参考pdf对应的页面。
以上就是和网卡检测相关的内容,下面看下该如何访问e1000网卡内部的寄存器,有两种访问方式,第一种方式类似于PCI配置空间的端口访问方式,有一个“IOADDR”的地址端口,用于指定需要访问的寄存器的偏移值,还有一个"IODATA"的数据端口,用于向"IOADDR"指定的寄存器中读写数据。下面是
http://wiki.osdev.org/Intel_8254x 链接中,关于这种方式的演示代码:
out32(ioaddr + 0x00, reg); // set the IOADDR window
out32(ioaddr + 0x04, val); // write the value to the IOADDR window which will end up in the register in IOADDR
in32(ioaddr + 0x04); // read back the value
|
上面演示代码里的ioaddr是一个基址,ioaddr + 0x00对应的就是"IOADDR"的地址端口,ioaddr + 0x04对应的就是"IODATA"的数据端口,那么ioaddr是如何得来的呢,在前面
图1所示的PCI配置空间的结构图里,有BAR0,BAR1到BAR5一共6个寄存器,这些都是Base Address Registers(基址寄存器),当BAR寄存器的位0为1时,那么该寄存器里就存储了上面演示代码里的ioaddr的值,这种情况下,BAR的结构如下图所示:
图4
上图里的位2到位31是4字节对齐的ioaddr基地址。
如果要使用上面这种方式,则只需先从设备的配置空间里循环对6个BAR寄存器的位0进行检测,当某个BAR的位0为1时,就将该BAR的位2到位31的基地址读取出来,作为上面演示代码里的ioaddr即可,作者一开始是采用的这种方式,不过,这种方式只能在VirtualBox与VMware下运行,在bochs下,它的e1000插件暂时还不支持这种访问方式,因此,作者最终就没用这种访问方式。
其实,e1000网卡已经将内部寄存器都映射到了物理内存里了,因此,第二种访问方式,就是直接对这段物理内存进行读写操作即可,对这段物理内存的所有读写操作都会被自动映射到对应的内部寄存器中。下面是
http://wiki.osdev.org/Intel_8254x 链接中,关于这种方式的演示代码:
*(uint32_t *)(ioaddr + reg) = val; // writes "val" to an MMIO address
val = *(uint32_t *)(ioaddr + reg); // reads "val" from an MMIO address
|
上面演示代码里的ioaddr是内部寄存器被映射到物理内存中的基地址,代码中的reg为寄存器在物理内存中的偏移值,可以看到,直接按照常规的内存访问方式,就可以对e1000网卡的内部寄存器进行读写操作了。那么,上面演示代码中的ioaddr的值是从哪得来的呢,其实,该值也存储在配置空间的BAR寄存器里,当BAR寄存器的位0为0时,对应的结构如下图所示:
图5
上图中的位4到位31的部分就是16字节对齐的ioaddr(物理内存基地址),zenglOX采用的就是这种访问方式,该方式在各虚拟机下都测试通过。
不过,在保护模式下,我们无法直接访问这段物理内存,需要先将这段物理内存给映射到虚拟地址空间,目前,我们将其映射到了0xB0000000开始的虚拟地址空间,在zlox_e1000.h头文件里有该虚拟地址的宏定义:
#define ZLOX_E1000_MMIO_ADDR 0xB0000000 |
在zlox_e1000.c文件的zlox_e1000_init函数里,就有映射相关的代码:
ZLOX_VOID zlox_e1000_init()
{
.................................................
ZLOX_UINT32 phy_bar = (e1000_dev.pci_devconf->cfg_hdr.bars[0] & 0xFFFFFFF0);
e1000_dev.mmio_base = ZLOX_E1000_MMIO_ADDR + (phy_bar & 0x0FFFFFFF);
ZLOX_UINT32 mmio_mem_amount = zlox_pci_get_bar_mem_amount(e1000_dev.pci_devconf->cfg_addr, 0);
ZLOX_UINT32 i,j;
for (i = e1000_dev.mmio_base,j = phy_bar;
i < e1000_dev.mmio_base + 0x20000;
i += 0x1000, j += 0x1000)
zlox_alloc_frame_do_ext(zlox_get_page(i, 1, kernel_directory), j, 0, 0);
zlox_page_Flush_TLB();
.................................................
}
|
上面代码中的
zlox_alloc_frame_do_ext函数是zlox_paging.c文件里新增的函数,可以为某个页面的虚拟地址指定具体的物理内存地址,不过,该函数只能用于那些设备映射的物理内存,另外,在获取映射的物理内存基址时,上面函数中直接使用的是配置空间里的BAR0寄存器的值,这是因为,根据手册里的说明,e1000系列网卡的物理内存基地址就是存储在BAR0中的(也就无需对它的位0的值进行判断了)。
在将物理内存映射到虚拟地址空间后,就可以在保护模式下,直接对这段内存所对应的寄存器进行读写操作了,读写操作相关的代码位于zlox_e1000.c文件的zlox_e1000_reg_read与zlox_e1000_reg_write函数中:
ZLOX_UINT32 zlox_e1000_reg_read(ZLOX_UINT32 reg)
{
if(reg >= 0x1ffff)
return 0;
ZLOX_UINT32 value;
value = *(volatile ZLOX_UINT32 *)(e1000_dev.mmio_base + reg);
return value;
}
ZLOX_VOID zlox_e1000_reg_write(ZLOX_UINT32 reg, ZLOX_UINT32 value)
{
if(reg >= 0x1ffff)
return;
*(volatile ZLOX_UINT32 *)(e1000_dev.mmio_base + reg) = value;
}
|
上面介绍了读写e1000网卡的内部寄存器的两种方式,那么,e1000系列的网卡到底有哪些寄存器呢,在e1000开发手册的pdf电子档的第233页,有一个如下所示的表格:
图6(位于手册的第233页,上面只是表格内容的一小部分)
上图只是表格中的内容的一小部分,上图列举了寄存器的名称缩写,如CTRL是Device Control Register(设备控制寄存器)的名称缩写,还有寄存器的Offset(在映射的物理内存里的偏移值),根据这些偏移值和寄存器名,就有了zlox_e1000.c文件中,如下所示的宏定义:
// zlox_e1000.c -- 和Intel Pro/1000 (e1000)网卡驱动相关的函数定义
.................................................
#define ZLOX_E1000_REG_CTRL 0x0000
#define ZLOX_E1000_REG_EERD 0x0014
#define ZLOX_E1000_REG_ICR 0x00C0
/** Interrupt Mask Set/Read Register. */
#define ZLOX_E1000_REG_IMS 0x000d0
#define ZLOX_E1000_REG_RDBAL 0x2800
#define ZLOX_E1000_REG_RDBAH 0x2804
#define ZLOX_E1000_REG_RDLEN 0x2808
#define ZLOX_E1000_REG_RDH 0x2810
#define ZLOX_E1000_REG_RDT 0x2818
#define ZLOX_E1000_REG_RCTL 0x0100
#define ZLOX_E1000_REG_TCTL 0x0400
#define ZLOX_E1000_REG_TDBAL 0x3800
#define ZLOX_E1000_REG_TDBAH 0x3804
#define ZLOX_E1000_REG_TDLEN 0x3808
#define ZLOX_E1000_REG_TDH 0x3810
#define ZLOX_E1000_REG_TDT 0x3818
/** Receive Address Low. */
#define ZLOX_E1000_REG_RAL 0x05400
/** Receive Address High. */
#define ZLOX_E1000_REG_RAH 0x05404
/** Flow Control Address Low. */
#define ZLOX_E1000_REG_FCAL 0x00028
/** Flow Control Address High. */
#define ZLOX_E1000_REG_FCAH 0x0002c
/** Flow Control Type. */
#define ZLOX_E1000_REG_FCT 0x00030
/** Flow Control Transmit Timer Value. */
#define ZLOX_E1000_REG_FCTTV 0x00170
/** Multicast Table Array. */
#define ZLOX_E1000_REG_MTA 0x05200
/** CRC Error Count. */
#define ZLOX_E1000_REG_CRCERRS 0x04000
.................................................
|
以上就是e1000网卡的内部寄存器的相关内容,在e1000的初始化函数中,有如下一段代码:
ZLOX_VOID zlox_e1000_init()
{
.................................................
/* FIXME: enable DMA bus mastering if necessary. This is disabled by
* default on VMware. Eventually, the PCI driver should deal with this.
*/
ZLOX_UINT16 cr = zlox_pci_reg_inw(e1000_dev.pci_devconf->cfg_addr, ZLOX_PCI_REG_COMMAND);
if (!(cr & ZLOX_PCI_COMMAND_MAST_EN))
zlox_pci_reg_outw(e1000_dev.pci_devconf->cfg_addr, ZLOX_PCI_REG_COMMAND, cr | ZLOX_PCI_COMMAND_MAST_EN);
................................................
}
|
之所以有这段代码,是因为,默认情况下,VMware将PCI设备的DMA Bus Master功能给关闭掉了,在关闭的情况下,e1000网卡就无法使用DMA来传输物理内存里的数据了,要将Bus Master功能给打开,就需要对配置空间里的Command(命令寄存器)进行相应的设置,在之前
图1所示的PCI配置空间的结构里,可以看到一个Command(命令寄存器),该寄存器的结构如下图所示:
Bits 11 to 15 |
Bit 10 |
Bit 9 |
Bit 8 |
Bit 7 |
Bit 6 |
Bit 5 |
Bit 4 |
Bit 3 |
Bit 2 |
Bit 1 |
Bit 0 |
Reserved |
Interupt Disable |
Fast Back-to-Back Enable |
SERR# Enable |
Reserved |
Parity Error Response |
VGA Palette Snoop |
Memory Write and Invalidate Enable |
Special Cycles |
Bus Master |
Memory Space |
I/O Space |
上表里的位2就是Bus Master,因此只需将该位设置为1即可,上面代码中的
ZLOX_PCI_COMMAND_MAST_EN是定义在zlox_pci.h头文件中的宏,其值为0x0004(该值的位2为1),另外,上面代码中的zlox_pci_reg_inw与zlox_pci_reg_outw函数,都是定义在zlox_pci.c文件里的用于读写配置空间的寄存器的函数。上表中其他字段的含义请参考
http://wiki.osdev.org/PCI 该链接对应的文章。
在开启Bus Master功能后,接着就是注册e1000网卡的中断处理例程:
ZLOX_VOID zlox_e1000_init()
{
.................................................
zlox_register_interrupt_callback(ZLOX_IRQ0 + e1000_dev.pci_devconf->cfg_hdr.int_line,
&zlox_e1000_callback);
.................................................
}
|
上面的e1000_dev.pci_devconf->cfg_hdr.
int_line是配置空间中的Interrupt Line字段(可以参考前面的
图1),用于表示该PCI设备所对应的中断号。
注册完中断号后,就会进入zlox_e1000_start函数,e1000网卡的关键性的初始化代码,都位于这个函数以及该函数所调用的函数里:
ZLOX_VOID zlox_e1000_start()
{
ZLOX_SINT32 i;
/* Reset hardware. */
zlox_e1000_reset_hw();
/*
* Initialize appropriately, according to section 14.3 General Configuration
* of Intel's Gigabit Ethernet Controllers Software Developer's Manual.
*/
zlox_e1000_reg_set(ZLOX_E1000_REG_CTRL, ZLOX_E1000_REG_CTRL_ASDE | ZLOX_E1000_REG_CTRL_SLU);
zlox_e1000_reg_unset(ZLOX_E1000_REG_CTRL, ZLOX_E1000_REG_CTRL_LRST);
zlox_e1000_reg_unset(ZLOX_E1000_REG_CTRL, ZLOX_E1000_REG_CTRL_PHY_RST);
zlox_e1000_reg_unset(ZLOX_E1000_REG_CTRL, ZLOX_E1000_REG_CTRL_ILOS);
zlox_e1000_reg_write(ZLOX_E1000_REG_FCAL, 0);
zlox_e1000_reg_write(ZLOX_E1000_REG_FCAH, 0);
zlox_e1000_reg_write(ZLOX_E1000_REG_FCT, 0);
zlox_e1000_reg_write(ZLOX_E1000_REG_FCTTV, 0);
zlox_e1000_reg_unset(ZLOX_E1000_REG_CTRL, ZLOX_E1000_REG_CTRL_VME);
//have to clear out the multicast filter
for (i = 0; i < 128; i++)
{
zlox_e1000_reg_write(ZLOX_E1000_REG_MTA + i, 0);
}
/* Initialize statistics registers. */
for (i = 0; i < 64; i++)
{
zlox_e1000_reg_write(ZLOX_E1000_REG_CRCERRS + (i * 4), 0);
}
/*
* Aquire MAC address and setup RX/TX buffers.
*/
zlox_e1000_init_addr();
zlox_e1000_init_buf();
/* Enable interrupts. */
zlox_e1000_reg_set(ZLOX_E1000_REG_IMS, ZLOX_E1000_REG_IMS_LSC |
ZLOX_E1000_REG_IMS_RXO |
ZLOX_E1000_REG_IMS_RXT |
ZLOX_E1000_REG_IMS_TXQE |
ZLOX_E1000_REG_IMS_TXDW);
}
|
上面代码移植自Minix3里的e1000的驱动,这段驱动是按照e1000开发手册的第14.3节的内容来写的,这一节给出了通用的初始化过程的描述,上面初始化时涉及到的寄存器的含义请参考开发手册的第233页的寄存器部分。
上面的代码中,主要是需要注意zlox_e1000_init_addr与zlox_e1000_init_buf函数,其中的zlox_e1000_init_addr函数用于获取并显示出网卡的mac地址:
// zlox_e1000.c -- 和Intel Pro/1000 (e1000)网卡驱动相关的函数定义
.................................................
ZLOX_VOID zlox_e1000_eeprom_gettype()
{
ZLOX_UINT32 val = 0;
zlox_e1000_reg_write(ZLOX_E1000_REG_EERD, 0x1);
for(ZLOX_UINT32 i = 0; i < 1000; i++)
{
val = zlox_e1000_reg_read(ZLOX_E1000_REG_EERD);
if(val & 0x10)
e1000_dev.is_e = ZLOX_FALSE;
else
e1000_dev.is_e = ZLOX_TRUE;
}
}
ZLOX_VOID zlox_e1000_getmac()
{
ZLOX_UINT32 temp = 0;
temp = zlox_e1000_eeprom_read(0);
e1000_dev.mac[0] = temp & 0xff;
e1000_dev.mac[1] = temp >> 8;
temp = zlox_e1000_eeprom_read(1);
e1000_dev.mac[2] = temp & 0xff;
e1000_dev.mac[3] = temp >> 8;
temp = zlox_e1000_eeprom_read(2);
e1000_dev.mac[4] = temp & 0xff;
e1000_dev.mac[5] = temp >> 8;
}
.................................................
/*===========================================================================*
* zlox_e1000_init_addr *
*===========================================================================*/
ZLOX_VOID zlox_e1000_init_addr()
{
ZLOX_SINT32 i;
zlox_e1000_eeprom_gettype();
zlox_e1000_getmac();
/*
* Set Receive Address.
*/
zlox_e1000_reg_write(ZLOX_E1000_REG_RAL, *(ZLOX_UINT32 *)(&e1000_dev.mac[0]));
zlox_e1000_reg_write(ZLOX_E1000_REG_RAH, *(ZLOX_UINT16 *)(&e1000_dev.mac[4]));
zlox_e1000_reg_set(ZLOX_E1000_REG_RAH, ZLOX_E1000_REG_RAH_AV);
zlox_e1000_reg_set(ZLOX_E1000_REG_RCTL, ZLOX_E1000_REG_RCTL_MPE);
zlox_monitor_write("e1000 mac address: ");
for(i=0;i < 6;i++)
{
zlox_monitor_write_hex(e1000_dev.mac[i]);
zlox_monitor_write(" ");
}
zlox_monitor_write("\n");
}
|
上面之所以有一个zlox_e1000_eeprom_gettype的函数,是因为mac地址是存储在EEPROM中的,而要读取EEPROM里的数据,是需要通过EERD(EEPROM Read Register即EEPROM读寄存器)来进行读取操作,而EERD寄存器有两种位结构,第一种结构如下图所示(该图位于手册的第248页):
图7(位于手册的第248页)
需要读取EEPROM某个位置里的数据时,只需将位置信息写入到Address部分,同时将START位设置为1,然后e1000网卡就会到EEPROM里去执行读操作,当读操作结束时,会将DONE位设置为1,同时将数据写入到Data部分,用户程式通过检测DONE位为1了,就可以将Data里的数据给读取出来了。
在手册的第249页,还有EERD的另一种结构(该结构主要用于82541xx及82547GI/EI的网卡中):
图8(位于手册的第249页)
可以看到,该结构中DONE在位1的位置,Address为位2到位15的部分,因此需要先通过上面的zlox_e1000_eeprom_gettype函数来判断EERD是属于哪种结构,这样后面才能从EEPROM里获取到正确的mac地址来。
刚才提到,mac地址信息位于EEPROM里,EEPROM的Address Map(地址映射)结构如下图所示(该图位于手册的第112页):
图9(位于手册的第112页,上图只显示了表格的一小部分)
上图中的Word就是需要写入到EERD寄存器中的Address,可以看到:00,01及02这三个位置中存储了mac地址的6个字节。上面代码中的zlox_e1000_getmac函数就是通过读取这3个位置里的数据来获取到mac地址的。
下面再来看下zlox_e1000_init_buf函数:
ZLOX_VOID zlox_e1000_init_buf()
{
ZLOX_UINT32 rx_buff_p;
ZLOX_UINT32 tx_buff_p;
ZLOX_SINT32 i;
e1000_dev.rx_descs_count = ZLOX_NUM_RX_DESC;
e1000_dev.tx_descs_count = ZLOX_NUM_TX_DESC;
e1000_dev.rx_descs = (ZLOX_E1000_RX_DESC *)zlox_kmalloc_ap(e1000_dev.rx_descs_count * sizeof(ZLOX_E1000_RX_DESC), &e1000_dev.rx_descs_p);
zlox_memset((ZLOX_UINT8 *)e1000_dev.rx_descs, 0, e1000_dev.rx_descs_count * sizeof(ZLOX_E1000_RX_DESC));
/*
* Allocate 2048-byte buffers.
*/
e1000_dev.rx_buffer_size = ZLOX_NUM_RX_DESC * ZLOX_RX_BUFFER_SIZE;
e1000_dev.rx_buffer = (ZLOX_UINT8 *)zlox_kmalloc_ap(e1000_dev.rx_buffer_size, &rx_buff_p);
/* Setup receive descriptors. */
for(i = 0; i < ZLOX_NUM_RX_DESC; i++)
{
e1000_dev.rx_descs[i].buffer = rx_buff_p + (i * ZLOX_RX_BUFFER_SIZE);
}
/*
* Then, allocate transmit descriptors.
*/
e1000_dev.tx_descs = (ZLOX_E1000_TX_DESC *)zlox_kmalloc_ap(e1000_dev.tx_descs_count * sizeof(ZLOX_E1000_TX_DESC), &e1000_dev.tx_descs_p);
zlox_memset((ZLOX_UINT8 *)e1000_dev.tx_descs, 0, e1000_dev.tx_descs_count * sizeof(ZLOX_E1000_TX_DESC));
/*
* Allocate 2048-byte buffers.
*/
e1000_dev.tx_buffer_size = ZLOX_NUM_TX_DESC * ZLOX_TX_BUFFER_SIZE;
/* Attempt to allocate. */
e1000_dev.tx_buffer = (ZLOX_UINT8 *)zlox_kmalloc_ap(e1000_dev.tx_buffer_size, &tx_buff_p);
/* Setup transmit descriptors. */
for(i = 0; i < ZLOX_NUM_TX_DESC; i++)
{
e1000_dev.tx_descs[i].buffer = tx_buff_p + (i * ZLOX_TX_BUFFER_SIZE);
}
/*
* Setup the receive ring registers.
*/
zlox_e1000_reg_write(ZLOX_E1000_REG_RDBAL, e1000_dev.rx_descs_p);
zlox_e1000_reg_write(ZLOX_E1000_REG_RDBAH, 0);
zlox_e1000_reg_write(ZLOX_E1000_REG_RDLEN, (e1000_dev.rx_descs_count * sizeof(ZLOX_E1000_RX_DESC)));
zlox_e1000_reg_write(ZLOX_E1000_REG_RDH, 0);
zlox_e1000_reg_write(ZLOX_E1000_REG_RDT, (e1000_dev.rx_descs_count - 1));
zlox_e1000_reg_unset(ZLOX_E1000_REG_RCTL, ZLOX_E1000_REG_RCTL_BSIZE);
zlox_e1000_reg_set(ZLOX_E1000_REG_RCTL, ZLOX_E1000_REG_RCTL_EN);
/*
* Setup the transmit ring registers.
*/
zlox_e1000_reg_write(ZLOX_E1000_REG_TDBAL, e1000_dev.tx_descs_p);
zlox_e1000_reg_write(ZLOX_E1000_REG_TDBAH, 0);
zlox_e1000_reg_write(ZLOX_E1000_REG_TDLEN, e1000_dev.tx_descs_count * sizeof(ZLOX_E1000_TX_DESC));
zlox_e1000_reg_write(ZLOX_E1000_REG_TDH, 0);
zlox_e1000_reg_write(ZLOX_E1000_REG_TDT, 0);
zlox_e1000_reg_set(ZLOX_E1000_REG_TCTL, ZLOX_E1000_REG_TCTL_EN | ZLOX_E1000_REG_TCTL_PSP);
}
|
要理解这段函数的含义,请参考手册的第33页,也就是第三章
"Receive and Transmit Description"的部分,这一部分详细的讲解了网卡的发送与接收的原理,以及所涉及到的各种结构,限于篇幅,作者不可能都解释一遍,只能介绍一个大致的过程,接收的过程其实就是:当网卡在以太网中收到一个数据包后,它会先对数据包进行地址过滤,符号条件的,比如说是发给自己的就接收,并存储到网卡内部的FIFO数据区域(FIFO的意思就是先进先出,先进来的数据就先传给主机,和栈的后进先出刚好相反),再将FIFO数据区域里的数据包通过DMA传输到主机的物理内存里,这段物理内存就是上面zlox_e1000_init_buf函数中设置的2048字节的接收缓冲。
接收缓冲的物理内存地址位于Receive Description(接收描述符)的结构里,在网卡将数据传递到接收缓冲区域后,就会向主机发送中断,接着在对应的中断处理例程里,就可以将接收缓冲里的数据给读取出来了,同时网卡还会更新Receive Description(接收描述符)里的某些字段信息,通过这些信息就可以知道具体接收了多少个字节的数据等。由于DMA传输所使用的是物理内存地址,因此,上面函数中,就用的是
zlox_kmalloc_ap函数来创建缓冲区的堆空间,因为该函数还可以同时获取到堆空间的物理内存地址。
接收描述符的格式如下图所示(该图位于手册的第34页):
图10(位于手册的第34页)
根据该图,在zlox_e1000.h头文件中就定义了一个ZLOX_E1000_RX_DESC结构:
struct _ZLOX_E1000_RX_DESC {
ZLOX_UINT32 buffer;
ZLOX_UINT32 buffer_h;
ZLOX_UINT16 length;
ZLOX_UINT16 checksum;
ZLOX_UINT8 status;
ZLOX_UINT8 errors;
ZLOX_UINT16 special;
} __attribute__((packed));
typedef struct _ZLOX_E1000_RX_DESC ZLOX_E1000_RX_DESC;
|
上面结构里的buffer与buffer_h两个字段连在一起表示
图10中的Buffer Address(缓冲区的物理地址),这是一个64位的地址值,主要是考虑到某些主机上的物理内存会用到64位的地址,length表示接收到的数据包的字节大小,status状态字段用于表示数据接收的情况,当status的位0为1时说明数据已经接收完毕,其他字段的含义则请参考手册的第34页到第38页的内容。
此外,并不是设置一个接收描述符就可以了,因为一个描述符只能管理一个缓冲区,而一个缓冲区只有2048个字节,当一次接收到多个数据包时(有时e1000网卡会在接收到多个数据包后才发出中断请求,这里涉及到一个网卡的内部定时器问题,可以参考手册的第43到第44页的定时器部分),一个缓冲区就无法将这些数据包都获取下来了,因此,实际上需要分配多个描述符,这些描述符连在一起,构成了网卡的描述符队列,为了能让这些描述符与描述符所管理的缓冲区能够被循环使用,这些描述符在网卡内部的Head与Tail指针的作用下(Head与Tail指针可以通过特定的寄存器来进行访问,下面会进行介绍),构成了描述符的环形结构,如下图所示(该图位于手册的第41页):
图11
上图中Head到Tail之前的白色区域是硬件拥有的部分,所谓硬件拥有的含义是:当网卡接收到数据包后,只能将其放入Head到Tail之间的白色区域里的描述符所对应的缓冲区中,而Tail到Head之前的灰色区域才是驱动程式需要访问的部分,该区域对应的缓冲区里已经存储了接收好的数据包,驱动程式每处理完一个灰色部分的缓冲区的数据后,就需要将Tail指针向下移动一格,这样原来的灰色部分就会变为白色,网卡就又可以向该区域中写入数据了,当Tail移动到底部后,需要驱动程式手动将其移动到顶部,Head指针则是在接收到数据包后,由硬件来自动进行移动(无需驱动程式干预),Head指针到了底部后,也会被自动挪到顶部,这样描述符及其相应的缓冲区就可以被循环使用了。
从之前的代码中,可以看到,e1000驱动里会为网卡分配ZLOX_NUM_RX_DESC个接收描述符以及相对应的缓冲区,ZLOX_NUM_RX_DESC是zlox_e1000.h头文件中定义的宏,其值为256 。
在创建好接收描述符后,可以通过RDBAL(Receive Descriptor Base Low 接收描述符基址低位寄存器)与RDBAH(Receive Descriptor Base Address High 接收描述符基址高位寄存器)来设置接收描述符队列的起始物理内存地址(低位与高位一起可以存储64位的物理内存地址),这两个寄存器的结构可以参考手册的第320页,RDBAL的低4位必须是0(如果低4位不是0,则低4位的值会被忽略掉),因此,队列的起始物理地址必须是16字节对齐的(上面的初始化函数中使用的是zlox_kmalloc_ap函数,该函数会让虚拟地址与物理地址都按照4K页对齐,4096字节对齐的情况下,也就确保了是16字节对齐)。
通过RDLEN(Receive Descriptor Length 接收描述符队列长度寄存器)可以设置接收描述符队列的总字节大小(描述符的数目 * 单个描述符的字节大小),该寄存器可以参考手册的第321页。
可以通过RDH(Receive Descriptor Head 接收描述符Head指针寄存器)与RDT(Receive Descriptor Tail 接收描述符Tail指针寄存器),来访问和修改图11里所示的Head与Tail指针,这两个寄存器的结构,请参考手册的第321页到第322页。
最后还有一个RCTL(Receive Control 接收控制寄存器),该寄存器可以对网卡的接收功能进行控制,该寄存器的位1(EN位)可以控制是否开启接收功能,当该位被设置为1时,网卡的接收功能就会被开启,此外,当该寄存器的位25(BSEX位)被清0时,同时,位16到位17都为0时,则表示接收描述符的接收缓冲区的字节大小为2048字节,该寄存器的具体结构请参考手册的第314页。
以上是和网卡数据接收相关的结构,下面再看下和发送相关的结构。
与接收相类似的是,发送也有对应的发送描述符,发送描述符有好几种结构,不过我们只使用Legacy Transmit Descriptor Format(传统的发送描述符格式),如下图所示:
图12(该图位于手册的第50页)
当上图中的CMD字段的位5(DEXT位)或者说是第二个8字节的位29为0时,那么对应的发送描述符就使用的是一个传统的发送描述符格式。根据该格式,在zlox_e1000.h的头文件里就定义了一个ZLOX_E1000_TX_DESC结构:
struct _ZLOX_E1000_TX_DESC {
ZLOX_UINT32 buffer;
ZLOX_UINT32 buffer_h;
ZLOX_UINT16 length;
ZLOX_UINT8 cso;
ZLOX_UINT8 cmd;
ZLOX_UINT8 status;
ZLOX_UINT8 css;
ZLOX_UINT16 special;
} __attribute__((packed));
typedef struct _ZLOX_E1000_TX_DESC ZLOX_E1000_TX_DESC;
|
上面结构里的buffer与buffer_h字段连在一起就表示图12里的Buffer Address部分,该部分存储的是对应的缓冲区的64位的物理内存地址。length字段表示需要发送的数据包的字节大小。cmd字段中可以写入一些和发送相关的命令位,例如将cmd里的位3(RS位)设置为1时,那么网卡将数据包发送出去后(或者存储到网卡内部的发送FIFO区域后),就会设置发送描述符里的status字段的位0(DD位),这样,驱动程式就可以通过循环检测status字段的DD位来判断数据包是否已经被发送出去了。发送描述符中的其他字段则请参考手册的第50页到第54页。
和接收相类似的是:多个发送描述符连在一起,可以构成一个如下所示的环形结构:
图13(该图位于手册的第66页)
上图中Head到Tail指针之间的白色区域的描述符所对应的缓冲区,就是网卡要发送的数据包,当Head与Tail指向同一个位置时,就表示发送队列为空,即没有要发送的数据,如果要发送数据的话,可以先将数据写入到Head指向的描述符所对应的缓冲区中,接着将Tail指针加一,这样Head与Tail之间的发送队列就不为空了,网卡就会立即将数据发送出去(当修改Tail指针时,就会触发网卡的发送操作),驱动程式在修改Tail指针后,可以循环检测发送描述符的status的DD位是否被设置,当被设置时,则说明数据发送出去了。当数据发送出去后,Head指针会被自动往下移,当Head指针移动到Tail所在的位置后,发送队列就又变为空了,这样网卡就会停止继续发送数据。
在初始化函数创建了发送描述符队列后,可以通过TDBAL(Transmit Descriptor Base Low 发送描述符基址低位寄存器)和TDBAH(Transmit Descriptor Base High 发送描述符基址高位寄存器)来设置发送描述符队列的物理内存地址,这两个寄存器的结构可以参考手册的第329页和第330页,TDBAL寄存器的低4位的值,会被忽略掉,因此发送队列的起始物理内存地址需要按照16字节对齐。
通过TDLEN(Transmit Descriptor Length 发送描述符队列长度寄存器)来设置队列的总字节大小,该寄存器的结构可以参考手册的第330页。
另外,通过TDH(Transmit Descriptor Head 发送描述符队列Head指针寄存器)可以设置队列环形结构里的Head指针,通过TDT(Transmit Descriptor Tail 发送描述符Tail指针寄存器)可以设置队列环形结构里的Tail指针,这两个寄存器请参考手册的第331页和第332页。
最后,还有个TCTL(Transmit Control 发送控制寄存器),该寄存器可以对网卡的发送功能进行控制,当该寄存器的位1(EN位)被设置为1时,则网卡的发送功能会被开启,另外当该寄存器的位3(PSP位)被设置为1时,表示网卡会对小于64字节的短包进行Pad(填充),从而让数据包能达到64字节的大小。该寄存器的结构请参考手册的第325页。
以上就是e1000网卡的发送与接收的原理,以及所涉及到的数据结构,zlox_e1000.c文件的zlox_e1000_received函数的代码就是根据接收原理来写的:
ZLOX_VOID zlox_e1000_received()
{
ZLOX_UINT32 tail = zlox_e1000_reg_read(ZLOX_E1000_REG_RDT);
ZLOX_UINT32 cur = (tail + 1) % e1000_dev.rx_descs_count;
ZLOX_UINT32 old_cur;
while(e1000_dev.rx_descs[cur].status & 0x1)
{
ZLOX_UINT8 * buf = e1000_dev.rx_buffer + cur * ZLOX_RX_BUFFER_SIZE;
ZLOX_UINT16 length = e1000_dev.rx_descs[cur].length;
zlox_network_received(buf, length);
//zlox_monitor_write("\n### receive packet! ###\n");
e1000_dev.rx_descs[cur].status = 0;
old_cur = cur;
cur = (cur + 1) % ZLOX_NUM_RX_DESC;
zlox_e1000_reg_write(ZLOX_E1000_REG_RDT, old_cur);
}
}
static ZLOX_VOID zlox_e1000_callback()
{
ZLOX_UINT32 icr = zlox_e1000_reg_read(ZLOX_E1000_REG_ICR);
if(icr & 0x04)
{
zlox_e1000_linkup();
}
if(icr & 0x10)
{
zlox_monitor_write("\n### e1000 warning: Receive Descriptor Minimum Threshold Reached ###\n");
}
// Receiver Timer Interrupt
if(icr & 0x80)
{
zlox_e1000_received();
}
//zlox_monitor_write("\n### e1000 irq's icr is : [");
//zlox_monitor_write_hex(icr);
//zlox_monitor_write("] ###\n");
}
|
上面的zlox_e1000_callback函数是中断处理例程,这里还涉及到一个ICR(Interrupt Cause Read 中断原因读寄存器),通过读取该寄存器的值,并对该值进行判断,就可以知道发生中断的具体原因了,例如当读取出来的值的位7(RXT0位)为1时,就说明这是一个Receiver Timer Interrupt(接收定时器中断),当收到该中断时,就说明网卡接收到了数据,并将数据存储到接收描述符所对应的接收缓冲区里了,这时,就可以调用zlox_e1000_received函数来处理缓冲区中的数据。ICR寄存器的详情可以参考手册的第307页。
和网卡的数据发送相关的函数为zlox_e1000_send函数:
ZLOX_SINT32 zlox_e1000_send(ZLOX_UINT8 * buf, ZLOX_UINT16 length)
{
ZLOX_UINT32 tail = zlox_e1000_reg_read(ZLOX_E1000_REG_TDT);
ZLOX_E1000_TX_DESC * desc;
desc = &e1000_dev.tx_descs[tail];
if(length > ZLOX_TX_BUFFER_SIZE)
length = ZLOX_TX_BUFFER_SIZE;
zlox_memcpy(e1000_dev.tx_buffer + (tail * ZLOX_TX_BUFFER_SIZE), buf, length);
/* Mark this descriptor ready. */
desc->status = 0;
desc->cmd = 0;
desc->length = length;
/* Marks End-of-Packet. */
desc->cmd = ZLOX_E1000_TX_CMD_EOP | ZLOX_E1000_TX_CMD_FCS | ZLOX_E1000_TX_CMD_RS;
/* Move to next descriptor. */
tail = (tail + 1) % e1000_dev.tx_descs_count;
/* Increment tail. Start transmission. */
zlox_e1000_reg_write(ZLOX_E1000_REG_TDT, tail);
// 5 seconds time out!
for(ZLOX_UINT32 j = 0; j < 10 * 5; j++)
{
for(ZLOX_UINT32 i=0;i < 10000 && !(desc->status & 0xf); i++)
{
asm("pause");
}
if(desc->status & 0xf)
break;
zlox_timer_sleep(5, ZLOX_FALSE); // 5 * 20ms = 100ms
}
if(desc->status & 0xf)
return (ZLOX_SINT32)length;
else
return -1;
return 0;
}
|
上面函数会先将buf即用户传递过来的数据写入到e1000_dev.tx_buffer + (tail * ZLOX_TX_BUFFER_SIZE)即发送缓冲区里,这里一开始tail指针的值等于head指针的值,因此也就是写入到head指针指向的缓冲区,然后,对该缓冲区所属的发送描述符进行相关设置,例如desc->cmd = ZLOX_E1000_TX_CMD_EOP | ZLOX_E1000_TX_CMD_FCS | ZLOX_E1000_TX_CMD_RS;的语句表示将描述符的cmd字段的EOP位,FCS及RS位设置为1,该字段前面提到过,可以参考手册的第52页。接着,就可以将tail指针值加一,并设置到TDT寄存器,这样就可以立即触发网卡的发送操作了,最后通过循环检测发送描述符里的status字段的低4位是否被设置(严格来讲,这里应该是测试位0(即status的DD位),不过测试低4位也是一样的效果),当被设置时,就说明数据被发送出去了。
以上就是e1000驱动相关的大致内容,读者最好能先将e1000开发手册简单的浏览一遍,这样可以加深理解,再通过gdb调试器对源代码进行调试分析,就可以掌握其原理。
下面介绍网络模块,以及ARP之类的网络协议,网络模块可以看成是用户与e1000驱动之间的中间层,e1000驱动在中断时所收到的数据,会通过zlox_network_received函数来交给网络模块去处理。当网络模块有数据需要发送时,会通过上面的zlox_e1000_send函数将数据交给e1000驱动,从而将数据发送出去。
网络模块,及各网络工具的使用:
[zengl pagebreak]
网络模块,及各网络工具的使用:
网络模块相关的函数定义在zlox_network.c文件中,网络模块相关的结构定义在zlox_network.h头文件里,前一节提到过,e1000驱动在中断时所收到的数据,都会交给该模块的zlox_network_received函数去处理,该函数的定义如下:
ZLOX_SINT32 zlox_network_received(ZLOX_UINT8 * buf, ZLOX_UINT16 length)
{
ZLOX_SINT32 index = -1;
if(buf == ZLOX_NULL)
return -1;
if(length == 0)
return -1;
// 先对收到的数据包进行检测,判断其
// 是否是ARP请求包,当以太网头部的类型字段为
// 0x0806时,说明这是一个使用ARP协议的数据包
// 当ARP数据包的op操作字段的值为0x1时,说明
// 这是一个ARP请求包,则对该请求进行响应,
// 通过组建一个ARP响应包,来告诉对方自己的mac地址
ZLOX_ETH_ARP_PACKET * darp = (ZLOX_ETH_ARP_PACKET *)buf;
if(zlox_net_swap_word(darp->eth.hdr_type) == 0x0806 &&
zlox_net_swap_word(darp->op) == 0x1)
{
zlox_net_make_arp((ZLOX_UINT8 *)&network_arp_for_reply, network_info.mac, darp->tx_mac,
network_info.ip_addr, *((ZLOX_UINT32 *)darp->d_ip), 0x2);
zlox_network_send((ZLOX_UINT8 *)&network_arp_for_reply, sizeof(ZLOX_ETH_ARP_PACKET));
}
// 如果没有用于接收数据包的任务,则
// 直接返回。
if(network_focus_task == ZLOX_NULL)
return 0;
// 收到的数据包会先存储到network_recv_array
// 所对应的动态数组里,可以将接收的数据包
// 想象成快递包裹,这些包裹会先存储在邮局里
// network_recv_array动态数组就是邮局,
// 存储好后,会向任务发送消息,通知它来取包裹,
// 所以下面会先对network_recv_array进行检测,
// 如果它没有被初始化,则初始化该动态数组。
if(network_recv_array.isInit == ZLOX_FALSE)
{
network_recv_array.size = ZLOX_NETWORK_RECEIVE_ARR_SIZE;
network_recv_array.count = 0;
network_recv_array.ptr =
(ZLOX_NETWORK_RECEIVE_MEMBER *)zlox_kmalloc(network_recv_array.size * sizeof(ZLOX_NETWORK_RECEIVE_MEMBER));
zlox_memset((ZLOX_UINT8 *)network_recv_array.ptr, 0,
network_recv_array.size * sizeof(ZLOX_NETWORK_RECEIVE_MEMBER));
network_recv_array.isInit = ZLOX_TRUE;
}
// 如果已存储的数据包的个数count等于容量size了,
// 则对network_recv_array动态数组进行扩容操作。
else if(network_recv_array.count == network_recv_array.size)
{
network_recv_array.size += ZLOX_NETWORK_RECEIVE_ARR_SIZE;
ZLOX_NETWORK_RECEIVE_MEMBER * tmp =
(ZLOX_NETWORK_RECEIVE_MEMBER *)zlox_kmalloc(network_recv_array.size * sizeof(ZLOX_NETWORK_RECEIVE_MEMBER));
zlox_memcpy((ZLOX_UINT8 *)tmp, (ZLOX_UINT8 *)network_recv_array.ptr,
network_recv_array.count * sizeof(ZLOX_NETWORK_RECEIVE_MEMBER));
zlox_memset((ZLOX_UINT8 *)(tmp + network_recv_array.count), 0,
ZLOX_NETWORK_RECEIVE_ARR_SIZE * sizeof(ZLOX_NETWORK_RECEIVE_MEMBER));
zlox_kfree(network_recv_array.ptr);
network_recv_array.ptr = tmp;
}
// 存储到动态数组里的每个数据包,
// 目前最大只能是2048个字节。
if(length > ZLOX_NETWORK_RECEIVE_BUF_SIZE)
length = ZLOX_NETWORK_RECEIVE_BUF_SIZE;
for(ZLOX_SINT32 i = 0; i < network_recv_array.size ; i++)
{
// 当找到空位置可以存储数据包时,
// 或者某个位置里存储的数据包一直
// 没有任务来取,达到了超时时间,
// 目前的超时时间为3000个时钟滴答,
// 则将接收到的数据包存储在该位置。
if((network_recv_array.ptr[i].used_task == ZLOX_NULL) ||
((tick - network_recv_array.ptr[i].tick) > ZLOX_NET_RECV_TIMEOUT_TICKS))
{
if(network_recv_array.ptr[i].used_task == ZLOX_NULL)
network_recv_array.count++;
index = i;
zlox_memcpy(network_recv_array.ptr[index].buf, buf, length);
network_recv_array.ptr[index].used_task = network_focus_task;
network_recv_array.ptr[index].tick = tick;
network_recv_array.ptr[index].length = length;
break;
}
}
if(index == -1)
return -1;
ZLOX_TASK_MSG msg = {0};
msg.type = ZLOX_MT_NET_PACKET;
msg.packet_idx = index;
// 向network_focus_task任务发送消息
// 通知该任务来获取数据包的内容。
zlox_send_tskmsg(network_focus_task,&msg);
return index;
}
|
上面代码中,棕色的注释是此处为了方便说明,额外添加的,在源文件中暂时没有。目前任务需要接收数据包的话,就需要通过syscall_network_set_focus_task系统调用来将自己设置为"网络焦点任务",即上面函数里的network_focus_task(这是一个任务指针,可以指向一个需要接收数据包的任务),这样网络模块就会向该任务发送
ZLOX_MT_NET_PACKET类型的消息,作者暂时没有使用绑定端口的方式,等以后引入TCP协议时,再考虑加进来。
上面函数所涉及到的ARP协议,下面会进行介绍,当任务收到
ZLOX_MT_NET_PACKET类型的消息时,就可以通过syscall_network_get_packet系统调用来获取数据包的具体内容,该系统调用最终会调用zlox_network.c文件里的zlox_network_get_packet函数:
ZLOX_SINT32 zlox_network_get_packet(ZLOX_TASK * recv_task, ZLOX_SINT32 index, ZLOX_UINT8 * buf,
ZLOX_UINT16 length)
{
if(recv_task == ZLOX_NULL)
return -1;
if(buf == ZLOX_NULL)
return -1;
if(length == 0)
return -1;
if(index < 0)
return -1;
if(network_recv_array.ptr[index].used_task != recv_task)
return -1;
if(length > network_recv_array.ptr[index].length)
length = network_recv_array.ptr[index].length;
zlox_memcpy(buf, network_recv_array.ptr[index].buf, length);
network_recv_array.ptr[index].used_task = ZLOX_NULL;
network_recv_array.ptr[index].tick = 0;
network_recv_array.ptr[index].length = 0;
network_recv_array.count--;
return (ZLOX_SINT32)length;
}
|
上面函数里,index参数是需要接收的数据包在network_recv_array动态数组中的索引值,buf是用户提供的缓冲区域,该函数会将index索引所对应的数据包的内容通过zlox_memcpy函数拷贝到buf(用户缓冲区),接着再将数组的该位置的used_task字段设置为ZLOX_NULL,即释放掉该位置。
以上就是网络模块里,和接收相关的函数,在该模块里,和发送相关的函数为zlox_network_send函数,用户态程式可以通过syscall_network_send系统调用进入该函数,从而将数据包发送出去,该函数的定义如下:
ZLOX_SINT32 zlox_network_send(ZLOX_UINT8 * buf, ZLOX_UINT16 length)
{
if(buf == ZLOX_NULL)
return -1;
if(length == 0)
return -1;
if(network_dev_type == ZLOX_NET_DEV_TYPE_E1000)
{
return zlox_e1000_send(buf, length);
}
return 0;
}
|
从上面的函数定义中,可以看到,该函数最终会调用e1000驱动里的zlox_e1000_send函数,来将用户buf缓冲区里的数据给发送出去。
网络模块里面还有一个zlox_network_init的初始化函数:
ZLOX_BOOL zlox_network_init()
{
zlox_e1000_init();
if(e1000_dev.isInit == ZLOX_TRUE)
{
network_info.isInit = network_isInit = ZLOX_TRUE;
zlox_memcpy(network_info.mac, e1000_dev.mac, 6);
network_dev_type = ZLOX_NET_DEV_TYPE_E1000;
return ZLOX_TRUE;
}
else
{
network_isInit = ZLOX_FALSE;
network_dev_type = ZLOX_NET_DEV_TYPE_NONE;
}
return ZLOX_FALSE;
}
|
该初始化函数中,会调用e1000驱动里的zlox_e1000_init函数来对e1000网卡进行初始化,如果以后添加了其他类型的网卡驱动的话,就可以将这些网卡驱动的初始化函数添加在这里,这样当一种网卡初始化失败后(比如电脑里没有这种类型的网卡时),则可以调用其他类型的网卡初始化函数。zlox_kernel.c文件的zlox_kernel_main(内核主入口)函数里,就是调用上面的zlox_network_init函数来完成网络方面的初始化操作的:
//zenglOX kernel main entry
ZLOX_SINT32 zlox_kernel_main(ZLOX_MULTIBOOT * mboot_ptr, ZLOX_UINT32 initial_stack)
{
.................................................
if(zlox_network_init() == ZLOX_TRUE)
zlox_monitor_write("network is init now!\n");
.................................................
}
|
上面在网络模块的接收函数里,提到了ARP协议,该协议相关的结构可以参考
http://en.wikipedia.org/wiki/Address_Resolution_Protocol 链接对应的文章。
ARP协议之前还有个以太网帧的头部结构,以太网帧结构可以参考
http://en.wikipedia.org/wiki/Ethernet_frame 该链接对应的文章。
作者在CSDN上找到一个
"《TCP/IP详解》全三卷 中文有书签"的资源,链接地址:
http://download.csdn.net/download/gnmtc/4400185 ,这是一个专门介绍TCP/IP协议的非常优秀的资源(如果CSDN上,该资源失效了的话,可以在百度或谷歌中搜索书名,还有很多其他的站点提供了该资源的下载)。
在该资源的卷一的第4章,就是专门介绍ARP(Address Resolution Protocol 地址解析协议)的,在该章节里可以看到,ARP协议的数据包格式,如下图所示:
图14(位于卷一第4章的4.4节)
根据上图,就有了zlox_network.h头文件里的ZLOX_ETH_ARP_PACKET结构的定义:
struct _ZLOX_ETH_HEADER {
ZLOX_UINT8 hdr_dmac[6];
ZLOX_UINT8 hdr_smac[6];
ZLOX_UINT16 hdr_type;
} __attribute__((packed));
typedef struct _ZLOX_ETH_HEADER ZLOX_ETH_HEADER;
struct _ZLOX_ETH_ARP_PACKET {
ZLOX_ETH_HEADER eth;
ZLOX_UINT16 hw_type;
ZLOX_UINT16 proto_type;
ZLOX_UINT8 hw_len;
ZLOX_UINT8 p_len;
ZLOX_UINT16 op;
ZLOX_UINT8 tx_mac[6];
ZLOX_UINT8 tx_ip[4];
ZLOX_UINT8 d_mac[6];
ZLOX_UINT8 d_ip[4];
} __attribute__((packed));
typedef struct _ZLOX_ETH_ARP_PACKET ZLOX_ETH_ARP_PACKET;
|
该结构,完全按照
图14所示的结构来定义的,上面的ZLOX_ETH_HEADER是以太网头部结构,当该结构里的hdr_type的值为0x0806时,就表示该数据包是ARP协议的数据包,当ZLOX_ETH_ARP_PACKET结构里的op字段为
0x1时,说明这是一个ARP请求包,当op字段为
0x2时,说明这是一个ARP响应包。
在zlox_network.c文件里,有一个zlox_net_make_arp函数,通过这个函数可以用于设置ARP数据包里的各个字段,该函数的定义如下:
ZLOX_UINT16 zlox_net_swap_word(ZLOX_UINT16 val)
{
ZLOX_UINT16 temp;
temp = val << 8 | val >> 8;
return temp;
}
ZLOX_SINT32 zlox_net_make_eth_header(ZLOX_ETH_HEADER * eth, ZLOX_UINT8 * hdr_smac, ZLOX_UINT8 * hdr_dmac, ZLOX_UINT16 hdr_type)
{
zlox_memcpy(eth->hdr_dmac, hdr_dmac, 6);
zlox_memcpy(eth->hdr_smac, hdr_smac, 6);
eth->hdr_type = zlox_net_swap_word(hdr_type);
return 0;
}
ZLOX_SINT32 zlox_net_make_arp(ZLOX_UINT8 * header_ptr,
ZLOX_UINT8 * hdr_smac, ZLOX_UINT8 * hdr_dmac,
ZLOX_UINT32 ip_src, ZLOX_UINT32 ip_dst,
ZLOX_UINT16 op)
{
ZLOX_ETH_HEADER * eth = (ZLOX_ETH_HEADER *)header_ptr;
ZLOX_ETH_ARP_PACKET * arp = (ZLOX_ETH_ARP_PACKET *)header_ptr;
zlox_net_make_eth_header(eth, hdr_smac, hdr_dmac, 0x0806); // 0x0806 -- arp数据报
arp->hw_type = zlox_net_swap_word(0x01);
arp->proto_type = zlox_net_swap_word(0x800);
arp->hw_len = 0x06;
arp->p_len = 0x04;
arp->op = zlox_net_swap_word(op);
zlox_memcpy(arp->tx_mac, hdr_smac, 6);
zlox_memcpy(arp->tx_ip, (ZLOX_UINT8 *)&ip_src, 4);
zlox_memcpy(arp->d_mac, hdr_dmac, 6);
zlox_memcpy(arp->d_ip, (ZLOX_UINT8 *)&ip_dst, 4);
return 0;
}
|
上面的zlox_net_make_eth_header函数用于设置以太网的头部结构,zlox_net_make_arp函数会先调用zlox_net_make_eth_header函数来设置好以太网头部里的目标mac地址,源mac地址,以及以太网的类型字段(此处为
0x0806),然后再设置ARP协议里的各个字段。
这里需要特别注意的是:在以太网中,数据是以大字节序进行传输的,而x86体系结构的内存里的数据是以小字节序进行存储的,因此,在设置数据包中的长度为字大小的字段时,需要通过zlox_net_swap_word函数将其由小字节序转换为大字节序,否则,目标主机在接收到数据时,就无法解析出正确的数值出来。所以,上面的eth->hdr_type,arp->hw_type,arp->proto_type,以及arp->op字段在设置时,都经过了
zlox_net_swap_word函数的转换操作,该函数其实就是将word(字)里的两个字节的位置进行交换。另外,在接收数据包时,数据包里的这些字段,也必须在经过
zlox_net_swap_word函数的转换后,才能使用。
由于网络模块目前只需要对ARP请求进行响应,所以网络模块里只有ARP协议的相关定义,其他的如IP,ICMP,DHCP之类的协议都是由用户态程式来组建的,用户程式组建好数据包后,需要发送数据包时,就直接调用网络模块里的zlox_network_send函数即可(通过相关的系统调用进入该函数)。网络模块从e1000驱动那里,接收到数据包时,会先存储到上面介绍过的network_recv_array动态数组中,并发消息给需要进行接收的任务,接着,由用户程式通过zlox_network_get_packet函数从动态数组里提取出数据包来,并由用户程式去分析数据包里的协议,网络模块不负责数据包协议的解析工作(只负责中间的转发工作,就像邮局一样)。
限于篇幅,作者不能对每个协议都一一做解释,下面要介绍的工具使用部分,会给出不同协议的参考链接,在v2.0.0版本的网盘里,有个readme.txt文件,里面也有类似下面的内容描述。
各网络工具的使用:
当前版本, 新增了5个工具: arp dhcp ipconf ping lspci可以先使用dhcp来动态获取IP地址,当然前提是你的网络环境下有网关提供DHCP服务。如果没有DHCP服务,就只能使用ipconf工具来手动设置IP地址等信息。下面看下这些工具的基本使用方法:
>>>> arp 工具
图15
使用arp工具可以检测你的模拟器所能访问的局域网内的某台主机的mac地址,如果目标主机不存在,则会有3000个tick的超时时间,在bochs下tick过的非常快,尽管一个tick的理论时间是20ms(因为目前的时钟频率为50Hz),那么3000个tick就应该是60000ms即60s。
但是在bochs里,一个tick完全不按实际来,可以快达2ms(估计的)。因此也就5到6秒钟的超时时间,如果将生成的zenglOX.iso镜像放到VirtualBox或VMware下的话,那就
确实是60秒的超时时间,如果你不想在VirtualBox或VMware下等那么久的话,可以手动按ESC键来退出arp程式,其他的 dhcp 以及 ping 工具也都可以使用ESC键来退出程式。(下面会提到如何在VirtualBox及VMware里设置和使用e1000网卡)
之所以设置为3000个tick的等待超时, 完全是为了照顾Bochs!
arp工具使用的是ARP协议,和ARP协议相关的内容,在上面介绍网络模块时已经讲解过了。
此外,上面
图15里,arp的参数是10.0.2.2 ,该IP地址是VirtualBox的NAT网关的IP ,因此,我这里是将zenglOX的镜像放在VirtualBox里运行时的输出情况。
>>>> dhcp 工具
图16
dhcp工具会使用DHCP协议来从网关那里获取到本机的IP地址,子网掩码,以及网关的IP地址信息。这里暂时没有包含dns信息,因为作者暂时还没添加dns相关的协议。
同样的, 如果你的网络环境下没有主机提供DHCP服务的话,就会有3000个tick的超时时间。可以按ESC键来退出超时等待过程。
DHCP协议的相关内容可以参考
http://en.wikipedia.org/wiki/Dynamic_Host_Configuration_Protocol 该链接对应的文章
DHCP协议还涉及到IP协议,UDP协议:
和IP协议相关的内容可以参考
http://en.wikipedia.org/wiki/IPv4 该链接对应的文章。
和UDP协议相关的内容可以参考
http://en.wikipedia.org/wiki/User_Datagram_Protocol 该链接对应的文章
以太网的帧结构可以参考
http://en.wikipedia.org/wiki/Ethernet_frame#Header 在程序里也就用到了以太网帧结构里的Header部分(目标mac,源mac,以及EtherType即以太网的类型字段)
>>>> ipconf 工具
图17
使用ipconf -a命令可以查看到当前配置的ip地址等信息。
如果想手动配置ip地址的话,可以使用ipconf -set命令:
图18
如果你是在路由器的环境下,那么手动配置时,需要注意ip地址的网络段,子网掩码,和网关信息,配置的IP地址必须和路由器的ip在同一个网络段里,否则就无法正常访问路由器(路由器会拒绝接受非同一网段的数据包)。
>>>> ping 工具
图19
上图中,由于我之前用ipconf -set命令手动设置了ip地址,因此,在第一次ping 4 60 10.0.2.2的操作时,VirtualBox的NAT网关会丢弃掉这个包,因为它还不知道新IP地址所对应的mac地址,它会向我发送arp请求,zenglOX的网络模块会响应它的请求,告诉它,我的mac地址,由于第一个包被丢弃掉了,所以在VirtualBox上就会有60秒的超时等待,上面的第二个参数60为超时时间,你当然也可以设置短点,如果超时了,可以按ESC键来退出当前的命令,再次进行ping操作时,由于它知道了我的mac地址,所以就ping成功了,没有再出现超时。
ping工具目前只能ping目标的IP地址,这是因为目前还没有加入dns协议,所以还不能直接ping域名。
从上面的
图19中可以看到,每次ping操作时,都会先向网关发送arp请求,来获取网关的mac地址,这是因为,作者暂时还没有在网络模块里加入arp缓存地址表,因此,每次都要通过arp请求来重新获取一次。
ping工具是使用ICMP协议来检测目标主机的。ping工具的第一个参数表示ping多少次,第二个参数表示每次ping的超时时间,由于bochs下的tick(时钟滴答)过得很快,上面的60秒超时,在bochs里也就是5到6秒的样子。如果在VirtualBox或VMware下,则建议设置为5到10秒的超时,因为VirtualBox或VMware下是按照实际的时间来执行的。可以按ESC键来退出超时等待过程。
有关ICMP协议的相关说明请参考
http://en.wikipedia.org/wiki/Internet_Control_Message_Protocol 该链接对应的文章。
在虚拟机的网络配置为NAT的情况下,比如VirtualBox与VMware默认就是用的NAT方式,这种方式下,ping包很容易超时,我的某些在VirtualBox里安装的linux系统,在NAT方式下,ping的丢包率高达80%以上。而且NAT方式也会偷偷摸摸的神不知鬼不觉的过滤掉一些ICMP数据包,例如,在VirtualBox中,NAT方式会将unreached的ICMP响应包给过滤掉,所以在VirtualBox的NAT方式下,是看不到unreached的提示的。因此,如果你要ping外网的话,建议用Bridge桥接模式。
>>>> lspci 工具
图20
lspci工具可以将PCI总线上的设备信息显示出来,vend id为厂商id,
0x8086表示是intel的设备,dev id为设备ID,如上面的
0x100e就是intel 82540EM网卡的设备ID,可以从intel的e1000开发手册上看到网卡的设备ID信息,这在之前的介绍e1000系列的网卡驱动时讲解过。
lspci还可以接一个显示数目的参数:
图21
上面lspci接了一个参数3,表示只显示前3个PCI设备信息。
以上就是各工具的使用方法。
下面看下如何在VirtualBox与VMware下使用e1000网卡。
>>>> VirtualBox设置e1000网卡:
在VirtualBox界面,对zenglOX虚拟机通过右键选择设置,进入设置界面,在设置界面,选择网络,在网络界面点击高级来展开一个界面,在展开的高级界面里将控制芯片设置为:Intel PRO/1000 MT 桌面 (82540EM) 或者 Intel PRO/1000 T 服务器 (82543GC) 或者 Intel PRO/1000 MT 服务器 (82545EM) ,这三个都是e1000系列的网卡,如下图所示:
图22
在网络界面的"网卡 1"的"连接方式"里可以选择
"网络地址转换(NAT)"方式或者
"桥接网卡"的方式,在桥接网卡下的界面名称里,可以选择需要桥接的实际的物理网卡名字,如下图所示:
图23
桥接方式下, 就会使用实际的局域网中的路由器来获取ip地址,也不会出现NAT方式下的超时情况(前提是对方主机确实ping的通的情况下)。
>>>> VMware设置e1000网卡:
VMware设置E1000网卡没有图形界面, 需要修改配置文件。进入zenglOX虚拟机所在的目录,找到vmx后缀的配置文件,作者的配置文件名为:zenglOX.vmx ,打开该文件,在该文件里添加一行如下的配置:
ethernet0.virtualDev ="e1000" |
然后,保存配置文件。
这样再在VMware里启动zenglOX时,就使用的是e1000的网卡了,VMware模拟的是e1000系列中的Intel 82545EM的网卡。
默认情况下VMware使用的是NAT方式,如果你想使用桥接方式,可以进入虚拟机的设置界面,将Network Adapter设置为Bridged桥接方式。如下图所示:
图24
这里需要注意的是VMware的桥接模式对某些无线网卡支持不好,尽量使用有线网卡。例如作者的笔记本在使用无线网卡时,桥接无法联系到局域网,在切换到有线网卡后,桥接就可以正常工作了。
另外, 在设置桥接方式前, 还需要在Edit菜单里选择Virtual Network Editor ,在出现的虚拟网络接口编辑器界面,将Type为Bridged的接口(一般为VMnet0)设置到实际的物理网卡,如下图所示:
图25
上图中的JMicron PCI Express Gigabit Ethernet Adapter是作者笔记本上的有线网卡。
此外,如果要在VMware中使用NAT模式,还必须确保windows服务管理器里的VMware DHCP Service与VMware NAT Service服务处于"已启动"状态,bridge桥接模式下则可以不开启这两个服务。
在键盘初始化前,当前版本还添加了一个PS/2控制器的初始化驱动,可以有效的防止出现之前版本频繁出现的按键失灵的情况。
之前的zenglOX版本容易出现按键失灵的情况,是因为grub引导器在将执行权交给zenglOX内核时,让PS/2控制器处于一种比较dirty的状态。这种状态下,容易出现问题,因此有必要对PS/2控制器重新进行初始化操作,并进行必要的自检操作。和PS/2相关的代码位于zlox_ps2.c文件里。
有关PS/2控制器的信息,可以参考
http://wiki.osdev.org/%228042%22_PS/2_Controller 该链接,该链接的"Initialising the PS/2 Controller"节里介绍了PS/2的完整的初始化步骤。
目前版本,只有一种情况会出现按键失灵,那就是PS/2或键盘所在的PS/2端口自检失败时。
当自检失败时,PS/2控制器的初始化函数会显示一条
"PS/2 Controller self test failed ! , you can't use keyboard and mouse..."的信息,或者是
"first PS/2 port test failed, you can't use keyboard"的信息给用户,这种情况下,就只有重新启动zenglOX了。
最后,在Linux下可以使用
tcpdump工具进行抓包分析,windows平台下可以使用
科来网络分析系统来进行网络数据包的分析,这两个工具都可以辅助网络协议的开发工作。
以上就是v2.0.0版本的相关内容,没有讲解的代码,请读者通过gdb调试器来进行分析。
OK,就到这里,休息,休息一下 o(∩_∩)o~~