Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyで作る独自Linuxツール:/procファイルシステムを使ってps・netstat・fuserを再現する方法

一緒にプロジェクトを作ってみませんか?

pstopnetstatといったLinuxツールは本当に便利です。

システム上で何が起こっているのか、多くの情報を教えてくれます。

  • しかし、これらのツールはどうやって動作しているのでしょうか?
  • 情報は一体どこから取得しているのか?
  • その仕組みを応用して、自分専用のツールを作るにはどうすればいいのか?

この記事では、3つの人気Linuxツールを一緒に再現していきます。

Rubyのテクニックと実用的なLinuxの知識を同時に身につけられる、まさに一石二鳥の内容です!🙂

ステータス情報はどこにあるのか

まずは「これらのツールはどこから情報を得ているのか?」という疑問の答えから探っていきましょう。

答えはprocファイルシステムにあります!

/procディレクトリの中を覗いてみると、他のディレクトリと同じように、たくさんのディレクトリやファイルが並んでいるように見えます。

しかし、これらは実際のファイルではありません。これはLinuxカーネルがユーザーへデータを公開するための仮想的な仕組みなのです。

通常のファイルと同じように扱えるため、特別なツールなしで読み取れるのが非常に便利なポイントです。

実はLinuxの世界では、多くのものがこのような設計になっています。別の例を見たい方は、/devディレクトリを確認してみてください。

仕組みが理解できたところで、/procディレクトリの中身を見てみましょう。

1
10
104
105
11
11015
11469
11474
11552
11655

ほんの一部のサンプルですが、すぐに規則性に気づくはずです。

この数字の羅列は何なのでしょうか?

実は、これらはPID(プロセスID)なのです。

各エントリには、特定のプロセスに関する情報が含まれています。

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 プロセスが起動されたときの環境変数
status プロセスの状態(実行中、スリープ中など)とメモリ使用量
fd ファイルディスクリプタ(開いているファイル、ソケットなど)を格納するディレクトリ

ここまでわかれば、いよいよツールの作成に取り掛かれます!

実行中のプログラム一覧を表示する方法

まずは/proc配下の全ディレクトリ一覧を取得するところから始めましょう。

RubyのDirクラスを使えば簡単に実現できます。

Dir.glob("/proc/[0-9]*")

数字の範囲パターンを使っている点に注目してください。/procの下には今回不要なファイルも存在するため、番号付きディレクトリだけを対象にしています。

次に、このリストを反復処理して、PIDとプログラム名の2列を表示してみましょう。

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

ということは、正規表現を使ったパース処理が必要になります。今回はまずローカルアドレスとステータスだけに注目しましょう。

作成した正規表現

LINE_REGEX = /\s+\d+: (?<local_addr>\w+):(?<local_port>\w+) \w+:\w+ (?<status>\w+)/

これで16進数の値が取得できるので、10進数への変換が必要です。その処理を行うクラスを作成しましょう。

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

  # 16進数を通常のIP表記に変換する
  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

  # ここはビット演算の処理なので、細部は気にしなくてOKです
  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は本当に優秀です!

最近発見したばかりですが、これでもうljustrjustと格闘する必要はなさそうです🙂

私のポートを使わないで!

こんなエラーメッセージを見たことはありませんか?

Address already in use - bind(2) for "localhost" port 5000

うーん…

どのプログラムがそのポートを掴んでいるのだろう?

実際に調べてみましょう

fuser -n tcp -v 5000

PORT       USER        PID   ACCESS CMD
5000/tcp   rubyguides  30893 F....  nc

ああ、犯人が判明しました!

実行したくなければ、このプログラムを停止すればポートが解放されます。では、「fuser」はどうやってポートを使用中のプロセスを特定したのでしょうか?

お察しの通り!

答えはまたしても/procファイルシステムです。

実際、これまでに解説した2つの要素——プロセスリストの走査/proc/net/tcpからのアクティブ接続の読み取り——を組み合わせています。

あと1ステップだけ追加が必要

それは、開いているポートの情報とPIDを紐付ける方法を見つけることです。

/proc/net/tcpから取得できるTCPデータにはPIDが含まれていません。そこで活躍するのがinode番号です。

「inodeとは、ファイルシステムオブジェクトを表現するために使用されるデータ構造である。」—— Wikipedia

inodeを使って該当プロセスを見つけるにはどうすればいいでしょうか?開いているポートを持つことがわかっているプロセスのfdディレクトリを覗いてみると、次のような行が見つかります。

/proc/3295/fd/5 -> socket:[12345]

角括弧内の数字こそがinode番号です。あとはすべてのファイルを反復処理すれば、目的のプロセスが見つかるというわけです。

実装例

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ファイルシステムを通じて大量のデータを公開していることを学びました。さらに、/proc配下のデータを活用して、psnetstatfuserといった人気LinuxツールをRubyで再現する方法も習得しました。

下のニュースレターを購読すれば、次回の記事をお見逃しなく!🙂

  1. Rubyのメソッド入門:defキーワードで独自のメソッドを定義する方法

    Rubyにおける「メソッド」とは、特定の目的のためにひとまとめにされた、1行以上のRubyコードのことです。 このコードのまとまりには名前が付けられており、一度定義してしまえば、同じコードを何度も書いたりコピー&ペーストしたりすることなく、必要なときにいつでも再利用できます。 メソッドの主な役割 メソッドの目的は、大きく分けて以下のようなものがあります。 情報を取得する オブジェクトを変更・生成する データをフィルタリング・整形する 具体例 例1: Arrayオブジェクトに対するsizeメソッドは、配列内の要素数を返します(情報の取得)。 例2: popメソッドは、配列から最後の要素を取り

  2. SUSE Studio – Webブラウザだけで独自のLinuxディストリビューションを作成する方法

    SUSE Studioは、Novellが支援するオンラインサービスです。特別な環境や専門知識がなくても、Webブラウザさえあれば、誰もが自分だけのカスタマイズ版openSUSE(アプライアンス)を作成できます。「信じられない?」と思うかもしれませんが、これは現実の話です。SUSE Studioは、SUSEユーザーに手軽なOSイメージ作成をもたらしてきたKiwiイメージングシステムの発展形であり、その成功体験をさらに進化させたものと言えます。はじめにKiwiは、Image CreatorやProduct Creatorといった使いやすいフロントエンドツールと組み合わせることで、初心者から上級者ま