C语言之extern声明辨析

1 基本解释
  extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。

  另外,extern也可用来进行链接指定。

2 问题:extern 变量

  在一个源文件里定义了一个数组:

char a[6];

  在另外一个文件里用下列语句进行了声明:

extern char *a;

  请问,这样可以吗?

  答案与分析:

  1)、不可以,程序运行时会告诉你非法访问。原因在于,指向类型T的指针并不等价于类型T的数组。extern char *a声明的是一个指针变量而不是字符数组,因此与实际的定义不同,从而造成运行时非法访问。应该将声明改为extern char a[ ]。

  2)、例子分析如下,如果a[] = “abcd”,则外部变量a=0x61626364 (abcd的ASCII码值),*a显然没有意义,如下图:


  显然a指向的空间(0x61626364)没有意义,易出现非法内存访问。

  3)、这提示我们,在使用extern时候要严格对应声明时的格式,在实际编程中,这样的错误屡见不鲜。

  4)、extern用在变量声明中常常有这样一个作用,你在*.c文件中声明了一个全局的变量,这个全局的变量如果要被引用,就放在*.h中并用extern来声明。
3 问题:extern 函数1

  常常见extern放在函数的前面成为函数声明的一部分,那么,C语言的关键字extern在函数的声明中起什么作用?

  答案与分析:

  如果函数的声明中带有关键字extern,仅仅是暗示这个函数可能在别的源文件里定义,没有其它作用。即下述两个函数声明没有明显的区别:


  extern int f(); 和int f();


  当然,这样的用处还是有的,就是在程序中取代include “*.h”来声明函数,在一些复杂的项目中,我比较习惯在所有的函数声明前添加extern修饰。
4 问题:extern 函数2

  当函数提供方单方面修改函数原型时,如果使用方不知情继续沿用原来的extern申明,这样编译时编译器不会报错。但是在运行过程中,因为少了或者多了输入参数,往往会照成系统错误,这种情况应该如何解决?

  答案与分析:

  目前业界针对这种情况的处理没有一个很完美的方案,通常的做法是提供方在自己的xxx_pub.h中提供对外部接口的声明,然后调用方include该头文件,从而省去extern这一步。以避免这种错误。

  宝剑有双锋,对extern的应用,不同的场合应该选择不同的做法。
5 问题:extern “C”

  在C++环境下使用C函数的时候,常常会出现编译器无法找到obj模块中的C函数定义,从而导致链接失败的情况,应该如何解决这种情况呢?

  答案与分析:

   C++语言在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个中间的函数名称,而C语言则不会,因此会造成链接时找不到对应函数的 情况,此时C函数就需要用extern “C”进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间函数名。

  下面是一个标准的写法:


  //在.h文件的头上

#ifdef __cplusplus

#if __cplusplus

extern “C”{

 #endif

 #endif /* __cplusplus */

 …

 …

 //.h文件结束的地方

 #ifdef __cplusplus

 #if __cplusplus

}

#endif

#endif /* __cplusplus */

Linux 内核笔记 — 信号




1 前言

写作本文的目的和其它文章略有不同,不是为了系统和全面的介绍”信号”这个子系统,–虽然它不复杂,其内容也不是一篇短短的文章所能够覆盖的,而是要回答自己的疑惑,解决工作中遇到的一些问题,理解那些不能够马上了解的部分。说到底,可以将这篇文章看作问题的答案。

曾经遇到的问题放在最后一节”问题与答案”中,在阅读正文之前先扫描一下问题可能更加有助于理解文章中的内容。

欢迎大家对这篇文章提出意见和指正,我的email是:shisheng_liu@yahoo.com.cn


2 许可协议

本文的许可协议遵循GNU Free Document License。协议的具体内容请参见http://www.gnu.org/copyleft/fdl.html。在遵循GNU Free Document License的基础上,可以自由地传播或发行本文,但请保留本文的完整性。


3 什么是信号
信号是UNIX进程间通信的一种标准方式,在最早期的UNIX系统中已经存在。信号的出现允许内核和其它进程通知进程特定事件的发生。现代UNIX中也存在其它的进程间通信方式,但由于信号相对简单和有效,它们仍然被广泛使用。
信号是最简单的消息,当一个信号被发送时,它没有参数等附加信息,唯有一个整数来表示信号的值。信号的值在所有的UNIX系统中已经标准化了,每一个信号都有一个名字,它以三个字母SIG开头,对应于特定的事件。例如:SIGKILL表示终止进程;SIGBUS代表硬件故障;SIGCHLD代表子进程状态改变。

UNIX中常用的信号有31个,除了前面提到的三个信号外,还有如下信号是文章中用到的,其它的不再一一列出。

SIGSTOP 暂停程序执行

SIGCONT 恢复程序执行

3.1 进程对信号的处理

进程可以对每一个信号设置单独的处理方式,它可以忽略该信号,也可以设置自己的信号处理程序(称为捕捉),或者对信号什么也不做,信号发生的时候执行系统默认动作。

进程还可以设置对某些信号的阻塞(block)标志。如果一个信号被设置为阻塞,当信号发生的时候,它会与正常的信号一样被递送(deliver)给进程,但只有进程解除对信号的阻塞时才会被处理。

从一个非阻塞信号被递送给进程到信号得到处理之间的时间间隔,称为信号未决(pending)。有的资料将pending翻译为”信号挂起”。

所有的信号中,有两个信号SIGSTOP和SIGKILL是特殊的,他们既不能被捕捉,也不能被忽略,也不能被阻塞,这个特性保证了系统管理员在任何时候都可以用信号暂停和结束程序。

3.2 进程数据结构中与信号相关的部分

从这一部份开始,我们转入对linux内核部分的分析。在内核中,一个基本的数据结构sigset_t用来表示信号。不同的CPU架构中sigset_t的长度略有不同,对Intel i386架构来说,sigset_t是一个64位整数,每一位表示一个信号。Sigset_t中的后32位表示实时信号,它和普通信号的唯一区别是支持同一个信号的排队,这保证了发送的多个实时信号都被接收。实时信号是POSIX标准的一部分,但linux并没有对它做单独的处理,除了支持排队以外,因此它不是我们关心的重点。

进程结构task_struct中包含下列数据成员与信号相关

l sigpending

类型为int的一个标志,如果非0表示进程中存在着未决的非阻塞信号等待处理。

l pending

类型为struct sigpending的变量。可以看作进程的信号队列,它存放所有未决的信号,对某些信号来说,还包括相关的信息。例如SIGCHLD信号是子进程结束时发送给父进程的,它的附加信息中包括了子进程的ID和结束值。

l blocked

类型为sigset_t的变量。表示当前进程中哪些信号被阻塞。

l sig

