1. 输入和输出

本系列介绍的重点是获取系统性能数据的各种数据源,所以注意力会放在命令行模式的软件上,本篇将重点讲一下如何与外部程序交互,通过输出重定向获取系统性能数据。

命令行软件在进行交互时,交互类型不外乎输入和输出两种(输出又分标准输出和错误输出)。标准输入一般靠键盘,标准输出或错误输出的设备一般是显示器或者终端,在编写批处理或shell脚本时,经常会通过管道将输出重定向到文件或其它程序的输入端。

如果按软件是否持续运行来区分,软件可以分为持续运行的应用(比如JAVA Web APP)和一次执行便终止的应用(比如ifconfig命令);它们的区别在于前者一直会处于运行状态,除非人为终止或者达到指定的退出条件才会退出运行,后者往往只需要很短的时间,执行完当次任务便即退出。

2. 获取程序运行的结果

数据源按形式可以分为两类:程序的输出、外部数据来源(文件、数据库、缓存等等),而程序的输出经过重定向,也可以转换为外部数据源形式。

当然,不同的软件会输出不同的结果,并且输出内容也有自己特有的格式,所以获取一项数据结果有两件事需要做:一是数据获取,二是数据处理(一般都是用字符串处理)。

正如上面所介绍,数据源存在两种形式,这两种形式的数据获取对应也分为两种途径:要么直接获取程序输出,要么将程序输出保存到外部再读取外部存储的数据。

3. 管道

管道相当于一条流水线,获得原始数据以后(可以是文件、可以是程序的输出),经由一道道处理工序,将数据变成我们需要的样子。举一个最简单的例子,获取当前系统运行的java程序的数量:

root@hostname:~# ps -ef | egrep java | grep -v grep | wc -l
2

又或者,查看当前系统所有80端口的客户端数量:

[root@hostname ~]# netstat -anopt |grep "172.21.0.10:80" | wc -l
3

稍微改一下,判断应用abc是否在运行,如果未运行,则启动abc:

#!/bin/bash
if [ `ps -ef|grep abc|wc -l` -eq 1 ]; then
  /opt/abc
fi

这一系列脚本演示了如何通过管道一步步获取需要的信息,并将所获取到的数据用到判断条件中。

4. 直接获取程序的运行结果(.Net Process类StandardOutput流的同步读取)

直接获取程序运行结果一般用于一次性运行的命令,命令执行以后立即退出,对于这种类型的命令只需要使用同步方式读取标准输出流的所有内容即可。

需求示例:使用df命令获取分区大小及使用率,ifconfig命令获取IP地址,hostname命令获取主机名。

        static void Main(string[] args)
        {
            Process proc = Process.Start(
                new ProcessStartInfo("ifconfig")
                {
                    UseShellExecute = false,
                    RedirectStandardOutput = true
                });
            string output = proc.StandardOutput.ReadToEnd();
            proc.WaitForExit();
            Console.WriteLine("IP:\r\n" +
                String.Join(
                    "\r\n",
                    Regex.Matches(
                        output,
                        @"(\S+)\s+flags.*?inet\s+((\d+\.?){4})\s+netmask\s+((\d+\.?){4}).*?ether\s+(([a-f0-9]{2}:?){6})",
                        RegexOptions.Singleline).
                        Select(m => $"{m.Groups[1].Value}\t{m.Groups[2].Value}/{m.Groups[3].Value}\t{m.Groups[4].Value}")));

            proc = Process.Start(
                new ProcessStartInfo("df")
                {
                    Arguments = "--output=target,size,avail,pcent -h",
                    UseShellExecute = false,
                    RedirectStandardOutput = true
                });
            output = proc.StandardOutput.ReadToEnd();
            proc.WaitForExit();
            Console.WriteLine("\r\nPartitions:\r\n" +
                string.Join(
                    "\r\n",
                    Regex.Matches(
                        output,
                        @"(\S+)\s+([0-9.]+[TGMK])\s+([0-9.]+[TGMK])\s+(\S+)").
                        Select(m => $"{m.Groups[1].Value}: {m.Groups[2].Value}: {m.Groups[3].Value}: {m.Groups[4].Value}")));

            proc = Process.Start(
                new ProcessStartInfo("hostname")
                {
                    UseShellExecute = false,
                    RedirectStandardOutput = true
                });
            Console.WriteLine("\r\nHOSTNAME:\t" + proc.StandardOutput.ReadToEnd());
            proc.WaitForExit();
            Console.ReadLine();
        }

5. 接管程序的标准输出(.Net Process类的StandardOutput重定向)

如果程序一直处于运行状态,并且持续不断地产生输出,则可以利用异步读取的方式来重定向标准输出,捕获其OutputDataReceived事件,进而获取连续数据。

需求示例:通过重定向获取top命令的输出结果,提取它每一次输出的CPU空闲率,以及可用内存和总内存。

    class Program
    {
        static void Main(string[] args)
        {
            StringBuilder buffer = new StringBuilder();
            Process proc = Process.Start(
                new ProcessStartInfo("top")
                {
                    Arguments = "-b",
                    UseShellExecute = false,
                    RedirectStandardOutput = true
                });
            proc.OutputDataReceived += (s, e) =>
            {
                string line = e.Data;
                if (Regex.IsMatch(
                    line,
                    @"^\s+PID\s+USER\s+PR\s+NI\s+VIRT\s+RES\s+SHR\s+S\s+%CPU\s+%MEM\s+TIME\+\s+COMMAND"))
                {
                    var m =
                    Regex.Match(
                        buffer.ToString(),
                        @"ni,\s+([0-9.]+)\s+id,.*MiB\s+Mem.*?([0-9.]+)\s+total,.*?([0-9.]+)\s+used,.*?MiB Swap.*?([0-9.]+)\s+total,.*?([0-9.]+)\s+used\.",
                        RegexOptions.Singleline);
                    Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss\r\n") +
                        $"CPU IDLE:\t{m.Groups[1]}%\r\n" +
                        $"Mem Used:\t{m.Groups[3].Value} M of {m.Groups[2].Value}M\r\n" +
                        $"SwapUsed:\t{m.Groups[5].Value} M of {m.Groups[4].Value}M\r\n");
                    buffer.Clear();
                }
                else
                {
                    buffer.AppendLine(line);
                }
            };
            proc.BeginOutputReadLine();
            proc.WaitForExit();
        }
    }