มาสร้างโปรเจกต์ร่วมกันกันเถอะ!
เครื่องมือ Linux เช่น ps, top และ netstat เป็นเครื่องมือที่ยอดเยี่ยม พวกมันให้ข้อมูลมากมายเกี่ยวกับสิ่งที่กำลังเกิดขึ้นในระบบของคุณ
- แต่มันทำงานอย่างไร?
- มันดึงข้อมูลมาจากไหน?
- เราสามารถใช้ข้อมูลเหล่านี้สร้างเครื่องมือของตัวเองได้อย่างไร?
ในบทความนี้ เราจะสร้างเครื่องมือ Linux 3 ตัวที่นิยมใช้ร่วมกัน คุณจะได้เรียนรู้ 2 เรื่องใน 1 ครั้ง: ทั้งเทคนิค Ruby และความรู้ Linux ที่มีประโยชน์พร้อมกัน! 🙂
ค้นหาข้อมูลสถานะ
ลองตอบคำถามว่าเครื่องมือเหล่านี้หาข้อมูลจากที่ไหน คำตอบอยู่ที่ proc filesystem!
ถ้าคุณดูภายในไดเรกทอรี /proc จะเห็นเป็นชุดของไดเรกทอรีและไฟล์ คล้ายกับไดเรกทอรีอื่นๆ บนคอมพิวเตอร์ของคุณ แต่นี่ไม่ใช่ไฟล์จริงๆ มันเป็นเพียงวิธีที่ Linux kernel ใช้เปิดเผยข้อมูลให้ผู้ใช้ ซึ่งสะดวกมากเพราะสามารถอ่านได้เหมือนไฟล์ทั่วไปโดยไม่ต้องใช้เครื่องมือพิเศษ ในโลก Linux หลายอย่างทำงานแบบนี้ เช่นลองดูที่ /dev directory
ตอนนี้เราเข้าใจแล้ว มาดูเนื้อหาของ /proc directory...
1 10 104 105 11 11015 11469 11474 11552 11655
นี่เป็นเพียงตัวอย่างเล็กน้อย แต่คุณจะสังเกตรูปแบบได้เร็ว ตัวเลขเหล่านี้คืออะไร?
ได้คำตอบแล้ว: นี่คือ PIDs (Process IDs) ทุก entry มีข้อมูลเกี่ยวกับ process เฉพาะเจาะจง ถ้ารัน ps จะเห็นว่าแต่ละ process มี PID ที่เชื่อมโยง:
PID TTY TIME CMD 15952 pts/5 00:00:00 ps 22698 pts/5 00:00:01 bash
จากนี้เราสรุปได้ว่า ps ทำงานโดยวนลูปผ่าน /proc directory และพิมพ์ข้อมูลที่พบ มาดูว่ามีอะไรอยู่ในหนึ่งในไดเรกทอรีที่มีตัวเลข:
attr autogroup auxv cgroup clear_refs cmdline comm cpuset cwd environ exe fd
นี่เป็นตัวอย่างบางส่วน เพื่อประหยัดพื้นที่ แต่ขอแนะนำให้ดูรายการเต็ม นี่คือ entry ที่น่าสนใจ:
| Entry | คำอธิบาย |
|---|---|
| comm | ชื่อของโปรแกรม |
| cmdline | คำสั่งที่ใช้เปิด process นี้ |
| environ | ตัวแปรสภาพแวดล้อมที่ process เริ่มต้นด้วย |
| status | สถานะ process (running, sleeping...) และการใช้หน่วยความจำ |
| fd | ไดเรกทอรีที่มี file descriptors (ไฟล์เปิด, sockets...) |
ตอนนี้เรารู้แล้ว ลองเริ่มเขียนเครื่องมือกัน!
วิธีแสดงรายการโปรแกรมที่กำลังทำงาน
เริ่มจากการดึงรายการไดเรกทอรีทั้งหมดภายใต้ /proc โดยใช้ class Dir:
Dir.glob("/proc/[0-9]*")
สังเกตว่าฉันใช้ช่วงตัวเลข เพราะมีไฟล์อื่นๆ ใน /proc ที่เราไม่สนใจ ตอนนี้เราต้องการเฉพาะไดเรกทอรีที่เป็นตัวเลข ต่อมาเราสามารถวนลูปและพิมพ์ 2 คอลัมน์: PID และชื่อโปรแกรม
pids = Dir.glob("/proc/[0-9]*")
puts "PID\tCMD"
puts "-" * 15
pids.each do |pid|
cmd = File.read(pid + "/comm")
pid = pid.scan(/\d+/).first
puts "#{pid}\t#{cmd}"
end
ผลลัพธ์:
PID CMD --------------- 1 systemd 2 kthreadd 3 ksoftirqd/0 5 kworker/0 7 migration/0 8 rcu_preempt 9 rcu_bh 10 rcu_sched
เฮ้ย! เราเพิ่งสร้าง ps ขึ้นมา แม้จะไม่รองรับ options ทั้งหมดของต้นฉบับ แต่เราสร้างอะไรบางอย่างที่ทำงานได้แล้ว!
ใครกำลัง Listen อยู่?
ลองเลียนแบบ netstat ซึ่ง output แบบนี้ (ใช้ flags -ant):
Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State tcp 0 0 127.0.0.1:5432 0.0.0.0:* LISTEN tcp 0 0 192.168.1.82:39530 182.14.172.159:22 ESTABLISHED
ข้อมูลนี้อยู่ที่ไหน? ถ้าคุณตอบ /proc ถูกต้อง! อย่างเฉพาะเจาะจงอยู่ที่ /proc/net/tcp แต่มีปัญหาเล็กน้อย: มันไม่ได้ดูเหมือน output ของ netstat เลย!
0: 0100007F:1538 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1001 0 9216 1: 2E58A8C0:9A6A 9FBB0EB9:0016 01 00000000:00000000 00:00000000 00000000 1000 0 258603
หมายความว่าเราต้องทำ parsing ด้วย regular expressions ตอนนี้มาจัดการแค่ local address และ status ก่อน
Regex ที่ผมสร้าง:
LINE_REGEX = /\s+\d+: (?<local_addr>\w+):(?<local_port>\w+) \w+:\w+ (?<status>\w+)/
จะได้ค่า hexadecimal ที่ต้องแปลงเป็น decimal มาสร้าง class เพื่อจัดการ:
class TCPInfo
def initialize(line)
@data = parse(line)
end
def parse(line)
line.match(LINE_REGEX)
end
def local_port
@data["local_port"].to_i(16)
end
# แปลง hex เป็น IP notation ปกติ
def local_addr
decimal_to_ip(@data["local_addr"].to_i(16))
end
STATUSES = {
"0A" => "LISTENING",
"01" => "ESTABLISHED",
"06" => "TIME_WAIT",
"08" => "CLOSE_WAIT"
}
def status
code = @data["status"]
STATUSES.fetch(code, "UNKNOWN")
end
# อาจารย์คณิตศาสตร์ไบนารี - อย่ากังวลมาก
def decimal_to_ip(decimal)
ip = []
ip << (decimal >> 24 & 0xFF)
ip << (decimal >> 16 & 0xFF)
ip << (decimal >> 8 & 0xFF)
ip << (decimal & 0xFF)
ip.join(".")
end
end
สิ่งที่เหลือคือพิมพ์ผลลัพธ์ในรูปแบบตารางที่สวยงาม:
require 'table_print' tp connections
ตัวอย่าง output:
STATUS | LOCAL_PORT | LOCAL_ADDR ------------|------------|-------------- LISTENING | 5432 | 127.0.0.1 ESTABLISHED | 39530 | 192.168.88.46
Gem นี้เก๋จริงๆ! เพิ่งรู้จัก และดูเหมือนจะไม่ต้องมานั่งลุยกับ ljust / rjust อีกต่อไป 🙂
หยุดใช้ Port ของผม!
เคยเห็นข้อความนี้ไหม?
Address already in use - bind(2) for "localhost" port 5000
อึม... ตัวไหนใช้ port นี้อยู่? มาหาลองดู:
fuser -n tcp -v 5000 PORT USER PID ACCESS CMD 5000/tcp rubyguides 30893 F.... nc
เจอแล้ว! ตอนนี้เราสามารถหยุดโปรแกรมนั้นได้ถ้าไม่ต้องการให้มันรันต่อ ซึ่งจะปลด port ออกมา โปรแกรม fuser หาได้อย่างไร? คุณเดาถูก! /proc filesystem อีกครั้ง มันรวม 2 เรื่องที่เราเรียนมา: การเดินผ่าน process list และการอ่าน active connections จาก /proc/net/tcp
เราต้องการขั้นตอนเพิ่มเติมอีก 1 ขั้น: หาวิธี match ข้อมูล open port กับ PID จากข้อมูล TCP ใน /proc/net/tcp ไม่มี PID แต่เราสามารถใช้ inode number ได้
"Inode คือโครงสร้างข้อมูลที่ใช้แทน filesystem object" — Wikipedia
เราจะใช้ inode หา process ที่ตรงกันอย่างไร? ถ้าดูที่ fd directory ของ process ที่รู้ว่ามี open port จะเจอบรรทัดแบบนี้:
/proc/3295/fd/5 -> socket:[12345]
ตัวเลขในวงเล็บคือ inode number ตอนนี้แค่วนลูปหาไฟล์ทั้งหมด จะเจอ process ที่ตรงกัน
วิธีทำหนึ่งแบบ:
x = Dir.glob("/proc/[0-9]*/fd/*").find do |fd|
File.readlink(fd).include? "socket:[#{socket_inode}]"
rescue nil
end
pid = x.scan(/\d+/).first
name = File.readlink("/proc/#{pid}/exe")
puts "Port #{hex_port.to_i(16)} in use by #{name} (#{pid})"
ตัวอย่าง output:
Port 5432 in use by /usr/bin/postgres (474)
หมายเหตุ: คุณต้องรันโค้ดนี้เป็น root หรือเป็นเจ้าของ process มิฉะนั้นจะอ่านรายละเอียด process ใน /proc ไม่ได้
สรุป
ในบทความนี้คุณเรียนรู้ว่า Linux เปิดเผยข้อมูลมากมายผ่าน virtual /proc filesystem และเรียนรู้วิธีสร้างเครื่องมือ Linux ยอดนิยมอย่าง ps, netstat และ fuser โดยใช้ข้อมูลภายใต้ /proc
อย่าลืมสมัครรับ newsletter ด้านล่างเพื่อไม่พลาดบทความถัดไป! 🙂