类型为struct signal_struct的变量。描述进程怎样处理每个信号

3.3 相关函数

下面是进程处理信号时经常用到的系统调用

l sigaction

设置或读取进程对某个信号的处理方式。进程可以忽略或用默认方式处理该信号,也可以设置自己的信号处理程序。

l Sigprocmask

设置或读取进程的信号阻塞掩码。

l Sigpending

返回当前未决的信号集。被阻塞的未决信号并不返回。

4 信号的发送
信号是异步事件,当一个信号被发送给进程时,接收进程可能运行在任何时刻,处于任何一种状态。为了使程序处于不同状态时都能够正确的处理信号,系统在信号发送的时候需要进行适当的预处理。我们讨论的过程实际上包括信号的产生和信号的递送,下面会进行详细的分析。

4.1 进程的状态

从进程本身的状态来看,它有可能处在运行态(RUNNING),等待态(INTERRUPTBLE &UNINTERRUPTBLE), 停止态(STOPPED)和僵死态(ZOMBIE)。而从进程运行的模式(mode)来看,又可能位于内核模式和用户模式的任一种,

4.2 程序分析

有几种方法可以将信号发送给某个进程,但它们最终会调用内核中的一个函数send_sig_info来完成发送。在这个函数里,内核会进行一系列检查,只有满足适当条件*的信号才会被放在进程的信号队列中;接着检查程序是否处于INTERRUPTABLE态,如果是就唤醒该进程,将进程的状态改为RUNNING态,并且放在系统运行队列内。

系统对发送信号的处理有两点比较有意思:

1) 只有SIGKILL和SIGCONT信号能够被状态为STOPPED进程接收。

所有其他信号都被忽略。这是由STOPPED状态本身的特性决定的,它被设置来控制进程的执行和暂停,SIGCONT信号能够使暂停的程序恢复执行,而SIGKILL被接收则提供杀死暂停进程的能力。

2)所谓满足适当的信号是满足一系列检查条件的信号

.a)发送信号的进程满足POSIX.1中对发送者的要求

.b)该进程没有显式/隐含忽略该信号

.c)该进程没有阻塞该信号

.d)同样的信号没有已经在进程中挂起。同一进程的同一信号是不排队的,如果一个信号被连续发送多次,而它已经在接收进程中被挂起时,后续的信号被简单丢弃。

4.3 相关函数

4.3.1 内核部分

l send_sig_info(kernel/signal.c)

完成信号发送的入口函数,其他所有函数最终都通过它完成信号的发送。

l force_sig_info(kernel/signal.c)

仅供内核函数使用的强制信号发送函数。它做了一些努力以确保进程既不能忽略,又不能阻塞该信号,然后调用send_sig_info来完成信号的发送。

4.3.2 用户部分

l kill系统调用

进程通过kill系统调用向其他进程发送信号,kill系统调用在内核中的实现参见kernel/signal.c中的sys_kill() 函数,它调用send_sig_info来完成信号的发送。

Kill能够向一个进程或整个进程组发送信号。通过在kill系统调用时指定负的接收进程ID(”pid”),信号被发送给ID为”-pid”的进程组;如果将接收进程ID指定为-1,则信号被发送给除自己外的全体进程,这显然是一个不应该经常使用的功能。

5 信号的处理

5.1 信号处理程序
信号处理程序是进程本身的执行程序的一部分,只有进程正在CPU上运行的时候才能得到执行,也就是说,如果进程得不到执行的机会,例如状态是UNINTERRUPTABLE, STOPPED(如前面所说,可以接收SIGKILL, SIGCONT信号)等状态,发给进程的信号永远不会得到处理的机会。

5.2 何时执行

内核会在一些特定的时间点,– 具体的说,在每一个系统调用结束,程序从内核态返回到用户态之前和程序从中断和异常代码中返回的时候[参见arch/kernel/entry.S文件中的ret_from_sys_call]代码 –, 检查当前进程是否有信号未被处理,然后调用信号处理的主函数do_signal。

5.3 程序分析

作为一个主函数,do_signal有相当多值得注意的地方。它循环地工作,每一个循环都从进程中取出一个未决的信号,取信号的顺序是由小到大,然后加以处理。处理的过程如下:

1) 如果程序正在被跟踪,程序会被转入STOPPED态,同时一个SIGCHLD信号被发送给跟踪者。一个有意思的事情是:当程序再次得到运行权后,它是怎样运行的呢?

2) 如果信号的处理操作是忽略该信号,则立刻完成本次循环,但SIGCHLD信号是一个例外,它调用sys_wait4函数,强迫这个进程读子进程的信息,借此清理由终止的子进程所留下的内存。

3) 如果信号的处理操作是默认处理,do_signal会执行信号的缺省操作,一个例外当接收进程是init时,信号被丢弃,本次循环会立即完成。

不同信号的缺省操作是不同的,可以分为四类,第一类信号的缺省操作是忽略,例如SIGCHLD信号;第二类信号会将进程转入停止态(注意,并不是结束进程),例如SIGSTOP信号;第三类信号会结束进程并将结束前的状态写入core文件,例如表示非法内存访问的SIGSEGV信号;而另一些信号仅仅简单的结束进程(do_exit函数),例如SIGKILL信号。

4)最后,调用进程本身的信号处理程序来处理信号,在完成了这个调用后,do_signal直接返回,也就是说,与其他被通过循环处理的信号不同,带有自己的信号处理函数的信号在一次do_signal调用中只能处理一个。这样做的原因是,与其它信号处理方式不同,进程的信号处理函数为用户态函数,它不可能直接在do_signal循环中被调用,否则会带来严重的安全性问题,do_signal能做的是设置程序的执行寄存器环境和堆栈代码,使进程回到用户态首先执行信号处理函数。设置进程的栈环境是一项相当复杂的工作,值得用另一篇文章来单独说明了,在此不再赘述,可以参考具体代码arch/kernel/signal.c中的handle_signal函数。


5.4 相关函数

所有的相关函数都位于内核态。

l ret_from_syscall

系统调用结束,返回到用户态前执行的函数。它检查进程中是否存在信号未决,并调用do_signal函数来处理未决的信号。Ret_from_syscall位于汇编文件arch/kernel/entry.S中,它其实并不是一个函数,而是一个汇编语言程序点,调用者用jmp指令来进入它。

l do_signal
信号处理的主函数。位于arch/kernel/signal.c文件中。

l handle_signal

处理有”自定义信号处理函数”的信号,完成建立堆栈等复杂工作。

6 信号与系统调用

6.1 允许被信号中断的系统调用

正常执行的系统调用是不会被信号中断的,但某些系统调用可能需要很长的等待时间来完成,对于这种情况,允许信号中断它的执行提供了更好的程序控制能力。要达到允许被信号中断的功能,系统调用的实现上需要满足如下条件:

