读《linux内核完全注释》的FAQ
以下只是个人看了《linux内核完全注释》的一点理解,如果有错误,欢迎指正!
1 eip中保存的地址是逻辑地址、线性地址还是物理地址?
这个应该要分情况。eip保存的是下一条要执行的指令地址,也就是说cpu是根据eip到内存中去寻找指定的内容。如果cpu工作在实模式,那么eip保存的就是物理地址;如果cpu工作在保护模式下,那么cpu在去内存寻找指定的内容之前要先将eip加上当前程序代码段的基址(通过当前cs所指向的代码段描述符获得),即获得当前程序的线性地址,如果cpu没有开启分页机制,那么这个线性地址就是实际的物理地址了;如果cpu开启了分页机制,那么就要通过线性地址查找页目录表和页表来获得实际的物理地址。所以如果cpu在保护模式下,不管有没有开启分页机制,eip保存的都是程序的逻辑地址。
2 每个进程有几个堆栈?
每个进程都有两个堆栈,一个是工作在用户态的用户堆栈,一个是工作在内核态的内核堆栈。内核堆栈只有一页,即4k,该页的低地址保存了任务结构。
3 进程的状态是怎么变化的?
一般发生硬件中断,程序异常,程序执行系统调用,cpu会将当前执行的程序转换为内核状态去执行中断处理程序,并且使用内核堆栈。系统调用是进程让自己主动转换为内核状态的唯一方法,而硬件中断和程序异常是cpu强制将当前进程转换为内核态去执行中断处理程序。linux0.12是通过将系统调用0x80设置为DPL为3的陷阱门,而硬件中断和程序异常都被设置为DPL为0的中断门来实现的。
4 任务是怎样被切换的?
是通过ljmp指令实现的,如果ljmp的操作数是GDT表(全局描述符表)中某个任务的任务段描述符或者IDT表(中断描述符表)中的任务门描述符,那么cpu会切换去执行这个新任务,在切换前会将cpu中各种寄存器的状态存放到被切换出去的任务的任务段中。并把该新任务任务段中的信息恢复到cpu的各个寄存器中。
5 进程为什么会被切换?
让进程切换有几个原因,一个是进程的时间片用完了,时间中断处理程序会在每次被调用的时候检查当前进程的时间片是否用户,如果用完了就切换当前进程;一个是进程通过调用系统调用(如pause,wait等)让出cpu,即程序为了等待资源而主动让出cpu。
6 信号处理程序是什么时候被调用的?
每次中断处理程序(包括时钟中断,系统调用等)结束之前都会去检查当前程序的信号位图,如果某个信号位图相应的位为1,并且该信号未被阻塞,则通过调用do_signal中断程序来设置内核态堆栈(通过修改保存在堆栈中的用户态eip来指向信号处理程序)和用户态堆栈(保存调用系统调用的下一条指令地址,信号处理程序参数等),使得在系统调用返回后能马上去执行信号处理程序而不是执行调用系统调用的程序的下一条指令。
7 等待信号发生的程序是怎样被唤醒的?
每次调度程序schedule被调用的时候,都会去检查系统中所有任务,如果该任务的状态为可中断的等待,并且该任务有未阻塞的信号到达,则将该任务的状态设置为可运行状态等待被调度。
8 每个进程的线性地址是怎么计算的?
每个进程都有64M的地址空间,每个进程都有自己的页表,但都共用存放在物理地址为0的页目录表,每个进程在页目录表中有16项。页目录表占一个内存页,即4k,每个目录项为4B,所以目录表中对多有1024项,即系统总共可以运行64个进程。每个进程的段基址可以通过该进程的任务号nr乘以64M得到,段基址加上逻辑地址即得到进程的线性地址。
9 内核态的进程是否可以访问所有的内存空间?
linux0.12能访问的内存最大为16M,head.s程序将4个4k的页表放在页目录表的后面,并且该四个页表占用页目录的前四项,也即内核程序最多为16M。并且将GDT表中的代码段和数据段的段基址设置为0,也就是说此时内核程序的逻辑地址和物理地址是一样的,所以内核程序都能访问16M的物理地址,即所有的内存空间。
10 处于内核态的进程是否会发生任务切换?
不会,即使进程的时间片用完了,当然如果进程自己主动让出cpu,那也是会切换任务的。
11 进程是怎样主动让出cpu的?
不管进程处在用户态还是内核态,只要进程调用interruptible_sleep_on或者sleep_on函数,进程就会主动让出cpu。interruptible_sleep_on和sleep_on函数都是调用__sleep_on函数,__sleep_on函数输入参数是某等待队列的头指针(这个等待队列是task_struct结构的,用于存储等待同一类资源的所有任务指针,至于要传入等待队列头指针的原因是一般程序主动让出cpu是由于所需的资源当前不可用)。值得注意的是如果等待同一资源的进程调用__sleep_on函数,表示等待该资源的等待队列的头指针会指向这个新的进程,而在__sleep_on函数中有一个task_struct结构的指针temp指向上一个等待该资源的进程,这样就形成了一个由task_struct结构组成的链表等待队列。当等待队列中某个进程获得cpu时,如果它不是等待队列的头,该进程会唤醒等待队列的头运行,而自己则继续等待,重复这一过程,直到等待队列的头是当前进程。
图1 buffer_wait等待队列
12 linux缓冲区管理的原理是什么?
linux在内存中内核代码的结束后面放了一定大小的缓冲区(大小根据内存实际大小安排)。初始化缓冲区时,从缓冲区头和尾同时进行,缓冲区头存放缓冲块头结构,缓冲区尾存放实际的缓存块,缓冲块大小和实际磁盘块大小一样,为1K,一个缓冲区头管理一个缓冲区块。所有缓存块的头被链接成一个双向链表结构,如图2。free_list是指向空闲缓冲块链表的头,free_list的b_prev_free指向指向空闲缓冲块头链表的尾,而缓冲块头链表的尾则指向缓冲块链表的头,这样空闲缓冲块链表就形成了一个循环双链表,如图3。
图2 缓冲区初始化
图2 空闲缓冲块双向循环链表
缓冲块头结构如下:
struct buffer_head { char * b_data; /* pointer to data block (1024 bytes) */ unsigned long b_blocknr; /* block number */ unsigned short b_dev; /* device (0 = free) */ unsigned char b_uptodate; unsigned char b_dirt; /* 0-clean,1-dirty */ unsigned char b_count; /* users using this block */ unsigned char b_lock; /* 0 - ok, 1 -locked */ struct task_struct * b_wait; struct buffer_head * b_prev; struct buffer_head * b_next; struct buffer_head * b_prev_free; struct buffer_head * b_next_free; };
b_data是缓冲块地址;b_blocknr和b_dev表示缓冲块中存放的是哪个设备的几号块;b_uptodate表示当前缓冲块是否有效,为1是有效,为0是无效;b_dir表示当前缓存块是否和磁盘块的数据一致,为1表示不一致,为0表示一致;b_count表示引用当前缓冲块的进程数;b_lock表示当前缓冲块是否被锁定(一般缓冲块与实际物理磁盘传输数据时,缓冲块是被锁定的);b_wait指向等待当前缓冲块的任务队列的头;b_prev和b_next主要用于查找缓冲块,具体见下面;b_prev_free和b_next_free用于形成空闲缓冲块双向循环链表。
为了快速而有效地在缓冲区寻找并判断出请求的数据块已经被读入缓冲区中,将每个已经读入缓冲区的数据块加入到一个hash链表中,每个hash链表的头指针存放在hash数组中,通过b_blocknr和b_dev共同决定将数据块放到哪个hash链中,如图3所示。
图3 hash缓冲链
从图中可以看出每个hash链都是一个双向链表,通过b_prev和b_nex来实现。如果要判断某个数据块是否在缓冲区中, 通过b_blocknr和b_dev直接找到对应的hash链表,在链表中查看是否有b_blocknr和b_dev一致的数据块就可以判断数据块是否在缓冲区中了。为了实现缓冲区满了要替换出最近最久未使用(LRU)的缓冲块,每次都将新分配的缓冲块放到free_list的最后面,而每次找空闲缓存块则从free_list最前面开始找,这样就实现LRU了。