Linux 监控之 IO

落爺英雄遲暮 提交于 2019-11-30 21:51:15

简单介绍下 Linux 中与 IO 相关的内容。

简介

可以通过如下命令查看与 IO 相关的系统信息。

# tune2fs -l /dev/sda7                       ← 读取superblock信息  # blockdev --getbsz /dev/sda7                ← 获取block大小  # tune2fs -l /dev/sda7 | grep "Block size"   ← 同上  # dumpe2fs /dev/sda7 | grep "Block size"     ← 同上  # stat /boot/ | grep "IO Block"              ← 同上  # fdisk -l                                   ← 硬盘的扇区大小(Sector Size)

在 WiKi 中的定义:A “block”, a contiguous number of bytes, is the minimum unit of memory that is read from and written to a disk by a disk driver。

块是文件系统的抽象,而非磁盘的属性,一般是 Sector Size 的倍数;扇区大小则是磁盘的物理属性,它是磁盘设备寻址的最小单元。另外,内核中要求 Block_Size = Sector_Size * (2的n次方),且 Block_Size <= 内存的 Page_Size (页大小)。

磁盘使用空间

实际上是通过 statvfs() 方法查询磁盘数据,可以通过如下命令查看。

$ python -c 'import os; os.statvfs("/")'

其空间占用大致如下。

   +--------------------------+----------------+-------------------------------------------------------+     |                          |                |                                                       |     +--------------------------+----------------+-------------------------------------------------------+     |<-- f_bavail(non-root) -->|<-- reserved -->|<------------- f_bused=f_blocks-f_bfree -------------->|     |<------------- f_bfree(root) ------------->|                                                       |     |<----------------------------------------- f_blocks ---------------------------------------------->|

前者是非 root 用户已经使用的占非 root 用户可用空间百分数;后者是保留给 root 用户以及已经使用磁盘占整个磁盘空间百分数。对于 extN 类的文件系统一般会保留 1%~5% 的磁盘空间给 root 使用,当 reserved 占比较大时会导致两者的计算差较大。

磁盘类型

主要是要获取当前系统使用的什么类型的磁盘 (SCSI、IDE、SSD等),甚至是制造商、机器型号、序列号等信息。

$ dmesg | grep scsi

监控指标

简单列举磁盘监控时常见的指标。

IOPS 每秒IO数    对磁盘来说,一次磁盘的连续读或写称为一次磁盘 IO,当传输小块不连续数据时,该指标有重要参考意义。  Throughput 吞吐量    硬盘传输数据流的速度,单位一般为 MB/s,在传输大块不连续数据的数据,该指标有重要参考作用。  IO平均大小    实际上就是吞吐量除以 IOPS,用于判断磁盘使用模式,一般大于 32K 为顺序读取为主,否则随机读取为主。  Utilization 磁盘活动时间百分比    磁盘处于活动状态 (数据传输、寻道等) 的时间百分比,也即磁盘利用率,一般该值越高对应的磁盘资源争用越高。  Service Time 服务时间    磁盘读写操作执行的时间,对于机械磁盘包括了寻道、旋转、数据传输等,与磁盘性能相关性较高,另外,也受 CPU、内存影响。  Queue Length 等待队列长度    待处理的 IO 请求的数目,注意,如果该磁盘为磁盘阵列虚拟的逻辑驱动器,需要除以实际磁盘数,以获取单盘的 IO 队列。  Wait Time 等待时间    在队列中排队的时间。

iostat 系统级

除了可以通过该命令查看磁盘信息之外,还可以用来查看 CPU 信息,分别通过 -d 和 -c 参数控制;可直接通过 iostat -xdm 1 命令显示磁盘队列的长度等信息。

Device: rrqm/s wrqm/s   r/s   w/s  rMB/s  wMB/s avgrq-sz avgqu-sz await r_await w_await svctm %util  sda       0.02   1.00  0.99  1.84   0.03   0.04    46.98     0.01  2.44    0.47    3.49  0.25  0.07

其中参数如下:

rrqm/s wrqm/s    读写请求每秒合并后发送给磁盘的请求。    r/s w/s    应用发送给系统的请求数目。    argrq-sz    提交给驱动层IO请求的平均大小(sectors),一般不小于4K,不大于max(readahead_kb, max_sectors_kb);    可用于判断当前的 IO 模式,越大代表顺序,越小代表随机;计算公式如下:    argrq-sz = (rsec + wsec) / (r + w)    argqu-sz Average Queue Size    在驱动层的队列排队的平均长度。    await Average Wait    平均的等待时间,包括了在队列中的等待时间,以及磁盘的处理时间。    svctm(ms) Service Time    请求发送给IO设备后的响应时间,也就是一次磁盘IO请求的服务时间,不过该指标官网说不准确,要取消。    对于单块SATA盘,完全随机读时,基本在7ms左右,既寻道+旋转延迟时间。    %util    一秒内IO操作所占的比例,计算公式是(r/s+w/s)*(svctm/1000),例如采集时间为 1s 其中有 0.8s 在处    理 IO 请求,那么 util 为 80% ;对于一块磁盘,如果没有并发IO的概念,那么这个公式是正确的,但    是对于RAID磁盘组或者SSD来说,这个计算公式就有问题了,就算这个值超过100%,也不代表存储有瓶颈,    容易产生误导。

iostat 统计的是通用块层经过合并 (rrqm/s, wrqm/s) 后,直接向设备提交的 IO 数据,可以反映系统整体的 IO 状况,但是距离应用层比较远,由于系统预读、Page Cache、IO调度算法等因素,很难跟代码中的 write()、read() 对应。

简言之,这是系统级,没办法精确到进程,比如只能告诉你现在磁盘很忙,但是没办法告诉你是那个进程在忙,在忙什么?

/proc/diskstats

该命令会读取 /proc/diskstats 文件,各个指标详细的含义可以参考内核文档 iostats.txt,其中各个段的含义如下。

filed1  rd_ios    成功完成读的总次数;  filed2  rd_merges    合并写完成次数,通过合并提高效率,例如两次4K合并为8K,这样只有一次IO操作;合并操作是由IO Scheduler(也叫 Elevator)负责。  filed3  rd_sectors    成功读过的扇区总次数;  filed4  rd_ticks    所有读操作所花费的毫秒数,每个读从__make_request()开始计时,到end_that_request_last()为止,包括了在队列中等待的时间;  filed5  wr_ios    成功完成写的总次数;  filed6  wr_merges    合并写的次数;  filed7  wr_sectors    成功写过的扇区总次数;  filed8  wr_ticks    所有写操作所花费的毫秒数;  filed9  in_flight    现在正在进行的IO数目,在IO请求进入队列时该值加1,在IO结束时该值减1,注意是在进出队列时,而非交给磁盘时;  filed10 io_ticks    输入/输出操作花费的毫秒数;  filed11 time_in_queue    是一个权重值,当有上面的IO操作时,这个值就增加。

需要注意 io_ticks 与 rd/wr_ticks 的区别,后者是把每一个 IO 所消耗的时间累加在一起,因为硬盘设备通常可以并行处理多个 IO,所以统计值往往会偏大;而前者表示该设备有 IO 请求在处理的时间,也就是非空闲,不考虑 IO 有多少,只考虑现在有没有 IO 操作。在实际计算时,会在字段 in_flight 不为零的时候 io_ticks 保持计时,为 0 时停止计时。

另外,io_ticks 在统计时不考虑当前有几个 IO,而 time_in_queue 是用当前的 IO 数量 (in_flight) 乘以时间,统计时间包括了在队列中的时间以及磁盘处理 IO 的时间。

重要指标

简单介绍下常见的指标,包括了经常误解的指标。

util

这里重点说一下 iostat 中 util 的含义,该参数可以理解为磁盘在处理 IO 请求的总时间,如果是 100% 则表明磁盘一直在处理 IO 请求,这也就意味着 IO 在满负载运行。

对于一块磁盘,如果没有并发 IO 的概念,所以这个公式是正确的,但是现在的磁盘或者对于RAID磁盘组以及SSD来说,这个计算公式就有问题了,就算这个值超过100%,也不代表存储有瓶颈,容易产生误导。

举个简化的例子:某硬盘处理单个 IO 需要 0.1 秒,也就是有能力达到 10 IOPS,那么当 10 个 IO 请求依次顺序提交的时候,需要 1 秒才能全部完成,在 1 秒的采样周期里 %util 达到 100%;而如果 10 个 IO 请求一次性提交的话,0.1 秒就全部完成,在 1 秒的采样周期里 %util 只有 10%。

