新闻详情

新闻详情

首页 / 资讯中心 / 详情

Linux PCI驱动框架解析:核心结构与probe/remove机制

发布时间:2026/9/25 19:07:53来源:尧图网络
Linux PCI驱动框架解析:核心结构与probe/remove机制
刚开始接触Linux PCI驱动的时候我其实走过一段弯路。当时照着网上示例代码把pci_enable_device、ioremap、request_irq一股脑往probe函数里塞结果不是设备枚举失败就是驱动压根没被绑定再要么一卸载模块就oops。后来才想明白一个道理PCI驱动不是凭空“写”出来的而是“挂”到Linux内核PCI子系统上的。这套框架替你干了大量脏活累活比如总线枚举、配置空间解析、资源分配你要做的是理解它怎么运作然后在正确的位置填你自己的逻辑。这一篇是《Linux PCI驱动框架分析》的第一部分先把地基打牢。我会重点拆解struct pci_driver、struct pci_device_id、probe/remove这套核心机制然后带你从零写一个最小可跑的PCI驱动并聊聊实践里最容易踩的坑。内容面向刚开始接触内核驱动开发的读者也适合那些已经能调字符设备驱动、但对PCI子系统还比较陌生的同学。中断和DMA相关的内容我放到第二部分再展开先把框架讲透否则直接上中断你会被Xarray和三层映射搞得怀疑人生。1. 先从框架说起Linux到底替你做了多少事1.1 为什么我只递给你一张“申请表”很多人第一次看到PCI驱动代码会觉得奇怪怎么驱动里没有“找设备”的代码没有遍历总线的逻辑甚至没有读取vendor/device号的代码这是因为Linux内核的PCI核心PCI Core在驱动加载之前就已经完成了几件大事枚举PCI/PCIe总线发现所有挂在上面的设备读取每个设备的配置空间Configuration Space解析出供应商ID、设备ID、类别码、BAR等信息为每个设备分配内存/IO资源并把结果写到struct pci_dev里建立sysfs节点把设备信息暴露到用户态比如/sys/bus/pci/devices/。驱动的角色实际上是个“住户”。楼是内核盖好的水电资源也是内核通的你只需要拿一张“申请表”struct pci_driver填好自己的身份信息id_table然后在probe回调里办入住手续在remove回调里办退房。整个框架设计的目标就是谁匹配、谁来管、什么时候管这三件事交给内核统一调度驱动本身只管自己的业务逻辑。我见过一些初学者试图在驱动里用pci_find_device去手动扫总线这在2.6时代还能偶遇到了现代内核基本就是给自己挖坑。因为热插拔、电源管理、IOMMU这些机制都挂在PCI子系统的统一流程里你绕过框架出问题只是时间问题。1.2 PCI/PCIe设备在主机上的基本形态在动手写驱动之前脑子里得有一张PCI/PCIe拓扑图。一条典型的PCIe链路从CPU侧看大致是这样Root Complex根复合体CPU与PCIe世界的入口负责生成配置事务、管理中断控制器等Switch/Bridge交换机/桥用于扩展总线数量pcieport驱动的节点往往就在这里热词里常见的“PCI Express Root Port”指的就是根端口设备Endpoint端点设备这才是我们通常要写驱动的目标比如网卡、显卡、FPGA板卡、采集卡。Linux内核用struct pci_bus表示一条总线用struct pci_dev表示一个设备。每个pci_dev里都带一个resource[]数组保存内核分配给它的BAR映射结果。BARBase Address Register是配置空间里的一组寄存器设备通过BAR声明自己需要多大的内存/IO空间枚举时内核看到这个声明后在地址空间里找一块空闲区域分配给它。这里有个很关键的理解点BAR地址不是设备自己定的是内核分配的。所以驱动里绝对不该硬编码BAR的物理地址而是要用pci_resource_start(pdev, bar)这一族接口从struct pci_dev里拿。硬编码地址这件事在我见过的实际故障案例里排得上前三。2. PCI驱动框架中的三个关键角色2.1 驱动侧身份证struct pci_driver先来看最核心的数据结构它在include/linux/pci.h里。现代内核的struct pci_driver大致长这样struct pci_driver { struct list_head node; const char *name; const struct pci_device_id *id_table; int (*probe)(struct pci_dev *dev, const struct pci_device_id *id); void (*remove)(struct pci_dev *dev); int (*suspend)(struct pci_dev *dev, pm_message_t state); int (*resume)(struct pci_dev *dev); void (*shutdown)(struct pci_dev *dev); int (*sriov_configure)(struct pci_dev *dev, int num_vfs); const struct pci_error_handlers *err_handler; ... };逐个说下关键字段node内核链表节点驱动注册时会被挂到全局驱动链表上总线匹配时挨个遍历。这个字段不需要你初始化框架自己维护。name驱动名会出现在/sys/bus/pci/drivers/name也是lspci -k里显示的名字。建议起得简短且能标识功能调试时一眼能认出来。id_table这是驱动的“准入名单”非常重要。总线匹配设备时会拿着这个表跟每个pci_dev的ID去比对只有命中了才会回调你的probe。如果这个表为空设备永远不会绑定到你的驱动。probe设备与驱动匹配成功后内核调用这个回调参数里有匹配到的struct pci_dev设备对象以及命中的那条id_table表项。你的初始化工作基本都放这里。remove设备被移除、热拔插、或者驱动卸载时调用做资源释放。注意probe里申请了什么这里就得逆序释放什么。suspend/resume、err_handler这类属于电源管理和错误处理的高级功能第一篇文章先不展开但你写驱动时至少留个意识如果设备支持休眠唤醒光靠标配的probe/remove是不够的。2.2 匹配规则struct pci_device_id 与 PCI_DEVICE 宏struct pci_device_id是匹配工作的最小单元定义如下struct pci_device_id { __u32 vendor, device; /* 供应商ID和设备ID */ __u32 subvendor, subdevice; /* 子系统ID通常可以填PCI_ANY_ID */ __u32 class, class_mask; /* 类别码及其掩码 */ kernel_ulong_t driver_data; /* 驱动私有数据随匹配结果传给probe */ };匹配时内核按vendor/device/subvendor/subdevice的规则逐一比对任一字段是PCI_ANY_ID就表示“不关心什么都行”。注意PCI_ANY_ID的值是~0所以只要有一个字段填了它匹配就会放宽那一位。最常见的初始化方式是PCI_DEVICE宏#define PCI_DEVICE(vend, dev) \ .vendor (vend), .device (dev), \ .subvendor PCI_ANY_ID, .subdevice PCI_ANY_ID这个宏适合大多数场景我只关心主vendor/device不管子系统ID是什么。如果你的设备会有多个功能版本还想在probe里区分它们可以单独写一条表项并带上driver_datastatic const struct pci_device_id my_pci_ids[] { { PCI_DEVICE(0x1234, 0x5678), .driver_data (kernel_ulong_t)board_ver_a_ops }, { PCI_DEVICE(0x1234, 0x9999), .driver_data (kernel_ulong_t)board_ver_b_ops }, { } };然后probe里拿id-driver_data做区分比在probe里再去读硬件寄存器判断版本要干净得多。还有一行不能被简化掉的代码MODULE_DEVICE_TABLE(pci, my_pci_ids);它的作用不是给内核用的而是给用户态的modprobe用的。编译时会生成一个modules.pcimap文件modprobe看到设备ID匹配后才可能自动帮你加载对应的驱动模块。没有这行insmod手动加载没问题一旦遇到热插拔或者modprobe自动加载设备就是驱动不起来。2.3 probe/remove驱动和设备“对上眼”之后的两条主线整个PCI驱动子系统的调度主线其实非常清晰驱动调用pci_register_driver()把自己注册进内核PCI核心遍历总线上所有pci_dev拿该驱动的id_table逐个匹配命中后创建驱动-设备之间的绑定关系调用probeprobe成功返回0设备正式归你管设备拔出或驱动卸载时调用remove解除绑定。probe是在内核线程的进程上下文里调用的可以睡眠可以分配内存可以注册字符设备。匹配成功之后设备的dev指针下会挂上驱动节点/sys/bus/pci/drivers/name/目录下会出现以0000:00:01.0这种“域:总线:设备.功能”格式命名的符号链接。我说两个新手最容易被绕晕的点第一probe不是“扫描”设备而是“响应”匹配。你不需要在probe里判断这个设备在不在因为进来的时候一定在。第二remove不是只有在物理拔卡时才触发。rmmod卸载驱动、系统关机流程里走的shutdown路径也都可能走到remove。所以remove里必须有完整的资源清理逻辑不能假设设备还在。真做过热插拔测试的人都知道remove写得烂比probe写得烂危害更大因为它发生在设备可能已经半失效的状态下。3. 从零写一个最小PCI驱动跑起来3.1 工程铺垫Makefile与模块骨架从空目录开始。先建一个my_pci.c最小骨架如下#include linux/kernel.h #include linux/module.h #include linux/pci.h #define MY_VENDOR_ID 0x1234 #define MY_DEVICE_ID 0x5678 static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) { dev_info(pdev-dev, my_pci_probe: vendor0x%04x device0x%04x\n, id-vendor, id-device); return 0; } static void my_pci_remove(struct pci_dev *pdev) { dev_info(pdev-dev, my_pci_remove\n); } static const struct pci_device_id my_pci_ids[] { { PCI_DEVICE(MY_VENDOR_ID, MY_DEVICE_ID) }, { } }; MODULE_DEVICE_TABLE(pci, my_pci_ids); static struct pci_driver my_pci_driver { .name my_pci, .id_table my_pci_ids, .probe my_pci_probe, .remove my_pci_remove, }; module_pci_driver(my_pci_driver); MODULE_LICENSE(GPL);这个模块没干任何实事但它是一个“框架完整”的最小驱动非常适合作为调试的起点。具体约定module_pci_driver宏展开后会自动帮你生成module_init/module_exit内部就是调pci_register_driver和pci_unregister_driver。这个宏存在的意义就是减少样板代码如果probe之前要做额外准备可以拆开写。数组末尾的{}是哨兵绝对不能省略。省略后内核遍历表项时会踩过界轻则匹配异常重则直接崩溃。MODULE_LICENSE(GPL)在需要导出符号或使用某些GPL-only API时是必须的否则链接时直接报Unknown symbol。建议从一开始就写成GPL别贪省事。Makefile同样极其简单obj-m : my_pci.o KDIR : /lib/modules/$(shell uname -r)/build all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean我习惯加上-Wall在ccflags-y -Wall里开一下内核自身编译选项已经比较严格但多一层警告对排错没坏处。编译命令就是make得到my_pci.ko。3.2 probe里的标准动作enable、request、map骨架跑通之后probe里至少要完成三件标准动作顺序几乎可以说是行业惯例static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) { int ret; resource_size_t start, len; void __iomem *bar0_va; /* 1. 使能设备 */ ret pci_enable_device(pdev); if (ret) { dev_err(pdev-dev, pci_enable_device failed: %d\n, ret); return ret; } /* 2. 检查并申请BAR0资源 */ if (!(pci_resource_flags(pdev, 0) IORESOURCE_MEM)) { dev_err(pdev-dev, BAR0 is not memory region\n); ret -ENODEV; goto err_disable; } ret pci_request_regions(pdev, my_pci); if (ret) { dev_err(pdev-dev, pci_request_regions failed: %d\n, ret); goto err_disable; } /* 3. 将BAR0的物理地址映射到内核虚拟地址 */ start pci_resource_start(pdev, 0); len pci_resource_len(pdev, 0); bar0_va ioremap(start, len); if (!bar0_va) { dev_err(pdev-dev, ioremap failed for BAR0\n); ret -ENOMEM; goto err_release; } dev_info(pdev-dev, BAR0 phys%pa len0x%llx va%px\n, start, (unsigned long long)len, bar0_va); /* 这里之后才适合做查ID、初始化寄存器、注册file_operations等业务 */ pci_disable_device(pdev); return ret; }逐个解释为什么是这个顺序第一步先pci_enable_device。这个调用会让PCI核心为你设置命令寄存器里的BUS MASTER、IO/MEMORY访问使能位同时校准电源管理状态。很多新手上来就ioremap结果读寄存器全读到FFFFFFFF再排查半天才发现是设备根本没被enabled。注意pci_enable_device可能因为资源不足返回错误别忽略返回值。第二步pci_request_regions。这个函数像给设备资源“挂牌”告诉内核这块BAR区域我占用了。它跟裸的request_mem_region的区别是它会自动遍历设备的所有BAR挨个申请。如果其他驱动已经声明占用这里会返回-EBUSY。这个调用是可选的但强烈建议做否则别的驱动可能把你正在用的BAR区域抢走。第三步ioremap。PCI资源管理的是物理地址驱动不能直接访问物理地址必须映射到内核虚拟地址空间。映射之后拿到的void __iomem *指针通过readl/writel或ioread32/iowrite32访问。别直接解引用。我在第三步前面特意加了对BAR属性的判断。这个判断极其实用不是每个设备的BAR0都映射到内存空间有些老设备BAR类型是IORESOURCE_IO走的是IO端口空间处理方式完全不同。拿到BAR就无脑ioremap是新手最常见的问题之一。3.3 remove与错误路径逆序释放是一门课有申请就必须有释放且要严格逆序。完整的remove长这样static void my_pci_remove(struct pci_dev *pdev) { void __iomem *bar0_va pci_get_drvdata(pdev); if (bar0_va) iounmap(bar0_va); pci_release_regions(pdev); pci_disable_device(pdev); dev_info(pdev-dev, my_pci_remove done\n); }顺序逻辑很简单probe是“enable → request → iomap”remove就是“iounmap → release → disable”。为什么不能反过来因为如果先disable设备再释放区域某些设备在disable状态下访问BAR会异常而先disable再iounmap倒是不会崩但可能让设备在映射仍然存在的前提下失去电源引发固件层面的怪问题。keep顺序一致是成本最低的做法。这里还引出一个细节probe里怎么把映射好的虚拟地址传给remove官方推荐做法是pci_set_drvdata(pdev, bar0_va)在remove里用pci_get_drvdata(pdev)取回。不要自己开全局变量存多设备实例时全局变量一定翻车。更值得注意的是probe的错误路径。上面的示例中pci_request_regions失败时跳到了err_disableioremap失败时跳到err_release。这种err label的设计要求每当你申请了一个资源就要记下它处于“已申请”状态后续一旦失败按最近申请的那个资源开始逆序释放。我再三强调逆序是因为我见过太多人probe里错误路径漏掉pci_disable_device结果模块卸载时设备还处于enable状态下次加载时直接资源冲突。3.4 编译、加载和验证lspci与sysfs双看编译流程make如果没报错加载sudo insmod my_pci.ko然后立刻看内核日志dmesg | tail -20如果你的设备在机器上并且vendor/device匹配应该能看到类似my_pci 0000:01:00.0: my_pci_probe: vendor0x1234 device0x5678这里0000:01:00.0就是设备的“域:总线:设备.功能”编号。如果看不到这行先别急着怀疑驱动按下一节的排查思路走。再验证绑定关系lspci -k你应该能在设备那一行下面看到Kernel driver in use: my_pci。lspci -vvv还能看到BAR地址范围和IO资源分配情况这是后文排查必须的工具。另一个验证入口是sysfscat /sys/bus/pci/devices/0000:01:00.0/vendor cat /sys/bus/pci/devices/0000:01:00.0/device ls -l /sys/bus/pci/drivers/my_pci/如果列表里出现设备的符号链接说明内核已经确认绑定。这两个验证动作比看打印可靠得多因为有些probe日志被quiet参数淹没了。如果你手头暂时没有真实硬件用QEMU虚拟一个PCI设备也是可行的验证途径。比如-device pci-bridge再挂一个模拟设备或者直接用Linux内核自带的pci_epf_test这类端点测试驱动做实验。驱动开发初期用模拟环境把框架跑通比直接上真机省心不少。4. 实践中的高频问题与排查实录4.1 设备就是驱动不起来先查id_table和驱动绑定这是所有PCI驱动开发的第一道坎。现象是lspci能看到设备vendor/device你也确认过没问题但dmesg里就是不见probe日志。我按照概率从高到低列排查顺序第一查id_table。很多人把vendor和device写反了或者十六进制少写一位。对照lspci -n输出的原始数字逐位核对别用眼睛跟手册上的十进制转换值较劲。第二查驱动是否已经绑定给别的驱动。打个lspci -k如果设备下面显示Kernel driver in use: xxx说明别家驱动已经占坑了。此时你即使insmod自己的模块PCI核心也不会解绑现有驱动来“重婚”。需要先卸载那个驱动或者通过sysfs手动unbind再绑定你的驱动。第三查模块有没有真的注册成功。lsmod | grep my_pci确认在不在再看/sys/bus/pci/drivers/my_pci/目录是否存在。有时候insmod返回成功但因为module_pci_driver展开的问题驱动根本没注册。第四查是不是自动加载的锅。如果你用的是modprobe my_pci而不是insmod而代码里漏了MODULE_DEVICE_TABLE(pci, ...)那么modprobe不会从modules.pcimap里拿到匹配关系模块虽然加载了但系统不会自动触发绑定。手动echo -n 0000:01:00.0 /sys/bus/pci/drivers/my_pci/bind能通就是这个原因。我还遇到过一种不大不小的情况设备在probe里返回了非零错误码PCI核心会理解为绑定失败设备回到无驱动状态而后面的日志被别的输出冲走了。所以排查第一步永远是dmesg里搜自己的驱动名确认到底是没进probe还是probe里炸了。4.2 probe返回错误码三步定位法如果probe进去了但设备最终没被绑定日志里通常有我们的dev_err打印错误码能告诉我们大致方向。我习惯用三步定位法第一步确认pci_enable_device是否成功。失败时错误码多为-EIO或-EBUSY重点看BAR资源有没有被内核分配。可以用cat /sys/bus/pci/devices/0000:01:00.0/resource查看各resource段的起止地址。如果全部是0说明内核枚举时就没分配成功多半是系统地址窗口不足或者固件ACPI表把窗口限制死了。第二步确认pci_request_regions是否返回-EBUSY。这种情况通常是你重复加载了两次驱动或者另一个驱动已经把同一块BAR申请走了。用cat /proc/iomem在资源树里搜索对应地址段就能看到谁占着。第三步确认ioremap之后指针是否非空。30%的情况是BAR长度和起始地址算错了比如用错了索引明明BAR1才是你要的寄存器块结果BAR0映射出来一块全零区域。ioremap对非法地址不会直接崩而是返回NULL所以一定要判空然后输出pci_resource_start结果。这里再强调一个我经常说的point收集现场信息比猜原因快得多。probe里每一阶段都留一行dev_info/dev_err打印阶段名和错误码。框架跑通后再删成本低收益高。很多内核开发者嘴上说着打印不好实际调试时打印真香。PCI驱动排查常见问题速查现象可能原因优先排查动作lspci有设备dmesg无probeid_table不匹配/驱动被占用lspci -n核对IDlspci -k查驱动占用probe里enable失败BAR资源未分配cat resource文件看BAR地址request_regions返回EBUSY资源被占用/驱动重复加载grep /proc/iomem定位占用者ioremap返回NULLBAR起始地址非法打印pci_resource_start核对卸载模块卡死或oopsremove里释放顺序错检查是否有iounmap/release/disable顺序错误模块加载成功但modprobe不自动绑定缺少MODULE_DEVICE_TABLE检查modules.pcimap是否有对应条目4.3 三个容易踩的坑BAR属性、64位BAR、DMA掩码最后说三个我在真实项目里反复遇到的坑虽然部分细节第二部分还会展开但第一篇文章就可以先预防。坑一BAR0不一定是内存映射空间。前面代码里加了pci_resource_flags(pdev, 0) IORESOURCE_MEM的判断就是这个原因。碰到IORESOURCE_IO类型的BAR你不能用ioremap而应该用pci_iomap或者request_region加inb/outb系列函数。很多老式工业IO卡就是这种设计不去查flags直接map读回来的寄存器全是乱的。坑二64位BAR在32位上下文里被截断。现代DDR控制器、GPU、FPGA板卡的BAR长度经常超过4GB。内核里resource_size_t在64位平台是u64但如果你习惯用它给的值强转unsigned long在32位内核或者某些封装代码里就截断了。访问BAR地址时用pci_resource_start返回的resource_size_t原样传递打印时用%pa或者显示(unsigned long long)强转别用裸的%x。坑三DMA相关的掩码和总线master位要在第一次DMA之前配好。pci_enable_device之后、真正启动DMA之前至少要调dma_set_mask_and_coherent()设置DMA地址范围如果设备要发起总线读写还需要调pci_set_master()开启总线主控。这些顺序出问题典型症状是DMA传输时数据全是零或者直接报总线错误。掩码值怎么选跟设备硬件支持的地址宽度直接相关这块内容属于DMA专题第二部分我会带详细的配置流程和实测记录。写到这里我要说一个自己的习惯拿到一块新板卡不管业务再简单我第一版驱动永远先写一个只打log的probe/remove跑通框架再往上加具体逻辑。这样做有三个好处第一隔离问题域框架出问题跟业务出问题不纠缠第二验证硬件基本访问路径probe能打日志说明设备枚举、资源分配、映射路径都是通的第三以后每次改代码都有一份“最小可跑版本”做回归对照。这个做法帮我省了大量调试时间。最典型的一次是某个FPGA加速卡用户说自己的驱动偶尔加载失败我远程上去一看他新版本probe里加了一个超长的寄存器初始化流程最后在某个I2C读取上睡了一百多毫秒导致热插拔事件窗口内probe没跑完被内核判定为超时。用最小probe一对照问题立刻暴露出来。所以别嫌我这个“骨架驱动”太简单它其实是你在PCI世界里最可靠的参照物。下一篇我会接着聊这个框架的中断路径和DMA映射MSI/MSI-X怎么申请、request_threaded_irq在线程中断里怎么用、dma_alloc_coherent和streaming映射怎么选以及iommu介入之后地址映射发生了什么变化。这些才是让PCI驱动真正“跑业务”的部分。有真实板卡的同学建议在动手前先把今天这篇的最小驱动在你的设备上跑通跑不通过后面的中断和DMA调试会让你怀疑人生。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Agent Skills 实战指南:为 AI Agent 构建可复用的任务执行能力 2026/9/25 19:45:42