系统调用必须在需要等待的时候将进程转入睡眠状态,主动让出CPU。因为作为内核态的程序,系统调用的执行是不可抢占的,不主动放弃CPU的系统调用会一直运行直到结束。而且当前进程的睡眠状态必须设置为TASK_INTERRUPTABLE,只有在这个状态下,当信号被发送给进程时,进程的状态被(信号发送函数)唤醒,并重新在运行队列中排队等待调度。

当进程获得了一个CPU时间片后,它接着睡眠时的下一条指令开始运行(还在系统调用内),系统调用判断出收到了信号,会设置一个与信号有关的退出标志并迅速结束。退出标志为:ERESTARTNOHAND,ERESTARTSYS,ERESTARTNOINTR中的一种,这些标志都是和接收信号程序do_signal通信用的,它们和进程对特定信号的处理标志一起,决定了系统调用中断后是否重新执行。

ERESTARTNOINTR:要求系统调用被无条件重新执行。

ERESTARTSYS: 要求系统调用重新执行,但允许信号处理程序根据自己的标志来做选择。

ERESTARTNOHAND:在信号没有设置自己的信号处理函数时系统调用重新执行。


6.2 系统调用的重新执行

在系统调用结束的时候,do_signal函数得以执行,它会与系统调用的退出标志一起来决定是否重新执行系统调用。do_sigal函数需要对两种情况进行处理,一种是进程对收到的信号设置了自己的信号处理程序,而另一种是进程没有设置收到的信号信号处理程序,而且在处理完所有信号后,进程没有被停止或者结束掉。在后一种情况中,当系统调用退出的标志为ERESTARTNOHAND,ERESTARTSYS,ERESTARTNOINTR中的任意一个时,do_signal会使系统调用重新启动。而对前一种情况,只有系统调用的退出标志为ERESTARTNOINTR或者ERESTARTSYS并且信号处理标志也满足条件的时候系统调用才能重新启动。

6.3 相关函数

l sys_sigaction

系统调用sigaction的实现。通过sigaction系统调用,进程可以设置特定信号的处理程序和处理标志,其中一个标志SA_RESTART影响是否允许系统调用的重新执行。

7 问题与答案

l 为什么有时候进程阻塞于系统调用中时,该阻塞可以被信号中断;而阻塞于另一些系统调用的时候就不可以被中断?

如”信号与系统调用”一节所述,只有将进程以TASK_INTERRUPTABLE方式阻塞的系统调用才能够被中断。而设置以TASK_UNINTERRUPTABLE方式阻塞的系统调用不能中断。

l 系统是如何确保SIGKILL/SIGSTOP信号不能被捕捉和忽略的?

进程通过系统调用sigaction来设置对信号的处理方式,sigaction在内核中的实现函数是sys_sigaction(kernel/signal.c),它对进程传递信号值进行检查,确保信号SIGKILL和SIGSTOP不能被设置。

另外两个系统调用sigprocmask和sigsuspend会更改信号的屏蔽字,与sigaction类似,它们在内核中的实现函数会检查参数,确保SIGKILL和SIGSTOP不被屏蔽。

l 阻塞信号和忽略信号两种操作有何不同?

阻塞信号允许信号被发送给进程,但不进行处理,需要等到阻塞解除后再处理。而忽略信号是进程根本不接收该信号,所有被忽略的信号都被丢弃。

系统调用sigprocmask和sigsuspend被用来设置信号的阻塞与否;而系统调用sigaction则设置进程是否忽略一个信号

linux信号…..

  信号是Linux编程中非常重要的部分,本文将详细介绍信号机制的基本概念、Linux对信号机制的大致实现方法、如何使用信号,以及有关信号的几个系统调用。

  信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号,也有人称作软中断。从它的命名可以看出,它的实质和使用很象中断。所以,信号可以说是进程控制的一部分。

1.信号的基本概念

  本节先介绍信号的一些基本概念,然后给出一些基本的信号类型和信号对应的事件。基本概念对于理解和使用信号,对于理解信号机制都特别重要。下面就来看看什么是信号。

1.1 基本概念

  软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。

  收到信号的进程对各种信号有不同的处理方法。处理方法可以分为三类:第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。第二种方法是,忽略某个信号,对该信号不做任何处理,就象未发生过一样。第三种方法是,对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是使得进程终止。进程通过系统调用signal来指定进程对某个信号的处理行为。

  在进程表的表项中有一个软中断信号域,该域中每一位对应一个信号,当有信号发送给进程时,对应位置位。由此可以看出,进程对不同的信号可以同时保留,但对于同一个信号,进程并不知道在处理之前来过多少个。

1.2 信号的类型

  发出信号的原因很多,这里按发出信号的原因简单分类,以了解各种信号:

  1. 与进程终止相关的信号。当进程退出,或者子进程终止时,发出这类信号。
  2. 与进程例外事件相关的信号。如进程越界,或企图写一个只读的内存区域(如程序正文区),或执行一个特权指令及其他各种硬件错误。
  3. 与在系统调用期间遇到不可恢复条件相关的信号。如执行系统调用exec时,原有资源已经释放,而目前系统资源又已经耗尽。
  4. 与执行系统调用时遇到非预测错误条件相关的信号。如执行一个并不存在的系统调用。
  5. 在用户态下的进程发出的信号。如进程调用系统调用kill向其他进程发送信号。
  6. 与终端交互相关的信号。如用户关闭一个终端,或按下break键等情况。
  7. 跟踪进程执行的信号。

  Linux支持的信号列表如下。很多信号是与机器的体系结构相关的。

  首先列出的是POSIX.1中列出的信号:

信号   值  处理动作 发出信号的原因
----------------------------------------------------------------------
SIGHUP 1     A  终端挂起或者控制进程终止
SIGINT 2     A  键盘中断(如break键被按下)
SIGQUIT 3     C  键盘的退出键被按下
SIGILL 4     C  非法指令
SIGABRT 6     C  由abort(3)发出的退出指令
SIGFPE 8     C  浮点异常
SIGKILL 9     AEF  Kill信号
SIGSEGV 11     C  无效的内存引用
SIGPIPE 13     A  管道破裂: 写一个没有读端口的管道
SIGALRM 14    A  由alarm(2)发出的信号
SIGTERM 15    A  终止信号
SIGUSR1 30,10,16 A  用户自定义信号1
SIGUSR2 31,12,17 A  用户自定义信号2
SIGCHLD 20,17,18 B  子进程结束信号
SIGCONT 19,18,25    进程继续(曾被停止的进程)
SIGSTOP 17,19,23 DEF 终止进程
SIGTSTP 18,20,24 D  控制终端(tty)上按下停止键
SIGTTIN 21,21,26 D  后台进程企图从控制终端读
SIGTTOU 22,22,27 D  后台进程企图从控制终端写

  下面的信号没在POSIX.1中列出,而在SUSv2列出