可见,即使 %util 高达 100%,硬盘也仍然有可能还有余力处理更多的 IO 请求,即没有达到饱和状态。不过遗憾的是现在 iostat 没有提供类似的指标。

在 CentOS 中使用的是 github sysstat,如下是其计算方法。

rw_io_stat_loop()  循环读取   |-read_diskstats_stat()            从/proc/diskstats读取状态   |-write_stats()                    输出采集的监控指标     |-write_ext_stat()       |-compute_ext_disk_stats()     计算ext选项,如util       |-write_plain_ext_stat()

关于该参数的代码详细介绍如下。

#define S_VALUE(m,n,p)  (((double) ((n) - (m))) / (p) * HZ)    void read_diskstats_stat(int curr)  {   struct io_stats sdev;   ... ...   if ((fp = fopen(DISKSTATS, "r")) == NULL)    return;     while (fgets(line, sizeof(line), fp) != NULL) {    /* major minor name rio rmerge rsect ruse wio wmerge wsect wuse running use aveq */    i = sscanf(line, "%u %u %s %lu %lu %lu %lu %lu %lu %lu %u %u %u %u",        &major, &minor, dev_name,        &rd_ios, &rd_merges_or_rd_sec, &rd_sec_or_wr_ios, &rd_ticks_or_wr_sec,        &wr_ios, &wr_merges, &wr_sec, &wr_ticks, &ios_pgr, &tot_ticks, &rq_ticks);      if (i == 14) {     /* Device or partition */     if (!dlist_idx && !DISPLAY_PARTITIONS(flags) &&         !is_device(dev_name, ACCEPT_VIRTUAL_DEVICES))      continue;     sdev.rd_ios     = rd_ios;     sdev.rd_merges  = rd_merges_or_rd_sec;     sdev.rd_sectors = rd_sec_or_wr_ios;     sdev.rd_ticks   = (unsigned int) rd_ticks_or_wr_sec;     sdev.wr_ios     = wr_ios;     sdev.wr_merges  = wr_merges;     sdev.wr_sectors = wr_sec;     sdev.wr_ticks   = wr_ticks;     sdev.ios_pgr    = ios_pgr;     sdev.tot_ticks  = tot_ticks;     sdev.rq_ticks   = rq_ticks;          }       ... ...    save_stats(dev_name, curr, &sdev, iodev_nr, st_hdr_iodev);   }   fclose(fp);  }    void write_json_ext_stat(int tab, unsigned long long itv, int fctr,        struct io_hdr_stats *shi, struct io_stats *ioi,        struct io_stats *ioj, char *devname, struct ext_disk_stats *xds,        double r_await, double w_await)  {   xprintf0(tab,     "{\"disk_device\": \"%s\", \"rrqm\": %.2f, \"wrqm\": %.2f, "     "\"r\": %.2f, \"w\": %.2f, \"rkB\": %.2f, \"wkB\": %.2f, "     "\"avgrq-sz\": %.2f, \"avgqu-sz\": %.2f, "     "\"await\": %.2f, \"r_await\": %.2f, \"w_await\": %.2f, "     "\"svctm\": %.2f, \"util\": %.2f}",     devname,     S_VALUE(ioj->rd_merges, ioi->rd_merges, itv),     S_VALUE(ioj->wr_merges, ioi->wr_merges, itv),     S_VALUE(ioj->rd_ios, ioi->rd_ios, itv),     S_VALUE(ioj->wr_ios, ioi->wr_ios, itv),     S_VALUE(ioj->rd_sectors, ioi->rd_sectors, itv) / fctr,     S_VALUE(ioj->wr_sectors, ioi->wr_sectors, itv) / fctr,     xds->arqsz,     S_VALUE(ioj->rq_ticks, ioi->rq_ticks, itv) / 1000.0,     xds->await,     r_await,     w_await,     xds->svctm,     shi->used ? xds->util / 10.0 / (double) shi->used        : xds->util / 10.0); /* shi->used should never be zero here */  }      void compute_ext_disk_stats(struct stats_disk *sdc, struct stats_disk *sdp,         unsigned long long itv, struct ext_disk_stats *xds)  {   double tput    = ((double) (sdc->nr_ios - sdp->nr_ios)) * HZ / itv;     xds->util  = S_VALUE(sdp->tot_ticks, sdc->tot_ticks, itv);   xds->svctm = tput ? xds->util / tput : 0.0;   /*    * Kernel gives ticks already in milliseconds for all platforms    * => no need for further scaling.    */   xds->await = (sdc->nr_ios - sdp->nr_ios)  ?  ((sdc->rd_ticks - sdp->rd_ticks) + (sdc->wr_ticks - sdp->wr_ticks)) /    ((double) (sdc->nr_ios - sdp->nr_ios)) : 0.0;   xds->arqsz = (sdc->nr_ios - sdp->nr_ios)  ?  ((sdc->rd_sect - sdp->rd_sect) + (sdc->wr_sect - sdp->wr_sect)) /    ((double) (sdc->nr_ios - sdp->nr_ios)) : 0.0;  }

