18.4. os — 便捷地访问操作系统专属功能

未匹配的标注

目的:为操作系统相关的特性提供便捷且可移植的操作。

os 模块提供了平台相关模块的封装,常见的平台有 posixnt 和 mac。在所有平台上可用函数的 API 应该都是相同的,这样使用 os 模块为程序提供了一些可移植性。但并非所有的函数在每个平台上都可用, 比如后文中提到的一些进程管理函数在 Windows 上就不可用。

Python 文档中 os 模块的的副标题是「各种各样的操作系统接口」。模块包含的大部分函数用于创建和管理进程或文件系统,例如:目录与文件,此外还有一些其他函数。

查看文件系统的内容

要查看文件系统上某个目录的内容,请使用 listdir()

os_listdir.py

import os
import sys

print(sorted(os.listdir(sys.argv[1])))

返回值包含了指定目录中未排序的成员名列表。但你不能从返回值中分辨它们是文件、目录还是符号连接。

$ python3 os_listdir.py .

['index.rst', 'os_access.py', 'os_cwd_example.py',
'os_directories.py', 'os_environ_example.py',
'os_exec_example.py', 'os_fork_example.py',
'os_kill_example.py', 'os_listdir.py', 'os_listdir.py~',
'os_process_id_example.py', 'os_process_user_example.py',
'os_rename_replace.py', 'os_rename_replace.py~',
'os_scandir.py', 'os_scandir.py~', 'os_spawn_example.py',
'os_stat.py', 'os_stat_chmod.py', 'os_stat_chmod_example.txt',
'os_strerror.py', 'os_strerror.py~', 'os_symlinks.py',
'os_system_background.py', 'os_system_example.py',
'os_system_shell.py', 'os_wait_example.py',
'os_waitpid_example.py', 'os_walk.py']

walk() 函数经过一个目录时,会递归的访问它的子目录,并产生一个 tuple,其中包含了目录路径、该路径下任何直接子目录和指定目录中所有文件名的列表。

os_walk.py

import os
import sys

# If we are not given a path to list, use /tmp
if len(sys.argv) == 1:
    root = '/tmp'
else:
    root = sys.argv[1]

for dir_name, sub_dirs, files in os.walk(root):
    print(dir_name)
    # Make the subdirectory names stand out with /
    sub_dirs = [n + '/' for n in sub_dirs]
    # Mix the directory contents together
    contents = sub_dirs + files
    contents.sort()
    # Show the contents
    for c in contents:
        print('  {}'.format(c))
    print()

这个例子展示了目录的递归输出。

$ python3 os_walk.py ../zipimport

../zipimport
  __init__.py
  example_package/
  index.rst
  zipimport_example.zip
  zipimport_find_module.py
  zipimport_get_code.py
  zipimport_get_data.py
  zipimport_get_data_nozip.py
  zipimport_get_data_zip.py
  zipimport_get_source.py
  zipimport_is_package.py
  zipimport_load_module.py
  zipimport_make_example.py

../zipimport/example_package
  README.txt
  __init__.py

如果你想要比文件名更多的信息,使用 scandir() 会比用 listdir() 更有效率。因为用它扫描目录时,它能在一次系统调用中返回更多的信息。

os_scandir.py

import os
import sys

for entry in os.scandir(sys.argv[1]):
    if entry.is_dir():
        typ = 'dir'
    elif entry.is_file():
        typ = 'file'
    elif entry.is_symlink():
        typ = 'link'
    else:
        typ = 'unknown'
    print('{name} {typ}'.format(
        name=entry.name,
        typ=typ,
    ))

scandir() 返回目录中每一个项目 DirEntry 实例的序列。这种对象有几种属性和方法,可以用于访问文件的元数据。

$ python3 os_scandir.py .

index.rst file
os_access.py file
os_cwd_example.py file
os_directories.py file
os_environ_example.py file
os_exec_example.py file
os_fork_example.py file
os_kill_example.py file
os_listdir.py file
os_listdir.py~ file
os_process_id_example.py file
os_process_user_example.py file
os_rename_replace.py file
os_rename_replace.py~ file
os_scandir.py file
os_scandir.py~ file
os_spawn_example.py file
os_stat.py file
os_stat_chmod.py file
os_stat_chmod_example.txt file
os_strerror.py file
os_strerror.py~ file
os_symlinks.py file
os_system_background.py file
os_system_example.py file
os_system_shell.py file
os_wait_example.py file
os_waitpid_example.py file
os_walk.py file

文件系统权限管理