信号    值   处理动作 发出信号的原因
--------------------------------------------------------------------
SIGBUS 10,7,10 C 总线错误(错误的内存访问)
SIGPOLL A Sys V定义的Pollable事件,与SIGIO同义
SIGPROF 27,27,29 A Profiling定时器到
SIGSYS 12,-,12 C 无效的系统调用 (SVID)
SIGTRAP 5 C 跟踪/断点捕获
SIGURG 16,23,21 B Socket出现紧急条件(4.2 BSD)
SIGVTALRM 26,26,28 A 实际时间报警时钟信号(4.2 BSD)
SIGXCPU 24,24,30 C 超出设定的CPU时间限制(4.2 BSD)
SIGXFSZ 25,25,31 C 超出设定的文件大小限制(4.2 BSD)

(对于SIGSYS,SIGXCPU,SIGXFSZ,以及某些机器体系结构下的SIGBUS,Linux缺省的动作是A (terminate),SUSv2 是C (terminate and dump core))。

  下面是其它的一些信号:

信号    值   处理动作 发出信号的原因
----------------------------------------------------------------------
SIGIOT 6 C IO捕获指令,与SIGABRT同义
SIGEMT 7,-,7 SIGSTKFLT -,16,- A 协处理器堆栈错误
SIGIO 23,29,22 A 某I/O操作现在可以进行了(4.2 BSD)
SIGCLD -,-,18 A 与SIGCHLD同义
SIGPWR 29,30,19 A 电源故障(System V)
SIGINFO 29,-,- A 与SIGPWR同义
SIGLOST -,-,- A 文件锁丢失
SIGWINCH 28,28,20 B 窗口大小改变(4.3 BSD, Sun)
SIGUNUSED -,31,- A 未使用的信号(will be SIGSYS)

(在这里,- 表示信号没有实现;有三个值给出的含义为,第一个值通常在Alpha和Sparc上有效,中间的值对应i386和ppc以及sh,最后一个值对应mips。信号29在Alpha上为SIGINFO / SIGPWR ,在Sparc上为SIGLOST。)

  处理动作一项中的字母含义如下:

  • A 缺省的动作是终止进程
  • B 缺省的动作是忽略此信号
  • C 缺省的动作是终止进程并进行内核映像转储(dump core)
  • D 缺省的动作是停止进程
  • E 信号不能被捕获
  • F 信号不能被忽略

  上面介绍的信号是常见系统所支持的。以表格的形式介绍了各种信号的名称、作用及其在默认情况下的处理动作。各种默认处理动作的含义是:终止程序是指进程退出;忽略该信号是将该信号丢弃,不做处理;停止程序是指程序挂起,进入停止状况以后还能重新进行下去,一般是在调试的过程中(例如ptrace系统调用);内核映像转储是指将进程数据在内存的映像和进程在内核结构中存储的部分内容以一定格式转储到文件系统,并且进程退出执行,这样做的好处是为程序员提供了方便,使得他们可以得到进程当时执行时的数据值,允许他们确定转储的原因,并且可以调试他们的程序。

  注意: 信号SIGKILL和SIGSTOP既不能被捕捉,也不能被忽略。信号SIGIOT与SIGABRT是一个信号。可以看出,同一个信号在不同的系统中值可能不一样,所以建议最好使用为信号定义的名字,而不要直接使用信号的值。

2.信 号 机 制

  上一节中介绍了信号的基本概念,在这一节中,我们将介绍内核如何实现信号机制。即内核如何向一个进程发送信号、进程如何接收一个信号、进程怎样控制自己对信号的反应、内核在什么时机处理和怎样处理进程收到的信号。还要介绍一下setjmp和longjmp在信号中起到的作用。

2.1 内核对信号的基本处理方法

  内核给一个进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应于该信号的位。这里要补充的是,如果信号发送给一个正在睡眠的进程,那么要看该进程进入睡眠的优先级,如果进程睡眠在可被中断的优先级上,则唤醒进程;否则仅设置进程表中信号域相应的位,而不唤醒进程。这一点比较重要,因为进程检查是否收到信号的时机是:一个进程在即将从内核态返回到用户态时;或者,在一个进程要进入或离开一个适当的低调度优先级睡眠状态时。

  内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时。所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。

  内核处理一个进程收到的软中断信号是在该进程的上下文中,因此,进程必须处于运行状态。前面介绍概念的时候讲过,处理信号有三种类型:进程接收到信号后退出;进程忽略该信号;进程收到信号后执行用户设定用系统调用signal的函数。当进程接收到一个它忽略的信号时,进程丢弃该信号,就象没有收到该信号似的继续运行。如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。而且执行用户定义的函数的方法很巧妙,内核是在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时,才返回原先进入内核的地方。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核态下运行的话,用户就可以获得任何权限)。

  在信号的处理方法中有几点特别要引起注意。

  1. 在一些系统中,当一个进程处理完中断信号返回用户态之前,内核清除用户区中设定的对该信号的处理例程的地址,即下一次进程对该信号的处理方法又改为默认值,除非在下一次信号到来之前再次使用signal系统调用。这可能会使得进程在调用signal之前又得到该信号而导致退出。在BSD中,内核不再清除该地址。但不清除该地址可能使得进程因为过多过快的得到某个信号而导致堆栈溢出。为了避免出现上述情况。在BSD系统中,内核模拟了对硬件中断的处理方法,即在处理某个中断时,阻止接收新的该类中断。
  2. 如果要捕捉的信号发生于进程正在一个系统调用中时,并且该进程睡眠在可中断的优先级上,这时该信号引起进程作一次longjmp,跳出睡眠状态,返回用户态并执行信号处理例程。当从信号处理例程返回时,进程就象从系统调用返回一样,但返回了一个错误代码,指出该次系统调用曾经被中断。这要注意的是,BSD系统中内核可以自动地重新开始系统调用。
  3. 若进程睡眠在可中断的优先级上,则当它收到一个要忽略的信号时,该进程被唤醒,但不做longjmp,一般是继续睡眠。但用户感觉不到进程曾经被唤醒,而是象没有发生过该信号一样。
  4. 内核对子进程终止(SIGCLD)信号的处理方法与其他信号有所区别。当进程检查出收到了一个子进程终止的信号时,缺省情况下,该进程就象没有收到该信号似的,如果父进程执行了系统调用wait,进程将从系统调用wait中醒来并返回wait调用,执行一系列wait调用的后续操作(找出僵死的子进程,释放子进程的进程表项),然后从wait中返回。SIGCLD信号的作用是唤醒一个睡眠在可被中断优先级上的进程。如果该进程捕捉了这个信号,就象普通信号处理一样转到处理例程。如果进程忽略该信号,那么系统调用wait的动作就有所不同,因为SIGCLD的作用仅仅是唤醒一个睡眠在可被中断优先级上的进程,那么执行wait调用的父进程被唤醒继续执行wait调用的后续操作,然后等待其他的子进程。

  如果一个进程调用signal系统调用,并设置了SIGCLD的处理方法,并且该进程有子进程处于僵死状态,则内核将向该进程发一个SIGCLD信号。

