2010年6月8日火曜日

/dev/console,/dev/tty和/dev/null含义

UNIX和Linux中比较重要的三个设备文件是:/dev/console,/dev/tty和/dev/null。

/dev/console
这个设备代表的是系统控制台,错误信息和诊断信息通常会被发送到这个设备。
每个UNIX系统都会有一个指定的终端或显示屏用来接收控制台信息。

/dev/tty
如果一个进程有控制终端的话,那么 /dev/tty就是这个控制终端的别名。
像cron这样的进程是没有控制终端的,因此它也就无法打开/dev/tty。
tty 这个词源于Teletypes,最早是源于电传打印机。
如果你登录了一个shell,那么/dev/tty就是你当前使用的终端。你也可以用tty命令查看/dev/tty设备实际链接到的设备是哪个。
/dev/tty其实就是类似于“符号链接”一样的东西。像我的 tty输出就是:
[root@wupengchong dev]# tty
/dev/pts/0

/dev/null
这是个空设备,也称为“位桶bit bucket”。所有写向这个设备的输出都将被丢弃,而如果你读/dev/null,则会立即得到一个文件尾标志而返回。
在cp命令里,可以用/dev/null来作为拷贝空文件的源文件。
在shell中,通常将不需要的东西重定向到/dev/null中。
这里要提一下/dev/zero,它和null是有区别的。通俗的讲,/dev/null是一个饭桶,你可以无穷无尽的往里塞垃圾,它不会埋怨。而/dev /zero是一个输入设备,它给你无穷无尽的提供0(就是null),它可以用于向文件或设备写入无穷无尽的0.
/dev/zero和 /dev/null的用法比较:
$dd if=/dev/zero of=mydoc.txt bs=1k count=1
$find . -name “*.c” 2>/dev/null

2010年6月4日金曜日

Linux下的多线程编程

1 引言
  线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者。传统的Unix也支持线程的概念,但是在一个进程(process)中只允许有一个线程,这样多线程就意味着多进程。现在,多线程技术已经被许多操作系统所支持,包括Windows/NT,当然,也包括Linux。
  为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?我们首先必须回答这些问题。
  使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来桓鼋痰目笤际且桓鱿叱炭?0倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。
  使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。
  除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:
  1) 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
  2) 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
  3) 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
  下面我们先来尝试编写一个简单的多线程程序。

2 简单的多线程编程
  Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a。顺便说一下,Linux下pthread的实现是通过系统调用clone()来实现的。clone()是Linux所特有的系统调用,它的使用方式类似fork,关于clone()的详细情况,有兴趣的读者可以去查看有关文档说明。下面我们展示一个最简单的多线程程序 example1.c。

/* example.c*/
#include
#include
void thread(void)
{
int i;
for(i=0;i<3;i++)
printf("This is a pthread.\n");
}

int main(void)
{
pthread_t id;
int i,ret;
ret=pthread_create(&id,NULL,(void *) thread,NULL);
if(ret!=0){
printf ("Create pthread error!\n");
exit (1);
}
for(i=0;i<3;i++)
printf("This is the main process.\n");
pthread_join(id,NULL);
return (0);
}

我们编译此程序:
gcc example1.c -lpthread -o example1
运行example1,我们得到如下结果:
This is the main process.
This is a pthread.
This is the main process.
This is the main process.
This is a pthread.
This is a pthread.
再次运行,我们可能得到如下结果:
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.

  前后两次结果不一样,这是两个线程争夺CPU资源的结果。上面的示例中,我们使用到了两个函数,  pthread_create和pthread_join,并声明了一个pthread_t型的变量。
  pthread_t在头文件 /usr/include/bits/pthreadtypes.h中定义:
  typedef unsigned long int pthread_t;
  它是一个线程的标识符。函数pthread_create用来创建一个线程,它的原型为:
  extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr,
  void *(*__start_routine) (void *), void *__arg));
  第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是运行函数的参数。这里,我们的函数 thread不需要参数,所以最后一个参数设为空指针。第二个参数我们也设为空指针,这样将生成默认属性的线程。对线程属性的设定和修改我们将在下一节阐述。当创建线程成功时,函数返回0,若不为0则说明创建线程失败,常见的错误返回代码为EAGAIN和EINVAL。前者表示系统限制创建新的线程,例如线程数目过多了;后者表示第二个参数代表的线程属性值非法。创建线程成功后,新创建的线程则运行参数三和参数四确定的函数,原来的线程则继续运行下一行代码。
  函数pthread_join用来等待一个线程的结束。函数原型为:
  extern int pthread_join __P ((pthread_t __th, void **__thread_return));
  第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。一个线程的结束有两种途径,一种是象我们上面的例子一样,函数结束了,调用它的线程也就结束了;另一种方式是通过函数 pthread_exit来实现。它的函数原型为:
  extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));
  唯一的参数是函数的返回代码,只要 pthread_join中的第二个参数thread_return不是NULL,这个值将被传递给thread_return。最后要说明的是,一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用pthread_join的线程则返回错误代码ESRCH。
  在这一节里,我们编写了一个最简单的线程,并掌握了最常用的三个函数pthread_create,pthread_join和pthread_exit。下面,我们来了解线程的一些常用属性以及如何设置这些属性。

3 修改线程的属性
  在上一节的例子里,我们用pthread_create函数创建了一个线程,在这个线程中,我们使用了默认参数,即将该函数的第二个参数设为NULL。的确,对大多数程序来说,使用默认属性就够了,但我们还是有必要来了解一下线程的有关属性。
  属性结构为pthread_attr_t,它同样在头文件/usr/include/pthread.h中定义,喜欢追根问底的人可以自己去查看。属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。属性对象主要包括是否绑定、是否分离、堆栈地址、堆栈大小、优先级。默认的属性为非绑定、非分离、缺省1M的堆栈、与父进程同样级别的优先级。
  关于线程的绑定,牵涉到另外一个概念:轻进程(LWP:Light Weight Process)。轻进程可以理解为内核线程,它位于用户层和系统层之间。系统对线程资源的分配、对线程的控制是通过轻进程来实现的,一个轻进程可以控制一个或多个线程。默认状况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定的。绑定状况下,则顾名思义,即某个线程固定的"绑"在一个轻进程之上。被绑定的线程具有较高的响应速度,这是因为CPU时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它总有一个轻进程可用。通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求。
  设置线程绑定状态的函数为 pthread_attr_setscope,它有两个参数,第一个是指向属性结构的指针,第二个是绑定类型,它有两个取值:PTHREAD_SCOPE_SYSTEM(绑定的)和PTHREAD_SCOPE_PROCESS(非绑定的)。下面的代码即创建了一个绑定的线程。
#include
pthread_attr_t attr;
pthread_t tid;

/*初始化属性值,均设为默认值*/
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);

pthread_create(&tid, &attr, (void *) my_function, NULL);

  线程的分离状态决定一个线程以什么样的方式来终止自己。在上面的例子中,我们采用了线程的默认属性,即为非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。程序员应该根据自己的需要,选择适当的分离状态。设置线程分离状态的函数为 pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第二个参数可选为PTHREAD_CREATE_DETACHED(分离线程)和 PTHREAD _CREATE_JOINABLE(非分离线程)。这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在 pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用 pthread_cond_timewait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。
  另外一个可能常用的属性是线程的优先级,它存放在结构sched_param中。用函数pthread_attr_getschedparam和函数 pthread_attr_setschedparam进行存放,一般说来,我们总是先取优先级,对取得的值修改后再存放回去。下面即是一段简单的例子。
#include
#include
pthread_attr_t attr;
pthread_t tid;
sched_param param;
int newprio=20;

pthread_attr_init(&attr);
pthread_attr_getschedparam(&attr, ¶m);
param.sched_priority=newprio;
pthread_attr_setschedparam(&attr, ¶m);
pthread_create(&tid, &attr, (void *)myfunction, myarg);
  
4 线程的数据处理
  和进程相比,线程的最大优点之一是数据的共享性,各个进程共享父进程处沿袭的数据段,可以方便的获得、修改数据。但这也给多线程编程带来了许多问题。我们必须当心有多个不同的进程访问相同的变量。许多函数是不可重入的,即同时不能运行一个函数的多个拷贝(除非使用不同的数据段)。在函数中声明的静态变量常常带来问题,函数的返回值也会有问题。因为如果返回的是函数内部静态声明的空间的地址,则在一个线程调用该函数得到地址后使用该地址指向的数据时,别的线程可能调用此函数并修改了这一段数据。在进程中共享的变量必须用关键字volatile来定义,这是为了防止编译器在优化时(如gcc中使用-OX参数)改变它们的使用方式。为了保护变量,我们必须使用信号量、互斥等方法来保证我们对变量的正确使用。下面,我们就逐步介绍处理线程数据时的有关知识。

