《Essential Linux Device Drivers》中文版第1章
概述
http://21cnbao.blog.51cto.com/109393/995302
Linux 具有诱人的魅力,它是一个由全世界不同民族、不同信仰、不同性别的人共同参与和协作的国际性项目。Linux免费提供源代码,并且具有与Unix类似的为 人们所熟悉的应用程序编程环境,这一切造就了它今天的巨大成功。通过互联网从专家处即时获得的高质量的免费支持也发挥了重要作用,它促成了一个庞大的 Linux社区。
在技术方面,开发人员可以获得所有源码,并由此得出一些创新方案,他们因此感到无比振奋。譬如,你可以修改(hack)[①] Linux的源码,并做定制,让设备在几秒钟之内启动,而使用一个有专利的商业操作系统则很难完成这样的壮举。
1.1 演进
1991 年,一位名为Linus Torvalds的芬兰大学生开发了Linux操作系统。起初这只是他个人的爱好,但它很快就发展成为在全世界范围内广受欢迎的先进的操作系统。 Linux第一次发布时仅支持Intel 386处理器,但是后来,它的内核复杂性逐步增加,可以支持众多的体系架构、多处理器硬件和高性能集群。Linux所支持的体系结构非常多,主要支持的一 些硬件架构是x86、IA64、ARM、PowerPC、Alpha、s390、MIPS和SPARC。Linux已经被移植到成千上万的基于这些处理器 的硬件平台之上。与此同时,其内核还在不断完善,系统性能也在飞速提升。
虽然开始的时候Linux只是一个桌面操作系统,但目前它已经进入 嵌入式和企业级计算领域,并融入了我们的日常生活。当你按动掌上电脑的按键,用遥控器把电视切换到天气频道,或者在医院接受体检的时候,很有可能就在享受 某些Linux代码提供的服务。技术优势和开源特性促进了Linux的演进。无论是试图开发不到100美元的计算机以改善世界贫困地区的教育状况,还是要 降低消费类电子产品的价格,Linux如今都已成为一个绝好的选择,因为商业操作系统的价格有时候比计算机本身的价格更贵。
1.2 GNU Copyleft
GNU 工程比Linux更早诞生,发起它的目标是定制一个免费的类Unix操作系统(GNU是GNU’s Not Unix的递归缩写,意为“GNU不是Unix”。一个完整的GNU操作系统基于Linux内核构建,但也包含一些其他组件,如库、编译器和实用程序 (utility)。因此,基于Linux的计算机的更准确称呼应该是GNU/Linux系统。GNU/Linux系统的所有组成部分都建立在免费软件之 上。
免费软件有许多种,其中的一种是公共领域(public domain)软件。公共领域发布的软件没有版权,对于它的使用也不会强加任何限制。你可以免费使用它,随意修改它,甚至限制别人发布你修改后的代码。发 现了吗?所谓“没有限制”条款居然暗含了对下游施加限制的权力。
GNU工程的主要发起者——自由软件基金会——创造了GNU公共许可证(GPL),它也被称为“版权左派”(copyleft)[②], 以防止有人中途将免费软件转化为商业软件。谁修改了copyleft的软件就必须以copyleft的方式分享他的软件。GNU系统中Linux内核以及 大部分组件(如GNU编译器GCC)都以GPL发布。因此,如果你修改了内核,你就必须在社区分享此修改。实际上,你必须以copyleft的形式将授予 你的权利传递出去。
Linux内核基于GPL第2版。在内核社区,人们一直在争论是否应该采用GPL的最新版本GPLv3。目前的趋势似乎是反对采用GPLv3。
通过系统调用访问内核服务的Linux应用程序没有被看作衍生的工作,因此并不受限于GPL。而库则采用GNU轻量级通用公共许可证(LGPL),其限制要少于GPL。商业软件也允许与LGPL下的库动态链接。
1.3 kernel.org
Linux内核源代码主要存放在www.kernel.org。该网站包含所有已经发布的内核版本,世界各地有大量的kernel.org镜像网站。
除 了已经发布的内核以外,kernel.org还包含了由一线开发人员提供的补丁,这些补丁可以作为未来稳定版本的试验平台。补丁是一种文本文件,包含了新 开发版本和开发之初制订的原始版本之间的源码差异。由Linux内核第一维护人Andrew Morton定期提供的-mm补丁是一种很受欢迎的补丁。在该补丁中,我们可以找到在主线源代码树中尚未提供的实验性的功能。另一个会定期公布的补丁是由 Ingo Molnar维护的-rt(realtime,实时)补丁。-rt补丁的数个功能已经被纳入Linux主线内核。
1.4 邮件列表和论坛
LKML(Linux Kernel Mailing List,内核邮件列表)是开发人员就开发问题进行辩论并决定Linux未来要包含哪些功能的论坛。你可以在www.lkml.org看到实时的邮件列表。Linux内核目前包含世界各地的成千上万的开发人员贡献的数百万行代码,正是LKML将这些开发人员联结在一起。
LKML的定位不在于解答一般的Linux问题,其基本规则是只能张贴以前没有被回答过并且在众所周知的文档中没有提及的内核问题。如果你编译Linux应用程序的时候C编译器崩溃了,你应该去其他地方张贴这样的问题。
LKML中的一些讨论甚至比某些《纽约时报》畅销书更有意思,花几个小时浏览LKML的压缩包有助于洞察Linux内核背后的理念。
内 核的大部分子项目都拥有自己的邮件列表。因此,如果你正在开发闪存设备的驱动程序,就可以订阅linux-mtd邮件列表;如果你发现了Linux USB存储设备驱动程序的bug,就可以在linux-usb-devel 邮件列表发起一个讨论。本书一些章的末尾介绍了相关的邮件列表。
在 各种论坛上,来自世界各地的内核专家会聚集于同一个屋檐下共同商讨Linux技术。加拿大渥太华每年举行一次的Linux Symposium就是这样的一个会议。其他的还包括在德国举行的Linux Kongress,在澳大利亚组织举行的linux.conf.au。也有一些商业化的Linux论坛,例如每年在北美举行的LinuxWorld Conference and Expo,众多的商界领袖在该论坛上聚会并分享真知灼见。
在http://lwn.net/上可以获得Linux开发社区的最新消息。如果你只是想简单地了解内核的最新发布版,不想阅读太多的资料,http://lwn.net/可能是一个好地方。另一个网络社区http://kerneltrap.org/则讨论当前的内核议题。
在每个主要的Linux内核版本中,都会有重大的改进,如内核抢占、无锁(lock-free)的读操作、分担中断处理程序工作的新服务或者对新体系结构的支持。因此,要不断学习最新的Linux技术,就要一直跟踪邮件列表、网站和论坛。
1.5 Linux发行版
一 个GNU/Linux系统除了内核以外,还包括大量的实用程序、程序、库和工具,因此,获得和正确安装所有的组件是一项艰巨的任务。而Linux发行版有 序地将这些组件进行了分类,并捆绑成相应的包,从而分担了这一艰巨任务。一个常见的发行版包含数以千计的捆绑好的包。这使得用户无需担心下载不到正确版本 的程序,也无需关心程序间的依赖问题。
因为打包是GNU许可证范围内的一种有效的赚钱方式,因此,目前的市场上诞生了数个Linux发行 版。其中,Red Hat/Fedora、Debian、SuSE、Slackware、Gentoo、Ubuntu和Mandriva这些发行版面向桌面用户,而 MontaVista、TimeSys和Wind River发行版则面向嵌入式系统开发。嵌入式Linux的发行版还包括一套可动态配置的紧凑的应用程序集,以便针对资源的限制为系统进行量体裁衣。
除了打包以外,发行版还为内核的开发提供了增值服务。因此,许多项目都开始于发行版提供的内核而非kernel.org发布的官方内核,这样做的理由如下。
遵守设备行业领域标准的Linux发行版更适合作为开发的起点。特殊兴趣组(SIG)已经成立,其目的是促进Linux在各个领域的应用。消费电子产品Linux论坛(CELF,网址为www.celinuxforum.org)主要讨论消费类电子领域的Linux应用。CELF标准定义了一些功能的支持等级,如可扩展性、快速启动、片上执行以及电源管理等。开源开发实验室(OSDL,网址为www.osdl.org)则致力于讨论电信级设备。OSDL的电信级Linux(CGL)标准包含了对可靠性、高可用性、运行时补丁、增强的错误恢复能力的诠释,这些问题在电信领域非常重要。
主线内核版本可能并未包含对用户所选择的嵌入式控制器的充分支持,即使用户的系统建立在内核所支持的CPU核心之上。但是,一个Linux的发行版则可能包含了控制器内所有外围设备模块的设备驱动程序。
在 内核开发过程中你计划使用的调试工具可能不包含在主线内核中。例如,内核并不包含内嵌的调试器支持。如果想在内核开发过程中使用内嵌的调试器,用户必须下 载并打上相应的补丁。如果针对用户内核版本的补丁并不齐备,用户将必须忍受更多的麻烦。而发行版则包装了很多有用的调试功能,所以你可以立即开始使用它 们。
一些发行版提供了法律保护,让你的公司无须为任何由于内核bug所引发的诉讼承担责任。
发行版往往会对它们发布的内核进行较多的测试[③]。
用户可以从内核发行版的供应商处购买它们提供的服务以及软件包支持。
1.6 查看源代码
在研究内核源代码之前,让我们先下载Linux源代码,学会打补丁,并查看内核源码树的布局。
首先,请到www.kernel.org下载最新的稳定的源代码[源代码以gzip(.zip)和bzip2 (.bz2)两种压缩格式提供],之后请进行解压缩。在下列命令中,请用最新的内核版本号(譬如2.6.23)代替X.Y.Z:
bash> cd /usr/src
bash> wget www.kernel.org/pub/linux/kernel/vX.Y/linux-X.Y.Z.tar.bz2
...
bash> tar xvfj linux-X.Y.Z.tar.bz2
现在,你已经拥有位于/usr/src/linux-X.Y.Z/目录的源代码树,下面通过打-mm补丁(Andrew Morton)启动一些实验性测试特性。运行如下命令:
bash> cd /usr/src
bash> wget www.kernel.org/pub/linux/kernel/people/akpm/patches/X.Y/X.Y.Z/X.Y.Z-
mm2/X.Y.Z-mm2.bz2
打上这个补丁:
bash> cd /usr/src/linux-X.Y.Z/
bash> bzip2 -dc ../X.Y.Z-mm2.bz2 | patch -p1
命令中的-dc选项意味着让bzip2将指定的文件解压缩到标准输出。它被以管道方式输送到补丁实用程序,补丁程序会将补丁中的代码修改应用到源码树中的每个需要修改的文件。
如果你需要打多个补丁,请注意要采取正确的顺序。为了生成一个包含X.Y.Z-aa-bb补丁的内核,应首先下载X.Y.Z内核的完整源代码,再打上X.Y.Z-aa补丁,最后打上X.Y.Z-aa-bb补丁。
补丁提交
使用diff命令可以为你更改的内核产生补丁:
bash> diff –Nur /path/to/original/kernel /path/to/your/kernel > changes.patch
要注意的是,在diff命令中,原始内核的路径应该放在修改后内核路径的前面。基于2.6内核补丁提交的公约,你需要在补丁的最后加上这样的一行:
Signed-off-by: Name
这一行阐明了这些代码是由你编写的,你拥有贡献它的权利。
你现在就可以在相关的邮件列表(如LKML)中张贴你的补丁了。
文档Documentation/SubmittingPatches包含了一个创建和提交补丁的向导,而Documen- tation/applying-patches.txt是一个教你如何打补丁的教程。
现在,你打好补丁后的/usr/src/linux-X.Y.Z/已经准备好投入使用了。接下来,我们花一些时间来查看内核源代码树的结构。进入内核源代码树的根目录并列出它的子目录。
(1) arch。该目录包含了与体系结构相关的文件。可以在arch/目录下看到针对ARM、Motorola 68K、s390、MIPS、Alpha、SPARC和IA64等处理器的子目录。
(2) block。该目录主要包含块存储设备I/O调度算法的实现。
(3) crypto。该目录实现了密码操作以及与加密相关的API,它们可被应用于WiFi设备驱动的加密算法等场合。
(4) Documentation。该目录包含了内核中各个子系统的简要描述,它是你探究内核方面问题的第一站。
(5) drivers。这个目录包含了大量设备类和外设控制器的驱动,包括字符、串口、内置集成电路(I2C)、 个人计算机存储卡国际联盟(PCMCIA)、外围组件互连(PCI)、通用串行总线(USB)、视频、音频、块、集成驱动电子设备(IDE)、小型计算机 系统接口(SCSI)、CD-ROM、网络适配器、异步传输模式(ATM)、蓝牙和内存技术设备(MTD)等。每一类设备对应drivers/下面的一个 子目录,譬如PCMCIA驱动程序的源代码位于drivers/pcmcia/目录,MTD驱动程序位于drivers/mtd/目录。drivers /下的这些子目录是本书的主要议题。
(6) fs。这个目录包含了EXT3、EXT4、reiserfs、FAT、VFAT、sysfs、procfs、isofs、JFFS2、XFS、NTFS和NFS等文件系统的实现。
(7) include。内核头文件位于此目录。该目录下以asm开头的子目录包含了与体系结构相关的头文件,比如include/asm-x86/子目录包含了x86体系架构的头文件,include/asm-arm/包含了ARM体系架构的头文件。
(8) init。这个目录包含了高级别初始化和启动代码。
(9) ipc。这个目录包含了对消息队列、信号、共享内存等进程间通信(IPC)机制的支持。
(10) kernel。基本内核中与体系架构无关的部分。
(11) lib。通用内核对象(kobject)处理程序、循环冗余码校验(CRC)计算函数等库函数例程位于此目录。
(12) mm。这个目录包含了内存管理的实现。
(13) net。该目录实现了网络协议,包括Internet协议第4版(IPv4)、IPv6、网际互联交换协议(IPX)、蓝牙、ATM、红外、链路访问过程平衡(LAPB)以及逻辑链路控制(LLC)。
(14) scripts。内核编译过程中要使用的脚本位于此目录。
(15) security。这个目录包含了针对安全的框架。
(16) sound。Linux音频子系统位于此目录。
(17) usr。此目录包含了initramfs 的实现。
统一的x86架构源码树
从 2.6.24内核版本开始,i386和x86_64(与32位的i386系统对应的64位系统)架构源码树已被统一纳入公共的arch/x86/目录。如 果你使用的是比2.6.24老的内核,请用arch/i386 /代替本书中所说的arch/x86 /目录。同样地,也请将include/asm-x86/替换为include/asm-i386/。此外,这些目录中的一些文件名也会有所不同。
在这么庞大的目录树中查找符号和代码是一项艰巨的任务,表1-1中的一些工具可以帮助你更方便地浏览内核源码树。
表1-1 源码树浏览工具
工 具 | 描 述 |
lxr | Linux cross-referencer(Linux交叉引用程序),可从http://lxr.sourceforge.net/下载。它可以让你通过网页浏览器遍历内核源码树,因为它为内核符号的定义和使用提供了超链接 |
cscope | cscope(网址为http://cscope.sourceforge.net/) 为内核源码树内的所有文件建立一个符号数据库,通过它可以快速地搜索声明、定义以及正则表达式等。cscope可能不如lxr那般多才多艺,但是它很灵 活,允许你使用最喜欢的文本编辑器而不是浏览器的搜索功能。在内核源码树的根目录,运行cscope-qkRv命令就可建立交叉引用数据库。-q选项将产 生更多的索引信息以加快搜索速度,但是初始启动会消耗更多的时间。-k要求cscope调整它的行为以使用内核源代码,-R选项意味着递归遍历子目录。在 手册页面可以找到详细的调用规则 |
ctags/etags | ctags(网址为http://ctags.sourceforge.net/) 可用于为许多语言产生交叉引用的标签。通过它,你可以在vi等编辑器中找到源码树中的符号和函数定义。从内核源码树的根目录运行make tags可以为所有文件建立标签。etags为emacs编辑器产生相似的索引信息。运行make TAGS可以采用etags为内核源文件创建标签 |
(续)
工 具 | 描 述 |
实用程序 | grep、find、sdiff、strace、od、dd、make、tar、file和objdump等工具 |
GCC选项 | 使用-E选项可以让GCC产生预处理源代码。预处理代码包含头文件的扩展,并减少了为扩展多层宏定义在多个嵌套的头文件间进行跳跃的需要。下面的例子预处理drivers/char/mydrv.c并产生扩展后的输出文件mydrv.i: bash> gcc -E drivers/char/mydrv.c -D__KERNEL__ -Iinclude -Iinclude/asm-x86/mach-default > mydrv.i 使用-I选项可以指定你的代码所依赖的include的路径 使用-S选项可以让GCC产生汇编列表。下面的命令可以为drivers/char/mydrv.c产生汇编文件mydrv.s: bash> gcc -S drivers/char/mydrv.c -D__KERNEL__ -Iinclude -Ianother/include/path |
1.7 编译内核
了解了内核源码树布局后,现在我们来对代码稍做修改,并编译和运行它。进入位于顶层的init/目录,对初始化文件main.c做一项小的修改,即在start_kernel()函数的开头加上一行打印信息,宣布你对北极熊的喜爱:
asmlinkage void __init start_kernel(void)
{
char *command_line;
extern struct kernel_param __start___param[],
__stop___param[];
+ printk("Penguins are cute, but so are polar bears/n");
/* ... */
rest_init();
}
编译内核准备工作已经就绪,进入内核源码树并运行清除命令:
bash> cd /usr/src/linux-X.Y.Z/
bash> make clean
接下来进行内核配置工作。这一步的主要工作是选择要编译的组件,你可以指定需要的组件以静态还是动态链接的方式编译进内核:
bash> make menuconfig
menuconfig 是内核配置菜单的文本界面,使用make xconfig可以产生一个图形界面。所选择的配置信息被存放在内核源码树根目录的.config文件中。如果不想从头开始进行配置,可以使用 arch/your-arch/defconfig作为起点或者若你的体系架构支持多个平台,也可以用)arch/your-arch/ configs/your-machine_defconfig文件作为起点。因此,如果正在为32位x86体系架构编译内核,运行如下命令:
bash> cp arch/x86/configs/i386_defconfig .config
编译内核并产生一个压缩的启动映像:
bash> make bzImage
现在,内核映像将位于arch/x86/boot/bzImage,更新启动分区:
bash> cp arch/x86/boot/bzImage /boot/vmlinuz
也许需要根据新的启动映像更新引导程序。如果正在使用GRUB这个引导程序,它将自动完成配置;如果正在使用LILO,请增加一个标记:
bash> /sbin/lilo
Added linux *
最后,重新启动Linux并启动到新内核:
bash> reboot
启动后的第一条信息显示了你添加的喜爱北极熊的那句话。
1.8 可加载的模块
由于Linux可运行于各种各样的体系架构中,并且支持无数的I/O设备,把所有要支持的设备都直接编译进内核并不合适。发行版通常包含一个最小的内核映像,而以内核模块的形式提供其他的功能。在运行的时候,可以动态地按需加载模块。
为了生成模块,进入内核源码树根目录并运行:
bash> cd /usr/src/linux-X.Y.Z/
bash> make modules
运行如下命令安装编译生成的模块:
bash> make modules_install
此命令将在/lib/modules/X.Y.Z/kernel/目录下构造一个内核源代码目录结构,并将可加载的模块放入其中。它也将激活depmod实用程序,以便生成模块依赖文件/lib/modules/X.Y.Z/modules.dep。
如 下工具可用于操纵模块:insmod、rmmod、lsmod、modprobe、modinfo和depmod。前两个工具用于加载和移除模 块,lsmod用于列出目前已经加载的模块,modprobe是insmod的一个更智能的版本,它先分析/lib/modules/X.Y.Z /modules.dep文件再加载它所依赖的模块。例如,假定你需要挂载一个USB笔式驱动器上的VFAT(Virtual File Allocation Table,虚拟文件分配表)分区,可使用modprobe加载VFAT文件系统驱动程序[④]:
bash> modprobe vfat
bash> lsmod
Module Size Used by
vfat 14208 0
fat 49052 1 vfat
nls_base 9728 2 vfat, fat
从 lsmod命令的输出可以看出,modprobe加载了3个而不仅仅是1个模块。modprobe首先发现它不得不加载/lib/modules /X.Y.Z/kernel/fs/vfat/vfat.ko,当查看/lib/modules/X.Y.Z/modules.dep模块依赖文件的时 候,它发现了如下代码并由此意识到自己必须首先加载另外2个模块:
/lib/modules/X.Y.Z/kernel/fs/vfat/vfat.ko:
/lib/modules/X.Y.Z/kernel/fs/fat/fat.ko
/lib/modules/X.Y.Z/kernel/fs/nls/nls_base.ko
于是它首先加载了fat.ko和nls_base.ko这2个模块,之后加载vfat.ko,这样,所有挂载VFAT分区时所需要的模块都被自动加载了。
使用modinfo程序可以提取刚加载的模块的详细信息:
bash> modinfo vfat
filename: /lib/modules/X.Y.Z/kernel/fs/vfat/vfat.ko
license: GPL
description: VFAT filesystem support
...
depends: fat, nls_base
为了将内核驱动程序编译为模块,在配置内核的时候,请将相应的菜单选择按钮置为。本书中的大部分设备驱动程序例子都以内核模块的形式实现。为了从mymodule.c源文件构造mymodule.o模块,可以创建一个一行的Makefile文件,并且以如下方式执行它:
bash> cd /path/to/module-source/
bash> echo "obj-m += mymodule.ko" > Makefile
bash> make –C /path/to/kernel-sources/ M=`pwd` modules
make: Entering directory '/path/to/kernel-sources'
Building modules, stage 2.
MODPOST
CC /path/to/module-sources/mymodule.mod.o
LD [M] /path/to/module-sources/mymodule.ko
make: Leaving directory '/path/to/kernel-sources'
bash> insmod ./mymodule.ko
内核模块减小了内核的大小,并缩短了开发——编译——测试的周期。为了让一次修改生效,你仅仅需要重新编译特定的模块并重新加载它。在第21章中,我们将学习模块调试技术。
将驱动程序设计为内核模块也有一些缺陷。与内建的驱动程序不同,模块无法在系统启动时预留资源,因为首要的是必须保证启动成功。
1.9 整装待发
Linux已经涉及的领域十分广泛,代表着最新的技术水平,所以可以基于它来学习操作系统的概念、处理器体系架构,甚至了解各种行业领域。在学习某一设备驱动程序子系统用到的技术时,不妨在更深层次上探索其背后的设计动机。
在 没有明确指明的情况下,本书默认的都是32位x86架构。但是,本书也考虑到你更有可能要为嵌入式设备而非传统的PC兼容的系统编写驱动程序。因此,第6 章讲解了两种设备:一个PC衍生器件上的触摸控制器和一个手机上的UART。第8章则讲解了PC系统中的EEPROM和嵌入式设备中的实时钟。本书也介绍 了内核为大多数设备驱动程序类所提供的基础设施,它们隐藏了设备驱动程序与体系架构的相关性。
在本书接近尾声的第21章讨论了设备驱动程序的调试技术,开发驱动程序的时候,提前阅读该章会很有用。
本书基于2.6内核,它包含了对2.4内核的大量更新,覆盖了所有主要的子系统。因此,希望你已经在系统中安装了基于2.6的内核并开始研究内核源代码。基于以下两个主要的原因,本书的每一章都反复要求读者去阅读相关的内核源文件。
(1) 因为内核中的每个驱动程序子系统都包含数万行源代码,以本书的篇幅只能列出相对简单的部分内容,对照查看源代码中与书中例子相关的真实驱动程序会让你豁然开朗。
(2) 在开发驱动程序之前,先参考一个drivers目录中与你的要求相似的现成的驱动程序,把它作为起点是一个好方法。
因此,为了能更好地消化本书内容,请频繁地浏览源码树并仔细研究代码来熟悉内核。在探索代码的过程中,也请跟踪邮件列表的进展。
[①] 意指对源码进行一些有针对性的修改。——译者注
[②] 版权为copyright,这里故意用copyleft。但是,copyleft作品是有版权的,只是加入了法律上的分发条款。——译者注
[③] 因为需要将内核冻结在一个版本上进行测试(而这个版本不是最新的),所以发行版内核经常会引入比其版本更新的官方内核的一些功能。
[④] 这个例子假定这个模块没有被内核自动加载。如果你在配置过程中启用了自动内核模块加载(CONFIG_KMOD)选项,当侦测到缺失的子系统时,内核将自动以相应的参数运行modprobe。第4章将介绍模块自动加载的知识。
最后
以上就是心灵美钥匙为你收集整理的《Essential Linux Device Drivers》中文版第1章的全部内容,希望文章能够帮你解决《Essential Linux Device Drivers》中文版第1章所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
- 本文分类:内核
- 浏览次数:68 次浏览
- 发布日期:2024-01-09 09:05:23
- 本文链接:https://www.kaopuke.com/article/k-p-k_13_u_23_ogfy_13__23_gy.html
发表评论 取消回复