2.2 setjmp和longjmp的作用

  前面在介绍信号处理机制时,多次提到了setjmp和longjmp,但没有仔细说明它们的作用和实现方法。这里就此作一个简单的介绍。

  在介绍信号的时候,我们看到多个地方要求进程在检查收到信号后,从原来的系统调用中直接返回,而不是等到该调用完成。这种进程突然改变其上下文的情况,就是使用setjmp和longjmp的结果。setjmp将保存的上下文存入用户区,并继续在旧的上下文中执行。这就是说,进程执行一个系统调用,当因为资源或其他原因要去睡眠时,内核为进程作了一次setjmp,如果在睡眠中被信号唤醒,进程不能再进入睡眠时,内核为进程调用longjmp,该操作是内核为进程将原先setjmp调用保存在进程用户区的上下文恢复成现在的上下文,这样就使得进程可以恢复等待资源前的状态,而且内核为setjmp返回1,使得进程知道该次系统调用失败。这就是它们的作用。

3.有关信号的系统调用

  前面两节已经介绍了有关信号的大部分知识。这一节我们来了解一下这些系统调用。其中,系统调用signal是进程用来设定某个信号的处理方法,系统调用kill是用来发送信号给指定进程的。这两个调用可以形成信号的基本操作。后两个调用pause和alarm是通过信号实现的进程暂停和定时器,调用alarm是通过信号通知进程定时器到时。所以在这里,我们还要介绍这两个调用。

3.1 signal 系统调用

  系统调用signal用来设定某个信号的处理方法。该调用声明的格式如下:

void (*signal(int signum, void (*handler)(int)))(int);

  在使用该调用的进程中加入以下头文件:

#include

  上述声明格式比较复杂,如果不清楚如何使用,也可以通过下面这种类型定义的格式来使用(POSIX的定义):

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

  但这种格式在不同的系统中有不同的类型定义,所以要使用这种格式,最好还是参考一下联机手册。

  在调用中,参数signum指出要设置处理方法的信号。第二个参数handler是一个处理函数,或者是

  • SIG_IGN:忽略参数signum所指的信号。
  • SIG_DFL:恢复参数signum所指信号的处理方法为默认值。

  传递给信号处理例程的整数参数是信号值,这样可以使得一个信号处理例程处理多个信号。系统调用signal返回值是指定信号signum前一次的处理例程或者错误时返回错误代码SIG_ERR。下面来看一个简单的例子:

#include

#include

#include

void sigroutine(int dunno)

{ /* 信号处理例程,其中dunno将会得到信号的值 */

switch (dunno) {

case 1:

printf(“Get a signal — SIGHUP “);

break;

case 2:

printf(“Get a signal — SIGINT “);

break;

case 3:

printf(“Get a signal — SIGQUIT “);

break;

}

return;

}


int main() {


printf(“process id is %d “,getpid());

signal(SIGHUP, sigroutine); //* 下面设置三个信号的处理方法

signal(SIGINT, sigroutine);

signal(SIGQUIT, sigroutine);


for (;;) ;

}

  其中信号SIGINT由按下Ctrl-C发出,信号SIGQUIT由按下Ctrl-(back slash)发出。该程序执行的结果如下:

localhost:~$ ./sig_test

process id is 463

Get a signal -SIGINT //按下Ctrl-C得到的结果

Get a signal -SIGQUIT //按下Ctrl-得到的结果

//按下Ctrl-z将进程置于后台

[1]+ Stopped ./sig_test

localhost:~$ bg

[1]+ ./sig_test &

localhost:~$ kill -HUP 463 //向进程发送SIGHUP信号

localhost:~$ Get a signal – SIGHUP

kill -9 463 //向进程发送SIGKILL信号,终止进程

localhost:~$

3.2 kill 系统调用

  系统调用kill用来向进程发送一个信号。该调用声明的格式如下:

int kill(pid_t pid, int sig);

  在使用该调用的进程中加入以下头文件:

#include

#include

  该系统调用可以用来向任何进程或进程组发送任何信号。如果参数pid是正数,那么该调用将信号sig发送到进程号为pid的进程。如果pid等于0,那么信号sig将发送给当前进程所属进程组里的所有进程。如果参数pid等于-1,信号sig将发送给除了进程1和自身以外的所有进程。如果参数pid小于-1,信号sig将发送给属于进程组-pid的所有进程。如果参数sig为0,将不发送信号。该调用执行成功时,返回值为0;错误时,返回-1,并设置相应的错误代码errno。下面是一些可能返回的错误代码:

  • EINVAL:指定的信号sig无效。
  • ESRCH:参数pid指定的进程或进程组不存在。注意,在进程表项中存在的进程,可能是一个还没有被wait收回,但已经终止执行的僵死进程。
  • EPERM:进程没有权力将这个信号发送到指定接收信号的进程。因为,一个进程被允许将信号发送到进程pid时,必须拥有root权力,或者是发出调用的进程的UID或EUID与指定接收的进程的UID或保存用户ID(savedset-user-ID)相同。如果参数pid小于-1,即该信号发送给一个组,则该错误表示组中有成员进程不能接收该信号。

3.3 pause系统调用

  系统调用pause的作用是等待一个信号。该调用的声明格式如下:

int pause(void);

  在使用该调用的进程中加入以下头文件:

#include

  该调用使得发出调用的进程进入睡眠,直到接收到一个信号为止。该调用总是返回-1,并设置错误代码为EINTR(接收到一个信号)。下面是一个简单的范例:

#include

#include

#include

void sigroutine(int unused)

{

printf(“Catch a signal SIGINT “);

}


int main() {

signal(SIGINT, sigroutine);

pause();

printf(“receive a signal “);

}

  在这个例子中,程序开始执行,就象进入了死循环一样,这是因为进程正在等待信号,当我们按下Ctrl-C时,信号被捕捉,并且使得pause退出等待状态。

3.4 alarm和 setitimer系统调用

  系统调用alarm的功能是设置一个定时器,当定时器计时到达时,将发出一个信号给进程。该调用的声明格式如下:

unsigned int alarm(unsigned int seconds);

  在使用该调用的进程中加入以下头文件:

#include

  系统调用alarm安排内核为调用进程在指定的seconds秒后发出一个SIGALRM的信号。如果指定的参数seconds为0,则不再发送SIGALRM信号。后一次设定将取消前一次的设定。该调用返回值为上次定时调用到发送之间剩余的时间,或者因为没有前一次定时调用而返回0。注意,在使用时,alarm只设定为发送一次信号,如果要多次发送,就要多次使用alarm调用。

  对于alarm,这里不再举例。现在的系统中很多程序不再使用alarm调用,而是使用setitimer调用来设置定时器,用getitimer来得到定时器的状态,这两个调用的声明格式如下:

int getitimer(int which, struct itimerval *value);

int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);

  在使用这两个调用的进程中加入以下头文件:

#include

  该系统调用给进程提供了三个定时器,它们各自有其独有的计时域,当其中任何一个到达,就发送一个相应的信号给进程,并使得计时器重新开始。三个计时器由参数which指定,如下所示:

  • TIMER_REAL:按实际时间计时,计时到达将给进程发送SIGALRM信号。
  • ITIMER_VIRTUAL:仅当进程执行时才进行计时。计时到达将发送SIGVTALRM信号给进程。
  • ITIMER_PROF:当进程执行时和系统为该进程执行动作时都计时。与ITIMER_VIR-TUAL是一对,该定时器经常用来统计进程在用户态和内核态花费的时间。计时到达将发送SIGPROF信号给进程。

  定时器中的参数value用来指明定时器的时间,其结构如下:

struct itimerval {

struct timeval it_interval; /* 下一次的取值 */

struct timeval it_value; /* 本次的设定值 */

};

  该结构中timeval结构定义如下:

struct timeval {

long tv_sec; /* 秒 */

long tv_usec; /* 微秒,1秒 = 1000000 微秒*/

};

  在setitimer调用中,参数ovalue如果不为空,则其中保留的是上次调用设定的值。定时器将it_value递减到0时,产生一个信号,并将it_value的值设定为it_interval的值,然后重新开始计时,如此往复。当it_value设定为0时,计时器停止,或者当它计时到期,而it_interval为0时停止。调用成功时,返回0;错误时,返回-1,并设置相应的错误代码errno:

  • EFAULT:参数value或ovalue是无效的指针。
  • EINVAL:参数which不是ITIMER_REAL、ITIMER_VIRT或ITIMER_PROF中的一个。

  下面是关于setitimer调用的一个简单示范,在该例子中,每隔一秒发出一个SIGALRM,每隔0.5秒发出一个SIGVTALRM信号:

#include

#include

#include

#include


int sec;

void sigroutine(int signo) {

switch (signo) {

case SIGALRM:

printf(“Catch a signal — SIGALRM “);

break;

case SIGVTALRM:

printf(“Catch a signal — SIGVTALRM “);

break;

}

return;

}


int main() {

struct itimerval value,ovalue,value2;

sec = 5;

printf(“process id is %d “,getpid());

signal(SIGALRM, sigroutine);

signal(SIGVTALRM, sigroutine);

value.it_value.tv_sec = 1;

value.it_value.tv_usec = 0;

value.it_interval.tv_sec = 1;

value.it_interval.tv_usec = 0;

setitimer(ITIMER_REAL, &value, &ovalue);


value2.it_value.tv_sec = 0;

value2.it_value.tv_usec = 500000;

value2.it_interval.tv_sec = 0;

value2.it_interval.tv_usec = 500000;

setitimer(ITIMER_VIRTUAL, &value2, &ovalue);

for (;;) ;

}

  该例子的屏幕拷贝如下:

localhost:~$ ./timer_test

process id is 579

Catch a signal – SIGVTALRM

Catch a signal – SIGALRM

Catch a signal – SIGVTALRM

Catch a signal – SIGVTALRM

Catch a signal – SIGALRM

Catch a signal –GVTALRM

  本文简单介绍了Linux下的信号,如果希望了解其他调用,请参考联机手册或其他文档。

低级的失败……

看了linux系统时间和定时这章….有这么一个宏定义

#define CURRENT_TIME (startup_time + jiffies/HZ)

其中HZ=100

startup_time单位是秒,jiffies/HZ总看不懂,看不懂的原因是不知道秒和毫秒的转换….失败呀…

百度一下…答案如下…

1秒=1000毫秒(ms) 1毫秒=1/1,000秒(s)

1秒=1,000,000 微秒(μs) 1微秒=1/1,000,000秒(s)

1秒=1,000,000,000 纳秒(ns) 1纳秒=1/1,000,000,000秒(s)

1秒=1,000,000,000,000 皮秒(ps) 1皮秒=1/1,000,000,000,000秒(s)

记牢哦….呵呵

一个很短的内核代码….mktime.c

#define MINUTE 60   // 1 分钟的秒数。

#define HOUR (60*MINUTE) // 1 小时的秒数。

#define DAY (24*HOUR)   // 1 天的秒数。

#define YEAR (365*DAY)   // 1 年的秒数。

/* interestingly, we assume leap-years */

/* 有趣的是我们考虑进了闰年 */

// 下面以年为界限,定义了每个月开始时的秒数时间数组。

static int month[12] = {

0,

DAY * (31),

DAY * (31 + 29),

DAY * (31 + 29 + 31),

DAY * (31 + 29 + 31 + 30),

DAY * (31 + 29 + 31 + 30 + 31),

DAY * (31 + 29 + 31 + 30 + 31 + 30),

DAY * (31 + 29 + 31 + 30 + 31 + 30 + 31),

DAY * (31 + 29 + 31 + 30 + 31 + 30 + 31 + 31),

DAY * (31 + 29 + 31 + 30 + 31 + 30 + 31 + 31 + 30),

DAY * (31 + 29 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31),

DAY * (31 + 29 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30)

};

// 该函数计算从1970 年1 月1 日0 时起到开机当日经过的秒数,作为开机时间。

long

kernel_mktime (struct tm *tm)

{

long res;

int year;

year = tm->tm_year – 70; // 从70 年到现在经过的年数(2 位表示方式),

// 因此会有2000 年问题。

/* magic offsets (y+1) needed to get leapyears right. */

/* 为了获得正确的闰年数,这里需要这样一个魔幻偏值(y+1) */

res = YEAR * year + DAY * ((year + 1) / 4); // 这些年经过的秒数时间 + 每个闰年时多1 天

res += month[tm->tm_mon]; // 的秒数时间,在加上当年到当月时的秒数。

/* and (y+2) here. If it wasn’t a leap-year, we have to adjust */

/* 以及(y+2)。如果(y+2)不是闰年,那么我们就必须进行调整(减去一天的秒数时间)。 */

if (tm->tm_mon > 1 && ((year + 2) % 4))

    res -= DAY;

res += DAY * (tm->tm_mday – 1); // 再加上本月过去的天数的秒数时间。

res += HOUR * tm->tm_hour; // 再加上当天过去的小时数的秒数时间。

res += MINUTE * tm->tm_min; // 再加上1 小时内过去的分钟数的秒数时间。

res += tm->tm_sec;   // 再加上1 分钟内已过的秒数。

return res;    // 即等于从1970 年以来经过的秒数时间。

}