4.1 线程数据
  在单线程的程序里,有两种基本的数据:全局变量和局部变量。但在多线程程序里,还有第三种数据类型:线程数据(TSD: Thread-Specific Data)。它和全局变量很象,在线程内部,各个函数可以象使用全局变量一样调用它,但它对线程外部的其它线程是不可见的。这种数据的必要性是显而易见的。例如我们常见的变量errno,它返回标准的出错信息。它显然不能是一个局部变量,几乎每个函数都应该可以调用它;但它又不能是一个全局变量,否则在 A线程里输出的很可能是B线程的出错信息。要实现诸如此类的变量,我们就必须使用线程数据。我们为每个线程数据创建一个键,它和这个键相关联,在各个线程里,都使用这个键来指代线程数据,但在不同的线程里,这个键代表的数据是不同的,在同一个线程里,它代表同样的数据内容。
  和线程数据相关的函数主要有4个:创建一个键;为一个键指定线程数据;从一个键读取线程数据;删除键。
  创建键的函数原型为:
  extern int pthread_key_create __P ((pthread_key_t *__key,
  void (*__destr_function) (void *)));
  第一个参数为指向一个键值的指针,第二个参数指明了一个 destructor函数,如果这个参数不为空,那么当每个线程结束时,系统将调用这个函数来释放绑定在这个键上的内存块。这个函数常和函数 pthread_once ((pthread_once_t*once_control, void (*initroutine) (void)))一起使用,为了让这个键只被创建一次。函数pthread_once声明一个初始化函数,第一次调用pthread_once时它执行这个函数,以后的调用将被它忽略。

  在下面的例子中,我们创建一个键,并将它和某个数据相关联。我们要定义一个函数 createWindow,这个函数定义一个图形窗口(数据类型为Fl_Window *,这是图形界面开发工具FLTK中的数据类型)。由于各个线程都会调用这个函数,所以我们使用线程数据。
/* 声明一个键*/
pthread_key_t myWinKey;
/* 函数 createWindow */
void createWindow ( void ) {
Fl_Window * win;
static pthread_once_t once= PTHREAD_ONCE_INIT;
/* 调用函数createMyKey,创建键*/
pthread_once ( & once, createMyKey) ;
/*win 指向一个新建立的窗口*/
win=new Fl_Window( 0, 0, 100, 100, "MyWindow");
/* 对此窗口作一些可能的设置工作,如大小、位置、名称等*/
setWindow(win);
/* 将窗口指针值绑定在键myWinKey上*/
pthread_setpecific ( myWinKey, win);
}

/* 函数 createMyKey,创建一个键,并指定了destructor */
void createMyKey ( void ) {
pthread_keycreate(&myWinKey, freeWinKey);
}

/* 函数 freeWinKey,释放空间*/
void freeWinKey ( Fl_Window * win){
delete win;
}

  这样,在不同的线程中调用函数createMyWin,都可以得到在线程内部均可见的窗口变量,这个变量通过函数pthread_getspecific得到。在上面的例子中,我们已经使用了函数 pthread_setspecific来将线程数据和一个键绑定在一起。这两个函数的原型如下:
  extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer));
  extern void *pthread_getspecific __P ((pthread_key_t __key));
  这两个函数的参数意义和使用方法是显而易见的。要注意的是,用pthread_setspecific为一个键指定新的线程数据时,必须自己释放原有的线程数据以回收空间。这个过程函数pthread_key_delete用来删除一个键,这个键占用的内存将被释放,但同样要注意的是,它只释放键占用的内存,并不释放该键关联的线程数据所占用的内存资源,而且它也不会触发函数pthread_key_create中定义的destructor函数。线程数据的释放必须在释放键之前完成。

4.2 互斥锁
  互斥锁用来保证一段时间内只有一个线程在执行一段代码。必要性显而易见:假设各个线程向同一个文件顺序写入数据,最后得到的结果一定是灾难性的。
  我们先看下面一段代码。这是一个读/写程序,它们公用一个缓冲区,并且我们假定一个缓冲区只能保存一条信息。即缓冲区只有两个状态:有信息或没有信息。

void reader_function ( void );
void writer_function ( void );

char buffer;
int buffer_has_item=0;
pthread_mutex_t mutex;
struct timespec delay;
void main ( void ){
pthread_t reader;
/* 定义延迟时间*/
delay.tv_sec = 2;
delay.tv_nec = 0;
/* 用默认属性初始化一个互斥锁对象*/
pthread_mutex_init (&mutex,NULL);
pthread_create(&reader, pthread_attr_default, (void *)&reader_function), NULL);
writer_function( );
}

void writer_function (void){
while(1){
/* 锁定互斥锁*/
pthread_mutex_lock (&mutex);
if (buffer_has_item==0){
buffer=make_new_item( );
buffer_has_item=1;
}
/* 打开互斥锁*/
pthread_mutex_unlock(&mutex);
pthread_delay_np(&delay);
}
}

void reader_function(void){
while(1){
pthread_mutex_lock(&mutex);
if(buffer_has_item==1){
consume_item(buffer);
buffer_has_item=0;
}
pthread_mutex_unlock(&mutex);
pthread_delay_np(&delay);
}
}
  这里声明了互斥锁变量mutex,结构pthread_mutex_t为不公开的数据类型,其中包含一个系统分配的属性对象。函数 pthread_mutex_init用来生成一个互斥锁。NULL参数表明使用默认属性。如果需要声明特定属性的互斥锁,须调用函数 pthread_mutexattr_init。函数pthread_mutexattr_setpshared和函数 pthread_mutexattr_settype用来设置互斥锁属性。前一个函数设置属性pshared,它有两个取值,PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED。前者用来不同进程中的线程同步,后者用于同步本进程的不同线程。在上面的例子中,我们使用的是默认属性PTHREAD_PROCESS_ PRIVATE。后者用来设置互斥锁类型,可选的类型有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、 PTHREAD_MUTEX_RECURSIVE和PTHREAD _MUTEX_DEFAULT。它们分别定义了不同的上所、解锁机制,一般情况下,选用最后一个默认属性。
   pthread_mutex_lock声明开始用互斥锁上锁,此后的代码直至调用pthread_mutex_unlock为止,均被上锁,即同一时间只能被一个线程调用执行。当一个线程执行到pthread_mutex_lock处时,如果该锁此时被另一个线程使用,那此线程被阻塞,即程序将等待到另一个线程释放此互斥锁。在上面的例子中,我们使用了pthread_delay_np函数,让线程睡眠一段时间,就是为了防止一个线程始终占据此函数。
  上面的例子非常简单,就不再介绍了,需要提出的是在使用互斥锁的过程中很有可能会出现死锁:两个线程试图同时占用两个资源,并按不同的次序锁定相应的互斥锁,例如两个线程都需要锁定互斥锁1和互斥锁2,a线程先锁定互斥锁1,b线程先锁定互斥锁2,这时就出现了死锁。此时我们可以使用函数 pthread_mutex_trylock,它是函数pthread_mutex_lock的非阻塞版本,当它发现死锁不可避免时,它会返回相应的信息,程序员可以针对死锁做出相应的处理。另外不同的互斥锁类型对死锁的处理不一样,但最主要的还是要程序员自己在程序设计注意这一点。

4.3 条件变量
  前一节中我们讲述了如何使用互斥锁来实现线程间数据的共享和通信,互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线承间的同步。
  条件变量的结构为pthread_cond_t,函数pthread_cond_init()被用来初始化一个条件变量。它的原型为:
  extern int pthread_cond_init __P ((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr));
  其中cond是一个指向结构pthread_cond_t的指针,cond_attr是一个指向结构pthread_condattr_t的指针。结构 pthread_condattr_t是条件变量的属性结构,和互斥锁一样我们可以用它来设置条件变量是进程内可用还是进程间可用,默认值是 PTHREAD_ PROCESS_PRIVATE,即此条件变量被同一进程内的各个线程使用。注意初始化条件变量只有未被使用时才能重新初始化或被释放。释放一个条件变量的函数为pthread_cond_ destroy(pthread_cond_t cond)。 
  函数 pthread_cond_wait()使线程阻塞在一个条件变量上。它的函数原型为:
  extern int pthread_cond_wait __P ((pthread_cond_t *__cond,
  pthread_mutex_t *__mutex));
  线程解开mutex指向的锁并被条件变量cond阻塞。线程可以被函数pthread_cond_signal和函数 pthread_cond_broadcast唤醒,但是要注意的是,条件变量只是起阻塞和唤醒线程的作用,具体的判断条件还需用户给出,例如一个变量是否为0等等,这一点我们从后面的例子中可以看到。线程被唤醒后,它将重新检查判断条件是否满足,如果还不满足,一般说来线程应该仍阻塞在这里,被等待被下一次唤醒。这个过程一般用while语句实现。
  另一个用来阻塞线程的函数是pthread_cond_timedwait(),它的原型为:
  extern int pthread_cond_timedwait __P ((pthread_cond_t *__cond,
   pthread_mutex_t *__mutex, __const struct timespec *__abstime));
  它比函数pthread_cond_wait()多了一个时间参数,经历abstime段时间后,即使条件变量不满足,阻塞也被解除。
  函数 pthread_cond_signal()的原型为:
  extern int pthread_cond_signal __P ((pthread_cond_t *__cond));
  它用来释放被阻塞在条件变量cond上的一个线程。多个线程阻塞在此条件变量上时,哪一个线程被唤醒是由线程的调度策略所决定的。要注意的是,必须用保护条件变量的互斥锁来保护这个函数,否则条件满足信号又可能在测试条件和调用 pthread_cond_wait函数之间被发出,从而造成无限制的等待。下面是使用函数pthread_cond_wait()和函数 pthread_cond_signal()的一个简单的例子。

pthread_mutex_t count_lock;
pthread_cond_t count_nonzero;
unsigned count;
decrement_count () {
pthread_mutex_lock (&count_lock);
while(count==0)
pthread_cond_wait( &count_nonzero, &count_lock);
count=count -1;
pthread_mutex_unlock (&count_lock);
}

increment_count(){
pthread_mutex_lock(&count_lock);
if(count==0)
pthread_cond_signal(&count_nonzero);
count=count+1;
pthread_mutex_unlock(&count_lock);
}
  count值为0时,decrement函数在pthread_cond_wait处被阻塞,并打开互斥锁count_lock。此时,当调用到函数increment_count时,pthread_cond_signal()函数改变条件变量,告知decrement_count()停止阻塞。读者可以试着让两个线程分别运行这两个函数,看看会出现什么样的结果。
  函数 pthread_cond_broadcast(pthread_cond_t *cond)用来唤醒所有被阻塞在条件变量cond上的线程。这些线程被唤醒后将再次竞争相应的互斥锁,所以必须小心使用这个函数。

