如何高效阅读代码?

阅读软件源代码是每个开发者的必由之路,尤其是内核开发者。因为内核开发在很大程度上并不是重新发明轮子,而是深入理解并尽量复用现有的内核设计框架,然后参照相似的功能模块去添加或改写某项需要的功能。在对内核整体框架以及某些子系统融会贯通以后,才有可能站在巨人的肩膀上去改进框架本身,实现自主创新。就我的个人经验来说,阅读代码与编写代码的时间大概是6 : 4。

自由软件的开发与商业软件相比,有一个很大的不同就是文档相对比较缺乏。但同时有一种说法叫做“代码就是最好的文档”——只要你愿意,没什么学不会的。那么,像Linux内核这样动辄上千万行代码的浩大工程,该如何读懂,又从哪里读起?

为此,我梳理了自己在十余年Linux内核相关的工作经验,总结了一套可行的代码阅读方法:基于“广度优先”的大原则,“三部曲”具体实现,理解补丁文件。

广度优先原则

建议用“广度优先”的方式阅读代码。

“广度优先”指的是当我们在阅读一个函数的时候,先看看这个函数整体上在一步一步完成什么工作,等基本掌握要领以后,再逐个去了解其所调用的子函数。与之对应的“深度优先”方法,一上来就把第一个子函数以及子函数的子函数弄清楚,这里并不推荐采用。

代码好比一棵树,“广度优先”就是说我们要先找到主干,然后搞清楚主干上有几根树枝,再去某条感兴趣的树枝上寻找有意义的叶子;而“深度优先”则是随便碰到一根树枝,就赶紧深入进去把所有的叶子给找出来。广度优先会让你有一种“会当临绝顶,一览众山小”的自信;而深度优先会让你有一种“不识庐山真面目,只缘身在此山中”的迷茫。

基于“广度优先”的大原则,我们更进一步来详细解析“三部曲”的具体实现方法。“三部曲”依次是:找准入口点,理清主脉络,顾名思义看功能。

找准入口点

“三部曲”的第一步是找准入口点。

Linux内核基本上由C语言和汇编语言组成,在C语言里面,大家都知道应用程序的入口点是main()函数,其实内核也是比较类似的。以相对独立的内核模块为例,在绝大多数情况下,如果模块的名字是modname,那么模块的入口函数就是modname_init()。万一不是,可以在相关文件目录里面用module_init进行搜索,因为模块入口一般会用module_init(mod_entry)或者类似的方式进行声明(这里mod_entry指的是模块的入口函数名字)。

那么内核本身的入口又在哪里呢?凭直觉,凭经验,总会跟start、init、entry之类的词汇有些关联。初始入口(第一入口)是体系结构相关的,在汇编语言代码里面;而通用入口(第二入口)是体系结构无关的,在C语言里面——实际上就是init/main.c里面的start_kernel()。顺便说一下,包括龙芯在内的所有MIPS处理器,第一入口都是arch/mips/ kernel/head.S里面的kernel_entry。CPU执行的第一条内核指令就在初始入口kernel_entry,而kernel_entry之后的代码在完成与体系结构高度相关的初始化以后,会跳转到通用入口start_kernel()执行。

理清主脉络

“三部曲”的第二步是理清主脉络。这一步的原则是“去粗取精”:去掉没用的,留下有用的

眼不见为净,影响阅读的直接统统删除。当然了,我们并不是建议在原始代码库中直接删除,而是建议一边看代码一边做笔记,先把函数复制到笔记里面,然后再根据需要逐步删除,直到留下主脉络

