調試學習1)
一個進程可以簡單想成Process │ ├── PID ├── 虛擬內存空間 ├── 文件描述符 ├── socket ├── 環(huán)境變量 ├── 當前工作目錄 ├── 信號狀態(tài) └── 一個或多個線程所以以后你排查一個機器人節(jié)點時其實是在看這個進程 用了多少 CPU 用了多少內存 開了多少 socket 開了多少文件 有多少線程 收到了什么 signal這就是 Linux 系統(tǒng)工程的核心視角。1、進程之間怎么通信既然兩個進程內存默認隔離那mocap_driver_node怎么把數據給gmr_retarget_node就需要IPC Inter-Process Communication 進程間通信常見pipe socket shared memory message queue而 ROS2 Topic 本質上也是在幫不同進程之間進行通信只不過它通過 DDS / RMW 做了大量封裝。所以進程隔離 ↓ 不能直接隨便訪問對方內存 ↓ 需要 IPC對比進程線程內存空間相對獨立同進程共享創(chuàng)建開銷較大較小通信IPC直接共享內存故障隔離較強較弱數據共享麻煩一些容易但要同步崩潰影響通常本進程可能拖垮整個進程舉個最直觀的例子Thread A 野指針崩了整個Process通常都會崩。不是只死 Thread A。例如你看到CPU 100%先問哪個進程用top ps再問這個進程哪個線程用top -H然后再哪個函數用perf于是完整鏈路系統(tǒng)異常 ↓ 進程 ↓ 線程 ↓ 函數這就是前面 perf 那課已經碰到的思路。查看自己的 shell PID在 Bashecho $$會輸出例如2450意思當前 shell 進程的 PID 2450這個$$以后寫 shell 腳本也會見到。查看 PID / PPID可以ps -ef看到UID PID PPID CMD skr 3000 2000 bash skr 3100 3000 ./robot_control說明robot_control PID 3100 父進程 PID 3000也就是 bash。join()為什么重要假設std::thread worker(receiveLoop);main 要退出前worker.join();意思大概就是等 worker 真正結束。之前 TSan 課講過通知停止 ↓ 線程退出 ↓ join ↓ 釋放資源現(xiàn)在你應該理解得更具體了。進程狀態(tài)ps/top中可能看到R S D T Z現(xiàn)在先認識最常見幾個。R Running / Runnable正在跑或者準備被調度。S Sleeping等待事件比如socket數據 condition variable timer非常正常。D Uninterruptible Sleep通常和某些 I/O 等待有關。Z Zombie 僵尸進程后面會講。什么時候更適合進程什么時候線程先簡單記更適合多進程如果你重視故障隔離 模塊邊界 獨立重啟 獨立部署例如mocap_driver_node gmr_node robot_output_node更適合多線程如果你重視低延遲共享數據 同一模塊內并發(fā) 避免進程間序列化例如同一個 driver 內 receive thread processing thread現(xiàn)實系統(tǒng)經常多進程 每個進程內部多線程。而不是二選一。2、signal一句話signal Linux 內核或其他進程給某個進程發(fā)送的一種異步通知。重點掌握SIGINT SIGTERM SIGKILL SIGSEGV SIGABRT SIGHUPSIGINTCtrlC你在終端運行./robot_control然后按Ctrl C終端通常會給前臺進程發(fā)送SIGINTINTInterrupt所以CtrlC → SIGINT它的意思大概是用戶請求中斷程序。終端發(fā) SIGINT ↓ ROS2 runtime 收到 ↓ 開始 shutdown ↓ executor 退出 ↓ node 析構 ↓ 進程結束所以CtrlC其實就是 signal 機制在工作。SIGTERM請求進程正常終止執(zhí)行kill 4321默認發(fā)SIGTERMTERMTerminate可以理解請你盡快正常結束。進程可以捕獲 SIGTERM然后停止接收新任務 ↓ 停止控制循環(huán) ↓ 關閉 socket ↓ 斷開 EtherCAT ↓ 保存日志 ↓ 釋放資源 ↓ 退出所以對于機器人服務SIGTERM通常比 SIGKILL 更友好。SIGKILL真正的“強殺”執(zhí)行kill -9 4321這里9通常就是SIGKILLSIGKILL 非常特殊進程不能捕獲、不能忽略、不能處理。Linux 直接終止它。所以kill -9確實很強程序沒有機會釋放資源 保存數據 關閉設備 safe disable 寫最后日志 join 線程SIGSEGV段錯誤這個你前面已經見過。SIGSEGV通常表示非法訪問內存。例如int* p nullptr; *p 10;程序可能SIGSEGV ↓ Segmentation fault ↓ core dump然后你gdb core dump ASan排查。所以現(xiàn)在能串起來非法內存訪問 ↓ 內核發(fā)現(xiàn) ↓ 向進程發(fā)送 SIGSEGV ↓ 默認動作終止 ↓ 可能生成 core dumpSIGABRTSIGABRTAbort一般意味著程序主動或運行庫要求“異常終止”。例如std::abort();會觸發(fā)SIGABRT另外某些嚴重運行時錯誤也可能最終導致SIGABRTSIGSEGV更多想到非法內存訪問而SIGABRT更多想到程序/運行庫主動終止SIGHUPHangup歷史上和終端斷開有關?,F(xiàn)代 Linux 服務里經常把它用作重新加載配置例如某些 daemonkill -HUP PID不會完全退出而是重新讀取配置文件是不是這樣取決于具體程序。所以signal 的名字是一回事程序怎么處理又是另一回事。怎么查看 signal 編號可以kill -l會看到1 SIGHUP 2 SIGINT ... 9 SIGKILL ... 11 SIGSEGV 15 SIGTERM你不需要全部記。重點2 SIGINT 9 SIGKILL 11 SIGSEGV 15 SIGTERM以后看到exit signal 11你應該反應SIGSEGVkill -TERM有什么區(qū)別CtrlCSIGINTkill PIDSIGTERM它們都可以請求退出但來源和語義稍有不同。通常SIGINT 交互用戶中斷 SIGTERM 系統(tǒng)/管理工具請求終止例如systemd stop通常更接近SIGTERM所以服務程序一定應該認真處理 SIGTERM。怎么找到某個進程 PID最基本ps -ef | grep robot_control也可以pgrep robot_control如果想看詳細pgrep -a robot_control例如4321 ./robot_control --config robot.yamlsignal 和 core dump 連起來有些 signal 默認動作會終止進程 生成 core例如常見SIGSEGV SIGABRT所以程序崩了 ↓ 看日志里的 signal ↓ 判斷大方向 ↓ coredumpctl ↓ gdb這就是一條完整故障分析鏈。3、socketsocket 進程和網絡協(xié)議棧之間的一個通信端點。例如你的程序 ↓ socket ↓ Linux 內核網絡協(xié)議棧 ↓ 網卡 ↓ 網絡程序不會直接“控制網卡發(fā) UDP”。它通常是創(chuàng)建 socket ↓ bind / connect ↓ send / recv ↓ 內核幫你處理協(xié)議和網卡socket() ↓ 內核創(chuàng)建 socket 對象 ↓ 返回一個 fd ↓ 程序以后通過 fd 操作它例如recv(fd, ...); send(fd, ...); close(fd);這就是 Linux 很經典的Everything is a file-like interface不是說 socket 真的是磁盤文件而是 Linux 用統(tǒng)一的 fd 接口管理很多內核資源。怎么查誰占用了端口以后非常實用。例如ss -lunp看 UDP?;蛘遱s -ltnp看 TCP。例如可能看到udp UNCONN 0 0 0.0.0.0:7014 ... users:((mocap,pid4321,fd7))這里你一下就看到port 7014 PID 4321 fd 7這三個概念全串起來了。socket buffer 是什么Linux 內核會給 socket 維護發(fā)送緩沖區(qū) 接收緩沖區(qū)例如 UDP 包到了以后不是一定立刻進你的用戶代碼。而是先進入內核 socket receive buffer然后你recvfrom()再把數據從內核拿到用戶空間。如果你處理不夠快buffer 越積越多。最后滿可能開始丟包。EINTR一個阻塞系統(tǒng)調用被 signal 打斷了。例如recv() ↓ 正在阻塞 ↓ SIGINT到達 ↓ recv返回錯誤 ↓ errno EINTR所以signal和socket blocking I/O也會互相影響。這就是為什么 Linux 系統(tǒng)知識必須串著學。Network ↓ NIC ↓ Kernel Network Stack ↓ Socket Receive Buffer ↓ fd ↓ recvfrom() ↓ User Buffer ↓ Queue ↓ Worker這就是機器人系統(tǒng)工程師應該逐漸形成的層次感。socket 內核提供的通信對象socket() → 返回 fd以及blocking 沒數據時線程等待 non-blocking 沒數據立即返回4、file descriptorfd 進程用來引用某個已打開內核對象的整數編號。例如int fd open(config.txt, O_RDONLY);可能返回fd 3或者int sock socket(AF_INET, SOCK_DGRAM, 0);可能返回sock 4這里3 4都叫file descriptor簡稱fd假設fd 5這個5本身不是文件也不是 socket。它只是當前進程里的一個“索引號”??梢韵胂蟪蒔rocess │ ├── fd 0 → stdin ├── fd 1 → stdout ├── fd 2 → stderr ├── fd 3 → config.txt ├── fd 4 → UDP socket └── fd 5 → device所以fd 進程內部訪問內核資源的句柄open比如int fd open(/tmp/data.txt, O_RDONLY);大概可以理解應用調用 open() ↓ 進入內核 ↓ 內核找到文件 ↓ 建立打開狀態(tài) ↓ 給進程分配一個 fd ↓ 返回整數以后read(fd, ...)Linux 就知道這個 fd 對應哪個打開的對象。用完以后close(fd);意思我這個進程不再需要這個 fd 了。如果一直不 closefd 會一直占用這就叫fd leak 文件描述符泄漏怎么查看一個進程打開了哪些 fdLinux 非常好用ls -l /proc/PID/fd例如ls -l /proc/4321/fd可能看到0 - /dev/pts/0 1 - /dev/pts/0 2 - /dev/pts/0 3 - socket:[123456] 4 - /tmp/robot.log 5 - socket:[123789]這非常直觀。如果你懷疑程序 fd 泄漏可以ls /proc/4321/fd | wc -l得到28過 10 分鐘120再過 10 分鐘300那就非??梢伞H绻麛的恳恢痹鲩L不下降說明可能有 fd 泄漏。另一個非常常用的工具lsof -p 4321lsoflist open files它可以列出進程打開的普通文件 socket pipe 設備 共享庫非常適合查這個進程到底打開了什么fd 是進程內部編號。所以fd 3單獨拿出來沒有意義。必須結合哪個進程一起看。PID fd 才能真正定位資源所以你以后經??吹絇ID 4321 fd 7意思是進程 4321 的第 7 號文件描述符。這就是為什么ss -lunp可能顯示pid4321,fd7現(xiàn)在你應該能完全看懂了。同一個進程的線程共享 fd 表。比如Thread A 創(chuàng)建 socket fd7Thread B 通常也可以使用 fd7因為fd table屬于進程資源。這也意味著Thread A close(fd)而 Thread B 正在recv(fd)可能產生復雜的并發(fā)行為。所以 fd 生命周期也要同步。shutdown()和close()TCP socket 經常見shutdown(fd, SHUT_RDWR); close(fd);簡單理解shutdown 關閉 socket 通信方向例如不再讀 不再寫而close 當前進程釋放這個 fd兩者不是同一個概念?,F(xiàn)階段知道區(qū)別即可不需要深入 TCP 狀態(tài)機。5、pollpoll 把一組 fd 交給內核問“這里面哪些現(xiàn)在可以讀、可以寫、或者出錯了”例如fd 3 → UDP A fd 4 → UDP B fd 5 → TCP fd 6 → pipe你不需要輪流 recv 輪流試 輪流空轉而是poll() ↓ 線程睡眠 ↓ 某個 fd 有事件 ↓ 內核喚醒線程 ↓ 你處理對應 fd沒有任何 fd 有事 ↓ 線程睡眠只有數據來了 連接斷了 超時了才醒。所以poll/epoll本質是在做“事件驅動等待”。POLLIN這個 fd 現(xiàn)在有數據可以讀。比如UDP 包到了 ↓ POLLIN或者 TCP對端數據到了 ↓ POLLIN程序看到if (fds[i].revents POLLIN) { recv(...); }就去讀。poll timeout例如poll(fds, n, 1000);情況 1某個 fd 很快有事件比如 20 ms 后來了數據20ms ↓ poll 立即返回不會傻等 1 秒。情況 21 秒都沒事件poll 返回 0表示timeout。情況 3出錯poll 返回負數再看errnoepollepoll 是 Linux 專門為大量 fd 事件管理設計的一套機制。你可以先理解poll 每次把整組 fd 拿去問內核 epoll 先把要關心的 fd 注冊給內核 以后只等“哪些 fd 真有事件”這是最重要的區(qū)別之一。epoll 常見三步① epoll_create ↓ 創(chuàng)建 epoll 實例 ② epoll_ctl ↓ 把 fd 注冊進去 ③ epoll_wait ↓ 等待事件也就是說創(chuàng)建一個“事件管理器” ↓ 告訴它我關心哪些 fd ↓ 以后一直問 誰有事fd 數量少 邏輯簡單 → poll 很合適 大量 fd 高并發(fā)連接 → epoll 更適合最常見EPOLLIN表示可讀還有EPOLLOUT表示可寫以及EPOLLERR表示錯誤EPOLLHUP表示掛斷 / 對端關閉相關事件現(xiàn)在先認識即可。LT 和 ETepoll 里以后很容易聽到Level Triggered LT和Edge Triggered ET先用大白話理解。LT只要還有數據就一直提醒你假設 socket 里有 1000 bytes你只讀100 bytes還剩900 bytesLT 模式下下次 epoll_wait ↓ 還會告訴你 這個 fd 可讀因為里面還有數據這比較容易寫對。ET狀態(tài)變化時重點提醒一次ET 更像剛才沒數據 ↓ 現(xiàn)在有數據 ↓ 提醒你如果你收到事件以后只讀了一點剩下的數據可能不會像 LT 那樣持續(xù)提醒你。所以 ET 常要求non-blocking 一直讀到 EAGAIN也就是一次事件來了 ↓ 把當前能讀的數據盡量讀干凈LT 只要條件還成立就繼續(xù)提醒ET 更強調狀態(tài)邊沿變化需要更謹慎處理實際項目里先把 LT 寫對比為了“高級”強上 ET 更重要。poll / epoll 和 blocking/non-blockingpoll/epoll non-blocking socket為什么因為先用 poll/epoll 等待 ↓ 確認 fd 有事件 ↓ 再 read/recv如果某些邊界情況下狀態(tài)已經變了non-blocking 可以避免線程意外卡死在 read尤其 ET 模式里更常見。eventfd先認識概念。Linux 可以創(chuàng)建一種eventfd它本身也是fd可以用來線程間通知事件例如main thread ↓ 要停止 receiver ↓ 向 eventfd 寫一下而 receiver 線程epoll_wait()同時等socket fd eventfd于是數據來了 → 處理數據 停止事件來了 → 退出線程這個設計非常漂亮。timerfdLinux 還有timerfd它也是 fd。你可以創(chuàng)建一個定時器 ↓ 定時到期 ↓ 這個 fd 變成可讀于是 epoll 可以同時等socket timer event全部統(tǒng)一成 fd。這就是 Linux fd 抽象很強的地方。只解決怎么高效等多個 I/O 事件。如果收到一幀 ↓ 處理 50ms那你的 event loop 還是被拖住。常見架構epoll thread ↓ 快速收數據 ↓ 投遞 queue ↓ worker thread處理所以I/O multiplexing和task parallelism是兩件事。I/O multiplexing這就是 poll/epoll 所屬的大類I/O multiplexing I/O 多路復用意思不是同時傳很多路數據而是一個執(zhí)行線程同時管理多個 I/O fd 的就緒事件。這個詞面試很常見。select 老牌方案有 fd 數量/接口方面限制poll 接口更直接沒有 select 那種固定 fd_set 限制epoll Linux 特有更適合大量 fd 高并發(fā)事件管理不要現(xiàn)在深挖每個內核復雜度。怎么判斷 poll/epoll 問題比如進程CPU 100%strace 發(fā)現(xiàn)一直recvfrom(...) -1 EAGAIN recvfrom(...) -1 EAGAIN recvfrom(...) -1 EAGAIN瘋狂刷。這通常說明non-blocking socket busy polling可能需要poll/epoll而不是不停重試。6、CPU以后top里可能看到us sy id wa先認識最重要幾個。us user用戶態(tài)代碼占用。比如GMR Eigen protobuf 你的 C 邏輯sy system內核態(tài) CPU。比如socket 系統(tǒng)調用 文件 I/O 網絡棧 驅動id idleCPU 空閑。wa I/O waitCPU 在等待某些 I/O 完成相關狀態(tài)?,F(xiàn)階段先有這個概念即可。上下文切換是什么假設 CPU 只有一個核?,F(xiàn)在Thread A Thread B Thread C都想運行。CPU 不可能真正同時跑三個。于是跑 A ↓ 保存 A 狀態(tài) ↓ 跑 B ↓ 保存 B 狀態(tài) ↓ 跑 C這就叫context switch 上下文切換多線程不是免費的。線程太多可能帶來調度開銷 cache miss 上下文切換所以線程不是越多越快。memory程序內存一個進程的內存可以粗略想成Process Memory │ ├── code ├── stack ├── heap ├── shared libraries ├── mmap └── shared memory你現(xiàn)在不用背內存布局。重點是理解幾個常見指標VIRT RES / RSS SHRVIRTVirtual Memory簡單理解進程擁有或映射的虛擬地址空間總量。例如VIRT 8 GB不代表真的用了 8 GB 物理內存因為里面可能包括共享庫 mmap 預留地址空間 尚未實際觸碰的內存所以不要看到 VIRT 很大就直接判斷內存泄漏。RSSResident Set Sizetop里經常叫RES它更接近當前真正駐留在物理 RAM 里的進程內存。所以如果你排查程序是不是實際越來越吃內存通常RSS / RES比VIRT更有參考價值。SHRShared Memory簡單理解其中有一部分內存是可共享的。例如動態(tài)庫代碼頁 共享內存多個進程可能共同使用。所以RES也不能簡單相加就認為等于整機實際獨占內存。內存統(tǒng)計比 CPU 稍微復雜。OOMOut Of Memory內存耗盡。Linux 在極端情況下可能啟動OOM Killer選擇某些進程殺掉以保證系統(tǒng)繼續(xù)運行。日志里可能看到Killed process xxxx Out of memory機器人上如果控制程序被 OOM Killer 干掉控制進程突然消失所以生產系統(tǒng)要關注內存邊界。swap如果 RAM 不夠Linux 可能把部分內存頁交換到swap可以簡單理解RAM 不夠 ↓ 把一部分暫時不用的頁面放到磁盤普通桌面系統(tǒng)這是正常機制。但實時控制非常不喜歡page 被 swap out ↓ 實時線程突然訪問 ↓ 需要從磁盤調回來 ↓ 延遲巨大這就是后面為什么要學mlockallCPU cacheCPU 比 RAM 快很多。所以現(xiàn)代 CPU 有L1 L2 L3 cache如果數據剛好在 cache訪問快如果總是cache miss就要去更慢的內存層級。所以性能問題不只是“這個函數做了多少次加法”還包括數據布局 內存訪問模式 cache locality這就是為什么連續(xù)數組通常很高效。怎么查看進程內存很簡單。例如ps -o pid,%cpu,%mem,rss,vsz,cmd -p 4321可能看到PID %CPU %MEM RSS VSZ 4321 80.0 2.5 ... ...其中RSS通常單位是 KB。CPU / memory 排障可以形成這個流程程序變慢① 看 CPU ↓ 哪個進程高 ② 看線程 ↓ 哪個線程高 ③ perf ↓ 哪個函數高如果CPU 不高 但機器很卡再看memory swap I/O wait page fault如果內存持續(xù)增長再RSS趨勢 ↓ ASan/LSan/Valgrind ↓ 代碼生命周期CPU 100% ≈ 一個邏輯核滿載CPU 100% ≈ 多線程用了多個核VIRT 虛擬地址空間RSS / RES 更接近實際駐留物理內存