Linux系统的构建过程

Tags: 系统设计 

目录

Linux启动

Linux操作系统不仅仅有Linux的内核, 更需要具备各种服务才能成为一个完整的操作系统. 了解Linux系统的启动过程, 对理解Linux系统是非常有益的.但是Linux系统涉及的服务众多, 不同的发行版又有差异, 所以这里只描述一个轮廓, 日后逐渐充实.

BIOS

按下电源的时候, CPU执行的第一条指令是BIOS(基本输入输出系统).

对BIOS的细节知之甚少, 以后遇到的时候, 回来补充.

BIOS执行的最后, 将启动磁盘的第一个扇区(MBR)加载到内存中固定的位置(0000:7C00), 然后CPU开始执行0000:7C00处的指令, 也就是MBR中存储的指令.

MBR(Master Boot Record, 主引导记录), 是磁盘的第一个扇区, 前446字节存放引导程序, 中间64字节存放磁盘分区表, 最后两个字节固定为0x55、0xAA(0x550xAA是识别MBR的标记). 内存中0000:7C00之前的位置用来存放BIOS中断表等BIOS程序加载进去的内容.

引导程序

跳转到0000:7C00后, 开始执行的就是引导程序. 因为MBR只有512字节, 所以引导程序不完成太多任务, 主要是继续加载更多的内容. Linux系统的引导程序通常是grub.

grub2.0中,执行的第一条指令(加载到0000:7C00处的指令)在grub-2.00.tar\grub-2.00\grub-2.00\grub-core\boot\i386\pc\boot.S中.

.globl _start, start;
_start:
start:
	 /*
	 * _start is loaded at 0x7c00 and is jumped to with CS:IP 0:0x7c00
	 */
	 /*
	 * Beginning of the sector is compatible with the FAT/HPFS BIOS
	 * parameter block.
	 */
	 jmp     LOCAL(after_BPB)
	 nop     /* do I care about this ??? */

可以从boot.S开始梳理Grub的引导过程.

引导程序通过BIOS中断完成对硬件的操作.

中断表大全 http://www.oldlinux.org/Linux.old/docs/interrupts/int-html/int.htm

grub的安装方式非常简单.假设我们要在磁盘sda中安装grub, 并且将grub需要的文件存放在第一个分区sda1中.

mount /dev/sda1  /mnt
grub-install --root-directory=/mnt --no-floppy sda

执行上述命令后, sda的MBR就被写入了grub的引导程序, 在/mnt目录下则多出一个boot目录, 里面存放着grub需要的文件. 

这时候从sda启动就可以看到grub的执行.(如果要看到完整的引导,需要在boot目录下进行grub的引导配置, 指定内核和initrd, 参见grub的使用)

grub引导的具体过程涉及的内容比较深入, 目前还不甚明了, 日后有需要时补充.

grub将内核加载, 并加载了一个位于内存的根文件系统, 作为内核看到的第一个根文件系统.

initrd

initrd是内核看到的第一个根文件系统, 这个根文件系统中包含必要的驱动和设备文件, 在这里加载驱动后, 才能读取到最终的文件系统, 这时切换到最终的根文件系统.

可以将initrd解压, 查看包含的内容和完成的操作.

CentOS系列的发行版, 可以在/boot下找到img文件, 例如initrd-2.6.18-308.el5.img, 这个就是initrd. 在grub的grub.conf可以看到系统启动时使用的时那个img.

initrd-2.6.18-308.el5.img是一个gzip压缩文件, 首先需要修改后缀, 然后用gunzip解压, 再用cpio读取.

	cp initrd-2.6.18-308.el5.img  /tmp/initrd-2.6.18-308.el5.img.gz
	cd /tmp
	gunzip initrd-2.6.18-308.el5.img.gz
	mkdir cpio;
	cd cpio;
	cpio -i -d < ../initrd-2.6.18-308.el5.img

initrd中包含一个init程序, 这是内核执行的第一个程序. CentOS中init是一个nash脚本.

CentOS中init的内容:

(挂载虚拟文件系统)

	echo Creating /dev						
	mount -o mode=0755 -t tmpfs /dev /dev			#dev使用虚拟文件系统tmpfs(内存)中
	mkdir /dev/pts						#/dev/pts存放ssh登陆时生成的虚拟终端设备文件
	mount -t devpts -o gid=5,mode=620 /dev/pts /dev/pts		#/dev/pts的文件系统类型devpts, 虚拟终端文件设备
	mkdir /dev/shm						#/dev/shm  只建立目录
	mkdir /dev/mapper						#/dev/mapper 只建立目录