4.4 信号量
  信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。当公共资源增加时,调用函数sem_post()增加信号量。只有当信号量值大于0时,才能使用公共资源,使用后,函数sem_wait()减少信号量。函数sem_trywait()和函数pthread_ mutex_trylock()起同样的作用,它是函数sem_wait()的非阻塞版本。下面我们逐个介绍和信号量有关的一些函数,它们都在头文件 /usr/include/semaphore.h中定义。
  信号量的数据类型为结构sem_t,它本质上是一个长整型的数。函数 sem_init()用来初始化一个信号量。它的原型为:
  extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));
  sem为指向信号量结构的一个指针;pshared不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享;value给出了信号量的初始值。
  函数sem_post( sem_t *sem )用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不在阻塞,选择机制同样是由线程的调度策略决定的。
  函数sem_wait( sem_t *sem )被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。函数sem_trywait ( sem_t *sem )是函数sem_wait()的非阻塞版本,它直接将信号量sem的值减一。
  函数sem_destroy(sem_t *sem)用来释放信号量sem。
  下面我们来看一个使用信号量的例子。在这个例子中,一共有4个线程,其中两个线程负责从文件读取数据到公共的缓冲区,另两个线程从缓冲区读取数据作不同的处理(加和乘运算)。
/* File sem.c */
#include
#include
#include
#define MAXSTACK 100
int stack[MAXSTACK][2];
int size=0;
sem_t sem;
/* 从文件1.dat读取数据,每读一次,信号量加一*/
void ReadData1(void){
FILE *fp=fopen("1.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
sem_post(&sem);
++size;
}
fclose(fp);
}
/* 从文件2.dat读取数据*/
void ReadData2(void){
FILE *fp=fopen("2.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
sem_post(&sem);
++size;
}
fclose(fp);
}
/* 阻塞等待缓冲区有数据,读取数据后,释放空间,继续等待*/
void HandleData1(void){
while(1){
sem_wait(&sem);
printf("Plus:%d+%d=%d\n",stack[size][0],stack[size][1],
stack[size][0]+stack[size][1]);
--size;
}
}

void HandleData2(void){
while(1){
sem_wait(&sem);
printf("Multiply:%d*%d=%d\n",stack[size][0],stack[size][1],
stack[size][0]*stack[size][1]);
--size;
}
}
int main(void){
pthread_t t1,t2,t3,t4;
sem_init(&sem,0,0);
pthread_create(&t1,NULL,(void *)HandleData1,NULL);
pthread_create(&t2,NULL,(void *)HandleData2,NULL);
pthread_create(&t3,NULL,(void *)ReadData1,NULL);
pthread_create(&t4,NULL,(void *)ReadData2,NULL);
/* 防止程序过早退出,让它在此无限期等待*/
pthread_join(t1,NULL);
}

  在Linux下,我们用命令gcc -lpthread sem.c -o sem生成可执行文件sem。 我们事先编辑好数据文件1.dat和2.dat,假设它们的内容分别为1 2 3 4 5 6 7 8 9 10和 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 ,我们运行sem,得到如下的结果:
Multiply:-1*-2=2
Plus:-1+-2=-3
Multiply:9*10=90
Plus:-9+-10=-19
Multiply:-7*-8=56
Plus:-5+-6=-11
Multiply:-3*-4=12
Plus:9+10=19
Plus:7+8=15
Plus:5+6=11

  从中我们可以看出各个线程间的竞争关系。而数值并未按我们原先的顺序显示出来这是由于size这个数值被各个线程任意修改的缘故。这也往往是多线程编程要注意的问题。

5 小结
  多线程编程是一个很有意思也很有用的技术,使用多线程技术的网络蚂蚁是目前最常用的下载工具之一,使用多线程技术的grep比单线程的grep要快上几倍,类似的例子还有很多。希望大家能用多线程技术写出高效实用的好程序来。

2010年6月3日木曜日

七款嵌入式Linux操作系统简介



除了智能数字终端领域以外,Linux在移动计算平台、智能工业控制、金融业终端系统,甚至军事领域都有着广泛的应用前景。这些Linux被统称为“嵌入式Linux”。下面就来看看都有哪些嵌入式Linux在以上领域纵横驰骋吧!

RT-Linux

这是由美国墨西哥理工学院开发的嵌入式Linux操作系统。到目前为止,RT- Linux已经成功地应用于航天飞机的空间数据采集、科学仪器测控和电影特技图像处理等广泛领域。RT-Linux开发者并没有针对实时操作系统的特性而重写Linux的内核,因为这样做的工作量非常大,而且要保证兼容性也非常困难。为此,RT-Linux提出了精巧的内核,并把标准的Linux核心作为实时核心的一个进程,同用户的实时进程一起调度。这样对Linux内核的改动非常小,并且充分利用了Linux下现有的丰富的软件资源。

uClinux

uCLinux是Lineo公司的主打产品,同时也是开放源码的嵌入式Linux的典范之作。uCLinux主要是针对目标处理器没有存储管理单元MMU (Memory Management Unit)的嵌入式系统而设计的。它已经被成功地移植到了很多平台上。由于没有MMU,其多任务的实现需要一定技巧。uCLinux是一种优秀的嵌入式 Linux版本,是micro-Conrol-Linux的缩写。它秉承了标准Linux的优良特性,经过各方面的小型化改造,形成了一个高度优化的、代码紧凑的嵌入式Linux。虽然它的体积很小,却仍然保留了Linux的大多数的优点:稳定、良好的移植性、优秀的网络功能、对各种文件系统完备的支持和标准丰富的API。它专为嵌入式系统做了许多小型化的工作,目前已支持多款CPU。其编译后目标文件可控制在几百KB数量级,并已经被成功地移植到很多平台上。

Embedix

Embedix是由嵌入式Linux行业主要厂商之一Luneo推出的,是根据嵌入式应用系统的特点重新设计的Linux发行版本。Embedix提供了超过25种的Linux系统服务,包括Web服务器等。系统需要最小8MB内存,3MB ROM或快速闪存。Embedix基于Linux 2.2内核,并已经成功地移植到了Intel x86和PowerPC处理器系列上。像其它的Linux版本一样,Embedix可以免费获得。Luneo还发布了另一个重要的软件产品,它可以让在 Windows CE上运行的程序能够在Embedix上运行。Luneo还将计划推出Embedix的开发调试工具包、基于图形界面的浏览器等。可以说,Embedix 是一种完整的嵌入式Linux解决方案。

XLinux

XLinux是由美国网虎公司推出,主要开发者是陈盈豪。他在加盟网虎几个月后便开发出了基于XLinux的、号称是世界上最小的嵌入式Linux系统,内核只有143KB,而且还在不断减小。XLinux核心采用了“超字元集”专利技术,让Linux核心不仅可能与标准字符集相容,还含盖了12个国家和地区的字符集。因此,XLinux在推广Linux的国际应用方面有独特的优势。

PoketLinux

PoketLinux由Agenda公司采用、作为其新产品“VR3 PDA”的嵌入式Linux操作系统。它可以提供跨操作系统构造统一的、标准化的和开放的信息通信基础结构,在此结构上实现端到端方案的完整平台。 PoketLinux资源框架开放,使普通的软件结构可以为所有用户提供一致的服务。PoketLinux平台使用户的视线从设备、平台和网络上移开,由此引发了信息技术新时代的产生。在PoketLinux中,称之为用户化信息交换(CIE),也就是提供和访问为每个用户需求而定制的“主题”信息的能力,而不管正在使用的设备是什么。

MidoriLinux

由Transmeta公司推出的MidoriLinux操作系统代码开放,在GUN普通公共许可(GPL)下发布,可以在http://midori.transmeta.com上立即获得。该公司有个名为“MidoriLinux计划”。“MidoriLinux”这个名字来源于日本的“绿色”——Midori,用来反映其 Linux操作系统的环保外观。

红旗嵌入式Linux

由北京中科院红旗软件公司推出的嵌入式Linux是国内做得较好的一款嵌入式操作系统。目前,中科院计算所自行开发的开放源码的嵌入式操作系统—— Easy Embedded OS(EEOS)也已经开始进入实用阶段了。该款嵌入式操作系统重点支持p-Java。系统目标一方面是小型化,另一方面能重用Linux的驱动和其它模块。由于有中科院计算所的强大科研力量做后盾,EEOS有望发展成为功能完善、稳定、可靠的国产嵌入式操作系统平台。

思考与展望

以上列举的众多嵌入式Linux操作系统中,国内对于uClinux和RT-Linux 研究的较多,很多基于它们的产品已经面世,比如华恒公司已经把uClinux成功移植,并投放到市场。

正是由于Linux开放源代码的特点,所以全世界的开发厂商都站在同一个起跑线上。国内的研究机构和企业也正在积极投入人力、物力,力争在嵌入式操作系统上有所为。但应该清醒认识到,绝大多数的嵌入式系统的硬件平台还掌握在外国公司的手中。国产的嵌入式操作系统在技术含量、兼容性、市场运作模式等方面还有很多工作要做。国家对嵌入式领域的发展也极为重视。信息产业部《2003年度电子发展基金项目指南》在软件类重点产品项目中,第五小类就是关于嵌入式软件与系统开发的,并提出要重点进行如下重点项目的研制与开发:嵌入式实时操作系统、嵌入式软件集成开发平台和嵌入式数据库管理软件。由于嵌入式系统研发在国内起步比较晚,我国目前还基本处于实验室阶段。但是嵌入式操作系统的巨大的商业价值和 Linux的开放性,为民族软件产业的发展提供了难得的机会。在跟踪国外嵌入式操作系统最新技术的同时,国内厂商要坚持自主产权,力争找到自己的突破点,探索出一条适合中国国情的嵌入式操作系统的发展道路。

