内核参数调优:
- 调整 vm.swappiness , dirty_ratio 参数,减少内存抖动
内核参数调优:
内核参数调优:
Virtio-fs + DAX (Direct Access): 这是高密部署的“杀手锏”。
原理: 传统的虚拟化中,Guest 读取文件会将其缓存在 Guest Page Cache 中,Host 也会有一份缓存,造成双重内存浪费。
方案: 使用 Virtio-fs 将 Host 的文件系统直接透传给 Guest。开启 DAX Window 功能,允许 Guest 直接映射 Host 的文件内容到自己的内存地址空间,绕过 Guest 的 Page Cache。
效果: 多个 MicroVM 共享同一个只读 Rootfs(底包),这些文件在物理内存中只有一份副本。这可以极大地节省内存。
https://docs.kernel.org/filesystems/dax.html
Direct Access for files
页面缓存(page cache)通常用于缓冲对文件的读写操作。它还用于通过调用 mmap 映射到用户空间的页面。
对于类内存的块设备,页缓存页面(page cache pages)将是原始存储的不必要副本。DAX 代码通过直接对存储设备执行读写操作来消除这个额外副本。对于文件映射,存储设备会直接映射到用户空间。
如果你有一个支持 DAX 的块设备,可以像往常一样在其上创建文件系统。目前 DAX 代码仅支持块大小等于内核 PAGE_SIZE 的文件,因此在创建文件系统时可能需要指定块大小。
目前有 5 个文件系统支持 DAX:ext2、ext4、xfs、virtiofs 和 erofs。在这些文件系统上启用 DAX 的方式各不相同。
挂载文件系统时,在命令行上使用 -o dax 选项或在 /etc/fstab 的选项中添加 ‘dax’。这可以启用文件系统中所有文件的 DAX 功能。它等同于下面的 -o dax=always 行为。
存在一个内核文件访问模式标志 S_DAX,对应于 statx 标志 STATX_ATTR_DAX。有关此访问模式的详细信息,请参阅 statx(2) 的手册页。
存在一个持久的标志 FS_XFLAG_DAX,它可以应用于常规文件和目录。这个建议标志可以在任何时候设置或清除,但这样做不会立即影响 S_DAX 状态。
如果在目录上设置了持久的 FS_XFLAG_DAX 标志,那么随后在此目录中创建的所有常规文件和子目录都将继承此标志。在父目录上设置或清除此标志时已存在的文件和子目录不会受到此父目录修改的影响。
存在一些 dax 挂载选项可以覆盖 FS_XFLAG_DAX 在 S_DAX 标志设置中的值。假设底层存储支持 DAX,则以下情况成立:
警告: 选项 -o dax 可能会在未来被移除,因此 -o dax=always 是指定此行为的推荐方法。
注意: 即使文件系统以 dax 选项挂载,FS_XFLAG_DAX 的修改和继承行为也保持不变。但是,直到文件系统以 dax=inode 重新挂载并且 inode 从内核内存中驱逐之前,内存中的 inode 状态(S_DAX)将被覆盖。
可以通过以下方式更改 S_DAX 策略:
在创建文件前根据需要设置父目录的 FS_XFLAG_DAX 标志
设置适当的 dax=“foo” 挂载选项
更改现有常规文件和目录上的 FS_XFLAG_DAX 标志。这具有下文 6) 中描述的运行时限制和约束。
通过切换持久化的 FS_XFLAG_DAX 标志来更改 S_DAX 策略时,对现有常规文件的更改只有在所有进程关闭这些文件后才会生效。
每个文件有 2 个 dax 标志。一个是持久的 inode 设置(FS_XFLAG_DAX),另一个是表示该功能活动状态的易失性标志(S_DAX)。
FS_XFLAG_DAX 在文件系统中被保留。这个持久性配置设置可以使用 FS_IOC_FS[GS]`ETXATTR ioctl(参见 ioctl_xfs_fsgetxattr(2))或诸如 ‘xfs_io’ 之类的工具进行设置、清除和/或查询。
新文件和目录在创建时会自动从其父目录继承 FS_XFLAG_DAX。因此,在创建目录时设置 FS_XFLAG_DAX 可以为整个子树设置默认行为。
为阐明继承关系,以下是 3 个示例:
Example A: 示例 A:
mkdir -p a/b/c
xfs_io -c 'chattr +x' a
mkdir a/b/c/d
mkdir a/e
------[outcome]------
dax: a,e
no dax: b,c,d
Example B: 示例 B:
mkdir a
xfs_io -c 'chattr +x' a
mkdir -p a/b/c/d
------[outcome]------
dax: a,b,c,d
no dax:
Example C: 示例 C:
mkdir -p a/b/c
xfs_io -c 'chattr +x' c
mkdir a/b/c/d
------[outcome]------
dax: c,d
no dax: a,b
当前启用状态(S_DAX)是在内核将文件 inode 实例化到内存时设置的。该状态基于底层媒体支持、FS_XFLAG_DAX 的值以及文件系统的 dax 挂载选项进行设置。
statx 可用于查询 S_DAX。
即使底层介质不支持 dax 和/或文件系统被挂载选项覆盖,设置 FS_XFLAG_DAX 标志(无论是直接设置还是通过继承)仍然会发生。
virtiofs 上 DAX 的语义基本上与 ext4 和 xfs 上的语义相同,只是当指定了’-o dax=inode’时,virtiofs 客户端通过 FUSE 协议从 virtiofs 服务器获取是否启用 DAX 的提示,而不是持久的 FS_XFLAG_DAX 标志。也就是说,是否启用 DAX 完全由 virtiofs 服务器决定,而 virtiofs 服务器本身可能部署各种算法来做出此决定,例如取决于主机上的持久 FS_XFLAG_DAX 标志。
在客户机内部设置或清除持久性 FS_XFLAG_DAX 标志仍然受支持,但不能保证相应文件会启用或禁用 DAX。客户机内的用户仍需调用 statx(2) 并检查 statx 标志 STATX_ATTR_DAX,以查看是否为此文件启用了 DAX。
要在块驱动程序中支持 DAX,请实现’direct_access’块设备操作。该操作用于将扇区号(以 512 字节扇区为单位)转换为标识内存物理页面的页框号(pfn)。它还会返回一个可用于访问内存的内核虚拟地址。
direct_access 方法接受一个 ‘size’ 参数,表示请求的字节数。该函数应返回在该偏移量处可以连续访问的字节数。如果发生错误,它也可以返回一个负的 errno。
为了支持此方法,存储必须始终能够被 CPU 字节访问。如果您的设备使用分页技术通过较小的窗口暴露大量内存,那么您就无法实现 direct_access。同样,如果您的设备偶尔会使 CPU 长时间停滞,您也不应尝试实现 direct_access。
这些块设备可作为参考: - pmem:NVDIMM 持久内存驱动
文件系统支持包括:
dax_iomap_rw()dax_iomap_fault() ,并传递适当的故障大小和 iomap 操作。iomap_zero_range() 为 DAX 文件传递适当的 iomap 操作,而不是 block_truncate_page()用于分配块的 iomap 处理程序必须确保分配的块在返回前已清零并转换为已写入范围,以避免通过 mmap 暴露未初始化的数据。
libnvdimm 子系统为每个 pmem 块设备(在 gendisk->badblocks 中)存储已知介质错误位置的记录。如果我们在此类位置或存在尚未发现的潜在错误的位置发生故障,应用程序可以预期会收到 SIGBUS。libnvdimm 还允许通过简单写入受影响的扇区来清除这些错误(通过 pmem 驱动,并且如果底层 NVDIMM 支持 ACPI 定义的 clear_poison DSM)。
由于 DAX IO 通常不会经过 driver/bio 路径,应用程序或系统管理员可以通过以下方式从先前的 backup/inbuilt 冗余中恢复丢失的数据:
这是两条基本路径,它们允许 DAX 文件系统在媒体错误存在的情况下继续运行。未来可以在此基础上构建更强大的错误恢复机制,例如,通过 DM 在块层提供的冗余/镜像,或者额外地在文件系统级别提供。这些机制必须依赖于上述两个基本原则:错误清除可以通过驱动程序发送 IO 来完成,或者通过清零(同样通过驱动程序)来完成。
即使内核或其模块存储在支持 DAX 的文件系统上,该文件系统位于支持 DAX 的块设备上,它们仍然会被复制到 RAM 中。
DAX 代码在具有虚拟映射缓存的架构(如 ARM、MIPS 和 SPARC)上无法正常工作。
对从 DAX 文件 mmapped 的用户内存范围调用 get_user_pages() ,当没有 struct page 来描述这些页面时会失败。这个问题已经在一些设备驱动程序中得到解决,通过添加可选的 struct page 支持来支持驱动程序控制的页面(参见 drivers/nvdimm 中的 CONFIG_NVDIMM_PFN 示例,了解如何实现这一点)。在非 struct page 情况下,从非 DAX 文件对这些内存范围进行 O_DIRECT 读取/写入将会失败
struct page 情况下,其他无法实现的功能包括 RDMA、 sendfile() 和 splice() 。
https://aistudio.google.com/prompts/1xw2bDNLkdRaZq5u8OqOhAuIZm5Nwox1j
输入:
我在设计 bms 机器上启动 cloud hypervisor 作为 microvm 运行 ai agent 的方案。现在我想实现高密部署,其中有一个优化策略是降低每个 microvm 实例的内存使用。我想深入了解一下 Virtio-fs + DAX (Direct Access) 在这方面的作用,另外详细介绍一下这个技术的原理。
这是一个非常专业且切中要害的问题。在基于 BMS(裸金属服务器)+ Cloud Hypervisor + MicroVM 的架构下,Virtio-fs + DAX 是实现高密度部署(High Density)、尤其是 AI Agent 这种依赖大文件(模型、Python 库)场景的核心优化技术。
简单来说,它的核心价值在于消除了 Host 和 Guest 之间的“双重缓存”(Double Caching),实现了内存的真正共享。
以下是关于 Virtio-fs + DAX 的深入原理解析及其在 AI Agent 场景下的作用。
在没有 DAX 的传统虚拟化文件系统(如 virtio-9p 或未开启 DAX 的 virtio-fs)中,当 MicroVM 需要读取一个文件(例如 500MB 的 PyTorch 库或 2GB 的 LLM 模型)时,数据流向如下:
后果:
DAX (Direct Access) 技术允许 Guest VM 的文件系统绕过自己的 Page Cache,直接映射宿主机的文件缓存。
Virtio-fs + DAX 的工作流程利用了 PCI BAR (Base Address Register) 和 内存映射(mmap)技术。
PCI BAR 窗口(DAX Window):
文件映射(The Mapping):
read 请求读取 pytorch.so 时。virtiofsd(后端守护进程),请求建立映射(FUSE_SETUPMAPPING)。建立页表关联(The Magic):
virtiofsd 收到请求后,会把宿主机上该文件对应的 Page Cache(Host 物理内存)mmap 到 Cloud Hypervisor 的 DAX Window 中。直接访问:
传统模式 (Without DAX):
Host Disk -> Host RAM (Page Cache) -> (Copy) -> Guest RAM (Page Cache) -> Guest App
100 个 VM = 100 份 Guest Cache + 1 份 Host Cache
Virtio-fs + DAX 模式:
Host Disk -> Host RAM (Page Cache) <-- (Map/Ptr) -- Guest App (直接读取)
100 个 VM = 0 份 Guest Cache + 1 份 Host Cache
对于 AI Agent 场景,Virtio-fs + DAX 的收益是巨大的,主要体现在以下三个方面:
AI Agent 通常运行在 Python 环境中,依赖 torch, numpy, tensorflow 等巨型库。
torch 库解压后可能占用 500MB~1GB 空间。如果你的 Agent 是同质化的(例如都调用同一个本地 Embedding 模型或小型 LLM)。
在 Cloud Hypervisor 中启用 Virtio-fs DAX 需要配合 virtiofsd 使用。
1. 启动 virtiofsd (Host 端): 你需要先运行 virtio-fs 的后端守护进程。
# --cache=auto 或 always 配合 DAX 效果最好
# --socket-path 指定通信 socket
# --shared-dir 指定宿主机共享目录(存放 rootfs, models, python envs)
./virtiofsd --socket-path=/tmp/vhost-user.sock --shared-dir=/path/to/shared/agents --cache=auto
2. 配置 Cloud Hypervisor (JSON 或 CLI): 在启动 Cloud Hypervisor 时,需要指定 DAX 缓存窗口的大小。这个大小限制了 Guest 可以同时映射多少文件内容,建议设置得大一些(覆盖你的模型+库的大小)。
{
"fs": [
{
"tag": "myfs",
"socket": "/tmp/vhost-user.sock",
"num_request_queues": 1,
"queue_size": 1024,
"cache_size": "4G" <-- 这里开启 DAX,设置 DAX Window 大小为 4GB
}
],
"kernel": "...",
"cpus": "...",
"memory": "..."
}
或者 CLI 参数:
./cloud-hypervisor \
... \
--fs tag=myfs,socket=/tmp/vhost-user.sock,num_request_queues=1,queue_size=1024,cache_size=4G
3. Guest 内部挂载:
Guest 启动后,挂载文件系统需要指定 dax 选项:
mount -t virtiofs -o dax myfs /mnt/shared
Virtio-fs + DAX 是 AI Agent MicroVM 高密部署的基石。
cache_size 不要太小,至少要大于“常用热点文件总和”(如 OS 基础库 + Python 解释器 + PyTorch + 模型权重)。这一方案能让你在有限的物理内存上,将 MicroVM 的密度提高 3-10 倍(取决于 AI 负载中静态资源的占比)。
https://aistudio.google.com/prompts/1xw2bDNLkdRaZq5u8OqOhAuIZm5Nwox1j
输入:
有一个关键问题,Virtio-fs + DAX 在 arm64 平台上可以使用吗?
这是一个非常关键且实际的问题。因为 ARM64(AArch64)架构在云原生和 AI 推理场景(如 AWS Graviton, Huawei Kunpeng, Ampere Altra)中越来越普及,但很多虚拟化特性最初都是围绕 x86 开发的。
结论是:可以使用。Virtio-fs + DAX 目前已经支持 ARM64 平台,但有严格的版本要求和配置限制(尤其是页大小对齐)。
以下是针对 ARM64 平台上使用 Virtio-fs + DAX 的详细现状、限制和注意事项:
Virtio-fs 的 DAX 支持引入 ARM64 架构比 x86 晚一些,主要涉及到 ARM 架构下的内存管理(MMU)、缓存一致性(Cache Coherency)和 PCI BAR 映射机制的差异。
要使其正常工作,你需要满足以下软件栈要求:
CONFIG_DAX, CONFIG_FS_DAX, CONFIG_VIRTIO_FS。这是在 ARM64 上使用 DAX 最容易踩的坑,也是与 x86 最大的不同点。
DAX 的硬性限制:Host 和 Guest 的 Page Size 必须一致。
因为 DAX 机制是通过 mmap 将 Host 的物理页直接映射到 Guest 的物理地址空间。如果页大小不一致(例如 Host 4KB,Guest 64KB),Guest 内核在建立页表映射时会因为对齐(Alignment)问题而失败,或者回退到无 DAX 模式。
避坑指南:
getconf PAGESIZE,确认是否为 4096 (4KB)。CONFIG_ARM64_4K_PAGES=y。在 ARM64 架构上,PCI 设备(这里指 Virtio-fs 设备)映射的 MMIO 空间(DAX Window)大小可能会受到物理地址布局的限制。
cache_size 设置 8G、16G。ARM64 是弱内存序(Weakly Ordered)架构,且不同 SoC 对 DMA 一致性的处理不同。
在 ARM64 机器上部署后,务必在 Guest 内部验证 DAX 是否真正生效:
启动 Guest 后检查挂载信息:
# 在 Guest 内部
mount | grep virtiofs
# 输出应该包含 "dax" 关键字
# myfs on /mnt type virtiofs (rw,relatime,dax)
查看内核日志(dmesg): 如果 DAX 失败,内核日志通常会非常诚实地报错:
dmesg | grep -i "virtio-fs"
# 如果看到类似 "DAX unsupported" 或 "alignment error" 的警告,说明降级到了传统模式。
在 ARM64 BMS 上设计高密 AI Agent:
cache_size,初始设置为 4GB。https://www.cnblogs.com/dream397/p/13900936.html
直接访问(Direct Access,DAX) 机制是一种支持用户态软件直接访问存储于持久内存(Persistent Memory,PM) 的文件的机制,用户态软件无需先将文件数据拷贝到页高速缓存(Page Cache)。
上述描述对应到下面这张图(Typical NVDIMM Software Architecture)中,就是说(File)和(Memory)这两条 IO 路径都能绕过页高速缓存。

以下将结合 Linux v5.8-rc1 中的 XFS 为例进行介绍。
以文件写路径为例,其由 xfs_file_write_iter 提供,该函数部分代码如下所示:
STATIC ssize_t
xfs_file_write_iter(
struct kiocb *iocb,
struct iov_iter *from)
{
struct file *file = iocb->ki_filp;
struct address_space *mapping = file->f_mapping;
struct inode *inode = mapping->host;
ssize_t ret;
if (IS_DAX(inode))
return xfs_file_dax_write(iocb, from);
if (iocb->ki_flags & IOCB_DIRECT) {
ret = xfs_file_dio_aio_write(iocb, from);
if (ret != -EREMCHG)
return ret;
}
return xfs_file_buffered_aio_write(iocb, from);
}
这段代码在执行写操作的时候,将分别处理三种情况:
mount -o dax),则持久内存文件系统将为所有写操作采用该路径;O_DIRECT) ,则文件操作将采用该路径;调用 mmap 时,文件系统仅仅在进程的 mm_struct 中注册了一段使用虚拟内存区域(Virtual Memory Area,VMA)描述的虚拟地址。后续当用户态软件首次访问映射文件时,内存管理单元(Memory Managment Unit)发现页表项(Page Table Entry,PTE)为空,于是触发 14 号故障,即页故障(Page Fault),使得操作系统开始执行请求调页(Demand Paging)。此时,由虚拟内存管理器(Virtual Memory Manager)和文件系统共同管理页表,以建立虚拟内存到物理内存之间的映射关系。注意,以上为同步过程,而非异步过程,因为页故障是一个异常(Exception)而非软/硬件中断(Software/Hardware Interrupt)。
在执行 mmap 系统调用时,主要执行 do_mmap 中的 mmap_region,其根据用户态软件的请求,返回一个用于描述一段可用进程虚拟地址空间的 VMA,接着通过 call_mmap 执行文件系统注册的 mmap 实现,最后将该段 VMA 之添加在进程的 mm_struct 中。
XFS 中 mmap 由 xfs_file_mmap 实现,其中主要语句就一个:vma->vm_ops = &xfs_file_vm_ops;。它告诉异常处理例程(Exception Handler)应该调用 xfs_file_vm_ops 中相应的函数处理页错误。
请求调页主要执行 __xfs_filemap_fault,其代码如下所示:
static vm_fault_t
__xfs_filemap_fault(
struct vm_fault *vmf,
enum page_entry_size pe_size,
bool write_fault)
{
struct inode *inode = file_inode(vmf->vma->vm_file);
struct xfs_inode *ip = XFS_I(inode);
vm_fault_t ret;
trace_xfs_filemap_fault(ip, pe_size, write_fault);
if (write_fault) {
sb_start_pagefault(inode->i_sb);
file_update_time(vmf->vma->vm_file);
}
xfs_ilock(XFS_I(inode), XFS_MMAPLOCK_SHARED);
if (IS_DAX(inode)) {
pfn_t pfn;
ret = dax_iomap_fault(vmf, pe_size, &pfn, NULL,
(write_fault && !vmf->cow_page) ?
&xfs_direct_write_iomap_ops :
&xfs_read_iomap_ops);
if (ret & VM_FAULT_NEEDDSYNC)
ret = dax_finish_sync_fault(vmf, pe_size, pfn);
} else {
if (write_fault)
ret = iomap_page_mkwrite(vmf,
&xfs_buffered_write_iomap_ops);
else
ret = filemap_fault(vmf);
}
xfs_iunlock(XFS_I(inode), XFS_MMAPLOCK_SHARED);
if (write_fault)
sb_end_pagefault(inode->i_sb);
return ret;
}
显然,这段代码有两条分支:
DAX: 该路径主要调用 dax_iomap_fault。该函数首先通过 grab_mapping_entry 获取页缓存中的 DAX Exception Entry(详见3),接着通过 xfs_bmbt_to_iomap 准备一个名为 struct iomap 的数据结构。
struct iomap {
u64 addr; /* disk offset of mapping, bytes */
loff_t offset; /* file offset of mapping, bytes */
u64 length; /* length of mapping, bytes */
u16 type; /* type of mapping */
u16 flags; /* flags for mapping */
struct block_device *bdev; /* block device for I/O */
struct dax_device *dax_dev; /* dax_dev for dax operations */
void *inline_data;
void *private; /* filesystem private */
const struct iomap_page_ops *page_ops;
};
准备好 struct iomap 之后,通过 dax_iomap_pfn ,结合 struct iomap 所提供的信息,获取目标 PM 页的物理页号(Physical Page Number,pfn)。之后由 dax_insert_entry 将与该页相关联的 DAX Exception Entry 添加到用于维护页缓存的数据结构 XArray 中。最后调用 vmf_insert_mixed_mkwrite 将从 DAX Exception Entry 中缓存的 pfn 填写到对应虚拟页的 PTE 中。
正常请求调页
该路径主要调用 filemap_fault。该函数首先通过 do_sync_mmap_readahead 试图同步地预读文件数据(预读行为可受 madvise 系统调用的影响,因此也可能完全不读取),接着通过 pagecache_get_page 分配页缓存,再通过 xfs_vm_readpage 将读取块设备数据的请求发送到块设备层,从而将文件数据读取到页缓存中。在将文件数据拷贝到页缓存之后,取决于映射文件的类型(MAP_SHARED、MAP_PRIVATE)执行不同的分支。最后返回的页保存在 vmf->page 中。
当是 MAP_SHARED,主要调用 do_shared_fault 函数,该函数:
调用 xfs_file_vm_ops 中注册的 xfs_filemap_page_mkwrite,从而调用 iomap_page_create 在 vmf->page 对应的 struct page 的 private 字段中塞进去一个 struct iomap_page。
/*
* Structure allocated for each page when block size < PAGE_SIZE to track
* sub-page uptodate status and I/O completions.
*/
struct iomap_page {
atomic_t read_count;
atomic_t write_count;
spinlock_t uptodate_lock;
DECLARE_BITMAP(uptodate, PAGE_SIZE / 512);
};
finish_fault
alloc_set_pte
信息以支持空闲页回收。
当是 MAP_PRIVATE,调用 do_cow_fault。该函数首先通过 alloc_page_vma 为 VMA 分配一个页,该页保存在 vmf->cow_page 中。接着通过 copy_user_highpage 将 vmf->page 中的数据拷贝到 vmf->cow_page 中。最后通过 finish_fault 将 vmf->cow_page 映射到虚拟页上。
持久内存(Persistent Memory,PM):指能通过访存指令(区别于系统调用)访问、可按字节寻址的(区别于块)非易失存储器(Non-volatile Memory,NVM)。其中可按字节寻址(Byte-addressable)表示每个寻址单位对应一个 PM 单元(而非字或块)。
持久内存系统(PM-aware File System):指支持 DAX 机制的文件系统。该词的常见表述也包括「Persistent Memory File System」、「DAX Enabled File System」。
映射文件(Memory-mapped File):是一段虚内存逐字节对应于一个文件或类文件的资源,使得应用程序处理映射部分如同访问主内存。进程地址空间由映射文件和匿名内存(Anonymous Memory)组成。
get_block 路径替代 struct address_space_operations 中的 get_xip_mem操作。KSM (Kernel Same-page Merging):
方案: 在 Host (BMS) 内核开启 KSM。扫描内存页,将内容相同的页合并为只读页。
适用场景: 如果你的 MicroVM 运行着相同的应用程序或库,KSM 能进一步合并代码段和数据段内存。
注意: KSM 消耗 CPU 资源,需权衡扫描频率(sleep_millisecs)和 CPU 占用。
利用 ksm 合并系统内存页
Hugepages (大页内存):
方案: 在 Host 上为 Cloud Hypervisor 进程分配 2MB 或 1GB 的 Hugepages。
效果: 减少 Host CPU 的 TLB (Translation Lookaside Buffer) Miss,降低页表本身的内存占用(Page Table Overhead)。对于高密场景,海量的 4KB 页表项会占用大量内存,使用 2MB 大页是必须的。
优化 Hugepage 使用,减少 TLB miss。
去除固件 (Direct Kernel Boot):
方案: Cloud Hypervisor 支持直接加载 Linux Kernel (vmlinux),跳过 BIOS/UEFI 阶段。
效果: 既加快了启动速度,又节省了用于模拟固件的内存开销。
通过 ballooning 动态调整内存
通过 virtio-mem 动态内存热插拔