(创建必要的设备文件)

	echo Creating initial device nodes
	mknod /dev/null c 1 3
	mknod /dev/zero c 1 5
	mknod /dev/urandom c 1 9
	mknod /dev/systty c 4 0
	...
	...
	mknod /dev/ttyS3 c 4 67
	echo Setting up hotplug.
	hotplug
	echo Creating block device nodes.
	mkblkdevs

(加载必要的驱动)

	echo "Loading ehci-hcd.ko module"
	insmod /lib/ehci-hcd.ko
	echo "Loading ohci-hcd.ko module"
	insmod /lib/ohci-hcd.ko
	echo "Loading uhci-hcd.ko module"
	insmod /lib/uhci-hcd.ko
	mount -t usbfs /proc/bus/usb /proc/bus/usb
	echo "Loading jbd.ko module"
	insmod /lib/jbd.ko
	echo "Loading vmxnet3.ko module"
	...
	...
	insmod /lib/vmxnet3.ko
	echo "Loading pvscsi.ko module"
	insmod /lib/pvscsi.ko
	echo "Loading vmxnet.ko module"
	insmod /lib/vmxnet.ko
	echo Waiting for driver initialization.
	stabilized --hash --interval 1000 /proc/scsi/scsi
	mkblkdevs
	echo Scanning and configuring dmraid supported devices
	resume LABEL=SWAP-sda2

(切换到最终的根文件系统)

	echo Creating root device.
	mkrootdev -t ext3 -o defaults,ro /dev/sda3		#定义root路径
	echo Mounting root filesystem.
	mount /sysroot					#将实体根目录挂载到sysroot	
	echo Setting up other filesystems.
	setuproot						#将通过initrd的init建立的/proc /sys /dev目录中的资料转移到/sysroot
	echo Switching to new root and running init.
	switchroot						#切入实体根目录,将原先系统的所有内容清空

initrd最后切换的最终的根文件系统, 位于内存中的临时的根文件系统随之被清空.

系统的初始化

切换到最终的根文件系统后, 开始了Linux操作系统的初始化. 这时候就看到发行版之间的差异了, 不同的发行版有不同的处理方式, 初始化的内容也不尽相同.

初始化程序是一系列的shell脚本, 繁多复杂, 牵扯到太多的服务, 如果能够全部了解, 那么对Linux系统将会非常熟悉.

初始化脚本的组织方式主要分为System V(CentOS)和event-base(Ubuntu)两种风格.

在System V风格中,init通过/etc/inittab文件的内容得知初始化脚本、运行级别、每个级别的初始化脚本.

在event-base风格中,init遍历/etc/init目录中的事件文件,执行需要执行的脚本.

CentOS5.8版本中使用的System V风格,6.4版本中换用了event-base风格.

sysvinit -System V风格

System V风格使用sysinit机制,所有init要执行的事项都在/etc/inittab文件中指明.

/etc/inittab是init的配置文件,指定了需要运行的脚本.可以查看对应的linux手册,man 5 inittab./etc/inittab中的每一行都使用下面的格式.

id:runlevels:action:process

id是由1-4个字符组成,用来唯一标记每一行.
runlevels指定该行使用的运行级别.
action指定init应该采取的动作
	respawn:           如果process运行结束,init立即重新调用process
	wait:                  process执行一次,init等待process运行结束
	once:                 process执行一次
	boot:                 在系统启动时运行process,忽略runlevel
	bootwait:           在系统启动时运行process,init等待process运行结束
	off:                    什么都不做 
	ondemand:         
	initdefault:          默认的运行级别
	sysinit:               系统初始化执行,在boot和bootwait之前执行
	powerwait:         
	powerfail:
	powerokwait:
	powerfailnow:
	ctrlaltdel:           init收到ctl_alt_del组合键信号时执行
	kbrequest:
process是要执行的命令.    

下面是CentOS5.8的/etc/inittab文件.

默认运行级别是5
init进程依次执行/etc/rc.d/rc.sysinit
对应运行级别的process(/etc/rd.c/rc 0)
重复运行/sbin/mingetty ttyXX.

