一个有趣的现象

最近在翻 Python 的 Tutorial 的对象一章,随手在 Python 的交互 Shell 中敲了几段代码测试一下,发现了一个有趣的情况。代码如下:

>>> class TestCls(object):
...     
...     def say_hi(self):
...         print 'Hi!'
... 
>>> t = TestCls()
>>> t.say_hi()
Hi!
>>> t.ins_new_var = 101
>>> t.ins_new_var
101
>>> TestCls.ins_new_var
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'TestCls' has no attribute 'ins_new_var'
>>> TestCls.new_var = 100
>>> TestCls.new_var
100
>>> t.new_var
100

这段代码中,定义了一个类 TestCls ,然后实例化了一个 TestCls 的对象 t。在 Python 中,一切皆对象,这是老生长谈了。而 Python 中的对象还有另外一个特性,就是可以在创建之后修改这个对象的属性和方法。如上所示,我们可以在创建了一个类对象 TestCls 和一个实例对象 t 之后,修改这两个对象,给它们分别添加了 new_varins_new_var 属性。从上面的运行结果可以看到,当我们给实例对象 t 添加属性 ins_new_var 之后,类对象 TestCls 中访问不了这个属性,但是对于类对象 TestCls 添加的新属性 new_var ,这个类对象的实例 t 却可以访问到。

从 Python 代码的这个表现,我们可以推测出一些事情。那就是 Python 中,对一个对象的属性的访问会首先在这个对象的命名空间搜索,如果找不到,那就去搜索这个对象的类的命名空间,直到找到,然后取值,或者抛出没有这个属性的异常。很明显, Python 中一个对象的实例,同时还共享了这个对象的命名空间。如下:

>>> dir(t)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__',
'__getattribute__', '__hash__', '__init__', '__module__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__', '__weakref__', 'ins_new_var', 'new_var',
'say_hi']
>>> dir(TestCls)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__',
'__getattribute__', '__hash__', '__init__', '__module__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__', '__weakref__', 'new_var', 'say_hi']
>>> 

可以看到,dir 函数搜索到的实例对象 t 和类对象 TestCls 的基本一致,但是区别在于 t 比 TestCls 多了一个 ins_new_var

>>> t.__dict__
{'ins_new_var': 101}
>>> TestCls.__dict__
dict_proxy({'__module__': '__main__', 'say_hi': <function say_hi at
0xb771e95c>, '__dict__': <attribute '__dict__' of 'TestCls' objects>,
'__weakref__': <attribute '__weakref__' of 'TestCls' objects>, '__doc__':
None, 'new_var': 100})
>>> t.new_var = 100
>>> t.__dict__
{'ins_new_var': 101, 'new_var': 100}
>>> 

从这里看到,当我们试图对 t.new_var 进行赋值时,t 的 __dict__ 增加了一个 new_var

上面的推测是否正确?也许直接去查看源码会得到答案。在本文中, Python 的源码均指 CPython 源码,版本为 2.7.4。

注1:一般是代码片段在上,分析在下。

数据结构

CPython 是 C 写的(很明显),类对象和实例对象的数据结构都是 struct,定义在 CPython 源码目录的 Include/classobject.h 中:

typedef struct {
    PyObject_HEAD
    PyObject    *cl_bases;  /* A tuple of class objects */
    PyObject    *cl_dict;   /* A dictionary */
    PyObject    *cl_name;   /* A string */
    /* The following three are functions or NULL */
    PyObject    *cl_getattr;
    PyObject    *cl_setattr;
    PyObject    *cl_delattr;
    PyObject    *cl_weakreflist; /* List of weak references */
} PyClassObject;

typedef struct {
    PyObject_HEAD
    PyClassObject *in_class;    /* The class object */
    PyObject      *in_dict; /* A dictionary */
    PyObject      *in_weakreflist; /* List of weak references */
} PyInstanceObject;

这两个结构体并不复杂,除了所有 Python 对象都有的 PyObject_HEAD 宏之外,类对象 PyClassObject 中还有几个属性,分别是: cl_bases ,保存了这个类对象的所有父类(如果有的话),这个属性是一个元组;cl_dict ,一个字典,保存的是属于这个类对象的属性和方法;cl_name ,保存的是这个类对象的名称,此外还有几个对象 cl_getattr, cl_setattr, cl_delattr ,。而实例对象则有 in_class 表示从哪个类对象实例化而来,还有 in_dict 同样是一个字典对象,保存了这个实例对象的属性和方法。可以看到,一个类的实例对象保存了这个实例对象实例化自哪个类对象。

PyObject_HEAD 的相关定义如下:

 /* Define pointers to support a doubly-linked list of all live heap objects. */
#define _PyObject_HEAD_EXTRA            \
struct _object *_ob_next;           \
struct _object *_ob_prev;

/* PyObject_HEAD defines the initial segment of every PyObject. */
#define PyObject_HEAD                   \
_PyObject_HEAD_EXTRA                \
Py_ssize_t ob_refcnt;               \
struct _typeobject *ob_type;

可以看到这两个宏定义了 Python 中一个对象的常见属性,包括对象类型 ob_type 和对象的引用计数 ob_refcnt,这是因为 Python 的 GC 方式是引用计数。

创建函数

在 Python 中对于类对象 (PyClassObject) 和实例对象 (PyInstanceObject) 的相关函数有很多,在这里我们只是简单分析下创建类对象及实例对象的函数和关于查找属性部分的函数。

注2:这里对这几个函数的代码引用不是完全的。

实例对象的创建函数

首先是创建类对象的函数:

static PyObject *
class_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{

创建类对象的函数是 class_new ,参数是类型 type,还有多个参数元组对象 args 和多个关键字参数字典对象 kwds。

    PyObject *name, *bases, *dict;
    static char *kwlist[] = {"name", "bases", "dict", 0};

这里新建了几个 PyObject 类型的指针,分别是 name, bases 和 dict ,分别用来保存类对象的名称,继承的父类和属性方法字典。此外还有一个字符串数组 kwlist。

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "SOO", kwlist,
                                     &name, &bases, &dict))
        return NULL;
    return PyClass_New(bases, dict, name);

然后这里是调用 PyArg_ParseTupleAndKeywords 函数,这个函数的主要效果是解析参数 args 和 kwds ,得到创建新的类对象的参数 bases,dict,name,然后调用真正创建一个类对象的函数 PyClass_New

PyObject *
PyClass_New(PyObject *bases, PyObject *dict, PyObject *name)
     /* bases is NULL or tuple of classobjects! */
{

PyClass_New 函数的有三个参数,分别是父类们 bases,类的属性方法字典 dict 和 类的名称 name。

接下来很长的一段代码都是对参数的解析及检查参数是否合法,比如 name 必须是一个字符串, dict 必须是一个字典等等,在这里略去。

    if (PyDict_GetItem(dict, docstr) == NULL) {
        if (PyDict_SetItem(dict, docstr, Py_None) < 0)
            return NULL;
    }
    if (PyDict_GetItem(dict, modstr) == NULL) {
        PyObject *globals = PyEval_GetGlobals();
        if (globals != NULL) {
            PyObject *modname = PyDict_GetItem(globals, namestr);
            if (modname != NULL) {
                if (PyDict_SetItem(dict, modstr, modname) < 0)
                    return NULL;
            }
        }
    }

检查参数 dict 中是否有 __doc____module__ 这两个键,如果 __doc__ 不存在则设置并将其值设置为 Py_None,如果 __module__ 也不存在则获取当前范围的全局变量,从中取得 __module__ 所对应的值,赋给这个新类对象的 __module__

    if (bases == NULL) {
        bases = PyTuple_New(0);
        if (bases == NULL)
            return NULL;
    }
    else {
        Py_ssize_t i, n;
        PyObject *base;
        if (!PyTuple_Check(bases)) {
            PyErr_SetString(PyExc_TypeError,
                            "PyClass_New: bases must be a tuple");
            return NULL;
        }
        n = PyTuple_Size(bases);
        for (i = 0; i < n; i++) {
            base = PyTuple_GET_ITEM(bases, i);
            if (!PyClass_Check(base)) {
                if (PyCallable_Check(
                    (PyObject *) base->ob_type))
                    return PyObject_CallFunctionObjArgs(
                        (PyObject *) base->ob_type,
                        name, bases, dict, NULL);
                PyErr_SetString(PyExc_TypeError,
                    "PyClass_New: base must be a class");
                return NULL;
            }
        }
        Py_INCREF(bases);
    }

检查 bases 参数是否为空,如果为空则新建一个值为 0 的元组赋给 bases。不为空,则 bases 应该是一个类对象的元组,依次对这个元组中的类对象进行检测,是否为类对象,如果不是类对象,则检测是否可调用 (callable) ,然后返回相应的错误信息或者一个可调用函数对象的执行结果(可调用)。

最后如果 bases 参数合法,这个参数对象的引用计数加一。

    if (getattrstr == NULL) {
        getattrstr = PyString_InternFromString("__getattr__");
        if (getattrstr == NULL)
            goto alloc_error;
        setattrstr = PyString_InternFromString("__setattr__");
        if (setattrstr == NULL)
            goto alloc_error;
        delattrstr = PyString_InternFromString("__delattr__");
        if (delattrstr == NULL)
            goto alloc_error;
    }

getattrstrsetattrstrdelattrstr 是三个全局的 static PyObject 指针变量,上面这一段分别给它们赋值字符串对象。

    op = PyObject_GC_New(PyClassObject, &PyClass_Type);
    if (op == NULL) {
alloc_error:
        Py_DECREF(bases);
        return NULL;
    }

给这个类对象分配内存,这个内存是在堆分配的而且受到 CPython 的 GC 管理的。

    op->cl_bases = bases;
    Py_INCREF(dict);
    op->cl_dict = dict;
    Py_XINCREF(name);
    op->cl_name = name;
    op->cl_weakreflist = NULL;

将三个参数分别赋给这个新建的类对象 op。

    op->cl_getattr = class_lookup(op, getattrstr, &dummy);
    op->cl_setattr = class_lookup(op, setattrstr, &dummy);
    op->cl_delattr = class_lookup(op, delattrstr, &dummy);
    Py_XINCREF(op->cl_getattr);
    Py_XINCREF(op->cl_setattr);
    Py_XINCREF(op->cl_delattr);
    _PyObject_GC_TRACK(op);
    return (PyObject *) op;
}

然后分别设置这个新类对象的 getattr , setattr 和 delattr 函数,增加这几个函数的引用计数等等,最后返回这个新建的类对象的指针。

实例对象的创建函数

实例对象 PyInstanceObject 同样也有个类似的 instance_new 函数:

