了解你自己的程序

前言

因為在做nodejs程序的性能分析的時候,了解到了Perf和FlameGraph這兩個神奇的工具,接著就知道了Brendan D. Gregg這個大神,跪著拜讀了他的博客和他寫的System Performance。從前寫程序和調優只知道從設計的思路去思考,讀完大神的文章,感覺真的給自己打開了一個全新的世界。

把自己的程序看做一個黑盒,它運行的時候到底占多少內存,多少CPU?這個問題看起來不難,那么它多少時間在等待I/O,多少時間在計算,如果CPU是多核,它是否很好的平衡了負載?它訪問文件系統頻率多少,每次的相應時間多少?它運行中的熱點是哪里,瓶頸是哪里?

再復雜點,它在運行時內存是否足夠,page cache和CPU cache命中率如何?有沒有導致系統發生swap等非常耗時的操作?現在如果程序運行不穩定,如何去觀察定位和調優?

目前為止我還在每天跪著閱讀大神的文章中,這篇筆記會按一定順序記錄我的很多理解。

Perf

Perf是一個神奇的工具,主要用于事件監測。
每當linux內核調用某一個函數時,可以視作一個事件,Perf可以記錄這些事件發生的時間和內核調用棧。

基本用法

perf command [options] [execute]

舉個例子:

perf stat -e sched:sched_switch -a sleep 5

統計5s之內,操作系統一共調用了多少個sched_switch。對linux熟悉的朋友應該知道這個表示進程切換。下面我的虛擬機里返回的結果

Performance counter stats for 'system wide':

             1,170      sched:sched_switch                                          

       5.001593514 seconds time elapsed

也就是5秒鐘之內發生了1170次進程切換。

這里解釋下這個命令,perf stat是統計事件次數,-e sched:sched_switch表示統計sched:sched_switch事件,然后本來應該只統計sleep 5這個process內部的事件的,加上-a表示統計整個系統內的事件,由于sleep 5要在5s后結束,所以這個命令的實際功能就是統計了系統5s內發生的sched:sched_switch事件個數。

再舉個栗子:

perf record -e block:block_rq_complete --filter 'nr_sector > 200'

block_rq_complete是塊設備請求完成的事件,該條命令會統計所有涉及到200扇區以上的設備I/O事件。
命令:

perf record -e page-faults -ag

則會統計所有缺頁中斷。

事件列表

調用perf list (注意root權限可以看到更多)可以查看當前支持的事件列表。

List of pre-defined events (to be used in -e):

  alignment-faults                                   [Software event]
  bpf-output                                         [Software event]
  context-switches OR cs                             [Software event]
  cpu-clock                                          [Software event]
  cpu-migrations OR migrations                       [Software event]
  dummy                                              [Software event]
  emulation-faults                                   [Software event]
  major-faults                                       [Software event]
  minor-faults                                       [Software event]
  page-faults OR faults                              [Software event]
  task-clock                                         [Software event]

  L1-dcache-load-misses                              [Hardware cache event]
  L1-dcache-loads                                    [Hardware cache event]
  L1-dcache-stores                                   [Hardware cache event]
  L1-icache-load-misses                              [Hardware cache event]
  branch-load-misses                                 [Hardware cache event]
  branch-loads                                       [Hardware cache event]
  dTLB-load-misses                                   [Hardware cache event]
  dTLB-loads                                         [Hardware cache event]
  dTLB-store-misses                                  [Hardware cache event]
  dTLB-stores                                        [Hardware cache event]
  iTLB-load-misses                                   [Hardware cache event]
  iTLB-loads                                         [Hardware cache event]

  cycles-ct OR cpu/cycles-ct/                        [Kernel PMU event]
  cycles-t OR cpu/cycles-t/                          [Kernel PMU event]
  el-abort OR cpu/el-abort/                          [Kernel PMU event]
  el-capacity OR cpu/el-capacity/                    [Kernel PMU event]

Perf可以監控的事件類型很多,甚至連L1 cache miss這種都可以。