一个文件的详细信息可以通过 stat() 和 lstat() 函数查看,后者常用于检查疑似符号链接文件的状态。

os_stat.py

import os
import sys
import time

if len(sys.argv) == 1:
    filename = __file__
else:
    filename = sys.argv[1]

stat_info = os.stat(filename)

print('os.stat({}):'.format(filename))
print('  Size:', stat_info.st_size)
print('  Permissions:', oct(stat_info.st_mode))
print('  Owner:', stat_info.st_uid)
print('  Device:', stat_info.st_dev)
print('  Created      :', time.ctime(stat_info.st_ctime))
print('  Last modified:', time.ctime(stat_info.st_mtime))
print('  Last accessed:', time.ctime(stat_info.st_atime))

程序的执行方式不同时,程序的输出变化很大。你可以通过命令行尝试传递不同的参数给  os_stat.py,并观察输出。

$ python3 os_stat.py

os.stat(os_stat.py):
  Size: 593
  Permissions: 0o100644
  Owner: 527
  Device: 16777218
  Created      : Sat Dec 17 12:09:51 2016
  Last modified: Sat Dec 17 12:09:51 2016
  Last accessed: Sat Dec 31 12:33:19 2016

$ python3 os_stat.py index.rst

os.stat(index.rst):
  Size: 26878
  Permissions: 0o100644
  Owner: 527
  Device: 16777218
  Created      : Sat Dec 31 12:33:10 2016
  Last modified: Sat Dec 31 12:33:10 2016
  Last accessed: Sat Dec 31 12:33:19 2016

在类 Unix 系统上,你可以通过 chmod() 改变文件的权限。文件的权限可以用一个整数表示,你可以通过 stat 模块的常量来构造文件权限值。下面这个例子改变了文件的用户执行权限位。

os_stat_chmod.py

import os
import stat

filename = 'os_stat_chmod_example.txt'
if os.path.exists(filename):
    os.unlink(filename)
with open(filename, 'wt') as f:
    f.write('contents')

# 使用 stat 函数判断文件当前的权限
existing_permissions = stat.S_IMODE(os.stat(filename).st_mode)

if not os.access(filename, os.X_OK):
    print('Adding execute permission')
    new_permissions = existing_permissions | stat.S_IXUSR
else:
    print('Removing execute permission')
    # 使用 xor 异或清除用户的执行权限
    new_permissions = existing_permissions ^ stat.S_IXUSR

os.chmod(filename, new_permissions)

以上脚本假定运行时,它有足够的权限用于改变文件的权限。

$ python3 os_stat_chmod.py

Adding execute permission

access() 函数用于检测文件或进程的访问权限。

os_access.py

import os

print('Testing:', __file__)
print('Exists:', os.access(__file__, os.F_OK))
print('Readable:', os.access(__file__, os.R_OK))
print('Writable:', os.access(__file__, os.W_OK))
print('Executable:', os.access(__file__, os.X_OK))

程序的执行方式不同,输出结果将有很大的变化。但输出都类似于这样:

$ python3 os_access.py

Testing: os_access.py
Exists: True
Readable: True
Writable: True
Executable: False

access() 函数的库文档中有两个特殊的警告。第一个,没有必要在用 open() 打开文件前使用 access() 检查是否有打开文件的权限。原因基于一个小事实:在两次函数调用的时间窗口之间,文件权限可能发生了变化。第二个警告与扩展了 POSIX 含义的网络文件系统有有关。有些文件系统类型可能会响应 POSIX 系统调用,并返回进程有权限访问文件。之后使用 open() 打开文件时,又因为一些 POSIX 调用没检测到的原因导致打开操作失败。总的来说,打开文件时直接使用 open() 函数再带上相应的模式就好,问题出现时就捕获 IOError,没必要在打开前先检查权限。

创建或删除文件夹

os 模块也提供了一些处理文件夹的函数,使用这些函数,你可以创建、删除或列出目录。

os_directories.py

import os

dir_name = 'os_directories_example'

print('Creating', dir_name)
os.makedirs(dir_name)

file_name = os.path.join(dir_name, 'example.txt')
print('Creating', file_name)
with open(file_name, 'wt') as f:
    f.write('example file')

print('Cleaning up')
os.unlink(file_name)
os.rmdir(dir_name)