实际上就是 /proc/diskstats 中的 filed10 消耗时间占比。

await

在 Linux 中,每个 IO 的平均耗时用 await 表示,包括了磁盘处理时间以及队列排队时间,所以该指标不能完全表示设备的性能,包括 IO 调度器等,都会影响该参数值。一般来说,内核中的队列时间几乎可以忽略不计,而 SSD 不同产品从 0.01ms 到 1.00 ms 不等,对于机械磁盘可以参考 io 。

svctm

这个指标在 iostat 以及 sar 上都有注释 Warning! Do not trust this field any more. This field will be removed in a future sysstat version.,该指标包括了队列中排队时间以及磁盘处理时间。

实际上,在 UNIX 中通常通过 avserv 表示硬盘设备的性能,它是指 IO 请求从 SCSI 层发出到 IO 完成之后返回 SCSI 层所消耗的时间,不包括在 SCSI 队列中的等待时间,所以该指标体现了硬盘设备处理 IO 的速度,又被称为 disk service time,如果 avserv 很大,那么肯定是硬件出问题了。

iowait

从 top 中的解释来说,就是 CPU 在 time waiting for I/O completion 中消耗的时间,而实际上,如果需要等待 IO 完成,实际 CPU 不会一直等待该进程,而是切换到另外的进程继续执行。

所以在 Server Fault 中将该指标定义为如下的含义:

iowait is time that the processor/processors are waiting (i.e. is in an  idle state and does nothing), during which there in fact was outstanding  disk I/O requests.

那么对于多核,iowait 是只在一个 CPU 上,还是会消耗在所有 CPU ?如果有 4 个 CPUs,那么最大是 20% 还是 100% ?

可以通过 dd if=/dev/sda of=/dev/null bs=1MB 命令简单测试下,一般来说,为了提高 cache 的命中率,会一直使用同一个 CPU ,不过部分系统会将其均分到不同的 CPU 上做均衡。另外,也可以通过 taskset 1 dd if=/dev/sda of=/dev/null bs=1MB 命令将其绑定到单个 CPU 上。

按照二进制形式,从最低位到最高位代表物理 CPU 的 #0、#1、#2、…、#n 号核,例如:0x01 代表 CPU 的 0 号核,0x05 代表 CPU 的 0 号和 2 号核。

例如,将 9865 绑定到 #0、#1 上面,命令为 taskset -p 0x03 9865;将进程 9864 绑定到 #1、#2、#5~#11 号核上面,从 1 开始计数,命令为 taskset -cp 1,2,5-11 9865 。

可以看出,如果是 top <1> 显示各个 CPU 的指标,则是 100% 计算,而总的统计值则按照 25% 统计。

其它

常见问题处理。

问题1:如何获取真正的 serviice time(svctm)

?可以通过 fio 等压测工具,通过设置为同步 IO,仅设置一个线程,以及 io_depth 也设置为 1,压测出来的就是真正的 service time(svctm)。

问题2:怎样获得 IO 最大并行度,或者说如何获得真正的 util% 使用率?
最大并行度 = 压测满(r/s + w/s) * (真实svctm / 1000)

公式基本一样,只是将 svctm 换成了上次计算的值。

问题3:如何判断存在 IO 瓶颈了?

