Linux 2.6.20 根文件系统的构建(RedHat 9)

本文介绍了构建嵌入式Linux根文件系统的步骤与注意事项,涵盖了根文件系统的结构、设备文件的创建、核心应用程序的选择以及BusyBox的配置与使用等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

根文件系统的构建

Linux 内核在系统启动期间的最后操作之一就是安装根文件系统。根文件系统一直都是所有 Unix 系统不可或缺的组件。根文件系统目前的结构有点独特,而且包含了一些多余之处,这是因为它与日俱进的成长深受 Unix 发展的影响。

根文件系统的基本结构

根文件系统顶层目录各有其特殊的用法和目的。其中有许多目录只对用户系统有意义,而这些由不同用户使用的服务器或工作站,则是由系统管理员负责维护。在大多数的嵌入式 Linux 系统中,通常不会有所谓的用户和管理者,所以不必严格看待用来建立根文件系统的规则。这并不表示你可以违反所有的规则,只是意味着破坏某些规则对系统的正常操作几乎没有影响。值得注意的是,即使是用于工作站和服务器的主流商业发行套件也不一定会遵守根文件系统的建立规则。

用来建立根文件系统的“正式”规则列在FHS中,在下面的附页中我们提到了。如果在寻求与根文件系统有关的答案或说明,FHS 或许是绝佳的起点。下表提供了根文件系统顶层目录的完整清单以及 FHS 指定的内容。

根文件系统顶层目录:

如果 Linux 是你每天必用的工作平台,应该已经熟悉以上提到的目录了。不过,现在让我们来进一步检查嵌入式 Linux 系统如何使用这些典型的根文件系统内容。

首先,为多用户提供可扩展的所有目录(如 /home/mnt/opt/root)都应该省略。调整根文件系统的时候,我们甚至可以进一步移除 /mnt/ /var ,不过这么做可能会危害到某些软件的运行。因此不建议采用这种过于简化的做法。

根据引导加载程序和它的配置情况,可能不需要 /boot 目录。这取决于引导加载程序是否会在内核被启动之前从根文件系统取回内核映像。在此我们可以不要,日后如果觉得有此需要,还可以重新设计根文件系统。

其余的目录,/bin/dev/etc/lib/proc/sbin/ usr ,都是不可或缺的。

极端情况下我们还可以省略 /proc ,因为它只能用来安装与其同名的虚拟文件系统。然而这么做之后,如果需要实地分析目标板,将会很难了解目标板发生了什么事。如果为了缩减存储空间,可能会将内核设定成不支持 /proc ,但是还是建议尽可能启用此功能。

/usr /var 这两个顶层目录与根目录非常像,有自己的目录结构。在接下来的步骤中,当我们在摆放这两个目录的时候,将会简述它们的目录结构。

令人困惑的相似性

根文件系统最令人困惑的一点就是有些目录看起来具有类似的用途。尤其是,新手常会问、不同目录包含的二进制文件,以及不同目录包含的链接库,有何差异。

在根文件系统上,存放二进制文件的目录主要有四个:/bin/sbin/usr/bin/usr/sbin 。二进制文件要放在其中哪个目录,这与它在系统中所扮演的角色有很大的关系。如果这是用户和系统管理员必备的二进制文件,就会放在 /bin 。如果这是系统管理员必备、但是一般用户根本不会用到的二进制文件,就会放在 /sbin 。相对而言,如果不是用户必备的二进制文件,多半会放在 /usr/bin ;如果不是系统管理员必备的工具,多半会放在 /usr/bin

至于链接库的摆放位置,也是同样的道理。系统引导以及执行最基本命令需要的链接库摆在 /lib 。所有其他的链接库则会摆在 /usr/lib 。通常,套件安装时,会在 /usr/lib 中产生子目录,以便摆放它自己的链接库。以 Perl 5.x 为例,它会产生 /usr/lib/perl5 目录,里面摆放的都是与 Perl 有关的链接库和模块。

回过头来看看自己使用的 Linux 工作站,可以从它的根文件系统看到,发行套件设计者应用这些标准的实际范例。

为了建立根文件系统,我们可以建立以下目录:

[root@Binnary ~ ] # mkdir rootfs

[root@Binnary ~ ] #cd rootfs

现在我们可以针对系统的需要建立根文件系统的顶层目录:

