Chinaunix首页 | 论坛 | 博客
  • 博客访问: 5382101
  • 博文数量: 671
  • 博客积分: 10010
  • 博客等级: 上将
  • 技术积分: 7310
  • 用 户 组: 普通用户
  • 注册时间: 2006-07-14 09:56
文章分类

全部博文(671)

文章存档

2011年(1)

2010年(2)

2009年(24)

2008年(271)

2007年(319)

2006年(54)

我的朋友

分类:

2007-12-17 15:15:44

囫囵C语言(二):陷阱,中断和异常
  
  上一章怀疑笔者老糊涂的读者,看到这个标题,基本上已经打消了疑虑:老家伙确实糊涂了。
 
这三个概念和C语言有什么关系呢?
  
  中断这个词恐怕人民群众都不陌生。很多人把中断分为两种:硬件中断和软件中断。其实怎么叫关系都不大,关键是我们要明白他们之间的异同点。
  
  笔者本身比较喜欢把 “中断”,分为三种即陷阱,中断和异常,我似乎记得Intel是这么划分的(这句话我不保证正确啊,有兴趣的读者自己看一下 Intel 的手册)。他们的英文分别是 trap,interrupt 和 exception。
  
  陷阱 (trap):
  大家都知道,现代的CPU都是有优先级概念的,用户程序运行在低优先级,操作系统运行在高优先级。高优先级的一些指令低优先级无法执行。有一些操作只能由操作系统来执行,用户想要执行这些操作的时候就要通知操作系统,让操作系统来执行。用户态的程序就是用这种方法来通知操作系统的。
  
  具体怎样做的呢?操作系统会把这些功能编号,比如向一个端口写一个字符的功能调用编号 12,有两个参数,端口号 port 和写入的字符 bytevalue。我们可以如下实现:(这个例子无法编译,但是这种汇编和 C 混合编程的风格微软的编译器支持,十分好用,顺便夸一句微软,他们的编译器是我用过得最优秀的商业编译器)
  
  int outb(int port, int bytevalue)
  {
   __asm mov r0, 12; /* 功能号 */
   __asm mov r1, port; /* 参数 port */
   __asm mov r2, bytevalue; /* 参数 bytevalue */
   __asm trap /* 陷入内核 */
  
   return r0; /* 返回值 */
  }
  
  在操作系统的 trap 处理的 handler 里面,相信大家已经知道怎么办了。有些敏感的读者可能已经明白了,原来一部分 C 的库函数是用这种方法实现的。
  
  中断:
  中断我们这里专指来自于硬件的中断,通常分为电平触发和边沿触发(请参考数字电路)。简单的说就是 CPU 每执行完一条都去检测一条管腿的电平是否变化。如果满足条件,CPU 转向事先注册好的函数。系统中最重要的一个中断就是我们经常说的时钟中断。为什么要说这个呢?这和 C 程序有什么关系呢?书上说了中断是由操作系统处理的,操作系统会保存程序的现场啊,用户程序根本感觉不到中断的存在啊。书上说得没错,但是它有两件事情没有告诉你:
  1. 线程调度策略。
  2. 程序的现场不包括什么?
  
  这里笔者想插一句话表达对国内操作系统教材作者的敬仰,他们是怎么把操作系统拆成一块一块儿的呢?因为,进程管理,线程调度,内存管理,中断管理,IPC,都是互相关联的。笔者十分怀疑分块讨论的意义到底有多大。
  
  言归正传,先回答第一个问题,线程调度时机。在哪些情况下操作系统会运行 scheduler 呢?现代操作系统调度的基本单位都是线程,所以我们不讨论进程的概念。
  
  1. 一些系统调用
  2. I/O 操作
  3. 一个线程创建
  4. 一个线程结束
  5. mutex lock
  6. P semaphore
  7. 硬件中断 / 时钟中断
  8. 主动放弃 CPU,比如 sleep(), yield()
  9. 给另外一个线程发消息,信号
  10. 主动唤醒另外一个线程
  11. 进程结束
  :
  :
  欢迎大家来电来函补充 (我记不住那么多了)
  
  第二个问题,现场不包括什么。至少不包括全局变量。
  
  于是就有了一个经典的面试题:
  
  
  int a;
  
  void thread_1()
  {
   for (;;)
   {
   do something;
   a++;
   }
  }
  
  void thread_2()
  {
   for (;;)
   {
   do something;
   a--;
   }
  }
  
  main()
  {
   create_thread(thread_1);
   create_thread(thread_2);
  }
  
  现在大家应该明白这种写法的错误了吧。因为 a++,a--,并不是一条汇编语言,它会被中断打断,而中断又会引起线程调度。有可能将另外一个线程投入运行。所以结果是无法预测的。讨论这个问题的文章很多,笔者也就不多费口舌了。
  
  提个思考题,操作系统内部,中断和中断之间,中断和线程之间,怎么保护临界资源的呢?多个 CPU 之间呢?
  
  异常:exception
  异常是指一条指令会引起 CPU 的不快,比如除零。有群众说了,如果我除零错了,操作系统把我终止了不就完了,我回去改程序,改对了重新运行不就行了么。
  
  但是有时候 CPU 希望操作系统能够排除这个异常,然后 CPU 重新尝试去执行这条引起异常的指令。这有什么用呢?下面我给大家介绍一个十分重要的异常,缺页异常。
  
  大家都知道,现代的 CPU 都支持虚拟内存管理,我们还是在我们的虚拟 CPU 上讨论这个问题,上面我们说过了,我们的 CPU 使用 2 级页表映射,叶面大小 4K。我实在懒得写如何映射了,请大家参考 Intel 的手册。因为我们的重点不在这里。看下面的语句:
  
   char *p = (char *)malloc(100 * 1024 * 1024);
  
  有人说,没什么不同啊,只不过申请的内存稍微有点儿多啊。但操作系统真地给你那么多内存了么?如果这样的程序来上几个,系统内存岂不是早被耗光,但实际上并没有。所以操作系统采用了在我国盛行的一种机制:打白条!其实我们申请内存的时候操作系统仅仅在虚空间中分配了内存,也就是说仅仅是标记着,这100M的内存归你用,但是我先不给你,当你真的用的时候我再给你分配,这个分配指的就是实实在在的物理页面了。具体怎么实现的呢?我们看下面的语句发生了什么?
  
   p[0x4538] = 'A';
  
  有人疑问了,普通的赋值语句啊。没错,但是这条赋值语句执行了两次(这可不一定啊,我没说绝对,我只是在介绍一种机制),第一次没成功,因为发生了缺页异常,我们刚才说了操作系统仅仅是把这 100M 内存分配给用户了,但是没有对应真正的物理页面。操作系统并没有为 p+0x4538 所在的页面建立页表映射。所以缺页异常发生了。然后操作系统一看这个地址是已经分配给你了,我给你找个物理页面,给你建立好映射,你再执行一次试试。就这一点来说,操作系统比我们的某些官老爷信誉要良好的多,白条兑现了。
  
  于是第二次执行成功了。有人看到这里已经满头雾水了,这个老家伙到底想说什么?
  
  注意到了么,操作系统要给他临时找一个页面,找不到怎么办?对,页面交换,找个倒霉蛋,把它的一部分页面写到硬盘上,实际上操作系统只要空闲物理页面少于一定的程度就会做 swap。那么,如果你有个程序需要较高的效率,较好的反应速度,算法写得再好也没用,一个页面被交换出去全完。
  
  现在明白了吧,优化程序,了解操作系统的运行机制是必不可少的。当然了优化程序绝不仅仅是这些。所以一个优秀的程序员十分有必要知道,你的程序到底运行在 “什么” 上面。
  
  稍微总结一下:
  陷阱:由 trap 指令引起,恢复后 CPU 执行下一条指令
  中断:由硬件电平引起,恢复后 CPU 执行下一条指令
  异常:由软件指令引起,恢复后 CPU 重新执行该条指令
  
  有个牛人说过,Oracle 的数据库为什么总比别人的快一点点呢?因为那批人是写操作系统的。
阅读(2415) | 评论(1) | 转发(0) |
给主人留下些什么吧!~~

chinaunix网友2009-03-15 08:26:57

请不要勿导大家, 谬论太多, 去看看Intel书IA-32, Software Developer's Manual System Programming Guide, 上边写的清楚. 异常包括三种: Fault, Trap(也就是陷阱)和Abort. 具体的区别和用法自己去查好了.