思考
- 将CPU,内存等监控到的信息直接放到ftpServer生成的log中去
- 尝试将函数内存使用打印出来
- 想想怎么把事件模型对应到代码中去,如感受器等,。。
- 如何限制资源
实验方法与记录
raw experiment
Python代码占用内存的情况的实验
利用两种方法采集内存变化信息并 > log 重定向到日志中
第一次测试
发现并没有记录内存日志
第二次测试
- 写了一个简单的test.py
1 | from guppy import * |
输出日志信息有
可以看出以上日志信息包含的是代码中各种数据类型在系统中的内存使用情况,跟我们需要的数据有所出入。故再做memory_profiler实验
第三次测试
- 在函数前添加装饰器,将test.py代码修改如下
1 | from memory_profiler import profile |
- 执行完成后,我们生成的memoryTest.log中包含
ftpServer的改造工作
想法
- 经调试ftpServer发现在调用pyftpdlib中各个模块的函数时,有
servers.py
,__init__.py
比较频繁的被调用到,故目标时对这几项进行适当的改造 - 初步只对某一两个函数的内存使用进行输出
对servers.py的改造
- 首先解决运行问题,如果在自己的项目中添加同名py代码后import它,要将原pyftpdlib包中servers.py的import部分也做相应的修改。将ioloop和log部分都添加
pyftpdlib.
的前缀,否则无法运行 - 在servers.py中某些函数前加上修饰器
- 由于在调试时发现,
server_forever
会必经self.handler
,因此过去看看,其在servers.py
的__init__
函数中,所以添加对__init__
内存情况的监控
1 |
- 但是以上操作并未保存日志
- 再次调试,发现并不能单步进入
__init__
所以理论上确实不会产生日志 - 给ftpServer.py中的
server.serve_forever()
设断点,然后在控制台ftp指令连接本ftp站点。 - 开始Debug,先step into再step over,发现在servers.py中的248行左右会
self.ioloop.loop
进入ioloop.py
,这个时候会成功进入交互,提示输入用户名,所以下一步将对pyftpdlib.ioloop
进行改造尝试的工作
暂停改造servers,转至ioloop
对ioloop的改造
- 某个函数必然需要使用过才能使用内存
- 在ioloop.py中
loop
函数前加装饰器@profile(precision=4, stream=open('loopMemoryRecord.log', 'w+'))
- 开始servers.py中有个库没有改成本项目已修改的ioloop导致还是指向原来的ioloop因此没有成效,修改后依然不行
在server.server_forever()
加断点
发现问题
我用一个
while 1
的简单的加1函数对memory_profiler进行测试,发现,对这种持续执行的函数,无法记录其内存分配!为了解决问题,我对代码进行再一次的调试工作,同样的在ioloop中要求输入用户名,这次我进行了更深入的试验,我在调试环境下进行ftp的put操作上传文件。发现会转到
ioloop.Select.poll()
函数,所以在该函数的上方加一个装饰器@profile()
,这个时候终于在命令行打印内存使用信息了。
尝试将其写入log文件中
@profile(precision=10, stream=open("ioloopSelectPollMemory.log", "w+"))
也是可行的
限制资源的实验
2020.11.04实验
实验目标
- 搞清楚FTP服务运作过程中,各项动作的关联(代码中)
实验过程
函数调用图
- 为方便在代码运行过程中更好的了解流程,需要对文件上传和下载速度进行限制
- 在
serve_forever()
下断点,调试发现,在客户机ftp 121.248.52.75
连接服务器时,前期一直在做日志相关的工作。现阶段并不将日志记录工作纳入研究范围。一直到servers.py中跑到在窗口打印“start server on … pid=…”。准备进入ioloop,跑到self.ioloop.loop(timeout, blocking)
然后在客户机输入账号密码即可连接服务器,并传输文件。 - 那么问题来了:应该在ioloop中怎样下断点才可以控制文件流的传输?
- 为进一步简化问题,我将ftpServer.py中前半部分记录日志的代码删除,被删除的代码可以在gitee的infosecLab/FTPExperiment项目查看
- 将
serve_forever()
断点取消,在servers.py中self.ioloop.loop(timeout, blocking)
处下断点,再调试一次。 - 当到断点时直接单步进入函数看看发生了什么
- 有没有现成的方法可以将python代码的执行过程导出呢?
- 找到了现成的工具graphviz和pycallgraph,安装和使用方法参考1和参考2,这里遇到一个小问题,因为ftp项目代码涉及各种端口,因此会遇到权限问题,只有在root用户时才能正常运作。以往的实验中,由于未使用特殊工具所以直接
sudo su
转root权限即可,但是,这次实验中,用到了pycallgraph,需要在虚拟环境中直接使用这个指令。因此还需使root用户也可以使用infosec用户的anaconda环境。配置方法参考 - 接下来只要先
sudo su
进入root权限,再激活infosecEnv虚拟环境(source activate infosecEnv
),到ftpProjects项目目录下执行pycallgraph graphviz -- ./ftpServer.py
即可启动ftp服务
- 然后在客户机
ftp 121.248.52.75
登录账号传输文件即可,此次实验put也即上传了note.txt和一个py文件,完成后直接bye
然后在服务端ctrl+c终止ftp服务
- 在同级目录生成了一个调用图
- 可以看出这个调用图很复杂,为简化工作,想将搜索深度调整为1,然后启动ftp服务上传note.txt,具体方法参考
pycallgraph --max-depth 1 graphviz -- ./ftpServer.py
随后得到调用图有
这又太简单了,所以再上调深度为2 pycallgraph --max-depth 2 graphviz -- ./ftpServer.py
- 深度3
pycallgraph --max-depth 3 graphviz -- ./ftpServer.py
- 深度的实验先做到这里,展开下一步实验:对上传文件时涉及的各函数搞清楚其动作意义
2020.11.05实验
- 寻找深度3时,“调用图”中某个函数的方法,比如需要查找
find_and_load
函数,则在我们创建的Anaconda的虚拟环境infisecEnv的目录下grep -r "find_and_load" *
即可查看所有包含“find_and_load”字符串所在的文件及位置
- python中的importlib的作用 参考,所以其包含的_bootstrap.py并不在我们的考虑范围内,那么这个调用函数的分支暂不考虑
grep -r "handle_fromlist" *
发现也在_bootstrap.py故同样暂不考虑
servers
servers.FTPServer.serve_forever
其实就不用再搜索了,显然是我们改到本项目的servers.py下的FTPServer类的serve_forever函数。查看servers.py的文件开头的注释,说到这个模块主要包含FTPServer类,其侦听host:port并将传入的连接分配到handler,handler是无法阻止的,除非将整个服务挂起。另外,有两个FTPServer的子类使用多线程或多进程更改异步并发模型。- 进一步走到
serve_forever
函数,该函数执行时即开启FTP服务 servers.FTPServer.__init__
用于初始化,创建一个侦听地址的socket以将连接分配给一个handler
else01 在初始化时,还调用了bind_af_unspecified
函数,该函数位于ioloop
else02 还调用了ioloop的listen
函数监听IO事件
else03 还调用了ioloop的__init__
函数
servers.FTPServer.address
用于返回监听的host:port,从程序启动后的打印信息得
servers.FTPServer._map_len
返回socket_map长度servers.FTPServer.close_all
停止服务并断连所有用户servers.FTPServer._log_start
用于记录服务器日志
ioloop
- 同样的ioloop我们也是改到了本地项目。看看
ioloop.Epoll.loop
中这个Epoll
类,其继承自_BasePollEpoll
,而该类中并未定义loop
函数。同时_BasePollEpoll
又继承自_IOLoop
(基类)
- 在IOLoop中找到了
loop
函数:开始异步IO bind_af_unspecified
和bind()
类似,但可从地址族中猜测地址,返回刚确定的地址族。listen
函数监听IO事件__init__
函数IOLoop的初始化
从以上看出,上传文件的关键操作其实在深度3的实验中并未体现出来,故再做深度4的实验
深度4的实验
pycallgraph --max-depth 4 graphviz -- ./ftpServer.py
- 可以发现,loop又进一步调用了一些函数,其中包括调用10次
ioloop._Scheduler.poll
和11次ioloop.Epoll.poll
- 查看
ioloop._Scheduler.poll
,运行计划中的功能并返回下一功能的超时时间(如果没有就pass) ioloop.Epoll.poll
,Epoll没有poll所以还是看其父类_BasePollEpoll的poll,这样还是不够清晰,继续深度5看看
深度5的实验
pycallgraph --max-depth 5 graphviz -- ./ftpServer.py
ioloop._Scheduler.poll
进一步调用了ioloop._CallLater.call
(调用调度函数)和ioloop._CallLater.__lt__
(与其他超时时间作比较)
深度6的实验
pycallgraph --max-depth 6 graphviz -- ./ftpServer.py
深度7的实验
pycallgraph --max-depth 7 graphviz -- ./ftpServer.py
深度8
问题
- 好像找不到“文件上传”这个动作对应的函数。再去Github看看pyftpdlib的源码
- 有没有可能时上传的文件note.txt太小导致没有检测到调用过程呢?试试较大的文件。用此前F2FS的1G镜像文件做上传测试
说明跟文件大小无关 - 利用pycall先到这里,再手动调试看看。
再一次手动调试
- 先将断点放ftpServer.py的
server.serve_forever()
处,然后再调整至servers.py的self.ioloop.loop(timeout, blocking)
处。在客户机连接服务器,然后F7单步进入ioloop.py中的_IOLoop类的loop
函数,然后F8直接step out看执行流程。研究发现当客户机ftp 121.248.52.75
请求连接服务后,每条与服务端的交互指令的执行都会循环检查有无socketmap
这段代码在前面提到的loop
函数中 - 这次step into其中的poll看看(从前面深度4的实验中我们得知这里_BasePollEpoll的
poll
被调了11次?) - 在调试过程中,如果客户机ftp命令行中不再输入新的交互指令,那服务端step out到
while socket_map
检测不到,也就不会再执行下一步。除非在客户机输入新指令 - 研究发现,上传
note.txt
:
在第二次进入poll
时,建立数据连接。
在第三次进入时,开始传输数据。
第五次进入后,完成传输。
- 第二次
poll
时step into看看发生了什么。
本次poll
的第一次for循环中,它就在执行了_write(inst)
后建立了数据连接 - 所以在
_write(inst)
中究竟发生了什么呢,重新发起上传请求,到第二次poll
执行到这个函数的时候再step into看看 - 发现这个
_write
在ioloop.py开头有写_write = asyncore.write
也即asyncore.py中的write
函数 - 下一步,看看
write
函数。
会调用asyncore.py的handle_write_event()
,跟进该函数,同样仍在asyncore.py模块中,经过判断会调用handle_connect_event()
- 有进一步调用
handle_connect()
执行后,会发现客户机完成了数据连接的建立
实验阶段性小结: 由以上可知,handle_connect()
标志连接的建立。但这里可能还是存在一些问题,我认为,这里所做的操作只不过是在完成数据连接建立的基础上,给用户的反馈信息。而不是正儿八经的创建过程。先留这里吧。。
- 在
handle_connect()
上方加装饰器看看,能能导出内存信息?
当然这么做之前,本意是将该函数所在的handlers.py文件创建在本地。但实验发现,想这么做会面临很多意料之外的问题,故将装饰器放在源文件也即pyftpdlib包下的handlers.py文件中。因此在定义handle_connect()
之前加上以下@profile(precision=4, stream=open("/home/infosec/ftpProjects/handle_connect_memory.log", "w+"))
- ftp连接服务器,然后登陆uesr账户,向服务器上传notex.txt,传输完成后,一旦终止程序即可采集到建立数据连接占用的内存信息
- 至此,我们可以采集数据连接建立相关函数占用的内存,下一步,看看文件上传开始,所需要的内存
- 与数据连接建立的调试过程同理,可梳理出流程有
ioloop.pypoll
中的_read
,然后到asyncore.py中的read
函数,再到handlers.py的handle_read
函数。故在handle_read
函数定义前加上装饰器@profile(precision=4, stream=open("/home/infosec/ftpProjects/handle_read_memory.log", "w+"))
重新做上传,读日志加上此前设置的连接日志,但这两个日志没有记录到有哪一行新增消耗,全是0
- 经调试发现,
get
从服务器下载数据,也仍是poll
不断循环读写完成的。 - 在ioloop的poll前加装饰器。同时又猜测前面两个日志全0消耗的原因可能是精度不够,所以这里将精度统一改为10
@profile(precision=10, stream=open("/home/infosec/ftpProjects/poll_memory.log", "w+"))
此时应有三个日志,但只有poll中显示有内存信息的变化,其他两个仍和前面的实验结果类似