在這篇文章里,我們將會探索如何使用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愉快。