[root@Binnary rootfs ] #mkdir bin dev etc lib proc sbin tmp usr var

[root@Binnary rootfs ] #chmod 1777 tmp

请注意:我们并未建立 /boot 。如果日后需要再建立也不迟。同时请注意,我们变更了 /tmp 目录的使用权,让它开启 sticky 位,为 /tmp 目录的使用权开启此位,可确保 /tmp 目录底下建立的文件,只有建立它的用户有权删除。嵌入式系统多半是单用户系统,不过有些嵌入式应用一定不能用 root 的特权来执行,因此需要遵照根文件系统权限位的一些基本规定。如 OpenSSH 套件便属于此类应用。

接着我们可以建立 /usr 的目录结构:

[root@Binnary rootfs ] # mkdir usr/bin usr/lib usr/sbin

在一个全功能的根文件系统上,/usr 目录通常会包含更多条目。你只要在自己的工作站上键入 ls –al /usr 这个命令就能够展示这样的简单范例。你将会发现如 man src local 等这样的目录。FHS 中有一节专门探讨此目录的布局。然而,以大多数嵌入式 Linux 系统的用途来说,上面所建立的这三个目录已经够用了。

最后我们可以建立 /var 的目录结构:

[root@Binnary rootfs ] # mkdir var/lib var/lock var/log var/run var/tmp

[root@Binnary rootfs ] #chmod 1777 var/tmp

同样地,此目录通常会包含更多的条目。尽管 cache mail spool 等目录对工作站或服务器来说很有用,但是嵌入式系统多半不需要这些目录。我们建立的目录必须符合“能够让嵌入式 Linux 系统上可以找到的大多数应用程序正常运行”这个最起码的要求。当然,如果需要 Web 服务或打印功能,那么你或许会想要加入额外的目录,以符合应用程序提供功能的需要。FHS 和应用程序提供的文档可以找到实际需求。

装备好根文件系统的骨架之后,让我们将各软件组安装到正确的位置上去。

以不同的根文件系统结构来执行 Linux

正如前所说,可以在 FHS 中找到建立根文件系统的规则。尽管大多数的 Linux 应用和发行套件都会遵照这些规则,但是 Linux 内核本身并未强制实施这些规则。事实上,内核源码与根文件系统结构的几乎没有什么关联。它让你可以使用截然不同的根文件系统结构来建立嵌入式 Linux 系统。然后你必须修改大多数软件套件的默认值,以便与新的结构相符。有些人甚至采取更极端的做法,建立完全没有根文件系统的嵌入式 Linux 系统。我会建立你别这么做。以上描述的根文件系统的规则,是开放源码和自由软件所有开发者一致奉告的原则。如果使用其他规则来建立嵌入式 Linux 系统,无疑将自己排斥在大多数的开放源码和自由软件套件有以及它们的开发者之外。

设备文件

依照 Unix 的传统,在 Linux 系统中任何对象都可以视为文件。在 Linux 根文件系统中,所有的设备文件(也称为设备节点)都放在 /dev 目录里。对各种可能的系统变体来说,大多数的工作站和服务器发行套件为 /dev 目录附带了内容,这个数目超过了 2000 套。因为嵌入式 Linux 系统是个定制的的系统,所以目标板的 /dev 目录里并不需要像 Linux 工作站和服务器那样填入那么多条目。事实上,只需建立让系统正常操作的必要条目即可。

如果手上没有可供参考的信息,很判断自己需要哪些条目。倘若选择用devfs(设备文件系统)来取代固定的静态设备文件,则可免去寻找设备信息的麻烦。然而,devfs 并没有得到广泛采用,静态的设备仍是标准。

先前我们也说过静态设备主要和次要编号的信息。

下表列出了 /dev 目录中需要填入的最基本条目。根据设备情况,或许应该加入若干额外的条目。在某些情况下,可能甚至需要使用下表以外的条目。如在某些系统上第一个串行端口并非ttyS0StrongARM-base 系统的第一个串行端口则是 ttySAC0(主要编号:204,次要编号:5)。

表:基本的 /dev 条目

文件名

说明

类型

主编号

次编号

权限位

mem

物理内存

字符

1

1

660

null

黑洞

字符

1

3

666

zero

null byte(零值字节)为数据来源

字符

1

5