块设备简介.

我们将重点浏览Gentoo Linux和普通Linux中有关磁盘的方面的知识,包括Linux文件系统、分区和块设备。然后,一旦你熟悉了磁盘和文件系统的方方面面,我们将指导你设置好分区和文件系统,为你安装Gentoo Linux做好准备。

       一开始我们先介绍块设备。最有名的块设备可能就是Linux系统中表示第一个IDE驱动器的/dev/hda。如果你的系统使用SCSI或SATA驱动器,则你的第一个硬盘驱动器会是/dev/sda

       上面介绍的块设备代表磁盘的抽象接口。用户程序可以使用这些块设备来与你的磁盘进行交互,而不用理会你的驱动器到底是IDE、SCSI还是其他的。程序可以把磁盘当作一系列连续的、可随机访问的512字节大小的块来访问。

gcc对C语言的扩展:语句内嵌表达式(statement-embedded expression)

在gnu c 中,用括号将复合语句括起来也形成了表达式。他允许你在一个表达式内使用循环,跳转和局部变量。


一个复合语句是用大括号{}括起来的一组语句。在包含语句的表达式这种结构中,再用括号( )将大括号括起来,例如:

({ int y = foo (); int z;

if (y > 0) z = y;

else z = – y;

z; })


就是一个合法表达式,用于计算foo( )函数返回值的绝对值。

在上面的复合语句中,最后的一句必须是一个以分号结尾的表达式。这个表达式代表了整个结构的值。如果你在大括号里的最后一句用的是其他的语句,则整个结构的返回类型为void,即没有合法的返回值。


这种特性使得宏定义变得更加安全(因为每个操作数都只被计算一次,例如++运算)。例如计算最大值通常在c语言中被定义为这样的宏:

#define max(a,b) ((a) > (b) ? (a) : (b))


但是其中的a和b可能会被计算两次,如果操作数带有副作用,则会产生错误的结果。在gnu c中,如果你知道了操作数的类型(假设为int),你可以这样安全的定义宏:

#define maxint(a,b)

({int _a = (a), _b = (b); _a > _b ? _a : _b; })

语句内嵌在常量表达式(例如枚举类型),位域尺寸或静态变量初始化中是不允许的。如果你不知道操作数的类型,你也可以使用typeof来获得类型。

语句表达式内嵌在g++中并不支持,而且将来是否支持目前也不清楚(他们在某时被完全支持或者被抛弃掉,或者作为bug会一直存在)。就目前而言,语句内嵌表达式在默认情况下工作的并不好。


此外,在c++中语句内嵌表达式还存在很多语义问题。如果你希望在c++中用语句内嵌表达式来代替内联函数(inline function),对象的析构处理可能会让你惊讶。例如:

#define foo(a) ({int b = (a); b + 3; })

并不等同于

inline int foo(int a) { int b = a; return b + 3; }

具体而言,当传递给foo的表达式的会引入临时对象的生成的时候,这些临时对象的析构在用宏时会早于用函数的情况。


以上情况说明在用于c++代码的.h头文件中使用语句内联表达式并不是一个好主意。一些gnu c的库的某些版本中的使用语句内联表达式的头文件已经造成了这样的bug。

小议volatile和const类型变量的区别

刚看到前几行..就蹦出来个这个变量….以前见过..就是没用心学过..百度一下….轻松搞定

Const

关键字const有什么含意?

我只要一听到被面试者说:“const意味着常数”,我就知道我正在和一个业余者打交道。去年Dan Saks已经在他的文章里完全概括了const的所有用法,因此ESP(译者:Embedded Systems Programming)的每一位读者应该非常熟悉const能做什么和不能做什么.如果你从没有读到那篇文章,只要能说出const意味着“只读”就可以了。尽管这个答案不是完全的答案,但我接受它作为一个正确的答案。(如果你想知道更详细的答案,仔细读一下Saks的文章吧。)

如果应试者能正确回答这个问题,我将问他一个附加的问题:

下面的声明都是什么意思?

const int a;

int const a;

const int *a;

int * const a;

int const * a const;

/******/

前两个的作用是一样,a是一个常整型数。第三个意味着a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。第四个意思a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。如果应试者能正确回答这些问题,那么他就给我留下了一个好印象。顺带提一句,也许你可能会问,即使不用关键字 const,也还是能很容易写出功能正确的程序,那么我为什么还要如此看重关键字const呢?我也如下的几下理由:

•; 关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const的程序员很少会留下的垃圾让别人来清理的。)

•; 通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。

•; 合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。


Volatile

关键字volatile有什么含意?并给出三个不同的例子。

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:

•; 并行设备的硬件寄存器(如:状态寄存器)

•; 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)

•; 多线程应用中被几个任务共享的变量

回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道,所有这些都要求用到volatile变量。不懂得volatile的内容将会带来灾难。

假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性。

•; 一个参数既可以是const还可以是volatile吗?解释为什么。

•; 一个指针可以是volatile 吗?解释为什么。

•; 下面的函数有什么错误:

int square(volatile int *ptr)

{

return *ptr * *ptr;

}

下面是答案:

•; 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。

•; 是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。

•; 这段代码有点变态。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:



int square(volatile int *ptr)

{

int a,b;

a = *ptr;

b = *ptr;

return a * b;

}



由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:

long square(volatile int *ptr)

{

int a;

a = *ptr;

return a * a;

}

gcc内嵌汇编….

GCC内嵌汇编之语法详解


内嵌汇编语法如下:

__asm__(汇编语句模板: 输出部分: 输入部分: 破坏描述部分)

共四个部分:汇编语句模板,输出部分,输入部分,破坏描述部分,各部分使用“:”格开,汇编语句模板必不可少,其他三部分可选,如果使用了后面的部分,而前面部分为空,也需要用“:”格开,相应部分内容为空。例如:

__asm__ __volatile__(“cli”: : :”memory”)


1、汇编语句模板

汇编语句模板由汇编语句序列组成,语句之间使用“;”、“n”或“nt”分开。指令中的操作数可以使用占位符引用C语言变量,操作数占位符最多10个,名称如下:%0,%1,…,%9。指令中使用占位符表示的操作数,总被视为long型(4个字节),但对其施加的操作根据指令可以是字或者字节,当把操作数当作字或者字节使用时,默认为低字或者低字节。对字节操作可以显式的指明是低字节还是次字节。方法是在%和序号之间插入一个字母,“b”代表低字节,“h”代表高字节,例如:%h1。