有两类函数用于创建或删除文件夹。使用 mkdir() 创建目录时,目录的所有父目录必须存在;同样的,使用 rmdir() 删除目录时,你只能删除子叶目录,即一条路径的最后一个目录,它不再包含其他目录了。与之不同的是,当你使用 makedirs() 与 removedirs() 函数来操作目录时,makedirs() 会创建完整的路径,父目录不存在时,它也会创建父目录。类似的,只要目录是空的,不包含除子目录外的其他文件了。用 removedirs() 删除目录时,会删除整条目录,包括父目录与子目录。

$ python3 os_directories.py

Creating os_directories_example
Creating os_directories_example/example.txt
Cleaning up

处理符号链接

在支持符号链接的平台与系统上,你可以用这些函数处理符号链接。

os_symlinks.py

import os

link_name = '/tmp/' + os.path.basename(__file__)

print('Creating link {} -> {}'.format(link_name, __file__))
os.symlink(__file__, link_name)

stat_info = os.lstat(link_name)
print('Permissions:', oct(stat_info.st_mode))

print('Points to:', os.readlink(link_name))

# 清理工作
os.unlink(link_name)

symlink() 可以创建一个符号链接。readlink() 可以读取符号链接,用于判断链接指向的源文件。lstat() 类似 stat() 函数,只不过用于处理符号链接,它可以查看符号链接的一些属性。

$ python3 os_symlinks.py

Creating link /tmp/os_symlinks.py -> os_symlinks.py
Permissions: 0o120755
Points to: os_symlinks.py

安全的替换现有文件

替换或重命名现有文件的操作不是幂等的,也就是说它们可能有一些副作用。导致同一个操作重复多次时,结果有可能并不相同。所以使用这两个操作可能会让程序暴露在竞态条件之下,导致一些意料之外的错误。rename() 和 replace() 函数都使用了安全的算法实现,在 POSIX 兼容的系统上,它们会尽可能的使用原子操作以避免错误。

os_rename_replace.py

import glob
import os

with open('rename_start.txt', 'w') as f:
    f.write('starting as rename_start.txt')

print('Starting:', glob.glob('rename*.txt'))

os.rename('rename_start.txt', 'rename_finish.txt')

print('After rename:', glob.glob('rename*.txt'))

with open('rename_finish.txt', 'r') as f:
    print('Contents:', repr(f.read()))

with open('rename_new_contents.txt', 'w') as f:
    f.write('ending with contents of rename_new_contents.txt')

os.replace('rename_new_contents.txt', 'rename_finish.txt')

with open('rename_finish.txt', 'r') as f:
    print('After replace:', repr(f.read()))

for name in glob.glob('rename*.txt'):
    os.unlink(name)

大多数情况下,rename() 与 replace() 函数能跨文件系统工作。重命名文件时,如果源文件已经被移动一个文件到新文件系统上或目标文件已存在,则操作可能会失败。

$ python3 os_rename_replace.py

Starting: ['rename_start.txt']
After rename: ['rename_finish.txt']
Contents: 'starting as rename_start.txt'
After replace: 'ending with contents of rename_new_contents.txt'

检测和更改进程所有者

os 提供的下一组函数用于确定和更改进程所有者 ID 。 这些是守护进程或特殊系统程序的作者最常使用的,它们需要更改权限级别而不是以 root 身份运行。 本节不会尝试解释 Unix 安全性以及进程所有者等的复杂细节。更多有关详细信息,请参阅本节末尾的参考列表。

以下示例显示进程的真实有效用户和组信息,然后更改有效值。 这类似于守护进程在系统引导期间以 root 身份启动时需要执行的操作,以降低权限级别并以其他用户身份运行。

注意

在运行示例之前,更改 TEST_GIDTEST_UID 值以匹配系统上定义的真实用户。

os_process_user_example.py

import os

TEST_GID = 502
TEST_UID = 502

def show_user_info():
    print('User (actual/effective)  : {} / {}'.format(
        os.getuid(), os.geteuid()))
    print('Group (actual/effective) : {} / {}'.format(
        os.getgid(), os.getegid()))
    print('Actual Groups   :', os.getgroups())

print('BEFORE CHANGE:')
show_user_info()
print()

try:
    os.setegid(TEST_GID)
except OSError:
    print('ERROR: Could not change effective group. '
          'Rerun as root.')
else:
    print('CHANGE GROUP:')
    show_user_info()
    print()

try:
    os.seteuid(TEST_UID)
except OSError:
    print('ERROR: Could not change effective user. '
          'Rerun as root.')
else:
    print('CHANGE USER:')
    show_user_info()
    print()

当在 OS X 上以 id 为 502 和用户组为 502 的用户身份运行时,将生成以下输出:

$ python3 os_process_user_example.py