static PyObject *
instance_new(PyTypeObject* type, PyObject* args, PyObject *kw)
{

参数也和 class_new 类似,三个参数分别为 type , args 和 kw,

    PyObject *klass;
    PyObject *dict = Py_None;

    if (!PyArg_ParseTuple(args, "O!|O:instance",
                          &PyClass_Type, &klass, &dict))
        return NULL;

解析参数,

    if (dict == Py_None)
        dict = NULL;
    else if (!PyDict_Check(dict)) {
        PyErr_SetString(PyExc_TypeError,
              "instance() second arg must be dictionary or None");
        return NULL;
    }

检查 dict 参数的合法性,

    return PyInstance_NewRaw(klass, dict);
}

调用 PyInstance_NewRaw 函数,这个才是返回新实例对象的函数:

PyObject *
PyInstance_NewRaw(PyObject *klass, PyObject *dict)
{
    PyInstanceObject *inst;

参数只有所实例化自的类对象和属性方法字典 dict ,

    if (!PyClass_Check(klass)) {
        PyErr_BadInternalCall();
        return NULL;
    }
    if (dict == NULL) {
        dict = PyDict_New();
        if (dict == NULL)
            return NULL;
    }
    else {
        if (!PyDict_Check(dict)) {
            PyErr_BadInternalCall();
            return NULL;
        }
        Py_INCREF(dict);
    }

检查参数的合法性,如果 dict 为空 (NULL) 则调用 PyDict_New 参数新建一个字典对象赋给 dict,否则检查 dict 是否是一个 CPython 的字典对象,

    inst = PyObject_GC_New(PyInstanceObject, &PyInstance_Type);
    if (inst == NULL) {
        Py_DECREF(dict);
        return NULL;
    }

同样是调用 PyObject_GC_New 函数,给这个新建的实例对象分配内存,PyInstance_Type 是一个全局的 PyTypeObject 类型的变量,

    inst->in_weakreflist = NULL;
    Py_INCREF(klass);
    inst->in_class = (PyClassObject *)klass;
    inst->in_dict = dict;
    _PyObject_GC_TRACK(inst);
    return (PyObject *)inst;
}

最后给新建的实例对象赋值相关属性,然后返回这个新建实例对象的指针。

对于 CPython 的实例对象而言,除了 instance_new 之外,还有另外的一个函数也可以创建一个实例对象:

PyObject *
PyInstance_New(PyObject *klass, PyObject *arg, PyObject *kw)
{
    register PyInstanceObject *inst;
    PyObject *init;
    static PyObject *initstr;

PyInstance_New 函数也有三个参数,除了第一个是 klass 表示类对象之外,另外两个和 instance_new 函数类似,

    if (initstr == NULL) {
        initstr = PyString_InternFromString("__init__");
        if (initstr == NULL)
            return NULL;
    }
    inst = (PyInstanceObject *) PyInstance_NewRaw(klass, NULL);

可以看到在这里调用了 PyInstance_NewRaw 函数创建一个新的实例对象,区别在于 dict 参数为 NULL ,这意味着新建的实例对象没有自己的属性和方法,

    if (inst == NULL)
        return NULL;
    init = instance_getattr2(inst, initstr);
    if (init == NULL) {
        if (PyErr_Occurred()) {
            Py_DECREF(inst);
            return NULL;
        }
        if ((arg != NULL && (!PyTuple_Check(arg) ||
                             PyTuple_Size(arg) != 0))
            || (kw != NULL && (!PyDict_Check(kw) ||
                              PyDict_Size(kw) != 0))) {
            PyErr_SetString(PyExc_TypeError,
                       "this constructor takes no arguments");
            Py_DECREF(inst);
            inst = NULL;
        }
    }

在新建的实例对象中查找初始化函数 init ,如果不存在 (init 为 NULL) 且发生错误,则返回 NULL ,没有错误则检查 arg 和 kw 这两个参数,设置错误字符串,同样将新建实例对象 inst 置为 NULL,

    else {
        PyObject *res = PyEval_CallObjectWithKeywords(init, arg, kw);
        Py_DECREF(init);
        if (res == NULL) {
            Py_DECREF(inst);
            inst = NULL;
        }
        else {
            if (res != Py_None) {
                PyErr_SetString(PyExc_TypeError,
                           "__init__() should return None");
                Py_DECREF(inst);
                inst = NULL;
            }
            Py_DECREF(res);
        }
    }

init 不为空即意味找到了初始化实例的函数,将初始化函数和参数 arg ,kw 作为参数调用,初始化这个实例对象,

    return (PyObject *)inst;
}

最后返回这个新建的实例对象。

查找函数与 getattr, setattr 函数

分析完创建类对象和实例对象的函数之后,我们来分析相关的查找函数,然后还有最重要的 getattr 和 setattr。类对象和实例对象都有自己特有的 getattr 和 setattr 函数,这两类函数正是 Python 中使用 dot 操作符取对象的属性值或者给对象属性赋值所调用的函数。

类对象的查找函数

首先是类对象的查找函数 class_lookup,在类对象的创建函数中也曾调用这个函数:

static PyObject *
class_lookup(PyClassObject *cp, PyObject *name, PyClassObject **pclass)
{

class_lookup 函数有三个参数,分别是类对象指针 cp,查找的属性名称 name 和指向类对象指针的指针变量 pclass,

    Py_ssize_t i, n;
    PyObject *value = PyDict_GetItem(cp->cl_dict, name);
    if (value != NULL) {
        *pclass = cp;
        return value;
    }

首先查找的是类对象 cp 的 cl_dict 字典,如果找到的值 value 不为空,即已经找到了这个属性的值,则将 pclass 所指向的地址为 cp 类对象的地址,然后返回这个 value,

    n = PyTuple_Size(cp->cl_bases);

否则计算类对象 cp 的父类的个数,也就是 cl_bases 元组的大小,

    for (i = 0; i < n; i++) {
        /* XXX What if one of the bases is not a class? */
        PyObject *v = class_lookup(
            (PyClassObject *)
            PyTuple_GetItem(cp->cl_bases, i), name, pclass);
        if (v != NULL)
            return v;
    }

对 cp 的所有父类递归调用 class_lookup 函数,直到找到这个 name 属性的值,返回到 v 变量,如果 v 非 NULL 则返回 v,

    return NULL;
}

否则返回 NULL ,表示查找不到这个 name 属性的值。

类对象的 getattr 函数

类对象的 getattr 函数实际上调用了 class_lookup函数,如下:

static PyObject *
class_getattr(register PyClassObject *op, PyObject *name)
{
    register PyObject *v;
    register char *sname;
    PyClassObject *klass;
    descrgetfunc f;

有两个参数,分别为类对象指针 op 和 所要获取的属性名称 name,

    if (!PyString_Check(name)) {
        PyErr_SetString(PyExc_TypeError, "attribute name must be a string");
        return NULL;
    }

首先也是检查参数的合法性,确定 name 为 PyString 对象,以防错误,

    sname = PyString_AsString(name);
    if (sname[0] == '_' && sname[1] == '_') {
        if (strcmp(sname, "__dict__") == 0) {
            if (PyEval_GetRestricted()) {
                PyErr_SetString(PyExc_RuntimeError,
               "class.__dict__ not accessible in restricted mode");
                return NULL;
            }
            Py_INCREF(op->cl_dict);
            return op->cl_dict;
        }
        if (strcmp(sname, "__bases__") == 0) {
            Py_INCREF(op->cl_bases);
            return op->cl_bases;
        }
        if (strcmp(sname, "__name__") == 0) {
            if (op->cl_name == NULL)
                v = Py_None;
            else
                v = op->cl_name;
            Py_INCREF(v);
            return v;
        }
    }

这一段首先是检查要获取的是否为特殊属性 __dict__, __bases____name__,如果是则返回这个类对象的那个特殊属性。之所以作这样的检查是因为接下来就要执行 class_lookup 函数查找,从上面的分析可以知道, class_lookup 函数还会查找其父类,而这些特殊属性每个类对象都有的,所以先做检查可以防止返回错误的属性值,

    v = class_lookup(op, name, &klass);
    if (v == NULL) {
        PyErr_Format(PyExc_AttributeError,
                     "class %.50s has no attribute '%.400s'",
                     PyString_AS_STRING(op->cl_name), sname);
        return NULL;
    }

通过 class_lookup 函数查找这个值,如果找不到则返回 NULL,

    f = TP_DESCR_GET(v->ob_type);
    if (f == NULL)
        Py_INCREF(v);
    else
        v = f(v, (PyObject *)NULL, (PyObject *)op);

    return v;
}

如果找到则尝试获取这个属性值对象的描述符,如果找到(实现了 __get__ 方法),则调用这个描述符方法,因为是类对象,所以第二个参数为 NULL。最后返回值 v 。

类对象的 setattr 函数

接下来的是类对象的 setattr 函数:

static int
class_setattr(PyClassObject *op, PyObject *name, PyObject *v)
{

class_setattr 函数有三个参数,分别是类对象指针 op,属性名称 name 和属性的值 v,

    char *sname;
    if (PyEval_GetRestricted()) {
        PyErr_SetString(PyExc_RuntimeError,
                   "classes are read-only in restricted mode");
        return -1;
    }

注意到这里首先检查了此时是否处于受限制模式,如果处于受限制模式,此时类对象是只读的,函数将返回错误码 -1。受限模式下,不受信任的代码的执行将会受到限制。

    if (!PyString_Check(name)) {
        PyErr_SetString(PyExc_TypeError, "attribute name must be a string");
        return -1;
    }
    sname = PyString_AsString(name);

然后是同样检查 name 参数是否为一个 PyString 对象,是则根据这个字符串对象返回一个 C 中的字符串,方便下面的比较。

接下来的一大段代码都是检查上面得到的这个 sname 字符串是否为特殊方法或者特殊的属性,比如 __dict__ 或者 __getattr__ 等,如果是则调用相关的函数 set_dict 等,一般来说这些特殊属性是不可以修改的,所以会返回错误提示。

    if (v == NULL) {
        int rv = PyDict_DelItem(op->cl_dict, name);
        if (rv < 0)
            PyErr_Format(PyExc_AttributeError,
                         "class %.50s has no attribute '%.400s'",
                         PyString_AS_STRING(op->cl_name), sname);
        return rv;
    }

参数 v 为空则将这个保存在类对象结构体 cl_dict 成员中的 name 属性删除掉,

    else
        return PyDict_SetItem(op->cl_dict, name, v);
}

否则,给这个属性 name 赋值 v,保存在类对象的 cl_dict 中。PyDict_SetItem 函数将会检测第一个字典参数中是否具有第二个参数 name 这个键,存在则更新其对应的值为 v,不存在则新建一个键,其值也是 v。

实例对象的 getattr 函数

