18.4. os — 便捷地访问操作系统专属功能
目的:为操作系统相关的特性提供便捷且可移植的操作。
os
模块提供了平台相关模块的封装,常见的平台有 posix
、nt
和 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_GID
和TEST_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.environ
或 getenv()
读取。环境变量通常用于配置值,例如搜索路径、文件位置、和调试标识。这个例子演示如何获取一个环境变量,并给子进程传递一个值。
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.environ
和 os.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
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。