dpdk用户态驱动初始化完成后,应用层就可以来对网卡进行设置操作。对于每一个网卡,应用层都需要调用相应接口进行配置。可以对网卡进行哪些设置操作呢? 例如应用层将对网卡进行配置下发,将配置信息下发给网卡;应用层对网卡接收队列进行设置;应用层对网卡发送队列进行设置; 应用层启动网卡等操作。以l2fwd为例, 应用层调用rte_eth_dev_configure进行配置下发; 调用rte_eth_rx_queue_setup对网卡接收队列进行设置; 调用rte_eth_tx_queue_setup对网卡发送队列进行设置; 调用rte_eth_dev_start启用网卡。下面分别来看下这些接口的实现。
一、rte_eth_dev_configure配置下发
rte_eth_dev_configure接口使得应用层可以对网卡进行配置下发操作,将应用层提供的网卡信息保存到网卡数据空间struct rte_eth_dev_data中,这样pmd用户态驱动就可以根据应用层提供的配置对网卡进行设置操作。另外这个接口还会为网卡的发送队列,接收队列开辟二级指针空间;
需要注意的是,每个网卡都有一个数据空间struct rte_eth_dev_data结构,这个网卡数据空间在pmd驱动初始化过程中开辟, 这在上一篇dpdk pmd驱动初始化已经分析过了。
1、参数合法性校验
函数内部首先对网卡的发送队列,接收队列个数进行合法性校验。根据应用层传递进来的参数和pmd用户态驱动提供的值进行比较,就可以知道队列个数有没超过网卡真实队列的个数。另外也会对网卡是否支持巨帧进行判断, 所谓的巨帧就是判断网卡是否支持发送或者接收超过一定大小的报文。
2、配置下发
将应用层提供的网卡配置参数保存到网卡数据空间中,后续pmd用户态驱动就可以根据应用层提供的配置对网卡进行设置操作。
int rte_eth_dev_configure(uint8_t port_id, uint16_t nb_rx_q, uint16_t nb_tx_q,const struct rte_eth_conf *dev_conf)
{
//保存应用层传进来的网卡配置信息
memcpy(&dev->data->dev_conf, dev_conf, sizeof(dev->data->dev_conf));
}
3、网卡接收队列二级指针空间开辟
网卡不管是支持一个接收队列,还是支持多个接收队列,都需要为这个接收队列开辟二级指针空间,保存到网卡数据空间的rx_queues中,每一个元素代表这个网卡的一个接收队列。需要注意的是,这里只是开辟了一个二级指针空间,并没有开辟一级指针空间,一级指针空间才是真正的接收队列,这个一级指针空间在rte_eth_rx_queue_setup函数中开辟。
函数内部会判断是第一次对接收队列二级指针空间开辟,还是重新设置。如果是第一次开辟,则直接开辟指定接收队列个数的二级指针空间;如果是重复设置,则需要判断重新设置的接收队列个数比上一次设置的接收队列个数多了还是少了,并相应增加或者减少队列的大小。
static int rte_eth_dev_rx_queue_config(struct rte_eth_dev *dev, uint16_t nb_queues)
{
//为接收队列开辟二级指针空间。每个元素都代表一个接收队列空间
dev->data->rx_queues = rte_zmalloc("ethdev->rx_queues", sizeof(dev->data->rx_queues[0]) * nb_queues, RTE_CACHE_LINE_SIZE);
}
4、网卡发送队列二级指针空间开辟
网卡不管是支持一个发送队列,还是支持多个发送队列,都需要为这个发送队列开辟二级指针空间,保存到网卡数据空间的tx_queues中,每一个元素代表这个网卡的一个发送队列。需要注意的是,这里只是开辟了一个二级指针空间,并没有开辟一级指针空间,一级指针空间才是真正的发送队列,这个一级指针空间在rte_eth_tx_queue_setup函数中开辟。
函数内部会判断是第一次对发送队列二级指针空间开辟,还是重新设置。如果是第一次开辟,则直接开辟指定发送队列个数的二级指针空间;如果是重复设置,则需要判断重新设置的发送队列个数比上一次设置的发送队列个数多了还是少了,并相应增加或者减少队列的大小。
int rte_eth_dev_tx_queue_config(struct rte_eth_dev *dev, uint16_t nb_queues)
{
//为发送队列开辟二级指针空间。每个元素都代表一个发送队列空间
dev->data->tx_queues = rte_zmalloc("ethdev->tx_queues", sizeof(dev->data->tx_queues[0]) * nb_queues, RTE_CACHE_LINE_SIZE);
}
5、设置网卡链路更新标记
为什么要设置网卡的链路更新标记? 这是因为在打上这个标记后,表示应用层已经对网卡进行了配置操作。在后续调用rte_eth_dev_start接口启用网卡时,将会触发链路状态中断。在中断处理函数eth_igb_interrupt_action中判断如果已经打上了这个标记,则会根据网卡当前链路是否up。如果是up,则写收发寄存器,允许报文开始收发操作;如果是down, 则写收发寄存器,禁止报文收发操作。简单来说,这个网卡链路更新标记,是用于控制网卡是否允许收发报文。
int rte_eth_dev_configure(uint8_t port_id, uint16_t nb_rx_q, uint16_t nb_tx_q,const struct rte_eth_conf *dev_conf)
{
//配置网卡,打上E1000_FLAG_NEED_LINK_UPDATE标记,在eth_igb_interrupt_action中断处理中判断如果已经打上了
//这个标记,则会根据网卡当前链路是否up。如果是up,则写收发寄存器,允许报文开始收发操作;如果是down,
//则写收发寄存器,禁止报文收发操作。
//如果是e1000网卡,则驱动接口为eth_igb_configure
diag = (*dev->dev_ops->dev_configure)(dev);
}
如果是e1000网卡,则中断处理函数为eth_igb_interrupt_action,来看下这个函数的实现。
int eth_igb_interrupt_action(struct rte_eth_dev *dev)
{
//判断是否已经设置了网卡链路更新标记
if (intr->flags & E1000_FLAG_NEED_LINK_UPDATE)
{
intr->flags &= ~E1000_FLAG_NEED_LINK_UPDATE;
//根据网关是否up, 设置发送,接收控制寄存器。从而允许或者禁止报文收发操作
tctl = E1000_READ_REG(hw, E1000_TCTL);
rctl = E1000_READ_REG(hw, E1000_RCTL);
if (link.link_status)
{
//允许报文开始收发操作
tctl |= E1000_TCTL_EN;
rctl |= E1000_RCTL_EN;
}
else
{
//禁止报文收发操作
tctl &= ~E1000_TCTL_EN;
rctl &= ~E1000_RCTL_EN;
}
E1000_WRITE_REG(hw, E1000_TCTL, tctl);
E1000_WRITE_REG(hw, E1000_RCTL, rctl);
}
}
二、rte_eth_rx_queue_setup网卡接收队列设置
网卡接收队列设置,主要是为网卡开辟接收队列空间,也就是一级指针。上面的rte_eth_dev_configure接口只是开辟一个二级指针空间,这里是为网卡接收队列开辟一级指针空间。来看下这个接口的实现,如果是e1000网卡,则内部会调用pmd用户态驱动提供的接口eth_igb_rx_queue_setup
int rte_eth_rx_queue_setup(uint8_t port_id, uint16_t rx_queue_id, uint16_t nb_rx_desc,
unsigned int socket_id, const struct rte_eth_rxconf *rx_conf,struct rte_mempool *mp)
{
//设置网卡接收队列eth_igb_rx_queue_setup
ret = (*dev->dev_ops->rx_queue_setup)(dev, rx_queue_id, nb_rx_desc, socket_id, rx_conf, mp);
}
1、开辟接收队列空间
开辟一个接收队列空间,然后保存到网卡数据空间rx_queues接收队列相应元素中。这里是真正开辟一个接收队列空间,也就是一级指针空间,而rte_eth_dev_configure接口里面只是开辟了一个rx_queues二级指针空间。
int eth_igb_rx_queue_setup(struct rte_eth_dev *dev, uint16_t queue_idx, uint16_t nb_desc,
unsigned int socket_id, const struct rte_eth_rxconf *rx_conf,struct rte_mempool *mp)
{
//开辟一个接收队列空间
rxq = rte_zmalloc("ethdev RX queue", sizeof(struct igb_rx_queue), RTE_CACHE_LINE_SIZE);
rxq->mb_pool = mp;
rxq->nb_rx_desc = nb_desc;
rxq->queue_id = queue_idx;
//保存接收队列
dev->data->rx_queues[queue_idx] = rxq;
}
2、开辟硬件接收空间,保存到接收队列中
这里的硬件接收空间,也就是描述符空间,是用于告诉dma控制器,网卡收到报文后,将报文放到哪个位置。描述符空间里面的每一个成员union e1000_adv_rx_desc结构,里面记录了dma控制器收到报文后,将报文放到pkt_addr指向的位置;length记录了报文的长度;vlan记录了报文携带的vlanid
//接收描述符
union e1000_adv_rx_desc
{
struct
{
__le64 pkt_addr; /* mbuf的物理地址,Packet buffer address */
__le64 hdr_addr; /* Header buffer address */
} read;
struct
{
struct
{
__le32 status_error; /* 状态位,ext status/error */
__le16 length; /* 报文长度,Packet length */
__le16 vlan; /* vlan id;VLAN tag */
} upper;
} wb;
};
硬件描述符空间开辟好后,会将这个空间保存到接收队列rx_ring成员中
int eth_igb_rx_queue_setup(struct rte_eth_dev *dev, uint16_t queue_idx, uint16_t nb_desc,
unsigned int socket_id, const struct rte_eth_rxconf *rx_conf,struct rte_mempool *mp)
{
//开辟硬件接收空间,也就是接收描述符空间
size = sizeof(union e1000_adv_rx_desc) * IGB_MAX_RING_DESC;
rz = ring_dma_zone_reserve(dev, "rx_ring", queue_idx, size, socket_id);
//设置硬件空间的物理地址
rxq->rx_ring_phys_addr = (uint64_t) rz->phys_addr;
//保存硬件接收空间到接收队列中
rxq->rx_ring = (union e1000_adv_rx_desc *) rz->addr;
}
3、开辟软件接收空间,保存到接收队列中
软件接收空间,也就是mbuf空间,是用来告诉dma控制器,收到的报文存放到e1000_adv_rx_desc描述符成员pkt_addr指向的位置,这个pkt_addr指向的位置就是mbuf的地址。 从中也可以看出e1000_adv_rx_desc描述符充当mbuf和dma控制器之间的中介,真正存放报文的地方就是mbuf。 需要注意的是,这里仅是开辟软件接收空间,并没有将开e1000_adv_rx_desc描述符与软件接收空间,也就是mbuf关联起来,这两者的关联在rte_eth_dev_start中完成。
开辟好软件接收空间后,将这个软件接收空间保存到接收队列sw_ring成员中。
int eth_igb_rx_queue_setup(struct rte_eth_dev *dev, uint16_t queue_idx, uint16_t nb_desc,
unsigned int socket_id, const struct rte_eth_rxconf *rx_conf,struct rte_mempool *mp)
{
//开辟软件空间,保存到接收队列中
rxq->sw_ring = rte_zmalloc("rxq->sw_ring", sizeof(struct igb_rx_entry) * nb_desc, RTE_CACHE_LINE_SIZE);
}
dma控制器从网卡收包后,放到硬件描述符空间e1000_adv_rx_desc描述符成员pkt_addr指向的位置,也就是mbuf位置。最终报文是存放到mbuf中
三、rte_eth_tx_queue_setup网卡发送队列设置
网卡发送队列设置,主要是为网卡开辟发送队列空间,也就是一级指针。上面提到的rte_eth_dev_configure接口只是开辟一个二级指针空间,这里是为网卡发送队列开辟一级指针空间。来看下这个接口的实现,如果是e1000网卡,则内部会调用pmd用户态驱动提供的接口eth_igb_tx_queue_setup
int rte_eth_tx_queue_setup(uint8_t port_id, uint16_t tx_queue_id, uint16_t nb_tx_desc, unsigned int socket_id, const struct rte_eth_txconf *tx_conf)
{
//设置网卡的发送队列,e1000接口为eth_igb_tx_queue_setup
return (*dev->dev_ops->tx_queue_setup)(dev, tx_queue_id, nb_tx_desc, socket_id, tx_conf);
}
1、开辟发送队列空间
开辟一个发送队列空间,然后保存到网卡数据空间tx_queues发送队列相应元素中。这里是真正开辟一个发送队列空间,也就是一级指针空间,而rte_eth_dev_configure接口里面只是开辟了一个tx_queues二级指针空间。
int eth_igb_tx_queue_setup(struct rte_eth_dev *dev,uint16_t queue_idx, uint16_t nb_desc,
unsigned int socket_id, const struct rte_eth_txconf *tx_conf)
{
//开辟一个发送队列结构
txq = rte_zmalloc("ethdev TX queue", sizeof(struct igb_tx_queue), RTE_CACHE_LINE_SIZE);
txq->nb_tx_desc = nb_desc;
txq->queue_id = queue_idx;
txq->port_id = dev->data->port_id;
//保存发送队列结构
dev->data->tx_queues[queue_idx] = txq;
}
2、开辟硬件发送空间,保存到发送队列中
这里的硬件发送空间,也就是描述符空间,是用于告诉dma控制器,dma应该从哪个位置获取报文后发送出去。描述符空间里面的每一个成员都是e1000_adv_tx_desc结构,里面记录了dma控制器应该从buffer_addr指向的位置获取报文,然后发送出去。
//网卡发送队列描述符
union e1000_adv_tx_desc
{
struct
{
__le64 buffer_addr; /* mbuf的物理地址,告诉dma从这个位置读取报文发出去,Address of descriptor's data buf */
__le32 cmd_type_len; //报文长度
__le32 olinfo_status;
} read;
struct
{
__le32 status; //例如E1000_TXD_STAT_DD
} wb;
};
硬件描述符空间开辟好后,会将这个空间保存到发送队列tx_ring成员中
int eth_igb_tx_queue_setup(struct rte_eth_dev *dev,uint16_t queue_idx, uint16_t nb_desc,
unsigned int socket_id, const struct rte_eth_txconf *tx_conf)
{
//开辟硬件发送空间,也就是发送描述符空间
size = sizeof(union e1000_adv_tx_desc) * IGB_MAX_RING_DESC;
tz = ring_dma_zone_reserve(dev, "tx_ring", queue_idx, size, socket_id);
//设置硬件空间的物理地址
txq->tx_ring_phys_addr = (uint64_t) tz->phys_addr;
//保存硬件发送空间到发送队列中
txq->tx_ring = (union e1000_adv_tx_desc *) tz->addr;
}
3、开辟软件发送空间,保存到发送队列中
软件发送空间,也就是mbuf空间,是用来告诉dma控制器,e1000_adv_tx_desc描述符成员buffer_addr指向mbuf空间中。 dma控制器从buffer_addr获取报文,实际上就是从mbuf中获取报文,进而从网卡发送出去。从中也可以看出e1000_adv_tx_desc描述符充当mbuf和dma控制器之间的中介,真正待发送的报文存储在mbuf。 需要注意的是,这里仅是开辟软件发送空间,并没有将e1000_adv_tx_desc描述符与软件发送空间,也就是mbuf关联起来,这两者的关联在rte_eth_dev_start中完成。
开辟好软件发送空间后,将这个软件发送空间保存到发送队列sw_ring成员中。
int eth_igb_tx_queue_setup(struct rte_eth_dev *dev,uint16_t queue_idx, uint16_t nb_desc,
unsigned int socket_id, const struct rte_eth_txconf *tx_conf)
{
//开辟软件空间,保存到发送队列中
txq->sw_ring = rte_zmalloc("txq->sw_ring", sizeof(struct igb_tx_entry) * nb_desc, RTE_CACHE_LINE_SIZE);
}
dma控制器从描述符空间e1000_adv_tx_desc成员buffer_addr指向的位置获取报文,实际上就是从mbuf中获取报文。然后将报文从网卡发送出去
四、rte_eth_dev_start启用网卡
rte_eth_dev_start启用网卡,也就是使得网卡up起来,能够执行收发报文操作。 这个接口执行的操作就非常多了,有对接收队列进行配置,将mbuf, 内存池,dma进行关联;有对发送队列进行配置,将mbuf, 内存池,dma进行关联;有对中断进行配置;有对寄存器进行设置操作。 这个接口绝大部分是在对寄存器进行配置,我们重点关注发送队列、接收队列配置、中断设置。其他的配置操作,有需要的话查看网卡数据手册就好了,就可以知道每个寄存器都在操作什么内容。
int rte_eth_dev_start(uint8_t port_id)
{
//启用网卡,如果是e1000网卡,则调用pmd驱动提供的接口eth_igb_start
diag = (*dev->dev_ops->dev_start)(dev);
}
1、接收队列与mbuf以及dma的关联
如果网关有多个接收队列,则会对每个接收队列都进行mbuf, dma, 内存池关联操作。igb_alloc_rx_queue_mbufs负责将描述符,mbuf, dma,接收队列给关联起来,这个稍后进行分析; 这个接口大部分都是对寄存器进行设置,例如将接收描述符的地址位置写入寄存器,将接收描述符总大小写入寄存器,这样dma控制器通过读取寄存器就知道了接收描述符的地址位置,总空间大小。寄存器的操作,读者自行查看网卡的数据手册,不会复杂的, 数据手册明确指出了每个寄存器时是做什么用的,以及如何设置寄存器。
需要注意的是,将描述符队列的物理地址写入到寄存器后。dma通过读这个寄存器就知道了描述符队列的地址,进而dma收到报文后,会将报文保存到描述符指向的mbuf空间。
static int eth_igb_start(struct rte_eth_dev *dev)
{
//接收队列与mbuf以及dma的关联
ret = eth_igb_rx_init(dev);
}
int eth_igb_rx_init(struct rte_eth_dev *dev)
{
for (i = 0; i < dev->data->nb_rx_queues; i++)
{
rxq = dev->data->rx_queues[i];
//将描述符,mbuf, 队列关联起来
ret = igb_alloc_rx_queue_mbufs(rxq);
bus_addr = rxq->rx_ring_phys_addr;
//将描述符的总大小,也就是占用的字节数写入到寄存器
E1000_WRITE_REG(hw, E1000_RDLEN(rxq->reg_idx), rxq->nb_rx_desc * sizeof(union e1000_adv_rx_desc));
//接收队列描述符高32位地址写入寄存器
E1000_WRITE_REG(hw, E1000_RDBAH(rxq->reg_idx), (uint32_t)(bus_addr >> 32));
//接收队列描述符低32位地址写入寄存器
E1000_WRITE_REG(hw, E1000_RDBAL(rxq->reg_idx), (uint32_t)bus_addr);
//使能接收描述符队列功能
rxdctl = E1000_READ_REG(hw, E1000_RXDCTL(rxq->reg_idx));
rxdctl |= E1000_RXDCTL_QUEUE_ENABLE;
E1000_WRITE_REG(hw, E1000_RXDCTL(rxq->reg_idx), rxdctl);
}
}
igb_alloc_rx_queue_mbufs负责将描述符,mbuf, dma,接收队列给关联起来,来看下这个函数的实现。对于每个接收队列,这个接口会从内存池中获取mbuf, 保存到软件队列中。同时将这个mbuf的地址保存到接收描述符中,相当于告诉dma控制器将收到报文存放到mbuf位置。这样,mbuf, 内存池,dma,描述符队列就关联起来了。
static int igb_alloc_rx_queue_mbufs(struct igb_rx_queue *rxq)
{
struct igb_rx_entry *rxe = rxq->sw_ring;
for (i = 0; i < rxq->nb_rx_desc; i++)
{
//从内存池中获取mbuf,此时mbuf与内存池关联起来了
volatile union e1000_adv_rx_desc *rxd;
struct rte_mbuf *mbuf = rte_rxmbuf_alloc(rxq->mb_pool);
dma_addr = rte_cpu_to_le_64(RTE_MBUF_DATA_DMA_ADDR_DEFAULT(mbuf));
rxd = &rxq->rx_ring[i];
//设置接收描述符里面的成员,将mbuf地址保存到这个成员中,相当于告诉dma控制器将收到报文存放到mbuf位置
rxd->read.hdr_addr = dma_addr;
rxd->read.pkt_addr = dma_addr;
//将mbuf放到mbuf软件队列中
rxe[i].mbuf = mbuf;
}
}
2、发送队列与mbuf以及dma的关联
如果网卡有多个发送队列,则会对每个发送队列都进行寄存器的配置。这个接口大部分是对寄存器进行设置,例如将发送描述符队列的位置,队列总大小,队列的开始位置,队列的结束位置等信息写入寄存。通过写寄存器的方式,dma控制器直接读取寄存器就可以知道发送队列的相关信息。相关寄存器的设置,读者自行查看网卡的数据手册,很容易看懂的。
需要注意的是,将描述符队列的物理地址写入到寄存器后。dma通过读这个寄存器就知道了描述符队列的地址,进而dma知道从描述符指向的地址,也就是从mbuf获取报文后通过网卡发送出去。
static int eth_igb_start(struct rte_eth_dev *dev)
{
//接收队列与mbuf以及dma的关联
ret = eth_igb_tx_init(dev);
}
//设置发送队列
void eth_igb_tx_init(struct rte_eth_dev *dev)
{
//对每个发送队列,将mbuf, 队列,dma关联起来
for (i = 0; i < dev->data->nb_tx_queues; i++)
{
uint64_t bus_addr;
txq = dev->data->tx_queues[i];
bus_addr = txq->tx_ring_phys_addr;
//发送描述符队列的总长度
E1000_WRITE_REG(hw, E1000_TDLEN(txq->reg_idx), txq->nb_tx_desc * sizeof(union e1000_adv_tx_desc));
//发送描述符队列的高32位地址
E1000_WRITE_REG(hw, E1000_TDBAH(txq->reg_idx), (uint32_t)(bus_addr >> 32));
//发送描述符队列的低32位地址
E1000_WRITE_REG(hw, E1000_TDBAL(txq->reg_idx), (uint32_t)bus_addr);
//发送队列描述符尾部位置下标
E1000_WRITE_REG(hw, E1000_TDT(txq->reg_idx), 0);
//发送队列描述符头部位置下标
E1000_WRITE_REG(hw, E1000_TDH(txq->reg_idx), 0);
//发送队列描述符控制寄存器
txdctl = E1000_READ_REG(hw, E1000_TXDCTL(txq->reg_idx));
txdctl |= E1000_TXDCTL_QUEUE_ENABLE;
E1000_WRITE_REG(hw, E1000_TXDCTL(txq->reg_idx), txdctl);
}
}
从中可以看出,这个接口并没有从内存池中获取mbuf, 然后插入到软件发送空间struct igb_tx_entry。这个操作过程只有在真正发送报文的时候才会执行这个操作,例如e1000网卡发送报文的接口eth_igb_xmit_pkts就会执行这个将mbuf与描述符队列关联的操作。
3、中断掩码的设置
在eth_igb_start函数内部,会调用igb_intr_enable设置中断掩码。表示哪些中断发生时,会触发应用层设置的中断回调。dpdk目前只设置了链路中断掩码,也就是说只有网卡状态改变的时候,才会触发应用层中断回调。从中也可以看出dpdk没有设置收发报文的中断掩码,进而收发报文产生硬件中断时,并不会触发应用层设置的中断回调,这个读者可以自行加日志打印这个中断掩码的内容就可以发现。
//写寄存器开中断
static inline void igb_intr_enable(struct rte_eth_dev *dev)
{
struct e1000_interrupt *intr = E1000_DEV_PRIVATE_TO_INTR(dev->data->dev_private);
struct e1000_hw *hw = E1000_DEV_PRIVATE_TO_HW(dev->data->dev_private);
//设置中断掩码,目前dpdk只设置了链路状态中断掩码,并没有设置收发报文中断掩码
E1000_WRITE_REG(hw, E1000_IMS, intr->mask);
E1000_WRITE_FLUSH(hw);
}
eth_igb_start这个接口内部还执行了很多对寄存器的操作,篇幅有限不能对所有的寄存器进行详细分析。读者分析这个接口源码的时候,可以结合网卡的数据手册,也就不复杂了。
到此为止,应用层对网卡的配置过程就分析完成了。 下一篇文章将详细分析网卡收发包过程。