DPDK系列之十五虚拟化virtio源码分析之vhost-user

news/2024/4/23 15:44:38/文章来源:https://blog.csdn.net/fpcc/article/details/130351554

一、vhost-user说明

在网络IO的半虚拟中,vhost-user是目前最优秀的解决方案。在DPDK中,同样也采用了这种方式。vhost-user是为了解决内核状态数据操作复杂的情况提出的一种解决方式,通过在用户进程来替代内核进程来实现数据交互的最少化。在vhost-user中,使用Socket进行设备文件间通信(替代了Kernel模式),而数据交换则采用mmap的进程内存宰共享的模式减少数据的交互。
vhost-user在DPDK中的vhost库中实现,其包含了完整的virtio的后端逻辑功能。在软件虚拟交的机OVS中,就应用到了DPDK这个库。
一般来说,vhost-user由OVS为每个虚拟创建珍上vhost端口,来实现相关数据操作。而此时的virtio前端和会调用此相关的端口进行通信。

二、数据结构

基本的数据结构如下(lib/librte_vhost/vhost_user.h):

/* Same structure as vhost-user backend session info */
typedef struct VhostUserCryptoSessionParam {int64_t session_id;uint32_t op_code;uint32_t cipher_algo;uint32_t cipher_key_len;uint32_t hash_algo;uint32_t digest_len;uint32_t auth_key_len;uint32_t aad_len;uint8_t op_type;uint8_t dir;uint8_t hash_mode;uint8_t chaining_dir;uint8_t * ciphe_key;uint8_t * auth_key;uint8_t cipher_key_buf[VHOST_USER_CRYPTO_MAX_CIPHER_KEY_LENGTH];uint8_t auth_key_buf[VHOST_USER_CRYPTO_MAX_HMAC_KEY_LENGTH];
} VhostUserCryptoSessionParam;typedef struct VhostUserVringArea {uint64_t u64;uint64_t size;uint64_t offset;
} VhostUserVringArea;typedef struct VhostUserInflight {uint64_t mmap_size;uint64_t mmap_offset;uint16_t num_queues;uint16_t queue_size;
} VhostUserInflight;typedef struct VhostUserMsg {union {uint32_t master; /* a VhostUserRequest value */uint32_t slave;  /* a VhostUserSlaveRequest value*/} request;#define VHOST_USER_VERSION_MASK     0x3
#define VHOST_USER_REPLY_MASK       (0x1 << 2)
#define VHOST_USER_NEED_REPLY		(0x1 << 3)uint32_t flags;uint32_t size; /* the following payload size */union {
#define VHOST_USER_VRING_IDX_MASK   0xff
#define VHOST_USER_VRING_NOFD_MASK  (0x1<<8)uint64_t u64;struct vhost_vring_state state;struct vhost_vring_addr addr;VhostUserMemory memory;VhostUserLog    log;struct vhost_iotlb_msg iotlb;VhostUserCryptoSessionParam crypto_session;VhostUserVringArea area;VhostUserInflight inflight;} payload;int fds[VHOST_MEMORY_MAX_NREGIONS];int fd_num;
} __attribute((packed)) VhostUserMsg;

其中最主要的就是最后一个数据结构VhostUserMsg,这个消息里包含着消息的种类、内容和相关内容的数据大小。而这个消息,正是通过vhost_user.c(lib/librte_vhost)中的vhost_user_msg_handler这个函数来处理的。它们之间的消息类型定义如下:

