Linux内核学习笔记之网卡驱动的详细分析(3)

好了,到此我们把rtl8139_init_one函数介绍完了,初始化个设备完了之后呢,我们通过ifconfig eth0 up命令来把我们的设备激活。这个命令直接导致了我们刚刚注册的rtl8139_open的调用。这个函数激活了设备。这个函数主要做了三件事。

①注册这个设备的中断处理函数。当网卡发送数据完成或者接收到数据时,是用中断的形式来告知的,比如有数据从网线传来,中断也通知了我们,那么必须要有一 个处理这个中断的函数来完成数据的接收。关于Linux的中断机制不是我们详细讲解的范畴,有兴趣的可以参考《Linux内核源代码情景分析》,但是有个 非常重要的资源我们必须注意,那就是中断号的分配,和内存地址映射一样,中断号也是BIOS在初始化阶段分配并写入设备的配置空间的,然后Linux在建 立pci_dev时从配置空间读出这个中断号然后写入pci_dev的irq成员中,所以我们注册中断程序需要中断号就是直接从pci_dev里取就可以 了。

retval = request_irq (dev->irq, rtl8139_interrupt, SA_SHIRQ, dev->name, dev);
if (retval) {
return retval;
}

我们注册的中断处理函数是rtl8139_interrupt,也就是说当网卡发生中断(如数据到达)时,中断控制器8259A把中断号发给CPU, CPU根据这个中断号找到处理程序,这里就是rtl8139_interrupt,然后执行。rtl8139_interrupt也是在我们的程序中定义 好了的,这是驱动程序的一个重要的义务,也是一个基本的功能。request_irq 的代码在arch/i386/kernel/irq.c中。

②分配发送和接收的缓存空间

根据官方文档,发送一个数据包的过程是这样的:先从应用程序中把数据包拷贝到一段连续的内存中(这段内存就是我们这里要分配的缓存),然后把这段内存的地 址写进网卡的数据发送地址寄存器(TSAD)中,这个寄存器的偏移量是TxAddr0 = 0x20。在把这个数据包的长度写进另一个寄存器(TSD)中,它的偏移量是TxStatus0 = 0x10。然后就把这段内存的数据发送到网卡内部的发送缓冲中(FIFO),最后由这个发送缓冲区把数据发送到网线上。

好了现在创建这么一个发送和接收缓冲内存的目的已经很显然了。

tp->tx_bufs = pci_alloc_consistent(tp->pci_dev, TX_BUF_TOT_LEN,
&tp->tx_bufs_dma);
tp->rx_ring = pci_alloc_consistent(tp->pci_dev, RX_BUF_TOT_LEN,
&tp->rx_ring_dma);

tp 是net_device的priv的指针,tx_bufs是发送缓冲内存的首地址,rx_ring是接收缓存内存的首地址,他们都是虚拟地址,而最后一个 参数tx_bufs_dma和rx_ring_dma均是这一段内存的物理地址。为什么同一个事物,既用虚拟地址来表示它还要用物理地址呢,是这样的, CPU执行程序用到这个地址时,用虚拟地址,而网卡设备向这些内存中存取数据时用的是物理地址(因为网卡相对CPU属于头脑比较简单型的)。 pci_alloc_consistent的代码在Linux/arch/i386/kernel/pci-dma.c中。

③发送和接收缓冲区初始化和网卡开始工作的操作