那么什么叫“有用”的,什么叫“没用”的?既然已经进入了内核源代码库,当然每一句都是有用的,这里所说的有用或者没用,仅仅指的是对理解主脉络有利还是不利。而“有利”或者“不利”又是分多个层次的。

  • 1.代码 vs. 注释
  • 注释是非常有用的,可以帮助我们理解代码,但是注释在很大程度上是用于了解细节的,而在对主脉络的了解上面,用处不大。所以首先考虑的就是去掉注释,包括//…和/*…*/格式的直接注释,以及#if 0 … #endif格式的条件汇编。
  • 2.程序流程 vs. 变量声明
  • 去掉注释以后,纯粹的代码会变得清爽很多。但是如果依旧十分复杂,影响阅读,那么就可以删除一些变量声明和简单的初始化赋值语句,留下重要的程序流程。
  • 3.功能语句 vs. 调试语句
  • 如果到了这里主脉络依旧不是十分清晰,那么可以开始考虑去掉各种调试和打印语句,比如printf()、printk()、debug()等。
  • 4.正常流程 vs. 异常流程
  • 异常处理是程序健壮性必不可少的部分,但是如果异常处理本身代码过于复杂,那它势必会影响可读性。因此在必要的时候,为了方便阅读,可以去掉返回值检查、try-catch中的catch子句等。
  • 5.常见路径 vs. 罕见路径
  • 通常情况下,代码精简到这一步就已经比较容易理解了,但如果有必要,可以对switch-case、if-else等结构进行处理,只保留最常见的一种情况。

下面举一个实际的例子,代码来源是GXemul。这是一个模拟MIPS(包括龙芯)机器的模拟器软件,代码入口为main()。相比于Linux内核的庞大复杂,GXemul是一个设计精巧的应用程序,非常适合用来举例。(PS:这是一个长达106行的函数~)

int main(int argc, char *argv[]) 
{
	/*  Setting constants:  */
	const int constant_yes = 1;
	const int constant_true = 1;
	const int constant_no = 0;
	const int constant_false = 0;
	struct emul **emuls;
	char **diskimages = NULL;
	int n_diskimages = 0;
	int n_emuls;
	int i;
	progname = argv[0];
	/*  Initialize all emulator subsystems:  */
	console_init();
	cpu_init();
	device_init();
	machine_init();
	timer_init();
	useremul_init();
	emuls = malloc(sizeof(struct emul *));
	if (emuls == NULL) {
		fprintf(stderr, "out of memory\n");
		exit(1);
	}
	/*  Allocate space for a simple emul setup:  */
	n_emuls = 1;
	emuls[0] = emul_new(NULL);
	if (emuls[0] == NULL) {
		fprintf(stderr, "out of memory\n");
		exit(1);
	}
	get_cmd_args(argc, argv, emuls[0], &diskimages, &n_diskimages);
	if (!skip_srandom_call) {
		struct timeval tv;
		gettimeofday(&tv, NULL);
		srandom(tv.tv_sec ^ getpid() ^ tv.tv_usec);
	}
	/*  Print startup message:  */
	debug("GXemul");
	debug("    Copyright (C) 2003-2006  Anders Gavare\n");
	debug("Read the source code and/or documentation for other Copyright messages.\n\n");
	if (emuls[0]->machines[0]->machine_type == MACHINE_NONE) {
		n_emuls --;
	} else {
		for (i=0; i<n_diskimages; i++) diskimage_add(emuls[0]->machines[0], diskimages[i]);
	}
	/*  Simple initialization, from command line arguments:  */
	if (n_emuls > 0) {
		/*  Make sure that there are no configuration files as well:  */
		for (i=1; i<argc; i++)
		        if (argv[i][0] == '@') {
			        fprintf(stderr, "You can either start one "
			             "emulation with one machine directly from "
			             "the command\nline, or start one or more "
			             "emulations using configuration files."
			             " Not both.\n");
			        exit(1);
		        }
		/*  Initialize one emul:  */
		emul_simple_init(emuls[0]);
	}
	/*  Initialize emulations from config files:  */
	for (i=1; i<argc; i++) {
		if (argv[i][0] == '@') {
			char tmpstr[50];
			char *s = argv[i] + 1;
			if (strlen(s) == 0 && i+1 < argc && argv[i+1][0] != '@') {
			        i++;
				s = argv[i];
			}
			n_emuls ++;
			emuls = realloc(emuls, sizeof(struct emul *) * n_emuls);
			if (emuls == NULL) {
				fprintf(stderr, "out of memory\n");
				exit(1);
			}
			/*  Always allow slave xterms when using multiple emulations:  */
			console_allow_slaves(1);
			/*  Destroy the temporary emuls[0], since it will be overwritten:  */
			if (n_emuls == 1) {
				emul_destroy(emuls[0]);
			}
			emuls[n_emuls - 1] = emul_create_from_configfile(s);
			snprintf(tmpstr, sizeof(tmpstr), "emul[%i]", n_emuls-1);
		}
	}
	if (n_emuls == 0) {
		fprintf(stderr, "No emulations defined. Maybe you forgot to "
		     "use -E xx and/or -e yy, to specify\nthe machine type."
		     " For example:\n\n    %s -e 3max -d disk.img\n\n"
		     "to boot an emulated DECstation 5000/200 with a disk "
		     "image.\n", progname);
		exit(1);
	}
	device_set_exit_on_error(0);
	console_warn_if_slaves_are_needed(1);
	/*  Run all emulations:  */
	emul_run(emuls, n_emuls);
	/*
         *  Deinitialize everything:
         */
	console_deinit();
	for (i=0; i<n_emuls; i++) emul_destroy(emuls[i]);
	return 0;
}