typedef enum VhostUserRequest {VHOST_USER_NONE = 0,VHOST_USER_GET_FEATURES = 1,VHOST_USER_SET_FEATURES = 2,VHOST_USER_SET_OWNER = 3,VHOST_USER_RESET_OWNER = 4,VHOST_USER_SET_MEM_TABLE = 5,VHOST_USER_SET_LOG_BASE = 6,VHOST_USER_SET_LOG_FD = 7,VHOST_USER_SET_VRING_NUM = 8,VHOST_USER_SET_VRING_ADDR = 9,VHOST_USER_SET_VRING_BASE = 10,VHOST_USER_GET_VRING_BASE = 11,VHOST_USER_SET_VRING_KICK = 12,VHOST_USER_SET_VRING_CALL = 13,VHOST_USER_SET_VRING_ERR = 14,VHOST_USER_GET_PROTOCOL_FEATURES = 15,VHOST_USER_SET_PROTOCOL_FEATURES = 16,VHOST_USER_GET_QUEUE_NUM = 17,VHOST_USER_SET_VRING_ENABLE = 18,VHOST_USER_SEND_RARP = 19,VHOST_USER_NET_SET_MTU = 20,VHOST_USER_SET_SLAVE_REQ_FD = 21,VHOST_USER_IOTLB_MSG = 22,VHOST_USER_MAX
} VhostUserRequest;

随着版本的迭代和新的设备及相关控制手段增加会引起些消息的增加。
再看一下相关的共享内存数据结构(lib/librte_vhost/vhost_user.h):

/*对应qemu端的region结构*/
typedef struct VhostUserMemoryRegion {uint64_t guest_phys_addr;uint64_t memory_size;uint64_t userspace_addr;uint64_t mmap_offset;
} VhostUserMemoryRegion;typedef struct VhostUserMemory {uint32_t nregions;uint32_t padding;VhostUserMemoryRegion regions[VHOST_MEMORY_MAX_NREGIONS];
} VhostUserMemory;//lib/librte_vhost/rte_vhost.h
/*** Information relating to memory regions including offsets to* addresses in QEMUs memory file.*/
struct rte_vhost_mem_region {uint64_t guest_phys_addr;uint64_t guest_user_addr;uint64_t host_user_addr;uint64_t size;void	 *mmap_addr;uint64_t mmap_size;int fd;
};/*** Memory structure includes region and mapping information.*/
struct rte_vhost_memory {uint32_t nregions;struct rte_vhost_mem_region regions[];
};

上面的两个不同文件夹的相关数据结构体互相对应。

三、基本流程

1、连接和初始化
连接的建立是使用Sokcet来进行的。在前面的消息数据结构体中,其实是定义了很多消息枚举和相关的数组的。这个上面的数据结构体中已经有所体现。

int
rte_vhost_driver_start(const char * path)
{struct vhost_user_socket * vsocket;static pthread_t fdset_tid;pthread_mutex_lock(&vhost_user.mutex);vsocket = find_vhost_user_socket(path);pthread_mutex_unlock(&vhost_user.mutex);if (!vsocket)return -1;if (fdset_tid == 0) {/*** create a pipe which will be waited by poll and notified to* rebuild the wait list of poll.*/if (fdset_pipe_init(&vhost_user.fdset) < 0) {RTE_LOG(ERR, VHOST_CONFIG,"failed to create pipe for vhost fdset\n");return -1;}int ret = rte_ctrl_thread_create(&fdset_tid,"vhost-events", NULL, fdset_event_dispatch,&vhost_user.fdset);if (ret != 0) {RTE_LOG(ERR, VHOST_CONFIG,"failed to create fdset handling thread\n");fdset_pipe_uninit(&vhost_user.fdset);return -1;}}if (vsocket->is_server)return vhost_user_start_server(vsocket);elsereturn vhost_user_start_client(vsocket);
}

通过线程来启动分发控制,最后根据是客户端或者服务端来启动相应的功能函数。
当有一个新的连接时,则处理为:

/* call back when there is new vhost-user connection from client  */
static void
vhost_user_server_new_connection(int fd, void *dat, int * remove __rte_unused)
{struct vhost_user_socket *vsocket = dat;fd = accept(fd, NULL, NULL);if (fd < 0)return;RTE_LOG(INFO, VHOST_CONFIG, "new vhost user connection is %d\n", fd);vhost_user_add_connection(fd, vsocket);
}

其实你看lib/librte_vhost/socket.c中的代码,如果有Socket编程的经验的一眼就可以看出好多相关的处理函数和处理手段。

2、数据通信
数据通信中数据交互使用mmap,相关设置代码:

