斯坦福大学公开课-编程范式 笔记 1

Posted on 日 07 七月 2013 in opencourse • Tagged with opencourse, programming-paradigms, notes • 2 min read

前言

最近在跟一门斯坦福大学的公开课 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()的一种简单原理性实现

参考:


Linux中fork系统调用分析

Posted on 四 27 六月 2013 in linux • Tagged with linux, system-call, fork, source-reading • 10 min read

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 ,所以子进程可以通过 getpid 来获取父进程的 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_fork ,进行一些清理工作,然后退出到用户空间。用户空间函数可以通过寄存器中的值得到 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 系统中很重要的一个创建进程的方式,系统级别的进程和线程都是通过 fork 系统调用来实现的,它的实现其实也依靠了 clone 系统调用;
  • 在 Linux 系统中,一个进程内多个线程其实就是共享了父进程大部分资源的子进程,内核通过 clone_flags 来控制创建这种特别的进程;
  • Linux 其实也是一个软件,但是它是一个复杂无比的软件。虽然从源码来说,不同的部分分得挺清楚,但是具体到一个个函数的执行,对于我们新手而言,如果没有注释,有时候真的很难知道一个函数的参数是什么意思。这时候就要依靠搜索引擎的力量了。

5 主要参考文献


Python 函数的命名参数相关

Posted on 三 15 五月 2013 in python • Tagged with python • 2 min read

起因

今天师弟问了一个关于 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 内置的数据类型中,有两种不同的数据类型。一种是可变类型(mutable),比如 list , dict 等;另外一种就是不可变类型(immutable),比如字符串或者 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,这样明白了,就是将该对象的引用计数值加一。在执行了函数函数之后,该命名函数的缺省值对象并没有被销毁,而是随着该函数对象的存在而存在。对这个缺省之对象的修改当然也会被保留下来。


Python 中 sqlite3 模块使用小记

Posted on 日 12 五月 2013 in python • Tagged with python, sqlite3 • 2 min read

前记

Python 的标准库中包含了对 sqlite 这个轻巧的数据库的支持模块,也就是 sqlite3 模块。sqlite 数据库的好处我就不多说了,小型而强大,适合很多小型或者中型的数据库应用。最近在使用 sqlite3 模块遇到一些问题,解决了,顺便就记下来。

问题

sqlite3 模块的使用很简单,如下这段测试代码,创建一个 person 数据表然后进行一次数据库查询操作。

#!/usr/bin/env pypthon
#_*_ coding: utf-8 _*_


import sqlite3

SCHEMA = """
         CREATE TABLE person (
             p_id int,
             p_name text
         )
         """

def init():
    data = [(1, 'tony'), (1, 'jack')]
    conn = sqlite3.connect(':memory:')
    c = conn.cursor()
    try:
        c.execute(SCHEMA)
        for person in data:
            c.execute('insert into person values(?, ?)', person)
        conn.commit()
    except sqlite3.Error as e:
        print 'error!', e.args[0]
    return conn


if __name__ =='__main__':
    conn = init()
    c = conn.cursor()
    #Do a query.
    c.execute('select * from person where p_name = ?', 'tony')
    person = c.fetchone()
    print person

运行这段代码,抛出了个异常,如下提示:

Traceback (most recent call last):
      File "sqlite3_test.py", line 32, in <module>
          c.execute('select * from person where p_name = ?', 'tony')
          sqlite3.ProgrammingError: Incorrect number of bindings supplied. The current statement uses 1, and there are 4 supplied.

很莫名奇妙是不?明明我提供的占位符?绑定只有一个字符串参数,可是却说我提供了四个。再看仔细点,说提供了四个,正好字符串 'tony' 是四个字符。

解决

翻了翻文档,发现也给出了一个占位符查询的例子如下:

t = (RHAT,)
c.execute(SELECT * FROM stocks WHERE symbol=?’, t)

所以将字符参数放到元组中就可以了,修改如下:

c.execute('select * from person where p_name = ?', ('tony'))

结果依旧是抛出了同样的异常。再仔细看下,漏了个',',于是加上:

c.execute('select * from person where p_name = ?', ('tony',))

这次终于得到最终的结果了,其中的字符为 unicode 类型:

(1, u'tony')

原因

但是为什么? Python 中的 sqlite3 模块提供了对 sqlite 数据操作的 API,执行查询的函数是在 sqlite3 模块源码中定义的,很明显想要知道为啥,最好的方式是去看 sqlite3 模块的源码。我手上的 Python 源码是 Python-2.7.4,在源码 Python-2.7.4/Modules/_sqlite/cursor.c 的函数 PyObject _pysqlite_query_execute(pysqlite_Cursor self, int multiple, PyObject* args) 中 497-529 行:

...

/* execute() */
if (!PyArg_ParseTuple(args, "O|O", &operation, &second_argument)) {
    goto error;
}

