论坛首页 编程语言技术论坛

c开发策略-之-错误处理

浏览 14050 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2007-06-27  
C

在使用任何语言进行应用程序开发时,我们都应该提前规划好如何处理错误。Java和c++中普遍使用异常来进行错误处理,但是c语言,因为没有提供一个很优雅的异常机制,所以明确如何进行错误处理显得很重要。C语言中的错误处理有多种方式,总结如下:大家可以讨论这些处理方式的优劣,这样等以后在程序开发中,我们可以从整体上为程序设计更好的错误处理方法。

1. 返回值方式:用函数的返回值标志函数是否执行成功。比如成功返回1,失败返回0。这种方式的好处是简单方便,而且不影响效率,保持了c语言的高效率。但是仍然有问题,一个问题是代码可读性的问题,如果每个函数都有这样的返回值的话,为了保持程序的正确运行,我们必须对每个函数进行正确性验证,就是在调用函数的时候检查他的返回值,这样程序代码很大一部分就可能花费在错误处理上。第二个问题就是函数的返回值冲突的问题。假设strlen函数也可能会出错,使用这种错误处理策略他的返回值应该标志它是否执行成功,但是函数计算的字符串的长度值如何自然地传递出来?最后一个问题可能是最重要的:它不强制你处理错误,而且在不进行处理的情况下,程序仍然能够运行,但结果是不可预知的。

2. 全局errno方式:就是在出现错误的时候,将错误代码记录到一个全局变量errno中。比如waitpid()函数在被信号中断的情况下,将errno设置为EINTR(一宏定义常量)。这种方式解决了返回值方式遇到的返回值冲突问题,而且效率方面也是非常令人愉悦的。但是它要求用户在调用函数后检查errno的值,这种保证是脆弱的,程序仍然有可能在不处理那些errno的情况下”安然”地运行,导致未定义的结果。另一个问题出在多线程方面,errno不是线程安全的,多个线程操作同一个errno会造成混乱。

3. 错误封装:就是将每个有错误返回值的函数分别用一个函数包起来,比如waitpid()函数可以封装成Waitpid()(首字母大写),在这个函数中处理相应的错误。这种错误处理方法可以很好的解决很多问题,应该说效果很好,但是有几个方面需要商榷,一是,并不是每个函数的错误都以一种方式进行处理,另一方面,听说c语言的函数调用开销相对很高,在函数外面再包上一层会影响性能。

4. 异常:关于异常的说明和实现可以参考http://xombat.iteye.com/admin/show/94540,它的优点是能模拟实现c++中异常的一些优点。但是这个异常机制很脆弱,使用时要注意很多问题,而且它的性能开销肯定也会不小。

5. Goto语句:,当发生错误时,利用goto语句跳到相应的错误处理函数中。因为一直以来对goto语句的偏见,和goto语句本身对程序结构性的影响,所以本人一直以来没有用过这种方式,也不知道这种方式会有什么优劣。

总的来说,每个方式都不是尽善尽美的,不知道大家遇到这些问题是怎么处理的?

另外希望还可以讨论如下问题:

1. C语言中的函数调用是不是像一些人说的那样成本很高?

2. 错误处理应该如何区分用户错误和应用程序错误?

3. 如果采用错误封装的方法(方法3),错误处理需要程序结束的地方应该使用exit还是abort?

4. 最好的方法应该是混合使用吧,(比如返回值的方法和全局errno的方法经常混合使用)该用返回值的时候用返回值,该异常的时候异常,但是什么时候该用返回,什么时候该用异常?

   发表时间:2007-06-28  
系统实现的errno是线程安全的,它使用了线程专有存储,你自己实现的的errno也应该这样。返回值方式和errno方式相似,不过通常一些库分返回-1并设置errno,比如socket的recv,因为它还要返回0和正数值,另外使用一个errno就可以把错误号设置为任意值,否则只能使用负数了。

“错误封装”我没看出来这个封装的好处,只是业务特定的错误处理方法?如果是作为库提供,封装一定不能把灵活性、效率任意一方面给封没了。

引用

1. C语言中的函数调用是不是像一些人说的那样成本很高?

函数调用的成本主要是参数压栈、跳转,我一直不认为这个问题很明显呢,现在的计算机都这么强,而且不是经常听说不要过早优化嘛。

我前2天在维护同事的一份代码,一个长函数,600多行,一堆goto,现在被我拆得每个函数都不超过20行,函数调用效率上的降低是难以察觉的,而且这个程序本来CPU占用也不到10%。
引用

2. 错误处理应该如何区分用户错误和应用程序错误?

一般对于数据处理、服务器程序,都要确定对于输入数据的错误如何处理,简单丢掉或者程序退出都是可能的选项。应用程序错误?如果是磁盘满、数据库连接不上等等,依旧要确定程序如何动作。通常对于一个不间断运行的程序,任何错误都要写日志,不同的是根据程序的需求确定日志的错误级别。
引用

