Chinaunix首页 | 论坛 | 博客
  • 博客访问: 807531
  • 博文数量: 142
  • 博客积分: 3505
  • 博客等级: 中校
  • 技术积分: 1501
  • 用 户 组: 普通用户
  • 注册时间: 2011-07-30 19:30
文章分类

全部博文(142)

文章存档

2012年(33)

2011年(109)

分类: LINUX

2011-08-13 21:19:50

[Luo Yuan]第30篇博客,本想作一篇原创,但是在某论坛上偶遇此文,解释了很多我自己没有理解的环节,像是打通任督二脉。特此转载,以飨Linux Kernel爱好者。
==================================================================================
一、看《完全注释》第二章的几个困惑:

1.书第10页,2.3 Linux系统定时不太好理解,看了两遍还是有点晕。其中说如果被中断的当前进程工作在内核态,即在内核程序中运行时被中断,则do_timer()会立刻退出
这里退出后是接着在内核态执行吗?那为什么还要对内核运行时间进行统计呢?
2.
还是系统定时这一节的如果程序添加过定时器,这里的定时器,指的就是一个函数调用吗?定时器链表是什么意思?
3.
看了这么久还是不太明白进程运行在用户态和内核态的区别,能通俗地讲一下,什么情况是运行在用户态,什么情况是运行在内核态的吗?
 我原来的理解是,执行程序自身代码是运行在用户态,调用系统调用时是运行在内核态
 但书第13页,即2.4.3进程初始化那节中有一句话:它(move_to_user_mode)把main.c程序执行流从内核态(特权级0)移动到了用户态(特权级3)的任务0中继续执行”-----这句话我觉得很迷惑,main.c程序本身就是操作系统的内部代码,怎么还存在内核态和用户态的问题?要不就是我原来对用户态和内核态的理解是不正确的。
4.
书第15页,即2.4.5进程调度这一节中,有一句话如果此时没有其他进程可运行,系统就会选择进程0运行...只要系统空闲就调度进程0运行,这里进程0被调度运行的作用是什么?

答:挨着理解的顺序来吧
3
、什么情况是运行在用户态,什么情况是运行在内核态?
我原来的理解是,执行程序自身代码是运行在用户态,调用系统调用时是运行在内核态
但书第13页,即2.4.3进程初始化那节中有一句话:它(move_to_user_mode)把main.c程序执行流从内核态(特权级0)移动到了用户态(特权级3)的任务0中继续执行”-----这句话我觉得很迷惑,main.c程序本身就是操作系统的内部代码,怎么还存在内核态和用户态的问题?

其实内核态和用户态的没有本质的区别,只是规定了一种访问权限罢了。最简单的回答是cs, ss这两个段寄存器里的选择子的 RPL,就是最低两位就代表了 CPU 当前所运行的特权级,也被称做 CPL。比如你会在 head.S 看到 0x10,或者在 move_to_user_mode 这个宏里看到 0x17 这些数。这就是段选择子。如果 cs 中是0x10的话,0x10 写成二进制数就是 00010000,注意看最低两位是00,那么,当前CPU的特权级就是0,就叫处于核心态。而如果 cs 中是 0x17(二进制为00010111,最后两位是11),当前特权级就是 3(二进制的11),这就叫处于用户态。
执行程序自身代码是运行在用户态,调用系统调用时是运行在内核态这句话没错,但是要搞清楚哪些才是系统调用。你在 Linux 上写程序,使用的系统调用最终会变成 int 0x80 的中断调用。然后通过中断服务程序进入内核空间,包括使用内核代码,访问内核堆栈。Linux 是这样做的,规定中断服务程序运行在内核态,因为内核态对各种资源有充分的使用权限,而其他的程序都运行在用户态,这是为了保护计算机资源不被随便占用。所以写在内核里的程序也不一定是运行在内核态的,除了中断服务程序,其他程序都是运行在用户态的。
开机以后一直运行到main()函数,然后进行各种初始化,这都是运行在内核态里的,因为进入保护模式之后,默认就是在内核态。等到初始化完了,不再需要调度各种资源了,再在内核态里呆着就不合适了,于是使用 iret(在move_to_user_mode这个宏里)这个常用的技巧转到了用户态去了。所以在那之后的程序就都是运行在用户态里的了。直到遇到中断(比如时钟中断)CPU才会重新回到内核态,执行中断服务程序。

