彻底弄懂Python标准库源码(二)—— os模块(续)

本文续 彻底弄懂Python标准库源码(一)—— os模块

本文所用 Python3.8.3 标准库 os.py文件, 可以在CPython官方GitHub地址下载 。


目录

第423~529行 fwalk——目录树生成器

第531~654行 exec函数族

第657~713行 _Environ——环境变量信息的类

第715~721行 os.putenv——设置环境变量

第723~729行 os.unsetenv——删除环境变量

第731~759行 os.environ——环境变量信息

第766~770行 getenv——获取环境变量

第772~794行 environb,getenvb——字节型(byte)环境变量

第796~827行 fsencode,fsdecode——路径名的编码和解码

第829~970行 spawn函数族

第973~1016行 popen——执行cmd或者shell命令

第1018~1023行 fdopen——打开文件

第1026~1061行 fspath——路径标准化

第1064~1077行 PathLike——什么是PathLike

第1080~1115行 add_dll_directory—— 添加DLL文件搜索路径

总结

参考链接


第423~529行 fwalk——目录树生成器

if {open, stat} <= supports_dir_fd and {scandir, stat} <= supports_fd:

    def fwalk(top=".", topdown=True, onerror=None, *, follow_symlinks=False, dir_fd=None):
        """Directory tree generator.

        This behaves exactly like walk(), except that it yields a 4-tuple

            dirpath, dirnames, filenames, dirfd

        `dirpath`, `dirnames` and `filenames` are identical to walk() output,
        and `dirfd` is a file descriptor referring to the directory `dirpath`.

        The advantage of fwalk() over walk() is that it's safe against symlink
        races (when follow_symlinks is False).

        If dir_fd is not None, it should be a file descriptor open to a directory,
          and top should be relative; top will then be relative to that directory.
          (dir_fd is always supported for fwalk.)

        Caution:
        Since fwalk() yields file descriptors, those are only valid until the
        next iteration step, so you should dup() them if you want to keep them
        for a longer period.

        Example:

        import os
        for root, dirs, files, rootfd in os.fwalk('python/Lib/email'):
            print(root, "consumes", end="")
            print(sum(os.stat(name, dir_fd=rootfd).st_size for name in files),
                  end="")
            print("bytes in", len(files), "non-directory files")
            if 'CVS' in dirs:
                dirs.remove('CVS')  # don't visit CVS directories
        """
        if not isinstance(top, int) or not hasattr(top, '__index__'):
            top = fspath(top)
        # Note: To guard against symlink races, we use the standard
        # lstat()/open()/fstat() trick.
        if not follow_symlinks:
            orig_st = stat(top, follow_symlinks=False, dir_fd=dir_fd)
        topfd = open(top, O_RDONLY, dir_fd=dir_fd)
        try:
            if (follow_symlinks or (st.S_ISDIR(orig_st.st_mode) and
                                    path.samestat(orig_st, stat(topfd)))):
                yield from _fwalk(topfd, top, isinstance(top, bytes),
                                  topdown, onerror, follow_symlinks)
        finally:
            close(topfd)

    def _fwalk(topfd, toppath, isbytes, topdown, onerror, follow_symlinks):
        # Note: This uses O(depth of the directory tree) file descriptors: if
        # necessary, it can be adapted to only require O(1) FDs, see issue
        # #13734.

        scandir_it = scandir(topfd)
        dirs = []
        nondirs = []
        entries = None if topdown or follow_symlinks else []
        for entry in scandir_it:
            name = entry.name
            if isbytes:
                name = fsencode(name)
            try:
                if entry.is_dir():
                    dirs.append(name)
                    if entries is not None:
                        entries.append(entry)
                else:
                    nondirs.append(name)
            except OSError:
                try:
                    # Add dangling symlinks, ignore disappeared files
                    if entry.is_symlink():
                        nondirs.append(name)
                except OSError:
                    pass

        if topdown:
            yield toppath, dirs, nondirs, topfd

        for name in dirs if entries is None else zip(dirs, entries):
            try:
                if not follow_symlinks:
                    if topdown:
                        orig_st = stat(name, dir_fd=topfd, follow_symlinks=False)
                    else:
                        assert entries is not None
                        name, entry = name
                        orig_st = entry.stat(follow_symlinks=False)
                dirfd = open(name, O_RDONLY, dir_fd=topfd)
            except OSError as err:
                if onerror is not None:
                    onerror(err)
                continue
            try:
                if follow_symlinks or path.samestat(orig_st, stat(dirfd)):
                    dirpath = path.join(toppath, name)
                    yield from _fwalk(dirfd, dirpath, isbytes,
                                      topdown, onerror, follow_symlinks)
            finally:
                close(dirfd)

        if not topdown:
            yield toppath, dirs, nondirs, topfd

    __all__.append("fwalk")

第423行 {open, stat}  是定义一个集合 (set),判断这个集合在 supports_dir_fd 这个集合里并且{scandir, stat} 在 supports_fd 这个集合里,才会定义 fwalk 函数。那么什么情况下判断才会成立呢? 先看一下 supports_dir_fd 和supports_fd是什么意思,这两个集合是在前面123行和143行定义的。

