恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

06-字符设备驱动

  • 首页
  • 资讯中心
  • /
  • 06-字符设备驱动

相关资讯

impeccable:面向 Playwright 的可信 CLI 环境交付工具 2026/10/7 6:54:27
MPU6050姿态解算:万向节死锁原因与四元数互补滤波实战 2026/10/7 6:54:27
Java后端转Agent开发:架构、记忆机制与框架选型实战 2026/10/7 6:49:25

最新资讯

Type-C、USB-A、Lightning接口针脚定义与协议差异全解析
全彩夜视技术解析:从红外补光到ADAS集成的工程实践
U-Boot移植实战:从DDR初始化到串口调试的完整指南
OpenClaw 应用场景有哪些?从 AI 智能体到自动化任务落地
弃用Trae转投Kiro后,我把AI编程工具对比做成了可复现清单
TPU薄膜供应商怎么选?实战经验谈:参数、验厂与合同避坑

今日推荐

SSD不认盘怎么修?金士顿SV300板级排查与短接ROM进工厂模式
Unity 3D RPG开发:C#状态机与物理更新时机实战指南
AIoT开发工程师岗位全景:从嵌入式Linux到边缘计算与端侧AI部署

本周热门

MR25H40CDF + PIC18F65K40:工业记录仪高可靠存储实战
基于STM32的数控恒压恒流电源设计:从硬件到PID调参全解析
LT9211 MIPI重定时器原理与双路扇出实战指南

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

06-字符设备驱动