3. 如果采用错误封装的方法(方法3),错误处理需要程序结束的地方应该使用exit还是abort?

我依旧看不出这种封装的好处,无论是exit还是其它操作,你这个封装都是个降低灵活性的,通常一个程序退出还要处理已经打开的资源。

引用

4. 最好的方法应该是混合使用吧,(比如返回值的方法和全局errno的方法经常混合使用)该用返回值的时候用返回值,该异常的时候异常,但是什么时候该用返回,什么时候该用异常?

C不是没提供异常嘛,C++提供了,不过效率很低的。什么时候使用什么方式取决于编码风格、效率要求等因素。
0 请登录后投票
   发表时间:2007-06-28  
KISS is one of the principles of C.
errno and negative return value compose a perfect solution as can be seen in kernels.
try/catch is by no means elegant as far as C is concerned. Imagine someday you are asked to inline Java code in C.
As I've mentioned, the kernel _can_ be designed so that exception stack is open to user space. Hence you will see try/catch is no longer valuable.
However, I don't like the idea of make it up in user space, though it looks cute.
BTW, sometimes panic routine is the way out.
0 请登录后投票
   发表时间:2007-06-28  
各种函数调用方式的开销差不多,总的来说比if等分支语句要小。所以不必担心函数调用的开销。

在c程序里用goto实现类似于异常中的catch和final还是很方便的。只要运用得当,goto能够使程序的流程和结构更清晰。
0 请登录后投票
   发表时间:2007-06-28  
to qiezi:
引用
“错误封装”我没看出来这个封装的好处,只是业务特定的错误处理方法?

错误处理封装函数最初是在史蒂芬的《unix网络编程:unix APIs》中被提出的,用处很广。
典型的一个错误处理函数Fork()(首字母大写的,区分系统函数fork()):
pid_t Fork(void)
{
  pid_t pid;
  if((pid = fork())<0)
  {
    fprintf(stderr,"Fork error: %s\n",strerror(errno));
    exit(0);
  }
  return pid;
}

依次将所有unix系统调用级函数全部封装,posix函数的封装方法有所不同,他将错误情况存储在code中而不是errno中。

引用
如果是作为库提供,封装一定不能把灵活性、效率任意一方面给封没了

我不认为它能够被当作库来提供,提供这些封装函数只是用来解决处理错误而导致的代码臃肿和代码可读性下降的种种问题,另外因为在不同的情况下对错误的处理情况可能会不同,所以将它们用作库不合适,但可以作为一种错误处理的好的解决方案。

引用
系统实现的errno是线程安全的,它使用了线程专有存储

qiezi有这方面的资料不?我一直以为errno就是一个全局整型变量呢

引用
通常一个程序退出还要处理已经打开的资源

操作系统自己会回收已退出的应用程序的资源,为什么我们还要操心。我一直以来在exit之前都没有考虑过回收资源的问题,要不你怎么解决?

引用
C不是没提供异常嘛

这里实现了一个:http://xombat.iteye.com/admin/show/94540
0 请登录后投票
   发表时间:2007-06-28  
to netpcc:
引用
各种函数调用方式的开销差不多,总的来说比if等分支语句要小

函数调用要压栈,call,ret,出栈,典型的if语句只要cmpl,jmp就行了,函数调用比if开销小这还是第一次听说。

引用
在c程序里用goto实现类似于异常中的catch和final还是很方便的

goto语句能从一个函数调到另一个函数吗?所以他根本无法模拟异常的好处。
0 请登录后投票
   发表时间:2007-06-29  
我一般采用返回值+日志方式。不过用起来不是很爽。
0 请登录后投票
   发表时间:2007-06-29  
to xombat
成本不是按照指令个数来判断的。

函数调用操作只有堆栈操作和跳转指令,不会中断流水线,而且栈操作都可以在L2 Cache中完成,而函数跳转的话,Cache命中率也相当高。所以一次函数调用不过10来个时钟周期。而if可能会造成流水线中断(分支预测失败),对于P4这样的CPU,重新填充流水线需要几十(>40)个时钟周期。

在C程序中,跨函数调用处理异常并不是什么好主意。一般来说用返回值就够了,errno起辅助性作用。就像Windows API和c runtime这样。
你给出的那个用C实现Exception的方法会引入一定的OverHead。如果你能接受OverHead的话,为什么不用C++呢?做为一种更强的C来用好了。不用Class就是了。
0 请登录后投票
   发表时间:2007-06-29  
xombat 写道
to qiezi:
引用
“错误封装”我没看出来这个封装的好处,只是业务特定的错误处理方法?

错误处理封装函数最初是在史蒂芬的《unix网络编程:unix APIs》中被提出的,用处很广。
典型的一个错误处理函数Fork()(首字母大写的,区分系统函数fork()):
pid_t Fork(void)
{
  pid_t pid;
  if((pid = fork())<0)
  {
    fprintf(stderr,"Fork error: %s\n",strerror(errno));
    exit(0);
  }
  return pid;
}