2、输出部分

输出部分描述输出操作数,不同的操作数描述符之间用逗号格开,每个操作数描述符由限定字符串和C 语言变量组成。每个输出操作数的限定字符串必须包含“=”表示他是一个输出操作数。

例:

__asm__ __volatile__(“pushfl ; popl %0 ; cli”:”=g” (x) )

描述符字符串表示对该变量的限制条件,这样GCC 就可以根据这些条件决定如何分配寄存器,如何产生必要的代码处理指令操作数与C表达式或C变量之间的联系。


3、输入部分

输入部分描述输入操作数,不同的操作数描述符之间使用逗号格开,每个操作数描述符由限定字符串和C语言表达式或者C语言变量组成。

例1 :

__asm__ __volatile__ (“lidt %0″ : : “m” (real_mode_idt));

例二(bitops.h):


Static __inline__ void __set_bit(int nr, volatile void * addr)

{

__asm__(

“btsl %1,%0″

:”=m” (ADDR)

:”Ir” (nr));

}


后例功能是将(*addr)的第nr位设为1。第一个占位符%0与C 语言变量ADDR对应,第二个占位符%1与C语言变量nr对应。因此上面的汇编语句代码与下面的伪代码等价:btsl nr, ADDR,该指令的两个操作数不能全是内存变量,因此将nr的限定字符串指定为“Ir”,将nr 与立即数或者寄存器相关联,这样两个操作数中只有ADDR为内存变量。


4、限制字符

4.1、限制字符列表

限制字符有很多种,有些是与特定体系结构相关,此处仅列出常用的限定字符和i386中可能用到的一些常用的限定符。它们的作用是指示编译器如何处理其后的C语言变量与指令操作数之间的关系。


分类 限定符 描述

通用寄存器 “a” 将输入变量放入eax

这里有一个问题:假设eax已经被使用,那怎么办?

其实很简单:因为GCC 知道eax 已经被使用,它在这段汇编代码

的起始处插入一条语句pushl %eax,将eax 内容保存到堆栈,然

后在这段代码结束处再增加一条语句popl %eax,恢复eax的内容

“b” 将输入变量放入ebx

“c” 将输入变量放入ecx

“d” 将输入变量放入edx

“s” 将输入变量放入esi

“d” 将输入变量放入edi

“q” 将输入变量放入eax,ebx,ecx,edx中的一个

“r” 将输入变量放入通用寄存器,也就是eax,ebx,ecx,

edx,esi,edi中的一个

“A” 把eax和edx合成一个64 位的寄存器(use long longs)


内存 “m” 内存变量

“o” 操作数为内存变量,但是其寻址方式是偏移量类型,

也即是基址寻址,或者是基址加变址寻址

“V” 操作数为内存变量,但寻址方式不是偏移量类型

“ ” 操作数为内存变量,但寻址方式为自动增量

“p” 操作数是一个合法的内存地址(指针)


寄存器或内存 “g” 将输入变量放入eax,ebx,ecx,edx中的一个

或者作为内存变量

“X” 操作数可以是任何类型


立即数

“I” 0-31之间的立即数(用于32位移位指令)

“J” 0-63之间的立即数(用于64位移位指令)

“N” 0-255之间的立即数(用于out指令)

“i” 立即数

“n” 立即数,有些系统不支持除字以外的立即数,

这些系统应该使用“n”而不是“i”

匹配 “ 0 ”, 表示用它限制的操作数与某个指定的操作数匹配,

“1” … 也即该操作数就是指定的那个操作数,例如“0”

“9” 去描述“%1”操作数,那么“%1”引用的其实就

是“%0”操作数,注意作为限定符字母的0-9 与

指令中的“%0”-“%9”的区别,前者描述操作数,

后者代表操作数。

& 该输出操作数不能使用过和输入操作数相同的寄存器


操作数类型 “=” 操作数在指令中是只写的(输出操作数)

“+” 操作数在指令中是读写类型的(输入输出操作数)

浮点数 “f” 浮点寄存器

“t” 第一个浮点寄存器

“u” 第二个浮点寄存器

“G” 标准的80387浮点常数

% 该操作数可以和下一个操作数交换位置

例如addl的两个操作数可以交换顺序

(当然两个操作数都不能是立即数)

# 部分注释,从该字符到其后的逗号之间所有字母被忽略

* 表示如果选用寄存器,则其后的字母被忽略

5、破坏描述部分

破坏描述符用于通知编译器我们使用了哪些寄存器或内存,由逗号格开的字符串组成,每个字符串描述一种情况,一般是寄存器名;除寄存器外还有“memory”。例如:“%eax”,“%ebx”,“memory”等。

memory”比较特殊,可能是内嵌汇编中最难懂部分。为解释清楚它,先介绍一下编译器的优化知识,再看C关键字volatile。最后去看该描述符。


6、编译器优化介绍

内存访问速度远不及CPU处理速度,为提高机器整体性能,在硬件上引入硬件高速缓存Cache,加速对内存的访问。另外在现代CPU中指令的执行并不一定严格按照顺序执行,没有相关性的指令可以乱序执行,以充分利用CPU的指令流水线,提高执行速度。以上是硬件级别的优化。再看软件一级的优化:一种是在编写代码时由程序员优化,另一种是由编译器进行优化。编译器优化常用的方法有:将内存变量缓存到寄存器;调整指令顺序充分利用CPU指令流水线,常见的是重新排序读写指令。对常规内存进行优化的时候,这些优化是透明的,而且效率很好。由编译器优化或者硬件重新排序引起的问题的解决办法是在从硬件(或者其他处理器)的角度看必须以特定顺序执行的操作之间设置内存屏障(memory barrier),linux 提供了一个宏解决编译器的执行顺序问题。

void Barrier(void)

这个函数通知编译器插入一个内存屏障,但对硬件无效,编译后的代码会把当前CPU寄存器中的所有修改过的数值存入内存,需要这些数据的时候再重新从内存中读出。


7、C语言关键字volatile

C语言关键字volatile(注意它是用来修饰变量而不是上面介绍的__volatile__)表明某个变量的值可能在外部被改变,因此对这些变量的存取不能缓存到寄存器,每次使用时需要重新存取。该关键字在多线程环境下经常使用,因为在编写多线程的程序时,同一个变量可能被多个线程修改,而程序通过该变量同步各个线程,例如:

DWORD __stdcall threadFunc(LPVOID signal)

{

int* intSignal=reinterpret_cast<int*>(signal);

*intSignal=2;

while(*intSignal!=1)

sleep(1000);

return 0;

}

该线程启动时将intSignal 置为2,然后循环等待