Agent Skills 实战指南:为 AI Agent 构建可复用的任务执行能力

很多做 AI Agent 的朋友应该都有过这种体验:单次对话里让模型写个文案、改段代码,效果挺惊艳的;但只要一进入“多步骤任务”,比如“帮我把这周的用户反馈全部读一遍,分类整理,再挑出最严重的十条给产品经理…

阅读更多 →
AI人才缺口500万!零基础小白也能入行的3条高薪路径全解析 2026/9/25 19:45:29

AI人才缺口500万!零基础小白也能入行的3条高薪路径全解析

人社部数据显示,我国AI人才缺口已超500万,大厂高薪抢人。文章解析AI行业三个层级:应用层(门槛低,如AI应用/训练师)、模型层(薪资高,需编程数学基础)、数据层(…

阅读更多 →
AI真的按你声明的流程做了吗?CANNBot-Sentry Skill执行遵循性审计实战教程 2026/9/25 19:45:16

AI真的按你声明的流程做了吗?CANNBot-Sentry Skill执行遵循性审计实战教程

AI真的按你声明的流程做了吗?CANNBot-Sentry Skill执行遵循性审计实战教程 【免费下载链接】cannbot-sentry CANN 生态中面向 Agent 工作流的“哨兵”:观测 审计 评测三位一体的质量基础设施 项目地址: https://gitcode.com/cann/cannbot-sentry …

阅读更多 →
收藏 | 转行FDE:AI时代高薪职业,小白也能学的大模型实战指南 2026/9/25 19:44:57

收藏 | 转行FDE:AI时代高薪职业,小白也能学的大模型实战指南

本文介绍了Forward-Deployed Engineer(FDE)这一新兴职业,强调其结合了软件工程、AI技术和解决方案架构等多方面能力,是希望进入AI行业人士的理想转型方向。文章详细阐述了FDE的核心能力要求,包括软件工程基础、LLM与RA…

阅读更多 →
遗嘱继承与法定继承:规范分析与裁判规则——以宁波地区司法实践为例(基于民法典继承编) 2026/9/25 19:43:54

遗嘱继承与法定继承:规范分析与裁判规则——以宁波地区司法实践为例(基于民法典继承编)

以下从规范分析与裁判规则角度,对相关问题作技术梳理,供研究与实务参考。 一、问题的提出 高净值家庭的财富代际传递中,被继承人多倾向于以遗嘱自主安排遗产。然而,自书、代书、打印、录音录像、口头、公证等六类遗嘱的形式要求各…

阅读更多 →
AI改写为什么必须验证?avoid-ai-writing保留性校验器validate.js与编辑契约设计详解 2026/9/25 19:43:42

AI改写为什么必须验证?avoid-ai-writing保留性校验器validate.js与编辑契约设计详解

AI改写为什么必须验证?avoid-ai-writing保留性校验器validate.js与编辑契约设计详解 【免费下载链接】avoid-ai-writing Skill that audits and rewrites content to remove AI writing patterns. Use it with your favorite agents including Claude Code, OpenCla…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