在這篇文章裡,我們將會探索如何使用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愉快。
資源
Python資源
- 列表
- 元組
- 命名元組
- 有序字典
- split()函式
- strip()、rstrip()和其他的字串方法
- 讀寫檔案
- os模組
- platform模組
- pwd模組
- spwd模組
- grp模組
- subprocess模組
- ConfigParser模組
- readline模組
系統資訊