从内核 3.3 开始

从内核 3.3 开始

我想确定哪个进程拥有 UNIX 套接字的另一端。

具体来说,我询问的是使用 创建的套接字socketpair(),尽管问题对于任何 UNIX 套接字都是相同的。

我有一个程序parent可以创建 asocketpair(AF_UNIX, SOCK_STREAM, 0, fds)fork()s。父进程关闭fds[1]并保持fds[0]通信。孩子却反其道而行之close(fds[0]); s=fds[1]。然后孩子exec()的另一个程序,child1。两者可以通过该套接字对来回通信。

现在,假设我知道parent是谁,但我想弄清楚child1是谁。我该怎么做呢?

我可以使用多种工具,但没有一个可以告诉我套接字的另一端是哪个进程。我努力了:

  • lsof -c progname
  • lsof -c parent -c child1
  • ls -l /proc/$(pidof server)/fd
  • cat /proc/net/unix

基本上,我可以看到两个套接字以及它们的所有内容,但无法判断它们是否已连接。我试图确定父进程中的哪个 FD 正在与哪个子进程通信。

答案1

笔记:我现在维护一个lsof包装器,它结合了此处描述的两种方法,并且还在以下地址添加了环回 TCP 连接对等点的信息:https://github.com/stephane-chazelas/misc-scripts/blob/master/lsofc

Linux-3.3 及以上版本。

在 Linux 上,从内核版本 3.3 开始(并提供了UNIX_DIAG功能是在内核中内置的),给定 UNIX 域套接字(包括套接字对)的对等点可以使用新的网络链接基于API。

lsof从 4.89 版本开始可以使用该 API:

lsof +E -aUc Xorg

将列出所有具有名称以Xorg两端开头的进程的 Unix 域套接字,格式类似于:

Xorg       2777       root   56u  unix 0xffff8802419a7c00      0t0   34036 @/tmp/.X11-unix/X0 type=STREAM ->INO=33273 4120,xterm,3u

如果您的版本lsof太旧,还有更多选择。

ss实用程序(来自 参考资料iproute2)利用相同的 API 来检索和显示系统上 UNIX 域套接字列表上的信息,包括对等信息。

套接字由它们来标识索引节点号。请注意,它与套接字文件的文件系统 inode 无关。

例如在:

$ ss -x
[...]
u_str  ESTAB    0    0   @/tmp/.X11-unix/X0 3435997     * 3435996