Perf 命令

  • perf stat
    stat命令用于簡單統計次數

    • 統計PID進程的事件
      perf stat -p PID
    • 統計整個系統的事件
      perf stat -a sleep [seconds]
    • 統計command內的事件
      perf stat [command]
  • perf record & report/script
    stat命令只會統計事件發生次數,如果想查看更詳細的信息,比如事件發生時的堆棧,就需要用到record命令了。
    perf record把統計結果放到當前目錄內perf.data文件,用perf report/script命令可以解析展示統計結果。

    • 以固定頻率對程序進行抽樣,并且記錄堆棧
      perf record -F [freq] [command] [-p PID] -g -- sleep [sec]
      //-g 表示統計stack, sec表示統計時長
      例如
     perf record -F 99 -a -g -- sleep 2
     //統計兩秒內的默認事件 (cpu clock事件)
     perf script
    
     swapper     0 [000] 19822.660852:   10101010 cpu-clock: 
                 7fff810665d6 native_safe_halt ([kernel.kallsyms])
                 7fff8103ad7e default_idle ([kernel.kallsyms])
                 7fff8103b58f arch_cpu_idle ([kernel.kallsyms])
                 7fff810c5faa default_idle_call ([kernel.kallsyms])
                 7fff810c6311 cpu_startup_entry ([kernel.kallsyms])
                 7fff818239dc rest_init ([kernel.kallsyms])
                 7fff81f5d011 start_kernel ([kernel.kallsyms])
                 7fff81f5c339 x86_64_start_reservations ([kernel.kallsyms])
                 7fff81f5c485 x86_64_start_kernel ([kernel.kallsyms])
    
     swapper     0 [001] 19822.660853:   10101010 cpu-clock: 
                 7fff810665d6 native_safe_halt ([kernel.kallsyms])
                 7fff8103ad7e default_idle ([kernel.kallsyms])
                 7fff8103b58f arch_cpu_idle ([kernel.kallsyms])
                 7fff810c5faa default_idle_call ([kernel.kallsyms])
                 7fff810c6311 cpu_startup_entry ([kernel.kallsyms])
                 7fff810536e4 start_secondary ([kernel.kallsyms])
    
     swapper     0 [001] 19822.671003:   10101010 cpu-clock: 
                 7fff810665d6 native_safe_halt ([kernel.kallsyms])
                 7fff8103ad7e default_idle ([kernel.kallsyms])
                 7fff8103b58f arch_cpu_idle ([kernel.kallsyms])
                 7fff810c5faa default_idle_call ([kernel.kallsyms])
                 7fff810c6311 cpu_startup_entry ([kernel.kallsyms])
                 7fff810536e4 start_secondary ([kernel.kallsyms])
    
     swapper     0 [000] 19822.671003:   10101010 cpu-clock: 
                 7fff810665d6 native_safe_halt ([kernel.kallsyms])
                 7fff8103ad7e default_idle ([kernel.kallsyms])
                 7fff8103b58f arch_cpu_idle ([kernel.kallsyms])
                 7fff810c5faa default_idle_call ([kernel.kallsyms])
    

由于我統計期間沒有做任何事,所以每次時鐘中斷發生時,CPU都停留在native_safe_halt函數這里。

nodejs對perf的支持

由于nodejs等虛機語言通常采用了JIT技術在運行時改寫函數,其堆棧符號表會在運行時變動。
為了能讓perf命令監測運行過程,nodejs提供了--perf_basic_prof參數,當加上此參數運行時,node會在/tmp目錄下生成perf-PID.map文件,里面給出了地址到函數名稱的映射。
perf record通過-p PID抽樣程序時,會自動去/tmp目錄下找對應的map文件并加載。

FlameGraph

查看perf script的報告仍然不夠直觀,大神給出了一個工具,可以將perf統計結果轉化成SVG圖,并且會將相同的堆棧合并,這樣可以很直觀的看出來程序在那些調用上花費了大量時間。

FlameGraph

Markdown好像不支持SVG,只能截個圖表示效果,實際的SVG是可以交互的了解具體細節。
這樣對程序的運行過程會有非常直觀的了解。

用FlameGraph調查nodejs程序內存使用

