理解DPDK的收发包过程
本文整理下之前的学习笔记,基于DPDK17.11版本源码,主要分析一下收发包流程。
使用DPDK的APP收发报文流程如下
main
//环境抽象层初始化,比如网卡,cpu,内存等
rte_eal_init(argc, argv);
//为rx和tx队列分配内存,将用户指定的配置信息dev_conf保存到dev
rte_eth_dev_configure(portid, 1, 1, &port_conf);
//分配网卡接收队列结构体,接收ring硬件描述符和软件ring等内存
rte_eth_rx_queue_setup(portid, 0, nb_rxd,
rte_eth_dev_socket_id(portid),
NULL,
l2fwd_pktmbuf_pool);
//分配网卡发送队列结构体,发送ring硬件描述符等内存
rte_eth_tx_queue_setup(portid, 0, nb_txd,
rte_eth_dev_socket_id(portid),
NULL);
//启动网卡,设置网卡寄存器,将网卡和系统内存关联起来
rte_eth_dev_start(portid);
while (1) {
//接收报文
rte_eth_rx_burst(portid, 0, pkts_burst, MAX_PKT_BURST);
//处理报文
//发送报文,此函数只是将报文放到一个buffer中,满32个后才调用rte_eth_tx_burst真正发送
rte_eth_tx_buffer(dst_port, 0, buffer, m);
}
以ixgbe驱动为例,相关的数据结构如下
收包流程
我们都知道网卡会通过DMA将报文放在系统内存中,那网卡如何知道应该放在哪里呢?如何将网卡和系统内存关联起来?这需要用到网卡的几个寄存器:
RDBAL(Receive Descriptor Base Address Low),
RDBAH(Receive Descriptor Base Address High)
RDLEN(Receive Descriptor Length)
驱动初始化时会分配一块内存,将这块内存的起始物理地址(64位)写到寄存器RDBAL(保存物理地址的低32位)和RDBAH(保存物理地址的高32位),然后将这块内存的大小写到寄存器RDLEN中。这块内存称为硬件描述符,大小为接收队列硬件描述符个数乘接收队列硬件描述符大小。
一个接收队列硬件描述符大小为16字节,有两种格式: 读格式和回写格式。
读格式是从网卡角度来说的,由驱动将mbuf的物理地址写到packet buffer address字段,网卡读取此字段获取内存物理地址,收到的报文就可以存到此内存。
回写格式也是从网卡角度来说,网卡将报文写到指定的内存后,就会以下面的格式将报文的相关信息回写到描述符中,最后设置DD位(第二个8字节的最低位),驱动通过判断DD位是否为1来接收报文。
总结一下接收队列硬件描述符就是一块内存,网卡先以读格式获取内存的物理地址,将报文写到内存后,就以回写格式将报文额外信息写到描述符中,驱动可以以回写格式读取描述符,获取报文的长度,类型等信息。
网卡和内存关联起来后,就可以收取报文了,此时又用到两个寄存器: RDH(Receive Descriptor Head)和RDT(Receive Descriptor Tail)。
RDH为头指针,指向第一个可用描述符,网卡收取报文并回写成功后,由网卡来移动RDH到下一个可用描述符。
RDT为尾指针,指向最后一个可用描述符,RDH和RDT之间的描述符为网卡可用描述符,RDT由驱动来移动,驱动从第一个描述符开始,轮询DD位是否为1,为1就认为此描述符对应的mbuf有报文,此时会申请新的mbuf,将新mbuf物理地址写到此描述符的pkt_addr,并将DD位置0,这样的话此描述符就又可用被网卡使用了,同时将老的有报文的mbuf返回给用户。描述符再次可用后,驱动就可以更新RDT指向此描述符,为了性能考虑不会每次都会更新RDT,而是等可用描述符超过一定阈值(rx_free_thresh)才更新一次。
如下为接收描述符的格式,是union类型,可同时有读和回写两种格式。
/* Receive Descriptor - Advanced */
union ixgbe_adv_rx_desc {
struct {
__le64 pkt_addr; /* Packet buffer address */
__le64 hdr_addr; /* Header buffer address */
} read;
struct {
struct {
union {
__le32 data;
struct {
__le16 pkt_info; /* RSS, Pkt type */
__le16 hdr_info; /* Splithdr, hdrlen */
} hs_rss;
} lo_dword;
union {
__le32 rss; /* RSS Hash */
struct {
__le16 ip_id; /* IP id */
__le16 csum; /* Packet Checksum */
} csum_ip;
} hi_dword;
} lower;
struct {
__le32 status_error; /* ext status/error */
__le16 length; /* Packet length */
__le16 vlan; /* VLAN tag */
} upper;
} wb; /* writeback */
};
了解网卡接收原理后,下面从代码角度看一下实现,大概分为如下几步:
a. 分配接收队列硬件描述符rx_ring,分配软件ring sw_ring
b. 将接收队列硬件描述符的物理地址和长度写到寄存器
c. 分配mbuf,将mbuf接收报文的物理地址赋给接收队列硬件描述符 rx_ring->pkt_addr,虚拟地址赋给 sw_ring
d. 设置头尾寄存器,头指针寄存器RDH为0,指向第一个可用描述符,尾指针寄存器RDT指向最后一个可用描述符
a. rte_eth_rx_queue_setup
接收队列设置
- 分配队列结构体 struct ixgbe_rx_queue
- 分配接收ring硬件描述符(一般为4096),每个描述符16字节,保存到 rxq->rx_ring
- 分配软件ring,用来保存mbuf,保存到 rxq->sw_ring
rte_eth_rx_queue_setup -> ixgbe_dev_rx_queue_setup
int __attribute__((cold))
ixgbe_dev_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)
const struct rte_memzone *rz;
struct ixgbe_rx_queue *rxq;
struct ixgbe_hw *hw;
uint16_t len;
struct ixgbe_adapter *adapter = (struct ixgbe_adapter *)dev->data->dev_private;
hw = IXGBE_DEV_PRIVATE_TO_HW(dev->data->dev_private);
/* First allocate the rx queue data structure */
rxq = rte_zmalloc_socket("ethdev RX queue", sizeof(struct ixgbe_rx_queue),
RTE_CACHE_LINE_SIZE, socket_id);
rxq->mb_pool = mp;
rxq->nb_rx_desc = nb_desc;
rxq->rx_free_thresh = rx_conf->rx_free_thresh;
rxq->queue_id = queue_idx;
rxq->reg_idx = (uint16_t)((RTE_ETH_DEV_SRIOV(dev).active == 0) ?
queue_idx : RTE_ETH_DEV_SRIOV(dev).def_pool_q_idx + queue_idx);
rxq->port_id = dev->data->port_id;
rxq->crc_len = (uint8_t) ((dev->data->dev_conf.rxmode.hw_strip_crc) ? 0 : ETHER_CRC_LEN);
rxq->drop_en = rx_conf->rx_drop_en;
rxq->rx_deferred_start = rx_conf->rx_deferred_start;
#define IXGBE_MAX_RING_DESC 4096 /* replicate define from rxtx */
#define RTE_PMD_IXGBE_RX_MAX_BURST 32
#define RX_RING_SZ ((IXGBE_MAX_RING_DESC + RTE_PMD_IXGBE_RX_MAX_BURST) * \
sizeof(union ixgbe_adv_rx_desc))
/*
* Allocate RX ring hardware descriptors. A memzone large enough to
* handle the maximum ring size is allocated in order to allow for
* resizing in later calls to the queue setup function.
*/
//分配接收队列硬件描述符内存,注意这里是按最大值分配。
//注意要128字节对齐,因为82599网卡芯片手册规则物理地址必须是128字节对齐
rz = rte_eth_dma_zone_reserve(dev, "rx_ring", queue_idx,
RX_RING_SZ, IXGBE_ALIGN, socket_id);
/*
* Zero init all the descriptors in the ring.
*/
memset(rz->addr, 0, RX_RING_SZ);
rxq->rdt_reg_addr =
IXGBE_PCI_REG_ADDR(hw, IXGBE_RDT(rxq->reg_idx));
rxq->rdh_reg_addr =
IXGBE_PCI_REG_ADDR(hw, IXGBE_RDH(rxq->reg_idx));
//保存接收队列硬件描述符的物理地址
rxq->rx_ring_phys_addr = rz->iova;
//保存接收队列硬件描述符的虚拟地址
rxq->rx_ring = (union ixgbe_adv_rx_desc *) rz->addr;
/*
* Allocate software ring. Allow for space at the end of the
* S/W ring to make sure look-ahead logic in bulk alloc Rx burst
* function does not access an invalid memory region.
*/
len = nb_desc;
if (adapter->rx_bulk_alloc_allowed)
len += RTE_PMD_IXGBE_RX_MAX_BURST;
//分配软件ring内存,这里的大小为参数指定的描述符个数 nb_desc
rxq->sw_ring = rte_zmalloc_socket("rxq->sw_ring",
sizeof(struct ixgbe_rx_entry) * len,
RTE_CACHE_LINE_SIZE, socket_id);
//将接收队列结构保存到对应位置
dev->data->rx_queues[queue_idx] = rxq;
b. ixgbe_dev_rx_init
将接收队列硬件描述符的物理地址写到网卡寄存器RDBAL和RDBAH,将接收队列硬件描述符的长度写到网卡寄存器RDLEN。
rte_eth_dev_start -> ixgbe_dev_start -> ixgbe_dev_rx_init
接收队列初始化
/*
* Initializes Receive Unit.
*/
int __attribute__((cold))
ixgbe_dev_rx_init(struct rte_eth_dev *dev)
{
struct ixgbe_hw *hw;
struct ixgbe_rx_queue *rxq;
uint64_t bus_addr;
uint32_t rxctrl;
uint32_t fctrl;
uint32_t hlreg0;
uint16_t i;
struct rte_eth_rxmode *rx_conf = &dev->data->dev_conf.rxmode;
int rc;
hw = IXGBE_DEV_PRIVATE_TO_HW(dev->data->dev_private);
/*
* Make sure receives are disabled while setting
* up the RX context (registers, descriptor rings, etc.).
*/
//确保网卡的接收功能是关闭的
rxctrl = IXGBE_READ_REG(hw, IXGBE_RXCTRL);
IXGBE_WRITE_REG(hw, IXGBE_RXCTRL, rxctrl & ~IXGBE_RXCTRL_RXEN);
//使能接收广播,丢弃pause报文
/* Enable receipt of broadcasted frames */
fctrl = IXGBE_READ_REG(hw, IXGBE_FCTRL);
fctrl |= IXGBE_FCTRL_BAM; /* Broadcast Accept Mode */
fctrl |= IXGBE_FCTRL_DPF; /* Discard Pause Frame */
fctrl |= IXGBE_FCTRL_PMCF; /* Pass MAC Control Frames */
IXGBE_WRITE_REG(hw, IXGBE_FCTRL, fctrl);
/*
* Configure CRC stripping, if any.
*/
//设置硬件自动去掉crc
hlreg0 = IXGBE_READ_REG(hw, IXGBE_HLREG0);
if (rx_conf->hw_strip_crc)
hlreg0 |= IXGBE_HLREG0_RXCRCSTRP;
else
hlreg0 &= ~IXGBE_HLREG0_RXCRCSTRP;
/*
* Configure jumbo frame support, if any.
*/
//使能接收巨帧
if (rx_conf->jumbo_frame == 1) {
hlreg0 |= IXGBE_HLREG0_JUMBOEN;
maxfrs = IXGBE_READ_REG(hw, IXGBE_MAXFRS);
maxfrs &= 0x0000FFFF;
maxfrs |= (rx_conf->max_rx_pkt_len << 16);
IXGBE_WRITE_REG(hw, IXGBE_MAXFRS, maxfrs);
} else
hlreg0 &= ~IXGBE_HLREG0_JUMBOEN;
IXGBE_WRITE_REG(hw, IXGBE_HLREG0, hlreg0);
/* Setup RX queues */
for (i = 0; i < dev->data->nb_rx_queues; i++) {
rxq = dev->data->rx_queues[i];
//将接收队列硬件描述符的物理地址写到网卡接收描述符寄存器中
/* Setup the Base and Length of the Rx Descriptor Rings */
bus_addr = rxq->rx_ring_phys_addr;
IXGBE_WRITE_REG(hw, IXGBE_RDBAL(rxq->reg_idx), (uint32_t)(bus_addr & 0x00000000ffffffffULL));
IXGBE_WRITE_REG(hw, IXGBE_RDBAH(rxq->reg_idx), (uint32_t)(bus_addr >> 32));
//将用户请求的nb_tx_desc个数的接收队列硬件描述符长度写到寄存器
IXGBE_WRITE_REG(hw, IXGBE_RDLEN(rxq->reg_idx), rxq->nb_rx_desc * sizeof(union ixgbe_adv_rx_desc));
//头尾指针先设置为0
IXGBE_WRITE_REG(hw, IXGBE_RDH(rxq->reg_idx), 0);
IXGBE_WRITE_REG(hw, IXGBE_RDT(rxq->reg_idx), 0);
}
//根据设置选择不同的接收函数,后面会以 ixgbe_recv_pkts 为例说明
ixgbe_set_rx_function(dev);
...
return 0;
}
c. ixgbe_dev_rx_queue_start
申请mbuf,将mbuf存放报文的物理地址设置到接收队列硬件描述符的pkt_addr字段,这样网卡就知道收到报文后将报文放在哪里了。
rte_eth_dev_start -> ixgbe_dev_start -> ixgbe_dev_rxtx_start -> ixgbe_dev_rx_queue_start
/*
* Start Receive Units for specified queue.
*/
int __attribute__((cold))
ixgbe_dev_rx_queue_start(struct rte_eth_dev *dev, uint16_t rx_queue_id)
{
struct ixgbe_hw *hw;
struct ixgbe_rx_queue *rxq;
uint32_t rxdctl;
int poll_ms;
hw = IXGBE_DEV_PRIVATE_TO_HW(dev->data->dev_private);
if (rx_queue_id < dev->data->nb_rx_queues) {
rxq = dev->data->rx_queues[rx_queue_id];
//分配mbuf,填充到 rxq->sw_ring 中
/* Allocate buffers for descriptor rings */
if (ixgbe_alloc_rx_queue_mbufs(rxq) != 0) {
PMD_INIT_LOG(ERR, "Could not alloc mbuf for queue:%d",
rx_queue_id);
return -1;
}
...
//头指针为0,指向第一个可用描述符
IXGBE_WRITE_REG(hw, IXGBE_RDH(rxq->reg_idx), 0);
//尾指针为最大描述符,指向最后一个可用描述符
IXGBE_WRITE_REG(hw, IXGBE_RDT(rxq->reg_idx), rxq->nb_rx_desc - 1);
dev->data->rx_queue_state[rx_queue_id] = RTE_ETH_QUEUE_STATE_STARTED;
}
return 0;
}
static int __attribute__((cold))
ixgbe_alloc_rx_queue_mbufs(struct ixgbe_rx_queue *rxq)
{
struct ixgbe_rx_entry *rxe = rxq->sw_ring;
uint64_t dma_addr;
unsigned int i;
/* Initialize software ring entries */
for (i = 0; i < rxq->nb_rx_desc; i++) {
volatile union ixgbe_adv_rx_desc *rxd;
//分配mbuf
struct rte_mbuf *mbuf = rte_mbuf_raw_alloc(rxq->mb_pool);
mbuf->data_off = RTE_PKTMBUF_HEADROOM;
mbuf->port = rxq->port_id;
//获取mbuf存放报文的物理地址,注意不是mbuf的首地址
dma_addr =
rte_cpu_to_le_64(rte_mbuf_data_iova_default(mbuf));
rxd = &rxq->rx_ring[i];
//清空接收描述符的DD位
rxd->read.hdr_addr = 0;
//将mbuf接收报文的物理地址赋给描述符
rxd->read.pkt_addr = dma_addr;
rxe[i].mbuf = mbuf;
}
return 0;
}
最后使能网卡的接收功能 hw->mac.ops.enable_rx_dma(hw, rxctrl);
下面是正式收包流程,还以ixgbe驱动为例 rte_eth_rx_burst -> ixgbe_recv_pkts
uint16_t
ixgbe_recv_pkts(void *rx_queue, struct rte_mbuf **rx_pkts, uint16_t nb_pkts)
struct ixgbe_rx_queue *rxq;
volatile union ixgbe_adv_rx_desc *rx_ring;
volatile union ixgbe_adv_rx_desc *rxdp;
struct ixgbe_rx_entry *sw_ring;
struct ixgbe_rx_entry *rxe;
struct rte_mbuf *rxm;
struct rte_mbuf *nmb;
union ixgbe_adv_rx_desc rxd;
uint64_t dma_addr;
uint32_t staterr;
uint32_t pkt_info;
uint16_t pkt_len;
uint16_t rx_id;
uint16_t nb_rx;
uint16_t nb_hold;
uint64_t pkt_flags;
uint64_t vlan_flags;
nb_rx = 0;
nb_hold = 0;
rxq = rx_queue;
rx_id = rxq->rx_tail;
rx_ring = rxq->rx_ring;
sw_ring = rxq->sw_ring;
vlan_flags = rxq->vlan_flags;
while (nb_rx < nb_pkts) {
/*
* The order of operations here is important as the DD status
* bit must not be read after any other descriptor fields.
* rx_ring and rxdp are pointing to volatile data so the order
* of accesses cannot be reordered by the compiler. If they were
* not volatile, they could be reordered which could lead to
* using invalid descriptor fields when read from rxd.
*/
//获取硬件描述符
rxdp = &rx_ring[rx_id];
//获取硬件描述符的 status_error
staterr = rxdp->wb.upper.status_error;
//判断DD位是否被硬件置1,为1说明有报文,不是1就break
if (!(staterr & rte_cpu_to_le_32(IXGBE_RXDADV_STAT_DD)))
break;
rxd = *rxdp;
//分配一个新的mbuf
nmb = rte_mbuf_raw_alloc(rxq->mb_pool);
nb_hold++;
//获取软件ring的当前元素
rxe = &sw_ring[rx_id];
//尾指针加1
rx_id++;
//如果达到最大值,则翻转为0,相当于环形效果
if (rx_id == rxq->nb_rx_desc)
rx_id = 0;
//从rxe->mbuf取出mbuf地址,此mbuf已经有报文内容
rxm = rxe->mbuf;
//rxe->mbuf被赋予一个新的mbuf
rxe->mbuf = nmb;
//获取新mbuf的物理地址
dma_addr =
rte_cpu_to_le_64(rte_mbuf_data_iova_default(nmb));
//hdr_addr清0,就会将DD位也清0,否则下次循环到此描述符就会错误的认为有报文
rxdp->read.hdr_addr = 0;
//将mbuf的物理地址赋给描述符,网卡就可以把新报文写到新mbuf中
rxdp->read.pkt_addr = dma_addr;
//从描述符的wb字段获取报文相关的信息,包括长度,vlanid等,并填到mbuf中
pkt_len = (uint16_t) (rte_le_to_cpu_16(rxd.wb.upper.length) - rxq->crc_len);
rxm->data_off = RTE_PKTMBUF_HEADROOM;
rte_packet_prefetch((char *)rxm->buf_addr + rxm->data_off);
rxm->nb_segs = 1;
rxm->next = NULL;
rxm->pkt_len = pkt_len;
rxm->data_len = pkt_len;
rxm->port = rxq->port_id;
pkt_info = rte_le_to_cpu_32(rxd.wb.lower.lo_dword.data);
/* Only valid if PKT_RX_VLAN set in pkt_flags */
rxm->vlan_tci = rte_le_to_cpu_16(rxd.wb.upper.vlan);
...
/*
* Store the mbuf address into the next entry of the array
* of returned packets.
*/
//将已经有报文的mbuf返回给调用者
rx_pkts[nb_rx++] = rxm;
}
//更新尾指针
rxq->rx_tail = rx_id;
//nb_hold表示本次调用成功读取的报文个数,也同时意味着本次调用重新可用mbuf的个数,
//因为读取一次报文,就会分配新的mbuf,并赋给描述符,这个描述符就可以被网卡再次使用。
//rxq->nb_rx_hold是累计可用的描述符个数。
nb_hold = (uint16_t) (nb_hold + rxq->nb_rx_hold);
//如果累计的可用描述符个数超过了阈值,就要更新网卡能看到的描述符尾指针了。
//如果不更新尾指针,随着收包头指针一直增加,和尾指针重合时,就没有可用描述符了。
if (nb_hold > rxq->rx_free_thresh) {
PMD_RX_LOG(DEBUG, "port_id=%u queue_id=%u rx_tail=%u "
"nb_hold=%u nb_rx=%u",
(unsigned) rxq->port_id, (unsigned) rxq->queue_id,
(unsigned) rx_id, (unsigned) nb_hold,
(unsigned) nb_rx);
rx_id = (uint16_t) ((rx_id == 0) ?
(rxq->nb_rx_desc - 1) : (rx_id - 1));
IXGBE_PCI_REG_WRITE(rxq->rdt_reg_addr, rx_id);
//清空计数
nb_hold = 0;
}
//更新nb_rx_hold
rxq->nb_rx_hold = nb_hold;
return nb_rx;
发包流程
发送报文时也需要将网卡和内存关联起来,即将要发送的报文地址告诉网卡,这也是通过硬件描述符来实现的。
发送队列硬件描述符格式如下,也分为读和回写两种格式,都从网卡的角度来说。
对于读格式,驱动将报文的物理地址设置到第一个8字节的address字段,网卡读取此字段就能获取发送报文的物理地址,同时驱动也会设置第二个8字节的相关字段,比如报文长度,是否是最后一个报文段,何时回写等,网卡根据这些信息正确的将报文发送出去。
对于回写格式,只有一个字段有效,第二个8字节的第32位,此位代表DD(Descriptor Done)位,网卡完成报文发送后,并且此描述符设置了RS标志位,则会将此DD位设置为1,驱动读取此位就知道此描述符及它之前的描述符都可以被驱动使用。
DCMD字段中的RS(report status)位用来控制网卡何时回写DD位。注意和接收方向的区别,在接收方向网卡每收到一个报文就会回写一次接收描述符,将报文长度等信息填写到接收描述符,这是必须的,否则驱动怎么知道接收的报文多长呢,但是发送方向网卡不需要每发送一个报文就回写一次,并且每个报文回写会影响性能,驱动只关心报文是否发送成功,对应的发送描述符是否可用,可以通过参数tx_rs_thresh设置网卡多久回写一次,如果发送报文个数超过tx_rs_thresh,就会设置DCMD的RS位。
发送方向代码流程和接收方向大体相似,不再赘述。
总结
在pmd中,对于接收方向(从网卡收数据)来说,初始状态head指针指向base,tail指向指向base+len。网卡是生产者,通过移动head指针将数据放在mbuf中,驱动是消费者,将接收ring中buf_addr换成新mbuf的地址,旧的mbuf可以返回给应用程序来处理。驱动通过移动tail指针,将接收描述符还给网卡,但是并没有每次收包都更新收包队列尾部索引寄存器,而是在可释放的收包描述符数量达到一个阈值(rx_free_thresh)的时候才真正更新收包队列尾部索引寄存器。设置合适的可释放描述符数量阈值,可以减少没有必要的过多的收包队列尾部索引寄存器的访问,改善收包的性能。
对于发送方向来说,初始状态head和tail都指向base。驱动是生产者,发包时,先将发送数据的物理地址赋值给发送描述符的txd->read.buffer_addr,最后通过移动tail指针通知网卡有数据要发送。网卡是消费者,当获知tail指针移动就会发送数据,网卡发送完数据,会移动head指针。
Q && A
a. pmd发包时,如何通知网卡有新数据需要发送?
更新tail指针时就会触发网卡发送数据。比如在ixgbe_xmit_pkts函数最后,都会更新tail指针: IXGBE_PCI_REG_WRITE_RELAXED(txq->tdt_reg_addr, tx_id);
从网卡datasheet也能看到相关说明:
b. 网卡发送成功后,驱动怎么知道描述符可用?
从datasheet看到,有四种方法,默认采用第三种,即通过DD标志位获取
c. 网卡驱动发送方向,mbuf什么时候释放?
许多驱动程序并没有在数据包传输后立即将mbuf释放回到mempool或本地缓存中。相反,他们将mbuf留在Tx环中,当需要在Tx环中插入,或者 tx_rs_thresh 已经超过时,执行批量释放。
推荐阅读
-
14-傅里叶变换的代码实现-一、numpy实现傅里叶变换和逆傅里叶变换 1.numpy实现傅里叶变换numpy.fft.fft2实现傅里叶变换,返回一个复数数组(complex ndarray),也就是频谱图像numpy.fft.fftshift将零频率分量移到频谱中心(将左上角的低频区域,移到中心位置) 20*np.log(np.abs(fshift))设置频谱的范围。可以理解为,之前通过傅里叶变换得到复数的数组,是不能通过图像的方法展示出来的,需要转换为灰度图像(映射到[0,255]区间)需要注意的是1> 傅里叶得到低频、高频信息,针对低频、高频处理能够实现不同的目的2> 傅里叶过程是可逆的,图像经过傅里叶变换、逆傅里叶变换后,能够恢复到原始图像3> 在频域对图像进行处理,在频域的处理会反映在逆变换图像上 # 将绘制的图显示在窗口 %matplotlib qt5 import cv2 import numpy as np import matplotlib.pyplot as plt img = cv2.imread(r"image\lena.bmp",cv2.IMREAD_GRAYSCALE) # 傅里叶变换 f = np.fft.fft2(img) # 移动中心位置 fshift = np.fft.fftshift(f) # 调整值范围 result = 20*np.log(np.abs(fshift)) plt.subplot(1,2,1) plt.imshow(img,cmap=plt.cm.gray) plt.title("original") plt.axis("off") plt.subplot(1,2,2) plt.imshow(result,cmap=plt.cm.gray) plt.title("result") plt.axis("off") plt.show 傅里叶变换的频谱图像: 2.numpy实现逆傅里叶变换numpy.fft.ifft2实现逆傅里叶变换,返回一个复数数组(complex ndarray)numpy.fft.ifftshiftfftshift函数的逆函数,将中心位置的低频,重新移到左上角iimg = np.abs(逆傅里叶变化结果)设置值的范围,映射到[0,255]区间 # 将绘制的图显示在窗口 %matplotlib qt5 import cv2 import numpy as np import matplotlib.pyplot as plt img = cv2.imread(r"image\boat.bmp",cv2.IMREAD_GRAYSCALE) # 傅里叶变换 f = np.fft.fft2(img) fshift = np.fft.fftshift(f) # 逆傅里叶变换 ishift = np.fft.ifftshift(fshift) iimg = np.fft.ifft2(ishift) iimg = np.abs(iimg) plt.subplot(1,2,1) plt.imshow(img,cmap=plt.cm.gray) plt.title("original") plt.axis("off") plt.subplot(1,2,2) plt.imshow(iimg,cmap=plt.cm.gray) plt.title("iimg") plt.axis("off") plt.show 将一副图像,进行傅里叶变换和逆傅里叶变换后,进行对比(一样的) 实例:通过numpy实现高通滤波,保留图像的边缘信息 获取图像的形状rows,cols = img.shape获取图像的中心点crow,ccol = int(rows/2),int(cols/2)将频谱图像的中心区域(低频区域)设置为0(黑色)fshift[crow-30:crow+30,ccol-30:ccol+30] = 0 # 将绘制的图显示在窗口 %matplotlib qt5 import cv2 import numpy as np import matplotlib.pyplot as plt img = cv2.imread(r"image\boat.bmp",cv2.IMREAD_GRAYSCALE) # 傅里叶变换 f = np.fft.fft2(img) fshift = np.fft.fftshift(f) # 高通滤波 rows,cols = img.shape crow,ccol = int(rows/2),int(cols/2) fshift[crow-30:crow+30,ccol-30:ccol+30] = 0 # 逆傅里叶变换 ishift = np.fft.ifftshift(fshift) iimg = np.fft.ifft2(ishift) iimg = np.abs(iimg) plt.subplot(1,2,1) plt.imshow(img,cmap=plt.cm.gray) plt.title("original") plt.axis("off") plt.subplot(1,2,2) plt.imshow(iimg,cmap=plt.cm.gray) plt.title("iimg") plt.axis("off") plt.show 使用numpy实现高通滤波的实验结果: 二、opencv实现傅里叶变换和逆傅里叶变换 1.opencv实现傅里叶变换 返回结果 = cv2.dft(原始图像,转换标识)1> 返回结果:是双通道的,第一个通道是结果的实数部分,第二个通道是结果的虚数部分2> 原始图像:输入图像要首先转换成np.float32(img)格式3> 转换标识:flags = cv2.DFT_COMPLEX_OUTPUT,输出一个复数阵列numpy.fft.fftshift将零频率分量移到频谱中心(将左上角的低频区域,移到中心位置)调整频谱的范围,将上面频谱图像的复数数组,转换为可以显示的灰度图像(映射到[0,255]区间)返回值 = 20*np.log(cv2.magnitude(参数1,参数2))1> 参数1:浮点型X坐标值,也就是实部2> 参数2:浮点型Y坐标值,也就是虚部 # 将绘制的图显示在窗口 %matplotlib qt5 import cv2 import numpy as np import matplotlib.pyplot as plt img = cv2.imread(r"image\lena.bmp",cv2.IMREAD_GRAYSCALE) # 傅里叶变换 dft = cv2.dft(np.float32(img),flags = cv2.DFT_COMPLEX_OUTPUT) # 移动中心位置 dftShift = np.fft.fftshift(dft) # 调整频谱的范围 result = 20*np.log(cv2.magnitude(dftShift[:,:,0],dftShift[:,:,1])) plt.subplot(1,2,1) plt.imshow(img,cmap=plt.cm.gray) plt.title("original") plt.axis("off") plt.subplot(1,2,2) plt.imshow(result,cmap=plt.cm.gray) plt.title("result") plt.axis("off") plt.show 傅里叶变换的频谱图像: 2.opencv实现逆傅里叶变换返回结果 = cv2.idft(原始数据)1> 返回结果:取决于原始数据的类型和大小2> 原始数据:实数或者复数均可numpy.fft.ifftshiftfftshift函数的逆函数,将中心位置的低频,重新移到左上角调整频谱的范围,映射到[0,255]区间返回值 = cv2.magnitude(参数1,参数2)1> 参数1:浮点型X坐标值,也就是实部2> 参数2:浮点型Y坐标值,也就是虚部 # 将绘制的图显示在窗口 %matplotlib qt5 import cv2 import numpy as np import matplotlib.pyplot as plt img = cv2.imread(r"image\lena.bmp",cv2.IMREAD_GRAYSCALE) # 傅里叶变换 dft = cv2.dft(np.float32(img),flags = cv2.DFT_COMPLEX_OUTPUT) dftShift = np.fft.fftshift(dft) # 逆傅里叶变换 ishift = np.fft.ifftshift(dftShift) iimg = cv2.idft(ishift) iimg = cv2.magnitude(iimg[:,:,0],iimg[:,:,1]) plt.subplot(1,2,1) plt.imshow(img,cmap=plt.cm.gray) plt.title("original") plt.axis("off") plt.subplot(1,2,2) plt.imshow(iimg,cmap=plt.cm.gray) plt.title("inverse") plt.axis("off") plt.show 将一副图像,进行傅里叶变换和逆傅里叶变换后,进行对比(一样的) 实例:通过opencv实现低通滤波,模糊一副图像
-
理解PCIE扫描的过程
-
理解DPDK的收发包过程
-
理解Oracle中的存储过程与函数的本质差异
-
【摩尔线程+Colossal-AI强强联手】MusaBert登上CLUE榜单TOP10:技术细节揭秘 - 技术实力:摩尔线程凭借"软硬兼备"的技术底蕴,让MusaBert得以从底层优化到顶层。其内置多功能GPU配备AI加速和并行计算模块,提供了全面的AI与科学计算支持,为AI推理和低资源条件下的大模型训练等场景带来了高效、经济且环保的算力。 - 算法层面亮点:依托Colossal-AI AI大模型开发系统,MusaBert在训练过程中展现出了卓越的并行性能与易用性,特别在预处理阶段对DataLoader进行了优化,适应低资源环境高效处理海量数据。同时,通过精细的建模优化、领域内数据增强以及Adan优化器等手段,挖掘和展示了预训练语言模型出色的语义理解潜力。基于MusaBert,摩尔线程自主研发的MusaSim通过对比学习方法微调,结合百万对标注数据,MusaSim在多个任务如语义相似度、意图识别和情绪分析中均表现出色。 - 数据资源丰富:MusaBert除了自家高质量语义相似数据外,还融合了悟道开源200GB数据、CLUE社区80GB数据,以及浪潮公司提供的1TB高质量数据,保证模型即便在较小规模下仍具备良好性能。 当前,MusaBert已成功应用于摩尔线程的智能客服与数字人项目,并广泛服务于语义相似度、情绪识别、阅读理解与声韵识别等领域。为了降低大模型开发和应用难度,MusaBert及其相关高质量模型代码已在Colossal-AI仓库开源,可快速训练优质中文BERT模型。同时,通过摩尔线程与潞晨科技的深度合作,仅需一张多功能GPU单卡便能高效训练MusaBert或更大规模的GPT2模型,显著降低预训练成本,进一步推动双方在低资源大模型训练领域的共享目标。 MusaBert荣登CLUE榜单TOP10,象征着摩尔线程与潞晨科技联合研发团队在中文预训练研究领域的领先地位。展望未来,双方将携手探索更大规模的自然语言模型研究,充分运用上游数据资源,产出更为强大的模型并开源。持续强化在摩尔线程多功能GPU上的大模型训练能力,特别是在消费级显卡等低资源环境下,致力于降低使用大模型训练的门槛与成本,推动人工智能更加普惠。而潞晨科技作为重要合作伙伴,将继续发挥关键作用。
-
理解指令执行过程的四个关键步骤:总线周期与时钟周期详解
-
理解POMDP详解 - 第一章:透视部分可观测的马尔可夫决策过程
-
深入理解 Kubernetes 中的Pod驱逐过程拆解
-
理解CNN:卷积神经网络的前向传播过程
-
理解神经网络的正向计算与反向调整过程 - 公式详解