id:5:initdefault:

# System initialization.
si::sysinit:/etc/rc.d/rc.sysinit

l0:0:wait:/etc/rc.d/rc 0
l1:1:wait:/etc/rc.d/rc 1
l2:2:wait:/etc/rc.d/rc 2
l3:3:wait:/etc/rc.d/rc 3
l4:4:wait:/etc/rc.d/rc 4
l5:5:wait:/etc/rc.d/rc 5
l6:6:wait:/etc/rc.d/rc 6

# Trap CTRL-ALT-DELETE
ca::ctrlaltdel:/sbin/shutdown -t3 -r now

# When our UPS tells us power has failed, assume we have a few minutes
# of power left.  Schedule a shutdown for 2 minutes from now.
# This does, of course, assume you have powerd installed and your
# UPS connected and working correctly. 
pf::powerfail:/sbin/shutdown -f -h +2 "Power Failure; System Shutting Down"

# If power was restored before the shutdown kicked in, cancel it.
pr:12345:powerokwait:/sbin/shutdown -c "Power Restored; Shutdown Cancelled"


# Run gettys in standard runlevels
1:2345:respawn:/sbin/mingetty tty1
2:2345:respawn:/sbin/mingetty tty2
3:2345:respawn:/sbin/mingetty tty3
4:2345:respawn:/sbin/mingetty tty4
5:2345:respawn:/sbin/mingetty tty5
6:2345:respawn:/sbin/mingetty tty6

upstart -event-base风格

event-base风格的初始化机制,称为upstart机制.初始化过程被拆分成一个个事件,每个事件都各自的事件文件,事件文件中指明事件执行的时机和要执行的操作.

事件文件存放在/etc/init目录下,启动时init进程到/etc/init目录扫描执行需要执行的事件文件.

可以指定事件直接的依赖关系,例如B事件必须在A事件完成后开始.

init进程发出第一个事件startup,然后执行在startup时执行的事件,这批事件执行结束后各自发出相应的事件(事件文件中的emits命令),从而又启动一批事件,直至完成.

下面是centOS6.4中的事件文件/erc/init/rcS.conf.

指定了事件执行的时机(start on startup)V
停止的时机(stop on runlevel)
事件类型(task)
信息打印输出的位置(console output)
事件执行前进行的操作(pre-start script .... end script)
事件(exec /etc/rc.d/rc.sysinit)
事件结束后执行的操作(post-stop script ... end script)

# rcS - runlevel compatibility
#
# This task runs the old sysv-rc startup scripts.

start on startup

stop on runlevel

task

# Note: there can be no previous runlevel here, if we have one it's bad
# information (we enter rc1 not rcS for maintenance).  Run /etc/rc.d/rc
# without information so that it defaults to previous=N runlevel=S.
console output
pre-start script
		for t in $(cat /proc/cmdline); do
				case $t in
						emergency)
								start rcS-emergency
								break
						;;
				esac
		done
end script
exec /etc/rc.d/rc.sysinit
post-stop script
		if [ "$UPSTART_EVENTS" = "startup" ]; then
				[ -f /etc/inittab ] && runlevel=$(/bin/awk -F ':' '$3 == "initdefault" && $1 !~ "^#" { print $2 }' /etc/inittab)
				[ -z "$runlevel" ] && runlevel="3"
				for t in $(cat /proc/cmdline); do
						case $t in
								-s|single|S|s) runlevel="S" ;;
								[1-9])       runlevel="$t" ;;
						esac
				done
				exec telinit $runlevel
		fi
end script

从上面的事件文件中可以看到,CentOS6.4将原先在inittab中指定的rc.sysinit定位为一个rcS.conf事件,并且从inittab中获取运行级别,通过这种方式兼容了以往的System V风格的初始化过程.

Ubuntu中事件文件使用同样的格式.

查看事件文件的linux帮助手册,man 5 init.
第一批事件

以下几个事件要在startup时执行.

