原因是寄存器是使用触发器实现的,这也是一种
存储电路,工作速度极快,是纳秒级别的,
段界限用 20 个二进制位来
表示。只不过此段界限只是个单位量,它的单位要么是字节,要么是 4阻,这是由描述符中的 G 位来指
定的。最终段的边界是此段界限值*单位,故段的大小要么是 2 的 20 次方等于山田,要么是 2 的 32 次方
( 4阳等于 2 的 12 次方, 12+20=32 ) 等于 4GB 。
全局描述符表 GDT 只是一片内存区域,里面每隔 8 字节便是一个表项,即段描述符。
在我们的机器上即使只有 512MB 的内存,每个进程自己的内存空间也是 4GB ,
CPU 允许在描述符表中已注册的段不在内存中存在,这就是它提供给软件使用的策略,我们利用它实现段式内存管理。如果该描述符中的 P 位为 1 ,表示该段在内存中存在。访问过该段后, CPU 将段描述符中的 A 位置 l ,若b币丘来刚访问过该段。相反,如果 P 位为 0,说明内存中并不存在该段,这时候 CPU 将会抛出个 NP (段不存在)异常,转而去执行中断描述符表中 NP 异常对应的中断处理程序,此中断处理程序是操作系统负责提供的,该程序的工作是将相应的段从外存(比如硬盘)中载入到内存,并将段描述符的 P 位置 1 ,中断处理函数结束后返回, CPU 重复执行这个检查,继续查看该段描述符的 P 位,此时已经为 1 了,在检查通过后,将段描述符的 A 位置 1 。
查找页表的工作也是由硬件完成的
一个页表表示的内存容量是 4MB
拿 Linux 来说,它的系统调用是先往 eax寄存器中写入系统调用号,然后通过 Ox80 中断来实现的
汇编指令与机器指令几乎是一对一的,即一名汇编代码只对应一句具体的机器码,不会有更多对应的
选择,所以可以认为汇编指令就是机器指令。 C 语言的编译过程是先将 C 语言代码转换成汇编代码,然后
再将汇编代码转换成机器指令。所以用 C 语言写出来的程序,最终可以转换成对应的一句或多句汇编指
令。
先将源程序编译成目标文件(由 c 代码变成汇编代码后,再由汇编代码生成二进制的目标文件),再将目标文件链接成二进制可执行文件。
人们常说的汇编语言比 C 语言快,并不是汇编语言本身有多快(它也要变成机器指令后才能上 CPU 运行),而是汇编语言对应的机器指令是一对一的,简单,直接,可依赖,而 C 语言生成的机器指令是一对多的,复杂,间接,略冗余。
程序中段的大小和数量是不固定的,节的大小和数量也不固定,因此需要为它们专门找个数据结构来描述它们,这个描述结构就是程序头表( program header table )和节头表( section header table )
由于程序中段和节的数量不固定,程序头表和节头表的大小自然也就不固定了,而且各表在程序文件中的存储顺序自然也要有个先后,故这两个表在文件中的位直也不会固定。因此,必须要在一个固定的位置,用 一 个固定大小的数据结构来描述程序头表和节头表的大小及位置信息,这个数据结构便是 ELF header ,它位于文件最开始的部分,并具有固定大小,
ELF header 是个用来描述各种“头”的“头飞程序头表和节头表中的元素也是程序头和节头,
rep 指令 即指令是repeat重复的意思,该指令是按照民x 寄存器中指定的次数重复执行后面的指定的指令,每执行一次, ecx 自减 1 ,直到 ecx 等于 0 时为止,
对于 in 指令,如果源操作是 8 位寄存器,目的操作数必须是al 如果源操作数是 16 位寄存器,目的操作数必须是ax。
外部中断是指来自 CPU 外部的中断,而外部的中断源必须是某个硬件,
在 CPU 上运行的程序都是串行的,所有任务,包括中断
处理程序都是一个接一个在 CPU 上运行的,所有任务都共享
同一个 CPU, CPU 在各个任务间不断切换才实现了并发。
操作系统是中断驱动的,中断发生后会执行相应的中断处理程序,我们希望 CPU 中断响应的时间越
短越好,这样便能响应更多设备的中断。但是中断处理程序还是需要完整执行的,不能光为了提高中断响
应效率而只执行部分中断处理程序 。 于是,把中断处理程序分为上半部和下半部两部分,把中断处理程序
中需要立即执行的部分(分分钟不能耽误的部分)划分到上半部,这部分是要限时执行的,所以通常情况
下只完成中断应答或硬件复位等重要紧迫的工作。而中断处理程序中那些不紧急的部分则被推迟到下半部
中去完成。
不可屏蔽中断是通过 NMI 引脚进入 CPU 的,它表示系统中发生了致命的错误,它等同于宣布:计算机的运行到此结束了。
内部中断可分为软中断和异常。
int3 是调试断点指令,其所触发的中断向量号是 3,我们用 gdb 或 bochs 调试程序时,实际上就是调试器 fork 了一个子进程,子进程用于运行被调试的程序。调试器中经常要设置断点,其原理就是父进程修改了子进程的指令,将其用 int3指令替换,从而子进程调用了 int3 指令触发中断。
8259A 的作用是负责所有来自外设的中断,其中就包括来自时钟的中断,
每个独立运行的外部设备都是一个中断源,它们所发出的中断,只有接在中断请求 CIRQ: Interrupt
ReQuest )信号线上才能被 CPU 大神知晓,这也就是大家在开机时,电脑屏幕上会看到的 IRQI … IRQn,
这些都是为外部设备所分配的中断号 。
I. 在用户空间中实现线程
最后,线程在用户空间实现,和在内核空间实现相比,只是在内部调度时少了陷入内核的代价,确实相当于提速,但由于整个进程占据处理器的时间片是有限的,这有限的时间片还要再分给内部的线程,所以每个线程执行的时间片非常非常短暂,再加上进程内线程调度器维护线程表、运行调度算法的时间片消耗,反而抵销了内部调度带来的提速 。
内部时钟是指处理器中内部元件,如运算器、控制器的工作时序,主要用于控制、同步内部工作过程的步调。内部时钟是由晶体振荡器产生的,简称晶振,它位于主板上,其频率经过分频之后就是主板的外频,处理器和南北桥之间的通信就基于外频。 Intel 处理器将此外频乘以某个倍数(也称为倍频)之后便称为主频 。 处理器取指令、执行指令中所消耗的时钟周期,都是基于主频的 。 内部时钟是由处理器固件结构决定的,在出厂时就设定好啦,无法改变。处理器内部元件的工作速度是最快的,所以内部时钟的时间单位粒度比较精细,通常都是纳秒( ns )级的。
外部时钟是指处理器与外部设备或外部设备之间通信时采用的 一种时序,比如 IO 接口和处理器之间在 AID 转换时的工作时序、两个串口设备之间进行数据传输时也要事先同步时钟等 。 外部设备的速度对于处理器来说就很慢了,所以其时钟的时间单位粒度较大, 一般是毫秒( ms )级或秒 ( s )级的 。
在二元信号量 中, down 操作就是获得锁, up 操作就是释放锁
键盘是个独立的设备,在它内部有个叫作键盘编码器的芯片,通常是 Intel 8048 或兼容芯片,它的作用是:每当键盘上发生按键操作,它就向键盘控制器报告哪个键被按下,按键是否弹起。
这个键盘控制器可并不在键盘内部,它在主机内部的主板上,通常是 Intel 8042 或兼容芯片,它的作用是接收来自键盘编码器的按键信息,将其解码后保存,然后向中断代理发中断,之后处理器执行相应的中断处理程序读入 8042 处理保存过的按键信息 。
当某个键被按下时, 8048 把这个键对应的数值发送给 8042, 8042
根据这个数值便知道是哪个键被按下了。
一 个键的状态要么是按下,要么是弹起,因此一个键便有两个编码,按键被按下时的编码叫通码,也就是表示按键上的触点接通了内部电路,使硬件产生了一个码,故通码也称为 makecode 。按键在被按住不松手时会持续产生相同的码,直到按键被松开时才终止,因此按键被松开弹起时产生的编码叫断码,也就是电路被断开了,不再持续产生码了,故断码也称为 breakcode 。一个键的扫描码是由通码和断码组成的。
扫描码是硬件提供的编码集, ASCII 是软件中约定的编码集,这两个是不同的编码方案。
大多数’情况下第一套扫描码中的通码和断码都是 1 字节大小 。 您看,表 10-1 中的通码和断码,它们的关系是:断码= Ox80 +通码 。 顺便说一句,. 在第二套键盘扫描码中, 一般的通码是 1 宇节大小,断码是在通码前再加 1 字节的 OxFO ,共 2 字节,我们的 8042 工作之一就是根据第二套扫描码中通码和断码的关系将它们解码,然后按照第一套扫描码中通码和断码的关系转换成第一套扫描码。
TSS 是 Task State Segment 的缩写,即任务状态段,
TSS 就是任务的代表, CPU 用不同的 TSS 区分不同的任务,因此任务切换的本质就是 TSS 的换来换去。
TSS 和其他段一样,本质上是一片存储数据的内存区域, Intel 打算用这片内存区域保存任务的最新状态(也就是任务运行时占用的寄存器组等),因此它也像其他段那样,需要用某个描述符结构来“描述”它,这就是 TSS 描述符, TSS 描述符也要在 GDT 中注册
在 CPU 中有一个专门存储 TSS 信息的寄存器,这就是TR 寄存器,它始终指向当前正在运行的任务,因此,“在 CPU 眼里”,任务切换的实质就是 TR 寄存器指向不同的 TSS
Linux 系统调用是用中断门来实现的,通过软中断指令 int 来主动发起中断信号。
Linux 只占用一个中断向量号,即 Ox80 ,处理器执行指令 int0x80 时便触发了系统调用。
分区是逻辑上划分磁盘空间的方式,归根结底是人为地将硬盘上的柱 面扇区划分成不同的分组,每个分组都是单独的分区。各分区都有“描述符”来描述分区本身所在硬盘上的起止界限等信息,在硬盘的MBR 中有个 64 字节“固定大小”的数据结构,这就是著名的分区表,分区表中的每个表项就是一个分区的“描述符”,表项大小是 16 字节,因此 64 字节的分区表总共可容纳 4 个表项,这就是为什么硬盘仅支持 4 个分区的原因。
其实分区表的长度并不是由结构本身限制的,而是由其所在的位
置限制的,它必须存在于 MBR 引导扇区或 EBR 引导扇区中。在这 512 字节中,前 446 字节是硬盘的参数和引导程序,然后才是 64 宇节的分区表,最后是 2 字节的魔数 55础。随着计算机天长地久的发展,还真别小看这个扇区 , 很多程序已经对它有依赖了,尤其是一些引导型程序(如 BIOS ),都会在该扇区的512 字节中的固定位置读取关键数据,
分区表中共 4 个分区,哪个做扩展分区都可以,扩展分区是可选的,但最多只有 1 个,其余的都是主分区。在过去没有扩展分区时,这 4 个分区都是主分区 i 为了兼容 4 个主分区的情况,扩展分区中的第 1 个逻辑分区的编号从 5 开始。
硬盘是低速设备,其读写单位是扇区,为了避免频繁访问硬盘,操作系统不会有了一扇区数据就去读写一次磁盘,往往等数据积攒到“足够大小”时才一 次性访问硬盘,这足够大小的数据就是块,硬盘读写单位是扇区,因此一 个块是由多个扇区组成的,块大小是扇区大小的整数倍。
块是文件系统的读写单位,因此文件至少要占据一 个块,当文件体积大于 l 个块时,文件肯定被拆分成多个块来存储,
用索引结构的缺点是索引表本身要占用 一 定的存储空间,文件要是很大时,块就比较多,索引表项就要跟着增多
文件系统是针对各个分区来管理的, inode代表文件,因此各分区都有自己的 inode 数组 。
Windows 系统通常是 fat32 或 ntfs, Linux 系统通常是 ext2 或 ext3 ,
inode用于描述文件存储相关信息,文件结构用于描述“文件打开”后,文件读写偏移量等信息。文件与 inode一一对应,一个文件仅有一个 inode , 一个 inode 仅对应一个文件。一个文件可以被多次打开,因 此一个inode 可以有多个文件结构,多个文件结构可以对应同一个 inode.
程序是指在磁盘上存储的文件,是静态的,进程是指程序被加载到内存后,在内存中运行中的程序映像,简而言之,进程就是运行的程序。
fork 就是相当于同一个程序多次加载执行,因此在内存中产生了多个同名进程。
在 Linux 中, init 是用户级进程,它是第一个启动的程序,因此它的 pid
是 1 ,后续的所有进程都是它的孩子,故 init 是所有进程的父进程,所以它还负责所有子进程的资源回收,
管道是用于存储数据的中转站,当某个进程往管道中写入数据后,该数据很快就会被另 一个进程读取,之后可以用新的数据覆盖老数据,继续被别的进程读取,因此管道属于临时存储区,其中的数据在读取后可
被清除。
管道分为两种:匿名管道和命名管道,从概念上就可以知道,这是按照管道是否有名称来划分的。以上说的管道便是匿名管道,它没有名字。由于没有名字,匿名管道在创建之后只能通过内核为其返回的文件描述符来访问,此管道只对创建它的进程及其子进程可见,对其他进程不可见,因此除父子进程之外的其他进程便不知道此管道的存在,故匿名管道只能局限用于父子进程间的通信。
有名管道是专门为解决匿名管道的局限性而生的,在 Linux 中可以通过命令 mkfifo 来创建命名管道,成功创建之后便会在文件系统上存在个管道文件,这使得该管道对任何进程都“可见飞因此多个进程即
使没有父子关系也都通过访问该管道文件进行通信。