666

random

真随机产生器

字符

1

8

644

tty0

现行的虚拟控制台

字符

4

0

600

tty1

第一个虚拟控制台

字符

4

1

600

ttyS0

第一个UART串行端口

字符

4

64

600

tty

现行的TTY设备

字符

5

0

666

console

系统控制台

字符

5

1

600

[root@Binnary rootfs ] #mknod –m 600 console c 5 1

[root@Binnary rootfs ] #mknod –m 666 null c 1 3

主要的系统应用程序

除了内核的功能以及根文件系统的结构,Linux 还继承了 Unix 极其丰富的命令集。问题在于标准的工作站或服务器发行套件者配备有数以千计的二进制命令文件,而且不同发行套件还各不相同。显然,开发者没有能力逐一交叉编译这么多二进制文件,而且大多数嵌入式系统也不需要这么多二进制文件。

因此有两种做法:不是只挑选若干标准命令,就是尽可能将命令集浓缩成仅仅实现必要功能的极少数应用程序。接下来,我们会看第一种做法。然后我们再探讨第二种做法,这里面包括 BusyBox TinyLogin Embutils 这三个作为此用途的主要套件。

完整的标准应用程序

如果想有选择地利用主流发行套件中可以找到的若干标准应用程序,最好的对策就是以 Linux From Scratch计划位于 http://www.linuxfromscratch.org 的网站为起点。此计划提供有各种套件的说明和链接,其目的在于协助你定制自己的发行套件。该网站提供的《Linux From Scratch》便是此计划的主要文档,它逐一列出了每个应用程序的建立提示以及相关链接,并且为每个套件提供了建立时间和磁盘空间的估算值。

或者,可以从网络逐一下载应用程序并且依照每个套件的指示进行编译和交叉编译。因为只有少数套件提供有交叉编译的完整说明,所以你可能需要检查套件的 Makefile ,并据此决定适当的建立标志或套件进行适当的修改,让交叉编译顺利完成。

BusyBox

Bruce Pcrens 1996 年发起的 BusyBox 计划,目的在于协助 Debian 发行套件建立安装磁盘。从 1999 年开始,此计划由 uClibc 的维护者 Erik Andcrsen 接手维护,它最初是 Lineo 开放源码成果的一部分,目前是个与厂商无关的计划。其间,BusyBox 计划发展很快,它现在是许多嵌入式 Linux 系统的基石之一。它被收纳在大多数嵌入式 Linux 发行套件之中,并且拥有非常活跃的用户群。此计划的网站目前位于 http://www.busybox.net/ 。此计划网站包括了文档、链接以及以往的邮件清单记录。如果愿意接受 GNU GPL 的许可条款,可在同一个网站下载 BusyBox 套件。

BusyBox 之所以受到热烈的欢迎是因为它能够以一个极小型的应用程序来提供整个命令集功能。BuxyBox 实现了许多命令,以下列举一二:arcatchgrpchmodchownchrootcpcpiodatedddfifconfig 等等。

尽管 BusyBox 并不支持这些的命令的所有选项,它提供的子集已足以满足大多数应用的要求。你可以在 BusyBox 发行套件的 docs 目录里看到若干不同格式的文件。它可以用静态和动态的方式链接 glibc uClibc

配置、编译和安装BusyBox-1.0

[root@Binnary ~ ] #tar –jxvf busybox-1.00.tar.bz2

[root@Binnary ~ ] #cd busybox-1.00

[root@Binnary busybox-1.00 ] #make menuconfig

General configuration   --->

Build Options  --->

     [ ] Build BusyBox as a static binary (no shared libs)

     [ ] Do you want to build BusyBox with a Cross Compiler?

……

BusyBox 进行了配置后下面就可以对其进行编译了。

[root@Binnary busybox-1.00 ] # make

[root@Binnary busybox-1.00 ] #make install

当你使用 Busybox 提供的 shell 时,例如 ashlash msh 、可以使用 /etc/profile 文件为所有的 shell 用户定义全局变量。下面为单用户的目标板定义的 /etc/profile 范例文件:

#set path

PATH=/bin:/sbin:/usr/bin:/usr/sbin

export PATH

系统初始化