它表示套接字 3435997 (绑定到 ABSTRACT 套接字/tmp/.X11-unix/X0)与套接字 3435996 连接。该-p选项可以告诉您哪个进程打开了该套接字。它通过在readlink上执行一些操作来实现这一点/proc/$pid/fd/*,因此它只能在您拥有的进程上执行此操作(除非您是root)。例如这里:

$ sudo ss -xp
[...]
u_str  ESTAB  0  0  @/tmp/.X11-unix/X0 3435997 * 3435996 users:(("Xorg",pid=3080,fd=83))
[...]
$ sudo ls -l /proc/3080/fd/23
lrwx------ 1 root root 64 Mar 12 16:34 /proc/3080/fd/83 -> socket:[3435997]

要找出哪个进程有 3435996,您可以在输出中查找其自己的条目ss -xp

$ ss -xp | awk '$6 == 3435996'
u_str  ESTAB  0  0  * 3435996  * 3435997 users:(("xterm",pid=29215,fd=3))

您还可以使用此脚本作为包装器来lsof轻松显示相关信息:

#! /usr/bin/perl
# lsof wrapper to add peer information for unix domain socket.
# Needs Linux 3.3 or above and CONFIG_UNIX_DIAG enabled.

# retrieve peer and direction information from ss
my (%peer, %dir);
open SS, '-|', 'ss', '-nexa';
while (<SS>) {
  if (/\s(\d+)\s+\*\s+(\d+) ([<-]-[->])$/) {
    $peer{$1} = $2;
    $dir{$1} = $3;
  }
}
close SS;

# Now get info about processes tied to sockets using lsof
my (%fields, %proc);
open LSOF, '-|', 'lsof', '-nPUFpcfin';
while (<LSOF>) {
  if (/(.)(.*)/) {
    $fields{$1} = $2;
    if ($1 eq 'n') {
      $proc{$fields{i}}->{"$fields{c},$fields{p}" .
      ($fields{n} =~ m{^([@/].*?)( type=\w+)?$} ? ",$1" : "")} = "";
    }
  }
}
close LSOF;

# and finally process the lsof output
open LSOF, '-|', 'lsof', @ARGV;
while (<LSOF>) {
  chomp;
  if (/\sunix\s+\S+\s+\S+\s+(\d+)\s/) {
    my $peer = $peer{$1};
    if (defined($peer)) {
      $_ .= $peer ?
            " ${dir{$1}} $peer\[" . (join("|", keys%{$proc{$peer}})||"?") . "]" :
            "[LISTENING]";
    }
  }
  print "$_\n";
}
close LSOF or exit(1);

例如:

$ sudo that-lsof-wrapper -ad3 -p 29215
命令 PID 用户 FD 类型 设备大小/关闭 节点名称
xterm 29215 斯蒂芬 3u unix 0xffff8800a07da4c0 0t0 3435996 类型=流<-> 3435997[Xorg,3080,@/tmp/.X11-unix/X0]

linux-3.3之前

旧的 Linux API 是通过文本文件检索 unix 套接字信息的/proc/net/unix。它列出了所有 Unix 域套接字(包括套接字对)。其中的第一个字段(如果没有使用 sysctl 参数对非超级用户隐藏kernel.kptr_restrict)为@Totor 已经解释过包含一个结构体的内核地址unix_sock,该结构体包含一个peer指向相应的字段同行 unix_sock。这也是Unix 套接字上列lsof的输出内容。DEVICE

现在获取该字段的值peer意味着能够读取内核内存并知道该peer字段相对于unix_sock地址的偏移量。

一些gdb-基于systemtap-基于解决方案已经给出,但它们需要gdb/systemtap和 Linux 内核调试符号来安装正在运行的内核,而生产系统上通常不是这种情况。

对偏移量进行硬编码并不是一个真正的选择,因为它随内核版本的不同而变化。

现在我们可以使用启发式方法来确定偏移量:让我们的工具创建一个虚拟对象socketpair(然后我们知道两个对等点的地址),并搜索同行围绕另一端的内存来确定偏移量。

这是一个概念验证脚本,它正是使用perl(在 i386 上使用内核 2.4.27 和 2.6.32 以及在 amd64 上使用 3.13 和 3.16 成功测试的)。与上面一样,它作为以下内容的包装器工作lsof

例如:

$ that-lsof-wrapper -aUc nm-applet
命令 PID 用户 FD 类型 设备大小/关闭 节点名称
nm-applet 4183 Stephane 4u unix 0xffff8800a055eb40 0t0 36888 类型=STREAM-> 0xffff8800a055e7c0[dbus-守护进程,4190,@/tmp/dbus-AiBCXOnuP6]
nm-applet 4183 Stephane 7u unix 0xffff8800a055e440 0t0 36890 类型=STREAM-> 0xffff8800a055e0c0[Xorg,3080,@/tmp/.X11-unix/X0]
nm-applet 4183 Stephane 8u unix 0xffff8800a05c1040 0t0 36201 类型=STREAM-> 0xffff8800a05c13c0[dbus-守护进程,4118,@/tmp/dbus-yxxNr1NkYC]
nm-applet 4183 Stephane 11u unix 0xffff8800a055d080 0t0 36219 类型=STREAM-> 0xffff8800a055d400[dbus-守护进程,4118,@/tmp/dbus-yxxNr1NkYC]
nm-applet 4183 Stephane 12u unix 0xffff88022e0dfb80 0t0 36221 类型=流-> 0xffff88022e0df800[dbus-守护进程,2268,/var/run/dbus/system_bus_socket]
nm-applet 4183 Stephane 13u unix 0xffff88022e0f80c0 0t0 37025 类型=STREAM-> 0xffff88022e29ec00[dbus-守护进程,2268,/var/run/dbus/system_bus_socket]

这是脚本:

#! /usr/bin/perl
# wrapper around lsof to add peer information for Unix
# domain sockets. needs lsof, and superuser privileges.
# Copyright Stephane Chazelas 2015, public domain.
# example: sudo this-lsof-wrapper -aUc Xorg
use Socket;

open K, "<", "/proc/kcore" or die "open kcore: $!";
read K, $h, 8192 # should be more than enough
 or die "read kcore: $!";

# parse ELF header
my ($t,$o,$n) = unpack("x4Cx[C19L!]L!x[L!C8]S", $h);
$t = $t == 1 ? "L3x4Lx12" : "Lx4QQx8Qx16"; # program header ELF32 or ELF64
my @headers = unpack("x$o($t)$n",$h);

# read data from kcore at given address (obtaining file offset from ELF
# @headers)
sub readaddr {
  my @h = @headers;
  my ($addr, $length) = @_;
  my $offset;
  while (my ($t, $o, $v, $s) = splice @h, 0, 4) {
    if ($addr >= $v && $addr < $v + $s) {
      $offset = $o + $addr - $v;
      if ($addr + $length - $v > $s) {
        $length = $s - ($addr - $v);
      }
      last;
    }
  }
  return undef unless defined($offset);
  seek K, $offset, 0 or die "seek kcore: $!";
  my $ret;
  read K, $ret, $length or die "read($length) kcore \@$offset: $!";
  return $ret;
}

# create a dummy socketpair to try find the offset in the
# kernel structure
socketpair(Rdr, Wtr, AF_UNIX, SOCK_STREAM, PF_UNSPEC)
 or die "socketpair: $!";
$r = readlink("/proc/self/fd/" . fileno(Rdr)) or die "readlink Rdr: $!";
$r =~ /\[(\d+)/; $r = $1;
$w = readlink("/proc/self/fd/" . fileno(Wtr)) or die "readlink Wtr: $!";
$w =~ /\[(\d+)/; $w = $1;
# now $r and $w contain the socket inodes of both ends of the socketpair
die "Can't determine peer offset" unless $r && $w;

# get the inode->address mapping
open U, "<", "/proc/net/unix" or die "open unix: $!";
while (<U>) {
  if (/^([0-9a-f]+):(?:\s+\S+){5}\s+(\d+)/) {
    $addr{$2} = hex $1;
  }
}
close U;

die "Can't determine peer offset" unless $addr{$r} && $addr{$w};

# read 2048 bytes starting at the address of Rdr and hope to find
# the address of Wtr referenced somewhere in there.
$around = readaddr $addr{$r}, 2048;
my $offset = 0;
my $ptr_size = length(pack("L!",0));
my $found;
for (unpack("L!*", $around)) {
  if ($_ == $addr{$w}) {
    $found = 1;
    last;
  }
  $offset += $ptr_size;
}
die "Can't determine peer offset" unless $found;

my %peer;
# now retrieve peer for each socket
for my $inode (keys %addr) {
  $peer{$addr{$inode}} = unpack("L!", readaddr($addr{$inode}+$offset,$ptr_size));
}
close K;

# Now get info about processes tied to sockets using lsof
my (%fields, %proc);
open LSOF, '-|', 'lsof', '-nPUFpcfdn';
while (<LSOF>) {
  if (/(.)(.*)/) {
    $fields{$1} = $2;
    if ($1 eq 'n') {
      $proc{hex($fields{d})}->{"$fields{c},$fields{p}" .
      ($fields{n} =~ m{^([@/].*?)( type=\w+)?$} ? ",$1" : "")} = "";
    }
  }
}
close LSOF;

# and finally process the lsof output
open LSOF, '-|', 'lsof', @ARGV;
while (<LSOF>) {
  chomp;
  for my $addr (/0x[0-9a-f]+/g) {
    $addr = hex $addr;
    my $peer = $peer{$addr};
    if (defined($peer)) {
      $_ .= $peer ?
            sprintf(" -> 0x%x[", $peer) . join("|", keys%{$proc{$peer}}) . "]" :
            "[LISTENING]";
      last;
    }
  }
  print "$_\n";
}
close LSOF or exit(1);

答案2

从内核 3.3 开始,可以使用sslsof-4.89以上 — 参见斯特凡·查泽拉斯的回答

根据作者的说法,在旧版本中,lsof不可能发现这一点:Linux 内核不会公开此信息。来源:comp.unix.admin 上的 2003 线程

中显示的数字/proc/$pid/fd/$fd是虚拟套接字文件系统中套接字的索引节点号。当您创建管道或套接字对时,每一端都会连续接收一个索引节点号。这些数字是按顺序分配的,因此数字相差 1 的可能性很高,但这不能保证(因为第一个套接字是由于包装,或者因为在两个索引节点分配之间调度了其他一些线程并且该线程也创建了一些索引节点,+1 已经在使用中)。

我检查了socketpair内核 2.6.39 中的定义,并且套接字的两端除了特定类型之外不相关socketpair方法。对于unix套接字来说,就是unix_socketpairnet/unix/af_unix.c

答案3

从内核 3.3 开始

现在获取此信息ss:

# ss -xp

现在您可以在Peer列中看到一个 ID(索引节点号),它与Local列中的另一个 ID 相对应。匹配 ID 是套接字的两端。

注意:UNIX_DIAG必须在您的内核中启用该选项。

内核 3.3 之前

Linux 没有向用户空间公开这些信息。

然而,通过查看内核内存,我们可以访问这些信息。

注意:这个答案是通过使用 来实现的gdb,但是,请参阅@StéphaneChazelas 的回答这方面有更详细的阐述。

# lsof | grep whatever
mysqld 14450 (...) unix 0xffff8801011e8280 (...) /var/run/mysqld/mysqld.sock
mysqld 14450 (...) unix 0xffff8801011e9600 (...) /var/run/mysqld/mysqld.sock

有 2 个不同的套接字,1 个正在侦听,1 个已建立。十六进制数字是相应内核的地址unix_sock结构,有一个peer属性是地址插座的另一端(也是一个unix_sock结构实例)。

现在我们可以使用gdb来查找peer内核内存:

# gdb /usr/lib/debug/boot/vmlinux-3.2.0-4-amd64 /proc/kcore
(gdb) print ((struct unix_sock*)0xffff8801011e9600)->peer
$1 = (struct sock *) 0xffff880171f078c0

# lsof | grep 0xffff880171f078c0
mysql 14815 (...) unix 0xffff880171f078c0 (...) socket

在这里,套接字的另一端由 PID 14815 持有mysql

您的内核必须经过编译才能KCORE_ELF使用/proc/kcore。此外,您还需要带有调试符号的内核映像版本。在 Debian 7 上,apt-get install linux-image-3.2.0-4-amd64-dbg将提供此文件。

不需要可调试的内核映像...

如果系统上没有(或不想保留)调试内核映像,则可以提供gdb内存偏移量以“手动”访问该peer值。该偏移值通常随内核版本或体系结构的不同而不同。

在我的内核上,我知道偏移量是 680 字节,即 64 位的 85 倍。所以我可以这样做:

# gdb /boot/vmlinux-3.2.0-4-amd64 /proc/kcore
(gdb) print ((void**)0xffff8801011e9600)[85]
$1 = (void *) 0xffff880171f078c0

瞧,结果与上面相同。

如果您在多台机器上运行相同的内核,则使用此变体会更容易,因为您不需要调试映像,只需要偏移值。

为了(轻松)首先发现这个偏移值,您确实需要调试映像:

$ pahole -C unix_sock /usr/lib/debug/boot/vmlinux-3.2.0-4-amd64
struct unix_sock {
  (...)
  struct sock *              peer;                 /*   680     8 */
  (...)
}