RTL8139有4个发送描述符(包括4个发送缓冲区的基地址寄存器(TSAD0-TSAD3)和4个发送状态寄存器(TSD0-TSD3)。也就是说我 们分配的缓冲区要分成四个等分并把这四个空间的地址都写到相关寄存器里去,下面这段代码完成了这个操作。

for (i = 0; i < NUM_TX_DESC; i++)
((struct rtl8139_private*)dev->priv)->tx_buf =
&((struct rtl8139_private*)dev->priv)->tx_bufs[i * TX_BUF_SIZE];

上面这段代码负责把发送缓冲区虚拟空间进行了分割。

for (i = 0; i < NUM_TX_DESC; i++)
{
writel(tp->tx_bufs_dma+(tp->tx_buftp->tx_bufs),ioaddr+TxAddr0+(i*4));
readl(ioaddr+TxAddr0+(i * 4));
}

上面这段代码负责把发送缓冲区物理空间进行了分割,并把它写到了相关寄存器中,这样在网卡开始工作后就能够迅速定位和找到这些内存并存取他们的数据。

writel(tp->rx_ring_dma,ioaddr+RxBuf);

上面这行代码是把接收缓冲区的物理地址写到了相关寄存器中,这样网卡接收到数据后就能准确的把数据从网卡中搬运到这些内存空间中,等待CPU来领走他们。

writeb((readb(ioaddr+ChipCmd) & ChipCmdClear) |
CmdRxEnb | CmdTxEnb,ioaddr+ChipCmd);

重新RESET设备后,我们要激活设备的发送和接收的功能,上面这行代码就是向相关寄存器中写入相应值,激活了设备的这些功能。

writel ((TX_DMA_BURST << TxDMAShift),ioaddr+TxConfig);

上面这行代码是向网卡的TxConfig (位移是0x44)寄存器中写入TX_DMA_BURST << TxDMAShift这个值,翻译过来就是6<<8,就是把第8到第10这三位置成110,查阅管法文档发现6就是110代表着一次DMA的 数据量为1024字节。

另外在这个阶段设置了接收数据的模式,和开启中断等等,限于篇幅由读者自行研究。

下面进入数据收发阶段:

当一个网络应用程序要向网络发送数据时,它要利用Linux的网络协议栈来解决一系列问题,找到网卡设备的代表net_device,由这个结构来找到并 控制这个网卡设备来完成数据包的发送,具体是调用net_device的hard_start_xmit成员函数,这是一个函数指针,在我们的驱动程序里 它指向的是 rtl8139_start_xmit,正是由它来完成我们的发送工作的,下面我们就来剖析这个函数。它一共做了四件事。

①检查这个要发送的数据包的长度,如果它达不到以太网帧的长度,必须采取措施进行填充。

if( skb->len < ETH_ZLEN ){//if data_len < 60
if( (skb->data + ETH_ZLEN) <= skb->end ){
memset( skb->data + skb->len, 0x20, (ETH_ZLEN - skb->len) );
skb->len = (skb->len >= ETH_ZLEN) ? skb->len : ETH_ZLEN;}
else{
printk("%s:(skb->data+ETH_ZLEN) > skb->end\n",__FUNCTION__);
}
}

skb->data和skb->end就决定了这个包的内容,如果这个包本身总共的长度(skb->end- skb->data)都达不到要求,那么想填也没地方填,就出错返回了,否则的话就填上。

②把包的数据拷贝到我们已经建立好的发送缓存中。

memcpy (tp->tx_buf[entry], skb->data, skb->len);

其中skb->data就是数据包数据的地址,而tp->tx_buf[entry]就是我们的发送缓存地址,这样就完成了拷贝,忘记了这些内容的回头看看前面的介绍。

③光有了地址和数据还不行,我们要让网卡知道这个包的长度,才能保证数据不多不少精确的从缓存中截取出来搬运到网卡中去,这是靠写发送状态寄存器(TSD)来完成的。

writel(tp->tx_flag | (skb->len >= ETH_ZLEN ? skb->len : ETH_ZLEN),ioaddr+TxStatus0+(entry * 4));

我们把这个包的长度和一些控制信息一起写进了状态寄存器,使网卡的工作有了依据。

④判断发送缓存是否已经满了,如果满了在发就覆盖数据了,要停发。

if ((tp->cur_tx - NUM_TX_DESC) == tp->dirty_tx)
netif_stop_queue (dev);

谈完了发送,我们开始谈接收,当有数据从网线上过来时,网卡产生一个中断,调用的中断服务程序是rtl8139_interrupt,它主要做了三件事。

①从网卡的中断状态寄存器中读出状态值进行分析,status = readw(ioaddr+IntrStatus);

if ((status &(PCIErr | PCSTimeout | RxUnderrun | RxOverflow |
RxFIFOOver | TxErr | TxOK | RxErr | RxOK)) == 0)
goto out;

上面代码说明如果上面这9种情况均没有的表示没什么好处理的了,退出。

② if (status & (RxOK | RxUnderrun | RxOverflow | RxFIFOOver))/* Rx interrupt */

rtl8139_rx_interrupt (dev, tp, ioaddr);

如果是以上4种情况,属于接收信号,调用rtl8139_rx_interrupt进行接收处理。

③ if (status & (TxOK | TxErr)) {

spin_lock (&tp->lock);

rtl8139_tx_interrupt (dev, tp, ioaddr);
spin_unlock (&tp->lock);
}

如果是传输完成的信号,就调用rtl8139_tx_interrupt进行发送善后处理。

下面我们先来看看接收中断处理函数rtl8139_rx_interrupt,在这个函数中主要做了下面四件事

①这个函数是一个大循环,循环条件是只要接收缓存不为空就还可以继续读取数据,循环不会停止,读空了之后就跳出。

int ring_offset = cur_rx % RX_BUF_LEN;
rx_status = le32_to_cpu (*(u32 *) (rx_ring + ring_offset));
rx_size = rx_status >> 16;

上面三行代码是计算出要接收的包的长度。

②根据这个长度来分配包的数据结构

skb = dev_alloc_skb (pkt_size + 2);

③如果分配成功就把数据从接收缓存中拷贝到这个包中

eth_copy_and_sum (skb, &rx_ring[ring_offset + 4], pkt_size, 0);

这个函数在include/linux/etherdevice.h中,实质还是调用了memcpy()。

static inline void eth_copy_and_sum(struct sk_buff*dest, unsigned char *src, int len, int base)
{
memcpy(dest->data, src, len);
}

现在我们已经熟知,&rx_ring[ring_offset + 4]就是接收缓存,也是源地址,而skb->data就是包的数据地址,也是目的地址,一目了然。

④把这个包送到Linux协议栈去进行下一步处理

skb->protocol = eth_type_trans (skb, dev);
netif_rx (skb);

在netif_rx()函数执行完后,这个包的数据就脱离了网卡驱动范畴,而进入了Linux网络协议栈里面,把这些数据包的以太网帧头,IP头,TCP 头都脱下来,最后把数据送给了应用程序,不过协议栈不再本文讨论范围内。netif_rx函数在net/core/dev.c,中。

而rtl8139_remove_one则基本是rtl8139_init_one的逆过程。 

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wzwyjp.html