fd在Linux中一般指 file_descriptor 文件描述符。Linux为了实现一切皆文件的设计哲学,不仅将数据抽象成了文件,也将一切操作和资源抽象成了文件,比如说硬件设备,socket,磁盘,进程,线程等。在操作这些所谓的文件的时候,我们每操作一次就找一次名字会耗费大量的时间和效率。所以每一个文件对应一个索引,这样要操作文件的时候,我们直接找到索引就可以对其进行操作了。我们将这个索引叫做文件描述符(file descriptor),简称fd,在系统里面是一个非负的整数。每打开或创建一个文件,内核就会向进程返回一个fd,第一个打开文件是0,第二个是1,依次递增。(关于文件描述符详细理解,可以参考我的另一篇博文—— 彻底弄懂 Linux 下的文件描述符

我理解在这里也类似,知道两个集合命名中 fd 的意思有助于理解这两个集合的含义和后面 fwalk 函数最后一个参数的含义。 不同的系统都会支持对文件目录、文件进行一些操作,比如chmod,mkdir这些。这些操作会在Python解释器启动的时候被加载在builtins的globals()字典里变成了内置方法,chmod,mkdir 这些字符串作为键,对应的具体的操作函数作为值。通过102行的_add函数处理进行过滤,只留下当前系统所支持的那些,supports_dir_fd 里面放的就是对文件目录进行操作的函数,supports_fd里面放的就是文件进行操作的函数。过滤过程是根据chmod,mkdir这些键来过滤的,类似根据索引来找具体方法一样。总之,这一行的判断其实可以看做是为了识别在不同的系统中是否定义fwalk 方法,这里经过测试,windows下判断不通过,Linux下判断通过。也就是说os模块,在Linux下有 fwalk 方法,而在Wndows下没有 fwalk 方法。

第425行,可以看出 fwalk 这个函数和前面的 walk 类似,但是多了一个参数 dir_fd=None(参数入参中的那个星号 * 的意思可以参考我的另一篇博客——彻底弄懂 Python3中入参里的*号的作用)。

注释是说:函数作用是目录树生成器。

这个函数除了返回值是一个四元组(dirpath, dirnames, filenames, dirfd)之外,其他实现情况和 walk() 是完全相同的。前三个参数的意义和 walk() 也相同,最后一个参数 dirfd 是目录 dirpath 的文件描述符。

fwalk() 比walk() 的优点是,当可选参数 follow_symlinks is False 时,这个函数对于 symlink race 漏洞是安全的。(symlink race,符号链接竞争漏洞,见 Symlink race----Wikipedia)。如果 dir_fd 参数不是 None,它应该是指向某个目录的文件描述符,并且 top 参数也应该对应修改,改为相对于该目录的相对路径。(fwalk始终支持dir_fd)。

注意:由于fwalk()产生文件描述符,这些描述符仅在下一个迭代步骤之前有效,所以如果要想保留更长的时间应该用dup()函数处理它们。(dup是一个复制文件描述符的系统函数)。

第458~459行,将 top 转化为标准字符串目录。

第460~462行,当 follow_syslinks 为False时,调用系统函数 stat 获取 top 的基本信息。然后调用系统函数 open 获取 top 的文件描述符。(关于 stat 函数见 linux stat函数(获取文件详细信息))。通过这些处理能防止 symlink races 漏洞,具体原因见下面 43~49 行。

第465~471行,当follow_syslinks 为True时,直接调用 _fwalk 函数返回结果。而 follow_syslinks 为False时,后面的(st.S_ISDIR(orig_st.st_mode) and path.samestat(orig_st, stat(topfd))) 可以生效,首先判断 top 的st_mode(文件的类型和存取的权限)是目录而不是符号链接,然后判断 top 和 top的文件描述符(topfd) 的 st_dev(文件的设备编号) 和 st_dev(节点) 是相同的,只有两个条件都成立时才继续往下调用 _fwalk 函数来返回结果。 _fwalk 函数使用文件描述符进行操作,避免直接操作 top 目录时,中途被伪造成同名的指向其他路径的符号链接。并且经过前面两个条件的验证,保证了传入是原来的 top 目录的文件描述符。

下面的 _fwalk 逻辑和前面的 walk 基本类似,不同的地方是这里递归时用的是文件描述符。

第531~654行 exec函数族

def execl(file, *args):
    """execl(file, *args)

    Execute the executable file with argument list args, replacing the
    current process. """
    execv(file, args)

def execle(file, *args):
    """execle(file, *args, env)

    Execute the executable file with argument list args and
    environment env, replacing the current process. """
    env = args[-1]
    execve(file, args[:-1], env)

def execlp(file, *args):
    """execlp(file, *args)

    Execute the executable file (which is searched for along $PATH)
    with argument list args, replacing the current process. """
    execvp(file, args)

def execlpe(file, *args):
    """execlpe(file, *args, env)

    Execute the executable file (which is searched for along $PATH)
    with argument list args and environment env, replacing the current
    process. """
    env = args[-1]
    execvpe(file, args[:-1], env)

def execvp(file, args):
    """execvp(file, args)

    Execute the executable file (which is searched for along $PATH)
    with argument list args, replacing the current process.
    args may be a list or tuple of strings. """
    _execvpe(file, args)

def execvpe(file, args, env):
    """execvpe(file, args, env)

    Execute the executable file (which is searched for along $PATH)
    with argument list args and environment env, replacing the
    current process.
    args may be a list or tuple of strings. """
    _execvpe(file, args, env)

__all__.extend(["execl","execle","execlp","execlpe","execvp","execvpe"])

def _execvpe(file, args, env=None):
    if env is not None:
        exec_func = execve
        argrest = (args, env)
    else:
        exec_func = execv
        argrest = (args,)
        env = environ

    if path.dirname(file):
        exec_func(file, *argrest)
        return
    saved_exc = None
    path_list = get_exec_path(env)
    if name != 'nt':
        file = fsencode(file)
        path_list = map(fsencode, path_list)
    for dir in path_list:
        fullname = path.join(dir, file)
        try:
            exec_func(fullname, *argrest)
        except (FileNotFoundError, NotADirectoryError) as e:
            last_exc = e
        except OSError as e:
            last_exc = e
            if saved_exc is None:
                saved_exc = e
    if saved_exc is not None:
        raise saved_exc
    raise last_exc


def get_exec_path(env=None):
    """Returns the sequence of directories that will be searched for the
    named executable (similar to a shell) when launching a process.

    *env* must be an environment variable dict or None.  If *env* is None,
    os.environ will be used.
    """
    # Use a local import instead of a global import to limit the number of
    # modules loaded at startup: the os module is always loaded at startup by
    # Python. It may also avoid a bootstrap issue.
    import warnings

    if env is None:
        env = environ

    # {b'PATH': ...}.get('PATH') and {'PATH': ...}.get(b'PATH') emit a
    # BytesWarning when using python -b or python -bb: ignore the warning
    with warnings.catch_warnings():
        warnings.simplefilter("ignore", BytesWarning)

        try:
            path_list = env.get('PATH')
        except TypeError:
            path_list = None

        if supports_bytes_environ:
            try:
                path_listb = env[b'PATH']
            except (KeyError, TypeError):
                pass
            else:
                if path_list is not None:
                    raise ValueError(
                        "env cannot contain 'PATH' and b'PATH' keys")
                path_list = path_listb

            if path_list is not None and isinstance(path_list, bytes):
                path_list = fsdecode(path_list)

    if path_list is None:
        path_list = defpath
    return path_list.split(pathsep)

这里的6个函数类似Linux中6个以exec开头的函数族:execl、execv、execle、execve、execlp、execvp。(并非一一对应)

Linux中的 exec 函数族提供了一种方法,在不改变进程号的情况下用新的进程替代原来的进程。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新程序的内容替换了。这6个函数中真正的系统调用只有execve,其他5个都是库函数,它们最终都会调用execve这个系统调用。(详见 exec函数)

第531~536行,execl 函数,对应Linux中的execl函数,作用是调用python内置函数execv执行一个可执行文件替代现有进程,入参是文件完整路径和参数。

示例: os.execl('/usr/bin/cat', '/root/test.py', '/root/test2.py')

第538~544行,execle 函数,对应Linux中的execle函数,作用是调用python内置函数execve执行一个可执行文件替代现有进程,入参是文件完整路径、参数、新程序的环境变量路径(最后一个参数,字典类型)。这里注意第一个参数要传入新程序的完整路径。传入的环境路径作为新程序的环境变量路径。

示例 :os.execle('/usr/bin/cat', '/root/test.py', '/root/test2.py', {'SYS': '/root/bin'})

第546~551行,execlp 函数,对应Linux中的execlp函数,作用是调用python内置函数execvp执行一个可执行文件替代现有进程,入参是文件名、参数。这里第一个入参只需要填写新程序的文件名,会去系统默认环境变量路径查找该文件。

示例 :os.execlp('cat', '/root/test.py', '/root/test2.py')

第553~560行,execlpe 函数,对应Linux中的execve函数,作用是调用python内置函数execvpe执行一个可执行文件替代现有进程,入参是文件名、参数、新程序的环境变量路径。这里注意第一个入参只需要填写新程序的文件名,会去传入的环境路径查找该文件,如果没有则去系统默认环境变量PATH查找。

示例 :os.execlpe('cat', '/root/test.py', '/root/test2.py', {'SYS': '/root/bin'})

注意下面这种情况,比如 cat 程序是在系统环境变量PATH中的'/usr/bin'目录下,传入的环境变量覆盖了系统默认环境变量PATH,没有加'/usr/bin',此时将找不到 cat (示例1)。此时需要加上 cat 所在的目录,用冒号":"隔开(示例2,示例3)。

示例 1:os.execlpe('cat', '/root/test.py', '/root/test2.py', {'PATH': '/root/bin'})

示例 2:os.execlpe('cat', '/root/test.py', '/root/test2.py', {'PATH': '/root/bin:/usr/bin'})

示例 2:os.execlpe('cat', '/root/test.py', '/root/test2.py', {'SYS': '/root/bin:/usr/bin'})

 第562~568行,execvp 函数,作用同第546~551行 execlp 函数,依然对应Linux中的 execlp 函数。不同的地方是这里的参数要用列表类型传入。

示例 :os.execvp('cat', ['/root/test.py', '/root/test2.py'])

 第570~577行,execvpe 函数,作用同第553~560行 execlpe 函数,依然对应Linux中的 execve 函数。不同的地方是这里的参数要用列表类型传入。

示例 :os.execvpe('cat', ['/root/test.py', '/root/test2.py'], {'SYS': '/root/bin'})

以上可以看出这6个exec函数和Linux原生的6个exec函数虽然功能类似,但是还是有些区别的,需要使用时要注意一些细节。

第581~ 654行是_execvpe 的定义,execvp 函数和execvpe 函数均调用此函数。_execvpe 将列表参数解成字符串参数,最终还是落到execv和execve函数上。这一段的几个函数看着很冗余,不够优雅,不知道大佬们写的时候怎么想的。

第657~713行 _Environ——环境变量信息的类

# Change environ to automatically call putenv(), unsetenv if they exist.
from _collections_abc import MutableMapping

class _Environ(MutableMapping):
    def __init__(self, data, encodekey, decodekey, encodevalue, decodevalue, putenv, unsetenv):
        self.encodekey = encodekey
        self.decodekey = decodekey
        self.encodevalue = encodevalue
        self.decodevalue = decodevalue
        self.putenv = putenv
        self.unsetenv = unsetenv
        self._data = data

    def __getitem__(self, key):
        try:
            value = self._data[self.encodekey(key)]
        except KeyError:
            # raise KeyError with the original key value
            raise KeyError(key) from None
        return self.decodevalue(value)

    def __setitem__(self, key, value):
        key = self.encodekey(key)
        value = self.encodevalue(value)
        self.putenv(key, value)
        self._data[key] = value

    def __delitem__(self, key):
        encodedkey = self.encodekey(key)
        self.unsetenv(encodedkey)
        try:
            del self._data[encodedkey]
        except KeyError:
            # raise KeyError with the original key value
            raise KeyError(key) from None

    def __iter__(self):
        # list() from dict object is an atomic operation
        keys = list(self._data)
        for key in keys:
            yield self.decodekey(key)

    def __len__(self):
        return len(self._data)

    def __repr__(self):
        return 'environ({{{}}})'.format(', '.join(
            ('{!r}: {!r}'.format(self.decodekey(key), self.decodevalue(value))
            for key, value in self._data.items())))

    def copy(self):
        return dict(self)

    def setdefault(self, key, value):
        if key not in self:
            self[key] = value
        return self[key]

这个类实际上就是生成 os.environ 的类,能够获取系统所有的环境变量信息。

第658行,从抽象类模块_collections_abc 导入 MutableMapping类,python3中这个类也是字典的父类。平时我们经常用os.environ.get('xxx'),和字典很类似,就是因为_Environ这个类重写了__getitem__()等方法。但是os.environ并不是字典类型,如果用 print(type(os.environ)) 会发现,它的类型是 <class 'os._Environ'> 。

为什么要定义 encode和decode 那几个入参,见772~794行。

第715~721行 os.putenv——设置环境变量

try:
    _putenv = putenv
except NameError:
    _putenv = lambda key, value: None
else:
    if "putenv" not in __all__:
        __all__.append("putenv")

调用 nt 或者 posix 库的 putenv 方法, 通过os.putenv('key', 'value')将名为 key 的环境变量值设置为 value。该变量名修改会影响由 os.system(),popen(),fork()和execv()发起的子进程。但是putenv()的调用不会更新 os.environ,因此最好使用os.environ来更改环境变量。

第723~729行 os.unsetenv——删除环境变量

try:
    _unsetenv = unsetenv
except NameError:
    _unsetenv = lambda key: _putenv(key, "")
else:
    if "unsetenv" not in __all__:
        __all__.append("unsetenv")

调用 nt 或者 posix 库的 unsetenv 方法, 通过os.unsetenv ('key')删除名为 key 的环境变量。该变量名删除会影响由 os.system(),popen(),fork()和execv()发起的子进程。但是unsetenv()的调用不会更新 os.environ,因此最好直接删除 os.environ 中的变量。

第731~759行 os.environ——环境变量信息

def _createenviron():
    if name == 'nt':
        # Where Env Var Names Must Be UPPERCASE
        def check_str(value):
            if not isinstance(value, str):
                raise TypeError("str expected, not %s" % type(value).__name__)
            return value
        encode = check_str
        decode = str
        def encodekey(key):
            return encode(key).upper()
        data = {}
        for key, value in environ.items():
            data[encodekey(key)] = value
    else:
        # Where Env Var Names Can Be Mixed Case
        encoding = sys.getfilesystemencoding()
        def encode(value):
            if not isinstance(value, str):
                raise TypeError("str expected, not %s" % type(value).__name__)
            return value.encode(encoding, 'surrogateescape')
        def decode(value):
            return value.decode(encoding, 'surrogateescape')
        encodekey = encode
        data = environ
    return _Environ(data,
        encodekey, decode,
        encode, decode,
        _putenv, _unsetenv)

# unicode environ
environ = _createenviron()
del _createenviron

这段代码就是生成 os.environ 的过程。注意743行和755行,用到了 environ,而搜索整个 os.py 文件,只有在后面762行定义了 environ ,看上去好像自己定义自己了,这是怎么回事呢?

因为此 environ 非彼 environ,经过打桩调试,可以发现743行和755行的 environ 是字典类型,表示当前系统的所有环境变量的键值对,是在53行(from posix import *)或73行(from nt import *)从内置库引入的(取决于系统类型)。这段代码太有迷惑性了,还是挺坑的。理解了这个,也就可以明白 os.environ 其实还是对 nt 或者 posix 库里的environ的一层封装,整个os库其实做的就是封装这件事。至于为啥把原来的字典类型变成一种新的类型,我也不知道,额 ~。

第766~770行 getenv——获取环境变量

def getenv(key, default=None):
    """Get an environment variable, return None if it doesn't exist.
    The optional second argument can specify an alternate default.
    key, default and the result are str."""
    return environ.get(key, default)

 通过os.unsetenv ('key')获取环境变量key的值,如果存在,返回环境变量key的值,否则返回default的值。key和返回值均为str字符串类型。

第772~794行 environb,getenvb——字节型(byte)环境变量

supports_bytes_environ = (name != 'nt')
__all__.extend(("getenv", "supports_bytes_environ"))

if supports_bytes_environ:
    def _check_bytes(value):
        if not isinstance(value, bytes):
            raise TypeError("bytes expected, not %s" % type(value).__name__)
        return value

    # bytes environ
    environb = _Environ(environ._data,
        _check_bytes, bytes,
        _check_bytes, bytes,
        _putenv, _unsetenv)
    del _check_bytes

    def getenvb(key, default=None):
        """Get an environment variable, return None if it doesn't exist.
        The optional second argument can specify an alternate default.
        key, default and the result are bytes."""
        return environb.get(key, default)

    __all__.extend(("environb", "getenvb"))

设置标志位 supports_bytes_environ 判断当前系统类型是否是Windows。顾名思义,Windows不支持byte类型的环境变量。

在支持byte类型的环境变量的系统中,定义byte类型的 os.environb 对应字符串类型的 os.environ , 定义byte类型的 os.getenvb 对应字符串类型的 os.getenv 。到这里也就明白为什么前面 _Environ 类要定义 encode和decode 那些入参了。

第796~827行 fsencode,fsdecode——路径名的编码和解码

def _fscodec():
    encoding = sys.getfilesystemencoding()
    errors = sys.getfilesystemencodeerrors()

    def fsencode(filename):
        """Encode filename (an os.PathLike, bytes, or str) to the filesystem
        encoding with 'surrogateescape' error handler, return bytes unchanged.
        On Windows, use 'strict' error handler if the file system encoding is
        'mbcs' (which is the default encoding).
        """
        filename = fspath(filename)  # Does type-checking of `filename`.
        if isinstance(filename, str):
            return filename.encode(encoding, errors)
        else:
            return filename

    def fsdecode(filename):
        """Decode filename (an os.PathLike, bytes, or str) from the filesystem
        encoding with 'surrogateescape' error handler, return str unchanged. On
        Windows, use 'strict' error handler if the file system encoding is
        'mbcs' (which is the default encoding).
        """
        filename = fspath(filename)  # Does type-checking of `filename`.
        if isinstance(filename, bytes):
            return filename.decode(encoding, errors)
        else:
            return filename

    return fsencode, fsdecode

fsencode, fsdecode = _fscodec()
del _fscodec

先将路径名转化成标准路径名,即 os.PathLike 的,然后再进行编码和解码的操作,没啥好说的。

第829~970行 spawn函数族

# Supply spawn*() (probably only for Unix)
if _exists("fork") and not _exists("spawnv") and _exists("execv"):

    P_WAIT = 0
    P_NOWAIT = P_NOWAITO = 1

    __all__.extend(["P_WAIT", "P_NOWAIT", "P_NOWAITO"])

    # XXX Should we support P_DETACH?  I suppose it could fork()**2
    # and close the std I/O streams.  Also, P_OVERLAY is the same
    # as execv*()?

    def _spawnvef(mode, file, args, env, func):
        # Internal helper; func is the exec*() function to use
        if not isinstance(args, (tuple, list)):
            raise TypeError('argv must be a tuple or a list')
        if not args or not args[0]:
            raise ValueError('argv first element cannot be empty')
        pid = fork()
        if not pid:
            # Child
            try:
                if env is None:
                    func(file, args)
                else:
                    func(file, args, env)
            except:
                _exit(127)
        else:
            # Parent
            if mode == P_NOWAIT:
                return pid # Caller is responsible for waiting!
            while 1:
                wpid, sts = waitpid(pid, 0)
                if WIFSTOPPED(sts):
                    continue
                elif WIFSIGNALED(sts):
                    return -WTERMSIG(sts)
                elif WIFEXITED(sts):
                    return WEXITSTATUS(sts)
                else:
                    raise OSError("Not stopped, signaled or exited???")

    def spawnv(mode, file, args):
        """spawnv(mode, file, args) -> integer

Execute file with arguments from args in a subprocess.
If mode == P_NOWAIT return the pid of the process.
If mode == P_WAIT return the process's exit code if it exits normally;
otherwise return -SIG, where SIG is the signal that killed it. """
        return _spawnvef(mode, file, args, None, execv)

    def spawnve(mode, file, args, env):
        """spawnve(mode, file, args, env) -> integer

Execute file with arguments from args in a subprocess with the
specified environment.
If mode == P_NOWAIT return the pid of the process.
If mode == P_WAIT return the process's exit code if it exits normally;
otherwise return -SIG, where SIG is the signal that killed it. """
        return _spawnvef(mode, file, args, env, execve)

    # Note: spawnvp[e] isn't currently supported on Windows

    def spawnvp(mode, file, args):
        """spawnvp(mode, file, args) -> integer

Execute file (which is looked for along $PATH) with arguments from
args in a subprocess.
If mode == P_NOWAIT return the pid of the process.
If mode == P_WAIT return the process's exit code if it exits normally;
otherwise return -SIG, where SIG is the signal that killed it. """
        return _spawnvef(mode, file, args, None, execvp)

    def spawnvpe(mode, file, args, env):
        """spawnvpe(mode, file, args, env) -> integer

Execute file (which is looked for along $PATH) with arguments from
args in a subprocess with the supplied environment.
If mode == P_NOWAIT return the pid of the process.
If mode == P_WAIT return the process's exit code if it exits normally;
otherwise return -SIG, where SIG is the signal that killed it. """
        return _spawnvef(mode, file, args, env, execvpe)


    __all__.extend(["spawnv", "spawnve", "spawnvp", "spawnvpe"])


if _exists("spawnv"):
    # These aren't supplied by the basic Windows code
    # but can be easily implemented in Python

    def spawnl(mode, file, *args):
        """spawnl(mode, file, *args) -> integer

Execute file with arguments from args in a subprocess.
If mode == P_NOWAIT return the pid of the process.
If mode == P_WAIT return the process's exit code if it exits normally;
otherwise return -SIG, where SIG is the signal that killed it. """
        return spawnv(mode, file, args)

    def spawnle(mode, file, *args):
        """spawnle(mode, file, *args, env) -> integer

Execute file with arguments from args in a subprocess with the
supplied environment.
If mode == P_NOWAIT return the pid of the process.
If mode == P_WAIT return the process's exit code if it exits normally;
otherwise return -SIG, where SIG is the signal that killed it. """
        env = args[-1]
        return spawnve(mode, file, args[:-1], env)


    __all__.extend(["spawnl", "spawnle"])


if _exists("spawnvp"):
    # At the moment, Windows doesn't implement spawnvp[e],
    # so it won't have spawnlp[e] either.
    def spawnlp(mode, file, *args):
        """spawnlp(mode, file, *args) -> integer

Execute file (which is looked for along $PATH) with arguments from
args in a subprocess with the supplied environment.
If mode == P_NOWAIT return the pid of the process.
If mode == P_WAIT return the process's exit code if it exits normally;
otherwise return -SIG, where SIG is the signal that killed it. """
        return spawnvp(mode, file, args)

    def spawnlpe(mode, file, *args):
        """spawnlpe(mode, file, *args, env) -> integer

Execute file (which is looked for along $PATH) with arguments from
args in a subprocess with the supplied environment.
If mode == P_NOWAIT return the pid of the process.
If mode == P_WAIT return the process's exit code if it exits normally;
otherwise return -SIG, where SIG is the signal that killed it. """
        env = args[-1]
        return spawnvpe(mode, file, args[:-1], env)


    __all__.extend(["spawnlp", "spawnlpe"])

此处的 spawn 函数族对应 LInux 中的 spawn 函数族,并不完全相同。实际上,Linux中spawn()函数族与exec()函数族有相似也有区别。exec()是启动一个程序并取代原来的程序,spawn()函数既可以在结束原来的程序后启动另一个程序,也可以启动另一个程序并在该程序结束后返回到原来的程序,通过 mode 参数来控制。见 spawn 系列函数

第832~835行,定义 mode 参数的两个枚举值,P_WAIT = 0 ,表示阻塞的方式启动子进程,父进程挂起直到子进程执行完毕,P_NOWAIT = 1表示非阻塞的方式启动子进程,父进程和子进程同时运行。(Linux中mode还有另外一个枚举值 P_OVERLAY = 2 ,表示子进程将覆盖父进程原有的存储区位置。这与 exec族函数族调用是相同的,这也是为什么os库的spawn 函数族没有这个枚举值,下面的838行注释有说)。可以看出来spawn函数族和fork有点像,其实往下看代码可以知道,os库里的spawn确实是通过调用folk来实现的。

第841行,定义_spawnvef 函数,后面四个函数都是调用这个函数。

第843~846行,args 参数必须是元组或者列表,且不能为空。

第847行,folk一个子进程,获取返回pid。使用fork函数得到的子进程从父进程的继承了当前进程的整个地址空间,但是pid是不同的,所以可以根据这个来分别控制父进程和子进程的行为。关于fork见 linux中fork函数详解 和 spawn() Vs. fork()

第848~856行,在子进程中,fork返回0,所以进入 if not pid 分支,执行传入的func函数。出现异常时返回127,一般是环境变量或者当前路径没找到file这个文件。

第857~870行,在父进程中,fork返回新创建子进程的进程ID,所以进入 else 分支。如果是非阻塞模式即mode == P_NOWAIT,函数直接返回。如说是阻塞模式,则父进程一直检测子进程的状态,直到子进程执行结束。

第872~914行:

  • spawnv 函数, 调用_spawnvef,最终执行的是 execv,执行一个可执行文件替代现有进程。

  • spawnve 函数, 调用_spawnvef,最终执行的是 execve,执行一个可执行文件替代现有进程。

  • spawnvp 函数, 调用_spawnvef,最终执行的是 execvp,执行一个可执行文件替代现有进程。

  • spawnvp 函数, 调用_spawnvef,最终执行的是 execvpe,执行一个可执行文件替代现有进程。

第917~970行,这4个函数对应上面4个函数,不同的地方是将最后一个参数从列表或元组变为可变参数类型。例如调用spawnv(mode, file, [args1,  args2, args3]) 变成 spawnl(mode, file, args1,  args2, args3)。

  • spawnl 函数, 对应前面 spawnv 函数。

  • spawnle 函数, 对应前面 spawnve 函数。

  • spawnlp 函数, 对应前面 spawnvp 函数。

  • spawnlpe 函数, 对应前面 spawnvp 函数。

第973~1016行 popen——执行cmd或者shell命令

# Supply os.popen()
def popen(cmd, mode="r", buffering=-1):
    if not isinstance(cmd, str):
        raise TypeError("invalid cmd type (%s, expected string)" % type(cmd))
    if mode not in ("r", "w"):
        raise ValueError("invalid mode %r" % mode)
    if buffering == 0 or buffering is None:
        raise ValueError("popen() does not support unbuffered streams")
    import subprocess, io
    if mode == "r":
        proc = subprocess.Popen(cmd,
                                shell=True,
                                stdout=subprocess.PIPE,
                                bufsize=buffering)
        return _wrap_close(io.TextIOWrapper(proc.stdout), proc)
    else:
        proc = subprocess.Popen(cmd,
                                shell=True,
                                stdin=subprocess.PIPE,
                                bufsize=buffering)
        return _wrap_close(io.TextIOWrapper(proc.stdin), proc)

# Helper for popen() -- a proxy for a file whose close waits for the process
class _wrap_close:
    def __init__(self, stream, proc):
        self._stream = stream
        self._proc = proc
    def close(self):
        self._stream.close()
        returncode = self._proc.wait()
        if returncode == 0:
            return None
        if name == 'nt':
            return returncode
        else:
            return returncode << 8  # Shift left to match old behavior
    def __enter__(self):
        return self
    def __exit__(self, *args):
        self.close()
    def __getattr__(self, name):
        return getattr(self._stream, name)
    def __iter__(self):
        return iter(self._stream)

这个方法很常用,功能是执行一条命令,就像在cmd或者shell里执行一条命令一样。原来内部是使用 subprocess.Popen 实现的。下面重点看一下 io.TextIOWrapper 的意思:

为了使复杂的I/O软件具有清晰的结构,良好的可移植性和适应性,在I/O软件中普遍釆用了层次式结构,将系统输入/输出功能组织成一系列的层次,每一层都利用其下层提供的服务,完成输入/输出功能中的某些子功能,并屏蔽这些功能实现的细节,向高层提供服务。Python中主要分了三层,文本I/O,二进制I/O和原始I/O。每一种都有对应的类实现。I/O层次结构的顶部是抽象基类IOBase,它定义了流的基本接口,但读取和写入流之间没有分离。TextIOBase类继承IOBase,用于处理字节表示文本流,并从字符串处理编码和解码。而TextIOWrapper是TextIOBase类的子类,用来缓冲原始流BufferedIOBase的缓冲文本接口。(详见 python3之模块io 和 I/O子系统的层次结构) 。 其实在这里只需要知道 os.Popen 和 原始subprocess.Popen 的区别是 os.Popen会把返回值用 _wrap_close 封装一次。

第1018~1023行 fdopen——打开文件

# Supply os.fdopen()
def fdopen(fd, *args, **kwargs):
    if not isinstance(fd, int):
        raise TypeError("invalid fd type (%s, expected integer)" % type(fd))
    import io
    return io.open(fd, *args, **kwargs)

传入打开文件描述符 fd ,返回对应文件的对象。类似内建 open 函数,二者接受同样的参数。不同之处在于fdopen第一个参数应该为整数(文件描述符)。关于最后的 return  io.open , 官方文档 说 io.open 和内建函数 open 的区别是:io.open 是 open 的别名 (This is an alias for the builtin open() function)。

第1026~1061行 fspath——路径标准化

# For testing purposes, make sure the function is available when the C
# implementation exists.
def _fspath(path):
    """Return the path representation of a path-like object.

    If str or bytes is passed in, it is returned unchanged. Otherwise the
    os.PathLike interface is used to get the path representation. If the
    path representation is not str or bytes, TypeError is raised. If the
    provided path is not str, bytes, or os.PathLike, TypeError is raised.
    """
    if isinstance(path, (str, bytes)):
        return path

    # Work from the object's type to match method resolution of other magic
    # methods.
    path_type = type(path)
    try:
        path_repr = path_type.__fspath__(path)
    except AttributeError:
        if hasattr(path_type, '__fspath__'):
            raise
        else:
            raise TypeError("expected str, bytes or os.PathLike object, "
                            "not " + path_type.__name__)
    if isinstance(path_repr, (str, bytes)):
        return path_repr
    else:
        raise TypeError("expected {}.__fspath__() to return str or bytes, "
                        "not {}".format(path_type.__name__,
                                        type(path_repr).__name__))

# If there is no C implementation, make the pure Python version the
# implementation as transparently as possible.
if not _exists('fspath'):
    fspath = _fspath
    fspath.__name__ = "fspath"

第1028~1055行, _fspath 方法的作用是返回一个 os 模块能处理的标准路径形式,其实就是字符串。如果传入的是 str 或 bytes 类型的字符串,将原样返回。否则将调用传入参数的 __fspath__ 方法,如果得到的是一个 str 或 bytes 类型的对象,那就返回这个值。__fspath__ 方法应该是这样定义的:

def __fspath__(self):
    return self.path.encode('utf8')

 第1059~1061行,只有在 buildin 的内建方法 globals() 里不包含 fspath 时,才会调用上面的_fspath方法去定义 os.fspath。实测 windows 下不会走这个分支,也就是说 Windows 下内建方法已经存在 fspath 了。

第1064~1077行 PathLike——什么是PathLike

class PathLike(abc.ABC):

    """Abstract base class for implementing the file system path protocol."""

    @abc.abstractmethod
    def __fspath__(self):
        """Return the file system path representation of the object."""
        raise NotImplementedError

    @classmethod
    def __subclasshook__(cls, subclass):
        if cls is PathLike:
            return _check_methods(subclass, '__fspath__')
        return NotImplemented

前面提到好多次,路径要是 PathLike 的,这个抽象类就是为了解释到底什么样的类才是 “ PathLike的”。里面用到了@abc.abstractmethod 装饰器,说明这个类是抽象类不能被实例化。但是可以被继承, __subclasshook__ 检查子类是否实现了__fspath__ 方法。也就是说一个类如果是 “PathLike 的”,首先它必须要实现 __fspath__ 方法。

第1080~1115行 add_dll_directory—— 添加DLL文件搜索路径

if name == 'nt':
    class _AddedDllDirectory:
        def __init__(self, path, cookie, remove_dll_directory):
            self.path = path
            self._cookie = cookie
            self._remove_dll_directory = remove_dll_directory
        def close(self):
            self._remove_dll_directory(self._cookie)
            self.path = None
        def __enter__(self):
            return self
        def __exit__(self, *args):
            self.close()
        def __repr__(self):
            if self.path:
                return "<AddedDllDirectory({!r})>".format(self.path)
            return "<AddedDllDirectory()>"

    def add_dll_directory(path):
        """Add a path to the DLL search path.

        This search path is used when resolving dependencies for imported
        extension modules (the module itself is resolved through sys.path),
        and also by ctypes.

        Remove the directory by calling close() on the returned object or
        using it in a with statement.
        """
        import nt
        cookie = nt._add_dll_directory(path)
        return _AddedDllDirectory(
            path,
            cookie,
            nt._remove_dll_directory
        )

在Python中调用C/C++编写的dll文件时,需要提供这个dll文件的搜索路径(类似环境变量)。add_dll_directory 函数就用于在导入扩展模块或使用 ctypes 加载 DLL 时,依赖提供额外搜索路径。调用返回的_AddedDllDirectory 的close方法可以删除这个路径。主要调用 nt._add_dll_directory方法实现,所以只在Windows系统有效。

总结

《彻底弄懂Python标准库源码》计划,才完成第一篇,最大的感受就是读标准库源码跟我想象很不一样,太太太难了。我以为标准库一定都很通俗易懂吧,应该挺简单的吧,可事实上看过来,这些标准库方法在外面调用起来的确挺优雅的,它们的实现过程还挺复杂的。我们调用的时候只需要关注入参和出参是什么就行了,有时候感觉好神奇好方便啊,我传个A竟然能返回我想要的B。而研究源码就是要看它为什么是这样的,为什么这么神奇,这就要求每一行代码的意思都要搞明白。作为一个程序员,我们要一直保持着一颗好奇心,我们不会相信有什么黑科技,我们一定要看看这些 magic 的背后是什么原理 。

可能也和第一篇选的os库有关系,这个和系统交互的库,大部分是把C的库进行了一层封装。后半部分涉及很多系统底层的的东西,好多不了解的知识,导致看的很煎熬,中间各种查资料补课,还另外补充了几篇博客。好在没想过放弃,自己立的 flag 一定要把它完成。

本来想学习标准库的优雅写法,可是看完我发现我并学不会,os库也不是想象的那么优雅,更多的是为了更有效地实现功能。感觉要想达到学习代码写法的目的,还是看 requests 这种有名的第三方库比较好。还有这篇博客这种一行一行贴代码流水账式的分析意义也不大,白白占用很大篇幅,以后考虑换一种风格。

参考链接

1. 如果只是想看 os 库的用法,官方文档写的很全很详细

    官方文档 os --- 多种操作系统接口 

    官方文档  io --- 处理流的核心工具

2. 彻底弄懂 Linux 下的文件描述符 —— 杰克小麻雀

3. 彻底弄懂 Python3中入参里的*号的作用 —— 杰克小麻雀

4. Symlink race ---- Wikipedia

5. linux stat函数(获取文件详细信息)—— mastodon

6. exec函数 —— guoping16

7. spawn 系列函数 —— xiaosi2468

8. linux中fork函数详解 —— jason314 

9.  spawn() Vs. fork() —— The UNIX and Linux Forums

10. python3之模块io —— lincappu 

11. I/O子系统的层次结构 —— C语言中文网


前文见 

彻底弄懂Python标准库源码(零)—— 学习计划

彻底弄懂Python标准库源码(一)—— os模块

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值