给你,680 字节,这是 85 x 64 位,或 170 x 32 位。

这个答案的大部分功劳都归功于平均电压

答案4

这个解决方案虽然有效,但兴趣有限,因为如果您有一个足够新的 systemtap,那么您很可能会有一个足够新的内核,您可以在其中使用ss基于 方法,如果你使用的是较旧的内核,其他解决方案,虽然更多哈克更有可能工作并且不需要额外的软件。

systemtap作为如何使用此类任务的演示仍然很有用。

如果在具有可用 systemtap(1.8 或更高版本)的最新 Linux 系统上,您可以使用下面的脚本来后处理以下输出lsof

例如:

$ lsof -aUc nm 小程序 | sudo 该脚本
命令 PID 用户 FD 类型 设备大小/关闭 节点名称
nm-applet 4183 Stephane 4u unix 0xffff8800a055eb40 0t0 36888 类型=STREAM-> 0xffff8800a055e7c0[dbus-守护进程,4190,@/tmp/dbus-AiBCXOnuP6]
nm-applet 4183 Stephane 7u unix 0xffff8800a055e440 0t0 36890 类型=STREAM-> 0xffff8800a055e0c0[Xorg,3080,@/tmp/.X11-unix/X0]
nm-applet 4183 Stephane 8u unix 0xffff8800a05c1040 0t0 36201 类型=STREAM-> 0xffff8800a05c13c0[dbus-守护进程,4118,@/tmp/dbus-yxxNr1NkYC]
nm-applet 4183 Stephane 11u unix 0xffff8800a055d080 0t0 36219 类型=STREAM-> 0xffff8800a055d400[dbus-守护进程,4118,@/tmp/dbus-yxxNr1NkYC]
nm-applet 4183 Stephane 12u unix 0xffff88022e0dfb80 0t0 36221 类型=流-> 0xffff88022e0df800[dbus-守护进程,2268,/var/run/dbus/system_bus_socket]
nm-applet 4183 Stephane 13u unix 0xffff88022e0f80c0 0t0 37025 类型=STREAM-> 0xffff88022e29ec00[dbus-守护进程,2268,/var/run/dbus/system_bus_socket]