接下來是一個用FlameGraph來檢查nodejs程序的例子。

  1. 啟動nodejs程序,帶上 --perf-basic-prof 選項
    這里我啟動了一個簡單的deeplearning程序。
    node --perf-basic-prof main.js &
    進程ID為25586,于是node會在/tmp/目錄下生成/tmp/perf-25586.map文件,其實文件內容就是地址和函數名的映射表。
  2. 啟動perf監控程序
    這里我監控所有的缺頁中斷程序
    perf record -e kmem:mm_page_alloc -g -p 25586
    監控了一段時間后就Ctrl+c斷開監控,此時目錄下生成了perf.data文件。
  3. 輸出perf記錄
    調用perf script把結果存在一個臨時文件中
    perf script > node.tmp
  4. 調用Flamegraph工具將其生成SVG熱點圖
    stackcollapse-perf.pl node.tmp | flamegraph.pl > flamegrapsh.svg
  5. 用瀏覽器打開svg就可以看到熱點圖了


    flame_graph

可以查看具體細節:


detail

可以看到Rect.junc函數導致了大量的ProcessOldToNewSlot,緊接著導致了大量的缺頁中斷,接下來就可以調研下ProcessOldToNewSlot是什么,以及如何可以避免這種情況了。

ftrace

ftrace是linux提供的一個tracing工具,同perf一樣可以監控很多系統的事件。
ftrace
Dtrace & Systemtap
==========
比起Perf,Dtrace和Systemtap更為強大,它們除了可以檢測事件之外,還可以在事件發生時運行指定的命令去調查更詳細的信息,比如函數參數等。
Dtrace貌似在ubuntu上的支持并不好,接下來我會花些時間去學習Systemtap。

Linux系統提供的監測工具

vmstat

vmstat提供了關于系統虛擬內存的使用統計信息。
用法是

vmstat [options] [delay [count]]

delay表示刷新頻率,count表示統計次數。
重點是options

options

  • a
    統計active和inactive memory,正被程序引用的page為active。
  • m, slabs
    統計slab系統信息
  • s
    展示一些計數信息,
  • d
    統計硬盤信息

vmstat 統計的信息包括

  • 進程

    • r
      狀態為RUNNABLE的進程,運行中或者等待CPU。
    • b
      狀態為UNINTERRUPTIBLE SLEEP的進程,一般是在等待I/O操作。
  • 內存

    • swpd
      使用的swap內存
    • free
      空閑內存數量
    • buff
      被用作buffers的內存數量
    • cache
      被用作cache的內存數量
    • inact
    • active
      inactive/active內存數量
  • Swap

    • si
      從disk swap進來的內存數量(每秒)
    • so
      換出到disk的內存數量(每秒)
  • I/O

    • bi
      每秒收到的blocks數量
    • bo
      每秒發送出去的blocks數量
  • system

    • in
      每秒收到的interrupt數量,包括時鐘中斷
    • cs
      每秒的context switch數量
  • cpu
    這里統計的是百分比,按CPU核數平均之后的結果

    • us
      非內核態時間占比
    • sy
      內核態時間占比
    • id
      空閑時間占比,注意這里包括了wa時間
    • wa
      等待I/O時間占比
    • st
      Time stolen from virtual machine
  • Disk Mode

    • Reads
      total: 完成的Read總數
      merged: 被merge的Read次數
      sectors: 完成的Read sector數量
      ms: 花費在read上的時間總數
    • Writes
      total: 完成的Write總數
      merged: 被合并的Write次數
      sectors: 完成的write扇區總數
      ms: 花費在write上的時間總數
    • IO
      cur: 正在進行的I/O
      s: 花費在I/O上的時間

使用vmstat的時候,有幾個很重要的概念需要理清楚:

  • active/inactive memory
    active memory指的是被某個process使用在的memory。
    inactive memory指的是被曾經運行的process使用的memory
  • buffer/cache
    有關buffer和cache的區別,我到現在還沒有完全弄清楚,就目前的理解而言,buffer是供給I/O來保存傳輸數據塊的page,而cache是用來做文件內容緩存的page。

iostat

iostat統計了系統運行的一些io數據。

iostat [options] [interval [count]]

重點仍然是options

options

  • c
    展示CPU統計報告
  • d
    展示device統計報告
  • x
    展示擴展統計信息(很有用)
  • z
    忽略不活躍device

