DPDK
- eal初始化
- 内存管理:大页,内存池
- 驱动
开启调试信息
make config T=x86_64-native-linuxapp-gcc
export EXTRA_CFLAGS='-O0 -g3 -ggdb'
make -j8
dpdk通过makefile编译
meson + ninja没学过,太麻烦了,可以参考dpdk17的文档,里面有介绍make编译方式。
https://doc.dpdk.org/guides-17.11/index.html
x86_64-native-linuxapp-gcc的含义:ARCH-MACHINE-EXECENV-TOOLCHAIN
- ARCH:i686, x86_64, ppc_64, arm64
- MACHINE:native, power8, armv8a
- EXECENV:linuxapp, bsdapp
- TOOLCHAIN:gcc, icc
配置
make config -T=x86_64-native-linuxapp-gcc
安装
make install -T=x86_64-native-linuxapp-gcc
注意:构建的igb_uio和kni必须与运行dpdk的内核版本一致,如果编译的内核版本和运行dpdk的内核版本不一致,可以通过RTE_KERNELDIR环境变量指定具体内核版本的代码的目录
修改对应的dpdk配置
cd x86_64-native-linuxapp-gcc
vi .config
make
外部应用使用makefile添加dpdk依赖,必须指定以下2个环境变量
RTE_SDK:指向dpdk目录
RTE_TARGET:用于编译的目标,例如:x86_64-native-linuxapp-gcc
参考:
export RTE_SDK=/path/to/DPDK
export RTE_TARGET=x86_64-native-linuxapp-icc
make -f /path/to/my_app/Makefile S=/path/to/my_app O=/path/to/build_dir
S=是指定源码位置
O=是输出位置
- ${RTE_SDK}/mk/rte.extapp.mk:构建二进制可执行文件
- ${RTE_SDK}/mk/rte.extlib.mk:构建静态库(.a)
- ${RTE_SDK}/mk/rte.extobj.mk:构建objects(.o)
run-to-complete模式:特定端口的RX描述符环通过API进行了针对数据包进行轮询。然后将数据包在同一核心上处理,并通过API放置在端口的TX描述符环上以进行传输。
pipeline模式:一个核心通过API进行了一个或多个端口的RX描述符环。收到数据包并通过ring传递给另一个核心。另一个核心继续处理数据包,然后可以通过API将其放置在端口的TX描述符ring上。
Receive Side Scaling (RSS) :接收侧缩放(RSS)
Data Center Bridging (DCB):数据中心桥接(DCB)
cpu初始化
根据/sys/devices/system/cpu/cpu%u/topology/core_id来确认cpu%u对应的coreid,注意coreid != cpuid,同时也根据此文件判断是否存在此cpu(是否启用)。
根据/sys/devices/system/node/node%u/cpu%u文件是否存在来确认cpuid对应cpu所在的numa节点。(RTE_MAX_NUMA_NODES和RTE_MAX_LCORE是编译时就配置好的在.config文件中)
初始化lcore_config的cpuid、对应的socket id等,以及设置了rte_config中numa_nodes映射(numa到socket id的映射)、cpu数量、numa节点数量。
日志输出
全局变量rte_logs,记录当前输出的日志文件,默认rte_logs.file为null,如果default_log_stream不为null会取此值(通过为null,通过eal_log_set_default方法设置),如果default_log_stream也为null则返回stderr。
如果当前记录的level大于rte_logs.level会忽略此条日志,如果logtype(例如EAL)无效,大于等于rte_logs.dynamic_types_len的值也会忽略此条日志,如果level大于rte_logs.dynamic_types[logtype].loglevel对应的日志级别也会忽略此条日志。
最后记录前把level和logtype写到per_lcore_log_cur_msg线程变量中。
然后写到对应文件,然后fflush一下返回。
dpdk的日志级别,总共8个,默认为RTE_LOG_DEBUG(值为8,看rte_log_init方法),还有固定32个动态日志类型,默认日志级别为RTE_LOG_INFO(值为7)。
解析参数
解析完参数后,如果internal_config.no_shconf为0,则创建dpdk运行时目录/var/run/dpdk/rte。
如果没有指定lcore参数,则会通过pthread_getaffinity_np获取当前线程的cpu亲和的cpu掩码,并减少rte_config的lcore_count的值(减少dpdk可用cpu数量)。
如果master的lcore没有指定,则设置rte_config->master_lcore为第1个可用lcore(一般是0)。
设置ctrl线程的亲和性,默认是取除了可用lcore之外的cpu,如果全部lcore都使用了(就是role为ROLE_RTE,不指定lcore就是全部lcore的role都为ROLE_RTE),就使用master lcore同一个。
把参数中设置的socket_mem累加到internal_config->memory中。
检查配置是否合法,并将dpdk的参数去掉,后面会返回dpdk参数的偏移值给调用方。
初始化插件
不知道干嘛的,忽略
解析设备参数
不知道干嘛的,忽略
rte_config初始化
PRIMARY进程:
- 创建配置文件:/var/run/dpdk/rte/config,fd设置到mem_cfg_fd,设置文件大小为配置文件结构体大小(等于sizeof(rte_config.mem_config)),加文件锁(fcntl(mem_cfg_fd, F_SETLK, &wr_lock))。
- 映射配置文件:如果没有设置base_virtaddr,默认为0x100000000ULL(linux环境,查看eal_get_baseaddr、eal_get_virtual_area方法),通过尝试mmap获取一块可用的地址(最多尝试5次,每次在原来的基础上加pagesize大小),然后madvise设置对应内存地址DONT_DUMP(不进行coredump,pmap可以查看标志位为dd),然后再重新映射1遍为/var/run/dpdk/rte/config文件。(mapped_mem_cfg_addr = mmap(rte_mem_cfg_addr, cfg_len_aligned, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, mem_cfg_fd, 0)😉,接着设置rte_config.mem_config = rte_mem_cfg_addr,rte_config.mem_config->mem_cfg_addr = rte_mem_cfg_addr(secondary进程使用),接着设置RTE_VERSION到mfcg(rte_config.mem_config->version)中。
中断初始化
rte_eal_intr_init方法,创建eal-intr-thread线程,线程id:intr_thread,线程运行方法为eal_intr_thread_main。初始化intr_pipe管道fd,用于中断线程通信,初始化intr_sources链表。
告警初始化
intr_handle.type = RTE_INTR_HANDLE_ALARM;
/* create a timerfd file descriptor */
intr_handle.fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
多进程socket初始化
rte_mp_channel_init方法,对/var/run/dpdk/rte文件夹加文件锁(flock(dir_fd, LOCK_EX))创建/var/run/dpdk/rte/mp_socket文件为unix套接字mp_fd,创建rte_mp_handle线程,线程id:mp_handle_tid,线程运行方法:mp_handle,创建完线程,解开文件锁(flock(dir_fd, LOCK_UN)),关闭/var/run/dpdk/rte文件夹fd。
热拔插初始化
(不重要,忽略)
注册了mp方法:eal_dev_mp_request,对应方法:handle_secondary_request
总线扫描
重点看rte_pci_bus的scan方法:rte_pci_scan。
rte_pci_scan
- 检查是否pci设备?internal_config.no_pci为false就是有,没有的话直接返回。
- 检查是否有vfio是否启用?就是看/sys/module/vfio_pci模块是否加载。
- 打开/sys/bus/pci/devices文件夹,遍历对应下面的文件,解析pci地址(格式:domain🚌devid.func),然后调用pci_scan_one继续解析子文件夹(例如:/sys/bus/pci/devices/0000:52:00.1)
pci_scan_one
- malloc分配内存struct rte_pci_device *dev,把dev->device.bus = &rte_pci_bus.bus、dev->addr = *addr设置好,dev设置对应总线和pci地址。
- 分别读取/sys/bus/pci/devices/${pci_id}目录下的文件vendor_id、device、subsystem_vendor、subsystem_device、class的值,设置到dev->id.vendor_id、dev->id.device_id、dev->id.subsystem_vendor_id、dev->id.subsystem_device_id、dev->id.class_id。
- 如果存在/sys/bus/pci/devices/\({pci_id}/max_vfs文件读取值,设置到dev->max_vfs;否则如果存在/sys/bus/pci/devices/\){pci_id}/sriov_numvfs文件则读取值,设置到dev->max_vfs;都没有则dev->max_vfs为0。
- 如果存在/sys/bus/pci/devices/${pci_id}/numa_node文件读取值,设置到dev->device.numa_node,否则设置为0。
- 把pci id设置为dev->name的名称,例如:0000:52:00.1。
- 然后根据名称查看是否对应的设备参数,有就设置到dev->device.devargs,如果dev->device.devargs != NULL,就设置dev->device.name = dev->device.devargs->name,否则dev->device.name = dev->name。
- 打开/sys/bus/pci/devices/${pci_id}/resource文件,遍历文件每一行(格式:"起始地址 结束地址 标志位"),如果标志位 & 0x200就表示为内存bar,设置到dev->mem_resource[i].phys_addr(起始地址)、dev->mem_resource[i].len(起始地址和结束地址做差)、dev->mem_resource[i].addr为null。
- 打开/sys/bus/pci/devices/${pci_id}/driver文件,没有就设置dev->kdrv = RTE_KDRV_NONE,否则读取对应文件的软链路径的最后1个文件名,vfio-pci设置dev->kdrv = RTE_KDRV_VFIO、igb_uio设置dev->kdrv = RTE_KDRV_IGB_UIO、uio_pci_generic设置dev->kdrv = RTE_KDRV_UIO_GENERIC、都不匹配设置dev->kdrv = RTE_KDRV_UNKNOWN。
- 把设置加到rte_pci_bus.device_list链表中。
判断物理地址是否能获取到?
通过读取/proc/self/pagemap对应值进行判断(参考rte_mem_virt2phy方法),且大页可用(internal_config.no_hugetlbfs为false)。
iommu模式确认
如果没有设置参数--iova-mode=<pa|va>,那么默认为RTE_IOVA_DC(调用rte_bus_get_iommu_class确认)
rte_pci_bus对应方法为rte_pci_get_iommu_class。
遍历设备列表rte_pci_bus.device_list,针对第一个设备调用pci_device_iommu_support_va方法判断设备是否支持虚拟地址映射。如果不支持设置iommu_no_va为0,否则设置iommu_no_va为1。接着判断是否设备被过滤(黑名单、白名单、dev->kdrv的值是否为RTE_KDRV_UNKNOWN或者RTE_KDRV_NONE),没忽略,则遍历rte_pci_bus.driver_list判断是否设备对应驱动是否匹配(参考rte_pci_match方法),接着调用pci_device_iova_mode方法获取iova的值。如果存在VA就返回RTE_IOVA_VA,存在PA就返回RTE_IOVA_PA,否则打印告警日志返回RTE_IOVA_DC。对于总线也是优先VA、然后VA、最后DC。
设置rte_config.iova_mode。
如果rte_config.iova_mode为RTE_IOVA_PA,且无法获取物理地址就返回-1。
pci_device_iommu_support_va方法
读取文件/sys/bus/pci/devices/${pci_id}/iommu/intel-iommu/cap的值。
先忽略,不重要
rte_pci_match方法
把驱动pci_drv->id_table(struct rte_pci_id *数组)跟pci_dev->id(struct rte_pci_id)进行匹配。
pci_device_iova_mode方法
pdev->kdrv为RTE_KDRV_IGB_UIO、RTE_KDRV_UIO_GENERIC则是RTE_IOVA_PA,为RTE_KDRV_VFIO则进一步判断(忽略),其他的则判断是否pdrv->drv_flags & RTE_PCI_DRV_NEED_IOVA_AS_VA != 0,为真就是RTE_IOVA_VA,否则为RTE_IOVA_DC。
大页初始化
先忽略,明天看看,中优先级‼️
内存初始化
先忽略,明天看看,中优先级‼️
总线探测
rte_bus_probe方法,遍历rte_bus_list链表,对每个bus调用对应的probe方法。
rte_pci_bus对应的probe方法为pci_probe。
pci_probe方法。
遍历rte_pci_bus.device_list链表,然后对每个pci设备都调用一次pci_probe_all_drivers。
pci_probe_all_drivers方法。
遍历rte_pci_bus.driver_list链表,然后对每个驱动都调用rte_pci_probe_one_driver方法,传入设备,判断是否探测成功,成功探测则返回0,1没探测到,<0就是探测失败。
rte_pci_probe_one_driver方法。
调用rte_pci_match方法,匹配驱动的drv->id_tables和设备的pci地址:dev->addr,匹配失败返回1。如果设备pci地址被黑名单匹配到也表示匹配失败返回1。判断设备是否匹配过dev->drvier不为null,已经探测过且启动不支持再次探测,就返回-EEXIST(探测失败)。
- 没探测过,调用pci_device_iova_mode方法判断使用什么地址模式?igb_uio和uio是pa,vfio没有iommu就是pa、有就是va。获取当前eal的iova模式,rte_config.iova_mode。如果不匹配则返回-EINVAL。否则匹配成功,设置dev->driver = dr。如果驱动存在(dr->drv_flags & RTE_PCI_DRV_NEED_MAPPING) != 0标志,就调用rte_pci_map_device方法映射,igb_uio和uio会调用pci_uio_map_resource方法进行映射,从/sys/bus/pci/devices/\({pci_id}/uio/uio%u路径中获取对应的uio number,读取/sys/bus/pci/devices/\){pci_id}/uio/uio%u/dev文件的major:minor,调用makedev(major, minor)方法和mknod("/dev/uio%u", S_IFCHR | S_IRUSR | S_IWUSR, dev)方法创建uio设备。接着打开uio文件设置dev->intr_handle.fd = open("/dev/uio%u", O_RDWR),和打开uio配置dev->intr_handle.uio_cfg_fd = open("/sys/class/uio/uio%u/device/config", O_RDWR),如果内核驱动dev->kdrv为RTE_KDRV_IGB_UIO,设置dev->intr_handle.type = RTE_INTR_HANDLE_UIO,否则dev->intr_handle.type = RTE_INTR_HANDLE_UIO_INTX并调用写入dev->intr_handle.uio_cfg_fd文件16字节设置1个标志位PCI_COMMAND_MASTER。接着调用uio_res = rte_zmalloc("UIO_RES", sizeof(struct mapped_pci_resource), 0),设置(uio_res)->path为/dev/uio%u,(*uio_res)->pci_addr为dev->addr。接着根据从/sys/bus/pci/devices/\({pci_id}/resource文件读取到的内存bar,也就是dev->mem_resource[i].phys_addr != 0的bar调用pci_uio_map_resource_by_index方法进行处理,打开/sys/bus/pci/devices/\){pci_id}/resource%d文件,然后就通过mmap进行映射(mmap(requested_addr, size, PROT_READ | PROT_WRITE, MAP_SHARED | additional_flags, fd, offset)),并且将映射后的地址保存到dev->mem_resource[res_idx].addr = mapaddr中。最后把uio_res插入到全局链表uio_res_list中。接着调用驱动的dr->probe(dr, dev)方法,探测成功设置dev->device.driver = &dr->driver。
ixgbe驱动探测
eth_ixgbe_pci_probe方法
- 调用rte_eth_dev_create方法,调用rte_eth_dev_allocate方法分配struct rte_eth_dev(抽象的以太网设备),调用rte_eth_dev_find_free_port方法分配port id(按调用顺序从0开始分,最大port数量为32,可以在.config文件中修改),按照port id对应获取struct rte_eth_dev rte_eth_devices[RTE_MAX_ETHPORTS]的设备(struct rte_eth_dev *eth_dev = &rte_eth_devices[port_id]),并把eth_dev->data = &rte_eth_dev_shared_data->data[port_id]。再把网卡驱动私有的数据分配ethdev->data->dev_private = rte_zmalloc_socket(name, priv_data_size, RTE_CACHE_LINE_SIZE, device->numa_node)。并把pci的设备挂到rte设备上ethdev->device = device。然后调用eth_ixgbe_dev_init方法进行初始化。
eth_ixgbe_dev_init方法
- ixgbe_get_bus_info获取总线信息,看着也没获取啥。
- ixgbe_swfw_lock_reset解锁pending信号量,不知道干啥的。
- ixgbe_dcb_init,初始化dcb配置,不知道干啥的。
- 不重要忽略
- 中断处理方法初始化:rte_intr_callback_register(intr_handle, ixgbe_dev_interrupt_handler, eth_dev),用于链路状态更新,就是创建1个回调结构体加到intr_sources中
配置设备信息等
rte_eth_dev_configure方法,通过port_id也就是数组下标,从rte_eth_devices[port_id]数组获取对应rte_eth_device。
调ixgbe_dev_info_get方法获取对应配置信息。
调ixgbe_dev_configure方法进行配置。
对应发送和接收队列:dev->data->tx_queues数组、dev->data->rx_queues数组(数组是在rte_eth_dev_configure方法里创建的,但是在rte_eth_tx_queue_setup/rte_eth_rx_queue_setup方法中为队列分配内存进行dma映射操作)
ixgbe对应的发送队列是struct ixgbe_tx_queue,txq->tx_ring对应的元素是union ixgbe_adv_tx_desc,txq->sw_ring对应的元素是struct ixgbe_tx_entry。
ixgbe对应的接收队列是struct ixgbe_rx_queue,rxq->rx_ring对应的元素是union ixgbe_adv_rx_desc,rxq->sw_ring对应的元素是struct ixgbe_rx_entry。
rte_eth_tx_queue_setup方法
-> ixgbe_dev_tx_queue_setup
-> rte_eth_dma_zone_reserve从大页分配1块满足大小的连续内存(txq->tx_ring)
rte_eth_rx_queue_setup方法
-> ixgbe_dev_rx_queue_setup
-> rte_eth_dma_zone_reserve从大页分配1块满足大小的连续内存(rxq->rx_ring)
rte_eth_dev_start方法
-> ixgbe_dev_start
-> ixgbe_dev_tx_init:设置队列内存
-> ixgbe_dev_rx_init:设置队列内存
-> ixgbe_dev_rxtx_start
-> ixgbe_dev_tx_queue_start:写入控制寄存器启动队列
-> ixgbe_dev_rx_queue_start:写入控制寄存器启动队列
-> ixgbe_alloc_rx_queue_mbufs:预先填充好rxq->rx_ring
-> ixgbe_enable_rx_dma_generic:写寄存器开启接收
将分配好的大页内存的物理地址,写入网卡dma传输寄存器:内存物理地址、内存长度、头尾指针
rte_eth_rx_burst方法
-> ixgbe_recv_pkts_vec
-> _recv_raw_pkts_vec
rte_eth_tx_burst方法
-> ixgbe_xmit_pkts_vec
-> ixgbe_xmit_fixed_burst_vec
以上都是向量化的版本,可以看下不向量化的版本ixgbe_xmit_pkts、ixgbe_recv_pkts。
参考:https://blog.csdn.net/u014787815/article/details/109019273
预取是把数据从内存提前加载到L1、L2等cache中,提高缓存命中率,减少从内存中加载的时间,提高IPC(1个时钟周期内的指令数)。
问题:
- 内存管理?内存池初始化
- rx、tx释放阈值如何确认?