实例对象只有一个简单地搜索属性字典 dict 的函数 _PyInstance_Lookup,这个函数很简单,就是里面做了一点的检查,然后就调用了 PyDict_GetItem 函数从实例对象的 dict 中获取这个值。

而实例对象的 getattr 函数则更多地调用到了class_lookup 函数。CPython 的源码中,关于实例对象的 getattr 和 setattr 函数灰常蛋疼,getattr 函数有三个,分别是 instance_getattrinstance_getattr1instance_getattr2…而 setattr 函数也有两个,分别是 instance_setattr1instance_setattr。如下:

static PyObject *
instance_getattr(register PyInstanceObject *inst, PyObject *name)
{

参数是实例对象指针 inst 和属性名称 name,

    register PyObject *func, *res;
    res = instance_getattr1(inst, name);

其实在这里就调用 instance_getattr1 函数了,参数是一致的,如果 instance_getattr1 函数的返回非 NULL,则直接会返回这个结果,下面一段不会执行,

    if (res == NULL && (func = inst->in_class->cl_getattr) != NULL) {
        PyObject *args;
        if (!PyErr_ExceptionMatches(PyExc_AttributeError))
            return NULL;
        PyErr_Clear();
        args = PyTuple_Pack(2, inst, name);
        if (args == NULL)
            return NULL;
        res = PyEval_CallObject(func, args);
        Py_DECREF(args);
    }

如果 isntance_getattr1 函数的返回值为 NULL 并且实例对象的类的 getattr 函数存在,则调用这个类对像的 getattr 函数,参数是将实例对象指针 inst 和属性名称 name 打包成的元组。

    return res;
}

最后返回结果。

instance_getattr1 函数如下:

static PyObject *
instance_getattr1(register PyInstanceObject *inst, PyObject *name)
{

参数同样是 inst 和 name,

    register PyObject *v;
    register char *sname;

    if (!PyString_Check(name)) {
        PyErr_SetString(PyExc_TypeError, "attribute name must be a string");
        return NULL;
    }

    sname = PyString_AsString(name);

例行检查参数的合法性,合法则将 name 参数转化为 C 的字符串,

    if (sname[0] == '_' && sname[1] == '_') {
        if (strcmp(sname, "__dict__") == 0) {
            if (PyEval_GetRestricted()) {
                PyErr_SetString(PyExc_RuntimeError,
            "instance.__dict__ not accessible in restricted mode");
                return NULL;
            }
            Py_INCREF(inst->in_dict);
            return inst->in_dict;
        }
        if (strcmp(sname, "__class__") == 0) {
            Py_INCREF(inst->in_class);
            return (PyObject *)inst->in_class;
        }
    }

同样是检查是否为特殊的属性,主要是以 __ 作为开头的属性,这里处理的只有 __dict____class__。如果是 __dict__ ,在受限模式下,会抛出错误表明不可以读取,非受限模式下则返回这个实例对象的属性字典 dict。如果是 __class__ ,也是对应地返回实例对象的类。

    v = instance_getattr2(inst, name);
    if (v == NULL && !PyErr_Occurred()) {
        PyErr_Format(PyExc_AttributeError,
                     "%.50s instance has no attribute '%.400s'",
                     PyString_AS_STRING(inst->in_class->cl_name), sname);
    }
    return v;
}

然后调用了 instance_getattr2 函数,如果其返回值为 NULL 则表示不存在这个属性,输出提示,否则返回这个结果 v。

static PyObject *
instance_getattr2(register PyInstanceObject *inst, PyObject *name)
{
    register PyObject *v;
    PyClassObject *klass;
    descrgetfunc f;

同样的, instance_getattr2 函数也是有两个参数 inst 和 name,

    v = PyDict_GetItem(inst->in_dict, name);
    if (v != NULL) {
        Py_INCREF(v);
        return v;
    }

首先在这个实例对象的 in_dict 中查找这个属性,如果找到则直接返回其值,

    v = class_lookup(inst->in_class, name, &klass);

没有找到则去查找这个实例对象的类对象 in_class,通过上面对 class_lookup 函数的分析我们可以知道,这个查找会一直从实例对象所属的类,其类的父类,父类的父类一直搜索,直到搜索完毕。如果找到了,则返回这个属性的值对象。

    if (v != NULL) {
        Py_INCREF(v);
        f = TP_DESCR_GET(v->ob_type);
        if (f != NULL) {
            PyObject *w = f(v, (PyObject *)inst,
                            (PyObject *)(inst->in_class));
            Py_DECREF(v);
            v = w;
        }

在这里同样也试图获取这个实例对象对应类型的描述符方法,

    }
    return v;
}

返回结果 v ,有值或者 NULL。

从对上面三个 getattr 函数的分析可以看到,其实这三个函数各有其功能,比如 instance_getattr1 处理的是特殊属性,而 instance_getattr2 则是对应普通的属性,会一直搜索到其所属的类和其类的父类等等。如果这两个函数都没有结果,则会调用其类的 getattr 函数。

所以这三个函数其实是有其各自的职责的,当然它们三个是可以合并起来成为一个大函数的,但是估计就是不希望看到一个大函数的出现所以才分散为三个函数,这样职责更小更分明。

实例对象的 setattr 函数

static int
instance_setattr(PyInstanceObject *inst, PyObject *name, PyObject *v)
{
    PyObject *func, *args, *res, *tmp;
    char *sname;

instance_setattr 函数有三个参数,毫无疑问分别是实例对象指针 inst ,属性名称 name 和值 v,

    if (!PyString_Check(name)) {
        PyErr_SetString(PyExc_TypeError, "attribute name must be a string");
        return -1;
    }

    sname = PyString_AsString(name);

同样,惯例检查 name 参数的合法性,合法则转化为 C 的字符串类型变量,

    if (sname[0] == '_' && sname[1] == '_') {
        Py_ssize_t n = PyString_Size(name);
        if (sname[n-1] == '_' && sname[n-2] == '_') {

判断是否为特殊属性,

            if (strcmp(sname, "__dict__") == 0) {
                if (PyEval_GetRestricted()) {
                    PyErr_SetString(PyExc_RuntimeError,
                 "__dict__ not accessible in restricted mode");
                    return -1;
                }
                if (v == NULL || !PyDict_Check(v)) {
                    PyErr_SetString(PyExc_TypeError,
                       "__dict__ must be set to a dictionary");
                    return -1;
                }
                tmp = inst->in_dict;
                Py_INCREF(v);
                inst->in_dict = v;
                Py_DECREF(tmp);
                return 0;
            }

__dict__ 则检查是否为受限模式,检查传入的 v 参数是否为合法的 PyDict 对象,如果是则将 v 赋值给实例对象的 in_dict。可以注意到,这里用了一个 tmp 变量来保存实例对象之前的 in_dict 变量,然后将其引用计数减一。

            if (strcmp(sname, "__class__") == 0) {
                if (PyEval_GetRestricted()) {
                    PyErr_SetString(PyExc_RuntimeError,
                "__class__ not accessible in restricted mode");
                    return -1;
                }
                if (v == NULL || !PyClass_Check(v)) {
                    PyErr_SetString(PyExc_TypeError,
                       "__class__ must be set to a class");
                    return -1;
                }
                tmp = (PyObject *)(inst->in_class);
                Py_INCREF(v);
                inst->in_class = (PyClassObject *)v;
                Py_DECREF(tmp);
                return 0;
            }

如果是 __class__ 和上面的操作类似。通过这一段代码,我们可以看到在非受限模式的情况下,一个实例对象的类是可以被动态修改的。

        }
    }
    if (v == NULL)
        func = inst->in_class->cl_delattr;
    else
        func = inst->in_class->cl_setattr;

如果参数 v 为 NULL,则表示要将实例对象的这个属性删除掉,试图去获取实例对象所对应的类对象的 delattr 函数。v 不为 NULL 则获取类对象的 setattr 函数,

    if (func == NULL)
        return instance_setattr1(inst, name, v);

如果没有获取到任何的函数,则将会调用 instance_setattr1 函数。

    if (v == NULL)
        args = PyTuple_Pack(2, inst, name);
    else
        args = PyTuple_Pack(3, inst, name, v);
    if (args == NULL)
        return -1;
    res = PyEval_CallObject(func, args);

无论得到的是类对象的 delattr 还是 setattr 函数,这里将会调用这个函数,区别在于调用 delattr 函数参数元组只有 inst 和 name 而调用 setattr 函数参数则是多了一个参数 v。根据上面对类对象的 setattr 的分析可以知道,如果这个类有 setattr 函数,则将会调用它的 setattr 函数。

    Py_DECREF(args);
    if (res == NULL)
        return -1;
    Py_DECREF(res);
    return 0;
}

执行成功则返回 0。

static int
instance_setattr1(PyInstanceObject *inst, PyObject *name, PyObject *v)
{
    if (v == NULL) {
        int rv = PyDict_DelItem(inst->in_dict, name);
        if (rv < 0)
            PyErr_Format(PyExc_AttributeError,
                         "%.50s instance has no attribute '%.400s'",
                         PyString_AS_STRING(inst->in_class->cl_name),
                         PyString_AS_STRING(name));
        return rv;
    }

如果参数 v 为空,则表示删除这个属性,所以将会调用 PyDict_DelItem 函数将这个属性从实例对象的 dict 字典中删除,

    else
        return PyDict_SetItem(inst->in_dict, name, v);
}

否则就直接调用 PyDict_SetItem 函数更新 dict 中的这个值或者添加进 dict 中。

从上面对这两个 setattr 函数的分析,同样可以知道,这两个函数各自有其职责。instance_setattr 主要是对特殊属性进行处理或者是调用其类对象的 setattr 或者 delattr 函数,而 instance_setattr1 函数则是对这个实例对象的 dict 进行 set_item 或者 del_item 操作。

总结

其实写到后面已经有点头大了,引用了一大堆源码更像是给源码注释了。但是既然已经写了,那就当给源码注释把它给写完了。

虽然是罗嗦了一堆,但是通过这个分析过程,对于文章开头的那几段代码的情况还是很清晰的:

  • 首先,给实例对象 t 添加一个属性 ins_new_var 则将会保存到 t 的 __dict__ 中;
  • 而当试图在类对象 TestCls 中取 ins_new_var 的时候,只会去搜索这个类对象的 dict 和其父类的 dict ,这肯定是找不到的,所以返回属性错误;
  • 当给类对象 TestCls 添加一个属性 new_var 的时候,同样,会在 __dict__ 中添加一个 new_var 对象;
  • 当访问 t.new_var 的时候,在 t 的命名空间中搜索不到 new_var 的时候,就回去搜索其实例化自的类对象的命名空间,所以,就可以得到这个值了。

(Source: pyclassob-pyinstanceob-source-code)

