我如何确保Python”的;“零”;当内存被垃圾收集时,它会被丢弃吗?

我如何确保Python”的;“零”;当内存被垃圾收集时,它会被丢弃吗?,python,memory,memory-pool,Python,Memory,Memory Pool,我在Python3.2中遇到了一些与字节相关的内存管理问题。在某些情况下,ob_sval缓冲区似乎包含我无法解释的内存 对于特定的安全应用程序,我需要能够确保内存“归零”,并在不再使用后尽快返回操作系统。由于重新编译Python实际上不是一个选项,因此我正在编写一个模块,可用于: 通过将PyMem\u Malloc替换为PyMem\u Malloc,PyObject\u realoc替换为PyMem\u realoc,PyObject\u Free替换为PyMem\u Free,禁用内存池(例

我在Python3.2中遇到了一些与
字节相关的内存管理问题。在某些情况下,
ob_sval
缓冲区似乎包含我无法解释的内存

对于特定的安全应用程序,我需要能够确保内存“归零”,并在不再使用后尽快返回操作系统。由于重新编译Python实际上不是一个选项,因此我正在编写一个模块,可用于:

  • 通过将
    PyMem\u Malloc
    替换为
    PyMem\u Malloc
    PyObject\u realoc
    替换为
    PyMem\u realoc
    PyObject\u Free
    替换为
    PyMem\u Free
    ,禁用内存池(例如:如果不使用
    PyMem\u-Malloc
    编译,会得到什么)。我真的不在乎内存是否集中,但这似乎是最简单的方法
  • 包装
    malloc
    realloc
    free
    ,以便跟踪请求的内存量,并在释放内存时将其设置为
    0
粗略地看,这种方法似乎很有效:

>>> from ctypes import string_at
>>> from sys import getsizeof
>>> from binascii import hexlify
>>> a = b"Hello, World!"; addr = id(a); size = getsizeof(a)
>>> print(string_at(addr, size))
b'\x01\x00\x00\x00\xd4j\xb2x\r\x00\x00\x00<J\xf6\x0eHello, World!\x00'
>>> del a
>>> print(string_at(addr, size))
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x13\x00'
最后三个字节,
ous
,保存了下来

所以,我的问题是:

bytes
对象的剩余字节是怎么回事,为什么不在调用
del
时删除它们

我猜我的方法缺少类似于
realloc
的东西,但我看不出
bytesobject.c
中会有什么

我试图量化垃圾收集后剩余的“剩余”字节数,这在某种程度上是可以预测的

from collections import defaultdict
from ctypes import string_at
import gc
import os
from sys import getsizeof

def get_random_bytes(length=16):
    return os.urandom(length)

def test_different_bytes_lengths():
    rc = defaultdict(list)
    for ii in range(1, 101):
        while True:
            value = get_random_bytes(ii)
            if b'\x00' not in value:
                break
        check = [b for b in value]
        addr = id(value)
        size = getsizeof(value)
        del value
        gc.collect()
        garbage = string_at(addr, size)[16:-1]
        for jj in range(ii, 0, -1):
            if garbage.endswith(bytes(bytearray(check[-jj:]))):
                # for bytes of length ii, tail of length jj found
                rc[jj].append(ii)
                break
    return {k: len(v) for k, v in rc.items()}, dict(rc)

# The runs all look something like this (there is some variation):
# ({1: 2, 2: 2, 3: 81}, {1: [1, 13], 2: [2, 14], 3: [3, 4, 5, 6, 7, 8, 9, 10, 11, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 83, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100]})
# That is:
#  - One byte left over twice (always when the original bytes object was of lengths 1 or 13, the first is likely because of the internal 'characters' list kept by Python)
#  - Two bytes left over twice (always when the original bytes object was of lengths 2 or 14)
#  - Three bytes left over in most other cases (the exact ones varies between runs but never has '12' in it)
# For added fun, if I replace the get_random_bytes call with one that returns an encoded string or random alphanumerics then results change slightly: lengths of 13 and 14 are now fully cleared too. My original test string was 13 bytes of encoded alphanumerics, of course!
编辑1

我最初表示担心的是,如果在函数中使用
bytes
对象,它根本不会被清除:

>>> def hello_forever():
...     a = b"Hello, World!"; addr = id(a); size = getsizeof(a)
...     print(string_at(addr, size))
...     del a
...     print(string_at(addr, size))
...     gc.collect()
...     print(string_at(addr, size))
...     return addr, size
...
>>> addr, size = hello_forever()
b'\x02\x00\x00\x00\xd4J0x\r\x00\x00\x00<J\xf6\x0eHello, World!\x00'
b'\x01\x00\x00\x00\xd4J0x\r\x00\x00\x00<J\xf6\x0eHello, World!\x00'
b'\x01\x00\x00\x00\xd4J0x\r\x00\x00\x00<J\xf6\x0eHello, World!\x00'
>>> print(string_at(addr, size))
b'\x01\x00\x00\x00\xd4J0x\r\x00\x00\x00<J\xf6\x0eHello, World!\x00'
>>定义hello_forever():
...     a=b“你好,世界!”;addr=id(a);大小=getsizeof(a)
...     打印(字符串(地址、大小))
...     德拉
...     打印(字符串(地址、大小))
...     gc.collect()
...     打印(字符串(地址、大小))
...     返回地址、大小
...
>>>addr,size=hello\u forever()

b'\x02\x00\x00\x00\x00\xd4J0x\r\x00\x00\x00一般来说,您不能保证内存会及时归零,甚至垃圾回收。有一些启发法,但如果你担心安全到这种程度,这可能是不够的

相反,您可以直接处理可变类型,例如
bytearray
,并显式地将每个元素归零:

# Allocate (hopefully without copies)
bytestring = bytearray()
unbuffered_file.readinto(bytestring)

# Do stuff
function(bytestring)

# Zero memory
for i in range(len(bytestring)):
    bytestring[i] = 0
安全地使用它将要求您只使用您知道不会制作临时副本的方法,这可能意味着您自己的方法。不过,这并不能防止某些缓存把事情搞砸


在另一个问题中:使用一个子进程来完成这项工作,并在完成后立即将其杀死。

结果是,在我自己的代码中,这个问题是一个绝对愚蠢的错误,它导致了
memset
。在“接受”这个答案之前,我将联系@Calyth,他慷慨地为这个问题增加了一笔赏金

简而言之,简化后的
malloc
/
free
包装函数的工作方式如下:

  • 代码调用malloc请求N字节的内存。
    • 包装器调用实函数,但要求
      N+sizeof(size\u t)
      字节
    • 它将
      N
      写入范围的开头并返回偏移指针
  • 代码使用偏移指针,忽略了它附加到比请求的内存块稍大一点的事实
  • 代码调用free请求返回内存并传入偏移指针。
    • 包装器在偏移指针之前查找,以获取最初请求的内存大小
    • 它调用
      memset
      ,以确保所有内容都设置为零(编译库时未进行优化,以防止编译器忽略
      memset
    • 只有这样,它才调用真正的函数
我的错误是调用了等价的
memset(实际指针,0,请求的大小)
,而不是
memset(实际指针,0,实际大小)

我现在面临着一个令人难以置信的问题:为什么不总是有“3”剩余字节(我的单元测试验证了我随机生成的字节对象中没有一个包含任何空值),为什么字符串也不会有这个问题(Python可能会过度分配字符串缓冲区的大小)。然而,这些都是未来的问题


所有这些的结果是,在垃圾收集后,确保字节和字符串设置为零相对容易!(关于硬编码字符串、免费列表等有很多警告,因此任何其他尝试这样做的人都应该仔细阅读原始问题、问题的注释和这个“答案”)

Python可能正在插入字符串。Python肯定在这里插入字符串,它们被保存在函数的常量列表中--
hello\u forever.\uuu code\uu.co\u consts
。您是否考虑过将
\u Py\u Dealloc
Py\u DECREF
宏更改为释放后的内存为零?与在内存分配上乱搞相反。@Dunes:我不熟悉自动实习;我会再看一眼那些宏,看看是否能让它们工作。乍一看,它看起来不太有希望,因为我之前的注释表明Py_DECREF->Py_Dealloc->tp_Dealloc->object_Dealloc->tp_free->PyObject_free->PyMem_free->free(例如:如果Py_DECREF在哪里调用,那么内存应该归零)。我很可能错过了一些东西,但我有点错过了你说的不能重新编译的一点。此外,即使是相同的对象
# Allocate (hopefully without copies)
bytestring = bytearray()
unbuffered_file.readinto(bytestring)

# Do stuff
function(bytestring)

# Zero memory
for i in range(len(bytestring)):
    bytestring[i] = 0