找回密码
 注册
关于网站域名变更的通知
查看: 432|回复: 1
打印 上一主题 下一主题

这是一个诡异的时间问题追查

[复制链接]
  • TA的每日心情

    2019-11-20 15:22
  • 签到天数: 2 天

    [LV.1]初来乍到

    跳转到指定楼层
    1#
    发表于 2020-3-9 10:17 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

    EDA365欢迎您登录!

    您需要 登录 才可以下载或查看,没有帐号?注册

    x
    9 E6 \6 c% F  z. q4 W0 S
    问题引入:9 ]$ z, @* j* K/ c5 r
    问题是我们重装了一台电脑之后,发现apache# E. M  o  _* K# D, H$ J
    的日志里突然增加了很多服务时间超长的请求,如下面两条:
    2 B3 j9 m9 @/ x; [** - - 4913229 11227 [29/Mar/2012:09:59:58 +0800] "GET*****
    5 t4 n+ S: g7 r, |2 y  j** - - 4793564 9846 [29/Mar/2012:09:59:58 +0800] "GET *****
    - h0 s1 e  `- m+ R5 {请求服务时间都在4s以上!这绝对无法忍受啊!0 A- J3 }2 x, m* j! s1 U

    # H  A  y4 z0 ?# V分析过程:
    " P- s  z3 Q9 z/ f0 J+ e 3 q% G2 b& s* `9 Y8 }
    但是,当我们:
    ) y; g/ F% B$ \7 v * 手动访问这台服务器之后,发现基本上请求都是正常响应的;+ Y/ c# O4 o: p& p* v
    * 从前端反向代理的日志来看,服务时间也是正常的;
    . }6 Y  Z* |$ Z# `' j/ T) ~那这样推断,真相只有一个,那就是apache的日志时间记录有问题!
    % M5 H$ B1 \  D, z$ P 3 h4 m2 _" p0 x" O; \: A
    什么问题呢?不清楚,但是apache
    7 [1 s- B3 Z$ ?, a+ {的时间记录不外乎是在收到请求时记个时间,请求处理完成之后再记一个时间,既然是时?6 l" `9 s( |4 s6 ]
    涞奈侍猓敲淳涂纯聪低呈奔浒桑幼罴虻サ?date 开始,多执行几次:
    % y& B' W' P. O apache2 # date
    & y* A* V& B4 s3 s7 H9 \1 H+ lWed Mar 28 17:25:08 CST 2012
    + t) J2 P1 p4 W' s' I$ [! Capache2 # date
    2 [8 _$ [7 [* d& A" YWed Mar 28 17:25:04 CST 2012  时间减少了?
    " b- ^- B& f7 Q. o. L" }% Z apache2 # date
    8 v* u2 T8 _/ `: Z6 n! {& |Wed Mar 28 17:25:06 CST 2012
    + f$ k" i* ^( d" h6 G$ `/ X7 m) gapache2 # date
    ' F% E5 X* R% Y$ jWed Mar 28 17:25:12 CST 20121 q/ ^+ W5 D0 }2 Z9 p% [/ k- L  D' l
    apache2 # date! O  @% e* w7 w
    Wed Mar 28 17:25:13 CST 2012
    9 k1 t! g3 G2 |5 C+ q5 ~  Qapache2 # date
    8 t* g, L, I4 t& G. S( y$ o( m1 {Wed Mar 28 17:25:09 CST 2012 时间减少了?
    6 r+ ]: Z8 m9 E6 D0 _apache2 # date1 `$ B* l2 d8 Z1 u6 t# e3 @/ s
    Wed Mar 28 17:25:09 CST 20122 F9 @- N! @0 R( C2 Z' A
    apache2 # date
    2 ]5 ]- j7 I9 R- C( n; pWed Mar 28 17:25:15 CST 20125 I  n" z  F) z. x+ O, g- e
    那么第一个怀疑就是系统里有进程在自己不断的调时间,比如ntpdate, ntpd
    2 {: f! j1 ^/ G+ ~! a/ J,那么我们首先把 ntpd给停掉;然后系统也没有任何地方会调用ntpdate;
    ! z: ~4 B$ P4 }* M- @6 W6 {继续观察,发现停掉ntpd后,现象依旧!
    & @( D4 J& I) c$ D还有什么可能呢? date- [: q. c  b- C  p) {
    程序是从内核里面读取时间还是说需要读取硬件时间?到底是怎么计算的呢?; C$ ]( I6 Z; i& @2 e( K
    请教下同事们,据说 sys_time 是读的kernel里的时间,而 sys_gettimeofday- Y8 @0 ]* a2 j2 \3 S0 u! X
    是需要读硬件时钟的。
    # B: k+ [% J( |- G那么就测试一把吧:* R& ?, y' a" H! b6 Z% V
    先写个程序调用 time(), 然后打印结果,结果是这样的:
    . V1 m1 F$ t/ s  C7 c0 E" b1332986690
    . G) w: d8 s5 p6 q1332986692% O! W5 Q. K: \/ f+ S+ }
    1332986693
    - J/ [8 \; ?4 }+ D+ Y' X1332986693
    & d- W6 U/ A3 q6 \1332986694
    5 _+ ^, i5 w$ K1332986694
    1 B1 e) a! Y) j1332986695; K/ E, ~. n- q6 t; G8 A! j( p6 {
    1332986695
    1 `+ r" [- z8 J6 \: U( z6 d) p1332986696
    ( ]- Y, @* `5 `% z, p1332986696
    " B0 C) z4 a7 @5 l) c# |& A# k1332986697
    : W. S; h  U  |/ m1 Z( W* }1332986697; Y: [  a& a4 z9 j; e
    1332986698
    7 i, v% K$ l4 Y/ N! u1 W1332986698
    8 |* x* T" e! m( g" l13329866993 R) L+ p/ A2 h2 V2 i
    确实没有问题了!看来有可能是硬件问题了,再看看clock_gettime 的调用:
    , {0 v: |; r" C# b. ?& s/ Z. {6 P{
    * O" T" o! ^# i        struct timespec ts;                              
    : f4 c6 l4 K: K7 |        int ret = 0;                                      
    ( v, V7 o/ @; f5 B- z        ret = clock_gettime(CLOCK_REALTIME, ts);          8 U$ W- f) ~& f
            printf("ret: %d, time: %d %d", ts.tv_sec, ts.tv_ns
    % o+ u9 O4 Y9 ^4 f! L& G1 Y4 e& N}                                                          / g* G2 k5 R- X, e' }3 G; t' F
    编译:        gcc -g -o clock_gettime getdate.c -lrt            
    % N" a- D/ }. b5 r3 ~                                                          $ U; F! P3 R, l2 O+ Y8 d# T
    ~ # ./clock_gettime                                    
    1 v- u. h) P6 p% a5 bret: 1332991087, time: 594076000 -1 3 Z2 o4 ]2 N5 M
    ret: 1332991088, time: 379009000 -1
    * j' L% k+ x0 H7 U+ Z* u5 Cret: 1332991089, time: 68561000 -1
    - l; S/ G% \4 t+ l' H+ c* l3 o/ Oret: 1332991089, time: 714013000 -1
    # M) N- s* _$ u- v1 \% Yret: 1332991086, time: 511250000 -1 时间在跳
    , `6 ?# d4 ~: j9 ~( E% O% rret: 1332991091, time: 945626000 -1
    . n0 Q6 P% ~2 S0 Eret: 1332991092, time: 650479000 -1
    8 {! s- l, N# c$ N. Tret: 1332991088, time: 734780000 -1  R5 V- B) v/ P4 W7 _
    ret: 1332991094, time: 506114000 -1时间在跳                    
    # _% a) a  f4 t( E) i! R8 j- V看来果然有可能是硬件问题!
    8 f1 |7 s- N- P5 H那么,检查一下,直接调用一下 hwclock --show ,看看是不是可以验证这个问题:+ V, N* L! a1 e
    ~ # hwclock --show                               ' {4 U0 ]0 Z9 a8 s  S& _2 q% A
    Thu 29 Mar 2012 10:22:21 AM CST  -0.948923 seconds  
    - C9 D" S$ e* n- v) u& @0 { ~ # hwclock --show                               2 v6 v% @9 S( O5 w( m/ x% F- h
    Thu 29 Mar 2012 10:22:22 AM CST  -0.188950 seconds  3 l) h6 U! s3 [
    ~ # hwclock --show                              
    & q7 G1 ]5 w6 ~Thu 29 Mar 2012 10:22:23 AM CST  -0.244766 seconds  
    8 @  K* |1 T, e( S7 o& a! M ~ # hwclock --show                              
    ' X8 f$ b: H1 M% f1 h+ T, zThu 29 Mar 2012 10:22:24 AM CST  -0.336868 seconds  / Y' p- |3 x3 |+ V5 t
    ~ # hwclock --show                               4 H; P: A+ V' U8 I
    Thu 29 Mar 2012 10:22:25 AM CST  4.237159 seconds   
    + {: r# Q1 V' G4 n( i ~ # hwclock --show                              
      L0 t' I# f. _! O. oThu 29 Mar 2012 10:22:26 AM CST  4.238672 seconds   
    # A7 a; b3 W4 O- t ~ # hwclock --show                               4 {, ~) M2 A& Z' k3 k4 ?6 ?5 ]
    Thu 29 Mar 2012 10:22:27 AM CST  -0.379418 seconds  
    6 O+ x: I/ m% F时间居然都是正确的!显然问题没有那么简单。。。那到底是怎么回事??还是从date
      [. a6 k% R) ^+ d开始查起吧:. X0 O, o* a, |) A/ R# F
    date是一个glibc里面的程序,那它的时间源是从哪里来的?查看一下glibc
    9 q- C; W4 c+ u& {3 L2 U6 B- Y的源码,它的时间从这里来:
    ; L3 f$ B( `( K3 f/ k# Z& _3 s; \, z// glibc coreutils gettime.c, l5 o- {4 J0 s# Y9 u
    /* Get the system time into *TS.  */
    ; u" g4 }" @# |/ {8 ^0 evoid! w" Y* }; W. V. M7 h# ?
    gettime (struct timespec *ts)8 x! \! D' B  x0 R. M+ h8 W0 {
    {: l( K1 E2 Y" Y( j* q
    #if HAVE_NANOTIME! S  ]9 ^" m- H5 @9 g. Z
      nanotime (ts);! i( I1 p4 n  a0 r! e
    #else
    7 T* D2 A1 z/ j* L % g+ w  m- }% F7 j/ s
    # if defined CLOCK_REALTIME && HAVE_CLOCK_GETTIME
    2 n# e* S* F" {/ L  if (clock_gettime (CLOCK_REALTIME, ts) == 0)0 ?7 Q* Z0 M  p) J, R2 M
        return;
    $ n$ R. i. R' @# endif  T: w" V# ]9 t5 z3 ~

    . _7 i9 f6 E5 x2 D6 A  `# if HAVE_GETTIMEOFDAY3 N3 a# P3 W* W
      {
    1 _7 v. K* \6 u, u, |) m    struct timeval tv;
    , f+ |) ~3 b6 n: m7 X5 T! B5 p. B    gettimeofday (&tv, NULL);- |7 N9 _' P6 i; B( v
        ts->tv_sec = tv.tv_sec;# ~# \- M3 X+ b
        ts->tv_nsec = tv.tv_usec * 1000;* Q2 M: v$ o" P8 I
      }
    / Y' r6 i7 W  h: C- ]- R9 t# else
    # a" u- ^( ]& J8 U  c. z1 Z, r. o  ts->tv_sec = time (NULL);$ `+ d' L- y5 s
      ts->tv_nsec = 0;0 X& Z0 O3 c( L- |. V! ~% K
    # endif8 K' s& ?0 X9 X' f0 O

    7 K0 e6 X# S( C; h/ T  ^#endif
    , f1 y6 N) S& [3 c5 n}* r3 g- _. ~% ?: i$ q7 j, M) a
    按照 nanotime, clock_gettime, gettimeofday的顺序去读取,我们这个系统中就是 $ I9 L3 U( R7 X8 D) f# E  X  T
    clock_gettime,从上面的小程序中也可以看到clock_gettime的时间是有问题的,那么# G# M( a/ _) [5 H, e% Z
    clock_gettime的时间源是怎么来的呢?. W7 X8 Y- c( }7 f! c6 A2 Y
    // linux kernel 2.6.18 arch/x86_64/kernel/time.c
    3 g3 ~( x; Y+ P0 y: o9 W8 u' m- Rvoid do_gettimeofday(struct timeval *tv)$ j7 A8 ]- m2 ]2 |" V- D
    {
    ! }+ x8 ^% T0 m' m5 ^unsigned long seq, t;2 }* j  s- ^: {
    unsigned int sec, usec;
      I9 k  T# l" X3 q% |3 {8 J/ Z6 {
    # a9 _3 T2 F$ K3 `4 ^6 y& gdo {
    . b: x8 y1 c/ I, cseq = read_seqbegin(&xtime_lock);
    4 y# H. L6 Y" J) e! D1 d& W  k/ F
    + z$ o/ C# E; w; H( Lsec = xtime.tv_sec;
    0 H2 ]" V3 l8 s$ G5 P7 Lusec = xtime.tv_nsec / NSEC_PER_USEC;5 ]) l  G; i8 H5 Y2 n6 i
    t = (jiffies - wall_jiffies) * USEC_PER_TICK +
    3 _; g9 m. Z8 r/ r1 ~8 ddo_gettimeoffset();
    8 l* A8 F% K5 [7 f, H! L6 w4 r7 Jusec += t;
    2 ~7 r& J+ }7 M- V- S5 f
    ' s: b9 t! i0 n3 j8 l  h4 j} while (read_seqretry(&xtime_lock, seq));
    ' ^! r5 Z- ^, }+ Z5 T$ ~
    " ^' w/ ~( F0 `& `9 o+ }tv->tv_sec = sec + usec / USEC_PER_SEC;
    & B; S' c3 Q. [: U; a- R* n  ~/ Ctv->tv_usec = usec % USEC_PER_SEC;7 @$ w. v; F5 D  C" ?; x  Z) _
    }
    6 n1 J! ]9 W& `. u/ h+ q% |反正就是一通计算,其中比较重要的是 do_gettimeoffset/ w: F' L4 S- g/ _9 m
    ,这个函数的目的是计算上一次更新 xtime  g- u  u* M  x. D
    到当前执行这一段时间的偏移量,然后用这个偏移量去加到 xtime
    # x0 B; l# _2 F) @4 X上去,就是当前的时间(因为xtime的精度不够,clock_gettime" [7 E5 o8 _8 k5 T" j8 B$ _7 w
    的精度要求更高,所以必须计算一个偏移量)。
    , p6 n  t# a' R那么,这个 do_gettimeoffset又是从哪来的?同样的文件里面有:
    # A" O2 B) m8 l, ^static inline unsigned int do_gettimeoffset_tsc(void)0 Z7 j1 ?5 f' O
    {( U1 n6 G, E% \4 Q: y  P6 X9 y( J
    unsigned long t;
    $ h- L( l  d9 ?0 Q9 R2 b) i' [unsigned long x;
    , E% h. C9 ?9 u' yt = get_cycles_sync();' |& c) [1 a' o# A' I9 L6 P
    if (t < vxtime.last_tsc)
    ! p; S9 @+ ]1 U2 ~- Yt = vxtime.last_tsc; /* hack */- J1 v9 Q  y+ l+ n
    x = ((t - vxtime.last_tsc) * vxtime.tsc_quot) >> US_SCALE;
    3 o) x6 u/ e) h5 |% H" Yreturn x;! Z4 ?1 C3 T2 s" w& M5 I& ?4 W
    }
    . k7 f4 u5 L2 V* F, B3 x7 J / ]! d" ?) R, o2 S2 K
    static inline unsigned int do_gettimeoffset_hpet(void)
    6 Q2 r, }6 S! z5 L9 C3 B{( ?( A8 H5 o6 m! K" c
    /* cap counter read to one tick to avoid inconsistencies */( @, W7 s1 C; A0 S0 `' i6 W3 m
    unsigned long counter = hpet_readl(HPET_COUNTER) - vxtime.last;1 `4 o8 B0 d0 [6 M4 p
    return (min(counter,hpet_tick) * vxtime.quot) >> US_SCALE;
    2 F  g9 C+ z( `! w}
    4 F' j5 L8 j- z/ C0 P5 v# @ 2 g# q" Y; c' o3 i0 N/ k0 Y
    unsigned int (*do_gettimeoffset)(void) = do_gettimeoffset_tsc;
    1 \- K: t7 H! A: \3 g, {也就是说,go_gettimeoffset 是从 tsc寄存器从拿到的,这又是神马玩意呢? - J4 T4 C$ P  c) S/ e; R4 q
    参考一下下面的几篇文章【1】【2】【3】,基本就明白了:
    " h" t& I' V! q2 k3 i( n% Ztsc是一个cpu内的寄存器,用来累加从系统启动到目前,所有的时钟周期数,全称是 * H3 _$ z* r  k) d
    Timestamp Counter;
    " X0 b* J; c8 I( r2 G  e而且,最重要的是我们发现了一个重要的线索,那就是:4 w& x; n& d8 d8 O6 \
    在多核cpu的架构中,tsc寄存器是很有可能不一致的,特别是在intel的比较老的多核CPU
    + N$ z2 J- G: `) f$ y8 j+ x. j% I中!坑爹....0 z1 \0 m. C  X7 D6 l; W$ B$ E
    很多文章提到过这个问题,比如【1】【2】【4】,显然如果不同cpu
    ; `1 [5 ^; B  E: b读到的值不同的话,多次执行,显然时间是不一致的。1 Z2 ?, o- B9 Q% k
    看起来就是这个问题了,那么就来验证一把:% S4 m; n, I0 V) [
    #include<stdlib.h>
    ; x: B4 G9 V5 C* `#include<stdio.h>
    0 @4 p% O) X% z: s) t& H#include<sys/types.h>
    - U4 {( h1 d1 e" t! e: \#include<sys/sysinfo.h>! \# T) V' D& }8 [, _
    #include<unistd.h>9 e0 ]& w' V- E2 |( E: F
    #include<time.h>
    . g& ~5 p  Z; L& o8 p, r' @: F
    / ~& k) s" L2 G/ u  n: r4 S#define __USE_GNU
    / J$ F' F7 \. \, _& _#include<sched.h>
    / q# h* V% c/ t5 p9 X+ Z4 [7 K#include<ctype.h>
    4 B5 `8 l! N( g9 X0 m" C4 I/ {#include<string.h>
    0 k4 @: f# N( _ 8 k' O- X" Q4 o3 q

    4 f2 v: [% C4 q
    . N- Q2 X& {. V  v; jint main(int argc, char **argv){# E! R  r+ h. z2 {+ ?! o- C
    ) F1 Z% B' h. J" R4 \1 {' o
            cpu_set_t cs;- Q+ U% a5 I0 H: r9 s
            CPU_ZERO(&cs);+ _: q- U( ?  R5 \
            CPU_SET(0, &cs);
    5 v: k1 z' P8 _) c : Z8 U7 O; m: A& _& a4 l1 w! L. `5 V2 y
            sched_setaffinity(0, sizeof(cpu_set_t), &cs);
    . r# z2 n% G1 I! c2 m ! `# N0 P" `8 n; R
            struct timespec ts;( ~( M) i1 T( z/ N, q
            int ret = 0;9 Z. ?  l6 {# ?# ^: I% u
            ret = clock_gettime(CLOCK_REALTIME, &ts);
    ' D  `  }' L- c. `        printf("ret: %d, time: %lld %lld", ret,  ts.tv_sec, ts.tv_nsec);
    . I+ M' T4 S  q/ W. w! w
    % Y0 C8 [6 d: F& q) Q7 D3 a}
    " r- Q; C& X: `
    1 G) Y/ Z/ w# {0 z9 Hgcc -g -o cpu0_clock_gettime cpu_getdate.c -lrt
    , r  ?3 [" B$ n: W; x' p! d 7 W8 C6 r. z: K) Z9 V# i! z
    ret: 0, time: 1332992815 537481000
    8 ?. J0 r) z. ?& J% Q4 _, pret: 0, time: 1332992816 506704000; G6 d) i1 k" h" Q' ?& |3 `- U( B
    ret: 0, time: 1332992817 99100009 X$ a% l: m( d) L6 \* F6 x% G
    ret: 0, time: 1332992817 480431000
    2 y4 _0 ^4 ^; ~5 l) rret: 0, time: 1332992817 936965000  f% [# P1 h6 i* n( D
    ret: 0, time: 1332992818 416634000
    5 O, {: s: x* _; D1 Q+ @ret: 0, time: 1332992818 850319000% m& N. h% p+ B/ m
    ret: 0, time: 1332992819 305075000
    , G3 d' p$ k# U3 C! y% }3 v/ Oret: 0, time: 1332992819 7291080005 V  l7 e1 \  s; ~% F/ r
    ret: 0, time: 1332992820 164726000
    ; l# I8 v. j1 a; X3 R. iret: 0, time: 1332992820 577573000% C, ^" v- J  p' ?; {
    ret: 0, time: 1332992820 985025000
    , H! J, |* O1 s3 h) ~4 Wret: 0, time: 1332992821 394070000$ j: R0 _, J3 J9 R3 @* N
    ret: 0, time: 1332992821 816708000+ i( e9 X9 q2 O# Y7 A; a
    ret: 0, time: 1332992822 2168110000 w8 n( m  ~4 s1 @
    ret: 0, time: 1332992822 616901000
    6 a0 Q  `. R% Q: x/ vret: 0, time: 1332992823 18564000w4 x: b* Y5 r) I8 ^  t
    ret: 0, time: 1332992823 727585000( `% h8 U5 I$ d: v+ \  F
    通过把程序的调度固定在某一个具体的cpu中,那么clock_gettime的结果也是正确的!
    ' |  x% r9 G; {" Z9 @9 i经过几次测试,很容易找到两个cpu时间不一致的情况,具体这个例子来说就是 cpu0和/ D( p; u6 e$ ?: R1 y
    cpu5,把上面的程序修改一下,可以看到结果基本上每次都会跳一把。  O: p  @& l  R) d4 g$ g
    问题解决:$ ~- K) K" f) }$ p
    那么,tsc在多核cpu中的值不同,应该怎么解决呢? 如 【4】所示,在高版本的linux
    0 U/ p( s7 `/ Z9 I8 ~) s2 R/ V内核中,有人提交了补丁,可以在内核启动时检查并且同步一把,那么升级一下内核到 20 o8 e8 I) ^! X2 N+ n
    .6.38。
    & L" E; E* d6 A, |) e3 {1 O重新启动到新内核之后,问题解决!# g2 T+ R7 h  i  g/ |! L: O. g  i
    总结一下:
      N* k* G" Y+ Y  m& I1. apache的时间统计方式,没具体看实现,但是显然前后的两个时间可能是在不同的cpu3 p2 n" A& o4 S( ^
    核上计算出来的。
    : }' C9 G3 b! B1 C: D2. tsc的值,并不能保证是同步的,特别是在某些Intel cpu1 C, ^3 h+ Y6 Y$ e* |
    的情况下;但是新的内核会做一些检查和resync的工作;
    6 W+ k8 J( w& j3. time()调用直接读取内核的xtime值,不会触及硬件;但是 gettimeofday(),
    , m' a+ L6 Q* [0 q! n+ ^3 i; `9 kclock_gettime() 在 x86_64下都会读取tsc值;$ E+ p8 U! D8 n# I
    4. hwclock读的硬件时钟显然不是 tsc,应该是实时时钟RTC;
    " C# J4 d8 f# Y【1】 http://en.wikipedia.org/wiki/Time_Stamp_Counter
    , m/ \5 g& d9 j【2】 http://juliusdavies.ca/posix_clocks/clock_realtime_linux_faq.html
    " x7 @" m: y, N+ c【3】http://www.linuxforum.net/forum/gshowflat.php?Cat=&Board=driver&Number=1 i: p5 O. [2 n% {# {
    385219&page=0&view=collapsed&sb=5&o=all&fpart , linux时钟中断机制,Copyright
    / s5 a: m) `* ?5 q2003 by 詹荣开
      u6 Z; j) k7 @1 f【4】http://lwn.net/Articles/211051/ x86: unify/rewrite SMP TSC sync code  

    该用户从未签到

    2#
    发表于 2020-3-9 17:20 | 只看该作者
    时间问题追查,牛逼了
    您需要登录后才可以回帖 登录 | 注册

    本版积分规则

    关闭

    推荐内容上一条 /1 下一条

    EDA365公众号

    关于我们|手机版|EDA365电子论坛网 ( 粤ICP备18020198号-1 )

    GMT+8, 2025-11-26 01:15 , Processed in 0.156250 second(s), 24 queries , Gzip On.

    深圳市墨知创新科技有限公司

    地址:深圳市南山区科技生态园2栋A座805 电话:19926409050

    快速回复 返回顶部 返回列表