系统初始化是 Unix 系统的另一个特性,内核最后一个初始化动作就是启动 init 程序。此程序在终结系统启动程序之前会衍生各种应用程序,并且启动若干关键的软件组件。大多数 Linux 系统使用的 init System V init 相仿,配置设定的方式也极为相似。对嵌入式 Linux 系统来说,System V init 过于灵活,因为此类系统很少会被当成多用户系统来使用。

根文件系统上其实并不需要配置标准的 init 程序,例如 System V init 。内核本身并不在意所执行的是否为 init 程序。内核只要求系统完成初始化动作之后,它可以启动一个应用程序。例如你可以加入 init=PATH_TO_YOUR_INIT 这个引导参数,要求内核使用 init 程序。然而这么做会有些缺点,因为应用程序将会是内核所启动的惟一应用程序。而且,如果应用程序意外中止的话,将会造成内核恐慌并导臻系统重新引导;这如同 System V init 意外结束执行一样。尽管在某些情况下可能需要这么做,但是这么做多半会导致系统变得毫无用处。因此,比较安全和有用的做法通常是让根文件系统配备真正的 init 程序。

接下来我们将会探讨在BusyBox init

BusyBox init

除了缺省支持的命令,BusyBox 还提供与 init 类似的能力。如同原始的主流 init BusyBox 也可以处理系统的启动工作。BusyBox init 尤其适合在嵌入式系统中使用,因为它可以为嵌入式系统提供所需要的大部分 init 功能,却不会让嵌入式系统被 System V init 的额外特性拖累。此外,因为 BusyBox 是单个套件,所以当你要开发或维护系统时,不需要注意额外的软件套件。然而有些时候系统可能不适合使用 BusyBox init ,例如它不提供运行级别的支持。

因为 /sbin/init /bin/busybox 的符号链接,所以 BusyBox 是目标板系统上执行的第一个应用程序。当 BusyBox 知道调用它的目的是要执行 init ,它会立即跳转到 init 进程。

BusyBox init 进程会依次进行以下工作:

1  init 设置信号处理进程。

2  初始化控制台。

3  剖析 inittab 文件、/etc/inittab 文件。

4  执行系统初始化命令行。BusyBox 在缺省情况下会使用 /etc/init.d/rcS 命令行。

5  执行所有会导致 init 暂停的 inittab 命令(动作类型:wait )

6  执行所有仅执行一次的 inittab 命令(动作类型:once )。

一旦完成以上工作,init 进程便会循环执行以下工作:

1  执行所有终止时必须重新启动的 inittab 命令(动作类型:respawn )。

2  执行所有终止时必须重新启动但启动前必须先询问用户的 inittab 命令(动作类型:askfirst )。

在控制台初始化期间,BusyBox 会判断系统是否被设成在串行端口上执行控制台(如,console=ttyS0作为内核引导参数)。若是这样,BusyBox 0.60.4 之前的版本会停用所有的虚拟终端。但是,从 0.60.4 的版本开始,BusyBox 在初始化的过程中并不会停用虚拟终端。如果事实上没有虚拟终端,稍后若用户想在某些虚拟终端上启动 shell 则会导致执行失败,所以不必停用虚拟终端。

控制台初始化后,Busybox 会检查 /etc/inittab 文件是否存在。如果此文件不存在,BusyBox 会使用缺省的 inittab 配置。它主要会为系统重引导,系统挂起以及 init 重启动设置缺省的动作。此外,它还会为头四个虚拟控制台(/dev/tty1 /dev/tty4)设置启动shell 的动作。如果并未建立这些设备文件,BusyBox 将会报错。

如果存在 /etc/inittab 文件,Busybox 会予以剖析,并将其中的命令记录在内部的数据结构中,以便适时执行。BusyBox 能够识别的 inittab 文件格式,在 BusyBox 套件附带的文档中有很好的说明。BusyBox 套件附带的文档中还包含详尽的 inittab 文件范例。

inittab 文件中每一行的格式如下所示:

id:runlevel:action:process

尽管此格式与传的 System V init 类似,但请注意,id BusyBox init 中具有不同的意义。对 Busybox 而言,id 用来指定所启动进程的控制 tty 。如果所启动的进程并不是个可以交互的 shell ,大可以将此字段空着不填。可以交互的 shell ,例如 BusyBox sh ,应该会有个控制 tty 。如果控制 tty 不存在,BusyBox sh 将会报错。 BusyBox 将会完全忽略 runlevel 字段,所以你可以将它空着不填。process 字段用来指定所执行程序的路径,包括命令行选项。action 字段用来指定下表所列八个可应用到 process 的动作之一。

