[abtop] Linux 성능 최적화: lsof 대신 /proc/net/tcp 파싱으로 포트 탐색 개선
PR 링크: graykode/abtop#62 상태: Merged | 변경: +None / -None
들어가며
소프트웨어 개발에서 성능 최적화는 사용자 경험 향상과 시스템 자원 효율화의 핵심입니다. 특히 시스템 모니터링 도구는 실시간으로 시스템 정보를 수집해야 하므로, 정보 수집 방식의 효율성이 전체 성능에 지대한 영향을 미칩니다. abtop은 시스템의 프로세스 및 네트워크 상태를 실시간으로 보여주는 도구로, 이번 GitHub Pull Request(PR)는 Linux 환경에서 네트워크 포트 정보를 수집하는 방식을 획기적으로 개선합니다.
기존에는 lsof -i -P -n -sTCP:LISTEN 명령어를 사용하여 LISTEN 상태의 TCP 포트를 찾았습니다. 하지만 이 방식은 lsof 프로세스를 매번 새로 실행(fork+exec)해야 하므로 상당한 오버헤드가 발생했습니다. 이 PR은 lsof 호출을 제거하고 Linux 커널이 제공하는 /proc 파일 시스템의 net/tcp 및 net/tcp6 파일을 직접 파싱하여 동일한 정보를 더 빠르고 효율적으로 얻는 방법을 도입합니다.
이 글에서는 해당 PR의 코드 변경 내용을 상세히 분석하고, 왜 이러한 변경이 성능 향상에 기여하는지, 그리고 이 최적화가 주는 일반적인 교훈은 무엇인지 알아보겠습니다.
코드 분석
이번 PR은 크게 세 부분으로 나눌 수 있습니다. 첫째, Cargo.toml 파일에 Linux 특정 의존성을 추가합니다. 둘째, src/collector/codex.rs 파일에서 scan_proc_fds 함수를 활용하도록 변경합니다. 셋째, src/collector/process.rs 파일에서 Linux 환경에 대한 get_process_info와 get_listening_ports 함수를 /proc 파일 시스템을 사용하도록 재작성합니다.
1. Cargo.toml 변경
diff --git a/Cargo.toml b/Cargo.toml
index 8c6a489..7bfbd04 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -21,6 +21,9 @@
dirs = "6"
chrono = { version = "0.4", features = ["serde"] }
tempfile = "3"
+
+[target.'cfg(target_os = "linux")'.dependencies]
+libc = "0.2"
# The profile that 'dist' will build with
[profile.dist]
Linux 시스템 호출(sysconf 등)을 사용하기 위해 libc 크레이트를 추가했습니다. 이는 해당 기능이 Linux에 특화되어 있음을 명시하는 좋은 방법입니다. #[cfg(target_os = "linux")] 속성을 사용하여 이 의존성이 Linux 빌드에서만 포함되도록 합니다.
2. src/collector/codex.rs 변경
이 변경은 CodexCollector가 프로세스의 파일 디스크립터(FD)를 스캔하는 방식을 개선합니다. 이전에는 /proc/{pid}/fd 디렉토리를 직접 읽고 심볼릭 링크를 따라가며 rollout-*.jsonl 파일을 찾았습니다. 이제는 process::scan_proc_fds(pid)라는 새로운 헬퍼 함수를 사용하여 이 작업을 추상화합니다.
diff --git a/src/collector/codex.rs b/src/collector/codex.rs
index 4c2d28f..6dbe166 100644
--- a/src/collector/codex.rs
+++ b/src/collector/codex.rs
@@ -306,22 +306,13 @@ impl CodexCollector {
#[cfg(target_os = "linux")]
{
for &pid in pids {
- let fd_dir = format!("/proc/{}/fd", pid);
- let entries = match fs::read_dir(&fd_dir) {
- Ok(e) => e,
- Err(_) => continue,
- };
- for entry in entries.flatten() {
- if let Ok(target) = fs::read_link(entry.path()) {
- // Match on the file name component to avoid lossy UTF-8
- // conversion issues on the full path.
- let is_rollout = target.file_name()
- .and_then(|n| n.to_str())
- .is_some_and(|n| n.starts_with("rollout-") && n.ends_with(".jsonl"));
- if is_rollout {
- map.insert(pid, target);
- break;
- }
+ for target in process::scan_proc_fds(pid) {
+ let is_rollout = target.file_name()
+ .and_then(|n| n.to_str())
+ .is_some_and(|n| n.starts_with("rollout-") && n.ends_with(".jsonl"));
+ if is_rollout {
+ map.insert(pid, target);
+ break;
}
}
}
scan_proc_fds 함수는 /proc/{pid}/fd 디렉토리의 모든 항목을 읽고, 각 항목이 가리키는 심볼릭 링크의 실제 경로를 반환합니다. 이 변경 자체는 기능적인 변화보다는 코드의 재사용성과 가독성을 높이는 데 중점을 둡니다. scan_proc_fds는 이후 설명할 get_listening_ports 함수에서도 재사용됩니다.
3. src/collector/process.rs 변경
이 파일은 프로세스 정보 수집과 관련된 핵심 로직을 담고 있으며, 이번 PR에서 가장 큰 변화가 일어난 곳입니다. 특히 get_process_info와 get_listening_ports 함수가 Linux 환경에서 /proc 파일 시스템을 사용하도록 수정되었습니다.
get_process_info 함수
기존에는 ps 명령어를 사용하여 프로세스 정보를 수집했습니다. 이제는 /proc 디렉토리의 /proc/{pid}/stat 및 /proc/{pid}/cmdline 파일을 직접 읽어 동일한 정보를 얻습니다.
diff --git a/src/collector/process.rs b/src/collector/process.rs
index 03d41c7..0b7f9b5 100644
--- a/src/collector/process.rs
+++ b/src/collector/process.rs
@@ -1,4 +1,6 @@
use std::collections::HashMap;
+#[cfg(target_os = "linux")]
+use std::fs;
use std::process::Command;
#[derive(Debug)]
@@ -10,6 +12,96 @@
pub command: String,
}
+/// Resolve all symlinks in /proc/{pid}/fd, returning their targets.
+/// Used by both port discovery (socket inodes) and Codex JSONL discovery.
+#[cfg(target_os = "linux")]
+pub fn scan_proc_fds(pid: u32) -> Vec<std::path::PathBuf> {
+ let fd_dir = format!("/proc/{}/fd", pid);
+ let entries = match fs::read_dir(&fd_dir) {
+ Ok(e) => e,
+ Err(_) => return vec![],
+ };
+ entries.flatten()
+ .filter_map(|e| fs::read_link(e.path()).ok())
+ .collect()
+}
+
+#[cfg(target_os = "linux")]
+pub fn get_process_info() -> HashMap<u32, ProcInfo> {
+ let mut map = HashMap::new();
+
+ let clk_tck = unsafe { libc::sysconf(libc::_SC_CLK_TCK) } as f64;
+ let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as u64;
+
+ let uptime_secs: f64 = fs::read_to_string("/proc/uptime")
+ .ok()
+ .and_then(|s| s.split_whitespace().next()?.parse().ok())
+ .unwrap_or(0.0);
+
+ let entries = match fs::read_dir("/proc") {
+ Ok(e) => e,
+ Err(_) => return map,
+ };
+
+ for entry in entries.flatten() {
+ let name = entry.file_name();
+ let pid: u32 = match name.to_str().and_then(|s| s.parse().ok()) {
+ Some(p) => p,
+ None => continue,
+ };
+
+ // /proc/{pid}/stat - parse fields after (comm)
+ let stat = match fs::read_to_string(format!("/proc/{pid}/stat")) {
+ Ok(s) => s,
+ Err(_) => continue,
+ };
+ // comm can contain spaces/parens, so find last ')'
+ let after_comm = match stat.rfind(')') {
+ Some(pos) if pos + 2 < stat.len() => &stat[pos + 2..],
+ _ => continue,
+ };
+ let fields: Vec<&str> = after_comm.split_whitespace().collect();
+ // fields[0]=state, [1]=ppid, [11]=utime, [12]=stime, [19]=starttime, [21]=rss
+ if fields.len() < 22 {
+ continue;
+ }
+ let ppid: u32 = fields[1].parse().unwrap_or(0);
+ let utime: u64 = fields[11].parse().unwrap_or(0);
+ let stime: u64 = fields[12].parse().unwrap_or(0);
+ let starttime: u64 = fields[19].parse().unwrap_or(0);
+ let rss_pages: u64 = fields[21].parse().unwrap_or(0);
+
+ let rss_kb = rss_pages * page_size / 1024;
+
+ // CPU%: lifetime average (total CPU time / wall time).
+ // This differs from ps's instantaneous %CPU but is sufficient for
+ // abtop's Working/Waiting threshold (cpu_pct > 1.0). A long-idle
+ // process that was busy at startup will show a declining average,
+ // eventually dropping below 1.0 as elapsed time grows.
+ let uptime_ticks = (uptime_secs * clk_tck) as u64;
+ let elapsed_ticks = uptime_ticks.saturating_sub(starttime);
+ let cpu_pct = if elapsed_ticks > 0 {
+ ((utime + stime) as f64 / elapsed_ticks as f64) * 100.0
+ } else {
+ 0.0
+ };
+
+ // /proc/{pid}/cmdline: NUL-separated
+ let command = fs::read_to_string(format!("/proc/{pid}/cmdline"))
+ .unwrap_or_default()
+ .replace('\0', " ")
+ .trim()
+ .to_string();
+ if command.is_empty() {
+ continue; // kernel thread, skip
+ }
+
+ map.insert(pid, ProcInfo { pid, ppid, rss_kb, cpu_pct, command });
+ }
+ map
+}
+
+#[cfg(not(target_os = "linux"))]
pub fn get_process_info() -> HashMap<u32, ProcInfo> {
let mut map = HashMap::new();
let output = Command::new("ps")
/proc/{pid}/stat 파일은 프로세스의 상태, 부모 PID, CPU 사용 시간, 시작 시간 등 다양한 정보를 담고 있습니다. /proc/{pid}/cmdline 파일은 프로세스의 전체 명령줄 인자를 널(NUL) 문자로 구분하여 저장합니다. 이 두 파일을 파싱함으로써 ps 명령어 실행 시 발생하는 fork+exec 오버헤드를 피할 수 있습니다. 특히 CPU 사용률 계산 방식이 ps의 순간적인 값 대신 총 CPU 시간과 경과 시간을 기반으로 한 평균값으로 변경되었는데, 이는 abtop의 임계값(Working/Waiting) 판단에 충분하며 장기적으로 안정적인 지표를 제공합니다.
get_listening_ports 함수
이 함수는 이번 PR의 핵심 변경 사항입니다. 기존에는 lsof -i -P -n -sTCP:LISTEN 명령어를 실행하여 LISTEN 상태의 포트를 찾았습니다. 이제는 /proc/net/tcp와 /proc/net/tcp6 파일을 직접 파싱하고, scan_proc_fds 함수를 재사용하여 소켓의 inode를 통해 해당 포트를 소유한 프로세스를 찾습니다.
diff --git a/src/collector/process.rs b/src/collector/process.rs
index 03d41c7..0b7f9b5 100644
--- a/src/collector/process.rs
+++ b/src/collector/process.rs
@@ -1,4 +1,6 @@
use std::collections::HashMap;
+#[cfg(target_os = "linux")]
+use std::fs;
use std::process::Command;
#[derive(Debug)]
@@ -10,6 +12,96 @@
pub command: String,
}
+/// Resolve all symlinks in /proc/{pid}/fd, returning their targets.
+/// Used by both port discovery (socket inodes) and Codex JSONL discovery.
+#[cfg(target_os = "linux")]
+pub fn scan_proc_fds(pid: u32) -> Vec<std::path::PathBuf> {
+ let fd_dir = format!("/proc/{}/fd", pid);
+ let entries = match fs::read_dir(&fd_dir) {
+ Ok(e) => e,
+ Err(_) => return vec![],
+ };
+ entries.flatten()
+ .filter_map(|e| fs::read_link(e.path()).ok())
+ .collect()
+}
+
+#[cfg(target_os = "linux")]
+pub fn get_process_info() -> HashMap<u32, ProcInfo> {
+ let mut map = HashMap::new();
+
+ let clk_tck = unsafe { libc::sysconf(libc::_SC_CLK_TCK) } as f64;
+ let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as u64;
+
+ let uptime_secs: f64 = fs::read_to_string("/proc/uptime")
+ .ok()
+ .and_then(|s| s.split_whitespace().next()?.parse().ok())
+ .unwrap_or(0.0);
+
+ let entries = match fs::read_dir("/proc") {
+ Ok(e) => e,
+ Err(_) => return map,
+ };
+
+ for entry in entries.flatten() {
+ let name = entry.file_name();
+ let pid: u32 = match name.to_str().and_then(|s| s.parse().ok()) {
+ Some(p) => p,
+ None => continue,
+ };
+
+ // /proc/{pid}/stat - parse fields after (comm)
+ let stat = match fs::read_to_string(format!("/proc/{pid}/stat")) {
+ Ok(s) => s,
+ Err(_) => continue,
+ };
+ // comm can contain spaces/parens, so find last ')'
+ let after_comm = match stat.rfind(')') {
+ Some(pos) if pos + 2 < stat.len() => &stat[pos + 2..],
+ _ => continue,
+ };
+ let fields: Vec<&str> = after_comm.split_whitespace().collect();
+ // fields[0]=state, [1]=ppid, [11]=utime, [12]=stime, [19]=starttime, [21]=rss
+ if fields.len() < 22 {
+ continue;
+ }
+ let ppid: u32 = fields[1].parse().unwrap_or(0);
+ let utime: u64 = fields[11].parse().unwrap_or(0);
+ let stime: u64 = fields[12].parse().unwrap_or(0);
+ let starttime: u64 = fields[19].parse().unwrap_or(0);
+ let rss_pages: u64 = fields[21].parse().unwrap_or(0);
+
+ let rss_kb = rss_pages * page_size / 1024;
+
+ // CPU%: lifetime average (total CPU time / wall time).
+ // This differs from ps's instantaneous %CPU but is sufficient for
+ // abtop's Working/Waiting threshold (cpu_pct > 1.0). A long-idle
+ // process that was busy at startup will show a declining average,
+ // eventually dropping below 1.0 as elapsed time grows.
+ let uptime_ticks = (uptime_secs * clk_tck) as u64;
+ let elapsed_ticks = uptime_ticks.saturating_sub(starttime);
+ let cpu_pct = if elapsed_ticks > 0 {
+ ((utime + stime) as f64 / elapsed_ticks as f64) * 100.0
+ } else {
+ 0.0
+ };
+
+ // /proc/{pid}/cmdline: NUL-separated
+ let command = fs::read_to_string(format!("/proc/{pid}/cmdline"))
+ .unwrap_or_default()
+ .replace('\0', " ")
+ .trim()
+ .to_string();
+ if command.is_empty() {
+ continue; // kernel thread, skip
+ }
+
+ map.insert(pid, ProcInfo { pid, ppid, rss_kb, cpu_pct, command });
+ }
+ map
+}
+
+/// On Linux, parse /proc/net/tcp[6] for LISTEN sockets, then match inodes
+/// via scan_proc_fds. Only scans FDs for PIDs in `known_pids` (from
+/// get_process_info) to avoid scanning all 500+ /proc entries.
+#[cfg(target_os = "linux")]
+pub fn get_listening_ports() -> HashMap<u32, Vec<u16>> {
+ // Step 1: Parse /proc/net/tcp + tcp6 for LISTEN sockets -> inode -> port
+ let mut inode_to_port: HashMap<u64, u16> = HashMap::new();
+ for path in &["/proc/net/tcp", "/proc/net/tcp6"] {
+ if let Ok(content) = fs::read_to_string(path) {
+ for line in content.lines().skip(1) {
+ let fields: Vec<&str> = line.split_whitespace().collect();
+ if fields.len() < 10 || fields[3] != "0A" { // 0A is TCP_LISTEN state
+ continue;
+ }
+ if let Some(port_hex) = fields[1].rsplit(':').next() {
+ if let Ok(port) = u16::from_str_radix(port_hex, 16) {
+ if let Ok(inode) = fields[9].parse::<u64>() {
+ inode_to_port.insert(inode, port);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if inode_to_port.is_empty() {
+ return HashMap::new();
+ }
+
+ // Step 2: Scan FDs of all PIDs for matching socket inodes.
+ // We scan all /proc PIDs rather than just known agent PIDs because
+ // child processes (servers, databases) that own ports may not be in
+ // the agent PID set but are still relevant for orphan port detection.
+ let mut map: HashMap<u32, Vec<u16>> = HashMap::new();
+ let proc_entries = match fs::read_dir("/proc") {
+ Ok(e) => e,
+ Err(_) => return map,
+ };
+
+ for entry in proc_entries.flatten() {
+ let pid: u32 = match entry.file_name().to_str().and_then(|s| s.parse().ok()) {
+ Some(p) => p,
+ None => continue,
+ };
+ for target in scan_proc_fds(pid) {
+ let target_str = target.to_string_lossy();
+ if let Some(inode_str) = target_str
+ .strip_prefix("socket:[")
+ .and_then(|s| s.strip_suffix(']'))
+ {
+ if let Ok(inode) = inode_str.parse::<u64>() {
+ if let Some(&port) = inode_to_port.get(&inode) {
+ map.entry(pid).or_default().push(port);
+ }
+ }
+ }
+ }
+ }
+ map
+}
+
+#[cfg(not(target_os = "linux"))]
pub fn get_listening_ports() -> HashMap<u32, Vec<u16>> {
let mut map: HashMap<u32, Vec<u16>> = HashMap::new();
let output = Command::new("lsof")
@@ -86,9 +240,8 @@ pub fn get_listening_ports() -> HashMap<u32, Vec<u16>> {
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines().skip(1) {
let parts: Vec<&str> = line.split_whitespace().collect();
- let is_tcp_listen = parts.len() >= 9
- && parts[7] == "TCP"
- && line.contains("(LISTEN)");
+ let is_tcp_listen =
+ parts.len() >= 9 && parts[7] == "TCP" && line.contains("(LISTEN)");
if is_tcp_listen {
if let Ok(pid) = parts[1].parse::<u32>() {
if let Some(addr) = parts.get(8) {
/proc/net/tcp 파일은 현재 시스템의 TCP 연결 상태를 보여줍니다. 각 줄은 하나의 TCP 연결 또는 소켓에 대한 정보를 담고 있으며, 필드 중 하나는 소켓의 inode 번호입니다. 0A 상태 코드는 LISTEN 상태를 의미합니다. 이 파일들을 파싱하여 LISTEN 상태인 소켓들의 inode와 포트 번호를 추출합니다. 그 후, /proc/{pid}/fd 디렉토리를 스캔하여 각 프로세스가 열고 있는 파일 디스크립터 중 소켓 inode와 일치하는 것을 찾아 해당 포트를 프로세스에 매핑합니다. 이 과정은 lsof 명령어를 실행하는 것보다 훨씬 적은 시스템 콜(syscall)을 사용하며, 외부 프로세스 실행 없이 커널의 정보를 직접 읽기 때문에 성능이 크게 향상됩니다.
macOS와 같이 /proc 파일 시스템이 없는 환경에서는 기존의 lsof 기반 로직이 #[cfg(not(target_os = "linux"))] 속성을 통해 그대로 유지되어 호환성을 보장합니다.
왜 이게 좋은가?
성능 향상
이 PR의 가장 큰 장점은 Linux 환경에서의 성능 향상입니다. PR 설명에 따르면, 이 변경은 포트 탐색 경로에서 마지막으로 남아있던 fork+exec 오버헤드를 제거합니다. 이전에는 lsof를 호출할 때마다 약 400개의 시스템 콜이 발생했고, 이는 약 30ms의 지연 시간을 유발했습니다. 이제 /proc 파일을 직접 파싱함으로써 이러한 오버헤드가 사라졌습니다.
PR에서 제시된 Phase 1 완료 후 성능 비교표는 다음과 같습니다:
| 작업 | 이전 (fork+exec) | 이후 (/proc 직접 읽기) |
|---|---|---|
ps |
매 2초마다 fork+exec | /proc 직접 읽기 |
lsof (Codex) |
매 tick마다 fork+exec | /proc/pid/fd readlink |
lsof (ports) |
매 5틱마다 fork+exec | /proc/net/tcp 파싱 |
| 총합 | ~1500 syscalls, ~50ms | ~200 syscalls, ~5ms |
보시다시피, lsof를 사용한 포트 탐색 방식이 /proc 파일 파싱으로 대체되면서 총 시스템 콜 수가 약 1500개에서 200개로, 소요 시간은 약 50ms에서 5ms로 약 10배 가까이 단축되었습니다. 이는 abtop이 더 적은 리소스로 더 빠르게 시스템 정보를 업데이트할 수 있음을 의미합니다.
일반적인 교훈
- 운영체제별 최적화 활용: Linux는
/proc파일 시스템과 같이 커널 내부 정보를 사용자 공간에서 접근할 수 있는 강력한 메커니즘을 제공합니다. 이러한 OS별 특성을 이해하고 활용하면 외부 명령어 실행보다 훨씬 효율적인 데이터 수집이 가능합니다.#[cfg]속성을 사용하여 OS별로 다른 구현을 제공하는 것은 이식성과 성능을 동시에 잡는 좋은 전략입니다. - 외부 프로세스 호출 최소화:
ps,lsof,netstat등 외부 명령어를 사용하는 것은 편리하지만,fork+exec오버헤드로 인해 성능 병목이 될 수 있습니다. 특히 자주 호출되는 로직이라면, 해당 기능에 상응하는 시스템 콜이나/proc과 같은 커널 인터페이스를 직접 사용하는 것을 고려해야 합니다. - 코드 재사용 및 추상화:
scan_proc_fds와 같이 파일 디스크립터 스캔 로직을 별도 함수로 분리하고 재사용함으로써 코드 중복을 줄이고 유지보수성을 높였습니다. 이는abtop의 여러 기능(Codex, 포트 탐색)에서 공통적으로 필요한 작업이었기에 더욱 효과적입니다. - 명확한 성능 측정 및 비교: PR에서 제공된 성능 비교표는 변경의 효과를 명확하게 보여줍니다.
Before와After의 수치를 제시함으로써 변경의 가치를 객관적으로 증명하고, 리뷰어들이 변경의 중요성을 쉽게 인지하도록 돕습니다.
리뷰 피드백 반영
이 PR은 이전 PR(#60, #61)에 의존하는 세 번째 파트입니다. 리뷰 댓글은 제공되지 않았지만, PR 설명에서 "Phase 1 complete"라고 언급된 것으로 보아 이전 단계에서 관련 로직들이 점진적으로 개선되었음을 알 수 있습니다. 특히 scan_proc_fds 함수가 이전 PR에서 도입되어 CodexCollector와 get_listening_ports에서 재사용되는 구조는 코드의 모듈화와 점진적 개선이 잘 이루어졌음을 시사합니다.
결론
이번 PR은 abtop의 Linux 환경에서의 포트 탐색 성능을 획기적으로 개선했습니다. lsof 명령어 대신 /proc/net/tcp 파일을 직접 파싱하는 방식을 채택함으로써, 외부 프로세스 호출에 따른 오버헤드를 제거하고 시스템 콜 수를 대폭 줄였습니다. 이는 시스템 모니터링 도구에서 성능 최적화가 얼마나 중요한지를 보여주는 좋은 사례이며, 운영체제의 내부 메커니즘을 이해하고 활용하는 것의 가치를 다시 한번 강조합니다. 이와 같은 최적화는 abtop이 더 빠르고 효율적으로 사용자에게 시스템 정보를 제공하는 데 크게 기여할 것입니다.
참고 자료
⚠️ 알림: 이 분석은 AI가 실제 코드 diff를 기반으로 작성했습니다.
관련 포스트
- [onnxruntime] ONNX Runtime 스레드 풀의 지능형 대기: Exponential Backoff 도입으로 성능 및 전력 효율성 향상
- [sglang] SGLang 고성능 서빙: 비동기 알림 배치 처리와 SSE 고속 경로 최적화 분석
- [abtop] Codex 세션 파일 검색 성능 개선: lsof 대신 /proc/pid/fd 활용
- [ollama] Ollama MLX Gemma4 성능 최적화: Fused Operations를 통한 효율성 증대
- [sglang] sglang, AMD MI35x 환경에서 GLM-5-MXFP4 모델의 성능 및 정확도 테스트 추가
PR Analysis 의 다른글
- 이전글 [vllm] vLLM의 콜드 스타트 성능을 20% 향상시키는 비동기 최적화 기법
- 현재글 : [abtop] Linux 성능 최적화: lsof 대신 /proc/net/tcp 파싱으로 포트 탐색 개선
- 다음글 [abtop] Codex 세션 파일 검색 성능 개선: lsof 대신 /proc/pid/fd 활용
댓글