用Python制作检测Linux运行信息的工具的教程

时间:2022-06-26 20:28:51

在这篇文章里,我们将会探索如何使用Python语言作为一个工具来检测Linux系统各种运行信息。让我们一起来学习吧。

哪种Python?

当我提到Python时,我一般是指CPython 2(准确来说是2.7)。当同样的代码不能在CPython3(3.3)运行时,我们明确地把它指出并给出替代的代码,解释它们之间的不同点。请确保你已经安装了CPython,在终端输入python或者python3你会看到Python提示符出现在你的终端里。

请注意,所有的脚本程序都会以#!/usr/bin/env python作为第一行,意味着我们要Python解析器去运行这些脚本。因此,如果你使用 chmod +x your-script.py 命令给你的脚本添加可执行的权限,你可以使用./your-script.py命令直接运行你的脚本(你将会在这篇文章里看到这种操作)

探索platform模块

在标准库中的platform模块有大量的函数让我们去检查各种系统信息。我们一起来打开Python解释器(译者:直接在命令行输入python即可打开)并探索其中的一部分函数。我们先从platform.uname()函数开始:
 

?
1
2
3
>>> import platform
>>> platform.uname()
('Linux', 'fedora.echorand', '3.7.4-204.fc18.x86_64', '#1 SMP Wed Jan 23 16:44:29 UTC 2013', 'x86_64')

如果你知道Linux上的uname命令,你会意识到这个函数就是uname命令的一个接口。在Python 2,这个函数会返回一个由系统类型(或者内核类型),主机名,版本号,发行版号,主机硬件架构和处理器类型组成的元组。你可以使用索引来获取单个属性,像这样:
 

?
1
2
>>> platform.uname()[0]
'Linux'

在Python 3,这个函数会返回一个默认命名的元组:
 

?
1
2
3
4
5
>>> platform.uname()
 