表:BusyBox init 能够识别的 inittab 动作类型

动作

结果

sysinit

init 提供初始化命令行的路径

respawn

每当相应的进程终止执行便重启动

askfirst

类似 respawn ,不过它们的主要用途是减少系统上执行的终端应用程序的数量。它将会促使 init 在控制台上显示“Please press Enter to activate this console”的信息,并在重新启动进程等待用户按下 Enter

wait

告诉 init 必须等到相应的进程完成后才能继续执行

once

仅执行相应的进程一次,而且不会等待它完成

ctrlaltdel

当按下Ctrl-Alt-Delete 组合键时,执行相应的进程

shutdown

当系统关机时,执行相应的进程

restart

init 重新启动时,执行相应的进程。通常此处所执行的进程就是init 本身

一个简单的文件:

      ::sysinit:/etc/init.d/rcS

      ::respawn:-/bin/login

::restart:/sbin/init

::ctrlaltdel:/sbin/reboot

::shutdown:/bin/umount –a -r

::shutdown:/sbin/swapoff -a

此处将 id 空着不填,因为该字段与命令的正常操作无关。runlevel 也空着未填,因为 BusyBox 会完全忽略此字段。

然而,如同前文所述,除非 init 执行系统初始化命令行,否则不会进行以上提到的这些动作。这个命令行可能相当复杂,也可能会调用其他的命令行。你可以使用这个命令行来设定所有的基本设定值,初始化各种需要特别处理的系统组件。尤其是,此处是执行以下动作的地方:

·以读写模式重新安装根文件系统。

·安装额外的文件系统。

·初始化及启动网络接口。

·启动系统监控程序。

下面是我的初始化命令行:

#!/bin/sh

echo "mount all "

/bin/mount a

#/sbin/ifconfig eth0 192.168.1.100

echo "**********************************" echo "     zky embedded linux  " echo "**********************************"

上面的初始化命令要能够正常执行,目标板的根文件系统中必须存在 fstab 文件。下面我们看一下 fstab 文件。

         none     /proc    proc     defaults 0 0

         none     /dev/pts devpts   mode=0622        0 0

         tmpfs    /dev/shm tmpfs    defaults         0 0

链接库

正如前所说,glibc 套件包含若干链接库。你可以任套件建立期间列出 lib 目录的内容检查它所安装的所有链接库。此目录中主要包含四种类型的文件:

实际的共享链接库

这类文件的文件格式为 libLIBRARY_NAME_GLIBC_VERSION.so ,其中 LIBRARY_NAME 是链接库的名称,GLIBC_VERSION 是你使用的 glibc 套件的版本编号。如 glibc 2.2.3 的数学链接库的名称为 libm-2.2.3.so

主修订版本的符号链接

主修改版本的编号方式与实际的 glibc 版号不同。以 glibc 2.2.3 实际的共享 C 链接库 libc-2.2.3.so 为例,它的主修订版本编号为 6 。相对而言,libdl-2.2.3.so 的主修订版本编号为 2 。主修订版本的符号链接的名称格式为 libLIBRARY_NAME.so.MAJOR_REVISION_VERSION ,其中MAJOR_REVISION_VERSION 是链接库的主修订版本编号。以实际的 C 链接库为例,其符号链接的名称为 libc.so.6 libdl 则是 libdl.so.2 。程序一旦链接了特定的链接库,它将会参用其符号链接。程序启动时,加载器程序之前,会因此加载该文件。

与版本无关的符号链接指向主修订版本的符号链接

这些符号链接的主要功能,是为需要链接特定链接库的所有程序提供一个通用的条目,与主修订版本的编号或 glibc 涉及的版本无关。这些符号链接典型的格式为 libLIBRARY_NAME.so 。例如,libm.so 指向 lib.so.6 lib.so.6 指向实际的共享链接库 libm-2.2.3.so 。惟一的例外是 libc.so ,它是一个链接命令行。这个与版本无关之符号链接,就是链接程序时参用到的一个文件。

静态链接库包文件

