杂文笔记

    返回首页    发表留言
本文作者:李德强
          千万别小瞧基本功
 
 

        所谓:“台上一分钟,台下十年功。”或许很多人还不太能体会这句话。我觉得这句话大有道理,它不但揭示了演员在幕后的所付出艰辛的努力,也说明了几十年如一日的基本功不是所有人都能做到的。这句话也同样适合其它任何事情,想要把任何一件事情做好都要付出多年的努力。

        我曾经在《编程是一门艺术》中讲过这样一个故事:20世纪初,美国福特公司的一台电机出了毛病,为了减少损失,需要在不停机的情况下把毛病找出来。公司里的技术人员怎么也找不出毛病在哪儿,最后,只好请来原德国技术专家斯坦门茨。斯坦门茨在电机房躺了三天,听了三天,要了一架梯子,一会儿爬上去,一会儿爬下来,最后在马达的某一个部位用粉笔画了一道线,并说:“打开电机,在记号处把里面的线圈减少16圈。”人们照办后,毛病果然消除了。斯坦门茨的解决办法非常简单,但在这简单的背后却有他非常多的知识和经验做理论支撑。

        这个故事对很多人来说可能很遥远,下面我来讲一个发生在我自己身上的小故事:我所就职的公司正在使用一套开源的操作系统,和一套开源的软件程序。但最近程序有一个比较奇怪的问题:程序可以正常运行,但执行操作系统的一个free(统计内存使用情况)命令时,执行系统命令的终端控制台就会卡死,再也无法执行其它任何命令。我通过阅读开源系统的内核代码,并通过应用程序的历史版本排查,终于把这个问题解决了。解决的办法很简单,我只修改了一行代码:把一个bool类型的变量修改成int型。

        或许很多朋友并不觉得这个故事有什么深刻的意义,把一个变量的类型修改一下有什么难度?谁都会修改。可是,多数人并不知道为什么要这样做,更不知道在这背后所包含的基本功。

        所以,我的故事还没有讲完,让我来给大家讲讲这里面的复杂内容。让我们从问题着手,来看看我是如何解决这个问题的:

        首先,我们知道free命令是操作系统用于统计进程内存使用情况的命令,这个命令会统计每一个进程在运行过程中所申请的内存大小。执行free命令卡死,我们就要先从这个命令入手,在free命令统计内存时,我们首先要知道申请内存的原理,也就是malloc函数的底层实现,有兴趣的读者请参见《内存申请》。进程在申请内存时,系统会找到一块满足于申请内存大小的内存空间,并将前4个字节用于记录这块内存空间的大小,而将这4个字节后的内存地址返回给进程,如下图:

        我们可以看到三个进行申请的内存空间分别为8字节、4字节的和8字节。他们调用malloc函数所返回堆内存地址分别为2004、2016、2026,因为在这些内存地址的前4个字节中分别记录了这4个内存空间的大小,也就是8、4、8。

        那么free命令在统计这些使用内存占用时,就需要从内存堆内存的起始位置2000处开始统计,首先找到第一块内存,大小为8,地址为2004,于是在2004的基础上加上8就找到了下一块内存,大小为8,地址为2012,如此循环下去,就找到了所有内存申请的大小(当然还有一些更细节的问题,例如内存释放和碎片整理等等,我们暂不考虑),当我们执行循环统计时总是在当前内存块地址的基础上,加上当前内存块的大小,而得到下一内存块的大小和内存地址。而当内存地址大于整个分配内存空间时,则循环结束,代码如下:

//do something
for (	node = heap->mm_heapstart[region]; 
	node < heap->mm_heapend[region]; 
	node = (struct mm_allocnode_s *) ((char*) node + node->size))
{
	//其中有一个node->size的值为0,导致死循环
	//do something
}
//do something

        问题就出在这个循环统计里,我通过排查发现,其中有一个内存块明明是正常通过malloc申请内存,但是用于存放其大小的4个字节的值却是0,由于node->size是0,所以node + node->size的结果永远不变,所以程序无法结束循环。这就导致了free命令的卡死现象。

        进程通过malloc申请内存时,系统已经在返回内存地址的前4个字节中记录了申请内存的大小,以便为内存释放和内存统计做准备。而在进程使用方面,进程申请内存成功后可以使用这些内存地址。注意,进行虽然申请了一个内存块,但进程可以任意操作这内存块中的内存,甚至可以超出申请内存大小的范围。而操作系统则以“信任程序员”原则相信使用者会正确并合理的使用这些内存,所以操作系统并不会去校验和控制这些超出申请范围的内存操作。其实并不是申请内存块大小原本就是0,而是原本是一个有效的、用于记录内存块大小的数,被另一个申请内存的进程由于越界使用内存而置成了0,所以导致了free的卡死。于是我们找到了问题的原因,但如何去找到越界操作内存的代码呢?

        我查找了所有进程的所调用的malloc函数,并在调用时多传入一个编号,以便记录下每一个进程在调用malloc时所申请的内存地址和大小是多少,相借此寻找是哪一个进程所申请的内存大小被清除了。结果很失望,竟然没有任何程序申请了那个块大小为0内存地址。于是我再次陷入困境。

        由于进程程序是由C和C++混合编程,我忽然意识到除了在C语言中显式的调用malloc函数之外,C++中所有通过new来创建类的对象在被编译器编译之后,底层同样是调用的malloc函数。但是这种隐式调用无法通过传入编号来确定内存申请的进程和地址,况且程序中的类和对象非常多,无法直接定位到问题代码,我只好通过其它办法来解决。

        值得庆幸的是程序中早期的版本并没有现过这种问题,于是定位问题就有了新的方向,我通过折半查找的办法(程序有几百个历史版本,一个一个顺序查找,编译,运行,找问题是很慢的)在程序的历史库中找到没有出现这问题的最近版本,并与下一个版本(出现问题的版本)做比较,看看问题到底出在哪里。

左侧程序的free命令正常,右侧程序free命令卡死

        可以看到在没有问题的版本中类的成员变量多了一个4字节的数组char data[4],而下一个版本正是去掉了这个数组之后则出现了free卡死的问题。从表面上来说我们找到了问题的所在,但为什么会出现这一现象还是不得而知。

        故事还在继续……

        其实问题并不是出在这4个字节的数组上,而是出现在它前一个成员变量bool enabled;上。我们来看一下程序中为其赋值的函数

void getEnable(int *value)
{
	*value = some_value;
}

        大家要注意,getEnable(int *value)函数的参数是一个int *型的变量,在对这个指针解引用赋值时,编译器认为这个地址是一个占用4个字节的变量的地址。而事实上bool enabled变量只占1个字节,问题的根源就在这里。getEnable(int *value)函数在对bool enabled变量修改值时,是对其所在的内存地址做值的修改,由于是这个函数将enabled变量所在的内存当做一个占有4字节的int型变量来处理,所以原本在enabled下面的data[0]、data[1]、data[2]就被当作一个占用4个字节的int型变量来处理了,所以数组data[0]、data[1]、data[2]的值就被修改成了0。这对free命令的内存统计并没有什么影响,因为操作的内存是类的内部成员的内存地址,所以只会影响到类中使用到data这个数组的功能。而对于新版本中去掉了这个data数组之后,enabled则成为了这个类最后的一个成员变量,其后则是其它程序申请内存地址的大小。于是getEnable(int *value)函数把原本占用1个字节的变量当做占用4个字节来操作,就将原本存放申请内存块大小的内存修改了成了0。于是就导致了free命令的卡死。

 

        左侧程序中,对内存的越界使用只影响到类的内部成员data[0]、data[1]、data[2](蓝色部分);而右侧程序中对内存的越界使用则影响到了下一个内存区域中用于记录内存块大小的区域(蓝色部分,将原来的8变成了0。所以就导致了free命令的卡死。最后,我们想要解决free卡死的这个问题,只需要将bool enabled的类型修改成int类型,或将getEnable(int *value)的参数修改成bool *类型,问题就迎刃而解。当然,这种越界操作内存而将申请内存块大小变为0的问题不仅仅会使free命令卡死,还会使内存释放功能无法正确执行,从而导致一些系统内存使用的潜在问题。而上面例子中内存越界操作只影响到了类边界外的3个字节的内存地址,但对于更多内存越界时,就会有更多不易发觉的潜在问题。由于操作系统采用了“信任程序员”原则,所以我们在编写程序时就更要格外小心使用这些内存地址。

        解决这一问题我们只修改了一行代码,但整个解决问题的背后却有这如此多的知识作为根基。所以千万别说:就只修改一行代码有什么了不起的?千万别小看了这些基础知识,千万别说这东西没用。看完了上面的例子如果还有朋友这样想,就说明还没有体会到这些基本功的真正力量。学好《计算机基础》,学好《计算机体系结构》,学好《C/C++》,学好《数据结构》、学好《操作系统》学好所有该学的基础知识,打好扎实的基本功。

        记住文章开头的那句话:台上一分钟,台下十年功!

    返回首页    返回顶部
  看不清?点击刷新

 

  Copyright © 2015-2018 问渠网 辽ICP备15013245号