uname_result(system='Linux', node='fedora.echorand',
release='3.7.4-204.fc18.x86_64', version='#1 SMP Wed Jan 23 16:44:29
UTC 2013', machine='x86_64', processor='x86_64')

因为返回值是个默认命名的元组,所以我们可以轻易地通过变量名来获取单个属性而不用去记住各个属性的下标,像这样:
 

?
1
2
>>> platform.uname().system
'Linux'

platfrom模块还提供了一些直接的接口来获取上面的属性值,像这些:
 

?
1
2
3
4
5
>>> platform.system()
'Linux'
 
>>> platform.release()
'3.7.4-204.fc18.x86_64'

函数linx_distribution()返回你正在使用的Linux发行版的详细信息。举个例子,在Fedora 18系统中,这条命令会返回下面的信息:
 

?
1
2
>>> platform.linux_distribution()
('Fedora', '18', 'Spherical Cow')

返回值是一个由发行版本名,版本号,代号组成的元组。你可以通过_supported_dists属性来打印你所用的Python版本支持哪些发行版本:
 

?
1
2
3
4
>>> platform._supported_dists
('SuSE', 'debian', 'fedora', 'redhat', 'centos', 'mandrake',
'mandriva', 'rocks', 'slackware', 'yellowdog', 'gentoo',
'UnitedLinux', 'turbolinux')

如果你的Linux发行版本不是上面那些的其中一个(或者是其中一个的衍生版),那么你调用上面的函数时不会看到任何有用的信息。

最后一个我们要探索的platfrom函数是architecture()函数。当你不添加任何的参数来调用这个函数时,这个函数会返回一个由位架构和Python可执行文件格式组成的元组。比如:
 

?
1
2
>>> platform.architecture()
('64bit', 'ELF')

在32位的Linux系统中,你会看到:
 

?
1
2
>>> platform.architecture()
('32bit', 'ELF')

如果你指定其他任意的系统可执行程序作为参数,你会得到类似的结果:
 

?
1
2
>>> platform.architecture(executable='/usr/bin/ls')
('64bit', 'ELF')

我们鼓励你去探索platfrom模块中的其他函数,让你找到你当前使用的Python的版本。如果你非常想知道这个模块是怎样获取这些信息的,你可以去看Python源代码目录下的Lib/platfrom.py文件。

os和sys模块同样是一个获取统属性的有用模块就像本地BYTEORDER一样。下一步,我们将会不使用Python你标准库模块来探索一些获取Linux系统信息的通用方法,这次我们通过proc和sys文件系统来实现。要注意的是,通过这些文件系统获取的信息在不同的硬件架构里会有所不同。因此,在阅读这篇文章和写脚本从这些文件里获取系统信息时要它记住。

CPU信息

/proc/cpuinfo这个文件包含了你系统的处理单元信息。比如,这里有一个与在命令行输入cat /proc/cpuinfo 具备同样功能的Python脚本
 

?
1
2
3
4
5
6
7
8
9
10
#! /usr/bin/env python
""" print out the /proc/cpuinfo
  file
"""
 
from __future__ import print_function
 
with open('/proc/cpuinfo') as f:
  for line in f:
    print(line.rstrip('n'))

当你用Python 2或者Python 3运行这个脚本时,你会看到/proc/cpuinfo文件的所有内容都在你的屏幕上显示出来。(在上面这个脚本,rstrip()方法把每一行的换行符去掉)

下个代码清单使用了startwith()这个字符串方法来显示你电脑的处理单元型号
 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#! /usr/bin/env python
 
""" Print the model of your
  processing units
 
"""
 
from __future__ import print_function
 
with open('/proc/cpuinfo') as f:
  for line in f:
    # Ignore the blank line separating the information between
    # details about two processing units
    if line.strip():
      if line.rstrip('n').startswith('model name'):
        model_name = line.rstrip('n').split(':')[1]
        print(model_name)

当你运行这个脚本,你会看到你机器的所有处理单元的型号。比如,下面是我在我计算机里看到的:
 

?
1
2
3
4
Intel(R) Core(TM) i7-3520M CPU @ 2.90GHz
Intel(R) Core(TM) i7-3520M CPU @ 2.90GHz
Intel(R) Core(TM) i7-3520M CPU @ 2.90GHz
Intel(R) Core(TM) i7-3520M CPU @ 2.90GHz

到目前为止我们已经有好几种方法用来获取我们的计算机系统构架了。从技术的角度准确地说,所有的这些方法实际上是呈现了你运行的系统内核的架构。因此,如果你计算机实际上是64位的机器,但是运行着32位的内核,那么上面的方法将会显现你的计算机是32位架构的。为了找出计算机的正确架构,你可以查看在/proc/cpuinfo中的属性列表的lm属性。1m属性代表着长模式(Long mode)并且只会在64位架构的计算机上出现。下面的脚本跟你展示是怎样做的:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#! /usr/bin/env python
 
""" Find the real bit architecture
"""
 
from __future__ import print_function
 
with open('/proc/cpuinfo') as f:
  for line in f:
    # Ignore the blank line separating the information between
    # details about two processing units
    if line.strip():
      if line.rstrip('n').startswith('flags')
          or line.rstrip('n').startswith('Features'):
        if 'lm' in line.rstrip('n').split():
          print('64-bit')
        else:
          print('32-bit')

正如目前我们所看到的,我们能够访问/proc/cpuinfo文件并且使用简单的文本处理技术去读取我们在查找的信息。为了友好地提供数据给其他程序使用,最好的方法可能是把从/proc/cpuinfo里获取的内容转换为标准的数据机构,比如转换为字典类型。方法很简单:如果你看了这个文件,你会发现对于每一个处理单元都是以键值对形式存在(在之前的一个例子中,我们打印的处理器机型名时,这里的model name就是一个键。)每个不同处理器单元的信息都会用空行来分开。这使我们能方便地以每个处理单元数据为键来构建字典数据结构。这些键(key)都有一个值(value),每个值又对应着每一个处理单元在/proc/cupinfo文件中的所有信息。下一个代码清单告诉你怎样做:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/usr/bin/env/ python
 
"""
/proc/cpuinfo as a Python dict
"""
from __future__ import print_function
from collections import OrderedDict
import pprint
 
def cpuinfo():
  ''' Return the information in /proc/cpuinfo
  as a dictionary in the following format:
  cpu_info['proc0']={...}
  cpu_info['proc1']={...}
 
  '''
 
  cpuinfo=OrderedDict()
  procinfo=OrderedDict()
 
  nprocs = 0
  with open('/proc/cpuinfo') as f:
    for line in f:
      if not line.strip():
        # end of one processor
        cpuinfo['proc%s' % nprocs] = procinfo
        nprocs=nprocs+1
        # Reset
        procinfo=OrderedDict()
      else:
        if len(line.split(':')) == 2:
          procinfo[line.split(':')[0].strip()] = line.split(':')[1].strip()
        else:
          procinfo[line.split(':')[0].strip()] = ''
 
  return cpuinfo
 
if __name__=='__main__':
  cpuinfo = cpuinfo()
  for processor in cpuinfo.keys():
    print(cpuinfo[processor]['model name'])

这段代码使用了一个OrderedDict(有序的字典)代替常用的字典类型,目的是先对在文件中找到的键值对排序后再保存。因此,先展示第一个处理单元的数据信息其次是第二个,以此类推。如果你调用这个函数,它会返回一个字典类型给你。字典的每一个键都是一个处理单元。然后你可以使用键来筛选要找的信息(就像if __name='__main__'语句块里展示的一样)。当上面的脚本运行时会再次打印出每个处理单元的model name(通过print(cpuinfo[processor]['model name']语句来展示)
 

?
1
2
3
4
Intel(R) Core(TM) i7-3520M CPU @ 2.90GHz
Intel(R) Core(TM) i7-3520M CPU @ 2.90GHz
Intel(R) Core(TM) i7-3520M CPU @ 2.90GHz
Intel(R) Core(TM) i7-3520M CPU @ 2.90GHz

内存信息

与/proc/cpuinfo类似,/proc/meminfo文件包含了你计算机的主存信息。下一个脚本产生一个包含这个文件内容的字典并把它输出。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python
 
from __future__ import print_function
from collections import OrderedDict
 
def meminfo():
  ''' Return the information in /proc/meminfo
  as a dictionary '''
  meminfo=OrderedDict()
 
  with open('/proc/meminfo') as f:
    for line in f:
      meminfo[line.split(':')[0]] = line.split(':')[1].strip()
  return meminfo
 
if __name__=='__main__':
  #print(meminfo())
 
  meminfo = meminfo()
  print('Total memory: {0}'.format(meminfo['MemTotal']))
  print('Free memory: {0}'.format(meminfo['MemFree']))

和前面看到的一样,你同样可以使用特定的键去获取任意你想要的信息(在if __name__=='__main__'语句快里有展示)。当你运行这个脚本,你可以看到类似下面的输出:
 

?
1
2
Total memory: 7897012 kB
Free memory: 249508 kB

网络统计

下面,我们会探索我们计算机系统的网络设备。我们将会检索系统的网络接口和系统开启后发送和接收到的字节数据。这些信息可以在/proc/net/dev文件中获取。如果你审查过这个文件的内容,你会发现前两行包含了头信息-i.e.文件中的第一列是网络接口名,第二和第三列展示了接收和传输的字节信息(比如,总发送字节,数据包数量,错误统计,等等)。我们感兴趣的是如何获取不同的网络设备的总数据发送和接受量。下一个代码清单展示了我们如何从/proc/net/dev提出这些信息:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/env python
from __future__ import print_function
from collections import namedtuple
 
def netdevs():
  ''' RX and TX bytes for each of the network devices '''
 
  with open('/proc/net/dev') as f:
    net_dump = f.readlines()
 
  device_data={}
  data = namedtuple('data',['rx','tx'])
  for line in net_dump[2:]:
    line = line.split(':')
    if line[0].strip() != 'lo':
      device_data[line[0].strip()] = data(float(line[1].split()[0])/(1024.0*1024.0),
                        float(line[1].split()[8])/(1024.0*1024.0))
 
  return device_data
 
if __name__=='__main__':
 
  netdevs = netdevs()
  for dev in netdevs.keys():
    print('{0}: {1} MiB {2} MiB'.format(dev, netdevs[dev].rx, netdevs[dev].tx))

当你运行上面的脚本时,会以MiB为单位输出从你最近的一次重启后你的网络设备接受和发送的数据。正如下面展示的:
 

?
1
2
em1: 0.0 MiB 0.0 MiB
wlan0: 2651.40951061 MiB 183.173976898 MiB

你可能会利用一个持久性存储机制和这个脚本来写一个你自己的数据使用监控程序。

进程

/proc目录同样包含了每个运行进程的目录。这些目录的名称以相应的进程ID来命名。因此,如果你遍历/proc目录下的所有以数字命名的目录,你会得到一个所有当前运行进程的ID列表。下面的代码清单里的process_list()函数返回一个包含所有当前运行进程ID的列表。这个列表的长度等于系统运行进程的总数,正如你运行这个脚本看到的一样:
 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python
"""
 List of all process IDs currently active
"""
 
from __future__ import print_function
import os
def process_list():
 
  pids = []
  for subdir in os.listdir('/proc'):
    if subdir.isdigit():
      pids.append(subdir)
 
  return pids
 
if __name__=='__main__':
 
  pids = process_list()
  print('Total number of running processes:: {0}'.format(len(pids)))

运行上面的脚本时,输出结果和下面输出类似:

每个进程目录都包含了大量的其他文件和目录,这些目录包含了各种关于进程调用命令,使用的共享库和其他的信息。

块设备

接下来的脚本通过访问sysfs虚拟文件系统列出了所有的块设备信息。你能够在/sys/block目录下找到系统上的所有块设备。因此,你的系统上会有/sys/block/sda,/sys/block/sdb和其他的类似目录。为了找到这些设备,我们可以遍历/sys/block目录然后通过简单的正则表达式去匹配我们要查找的内容。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/env python
 
"""
Read block device data from sysfs
"""
 
from __future__ import print_function
import glob
import re
import os
 
# Add any other device pattern to read from
dev_pattern = ['sd.*','mmcblk*']
 
def size(device):
  nr_sectors = open(device+'/size').read().rstrip('n')
  sect_size = open(device+'/queue/hw_sector_size').read().rstrip('n')
 
  # The sect_size is in bytes, so we convert it to GiB and then send it back
  return (float(nr_sectors)*float(sect_size))/(1024.0*1024.0*1024.0)
 
def detect_devs():
  for device in glob.glob('/sys/block/*'):
    for pattern in dev_pattern:
      if re.compile(pattern).match(os.path.basename(device)):
        print('Device:: {0}, Size:: {1} GiB'.format(device, size(device)))
 
if __name__=='__main__':
  detect_devs()

如果你运行了这个脚本,你将会看到与下面类似的输出结果:
 

?
1
2
Device:: /sys/block/sda, Size:: 465.761741638 GiB
Device:: /sys/block/mmcblk0, Size:: 3.70703125 GiB

当我运行这个脚本时,我额外插入了一张SD卡。所以你会看到这个脚本检测到了它(上面输出的第二行,译者注)。你同样可以扩展这个脚本去识别其他的块设备(比如虚拟硬盘)。

构建命令行工具

允许用户指定命令行参数去自定义程序的默认行为是所有Linux命令行工具的一个普遍特征。argparse模块能使你程序拥有与内置工具界面类似的界面。下一个代码清单展示了一个获取你系统上所有用户并把它们相应的登陆shell打印出来的程序。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env python
 
"""
Print all the users and their login shells
"""
 
from __future__ import print_function
import pwd
 
# Get the users from /etc/passwd
def getusers():
  users = pwd.getpwall()
  for user in users:
    print('{0}:{1}'.format(user.pw_name, user.pw_shell))
 
if __name__=='__main__':
  getusers()

当运行上面的脚本时,它会打印出你系统上所有的用户和它们的登陆shell

现在,我们假设你想让脚本使用者能够选择是否想看到系统的其他用户(比如daemon,apache)。我们通过使用argparse模块扩展之前的代码来实现这个功能,就像下面的代码。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/usr/bin/env python
 
"""
Utility to play around with users and passwords on a Linux system
"""
 
from __future__ import print_function
import pwd
import argparse
import os
 
def read_login_defs():
 
  uid_min = None
  uid_max = None
 
  if os.path.exists('/etc/login.defs'):
    with open('/etc/login.defs') as f:
      login_data = f.readlines()
 
    for line in login_data:
      if line.startswith('UID_MIN'):
        uid_min = int(line.split()[1].strip())
 
      if line.startswith('UID_MAX'):
        uid_max = int(line.split()[1].strip())
 
  return uid_min, uid_max
 
# Get the users from /etc/passwd
def getusers(no_system=False):
 
  uid_min, uid_max = read_login_defs()
 
  if uid_min is None:
    uid_min = 1000
  if uid_max is None:
    uid_max = 60000
 
  users = pwd.getpwall()
  for user in users:
    if no_system:
      if user.pw_uid >= uid_min and user.pw_uid <= uid_max:
        print('{0}:{1}'.format(user.pw_name, user.pw_shell))
    else:
      print('{0}:{1}'.format(user.pw_name, user.pw_shell))
 
if __name__=='__main__':
 
  parser = argparse.ArgumentParser(description='User/Password Utility')
 
  parser.add_argument('--no-system', action='store_true',dest='no_system',
            default = False, help='Specify to omit system users')
 
  args = parser.parse_args()
  getusers(args.no_system)

使用–help选项来运行上面的脚本,你会看到一个带有可选项(和作用)的友好帮助信息
 

?
1
2
3
4
5
6
7
8
$ ./getusers.py --help
usage: getusers.py [-h] [--no-system]
 
User/Password Utility
 
optional arguments:
 -h, --help  show this help message and exit
 --no-system Specify to omit system users

上面脚本的一个例子调用如下:
 

?
1
2
$ ./getusers.py --no-system
gene:/bin/bash

当你传递一个无效的参数时,脚本会报错:
 

?
1
2
3
$ ./getusers.py --param
usage: getusers.py [-h] [--no-system]
getusers.py: error: unrecognized arguments: --param

让我们一起来简单地了解下在上面的脚本中我们是如何使用argparse模块的parser=argparse.ArgumentParser(description='User/Password Utility')这行代码使用一个描述脚本作用的可选参数创建了一个新的ArgumentParser对象。

然后,我们在下一行代码:parser.add_argument(‘–no-system', action='store_true', dest='no_system', default = False, help='Specify to omit system users')里使用了add_argument()方法添加一些参数,让脚本能够识别命令行选项。这个方法的第一个参数是脚本使用者在调用脚本时作为参数提供的选项名。下一个参数action=store_true表明了这是一个布尔选项。也就是说这个参数的有无在一定程度上会对程序产生影响。dest参数指定了一个变量来保存选项值并提供给脚本使用。如果用户没有提供选项,通过参数default=False可以设置默认值为False。最后一个参数是脚本展示关于这个选项的帮助信息。最后,使用parse_args()方法处理参数:args=parser.parse_args()。一旦处理完毕,使用args.option_dest可以获取到用户提供的选项值,这里的option_dest就是你设置参数时指定的dest变量。这行代码getusers(args.no_system)使用用户提供的no_system选项值作为参数调用了getusers()。

接下来的脚本展示了你该如何在你的脚本里提供用户一个非布尔值的选项。这个脚本重写了代码清单6,添加了额外的选项让你能够指定检测你感兴趣的网络设备。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/usr/bin/env python
from __future__ import print_function
from collections import namedtuple
import argparse
 
def netdevs(iface=None):
  ''' RX and TX bytes for each of the network devices '''
 
  with open('/proc/net/dev') as f:
    net_dump = f.readlines()
 
  device_data={}
  data = namedtuple('data',['rx','tx'])
  for line in net_dump[2:]:
    line = line.split(':')
    if not iface:
      if line[0].strip() != 'lo':
        device_data[line[0].strip()] = data(float(line[1].split()[0])/(1024.0*1024.0),
                          float(line[1].split()[8])/(1024.0*1024.0))
    else:
      if line[0].strip() == iface:
        device_data[line[0].strip()] = data(float(line[1].split()[0])/(1024.0*1024.0),
                          float(line[1].split()[8])/(1024.0*1024.0)) 
  return device_data
 
if __name__=='__main__':
 
  parser = argparse.ArgumentParser(description='Network Interface Usage Monitor')
  parser.add_argument('-i','--interface', dest='iface',
            help='Network interface')
 
  args = parser.parse_args()
 
  netdevs = netdevs(iface = args.iface)
  for dev in netdevs.keys():
    print('{0}: {1} MiB {2} MiB'.format(dev, netdevs[dev].rx, netdevs[dev].tx))

当你不提供任何参数运行这个脚本时,它的运行结果实际上和之前的版本一样。但是,你同样可以指定你可能感兴趣的网络设备。举个例子:
 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ ./net_devs_2.py
 
em1: 0.0 MiB 0.0 MiB
wlan0: 146.099492073 MiB 12.9737148285 MiB
virbr1: 0.0 MiB 0.0 MiB
virbr1-nic: 0.0 MiB 0.0 MiB
 
$ ./net_devs_2.py --help
usage: net_devs_2.py [-h] [-i IFACE]
 
Network Interface Usage Monitor
 
optional arguments:
 -h, --help      show this help message and exit
 -i IFACE, --interface IFACE
            Network interface
 
$ ./net_devs_2.py -i wlan0
wlan0: 146.100307465 MiB 12.9777050018 MiB

使你的脚本能在任意地方运行

在这篇文章的帮助下,你可能已经能够给自己写一个或者更多的有用脚本,这些脚本你想每天都使用,就像Linux的其他命令一样。去实现这个愿望最简单的方式就是使这些脚本能够运行(给脚本添加运行权限,译者注)并且给这些命令设置BASH别名(BASH alias)。你同样可以删除.py后缀并且把这个文件放到一个标准位置比如/usr/local/sbin。

其他的有用标准库模块

除了到目前为止我们在这篇文章里看到过的标准库外,这里还有很多其它可能有用的标准库:subprocess,ConfigParser, readline 和 curses。

下一步?

在这个阶段,根据你自己的Python经验和深入探索Linux,你选择以下方式的一种。如果你已经写了大量的shell脚本或者命令管道去深入探索各种Linux,请试一下Python。如果你想以一种更简易的方式去写你自己的脚本工具去执行各种任务,请试一下Python。最后,如果你已经使用其他种类的Python在Linux上编程,祝你使用Python深入探索Linux愉快。