iostat展示的信息包括

  • CPU報告
    • %user
      平均后的用戶態時間占比
    • %system
      平均后的內核態時間占比
    • %iowait
      io等待時間的占比
    • %idle
      空閑時間占比
  • Device報告
    • Device:
      device name
    • tps:
      transfers per second,每秒傳輸請求次數
    • Blk_read/s (kB_read/s, MB_read/s):
      每秒讀取Block數量或者數據量
    • Blk_wrtn/s (kB_wrtn/s, MB_wrtn/s):
      每秒寫入的Block數量或者數據量
    • rrqm/s:
      每秒merged read request數量
    • wrqm/s:
      每秒merged write request數量
    • r/s:
      merge之后的每秒read request數量
    • w/s:
      merge之后的每秒write request數量
    • rsec/s (rkB/s, rMB/s):
      每秒讀入數據量
    • wsec/s (wkB/s, wMB/s):
      每秒寫入數據量
    • avgrq-sz:
      平均每個request的size,一般以sector為單位,每個sector是512字節
      所以一般來說:(avgrq-sz * 0.5 * (r/s + w/s) = rkB/s + wkB/s)
    • avgqu-sz:
      device的request queue的平均長度
    • await:
      平均I/O時間,從發出request到request完成
    • r_await: w_await:
      r/w 平均等待時間
    • %util:
      I/O時間占比,也就是整個系統有多少的時間是處于I/O中,當100%時,該device就接近飽和了

mpstat

vmstat,iostat給出的都是根據CPU核數平均之后的數據,mpstat可以統計per kernel的數據。

vmstat [options] [delay [count]]

options

  • A
    顯示每個核的統計信息
  • I
    顯示中斷統計
  • u
    顯示CPU統計數據
    一般的用法是
    mpstat -p ALL [interval]

mpstat報告內容

  • CPU統計信息
    • CPU
      CPU核序號
    • %usr
    • %sys
    • %iowait
    • %irq
      CPU服務硬件中斷時間占比
    • %soft
      CPU服務軟中斷占比

uptime

uptime主要是以15,5,1分鐘為單位統計了過去這段時間CPU的負荷。

free

free是查看當前內存使用量的工具,它可以查看:

  • total
  • used
  • free
  • shared
  • buff/cache
  • available
    total = used + free + buff/cache
    ps

ps命令會按進程為單位顯示一些統計數據。事實上ps命令的實現就是去proc文件系統查詢對應的數據。

  • %CPU
  • %MEM
  • VSZ
    virtual size
  • RSS
    resident set size

pmap

proc文件系統

proc文件系統是linux提供的內核文件系統,在linux源碼Documentation/filesystems/proc.txt里有詳細介紹。

proc根目錄

proc/[PID]目錄

cmdline

cmdline給出了該process的運行命令。

/usr/lib/at-spi2-core/at-spi2-registryd^@--use-gnome-session^@

以^@(null)分割

cwd

指向當前工作目錄的軟鏈接

environ

環境變量,同cmdline一樣是null分隔的字符串

exe

指向執行程序的軟鏈接

fd

文件描述符目錄,里面是全部file descriptor

maps

maps文件描述了程序的線性地址映射列表,看這個文件可以了解到程序當前的地址空間分布

mem

root

stat

stat文件很有意思,就是一列數字,具體的含義需要去查文檔。

statm

statm提供了進程的內存統計數據,包括:

  • total memory
  • resident set size
  • shared pages

status

status提供了很多進程的監測數據,其中比較有用的有:

  • FDSize
    當前的file descriptor數量
  • VmSize
    進程線性空間大小
  • VmRss
    進程實際占用物理內存大小
  • VmPeak
    進程線性空間大小峰值
    <em>
    當進程向linux系統請求一些內存空間的時候,linux系統并不會立刻給進程分配物理頁面,它只是做了一個mark,增加了一下進程的線性空間(vm_area_struct),表示這個進程又多了一塊可訪問地址,這些值會被統計在VmSize里,因此VmSize表示進程邏輯上的內存大小。
    當進程真正訪問到請求的地址時,linux才會因為page fault去給進程真正分配物理page,這個實際分配的大小記錄在VmRss里。同樣,當進程內存不夠用時,系統可能將其它進程的物理page斷開然后swap到交換設備上,這時候,其它進程的VmRss是減小的,參見stackoverflow上的回答。
    </em>

pagemap

meminfo

meminfo里有內存的很多信息

diskstats

