崇尚新闻网

崇尚新闻网独家

cpython 教你阅读 Cpython 的源码(一)

2021-06-11 13:16:09 独家

正文:12920字,7位数

预计阅读时间:33分钟

来源:https://realpython.com/cpython-source-code-guide,

译者:陈翔安

就像题目一样,这篇文章是教你关于Cpython的文章。因为内容太长,打算先分开写,以后再合并。

前言

这篇文章很长,但很有用。如果你决定学习Cpython,我希望你能阅读它,你会发现它是一个很好的学习材料。

本文共分为五个部分。你可以根据自己的情况合理安排阅读时间。每个部分都需要一定的时间。通过学习一些案例,你会有成就感,因为你掌握了Python的核心概念,这让你成为了一个更好的Python程序员。

第一部分介绍了Cpython

其实我们平时说的python大多指的是Cpython,它是众多Python中的一种,除了Pypy,Jpython等等。CPython也是官方Python版本,还有很多在线案例。所以这里主要说一下Cpython。

注意:本文是为3.8.0b3版CPython源代码写的。

源代码里有什么?

CPython源代码分发包括各种工具、库和组件。我们将在本文中讨论这些内容。

首先,我们将关注编译器。先从git下载Cpython源代码。

git clonehttps://github . com/python/cpython

cdcpython

git check out 3 . 8 . 0 B3 #切换我们需要的分支

注意:如果没有Git,可以直接从GitHub网站下载ZIP文件形式的源代码。

解压我们下载的文件,它的目录结构如下:

cpython/

├──文档←源代码文档描述

├──语法←计算机可读语言定义

├──包含← C语言头文件(头文件中通常有一些可重用的代码)

├──lib←python编写的标准库文件

├── Mac ← Mac支持文件

├──杂项←杂项

├ ──模块编写的标准库文件← C

├──对象←核心类型和对象模块

├──解析器← Python解析器源代码

├──PC←windows编译支持的文件

├── PCbuild ←旧版本Windows系统支持的编译文件

├ ─ ─程序python可执行文件和其他二进制文件的源代码

├── Python ← CPython解析器源代码

└──工具←用于构建或扩展Python的独立工具

接下来,我们将从源代码中编译CPython。

这一步需要一个C编译器和一些构建工具。不同的系统编译不同。这里我用的是mac系统。

在macOS上编译CPython很简单。在终端中,运行以下命令安装C编译器和工具包:

$ xcode-选择-安装

这个命令会弹出一个提示下载安装一套工具,包括Git,Make,GNU C编译器。

你还需要一个OpenSSL的工作副本来从PyPi.org网站获取包。

如果您计划将来使用此版本安装其他软件包,则需要SSL身份验证。

在macOS上安装OpenSSL最简单的方法就是使用HomeBrew。

如果您已经安装了自制程序,您可以使用brew安装命令来安装CPython的依赖项。

$ brew install openssl xz zlib

现在您已经有了依赖关系,您可以在Cpython目录中运行配置脚本:

$ CPPFLAGS= "-I$(brew -前缀zlib)/include "

LDFLAGS= "-L$(brew -前缀zlib)/lib "

。/configure-with-OpenSSL = $(brew-prefix OpenSSL)-with-pydebug

在上面的安装命令中,

CPPFLAGS是c和c++编译器的一个选项,它指定zlib头文件的位置。

LDFLAGS是编译器(如gcc)使用的一些优化参数,其中指定了zlib库文件的位置。

(brew - prefix openssl)这部分意思是在终端执行括号中的命令,显示openssl的安装路径。您可以提前执行括号中的命令,并用返回的结果替换它们。效果是一样的。每行末尾的反斜杠可以使三行作为一个命令执行,而不是在换行时执行命令。

运行上述命令后,将在存储库的根目录下生成一个Makefile,您可以使用它来自动化构建过程。那个。/configure步骤只需运行一次。

您可以通过运行以下命令来构建CPython二进制文件。

$ make -j2 -s

-j2标志允许make同时运行两个作业。如果你有4核,可以改成4。-s标志将阻止Makefile打印它运行到控制台的每个命令。可以删除。输出项目太多。在构建过程中,您可能会收到一些错误。在总结中,它将通知您并非所有的包都可以构建。