前言

最近在跟一门斯坦福大学的公开课 Programming Paradigms ,网易公开课也有其中文翻译版,翻译已经完成了:斯坦福大学公开课:编程范式

课程内容:

Advanced memory management features of C and C++; the differences between imperative and object-oriented paradigms. The functional paradigm (using LISP) and concurrent programming (using C and C++). Brief survey of other modern languages such as Python, Objective C, and C#.

首先涉及的是 C/C++ 的高级内存管理,内容包括 C 的各种数据类型的内存布局,malloc 和 free 的实现,等等。然后还有命令式和面向对象,函数式编程等等几种不同的编程范式及他们的差别。

可以说这些内容应该是属于比较高级的内容。我之前偶尔也会接触到一些,有些书也会讲到,但是在我所在的大学的课堂上,这些内容基本上是不讲授的。在上 C 课程的时候,甚至连指针都语焉不详,更别提有一门专门的课程来讲述这些高级内容。上这个公开课刚好可以完整地补全我对这方面内容的不足,毕竟一个斯坦福大学的教授给你讲解这些内容总比自己看书效果要好得多。

在这里,我要记下的是在课程中碰到的一些有趣的内容。

malloc 与 free

我们知道,malloc 函数是 C 中动态分配内存的一个函数,通过这个函数可以在堆中申请一块限定大小的内存使用。对应地,有申请内存函数就有释放内存函数,这个函数就是 free 函数。这两个函数的原型如下:

#include <stdlib.h>

void *malloc(size_t size);
void free(void *ptr);

通过这两个函数的原型我们可以看到一些信息。malloc 函数的参数是一个 size_t 类型的变量 size ,而 malloc 返回的则是一个 void * 类型的指针。这不意外,因为我们知道通过 malloc 函数分配制定大小的内存成功之后,会将这块内存的首地址作为返回值返回给某个类型的指针变量。而通过 C 的自动类型转换,void * 类型的指针地址将会被转换为该指针变量类型的指针。

free 函数也是只有一个参数,类型为 void * 的指针变量 ptr ,无返回值。在这里问题出现了, free 函数如果只是接受某块内存的首地址作为参数,那它是如何得知这块内存的大小?或者说,free 函数怎么知道需要被释放的内存到底有多大?这块内存的大小是必要要知道的,因为如果不知道,free 函数是无法准确地将要释放的内存释放掉,也许会将后面接着的不允许释放的内存也释放掉,也许还遗留一部分内存没有释放掉。

所以,编译器,或者操作系统,肯定是提供了一种机制来告知 free 函数,这块在堆中的内存的大小。那这个机制是什么呢?

malloc 的机制

答案并不复杂,那就是在 malloc 函数返回的地址前面,有 4 字节或者 8 字节同样也是属于这块内存的内容,这几个字节中储存了该内存地址的大小。free 函数接受这个地址的时候,会回退一部分地址,根据这个结构的内容,得到该内存块的大小,然后将相关的释放掉。

以上是公开课中老师的简单讲述,那实际情况是怎样?下面进行一下简单的验证。首先测试的平台是 Fedora17 ,Linux 的内核版本为 3.6.11-5.fc17.i686.PAE ,使用的 C 编译器为 GCC 4.7.2 20120921 (Red Hat 4.7.2-2) 。测试代码如下:

#include <stdio.h>
#include <stdlib.h>


//Test the memory alloc by malloc.
int main(int argc, char *argv[])
{
    int *ptr1, *ptr2, num1, num2;
    char *cptr;

    ptr1 = &num1;
    ptr1 = (int *)malloc(512 * sizeof(int));
    ptr2 = &num2;
    ptr2 = (int *)malloc(1024 * sizeof(int));
    cptr = (char *)malloc(1024 * sizeof(char));


    printf("The start of num1 memory address is %x.\n", ptr1);
    printf("Before this address, the value is %d.\n", *(ptr1 - 1));
    printf("The start of num2 memory address is %x.\n", ptr2);
    printf("Before this address, the value is %d.\n", *(ptr2 - 1));
    printf("The start of char memory address is %x.\n", cptr);
    printf("Before this address, the value is %d.\n", *((int *)cptr - 1));
}

测试代码并不复杂,简单定义了两个 int 类型的指针变量和一个 char * 类型的指针变量,然后用 malloc 函数分配一定数量的内存,返回的内存块首地址赋值给这三个变量,然后输出这三个内存块首地址前一个位置的值。注意对于 char * 类型的地址,首先强制转换成 int * 类型的再进行 -1 操作。

代码的输出结果如下所示:

╰ ➤ ./a.out 
The start of num1 memory address is 9922008.
Before this address, the value is 2057.
The start of num2 memory address is 9922810.
Before this address, the value is 4105.
The start of char memory address is 9923818.
Before this address, the value is 1033.

通过代码我们可以知道,ptr1 申请的内存块大小为 512 * 4 = 2048,ptr2 申请的内存块大小为 1024 * 4 = 4096,cptr 申请的内存块大小为 1024 * 1 = 1024,以上单位均为字节。根据输出,有如下计算:

2057 - 2048 = 9
4105 - 4096 = 9
1033 - 1024 = 9

可见,如果 malloc 函数返回的地址前一个字节保存了该内存块的整体大小,那可以推测到,其中 9 个字节作为额外的结构,保存了这块内存的信息,以提供给其他函数比如 free 函数利用。当然这只是一个推测,真实的情况需要深入到 glibc 的库的 malloc 和 free 的源码中。

实际上,老师也说了,不同的编译器的实现是不同的,比如,可以参考下这篇文章:malloc/new函数及malloc()的一种简单原理性实现

参考:

1 相关概念及简单分析

在这一部分,我将会提及相关的概念比如进程,进程空间等,同时也对fork系统调用过程进行简单的文字描述。

1.1 进程

操作系统是在计算机硬件和应用程序或者用户程序之间的一个软件层,它通过对硬件资源的抽象,对应用程序隐藏了复杂的硬件资源,状态及操作,同时也隔离了应用程序和硬件资源,防止应用软件随意地操作硬件而带来的安全隐患。操作系统为应用程序提供了几种重要的抽象概念,进程就是操作系统中最基础的抽象概念之一。

通常情况下,我们认为进程是一个程序(program)的运行实例。当一个程序存放在储存介质上的时候,它只是一个指令,数据及其组织形式的描述。操作系统可以将一个程序加载到内存中以一个进程的形式运行起来,这就是这个程序的一个运行实例。所以我们也是可以多次加载一个程序到内存中,形成该程序的多个独立的运行实例。一个进程的内容不单只是程序的执行指令,还包括了诸如打开的文件,等待的信号,内部内核数据,处理器状态,内存地址空间及内存映射等等的资源。

在早期的分时系统和面向进程的操作系统(比如早期的Unix和Linux)中,进程被认为是运行的基本单位。而在面向线程的操作系统(比如Linux2.6或更高版本)中,进程是资源分配的基本单位,而线程才是运行的基本单位。进程是线程的容器,而一个进程中会有一个或多个线程。实际上,在Linux中,对线程和进程有着特别的统一实现,线程只是一种特别的进程。这在下面的分析中将会提及。

1.2 进程空间

在进程被创建的时候,操作系统同时也给这个进程创建了一个独立的虚拟内存地址空间。这个虚拟内存地址空间使得一个进程存在着它独自使用着所有的内存资源的错觉,而且这也是该进程独立的,完全不受其他进程的干扰,所以这也使得各个进程区分开来。虽然对于一个进程而言,它拥有着很大的一个虚拟内存地址空间,但是这并不意味着每个进程实际上都拥有这么大物理内存。只有在真正使用某一部分内存空间的时候,这一部分虚拟内存才会被映射到物理内存上。此外,一个进程也不是可以访问或者修改这个虚拟内存地址空间的所有地址的。一个典型的进程内存地址空间会被分为stack,heap,text,data,bss等多个段,如下图(来自Unix高级环境编程)所示,这是一个进程在Intel x86架构机器上面的进程空间的逻辑表示:

进程空间

从上图可以看到,从低地址到高地址,有:

  • text段,主要保存着程序的代码对应的机器指令,这也将会是CPU所将要执行的机器指令的集合。text段是可共享的,所以对于经常执行的程序只需保留一份text段的拷贝在内存中就可以了。特别地,text段是只读的,进程无法对text段进行修改,这样可以防止一个进程意外地修改它自己的指令。
  • data段,包含着程序已经被初始化的变量。
  • bss段,在这个段中的未初始化变量在程序开始运行之前将会被内核初始化为0或者控指针。
  • heap段,用户程序动态的内存分配将会在这里进行。
  • stack段,每次一个函数被调用,函数的返回地址和调用者函数的上下文比如一些寄存器变量将会保存在这里。同时,这个被调用的函数将会为它的临时变量在这里分配一定内存空间。
  • 在stack之上是命令行参数和一些环境变量。
  • 更高的空间是内核空间,一般的进程都是不被允许访问的。

此外,stack和heap段的增长方式是不同的,stack段的内存是从高地址向低地址增长的,而heap段是从低地址向高地址增长的。一般情况下,stack段的大小是有限制的,而heap段的大小是没有限制的,可以一直增长到整个系统的极限。在stack和heap之间是非常巨大的一个空间。

1.3 进程描述符

在Linux操作系统中,每个进程被创建的时候,内核会给这个进程分配一个进程描述符结构。进程描述符在一般的操作系统概念中也被称为PCB,也就是进程控制块。这个进程描述符保存了这个进程的状态,标识符,打开的文件,等待的信号,文件系统等待的资源信息。每个进程描述符都表示了独立的一个进程,而在系统中,每个进程的进程描述都加入到一个双向循环的任务队列中,由操作系统进行进程的调度,决定哪个进程可以占用CPU,哪个进程应该让出CPU。Linux中的进程描述符是一个task_struct类型的结构体。在Linux中,一个进程的进程描述符结构如下图所示:

进程描述符

task_struct是一个相当大的数据结构,同时里面也指向了其他类型的数据结构,比如thread_info,指向的是这个进程的线程信息;mm_struct指向了这个进程的内存结构;file_struct指向了这个进程打开的进程描述符结构,等等。task_struct是一个复杂的数据结构,我们将会在下面对其进行更详细的分析。

1.4 系统调用

操作系统内核的代码运行在内核空间中,而应用程序或者我们平时所写的程序是运行在用户空间中的。操作系统对内核空间有相关的限制和保护,以免操作系统内核的空间受到用户应用程序的修改。也就是说只有内核才具有访问内核空间的权限,而应用程序是无法直接访问内核空间的。结合虚拟内存空间和进程空间,我们可以知道,内核空间的页表是常驻在内存中,不会被替换出去的。