if (!PyString_Check(operation) && !PyUnicode_Check(operation)) {
    PyErr_SetString(PyExc_ValueError, "operation parameter must be str or unicode");
    goto error;
}

parameters_list = PyList_New(0);
if (!parameters_list) {
    goto error;
}

if (second_argument == NULL) {
    second_argument = PyTuple_New(0);
    if (!second_argument) {
        goto error;
    }
} else {
    Py_INCREF(second_argument);
}
if (PyList_Append(parameters_list, second_argument) != 0) {
    Py_DECREF(second_argument);
    goto error;
}
Py_DECREF(second_argument);

parameters_iter = PyObject_GetIter(parameters_list);
if (!parameters_iter) {
    goto error;
}

...

从这段源码中可以看到这段代码将参数 args 解析成为 Python 的一个元组作为 parameters_list ,然后最这个得到的元组进行 iter 操作,不断地读取这个元组的元素作为参数,而 Python 中对一个字符串进行 parse tuple 会怎样?我觉得 PyArg_ParseTuple 这个函数的操作和以下代码会是类似的:

...
>>> tuple('test')
('t', 'e', 's', 't')
...

所以现在我们可以看到我们的答案了,sqlite3 模块中,cursor 对象的 execute 方法会接受两个参数,第二个参数会被 PyArg_ParseTuple 函数转化成为 Python中 的 tuple。而对一个字符进行 tuple parse 导致的结果是将这个字符串的每个字符作为 tuple 的一个元素,所以上面抛出错误的时候提示的提供了四个所以错误也可以理解了。那为什么'('tony')'同样是错误呢?如下:

>>> type(('tony'))
<type 'str'>
>>> type(('tony',))
<type 'tuple'>

很明显,('tony')是一个 str 也就是一个字符串,相当于是 'tony',而 ('tony',) 才是一个单元素的 tuple 。同样,因为:

>>> tuple(['tony'])
('tony',)

所以如果那一行查询执行改为:

c.execute('select * from person where p_name = ?', ['tony'])

同样也是可以执行成功的。


Python 中的 New-style 和 Old-style classes

Posted on 六 06 四月 2013 in python • Tagged with python • 1 min read

使用 super() 的错误

super 函数是 Python 中的一个内置函数,提供对继承的类的函数调用,特别是在子类中被 overridden 的父类函数,比如

__init__()

最近在使用 super 函数的时候出现了个错误,例如下:

>>> class Base:
...     def __init__(self):
...         self.num = 1
... 
>>> class Next(Base):
...     def __init__(self):
...         super(Next, self).__init__()
... 
>>> obj = Next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __init__
TypeError: must be type, not classobj

可以看到抛出了参数类型错误的错误.一开始完全不知所措,然后将出错信息 google 了一下,找到了解决方式:

>>> class Base(object):
...     def __init__(self):
...         self.num = 1
... 
>>> class Next(Base):
...     def __init__(self):
...         super(Next, self).__init__()
... 
>>> obj = Next()
>>> obj.num
1
>>>

简单地将 Base 继承 object 就可以解决这个错误.其实这是 Python 中的 NewStyle classes 和 OldStyle classes 而导致的一个问题. super() 函数只适用于 NewStyle classes.

Newstyle 和 Oldstyle

Python 中,直至 Python2.1 ,类和类型是两种不相关的概念,例如:

>>> class Test:
...     pass
... 
>>> Test().__class__
<class __main__.Test at 0xb77373ec>
>>> type(Test())
<type 'instance'>
>>> type(type(Test()))
<type 'type'>
>>>

在这里 Test 类是 Oldstyle 的类.可以看到,Test 类的一个实例,它的类是 Test,但是类型却是 instance.这是因为 Oldstyle 的类与类型是不统一的概念, Oldstyle 类的实例是独立于它们的类,由一个 Python 内置类型 instance 实现的.

从2.2开始, Python 开始使用 New-style 类来统一类和类型.对于一个 New-style 的类,它的实例的类型和类都是一致的.为了兼容之前的代码,在 Python2.2 之后,默认的类定义还是 Old-style 的类.而一个 New-style 的类可以通过继承一个 New-style 的类或者在类继承中最顶端继承 object 来实现,如下:

>>> class Test(object):
...     pass
... 
>>> Test().__class__
<class '__main__.Test'>
>>> type(Test())
<class '__main__.Test'>
>>> type(type(Test()))
<type 'type'>
>>>

New-style 类的提出是为了统一 Python的 对象模型.在 Python3 中,Old-style 类已经完全移除了.

参考资料:

  • http://docs.python.org/2/library/functions.html#super
  • http://docs.python.org/2/reference/datamodel.html#newstyle
  • http://stackoverflow.com/questions/9698614/super-raises-typeerror-must-be-type-not-classobj-for-new-style-class
  • http://stackoverflow.com/questions/9699591/instance-is-an-object-but-class-is-not-a-subclass-of-object-how-is-this-po/9699961#9699961