Linux嵌入式系统浅谈

  • 手机
  • 2024年11月18日
  • 过去很多嵌入式系统不是一个操作系统,而是提供商的专有核心,或者是DOS操作系统的扩展。显然这些方法并不能适应今天嵌入式系统开发的要求!现有的一些商业实时操作系统,尽管提供了很小的核心和多任务开发环境,但性能并不理想,也不符合现在实时嵌入式市场的需求。 因此,人们把目光投向了通用操作系统(例如Windows、Solaris、linux),希望把它们“改造”为实时操作系统。通常这些操作系统功能强大

Linux嵌入式系统浅谈

过去很多嵌入式系统不是一个操作系统,而是提供商的专有核心,或者是DOS操作系统的扩展。显然这些方法并不能适应今天嵌入式系统开发的要求!现有的一些商业实时操作系统,尽管提供了很小的核心和多任务开发环境,但性能并不理想,也不符合现在实时嵌入式市场的需求。 因此,人们把目光投向了通用操作系统(例如Windows、Solaris、linux),希望把它们“改造”为实时操作系统。通常这些操作系统功能强大,结构复杂,易于软件的二次开发,实用性强,并且提供编程人员熟悉的标准API。此外,这些操作系统也提供了一些对实时软件开发的支持。然而,这些操作系统用于嵌入式系统的开发还存在不足。嵌入式系统要求具备高可靠性,满足应用需求的可剪裁性,以及比通用操作系统要求更高的实时性。 做为嵌入式系统开发的解决方案,linux在众多通用操作系统中具有独一无二的优势。 首先,Windows和Solaris等专有商业操作系统的剪裁受到商家的严格控制。这大大限制了开发者的剪裁深度。而linux遵循GPL协议,开放所有系统源代码,非常易于剪裁。 其次,同开放源码的通用操作系统(如FreeBSD)相比,linux在多种处理器、开发板支持和软件开发工具支持上有很强的优势。 linux最初也是作为通用操作系统而设计开发的,但提供了一些实时处理的支持。这包括支持大部分POSIX标准中的实时功能,支持多任务、多线程,具有丰富的通信机制等。 linux还提供符合了POSIX标准的调度策略,包括FIFO调度策略、时间片轮转调度策略和静态优先级抢占式调度策略。其默认的调度策略是第三种。Linux还提供了内存锁定功能,以避免在实时处理中存储页被换出,也提供了符合POSIX 标准的实时信号机制。 一个致命问题是,linux在用户态支持可抢占调度策略,而在核心态却不支持抢占式调度策略。这样运行在Linux核心态的任务(或系统调用)是不能被优先级更高的任务所抢占的,这样就会引起优先级逆转问题。另外,Linux操作系统的中断处理句柄是不可调度的,不能依优先级高低调度。而在实时系统中,却希望中断处理句柄同实时任务一样,可以有优先级来被系统的调度程序所调度。 此外,我们还关心和任务响应时间相关的时钟精度,以及由于资源共享而带来的优先级逆转问题。linux中硬件时钟中断的默认时间间隔是10ms,所有的软件时钟都是靠硬件来触发的。而简单同步机制(互斥)不支持优先级继承又很可能导致优先级逆转。 独立核方法 linux作为实时系统的独立核方法是指设计一种完全独立的实时核心,但其API 与Linux核心相兼容。这种方法的理论基础是一款优秀的实时操作系统必须在其设计之初就充分考虑到系统实时性的要求,并能够提供符合标准的API。这种实现方法对很多与POSIX 兼容的专有实时系统提供商很有吸引力。 这种方法的局限性是由于设计了一个完全独立的实时核心而没有使用原有linux核心,导致Linux系统的一些优势难以继承,尤其是与Linux核心相关的一些优势无法获得。比如Linux核心对大量硬件的广泛支持,Linux核心超群的可靠性、稳定性等。另外,由于这种方法并没有通过修改Linux核心代码来开发实时核心,而是在Linux系统之上重新设计了一个实时核心,这样的开发并不要求源代码开放。因此,Linux一些基于开放源代码的优势也势必受损。最后一点,任何基于Linux核心的开发成果也无法方便地应用到实时核心中。 当然这种实现方法也从linux系统中得到了很多好处。由于Linux系统的支撑,实时核心就并不需要“真”的去实现。而且熟悉Linux系统的开发人员也可以很快地熟悉这种方法开发出的实时系统。人们也会自然地想到用Linux系统做嵌入式系统的开发平台。此外,如果这种实时系统的API是Linux系统API子集的话,我们还可以只在Linux主机上仿真,进行应用程序的开发和调试,免去了远程调试之苦! 与linux API的兼容程度是评估这类实时系统的一个重要指标。如果一个实时系统兼容了所有Linux API,那么就允许所有Linux上的应用程序和库在其上运行使用。因此,这将会带来一个巨大的好处,所有在Linux上可用的第三方软件均可以在其上使用。当然,开发一款这样兼容所有Linux API的实时系统决不是件容易的事,尤其是对于单个开发商来说。 所以,大量的第三方软件并不能很容易地移植到实时系统中来,这点不足,也使linux的优势大打折扣! 双核方法 这种方法在同一硬件平台上采用了两个相互配合,共同工作的系统核心,一个核心提供精确的实时多任务管理,另一个核心提供复杂的非实时通用功能。 这种方法是通过在linux操作系统的最底层增加一层实时核心层来实现的。实时核心负责硬件管理并提供实时任务管理。实时核心还用软件“模拟”常规Linux系统对底层硬件的使用/禁止中断,而不是真正的操作中断控制寄存器。Linux核心被看做实时核心中优先级最低的任务来调度,只有当没有可运行的实时任务时Linux核心才被调度。 这种方法的一个关键所在是运行在常规linux核心上的所有非实时任务必须是支持可抢占式调度的。这样才能做到对实时核心提供精确实时保证没有任何影响。由于实时核心非常小,并不会增加整个系统的负载,所有这些对开发实时性要求严格的实时软件都提供了有力保障。 这种方法的弊端在于实时任务的开发是直接面向提供精确实时服务的小实时核心的,而不是功能强大的常规linux核心。因此,实时任务是运行在系统核心层的,这就意味着这些实时任务可以运行在没有内存保护的级别之上。所以,一个实时任务的错误可能会导致整个系统的瘫痪!更要命的是,这些实时任务的开发由于面对的是小的实时核心,而不能直接利用Linux API和第三方软件及运行库。 这种开发模式暗示我们必须要对应用进行静态分解。把它分解成实时部分和非实时部分。在大多情况下,这是件好事情。它迫使开发人员将应用系统分解成实时子系统和非实时子系统两部分。但很显然,使用这种开发模式也限制了应用的类型!因为,这种用二元论观点看待实时系统的方法并不适合所有的应用。在一些应用中,实时部分和非实时部分的界线并不是十分分明,期间可能存在着不同程度的软实时部分。 这种方法的另一个不足之处是,开发模式混合了实时应用的两个不相干维度——功能需求和实时需求。它要求应用的实时需求必须限制于由实时核心提供的功能需求限度以内。而实时核心提供的功能支持非常有限。当然我们也可以扩展实时核心的功能,比如增加实时网络功能等。然而,新增加的部分很有可能会重叠linux核心已有功能,而导致了不必要的系统“膨胀”,并折损这种方法的价值。 修改核方法 这种方法是基于已有linux系统对实时软件开发的支持,进行源代码级修改而使Linux变成一个真正的实时操作系统。这种方法也是和Linux哲学相吻合的。任何基于Linux核心源代码修改的产品,都要遵循GPL 协议,对所有软件人员开放源代码。一旦很多人认为它是有用的,就会有人对它进行维护,或者是混合在通用Linux核心中,或者是单独分出一个实时Linux分支。 这种方法的中心原则是精心选择部分改动,就可以满足一系列相关linux实时开发。此外,由于这些改动都是相对局部的,不会从根本上改变Linux的核心。而且一些改动还可以通过常规Linux的可加载模块方式完成。在需要时系统可以动态加载该功能模块,在不需要时还可以动态卸载该模块。 比如,修改之一是核心抢占式调度。把核心从非抢占式变成抢占式是结构上的大变动,并可能引起很多问题,但很多问题已经在linux支持SMP 的时候解决了。因此,核心的抢占式修改就可以简单地利用SMP 挂钩。另一个修改点是前面提到过的使中断处理句柄可调度。还有一些修改是全局的,例如修改系统时钟服务来提供更高精度的“心跳”,而不增加不必要的系统负载,或者是提供在核心实现互斥机制来支持优先级继承。 资源核方法 这种方法是为解决传统实时操作系统中固定优先级抢占式调度策略的局限性而产生的。固定优先级抢占式调度算法没有任务间的临时保护。因此,可预见的任务响应时间依赖于对所有更高优先级任务执行时间的预测。在这样的系统中,可预见性是与全局相关的,并且可能被一个糟糕任务而影响的。此外,这种用静态观点看待实时系统也是不妥的。在很多实时应用中,更希望实时系统可以根据应用程序获得资源动态地调整任务属性,以求得到最优效果。 资源核方法是一种以资源为中心来指导实时核心提供精确的、有保证的、可抢占的获取系统资源的方法。只要实时应用所需资源可以由核心后台资源管理程序调配满足,实时核心是允许实时应用可配置的。因此,实时核心其实是提供了实时应用可构建的基础——从配置简单的实时系统到复杂的实时系统,都可以通过动态地改变实时任务属性和它们在整个系统中的优先级来满足。 这种方法的最大优点是系统具有很好的健壮性、可精确预见的实时性。另一个优点是允许应用程序根据实际情况动态调整自身属性。此外,这种方法非常适合嵌入式系统的开发。 下面为裁剪Linux系统的简易步骤,仅供参考,如有雷同,绝对不是巧合! 我们的目标 linux 系统运行在一台普通的 Intel 386 PC 机上,可以有硬盘,也可以不要硬盘,而用 Flash Disk 来代替。如果是用 Flash 盘的话,需要能够支持从 Flash 盘启动,而且 Flash 盘的大小要在 16M 字节或者以上。我们希望用户一开机启动,就直接进入 X Window 图形界面,运行事先指定好的程序。不需要用户输入用户名和密码进行登录。 我们设定的这个目标有点像一个 X Terminal 终端工作站。稍加改进,还可以做成干脆无盘的形式,也就是说,连 16M 的 Flash 盘也不要了。不过,这也超出了本文的话题了。读者朋友们如果有兴趣,可以来信和我进行讨论。 系统启动 因为我们要考虑从 Flash 盘进行启动,所以我们选择用 LILO 作为我们的 Boot Loader,而不选用 GRUB。这是考虑到 GRUB 有较强的对硬盘和文件系统的识别能力,而 Flash 盘到底不是标准的硬盘,并且我们选用的文件系统 GRUB 又不一定认识,搞不好的话 GRUB 反会弄巧成拙。而 LILO 就简单的多了,它在硬盘开始的 MBR 写入一个小程序,这个小程序不经过文件系统,直接从硬盘扇区号,读出 Kernel Image 装入内存。这样,保险系数就大大增加。并且也给了我们自由选用文件系统的余地。那么,我们要如何安装 LILO 呢? 首先,我们要找一块普通的 800M 左右的 IDE 硬盘,连在目标机器的 IDE 线上。这样在我们的目标机器上,IDE1 上挂的是 Flash 盘,IDE2 上挂的是一块工作硬盘。我们用标准的步骤在 IDE2 的标准硬盘上装上一个 Debian GNU/linux 系统。当然,如果读者朋友们手头没有 Debian,也可以装 Red Hat 系统。装好工作系统之后,要首先做一些裁减工作,把不必要的 Service 和 X Window 等等东西都删掉。这样做的目的是增进系统启动速度,因为我们在后面的工作中,肯定要不停的重新启动机器,所以启动速度对我们的工作效率是很关键的。 装好工作系统之后,在 Falsh 盘上做一个 Ext2 文件系统,这个用 mke2fs 这个命令就可以完成。由于 Flash 盘是接在 IDE1 上的,所以在 linux 里面,它的身份是 /dev/hda。本文作者在操作的时候,把整个 Flash 盘划分了一个整个的分区,所以,调用 mke2fs 的时候,处理的是 /dev/hda1。读者朋友们应该可以直接在 /dev/hda 上做一个 Ext2 文件系统,而不用事先分区。 在 Flash 盘上做好了文件系统之后,就可以把一个编译好的内核映像文件 vmlinuz 拷贝到 Flash 盘上了。注意,必须要先把这个 vmlinuz 映像文件拷贝到 Flash 盘上,然后才能在 Flash 盘上安装 LILO。不然的话,LILO 到时候可是会 LILILILI 打结巴的,因为它会找不到 Kernel Image 在 Flash 盘上的位置的,那样的话 Flash 盘也就启动不起来了。还有,如果读者朋友们在 Flash 盘上用的是一个压缩的文件系统的话,到时候 LILO 也会出问题,它虽然能正确的找到 Kernel Image 在硬盘上的起始位置,但是它却没有办法处理被文件系统重新压缩过的这个 Kernel Image,不知道该如何把它展开到内存中去。 把 Kernel Image 拷贝过去以后,我们就可以动手编辑一份 lilo.conf 文件,这份文件可以就放在工作系统上就行了。但是注意在 lilo.conf 中索引的文件名的路径可要写对。这些路径名都是在工作系统上看上去的路径名。比如,如果 Flash 盘 Mount 在 /mnt 目录下面,那么,在 lilo.conf 中,vmlinuz 的路径名就是 /mnt/vmlinuz。注意这一点千万不要搞错。不然的话,如果一不小心把工作系统的 LILO 给破坏掉了,那就麻烦了。编辑好了 lilo.conf,然后再运行 lilo 命令,注意,要告诉它用这个新的 lilo.conf 文件,而不要用 /etc/lilo.conf。 安装好 LILO 之后,我们可以立即重新启动,测试一下。首先在 BIOS 里面,设置成从 IDE1 开始启动,如果我们看到 LILO 的提示符,按回车后还能看到 Kernel 输出的消息,这就算是 LILO 的安装成功了。记得这个操作的方法,以后每次我们更新 Flash 盘上的 Kernel Image,都记得要更新 LILO。也就是说,要重新运行一遍 lilo 命令。 编译内核 试验成功 LILO 的安装以后,我们开始考虑编译一个新的内核。当然,要编译新的内核,我们首先要进入我们的工作系统。这里有两个办法进入工作系统,一是在 BIOS 里面设置从 IDE2 启动,当然,这就要求当初安装工作系统的时候,要把 LILO 安装在 /dev/hdb 上;另一个办法是还是从 IDE1 启动,不改变 BIOS 的设置,但是在看到 LILO 的提示符的时候,要键入 linux root=/dev/hdb1,最前面的 linux 是在 lilo.conf 里面定义的一个 entry,我们只采用这个 entry 所指定的 Kernel Image,但是用 /dev/hdb1 作为 root 文件系统。两个办法可能有的时候一个比另一个好,更方便一些。这就要看具体的情况了。不过,它们的设置并不是互相冲突的。 在编译内核的时候,由于我们的内核是只有一台机器使用的,所以我们应该对它的情况了如指掌;另外一方面,为了减低不必要的复杂性,我们决定不用 kernel module 的支持,而把所有需要的东西直接编译到内核的里面。这样编译出来的内核,在一台普通的 586 主板上,把所有必要的功能都加进去,一般也不到 800K 字节。所以,这个办法是可行的。而且减低了 init scripts 的复杂程度。从运行方面来考虑,由于需要的 kernel 代码反正是要装载到内存中的,所以并不会引起内存的浪费。 在我们的目标平台上,我们希望使用 USB 存储设备。还有一点要注意的,就是对 Frame buffer 的支持。这主要是为了支持 XFree86。一般说来,如果我们的显卡是 XFree86 直接支持的,那当然最好,也就不需要 frame buffer 的内核支持。但是如果 XFree86 不支持我们的显卡,我们可以考虑用 VESA 模式。但是 XFree86 的 VESA 卡支持运行起来不太漂亮,还有安全方面的问题,有时在启动和退出 X Window 的时候会出现花屏。所以我们可以采用 kernel 的 vesa 模式的 frame buffer,然后用 xfree86 的 linux frame buffer 的驱动程序。这样一般就看不到花屏的现象了,而且安全方面也没有任何问题。 devfs 也是我们感兴趣的话题。如果 kernel 不使用 devfs,那么系统上的 root 文件系统就要有 /dev 目录下面的所有内容。这些内容可以用 /dev/MAKEDEV 脚本来建立,也可以用 mknod 手工一个一个来建。这个方法有其自身的好处。但是它的缺点是麻烦,而且和 kernel 的状态又并不一致。相反的,如果使用了 devfs,我们就再也不用担心 /dev 目录下面的任何事情了。/dev 目录下面的项目会有 kernel 的代码自己负责。实际使用起来的效果,对内存的消耗并不明显。所以我们选择 devfs。 busybox 有了 LILO 和 kernel image 之后,接下来,我们要安排 root 文件系统。由于 flash 盘的空间只有 16M 字节,可以说,这是对我们最大的挑战。这里首先要向大家介绍小型嵌入式 linux 系统安排 root 文件系统时的一个常用的利器:BusyBox。 Busybox 是 Debian GNU/linux 的大名鼎鼎的 Bruce Perens 首先开发,使用在 Debian 的安装程序中。后来又有许多 Debian developers 贡献力量,这其中尤推 busybox 目前的维护者 Erik Andersen,他患有癌症,可是却是一名优秀的自由软件开发者。 Busybox 编译出一个单个的独立执行程序,就叫做 busybox。但是它可以根据配置,执行 ash shell 的功能,以及几十个各种小应用程序的功能。这其中包括有一个迷你的 vi 编辑器,系统不可或缺的 /sbin/init 程序,以及其他诸如 sed, ifconfig, halt, reboot, mkdir, mount, ln, ls, echo, cat ... 等等这些都是一个正常的系统上必不可少的,但是如果我们把这些程序的原件拿过来的话,它们的体积加在一起,让人吃不消。可是 busybox 有全部的这么多功能,大小也不过 100K 左右。而且,用户还可以根据自己的需要,决定到底要在 busybox 中编译进哪几个应用程序的功能。这样的话,busybox 的体积就可以进一步缩小了。 使用 busybox 也很简单。只要建一个符号链接,比方 ln -s /bin/busybox /bin/ls,那么,执行 /bin/ls 的时候,busybox 就会执行 ls 的功能,也会按照 ls 的方式处理命令行参数。又比如 ln -s /bin/busybox /sbin/init,这样我们就有了系统运行不可或缺的 /sbin/init 程序了。当然,这里的前提是,你在 busybox 中编译进去了这两个程序的功能。 这里面要提出注意的一点是,busybox 的 init 程序所认识的 /etc/inittab 的格式非常简单,而且和常规的 inittab 文件的格式不一样。所以读者朋友们在为这个 busybox 的 init 写 inittab 的时候,要注意一下不同的语法。至于细节,就不在我们这里多说了,请大家参考 Busybox 的用户手册。 从启动到进入 shell busybox 安装好以后,我们就可以考虑重新启动,一直到进入 shell 提示符了。这之前,我们要准备一下 /etc 目录下的几个重要的文件,而且要把 busybox 用到的 library 也拷贝过来。 用 ldd 命令,后面跟要分析的二进制程序的路径名,就可以知道一个二进制程序,或者是一个 library 文件之间的互相依赖关系,比如 busybox 就依赖于 libc.so 和 ld-linux.so ,我们有了这些知识,就可把动手把所有需要的 library 拷贝到 flash 盘上。由于我们的 flash 盘说大不大,说小倒也不小,有 16M 字节之多。我们直接就用 Glibc 的文件也没有太多问题。如果读者朋友们有特殊的需要,觉得 Glibc 太庞大了的话,可以考虑用 uClibc,这是一个非常小巧的 libc 库,功能当然没有 Glibc 全,但是足够一个嵌入式系统使用了。本文就不再介绍 uClibc 了。 库程序拷贝过来以后,我们就可以考虑系统启动的步骤了。启动的时候,先是 lilo,接下来就是 kernel,kernel 初始化之后,就调用 /sbin/init,然后由 init 解释 /etc/inittab 运行各种各样的东西。inittab 会指导 init 去调用一个最重要的系统初始化程序 /etc/init.d/rcS,我们将要在 rcS 中完成各个文件系统的 mount,此外,还有在 rcS 中调用 dhcp 程序,把网络架起来。rcS 执行完了以后,init 就会在一个 console 上,按照 inittab 的指示开一个 shell,或者是开 getty + login,这样用户就会看到提示输入用户名的提示符。我们这里为了简单起见,先直接进入 shell,然后等到调试成功以后,再改成直接进入 X Window。 关于 inittab 的语法,我们上面已经提到过了,希望读者朋友们去查权威的 busybox 的用户手册。这里,我们先要讲一下文件系统的构成情况。 安排文件系统 大家已经看到,我们的 root 文件系统为了避免麻烦,用的是标准的 ext2 文件系统。由于我们的硬盘空间很小,只有不到 16M,而且我们还要在上面放上 X Window,所以,如果我们全部用 ext2 的话,Flash 盘的有限空间会很快耗尽。我们唯一的选择是采用一个适当的压缩文件系统。考虑到 /usr 目录下面的内容在系统运行的时候,是不需要被改写的。我们决定选择只读的压缩文件系统 cramfs 来容纳 /usr 目录下面的全部内容。 cramfs 是 Linus Torvalds 本人开发的一个适用于嵌入式系统的小文件系统。由于它是只读的,所以,虽然它采取了 zlib 做压缩,但是它还是可以做到高效的随机读取。既然 cramfs 不会影响系统读取文件的速度,又是一个高度压缩的文件系统,对于我们,它就是一个相当不错的选择了。 我们首先把 /usr 目录下的全部内容制成一个 cramfs 的 image 文件。这可以用 mkcramfs 命令完成。得到了这个 usr.img 文件之后,我们还要考虑怎样才能在系统运行的时候,把这个 image 文件 mount 上来,成为一个可用的文件系统。由于这个 image 文件不是一个通常意义上的 block 设备,我们必须采用 loopback 设备来完成这一任务。具体说来,就是在前面提到的 /etc/init.d/rcS 脚本的前面部分,加上一行 mount 命令: mount -o loop -t cramfs /usr.img /usr 这样,就可以经由 loopback 设备,把 usr.img 这个 cramfs 的 image 文件 mount 到 /usr 目录上去了。哦,对了,由于要用到 loopback 设备,读者朋友们在编译内核的时候,别忘了加入内核对这个设备的支持。对于系统今后的运行来说,这个 mount 的效果是透明的。cramfs 的压缩效率一般都能达到将近 50%,而我们的系统上绝大部分的内容是位于 /usr 目录下面,这样一来,原本可能要用到 18M 的 Flash 盘,现在可能只需要 11M 就可以了。一个 14M 的 /usr 目录,给压缩成了仅仅 7M。 上面考虑了压缩问题,下面还要考虑到,Flash 盘毕竟不像普通硬盘,多次的擦写毕竟不太好,所以我们考虑,在需要多次擦写的地方,使用内存来做。这个任务,我们考虑用 tmpfs 来完成。至于 tmpfs 和经典的 ramdisk 的比较,我们这里就不多说了。一般说来,tmpfs 更加灵活一些,tmpfs 的大小不像 ramdisk,可以顺着用户的需要增长或者缩小。我们选择把 /tmp、/var 等几个目录做成 tmpfs。这只需要我们在 /etc/fstab 里面加上两行类似下面的文字就可以了: none /var tmpfs default 0 0 然后别忘了在 /etc/init.d/rcS 里面靠近开头的地方,加上 mount -a。这样,就可以把 /etc/fstab 里面指定的所有的文件系统都 mount 上来了。 X Window 进行到这里,读者朋友们可能会以为,X Window 的安装可能会很复杂。其实不然,由于我们上面的架子搭好了,X Window 的安装非常简单,只需要把几个关键的程序拷贝过来就可以了。一般说来,只需要 /usr/X11R6 目录下面的 bin 和 lib 两个目录。然后,根据用户各自的需要,还可以做大幅的裁减。比如,如果你的局域网上有一个开放的 xfs 字体服务器的话,你可以把所有本地的字体都删掉,而使用远端的字体服务器。如果只需要运行有限的程序,别忘了把没有用的 library 都删掉。此外,还可以把多余的 X Window 的 driver 都删掉,只保留本机的显示卡所需要的 driver 就可以了。当然,这一关免不了要做多次测试。 技巧 如果你的工作系统式在另外一台机器上,通过局域网和本机互联的话,ssh 是一个不错的工具。此外,ssh 中带的 scp 用起来和普通的 cp 拷贝程序差不多,非常方便。用 ssh 和 scp 来共享文件,远程试验,你就可以不需要在办公室里跑来跑去的了。 如果你需要一个 MS Windows 上运行的 X Server 和 xfs 字体服务器,可以考虑包括在 Red Hat 的 Cygwin 工具箱中的 XFree86 系统

猜你喜欢