我们上面提到,操作系统将硬件资源和应用程序隔离开来,那应用程序如果需要操作一些硬件或者获取一些资源如何实现?答案是内核提供了一系列的服务比如IO或者进程管理等给应用程序调用,也就是通过系统调用(system call)。如下图:

系统调用

系统调用实际上就是函数调用,也是一系列的指令的集合。和普通的应用程序不同,系统调用是运行在内核空间的。当应用程序调用系统调用的时候,将会从用户空间切换到内核空间运行内核的代码。不同的架构实现内核调用的方式不同,在i386架构上,运行在用户空间的应用程序如果需要调用相关的系统调用,可以首先把系统调用编号和参数存放在相关的寄存器中,然后使用0x80这个值来执行软中断int。软中断发生之后,内核根据寄存器中的系统调用编号去执行相关的系统调用指令。

正如上面的图所展示的,应用程序可以直接通过系统调用接口调用内核提供的系统调用,也可以通过调用一些C库函数,而这些C库函数实际上是通过系统调用接口调用相关的系统调用。C库函数有些在调用系统调用前后做一些特别的处理,但也有些函数只是单纯地对系统调用做了一层包装。

1.5 fork系统调用

fork系统调用是Linux中提供的众多系统调用中的一个,是2号系统调用。在Linux中,需要一种机制来创建新的进程,而fork就是Linux中提供的一个从旧的进程中创建新的进程的方法。我们在编程中,一般是调用C库的fork函数,而这个fork函数则是直接包装了fork系统调用的一个函数。fork函数的效果是对当前进程进行复制,然后创建一个新的进程。旧进程和新进程之间是父子关系,父子进程共享了同一个text段,并且父子进程被创建后会从fork函数调用点下一个指令继续执行。fork函数有着一次调用,两次返回的特点。在父进程中,fork调用将会返回子进程的PID,而在子进程中,fork调用返回的是0。之所以这样处理是因为进程描述符中保存着父进程的PID,所以子进程可以通过getpidgetppid来获取父进程的PID,而进程描述符中却没有保存子进程的PID。

fork系统调用的调用过程简单描述如下:

  1. 首先是开始,父进程调用fork,因为这是一个系统调用,所以会导致int软中断,进入内核空间;
  2. 内核根据系统调用号,调用sys_fork系统调用,而sys_fork系统调用则是通过clone系统调用实现的,会调用clone系统调用;
  3. clone系统调用的参数有一系列的标志用来标明父子进程之间将要共享的内容,这些内容包括虚拟内存空间,文件系统,文件描述符等。而对于fork来说,它调用clone系统调用的时候只是给clone一个SIGCHLD的标志,这表示子进程结束后将会给父进程一个SIGCHLD信号;
  4. 在clone函数中,将会调用do_fork,这个函数是fork的主要执行部分。在do_fork中,首先做一些错误检查工作和准备复制父进程的初始化工作。然后do_fork函数调用copy_process。
  5. copy_process是对父进程的内核状态和相关的资源进行复制的主要函数。然后copy_process会调用copy_thread函数,复制父进程的执行状态,包括相关寄存器的值,指令指针和建立相关的栈;
  6. copy_thread中还干了一件事,就是把0值写入到寄存器中,然后将指令指针指向一个汇编函数ret_from_fork。所以在子进程运行的时候,虽然代码和父进程的代码是一致的,但是还是有些区别。在copy_thread完毕后,没有返回到do_fork,而是跳到ret_from_fokr,进行一些清理工作,然后退出到用户空间。用户空间函数可以通过寄存器中的值得到fork系统调用的返回值为0。
  7. copy_process将会返回一个指向子进程的指针。然后回到do_fork函数,当copy_process函数成功返回的时候,子进程被唤醒,然后加入到进程调度队列中。此外,do_fork将会返回子进程的PID;

在Linux中,创建一个新的进程的方式有三种,分别是fork,vfork和clone。fork是通过clone来实现的,而vfork和clone又是都通过do_fork函数来进行接下来的操作。

2 相关源码分析

本部分内容主要是对相关的具体源码进行分析,使用的Linux内核源码版本为3.6.11。被分析的源码并不是全部的相关源码,只是相关源码的一些重要部分。

2.1 进程描述符

在Linux中,进程描述符是一个task_struct类型的数据结构,这个数据结构的定义是在Linux源码的include/linux/sched.h中。

struct task_struct {
volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
void *stack;
atomic_t usage;
unsigned int flags; /* per process flags, defined below */
unsigned int ptrace;
...

task_struct中存放着一个进程的状态state。进程的状态主要有五种,同时也是在sched.h中定义的:

#define TASK_RUNNING        0
#define TASK_INTERRUPTIBLE  1
#define TASK_UNINTERRUPTIBLE    2
#define __TASK_STOPPED      4
#define __TASK_TRACED       8

TASK_RUNNING:表示该进程是可以运行的,有可能是正在运行或者处于一个运行队列中等待运行。

TASK_INTERRUPTIBLE:进程正在休眠,或者说是被阻塞,等待一写条件成立,然后就会被唤醒,进入TASK_RUNNING状态。

TASK_UNINTERRUPTIBLE:和TASK_INTERRUPTIBLE状态一样,区别在于处于这个状态的进程不会对信号做出反应也不会转换到TASK_RUNNING状态。一般在进程不能受干扰或者等待的事件很快就会出现的情况下才会出现这种状态。

__TASK_STOPPED:进程的执行已经停止了,进程没有在运行也不能够运行。在进程接收到SIGSTOP,SIGTSTP,SGITTIN或者SIGTOU信号的时候就会进入这个状态。

__TASK_TRACED:该进程正在被其他进程跟踪运行,比如被ptrace跟踪中。

...
int prio, static_prio, normal_prio;
unsigned int rt_priority;
const struct sched_class *sched_class;
struct sched_entity se;
struct sched_rt_entity rt;
...
unsigned int policy;

这一部分是有关于进程调度信息的内容,调度程序利用这部分的信息决定哪一个进程最应该运行,并结合进程的状态信息保证系统进程调度的公平及高效。其中prio,static_prio,normal_prio分别表示了进程的动态优先级,静态优先级,普通优先级。rt_priority表示进程的实时优先级,而sched_class则表示调度的类。se和rt表示的都是调度实体,一个用于普通进程,一个用于实时进程。policy则指出了进程的调度策略,进程的调度策略也是在include/linux/sched.h中定义的,如下:

/*
 * Scheduling policies
 */
#define SCHED_NORMAL        0
#define SCHED_FIFO      1
#define SCHED_RR        2
#define SCHED_BATCH     3
/* SCHED_ISO: reserved but not implemented yet */
#define SCHED_IDLE      5

也就是有这几种调度策略:

  • SCHED_NORMAL,用于普通进程;
  • SCHED_FIFO,先来先服务;
  • SCHED_RR,时间片轮转调度;
  • SCHED_BATCH,用于非交互的处理器消耗型进程;
  • SCHED_IDLE,主要是在系统负载低的时候使用。

一个进程还包括了各种的标识符,用来标识某一个特定的进程,同时也用来标识这个进程所属的进程组。如下:

...
pid_t pid;
pid_t tgid;
...

同时,在task_struct中也定义了一些特别指向其他进程的指针。

...
/*
 * pointers to (original) parent process, youngest child, younger sibling,
 * older sibling, respectively.  (p->father can be replaced with
 * p->real_parent->pid)
 */
struct task_struct __rcu *real_parent; /* real parent process */
struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
/*
 * children/sibling forms the list of my natural children
 */
struct list_head children;  /* list of my children */
struct list_head sibling;   /* linkage in my parent's children list */
struct task_struct *group_leader;   /* threadgroup leader */
...

正如上面这段代码中的注释所表示的,real_parent指向本进程真正的父进程,也就是原始的父进程,而parent则指向了接收SIGCHLD信号的进程,如果一个进程被托孤给另外一个进程,比如init进程,那init进程将会是这个进程的parent,但不是原始进程。childern则是一个本进程的子进程列表,sibling是本进程的父进程的子进程列表。而group_leader指针指向的是线程组的领头进程。

...
cputime_t utime, stime, utimescaled, stimescaled;
cputime_t gtime;
...
unsigned long nvcsw, nivcsw; /* context switch counts */
struct timespec start_time;         /* monotonic time */
struct timespec real_start_time;    /* boot based time */
/* mm fault and swap info: this can arguably be seen as either
mm-specific or thread-specific */
unsigned long min_flt, maj_flt;

struct task_cputime cputime_expires;
struct list_head cpu_timers[3];
...

一个进程,从创建到结束,这是它的生命周期。在进程生命周期中有许多与时间相关的内容,这些内容也包括在进程描述符中了。如上代码,我们可以看到有好几个数据类型为cputime的成员。utime和stime分别表示进程在用户态下使用CPU的时间和在内核态下使用CPU的时间,这两个成员的单位是一个click。而utimescaled和stimescaled同样也是分别表示进程在这两种状态下使用CPU的时间,只不过单位是处理器的频率。 gtime表示的是虚拟处理器的运行时间。start_time和real_start_time表示的都是进程的创建时间,real_start_time包括了进程睡眠的时间。cputime_expires表示的是进程或者进程组被跟踪的CPU时间,对应着cpu_timers的三个值。

/* filesystem information */
struct fs_struct *fs;
/* open file information */
struct files_struct *files;

如上,进程描述符还保存了进程的文件系统相关的信息,比如上面的两个成员,fs表示的是进程与文件系统的关联,包括当前目录和根目录,而files则是指向进程打开的文件

在进程描述符中,还有很多重要的信息,比如虚拟内存信息,进程间通信机制,pipe,还有一些中断和锁的机制等等。更具体的内容可以直接翻阅Linux源码中task_struct的定义。

2.2 fork系统调用

fork系统调用实际上调用的是sys_fork这个函数,在Linux中,sys_fork是一个定义在arch/alpha/kernel/entry.S中的汇编函数。