这是一个长达106行的函数,下面我们开始精简!

1、根据“三部曲”精简原则,我们首先去掉其中的注释。还剩下92行:

int main(int argc, char *argv[]) 
{
	const int constant_yes = 1;
	const int constant_true = 1;
	const int constant_no = 0;
	const int constant_false = 0;
	struct emul **emuls;
	char **diskimages = NULL;
	int n_diskimages = 0;
	int n_emuls;
	int i;
	progname = argv[0];
	console_init();
	cpu_init();
	device_init();
	machine_init();
	timer_init();
	useremul_init();
	emuls = malloc(sizeof(struct emul *));
	if (emuls == NULL) {
		fprintf(stderr, "out of memory\n");
		exit(1);
	}
	n_emuls = 1;
	emuls[0] = emul_new(NULL);
	if (emuls[0] == NULL) {
		fprintf(stderr, "out of memory\n");
		exit(1);
	}
	get_cmd_args(argc, argv, emuls[0], &diskimages, &n_diskimages);
	if (!skip_srandom_call) {
		struct timeval tv;
		gettimeofday(&tv, NULL);
		srandom(tv.tv_sec ^ getpid() ^ tv.tv_usec);
	}
	debug("GXemul");
	debug("    Copyright (C) 2003-2006  Anders Gavare\n");
	debug("Read the source code and/or documentation for other Copyright messages.\n\n");
	if (emuls[0]->machines[0]->machine_type == MACHINE_NONE) {
		n_emuls --;
	} else {
		for (i=0; i<n_diskimages; i++) diskimage_add(emuls[0]->machines[0], diskimages[i]);
	}
	if (n_emuls > 0) {
		for (i=1; i<argc; i++)
		        if (argv[i][0] == '@') {
			        fprintf(stderr, "You can either start one "
			             "emulation with one machine directly from "
			             "the command\nline, or start one or more "
			             "emulations using configuration files."
			             " Not both.\n");
			        exit(1);
		        }
		emul_simple_init(emuls[0]);
	}
	for (i=1; i<argc; i++) {
		if (argv[i][0] == '@') {
			char tmpstr[50];
			char *s = argv[i] + 1;
			if (strlen(s) == 0 && i+1 < argc && argv[i+1][0] != '@') {
				i++;
				s = argv[i];
			}
			n_emuls ++;
			emuls = realloc(emuls, sizeof(struct emul *) * n_emuls);
			if (emuls == NULL) {
				fprintf(stderr, "out of memory\n");
				exit(1);
			}
			console_allow_slaves(1);
			if (n_emuls == 1) {
				emul_destroy(emuls[0]);
			}
			emuls[n_emuls - 1] = emul_create_from_configfile(s);
			snprintf(tmpstr, sizeof(tmpstr), "emul[%i]", n_emuls-1);
		}
	}
	if (n_emuls == 0) {
		fprintf(stderr, "No emulations defined. Maybe you forgot to "
		     "use -E xx and/or -e yy, to specify\nthe machine type."
		     " For example:\n\n    %s -e 3max -d disk.img\n\n"
		     "to boot an emulated DECstation 5000/200 with a disk "
		     "image.\n", progname);
		exit(1);
	}
	device_set_exit_on_error(0);
	console_warn_if_slaves_are_needed(1);
	emul_run(emuls, n_emuls);
	console_deinit();
	for (i=0; i<n_emuls; i++) emul_destroy(emuls[i]);
	return 0;
}