diskstats文件包含了以下數據:
1 - major number
2 - minor mumber
3 - device name
4 - reads completed successfully
5 - reads merged
6 - sectors read
7 - time spent reading (ms)
8 - writes completed
9 - writes merged
10 - sectors written
11 - time spent writing (ms)
12 - I/Os currently in progress
13 - time spent doing I/Os (ms)
14 - weighted time spent doing I/Os (ms)
根據這些數據可以猜想iostat沒準就是通過/proc/diskstats文件來計算監測數據的,strace iostat果然證明了這個猜想

loadavg

看名字也能看出來大概是說啥了

性能分析方法:

CPU

CPU分析太細級別的不一定有特別大的意義,統計工具有:

  • uptime
    觀察CPU load average = number of [runnable, uninterruptable] processes

  • vmstat
    vmstat提供了user,system以及id的時間比率,以及r(run queue)的長度

  • mpstat
    mpstat -P ALL可以觀測每一個CPU核的統計數據,如果很不均勻,可以考慮多線程來提高CPU利用率

  • pidstat
    pidstat根據CPU或者進程來統計使用情況。

  • time
    /usr/bin/time,注意不是直接的time,加上-v可以顯示一個程序的統計信息。包括

    • Majo (I/O) page faults
    • Minor (reclaiming a frame) page faults
    • Swaps

    要理解上述參數可以參閱, Minor page faults表示程序訪問了可以復用的page,而Major page faults表示程序訪問了需要通過I/O調入的page。
    一個簡單的實驗就是調用兩次/usr/bin/time -v firefox,第一次調用啟動時間較長,400多個major page faults, 35481個minor,第二次調用就快多了,而且是0個major page faults, 34434個minor。因為linux page cache系統緩存了firefox的執行文件內容。

  • htop
    htop也是一個實時監測工具,可以用來初步判斷問題所在。

  • getdelay.c
    linux源代碼里Document目錄下有一個getdelay.c,編譯后可以通過-p PID的方式來讀取某一個程序的delay信息,包括:

    • Scheduler Latency
      進程花了多少時間等待CPU調度
    • Block I/O
      進程花了多少時間等待I/O完成
    • Swapping
      進程花了多少時間等待頁面調入
    • Memory reclaim
      進程花了多少時間等待cache頁面分配
  • profiling
    通過perf,systemtap等profiling工具,可以查看CPU熱點,分析CPU耗費在哪里。
    perf sched還可以統計scheduler latency,也就是進程切換導致的延時。(這個值我現在讀的有點疑問)

B神提到user:system CPU時間比反應了這個程序的類型,高user time說明是計算密集型。
在B神的業務服務器上(IO密集型,大概100K syscall每秒),一般負載系數在2到8(線程數/cpu核數),user/system時間比大概是60/40。

Memory

Memory監測一般是跟使用語言綁定,不過也有一些從系統層級觀測memory使用情況的工具。

概念

了解Memory工具前最好先了解linux系統里的幾個概念:

  • Main Memory
    也就是物理內存
  • Virtual Memory
    進程所看到的線性地址內存
  • Resident Memory
    有實際物理內存對應的virtual memory
  • Anonymous Memory
    沒有文件系統page對應的內存,一般就是進程的數據部分,包括stack和heap
  • Paging
    主存和存儲設備之間的page轉移
  • Anonymous Paging
    Anonymous Paging意味著把進程的stack或者heap swap出去,當再次運行的時候又會需要把它們從磁盤上讀回來,這是非常hurting的現象
  • Page States
    • Unallocated
    • Allocated, unmapped
    • Allocated, mapped to main memory
    • Allocated, mapped to swap device
  • Linux Page System
    當一個page request來的時候,linux按照以下順序分配內存page
    • Free List
      free page列表
    • Page Cache
      從文件系統用作cache的page中分配
    • Swapping
      kswapd系統線程通過swap一些page出去來騰出空間
    • OOM Killer
      殺死一些進程來空出空間
    • Page回收
      Linux將page分為以下幾類:
      1. 不可回收頁:
        空閑頁,保留頁(PG_reserved),內核分配頁,進程內核態堆棧頁,臨時鎖定頁(PG_locked)
      2. 可交換頁
        用戶態anonymous頁(堆棧,堆),回收時將內容保存到交換區
      3. 可同步頁
        用戶態地址空間(映射文件),有對應磁盤文件頁,回收時需要做同步操作

