内容概述
- 进程相关概念
- 进程工具
- 系统性能相关工具
- 计划任务
1 进程和内存管理
内核功用:进程管理、内存管理、文件系统、网络功能、驱动程序、安全功能等
1.1 什么是进程
Process:运行中的程序的一个副本,是被载入内存的一个指令集合,是资源分配的单位
- 进程 ID(Process ID,PID)号码被用来标记各个进程
- UID、GID 和 SELinux 语境决定对文件系统的存取和访问权限
- 通常从执行进程的用户来继承
- 存在生命周期
进程创建:
- init:第一个进程,从 Centos7 以后为 systemd
- 进程:都由其父进程创建,fork(),父子关系,Cow:Copy On Write
进程,线程和协程
1.1.1 进程
进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标志定义
进程的组成:
进程一般由程序、数据集合和进程控制块三部分组成。
程序用于描述进程要完成的功能,是控制进程执行的指令集:
数据集合是程序在执行时所需要的数据和工作区:
程序控制块(Program Control Block,简称 PCB),包含进程的描述信息和控制信息,是进程存在的唯一标志。
进程具有的特征:
动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的:
并发性:任何进程都可以同其他进程一起并发执行
独立性:进程是系统进行资源分配和调度的一个独立单位:
结构性:进程是由程序、数据和进程控制块三部分组成。
1.1.2 线程
在早期的操作系统中并没有线程的概念,进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。后来,随着计算机的发展,对 CPU 的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程。
线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程 ID、当前指令指针(PC)、寄存器和堆栈组成。而进程是由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。
进程与线程的区别
线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位:
一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线:
进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某个进程内的线程在其他进程不可见:
调度和切换:线程上下文切换比进程上下文切换要快的多
1.1.3 协程
协程,英文 Coroutines,是一种基于线程之上,但又比线程更加轻量级的存在,这种由程序员自己写程序来管理的轻量级线程叫做(用户空间线程),具有对内核来说不可见的特性。
因为是自主开辟的异步任务,所以很多人也更喜欢叫它们纤程(Fiber),或者绿色线程(GreenThread)。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。
协程的目的:
在传统的 J2EE 系统中都是基于每个请求占用一个线程去完成完整的业务逻辑(包扣事务)。所以系统的吞吐能力取决于每个线程的操作耗时。如果遇到很耗时的 I/O 行为,则整个系统的吞吐立刻下降,因为这个时候线程一直处于阻塞状态,如果线程很多的时候,会存在很多线程处于空闲状态(等待该线程执行完才能执行),造成了资源应用不彻底
最常见的例子就是 JDBC(他是同步阻塞的),这也是为什么很多人都说数据库是瓶颈的原因。这里的耗时其实是让 CPU 一直在等待 I/O 返回,说白了线程根本没有利用 CPU 去做运算,而是处于空转状态。而另外过多的线程,也会带来更多的 ContextSwitch 开销。
对于上述问题,现阶段行业里的比较流行的解决方案之一就是单线程加上异步回调。其代表派是 node.js 以及 java 里的新秀 vert.x。
而协程的目的就是当出现长时间的 I/O 操作时,通过让出目前的协程调度,执行下一个任务的方式,来消除 ContextSwitch 上的开销。
协程的特点
线程的切换由操作系统负责调度,协程由用户自己进行调度,因此减少了上下文切换,提高了效率。
线程的默认 Stack 大小是 1M,而协程更轻量,接近 1K。因此可以在相同的内存中开启更多的协程
由于在同一个线程上,因此可以避免竞争关系而使用锁
适用于被阻塞的,且需要大量并发的场景。但不适用于大量计算的多线程,遇到此种情况,更好实用线程去解决
协程的原理:
当出现 IO 阻塞的时候,由协程的调度器进行调度,通过将数据流立刻 yield 掉(主动让出),并且记录当前栈上的数据,阻塞完后立刻在通过线程恢复栈,并把阻塞的结果放到这个线程上去跑,这样看上去好像跟写同步代码没有任何差别,这整个流程可以称为 coroutine,而跑在由 coroutine 负责调度的线程称为 Fiber。比如 Golang 里的 go 关键字其实就是负责开启一个 Fiber,让 func 逻辑跑在上面
由于协程的暂停完全由程序控制,发生在用户态上:而线程的阻塞状态是由操作系统内核来进行切换,发生在内核态上。因此,协程的开销远远小于线程的开销,也就没有了 ContextSwitch 上的开销。
1.1.4 协程和线程的比较
1.1.5 查看进程中的线程
[17:56:04 root@centos8 ~]#grep -i threads /proc/945/status
Threads: 6
1.2 进程结构
内核把进程存放在叫做任务队列(task list)的双向循环链表中
链表中的每一项都是类型为 task_struct,称为进程控制块(Processing Control Block),PCB 中包含一个具体进程的所有信息
进程控制块 PCB 包含信息:
- 进程 id、用户 id 和组 id
- 程序计数器
- 进程的状态(由就绪、运行、阻塞)
- 进程切换时需要保存和恢复的 CPU 寄存器的值
- 描述虚拟地址空间的信息
- 描述控制终端的信息
- 当前工作目录
- 文件描述符表,包含很多指向 file 结构体的指针
- 进程可以使用的资源上限(ulimit -a 命令可以查看)
- 输入输出状态:配置进程使用 I/O 设备
1.3 进程相关概念
Page Frame:页框,用存储页面数据,存储 Page 4K
[18:46:50 root@centos8 ~]#getconf -a | grep -i size
PAGESIZE 4096
PAGE_SIZE 4096
SSIZE_MAX 32767
_POSIX_SSIZE_MAX 32767
_POSIX_THREAD_ATTR_STACKSIZE 200809
FILESIZEBITS 64
POSIX_ALLOC_SIZE_MIN 4096
POSIX_REC_INCR_XFER_SIZE
POSIX_REC_MAX_XFER_SIZE
POSIX_REC_MIN_XFER_SIZE 4096
LEVEL1_ICACHE_SIZE 32768
LEVEL1_ICACHE_LINESIZE 64
LEVEL1_DCACHE_SIZE 32768
LEVEL1_DCACHE_LINESIZE 64
LEVEL2_CACHE_SIZE 262144
LEVEL2_CACHE_LINESIZE 64
LEVEL3_CACHE_SIZE 6291456
LEVEL3_CACHE_LINESIZE 64
LEVEL4_CACHE_SIZE 0
LEVEL4_CACHE_LINESIZE 0
1.3.1 物理地址空间和虚拟地址空间
MMU:Menmory Management Unit 负责虚拟地址转换为物理地址
程序在访问一个内存地址指向的内存时,CPU 不是直接把这个地址传送到内存总线上,而是被传送到 MMU,然后把这个内存地址映射到实际的物理内存地址上,然后通过总线再去访问内存,程序操作的地址称为虚拟内存地址
TLB:Translation Lookaside Buffer 翻译后备缓冲区,用于保存虚拟地址和物理地址映射关系的缓存
1.3.2 用户和内核空间
1.3.3 C 代码和内存布局之间的对应关系
每个进程都包扣 5 种不同的数据段
- 代码段:用来存放可执行文件的操作指令,也就是说是它是可执行程序在内存中的镜像。代码需要防止在运行时被非法修改,所以只允许读取操作,而不允许写入(修改)操作——它是不可写的
- 数据段:用来存放可执行文件中已初始化全局变量,换句话说就是存放程序静态分配的变量和全局变量
- BSS 段:Block Started by Symbol 的缩写,意为“以符号开始的块,BSS 段包含了程序中未初始化的全局变量,在内存中 bss 段全部置零
- 堆(heap):存放数组和对象,堆是用于存放进程运行中被动态分配的内存段,他的大小并不固定,可动态扩张或缩减。当进程调用 malloc 等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用 free 等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
- 栈(stack):栈是用户存放程序临时创建的局部变量,也就是说我们函数括弧”{}“中定义的变量(但不包括 static 声明的变量,static 意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放会栈中。由于栈的后进先出特点,所以栈特别方便用来保存/恢复调用现场。可以把栈堆看成一个寄存器、交换临时数据的内存区
喝多了吐就是栈,吃多了拉就是队列:栈先进后出,队列先进先出
1.3.4 进程使用内存问题
1.3.4.1 内存泄露:Memory Leak
指程序中用 malloc 或 new 申请了一块内存,但是没有用 free 或 delete 将内存释放,导致这块内存一直处于占用状态
1.3.4.2 内存溢出:Memory Overflow
指程序申请了 10M 的空间,但是在这个空间写入 10M 以上字节的数据,就是溢出。
1.3.4.3 内存不足:OOM
OOM 即 Out Of Memory,“内存用完了”,在情况在 java 程序中比较常见。系统会选一个进程将之杀死, 在日志 messages 中看到类似下面的提示
Jul 10 10:20:30 kernel: Out of memory: Kill process 9527 (java) score 88 or sacrifice child
当 JVM 因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个 error,因为这个问题已经严重到不足以被应用处理)。
原因:
- 给应用分配内存太少:比如虚拟机本身可使用的内存(一般通过启动时的 VM 参数指定)太少。
- 应用用的太多,并且用完没释放,浪费了。此时就会造成内存泄露或者内存溢出。
使用的解决办法:
1,限制 java 进程的 max heap,并且降低 java 程序的 worker 数量,从而降低内存使用 2,给系统增加 swap 空间
设置内核参数(不推荐),不允许内存申请过量:
echo 2 > /proc/sys/vm/overcommit_memory
echo 80 > /proc/sys/vm/overcommit_ratio
echo 2 > /proc/sys/vm/panic_on_oom
说明:
Linux 默认是允许 memory overcommit 的,只要你来申请内存我就给你,寄希望于进程实际上用不到那么多内存,但万一用到那么多了呢?Linux 设计了一个 OOM killer 机制挑选一个进程出来杀死,以腾出部分内存,如果还不够就继续。也可通过设置内核参数 vm.panic_on_oom 使得发生 OOM 时自动重启系统。这都是有风险的机制,重启有可能造成业务中断,杀死进程也有可能导致业务中断。所以
Linux 2.6 之后允许通过内核参数 vm.overcommit_memory 禁止 memory overcommit。
vm.panic_on_oom 决定系统出现 oom 的时候,要做的操作。接受的三种取值如下:
0 - 默认值,当出现oom的时候,触发oom killer
1 - 程序在有cpuset、memory policy、memcg的约束情况下的OOM,可以考虑不panic,而是启动OOM killer。其它情况触发 kernel panic,即系统直接重启
2 - 当出现oom,直接触发kernel panic,即系统直接重启
vm.overcommit_memory 接受三种取值:
0 – Heuristic overcommit handling. 这是缺省值,它允许overcommit,但过于明目张胆的overcommit会被拒绝,比如malloc一次性申请的内存大小就超过了系统总内存。Heuristic的意思是“试 探式的”,内核利用某种算法猜测你的内存申请是否合理,它认为不合理就会拒绝overcommit。
1 – Always overcommit. 允许overcommit,对内存申请来者不拒。内核执行无内存过量使用处理。使用这个设置会增大内存超载的可能性,但也可以增强大量使用内存任务的性能。
2 – Don’t overcommit. 禁止overcommit。 内存拒绝等于或者大于总可用 swap 大小以及overcommit_ratio 指定的物理 RAM 比例的内存请求。如果希望减小内存过度使用的风险,这个设置就是最好的。
Heuristic overcommit 算法:
单次申请的内存大小不能超过以下值,否则本次申请就会失败。
free memory + free swap + pagecache的大小 + SLAB
vm.overcommit_memory=2 禁止 overcommit,那么怎样才算是 overcommit 呢?
kernel 设有一个阈值,申请的内存总数超过这个阈值就算 overcommit,在/proc/meminfo 中可以看到 这个阈值的大小:
[18:46:58 root@centos8 ~]#grep -i commit /proc/meminfo
CommitLimit: 2584584 kB
Committed_AS: 297536 kB
CommitLimit 就是 overcommit 的阈值,申请的内存总数超过 CommitLimit 的话就算是 overcommit。此值通过内核参数 vm.overcommit_ratio 或 vm.overcommit_kbytes 间接设置的,公式如下:
CommitLimit = (Physical RAM * vm.overcommit_ratio / 100) + Swap
vm.overcommit_ratio 是内核参数,缺省值是 50,表示物理内存的 50%。如果你不想使用比率,也可以直接指定内存的字节数大小,通过另一个内核参数 vm.overcommit_kbytes 即可;
如果使用了 huge pages,那么需要从物理内存中减去,公式变成:
CommitLimit = ([total RAM] – [total huge TLB RAM]) * vm.overcommit_ratio / 100 + swap
/proc/meminfo 中的 Committed_AS 表示所有进程已经申请的内存总大小,(注意是已经申请的,不是已经分配的),如果 Committed_AS 超过 CommitLimit 就表示发生了 overcommit,超出越多表示 overcommit 越严重。Committed_AS 的含义换一种说法就是,如果要绝对保证不发生 OOM (out of memory) 需要多少物理内存。
范例:
[root@centos8 ~]#cat /proc/sys/vm/panic_on_oom 0
[root@centos8 ~]#cat /proc/sys/vm/overcommit_memory 0
[root@centos8 ~]#cat /proc/sys/vm/overcommit_ratio 50
[root@centos8 ~]#grep -i commit /proc/meminfo
CommitLimit: 3021876 kB
Committed_AS: 340468 kB
1.4 进程状态
进程的基本状态
- 创建状态:进程在创建时需要申请一个空白 PCB(process control block 进程控制块),向其中填写 控制和管理进程的信息,完成资源分配。如果创建工作无法完成,比如资源无法满足,就无法被调 度运行,把此时进程所处状态称为创建状态
- 就绪状态:进程已准备好,已分配到所需资源,只要分配到 CPU 就能够立即运行执行状态:进程处于就绪状态被调度后,进程进入执行状态
- 阻塞状态:正在执行的进程由于某些事件(I/O 请求,申请缓存区失败)而暂时无法运行,进程受 到阻塞。在满足请求时进入就绪状态等待系统调用
- 终止状态:进程结束,或出现错误,或被系统终止,进入终止状态。无法再执行
状态之间转换六种情况
- 运行——> 就绪:1,主要是进程占用 CPU 的时间过长,而系统分配给该进程占用 CPU 的时间是有限的; 2,在采用抢先式优先级调度算法的系统中,当有更高优先级的进程要运行时,该进程就被迫让出 CPU, 该进程便由执行状态转变为就绪状态
- 就绪——> 运行:运行的进程的时间片用完,调度就转到就绪队列中选择合适的进程分配 CPU
- 运行——> 阻塞:正在执行的进程因发生某等待事件而无法执行,则进程由执行状态变为阻塞状态,如 发生了 I/O 请求
- 阻塞——> 就绪:进程所等待的事件已经发生,就进入就绪队列
以下两种状态是不可能发生的:
- 阻塞——> 运行:即使给阻塞进程分配 CPU,也无法执行,操作系统在进行调度时不会从阻塞队列进行挑选,而是从就绪队列中选取
- 就绪——> 阻塞:就绪态根本就没有执行,谈不上进入阻塞态
进程更多的状态:
- 运行态:running
- 就绪态:ready
- 睡眠态:分为两种,可中断:interruptable,不可中断:uninterruptable
- 停止态:stopped,暂停于内存,但不会被调度,除非手动启动
- 僵死态:zombie,僵尸态,结束进程,父进程结束前,子进程不关闭,杀死父进程可以关闭僵死态 的子进程
范例:僵尸态
[root@centos8 ~]#bash [root@centos8 ~]#echo $BASHPID 1809
[root@centos8 ~]#echo $PPID 1436
#将父进程设为停止态
[root@centos8 ~]#kill -19 1436
#杀死子进程,使其进入僵尸态
[root@centos8 ~]#kill -9 1809
[root@centos8 ~]#ps aux #可以看到上面图示的结果,STAT为Z,表示为僵尸态
#方法1:恢复父进程
[root@centos8 ~]#kill -18 1436 #方法2:杀死父进程
[root@centos8 ~]#kill -9 1436
#再次观察,可以僵尸态的进程不存在了
[root@centos8 ~]#ps aux
1.5 LRU 算法
LRU:Least Recently Used 近期最少使用算法(喜新厌旧),释放内存
范例:
假设序列为 4 3 4 2 3 1 4 2, 物理块有 3 个,则
第 1 轮 4 调入内存 4
第 2 轮 3 调入内存 3 4
第 3 轮 4 调入内存 4 3
第 4 轮 2 调入内存 2 4 3
第 5 轮 3 调入内存 3 2 4
第 6 轮 1 调入内存 1 3 2
第 7 轮 4 调入内存 4 1 3
第 8 轮 2 调入内存 2 4 1
1.6 IPC 进程间通信
IPC: Inter Process Communication
- 同一主机:
pipe 管道,单向传输
socket 套接字文件
Memory-maped file 文件映射,将文件中的一段数据映射到物理内存,多个进程共享这片内存
shm shared memory 共享内存
signal 信号
Lock 对资源上锁,如果资源已被某进程锁住,则其它进程想修改甚至读取这些资源,都将被 阻塞,直到锁被打开
semaphore 信号量,一种计数器
- 不同主机:socket=IP 和端口号
RPC remote procedure call
MQ 消息队列,生产者和消费者,如:Kafka,RabbitMQ,ActiveMQ
范例:利用管道文件实现进 IPC
[19:28:52 root@centos8 ~]#mkfifo /root/test.fifo
[19:33:27 root@centos8 ~]#ll /root/test.fifo
prw-r--r-- 1 root root 0 Jan 2 19:33 /root/test.fifo
[19:33:35 root@centos8 ~]#cat > /root/test.fifo
zhangzhuo
#在另一个终端可以从文件中读取数据
[19:34:12 root@centos8 ~]#cat /root/test.fifo
zhangzhuo
范例:查找 socket 文件
[19:36:17 root@centos8 ~]#find / -type s -ls
1.7 进程优先级
CentOS 优先级
进程优先级:
系统优先级:0-139, 数字越小,优先级越高,各有140个运行队列和过期队列
实时优先级: 99-0 值最大优先级最高
nice值:-20到19,对应系统优先级100-139或
Big O:时间(空间)复杂度,用时(空间)和规模的关系
O(1), O(logn), O(n)线性, O(n^2)抛物线, O(2^n)
1.8 进程分类
操作系统分类:
- 协作式多任务:早期 windows 系统使用,即一个任务得到了 CPU 时间,除非它自己放弃使用 CPU ,否则将完全霸占 CPU ,所以任务之间需要协作——使用一段时间的 CPU ,主动放弃使用
- 抢占式多任务:Linux 内核,CPU 的总控制权在操作系统手中,操作系统会轮流询问每一个任务是否需要使用 CPU ,需要使用的话就让它用,不过在一定时间后,操作系统会剥夺当前任务的 CPU 使用权,把它排在询问队列的最后,再去询问下一个任务
进程类型:
- 守护进程: daemon,在系统引导过程中启动的进程,和终端无关进程
- 前台进程:跟终端相关,通过终端启动的进程
注意:两者可相互转化
按进程资源使用的分类:
- CPU-Bound:CPU 密集型,非交互
- IO-Bound:IO 密集型,交互
1.9 IO 调度算法
- NOOP
- NOOP 算法的全写为 No Operation。该算法实现了最简单的 FIFO 队列,所有 IO 请求大致按照先来 后到的顺序进行操作。之所以说“大致”,原因是 NOOP 在 FIFO 的基础上还做了相邻 IO 请求的合并, 并不是完完全全按照先进先出的规则满足 IO 请求。NOOP 假定 I/O 请求由驱动程序或者设备做了优 化或者重排了顺序(就像一个智能控制器完成的工作那样)。在有些 SAN 环境下,这个选择可能是最好选择。Noop 对于 IO 不那么操心,对所有的 IO 请求都用 FIFO 队列形式处理,默认认为 IO 不会存在性能问题。这也使得 CPU 也不用那么操心。当然,对于复杂一点的应用类型,使用这个调度器,用户自己就会非常操心。
- CFQ
- CFQ 算法的全写为 Completely Fair Queuing。该算法的特点是按照 IO 请求的地址进行排序,而不是按照先来后到的顺序来进行响应。 在传统的 SAS 盘上,磁盘寻道花去了绝大多数的 IO 响应时间。CFQ 的出发点是对 IO 地址进行排序,以尽量少的磁盘旋转次数来满足尽可能多的 IO 请求。在 CFQ 算法下,SAS 盘的吞吐量大大提高了。但是相比于 NOOP 的缺点是,先来的 IO 请求并不一定能 被满足,可能会出现饿死的情况。
- Completely Fair Queuing (cfq, 完全公平队列) 在 2.6.18 取代了 Anticipatory scheduler 成为 Linux Kernel 默认的 IO scheduler 。cfq 对每个进程维护一个 IO 队列,各个进程发来的 IO 请求会被 cfq 以轮循方式处理。也就是对每一个 IO 请求都是公平的。这使得 cfq 很适合离散读的应用(eg: OLTP DB)
- Deadline scheduler
- DEADLINE 在 CFQ 的基础上,解决了 IO 请求饿死的极端情况。deadline 算法保证对于既定的 IO 请求以最小的延迟时间,除了 CFQ 本身具有的 IO 排序队列之外,DEADLINE 额外分别为读 IO 和写 IO 提 供了 FIFO 队列。读 FIFO 队列的最大等待时间为 500ms,写 FIFO 队列的最大等待时间为 5s。FIFO 队 列内的 IO 请求优先级要比 CFQ 队列中的高,,而读 FIFO 队列的优先级又比写 FIFO 队列的优先级高。优先级可以表示如下:
- FIFO(Read) > FIFO(Write) > CFQ
- Anticipatory scheduler
- CFQ 和 DEADLINE 考虑的焦点在于满足零散 IO 请求上。对于连续的 IO 请求,比如顺序读,并没有做 优化。为了满足随机 IO 和顺序 IO 混合的场景,Linux 还支持 ANTICIPATORY 调度算法。
- ANTICIPATORY 的在 DEADLINE 的基础上,为每个读 IO 都设置了 6ms 的等待时间窗口。如果在这 6ms 内 OS 收到了相邻位置的读 IO 请求,就可以立即满足 Anticipatory scheduler(as) 曾经一度是 Linux 2.6 Kernel 的 IO scheduler 。Anticipatory 的中文含义是”预料的, 预想的”, 这个词的确揭示了这个算法的特点,简单的说,有个 IO 发生的时候,如果又有进程请求 IO 操作,则将产生一个默认的 6 毫秒猜测时间,猜测下一个 进程请求 IO 是要干什么的。这对于随即读取会造成比较大的延时,对数据库应用很糟糕,而对于 Web Server 等则会表现的不错。这个算法也可以简单理解为面向低速磁盘的,因为那个”猜测”实际上的目的是为了减少磁头移动时间。
范例:查看 IO 调度算法
[19:36:37 root@centos8 ~]#cat /sys/block/sda/queue/scheduler
[mq-deadline] kyber bfq none
#每个Linux发行版可能都不一样
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于