Обзор
Инструменты ps
, top
и netstat
крутые, они дают много полезной информации о том, что происходит в системе. Но как они работают? Где берут и как получают всю эту информацию? В этой записи мы будем воссоздавать три популярных Linux инструмента вместе, вы убиваете сразу двух зайцев, изучаете и Ruby и Linux в одно время.
От переводчика: Оригинал статьи
Находим информацию о статусе
Итак, давайте попробуем ответит на вопрос “где все эти инструменты берут информацию?”. Ответ “в специальной файловой системе /proc
”. Если вы посмотрите в каталог /proc
то найдете там кучу папок и файлов, как и в любом другом каталоге на вашем компьютере. Но фишка в том, что это не настоящие файлы, это просто путь для ядра Linux предоставить данные пользователю.
Это очень удобно, потому что мы можем обрабатывать их как обычные файлы, т.е. читать без каких-либо особых инструментов. В мире Linux много вещей работают по этому принципу, если нужны еще примеры, посмотрите в директорию /dev
. Теперь, когда мы понимаем что к чему, давайте заглянем внутрь каталога /proc
.
1
10
104
105
11
11015
11469
11474
11552
11655
Это лишь часть списка для примера, чтобы вы могли понять шаблон. Что означают все эти цифры? Чтож, это так называемые PIDы (Process IDs). Каждая запись содержит информацию об определенном процессе. Если вы запустите ps
то увидите что каждый процесс имеет ассоциированный идентификатор - PID:
PID TTY TIME CMD
15952 pts/5 00:00:00 ps
22698 pts/5 00:00:01 bash
Отсюда мы видим, что ps просто обходит все элементы /proc
и выводит информацию, которую найдет. Давайте посмотрим что находится внутри одного из пронумерованных каталогов:
attr
autogroup
auxv
cgroup
clear_refs
cmdline
comm
cpuset
cwd
environ
exe
fd
Это сокращенный вывод чтобы сэкономить место, однако я настоятельно рекомендую вам посмотреть полный список. Приведем некоторые важные/интересные записи:
Запись | Описание |
---|---|
comm | имя программы |
cmdline | команда, использованная для запуска этого процесса |
environ | Environment переменные с которыми был запущен процесс |
status | Статус процесса (running, sleeping…) и использование памяти |
fd | Каталог содержащий дескрипторы файлов (open files, sockets…) |
Теперь, когда мы узнали это, можем начинать писать некоторые инструменты!
Список процессов
Давайте начнем с получения списка всех директорий в каталоге /proc
. Мы можем сделать это используя Ruby класс Dir
.
Пример:
Dir.glob("/proc/[0-9]*")
Обратите внимание как я использовал диапазон цифр, это нужно так как в каталоге
/proc
находится много других папок, которые сейчас нас не интересуют.
Теперь можем пройтись (проитерировать) по этому списку и вывести две колонки, в первой 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
! Да, наша программа не поддерживает все фишки оригинала, но мы сделали кое-что работающее.
Кто слушает?
Давайте теперь попробуем воспроизвести netstat
, вот так выглядит вывод этой утилиты (с флагами -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
. Но есть маленькая проблемка, данные лежащие там не совсем похожи на вывод 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
Это значит что нам придется их распарсить с помощью регулярных выражений. На данный момент давайте займемся только локальным адресом и статусом.
Вот регулярное выражение, которое я подобрал для этой задачи:
\s+\d+: (?<local_addr>\w+):(?<local_port>\w+) \w+:\w+ (?<status>\w+)
Оно позволит нам получить шестнадцатеричные значения, которые еще нужно конвертировать в десятичные. Давайте создадим класс, который будет этим заниматься.
class TCPInfo
LINE_REGEX = /\s+\d+: (?<local_addr>\w+):(?<local_port>\w+) \w+:\w+ (?<status>\w+)/
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
# Convert hex to regular 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
# Don't worry too much about this :)
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
Пример вывода:
STATUS | LOCAL_PORT | LOCAL_ADDR
------------|------------|--------------
LISTENING | 5432 | 127.0.0.1
ESTABLISHED | 39530 | 192.168.88.46
Да, этот gem крутой!
Я только недавно нашел его и выглядит так будто теперь не придется играться с ljust
/ rjust
:)
Хватит использовать мой порт!
Вы когда-нибудь видели подобное сообщение?
Address already in use - bind(2) for "localhost" port 5000
Хммм… интересно, что же использует этот порт …
fuser -n tcp -v 5000
PORT USER PID ACCESS CMD
5000/tcp: blackbytes 30893 F.... nc
Ага, вот наш виновник! Теперь мы можем остановить эту программу если не хотим чтобы она была запущена и это освободит наш порт. Как программа fuser
нашла кто использует запрашиваемый порт? Именно! Это снова файловая система /proc
. По сути, она объединяет две вещи, которые мы уже изучили ранее: обход списка процессов и чтение активных подключений из /proc/net/tcp
. Нам нужен только один дополнительный шаг: найти способ ассоциировать информацию по открытым портам с PID. Если посмотреть данные о TCP, которые мы можем получить из /proc/net/tcp
, то PID там нет. Но мы можем использовать номер inode (иноды).
“Индексный дескриптор — это структура данных в традиционных для ОС UNIX файловых системах (ФС). В этой структуре хранится метаинформация о стандартных файлах, каталогах или других объектах файловой системы, кроме непосредственно данных и имени.” – Википедия
Как мы можем использовать иноду для поиска совпадающего процесса? Если мы посмотрим в каталог fd
процесса, который, как мы знаем имеет открытый порт, то найдем строку, наподобие этой:
/proc/3295/fd/5 -> socket:[12345]
Число между квадратными скобками это номер иноды. Теперь, все что нам нужно, это проитерировать все файлы и мы найдем совпадающий процесс.
Вот один из способов сделать это:
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})"
Пример вывода:
Port 5432 in use by /usr/bin/postgres (474)
Помните, что запускать код, приведенный выше нужно от суперпользователя (root) или от пользователя - владельца процесса. В обратном случае вы не сможете читать информацию о процессе из /proc
.
Заключение
В этой заметке вы узнали, что Linux предоставляет много данных с помощью виртуальной файловой системы /proc
. Также вы научились воссоздавать популярные Linux утилиты такие как ps
, netstat
и fuser
с использованием данных, полученных из /proc
.