static int
vhost_user_set_mem_table(struct virtio_net **pdev, struct VhostUserMsg *msg,int main_fd)
{struct virtio_net *dev = *pdev;struct VhostUserMemory *memory = &msg->payload.memory;struct rte_vhost_mem_region *reg;void *mmap_addr;uint64_t mmap_size;uint64_t mmap_offset;uint64_t alignment;uint32_t i;int populate;if (validate_msg_fds(msg, memory->nregions) != 0)return RTE_VHOST_MSG_RESULT_ERR;if (memory->nregions > VHOST_MEMORY_MAX_NREGIONS) {RTE_LOG(ERR, VHOST_CONFIG,"too many memory regions (%u)\n", memory->nregions);goto close_msg_fds;}if (dev->mem && !vhost_memory_changed(memory, dev->mem)) {RTE_LOG(INFO, VHOST_CONFIG,"(%d) memory regions not changed\n", dev->vid);close_msg_fds(msg);return RTE_VHOST_MSG_RESULT_OK;}if (dev->mem) {free_mem_region(dev);rte_free(dev->mem);dev->mem = NULL;}/* Flush IOTLB cache as previous HVAs are now invalid */if (dev->features & (1ULL << VIRTIO_F_IOMMU_PLATFORM))for (i = 0; i < dev->nr_vring; i++)vhost_user_iotlb_flush_all(dev->virtqueue[i]);dev->nr_guest_pages = 0;if (dev->guest_pages == NULL) {dev->max_guest_pages = 8;dev->guest_pages = rte_zmalloc(NULL,dev->max_guest_pages *sizeof(struct guest_page),RTE_CACHE_LINE_SIZE);if (dev->guest_pages == NULL) {RTE_LOG(ERR, VHOST_CONFIG,"(%d) failed to allocate memory ""for dev->guest_pages\n",dev->vid);goto close_msg_fds;}}dev->mem = rte_zmalloc("vhost-mem-table", sizeof(struct rte_vhost_memory) +sizeof(struct rte_vhost_mem_region) * memory->nregions, 0);if (dev->mem == NULL) {RTE_LOG(ERR, VHOST_CONFIG,"(%d) failed to allocate memory for dev->mem\n",dev->vid);goto free_guest_pages;}dev->mem->nregions = memory->nregions;for (i = 0; i < memory->nregions; i++) {reg = &dev->mem->regions[i];reg->guest_phys_addr = memory->regions[i].guest_phys_addr;reg->guest_user_addr = memory->regions[i].userspace_addr;reg->size            = memory->regions[i].memory_size;reg->fd              = msg->fds[i];/** Assign invalid file descriptor value to avoid double* closing on error path.*/msg->fds[i] = -1;mmap_offset = memory->regions[i].mmap_offset;/* Check for memory_size + mmap_offset overflow */if (mmap_offset >= -reg->size) {RTE_LOG(ERR, VHOST_CONFIG,"mmap_offset (%#"PRIx64") and memory_size ""(%#"PRIx64") overflow\n",mmap_offset, reg->size);goto free_mem_table;}mmap_size = reg->size + mmap_offset;/* mmap() without flag of MAP_ANONYMOUS, should be called* with length argument aligned with hugepagesz at older* longterm version Linux, like 2.6.32 and 3.2.72, or* mmap() will fail with EINVAL.** to avoid failure, make sure in caller to keep length* aligned.*/alignment = get_blk_size(reg->fd);if (alignment == (uint64_t)-1) {RTE_LOG(ERR, VHOST_CONFIG,"couldn't get hugepage size through fstat\n");goto free_mem_table;}mmap_size = RTE_ALIGN_CEIL(mmap_size, alignment);if (mmap_size == 0) {/** It could happen if initial mmap_size + alignment* overflows the sizeof uint64, which could happen if* either mmap_size or alignment value is wrong.** mmap() kernel implementation would return an error,* but better catch it before and provide useful info* in the logs.*/RTE_LOG(ERR, VHOST_CONFIG, "mmap size (0x%" PRIx64 ") ""or alignment (0x%" PRIx64 ") is invalid\n",reg->size + mmap_offset, alignment);goto free_mem_table;}populate = (dev->dequeue_zero_copy) ? MAP_POPULATE : 0;mmap_addr = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE,MAP_SHARED | populate, reg->fd, 0);if (mmap_addr == MAP_FAILED) {RTE_LOG(ERR, VHOST_CONFIG,"mmap region %u failed.\n", i);goto free_mem_table;}reg->mmap_addr = mmap_addr;reg->mmap_size = mmap_size;reg->host_user_addr = (uint64_t)(uintptr_t)mmap_addr +mmap_offset;if (dev->dequeue_zero_copy)if (add_guest_pages(dev, reg, alignment) < 0) {RTE_LOG(ERR, VHOST_CONFIG,"adding guest pages to region %u failed.\n",i);goto free_mem_table;}RTE_LOG(INFO, VHOST_CONFIG,"guest memory region %u, size: 0x%" PRIx64 "\n""\t guest physical addr: 0x%" PRIx64 "\n""\t guest virtual  addr: 0x%" PRIx64 "\n""\t host  virtual  addr: 0x%" PRIx64 "\n""\t mmap addr : 0x%" PRIx64 "\n""\t mmap size : 0x%" PRIx64 "\n""\t mmap align: 0x%" PRIx64 "\n""\t mmap off  : 0x%" PRIx64 "\n",i, reg->size,reg->guest_phys_addr,reg->guest_user_addr,reg->host_user_addr,(uint64_t)(uintptr_t)mmap_addr,mmap_size,alignment,mmap_offset);if (dev->postcopy_listening) {/** We haven't a better way right now than sharing* DPDK's virtual address with Qemu, so that Qemu can* retrieve the region offset when handling userfaults.*/memory->regions[i].userspace_addr =reg->host_user_addr;}}if (dev->postcopy_listening) {/* Send the addresses back to qemu */msg->fd_num = 0;send_vhost_reply(main_fd, msg);/* Wait for qemu to acknolwedge it's got the addresses* we've got to wait before we're allowed to generate faults.*/VhostUserMsg ack_msg;if (read_vhost_message(main_fd, &ack_msg) <= 0) {RTE_LOG(ERR, VHOST_CONFIG,"Failed to read qemu ack on postcopy set-mem-table\n");goto free_mem_table;}if (validate_msg_fds(&ack_msg, 0) != 0)goto free_mem_table;if (ack_msg.request.master != VHOST_USER_SET_MEM_TABLE) {RTE_LOG(ERR, VHOST_CONFIG,"Bad qemu ack on postcopy set-mem-table (%d)\n",ack_msg.request.master);goto free_mem_table;}/* Now userfault register and we can use the memory */for (i = 0; i < memory->nregions; i++) {
#ifdef RTE_LIBRTE_VHOST_POSTCOPYreg = &dev->mem->regions[i];struct uffdio_register reg_struct;/** Let's register all the mmap'ed area to ensure* alignment on page boundary.*/reg_struct.range.start =(uint64_t)(uintptr_t)reg->mmap_addr;reg_struct.range.len = reg->mmap_size;reg_struct.mode = UFFDIO_REGISTER_MODE_MISSING;if (ioctl(dev->postcopy_ufd, UFFDIO_REGISTER,&reg_struct)) {RTE_LOG(ERR, VHOST_CONFIG,"Failed to register ufd for region %d: (ufd = %d) %s\n",i, dev->postcopy_ufd,strerror(errno));goto free_mem_table;}RTE_LOG(INFO, VHOST_CONFIG,"\t userfaultfd registered for range : ""%" PRIx64 " - %" PRIx64 "\n",(uint64_t)reg_struct.range.start,(uint64_t)reg_struct.range.start +(uint64_t)reg_struct.range.len - 1);
#elsegoto free_mem_table;
#endif}}for (i = 0; i < dev->nr_vring; i++) {struct vhost_virtqueue *vq = dev->virtqueue[i];if (vq->desc || vq->avail || vq->used) {/** If the memory table got updated, the ring addresses* need to be translated again as virtual addresses have* changed.*/vring_invalidate(dev, vq);dev = translate_ring_addresses(dev, i);if (!dev) {dev = *pdev;goto free_mem_table;}*pdev = dev;}}dump_guest_pages(dev);return RTE_VHOST_MSG_RESULT_OK;free_mem_table:free_mem_region(dev);rte_free(dev->mem);dev->mem = NULL;
free_guest_pages:rte_free(dev->guest_pages);dev->guest_pages = NULL;
close_msg_fds:close_msg_fds(msg);return RTE_VHOST_MSG_RESULT_ERR;
}

地址的转换是在下面的函数:

/* Converts QEMU virtual address to Vhost virtual address. */
static uint64_t
qva_to_vva(struct virtio_net *dev, uint64_t qva, uint64_t *len)
{struct rte_vhost_mem_region *r;uint32_t i;if (unlikely(!dev || !dev->mem))goto out_error;/* Find the region where the address lives. */for (i = 0; i < dev->mem->nregions; i++) {r = &dev->mem->regions[i];if (qva >= r->guest_user_addr &&qva <  r->guest_user_addr + r->size) {if (unlikely(*len > r->guest_user_addr + r->size - qva))*len = r->guest_user_addr + r->size - qva;return qva - r->guest_user_addr +r->host_user_addr;}}
out_error:*len = 0;return 0;
}

数据的实际通信,在前面分析过,就是“rte_vhost_enqueue_burst”和“rte_vhost_dequeue_burst”这两个函数。

3、通知机制

基本上是采用eventfd的方式,这和网络通信保持一致:

static int
vhost_user_set_vring_kick(struct virtio_net **pdev, struct VhostUserMsg *msg,int main_fd __rte_unused)
{struct virtio_net *dev = *pdev;struct vhost_vring_file file;struct vhost_virtqueue *vq;int expected_fds;expected_fds = (msg->payload.u64 & VHOST_USER_VRING_NOFD_MASK) ? 0 : 1;if (validate_msg_fds(msg, expected_fds) != 0)return RTE_VHOST_MSG_RESULT_ERR;file.index = msg->payload.u64 & VHOST_USER_VRING_IDX_MASK;if (msg->payload.u64 & VHOST_USER_VRING_NOFD_MASK)file.fd = VIRTIO_INVALID_EVENTFD;elsefile.fd = msg->fds[0];RTE_LOG(INFO, VHOST_CONFIG,"vring kick idx:%d file:%d\n", file.index, file.fd);/* Interpret ring addresses only when ring is started. */dev = translate_ring_addresses(dev, file.index);if (!dev) {if (file.fd != VIRTIO_INVALID_EVENTFD)close(file.fd);return RTE_VHOST_MSG_RESULT_ERR;}*pdev = dev;vq = dev->virtqueue[file.index];/** When VHOST_USER_F_PROTOCOL_FEATURES is not negotiated,* the ring starts already enabled. Otherwise, it is enabled via* the SET_VRING_ENABLE message.*/if (!(dev->features & (1ULL << VHOST_USER_F_PROTOCOL_FEATURES))) {vq->enabled = 1;if (dev->notify_ops->vring_state_changed)dev->notify_ops->vring_state_changed(dev->vid, file.index, 1);}if (vq->kickfd >= 0)close(vq->kickfd);vq->kickfd = file.fd;if (vq_is_packed(dev)) {if (vhost_check_queue_inflights_packed(dev, vq)) {RTE_LOG(ERR, VHOST_CONFIG,"failed to inflights for vq: %d\n", file.index);return RTE_VHOST_MSG_RESULT_ERR;}} else {if (vhost_check_queue_inflights_split(dev, vq)) {RTE_LOG(ERR, VHOST_CONFIG,"failed to inflights for vq: %d\n", file.index);return RTE_VHOST_MSG_RESULT_ERR;}}return RTE_VHOST_MSG_RESULT_OK;
}

上述交互使用Poll机制,也就是轮询,来不断驱动着数据的流动。

四、总结

可以说从内核转到用户空间本身就是一个非常大的进步。减少甚至不和内核打交道,这实际就是设计上的解耦,同时增加了内核的安全性。反而效率成为了一种为了需要产生的有益的副作用。从这一点可以看出,软件设计思想的提高和在实际上的普及应用,是非常重要的。想到,才有可能做到。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_102948.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

今天试了试chatgpt

今天试了试chatgpt&#xff0c;真是服了 arcade&#xff1f; Arcade是一个Python游戏开发库&#xff0c;它提供了一系列的工具和函数&#xff0c;可以帮助开发者快速地创建2D游戏。以下是Arcade的一些特点&#xff1a; 简单易用&#xff1a;Arcade提供了简单易用的API&#x…

Android分屏流程分析

本文基于Android 11。 SystemUI模块中的Divider管理着所有关于分屏的对象&#xff1a; DividerView&#xff08;分屏分割线&#xff0c;分屏显示界面&#xff09;SplitScreenTaskOrganizer&#xff08;分屏Task组织者&#xff0c;分屏逻辑&#xff09; 这里重点关注分屏逻辑…

Qt如何生成dump文件和pdb文件并进行调试定位

在main文件中增加下面代码用于可生成dump文件 #include "widget.h" #include <QApplication> #include <QDir> #include <QDateTime> #ifdef Q_OS_WIN#include <windows.h>#include <dbghelp.h> #endifstatic LONG WINAPI exceptionC…

简单介绍一下什么是“工作内存”和“主内存”(JMM中的概念)

在学习Java多线程编程里&#xff0c; volatile 关键字保证内存可见性的要点时&#xff0c;看到网上有些资料是这么说的&#xff1a;线程修改一个变量&#xff0c;会把这个变量先从主内存读取到工作内存&#xff1b;然后修改工作内存中的值&#xff0c;最后再写回到主内存。 对…

Spring 循环依赖处理之三级缓存设计

一、思考 1、Spring是如何解决循环依赖问题的? 2、为什么要使用三级缓存?二级缓存能否解决问题? 3、提前暴露对象暴露的是什么? 4、主要源码 二、循环依赖 1、介绍 如上图&#xff0c;创建A之前需要先创建B,创建B之前需要先创建A,造成循环依赖。 由于A没创建完成&am…

一个关于Mybatis和spring的公共组件starter

utils-springboot-starter 介绍使用说明 介绍 一个关于Mybatis和spring的公共组件starter&#xff0c;目前包含以下功能&#xff1a; 接口请求日志SQL执行日志数据自动加解密数据自动脱敏服务治理方面&#xff1a; 接口限流接口熔断降级&#xff1a;CPU、内存、异常数、异常率…

win11 环境下streamlit使用pycharm debug

目录 1. pycharm中配置run 脚本2. streamlit3. 开始debug调试 1. pycharm中配置run 脚本 &#xff08;一&#xff09;点击 Edit Configurations,按图操作. 2. streamlit 1.streamlit 安装在 anaconda 的 base 环境&#xff08;随意哈&#xff0c;安装哪里都可以&#xff0c…

问题定位及解决方案

1.视频沉浸页快速滑动后&#xff0c;必现不能向下划动 复现步骤&#xff1a; 进入视频沉浸页&#xff0c;快速向下划动&#xff0c;滑动到第一页最后一个时&#xff0c;不能再向下划动。 解决步骤&#xff1a; 1.确定请求API&#xff1a; mtop.aliexpress.ugc.feed.video.lis…

PE文件反编译为python脚本流程

1、查壳 DetectltEasy、PeiD查壳 2、脱壳 常见打包工具PyInstaller&#xff0c;脱壳方法 &#xff08;1&#xff09;用pyinstxtractor.py脱壳&#xff0c;用”python pyinstxtractor.py 1.exe“命令&#xff0c;生成“.exe文件名_extracted” &#xff08;2&#xff09;用…

深度学习技巧应用8-各种数据类型的加载与处理,并输入神经网络进行训练

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下深度学习技巧应用8-各种数据类型的加载与处理&#xff0c;并输入神经网络进行训练。在模型训练中&#xff0c;大家往往对各种的数据类型比较难下手&#xff0c;对于非结构化数据已经复杂的数据的要进行特殊处理&…

LeetCode:6390. 滑动子数组的美丽值

&#x1f34e;道阻且长&#xff0c;行则将至。&#x1f353; &#x1f33b;算法&#xff0c;不如说它是一种思考方式&#x1f340; 算法专栏&#xff1a; &#x1f449;&#x1f3fb;123 一、&#x1f331;6390. 滑动子数组的美丽值 题目描述&#xff1a;给你一个长度为 n 的整…

波形生成:均匀和非均匀时间向量

波形生成—— 脉冲、chirp、VCO、正弦函数、周期性/非周期性和调制信号 使用 chirp 生成线性、二次和对数 chirp。使用 square、rectpuls 和 sawtooth 创建方波、矩形波和三角形波。 如需了解此处未显示的其他无线波形生成功能&#xff0c;请参阅无线波形发生器 (Communicat…

完整的生产车间管理流程是怎样的?六大步骤分享

阅读本文您将了解&#xff1a;1.生产车间管理的特征&#xff1b;2.生产车间管理流程具体步骤&#xff1b;3.生产车间管理流程规范的重要性。 一、生产车间管理的特征 车间管理是指对车间所从事的各项生产经营活动进行计划、组织、指挥、协调和控制的一系列管理工作。生产车间…

巧用千寻位置GNSS软件| 电力线勘测如何实现?

正如大家所知&#xff0c;电力线勘测是在做电力线路设计之前对设计线路沿途自然环境进行勘察测量&#xff0c;最后把手簿测量数据在电脑端经过转换输出为电力软件专用格式数据的专用功能。 那么在千寻位置GNSS软件中该如何操作完成电力线的勘察测量呢&#xff1f; 点击【测量】…

SwiftUI 中 TabView 如何原生使用类 UIPageView 的翻页样式?

功能需求 我们知道 TabView 是 SwiftUI 中非常好用的布局组织容器,它可以分类组织视图并依次展示给用户。 从 SwiftUI 2.0 开始(iOS 14.0+),TabView 除了常规的以标签(Tab Label)样式显示外,还可以用类似 UIPageView 的样式分页原生显示视图,显得更加简洁: 如上图所…

AWT-对话框——Dialog以及其子类FileDialog

Dialog: Dialog时Window类的子类&#xff0c;时一个容器类&#xff0c;属于特殊组件。对话框是可以独立存在的顶级窗口&#xff0c;因此用法与普通窗口的用法几乎完全一样&#xff0c;但是使用对话框需要注意以下几点&#xff1a; 对话框通常依赖于其它窗口&#xff0c;就是通…

基于Java+SpringBoot+vue学生学习平台详细设计实现

基于JavaSpringBootvue学生学习平台详细设计实现 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言 文末获取源码联系方式 文章目…

AI Stable Diffusion Prompt参数【二】之 生成效果查验

AI Stable Diffusion Prompt参数【二】之 生成效果查验 效果国漫风生成参数配置prompt&#xff1a;Negative prompt:Model:Steps:Sampler:CFG scale:Clip skip:Model hash:Hires upscale:Hires upscaler:Denoising strength: 全部效果 效果 国漫风生成参数配置 prompt&#xf…

拉格朗日粒子扩散模式FLEXPART

为了高效、精准地治理区域大气污染&#xff0c;需要弄清污染物的来源。拉格朗日粒子扩散模式FLEXPART通过计算点、线、面或体积源释放的大量粒子的轨迹&#xff0c;来描述示踪物在大气中长距离、中尺度的传输、扩散、干湿沉降和辐射衰减等过程。该模式既可以通过时间的前向运算…

汇编与内联 x86-64

机器字长 x86是32位系统 64是64位系统 这里的32和64&#xff0c;指的都是机器字长 机器字长是 能直接进行整数/位运算的大小指针的大小(索引内存的范围) 容易与机器字长混淆的概念&#xff1a;字 字 字存储字长 字是MDR寄存器的位数&#xff0c;代表每个主存存储体中的存储…