/* $Id$ */
/** @file
*
* VBox Linux-specific Performance Classes implementation.
*/
/*
* Copyright (C) 2008-2012 Oracle Corporation
*
* This file is part of VirtualBox Open Source Edition (OSE), as
* available from http://www.virtualbox.org. This file is free software;
* you can redistribute it and/or modify it under the terms of the GNU
* General Public License (GPL) as published by the Free Software
* Foundation, in version 2 as it comes in the "COPYING" file of the
* VirtualBox OSE distribution. VirtualBox OSE is distributed in the
* hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/statvfs.h>
#include <errno.h>
#include <mntent.h>
#include <iprt/alloc.h>
#include <iprt/cdefs.h>
#include <iprt/ctype.h>
#include <iprt/err.h>
#include <iprt/param.h>
#include <iprt/path.h>
#include <iprt/string.h>
#include <iprt/system.h>
#include <iprt/mp.h>
#include <iprt/linux/sysfs.h>
#include <map>
#include <vector>
#include "Logging.h"
#include "Performance.h"
#define VBOXVOLINFO_NAME "VBoxVolInfo"
namespace pm {
class CollectorLinux : public CollectorHAL
{
public:
CollectorLinux();
virtual int preCollect(const CollectorHints& hints, uint64_t /* iTick */);
virtual int getHostMemoryUsage(ULONG *total, ULONG *used, ULONG *available);
virtual int getHostFilesystemUsage(const char *name, ULONG *total, ULONG *used, ULONG *available);
virtual int getHostDiskSize(const char *name, uint64_t *size);
virtual int getProcessMemoryUsage(RTPROCESS process, ULONG *used);
virtual int getRawHostCpuLoad(uint64_t *user, uint64_t *kernel, uint64_t *idle);
virtual int getRawHostNetworkLoad(const char *name, uint64_t *rx, uint64_t *tx);
virtual int getRawHostDiskLoad(const char *name, uint64_t *disk_ms, uint64_t *total_ms);
virtual int getRawProcessCpuLoad(RTPROCESS process, uint64_t *user, uint64_t *kernel, uint64_t *total);
virtual int getDiskListByFs(const char *name, DiskList& listUsage, DiskList& listLoad);
private:
virtual int _getRawHostCpuLoad();
int getRawProcessStats(RTPROCESS process, uint64_t *cpuUser, uint64_t *cpuKernel, ULONG *memPagesUsed);
void getDiskName(char *pszDiskName, size_t cbDiskName, const char *pszDevName, bool fTrimDigits);
void addVolumeDependencies(const char *pcszVolume, DiskList& listDisks);
void addRaidDisks(const char *pcszDevice, DiskList& listDisks);
char *trimTrailingDigits(char *pszName);
char *trimNewline(char *pszName);
struct VMProcessStats
{
uint64_t cpuUser;
uint64_t cpuKernel;
ULONG pagesUsed;
};
typedef std::map<RTPROCESS, VMProcessStats> VMProcessMap;
VMProcessMap mProcessStats;
uint64_t mUser, mKernel, mIdle;
uint64_t mSingleUser, mSingleKernel, mSingleIdle;
uint32_t mHZ;
ULONG totalRAM;
};
CollectorHAL *createHAL()
{
return new CollectorLinux();
}
// Collector HAL for Linux
CollectorLinux::CollectorLinux()
{
long hz = sysconf(_SC_CLK_TCK);
if (hz == -1)
{
LogRel(("CollectorLinux failed to obtain HZ from kernel, assuming 100.\n"));
mHZ = 100;
}
else
mHZ = hz;
LogFlowThisFunc(("mHZ=%u\n", mHZ));
uint64_t cb;
int rc = RTSystemQueryTotalRam(&cb);
if (RT_FAILURE(rc))
totalRAM = 0;
else
totalRAM = (ULONG)(cb / 1024);
}
int CollectorLinux::preCollect(const CollectorHints& hints, uint64_t /* iTick */)
{
std::vector<RTPROCESS> processes;
hints.getProcesses(processes);
std::vector<RTPROCESS>::iterator it;
for (it = processes.begin(); it != processes.end(); it++)
{
VMProcessStats vmStats;
int rc = getRawProcessStats(*it, &vmStats.cpuUser, &vmStats.cpuKernel, &vmStats.pagesUsed);
/* On failure, do NOT stop. Just skip the entry. Having the stats for
* one (probably broken) process frozen/zero is a minor issue compared
* to not updating many process stats and the host cpu stats. */
if (RT_SUCCESS(rc))
mProcessStats[*it] = vmStats;
}
if (hints.isHostCpuLoadCollected() || mProcessStats.size())
{
_getRawHostCpuLoad();
}
return VINF_SUCCESS;
}
int CollectorLinux::_getRawHostCpuLoad()
{
int rc = VINF_SUCCESS;
long long unsigned uUser, uNice, uKernel, uIdle, uIowait, uIrq, uSoftirq;
FILE *f = fopen("/proc/stat", "r");
if (f)
{
char szBuf[128];
if (fgets(szBuf, sizeof(szBuf), f))
{
if (sscanf(szBuf, "cpu %llu %llu %llu %llu %llu %llu %llu",
&uUser, &uNice, &uKernel, &uIdle, &uIowait,
&uIrq, &uSoftirq) == 7)
{
mUser = uUser + uNice;
mKernel = uKernel + uIrq + uSoftirq;
mIdle = uIdle + uIowait;
}
/* Try to get single CPU stats. */
if (fgets(szBuf, sizeof(szBuf), f))
{
if (sscanf(szBuf, "cpu0 %llu %llu %llu %llu %llu %llu %llu",
&uUser, &uNice, &uKernel, &uIdle, &uIowait,
&uIrq, &uSoftirq) == 7)
{
mSingleUser = uUser + uNice;
mSingleKernel = uKernel + uIrq + uSoftirq;
mSingleIdle = uIdle + uIowait;
}
else
{
/* Assume that this is not an SMP system. */
Assert(RTMpGetCount() == 1);
mSingleUser = mUser;
mSingleKernel = mKernel;
mSingleIdle = mIdle;
}
}
else
rc = VERR_FILE_IO_ERROR;
}
else
rc = VERR_FILE_IO_ERROR;
fclose(f);
}
else
rc = VERR_ACCESS_DENIED;
return rc;
}
int CollectorLinux::getRawHostCpuLoad(uint64_t *user, uint64_t *kernel, uint64_t *idle)
{
*user = mUser;
*kernel = mKernel;
*idle = mIdle;
return VINF_SUCCESS;
}
int CollectorLinux::getRawProcessCpuLoad(RTPROCESS process, uint64_t *user, uint64_t *kernel, uint64_t *total)
{
VMProcessMap::const_iterator it = mProcessStats.find(process);
if (it == mProcessStats.end())
{
Log (("No stats pre-collected for process %x\n", process));
return VERR_INTERNAL_ERROR;
}
*user = it->second.cpuUser;
*kernel = it->second.cpuKernel;
*total = mUser + mKernel + mIdle;
return VINF_SUCCESS;
}
int CollectorLinux::getHostMemoryUsage(ULONG *total, ULONG *used, ULONG *available)
{
AssertReturn(totalRAM, VERR_INTERNAL_ERROR);
uint64_t cb;
int rc = RTSystemQueryAvailableRam(&cb);
if (RT_SUCCESS(rc))
{
*total = totalRAM;
*available = cb / 1024;
*used = *total - *available;
}
return rc;
}
int CollectorLinux::getHostFilesystemUsage(const char *path, ULONG *total, ULONG *used, ULONG *available)
{
struct statvfs stats;
if (statvfs(path, &stats) == -1)
{
LogRel(("Failed to collect %s filesystem usage: errno=%d.\n", path, errno));
return VERR_ACCESS_DENIED;
}
uint64_t cbBlock = stats.f_frsize ? stats.f_frsize : stats.f_bsize;
*total = (ULONG)(cbBlock * stats.f_blocks / _1M);
*used = (ULONG)(cbBlock * (stats.f_blocks - stats.f_bfree) / _1M);
*available = (ULONG)(cbBlock * stats.f_bavail / _1M);
return VINF_SUCCESS;
}
int CollectorLinux::getHostDiskSize(const char *pszFile, uint64_t *size)
{
char *pszPath = NULL;
RTStrAPrintf(&pszPath, "/sys/block/%s/size", pszFile);
Assert(pszPath);
int rc = VINF_SUCCESS;
if (!RTLinuxSysFsExists(pszPath))
rc = VERR_FILE_NOT_FOUND;
else
{
int64_t cSize = RTLinuxSysFsReadIntFile(0, pszPath);
if (cSize < 0)
rc = VERR_ACCESS_DENIED;
else
*size = cSize * 512;
}
RTStrFree(pszPath);
return rc;
}
int CollectorLinux::getProcessMemoryUsage(RTPROCESS process, ULONG *used)
{
VMProcessMap::const_iterator it = mProcessStats.find(process);
if (it == mProcessStats.end())
{
Log (("No stats pre-collected for process %x\n", process));
return VERR_INTERNAL_ERROR;
}
*used = it->second.pagesUsed * (PAGE_SIZE / 1024);
return VINF_SUCCESS;
}
int CollectorLinux::getRawProcessStats(RTPROCESS process, uint64_t *cpuUser, uint64_t *cpuKernel, ULONG *memPagesUsed)
{
int rc = VINF_SUCCESS;
char *pszName;
pid_t pid2;
char c;
int iTmp;
long long unsigned int u64Tmp;
unsigned uTmp;
unsigned long ulTmp;
signed long ilTmp;
ULONG u32user, u32kernel;
char buf[80]; /* @todo: this should be tied to max allowed proc name. */
RTStrAPrintf(&pszName, "/proc/%d/stat", process);
FILE *f = fopen(pszName, "r");
RTStrFree(pszName);
if (f)
{
if (fscanf(f, "%d %79s %c %d %d %d %d %d %u %lu %lu %lu %lu %u %u "
"%ld %ld %ld %ld %ld %ld %llu %lu %u",
&pid2, buf, &c, &iTmp, &iTmp, &iTmp, &iTmp, &iTmp, &uTmp,
&ulTmp, &ulTmp, &ulTmp, &ulTmp, &u32user, &u32kernel,
&ilTmp, &ilTmp, &ilTmp, &ilTmp, &ilTmp, &ilTmp, &u64Tmp,
&ulTmp, memPagesUsed) == 24)
{
Assert((pid_t)process == pid2);
*cpuUser = u32user;
*cpuKernel = u32kernel;
}
else
rc = VERR_FILE_IO_ERROR;
fclose(f);
}
else
rc = VERR_ACCESS_DENIED;
return rc;
}
int CollectorLinux::getRawHostNetworkLoad(const char *pszFile, uint64_t *rx, uint64_t *tx)
{
char szIfName[/*IFNAMSIZ*/ 16 + 36];
RTStrPrintf(szIfName, sizeof(szIfName), "/sys/class/net/%s/statistics/rx_bytes", pszFile);
if (!RTLinuxSysFsExists(szIfName))
return VERR_FILE_NOT_FOUND;
int64_t cSize = RTLinuxSysFsReadIntFile(0, szIfName);
if (cSize < 0)
return VERR_ACCESS_DENIED;
*rx = cSize;
RTStrPrintf(szIfName, sizeof(szIfName), "/sys/class/net/%s/statistics/tx_bytes", pszFile);
if (!RTLinuxSysFsExists(szIfName))
return VERR_FILE_NOT_FOUND;
cSize = RTLinuxSysFsReadIntFile(0, szIfName);
if (cSize < 0)
return VERR_ACCESS_DENIED;
*tx = cSize;
return VINF_SUCCESS;
}
int CollectorLinux::getRawHostDiskLoad(const char *name, uint64_t *disk_ms, uint64_t *total_ms)
{
#if 0
int rc = VINF_SUCCESS;
char szIfName[/*IFNAMSIZ*/ 16 + 36];
long long unsigned int u64Busy, tmp;
RTStrPrintf(szIfName, sizeof(szIfName), "/sys/class/block/%s/stat", name);
FILE *f = fopen(szIfName, "r");
if (f)
{
if (fscanf(f, "%llu %llu %llu %llu %llu %llu %llu %llu %llu %llu %llu",
&tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &u64Busy, &tmp) == 11)
{
*disk_ms = u64Busy;
*total_ms = (uint64_t)(mSingleUser + mSingleKernel + mSingleIdle) * 1000 / mHZ;
}
else
rc = VERR_FILE_IO_ERROR;
fclose(f);
}
else
rc = VERR_ACCESS_DENIED;
#else
int rc = VERR_MISSING;
FILE *f = fopen("/proc/diskstats", "r");
if (f)
{
char szBuf[128];
while (fgets(szBuf, sizeof(szBuf), f))
{
char *pszBufName = szBuf;
while (*pszBufName == ' ') ++pszBufName; /* Skip spaces */
while (RT_C_IS_DIGIT(*pszBufName)) ++pszBufName; /* Skip major */
while (*pszBufName == ' ') ++pszBufName; /* Skip spaces */
while (RT_C_IS_DIGIT(*pszBufName)) ++pszBufName; /* Skip minor */
while (*pszBufName == ' ') ++pszBufName; /* Skip spaces */
char *pszBufData = strchr(pszBufName, ' ');
if (!pszBufData)
{
LogRel(("CollectorLinux::getRawHostDiskLoad() failed to parse disk stats: %s\n", szBuf));
continue;
}
*pszBufData++ = '\0';
if (!strcmp(name, pszBufName))
{
long long unsigned int u64Busy, tmp;
if (sscanf(pszBufData, "%llu %llu %llu %llu %llu %llu %llu %llu %llu %llu %llu",
&tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &u64Busy, &tmp) == 11)
{
*disk_ms = u64Busy;
*total_ms = (uint64_t)(mSingleUser + mSingleKernel + mSingleIdle) * 1000 / mHZ;
rc = VINF_SUCCESS;
}
else
rc = VERR_FILE_IO_ERROR;
break;
}
}
fclose(f);
}
#endif
return rc;
}
char *CollectorLinux::trimNewline(char *pszName)
{
unsigned cbName = strlen(pszName);
if (cbName == 0)
return pszName;
char *pszEnd = pszName + cbName - 1;
while (pszEnd > pszName && *pszEnd == '\n')
pszEnd--;
pszEnd[1] = '\0';
return pszName;
}
char *CollectorLinux::trimTrailingDigits(char *pszName)
{
unsigned cbName = strlen(pszName);
if (cbName == 0)
return pszName;
char *pszEnd = pszName + cbName - 1;
while (pszEnd > pszName && (RT_C_IS_DIGIT(*pszEnd) || *pszEnd == '\n'))
pszEnd--;
pszEnd[1] = '\0';
return pszName;
}
/**
* Use the partition name to get the name of the disk. Any path component is stripped.
* if fTrimDigits is true, trailing digits are stripped as well, for example '/dev/sda5'
* is converted to 'sda'.
*
* @param pszDiskName Where to store the name of the disk.
* @param cbDiskName The size of the buffer pszDiskName points to.
* @param pszDevName The device name used to get the disk name.
* @param fTrimDigits Trim trailing digits (e.g. /dev/sda5)
*/
void CollectorLinux::getDiskName(char *pszDiskName, size_t cbDiskName, const char *pszDevName, bool fTrimDigits)
{
unsigned cbName = 0;
unsigned cbDevName = strlen(pszDevName);
const char *pszEnd = pszDevName + cbDevName - 1;
if (fTrimDigits)
while (pszEnd > pszDevName && RT_C_IS_DIGIT(*pszEnd))
pszEnd--;
while (pszEnd > pszDevName && *pszEnd != '/')
{
cbName++;
pszEnd--;
}
RTStrCopy(pszDiskName, RT_MIN(cbName + 1, cbDiskName), pszEnd + 1);
}
void CollectorLinux::addRaidDisks(const char *pcszDevice, DiskList& listDisks)
{
FILE *f = fopen("/proc/mdstat", "r");
if (f)
{
char szBuf[128];
while (fgets(szBuf, sizeof(szBuf), f))
{
char *pszBufName = szBuf;
char *pszBufData = strchr(pszBufName, ' ');
if (!pszBufData)
{
LogRel(("CollectorLinux::addRaidDisks() failed to parse disk stats: %s\n", szBuf));
continue;
}
*pszBufData++ = '\0';
if (!strcmp(pcszDevice, pszBufName))
{
while (*pszBufData == ':') ++pszBufData; /* Skip delimiter */
while (*pszBufData == ' ') ++pszBufData; /* Skip spaces */
while (RT_C_IS_ALNUM(*pszBufData)) ++pszBufData; /* Skip status */
while (*pszBufData == ' ') ++pszBufData; /* Skip spaces */
while (RT_C_IS_ALNUM(*pszBufData)) ++pszBufData; /* Skip type */
while (*pszBufData != '\0')
{
while (*pszBufData == ' ') ++pszBufData; /* Skip spaces */
char *pszDisk = pszBufData;
while (RT_C_IS_ALPHA(*pszBufData))
++pszBufData;
if (*pszBufData)
{
*pszBufData++ = '\0';
listDisks.push_back(RTCString(pszDisk));
while (*pszBufData != '\0' && *pszBufData != ' ')
++pszBufData;
}
else
listDisks.push_back(RTCString(pszDisk));
}
break;
}
}
fclose(f);
}
}
void CollectorLinux::addVolumeDependencies(const char *pcszVolume, DiskList& listDisks)
{
char szVolInfo[RTPATH_MAX];
int rc = RTPathAppPrivateArch(szVolInfo,
sizeof(szVolInfo) - sizeof("/" VBOXVOLINFO_NAME " ") - strlen(pcszVolume));
if (RT_FAILURE(rc))
{
LogRel(("VolInfo: Failed to get program path, rc=%Rrc\n", rc));
return;
}
strcat(szVolInfo, "/" VBOXVOLINFO_NAME " ");
strcat(szVolInfo, pcszVolume);
FILE *fp = popen(szVolInfo, "r");
if (fp)
{
char szBuf[128];
while (fgets(szBuf, sizeof(szBuf), fp))
if (strncmp(szBuf, RT_STR_TUPLE("dm-")))
listDisks.push_back(RTCString(trimTrailingDigits(szBuf)));
else
listDisks.push_back(RTCString(trimNewline(szBuf)));
pclose(fp);
}
else
listDisks.push_back(RTCString(pcszVolume));
}
int CollectorLinux::getDiskListByFs(const char *pszPath, DiskList& listUsage, DiskList& listLoad)
{
FILE *mtab = setmntent("/etc/mtab", "r");
if (mtab)
{
struct mntent *mntent;
while ((mntent = getmntent(mtab)))
{
/* Skip rootfs entry, there must be another root mount. */
if (strcmp(mntent->mnt_fsname, "rootfs") == 0)
continue;
if (strcmp(pszPath, mntent->mnt_dir) == 0)
{
char szDevName[128];
char szFsName[1024];
/* Try to resolve symbolic link if necessary. Yes, we access the file system here! */
int rc = RTPathReal(mntent->mnt_fsname, szFsName, sizeof(szFsName));
if (RT_FAILURE(rc))
continue; /* something got wrong, just ignore this path */
/* check against the actual mtab entry, NOT the real path as /dev/mapper/xyz is
* often a symlink to something else */
if (!strncmp(mntent->mnt_fsname, RT_STR_TUPLE("/dev/mapper")))
{
/* LVM */
getDiskName(szDevName, sizeof(szDevName), mntent->mnt_fsname, false /*=fTrimDigits*/);
addVolumeDependencies(szDevName, listUsage);
listLoad = listUsage;
}
else if (!strncmp(szFsName, RT_STR_TUPLE("/dev/md")))
{
/* Software RAID */
getDiskName(szDevName, sizeof(szDevName), szFsName, false /*=fTrimDigits*/);
listUsage.push_back(RTCString(szDevName));
addRaidDisks(szDevName, listLoad);
}
else
{
/* Plain disk partition. Trim the trailing digits to get the drive name */
getDiskName(szDevName, sizeof(szDevName), szFsName, true /*=fTrimDigits*/);
listUsage.push_back(RTCString(szDevName));
listLoad.push_back(RTCString(szDevName));
}
if (listUsage.empty() || listLoad.empty())
{
LogRel(("Failed to retrive disk info: getDiskName(%s) --> %s\n",
mntent->mnt_fsname, szDevName));
}
break;
}
}
endmntent(mtab);
}
return VINF_SUCCESS;
}
}