实际上在如上算出真实的最大并行度,可以直接参考 avgqu-sz 值,也就是队列中的值,一般来说超过两倍可能就会存在问题。例如一块机械盘,串行 IO (每次1个IO),那么 avgqu-sz 持续大于 2 既代表持续有两倍读写能力的 IO 请求在等待;或者当 RAIDs、SSD 等并行,这里假定并行度为 5.63,那么 avgqu-sz 持续大于10,才代表持续有两倍读写能力的 IO 请求在等待。

iotop pidstat iodump 进程级

一个 Python 脚本,可以查看官网 guichaz.free.fr/iotop,另一个通过 C 实现的监控可参考 IOPP

pidstat 用于统计进程的状态,包括 IO 状况,可以查看当前系统哪些进程在占用 IO 。

----- 只显示IO  # pidstat -d 1

上述两者均是统计的 /proc/pid/io 中的信息;另可参考 io/iotop.stp,这是 iotop 的复制版。

iodump 是一个统计每一个进程(线程)所消耗的磁盘 IO 工具,是一个 perl 脚本,其原理是打开有关 IO 的内核记录消息开关,而后读取消息然后分析输出。

# echo 1 >/proc/sys/vm/block_dump                        # 打开有关IO内核消息的开关  # while true; do sleep 1; dmesg -c ; done | perl iodump  # 然后分析

上述输出的单位为块 (block),每块的大小取决于创建文件系统时指定的块大小。

ioprofile 业务级

ioprofile 命令本质上等价于 lsof + strace,可以查看当前进程。

blktrace

blktrace 是块层 IO 路径监控和分析工具,作者 Jens Axboe 是内核 IO 模块的维护者,目前就职于 FusionIO,同时他还是著名 IO 评测工具 fio 的作者,使用它可以深入了解 IO 通路。

# yum install blktrace                    # 在CentOS中安装  $ make                                    # 解压源码后直接安装  $ man -l doc/blktrace.8                   # 查看帮助

其源码可以从 brick.kernel.dk 下载,详细使用参考 blktrace User Guide 。

原理

该工具包括了内核空间和用户空间两部分实现,内核空间里主要是给块层 IO 路径上的关键点添加 tracepoint,然后借助于 relayfs 系统特性将收集到的数据写到 buffer 去,再从用户空间去收集。

目前,内核空间部分的代码已经集成到主线代码里面去了,可以看看内核代码 block/blktrace.c 文件是不是存在,编译的时候把对应的这个 trace 选项选择上就可以了。

$ grep 'CONFIG_BLK_DEV_IO_TRACE' /boot/config-`uname -r`

大部分实现代码都在 blktrace.c,利用 tracepoint 的特性,注册了一些 trace 关键点,可以查看 Documentation/tracepoint.txt 文件;交互机制利用了 relayfs 特性,看看 Documentation/filesystems/relay.txt 。

此时捞取的信息还比较原始,可以通过用户空间的 blkparse、btt、seekwatcher 这样的工具来分析收集到的数据。

注意,使用之前要确保 debugfs 已经挂载,默认会挂载在 /sys/kernel/debug 。

使用

典型的使用如下,其中 /dev/sdaa、/dev/sdc 作为 LVM volume adb3/vol。

# blktrace -d /dev/sda -o - | blkparse -i - -o blkparse.out       # 简单用法,Ctrl-C退出  # btrace /dev/sda                                                 # 同上    # blktrace /dev/sdaa /dev/sdc &                                   # 离线处理。1. 后台运行采集  % mkfs -t ext3 /dev/adb3/vol                                      # 2. 做些IO操作  % kill -15 9713                                                   # 3. 停止采集  % blkparse sdaa sdc sdo > events                                  # 4. 解析后查看

在 blktrace 中,-d 表示监控哪个设备,-o - 表示将监控输出到标准输出;在 blkparse 中,-i - 表示从标准输入获取信息,-o 表示将解析的内容记录在 blkparse.out 。

如下是输出的详细信息。

其中 event 对应了事件表;后面一列代表了操作类型,包括了 R(read)、W(write)、B(barrier operation)、S(synchronous operation),其中 event 有如下类型:

事件说明源码(block目录下) SetPosition
A IO was remapped to a different device blk-core.c/trace_block_remap
B IO bounced bounce.c/trace_block_bio_bounce
C IO completion blk-core.c/trace_block_rq_complete
D IO issued to driver elevator.c/trace_block_rq_issue
F IO front merged with request on queue blk-core.c/trace_block_bio_frontmerge
G Get request blk-core.c/trace_block_getrq
I IO inserted onto request queue elevator.c/trace_block_rq_insert
M IO back merged with request on queue blk-core.c/trace_block_bio_backmerge
P Plug request blk-core.c/trace_block_plug
Q IO handled by request queue code blk-core.c/trace_block_bio_queue
S Sleep request blk-core.c/trace_block_sleeprq
T Unplug due to timeout blk-core.c/trace_block_unplug_timer
U Unplug request blk-core.c/trace_block_unplug_io
X Split bio.c/trace_block_split

详解

仍以如下简单命令为例。

$ blktrace -d /dev/sda -o sda                 # 输出 sda.blktrace.N 文件,N 为物理 CPU 个数。  $ ls /sys/kernel/debug/block/sda              # 查看debugfs中的文件  dropped  msg  trace0  trace1  trace2  trace3  $ blkparse -i sda.blktrace.0                  # 解析成可读内容  $ blkrawverify sda                            # 校验,其中sda为blktrace的-o选项

其中 blktrace 通过 ioctl() 执行 BLKTRACESETUP、BLKTRACESTART、BLKTRACESTOP、BLKTRACETEARDOWN 操作,此时会在 debugfs 目录的 block/DEV 目录下写入数据。

FIO

FIO 是个非常强大的 IO 性能测试工具,其作者 Jens Axboe 是 Linux 内核 IO 部分的 maintainer,可以毫不夸张的说,如果你把所有的 FIO 参数都搞明白了,基本上就把 Linux IO 协议栈的问题搞的差不多明白了。

一个 IO 压测工具,源码以及二进制文件可以参考 github-axboe,或者直接从 freecode.com 上下载。另外,该工具同时提供了一个图形界面 gfio 。

在 CentOS 中可以通过如下方式安装。

# yum --enablerepo=epel install fio

源码编译

可以直接从 github 上下载源码,然后通过如下方式进行编译。

----- 编译,注意依赖libaio  $ make    ----- 查看帮助  $ man -l fio.1    ----- 通过命令行指定参数,进行简单测试  $ fio --name=global --rw=randread --size=128m --name=job1 --name=job2    ----- 也可以通过配置文件进行测试  $ cat foobar.fio  [global]  rw=randread  size=128m  [job1]  [job2]  $ fio foobar.fio

可以通过命令行启动,不过此时参数较多,可以使用配置文件。

源码解析

其版本通过 FIO_VERSION 宏定义,并通过 fio_version_string 变量定义。

main()    |-parse_options()    |  |-parse_cmd_line()                    解析命令行,如-i显示所有的ioengines    |  |  |-add_job()                        file1: xxxxxx 打印job信息    |  |-log_info()                          fio-2.10.0    |-fio_backend()    |  |-create_disk_util_thread()           用于实时显示状态    |  |  |-setup_disk_util()    |  |  |-disk_thread_main()               通过pthread创建线程    |  |     |-print_thread_status()    |  |    |  |-run_threads()                       Starting N processes    |  |  |-setup_files()                    Laying out IO file(s)    |  |  |-pthread_create()                 如果配置使用线程,调用thread_main    |  |  |-fork()                           或者调用创建进程,同样为thread_main    |  |    |  |-show_run_stats()    |     |-show_thread_status_normal()      用于显示最终的状态    |        |-show_latencies()              显示lat信息    |        |-... ...                       CPU、IO depth

ioengines 通过 fio_libaio_register() 类似的函数初始化。

其它

ionice

获取或设置程序的 IO 调度与优先级。

ionice [-c class] [-n level] [-t] -p PID...  ionice [-c class] [-n level] [-t] COMMAND [ARG]    ----- 获取进程ID为89、91的IO优先级  $ ionice -p 89 91

参考

关于 FIO 可以查看源码中的 HOWTO,其它的压测工具可以参考 Benchmarking ,或者参考 本地文档,该网站还包括了很多有用文章。

Block IO Layer Tracing: blktrace 介绍 blktrace 命令的使用;关于内核的 trace 功能参考 Kernel Trace Systems 。

Linux Block IO: Introducing Multi-queue SSD Access on Multi-core Systems 。

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!