成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

硬核!如何在容器中做時間漫游者

存儲 存儲軟件
分布式數據庫要實現全局一致性快照,很多方案使用時間做邏輯時鐘,所以需要解決不同節點之間時鐘一致的問題。

[[402031]]

本文轉載自微信公眾號「董澤潤的技術筆記」,作者董澤潤。轉載本文請聯系董澤潤的技術筆記公眾號。

題目稍有些標題黨,最近公司想用 chaos-mesh 對 k8s 做混沌測試,開始做前期的調研,發現 pingcap[1] 對時間的注入非常硬核,而且最終方案居然是實習生構思出來的 ^^ 感謝 pingcap 貢獻的項目

TL;DR: 通過劫持 vdso, 將時間函數跳轉到 hack 過的匯編指令來實現 time skew. 原理不難懂,但細節超多,參考官方文檔[2]

為什么需要 time skew

可以參考 Chaos Mesh - 讓時間在容器中自由搖擺[3], 簡單來說就是:

分布式數據庫要實現全局一致性快照,很多方案使用時間做邏輯時鐘,所以需要解決不同節點之間時鐘一致的問題。但往往物理節點上的物理時間總是會出現偏差,不管是使用 NPT 服務同步也好,或者其他方法總是沒辦法完全避免出現誤差,這時候如果我們的應用不能夠很好的處理這樣的情況的話,就可能造成無法預知的錯誤。

其實這很符合工程設計哲學:design for failure, 任何一個硬件或是軟件都會有錯誤(fault),系統如何在不影響對外提供服務的前提下,如何處理這些故障,就是我們常說的 fault tolerance

但是對于非金融業務來說,時間偏移一點影響并不大,相比其它 chaos, time的場景還是受限一些

如何注入

從實體機的經驗來看,所謂的混沌測試都比較直觀的,比如用 tc 做網絡的丟包,限速來模擬網絡故障,使用 stress 模擬 cpu 壓力。但是在容器中做如何模擬 time skew 呢?

如果直接使用 linux date 命令修改,會影響到宿主機上其它所有容器。有沒有方法能只影響某個容器?

之前發過一篇文章 時鐘源為什么會影響性能[4], 從中可以看到,go 調用系統時間函數時,會先調用 vdso 的代碼,如果時鐘源符合條件,直接在用戶空間完成,并不會進入內核空間,所以針對這一現象,syscall 劫持的方法就不能使用了

那么是否可以直接修改 vdso 段代碼呢?

查看 vdso

  1. # cat /proc/1970/maps 
  2. ...... 
  3. 7ffe8478a000-7ffe847ab000 rw-p 00000000 00:00 0                          [stack] 
  4. 7ffe847bb000-7ffe847be000 r--p 00000000 00:00 0                          [vvar] 
  5. 7ffe847be000-7ffe847bf000 r-xp 00000000 00:00 0                          [vdso] 
  6. ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall] 

可以看到 vdso 代碼段的起始邏輯地址,同時注意權限位是 r-xp, 這就意味著用戶態的進程是無法直接修改該內容。

真的就沒辦法了嘛?有的,ptrace[5] 法力無邊

The ptrace() system call provides a means by which one process (the "tracer") may observe and control the execution of another process (the "tracee"), and examine and change the tracee's memory and registers. It is primarily used to implement breakpoint debugging and system call tracing.

ptrace 提供了一種修改和觀察其它進程的手段,包括修改內存值和寄存器,巧了這些 chaos-mesh 都用到了。如何實現 go 調試器[6] 這篇文章也講了 ptrace 的用途,很棒的文章。

整體實現