glib库,Linux平台下最常用的C语言函数



推荐glib库是Linux平台下最常用的C语言函数库,它具有很好的可移植性和实用性。
glib是Gtk +库和Gnome的基础。glib可以在多个平台下使用,比如Linux、Unix、Windows等。glib为许多标准的、常用的C语言结构提供了相应的替代物。

使用glib库的程序都应该包含glib的头文件glib.h。

########################### glib基本类型定义: ##############################

整数类型:
gint8、guint8、gint16、guint16、gint32、guint32、gint64、guint64。
不是所有的平台都提供64位整型,如果一个平台有这些, glib会定义G_HAVE_GINT64。
类型gshort、glong、gint和short、long、int完全等价。
布尔类型:
gboolean:它可使代码更易读,因为普通C没有布尔类型。
Gboolean可以取两个值:TRUE和FALSE。实际上FALSE定义为0,而TRUE定义为非零值。
字符型:
gchar和char完全一样,只是为了保持一致的命名。
浮点类型:
gfloat、gdouble和float、double完全等价。
指针类型:
gpointer对应于标准C的void *,但是比void *更方便。
指针gconstpointer对应于标准C的const void *(注意,将const void *定义为const gpointer是行不通的

########################### glib的宏 ##############################

一些常用的宏列表
#i nclude
TRUE
FALSE
NULL
MAX(a, b)
MIN(a, b)
ABS ( x )
CLAMP(x, low, high)

TRUE / FALSE / NULL就是1 / 0 / ( ( v o i d * ) 0 )。
MIN ( ) / MAX ( )返回更小或更大的参数。
ABS ( )返回绝对值。
CLAMP(x,low,high )若X在[low,high]范围内,则等于X;如果X小于low,则返回low;如果X大于high,则返
回high。

有些宏只有g l i b拥有,例如在后面要介绍的gpointer-to-gint和gpointer-to-guint。
大多数glib的数据结构都设计成存储一个gpointer。如果想存储指针来动态分配对象,可以这样做。
在某些情况下,需要使用中间类型转换。
//////////////////////////////////////////////////////////////
gint my_int;
gpointer my_pointer;
my_int = 5;
my_pointer = GINT_TO_POINTER(my_int);
printf("We are storing %d\n", GPOINTER_TO_INT(my_pointer));
//////////////////////////////////////////////////////////////

这些宏允许在一个指针中存储一个整数,但在一个整数中存储一个指针是不行的。
如果要实现的话,必须在一个长整型中存储指针。
宏列表:
在指针中存储整数的宏
#i nclude
GINT_TO_POINTER ( p )
GPOINTER_TO_INT ( p )
GUINT_TO_POINTER ( p )
GPOINTER_TO_UINT ( p )

调试宏:
定义了G_DISABLE_CHECKS或G_DISABLE_ASSERT之后,编译时它们就会消失.
宏列表:
前提条件检查
#i nclude
g_return_if_fail ( condition )
g_return_val_if_fail(condition, retval)

///////////////////////////////////////////////////////////////////////////////////
使用这些函数很简单,下面的例子是g l i b中哈希表的实现:
void g_hash_table_foreach (GHashTable *hash_table,GHFunc func,gpointer user_data)
{
GHashNode *node;
gint i;
g_return_if_fail (hash_table != NULL);
g_return_if_fail (func != NULL);
for (i = 0; i < hash_table->size; i++)
for (node = hash_table->nodes[i]; node; node = node->next)
(* func) (node->key, node->value, user_data);
}
///////////////////////////////////////////////////////////////////////////////////

宏列表:
断言
#i nclude
g_assert( condition )
g_assert_not_reached ( )
如果执行到这个语句,它会调用abort()退出程序并且(如果环境支持)转储一个可用于调试的core文件。

断言与前提条件检查的区别:
应该断言用来检查函数或库内部的一致性。
g_return_if_fail()确保传递到程序模块的公用接口的值是合法的。
如果断言失败,将返回一条信息,通常应该在包含断言的模块中查找错误;
如果g_return_if_fail()检查失败,通常要在调用这个模块的代码中查找错误。
//////////////////////////////////////////////////////////////////////
下面glib日历计算模块的代码说明了这种差别:
GDate * g_date_new_dmy (GDateDay day, GDateMonth m, GDateYear y)
{
GDate *d;
g_return_val_if_fail (g_date_valid_dmy (day, m, y), NULL);
d = g_new (GDate, 1);
d->julian = FALSE;
d->dmy = TRUE;
d->month = m;
d->day = day;
d->year = y;
g_assert (g_date_valid (d));
return d;
}
//////////////////////////////////////////////////////////////////////
开始的预条件检查确保用户传递合理的年月日值;
结尾的断言确保glib构造一个健全的对象,输出健全的值。

断言函数g_assert_not_reached() 用来标识“不可能”的情况,通常用来检测不能处理的
所有可能枚举值的switch语句:
switch (val)
{
case FOO_ONE:
break ;
case FOO_TWO:
break ;
default:
/* 无效枚举值* /
g_assert_not_reached ( ) ;
break ;
}

所有调试宏使用glib的g_log()输出警告信息,g_log()的警告信息包含发生错误的应用程序或库函数名字,并且还可以

使用一个替代的警告打印例程.

########################### 内存管理 ##############################

glib用自己的g_变体包装了标准的malloc()和free(),即g_malloc()和g_free()。
它们有以下几个小优点:
* g_malloc()总是返回gpointer,而不是char *,所以不必转换返回值。
* 如果低层的malloc()失败,g_malloc()将退出程序,所以不必检查返回值是否是NULL。
* g_malloc() 对于分配0字节返回NULL。
* g_free()忽略任何传递给它的NULL指针。

函数列表: glib内存分配
#i nclude
gpointer g_malloc(gulong size)
void g_free(gpointer mem)
gpointer g_realloc(gpointer mem,gulong size)
gpointer g_memdup(gconstpointer mem,guint bytesize)

g_realloc()和realloc()是等价的。
g_malloc0(),它将分配的内存每一位都设置为0;
g_memdup()返回一个从mem开始的字节数为bytesize的拷贝。
为了与g_malloc()一致,g_realloc()和g_malloc0()都可以分配0字节内存。
g_memdup()在分配的原始内存中填充未设置的位,而不是设置为数值0。

宏列表:内存分配宏
#i nclude
g_new(type, count)
g_new0(type, count)
g_renew(type, mem, count)

########################### 字符串处理 ##############################

如果需要比gchar *更好的字符串,glib提供了一个GString类型。
函数列表: 字符串操作
#i nclude
gint g_snprintf(gchar* buf,gulong n,const gchar* format,. . . )
gint g_strcasecmp(const gchar* s1,const gchar* s2)
gint g_strncasecmp(const gchar* s1,const gchar* s2,guint n)

在含有snprintf()的平台上,g_snprintf()封装了一个本地的snprintf(),并且比原有实现更稳定、安全。
以往的snprintf()不保证它所填充的缓冲是以NULL结束的,但g_snprintf()保证了这一点。
g_snprintf函数在buf参数中生成一个最大长度为n的字符串。其中format是格式字符串,“...”是要插入的参数。

函数列表: 修改字符串
#i nclude
void g_strdown(gchar* string)
void g_strup(gchar* string)
void g_strreverse(gchar* string)
gchar* g_strchug(gchar* string)
gchar* g_strchomp(gchar* string)
宏g_strstrip()结合以上两个函数,删除字符串前后的空格。

函数列表: 字符串转换
#i nclude
gdouble g_strtod(const gchar* nptr,gchar** endptr)
gchar* g_strerror(gint errnum)
gchar* g_strsignal(gint signum)

函数列表: 分配字符串
#i nclude
gchar * g_strdup(const gchar* str)
gchar* g_strndup(const gchar* format,guint n)
gchar* g_strdup_printf(const gchar* format,. . . )
gchar* g_strdup_vprintf(const gchar* format,va_list args)
gchar* g_strescape(gchar* string)
gchar* g_strnfill(guint length,gchar fill_char)
/////////////////////////////////////////////////////////////////////////
gchar* str = g_malloc(256);
g_snprintf(str, 256, "%d printf-style %s", 1, "format");
用下面的代码,不需计算缓冲区的大小:
gchar* str = g_strdup_printf("%d printf-style %", 1, "format") ;
/////////////////////////////////////////////////////////////////////////

函数列表:连接字符串的函数
#i nclude
gchar* g_strconcat(const gchar* string1,. . . )
gchar* g_strjoin(const gchar* separator,. . . )

函数列表: 处理以NULL结尾的字符串向量
#i nclude
gchar** g_strsplit(const gchar* string,const gchar* delimiter,gint max_tokens)
gchar* g_strjoinv(const gchar* separator,gchar** str_array)
void g_strfreev(gchar** str_array)

########################### 数据结构 ##############################

链表~~~~~~~~~~

glib提供了普通的单向链表和双向链表,分别是GSList 和GList。

创建链表、添加一个元素的代码:
GSList* list = NULL;
gchar* element = g_strdup("a string");
list = g_slist_append(list, element);
删除上面添加的元素并清空链表:
list = g_slist_remove(list, element);
为了清除整个链表,可使用g_slist_free(),它会快速删除所有的链接;
g_slist_free()只释放链表的单元,它并不知道怎样操作链表内容。

访问链表的元素,可以直接访问GSList结构:
gchar* my_data = list->data;
为了遍历整个链表,可以如下操作:
GSList* tmp = list;
while (tmp != NULL)
{
printf("List data: %p\n", tmp->data);
tmp = g_slist_next(tmp);
}

/////////////////////////////////////////////////////////////////////////////
下面的代码可以用来有效地向链表中添加数据:
void efficient_append(GSList** list, GSList** list_end, gpointer data)
{
g_return_if_fail(list != NULL);
g_return_if_fail(list_end != NULL);
if (*list == NULL)
{
g_assert(*list_end == NULL);
*list = g_slist_append(*list, data);
*list_end = *list;
}
else
{
*list_end = g_slist_append(*list_end, data)->next;
}
}

要使用这个函数,应该在其他地方存储指向链表和链表尾的指针,并将地址传递给efficient_append ():
GSList* list = NULL;
GSList* list_end = NULL;
efficient_append(&list, &list_end, g_strdup("Foo"));
efficient_append(&list, &list_end, g_strdup("Bar"));
efficient_append(&list, &list_end, g_strdup("Baz"));
//////////////////////////////////////////////////////////////////////////////

函数列表:改变链表内容
#i nclude
/* 向链表最后追加数据,应将修改过的链表赋给链表指针* /
GSList* g_slist_append(GSList* list,gpointer data)
/* 向链表最前面添加数据,应将修改过的链表赋给链表指针* /
GSList* g_slist_prepend(GSList* list,gpointer data)
/* 在链表的position位置向链表插入数据,应将修改过的链表赋给链表指针* /
GSList* g_slist_insert(GSList* list,gpointer data,gint position)
/ *删除链表中的data元素,应将修改过的链表赋给链表指针* /
GSList* g_slist_remove(GSList* list,gpointer data)

访问链表元素可以使用下面的函数列表中的函数。
这些函数都不改变链表的结构。
g_slist_foreach()对链表的每一项调用Gfunc函数。
Gfunc函数是像下面这样定义的:
typedef void (*GFunc)(gpointer data, gpointer user_data);
在g_slist_foreach()中,Gfunc函数会对链表的每个list->data调用一次,将user_data传递到g_slist_foreach()函

数中。

////////////////////////////////////////////////////////////////////////////////
例如, 有一个字符串链表,并且想创建一个类似的链表,让每个字符串做一些变换。
下面是相应的代码,使用了前面例子中的efficient_append()函数。
typedef struct _AppendContext AppendContext;
struct _AppendContext {
GSList* list;
GSList* list_end;
const gchar* append;
} ;
static void append_foreach(gpointer data, gpointer user_data)
{
AppendContext* ac = (AppendContext*) user_data;
gchar* oldstring = (gchar*) data;
efficient_append(&ac->list, &ac->list_end, g_strconcat(oldstring, ac->append, NULL));
}
GSList * copy_with_append(GSList* list_of_strings, const gchar* append)
{
AppendContext ac;
ac.list = NULL;
ac.list_end = NULL;
ac.append = append;
g_slist_foreach(list_of_strings, append_foreach, &ac);
return ac.list;
}

函数列表:访问链表中的数据
#i nclude
GSList* g_slist_find(GSList* list,gpointer data)
GSList* g_slist_nth(GSList* list,guint n)
gpointer g_slist_nth_data(GSList* list,guint n)
GSList* g_slist_last(GSList* list)
gint g_slist_index(GSList* list,gpointer data)
void g_slist_foreach(GSList* list,GFunc func,gpointer user_data)

函数列表: 操纵链表
#i nclude
/* 返回链表的长度* /
guint g_slist_length(GSList* list)
/* 将list1和list2两个链表连接成一个新链表* /
GSList* g_slist_concat(GSList* list1,GSList* list2)
/ *将链表的元素颠倒次序* /
GSList* g_slist_reverse(GSList* list)
/ *返回链表list的一个拷贝* /
GSList* g_slist_copy(GSList* list)

还有一些用于对链表排序的函数,见下面的函数列表。要使用这些函数,必须写一个比较函数GcompareFunc,就像标准

C里面的qsort()函数一样。
在glib里面,比较函数是这个样子:
typedef gint (*GCompareFunc) (gconstpointer a, gconstpointer b);
如果a < b,函数应该返回一个负值;如果a > b,返回一个正值;如果a = b,返回0。

函数列表: 对链表排序
#i nclude
GSList* g_slist_insert_sorted(GSList* list,gpointer data,GCompareFunc func)
GSList* g_slist_sort(GSList* list,GCompareFunc func)
GSList* g_slist_find_custom(GSList* list,gpointer data,GCompareFunc func)

树~~~~~~~~~~~~~~

在glib中有两种不同的树:GTree是基本的平衡二叉树,它将存储按键值排序成对键值; GNode存储任意的树结构数据

,比如分析树或分类树。

函数列表:创建和销毁平衡二叉树
#i nclude
GTree* g_tree_new(GCompareFunc key_compare_func)
void g_tree_destroy(GTree* tree)

函数列表: 操纵G t r e e数据
#i nclude
void g_tree_insert(GTree* tree,gpointer key,gpointer value)
void g_tree_remove(GTree* tree,gpointer key)
gpointer g_tree_lookup(GTree* tree,gpointer key)

函数列表: 获得G Tr e e的大小
#i nclude
/ *获得树的节点数* /
gint g_tree_nnodes(GTree* tree)
/ *获得树的高度* /
gint g_tree_height(GTree* tree)

使用g_tree_traverse()函数可以遍历整棵树。
要使用它,需要一个GtraverseFunc遍历函数,它用来给g_tree_trave rse()函数传递每一对键值对和数据参数。
只要GTraverseFunc返回FALSE,遍历继续;返回TRUE时,遍历停止。
可以用GTraverseFunc函数按值搜索整棵树。
以下是GTraverseFunc的定义:
typedef gint (*GTraverseFunc)(gpointer key, gpointer value, gpointer data);
G Tr a v e r s e Ty p e是枚举型,它有四种可能的值。下面是它们在G t r e e中各自的意思:
* G_IN_ORDER (中序遍历)首先递归左子树节点(通过GCompareFunc比较后,较小的键),然后对当前节点的键值对调用

遍历函数,最后递归右子树。这种遍历方法是根据使用GCompareFunc函数从最小到最大遍历。
* G_PRE_ORDER (先序遍历)对当前节点的键值对调用遍历函数,然后递归左子树,最后递归右子树。
* G_POST_ORDER (后序遍历)先递归左子树,然后递归右子树,最后对当前节点的键值对调用遍历函数。
* G_LEVEL_ORDER (水平遍历)在GTree中不允许使用,只能用在Gnode中。

函数列表: 遍历GTree
#i nclude
void g_tree_traverse( GTree* tree,
GTraverseFunc traverse_func,
GTraverseType traverse_type,
gpointer data )

一个GNode是一棵N维的树,由双链表(父和子链表)实现。
这样,大多数链表操作函数在Gnode API中都有对等的函数。可以用多种方式遍历。

以下是一个GNode的声明:
typedef struct _GNode GNode;
struct _GNode
{
gpointer data;
GNode *next;
GNode *prev;
GNode *parent;
GNode *children;
} ;

宏列表:访问GNode成员
#i nclude
/ *返回GNode的前一个节点* /
g_node_prev_sibling ( node )
/ *返回GNode的下一个节点* /
g_node_next_sibling ( node )
/ *返回GNode的第一个子节点* /
g_node_first_child( node )

用g_node_new ()函数创建一个新节点。
g_node_new ()创建一个包含数据,并且无子节点、无父节点的Gnode节点。
通常仅用g_node_new ()创建根节点,还有一些宏可以根据需要自动创建新节点。
函数列表: 创建一个GNode
#i nclude
GNode* g_node_new(gpointer data)

函数列表: 创建一棵GNode树
#i nclude
/ *在父节点p a r e n t的p o s i t i o n处插入节点n o d e * /
GNode* g_node_insert(GNode* parent,gint position,GNode* node)
/ *在父节点p a r e n t中的s i b l i n g节点之前插入节点n o d e * /
GNode* g_node_insert_before(GNode* parent,GNode* sibling,GNode* node)
/ *在父节点p a r e n t最前面插入节点n o d e * /
GNode* g_node_prepend(GNode* parent,GNode* node)

宏列表:向Gnode添加、插入数据
#i nclude
g_node_append(parent, node)
g_node_insert_data(parent, position, data)
g_node_insert_data_before(parent, sibling, data)
g_node_prepend_data(parent, data)
g_node_append_data(parent, data)

函数列表: 销毁GNode
#i nclude
void g_node_destroy(GNode* root)
void g_node_unlink(GNode* node)

宏列表:判断G n o d e的类型
#i nclude
G_NODE_IS_ROOT ( node )
G_NODE_IS_LEAF ( node )

下面函数列表中的函数返回Gnode的一些有用信息,包括它的节点数、根节点、深度以及含有特定数据指针的节点。
其中的遍历类型GtraverseType在Gtree中介绍过。
下面是在Gnode中它的可能取值:
* G_IN_ORDER 先递归节点最左边的子树,并访问节点本身,然后递归节点子树的其他部分。
这不是很有用,因为多数情况用于Gtree中。
* G_PRE_ORDER 访问当前节点,然后递归每一个子树。
* G_POST_ORDER 按序递归每个子树,然后访问当前节点。
* G_LEVEL_ORDER 首先访问节点本身,然后每个子树,然后子树的子树,然后子树的子树的子树,以次类推。
也就是说,它先访问深度为0的节点,然后是深度为1,然后是深度为2,等等。
GNode的树遍历函数有一个GTraverseFlags参数。这是一个位域,用来改变遍历的种类。
当前仅有三个标志—只访问叶节点,非叶节点,或者所有节点:
* G_TRAVERSE_LEAFS 指仅遍历叶节点。
* G_TRAVERSE_NON_LEAFS 指仅遍历非叶节点。
* G_TRAVERSE_ALL 只是指( G_TRAVERSE_LEAFS | G_TRAVERSE_NON_LEAFS )快捷方式。

函数列表: 取得G N o d e属性
#i nclude
guint g_node_n_nodes(GNode* root,GTraverseFlags flags)
GNode* g_node_get_root(GNode* node)
Gboolean g_node_is_ancestor(GNode* node,GNode* descendant)
Guint g_node_depth(GNode* node)
GNode* g_node_find(GNode* root,GTraverseType order,GTraverseFlags flags,gpointer data)

GNode有两个独有的函数类型定义:
typedef gboolean (*GNodeTraverseFunc) (GNode* node, gpointer data);
typedef void (*GNodeForeachFunc) (GNode* node, gpointer data);
这些函数调用以要访问的节点指针以及用户数据作为参数。GNodeTraverseFunc返回TRUE,停止任何正在进行的遍历,

这样就能将GnodeTraverseFunc与g_node_traverse()结合起来按值搜索树。

函数列表: 访问GNode
#i nclude
/ *对Gnode进行遍历* /
void g_node_traverse( GNode* root,
GTraverseType order,
GTraverseFlags flags,
gint max_depth,
GNodeTraverseFunc func,
gpointer data )
/ *返回GNode的最大高度* /
guint g_node_max_height(GNode* root)
/ *对Gnode的每个子节点调用一次f u n c函数* /
void g_node_children_foreach( GNode* node,
GTraverseFlags flags,
GNodeForeachFunc func,
gpointer data )
/ *颠倒node的子节点顺序* /
void g_node_reverse_children(GNode* node)
/ *返回节点node的子节点个数* /
guint g_node_n_children(GNode* node)
/ *返回node的第n个子节点* /
GNode* g_node_nth_child(GNode* node,guint n)
/ *返回node的最后一个子节点* /
GNode* g_node_last_child(GNode* node)
/ *在node中查找值为d a t e的节点* /
GNode* g_node_find_child(GNode* node,GTraverseFlags flags,gpointer data)
/ *返回子节点child在node中的位置* /
gint g_node_child_position(GNode* node,GNode* child)
/ *返回数据data在node中的索引号* /
gint g_node_child_index(GNode* node,gpointer data)
/ *以子节点形式返回node的第一个兄弟节点* /
GNode* g_node_first_sibling(GNode* node)
/ *以子节点形式返回node的第一个兄弟节点* /
GNode* g_node_last_sibling(GNode* node)

哈希表~~~~~~~~~~`

GHashTable是一个简单的哈希表实现,提供一个带有连续时间查寻的关联数组。
要使用哈希表,必须提供一个GhashFunc函数,当向它传递一个哈希值时,会返回正整数:
typedef guint (*GHashFunc) (gconstpointer key);
除了GhashFunc,还需要一个GcompareFunc比较函数用来测试关键字是否相等。
不过,虽然GCompareFunc函数原型是一样的,但它在GHashTable中的用法和在GSList、Gtree中的用法不一样。
在GHashTable中可以将GcompareFunc看作是等式操作符,如果参数是相等的,则返回TRUE。

函数列表: GHashTable
#i nclude
GHashTable* g_hash_table_new(GHashFunc hash_func,GCompareFunc key_compare_func)
void g_hash_table_destroy(GHashTable* hash_table)

函数列表: 哈希表/比较函数
#i nclude
guint g_int_hash(gconstpointer v)
gint g_int_equal(gconstpointer v1,gconstpointer v2)
guint g_direct_hash(gconstpointer v)
gint g_direct_equal(gconstpointer v1,gconstpointer v2)
guint g_str_hash(gconstpointer v)
gint g_str_equal(gconstpointer v1,gconstpointer v2)

函数列表: 处理GHashTable
#i nclude
void g_hash_table_insert(GHashTable* hash_table,gpointer key,gpointer value)
void g_hash_table_remove(GHashTable * hash_table,gconstpointer key)
gpointer g_hash_table_lookup(GHashTable * hash_table,gconstpointer key)
gboolean g_hash_table_lookup_extended( GHashTable* hash_table,
gconstpointer lookup_key,
gpointer* orig_key,
gpointer* value )

函数列表: 冻结和解冻GHashTable
#i nclude
/ * *冻结哈希表/
void g_hash_table_freeze(GHashTable* hash_table)
/ *将哈希表解冻* /
void g_hash_table_thaw(GHashTable* hash_table)


####################################### GString #####################################

GString的定义:
struct GString
{
gchar *str; /* Points to the st’rsi ncgurrent \0-terminated value. */
gint len; /* Current length */
} ;

用下面的函数创建新的GString变量:
GString *g_string_new( gchar *init );
这个函数创建一个GString,将字符串值init复制到GString中,返回一个指向它的指针。
如果init参数是NULL,创建一个空GString。

void g_string_free( GString *string,gint free_segment );
这个函数释放string所占据的内存。free_segment参数是一个布尔类型变量。
如果free_segment参数是TRUE,它还释放其中的字符数据。

GString *g_string_assign( GString *lval,const gchar *rval );
这个函数将字符从rval复制到lval,销毁lval的原有内容。
注意,如有必要, lval会被加长以容纳字符串的内容。

下面的函数的意义都是显而易见的。其中以_c结尾的函数接受一个字符,而不是字符串。
截取string字符串,生成一个长度为l e n的子串:
GString *g_string_truncate( GString *string,gint len );
将字符串val追加在string后面,返回一个新字符串:
GString *g_string_append( GString *string,gchar *val );
将字符c追加到string后面,返回一个新的字符串:
GString *g_string_append_c( GString *string,gchar c );
将字符串val插入到string前面,生成一个新字符串:
GString *g_string_prepend( GString *string,gchar *val );
将字符c插入到string前面,生成一个新字符串:
GString *g_string_prepend_c( GString *string,gchar c );
将一个格式化的字符串写到string中,类似于标准的sprintf函数:
void g_string_sprintf( GString *string,gchar *fmt,. . . ) ;
将一个格式化字符串追加到string后面,与上一个函数略有不同:
void g_string_sprintfa ( GString *string,gchar *fmt,... );

################################## 计时器函数 ##################################

创建一个新的计时器:
GTimer *g_timer_new( void );
销毁计时器:
void g_timer_destroy( GTimer *timer );
开始计时:
void g_timer_start( GTimer *timer );
停止计时:
void g_timer_stop( GTimer *timer );
计时重新置零:
void g_timer_reset( GTimer *timer );
获取计时器流逝的时间:
gdouble g_timer_elapsed( GTimer *timer,gulong *microseconds );

################################## 错误处理函数 ##################################

gchar *g_strerror( gint errnum );
返回一条对应于给定错误代码的错误字符串信息,例如“ no such process”等。
使用g_strerror函数:
g_print("hello_world:open:%s:%s\n", filename, g_strerror(errno));

void g_error( gchar *format, ... );
打印一条错误信息。
格式与printf函数类似,但是它在信息前面添加“ ** ERROR **: ”,然后退出程序。它只用于致命错误。

void g_warning( gchar *format, ... );
与上面的函数类似,在信息前面添加“ ** WARNING **:”,不退出应用程序。它可以用于不太严重的错误。

void g_message( gchar *format, ... );
在字符串前添加“message: ”,用于显示一条信息。

gchar *g_strsignal( gint signum );
打印给定信号号码的Linux系统信号的名称。在通用信号处理函数中很有用。

################################## 其他实用函数 ##################################

glib还提供了一系列实用函数,可以用于获取程序名称、当前目录、临时目录等。
这些函数都是在glib.h中定义的。
/* 返回应用程序的名称* /
gchar* g_get_prgname (void);
/* 设置应用程序的名称* /
void g_set_prgname (const gchar *prgname);
/* 返回当前用户的名称* /
gchar* g_get_user_name (void);
/* 返回用户的真实名称。该名称来自“passwd”文件。返回当前用户的主目录* /
gchar* g_get_real_name (void);
/* 返回当前使用的临时目录,它按环境变量TMPDIR、TMPandTEMP 的顺序查找。
如果上面的环境变量都没有定义,返回“ / t m p”* /
gchar* g_get_home_dir (void);
gchar* g_get_tmp_dir (void);
/* 返回当前目录。返回的字符串不再需要时应该用g_free ( ) 释放* /
gchar* g_get_current_dir (void);
/ *获得文件名的不带任何前导目录部分的名称。它返回一个指向给定文件名字符串的指针* /
gchar* g_basename (const gchar *file_name);
/* 返回文件名的目录部分。如果文件名不包含目录部分,返回“ .”。
* 返回的字符串不再使用时应该用g_free() 函数释放* /
gchar* g_dirname (const gchar *file_name);
/* 如果给定的file_name是绝对文件名(包含从根目录开始的完整路径,比如/usr/local),返回TRUE * /
gboolean g_path_is_absolute (const gchar *file_name);
/* 返回一个指向文件名的根部标志(“/”)之后部分的指针。
* 如果文件名file_name不是一个绝对路径,返回NULL * /
gchar* g_path_skip_root (gchar *file_name);
/ *指定一个在正常程序终止时要执行的函数* /
void g_atexit (GVoidFunc func);

上面介绍的只是glib库中的一小部分, glib的特性远远不止这些。
如果想了解其他内容,参考glib.h文件。这里面的绝大多数函数都是简明易懂的。
另外,http://www.gtk.org上的glib文档也是极好的资源。

如果你需要一些通用的函数,但glib中还没有,考虑写一个glib风格的例程,将它贡献到glib库中!
你自己,以及全世界的glib使用者,都将因为你的出色工作而受益。

glib介绍

glib介绍
gnome是基于gtk+开发的一套桌面环境,gnome和KDE作为两大最流行的桌面环境,在全世界广泛使用。只要是在Linux下工作的开发人员,对于gtk+一定不陌生。而对于glib,这个gtk+下的无名英雄,其功能强大却鲜为人知。今天,在这里简要介绍一下,如果你是开发人员,看完本文,相信你会爱上它的。

glib不是gllibc,尽管两者都是基于(L)GPL的开源软件。但这一字之差却误之千里,glibc是GNU实现的一套标准C的库函数,而glib是gtk+的一套函数库。在linux平台上,像其它任何软件一样,glib依赖于glibc。

glib不是一个学院派的东西,也不是凭空想出来的,完全是在开发gtk+的过程中,慢慢总结和完善的结果。如果你是一个工作3年以上的C语言程序员,现在让你讲讲写程序的苦恼,你可能有很多话要说,但如果你有时间研究一下glib,你会发现,很多苦恼已不再成其为苦恼,glib里很多东西正是你期望已经久的。

gobject是glib的精粹,glib是用C实现的,但在很大程序是基于面向对象思想设计的,gobject是所有类的基类。signal在其中也是一大特色,signal与操作系统中的signal并不一样,它是类似消息一样的东西,让消息在各个对象间传递,但尽量降低对象间的耦合。仔细读一下它的代码,唯一想说的话就是“绝!”。

动态数组、链表、哈希表等通用容器,在不同的公司,在不同的时期,在不同的情况下,我们每个人对每一种容器,可能都实现过N次以上。甚至在同一个项目里,出现几份链表的实现,也并非罕见。一直在抱怨,标准C中为什么没有类似于STL的标准容器,让全世界的程序员在数以万次的重复实现它们。不过,还算走运,有了glib,恶梦在此终结了。glib提供了动态数组、单/双向链表、哈希表、多叉树、平衡二叉树、字符串等常用容器,完全是面向对象设计的,实现得非常精致。不用白不用,别客气了。

你开发过跨硬件平台的软件吗?是不是常常为硬件平台的差异而苦恼呢?字节顺序是常见的问题之一,大端格式,小端格式,还是PDP格式的?这样差异造成的BUG会浪费不少时间,同时让代码晦涩难读。glib提供了一套完整的宏,利用这些宏编写程序,问题大大简化了。

你开发过跨操作系统的软件吗?在不同的平台下,很多函数有相同的功能,但函数原型,返回值差异巨大,也有的貌合神离,相同的名称有着不同的意义,请看下面这张清单:
动态库加载函数
线程函数
信号量函数
互斥锁函数
事件函数
字符集转换函数
原子操作函数
创建进程函数
时间格式
IO函数

为完成相同的功能,你却要为此写出不同的代码。那怕只是一个简单的封装,这么多,你烦不烦?用glib吧,它的作者们都是高手中的高手,对不同的平台的理解程度,远远超出你和我,而且这些程序经过大量的应用程序测试,已经非常的稳定了,放心的用吧。

你写过读取配置文件的模块吧,像读取ini这样简单格式的文件,当然不在话下,要读markup语言格式的配置文件,难度可能大了点,虽然不是不可能,但除非是为了学习,何必自己找麻烦呢。glib里提供了词法分析、markup语言解析、ini文件存取等功能,你完全不用为此担忧。

不管是命令行程序,还是GUI程序,或者后台服务进程,通过命令参数来控制程序的行为,都是开发人员惯用的手法。从命令行参数取到有用的信息,不难,遍历一遍不就行了吗?但在每个程序中都来遍一次,你不觉得很单调吗?我们的时间很宝贵,浪费时间做这种事情,不值得吧。glib的作者们早考虑到了,它提供了这样的功能,你调用的它的函数,可能轻松的取得所要的命令行参数。


对于正则表达式,Win32下的开发人员可能用得少一点,而在Linux下的Shell里,它却是不可或缺之物。有没有想到在自己的程序中使用的它呢,在有的情况下,使用正则表达式,可能会大大降低开发的难度,提高易用性。glib也想到了这一点,它提供了简单的正则表达式功能,当然,用与不用完全取决于你自己。


在程序里,如果程序出错了,特别是调了不该调用的函数,你是不是很想知道是谁调了它?大多数时候用debuger是最好的选择,在某些情况下,没有debuger可用,或者在debuger里重现不了这个问题,你会不会觉得很无助?别怕,glib提供了一套跨平台的backtrace函数,有了它,你可以很容易找到元凶了。


如果读过一些大型的开源项目,你会发现,差不多每个项目都有一套自己的log函数,用于记录程序运行的调试或者审计信息。也有可能,你自己都曾在不同的项目里实现过好几次这个功能,第一次实现会觉得很好玩,会学到了一些东西,后来就会发现自己在做无用功。重复就是浪费,重复就是犯罪,glib想到了这一点,它实现了一套完整的log机制,供大家在不同的项目中使用。


产生质数的算法不难,在我们的程序里也像是星外来客,很少使用,常常忽略了它。所谓书到用时方恨少,有这样的东东,说不定就有用得到它的时候。glib提供了这样的功能,同时还提供了一个较好的随机数算法。

Linux Shell里的自动补完功能很好用吧,从Linux转移到Win32下工作的开发人员,常常抱怨Win32的Shell里没有这个功能,其实Win32下也是有的,把注册表里的一个选项打开就行了。不管怎么样,总之这个功能太有用了,没有它,你都会觉得少了点什么,全身不自在,glib里连这个功能都提供了,是不是很人性化呢?


内存问题!还是内存问题!如果你没有为内存问题而苦恼过,我都怀疑你是不是一个真正的C语言程序员。内存泄露、访问越界、空指针、野指针和内存优化等问题,是不是都曾让你夜不能昧?有些工具可以帮助你,但这不是万能的良药,好好写你的程序才是第一要义。glib提供了一些的手段,也可以在一定程度上减轻你的痛苦。

0pt;">

呵,怎么样?还快不去下载一个来玩玩?ftp://ftp.gtk.org/pub/gtk/v2.8/

Linux系统下的C语言开发都需要学些什么

  一、工具的使用

  1、学会使用vim/emacs,vim/emacs是linux下最常用的源码编辑具,不光要学会用它们编辑源码,还要学会用它们进行查找、定位、替换等。新手的话推荐使用vim,这也是我目前使用的文本编辑器。

  2、学会makefile文件的编写规则,并结合使用工具aclocal、autoconf和automake生成makefile文件。

  3、掌握gcc和gdb的基本用法。掌握gcc的用法对于构建一个软件包很有益处,当软件包包含的文件比较多的时候,你还能用gcc把它手动编译出来,你就会对软件包中各个文件间的依赖关系有一个清晰的了解。

  4、掌握svn/cvs的基本用法。这是linux,也是开源社区最常用的版本管理系统。可以去试着参加sourceforge上的一些开源项目。

  二、linux/unix系统调用与标准C库

  系统调用应用软件与操作系统的接口,其重要性自然不用说,一定要掌握。推荐学习资料为steven先生的UNIX环境高级编程(简称APUE)。

  三、库的学习

  无论是在哪个平台做软件开发,对于库的学习都很重要,linux下的开发库很多,我主要介绍一下我常常用到的一些库。

  1、glib库

  glib 库是gtk+和gnome的基础库,并具是跨平台的,在linux、unix和windows下都可以用。glib库对于linux平台开发的影响就像 MFC对windows平台开发的影响一样,很多开源项目都大量的使用了glib库,包括gimp、gnome、gaim、evolution和 linux下的集群软件heartbeat.因为glib库自带有基本的数据结构实现,所以在学习glib库的时候可以顺便学习一下基本的数据结构(包括链表、树、队列和hash表)。

  2、libxml库

  libxml是linux平台下解析XML文件的一个基础库,现在很多实用软件都用XML格式的配置文件,所以也有必要学习一下。

  3、readline库

  readline 库是bash shell用的库,如果要开发命令行程序,那么使用readline库可以减少很多工作量,比如bash里的命令行自动补全,在readline里就已经有实现,当然你也可以用自己的实现替代库的行为。readline库有很多网站介绍的,只要google一下readline就可以找到一堆了。

  4、curses库

  curses 库以前是vi程序的一部分,后来从vi里提取出来成为一个独立的库。curses库对于编写终端相关的程序特别有用,比如要在终端某一行某一列定位输出,改变终端字体的颜色和终端模式。linux下的curses库用的是GNU实现的ncurses(new curses的意思)。

  5、gtk+和KDE库

  这两个库是开发GUI应用程序的基础库,现在linux下的大部份GUI程序都是基于这两个库开发的,对于它们 的学习也是很有必要的。

  四、网络的学习

  网络这个东西太宽了,推荐学习资料steven先生的UNIX网络编程(简称UNP)和TCP/IP协议详解,更进一步的话可以学习使用libnet编写网络程序。

2010年6月2日水曜日

头文件与之实现文件的的关系



今天在网上看到一篇解释.h与.c(.cpp)的文章,我读完后感到有些地方不妥,特此按照我的理解,给初学者一些指导~
说几句题外话,刚才让女朋友陪我出去一会,她说她要先化化妆,我随口就来--简单就是美丽啊!
你猜她说什么:美丽除了天生丽质外,保养也是很重要的~ 我倒~

你理解简单的含义吗?
关于两者以前的关系,要从N年以前说起了~ long long ago,once aupon a time .......
那是一个被遗忘的年代,在编译器只认识.c(.cpp))文件,而不知道.h是何物的年代。
那时的人们写了很多的.c(.cpp)文件,渐渐地,人们发现在很多.c(.cpp)文件中的声明语句就是相同的,但他们却不得不一个字一个字地重复地将这些内容敲入每个.c(.cpp)文件。但更为恐怖的是,当其中一个声明有变更时,就需要检查所有的.c(.cpp)文件,并修改其中的声明,啊~简直是世界末日降临!
终于,有人(或许是一些人)再不能忍受这样的折磨,他(们)将重复的部分提取出来,放在一个新文件里,然后在需要的.c(.cpp)文件中敲入#include XXXX这样的语句。这样即使某个声明发生了变更,也再不需要到处寻找与修改了---世界还是那么美好!
因为这个新文件,经常被放在.c(.cpp)文件的头部,所以就给它起名叫做“头文件”,扩展名是.h.
从此,编译器(其实是预处理器)就知道世上除了.c(.cpp)文件,还有个.h的文件,以及一个叫做#include命令。

虽然后来又发生很多的变化,但是这样的用法一直延续至今,只是时日久远了,人们便淡忘了当年的缘由罢了。
提到了头文件,就说说它的作用吧~
想到了林锐GG写的高质量C/C++编程上头文件的作用的简短描述:
(1)通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。编译器会从库中提取相应的代码。
(2)头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。


预处理是编译器的前驱,作用是把存储在不同文件里的程序模块集成为一个完整的源程序.
#include本身只是一个简单的文件包含预处理命令,即为把include的后面文件放到这条命令这里,除此之外,没有其它的用处(至少我也样认为).


我对乾坤一笑兄的观点,十分赞同,基础的东东一定要弄明白.
我下面就乾坤一笑兄的例子做讲,完备他的一些让人迷惑不解的时候~

例子:
//a.h
void foo();


//a.c
#include "a.h" //我的问题出来了:这句话是要,还是不要?
void foo()
{
return;
}

//main.c
#include "a.h"
int main(int argc, char *argv[])
{
foo();
 return 0;
}

针对上面的代码,请回答三个问题:
a.c 中的 #include "a.h" 这句话是不是多余的?
1.为什么经常见 xx.c 里面 include 对应的 xx.h?
2.如果 a.c 中不写,那么编译器是不是会自动把 .h 文件里面的东西跟同名的 .c 文件绑定在一起?
3.第三个问题我给他改了一下:如果 a.c 中不写include<>,那么编译器是不是会自动把 .h 文件里面的东西跟同名的.c文件绑定在一起?

下面是乾坤一笑的原话:

从C编译器角度看,.h和.c皆是浮云,就是改名为.txt、.doc也没有大的分别。换句话说,就是.h和.c没啥必然联系。.h中一般放的是同名.c文件中定义的变量、数组、函数的声明,需要让.c外部使用的声明。这个声明有啥用?只是让需要用这些声明的地方方便引用。因为 #include "xx.h" 这个宏其实际意思就是把当前这一行删掉,把 xx.h 中的内容原封不动的插入在当前行的位置。由于想写这些函数声明的地方非常多(每一个调用 xx.c 中函数的地方,都要在使用前声明一下子),所以用 #include "xx.h" 这个宏就简化了许多行代码——让预处理器自己替换好了。也就是说,xx.h 其实只是让需要写 xx.c 中函数声明的地方调用(可以少写几行字),至于 include 这个 .h 文件是谁,是 .h 还是 .c,还是与这个 .h 同名的 .c,都没有任何必然关系。
这样你可能会说:啊?那我平时只想调用 xx.c 中的某个函数,却 include了 xx.h 文件,岂不是宏替换后出现了很多无用的声明?没错,确实引入了很多垃圾 ,但是它却省了你不少笔墨,并且整个版面也看起来清爽的多。鱼与熊掌不可得兼,就是这个道理。反正多些声明(.h一般只用来放声明,而放不定义,参见拙著 “过马路,左右看”)也无害处,又不会影响编译,何乐而不为呢?
翻回头再看上面的3个问题,很好解答了吧?
它的解答如下:

答:1.不一定。这个例子中显然是多余的。但是如果.c中的函数也需要调用同个.c中的其它函数,那么这个.c往往会include同名的.h,这样就不需要为声明和调用顺序而发愁了(C语言要求使用之前必须声明,而include同名.h一般会放在.c的开头)。有很多工程甚至把这种写法约定为代码规范,以规范出清晰的代码来。
2.答:1中已经回答过了。
3.答:不会。问这个问题的人绝对是概念不清,要不就是想混水摸鱼。非常讨厌的是中国的很多考试出的都是这种烂题,生怕别人有个清楚的概念了,绝对要把考生搞晕。

over!

在此里要明确一点,编译器是按照编译单元进行编译的,所谓的编译单元,是指一个.c文件以及它所include的所有.h文件.最直观的理解就是一个文件,一个工程中可以包含很多文件,其中有一个程序的入口点,即我们通常所说的main()函数(当然也可以没有这个函数,程序照样能启动,详细见我的 blog中).在没有这个程序入口点的情况下,编译单元只生成目标文件object file(.o文件,windows下叫做.obj).

这个例子中总共包含了二个编译单元,分别是a.c,main.c,按照我所说的,在编译阶段只是生成各自的.o文件.这个阶段不和其它的文件发生任何的关系.
而include这个预处理指令发生在预处理阶段(早先编译阶段,只是编译器的一个前驱处理程序).


.h .c不见得是浮云,脱离了编译器谈这些没有任何的意义,抛开更深层次的这些,比如说,OS如何启动这个文件,PE结构(linux 下为elf)等等
编译器首先要识别这个文件才可能去编译它,这是前提.如果你改了它的扩展名那么你的编译器还能认识它吗~上升到一个更高的层次上看待这个问题,XX兄说的也不错~我想XX兄说的意思就是两者不可因为名字相同就认为两者有什么关系,名字是可以随便的~
两者之间的联系,我在前面说过了,是由于历史的原因造成的,再加上人的习惯,我想谁也不想多去记那么多文件名吧.(拿我举个例子,一个数
据表如果多于30个字段,我就觉得头大了,现在弄的表有的多达上百个字段,真希望那位高人研究出什么好的方法来~,也让我们的世界美好一些~)

乾坤一笑的第三个问题很有代表性,多次在网上看到,现在的编译器绝对没有那么智能,而且也没有必须那么做.下面我们主要聊聊编译器的处理过程.(我想初学者有疑问的正在于此,即是对于编译过程.h .c(.cpp)的变化不太了解,)

下面我说举个简单的例子来聊聊~
例子如下:
//a.h
class A
{
pubic:
int f(int t);
};

//a.cpp
#include "a.h"
int A::f(int t)
{
return t;
}

//main.cpp
#include "a.h"
void main()
{
A a;
a.f(3);
}
在预处理阶段,预处理器看到#include "文件名"就把这个文件读进来,比如它编译main.cpp,看到#include "a.h",它就把a.h的内容读进来,它知道了,有一类A,包含一个成员函数f,这个函数接受一个int型的参数,返回一个int型的值。再往下编译很容易就把A a这行读懂了,它知道是要拿A这个类在栈上生成一个对象。再往下,它知道了下面要调用A的成员函数f了,参数是3,由于它知道这个函数要一个整形数用参数,这个3正好匹配,那就正好把它放到栈上,生成一条调用f(int)函数的指令(一般可能是一句call),至于这个f(int)函数到底在哪里,它不知道,它留着空,链接时再解决。它还知道f(int)函数要返回一个int,所以也许它也为这一点做好了准备(在例子中,我们没用这个返回值,也许它就不处理)。再往下到文件末尾了main.cpp编译好了,生成了main.obj。整个编译过程中根本就不需要知道a.cpp的内容。
同理,编译器再编译a.cpp,把f()函数编译好,编译a.cpp时,它也不用管别的,把f()编译好就行了。生成了a.obj。
最后一步就是链接的阶段了,链接器把项目中所有.cpp生成的所有.obj链接起来,
在这一步中,它就明确了f(int)函数的实现所在的地址,把main.obj中空着的这个地址位置填上正确的地址。最终生成了可执行文件main.exe。

明白了吗?不明白那就多说几句了,我们在学编译原理的时候都知道,编译器是分阶段进行的,每一个阶段将源程序从一种表示转换成另一种表示,一般情况下都进行如下顺序:源程序->词法分器->语法分析器->语义分析器->中间代码生成器->代码优化器->代码生成器->目标程序.
其中这中间6项活动都要涉及的两项主要活动是:符号管理器与错误处理器.
归根原因,这里有一个叫做符号表的东东在里面让你着魔一样不明白,其实符号表是一个数据结构.编译器的基本一项功能就是要记录源程序中使用的标识符并收集与每个标识符相关的各种属性信息.属性信息表明了该标识符的存储位置/类型/作用域(在那个阶段有效)等信息,通俗的说一下就是,当编译器看到一个符号声明时,例如你的函数名它就会把它放到这个符号表中去登记一下~符号表里存放着你的函数的入口地址,参数个数,返回信息等等一堆东西~而在联接阶段主要是处理工程中的符号表与调用对应处理关系,即我们通常所说的解引用.