lja@ubuntu:/etc/init$ grep startup *
hostname.conf:start on startup 
module-init-tools.conf:start on (startup 
mountall.conf:start on startup
udev-fallback-graphics.conf:start on (startup and
udev-finish.conf:start on (startup
udevmonitor.conf:start on (startup
udevtrigger.conf:start on (startup

过滤掉同时依赖别的事件的事件后,得到最早执行的两个事件.

hostname.conf   
mountall.conf 

通过下面分析可以知道,第一批事件完成了两个工作,设置主机名,加载文件系统(磁盘).

hostname事件只是简单的设置系统的主机名.

# hostname - set system hostname
#
# This task is run on startup to set the system hostname from /etc/hostname,
# falling back to "localhost" if that file is not readable or is empty and
# no hostname has yet been set.

description     "set system hostname"

start on startup

task
exec hostname -b -F /etc/hostname

mountall发出的多个事件,带动其他事件的执行.

# mountall - Mount filesystems on boot
#
# This helper mounts filesystems in the correct order as the devices
# and mountpoints become available.

description     "Mount filesystems on boot"

start on startup
stop on starting rcS

expect daemon
task

emits virtual-filesystems
emits local-filesystems
emits remote-filesystems
emits all-swaps
emits filesystem
emits mounting
emits mounted

# temporary, until we have progress indication
# and output capture (next week :p)
console output

script
	. /etc/default/rcS
	[ -f /forcefsck ] && force_fsck="--force-fsck"           # 环境变量指示是否需要修复文件系统
	[ "$FSCKFIX" = "yes" ] && fsck_fix="--fsck-fix"

	# set $LANG so that messages appearing in plymouth are translated
	if [ -r /etc/default/locale ]; then
		. /etc/default/locale
		export LANG LANGUAGE LC_MESSAGES LC_ALL     # 语言环境
	fi

	exec mountall --daemon $force_fsck $fsck_fix          # mountall命令,加载fstab中指定的挂载
end script

post-stop script                                             # 事件结束
	rm -f /forcefsck 2>dev/null || true
end script

systemd

systemd supports SysV and LSB init scripts and works as a replacement for sysvinit

systemd

redhat intro

Rethinking PID 1

CentOS initrd分析

解压initrd

在/boot下找到initrd-2.6.18-308.el5.img,复制到另一个目录下(防止操作将原文件覆盖),并修改后缀名为.gz,如下:

cp initrd-2.6.18-308.el5.img /root/initrd/initrd-2.6.18-308.el5.img.gz

initrd-2.6.18-308.el5.img是一个gzip压缩文件,修改后缀后,使gunzip可以识别解压:

gunzip initrd-2.6.18-308.el5.img.gz

解压后得到.img文件,使用cpio解压.imgd到一个新目录中:

mkdir cpio;
cd cpio;
cpio -i -d < ../initrd-2.6.18-308.el5.img

在cpio目录下得到initrd中所有文件:

bin  dev  etc  init  lib  proc  sbin  sys  sysroot

cpio中的文件就是内核在initrd环境中运行时使用的根文件系统。其中的init时执行的第一个进程。Cento5.8的initrd中的cpio/init是一个nash脚本,完成到磁盘上的根文件系统的切换.

nash是专门用于inird的类似shell的解析程序,内置了一些必用的命令。

cpio/init完成以下工作:

挂载虚拟文件系统

echo Creating /dev						
mount -o mode=0755 -t tmpfs /dev /dev			#dev使用虚拟文件系统tmpfs(内存)中
mkdir /dev/pts						#/dev/pts存放ssh登陆时生成的虚拟终端设备文件
mount -t devpts -o gid=5,mode=620 /dev/pts /dev/pts		#/dev/pts的文件系统类型devpts, 虚拟终端文件设备
mkdir /dev/shm						#/dev/shm  只建立目录
mkdir /dev/mapper						#/dev/mapper 只建立目录

最初需要的设备文件

echo Creating initial device nodes
mknod /dev/null c 1 3
mknod /dev/zero c 1 5
mknod /dev/urandom c 1 9
mknod /dev/systty c 4 0
mknod /dev/tty c 5 0
mknod /dev/console c 5 1
mknod /dev/ptmx c 5 2
mknod /dev/rtc c 10 135
mknod /dev/tty0 c 4 0
mknod /dev/tty1 c 4 1
mknod /dev/tty2 c 4 2
mknod /dev/tty3 c 4 3
mknod /dev/tty4 c 4 4
mknod /dev/tty5 c 4 5
mknod /dev/tty6 c 4 6
mknod /dev/tty7 c 4 7
mknod /dev/tty8 c 4 8
mknod /dev/tty9 c 4 9
mknod /dev/tty10 c 4 10
mknod /dev/tty11 c 4 11
mknod /dev/tty12 c 4 12
mknod /dev/ttyS0 c 4 64
mknod /dev/ttyS1 c 4 65
mknod /dev/ttyS2 c 4 66
mknod /dev/ttyS3 c 4 67
echo Setting up hotplug.
hotplug
echo Creating block device nodes.
mkblkdevs

加载相关模块

echo "Loading ehci-hcd.ko module"
insmod /lib/ehci-hcd.ko
echo "Loading ohci-hcd.ko module"
insmod /lib/ohci-hcd.ko
echo "Loading uhci-hcd.ko module"
insmod /lib/uhci-hcd.ko
mount -t usbfs /proc/bus/usb /proc/bus/usb
echo "Loading jbd.ko module"
insmod /lib/jbd.ko
echo "Loading ext3.ko module"
insmod /lib/ext3.ko
echo "Loading scsi_mod.ko module"
insmod /lib/scsi_mod.ko
echo "Loading sd_mod.ko module"
insmod /lib/sd_mod.ko
echo "Loading scsi_transport_spi.ko module"
insmod /lib/scsi_transport_spi.ko
echo "Loading mptbase.ko module"
insmod /lib/mptbase.ko
echo "Loading mptscsih.ko module"
insmod /lib/mptscsih.ko
echo "Loading mptspi.ko module"
insmod /lib/mptspi.ko
echo "Loading libata.ko module"
insmod /lib/libata.ko
echo "Loading ata_piix.ko module"
insmod /lib/ata_piix.ko
echo "Loading dm-mem-cache.ko module"
insmod /lib/dm-mem-cache.ko
echo "Loading dm-mod.ko module"
insmod /lib/dm-mod.ko
echo "Loading dm-log.ko module"
insmod /lib/dm-log.ko
echo "Loading dm-region_hash.ko module"
insmod /lib/dm-region_hash.ko
echo "Loading dm-message.ko module"
insmod /lib/dm-message.ko
echo "Loading dm-raid45.ko module"
insmod /lib/dm-raid45.ko
echo "Loading vmxnet3.ko module"
insmod /lib/vmxnet3.ko
echo "Loading pvscsi.ko module"
insmod /lib/pvscsi.ko
echo "Loading vmxnet.ko module"
insmod /lib/vmxnet.ko
echo Waiting for driver initialization.
stabilized --hash --interval 1000 /proc/scsi/scsi
mkblkdevs
echo Scanning and configuring dmraid supported devices
resume LABEL=SWAP-sda2

切入实体系统

echo Creating root device.
mkrootdev -t ext3 -o defaults,ro /dev/sda3		#定义root路径
echo Mounting root filesystem.
mount /sysroot					#将实体根目录挂载到sysroot	
echo Setting up other filesystems.
setuproot						#将通过initrd的init建立的/proc /sys /dev目录中的资料转移到/sysroot
echo Switching to new root and running init.
switchroot						#切入实体根目录,将原先系统的所有内容清空

切换完成

切换完成后,首先执行的是实体系统中的init进程。

实验

重新打包成cpio

find . | cpio -o -H newc > ../initrd_new.img	#将当前目录下所有文件打包成cpo
cd ..; gzip initrd_new.img

将得到的initrd_new.img.gz复制到/boot目录下。 在/boot/grub/grub.conf中增加新的启动项:

title initrd_new (2.6.18-308.el5)
	root (hd0,0)
	kernel /vmlinuz-2.6.18-308.el5 ro root=LABEL=/
	initrd /initrd_new.img.gz

将busybox集成到initrd

下载busybox源码: http://www.busybox.net/

解压,编译,安装

make menconfig   #需要安装curses-devel,最好选用编译成static,不然需要添加依赖的动态库
make 		 #如果内核版本还没直至ubi, 在配置中将ubi相关的选项关闭(.config中),ubi是新的flash文件系统
make CONFIG_PREFIX=/path/from/root install  #安装到的目录

Linux系统定制

自己动手定制一个linux,可以解除很多的疑惑,破除种种神秘。首先, 要有一台Linux机器。分为安装grub、编译内核、制作initrd三步。

安装grub

假设我们的目标磁盘的设备文件是sdb。首先sdb进行分区、格式化。假设分成sdb1、sdb2。然后将sdb1挂载到/mnt

执行grub安装

grub-install --root-directory=/mnt --no-floppy sdb

执行上述命令后, 在/mnt目录下会多出一个boot目录。将来内核、initrd都会存放在boot目录下。

在/mnt/boot/grub下配置grub.conf。

可以参照原有系统的grub配置文件(/boot/grub/grub.conf), 这时候从sdb启动时就会进入到grub。

制作initrd

新建一个目录initrd

mkdir initrd

将需要的东西复制到initrd目录中

例如将busybox安装到initrd中、创建必要的几个设备文件、init程序

可以查看当前使用的系统initrd,进行参照

解压方式: cpio -idmv < filename.img

生成img

find . | cpio -o -H newc |gzip -9 >initrd.img  

特别注意这一步必须在initrd目录下进行!这样才能保证解压后的内容

丰富initrd

当可以启动进入到initrd的时候, 就说明已经制作成功。接下来需要做的就是在其中添加各种新的程序。

参考

  1. 《深度探索Linux操作系统–系统构建和原理解析》 王柏生
  2. 《深入探索Linux操作系统的奥秘》

系统设计

  1. 各大云厂商的 API 设计风格
  2. Google 是如何实践 RESTful API 设计的?
  3. Netflix 的异地多活设计: Active-Active for Multi-Regional Resiliency
  4. Facebook 的缓存系统实践经验《Scaling Memcache at Facebook》
  5. 多机数据系统的正确性与一致性
  6. 《大型网站技术架构: 核心原理与案例分析》阅读摘录
  7. 《分布式金融架构课》阅读笔记2: 线性一致的分布式数据系统的实现过程
  8. 《分布式金融架构课》阅读笔记1: 单机&多机并发/多副本读写正确性和一致性
  9. 《消息队列高手课》阅读笔记: Rabbit/Rocket/Kafka/模型/消息事务/保序等
  10. 《消息队列高手课》阅读笔记: Rabbit/Rocket/Kafka/模型/消息事务/保序等
  11. 《Redis核心技术与实践》阅读笔记: 数据类型/存储开销/Rehash/案例等
  12. 《Redis核心技术与实践》阅读笔记: 数据类型/存储开销/Rehash/案例等
  13. 《高并发系统设计40问》阅读笔记: 数据库/缓存/消息队列/分布式服务
  14. 《高并发系统设计40问》阅读笔记: 数据库/缓存/消息队列/分布式服务
  15. 《MySQL实战45讲》阅读笔记: 索引类型/数据可靠性/事务/间隙锁/临时表等
  16. 系统性能分析方法论: 统计图谱工具
  17. 张磊《深入剖析Kubernetes》专栏的阅读笔记
  18. 代理服务软件haproxy、nginx、envoy对比,以及开源的API网关项目对比
  19. 蓝绿部署、金丝雀发布(灰度发布)、A/B测试的准确定义
  20. 阿里巴巴的应用限流和服务降级是怎样实现的?|如何打造平台稳定能力
  21. 陈皓《左耳听风》专栏的阅读笔记(持续更新)
  22. 好雨云帮,一款不错的国产开源PaaS
  23. 怎样为软件的不同版本命名?
  24. 怎样选择开源项目的license?
  25. Glusterfs的架构
  26. 怎样设计一个企业级的PaaS平台?
  27. 几种常见的LDAP系统
  28. DNS SRV介绍(一种用DNS做服务发现的方法)
  29. DNS,DNS-Domain Name System
  30. 思科的网络设备
  31. 虚拟化技术汇总
  32. 认证与授权系统的汇总
  33. 高可用实现方法汇总
  34. 编译器汇总
  35. Linux系统的优化方法
  36. CentOS7的一些变化
  37. 分布式系统的一些知识
  38. 计算机编程语言的特性汇总
  39. 网络通信的一些基础知识
  40. PCIE总线的一些知识
  41. 操作系统的API
  42. 网卡的一些知识
  43. Linux系统的构建过程
  44. 数据结构与算法
  45. CPU的相关知识

推荐阅读

Copyright @2011-2019 All rights reserved. 转载请添加原文连接,合作请加微信lijiaocn或者发送邮件: [email protected],备注网站合作

友情链接:  系统软件  程序语言  运营经验  水库文集  网络课程  微信网文  发现知识星球