Chinaunix首页 | 论坛 | 博客
  • 博客访问: 976353
  • 博文数量: 102
  • 博客积分: 10120
  • 博客等级: 上将
  • 技术积分: 2754
  • 用 户 组: 普通用户
  • 注册时间: 2006-09-13 23:00
文章分类

全部博文(102)

文章存档

2011年(6)

2010年(55)

2009年(16)

2008年(25)

分类: C/C++

2009-09-21 19:05:10

事件经过

事情还得从最近的一次hack过程说起。最近要写一个运行在Blackfin 561 uCLinux上的多进程程序。Blackfin 561是一个双核心的DSP处理器,为了提高性能,我希望能把进程绑定到处理器核心上,Linux用于实现这一功能的两个系统调用 是:sched_getaffinity()和sched_setaffinity()(这两个系统调用不是POSIX规定的,而是Linux中特有 的),与此相关的还有四个宏:CPU_ZERO(),CPU_SET(),CPU_CLR(),CPU_ISSET()。

在动手编写代码之前,我查了uCLinux 4 Blackfin的源代码,确认里面包含了对这两个系统调用的实现。同时,我也检查了uCLinux所用的uCLibc库,可惜的是,uCLibc没有包 含调用sched_getaffinity()和sched_setaffinity()的封装代码,这意味着,除非我进行某种程度的hack,否则我是 不能直接在应用程序里面调用sched_getaffinity()和sched_setaffinity()的。我手上没有uCLibc的源代码,因此 没法直接修改uCLibc,不过这不要紧,因为Linux中还有syscall()系统调用,syscall()赋予了应用程序开发者这样一种能力:以间 接的方式调用一个系统调用。这是什么意思呢?众所周知,系统调用是通过软件中断或者软件异常来实现的,每个系统调用都有一个系统调用号,只要知道了这个系 统调用号,那么执行一个系统调用无非就是产生一个软件中断或者软件异常罢了,syscall()干的就是这么个“为虎作伥”的事儿。因此我的好消息 是:uCLinux 4 Blackfin中实现了syscall(),同时uCLibc中也有syscall()的封装代码。这样一来,我只要搞清楚 sched_getaffinity()和sched_setaffinity()的系统调用号就OK了。

搞清楚sched_getaffinity()和sched_setaffinity()的系统调用号不难,因为它们必定是在uCLibc的某个头 文件中,于是我一路辗转,终于揪出了这两个“反革命分子”的狐狸尾巴。它们的系统调用号定义在中:

#define __NR_sched_setaffinity 241
#define SYS_sched_setaffinity __NR_sched_setaffinity
#define __NR_sched_getaffinity 242
#define SYS_sched_getaffinity __NR_sched_getaffinity

我核对了一下,和内核中定义的系统调用号是吻合的,因此方案可行。

需要事先说明的是,尽管系统调用号是定义在中的,但是的注 释非常清楚地表明了此头文件是自动生成的,应用程序不应该直接包含此头文件,而应该考虑包含文件。

系统调用号落实了之后,接下来考虑两个系统调用的函数原型和要用到的四个宏,这是在中声明的。比较奇怪的是,这部分 声明是受__USE_GNU宏控制的,只有当定义了__USE_GNU时,才会声明这两个函数和相关的四个宏。同时,还需说明的是,CPU_SET()等 四个宏实际上是另外四个宏的别名,比如,CPU_SET()就是__CPU_SET ()的别名:

#define CPU_SET(cpu, cpusetp) __CPU_SET (cpu, cpusetp)

而__CPU_SET()又是在中定义的。而中定义 __CPU_SET()的条件是已经定义_SCHED_H并且__cpu_set_t_defined未定义,也就是说,__CPU_SET()的定义是 包含在下述条件编译指令中的:

#if defined _SCHED_H && !defined __cpu_set_t_defined
/* 定义四个宏,以及cpu_set_t类型 */
#endif

之所以要求_SCHED_H已经定义,同样是因为不允许应用程序直接包含,而只能通过包 含访问中的内容。至于要求__cpu_set_t_defined未曾定义,则 是为了确保不会出现重复定义。

以上这些,看起来都中规中矩,没什么好指摘的,因为基本上所有的C函数库都是通过这些手段来确保不会直接包含一个文件、不会出现重复定义的。然而麻烦稍后就到。

我新建了一个文件bfin_sched_affinity.c用于存放采用syscall()实现的sched_getaffinity()和 sched_setaffinity()。包含的两个头文件是,如前所述, 这引入了系统调用号和系统调用的函数原型。sched_setaffinity()在bfin_sched_affinity.c中的实现看起来像下面这 样:

int sched_setaffinity(pid_t pid, unsigned int cpusetsize, cpu_set_t *mask)
{
return syscall(SYS_sched_setaffinity, pid, cpusetsize, mask);
}

文件bfin_sched_affinity.c正常编译,看起来似乎一切正常。

为了验证hack出来的sched_getaffinity()和sched_setaffinity()是否正常工作,我再写了一个测试程序 test_bfin_affinity.c,用来调用sched_getaffinity()和sched_setaffinity(),看它们是否能工 作。自然,为了能够访问到sched_getaffinity()和sched_setaffinity()的函数原型和相关的几个 宏,test_bfin_affinity.c也包含了。将 test_bfin_affinity.c编译为test_bfin_affinity.o正常通过了。但是当我试图把 test_bfin_affinity.o和bfin_sched_affinity.o像下面这样链接成为可执行的程序时,编译器报告发生了错误:

bfin-uclinux-gcc -o test_bfin_affinity test_bfin_affinity.o bfin_sched_affinity.o

编译器报告的错误情况是_CPU_ZERO和_CPU_ISSET是未定义的引用。显然,编译器把CPU_ZERO()和CPU_ISSET()当成了函数,但实际上它们是宏!

一开始我怀疑是因为没有定义__USE_GNU造成的,于是在包含之 前define了__USE_GNU,然而错误依旧。接下来,我陷入了和C预处理器的残酷搏斗中,我尝试遍了所有可能的组合,然而结果无非是“按下葫芦起 了瓢”,补上了这里的窟窿,那里又出问题了。一筹莫展!

我最后是以一种非常dirty的方式work around这个难缠的问题的。我新建了一个bfin_sched_affinity.h文件,在其中定义了所有需要用到的四个宏。在 test_bfin_affinity.c中,包含进bfin_sched_affinity.h,而本应包含在 test_bfin_affinity.c中的则被去掉了。通过这样的方 式,我hack出来的sched_getaffinity()和sched_setaffinity()终于可以工作了,但是最终采用的这个 workaround是如此的dirty,以至于不能不让人感到非常的沮丧。

当然,之所以出现这种让人沮丧的结果,归根结底,还是我的懒惰造成的。如果不“偷懒”,老老实实地修改uCLibc的话,也许过程不至于这么曲折。 但是,排除开这些因素来看的话,我们不得不得出的一个结论是:C语言的预处理的确是存在不少弊端的。关于这个问题,有太多的著作提供了有说服力的佐证。在 此之前,我虽然对C语言的预处理存在这样那样的问题有所了解,但这一次的经历却让我对这个问题有了切身的体会,同时这也让我对那些应用得最为广泛的C库的 开发人员与维护人员充满了敬意,为了保证最大限度的可移植性和健壮性,要和多少晦涩难懂的条件编译、宏定义打交道啊!

就我个人的观感而言,我觉得C语言的预处理至少存在这样一些弊端:

  1. 不清晰,不直观,不易读
  2. 容易产生依赖
  3. 容易出现重复定义
  4. 错误扩散、难以定位诊断
  5. 没有类型检查
  6. 潜在的副作用

不清晰,不直观,不易读

对于简单像min()、max()这样的宏,可能不存在清晰性、易读性的问题。但是当宏定义又大又长的时候,清晰性、易读性就大打折扣了。条件编译 指令也是清晰性、易读性的大敌,由于条件编译可以嵌套,因此当出现多层嵌套的条件编译指令的时候,要理解清楚每一个条件编译指令背后的理由就不是那么容易 的事情了,更不要说当条件编译的作用范围在屏幕上翻几屏都显示不完的情况。

容易产生依赖

头文件之间可能产生依赖,宏定义之间也可能存在依赖。如果头文件之间存在依赖,那么就对头文件的包含顺序有了隐含的要求,这对于使用头文件的人是一 个不好的负担,这方面的例子是GNU的MPFR库,当需要使用一些特别的功能时,MPFR库经常要求在包含头文件之前包含诸 如这样的头文件,让库的使用者不得不记住这些琐碎的规矩是让人很恼火的事情。

由于C语言允许在一个宏定义中使用一个已经定义的宏定义,这就能导致宏定义之间的依赖,宏定义依赖招致的结果是改动困难。

容易出现重复包含和重复定义

如果一个源文件多次包含同一个文件,就可能出现头文件的重复包含,而重复包含最大的问题就是会导致重复定义,有一些技巧用于解决头文件的重复包含。

错误延迟、难以定位诊断

预处理先于编译和链接过程,因此预处理时的错误有可能会延迟到编译或者链接时才出现,这导致预处理错误很难定位。比如,本文中编译器报告的错误是 _CPU_ZERO和_CPU_ISSET属于未定义的引用,但这并不是错误的真正原因,错误的真正原因是未能满足条件编译指令所需要的条件从而导致 CPU_ZERO()和CPU_ISSET()的定义被跳过,让编译器以为CPU_ZERO()和CPU_ISSET()是两个将要由其它源文件提供的函 数。本来,如果预处理器足够强大,这个错误应该由预处理器发现,但可惜的是,C语言的预处理器没有那么聪明。

没有类型检查

预处理缺乏类型检查,这是人所共知的事实。由于没有类型检查,那么宏是否是对正确的类型进行操作只有在程序实际运行过程中才能知道,然而问题是,天 晓得程序在运行时会传些什么东西给宏呢。预处理缺乏类型检查给程序留下了极大的安全隐患,一个错误类型会不会导致程序崩溃,这可谁也说不准。

潜在的副作用

当定义有参数的宏定义时,都会在宏定义内用括号把参数括起来,这是为了避免宏展开的结果和预期的不一致,这是宏具有的诸多副作用中的一种。其它不太明显的副作用包括:传入的宏参数是自增运算或者函数调用,在?:中重复求表达式的值也是一种常见的副作用。

对策

对于预处理,最重要的对策就是:能不用就尽量不用。这一条对于提高可读性、便于定位错误、保证类型检查、消除副作用都是适用的。如果实在无法避免, 那么首先要确保宏定义或者条件编译短小、简单、易理解,同时要尽量消除一切可能的潜在副作用,有时这是非常难以察觉的。对于重复包含的问题,C语言常见的 处理办法是:
#ifndef xxx
#define xxx

/* 头文件内容 */

#endif /* end xxx */

一定要确保xxx的唯一性,这通常是由头文件的文件名变形得到。

阅读(2736) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~