第一章概述
1、什么是POSIX标准,为什么现代造作系统的设计必须遵循POSIX标准?
答:POSIX表示可移植操作系统接口(Portable Operating System Interface)。
POSIX是在Unix标准化过程中出现的产物。
POSIX 1003.1标准定义了一个最小的Unix操作系统接口。
任何操作系统只有符合这一标准,才有可能运行Unix程序。
2、什么是GNU?Linux与GNU有什么关系?
答:GNU是GNU Is Not Unix的递归缩写,是自由软件基金会的一个项目。
GNU项目产品包括emacs编辑器、著名的GNU C和Gcc编译器等,这些软件叫做GNU 软件。
GNU软件和派生工作均适用GNU通用公共许可证,即GPL(General Public License)。
Linux的开发使用了众多的GUN工具。
3、Linux系统由哪些部分组成?Linux内核处于什么位置?
答:(1)符合POSIX标准的操作系统内核、Shell和外围工具。(2)C语言编译器和其他开发工具及函数库。(3)X Window窗口系统。(4)各种应用软件,包括字处理软件、图象处理软件等。
Linux内核是所有Linux 发布版本的核心。
第二章内存寻址
1、在80x86的寄存器中,哪些寄存器供一般用户使用?哪些寄存器只能被操作系统使用?答:通用寄存器EAX,EBX,ECX,EDX,EBP(基址指针),ESP(堆栈指针),ESI(扩展源指针),EDI(扩展目的指针),还有EFLAGS(标志寄存器).
专供操作系统使用的寄存器:IDTR中断描述符寄存器,GDTR全局描述符表寄存器,LDTR 局部描述符表寄存器,TaskRegisters任务寄存器,DebugRegister调试寄存器,ControlRegister 控制寄存器,Model-SpecificRegisters模型专用寄存器。
3、请用C语言描述段描述符表。
答:#include
#include
#define BASE_H 0xff00
#define BASE_M 0x00ff
#define BASE_L 0xffff
#define LIMIT_H 0x000f
#define LIMIT_L 0xffff
#define GRANULARITY 0x0080
#define SEGMENT_P 0x8000
#define SYSTEM_TYPE 0x1000
#define DPL 0x6000
#define TYPE_E 0x0800
#define TYPE_ED_C 0x0400
#define TYPE_RW 0x0200
#define TYPE_A 0x0100
struct {
unsigned char base_high;
unsigned char g : 1;
unsigned char other : 3;
unsigned char limit_high : 4;
unsigned char P : 1;
unsigned char dpl : 2;
unsigned char S : 1;
unsigned char E : 1;
unsigned char ED_C : 1;
unsigned char RW : 1;
unsigned char A : 1;
unsigned char base_middle;
unsigned short base_low;
unsigned short limit_low;
} gdt_t;
int main(int argc, char *argv[])
{
// int gdt[4] = {0x0fff, 0x0000, 0x9a00, 0x00c0} ; // 0x08
// int gdt[4] = {0x7fff, 0x0000, 0x9a01, 0x00c0} ; // 0x10
// int gdt[4] = {0x0002, 0x8000, 0x920b, 0x00c0} ; // display memory
// int gdt[4] = {0xffff, 0xffff, 0xffff, 0xffff} ; // test
int gdt[4] = {0x03ff, 0x0000, 0xf201, 0x00c0} ; // ldt0 unsigned int base, limit;
unsigned int granularity;
unsigned int present, dpl, system_type, segment_type; unsigned int type_e, type_ed_c, type_rw, type_a;
base = (gdt[3] & BASE_H) << 16;
base += (gdt[2] & BASE_M) << 16;
base += gdt[1] & BASE_L << 0;
limit = (gdt[0] & LIMIT_L) << 0;
limit += (gdt[3] & LIMIT_H) << 16;
granularity = (gdt[3] & GRANULARITY) >> 7;
present = (gdt[2] & SEGMENT_P) >> 15;
dpl = (gdt[2] & DPL) >> 13;
system_type = (gdt[2] & SYSTEM_TYPE) >> 12;
type_e = (gdt[2] & TYPE_E) >> 11;
type_ed_c = (gdt[2] & TYPE_ED_C) >> 10;
type_rw = (gdt[2] & TYPE_RW) >> 9;
type_a = (gdt[2] & TYPE_A) >> 8;
printf("base\t\t0x%x\t%d\n", base, base);
printf("limit\t\t0x%x\t%d\n", limit, limit);
printf("\n");
printf("G\t\t%d\t(0=bype, 1=4KB)\n", granularity);
printf("\n");
printf("P\t\t%d\n", present);
printf("DPL\t\t%d\n", dpl);
printf("S\t\t%d\t(0=system, 1=code or data)\n", system_type); printf("TYPE_E\t\t%x\n", type_e);
printf("TYPE_ED_C\t%x\n", type_ed_c);
printf("TYPE_RW\t\t%x\n", type_rw);
printf("TYPE_A\t\t%x\n", type_a);
printf("\n");
printf("E=0, data segment\n");
printf("ED=0, data segment\n");
printf("ED=1, stack segment\n");
printf("W=0, not writable\n");
printf("W=1, writable\n");
printf("\n");
printf("E=1, code segment\n");
printf("C=0, ingore privilege\n");
printf("C=1, use privilege\n");
printf("R=0, not readable\n");
printf("R=1, readable\n");
return EXIT_SUCCESS;
}
4、Linux是如何利用段机制又巧妙的绕过段机制的?在内核代码中如何表示各种段,查找最新源代码并进行阅读和分析。
答:IA32规定段机制是不可禁止的,因此不可能绕过它直接给出线性地址空间的地址。万般无奈之下,Linux的设计人员干脆让段的基地址为0,而段的界限为4GB,这时任意给出一个偏移量,则等式为“0+偏移量=线性地址”,也就是说“偏移量=线性地址”。另外由于段机制规定“偏移量< 4GB”,所以偏移量的范围为0H~FFFFFFFFH,这恰好是线性地址空间范围,也就是说虚拟地址直接映射到了线性地址,我们以后所提到的虚拟地址和线性地址指的也就是同一地址。看来,Linux在没有回避段机制的情况下巧妙地把段机制给绕过去了。
另外,由于IA32段机制还规定,必须为代码段和数据段创建不同的段,所以Linux必须为代码段和数据段分别创建一个基地址为0,段界限为4GB的段描述符。不仅如此,由于Linux内核运行在特权级0,而用户程序运行在特权级别3,根据IA32的段保护机制规定,特权级3的程序是无法访问特权级为0的段的,所以Linux必须为内核和用户程序分别创建其代码段和数据段。这就意味着Linux必须创建4个段描述符——特权级0的代码段和数据段,特权级3的代码段和数据段。
5、为什么在设计两级页表的线性地址结构时,给页目录和页表各分配10位?如果不是这
样,举例说明会产生什么样的结果?
答:所谓两级页表就是对页表再进行分页。第一级称为页目录,其中存放的是关于页表的信息。4MB的页表再次分页(4MB/4K)可以分为1K个页,同样对每个页的描述需要4个字节,于是可以算出页目录最多占用4KB个字节,正好是一个页,其示意图如2.9所示。页目录共有1K个表项,于是,线性地址的最高10位(即22位~ 31位)用来产生第一级的索引。两级表结构的第二级称为页表,每个页表也刚好存放在一个4K字节的页中,包含1K 个字节的表项。第二级页表由线性地址的中间10位(即21位~ 12位)进行索引,最低12位表示页内偏量。
举例:如果页目录占用31~24位,页表项占用23~12位,偏移地址占11~0位,则有2^8个页表项,一个页面有2^12(4k)个表项,每个表项大小为4字节,2^12的范围为4页面,而不是以一个页面为准。
6、深入理解图2.12,并结合图叙述线性地址到物理地址的转换。
答:第一步,用32位线性地址的最高10位第31~22位作为页目录项的索引,将它乘以4,与CR3中的页目录的起始地址相加,获得相应目录项在内存的地址。
第二步,从这个地址开始读取32位页目录项,取出其高20位,再给低12位补0,形成的32位就是页表在内存的起始地址。
第三步,用32位线性地址中的第21~12位作为页表中页表项的索引,将它乘以4,与页表的起始地址相加,获得相应页表项在内存的地址。
第四步,从这个地址开始读取32位页表项,取出其高20位,再将线性地址的第11~0位放在低12位,形成最终32位页面物理地址。
第三章进程
1、什么是进程控制块?它包含哪些基本信息?
答:对进程进行全面描述的数据结构,Linux中把对进程的描述结构叫做task_struct:
struct task_struct {
}传统上这样的数据结构被叫做进程控制块PCB(process control blaock)
系统为了管理进程设置的一个专门的数据结构,用它来记录进程的外部特征,描述进程的运动变化过程。系统利用PCB来控制和管理进程,所以PCB是系统感知进程存在的唯一标志。进程与PCB是一一对应的。
包含的基本信息:
(1)状态信息-描述进程动态的变化。(2)链接信息-描述进程的父/子关系。(3)各种标识符
-用简单数字对进程进行标识。(4)进程间通信信息-描述多个进程在同一任务上协作工作。
(5)时间和定时器信息-描述进程在生存周期内使用CPU时间的统计、计费等信息。(6)调度信息-描述进程优先级、调度策略等信息。(7)文件系统信息-对进程使用文件情况进行记录。(8)虚拟内存信息-描述每个进程拥有的地址空间。(9)处理器环境信息-描述进程的执行环境(处理器的寄存器及堆栈等)
3、Linux的进程控制块如何存放?为什么?假设ESP中存放的是栈顶指针,请用三句汇编语句描述如何获得current的PCB的地址。
答:当进程一进入内核态,CPU就自动设置进程的内核栈。这个栈位于内核的数据段上,为了节省空间,Linux把内核栈和一个紧挨近的PCB的小数据结构,thread_info放在一起,占用8kb的内存区。因为这样可以节省空间,内核很容易从ESP寄存器的值获得,当前在CPU上正在运行的thread_info结构的地址。
movl $0xfffe000, %eax
andl %esp, %ecx
movl %ecx, p
4、PCB的组织方式有哪几种?为什么要采取这些组织方式?
答:(1)进程链表(2)哈希表(3)就绪队列(4)等待队列
在一个系统中,通常可以拥有数十个、数百个乃至数千个进程,相应的就有这么多PCB。为了能有效的对它们加以管理,应该用适当的方式将这些PCB组织起来。
5、请编写内核模块,打印系统中各进程的名字以及PID,同时统计系统中进程的个数。答:static int print_pid(void)
{ struct task_struct *task,*p;
struct list_head *pos;
int count = 0;
printk("Hello World enter begin:\n");
task = &init_task;
list_for_each(pos,struct task_struct,tasks);
{ count++;
printk("%d->%s\n",p->pid,p->comm);
}
printk("the number of process is:%d\n",count);
return 0;
}
7、什么是写时复制技术,这种技术在什么情况下最能发挥其优势?
答:父进程和子进程共享页面而不是复制页面。然而,只要页面被共享,它们就不能被修改。无论父进程和子进程何时试图写一个共享的页面,就产生一个错误,这时内核就把这个页复制到一个新的页面中并标记为可写。原来的页面仍然是写保护的:当其它进程试图写入时,内核检查写进程是否是这个页面的唯一属主;如果是,它把这个页面标记为对这个进程是可写的。采用这种技术,显然只有预测到将要修改的页才会被复制,而且必须被复制,不然的话,就会破坏父进程的程序执行。
9、init内核线程与init进程是一回事吗?它们有什么本质的区别?
答:(1)init()函数是内核代码的一部分,在内核态运行,是独立的可执行代码的一部分。
(2)init进程在Linux操作系统中是一个具有特殊意义的进程,它是由内核启动并运行的第一个用户进程,因此它不是运行在内核态,而是运行在用户态。它的代码不是内核本身的一部分,而是存放在硬盘上可执行文件的映象中,和其他用户进程没有什么两样。
10、用fork写一个简单的测试程序,从父进程和子进程中打印信息。信息应该包括父进程和子进程的PID。执行程序若干次,看两个信息是否以同样的次序打印。
答:#include
int main(void)
{
pid_t pid;
pid = fork();
if(pid < 0)
{
printf("error");
return 0;
}
else if(pid == 0)
{
printf("this is chile process with pid of %d\n",getpid());
}