Антон Рябов bio photo

Антон Рябов

Не люблю бриться и у меня умный взгляд.

Email Twitter Telegram Github PGP RSS

Обзор




Инструменты ps, top и netstat крутые, они дают много полезной информации о том, что происходит в системе. Но как они работают? Где берут и как получают всю эту информацию? В этой записи мы будем воссоздавать три популярных Linux инструмента вместе, вы убиваете сразу двух зайцев, изучаете и Ruby и Linux в одно время. :wink:

От переводчика: Оригинал статьи

Находим информацию о статусе

Итак, давайте попробуем ответит на вопрос “где все эти инструменты берут информацию?”. Ответ “в специальной файловой системе /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.

#Linux #Ruby #Tutorial #TechAndDev