1207 lines
44 KiB
Python
1207 lines
44 KiB
Python
#!/usr/bin/env python
|
|
|
|
# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
"""Linux platform implementation."""
|
|
|
|
from __future__ import division
|
|
|
|
import base64
|
|
import errno
|
|
import functools
|
|
import os
|
|
import re
|
|
import socket
|
|
import struct
|
|
import sys
|
|
import warnings
|
|
from collections import namedtuple, defaultdict
|
|
|
|
from . import _common
|
|
from . import _psposix
|
|
from . import _psutil_linux as cext
|
|
from . import _psutil_posix as cext_posix
|
|
from ._common import isfile_strict, usage_percent
|
|
from ._common import NIC_DUPLEX_FULL, NIC_DUPLEX_HALF, NIC_DUPLEX_UNKNOWN
|
|
from ._compat import PY3, long
|
|
|
|
if sys.version_info >= (3, 4):
|
|
import enum
|
|
else:
|
|
enum = None
|
|
|
|
|
|
__extra__all__ = [
|
|
# io prio constants
|
|
"IOPRIO_CLASS_NONE", "IOPRIO_CLASS_RT", "IOPRIO_CLASS_BE",
|
|
"IOPRIO_CLASS_IDLE",
|
|
# connection status constants
|
|
"CONN_ESTABLISHED", "CONN_SYN_SENT", "CONN_SYN_RECV", "CONN_FIN_WAIT1",
|
|
"CONN_FIN_WAIT2", "CONN_TIME_WAIT", "CONN_CLOSE", "CONN_CLOSE_WAIT",
|
|
"CONN_LAST_ACK", "CONN_LISTEN", "CONN_CLOSING", ]
|
|
|
|
# --- constants
|
|
|
|
HAS_PRLIMIT = hasattr(cext, "linux_prlimit")
|
|
|
|
# RLIMIT_* constants, not guaranteed to be present on all kernels
|
|
if HAS_PRLIMIT:
|
|
for name in dir(cext):
|
|
if name.startswith('RLIM'):
|
|
__extra__all__.append(name)
|
|
|
|
# Number of clock ticks per second
|
|
CLOCK_TICKS = os.sysconf("SC_CLK_TCK")
|
|
PAGESIZE = os.sysconf("SC_PAGE_SIZE")
|
|
BOOT_TIME = None # set later
|
|
DEFAULT_ENCODING = sys.getdefaultencoding()
|
|
if enum is None:
|
|
AF_LINK = socket.AF_PACKET
|
|
else:
|
|
AddressFamily = enum.IntEnum('AddressFamily',
|
|
{'AF_LINK': socket.AF_PACKET})
|
|
AF_LINK = AddressFamily.AF_LINK
|
|
|
|
# ioprio_* constants http://linux.die.net/man/2/ioprio_get
|
|
if enum is None:
|
|
IOPRIO_CLASS_NONE = 0
|
|
IOPRIO_CLASS_RT = 1
|
|
IOPRIO_CLASS_BE = 2
|
|
IOPRIO_CLASS_IDLE = 3
|
|
else:
|
|
class IOPriority(enum.IntEnum):
|
|
IOPRIO_CLASS_NONE = 0
|
|
IOPRIO_CLASS_RT = 1
|
|
IOPRIO_CLASS_BE = 2
|
|
IOPRIO_CLASS_IDLE = 3
|
|
|
|
globals().update(IOPriority.__members__)
|
|
|
|
# taken from /fs/proc/array.c
|
|
PROC_STATUSES = {
|
|
"R": _common.STATUS_RUNNING,
|
|
"S": _common.STATUS_SLEEPING,
|
|
"D": _common.STATUS_DISK_SLEEP,
|
|
"T": _common.STATUS_STOPPED,
|
|
"t": _common.STATUS_TRACING_STOP,
|
|
"Z": _common.STATUS_ZOMBIE,
|
|
"X": _common.STATUS_DEAD,
|
|
"x": _common.STATUS_DEAD,
|
|
"K": _common.STATUS_WAKE_KILL,
|
|
"W": _common.STATUS_WAKING
|
|
}
|
|
|
|
# http://students.mimuw.edu.pl/lxr/source/include/net/tcp_states.h
|
|
TCP_STATUSES = {
|
|
"01": _common.CONN_ESTABLISHED,
|
|
"02": _common.CONN_SYN_SENT,
|
|
"03": _common.CONN_SYN_RECV,
|
|
"04": _common.CONN_FIN_WAIT1,
|
|
"05": _common.CONN_FIN_WAIT2,
|
|
"06": _common.CONN_TIME_WAIT,
|
|
"07": _common.CONN_CLOSE,
|
|
"08": _common.CONN_CLOSE_WAIT,
|
|
"09": _common.CONN_LAST_ACK,
|
|
"0A": _common.CONN_LISTEN,
|
|
"0B": _common.CONN_CLOSING
|
|
}
|
|
|
|
# set later from __init__.py
|
|
NoSuchProcess = None
|
|
ZombieProcess = None
|
|
AccessDenied = None
|
|
TimeoutExpired = None
|
|
|
|
|
|
# --- named tuples
|
|
|
|
def _get_cputimes_fields():
|
|
"""Return a namedtuple of variable fields depending on the
|
|
CPU times available on this Linux kernel version which may be:
|
|
(user, nice, system, idle, iowait, irq, softirq, [steal, [guest,
|
|
[guest_nice]]])
|
|
"""
|
|
with open('/proc/stat', 'rb') as f:
|
|
values = f.readline().split()[1:]
|
|
fields = ['user', 'nice', 'system', 'idle', 'iowait', 'irq', 'softirq']
|
|
vlen = len(values)
|
|
if vlen >= 8:
|
|
# Linux >= 2.6.11
|
|
fields.append('steal')
|
|
if vlen >= 9:
|
|
# Linux >= 2.6.24
|
|
fields.append('guest')
|
|
if vlen >= 10:
|
|
# Linux >= 3.2.0
|
|
fields.append('guest_nice')
|
|
return fields
|
|
|
|
|
|
scputimes = namedtuple('scputimes', _get_cputimes_fields())
|
|
|
|
svmem = namedtuple(
|
|
'svmem', ['total', 'available', 'percent', 'used', 'free',
|
|
'active', 'inactive', 'buffers', 'cached'])
|
|
|
|
pextmem = namedtuple('pextmem', 'rss vms shared text lib data dirty')
|
|
|
|
pmmap_grouped = namedtuple(
|
|
'pmmap_grouped', ['path', 'rss', 'size', 'pss', 'shared_clean',
|
|
'shared_dirty', 'private_clean', 'private_dirty',
|
|
'referenced', 'anonymous', 'swap'])
|
|
|
|
pmmap_ext = namedtuple(
|
|
'pmmap_ext', 'addr perms ' + ' '.join(pmmap_grouped._fields))
|
|
|
|
|
|
# --- system memory
|
|
|
|
def virtual_memory():
|
|
total, free, buffers, shared, _, _ = cext.linux_sysinfo()
|
|
cached = active = inactive = None
|
|
with open('/proc/meminfo', 'rb') as f:
|
|
for line in f:
|
|
if line.startswith(b"Cached:"):
|
|
cached = int(line.split()[1]) * 1024
|
|
elif line.startswith(b"Active:"):
|
|
active = int(line.split()[1]) * 1024
|
|
elif line.startswith(b"Inactive:"):
|
|
inactive = int(line.split()[1]) * 1024
|
|
if (cached is not None and
|
|
active is not None and
|
|
inactive is not None):
|
|
break
|
|
else:
|
|
# we might get here when dealing with exotic Linux flavors, see:
|
|
# https://github.com/giampaolo/psutil/issues/313
|
|
msg = "'cached', 'active' and 'inactive' memory stats couldn't " \
|
|
"be determined and were set to 0"
|
|
warnings.warn(msg, RuntimeWarning)
|
|
cached = active = inactive = 0
|
|
avail = free + buffers + cached
|
|
used = total - free
|
|
percent = usage_percent((total - avail), total, _round=1)
|
|
return svmem(total, avail, percent, used, free,
|
|
active, inactive, buffers, cached)
|
|
|
|
|
|
def swap_memory():
|
|
_, _, _, _, total, free = cext.linux_sysinfo()
|
|
used = total - free
|
|
percent = usage_percent(used, total, _round=1)
|
|
# get pgin/pgouts
|
|
with open("/proc/vmstat", "rb") as f:
|
|
sin = sout = None
|
|
for line in f:
|
|
# values are expressed in 4 kilo bytes, we want bytes instead
|
|
if line.startswith(b'pswpin'):
|
|
sin = int(line.split(b' ')[1]) * 4 * 1024
|
|
elif line.startswith(b'pswpout'):
|
|
sout = int(line.split(b' ')[1]) * 4 * 1024
|
|
if sin is not None and sout is not None:
|
|
break
|
|
else:
|
|
# we might get here when dealing with exotic Linux flavors, see:
|
|
# https://github.com/giampaolo/psutil/issues/313
|
|
msg = "'sin' and 'sout' swap memory stats couldn't " \
|
|
"be determined and were set to 0"
|
|
warnings.warn(msg, RuntimeWarning)
|
|
sin = sout = 0
|
|
return _common.sswap(total, used, free, percent, sin, sout)
|
|
|
|
|
|
# --- CPUs
|
|
|
|
def cpu_times():
|
|
"""Return a named tuple representing the following system-wide
|
|
CPU times:
|
|
(user, nice, system, idle, iowait, irq, softirq [steal, [guest,
|
|
[guest_nice]]])
|
|
Last 3 fields may not be available on all Linux kernel versions.
|
|
"""
|
|
with open('/proc/stat', 'rb') as f:
|
|
values = f.readline().split()
|
|
fields = values[1:len(scputimes._fields) + 1]
|
|
fields = [float(x) / CLOCK_TICKS for x in fields]
|
|
return scputimes(*fields)
|
|
|
|
|
|
def per_cpu_times():
|
|
"""Return a list of namedtuple representing the CPU times
|
|
for every CPU available on the system.
|
|
"""
|
|
cpus = []
|
|
with open('/proc/stat', 'rb') as f:
|
|
# get rid of the first line which refers to system wide CPU stats
|
|
f.readline()
|
|
for line in f:
|
|
if line.startswith(b'cpu'):
|
|
values = line.split()
|
|
fields = values[1:len(scputimes._fields) + 1]
|
|
fields = [float(x) / CLOCK_TICKS for x in fields]
|
|
entry = scputimes(*fields)
|
|
cpus.append(entry)
|
|
return cpus
|
|
|
|
|
|
def cpu_count_logical():
|
|
"""Return the number of logical CPUs in the system."""
|
|
try:
|
|
return os.sysconf("SC_NPROCESSORS_ONLN")
|
|
except ValueError:
|
|
# as a second fallback we try to parse /proc/cpuinfo
|
|
num = 0
|
|
with open('/proc/cpuinfo', 'rb') as f:
|
|
for line in f:
|
|
if line.lower().startswith(b'processor'):
|
|
num += 1
|
|
|
|
# unknown format (e.g. amrel/sparc architectures), see:
|
|
# https://github.com/giampaolo/psutil/issues/200
|
|
# try to parse /proc/stat as a last resort
|
|
if num == 0:
|
|
search = re.compile('cpu\d')
|
|
with open('/proc/stat', 'rt') as f:
|
|
for line in f:
|
|
line = line.split(' ')[0]
|
|
if search.match(line):
|
|
num += 1
|
|
|
|
if num == 0:
|
|
# mimic os.cpu_count()
|
|
return None
|
|
return num
|
|
|
|
|
|
def cpu_count_physical():
|
|
"""Return the number of physical cores in the system."""
|
|
mapping = {}
|
|
current_info = {}
|
|
with open('/proc/cpuinfo', 'rb') as f:
|
|
for line in f:
|
|
line = line.strip().lower()
|
|
if not line:
|
|
# new section
|
|
if (b'physical id' in current_info and
|
|
b'cpu cores' in current_info):
|
|
mapping[current_info[b'physical id']] = \
|
|
current_info[b'cpu cores']
|
|
current_info = {}
|
|
else:
|
|
# ongoing section
|
|
if (line.startswith(b'physical id') or
|
|
line.startswith(b'cpu cores')):
|
|
key, value = line.split(b'\t:', 1)
|
|
current_info[key] = int(value)
|
|
|
|
# mimic os.cpu_count()
|
|
return sum(mapping.values()) or None
|
|
|
|
|
|
# --- other system functions
|
|
|
|
def users():
|
|
"""Return currently connected users as a list of namedtuples."""
|
|
retlist = []
|
|
rawlist = cext.users()
|
|
for item in rawlist:
|
|
user, tty, hostname, tstamp, user_process = item
|
|
# note: the underlying C function includes entries about
|
|
# system boot, run level and others. We might want
|
|
# to use them in the future.
|
|
if not user_process:
|
|
continue
|
|
if hostname == ':0.0' or hostname == ':0':
|
|
hostname = 'localhost'
|
|
nt = _common.suser(user, tty or None, hostname, tstamp)
|
|
retlist.append(nt)
|
|
return retlist
|
|
|
|
|
|
def boot_time():
|
|
"""Return the system boot time expressed in seconds since the epoch."""
|
|
global BOOT_TIME
|
|
with open('/proc/stat', 'rb') as f:
|
|
for line in f:
|
|
if line.startswith(b'btime'):
|
|
ret = float(line.strip().split()[1])
|
|
BOOT_TIME = ret
|
|
return ret
|
|
raise RuntimeError("line 'btime' not found in /proc/stat")
|
|
|
|
|
|
# --- processes
|
|
|
|
def pids():
|
|
"""Returns a list of PIDs currently running on the system."""
|
|
return [int(x) for x in os.listdir(b'/proc') if x.isdigit()]
|
|
|
|
|
|
def pid_exists(pid):
|
|
"""Check For the existence of a unix pid."""
|
|
return _psposix.pid_exists(pid)
|
|
|
|
|
|
# --- network
|
|
|
|
class Connections:
|
|
"""A wrapper on top of /proc/net/* files, retrieving per-process
|
|
and system-wide open connections (TCP, UDP, UNIX) similarly to
|
|
"netstat -an".
|
|
|
|
Note: in case of UNIX sockets we're only able to determine the
|
|
local endpoint/path, not the one it's connected to.
|
|
According to [1] it would be possible but not easily.
|
|
|
|
[1] http://serverfault.com/a/417946
|
|
"""
|
|
|
|
def __init__(self):
|
|
tcp4 = ("tcp", socket.AF_INET, socket.SOCK_STREAM)
|
|
tcp6 = ("tcp6", socket.AF_INET6, socket.SOCK_STREAM)
|
|
udp4 = ("udp", socket.AF_INET, socket.SOCK_DGRAM)
|
|
udp6 = ("udp6", socket.AF_INET6, socket.SOCK_DGRAM)
|
|
unix = ("unix", socket.AF_UNIX, None)
|
|
self.tmap = {
|
|
"all": (tcp4, tcp6, udp4, udp6, unix),
|
|
"tcp": (tcp4, tcp6),
|
|
"tcp4": (tcp4,),
|
|
"tcp6": (tcp6,),
|
|
"udp": (udp4, udp6),
|
|
"udp4": (udp4,),
|
|
"udp6": (udp6,),
|
|
"unix": (unix,),
|
|
"inet": (tcp4, tcp6, udp4, udp6),
|
|
"inet4": (tcp4, udp4),
|
|
"inet6": (tcp6, udp6),
|
|
}
|
|
|
|
def get_proc_inodes(self, pid):
|
|
inodes = defaultdict(list)
|
|
for fd in os.listdir("/proc/%s/fd" % pid):
|
|
try:
|
|
inode = os.readlink("/proc/%s/fd/%s" % (pid, fd))
|
|
except OSError as err:
|
|
# ENOENT == file which is gone in the meantime;
|
|
# os.stat('/proc/%s' % self.pid) will be done later
|
|
# to force NSP (if it's the case)
|
|
if err.errno in (errno.ENOENT, errno.ESRCH):
|
|
continue
|
|
elif err.errno == errno.EINVAL:
|
|
# not a link
|
|
continue
|
|
else:
|
|
raise
|
|
else:
|
|
if inode.startswith('socket:['):
|
|
# the process is using a socket
|
|
inode = inode[8:][:-1]
|
|
inodes[inode].append((pid, int(fd)))
|
|
return inodes
|
|
|
|
def get_all_inodes(self):
|
|
inodes = {}
|
|
for pid in pids():
|
|
try:
|
|
inodes.update(self.get_proc_inodes(pid))
|
|
except OSError as err:
|
|
# os.listdir() is gonna raise a lot of access denied
|
|
# exceptions in case of unprivileged user; that's fine
|
|
# as we'll just end up returning a connection with PID
|
|
# and fd set to None anyway.
|
|
# Both netstat -an and lsof does the same so it's
|
|
# unlikely we can do any better.
|
|
# ENOENT just means a PID disappeared on us.
|
|
if err.errno not in (
|
|
errno.ENOENT, errno.ESRCH, errno.EPERM, errno.EACCES):
|
|
raise
|
|
return inodes
|
|
|
|
def decode_address(self, addr, family):
|
|
"""Accept an "ip:port" address as displayed in /proc/net/*
|
|
and convert it into a human readable form, like:
|
|
|
|
"0500000A:0016" -> ("10.0.0.5", 22)
|
|
"0000000000000000FFFF00000100007F:9E49" -> ("::ffff:127.0.0.1", 40521)
|
|
|
|
The IP address portion is a little or big endian four-byte
|
|
hexadecimal number; that is, the least significant byte is listed
|
|
first, so we need to reverse the order of the bytes to convert it
|
|
to an IP address.
|
|
The port is represented as a two-byte hexadecimal number.
|
|
|
|
Reference:
|
|
http://linuxdevcenter.com/pub/a/linux/2000/11/16/LinuxAdmin.html
|
|
"""
|
|
ip, port = addr.split(':')
|
|
port = int(port, 16)
|
|
# this usually refers to a local socket in listen mode with
|
|
# no end-points connected
|
|
if not port:
|
|
return ()
|
|
if PY3:
|
|
ip = ip.encode('ascii')
|
|
if family == socket.AF_INET:
|
|
# see: https://github.com/giampaolo/psutil/issues/201
|
|
if sys.byteorder == 'little':
|
|
ip = socket.inet_ntop(family, base64.b16decode(ip)[::-1])
|
|
else:
|
|
ip = socket.inet_ntop(family, base64.b16decode(ip))
|
|
else: # IPv6
|
|
# old version - let's keep it, just in case...
|
|
# ip = ip.decode('hex')
|
|
# return socket.inet_ntop(socket.AF_INET6,
|
|
# ''.join(ip[i:i+4][::-1] for i in xrange(0, 16, 4)))
|
|
ip = base64.b16decode(ip)
|
|
# see: https://github.com/giampaolo/psutil/issues/201
|
|
if sys.byteorder == 'little':
|
|
ip = socket.inet_ntop(
|
|
socket.AF_INET6,
|
|
struct.pack('>4I', *struct.unpack('<4I', ip)))
|
|
else:
|
|
ip = socket.inet_ntop(
|
|
socket.AF_INET6,
|
|
struct.pack('<4I', *struct.unpack('<4I', ip)))
|
|
return (ip, port)
|
|
|
|
def process_inet(self, file, family, type_, inodes, filter_pid=None):
|
|
"""Parse /proc/net/tcp* and /proc/net/udp* files."""
|
|
if file.endswith('6') and not os.path.exists(file):
|
|
# IPv6 not supported
|
|
return
|
|
with open(file, 'rt') as f:
|
|
f.readline() # skip the first line
|
|
for line in f:
|
|
try:
|
|
_, laddr, raddr, status, _, _, _, _, _, inode = \
|
|
line.split()[:10]
|
|
except ValueError:
|
|
raise RuntimeError(
|
|
"error while parsing %s; malformed line %r" % (
|
|
file, line))
|
|
if inode in inodes:
|
|
# # We assume inet sockets are unique, so we error
|
|
# # out if there are multiple references to the
|
|
# # same inode. We won't do this for UNIX sockets.
|
|
# if len(inodes[inode]) > 1 and family != socket.AF_UNIX:
|
|
# raise ValueError("ambiguos inode with multiple "
|
|
# "PIDs references")
|
|
pid, fd = inodes[inode][0]
|
|
else:
|
|
pid, fd = None, -1
|
|
if filter_pid is not None and filter_pid != pid:
|
|
continue
|
|
else:
|
|
if type_ == socket.SOCK_STREAM:
|
|
status = TCP_STATUSES[status]
|
|
else:
|
|
status = _common.CONN_NONE
|
|
laddr = self.decode_address(laddr, family)
|
|
raddr = self.decode_address(raddr, family)
|
|
yield (fd, family, type_, laddr, raddr, status, pid)
|
|
|
|
def process_unix(self, file, family, inodes, filter_pid=None):
|
|
"""Parse /proc/net/unix files."""
|
|
with open(file, 'rt') as f:
|
|
f.readline() # skip the first line
|
|
for line in f:
|
|
tokens = line.split()
|
|
try:
|
|
_, _, _, _, type_, _, inode = tokens[0:7]
|
|
except ValueError:
|
|
raise RuntimeError(
|
|
"error while parsing %s; malformed line %r" % (
|
|
file, line))
|
|
if inode in inodes:
|
|
# With UNIX sockets we can have a single inode
|
|
# referencing many file descriptors.
|
|
pairs = inodes[inode]
|
|
else:
|
|
pairs = [(None, -1)]
|
|
for pid, fd in pairs:
|
|
if filter_pid is not None and filter_pid != pid:
|
|
continue
|
|
else:
|
|
if len(tokens) == 8:
|
|
path = tokens[-1]
|
|
else:
|
|
path = ""
|
|
type_ = int(type_)
|
|
raddr = None
|
|
status = _common.CONN_NONE
|
|
yield (fd, family, type_, path, raddr, status, pid)
|
|
|
|
def retrieve(self, kind, pid=None):
|
|
if kind not in self.tmap:
|
|
raise ValueError("invalid %r kind argument; choose between %s"
|
|
% (kind, ', '.join([repr(x) for x in self.tmap])))
|
|
if pid is not None:
|
|
inodes = self.get_proc_inodes(pid)
|
|
if not inodes:
|
|
# no connections for this process
|
|
return []
|
|
else:
|
|
inodes = self.get_all_inodes()
|
|
ret = set()
|
|
for f, family, type_ in self.tmap[kind]:
|
|
if family in (socket.AF_INET, socket.AF_INET6):
|
|
ls = self.process_inet(
|
|
"/proc/net/%s" % f, family, type_, inodes, filter_pid=pid)
|
|
else:
|
|
ls = self.process_unix(
|
|
"/proc/net/%s" % f, family, inodes, filter_pid=pid)
|
|
for fd, family, type_, laddr, raddr, status, bound_pid in ls:
|
|
if pid:
|
|
conn = _common.pconn(fd, family, type_, laddr, raddr,
|
|
status)
|
|
else:
|
|
conn = _common.sconn(fd, family, type_, laddr, raddr,
|
|
status, bound_pid)
|
|
ret.add(conn)
|
|
return list(ret)
|
|
|
|
|
|
_connections = Connections()
|
|
|
|
|
|
def net_connections(kind='inet'):
|
|
"""Return system-wide open connections."""
|
|
return _connections.retrieve(kind)
|
|
|
|
|
|
def net_io_counters():
|
|
"""Return network I/O statistics for every network interface
|
|
installed on the system as a dict of raw tuples.
|
|
"""
|
|
with open("/proc/net/dev", "rt") as f:
|
|
lines = f.readlines()
|
|
retdict = {}
|
|
for line in lines[2:]:
|
|
colon = line.rfind(':')
|
|
assert colon > 0, repr(line)
|
|
name = line[:colon].strip()
|
|
fields = line[colon + 1:].strip().split()
|
|
bytes_recv = int(fields[0])
|
|
packets_recv = int(fields[1])
|
|
errin = int(fields[2])
|
|
dropin = int(fields[3])
|
|
bytes_sent = int(fields[8])
|
|
packets_sent = int(fields[9])
|
|
errout = int(fields[10])
|
|
dropout = int(fields[11])
|
|
retdict[name] = (bytes_sent, bytes_recv, packets_sent, packets_recv,
|
|
errin, errout, dropin, dropout)
|
|
return retdict
|
|
|
|
|
|
def net_if_stats():
|
|
"""Get NIC stats (isup, duplex, speed, mtu)."""
|
|
duplex_map = {cext.DUPLEX_FULL: NIC_DUPLEX_FULL,
|
|
cext.DUPLEX_HALF: NIC_DUPLEX_HALF,
|
|
cext.DUPLEX_UNKNOWN: NIC_DUPLEX_UNKNOWN}
|
|
names = net_io_counters().keys()
|
|
ret = {}
|
|
for name in names:
|
|
isup, duplex, speed, mtu = cext.net_if_stats(name)
|
|
duplex = duplex_map[duplex]
|
|
ret[name] = _common.snicstats(isup, duplex, speed, mtu)
|
|
return ret
|
|
|
|
|
|
net_if_addrs = cext_posix.net_if_addrs
|
|
|
|
|
|
# --- disks
|
|
|
|
def disk_io_counters():
|
|
"""Return disk I/O statistics for every disk installed on the
|
|
system as a dict of raw tuples.
|
|
"""
|
|
# man iostat states that sectors are equivalent with blocks and
|
|
# have a size of 512 bytes since 2.4 kernels. This value is
|
|
# needed to calculate the amount of disk I/O in bytes.
|
|
SECTOR_SIZE = 512
|
|
|
|
# determine partitions we want to look for
|
|
partitions = []
|
|
with open("/proc/partitions", "rt") as f:
|
|
lines = f.readlines()[2:]
|
|
for line in reversed(lines):
|
|
_, _, _, name = line.split()
|
|
if name[-1].isdigit():
|
|
# we're dealing with a partition (e.g. 'sda1'); 'sda' will
|
|
# also be around but we want to omit it
|
|
partitions.append(name)
|
|
else:
|
|
if not partitions or not partitions[-1].startswith(name):
|
|
# we're dealing with a disk entity for which no
|
|
# partitions have been defined (e.g. 'sda' but
|
|
# 'sda1' was not around), see:
|
|
# https://github.com/giampaolo/psutil/issues/338
|
|
partitions.append(name)
|
|
#
|
|
retdict = {}
|
|
with open("/proc/diskstats", "rt") as f:
|
|
lines = f.readlines()
|
|
for line in lines:
|
|
# http://www.mjmwired.net/kernel/Documentation/iostats.txt
|
|
fields = line.split()
|
|
if len(fields) > 7:
|
|
_, _, name, reads, _, rbytes, rtime, writes, _, wbytes, wtime = \
|
|
fields[:11]
|
|
else:
|
|
# from kernel 2.6.0 to 2.6.25
|
|
_, _, name, reads, rbytes, writes, wbytes = fields
|
|
rtime, wtime = 0, 0
|
|
if name in partitions:
|
|
rbytes = int(rbytes) * SECTOR_SIZE
|
|
wbytes = int(wbytes) * SECTOR_SIZE
|
|
reads = int(reads)
|
|
writes = int(writes)
|
|
rtime = int(rtime)
|
|
wtime = int(wtime)
|
|
retdict[name] = (reads, writes, rbytes, wbytes, rtime, wtime)
|
|
return retdict
|
|
|
|
|
|
def disk_partitions(all=False):
|
|
"""Return mounted disk partitions as a list of namedtuples"""
|
|
fstypes = set()
|
|
with open("/proc/filesystems", "r") as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line.startswith("nodev"):
|
|
fstypes.add(line.strip())
|
|
else:
|
|
# ignore all lines starting with "nodev" except "nodev zfs"
|
|
fstype = line.split("\t")[1]
|
|
if fstype == "zfs":
|
|
fstypes.add("zfs")
|
|
|
|
retlist = []
|
|
partitions = cext.disk_partitions()
|
|
for partition in partitions:
|
|
device, mountpoint, fstype, opts = partition
|
|
if device == 'none':
|
|
device = ''
|
|
if not all:
|
|
if device == '' or fstype not in fstypes:
|
|
continue
|
|
ntuple = _common.sdiskpart(device, mountpoint, fstype, opts)
|
|
retlist.append(ntuple)
|
|
return retlist
|
|
|
|
|
|
disk_usage = _psposix.disk_usage
|
|
|
|
|
|
# --- decorators
|
|
|
|
def wrap_exceptions(fun):
|
|
"""Decorator which translates bare OSError and IOError exceptions
|
|
into NoSuchProcess and AccessDenied.
|
|
"""
|
|
@functools.wraps(fun)
|
|
def wrapper(self, *args, **kwargs):
|
|
try:
|
|
return fun(self, *args, **kwargs)
|
|
except EnvironmentError as err:
|
|
# support for private module import
|
|
if NoSuchProcess is None or AccessDenied is None:
|
|
raise
|
|
# ENOENT (no such file or directory) gets raised on open().
|
|
# ESRCH (no such process) can get raised on read() if
|
|
# process is gone in meantime.
|
|
if err.errno in (errno.ENOENT, errno.ESRCH):
|
|
raise NoSuchProcess(self.pid, self._name)
|
|
if err.errno in (errno.EPERM, errno.EACCES):
|
|
raise AccessDenied(self.pid, self._name)
|
|
raise
|
|
return wrapper
|
|
|
|
|
|
def wrap_exceptions_w_zombie(fun):
|
|
"""Same as above but also handles zombies."""
|
|
@functools.wraps(fun)
|
|
def wrapper(self, *args, **kwargs):
|
|
try:
|
|
return wrap_exceptions(fun)(self)
|
|
except NoSuchProcess:
|
|
if not pid_exists(self.pid):
|
|
raise
|
|
else:
|
|
raise ZombieProcess(self.pid, self._name, self._ppid)
|
|
return wrapper
|
|
|
|
|
|
class Process(object):
|
|
"""Linux process implementation."""
|
|
|
|
__slots__ = ["pid", "_name", "_ppid"]
|
|
|
|
def __init__(self, pid):
|
|
self.pid = pid
|
|
self._name = None
|
|
self._ppid = None
|
|
|
|
@wrap_exceptions
|
|
def name(self):
|
|
fname = "/proc/%s/stat" % self.pid
|
|
kw = dict(encoding=DEFAULT_ENCODING) if PY3 else dict()
|
|
with open(fname, "rt", **kw) as f:
|
|
data = f.read()
|
|
# XXX - gets changed later and probably needs refactoring
|
|
return data[data.find('(') + 1:data.rfind(')')]
|
|
|
|
def exe(self):
|
|
try:
|
|
exe = os.readlink("/proc/%s/exe" % self.pid)
|
|
except OSError as err:
|
|
if err.errno in (errno.ENOENT, errno.ESRCH):
|
|
# no such file error; might be raised also if the
|
|
# path actually exists for system processes with
|
|
# low pids (about 0-20)
|
|
if os.path.lexists("/proc/%s" % self.pid):
|
|
return ""
|
|
else:
|
|
if not pid_exists(self.pid):
|
|
raise NoSuchProcess(self.pid, self._name)
|
|
else:
|
|
raise ZombieProcess(self.pid, self._name, self._ppid)
|
|
if err.errno in (errno.EPERM, errno.EACCES):
|
|
raise AccessDenied(self.pid, self._name)
|
|
raise
|
|
|
|
# readlink() might return paths containing null bytes ('\x00').
|
|
# Certain names have ' (deleted)' appended. Usually this is
|
|
# bogus as the file actually exists. Either way that's not
|
|
# important as we don't want to discriminate executables which
|
|
# have been deleted.
|
|
exe = exe.split('\x00')[0]
|
|
if exe.endswith(' (deleted)') and not os.path.exists(exe):
|
|
exe = exe[:-10]
|
|
return exe
|
|
|
|
@wrap_exceptions
|
|
def cmdline(self):
|
|
fname = "/proc/%s/cmdline" % self.pid
|
|
kw = dict(encoding=DEFAULT_ENCODING) if PY3 else dict()
|
|
with open(fname, "rt", **kw) as f:
|
|
data = f.read()
|
|
if data.endswith('\x00'):
|
|
data = data[:-1]
|
|
return [x for x in data.split('\x00')]
|
|
|
|
@wrap_exceptions
|
|
def terminal(self):
|
|
tmap = _psposix._get_terminal_map()
|
|
with open("/proc/%s/stat" % self.pid, 'rb') as f:
|
|
tty_nr = int(f.read().split(b' ')[6])
|
|
try:
|
|
return tmap[tty_nr]
|
|
except KeyError:
|
|
return None
|
|
|
|
if os.path.exists('/proc/%s/io' % os.getpid()):
|
|
@wrap_exceptions
|
|
def io_counters(self):
|
|
fname = "/proc/%s/io" % self.pid
|
|
with open(fname, 'rb') as f:
|
|
rcount = wcount = rbytes = wbytes = None
|
|
for line in f:
|
|
if rcount is None and line.startswith(b"syscr"):
|
|
rcount = int(line.split()[1])
|
|
elif wcount is None and line.startswith(b"syscw"):
|
|
wcount = int(line.split()[1])
|
|
elif rbytes is None and line.startswith(b"read_bytes"):
|
|
rbytes = int(line.split()[1])
|
|
elif wbytes is None and line.startswith(b"write_bytes"):
|
|
wbytes = int(line.split()[1])
|
|
for x in (rcount, wcount, rbytes, wbytes):
|
|
if x is None:
|
|
raise NotImplementedError(
|
|
"couldn't read all necessary info from %r" % fname)
|
|
return _common.pio(rcount, wcount, rbytes, wbytes)
|
|
else:
|
|
def io_counters(self):
|
|
raise NotImplementedError("couldn't find /proc/%s/io (kernel "
|
|
"too old?)" % self.pid)
|
|
|
|
@wrap_exceptions
|
|
def cpu_times(self):
|
|
with open("/proc/%s/stat" % self.pid, 'rb') as f:
|
|
st = f.read().strip()
|
|
# ignore the first two values ("pid (exe)")
|
|
st = st[st.find(b')') + 2:]
|
|
values = st.split(b' ')
|
|
utime = float(values[11]) / CLOCK_TICKS
|
|
stime = float(values[12]) / CLOCK_TICKS
|
|
return _common.pcputimes(utime, stime)
|
|
|
|
@wrap_exceptions
|
|
def wait(self, timeout=None):
|
|
try:
|
|
return _psposix.wait_pid(self.pid, timeout)
|
|
except _psposix.TimeoutExpired:
|
|
# support for private module import
|
|
if TimeoutExpired is None:
|
|
raise
|
|
raise TimeoutExpired(timeout, self.pid, self._name)
|
|
|
|
@wrap_exceptions
|
|
def create_time(self):
|
|
with open("/proc/%s/stat" % self.pid, 'rb') as f:
|
|
st = f.read().strip()
|
|
# ignore the first two values ("pid (exe)")
|
|
st = st[st.rfind(b')') + 2:]
|
|
values = st.split(b' ')
|
|
# According to documentation, starttime is in field 21 and the
|
|
# unit is jiffies (clock ticks).
|
|
# We first divide it for clock ticks and then add uptime returning
|
|
# seconds since the epoch, in UTC.
|
|
# Also use cached value if available.
|
|
bt = BOOT_TIME or boot_time()
|
|
return (float(values[19]) / CLOCK_TICKS) + bt
|
|
|
|
@wrap_exceptions
|
|
def memory_info(self):
|
|
with open("/proc/%s/statm" % self.pid, 'rb') as f:
|
|
vms, rss = f.readline().split()[:2]
|
|
return _common.pmem(int(rss) * PAGESIZE,
|
|
int(vms) * PAGESIZE)
|
|
|
|
@wrap_exceptions
|
|
def memory_info_ex(self):
|
|
# ============================================================
|
|
# | FIELD | DESCRIPTION | AKA | TOP |
|
|
# ============================================================
|
|
# | rss | resident set size | | RES |
|
|
# | vms | total program size | size | VIRT |
|
|
# | shared | shared pages (from shared mappings) | | SHR |
|
|
# | text | text ('code') | trs | CODE |
|
|
# | lib | library (unused in Linux 2.6) | lrs | |
|
|
# | data | data + stack | drs | DATA |
|
|
# | dirty | dirty pages (unused in Linux 2.6) | dt | |
|
|
# ============================================================
|
|
with open("/proc/%s/statm" % self.pid, "rb") as f:
|
|
vms, rss, shared, text, lib, data, dirty = \
|
|
[int(x) * PAGESIZE for x in f.readline().split()[:7]]
|
|
return pextmem(rss, vms, shared, text, lib, data, dirty)
|
|
|
|
if os.path.exists('/proc/%s/smaps' % os.getpid()):
|
|
|
|
@wrap_exceptions
|
|
def memory_maps(self):
|
|
"""Return process's mapped memory regions as a list of named tuples.
|
|
Fields are explained in 'man proc'; here is an updated (Apr 2012)
|
|
version: http://goo.gl/fmebo
|
|
"""
|
|
with open("/proc/%s/smaps" % self.pid, "rt") as f:
|
|
first_line = f.readline()
|
|
current_block = [first_line]
|
|
|
|
def get_blocks():
|
|
data = {}
|
|
for line in f:
|
|
fields = line.split(None, 5)
|
|
if not fields[0].endswith(':'):
|
|
# new block section
|
|
yield (current_block.pop(), data)
|
|
current_block.append(line)
|
|
else:
|
|
try:
|
|
data[fields[0]] = int(fields[1]) * 1024
|
|
except ValueError:
|
|
if fields[0].startswith('VmFlags:'):
|
|
# see issue #369
|
|
continue
|
|
else:
|
|
raise ValueError("don't know how to inte"
|
|
"rpret line %r" % line)
|
|
yield (current_block.pop(), data)
|
|
|
|
ls = []
|
|
if first_line: # smaps file can be empty
|
|
for header, data in get_blocks():
|
|
hfields = header.split(None, 5)
|
|
try:
|
|
addr, perms, offset, dev, inode, path = hfields
|
|
except ValueError:
|
|
addr, perms, offset, dev, inode, path = \
|
|
hfields + ['']
|
|
if not path:
|
|
path = '[anon]'
|
|
else:
|
|
path = path.strip()
|
|
ls.append((
|
|
addr, perms, path,
|
|
data['Rss:'],
|
|
data.get('Size:', 0),
|
|
data.get('Pss:', 0),
|
|
data.get('Shared_Clean:', 0),
|
|
data.get('Shared_Dirty:', 0),
|
|
data.get('Private_Clean:', 0),
|
|
data.get('Private_Dirty:', 0),
|
|
data.get('Referenced:', 0),
|
|
data.get('Anonymous:', 0),
|
|
data.get('Swap:', 0)
|
|
))
|
|
return ls
|
|
|
|
else:
|
|
def memory_maps(self):
|
|
msg = "couldn't find /proc/%s/smaps; kernel < 2.6.14 or " \
|
|
"CONFIG_MMU kernel configuration option is not enabled" \
|
|
% self.pid
|
|
raise NotImplementedError(msg)
|
|
|
|
@wrap_exceptions_w_zombie
|
|
def cwd(self):
|
|
# readlink() might return paths containing null bytes causing
|
|
# problems when used with other fs-related functions (os.*,
|
|
# open(), ...)
|
|
path = os.readlink("/proc/%s/cwd" % self.pid)
|
|
return path.replace('\x00', '')
|
|
|
|
@wrap_exceptions
|
|
def num_ctx_switches(self):
|
|
vol = unvol = None
|
|
with open("/proc/%s/status" % self.pid, "rb") as f:
|
|
for line in f:
|
|
if line.startswith(b"voluntary_ctxt_switches"):
|
|
vol = int(line.split()[1])
|
|
elif line.startswith(b"nonvoluntary_ctxt_switches"):
|
|
unvol = int(line.split()[1])
|
|
if vol is not None and unvol is not None:
|
|
return _common.pctxsw(vol, unvol)
|
|
raise NotImplementedError(
|
|
"'voluntary_ctxt_switches' and 'nonvoluntary_ctxt_switches'"
|
|
"fields were not found in /proc/%s/status; the kernel is "
|
|
"probably older than 2.6.23" % self.pid)
|
|
|
|
@wrap_exceptions
|
|
def num_threads(self):
|
|
with open("/proc/%s/status" % self.pid, "rb") as f:
|
|
for line in f:
|
|
if line.startswith(b"Threads:"):
|
|
return int(line.split()[1])
|
|
raise NotImplementedError("line not found")
|
|
|
|
@wrap_exceptions
|
|
def threads(self):
|
|
thread_ids = os.listdir("/proc/%s/task" % self.pid)
|
|
thread_ids.sort()
|
|
retlist = []
|
|
hit_enoent = False
|
|
for thread_id in thread_ids:
|
|
fname = "/proc/%s/task/%s/stat" % (self.pid, thread_id)
|
|
try:
|
|
with open(fname, 'rb') as f:
|
|
st = f.read().strip()
|
|
except IOError as err:
|
|
if err.errno == errno.ENOENT:
|
|
# no such file or directory; it means thread
|
|
# disappeared on us
|
|
hit_enoent = True
|
|
continue
|
|
raise
|
|
# ignore the first two values ("pid (exe)")
|
|
st = st[st.find(b')') + 2:]
|
|
values = st.split(b' ')
|
|
utime = float(values[11]) / CLOCK_TICKS
|
|
stime = float(values[12]) / CLOCK_TICKS
|
|
ntuple = _common.pthread(int(thread_id), utime, stime)
|
|
retlist.append(ntuple)
|
|
if hit_enoent:
|
|
# raise NSP if the process disappeared on us
|
|
os.stat('/proc/%s' % self.pid)
|
|
return retlist
|
|
|
|
@wrap_exceptions
|
|
def nice_get(self):
|
|
# with open('/proc/%s/stat' % self.pid, 'r') as f:
|
|
# data = f.read()
|
|
# return int(data.split()[18])
|
|
|
|
# Use C implementation
|
|
return cext_posix.getpriority(self.pid)
|
|
|
|
@wrap_exceptions
|
|
def nice_set(self, value):
|
|
return cext_posix.setpriority(self.pid, value)
|
|
|
|
@wrap_exceptions
|
|
def cpu_affinity_get(self):
|
|
return cext.proc_cpu_affinity_get(self.pid)
|
|
|
|
@wrap_exceptions
|
|
def cpu_affinity_set(self, cpus):
|
|
try:
|
|
cext.proc_cpu_affinity_set(self.pid, cpus)
|
|
except OSError as err:
|
|
if err.errno == errno.EINVAL:
|
|
allcpus = tuple(range(len(per_cpu_times())))
|
|
for cpu in cpus:
|
|
if cpu not in allcpus:
|
|
raise ValueError("invalid CPU #%i (choose between %s)"
|
|
% (cpu, allcpus))
|
|
raise
|
|
|
|
# only starting from kernel 2.6.13
|
|
if hasattr(cext, "proc_ioprio_get"):
|
|
|
|
@wrap_exceptions
|
|
def ionice_get(self):
|
|
ioclass, value = cext.proc_ioprio_get(self.pid)
|
|
if enum is not None:
|
|
ioclass = IOPriority(ioclass)
|
|
return _common.pionice(ioclass, value)
|
|
|
|
@wrap_exceptions
|
|
def ionice_set(self, ioclass, value):
|
|
if value is not None:
|
|
if not PY3 and not isinstance(value, (int, long)):
|
|
msg = "value argument is not an integer (gor %r)" % value
|
|
raise TypeError(msg)
|
|
if not 0 <= value <= 8:
|
|
raise ValueError(
|
|
"value argument range expected is between 0 and 8")
|
|
|
|
if ioclass in (IOPRIO_CLASS_NONE, None):
|
|
if value:
|
|
msg = "can't specify value with IOPRIO_CLASS_NONE " \
|
|
"(got %r)" % value
|
|
raise ValueError(msg)
|
|
ioclass = IOPRIO_CLASS_NONE
|
|
value = 0
|
|
elif ioclass == IOPRIO_CLASS_IDLE:
|
|
if value:
|
|
msg = "can't specify value with IOPRIO_CLASS_IDLE " \
|
|
"(got %r)" % value
|
|
raise ValueError(msg)
|
|
value = 0
|
|
elif ioclass in (IOPRIO_CLASS_RT, IOPRIO_CLASS_BE):
|
|
if value is None:
|
|
# TODO: add comment explaining why this is 4 (?)
|
|
value = 4
|
|
else:
|
|
# otherwise we would get OSError(EVINAL)
|
|
raise ValueError("invalid ioclass argument %r" % ioclass)
|
|
|
|
return cext.proc_ioprio_set(self.pid, ioclass, value)
|
|
|
|
if HAS_PRLIMIT:
|
|
@wrap_exceptions
|
|
def rlimit(self, resource, limits=None):
|
|
# If pid is 0 prlimit() applies to the calling process and
|
|
# we don't want that. We should never get here though as
|
|
# PID 0 is not supported on Linux.
|
|
if self.pid == 0:
|
|
raise ValueError("can't use prlimit() against PID 0 process")
|
|
try:
|
|
if limits is None:
|
|
# get
|
|
return cext.linux_prlimit(self.pid, resource)
|
|
else:
|
|
# set
|
|
if len(limits) != 2:
|
|
raise ValueError(
|
|
"second argument must be a (soft, hard) tuple, "
|
|
"got %s" % repr(limits))
|
|
soft, hard = limits
|
|
cext.linux_prlimit(self.pid, resource, soft, hard)
|
|
except OSError as err:
|
|
if err.errno == errno.ENOSYS and pid_exists(self.pid):
|
|
# I saw this happening on Travis:
|
|
# https://travis-ci.org/giampaolo/psutil/jobs/51368273
|
|
raise ZombieProcess(self.pid, self._name, self._ppid)
|
|
else:
|
|
raise
|
|
|
|
@wrap_exceptions
|
|
def status(self):
|
|
with open("/proc/%s/status" % self.pid, 'rb') as f:
|
|
for line in f:
|
|
if line.startswith(b"State:"):
|
|
letter = line.split()[1]
|
|
if PY3:
|
|
letter = letter.decode()
|
|
# XXX is '?' legit? (we're not supposed to return
|
|
# it anyway)
|
|
return PROC_STATUSES.get(letter, '?')
|
|
|
|
@wrap_exceptions
|
|
def open_files(self):
|
|
retlist = []
|
|
files = os.listdir("/proc/%s/fd" % self.pid)
|
|
hit_enoent = False
|
|
for fd in files:
|
|
file = "/proc/%s/fd/%s" % (self.pid, fd)
|
|
try:
|
|
file = os.readlink(file)
|
|
except OSError as err:
|
|
# ENOENT == file which is gone in the meantime
|
|
if err.errno in (errno.ENOENT, errno.ESRCH):
|
|
hit_enoent = True
|
|
continue
|
|
elif err.errno == errno.EINVAL:
|
|
# not a link
|
|
continue
|
|
else:
|
|
raise
|
|
else:
|
|
# If file is not an absolute path there's no way
|
|
# to tell whether it's a regular file or not,
|
|
# so we skip it. A regular file is always supposed
|
|
# to be absolutized though.
|
|
if file.startswith('/') and isfile_strict(file):
|
|
ntuple = _common.popenfile(file, int(fd))
|
|
retlist.append(ntuple)
|
|
if hit_enoent:
|
|
# raise NSP if the process disappeared on us
|
|
os.stat('/proc/%s' % self.pid)
|
|
return retlist
|
|
|
|
@wrap_exceptions
|
|
def connections(self, kind='inet'):
|
|
ret = _connections.retrieve(kind, self.pid)
|
|
# raise NSP if the process disappeared on us
|
|
os.stat('/proc/%s' % self.pid)
|
|
return ret
|
|
|
|
@wrap_exceptions
|
|
def num_fds(self):
|
|
return len(os.listdir("/proc/%s/fd" % self.pid))
|
|
|
|
@wrap_exceptions
|
|
def ppid(self):
|
|
fpath = "/proc/%s/status" % self.pid
|
|
with open(fpath, 'rb') as f:
|
|
for line in f:
|
|
if line.startswith(b"PPid:"):
|
|
# PPid: nnnn
|
|
return int(line.split()[1])
|
|
raise NotImplementedError("line 'PPid' not found in %s" % fpath)
|
|
|
|
@wrap_exceptions
|
|
def uids(self):
|
|
fpath = "/proc/%s/status" % self.pid
|
|
with open(fpath, 'rb') as f:
|
|
for line in f:
|
|
if line.startswith(b'Uid:'):
|
|
_, real, effective, saved, fs = line.split()
|
|
return _common.puids(int(real), int(effective), int(saved))
|
|
raise NotImplementedError("line 'Uid' not found in %s" % fpath)
|
|
|
|
@wrap_exceptions
|
|
def gids(self):
|
|
fpath = "/proc/%s/status" % self.pid
|
|
with open(fpath, 'rb') as f:
|
|
for line in f:
|
|
if line.startswith(b'Gid:'):
|
|
_, real, effective, saved, fs = line.split()
|
|
return _common.pgids(int(real), int(effective), int(saved))
|
|
raise NotImplementedError("line 'Gid' not found in %s" % fpath)
|