(如果您在上面看到 0x0000000000000000 而不是 0xffff...,这是因为kernel.kptr_restrict您的系统上设置了 sysctl 参数,导致内核指针对非特权进程隐藏,在这种情况下,您需要lsof以 root 身份运行才能获得有意义的结果)。

该脚本不会尝试处理带有换行符的套接字文件名,但也不会lsof(也不lsof处理空格或冒号)。

systemtap这里用于转储内核中哈希unix_sock中所有结构的地址和对等地址。unix_socket_table

仅在带有 systemtap 2.6 的 Linux 3.16 amd64 和带有 2.3 的 3.13 上进行了测试。

#! /usr/bin/perl
# meant to process lsof output to try and find the peer of a given
# unix domain socket. Needs a working systemtap, lsof, and superuser
# privileges. Copyright Stephane Chazelas 2015, public domain.
# Example: lsof -aUc X | sudo this-script
open STAP, '-|', 'stap', '-e', q{
  probe begin {
    offset = &@cast(0, "struct sock")->__sk_common->skc_node;
    for (i = 0; i < 512; i++) 
      for (p = @var("unix_socket_table@net/unix/af_unix.c")[i]->first;
           p;
           p=@cast(p, "struct hlist_node")->next
          ) {
        sock = p - offset;
        printf("%p %p\n", sock, @cast(sock, "struct unix_sock")->peer);
    }
    exit()
  }
};  
my %peer;
while (<STAP>) {
  chomp;
  my ($a, $b) = split;
  $peer{$a} = $b;
}
close STAP;

my %f, %addr;
open LSOF, '-|', 'lsof', '-nPUFpcfdn';
while (<LSOF>) {
  if (/(.)(.*)/) {
    $f{$1} = $2;
    if ($1 eq 'n') {
      $addr{$f{d}}->{"$f{c},$f{p}" . ($f{n} =~ m{^([@/].*?)( type=\w+)?$} ? ",$1" : "")} = "";
    }
  }
}
close LSOF;

while (<>) {
  chomp;
  for my $addr (/0x[0-9a-f]+/g) {
    my $peer = $peer{$addr};
    if (defined($peer)) {
      $_ .= $peer eq '0x0' ?
            "[LISTENING]" :
            " -> $peer\[" . join("|", keys%{$addr{$peer}}) . "]";
      last;
    }
  }
  print "$_\n";
}

相关内容