2、删除注释以后仍然很长,所以我们开始第二次精简,去掉变量声明和简单的赋值语句。还剩下78行:

int main(int argc, char *argv[]) 
{
	console_init();
	cpu_init();
	device_init();
	machine_init();
	timer_init();
	useremul_init();
	emuls = malloc(sizeof(struct emul *));
	if (emuls == NULL) {
		fprintf(stderr, "out of memory\n");
		exit(1);
	}
	emuls[0] = emul_new(NULL);
	if (emuls[0] == NULL) {
		fprintf(stderr, "out of memory\n");
		exit(1);
	}
	get_cmd_args(argc, argv, emuls[0], &diskimages, &n_diskimages);
	if (!skip_srandom_call) {
		gettimeofday(&tv, NULL);
		srandom(tv.tv_sec ^ getpid() ^ tv.tv_usec);
	}
	debug("GXemul");
	debug("    Copyright (C) 2003-2006  Anders Gavare\n");
	debug("Read the source code and/or documentation for other Copyright messages.\n\n");
	if (emuls[0]->machines[0]->machine_type == MACHINE_NONE) {
		n_emuls --;
	} else {
		for (i=0; i<n_diskimages; i++) diskimage_add(emuls[0]->machines[0], diskimages[i]);
	}
	if (n_emuls > 0) {
		for (i=1; i<argc; i++)
		        if (argv[i][0] == '@') {
			        fprintf(stderr, "You can either start one "
			             "emulation with one machine directly from "
			             "the command\nline, or start one or more "
			             "emulations using configuration files."
			             " Not both.\n");
			        exit(1);
		        }
		emul_simple_init(emuls[0]);
	}
	for (i=1; i<argc; i++) {
		if (argv[i][0] == '@') {
			if (strlen(s) == 0 && i+1 < argc && argv[i+1][0] != '@') {
				i++;
				s = argv[i];
			}
			n_emuls ++;
			emuls = realloc(emuls, sizeof(struct emul *) * n_emuls);
			if (emuls == NULL) {
				fprintf(stderr, "out of memory\n");
				exit(1);
			}
			console_allow_slaves(1);
			if (n_emuls == 1) {
				emul_destroy(emuls[0]);
			}
			emuls[n_emuls - 1] = emul_create_from_configfile(s);
			snprintf(tmpstr, sizeof(tmpstr), "emul[%i]", n_emuls-1);
		}
	}
	if (n_emuls == 0) {
		fprintf(stderr, "No emulations defined. Maybe you forgot to "
		     "use -E xx and/or -e yy, to specify\nthe machine type."
		     " For example:\n\n    %s -e 3max -d disk.img\n\n"
		     "to boot an emulated DECstation 5000/200 with a disk "
		     "image.\n", progname);
		exit(1);
	}
	device_set_exit_on_error(0);
	console_warn_if_slaves_are_needed(1);
	emul_run(emuls, n_emuls);
	console_deinit();
	for (i=0; i<n_emuls; i++) emul_destroy(emuls[i]);
	return 0;
}

3、现在看起来比较清爽了,但是仍然不够,因此我们进行第三轮精简,去掉各种调试和打印语句,还剩下52行:

int main(int argc, char *argv[]) 
{
	console_init();
	cpu_init();
	device_init();
	machine_init();
	timer_init();
	useremul_init();
	emuls = malloc(sizeof(struct emul *));
	emuls[0] = emul_new(NULL);
	get_cmd_args(argc, argv, emuls[0], &diskimages, &n_diskimages);
	if (!skip_srandom_call) {
		gettimeofday(&tv, NULL);
		srandom(tv.tv_sec ^ getpid() ^ tv.tv_usec);
	}
	if (emuls[0]->machines[0]->machine_type == MACHINE_NONE) {
		n_emuls --;
	} else {
		for (i=0; i<n_diskimages; i++) diskimage_add(emuls[0]->machines[0], diskimages[i]);
	}
	if (n_emuls > 0) {
		for (i=1; i<argc; i++)
		        if (argv[i][0] == '@') {
			        exit(1);
		        }
		emul_simple_init(emuls[0]);
	}
	for (i=1; i<argc; i++) {
		if (argv[i][0] == '@') {
			if (strlen(s) == 0 && i+1 < argc && argv[i+1][0] != '@') {
				i++;
				s = argv[i];
			}
			n_emuls ++;
			emuls = realloc(emuls, sizeof(struct emul *) * n_emuls);
			console_allow_slaves(1);
			if (n_emuls == 1) {
				emul_destroy(emuls[0]);
			}
			emuls[n_emuls - 1] = emul_create_from_configfile(s);
		}
	}
	if (n_emuls == 0) {
		exit(1);
	}
	device_set_exit_on_error(0);
	console_warn_if_slaves_are_needed(1);
	emul_run(emuls, n_emuls);
	console_deinit();
	for (i=0; i<n_emuls; i++) emul_destroy(emuls[i]);
	return 0;
}