发布时间:2026/10/7 6:54:27
06-字符设备驱动 6.1 Linux 字符设备驱动结构6.1.1cdev结构体/* include/linux/cdev.h7.2.5 */ struct cdev { struct kobject kobj; struct module *owner; const struct file_operations *ops; struct list_head list; dev_t dev; unsigned int count; } __randomize_layout; void cdev_init(struct cdev *, const struct file_operations *); struct cdev *cdev_alloc(void); void cdev_put(struct cdev *p); int cdev_add(struct cdev *, dev_t, unsigned); void cdev_del(struct cdev *); int cdev_device_add(struct cdev *cdev, struct device *dev); void cdev_device_del(struct cdev *cdev, struct device *dev); void cdev_set_parent(struct cdev *p, struct kobject *kobj);要点cdev是设备号到file_operations的映射表项。VFS 打开字符设备时用i_rdev在cdev_map中查找cdev取出ops。owner必须是THIS_MODULEVFS 会在open时自动try_module_get()这就是第 4 章说的通常不用手写引用计数。cdev可以内嵌在你的设备结构体里推荐也可以用cdev_alloc()动态分配旧写法容易忘记释放。6.1.2 分配和释放设备号两种方式/* 方式 1静态指定主设备号不推荐除非是内核文档中保留的号 */ int register_chrdev_region(dev_t from, unsigned count, const char *name); /* 方式 2动态分配主设备号推荐 */ int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name); /* 释放两者都用它 */ void unregister_chrdev_region(dev_t from, unsigned count);设备号宏include/linux/kdev_t.hMAJOR(dev) MINOR(dev) MKDEV(ma, mi)设备号的三个区间现代内核类型范围用途静态保留legacy1–254部分老驱动固定使用容易冲突动态分配由alloc_chrdev_region从 254 往下找新驱动必须用这个扩展extended主设备号 25512 位主 20 位次需要设备数非常多时# 查看当前已注册的字符设备主设备号 cat /proc/devicesregister_chrdev()的真相原书提到过static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops) { return __register_chrdev(major, 0, 256, name, fops); }它一次占用256 个次设备号且不能精确控制范围。新代码不要用只在维护老驱动时保留。6.1.3file_operations结构体7.2.5 的完整定义在include/linux/fs.h。驱动常用的成员成员原型要点说明ownerstruct module *必须THIS_MODULEfop_flagsfop_flags_t7.2.5 新增声明异步缓冲读写、mmap 同步、大页等能力llseekloff_t (*)(struct file *, loff_t, int)改变文件偏移readssize_t (*)(struct file *, char __user *, size_t, loff_t *)简单读复杂/异步场景改用read_iterwritessize_t (*)(struct file *, const char __user *, size_t, loff_t *)简单写read_iter/write_iterssize_t (*)(struct kiocb *, struct iov_iter *)支持 iovec、io_uring、直接 I/Oiopollint (*)(struct kiocb *, struct io_comp_batch *, unsigned int)轮询式完成高性能设备poll__poll_t (*)(struct file *, struct poll_table_struct *)select/poll/epollunlocked_ioctllong (*)(struct file *, unsigned int, unsigned long)不要再用已删除的.ioctlBKL 版本compat_ioctl同上32 位程序在 64 位内核上调用 ioctlmmapint (*)(struct file *, struct vm_area_struct *)把设备内存映射到用户空间mmap_prepareint (*)(struct vm_area_desc *)7.2.5 新增mmap 的现代替代入口openint (*)(struct inode *, struct file *)打开flushint (*)(struct file *, fl_owner_t)每次 close 都调用releaseint (*)(struct inode *, struct file *)最后一个引用消失时调用fsyncint (*)(struct file *, loff_t, loff_t, int)刷数据fasyncint (*)(int, struct file *, int)异步通知第 9 章uring_cmdint (*)(struct io_uring_cmd *, unsigned int)现代io_uring 直通命令struct file_operations是__randomize_layout的结构体布局随机化防御攻击。因此必须用指定初始化器.read my_read不能按位置初始化。不要假设成员的顺序或偏移。6.1.4 字符设备驱动的组成一个规范的字符设备驱动包含 5 部分1. 设备结构体 struct my_dev { struct cdev cdev; struct device *dev; ... } 2. file_operations 实现 open/read/write/llseek/ioctl/release 3. 设备号 cdev 注册 alloc_chrdev_region() cdev_init()/cdev_device_add() 4. 设备模型注册 class_create() device_create()或 cdev_device_add 5. 模块 init/exit module_init/module_exit或 module_platform_driver6.2 globalmem 虚拟设备实例描述globalmem一块全局内存用户可以通过字符设备读写它。它是原书的经典示例因为没有真实硬件纯粹用内存模拟完全适合 QEMU。覆盖了字符设备驱动的全部核心机制设备号、cdev、fops、读写、llseek、ioctl、私有数据、并发控制第 7 章、阻塞/轮询第 8 章、异步通知第 9 章。规格项目说明设备名/dev/globalmem可支持多个实例globalmem0、globalmem1容量默认 4 KiB模块参数sizeioctl 可改操作open/read/write/lseek/ioctl/releaseioctlMEM_CLEAR清零、MEM_GET_SIZE、MEM_SET_SIZE并发用 mutex 保护第 7 章展开6.3 globalmem 设备驱动6.3.1 头文件、宏及设备结构体/* globalmem.c —— 完整可编译的 Linux 7.2.5 字符设备驱动 */ #include linux/module.h #include linux/init.h #include linux/fs.h #include linux/cdev.h #include linux/device.h #include linux/slab.h #include linux/uaccess.h #include linux/mutex.h #include linux/kdev_t.h #include linux/moduleparam.h #define GLOBALMEM_SIZE_DEFAULT 0x1000 /* 4 KiB */ #define GLOBALMEM_MAGIC g /* ioctl 命令编码见 Documentation/userspace-api/ioctl/ioctl-number.rst */ #define MEM_CLEAR _IO(GLOBALMEM_MAGIC, 0) #define MEM_GET_SIZE _IOR(GLOBALMEM_MAGIC, 1, unsigned long) #define MEM_SET_SIZE _IOW(GLOBALMEM_MAGIC, 2, unsigned long) struct globalmem_dev { struct cdev cdev; /* 内嵌 cdev推荐 */ struct device *device; /* 设备模型对象 */ struct mutex lock; /* 保护 mem/size第 7 章详解 */ unsigned char *mem; /* 数据缓冲区 */ unsigned int size; /* 当前容量 */ }; static unsigned int globalmem_major; static unsigned int globalmem_minor; static unsigned int globalmem_count 1; module_param(globalmem_count, uint, 0444); MODULE_PARM_DESC(globalmem_count, Number of globalmem devices (default 1)); static unsigned int globalmem_size GLOBALMEM_SIZE_DEFAULT; module_param(globalmem_size, uint, 0444); MODULE_PARM_DESC(globalmem_size, Default device size in bytes); static struct globalmem_dev *globalmem_devices; static struct class *globalmem_class;6.3.2 加载与卸载设备驱动static int globalmem_setup(struct globalmem_dev *dev, int index) { int ret; mutex_init(dev-lock); dev-size globalmem_size; /* 用 devm 分配随 device 释放自动回收 */ dev-mem devm_kzalloc(dev-device, dev-size, GFP_KERNEL); if (!dev-mem) return -ENOMEM; cdev_init(dev-cdev, globalmem_fops); dev-cdev.owner THIS_MODULE; /* 现代推荐一次性完成 cdev_add device_add */ ret cdev_device_add(dev-cdev, dev-device); if (ret) return ret; dev_set_drvdata(dev-device, dev); dev_info(dev-device, globalmem%d registered: %u bytes\n, index, dev-size); return 0; } static int __init globalmem_init(void) { dev_t devno; int i, ret; /* 1) 动态分配设备号 */ ret alloc_chrdev_region(devno, 0, globalmem_count, globalmem); if (ret) return ret; globalmem_major MAJOR(devno); globalmem_minor MINOR(devno); /* 2) 创建 class7.2.5 签名只有一个参数 */ globalmem_class class_create(globalmem); if (IS_ERR(globalmem_class)) { ret PTR_ERR(globalmem_class); goto err_unreg; } /* 3) 为每个实例创建设备 */ globalmem_devices kcalloc(globalmem_count, sizeof(*globalmem_devices), GFP_KERNEL); if (!globalmem_devices) { ret -ENOMEM; goto err_class; } for (i 0; i globalmem_count; i) { dev_t dev MKDEV(globalmem_major, globalmem_minor i); struct device *d; /* device_initialize 设置字段 dev_set_name交给 cdev_device_add 添加 */ d device_create(globalmem_class, NULL, dev, NULL, globalmem%d, i); if (IS_ERR(d)) { ret PTR_ERR(d); goto err_devices; } globalmem_devices[i].device d; ret globalmem_setup(globalmem_devices[i], i); if (ret) goto err_devices; } pr_info(globalmem: registered %u device(s), major%u\n, globalmem_count, globalmem_major); return 0; err_devices: while (--i 0) { cdev_device_del(globalmem_devices[i].cdev, globalmem_devices[i].device); put_device(globalmem_devices[i].device); } kfree(globalmem_devices); err_class: class_destroy(globalmem_class); err_unreg: unregister_chrdev_region(MKDEV(globalmem_major, globalmem_minor), globalmem_count); return ret; } static void __exit globalmem_exit(void) { int i; for (i 0; i globalmem_count; i) { cdev_device_del(globalmem_devices[i].cdev, globalmem_devices[i].device); put_device(globalmem_devices[i].device); } kfree(globalmem_devices); class_destroy(globalmem_class); unregister_chrdev_region(MKDEV(globalmem_major, globalmem_minor), globalmem_count); pr_info(globalmem: unloaded\n); }关于device_create()与cdev_device_add()的组合说明device_create()内部已经调用device_register()即device_add()设备已上线。cdev_device_add()会再次调用device_add()。两者不能混用。上面示例是教学简化device_create()cdev_device_add()在 7.2.5 上会因为重复device_add()触发警告。正确写法二选一/* 写法 A推荐最现代device_initialize cdev_device_add */ dev kzalloc(sizeof(*dev), GFP_KERNEL); device_initialize(dev); dev-devt MKDEV(globalmem_major, globalmem_minor i); dev-class globalmem_class; dev-parent NULL; dev-release globalmem_release_dev; /* 必须提供 */ dev_set_name(dev, globalmem%d, i); cdev_init(globalmem_devices[i].cdev, globalmem_fops); globalmem_devices[i].cdev.owner THIS_MODULE; ret cdev_device_add(globalmem_devices[i].cdev, dev); /* 清理cdev_device_del(cdev, dev); put_device(dev); */ /* 写法 B最简单device_create cdev_add */ dev device_create(globalmem_class, NULL, devno, NULL, globalmem%d, i); cdev_init(globalmem_devices[i].cdev, globalmem_fops); globalmem_devices[i].cdev.owner THIS_MODULE; ret cdev_add(globalmem_devices[i].cdev, devno, 1); /* 清理cdev_del(cdev); device_destroy(globalmem_class, devno); */本手册后续示例统一使用写法 Bdevice_createcdev_add因为它最直观产品代码推荐写法 Acdev_device_add因为 sysfs 的父子关系与引用计数更正确。关键点无论哪种写法cdev的注册/注销与device的创建/销毁必须成对且顺序相反。6.3.3 读写函数static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { struct globalmem_dev *dev filp-private_data; unsigned long p *ppos; unsigned int cnt; if (p dev-size) return 0; /* EOF 语义 */ cnt min_t(unsigned int, count, dev-size - p); mutex_lock(dev-lock); if (copy_to_user(buf, dev-mem p, cnt)) { mutex_unlock(dev-lock); return -EFAULT; } mutex_unlock(dev-lock); *ppos cnt; pr_debug(globalmem: read %u bytes from offset %lu\n, cnt, p); return cnt; } static ssize_t globalmem_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos) { struct globalmem_dev *dev filp-private_data; unsigned long p *ppos; unsigned int cnt; if (p dev-size) return -ENOSPC; /* 写越界没有空间 */ cnt min_t(unsigned int, count, dev-size - p); mutex_lock(dev-lock); if (copy_from_user(dev-mem p, buf, cnt)) { mutex_unlock(dev-lock); return -EFAULT; } mutex_unlock(dev-lock); *ppos cnt; pr_debug(globalmem: wrote %u bytes at offset %lu\n, cnt, p); return cnt; }必须理解的细节count与size都是size_t但p是loff_t。减法与比较必须注意类型否则负数会变成巨大的无符号数经典漏洞。内核不能直接解引用用户指针必须copy_to_user/copy_from_user。返回非 0 表示有字节没拷成功要返回-EFAULT。*ppos是本次打开的文件偏移驱动必须更新它除非你要实现总是从头读的语义。返回0表示 EOF返回正值表示实际字节数返回负值表示错误。加锁范围要覆盖数据访问但不要把copy_to_user放在自旋锁里它可能缺页睡眠。这里用 mutex 是正确的。6.3.4 seek 函数static loff_t globalmem_llseek(struct file *filp, loff_t offset, int whence) { struct globalmem_dev *dev filp-private_data; loff_t newpos; switch (whence) { case SEEK_SET: newpos offset; break; case SEEK_CUR: newpos filp-f_pos offset; break; case SEEK_END: newpos dev-size offset; break; default: return -EINVAL; } if (newpos 0) return -EINVAL; filp-f_pos newpos; return newpos; }注意传统实现允许seek 到容量之外只有真正读写时才报错。7.2.5 中更严格的做法是限制在[0, size]内fix_size_perm()之类的辅助逻辑在很多驱动里出现。如果设备不支持 seek不要实现.llseek而是在.open中调用nonseekable_open(inode, filp)内核会自动拒绝lseek。现代替代方案用simple_attr、seq_file、bin_attribute等现成机制。6.3.5 ioctl 函数static long globalmem_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct globalmem_dev *dev filp-private_data; unsigned long size; /* 1) 先校验命令的类型避免误处理其他驱动的命令 */ if (_IOC_TYPE(cmd) ! GLOBALMEM_MAGIC) return -ENOTTY; switch (cmd) { case MEM_CLEAR: mutex_lock(dev-lock); memset(dev-mem, 0, dev-size); mutex_unlock(dev-lock); pr_info(globalmem: cleared %u bytes\n, dev-size); return 0; case MEM_GET_SIZE: return put_user((unsigned long)dev-size, (unsigned long __user *)arg); case MEM_SET_SIZE: if (get_user(size, (unsigned long __user *)arg)) return -EFAULT; if (size 0 || size (1 20)) /* 限制 1 MiB */ return -EINVAL; mutex_lock(dev-lock); /* 简化实现重新分配缓冲区 */ { unsigned char *newmem kzalloc(size, GFP_KERNEL); if (!newmem) { mutex_unlock(dev-lock); return -ENOMEM; } kfree(dev-mem); dev-mem newmem; dev-size size; } mutex_unlock(dev-lock); return 0; default: return -ENOTTY; /* 不认识的命令必须返回 ENOTTY */ } }ioctl 的现代规范Documentation/userspace-api/ioctl/ioctl-number.rst必须用_IO/_IOR/_IOW/_IOWR宏编码命令包含类型8 位、序号、方向、参数大小。命令的类型字节需要在ioctl-number.rst中登记避免冲突自用实验可用g产品必须查表。不认识命令返回-ENOTTY不是-EINVAL。32 位兼容如果 ioctl 参数含指针或long必须实现.compat_ioctl否则 32 位程序在 64 位内核上会失败。最简单的写法static long globalmem_compat_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { return globalmem_ioctl(filp, cmd, (unsigned long)compat_ptr(arg)); }不要用 ioctl 传递复杂结构体——考虑 netlink/configfs/sysfs 或专门的子系统接口。ioctl应该尽量短小、可重入、并检查权限capable(CAP_SYS_ADMIN)等。6.3.6 使用文件私有数据static int globalmem_open(struct inode *inode, struct file *filp) { /* 方式 1旧container_of 全局数组下标 */ /* struct globalmem_dev *dev * container_of(inode-i_cdev, struct globalmem_dev, cdev); */ /* 方式 2推荐从 cdev 的父 device 取 drvdata */ struct globalmem_dev *dev container_of(inode-i_cdev, struct globalmem_dev, cdev); filp-private_data dev; /* 关键后续操作都用它 */ return nonseekable_open(inode, filp); /* 若不想支持 lseek */ } static int globalmem_release(struct inode *inode, struct file *filp) { filp-private_data NULL; return 0; } static const struct file_operations globalmem_fops { .owner THIS_MODULE, .llseek globalmem_llseek, .read globalmem_read, .write globalmem_write, .unlocked_ioctl globalmem_ioctl, .compat_ioctl globalmem_compat_ioctl, .open globalmem_open, .release globalmem_release, }; module_init(globalmem_init); module_exit(globalmem_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Kevin); MODULE_DESCRIPTION(Global memory character device for Linux 7.2.5 study);private_data的三种常见来源来源适用场景container_of(inode-i_cdev, ...)一个 cdev 对应一个设备本章dev_get_drvdata(cdev-kobj.parent ...)与设备模型结合更通用每次 open 动态分配每个打开有独立状态如/dev/tty、socket7.2.5 提醒filp-private_data是void *没有类型检查。请统一用一个辅助函数取结构体避免每处都写container_ofstatic inline struct globalmem_dev *to_globalmem(struct file *filp) { return filp-private_data; }完整 Makefileobj-m : globalmem.o KSRC ? /home/works/study/linux-7.2.5 KBUILD ? /home/works/study/build/linux-7.2.5-x86_64 all: $(MAKE) -C $(KSRC) O$(KBUILD) M$(PWD) modules clean: $(MAKE) -C $(KSRC) O$(KBUILD) M$(PWD) clean6.4 globalmem 驱动在用户空间中的验证编译并加载cd /home/works/study/modules/06-globalmem make # 在 guest 中通过 9p 共享或放进 initramfs insmod globalmem.ko globalmem_count2 globalmem_size8192 dmesg | tail ls -l /dev/globalmem* cat /proc/devices | grep globalmem测试程序test_globalmem.c#include stdio.h #include stdlib.h #include string.h #include fcntl.h #include unistd.h #include sys/ioctl.h #include errno.h #define GLOBALMEM_MAGIC g #define MEM_CLEAR _IO(GLOBALMEM_MAGIC, 0) #define MEM_GET_SIZE _IOR(GLOBALMEM_MAGIC, 1, unsigned long) #define MEM_SET_SIZE _IOW(GLOBALMEM_MAGIC, 2, unsigned long) int main(int argc, char **argv) { const char *path argc 1 ? argv[1] : /dev/globalmem0; unsigned long size 0; char buf[256]; int fd; fd open(path, O_RDWR); if (fd 0) { perror(open); return 1; } if (ioctl(fd, MEM_GET_SIZE, size) 0) { perror(ioctl GET_SIZE); return 1; } printf(device size %lu\n, size); /* 写 读回 */ if (pwrite(fd, hello globalmem, 15, 0) 0) { perror(pwrite); return 1; } memset(buf, 0, sizeof(buf)); if (pread(fd, buf, 15, 0) 0) { perror(pread); return 1; } printf(read back: %s\n, buf); /* 越界写应失败 */ if (pwrite(fd, x, 1, size 10) 0) printf(expected failure beyond end: %s\n, strerror(errno)); /* 改容量 */ unsigned long newsize 4096; if (ioctl(fd, MEM_SET_SIZE, newsize) 0) { perror(ioctl SET_SIZE); return 1; } ioctl(fd, MEM_GET_SIZE, size); printf(new size %lu\n, size); /* 清零 */ ioctl(fd, MEM_CLEAR, 0); memset(buf, 0, sizeof(buf)); pread(fd, buf, 15, 0); printf(after clear: %s\n, buf); close(fd); return 0; }运行在 guest 中需要静态编译或用 busybox 环境# 宿主交叉/静态编译 gcc -static -O2 -o test_globalmem test_globalmem.c # guest ./test_globalmem /dev/globalmem0预期输出device size 8192 read back: hello globalmem expected failure beyond end: No space left on device new size 4096 after clear: 用 shell 也能验证echo hello /dev/globalmem0 cat /dev/globalmem0 dd if/dev/globalmem0 bs1 count5 2/dev/null观察点ls -l /dev/globalmem0的主次设备号与cat /proc/devices中globalmem的主设备号一致。ls /sys/class/globalmem/下能看到设备ls -l /sys/dev/char/major:minor能反查。rmmod globalmem后/dev/globalmem0消失devtmpfs 自动删除。两个实例globalmem_count2各自有独立的缓冲区互不干扰。常见错误现象原因处理open返回ENODEV没有注册设备号/cdev或主次设备号写错cat /proc/devices、ls -l /dev/xxx核对open返回ENXIO设备号没有对应 cdev检查cdev_add是否成功ioctl 返回ENOTTY命令类型不匹配或内核不认识核对_IO宏与_IOC_TYPEread返回EFAULT用户指针非法检查调用方驱动侧不要吞掉错误卸载时Module is in use还有进程打开设备lsof /dev/globalmem0后关闭模块加载后没有/dev节点没有注册 class/devicedevtmpfs 依赖它用device_create或手工mknod对比大量读写后 oops缓冲区越界count/ppos计算错误用 KASAN 复现第 21 章6.5 总结字符设备驱动 设备号 cdev file_operations 设备模型。现代做法alloc_chrdev_region()动态分配设备号class_create(name)创建类注意只有一个参数device_create()或cdev_device_add()创建设备不要用register_chrdev()。读写必须走copy_to_user/copy_from_user必须更新*ppos必须处理越界与 EOF 语义。ioctl必须用_IO*宏编码、校验类型、返回-ENOTTY并在需要时实现compat_ioctl。private_data是每次打开的上下文inode-i_cdev是设备本身的上下文。7.2.5 的file_operations新增了fop_flags、iopoll、uring_cmd、mmap_prepare等成员结构体是随机化布局必须用指定初始化器。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号