1
、其中说如果被中断的当前进程工作在内核态,即在内核程序中运行时被中断,则do_timer()会立刻退出,这里退出后是接着在内核态执行吗?那为什么还要对内核运行时间进行统计呢?

有了以上的理解,再来看这个问题。首先你要搞清楚中断机制。无论程序运行在什么特权级,中断都是会发生的。而且一旦发生中断就要求CPU马上响应进入中断服务程序。而由上面分析可知中断服务程序肯定都是运行在特权级0ring0)上的。于是进入中断不外乎两种情况:中断之前,CPU运行在核心态,另外就是运行在用户态。如果是运行在核心态,中断前后都是在RING0上运行,不会发生特权级的变化。如果是运行在用户态,则会有RING3RING0的变化。do_timer这个函数的参数就是CPL,也就是中断之前CPU的特权级。设想这样一种状况,CPU正在执行另一个必须响应的硬件中断服务程序,这时,时钟中断发生,CPU进入时钟中断服务程序(timer_interrupt),如果在 do_timer里进行了进程调度,让CPU跑去干别的活去了,那么那个硬件中断就无法被及时响应了。这是不允许的,于是 Linux 就采用这样的规则:如果是从RING0进入时钟中断的,那么do_timer就会在 schedule()之前退出(就是上面说的立即退出)继续执行原来的程序,退出之后仍然运行在核心态。如果是从Ring3进入时钟中断的,那就要检查一下当前条件是不是应该进行进程调度。如果满足,则进行进程调度,回到Ring3
至于时间统计,我想没什么好解释的,如果时钟中断之前程序运行在内核态,那就把内核时间加1,表示一个滴答,如果之前是运行在用户态,那就把用户态的时间加1。记住,时钟中断程序执行时间很短,所以这种统计的误差还是可以容忍的。

2.
还是系统定时这一节的
 如果程序添加过定时器,这里的定时器,指的就是一个函数调用吗?
 定时器链表是什么意思?

这个东西,光解释是没有用的,你要自己去看代码,定时器是一个函数这种说法不准确,但是可以这么理解着先。定时器链表你去看代码,很清楚的。

4
、有一句话如果此时没有其他进程可运行,系统就会选择进程0运行...只要系统空闲就调度进程0运行
 这里进程0被调度运行的作用是什么?


进程0只有 for(;;) pause();这句话而已,pause()是个系统调用,你自己去看 pause() 的代码好了。作用恐怕也就只有告诉CPU说现在很闲,没事可干,你不用瞎跑。我也拿不准,像这样边角的东西,等到大框架搭起来之后再来处理好了。现在还是赶紧弄清楚进程,中断等等问题是最重要的。

还有,不要和理论纠缠太多,多读代码,一边读一边回头看理论。光扑在第二章的理论上,收获不大。

二、对段选择符和段描述符的几点疑问

1.GDT中为什么要设置一个"空描述符"?有什么用呢?
2.
既然LDT的段选择符等信息都放在LDTR中了,为什么还要在GDT中再存一遍呢
?
3.
看书上说段寄存器可分为可见部分和隐藏部分,可见部分存放的是段选择符,隐藏部分存放的是段基地址,限长和属性信息,可是段寄存器只有16,段选择符就占了14位了,那个只剩两位作隐藏部分,如何能存放这么多信息呢?还是说我的理解有误
?
4.
我们知道每个段描述符是8字节,64,对于这64位的格式,其中把基地址分成三部分,把段限长分成两部分,处理器会把它们组合起来.那为什么要分成这么多个部分呢,还不如直接连起来,省得处理器重新组合一遍了...

答:1.GDT中为什么要设置一个"空描述符"?有什么用呢?
这应该是 Intel 的规定吧,有什么用就不知道了。


2.
既然LDT的段选择符等信息都放在LDTR中了,为什么还要在GDT中再存一遍呢?
因为每个进程都会有自己的局部描述符表,在 task_struct 这个结构中可以看到。为了方便管理,就把每个进程的局部描述符表的地址存入了GDT表。因为GDT只有一个,这样,某个进程的LDT 就可以在 GDT 中使用它的进程号计算得到。当然,如果你不怕麻烦的话,把 LDT  TSS的基址存到 task_struct 结构也未尝不可。只是像 lldt 这样的很多宏定义可就不是那么简洁易懂了。


3.
看书上说段寄存器可分为可见部分和隐藏部分,可见部分存放的是段选择符,隐藏部分存放的是段基地址,限长和属性信息,可是段寄存器只有16,段选择符就占了14位了,那个只剩两位作隐藏部分,如何能存放这么多信息呢?还是说我的理解有误?
16
位全是可见部分。隐藏部分其实是从GDT中自动加载过来的。所以隐藏部分应该是XX位(我记不清楚了,应该是64位,与GDT中描述符相对应的)。段选择符无论从什么意义上讲都不可能是14 位。段选择子有三部分信息:选择子在GDT(或者LDT)中的第几项,RPL  TI。如果抛去 RPL TI 不算的话,那也是 13 位。至于 RPL  TI 是什么意思,我想就不用我多嘴了。


4.
我们知道每个段描述符是8字节,64,对于这64位的格式,其中把基地址分成三部分,把段限长分成两部分,处理器会把它们组合起来.那为什么要分成这么多个部分呢,还不如直接连起来,省得处理器重新组合一遍了...
这是为了和以前的芯片兼容。这种事情很常见,a20 地址线,奇怪的键盘映射,都是为了兼容以前的硬件。所以写程序的时候,这些东西都很烦。尤其是不连续的地址,要对着图好好算一下才能保证正确。

 

补充:GDT相当于是一个总管家,它记录着所有进程的LDT, 但是 LDTR 中只记录着当前进程的LDT. 如果发生了进程调度,那么 LDTR 就要问 GDT 要下一个被调度的 LDT.

     LDTR 确实是为了提高访问局部段的速度。

 

三、1.系统调用的返回值问题

为什么系统调用的返回值在EAX中,是不是C函数的返回值一般会放在EAX中?

2.时钟中断问题

在时钟中断发生后,时钟中断处理程序会关中断,若此时外部硬件有中断,那么在时钟中断处理程序返回后,cpu会发现刚才的中断吗?

3.有关两个函数问题

在书本的P318129-130行的两函数中,(nr<<1+FIRST_TSS_ENTRY 和 (nr<<1+FIRST_LDT_ENTRY 我觉得应该是索引号啊,他们乘8才是地址啊
在书本P316中,67行与68行之间的倒数第2行注释中,他说94行调用sys_call_tablesys_fork函数时压入的返回地址。有吗?难道是压入的是P26795行的EAX,但这时sys_fork()结束后才压的。

答:1.系统调用处理函数也是函数啊,放到eax里面方便在C中读取。

2.
外部硬件中断此时会暂存在8259A的寄存器里面,如果多个相同的外部硬件中断,就会丢失。

3.
注意gdt的数据类型,
  4 typedef struct desc_struct {
  5         unsigned long a,b;
  6 } desc_table[256];
  7 
  8 extern unsigned long pg_dir[1024];
  9 extern desc_table idt,gdt;
也就是说,一个gdt是两个unsigned long的,所以在加指针的时候,真正加的是(nr<<1*两个unsigned long的大小

4.
压入的应该是sys_fork函数的返回值

阅读(1183) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~