這就是簡單的流程圖,主要代碼都是 time_linux_amd64.go[7], 當前僅支持 linux amd64 平臺,不支持 Windows/MacOS

  1. // ModifyTime modifies time of target process 
  2. func ModifyTime(pid int, deltaSec int64, deltaNsec int64, clockIdsMask uint64) error { 
  3.   ...... 
  4.  runtime.LockOSThread() // 將當前 goroutine 綁定底層線程 
  5.  defer func() { 
  6.   runtime.UnlockOSThread() 
  7.  }() 
  8.  
  9.  program, err := ptrace.Trace(pid) // ptrace 獲得 program 
  10.  if err != nil { 
  11.   return err 
  12.  } 
  13.  defer func() { 
  14.   err = program.Detach() 
  15.   if err != nil { 
  16.    log.Error(err, "fail to detach program""pid", program.Pid()) 
  17.   } 
  18.  }() 
  19.  
  20.  var vdsoEntry *mapreader.Entry // 遍歷 entry 找到 vdso 
  21.  for index := range program.Entries { 
  22.   // reverse loop is faster 
  23.   e := program.Entries[len(program.Entries)-index-1] 
  24.   if e.Path == "[vdso]" { 
  25.    vdsoEntry = &e 
  26.    break 
  27.   } 
  28.  } 
  29.  if vdsoEntry == nil { 
  30.   return errors.New("cannot find [vdso] entry"
  31.  } 
  32.  
  33.  // minus tailing variable part 
  34.  // 24 = 3 * 8 because we have three variables 
  35.  constImageLen := len(fakeImage) - 24 
  36.  var fakeEntry *mapreader.Entry 
  37.  
  38.  // find injected image to avoid redundant inject (which will lead to memory leak) 
  39.  for _, e := range program.Entries { 
  40.   e := e 
  41.  
  42.   image, err := program.ReadSlice(e.StartAddress, uint64(constImageLen)) 
  43.   if err != nil { 
  44.    continue 
  45.   } 
  46.  
  47.   if bytes.Equal(*image, fakeImage[0:constImageLen]) { 
  48.    fakeEntry = &e // 遍歷找到 fake Image Entry,不能重復生成 
  49.    log.Info("found injected image""addr", fakeEntry.StartAddress) 
  50.    break 
  51.   } 
  52.  } 
  53.  if fakeEntry == nil { // 如果 fakeEntry 不存在,用 Mmap 分配內存,內容是 fakeImage 匯編指令 
  54.   fakeEntry, err = program.MmapSlice(fakeImage) 
  55.   if err != nil { 
  56.    return err 
  57.   } 
  58.  } 
  59.  fakeAddr := fakeEntry.StartAddress 
  60.  
  61.  // 139 is the index of CLOCK_IDS_MASK in fakeImage 寫 clockidsmask 
  62.  err = program.WriteUint64ToAddr(fakeAddr+139, clockIdsMask) 
  63.  if err != nil { 
  64.   return err 
  65.  } 
  66.  
  67.  // 147 is the index of TV_SEC_DELTA in fakeImage 寫偏移量秒 
  68.  err = program.WriteUint64ToAddr(fakeAddr+147, uint64(deltaSec)) 
  69.  if err != nil { 
  70.   return err 
  71.  } 
  72.  
  73.  // 155 is the index of TV_NSEC_DELTA in fakeImage 寫偏移量納秒 
  74.  err = program.WriteUint64ToAddr(fakeAddr+155, uint64(deltaNsec)) 
  75.  if err != nil { 
  76.   return err 
  77.  } 
  78.   // 找到 clock_gettime 在 vdso 中的位置 
  79.  originAddr, err := program.FindSymbolInEntry("clock_gettime", vdsoEntry) 
  80.  if err != nil { 
  81.   return err 
  82.  } 
  83.   // originAddr 位置 hijack, 寫上 jump 指令,跳轉到 fakeImage 
  84.  err = program.JumpToFakeFunc(originAddr, fakeAddr) 
  85.  return err 

代碼寫上了注釋,分別對應上面的流程圖。下面分解來看。

1. Ptrace

  1. type TracedProgram struct { 
  2.  pid     int 
  3.  tids    []int 
  4.  Entries []mapreader.Entry 
  5.  
  6.  backupRegs *syscall.PtraceRegs 
  7.  backupCode []byte 

TracedProgram 結構體比較簡單,pid 是待注入 chaos 的進程 id, 同時 tids 保存所有的線程 id, Entries 是進程邏輯地址空間,

Trace 函數在代碼 ptrace_linux_amd64.go[8] 中

通過讀取 /proc/{pid}/task 獲取進程的所有線程,然后分別對所有線程執行 linux ptrace 調用。然后生成 Entries, 什么是 Entry 呢?就是上文提到的 /proc/{pid}/maps 內容

  1. ...... 
  2. 7ffe8478a000-7ffe847ab000 rw-p 00000000 00:00 0                          [stack] 
  3. 7ffe847bb000-7ffe847be000 r--p 00000000 00:00 0                          [vvar] 
  4. 7ffe847be000-7ffe847bf000 r-xp 00000000 00:00 0                          [vdso] 
  5. ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall] 

2. Mmap FakeImage

查找 vdso[9], 如何失敗,直接退出。一般 vdso 都在最后,所以從尾開始遍歷

同時還要查找 fakeEntry, 如果存在,直接復用。否則會造成內存泄漏,當然了,一直創建新的 fakeEntry .....

  1. // MmapSlice mmaps a slice and return it's addr 
  2. func (p *TracedProgram) MmapSlice(slice []byte) (*mapreader.Entry, error) { 
  3.  size := uint64(len(slice)) 
  4.  
  5.  addr, err := p.Mmap(size, 0) 
  6.  if err != nil { 
  7.   return nil, errors.WithStack(err) 
  8.  } 
  9.  
  10.  err = p.WriteSlice(addr, slice) 
  11.  if err != nil { 
  12.   return nil, errors.WithStack(err) 
  13.  } 
  14.  
  15.  return &mapreader.Entry{ 
  16.   StartAddress: addr, 
  17.   EndAddress:   addr + size
  18.   Privilege:    "rwxp"
  19.   PaddingSize:  0, 
  20.   Path:         ""
  21.  }, nil 

注意,這不是簡單的調用 Mmap Syscall !!! ptrace.Syscall[12] 是利用 ptrace 控制進程,讓目標進程單步執行 syscall

  1. // Syscall runs a syscall at main thread of process 
  2. func (p *TracedProgram) Syscall(number uint64, args ...uint64) (uint64, error) { 
  3.  err := p.Protect() // 保存目標進程的寄存器 
  4.  if err != nil { 
  5.   return 0, err 
  6.  } 
  7.  
  8.  var regs syscall.PtraceRegs 
  9.  
  10.  err = syscall.PtraceGetRegs(p.pid, &regs) 
  11.  if err != nil { 
  12.   return 0, err 
  13.  } 
  14.  regs.Rax = number // 設置操作 syscall number, 填充其它參數 
  15.  for index, arg := range args { 
  16.   // All these registers are hard coded for x86 platform 
  17.   if index == 0 { 
  18.    regs.Rdi = arg 
  19.   } else if index == 1 { 
  20.    regs.Rsi = arg 
  21.   } else if index == 2 { 
  22.    regs.Rdx = arg 
  23.   } else if index == 3 { 
  24.    regs.R10 = arg 
  25.   } else if index == 4 { 
  26.    regs.R8 = arg 
  27.   } else if index == 5 { 
  28.    regs.R9 = arg 
  29.   } else { 
  30.    return 0, fmt.Errorf("too many arguments for a syscall"
  31.   } 
  32.  } 
  33.  err = syscall.PtraceSetRegs(p.pid, &regs) 
  34.  if err != nil { 
  35.   return 0, err 
  36.  } 
  37.  
  38.  ip := make([]byte, ptrSize) 
  39.  
  40.  // We only support x86-64 platform now, so using hard coded `LittleEndian` here is ok. 設置 rip 寄存器 
  41.  binary.LittleEndian.PutUint16(ip, 0x050f) 
  42.  _, err = syscall.PtracePokeData(p.pid, uintptr(p.backupRegs.Rip), ip) 
  43.  if err != nil { 
  44.   return 0, err 
  45.  } 
  46.  
  47.  err = p.Step() // 單步執行 
  48.  if err != nil { 
  49.   return 0, err 
  50.  } 
  51.  
  52.  err = syscall.PtraceGetRegs(p.pid, &regs) 
  53.  if err != nil { 
  54.   return 0, err 
  55.  } 
  56.  
  57.  return regs.Rax, p.Restore() // 獲取返回值,并且恢復寄存器 

參考代碼的注釋,搞過嵌入式的肯定熟悉:保存寄存器現場,設置新的寄存器值為 syscall number 以及參數,最后設置指令寄存器 rip 單步執行,就完成了讓目標進程執行 mmap 的操作,最后也要恢復寄存器,還原現場。

這里為什么 rip 寄存器要設置成 0x050f 呢???其實這是 syscall 的操作碼

另外 p.WriteSlice 是使用 syscall process_vm_writev 將數據寫入目標進程的內存邏輯地址空間。

3. FindSymbolInEntry

  1. ~# file /tmp/vdso.so 
  2. /tmp/vdso.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=17d65245b85cd032de7ab130d053551fb0bd284a, stripped 
  3. ~# objdump -T /tmp/vdso.so 
  4.  
  5. /tmp/vdso.so:     file format elf64-x86-64 
  6.  
  7. DYNAMIC SYMBOL TABLE
  8. 0000000000000950  w   DF .text 00000000000000a1  LINUX_2.6   clock_gettime 
  9. 00000000000008a0 g    DF .text 0000000000000083  LINUX_2.6   __vdso_gettimeofday 
  10. 0000000000000a00  w   DF .text 000000000000000a  LINUX_2.6   clock_getres 
  11. 0000000000000a00 g    DF .text 000000000000000a  LINUX_2.6   __vdso_clock_getres 
  12. 00000000000008a0  w   DF .text 0000000000000083  LINUX_2.6   gettimeofday 
  13. 0000000000000930 g    DF .text 0000000000000015  LINUX_2.6   __vdso_time 
  14. 0000000000000930  w   DF .text 0000000000000015  LINUX_2.6   time 
  15. 0000000000000950 g    DF .text 00000000000000a1  LINUX_2.6   __vdso_clock_gettime 
  16. 0000000000000000 g    DO *ABS* 0000000000000000  LINUX_2.6   LINUX_2.6 
  17. 0000000000000a10 g    DF .text 000000000000002a  LINUX_2.6   __vdso_getcpu 
  18. 0000000000000a10  w   DF .text 000000000000002a  LINUX_2.6   getcpu 

FindSymbolInEntry 函數很簡單,就是要找到 clock_gettime 在 vdso 中的地址,參考我之前的文章,上面是 dump 出來的符號表

4. JumpToFakeFunc

  1. // JumpToFakeFunc writes jmp instruction to jump to fake function 
  2. func (p *TracedProgram) JumpToFakeFunc(originAddr uint64, targetAddr uint64) error { 
  3.  instructions := make([]byte, 16) 
  4.  
  5.  // mov rax, targetAddr; 
  6.  // jmp rax ; 
  7.  instructions[0] = 0x48 
  8.  instructions[1] = 0xb8 
  9.  binary.LittleEndian.PutUint64(instructions[2:10], targetAddr) 
  10.  instructions[10] = 0xff 
  11.  instructions[11] = 0xe0 
  12.  
  13.  return p.PtraceWriteSlice(originAddr, instructions) 

JumpToFakeFunc[13], 修改 vdso 符號表中的匯編代碼,使所有調用 clock_gettime 的都跳轉到我們 fakeEntry 的地址,劫持 vdso

FakeImage

  1. var fakeImage = []byte{ 
  2.  0xb8, 0xe4, 0x00, 0x00, 0x00, //mov    $0xe4,%eax 
  3.  0x0f, 0x05, //syscall 
  4.  0xba, 0x01, 0x00, 0x00, 0x00, //mov    $0x1,%edx 
  5.  0x89, 0xf9, //mov    %edi,%ecx 
  6.  0xd3, 0xe2, //shl    %cl,%edx 
  7.  0x48, 0x8d, 0x0d, 0x74, 0x00, 0x00, 0x00, //lea    0x74(%rip),%rcx        # <CLOCK_IDS_MASK> 
  8.  0x48, 0x63, 0xd2, //movslq %edx,%rdx 
  9.  0x48, 0x85, 0x11, //test   %rdx,(%rcx) 
  10.  0x74, 0x6b, //je     108a <clock_gettime+0x8a> 
  11.  0x48, 0x8d, 0x15, 0x6d, 0x00, 0x00, 0x00, //lea    0x6d(%rip),%rdx        # <TV_SEC_DELTA> 
  12.  0x4c, 0x8b, 0x46, 0x08, //mov    0x8(%rsi),%r8 
  13.  0x48, 0x8b, 0x0a, //mov    (%rdx),%rcx 
  14.  0x48, 0x8d, 0x15, 0x67, 0x00, 0x00, 0x00, //lea    0x67(%rip),%rdx        # <TV_NSEC_DELTA> 
  15.  0x48, 0x8b, 0x3a, //mov    (%rdx),%rdi 
  16.  0x4a, 0x8d, 0x14, 0x07, //lea    (%rdi,%r8,1),%rdx 
  17.  0x48, 0x81, 0xfa, 0x00, 0xca, 0x9a, 0x3b, //cmp    $0x3b9aca00,%rdx 
  18.  0x7e, 0x1c, //jle    <clock_gettime+0x60> 
  19.  0x0f, 0x1f, 0x40, 0x00, //nopl   0x0(%rax) 
  20.  0x48, 0x81, 0xef, 0x00, 0xca, 0x9a, 0x3b, //sub    $0x3b9aca00,%rdi 
  21.  0x48, 0x83, 0xc1, 0x01, //add    $0x1,%rcx 
  22.  0x49, 0x8d, 0x14, 0x38, //lea    (%r8,%rdi,1),%rdx 
  23.  0x48, 0x81, 0xfa, 0x00, 0xca, 0x9a, 0x3b, //cmp    $0x3b9aca00,%rdx 
  24.  0x7f, 0xe8, //jg     <clock_gettime+0x48> 
  25.  0x48, 0x85, 0xd2, //test   %rdx,%rdx 
  26.  0x79, 0x1e, //jns    <clock_gettime+0x83> 
  27.  0x4a, 0x8d, 0xbc, 0x07, 0x00, 0xca, 0x9a, //lea    0x3b9aca00(%rdi,%r8,1),%rdi 
  28.  0x3b,             // 
  29.  0x0f, 0x1f, 0x00, //nopl   (%rax) 
  30.  0x48, 0x89, 0xfa, //mov    %rdi,%rdx 
  31.  0x48, 0x83, 0xe9, 0x01, //sub    $0x1,%rcx 
  32.  0x48, 0x81, 0xc7, 0x00, 0xca, 0x9a, 0x3b, //add    $0x3b9aca00,%rdi 
  33.  0x48, 0x85, 0xd2, //test   %rdx,%rdx 
  34.  0x78, 0xed, //js     <clock_gettime+0x70> 
  35.  0x48, 0x01, 0x0e, //add    %rcx,(%rsi) 
  36.  0x48, 0x89, 0x56, 0x08, //mov    %rdx,0x8(%rsi) 
  37.  0xc3, //retq 
  38.  // constant 
  39.  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //CLOCK_IDS_MASK 
  40.  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //TV_SEC_DELTA 
  41.  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //TV_NSEC_DELTA 

fakeImage 最后三個參數是偏移量,以及傳遞的 CLOCK_IDS_MASK, 這些匯編是什么意思呢???

查看匯編操作碼[14],0xe4 是系統調用 clock_gettime 的操作碼,后續都是對結果進行注入,要么增要么減,制造偏移量 time skew

測試案例

  1. # git clone https://github.com/chaos-mesh/chaos-mesh 
  2. # cd chaos-mesh; make watchmaker 

首先下載 chaos-mesh, 然后編譯 watchmaker, 這是一個方便注入的小工具。

  1. package main 
  2.  
  3. import ( 
  4.         "fmt" 
  5.         "time" 
  6.  
  7. func main() { 
  8.         fmt.Println("start print time"
  9.         for { 
  10.                 fmt.Printf("now %v\n"time.Now()) 
  11.                 time.Sleep(time.Second * 20) 
  12.         } 

上面是測試的代碼,每隔 20 打印當前時間,編譯執行,同時用 watchmaker 注入 time skew

  1. # ./watchmaker -pid 1970 -sec_delta -300 

隔一段時間間后,再次執行停止執行注入

  1. # ./watchmaker -pid 1970 -sec_delta 0 
  1. # ./test 
  2. start print time 
  3. now 2021-05-26 03:31:46.701902309 +0000 UTC m=+0.000131483 
  4. now 2021-05-26 03:32:06.702230391 +0000 UTC m=+20.000459585 
  5. now 2021-05-26 03:32:26.702406569 +0000 UTC m=+40.000635793 
  6. now 2021-05-26 03:27:46.702688433 +0000 UTC m=+60.000918297 
  7. ^@now 2021-05-26 03:28:06.702914898 +0000 UTC m=+80.001145022 
  8. now 2021-05-26 03:28:26.703120914 +0000 UTC m=+100.001350878 
  9. now 2021-05-26 03:28:46.703398463 +0000 UTC m=+120.001628357 
  10. ^@now 2021-05-26 03:29:06.703707514 +0000 UTC m=+140.001937468 
  11. now 2021-05-26 03:29:26.704025346 +0000 UTC m=+160.002255480 
  12. now 2021-05-26 03:29:46.704302832 +0000 UTC m=+180.002532766 
  13. ^@now 2021-05-26 03:35:06.704505387 +0000 UTC m=+200.002735491 
  14.  
  15. now 2021-05-26 03:35:26.704931111 +0000 UTC m=+220.003161235 

上面是代碼執行的輸出,可以看到 03:32:26 之后時間變成了 03:27:46, 停止注入后恢復

Limits

當前的實現,停止注入,并不會還原 vdso 代碼,也就是說 fakeEntry 會一直存在,每次 clock_gettime 都會跳轉,只不過偏移量為 0 而己

由于以上原因的存在,注入及注入之后的 clock_gettime 都是走的 syscall 系統調用,性能很慢,敏感業務需要重啟,細節可以參考我之前的文章《時鐘源為什么會影響性能》

當前注入,只能針對容器里的主進程,那些 fork 出來,派生出來的無做做到注入

以上限制有沒有優化空間呢?當然有,問題都是用來解決的嘛~

小結

這次分享就這些,以后面還會分享更多的內容,如果感興趣,可以關注并點擊左下角的分享轉發哦(:

參考資料

[1]Chaos Mesh - 讓時間在容器中自由搖擺: https://www.jianshu.com/p/6425050591b7,

[2]官方文檔: https://chaos-mesh.org/docs/chaos_experiments/timechaos_experiment/#limitation,

[3]讓時間在容器中自由搖擺: https://www.jianshu.com/p/6425050591b7,

[4]時鐘源為什么會影響性能: https://mp.weixin.qq.com/s/06SDQLzDprJf2AEaDnX-QQ,

[5]ptrace: https://man7.org/linux/man-pages/man2/ptrace.2.html,

[6]如何實現 go 調試器: https://studygolang.com/articles/12804,

[7]time_linux_amd64.go: https://github.com/chaos-mesh/chaos-mesh/blob/master/pkg/time/time_linux_amd64.go#L72,

[8]ptrace_linux_amd64.go: https://github.com/chaos-mesh/chaos-mesh/blob/master/pkg/ptrace/ptrace_linux_amd64.go#L87,

[9]time_linux_amd64.go#L102 vdso: https://github.com/chaos-mesh/chaos-mesh/blob/master/pkg/time/time_linux_amd64.go#L102,

[10]program.MmapSlice: https://github.com/chaos-mesh/chaos-mesh/blob/master/pkg/time/time_linux_amd64.go#L132,

[11]FakeImage: https://github.com/chaos-mesh/chaos-mesh/blob/master/pkg/time/time_linux_amd64.go#L28,

[12]ptrace.Syscall: https://github.com/chaos-mesh/chaos-mesh/blob/master/pkg/ptrace/ptrace_linux_amd64.go#L251,

[13]JumpToFakeFunc: https://github.com/chaos-mesh/chaos-mesh/blob/master/pkg/ptrace/ptrace_linux_amd64.go#L480,

[14]匯編操作碼: https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md,

 

責任編輯:武曉燕 來源: 董澤潤的技術筆記
相關推薦

2021-09-03 09:06:42

代碼時間開發

2022-06-27 05:48:24

Kubernetes容器

2011-04-11 14:45:15

Oracle表系統時間

2020-09-19 18:03:42

Docker

2018-07-02 06:33:25

物聯網手機漫游網絡

2017-09-18 10:05:15

WindowsLinux容器

2015-08-07 10:10:18

LinuxDocker容器

2013-09-27 10:51:00

Github

2021-03-31 21:20:15

WiFi網絡漫游

2021-04-02 14:23:12

WiFi網絡技術

2018-11-05 14:53:14

Go函數代碼

2014-05-07 09:56:48

Docker管理Linux容器

2019-07-08 08:59:41

Docker容器主機

2014-11-18 00:45:58

UbuntuLXC容器

2018-07-02 09:00:27

Linux特定時間運行命令

2024-10-22 15:10:49

2022-06-22 09:56:19

PythonMySQL數據庫

2019-09-16 13:48:03

Linux管理日志

2021-04-30 13:19:20

Linux刪除分區

2021-07-09 12:37:31

GoPython編程語言
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产精品欧美一区二区三区 | 欧美日韩综合 | 日本三级电影在线免费观看 | 国产日韩电影 | 久久精品一区 | 久久久精选 | 精品久久久久久久久久久久 | 久久免费大片 | 麻豆精品久久久 | 久在草| 日韩乱码一二三 | 精品乱码久久久久 | 五月天激情电影 | 精品在线一区 | 久久久婷 | 日韩一区二区三区视频 | 亚洲一区二区三区在线视频 | 亚洲欧美日韩成人在线 | 久久久蜜桃 | 日韩视频区 | 日韩一区二区三区在线观看视频 | 亚洲国产成人精品一区二区 | 在线观看视频福利 | 国内精品视频一区二区三区 | 亚洲国产成人精品在线 | 国产一区二区免费在线 | 成人在线视频观看 | 青青草一区二区三区 | 国产精品爱久久久久久久 | 日韩中文一区二区三区 | 日本不卡一区 | 国产免费观看一区 | 久在线观看| 国产中文字幕在线观看 | 在线一区 | 91色网站 | 国产日韩一区二区三免费 | 97国产成人| a级片在线观看 | 久久久久久中文字幕 | 国产成人精品免费视频 |