BEFORE CHANGE:
User (actual/effective)  : 527 / 527
Group (actual/effective) : 501 / 501
Actual Groups   : [501, 701, 402, 702, 500, 12, 61, 80, 98, 398,
399, 33, 100, 204, 395]

ERROR: Could not change effective group. Rerun as root.
ERROR: Could not change effective user. Rerun as root.

值没有更改,因为当它不以 root 身份运行时,进程无法更改其有效所有者值。 任何尝试将有效用户 ID 或组 ID 设置为除当前用户之外的任何内容都会导致 OSError 。 使用 sudo 运行相同的脚本以便以 root 权限启动则是另外一种情况。

$ sudo python3 os_process_user_example.py

BEFORE CHANGE:

User (actual/effective)  : 0 / 0
Group (actual/effective) : 0 / 0
Actual Groups : [0, 1, 2, 3, 4, 5, 8, 9, 12, 20, 29, 61, 80,
702, 33, 98, 100, 204, 395, 398, 399, 701]

CHANGE GROUP:
User (actual/effective)  : 0 / 0
Group (actual/effective) : 0 / 502
Actual Groups   : [0, 1, 2, 3, 4, 5, 8, 9, 12, 20, 29, 61, 80,
702, 33, 98, 100, 204, 395, 398, 399, 701]

CHANGE USER:
User (actual/effective)  : 0 / 502
Group (actual/effective) : 0 / 502
Actual Groups   : [0, 1, 2, 3, 4, 5, 8, 9, 12, 20, 29, 61, 80,
702, 33, 98, 100, 204, 395, 398, 399, 701]

在这种情况下,由于它以 root 身份启动,因此脚本可以更改进程的有效用户和组。 更改有效 UID 后,该过程仅限于该用户的权限。 由于非 root 用户无法更改其有效组,因此程序需要在更改用户之前更改组。

管理进程环境

操作系统通过 os 模块暴露给程序的另一个特性是环境。在环境中设置的变量字符串能通过 os.environgetenv() 读取。环境变量通常用于配置值,例如搜索路径、文件位置、和调试标识。这个例子演示如何获取一个环境变量,并给子进程传递一个值。

os_environ_example.py

import os

print('Initial value:', os.environ.get('TESTVAR', None))
print('Child process:')
os.system('echo $TESTVAR')

os.environ['TESTVAR'] = 'THIS VALUE WAS CHANGED'

print()
print('Changed value:', os.environ['TESTVAR'])
print('Child process:')
os.system('echo $TESTVAR')

del os.environ['TESTVAR']

print()
print('Removed value:', os.environ.get('TESTVAR', None))
print('Child process:')
os.system('echo $TESTVAR')

os.environ 对象遵循标准 Python 映射 API 来获取与设置值。对 os.environ 的改动会输出给子进程。

$ python3 -u os_environ_example.py

Initial value: None
Child process:

Changed value: THIS VALUE WAS CHANGED
Child process:
THIS VALUE WAS CHANGED

Removed value: None
Child process:

管理进程工作目录

具备层级文件系统的操作系统有 当前工作目录 这个概念 -- 进程在使用相对路径存取文件时,将这个文件系统中的目录当作起始位置。用 getcwd() 获取当前工作目录,用 chdir() 改变当前工作目录。

os_cwd_example.py

import os

print('Starting:', os.getcwd())

print('Moving up one:', os.pardir)
os.chdir(os.pardir)

print('After move:', os.getcwd())

可移植的使用方式是用 os.curdir 引用当前目录,用 os.pardir 引用父目录。

$ python3 os_cwd_example.py

Starting: .../pymotw-3/source/os
Moving up one: ..
After move: .../pymotw-3/source

运行外部函数

警告

许多用于进程的函数可移植性有限。更一致的方式是以平台独立的方式来使用进程,请参考 subprocess 模块。

system() 是运行一个单独命令的最基本方式,完全不需要与之交互 。它只接受一个字符串参数,该字符串是 shell 子进程执行的命令行。

os_system_example.py

import os

# Simple command
os.system('pwd')

system() 的返回值是 shell 运行程序后退出状态码,它封装为16比特数值,高字节为退出状态,低字节为导致退出的信号值或0。

$ python3 -u os_system_example.py

.../pymotw-3/source/os

因为命令直接传递给 shell 处理,所以它可以含有 shell 语法,例如通配符或环境变量。

os_system_shell.py

import os

# Command with shell expansion
os.system('echo $TMPDIR')