檢測工具及方法

  • vmstat
    • swpd
      總共swap出去的memory
    • free
      當前free memory
    • si, so
      swapped in swapped out memory,這兩個值反應了系統memory壓力
  • slabtop
    slabtop可以了解linux kernel slab系統的內存使用情況
  • Systemtap, perf
    這些tracer可以監控系統事件了解內存使用情況

File System

同樣,首先需要大致了解一些File system的概念

  • File system

  • Page Cache
    Linux使用統一的Page Cache系統來做磁盤和塊設備的Cache。Page Cache中的page內包含的內容可能有:

    1. 普通文件的內容,page cache通過文件inode中的address_space結構對應起來
    2. 目錄內容,linux像處理普通文件一樣處理目錄文件
    3. 從塊設備直接讀出的內容(繞過文件系統)
    4. 用戶process被swap out的內容,雖然被swap out,但是有些內容會被先cache起來
    5. 特殊的文件系統文件,比如shm文件系統
  • Linux系統是如何讀取一個文件的(非Direct I/O, Memory Mapping, Asynchronous)

    1. read調用,最后到generic_file_read(filep, buf, count, ppos),參數分別表示文件句柄,存放內容的數組,讀取內容數量和起始偏移量。
    2. generic_file_read初始化一個iovec,存放buf和count和一個kiocb,用來控制I/O過程,然后call __generic_file_aio_read:
    3. 檢查緩沖區有效
    4. 建立一個read_descriptor_t,表示讀取操作狀態
    5. do_generic_file_read(filp, ppos, read_descriptor_t, &file_read_actor)
    6. do_generic_file_read會執行實際的拷貝,過程如下:
    7. 取得filep->f_mapping,address_space對象
    8. 將文件看過頁數組,算出起止序號
    9. 循環讀入頁,(read_page方法):
    * 處理預讀頁
    * 在頁緩存中尋找頁,找不到則申請空白頁
    * 在頁緩存中找到頁的話,讀取結束
    * 調用address_space->readpage方法讀取數據到頁緩存
    * 調用file_read_actor把數據拷貝到用戶緩存
    
    1. 修改文件inode的update_atime,mark this inode dirty
      <em>
      read_page方法:
      普通文件read_page: 首先計算文件在磁盤上的塊號,如果是連續的,發出一個block I/O請求,否則用一次一塊的方法讀取。
      塊設備文件read_page: 將page看做塊緩沖區,逐塊讀取。
      </em>
  • Linux系統如何寫入一個文件

    1. generic_file_write(filep, buf, count, ppos)
    2. 獲取文件inode->i_sem信號量用以控制同步寫入
    3. 創建kiocb, iovec,調用__generic_file_aio_write_block:
    • 首先在page cache中搜索對應頁
    • 如果沒有對應頁,新建一個頁框,調用address_space->prepare_write
    • 拷貝寫入內容到頁中,調用address_space->commit_write,標記頁dirty
    1. 釋放inode->i_sem
    2. 如果需要sync,調用address_space的writepages方法
    3. 到這一步,write操作已經返回了。標記為dirty的page最終寫到磁盤上,則是延遲執行的,調用address_space->writepages方法
  • 內存映射mmap
    內存映射簡單的說,就是直接把文件內容讀入page cache,并通過修改進程的mm_struct中的vm_area_struct來讓一部分線性空間指向page cache中的page的物理地址。這樣進程如果只需要讀取,就相對read調用少了一次往user buffer里拷貝的過程,但如果進程要把數據復制出來的話,其實跟直接調用read區別不大。
    同樣,文件內容讀取也是延后的,直到進程訪問地址產生page fault時,操作系統才會去讀入文件內容到對應page frame。
    所以修改內存數據會直接修改page cache中的page內容,導致page為dirty,操作系統之后會將臟page寫入磁盤,更新本地文件。

  • Direct I/O
    Linux還提供一個Direct I/O,數據直接從設備傳遞到用戶空間,繞過文件緩存系統,好處是少了一次數據從系統內核到用戶空間的拷貝。壞處是用戶空間的頁將會被鎖定不能換出,這是與linux系統內存使用理念相悖的。

檢測工具

Network

  • netstat -s
  • netstat -i
  • tcpdump
  • stap/perf
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容