选择静态方式链接库的应用程序便会使用这些包文件.这些包的文件名格式为 libLIBRARY_NAME.a 。例如 libdl 的静态包文件就是 libdl.a

以上描述的四种类型的文件,我们只需其中两种:实际的共享链接库和主修订版本的符号链接。其余两种类型的文件只有在链接执行文件的时候才会用到,执行应用程序的时候并不需要。

除了链接库文件,我们还需要复制动态链接器及其符号链接。动态链接器的文件名,依据 glibc 链接库的命名习惯,通常会叫作 ld-GLIBC_VERSION.so 。然而,这或许是 GNU 工具链中最奇特的地方,动态链接器符号链接的名称取决于工具链所应用的架构。如果这是为 i386 ARM SuperH m68k 建立的工具链,则动态链接器的符号链接通常会叫作 ld-linux.so.MAJOR_REVERSION_VERSION 。如果这是为 MIPS PowerPC 所建立的工具链,则动态链接器的符号链接通常会叫作 ld.so.MAJOR_REVISION_VERSION

然而,在向目标板的根文件系统实际复制任何 glibc 组件之前,必须先找出应用程序需要哪些 glibc 组件。下表提供了 glibc 中所有组件的简短说明,以及每个组件的引用提示。

表:glibc 里的链接库组件以及根文件系统的引用提示

链接库组件

内容

引用提示

ld

动态链接器

必要

libBrokenLocale

修正进程,让locale(本地)特性有问题的应用程序得以正常执行。经由预先加载来覆盖应用程序的预设值(需使用 LD_PRELOAD)。

很少用到

libSegFault

用来捕捉内存区段错误以及进行回溯的进程

很少用到

libanl

异步名称查询进程

很少用到

libc

C 链接库进程

必要

libcrypt

密码学进程

大多数涉及认证应用程序需要用到

libdl

用来动态加载共享目的文件的进程

使用 dlopen() 之类函数的应用程序需要用到

libm

数学进程

数学函数需要用到

libmemusage

用来进行堆(heap)和堆栈(stack)很少用到内存统计的进程

很少用到

libnsl

NIS 网络服务链接库进程

很少用到

libnss_compat

这是 NIS Name Switch ServiceNSS)兼容的进程

glibc NSS 自动加载

libnss_dns

DNS NSS 进程

glibc NSS 自动加载

libnss_files

文件查询的NSS进程

glibc NSS 自动加载

libnss_hesiod

Hesiod 名称服务的NSS进程

glibc NSS 自动加载

libnss_nis

NIS NSS进程

glibc NSS 自动加载

libnss_nisplus

NIS plus NSS 进程

glibc NSS 自动加载

libpcprofile

程序计数器统计进程

很少用到

libpthread

Linux POSIX 1003.1c 多线程

多线程设计需要用到

libresolv

名称解析器进程

名称解析需要用到

Librl

异步I/O 进程

很少用到

libpthread_db

多线程调试进程

对使用多线程的应用程序进行调试时,由 gdb 自动加载。事实上,任何应用程序都不会链接此链接库

Libutil

登录进程,它是用户记录数据库的一部分

终端联机需要用到

除了记下应用程序链接哪些链接库,通常还可以使用 ldd readelf 命令列出应用程序要依存哪些动态链接库。

决定需要哪些链接库组件之后,我们可以将这些链接库组件和相关的符号链接复制到目标板根文件系统中的 lib 目录里。

for file in libc libcrypt libdl libm libpthread libresolv libutil

> do

> cp $file-*.so /rootfs/lib

> cp –d $file.so.[*0-9] /rootfs/lib

> done

cp –d ld*.so.* /rootfs/lib

第一个 cp 命令会复制实际的共享链接库,第二个 cp 命令会复制主修订版本的符号链接,第三个 cp 命令则会复制动态链接器及其符号链接。在第二个和第三个 cp 命令中的 –d 选项用来复制符号链接本身。否则会变成复制符号链接指向的文件。

 

linux_cmd_line 的值改为“ noinitrd root=/dev/mtdblock3 init=/linuxrc  console=ttySAC0”

param set linux_cmd_line “noinitrd root=/dev/mtdblock3 init=/linuxrc  console=ttySAC0”

 

作者:binnary   (http://blog.youkuaiyun.com/bly7912/archive/2008/06/20/2569157.aspx)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值