当 shell 运行命令行时,这个字符串中的环境变量 $TMPDIR 被展开。

$ python3 -u os_system_shell.py

/var/folders/5q/8gk0wq888xlggz008k8dr7180000hg/T/

除非显式在后台运行该命令,否则 system() 的调用将阻塞,直到它完成。子进程的标准输入、输出和错误默认被绑定到调用者拥有的流,但可以被 shell 语法重定向。

os_system_background.py

import os
import time

print('Calling...')
os.system('date; (sleep 3; date) &')

print('Sleeping...')
time.sleep(5)

不过这容易掉进 shell 的陷阱,有更好的方式完成同样的事。

$ python3 -u os_system_background.py

Calling...
Sat Dec 31 12:33:20 EST 2016
Sleeping...
Sat Dec 31 12:33:23 EST 2016

使用 os.fork() 创建进程

通过 os 模块可使用 POSIX 函数 fork()exec() (在 Mac OS X,Linux,和其他 Unix 衍生系统下可以用)。关于如何可靠地使用这些函数能写好几本书,请去图书馆或书店了解这里提到的详细信息。

使用 fork() 创建一个当前进程的克隆进程:

os_fork_example.py

import os

pid = os.fork()

if pid:
    print('Child process id:', pid)
else:
    print('I am the child')

每次运行例子时,输出内容会随着系统状态变化,但看起来会是这样:

$ python3 -u os_fork_example.py

Child process id: 29190
I am the child

fork 之后,这两个进程运行同样的代码。程序要搞清楚运行在哪个进程里,需要检查 fork() 返回值。如果返回值为 0 ,程序正运行在子进程里。如果返回值非 0 ,程序运行在父进程里,返回值是子进程的进程ID。

os_kill_example.py

import os
import signal
import time

def signal_usr1(signum, frame):
    "Callback invoked when a signal is received"
    pid = os.getpid()
    print('Received USR1 in process {}'.format(pid))

print('Forking...')
child_pid = os.fork()
if child_pid:
    print('PARENT: Pausing before sending signal...')
    time.sleep(1)
    print('PARENT: Signaling {}'.format(child_pid))
    os.kill(child_pid, signal.SIGUSR1)
else:
    print('CHILD: Setting up signal handler')
    signal.signal(signal.SIGUSR1, signal_usr1)
    print('CHILD: Pausing to wait for signal')
    time.sleep(5)

父进程可以使用 kill()signal 模块给子进程发送信号。首先,定义一个信号处理函数,子进程收到信号后该函数会被调用。接着, fork() ,并在父进程中用 kill() 发送 USR1 信号之前暂停一会儿。这个例子利用这个暂停时间让子进程设置信号处理函数。真正的应用不会想用 sleep() 。子进程设置信号处理函数,然后 sleep 一会儿,给父进程留出时间发送信号。

$ python3 -u os_kill_example.py

Forking...
PARENT: Pausing before sending signal...
CHILD: Setting up signal handler
CHILD: Pausing to wait for signal
PARENT: Signaling 29193
Received USR1 in process 29193

在子进程中处理单独行为,一种简单方法是检查 fork() 的返回值,然后执行分支。比起简单分支,更复杂的情况需要更多的分离代码。在其他情况下,可能需要包装现有程序。对于这两种情况,exe*() 系列函数可用来运行另一个程序。

os_exec_example.py

import os

child_pid = os.fork()
if child_pid:
    os.waitpid(child_pid, 0)
else:
    os.execlp('pwd', 'pwd', '-P')

exec() 运行一个程序,该程序代码替换掉现有进程的代码。

$ python3 os_exec_example.py

.../pymotw-3/source/os

exec() 有很多变体,取决于可用参数的形式、父进程的路径和环境是否复制到子进程等。对于所有变体,第一个参数是路径或文件名,剩余的参数控制程序如何运行。它们要么作为命令行参数传递,要么覆盖进程「环境」(请看 os.environos.getenv )。全部细节请参阅库文档。

生成新进程

作为一种便利手段, spawn() 函数簇在一条语句中处理 fork()exec()

os_spawn_example.py

import os

os.spawnlp(os.P_WAIT, 'pwd', 'pwd', '-P')

第一个参数是模式,它指示在返回之前是否等待进程完成。本例子是等待。使用 P_NOWAIT 让其他进程启动,然后在当前进程中恢复。

$ python3 os_spawn_example.py

.../pymotw-3/source/os

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
上一篇 下一篇
Summer
讨论数量: 0
发起讨论 只看当前版本


暂无话题~