现在,您或许正在查看设备驱动程式,并感到奇怪:“函数 foo_read() 是怎样被调用的?”或可能疑惑: “当我输入 cat /proc/cpuinfo 时,cpuinfo() 函数是怎样被调用的?”内核完成引导后,控制流就从相对直观的“接下来调用哪个函数?”改变为取决于系统调用、异常和中断。
什么是系统调用?
字面上讲,系统调用(也称为“syscall”)就是一条类似于“add”或“jump”的指令。从更高的层面上讲,系统调用是用户级程式需要操作系统为他做某些事情的途径。假如您正在编写程式,需要读取某个文档,那么要使用一个系统调用来需要操作系统为您读取那个文档。
系统调用详述
这里是系统调用的工作原理。首先,用户程式为系统调用配置参数。其中一个参数是系统调用编号(稍后对此进行详述)。注意,任何这些都是由库函数自动完成的,除非您是使用汇编编程。参数配置完成后,程式执行“系统调用”指令。这个指令会导致一个异常:产生一个事件,这个事件会致使处理器跳转到一个新的地址,并开始执行那里的代码。
新地址的指令会保存程式的状态,计算出应该调用哪个系统调用,调用内核中实现那个系统调用的函数,恢复用户程式状态,然后将控制权返还给用户程式。系统调用是设备驱动程式中定义的函数最终被调用的一种方式。
这就是系统调用怎样工作的一个简短说明。接下来,我们将为那些对内核事实上怎样完成感到好奇的这些人提供详尽的细节。不要担心您是否完全理解任何细节 ?? 只需要记住这是内核中的函数最终被调用的一个途径 ?? 没有任何神秘之处。您能够追踪控制流在内核中的全部历程 ?? 有时会有些困难,但是您能够做得到。
系统调用示例:1
这里很适合于开始根据理论展示一些代码。我们将研究 read() 系统调用的过程,首先从系统调用指令被执行的时候开始。使用 PowerPC 体系结构作为代码体系结构相关部分的示例。在 PowerPC 上,当执行一个系统调用时,处理器跳转到地址 0xc00。那个位置的代码是在文档 arch/ppc/kernel/head.S 中定义的。类似如下:
/* System call */
. = 0xc00
SystemCall:
EXCEPTION_PROLOG
EXC_XFER_EE_LITE(0xc00, DoSyscall)
/* Single step - not used on 601 */
EXCEPTION(0xd00, SingleStep, SingleStepException, EXC_XFER_STD)
EXCEPTION(0xe00, Trap_0e, UnknownException, EXC_XFER_EE)
|
这段代码所做的事情是,保存一些状态,然后调用另一个名为 DoSyscall 的函数。
EXCEPTION_PROLOG 是个宏,负责从用户空间到内核空间的转换,这需要保存用户进程的寄存器状态。使用此例程的地址和函数 DoSyscall 的地址来调用 EXC_XFER_EE_LITE。最后,某些状态将会被保存,DoSyscall 将会被调用。后面的两行在地址 0xd00 和 0xe00 保存两个异常向量。
EXC_XFER_EE_LITE 类似如下:
#define EXC_XFER_EE_LITE(n, hdlr) \
EXC_XFER_TEMPLATE(n, hdlr, n+1, COPY_EE, transfer_to_handler, \
ret_from_except)
|
EXC_XFER_TEMPLATE 是另一个宏,代码类似如下:
#define EXC_XFER_TEMPLATE(n, hdlr, trap, copyee, tfer, ret) \
li r10,trap; \
stw r10,TRAP(r11); \
li r10,MSR_KERNEL; \
copyee(r10, r9); \
bl tfer; \
i##n: \
.long hdlr; \
.long ret
|
li 表示 立即加载(load immediate),即某个在编译时已知的常量保存在某个寄存器中。首先,trap 加载到寄存器 r10 中。在接下来的一行中,那个值存储在由 TRAP(r11) 给出的地址中。TRAP(r11) 连同接下来两行去做一些硬件相关的位操作。然后,我们调用 tfer 函数(transfer_to_handler 函数),他会处理更多内部事务并将控制转交给 hdlr(DoSyscall)。注意, transfer_to_handler 通过链接寄存器加载处理程式的地址,因此您看到的是 .long DoSyscall,而不是 bl DoSyscall。
系统调用示例:2
现在我们来研究 DoSyscall。他位于 arch/ppc/kernel/entry.S 文档中。这个函数最终使用系统调用编号将系统调用表的地址和索引加载到他。操作系统使用系统调用表将系统调用编号翻译为特定的系统调用。
系统调用表名为 sys_call_table,在 arch/ppc/kernel/misc.S 中定义。系统调用表包含有实现每个系统调用的函数的地址。例如,read() 系统调用函数名为 sys_read。 read() 系统调用编号是 3,所以 sys_read() 位于系统调用表的第四个条目中(因为系统调用起始编号为 0)。从地址 sys_call_table + (3 * word_size) 读取数据,得到 sys_read() 的地址。
DoSyscall 找到正确的系统调用地址后,他将控制权转交给那个系统调用。我们来看定义 sys_read() 的位置,即 fs/read_write.c 文档。这个函数会找到关联到 fd 编号(传递给 read() 函数的)的文档结构体。那个结构体包含指向用来读取特定类型文档数据的函数的指针。进行一些检查后,他调用和文档相关的 read() 函数,来真正从文档中读取数据并返回。和文档相关的函数是在其他地方定义的 ?? 比如套接字代码、文档系统代码,或设备驱动程式代码。这是特定内核子系统最终和内核其他部分协作的一个方面。
读取函数结束后,从 sys_read() 返回到 DoSyscall(),他将控制权转换给 ret_from_except(在 arch/ppc/kernel/entry.S 中定义)。他会去检查那些在转换回用户空间之前需要完成的任务。假如没有需要做的事情,那么就通过 restore 函数恢复用户进程的状态,并将控制权交还给用户程式。
就是这样!read() 调用就完成了!幸运的话,您会得到数据。
在关键的位置加入 printk,能够更深入地研究 syscalls。一定要限制这些 printk 的输出的数量。例如,假如向 sys_read() syscall 添加 printk,应该像这样去做:
static int mycount = 0;
if (mycount < 10) {
printk ("sys_read called\n");
mycount++;
}
|