例如,_dbm、_sqlite3、_uuid、nis、ossaudiodev、spwd和_tkinter将不使用这组指令来构建。如果您不打算开发这些包,这个错误没有影响。如果你真的需要,你可以参考https://devguide.python.org/.

构建和生成一个名为python.exe的二进制文件需要几分钟的时间。每次更改源代码时,都需要重新运行make来编译。

Python.exe二进制文件是CPython的调试二进制文件。执行以下命令查看Python的运行版本。

$ ./python.exe

Python 3.8.0b3 (tags/v3。8.0b3:4336222407,2019年8月21日,10:00:03)

【铿锵10.0。1(铿锵- 1001.0。46.4)]在达尔文

键入“帮助”、“版权”、“学分”或“许可证”以获取更多信息。

&gt。&gt。&gt。

(其实最新的一个已经到了Python3.9,我整理效果如下。)

编译器做了什么?

编译器的目的是将一种语言转换成另一种语言。我们可以把编译过程比作翻译,把英文的“hello”翻译成中文的“Hello”。

有些编译器把代码编译成只有机器才能理解的,可以在系统上直接执行的机器码。其他编译器会编译成中间语言,由虚拟机执行。

选择编译器的一个重要决定是系统可移植性的要求。Java和。NET CLR会编译成中间语言,这样编译出来的代码就可以适应其他的系统类型。C,Go,C ++和Pascal会编译成低级可执行文件,只能在类似编译的系统上运行。

一般我们直接发布Python的源代码,然后通过Python命令直接运行。事实上,在内部,CPPython会在运行时编译您的代码。大多数人认为Python是一种解释性语言。

严格来说,其实是编译型的。

Python代码不编译成机器码。

它被编译成一种特殊的低级中间语言,只有CPython才能理解。在Python3中,字节码存储在。隐藏目录中的pyc文件,它为下次快速执行提供了一个缓存。因此,如果你在不改变源代码的情况下运行同一个Python应用程序两次,第二次总是会快得多。原因是第二次直接加载字节码,然后运行程序,不像第一次需要编译。

为什么CPython是用c写的而不是Python?

CPPython中的C是对C编程语言的引用,暗示这个Python发行版是用C语言写的。

CPython中的编译器都是用纯C写的,然而很多标准库模块都是用纯Python或者C和Python的组合写的。

那么为什么CPython是用c写的而不是Python呢?

答案在于编译器是如何工作的。

有两种类型的编译器:

自托管编译器是用它们编译的语言编写的编译器,例如 Go 编译器。源到源编译器是用另一种已经有编译器的语言编写的编译器。 这也就意味着如果从头开始编写新的编程语言,则需要一个可执行的应用程序来编译你的编译器!你就需要一个编译器来执行任何操作,因此在开发新语言时,它们通常首先用较旧的,更成熟的语言编写。同时节省时间和学习成本。 一个很好的例子就是 Go 语言。 第一个 Go 编译器是用 C 编写的,然后 Go 可以编译,编译器就在 Go 中重写了。

CPython保留了它的C特性:很多标准库模块(比如ssl模块或者sockets模块)都是用C语言编写的,用于访问底层操作系统API。

Windows和Linux内核中用于创建网络套接字、使用文件系统或与监视器交互的API都是用c语言编写的。所以将Python的可扩展性层集中在C语言上是有意义的。在本文的后面,我们将介绍Python标准库和C模块。另外还有一个用Python写的Python编译器,叫PyPy。

PyPy的logo是一个Ouroboros,代表编译器的自我管理特性。Python交叉编译器的另一个例子是Jython。

还有一个是Jython。Jython用Java编写,从Python源代码编译成Java字节码。就像CPython可以轻松导入C库并从Python中使用它们一样,Jython使得导入和引用Java模块和类变得很容易。

Python语言规范

CPython源代码包含Python语言的定义。这是所有Python解释器使用的参考规范。该规范是人类可读和机器可读的格式。文档中详细描述了Python语言、允许的内容和每个语句的行为。

文件

在文档/引用目录中,重构文本文件用Python语言解释了每个函数属性。这是docs.python.org的官方Python参考指南。

目录中有您需要了解整个语言、结构和关键字的文件:

cpython/Doc/参考

|

├──化合物

├──数据模型

├──行政模式第一

├──表情

├──语法

├──进口公司

├──指数

├──介绍

├──词汇分析

├──simple _ stmt . rst

└──顶级组件

在compound _ stmts.rst文件中,您可以看到一个定义with语句的简单示例。with语句可以在Python中以多种方式使用,其中最简单的是上下文管理器和嵌套代码块的实例化:

withx:

...

您可以使用将其重命名为

withx asy:

...

您也可以同时定义多个链

withx asy,z asjk:

...

接下来,我们将探索Python语言的计算机可读文档。

语法

本文档包含存储在单个语法/语法文件中的人类可读规范和机器可读规范。

语法文件是用一种叫做巴克斯-诺尔形式(BNF)的上下文符号编写的。

BNF不是Python特有的,在许多其他语言中经常被用作语法符号。

编程语言中的语法结构受到了诺姆·乔姆斯基在20世纪50年代关于句法结构的工作的启发。

Python的语法文件使用带正则表达式语法的扩展BNF(EBNF)规范。

因此,在语法文件中,您可以使用:

*重复+至少重复一次[]为可选部分|任选一个用于分组 如果在语法文件中搜索 with 语句,你将看到 with 语句的定义: .. productionlist::

with_stmt:"with"`with_item `("," ` with _ item `) *:` suite ` s

with _ item:` expression `[" as " ` target `]

引号中的一切都是字符串,这是一个关键字的定义。因此,with_stmt被指定为:

1 .以单词开头

2.接下来是with_item,它是一个测试和(可选)作为表达式。

3.多个项目用逗号分隔

4.以字符结尾:

5.其次是套房。

这两行中提到了其他一些定义:

suite是指具有一个或多个语句的代码块。test是指一个被评估的简单语句。expr指的是一个简单的表达式 如果你想详细探索这些内容,可以在此文件中定义整个 Python 语法。

如果想看最近如何使用语法的例子,比如在PEP572中,语法文件中增加了:=运算符。

ATEQUAL'@= '

' RARROW '-& gt;'

省略号“...”

+COLONIAL ':= '

过强;管理员

ERRORTOKEN

使用pgen

Python编译器不使用语法文件本身。

是使用名为pgen的工具创建的解析器表。Pgen读取语法文件并将其转换成解析器表。如果对语法文件进行更改,必须重新生成解析器表并重新编译Python。

注意:pgen应用在Python3 .8中从C语言重写为纯Python语言。

为了了解pgen是如何工作的,让我们改变Python语法的一部分。并重新编译运行Python。

我在语法路径下看到了两个文件,语法和令牌。我们在语法中搜索pass_stmt,然后看到如下

pass_stmt: 'pass '

我们修改一下,改成下面这样

pass_stmt:“传递“|”继续”

在Cpython的根目录下使用make regen-Grammar命令运行pgen重新编译语法文件。

您应该会看到类似这样的输出,表明已经生成了新的Include/graminit.h和Python/graminit.c文件:

以下是部分输出

#重新生成Include/graminit.h和Python/graminit.c

#来自语法/使用pgen的语法

PYTHONPATH=。python3 -m Parser.pgen。/语法/语法

。/语法/标记

。/ Include/graminit.h. new

。/Python/graminit.c. new

蟒蛇3。/Tools/s/update_file.py。/ Include/graminit.h。/ Include/graminit.h. new

蟒蛇3。/Tools/s/update_file.py。/Python/graminit.c。/Python/graminit.c. new

使用重新生成的解析器表,需要重新编译CPython来查看新的语法。使用以前用于操作系统的相同编译步骤。

make-j4 -s

如果代码编译成功,执行新的CPython二进制文件并启动REPL。

。/python.exe

在REPL,您现在可以尝试定义一个函数,用编译成Python语法的proceed关键字替换pass语句。

Python 3.8.0b3 (tags/v3。8.0b3:4336222407,2019年8月21日,10:00:03)

【铿锵10.0。1(铿锵- 1001.0。46.4)]在达尔文

键入“帮助”、“版权”、“学分”或“许可证”以获取更多信息。

&gt。&gt。&gt。失败示例:

...进行

...

&gt。&gt。&gt。例子

以下是我手术的结果。有趣的是没有出错。

接下来,我们将讨论令牌文件及其与语法的关系。记号

与语法文件夹中的语法文件一起,它是一个标记文件,包含解析树中作为叶节点的每个唯一类型。稍后我们将深入介绍解析器树。每个令牌也有一个名称和一个生成的唯一标识,用于简化令牌化器中的引用。

注意:令牌文件是Python3 .8中的一个新函数。

比如左括号叫LPAR,分号叫SEMI。

您将在本文后面看到这些标签:

LPAR '('

RPAR ')'

LSQB '['

' RSQB ']'

冒号“:”

逗号','

SEMI ';'

与语法文件一样,如果您更改了令牌文件,您需要再次运行pgen。

要查看操作中的令牌,可以使用CPython中的tokenize模块。创建一个名为test _ tokens.py的简单Python脚本:

#你好世界!

defmy_function:

进行

然后通过标准库中内置的名为tokenize的模块传递该文件。您将按行和字符查看令牌列表。使用-e标志输出确切的令牌名称:

0,0-0,0: ENCODING 'utf-8 '

1,0-1,14: COMMENT'# Hello world!'

1,14-1,15: NL 'n '

2,0-2,3: NAME'def '

2,4-2,15: NAME'my_function '

2,15-2,16: LPAR '('

2,16-2,17: RPAR ')'

2,17-2,18: COLON ':'

2,18-2,19: NEWLINE'n '

3,0-3,3: INDENT ' '

3,3-3,7:名称“继续”

3,7-3,8: NEWLINE'n '

4,0-4,0: DEDENT ' '

4,0-4,0: ENDMARKER ' '

在输出中,第一列是行/列坐标的范围,第二列是令牌的名称,最后一列是令牌的值。

在输出中,tokenize模块暗示了文件中没有的一些标记。

utf-8的ENCODING标记末尾有一行空,它关闭了函数声明,并用ENDMARKER结束文件。Tokenize模块用纯Python编写,在CPPython源代码中位于Lib/tokenize.py。重要说明:CPython源代码中有两个记号化器:一个是用Python写的,上面演示的那个,一个是用c语言写的。用Python写的是作为一个实用程序,用C写的是用Python编译器。然而,它们具有相同的输出和行为。用C语言写的版本是为了性能而设计的,Python中的模块是为了调试而设计的。

要查看c语言的tokenizer的详细信息,可以用-d标志运行Python。

使用前面创建的test_tokens.py脚本,并使用以下命令运行它:

。/python . exe-d test _ token . py

获得了以下结果

令牌名称/ 'def '...这是一个关键词

DFA 'file_input ',状态0:推送' stmt '

DFA 'stmt ',状态0:推送' compound_stmt '

DFA 'compound_stmt ',状态0: Push 'funcdef '

DFA“funcdef”,状态0: Shift。

令牌名称/“我的函数”...这是我们知道的象征

DFA“funcdef”,状态1: Shift。

令牌LPAR/“(”...这是我们知道的象征

DFA“funcdef”,状态2:推送“参数”

DFA“参数”,状态0:移位。

令牌RPAR/')'...这是我们知道的象征

DFA“参数”,状态1: Shift。

DFA“参数”,状态2:直接弹出。

Token COLON/“:”...这是我们知道的象征

DFA“funcdef”,状态3: Shift。

Token NEWLINE/' '...这是我们知道的象征

DFA 'funcdef ',状态5:[将func_body_suite切换到suite]推送' suite '

DFA 'suite ',状态0: Shift。

Token INDENT/' '...这是我们知道的象征

DFA 'suite ',状态1: Shift。

令牌名称/“继续”...这是一个关键词

DFA '套件',状态3:推送' stmt '

...

接受。

在输出中,您可以看到它被突出显示为关键字。在下一章中,我们将看到如何将Python二进制文件执行到标记器,以及从那里执行代码时会发生什么。现在您已经概述了Python语法以及标记和语句之间的关系,有一种方法可以将pgen输出转换为交互式图形。

下面是Python 3.8a2语法的截图:

看不清楚也没关系。用于生成该图的Python包(instaviz)将在下一章介绍。先了解一下。

Python中的内存管理

在本文中,您将看到对PyArena对象的引用。

Arena是CPython的内存管理结构之一。Python/pyarena.c中的代码包括c的内存分配和解除分配方法。

在编写的C程序中,开发人员应该在写入数据之前为数据结构分配内存。这种分配将内存标记为属于操作系统的进程。开发人员还可以在已分配的内存不再使用时将其解除分配或“释放”,并返回到操作系统的可用内存块表中。如果一个进程为一个变量分配内存,比如在一个函数或循环中,当函数完成时,内存不会自动返回给C语言的操作系统。因此,如果没有在C代码中明确发布,就会造成内存泄漏。每次函数运行,进程都会继续占用更多内存,直到最终系统内存耗尽崩溃!Python把这个责任从程序员身上拿走,使用了两种算法:引用计数器和垃圾收集器。每当解释器被实例化时,PyArena方法在解释器中创建并附加一个内存区域。在CPython解释器的生命周期中,可以分配竞技场。它们与链表相关联。

Arenas将指向Python对象的指针列表存储为PyListObject方法。每次创建新的Python对象时,都会使用PyArena_AddPyObject方法向其添加一个指针。

函数调用将指针存储在arenas列表a_objects中。PyArena方法提供了第二个功能,就是分配和引用原来的内存块列表。例如,如果添加数千个附加值,C代码中的PyList将需要额外的内存。但是PyList不直接分配内存。这个对象通过从PyArena中调用PyArena_Malloc来获取PyArena的原始内存块。在对象分配模块中,您可以为Python对象分配、释放和重新分配内存。分配块的链表存储在arenas中,所以当解释器停止时,可以使用PyArena_Free一次性释放所有托管内存块。

以PyListObject为例。如果你用。追加将一个对象放在Python列表的末尾,不需要重新分配内存,而是使用现有列表中的内存。

那个。append方法调用list_resize来处理列表的内存分配。每个list对象都保存一个已分配内存量的列表。如果要添加的项目适合现有的可用内存,只需添加它。如果列表需要更多内存空,它将被扩展。列表长度扩展到0,4,8,16,25,35,46,58,72,88。

您可以通过调用PyMem_Realloc来扩展列表中分配的内存。

PyMem_Realloc是pymalloc_realloc的API包装器。Python还有一个特殊的c调用malloc的包装器,设置内存分配的最大大小,帮助防止缓冲区溢出错误(参见PyMem_RawMalloc)。

总而言之:

原始内存块的分配是通过PyMem_RawAlloc完成的。Python 对象的指针存储在PyArena中。PyArena还存储了已分配内存块的链表。 有关 API 的更多信息,请参阅 CPython 文档。引用计数

要在Python中创建变量并赋值,变量名必须是1。

my_variable= 180392

每当在Python中给变量赋值时,都会在局部变量和全局变量的作用域中检查该变量的名称,以查看它是否已经存在。因为my_variable不在局部变量或全局变量字典中,所以创建了这个新对象,并将该值指定为数字常量180392。现在有了对my_variable的引用,所以my_variable的引用计数器加1。

你可以在CPython的c源代码里看到函数Py _ INCREF和Py _ DECREF。

这两个函数分别计算对象的增量和减量。当变量超出声明的范围时,对对象的引用将减少。Python中的Scope可以指函数或方法、生成器或lambda函数。这些是一些更直观的作用域,但是还有很多其他的隐式作用域,比如将变量传递给函数调用。递增和递减引用的处理是在CPython编译器和核心执行循环ceval.c文件中进行的。我们将在本文后面详细介绍它。

每当调用Py _ DECREF并且计数器变为0时,就调用PyObject_Free函数。对于这个对象,所有分配的内存都调用PyArena_Free。

垃圾收集

CPython的垃圾收集器默认是启用的,它发生在后台,用于释放不再使用的对象的内存。

因为垃圾收集算法比引用计数器复杂得多,不会一直发生,否则会消耗大量CPU资源。经过一定次数的操作,会有规律的发生。CPython的标准库自带一个Python模块,用于连接arena和垃圾收集器的gc模块。

以下是如何在调试模式下使用gc模块:

&gt。&gt。&gt。导入gc

&gt。&gt。&gt。gc.set_debug(gc。调试_统计)

这将在您运行垃圾收集器时打印统计信息。

您可以通过调用get_threshold来获取运行垃圾收集器的阈值:

&gt。&gt。&gt。gc.get_threshold

( 700, 10, 10)

您还可以获得当前阈值计数:

&gt。&gt。&gt。gc.get_count

( 688, 1, 1)

最后,您可以手动运行收集算法:

&gt。&gt。&gt。gc.collect

24

这个调用收集在Modules/gcmodule.c文件中,该文件包含垃圾收集器算法的实现。

结论

在第1部分中,我们介绍了源代码库的结构,如何从源代码和Python语言规范进行编译。

当您对Python解释器过程有更深入的理解时,这些核心概念在第2部分将是至关重要的。

-跟进-

以上就是【cpython 教你阅读 Cpython 的源码(一)】的详细内容,如果您不了解cpython 教你阅读 Cpython 的源码(一)的具体信息,请浏览下面cpython 教你阅读 Cpython 的源码(一)的最新相关资讯。

查看更多cpython 教你阅读 Cpython 的源码(一)相关详细内容

相关推荐:

司法考试信息服务平台 2020全国司法考试报名入口已开通,今日考试报名!附视频网课下载

司法部发布《2020年全国统一法律职业资格考试公告》 清晰客观考试的网上报名时间为7月28日至8月12日 考试时间是10月31日和11月1日 主观题考试网上报名时间为11月10日至14日 考试时间是11月28日。 2020司法考试下载(主客观题VIP) 酷屋:kushe.com.cn/jiaocheng/sifa/1701.html 根据《全国统一法律职业资格考试实施办法》等有关规定,现将202...

情侣头像黑白系 黑白情侣头像

“喜欢一个人太累了 希望你以后能成为喜欢的人。" ▼ ▼ “可笑这世上只有一个人 路过。 这不是回家的路。" ▼ ▼ “说喜欢就好 光看到你就会很乱。" ▼ ▼ “女孩的女孩 上进上进上进。" ▼ ▼ “看到真相了吗 依然热爱生活才是勇敢。" ▼ “不容易欺负 不辜负还不错 爱身边的人。" ▼ #结束#...

陕西省固体废物信息网 陕西省最大的固废处置项目开工

五月的烈日发出红色的热浪。5月15日,陕西省最大、全国第三大关中固体废物处置利用中心项目奠基仪式在所有人的期待和关注下隆重举行。 陕西省生态环境厅总工程师李玉海;陕西省环保集团副书记、总经理顾峰;孟维川,蒲城县县委常委、常务副县长;渭南市生态环境局副局长周斌强;郝,陕西省环保集团公司成员、副董事长;滚动,陕西建工一建集团有限公司总经理;陕西建设工程第一建设集团有限公司副总经理、总经理刘东;中煤...

痘印怎么 脸上很多黑色痘印怎么办 8个小偏方消除痘印

配方一:敷黄瓜去痘印 黄瓜片不仅能促进皮肤新陈代谢,还能扩张皮肤毛细血管,促进血液循环,增强皮肤抗氧化功能。用新鲜黄瓜切片后,可以涂在脸上有痘印的部位。过几天皮肤不仅会有弹性,痘印也会变淡很多。 脱脂酸奶中的痘印脱脂 酸奶富含高活性无机矿物质、微量元素锌以及维生素A和维生素E的衍生物,有助于有毒物质的转化和排泄,减轻痤疮的肿胀,有助于伤口的愈合。只需将剩余的牛奶涂在有痘印的地方,第二天洗掉即可。...

丰巢官网 丰巢致歉并承诺……

最近几天 “丰巢快递柜加班收费”风波 还没有平息 5月15日晚, 深圳丰超科技有限公司官方微博 @丰巢智能GUI 发布关于用户服务调整的说明 丰巢道歉并调整服务:免费寄存时间由12小时延长至18小时。 以下是解释的全文: /先前报告/ 4月28日 凤起快递内阁宣布,从4月30日起, 启动“会员系统”服务 它需要 ▽ 「普通非会员用户可免费保管包裹12小时。加班后每小时收费0.5元,封顶3元。 消...

动态vps 固定vps和动态vps有啥区别

固定vps和动态vps的使用是不同的。千万不要以为固定vps和动态vps是一样的,功能是一样的。这是错误的说法。 其实人们所说的VPS一般指的是固定的VPS服务器,包括云服务器,主要用来搭建网站。近年来,随着互联网技术的快速发展和大数据时代的到来,出现了一种基于固定VPS服务器的动态VPS服务器。顾名思义,IP是动态的,不支持建网站。 那么,使用动态vps服务器的具体目的是什么呢?固定vps和动态...