lsofの仕組み?
Linuxでプロセスがオープンしてるlsofコマンドだが仕組みを知らずに使ってきた。 せっかくだから調べてみるも情報があまりないので自分用にまとめ。
lsofコマンドとは
プロセスが開いているファイルを表示するコマンドです。 GitHub上でもソースが公開されているし比較的欲しい情報が手に入りやすいやつです。
サーバプログラム作成
任意のポートで待ち受けるプロセスを生成するためのプログラムを作成。 実行時にpid.ppid,portを出力しポートに対して,ソケット生成、bind,listen,acceptをします。
#include <stdio.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #define QUEUELIMIT 5 int print_pid(unsigned short srvport) { pid_t pid, ppid; pid = getpid(); ppid = getppid(); fprintf(stdout, "pid=%d\nppid=%d\nport=%d\n", pid, ppid,srvport); return 0; } int main(int argc, char* argv[]) { int servSock; int clitSock; struct sockaddr_in servSockAddr; struct sockaddr_in clitSockAddr; unsigned short servPort; unsigned int clitLen; if ( argc != 2) { fprintf(stderr, "argument count mismatch error.\n"); exit(EXIT_FAILURE); } if ((servPort = (unsigned short) atoi(argv[1])) == 0) { fprintf(stderr, "invalid port number.\n"); exit(EXIT_FAILURE); } if ((servSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0 ){ perror("socket() failed."); exit(EXIT_FAILURE); } print_pid(servPort); memset(&servSockAddr, 0, sizeof(servSockAddr)); servSockAddr.sin_family = AF_INET; servSockAddr.sin_addr.s_addr = htonl(INADDR_ANY); servSockAddr.sin_port = htons(servPort); if (bind(servSock, (struct sockaddr *) &servSockAddr, sizeof(servSockAddr) ) < 0 ) { perror("bind() failed."); exit(1); } if (listen(servSock, QUEUELIMIT) < 0) { perror("listen() failed."); exit(1); } while(1) { clitLen = sizeof(clitSockAddr); if ((clitSock = accept(servSock, (struct sockaddr *) &clitSockAddr, &clitLen)) < 0) { perror("accept() failed."); exit(1); } printf("connected from %s.\n", inet_ntoa(clitSockAddr.sin_addr)); close(clitSock); } return 0; }
ソケット確認
プログラムを実行したら指定したポート番号でソケットファイルを探しに行きます。 ソケットのinodeを取得するためには「/proc/net/tcp」を見ていきます。
(procファイルシステムについてはぜひ下記を。。。!)
tcpファイルのヘッダ部分はこんな感じです。
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
sl : ソケットのカーネルハッシュスロットの値 local_address : ローカルアドレスとポート(16進数なので注意) rem_address : リモートアドレスとポート st : ソケットの内部状態 tx_queue : カーネルメモリを消費している 送信サイズ rx_queue : カーネルメモリを消費している 送信サイズ inode : inode
で、このヘッダの中で今回使用するのがlocal_addressとinode。 local_addressでポート番号を使ってinodeを特定していく。
# 今回は9000番ポートを想定してるので9000->0x2328 [root@cnt08 net]# cat /proc/net/tcp | grep ":2328" 3: 00000000:2328 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 4298572 1 00000000555aa095 100 0 0 10 0
使用しているinodeは4298572であることが特定できました。 inode4298572をopenしているプロセスを次はprocファイルシステムのfdをもとに探していきます。
[root@cnt08 net]# ls -dl /proc/[1-9]*/fd/* | grep 4298572 lrwx------. 1 root root 64 10月 21 22:18 /proc/21872/fd/3 -> socket:[4298572]
ここで見えるのがpid21872です lsofコマンドで見てみると
[root@cnt08 net]# lsof -i:9000 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME a.out 22100 root 3u IPv4 4301160 0t0 TCP *:cslistener (LISTEN)
同じ結果です。
lsofのそのへんの話はドキュメントにもありました。
lsofっぽいものを雑に実装
ポート番号からプロセスの特定の仕組みは分かったのでpythonで雑に実装
#!/usr/bin/python3 import os import sys import re TCP_PROC_FILE="/proc/net/tcp" def get_inode(hx): """ 渡されたポートを使用しているソケットファイルを特定しinodeを返却する """ inode_num = 0 with open(TCP_PROC_FILE, mode="r") as fd: for line in fd: ll = line.strip().split()[1].split(":") if len(ll) == 1: continue if hx == int(ll[1], 16): inode_num = line.strip().split()[9] return inode_num if inode_num else None def get_dir_list(path): """ 渡されたパス配下のディレクトリをリストで返す """ files = os.listdir(path) return [f for f in files if os.path.isdir(os.path.join(path, f))] def get_hardlink_list(proc_dir_list, inode_num): for path in proc_dir_list: for files in os.listdir(f"/proc/{path}/fd"): try: r = os.readlink(f"/proc/{path}/fd/{files}") if "socket" in r and r[8:-1] == inode_num: print(path, r, r[8:-1]) except FileNotFoundError as e: # Todo : エラー処理 pass def main(argv): pattern = "[0-9]*$" inode_num = get_inode(int(argv[1])) if inode_num is None: print("but port") sys.exit(1) path = "/proc/" proc_dir_list = [f for f in get_dir_list(path) if re.match(pattern, f)] l = get_hardlink_list(proc_dir_list, inode_num) if __name__ == "__main__": main(sys.argv)
1プロセスが1ポートを使ってるだけだと面白みもないので上で書いたサーバプログラムをマルチプロセスにして 2プロセスで9000番ポートをLISTEしている状態を作ります。
[root@cnt08 ~]# lsof -i:9000 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME a.out 24491 root 3u IPv4 4439589 0t0 TCP *:cslistener (LISTEN) a.out 24492 root 3u IPv4 4439589 0t0 TCP *:cslistener (LISTEN)
この状態で上のpythonを実行。第一フィールドがPIDなので一致してます。
[root@cnt08 ~]# python3 a.py 9000 24491 socket:[4439589] 4439589 24492 socket:[4439589] 4439589
特に面白みも無いのでkill機能をつけて見ました。 今後も改良すればちょっと使えそうな気がしてきた。