    .align  4
    .globl  sys_fork
    .ent    sys_fork
sys_fork:
    .prologue 0
    mov $sp, $21
    bsr $1, do_switch_stack
    bis $31, SIGCHLD, $16
    mov $31, $17
    mov $31, $18
    mov $31, $19
    mov $31, $20
    jsr $26, alpha_clone
    bsr $1, undo_switch_stack
    ret
.end sys_fork

如上,可以看到在sys_fork中,将相关的标志SIGCHLD等参数压栈后,然后就专跳到alpga_clone函数中执行。

2.3 alpha_clone

alpha_clone函数的定义在源码目录中的arch/alpah/kernel/process.c,具体代码如下:

/*
 * "alpha_clone()".. By the time we get here, the
 * non-volatile registers have also been saved on the
 * stack. We do some ugly pointer stuff here.. (see
 * also copy_thread)
 *
 * Notice that "fork()" is implemented in terms of clone,
 * with parameters (SIGCHLD, 0).
 */
int
alpha_clone(unsigned long clone_flags, unsigned long usp,
        int __user *parent_tid, int __user *child_tid,
        unsigned long tls_value, struct pt_regs *regs)
{
    if (!usp)
        usp = rdusp();

    return do_fork(clone_flags, usp, regs, 0, parent_tid, child_tid);
}

正如注释所提到的,在执行alpah_clone函数之前已经将寄存器的相关的值保存到栈中了,在此函数中将会根据相关的调用do_fork函数。

2.4 do_fork

创建一个新的进程的大部分工作是在do_fork中完成的,主要是根据标志参数对父进程的相关资源进行复制,得到一个新的进程。do_fork函数定义在源码目录的kernel/fork.c中。

/*
 *  Ok, this is the main fork-routine.
 *
 * It copies the process, and if successful kick-starts
 * it and waits for it to finish using the VM if required.
 */
long do_fork(unsigned long clone_flags,
          unsigned long stack_start,
          struct pt_regs *regs,
          unsigned long stack_size,
          int __user *parent_tidptr,
          int __user *child_tidptr)
{
    struct task_struct *p;
    int trace = 0;
    long nr;

首先我们来了解一下do_fork函数的参数。clone_flags是一个标志集合,主要是用来控制复制父进程的资源。clone_flags的低位保存了子进程结束时发给父进程的信号号码,而高位则保存了其他的各种常数。这些常数也是定义在include/linux/sched.h中的,如下:

/*
 * cloning flags:
 */
#define CSIGNAL     0x000000ff  /* signal mask to be sent at exit */
#define CLONE_VM    0x00000100  /* set if VM shared between processes */
#define CLONE_FS    0x00000200  /* set if fs info shared between processes */
#define CLONE_FILES 0x00000400  /* set if open files shared between processes */
#define CLONE_SIGHAND   0x00000800  /* set if signal handlers and blocked signals shared */
#define CLONE_PTRACE    0x00002000  /* set if we want to let tracing continue on the child too */
#define CLONE_VFORK 0x00004000  /* set if the parent wants the child to wake it up on mm_release */
#define CLONE_PARENT    0x00008000  /* set if we want to have the same parent as the cloner */
#define CLONE_THREAD    0x00010000  /* Same thread group? */
#define CLONE_NEWNS 0x00020000  /* New namespace group? */
#define CLONE_SYSVSEM   0x00040000  /* share system V SEM_UNDO semantics */
#define CLONE_SETTLS    0x00080000  /* create a new TLS for the child */
#define CLONE_PARENT_SETTID 0x00100000  /* set the TID in the parent */
#define CLONE_CHILD_CLEARTID    0x00200000  /* clear the TID in the child */
#define CLONE_DETACHED      0x00400000  /* Unused, ignored */
#define CLONE_UNTRACED      0x00800000  /* set if the tracing process can't force CLONE_PTRACE on this clone */
#define CLONE_CHILD_SETTID  0x01000000  /* set the TID in the child */
  • CLONE_VM表示在父子进程间共享VM;
  • CLONE_FS表示在父子进程间共享文件系统信息,包括工作目录等;
  • CLONE_FILES表示在父子进程间共享打开的文件;
  • CLONE_SIGHAND表示在父子进程间共享信号的处理函数;
  • CLONE_PTRACE表示如果父进程被跟踪,子进程也被跟踪;
  • CLONE_VFORK在vfork的时候使用;
  • CLONE_PARENT表示和复制的进程有同样的父进程;
  • CLONE_THREAD表示同一个线程组;

之前提到过,在Linux中,线程的实现是和进程统一的,就是说,在Linux中,进程和线程的结构都是task_struct。区别在于,多个线程会共享一个进程的资源,包括虚拟地址空间,文件系统,打开的文件和信号处理函数。线程的创建和一般的进程的创建差不多,区别在于调用clone系统调用时,需要通过传入相关的标志参数指定要共享的特定资源。通常是这样的:clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)。

do_fork函数的参数stack_start表示的是用户状态下,栈的起始地址。regs是一个指向寄存器集合的指针,在其中保存了调用的参数。当进程从用户态切换到内核态的时候,该结构体保存通用寄存器中的值,并存放到内核态的堆栈中。stack_size是用户态下的栈大小,一般是不必要的,设置为0。而parent_tidptr和child_tidptr则分别是指向用户态下父进程和和子进程的TID的指针。

/*
 * Do some preliminary argument and permissions checking before we
 * actually start allocating stuff
 */
if (clone_flags & CLONE_NEWUSER) {
    if (clone_flags & CLONE_THREAD)
        return -EINVAL;
    /* hopefully this check will go away when userns support is
     * complete
     */
    if (!capable(CAP_SYS_ADMIN) || !capable(CAP_SETUID) ||
            !capable(CAP_SETGID))
        return -EPERM;
}

上面这段代码主要是对参数的clone_flags组合的正确性进行检查,因为标志需要遵循一定的规则,如果不符合,则返回错误代码。此外还需要对权限进行检查。

/*
 * Determine whether and which event to report to ptracer.  When
 * called from kernel_thread or CLONE_UNTRACED is explicitly
 * requested, no event is reported; otherwise, report if the event
 * for the type of forking is enabled.
 */
if (likely(user_mode(regs)) && !(clone_flags & CLONE_UNTRACED)) {
    if (clone_flags & CLONE_VFORK)
        trace = PTRACE_EVENT_VFORK;
    else if ((clone_flags & CSIGNAL) != SIGCHLD)
        trace = PTRACE_EVENT_CLONE;
    else
        trace = PTRACE_EVENT_FORK;

    if (likely(!ptrace_event_enabled(current, trace)))
        trace = 0;
}

决定报告给ptracer的事件,如果是从kernel_thread中调用后者参数中指明了CLONE_UNTRACED,将不会有任何的事件被报告。否则,根据创建进程的类型clone,fork或者vfork报告支持的事件。

然后do_fork将会调用copy_process,如下:

p = copy_process(clone_flags, stack_start, regs, stack_size,
         child_tidptr, NULL, trace);

2.5 copy_process

copy_process函数也是定义在源码目录的kernel/fork.c中,这个函数将会复制父进程,作为新创建的一个进程,也就是子进程。copy_process会复制寄存器,然后也根据每个clone的标志,复制父进程环境的相关内容或者也可能共享父进程的内容。

static struct task_struct *copy_process(unsigned long clone_flags,
                    unsigned long stack_start,
                    struct pt_regs *regs,
                    unsigned long stack_size,
                    int __user *child_tidptr,
                    struct pid *pid,
                    int trace)
{
    int retval;
    struct task_struct *p;
    int cgroup_callbacks_done = 0;

从copy_process函数的参数来看,do_fork函数的所有参数也都被传入到这个函数中了,此外,后面还有一个参数trace标识是否对子进程进行跟踪和参数pid。在函数的开始,定义了一个未初始化的task_struct类型的指针p。

在copy_process这里也对clone标志的有效性进行了检查,如下:

if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
    return ERR_PTR(-EINVAL);

if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
    return ERR_PTR(-EINVAL);

if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
    return ERR_PTR(-EINVAL);

if ((clone_flags & CLONE_PARENT) &&
            current->signal->flags & SIGNAL_UNKILLABLE)
    return ERR_PTR(-EINVAL);

在copy_process函数中同样也进行了一系列的函数调用。比如dup_task_struct函数:

p = dup_task_struct(current);
if (!p)
    goto fork_out;

dup_task_struct函数将会为心的进程创建一个新的内核栈,thread_info结构和task_struct结构。thread_info结构是一个比较简单的数据结构,主要保存了进程的task_struct还有其他一些比较底层的内容。新值和当前进程的值是一致,所以可以说此时父子进程的进程描述符是一致的。current实际上是一个获取当前进程描述符的宏定义函数,返回当前调用系统调用的进程描述符,也就是父进程。

if (atomic_read(&p->real_cred->user->processes) >=
        task_rlimit(p, RLIMIT_NPROC)) {
    if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&
        p->real_cred->user != INIT_USER)
        goto bad_fork_free;
}

在创建新进程的相关核心数据结构后,将会对这个新的进程进行检查,看是否超出了当前用户的进程数限制。如果超出限制了,并且没有相关的权限,也不是init用户,将会转跳到相关的失败处理指令处。

p->did_exec = 0;
delayacct_tsk_init(p);    /* Must remain after dup_task_struct() */
copy_flags(clone_flags, p);
INIT_LIST_HEAD(&p->children);
INIT_LIST_HEAD(&p->sibling);
rcu_copy_process(p);
p->vfork_done = NULL;
spin_lock_init(&p->alloc_lock);

init_sigpending(&p->pending);

这段代码首先将进程描述符p的did_exec值设置为0,以保证这个新创建的进程不会被运行。因为子进程和父进程实际上还是有区别的,所以,接着将会将子进程的进程描述符的部分内容清除掉并设置为初始的值。如上,新创建的进程的描述符中children,sibling和等待的信号等值都被初始化了。然后,这段代码还调用了copy_flags函数,copy_flags函数如下:

static void copy_flags(unsigned long clone_flags, struct task_struct *p)
{
    unsigned long new_flags = p->flags;

    new_flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER);
    new_flags |= PF_FORKNOEXEC;
    p->flags = new_flags;
}

copy_flags函数将会更新这个新创建的子进程的标志,主要是清除PF_SUPERPRIV标志,这个标志表示一个进程是否使用超级用户权限。然后还有就是设置PF_FORKNOEXEC标志,表示这个进程还没有执行过exec函数。

retval = perf_event_init_task(p);
if (retval)
    goto bad_fork_cleanup_policy;
retval = audit_alloc(p);
if (retval)
    goto bad_fork_cleanup_policy;
/* copy all the process information */
retval = copy_semundo(clone_flags, p);
if (retval)
    goto bad_fork_cleanup_audit;
retval = copy_files(clone_flags, p);
if (retval)
    goto bad_fork_cleanup_semundo;
retval = copy_fs(clone_flags, p);
if (retval)
    goto bad_fork_cleanup_files;
retval = copy_sighand(clone_flags, p);
if (retval)
    goto bad_fork_cleanup_fs;
retval = copy_signal(clone_flags, p);
if (retval)
    goto bad_fork_cleanup_sighand;
retval = copy_mm(clone_flags, p);
if (retval)
    goto bad_fork_cleanup_signal;
retval = copy_namespaces(clone_flags, p);
if (retval)
    goto bad_fork_cleanup_mm;