依次将所有unix系统调用级函数全部封装,posix函数的封装方法有所不同,他将错误情况存储在code中而不是errno中。

引用
如果是作为库提供,封装一定不能把灵活性、效率任意一方面给封没了

我不认为它能够被当作库来提供,提供这些封装函数只是用来解决处理错误而导致的代码臃肿和代码可读性下降的种种问题,另外因为在不同的情况下对错误的处理情况可能会不同,所以将它们用作库不合适,但可以作为一种错误处理的好的解决方案。

不作为库来提供,自然是没这方面的问题,不过不建议在每个里面都去exit。

xombat 写道

引用
系统实现的errno是线程安全的,它使用了线程专有存储

qiezi有这方面的资料不?我一直以为errno就是一个全局整型变量呢

UNIX网络编程一书的socket api好像就有提到这个。线程专有存储我自己也做过简单的:

// tss.h
#ifndef TSS_H_
#define TSS_H_

#include <pthread.h>

typedef void(*CLEANUP_FUNC)(void* p);

class TSS
{
private:
	pthread_key_t key;

public:
	TSS(CLEANUP_FUNC cleanfunc);
	void set(void* p);
	void* get();
};

#endif // TSS_H_

// tss.cpp
#include "tss.h"

class TSSInitFail : public std::exception
{
};

class TSSSetSpecific : public std::exception
{
};


TSS::TSS(CLEANUP_FUNC f)
{
	int result = pthread_key_create(&key, f);
	if (result != 0)
		throw TSSInitFail();
}

void TSS::set(void* p)
{
	int result = pthread_setspecific(key, p);
	if (result != 0)
		throw TSSSetSpecific();
}

void* TSS::get()
{
	return pthread_getspecific(key);
}

// 一个使用例子:
#define TSS_SIZE (65536 * 4)

void cleanup(void* p)
{
	char* pp = (char*) p;
	delete[] pp;
}

static TSS __formatTSS(&cleanup);


const char* format(const char* fmt, ...)
{
	char* buffer = (char*)__formatTSS.get();
	if (!buffer)
	{
		buffer = new char[TSS_SIZE];
		__formatTSS.set(buffer);
	}

	va_list args;
	va_start(args, fmt);
	vsnprintf(buffer, TSS_SIZE, fmt, args);
	va_end(args);

	return buffer;
}

上面这个例子在多线程情况下,也可以使用const char* p = format("hello %s!", name),因为在同一线程中它总是指向同一个地址,不同线程刚好也不冲突。errno我不知道它如何实现的,通常这种应用需要一个函数才行,看它的头文件的确是个整型变量,不知道有没有哪些编译器选项让它自动生成TSS,VC里面是有的。

补充:找到了errno的实现方式:
http://www.acejoy.com/Html/Article/ace/2520060920135646.html

linux下的代码也找到了,/usr/include/sys/errno.h:
#  if !defined _LIBC || defined _LIBC_REENTRANT
/* When using threads, errno is a per-thread value.  */
#   define errno (*__errno_location ())
#  endif

的确是个函数调用,也有说明“When using threads, errno is a per-thread value”。

xombat 写道

引用
通常一个程序退出还要处理已经打开的资源

操作系统自己会回收已退出的应用程序的资源,为什么我们还要操心。我一直以来在exit之前都没有考虑过回收资源的问题,要不你怎么解决?

通常程序在退出前关闭资源是合理的,我们有些cache服务器使用了BDB,程序直接退出会很严重,BDB没有正常关闭,甚至有些线程没有关闭,所以下次重启服务器就起不来,虽然可以手动修复BDB,还是会丢失一些数据的,而且还影响了服务器在线时间。socket通常也要关闭的,比如listen一个端口,直接exit再重启程序会发现端口没有关闭。写服务器程序,我最先考虑的就是如何初始化、如何动态重新加载配置、如何安全退出、如何在异常退出后修复数据,把这几个考虑好了再写其它逻辑。
0 请登录后投票
   发表时间:2007-06-30  
to netpcc:
引用
而函数跳转的话,Cache命中率也相当高。

if语句指令的空间局部性更强,命中率应该更高吧。

引用
函数调用操作只有堆栈操作和跳转指令,不会中断流水线,而且栈操作都可以在L2 Cache中完成,

函数调用后,还要返回到调用函数,这时ret指令要从栈中读取地址,也会中断流水线,而且这种情况下pc还无法预测。

除非p4中有个硬件栈,用来存储每次函数调用的返回地址。

if有预测,如果策略足够好的话,中断流水线的概率就很低。并且不会访问mem

我是从处理器的体系结构来考虑的,没有着重偏向哪个处理器的哪个技术。对P4的一些新技术涉及还不算多。
0 请登录后投票
论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics