转自:http://os.51cto.com/art/200512/13510.htm
现在,您或许正在查看设备驱动程序,并感到奇怪:“函数 foo_read()
是如何被调用的?”或者可能疑惑: “当我输入 cat /proc/cpuinfo
时,cpuinfo()
函数是如何被调用的?”内核完成引导后,控制流就从相对直观的“接下来调用哪个函数?”改变为取决于系统调用、异常和中断。
什么是系统调用?
字面上讲,系统调用(也称为“syscall”)就是一条类似于“add”或者“jump”的指令。从更高的层面上讲,系统调用是用户级程序要求操作系统为它做某些事情的途径。如果您正在编写程序,需要读取某个文件,那么要使用一个系统调用来要求操作系统为您读取那个文件。
系统调用详述
这里是系统调用的工作原理:首先,用户程序为系统调用设置参数。其中一个参数是系统调用编号(稍后对此进行详述)。注意,所有这些都是由库函数自动完成的,除非您是使用汇编编程。参数设置完成后,程序执行“系统调用”指令。这个指令会导致一个异常:产生一个事件,这个事件会致使处理器跳转到一个新的地址,并开始执行那里的代码。新地址的指令会保存程序的状态,计算出应该调用哪个系统调用,调用内核中实现那个系统调用的函数,恢复用户程序状态,然后将控制权返还给用户程序。系统调用是设备驱动程序中定义的函数最终被调用的一种方式。这就是系统调用如何工作的一个简短说明。
接下来,我们将为那些对内核事实上如何完成感到好奇的这些人提供详尽的细节。不要担心您是否完全理解所有细节 —— 只需要记住这是内核中的函数最终被调用的一个途径 —— 没有任何神秘之处。您可以追踪控制流在内核中的全部历程 —— 有时会有些困难,但是您可以做得到。
系统调用示例:1
这里非常适合于开始根据理论展示一些代码。我们将研究 read()
系统调用的过程,首先从系统调用指令被执行的时候开始。使用 PowerPC 体系结构作为代码体系结构相关部分的示例。在 PowerPC 上,当执行一个系统调用时,处理器跳转到地址 0xc00
。那个位置的代码是在文件 arch/ppc/kernel/head.S
中定义的。类似如下:
|
这段代码所做的事情是,保存一些状态,然后调用另一个名为 DoSyscall
的函数。
EXCEPTION_PROLOG
是一个宏,负责从用户空间到内核空间的切换,这需要保存用户进程的寄存器状态。使用此例程的地址和函数 DoSyscall
的地址来调用 EXC_XFER_EE_LITE
。最后,某些状态将会被保存,DoSyscall
将会被调用。后面的两行在地址 0xd00
和 0xe00
保存两个异常向量。
EXC_XFER_EE_LITE
类似如下:
|
EXC_XFER_TEMPLATE
是另一个宏,代码类似如下:
|
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
,应该像这样去做:
|