retval = copy_io(clone_flags, p);
if (retval)
    goto bad_fork_cleanup_namespaces;
retval = copy_thread(clone_flags, stack_start, stack_size, p, regs);
if (retval)
    goto bad_fork_cleanup_io;

上面代码就是根据clone_flags集合中的值,共享或者复制父进程打开的文件,文件系统信息,信号处理函数,进程地址空间,命名空间等资源。这些资源通常情况下在一个进程内的多个线程才会共享,对于我们现在分析的fork系统调用来说,对于这些资源都会复制一份到子进程。

if (pid != &init_struct_pid) {
    retval = -ENOMEM;
    pid = alloc_pid(p->nsproxy->pid_ns);
    if (!pid)
        goto bad_fork_cleanup_io;
}

因为在do_fork函数中调用copy_process函数的时候,参数pid的值为NULL,所以此时新建进程的PID其实还没有被分配。所以接下来的就是要给子进程分配一个PID。

最后,copy_process函数做了一些清理工作,并且返回一个指向新建的子进程的指针给do_fork函数。

2.6 回到do_fork

if (!IS_ERR(p)) {
    ...
    wake_up_new_task(p);

    /* forking complete and child started to run, tell ptracer */
    if (unlikely(trace))
        ptrace_event(trace, nr);

    if (clone_flags & CLONE_VFORK) {
        if (!wait_for_vfork_done(p, &vfork))
            ptrace_event(PTRACE_EVENT_VFORK_DONE, nr);
    }

回到do_fork函数中,如果copy_process函数执行成功,没有错误,那么将会唤醒新创建的子进程,让子进程运行。自此,fork函数调用成功执行。

3 具体例程分析

在这一部分,我将会结合相关的具体例程,进行一些简单的分析。

3.1 例程代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

#define LEN 1024 * 1024

int main(int argc, char **argv)
{
    pid_t pid;
    int num = 10, i;
    char *p;

    p = malloc(LEN * sizeof(char));

    pid = fork();
    if (pid > 0) { 
        /*parent process.*/
        printf("parent %d process get %d!It stores in %x.\n",
                getpid(), num, &num);
        printf("parent have a piece of memory start from %x.\n", 
                p);
    } else {
        /*child process.*/
        printf("child %d process get %d!It stores in %x.\n",
                getpid(), num, &num);
        printf("child have a piece of memory start from %x.\n", 
                p);
    }
    while(1){}

    return 0;
}

这个程序只是简单地调用了一次fork,创建了一个子进程,然后分别在父子进程中查看申请的一块内存的起始地址。此外还添加了一个while死循环,方便父子进程的进程控制块进行查看。

3.2 相关分析

这个程序执行的结果截图如下:

执行结果

可以看到,通过对pid的值检测,我们让父子进程执行了不同的代码。

通过ps -aux | grep a.out指令,我们可以得到父子进程的PID:

$ps aux | grep a.out
tonychow 32261 93.8  0.0   3056   272 pts/1    R+   10:57   4:11 ./a.out
tonychow 32262 93.3  0.0   3056    52 pts/1    R+   10:57   4:10 ./a.out

每个进程,在其生命周期期间,都会在 /proc/进程号 目录中保存相关的进程内容,我们可以查看里面的内容对这个进程进行分析。根据上面的运行结果,我们可以通过 ls -al /proc/32261 这个指令来查看该文件夹中的内容:

$ls -al /proc/32261
总用量 0
dr-xr-xr-x   8 tonychow tonychow 0 6月  27 10:59 .
dr-xr-xr-x 267 root     root     0 5月  31 12:18 ..
dr-xr-xr-x   2 tonychow tonychow 0 6月  27 11:06 attr
-rw-r--r--   1 tonychow tonychow 0 6月  27 11:06 autogroup
-r--------   1 tonychow tonychow 0 6月  27 11:06 auxv
-r--r--r--   1 tonychow tonychow 0 6月  27 11:06 cgroup
--w-------   1 tonychow tonychow 0 6月  27 11:06 clear_refs
-r--r--r--   1 tonychow tonychow 0 6月  27 11:02 cmdline
-rw-r--r--   1 tonychow tonychow 0 6月  27 11:06 comm
-rw-r--r--   1 tonychow tonychow 0 6月  27 11:06 coredump_filter
-r--r--r--   1 tonychow tonychow 0 6月  27 11:06 cpuset
lrwxrwxrwx   1 tonychow tonychow 0 6月  27 11:06 cwd -> /home/tonychow/code/c/fork-analysis
-r--------   1 tonychow tonychow 0 6月  27 11:03 environ
lrwxrwxrwx   1 tonychow tonychow 0 6月  27 11:06 exe -> /home/tonychow/code/c/fork-analysis/a.out
dr-x------   2 tonychow tonychow 0 6月  27 10:59 fd
dr-x------   2 tonychow tonychow 0 6月  27 11:06 fdinfo
-r--------   1 tonychow tonychow 0 6月  27 11:06 io
-r--r--r--   1 tonychow tonychow 0 6月  27 11:06 latency
-r--r--r--   1 tonychow tonychow 0 6月  27 11:06 limits
-rw-r--r--   1 tonychow tonychow 0 6月  27 11:06 loginuid
-r--r--r--   1 tonychow tonychow 0 6月  27 11:06 maps
-rw-------   1 tonychow tonychow 0 6月  27 11:06 mem
-r--r--r--   1 tonychow tonychow 0 6月  27 11:06 mountinfo
-r--r--r--   1 tonychow tonychow 0 6月  27 11:06 mounts
-r--------   1 tonychow tonychow 0 6月  27 11:06 mountstats
dr-xr-xr-x   6 tonychow tonychow 0 6月  27 11:06 net
dr-x--x--x   2 tonychow tonychow 0 6月  27 11:06 ns
-rw-r--r--   1 tonychow tonychow 0 6月  27 11:06 oom_adj
-r--r--r--   1 tonychow tonychow 0 6月  27 11:06 oom_score
-rw-r--r--   1 tonychow tonychow 0 6月  27 11:06 oom_score_adj
-r--r--r--   1 tonychow tonychow 0 6月  27 11:06 pagemap
-r--r--r--   1 tonychow tonychow 0 6月  27 11:06 personality
lrwxrwxrwx   1 tonychow tonychow 0 6月  27 11:06 root -> /
-rw-r--r--   1 tonychow tonychow 0 6月  27 11:06 sched
-r--r--r--   1 tonychow tonychow 0 6月  27 11:06 schedstat
-r--r--r--   1 tonychow tonychow 0 6月  27 11:06 sessionid
-r--r--r--   1 tonychow tonychow 0 6月  27 11:06 smaps
-r--r--r--   1 tonychow tonychow 0 6月  27 11:06 stack
-r--r--r--   1 tonychow tonychow 0 6月  27 11:02 stat
-r--r--r--   1 tonychow tonychow 0 6月  27 11:06 statm
-r--r--r--   1 tonychow tonychow 0 6月  27 11:02 status
-r--r--r--   1 tonychow tonychow 0 6月  27 11:06 syscall
dr-xr-xr-x   3 tonychow tonychow 0 6月  27 11:06 task
-r--r--r--   1 tonychow tonychow 0 6月  27 11:06 wchan

从上面的结果可以看到列出了一堆的信息文件,包括状态,io,限制,文件,命名空间等等这些属于这个进程的一大堆资源。分别查看这两个进程的status信息:

$cat /proc/32261/status
Name:    a.out
State:  R (running)
Tgid:   32261
Pid:    32261
PPid:   12747
...

$cat /proc/32262/status
Name:    a.out
State:  R (running)
Tgid:   32262
Pid:    32262
PPid:   32261
...

从上面的结果可以看到,这两个进程都处于running状态,而进程32261是进程32262的父进程。接着查看一下内存映射信息:

$cat /proc/32261/maps  
08048000-08049000 r-xp 00000000 fd:02 20979068   /home/tonychow/code/c/fork-analysis/a.out
08049000-0804a000 rw-p 00000000 fd:02 20979068   /home/tonychow/code/c/fork-analysis/a.out
4b94d000-4b96c000 r-xp 00000000 fd:01 793014     /usr/lib/ld-2.15.so
4b96c000-4b96d000 r--p 0001e000 fd:01 793014     /usr/lib/ld-2.15.so
4b96d000-4b96e000 rw-p 0001f000 fd:01 793014     /usr/lib/ld-2.15.so
4b970000-4bb1b000 r-xp 00000000 fd:01 809017     /usr/lib/libc-2.15.so
4bb1b000-4bb1c000 ---p 001ab000 fd:01 809017     /usr/lib/libc-2.15.so
4bb1c000-4bb1e000 r--p 001ab000 fd:01 809017     /usr/lib/libc-2.15.so
4bb1e000-4bb1f000 rw-p 001ad000 fd:01 809017     /usr/lib/libc-2.15.so
4bb1f000-4bb22000 rw-p 00000000 00:00 0 
b76a4000-b77a6000 rw-p 00000000 00:00 0 
b77be000-b77c0000 rw-p 00000000 00:00 0 
b77c0000-b77c1000 r-xp 00000000 00:00 0          [vdso]
bf92a000-bf94b000 rw-p 00000000 00:00 0          [stack]

结合上面程序的输出,可以看到int的类型的变量num存放在栈中,而通过malloc得到的则是存放在堆中。

$ls -l /proc/32261/fd
总用量 0
lrwx------ 1 tonychow tonychow 64 6月  27 10:59 0 -> /dev/pts/1
lrwx------ 1 tonychow tonychow 64 6月  27 10:59 1 -> /dev/pts/1
lrwx------ 1 tonychow tonychow 64 6月  27 10:59 2 -> /dev/pts/1

查看下该进程的文件描述符,可以看到主要是有标准输出,标准输入和标准输出这三个。

$ cat /proc/32261/limits
Limit                     Soft Limit           Hard Limit           Units     
Max cpu time              unlimited            unlimited          seconds   
Max file size             unlimited            unlimited            bytes     
Max data size             unlimited            unlimited            bytes     
Max stack size            8388608              unlimited            bytes     
Max core file size        0                    unlimited            bytes     
Max resident set          unlimited            unlimited            bytes     
Max processes             1024                 31683            processes 
Max open files            1024                 4096                 files     
Max locked memory         65536                65536                bytes     
Max address space         unlimited            unlimited            bytes     
Max file locks            unlimited            unlimited            locks     
Max pending signals       31683                31683            signals   
Max msgqueue size         819200               819200               bytes     
Max nice priority         0                    0                    
Max realtime priority     0                    0                    
Max realtime timeout      unlimited            unlimited            us 

通过 cat /proc/32261/limits 命令我们可以看到系统对这个用户的一些资源限制,包括CPU时间,最大文件大小,最大栈大小,进程数,文件数,最大地址空间等等的资源。

4 总结

经过这次对Linux系统的fork系统调用的分析,主要有以下几点总结:

  • fork调用是Linux系统中很重要的一个创建进程的方式,它的实现其实也依靠了clone系统调用;
  • 在Linux系统中,线程其实就是共享了父进程大部分资源的子进程,内核通过clone_flags来控制创建这种特别的进程;
  • Linux其实也是一个软件,但是它是一个复杂无比的软件。虽然从源码来说,不同的部分分得挺清楚,但是具体到一个个函数的执行,对于我们新手而言,如果没有注释,有时候真的很难知道一个函数的参数是什么意思。这时候就要依靠搜索引擎的力量了。

5 主要参考文献

上周末投了某个公司的实习生,然后周二收到笔试邮件,周五交了笔试代码然后周六就来电话面试了。本来没想着这个时间了还会有公司收实习生,不过第一次面试还是很紧张。这次面试结果感觉很糟糕,有种心灰意冷的感觉。不过想着是第一次,还是记录一下好了。

面试之前胡乱复习了一下一些数据结构和常见算法,然后是带着紧张的心情接听了面试官的电话。一听到电话那边传来的声音,我一下子楞了:咋听不清楚他说啥。于是悲剧就由这悲剧的通话质量开始了,我紧张地猜者那边问的是啥,然后蒙蒙的脑袋也不知道在想啥集中不了。更悲催的是,面试官问到几个问题,最终都是迷迷糊糊回答了。比如问道数据库中的事务的ACID,结果我当作问得是CRUD,blabla了一下。还有问道守护进程的创建,这学期开始还写过咧,结果记不清楚了,只记得重定或者关闭向标准输入输出和标准错误,创建新的会话什么的。于是又是悲催。只记得整个过程说了好多次“不好意思我不清楚”……T_T。最后大概是面试官终于受不了我了,然后说做道题目吧,说个协作编程的网址让我写代码结果我愣是没敲对,然后我逐个单词读给面试官听他也说没错,最后还是用短信发了过来,原来是b不是d…..T_T。题目是写一个生成器逐行输出杨辉三角的每一行。这时候肿胀的脑袋不知道想啥,给了半个小时折腾了四十分钟。最后面试官说,这轮就结束了吧,准备接听下一位面试官的电话。

下一位面试官的通话质量倒是很清晰,一开始便是做一道题目,在一个乱序的数字序列中查找连续从小到大排列最长子序列的长度,不需要输出该子序列。这时候反倒是有种破罐子破摔的状态了,题目在要求的二十分钟内解决了。然后面试官问了一些问题,比如常上的技术网站是啥还有我的个人项目相关的一些问题,技术问题反而不多,估计是悲催了。最后,面试官说,这次面试就到这,过两天会有同事通知我结果。这简直就是酝酿怎么拒绝的节奏嘛……

其实这次面试问到的技术问题基本上都是覆盖了我平时了解的内容。可是!我记不清楚了……这次面试最大的收获可能是暴露了我的很多问题,对那些内容不熟悉,是因为平时不怎么用。看过了,然后慢慢就忘掉了。但是一些内容看多了也会记得的。所以最后的总结是,学艺不精,读的书少,写的代码更少。

起因

今天师弟问了一个关于Python函数参数的一个问题:

#1
def func(x, l=[]):
    pass

#2
def func(x, l=None):
    if l is None:
        l = []

为啥第一个函数会把l每次调用完的值保留下来?

起初我认为问的是这两个函数使用的时候,为何会保持对传入的参数l的修改。从这个方面来讲,是因为Python对于数据赋值的处理的原因。

在Python中,赋值是传引用的。一个列表,比如[1, 2, 3],或者一个字符串,’tonychow’,这些对象在创建的时候会在内存中分配一段空间。如果将这些对象赋值给一个变量名,那就会导致在Python的命名空间中该变量名指向内存中这个对象。对该变量名的操作就是对内存中这个对象的操作。所以如果尝试直接将一个变量a赋值给另外一个变量b,导致的后果是,命名空间中,这两个变量名a和b指向内存中同样一个对象,也就是所谓传引用赋值。对其中任意一个变量的操作,实质是对该对象进行操作,所以同样的操作后结果也会可以在另外一个变量中看到。如下:

>>> a = [1, 2, 3]
>>> b = a
>>> a.pop()
3
>>> b
[1, 2]
>>> a
[1, 2]
>>> 

从上面的代码可以看到,在将a赋值给b之后,对a列表调用pop方法,导致的是b列表也发生了变化。我们还可以通过Python内置的globals函数和id函数来加深这个理解。globals函数将会返回一个字典,这个字典是当前的全局符号表。而id函数则会返回一个对象的标识,实际上就是这个对象在内存中的地址。

>>> globals()
{'a': [1, 2], 'b': [1, 2],
'__builtins__': <module '__builtin__' (built-in)>,
'value': None, '__package__': None, 
'key': '__doc__', '__name__': '__main__', '__doc__': None}
>>> id(a)
3077280588L
>>> id(b)
3077280588L
>>> 

可以看到,a和b都在当前的全局字符表中,他们的值也都是一致的。此外,id函数的结果明确地说明了a和b这两个变量名都是指向了内存中的同一个对象。而在Python中,调用函数的时候,传入参数,也是进行传引用的赋值。所以我师弟说的这两个函数都会保留对于传入参数的修改,也就是:

>>> def func(l=None):
...     if l is None:
...         l = []
...     l.append(1)
... 
>>> bar = [2]
>>> bar
[2]
>>> func(bar)
>>> func(bar)
>>> bar
[2, 1, 1]
>>> 

题外话,在Python内置的数据类型中,有两种不同的数据类型。一种是可变类型,比如list,dict等;另外一种就是不可变类型,比如字符串或者tuple。

可是后来师弟贴出了另外一段代码:

>>> def func2(items=None):
...     if items == None:
...         items = []
...     items.append(1)
...     return items
... 
>>> func2()
[1]
>>> func2()
[1]
>>> 

这下我明白了,师弟说的不是我想到的那个问题,而是命名参数的问题。

解决

说实话这个问题一开始我也没有想到答案。大家在学习Python的时候,无论看的是哪本入门书,应该在开始的时候都会看到一句话“Python中一切都是对象”。看代码:

>>> isinstance(1, int)
True
>>> isinstance('test', str)
True
>>> def func():
...     pass
... 
>>> type(func)
<type 'function'>
>>> dir(func)
['__call__', '__class__', '__closure__', '__code__', '__defaults__',
'__delattr__', '__dict__', '__doc__', '__format__', '__get__', 
'__getattribute__', '__globals__', '__hash__', '__init__', 
'__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__',
'__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__',
'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc',
'func_globals', 'func_name']
>>> func.__code__
<code object func at 0xb76c8410, file "<stdin>", line 1>
>>> func.__name__
'func'
>>> 

对的,数字1是一个对象,字符串’test’也是一个对象,甚至一个函数也是一个类型为function的对象,也有一堆的属性和方法。对于function对象而言,有一个特殊属性defaults,这个属性用一个元组保存了是这个function对象的命名参数的缺省值,如下:

>>> def func(a=1, b=2):
...     pass
...
>>> func.__defaults__
(1, 2)
>>> def foo(a, b):
...     pass
...
>>> foo.__defaults__ is None
True
>>> def func_no():
...     pass
...
>>> func_no.__defaults__ is None
True
>>>

如果一个函数有命名参数,则按顺序保存了命名参数的缺省值。如果这个函数命名参数没有缺省值或者没有命名参数,则为None。回到问题,为什么第一个函数中指定缺省值为[]会导致随着执行过程中,缺省参数的值会被保留下来呢?代码如下:

>>> def foo(l=[]):
...     l.append(1)
...     return l
... 
>>> foo()
[1]
>>> foo()
[1, 1]
>>> foo()
[1, 1, 1]
>>> 

其实通过上面的罗嗦一大堆,答案很容易就可以得到了:foo是一个function类型的对象,这个对象中有个defaults属性,保存了命名参数l的值,而在一次次的调用过程中,因为没有传入参数,所以实际上foo函数改变的是命名参数的缺省值。也就是师弟所说的这个函数在一次次调用中保留了对命名参数l的结果的修改。而师弟贴出的第二个函数的命名参数缺省值是None,实质上就是没有缺省值,所以l的值修改没有在调用中保留下来。是不是真的这样?我们来看下:

>>> def foo(l=[]):
...     print 'default_arg_addr:' + str(id(l))
...     l.append(1)
...     print 'changed_var_addr:' + str(id(l))
...     print l
... 
>>> id(foo.__defaults__[0])
3077402860L
>>> foo()
default_arg_addr:3077402860
changed_var_addr:3077402860
[1]
>>> foo()
default_arg_addr:3077402860
changed_var_addr:3077402860
[1, 1]
>>> foo.__defaults__
([1, 1],)
>>> 

上面这个函数foo有一个命名参数l,它的命名参数缺省值是一个空的列表,虽然是空列表,可是它确确实实是一个对象,已经在内存给它分配了空间。我们可以通过id函数的结果看出来。然后是两次的调用foo函数可以看到,因为没有传入参数,所以这两次修改的都是这个缺省的命名参数的值,所以可以得到所谓的对l的值的修改保留下来了的感觉。

深入

首先我们应该明白,在Python中,一个对象的实例化和初始化是不同的。一个对象实例化调用的是对象的new函数,而初始化调用的是init函数。所以,要深入地去看在Python中,函数在实例化的时候到底发生了什么,我们应该要去看Python源码。如下,源码版本为Python2.7.4。

Python2.7.4/Objects/funcobject.c, func_new, L436-L439

if (defaults != Py_None) {
    Py_INCREF(defaults);
    newfunc->func_defaults  = defaults;
}

Python2.7.4/Include/object.h, L765-L767

#define Py_INCREF(op) (                     \
_Py_INC_REFTOTAL  _Py_REF_DEBUG_COMMA       \
((PyObject*)(op))->ob_refcnt++)

上面第一断代码是funcobject的func_new中的代码,也就应该是functions对象的new函数代码。可以看到,如果defaults不是None,也就是说有值,而我们上面也提到Python中一切都是对象,所以就会对这个对象进行Py_INCREF操作,并且将这个defaults值设定为func_defaults。Py_INCREF操作是什么?从第二段代码可以看到,这是一个宏定义,将参数op的ob_refcnt值加一。ob_refcnt是什么?refcnt——reference count,这样明白了,就是将该对象的引用计数值加一。在执行了函数函数之后,该命名函数的缺省值对象并没有被销毁,而是随着该函数对象的存在而存在。对这个缺省之对象的修改当然也会被保留下来。

-EOF-