4、一般来说,超过一屏的函数或多或少都会影响可读性,因此我们需要进行第四轮精简,去掉各种异常处理语句,还剩下43行:

int main(int argc, char *argv[]) 
{
	console_init();
	cpu_init();
	device_init();
	machine_init();
	timer_init();
	useremul_init();
	emuls = malloc(sizeof(struct emul *));
	emuls[0] = emul_new(NULL);
	get_cmd_args(argc, argv, emuls[0], &diskimages, &n_diskimages);
	if (!skip_srandom_call) {
		gettimeofday(&tv, NULL);
		srandom(tv.tv_sec ^ getpid() ^ tv.tv_usec);
	}
	if (emuls[0]->machines[0]->machine_type == MACHINE_NONE) {
		n_emuls --;
	} else {
		for (i=0; i<n_diskimages; i++) diskimage_add(emuls[0]->machines[0], diskimages[i]);
	}
	if (n_emuls > 0) {
		emul_simple_init(emuls[0]);
	}
	for (i=1; i<argc; i++) {
		if (argv[i][0] == '@') {
			if (strlen(s) == 0 && i+1 < argc && argv[i+1][0] != '@') {
				i++;
				s = argv[i];
			}
			n_emuls ++;
			emuls = realloc(emuls, sizeof(struct emul *) * n_emuls);
			console_allow_slaves(1);
			if (n_emuls == 1) {
				emul_destroy(emuls[0]);
			}
			emuls[n_emuls - 1] = emul_create_from_configfile(s);
		}
	}
	emul_run(emuls, n_emuls);
	console_deinit();
	for (i=0; i<n_emuls; i++) emul_destroy(emuls[i]);
	return 0;
}

对于一个熟练的开发者来说,该函数的逻辑精简到了这个状态以后已经比较清晰了(可以到此为止)。但如果是初次接触的话,还是相对显得有点复杂。

5、让我们来进行第五轮精简,去掉那些不常用的、罕见的代码路径,剩下18行:

int main(int argc, char *argv[]) 
{
	console_init();
	cpu_init();
	device_init();
	machine_init();
	timer_init();
	useremul_init();
	emuls = malloc(sizeof(struct emul *));
	emuls[0] = emul_new(NULL);
	get_cmd_args(argc, argv, emuls[0], &diskimages, &n_diskimages);
	for (i=0; i<n_diskimages; i++) diskimage_add(emuls[0]->machines[0], diskimages[i]);
	if (n_emuls > 0) emul_simple_init(emuls[0]);
	emul_run(emuls, n_emuls);
	console_deinit();
	for (i=0; i<n_emuls; i++) emul_destroy(emuls[i]);
	return 0;
}

这就是最终剩下的主脉络!非常清晰明了,那么,这个函数到底在干什么呢?让我们开始“三部曲”的第三步。

顾名思义看功能

