再次认识 errno之线程安全

目录

1、errno的由来

2、errno的线程安全

3、errno的实现(核心)

4、代码演练

5、errno的应用


1、errno的由来

在C编程中,errno是个不可缺少的变量,特别是在网络编程中。如果你没有用过errno,那只能说明你的程序不够健壮。当然,如果你是WIN32平台的GetLastError(),效果也是一样的。
为什么会使用errno呢?个人认为,这是系统库设计中的一个无奈之举,他更多的是个技巧,而不是架构上的需要。我们观察下函数结构,可以发现,函数的参数返回值只有一个,这个返回值一般可以携带错误信息,比如负数表示错误,而正数表述正确的返回值,比如recv函数。但是对于一些返回指针的函数,如:char *get_str();这个方法显然没有用的。NULL可以表示发生错误,但是发生什么错误却毫无办法。并且很多的系统调用(read/write,recv/send,open/close)的错误返回值均为-1,通过-1我们无法知晓系统到底发生了什么错误。于是,errno就诞生了。当错误发生时,函数的返回值是可以通过非法值来提示错误的发生,而全局变量errno可以存放错误原因。

2、errno的线程安全

errno是全局变量,但是在多线程环境下,就会变得很恐怖。当你调用一个函数时,发现这个函数发生了错误,但当你使用错误原因时,他却变成了另外一个线程的错误提示。想想就会觉得是件可怕的事情。
将errno设置为线程局部变量是个不错的主意,事实上,GCC中就是这么干的。他保证了线程之间的错误原因不会互相串改,当你在一个线程中串行执行一系列过程,那么得到的errno仍然是正确的。看下,bits/errno.h的定义:

# ifndef __ASSEMBLER__
/* Function to get address of global `errno' variable.  */
extern int *__errno_location (void) __THROW __attribute__ ((__const__));
 
#  if !defined _LIBC || defined _LIBC_REENTRANT
/* When using threads, errno is a per-thread value.  */
#   define errno (*__errno_location ())
#  endif
# endif /* !__ASSEMBLER__ */

而errno.h中是这样定义的:

/* Declare the `errno' variable, unless it's defined as a macro by
   bits/errno.h.  This is the case in GNU, where it is a per-thread
   variable.  This redeclaration using the macro still works, but it
   will be a function declaration without a prototype and may trigger
   a -Wstrict-prototypes warning.  */
#ifndef errno
extern int errno;
#endif

显然,errno实际上,并不是我们通常认为的是个整型数值,而是通过整型指针来获取值的。这个整型就是线程安全的。

3、errno的实现(核心)

static pthread_key_t key;
static pthread_once_t key_once = PTHREAD_ONCE_INIT;

static void make_key()
{
    (void) pthread_key_create(&key, NULL);
}

int *_errno()
{
    int *ptr ;

    (void) pthread_once(&key_once, make_key);
    if ((ptr = pthread_getspecific(key)) == NULL) 
    {
        ptr = malloc(sizeof(int));        
        (void) pthread_setspecific(key, ptr);
    }

    return ptr ;
}

从上面的代码可以看到errno的实现主要是通过线程的局部变量(tlb)实现的,这样可以保证每个执行线程均有一份errno,保证不会其他线程锁篡改。关于线程局部变量原理以及用法参见:《linux多线程:tls的实现方式》

4、代码演练

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include <pthread.h>

void *thread_deal_func(void *arg);

#define	TASK_NUM	2
pthread_t global_thread_no[TASK_NUM];

static pthread_key_t key;
static pthread_once_t key_once = PTHREAD_ONCE_INIT;
static void make_key()
{
    (void) pthread_key_create(&key, NULL);
}
int * _errno()
{
    int *ptr ;
    (void) pthread_once(&key_once, make_key);
    if ((ptr = pthread_getspecific(key)) == NULL)
    {
        ptr = malloc(sizeof(int));       
        (void) pthread_setspecific(key, ptr);
    }
    return ptr ;
}

#define	errno_test *_errno()	
//int errno_test = 0;

int main(){

	errno_test = 100;
	int tmp = 0,i = 0;
	for(i = 0;i < TASK_NUM; i++){
		if((tmp=pthread_create(&global_thread_no[i],NULL,thread_deal_func,&i))!= 0){
			printf("can't create thread: %s\n",strerror(tmp));
			return -1;
		}
	}

	while(1){
		printf("man thread     ,errno_test:%d\n",errno_test);
		sleep(1);
	}	
	return 0;
}

void *thread_deal_func(void *arg)
{
	int number = *(int*)arg;
	while(1){
		errno_test += 1;
		printf("thread number:%d,errno_test:%d\n",number,errno_test);
		sleep(1);
	}
}

定义errno_test为普通全局变量,执行结果:

[root@localhost some_func]# gcc errno_test.c -o errno_test -lpthread
[root@localhost some_func]# ./errno_test
thread number:1,errno_test:101
man thread     ,errno_test:101
thread number:2,errno_test:102
thread number:2,errno_test:103
man thread     ,errno_test:103
thread number:1,errno_test:104
thread number:2,errno_test:105
man thread     ,errno_test:105
thread number:1,errno_test:106
thread number:2,errno_test:107
man thread     ,errno_test:107

由于task1、task2功能主线程的静态区,因此修改errno_test变量后在线程之间相互影响。

定义errno_test为tlb,执行结果:

[root@localhost some_func]# gcc errno_test.c -o errno_test -lpthread
[root@localhost some_func]# ./errno_test
man thread     ,errno_test:100
thread number:1,errno_test:1
thread number:2,errno_test:1
thread number:2,errno_test:2
thread number:1,errno_test:2
man thread     ,errno_test:100
man thread     ,errno_test:100
thread number:2,errno_test:3
thread number:1,errno_test:3
thread number:2,errno_test:4
thread number:1,errno_test:4
man thread     ,errno_test:100
thread number:2,errno_test:5
thread number:1,errno_test:5
man thread     ,errno_test:100
^C
[root@localhost some_func]#

主线程、task1、task2三个线程均有自己的errno参数,修改errno互不影响,彼此独立。

5、errno的应用

errno在库中得到广泛的应用,但是,错误编码实际上不止那么多。我们需要在自己的系统中增加更多的错误编码。一种方式就是直接利用errno,另外一种方式就是定义自己的user_errno。使用errno,strerror可能无法解析,这需要自己解决。但errno使用线程变量的方式值得借鉴。关于常见的errno的定义列表参见《Linux errno详解之系统常见错误信息》

猜你喜欢

转载自blog.csdn.net/wangquan1992/article/details/108327310