尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

C语言PythonBashTcl:什么时候修改源码会影响程序?

C语言PythonBashTcl:什么时候修改源码会影响程序? 1、前言在软件开发过程中经常会遇到这样一个问题如果程序已经开始运行此时再修改磁盘上的源代码文件正在执行的程序是否会受到影响例如一个Python程序已经运行此时修改对应的.py文件后续执行到被修改的位置时是否会采用新代码一个Bash脚本执行到一半时修改脚本后半部分Shell是否会读取修改后的内容一个Tcl脚本在执行过程中被修改后续命令是否会发生变化对于已经编译完成并启动的C程序修改.c文件又是否可能改变当前进程的行为。这个问题不能简单地通过“编译型语言”和“解释型语言”进行区分。真正决定结果的是程序准备执行某段代码时这段代码是否已经从磁盘上的源文件中读取出来并转换成了当前进程内部可以执行的形式。换句话说需要区分“磁盘上的源代码文件”和“当前进程已经加载的代码”。编辑器修改的是磁盘文件而程序实际执行的通常是已经加载到进程地址空间中的机器指令、字节码、内部命令表示或已经定义好的函数、过程等对象。如果程序之后不会再次读取对应的源文件那么单纯修改磁盘文件一般不会影响当前程序如果某个文件尚未加载或者程序稍后会主动重新读取它那么修改后的内容就有可能影响程序行为。下面分别讨论C、Python、Bash和Tcl。2、C语言源码在程序运行之前已经退出执行链路2.1 修改.c文件不会影响已经运行的程序C语言是这个问题中最容易理解的一种情况。普通C程序在运行之前需要经过编译和链接源文件中的代码首先由编译器转换成目标代码再由链接器生成可执行文件。真正启动程序时操作系统加载的是已经生成的可执行文件而不是原始的.c文件。例如#include stdio.h #include unistd.h int main(void) { printf(start\n); fflush(stdout); sleep(10); printf(hello\n); return 0; }编译并运行gcc test.c -o test ./test程序输出start后会等待10秒。如果在这10秒内把printf(hello\n);修改成printf(world\n);当前正在运行的程序仍然会输出hello。这是因为printf(hello\n);对应的机器指令在运行之前就已经生成并且在程序启动时已经作为可执行文件的一部分加载到了当前进程中。此时修改test.c改变的只是磁盘上的源文件并不会反向修改已经生成的机器指令。因此对于普通C程序而言修改源代码之后必须重新编译并重新启动程序新代码才能生效。2.2 重新编译可执行文件通常也不会改变旧进程进一步来说即使程序正在运行时重新执行gcc test.c -o test生成了一个新的test通常也不会让已经运行的旧进程突然开始执行新程序中的机器指令。已经运行的程序有自己独立的进程地址空间所需代码已经被操作系统映射到该进程中。磁盘上的可执行文件被重新生成主要影响的是之后启动的新进程而不是已经存在的进程。因此C语言中可以把这个问题概括为程序启动以后普通.c源文件基本已经与当前程序的执行过程无关。当然如果程序主动通过动态库、插件系统等机制在运行期间加载新的代码例如使用dlopen()加载某个共享库那么之后才加载的代码版本仍然可能影响当前进程。但这属于运行时动态加载机制并不是修改.c文件本身直接影响了程序。3、Python当前文件通常已经编译但后续import仍可能读取新源码3.1 Python并不是执行一行才从磁盘读取一行Python经常被称为解释型语言因此容易产生一种误解即Python解释器会执行一行源码再从.py文件中读取下一行源码。对于常见的CPython实现来说实际情况并不是这样。考虑下面的程序import time print(start) time.sleep(10) print(hello)执行python test.py程序输出start后等待10秒。如果此时把print(hello)修改成print(world)当前程序通常仍然会输出hello。原因在于CPython执行一个Python代码单元时会先对源码进行词法和语法分析然后将其编译成code object其中包含供Python虚拟机执行的字节码。随后解释器执行的是已经存在于当前进程中的code object而不是每遇到一条语句就重新访问磁盘上的.py文件。因此当程序已经进入time.sleep(10)时后面的print(hello)通常早已完成编译。修改磁盘上的test.py不会修改当前进程中已经存在的code object所以不会影响此次运行。这也是为什么不能简单地认为“解释型语言修改源码会立即生效”。Python虽然通常不需要用户显式执行独立的编译命令但它依然存在源码到字节码的编译过程只不过这个过程通常由解释器自动完成。3.2 尚未执行的import可能受到源码修改影响Python中比较重要的特殊情况是模块导入。假设存在两个文件# main.py import time print(start) time.sleep(10) import module module.hello()# module.py def hello(): print(hello)执行python main.py程序进入10秒等待后如果将module.py修改为def hello(): print(world)那么程序之后第一次执行import module时就可能加载修改后的module.py最终输出world。这里需要注意真正发生变化的并不是已经运行的main.py。main.py本身仍然是在程序启动时完成解析和编译的但是module.py在此之前尚未加载。当程序执行到import module时Python才开始查找并加载该模块因此此时磁盘上module.py的内容可以影响本次导入。所以判断Python源码修改是否会影响程序时需要区分“已经加载的代码”和“尚未加载的模块”。3.3 已经导入的模块不会因为修改.py自动更新如果程序改成import module import time print(start) time.sleep(10) module.hello()那么module在sleep之前就已经完成了导入。此时再修改module.py后面的module.hello()通常仍然会调用原来已经加载到当前进程中的函数。Python会将已经导入的模块记录在sys.modules中因此普通的重复import module通常也不会再次读取并重新执行module.py。如果确实希望在程序运行期间重新读取模块可以显式使用import importlib import module importlib.reload(module)此时才会发生重新加载。因此Python中的规律可以概括为已经编译和加载的Python代码不会因为磁盘上的.py文件发生变化而自动改变但尚未导入的模块或者被显式重新加载的模块可以使用修改后的源码。另外Python生成的.pyc文件也不改变这一结论。.pyc文件本质上是字节码缓存可以减少后续模块加载时重复编译源码的开销但当前程序执行时使用的仍然是已经加载到进程中的code object而不是每执行一条字节码就重新读取磁盘上的.pyc文件。4、Bash修改正在执行的脚本有可能影响后续行为4.1 Bash不会像Python一样预先编译整个脚本Bash是这几种语言中比较特殊的一种。对于普通脚本Bash通常会持续从脚本输入中读取内容对读取到的Shell语法进行解析然后执行相应的命令而不是像Python那样先将整个当前模块编译成一个完整的code object。例如#!/bin/bash echo start sleep 10 echo hello执行bash test.sh程序输出start并进入sleep 10后如果修改脚本后面的echo hello为echo world当前正在运行的Bash脚本有可能受到影响。原因在于如果这一部分脚本内容在修改之前尚未被Bash从脚本文件中读取那么Bash后续继续读取脚本时就可能读取到已经发生变化的内容。这也是Bash和Python在这个问题上的重要区别。Python当前代码单元通常在执行前已经完成编译而Bash的脚本输入可能在执行过程中继续被读取。不过这里不能进一步简化成“Bash严格执行一行才读取一行”。Bash内部存在输入缓冲和语法解析过程一个完整的Shell语法结构也可能需要预先读取多行内容。例如if、for、while、函数定义以及命令替换等结构都不能简单地按照单个物理行理解。因此修改脚本中的某一行并不能保证Bash一定会读取修改后的版本。4.2 结果还与编辑器的文件保存方式有关Bash运行过程中修改脚本还有一个容易被忽略的问题编辑器保存文件的具体实现方式。一种保存方式是在原有文件上直接修改内容。在这种情况下Bash已经打开的文件描述符仍然指向同一个文件如果后面的内容尚未读取之后读取时就可能观察到变化。另一种常见方式是编辑器先创建一个临时文件将修改后的完整内容写入临时文件然后通过重命名操作替换原来的脚本文件。从目录角度来看test.sh已经变成了新文件但是Bash之前打开的文件描述符仍然可能指向旧文件。这样一来终端中正在运行的Bash可能继续读取旧文件而编辑器中看到的却已经是新文件。除此之外修改脚本长度还可能改变文件偏移等行为因此“运行过程中修改Bash脚本”并不是一种可靠的程序控制方法。它更适合作为理解Shell输入模型的实验而不应该成为实际程序设计的一部分。相对而言下面这种情况更加明确echo start sleep 10 source module.sh如果module.sh在sleep期间被修改那么执行到source module.sh时Bash才会打开并读取该文件所以一般会使用修改后的内容。因此Bash的核心特点是当前脚本本身可能在运行过程中继续被读取因此修改尚未读取的部分存在影响当前执行的可能而通过source等方式之后才读取的文件则更加明确地会受到修改影响。5、Tcl已经读取的脚本不会变化后续source可以读取新内容5.1 修改当前.tcl文件通常不会影响已经加载的脚本Tcl虽然也是解释执行的脚本语言但在“运行时修改当前源文件”这一点上不能简单地按照 Bash 来理解。例如puts start after 10000 puts hello执行tclsh test.tcl程序输出start后等待10秒。如果这时把puts hello修改为puts world通常不能指望当前Tcl程序随后执行新的内容。Tcl解释器在执行脚本时需要先获得相应的脚本文本然后对其中的Tcl命令进行解析和求值。对于已经读取并进入当前解释器求值过程的脚本而言后续修改磁盘上的.tcl文件不会自动替换解释器当前正在处理的脚本内容。因此从实际效果来看Tcl在这个问题上更接近Python而不是Bash修改已经加载的当前脚本一般不会直接影响此次执行。5.2 source是Tcl中最典型的运行时重新读取源码方式Tcl程序中经常使用source module.tcl加载其他 Tcl 文件。例如puts start after 10000 source module.tcl假设原来的module.tcl是puts hello在等待期间改成puts world程序执行到source module.tcl时会读取当时磁盘上的module.tcl因此修改后的代码可以生效。对于过程定义也是同样的道理。假设source module.tcl after 10000 hello而module.tcl中包含proc hello {} { puts hello }执行完第一次source module.tcl后hello过程已经在当前Tcl解释器中完成定义。此时即使把磁盘文件改成proc hello {} { puts world }当前解释器中已经存在的hello并不会自动发生变化因此之后调用hello时仍然会执行原来的过程体。如果再次执行source module.tcl新的proc hello会重新定义同名过程此后的调用才会使用修改后的版本。所以Tcl中需要特别区分“磁盘上的.tcl文件”和“当前解释器中已经通过source、proc等方式建立起来的命令和过程”。修改文件本身并不能修改已经存在于解释器中的过程定义只有重新执行包含这些定义的脚本变化才会进入当前运行环境。6、总结真正需要判断的是源码是否还会被重新读取通过比较C、Python、Bash和Tcl可以发现“修改源代码是否会影响正在运行的程序”实际上并不是一个简单的语言分类问题。C语言在程序运行之前已经完成编译和链接当前进程执行的是机器指令因此修改.c文件基本不会影响已经运行的程序。Python虽然通常被称为解释型语言但CPython会先把当前代码编译为code object和字节码因此修改已经加载的.py文件通常同样不会改变当前程序不过如果某个模块尚未执行import那么程序之后第一次导入它时可能读取修改后的源码。Bash的情况不同它可能在脚本执行期间继续读取脚本输入因此修改尚未读取的部分存在影响当前执行的可能。不过这种行为受到输入缓冲、语法结构、文件偏移以及编辑器保存方式等多种因素影响不能作为可靠机制。Tcl则更接近Python已经进入当前解释器求值过程的脚本不会因为磁盘文件变化而自动改变但是之后执行source时可以重新读取修改后的.tcl文件。
返回列表