前面我们提到,代码就像一棵树,因此本文选用一种树形视图来表示函数。依旧以GXemul为例,前面经过五轮精简的main()函数,稍作处理后可以按下面的方法表示:

	main()
      |-- console_init();
      |-- cpu_init();
      |-- device_init();
      |-- machine_init();
      |-- timer_init();
      |-- useremul_init();
      |-- emuls[0] = emul_new(NULL);
      |-- get_cmd_args(argc, argv, emuls[0], &diskimages, &n_diskimages);
      |-- for (i=0; i<n_diskimages; i++) diskimage_add(emuls[0]->machines[0], diskimages[i]);
      |-- emul_simple_init(emuls[0]);
      |-- emul_run(emuls, n_emuls);
      |      |-- console_init_main(emuls[0]);
      |      |-- for (j=0; j<e->n_machines; j++) cpu_run_init(e->machines[j]);
      |      |-- timer_start();
      |      |-- for (j=0; j<e->n_machines; j++) machine_run(e->machines[j]);
      |      |-- timer_stop();
      |      |-- for (j=0; j<e->n_machines; j++) cpu_run_deinit(e->machines[j]);
      |      \-- console_deinit_main();  
      |-- console_deinit();
      \-- emul_destroy(emuls[i]);

其中main()函数为根节点,五轮精简后的每一行为根节点的下级节点,进一步展开感兴趣的下级节点(如树形视图中的emul_run()函数),可以得到更下一级的叶子节点。树形视图中的每行代码,甚至不需要看其实现细节,光靠“顾名思义”就能大概知道其功能了。比如console_init()是控制台初始化,cpu_init()是处理器初始化,device_init()是设备初始化,machine_init()是机器架构初始化,timer_init()是时钟初始化,useremul_init()是用户模拟器初始化,diskimage_add()是添加磁盘设备,emul_simple_init()是模拟机器的初始化,而emul_run()显然是核心中的核心,即模拟器运行的主事件循环(通过进一步展开的下级节点也证实了这一点)。

树形视图是一种自顶向下鸟瞰全局的宏观视图,但有时候我们特别希望能有一种方法能够清晰地表述某个很深的函数调用,因此作为补充,本文推荐一种叫链式视图的表示法。比如Radeon显卡驱动中的模式设置函数drm_crtc_helper_set_mode(),在首次模式设置中从驱动入口函数开始一路向下的调用链展示如下:

这是一条很长的调用链,非常具有典型意义。

在很多解析源代码的书籍中,都会使用流程图来描述代码逻辑。然而,流程图虽然直观,但是其描述能力有限(尤其是缺乏树形源代码视图的层次化表达能力),往往很难精确描述一个函数的执行过程。而一个费尽心机画出来的精确的流程图,往往又会因为其复杂性而失去了直观的功能。并且,单靠流程图并不能完全理解源代码,而是需要将源代码与流程图两相对照。

因此用精简版的源代码(即树形视图和链式视图)来代替流程图,一方面可以快速理解多级函数的复杂调用关系,另一方面可以不需要在源代码和流程图之间反复切换。

理解补丁文件

阅读软件源代码,尤其是Linux内核源代码时,不可避免地要接触补丁文件,那么什么是补丁文件呢?其实,补丁文件就是一个变更集,它描述了源代码从旧版本到新版本之间的差异变化,或者更一般地说,描述了源代码从一个状态变到另一个状态的差异(不一定是从旧版本到新版本)。

如果用数学方法来表达,就是:

公式1:源代码差异 = 源代码状态B – 源代码状态A

也可以反过来表达:

公式2:源代码状态A + 源代码差异 = 源代码状态B

举个具体的例子,假设当前目录下有两个子目录linux-4.4.1和linux-4.4.2,分别是Linux-4.4.1版本和Linux-4.4.2版本的源代码顶级目录。那么可以用diff命令来执行公式1的过程,导出一个变更集(即源代码差异)到补丁文件kernel.patch:

diff -Naurp linux-4.4.1 linux-4.4.2 > kernel.patch

接下来可以先进入linux-4.4.1目录,用patch命令来执行公式2的过程,通过应用补丁文件kernel.patch将Linux-4.4.1的源代码状态变成跟Linux-4.4.2一致:

patch -p1 < kernel.patch

上面是对补丁文件的正向应用,使源代码状态A变成源代码状态B。实际上补丁文件还可以反向应用,使源代码状态从B变成源代码状态A。比如先进入linux-4.4.2目录,然后通过反向应用补丁文件kernel.patch将Linux-4.4.2的源代码状态变成跟Linux-4.4.1一致:

展开阅读全文

本文系作者在时代Java发表,未经许可,不得转载。

如有侵权,请联系nowjava@qq.com删除。

编辑于

关注时代Java

关注时代Java