Linux 系统调用再探
2009-01-30 18:16
260 查看
关于系统调用的实现机制以及在内核级加入新的系统调用的文章已经比较多了,在我们的《Linux操作系统原理与应用》一书中,给出了添加系统调用的两种方式,其中有一个实例通过系统调用对内核的运行过程进行跟踪,但那是针对2.4内核,以下文章转自IBM Developerworks, 其分析的清晰和透彻值得借鉴:
|
M. Tim Jones (mtj@mtjones.com), 顾问工程师, Emulex 2007 年 4 月 17 日 Linux® 系统调用 —— 我们每天都在使用它们。不过您清楚系统调用是如何在用户空间和内核之间执行的吗?本文将探究 Linux 系统调用接口(SCI),学习如何添加新的系统调用(以及实现这种功能的其他方法),并介绍与 SCI 有关的一些工具。 系统调用就是用户空间应用程序和内核提供的服务之间的一个接口。由于服务是在内核中提供的,因此无法执行直接调用;相反,您必须使用一个进程来跨越用户空间与内核之间的界限。在特定架构中实现此功能的方法会有所不同。因此,本文将着眼于最通用的架构 —— i386。 在本文中,我将探究 Linux SCI,演示如何向 2.6.20 内核添加一个系统调用,然后从用户空间来使用这个函数。我们还将研究在进行系统调用开发时非常有用的一些函数,以及系统调用的其他选择。最后,我们将介绍与系统调用有关的一些辅助机制,比如在某个进程中跟踪系统调用的使用情况。 SCI Linux 中系统调用的实现会根据不同的架构而有所变化,而且即使在某种给定的体架构上也会不同。例如,早期的 x86 处理器使用了中断机制从用户空间迁移到内核空间中,不过新的 IA-32 处理器则提供了一些指令对这种转换进行优化(使用 sysenter 和 sysexit指令)。由于存在大量的方法,最终结果也非常复杂,因此本文将着重于接口细节的表层讨论上。更详尽的内容请参看本文最后的 参考资料。 要对 Linux 的 SCI 进行改进,您不需要完全理解 SCI 的内部原理,因此我将使用一个简单的系统调用进程(请参看图 1)。每个系统调用都是通过一个单一的入口点多路传入内核。eax 寄存器用来标识应当调用的某个系统调用,这在 C 库中做了指定(来自用户空间应用程序的每个调用)。当加载了系统的 C库调用索引和参数时,就会调用一个软件中断(0x80 中断),它将执行 system_call函数(通过中断处理程序),这个函数会按照 eax 内容中的标识处理所有的系统调用。在经过几个简单测试之后,使用 system_call_table和 eax 中包含的索引来执行真正的系统调用了。从系统调用中返回后,最终执行 syscall_exit,并调用 resume_userspace返回用户空间。然后继续在 C 库中执行,它将返回到用户应用程序中。 图 1. 使用中断方法的系统调用的简化流程 SCI 的核心是系统调用多路分解表。这个表如图 2 所示,使用 eax 中提供的索引来确定要调用该表中的哪个系统调用( sys_call_table)。图中还给出了表内容的一些样例,以及这些内容的位置。(有关多路分解的更多内容,请参看侧栏 “系统调用多路分解”) 图 2. 系统调用表和各种链接
向内核中添加新系统调用,需要执行 3 个基本步骤: 添加新函数。 更新头文件。 针对这个新函数更新系统调用表。 注意: 这个过程忽略了用户空间的需求,我将稍后介绍。 最常见的情况是,您会为自己的函数创建一个新文件。不过,为了简单起见,我将自己的新函数添加到现有的源文件中。清单 1 所示的前两个函数,是系统调用的简单示例。清单 2 提供了一个使用指针参数的稍微复杂的函数。 清单 1. 系统调用示例的简单内核函数
jiffy,而第二个函数则返回当前值与所传递进来的值之间的差值。注意 asmlinkage修饰符的使用。这个宏(在 linux/include/asm-i386/linkage.h 中定义)告诉编译器将传递栈中的所有函数参数。 清单 2. 系统调用示例的最后内核函数
long类型,以及一个指向被定义为 __user的 long的指针。 __user宏简单告诉编译器(通过 noderef)不应该解除这个指针的引用(因为在当前地址空间中它是没有意义的)。这个函数会计算这两个 jiffies 值之间的差值,然后通过一个用户空间指针将结果提供给用户。 put_user函数将结果值放入 presult所指定的用户空间位置。如果在这个操作过程中出现错误,将立即返回,您也可以通知用户空间调用者。 对于步骤 2 来说,我对头文件进行了更新:在系统调用表中为这几个新函数安排空间。对于本例来说,我使用新系统调用号更新了 linux/include/asm/unistd.h 头文件。更新如清单 3 中的黑体所示。 清单 3. 更新 unistd.h 文件为新系统调用安排空间
个步骤,更新系统调用表。如清单 4 所示,我将为这个新函数更新 linux/arch/i386/kernel/syscall_table.S 文件,它会填充清单 3 显示的特定索引。 清单 4. 使用新函数更新系统调用表
NR_syscalls定义的。 现在,我们已经完成了对内核的更新。接下来必须对内核重新进行编译,并在测试用户空间应用程序之前使引导使用的新映像变为可用。 对用户内存进行读写 Linux 内核提供了几个函数,可以用来将系统调用参数移动到用户空间中,或从中移出。方法包括一些基本类型的简单函数(例如 get_user或 put_user)。要移动一块儿数据(如结构或数组),您可以使用另外一组函数: copy_from_user和 copy_to_user。可以使用专门的调用移动以 null 结尾的字符串: strncpy_from_user和 strlen_from_user。您也可以通过调用 access_ok来测试用户空间指针是否有效。这些函数都是在 linux/include/asm/uaccess.h 中定义的。 您可以使用 access_ok宏来验证给定操作的用户空间指针。这个函数有 3 个参数,分别是访问类型( VERIFY_READ或 VERIFY_WRITE),指向用户空间内存块的指针,以及块的大小(单位为字节)。如果成功,这个函数就返回 0:
get_user和 put_user轻松地实现。这两个宏都包含一个值以及一个指向变量的指针。 get_user 函数将用户空间地址( ptr)指定的值移动到所指定的内核变量( var)中。 put_user函数则将内核变量( var)指定的值移动到用户空间地址( ptr)。 如果成功,这两个函数都返回 0:
copy_from_user和 copy_to_user函数。这些函数将在用户空间和内核之间移动完整的数据块。 copy_from_user函数会将一块数据从用户空间移动到内核空间, copy_to_user 则会将一块数据从内核空间移动到用户空间:
strncpy_from_user函数将一个以 NULL 结尾的字符串从用户空间移动到内核空间中。在调用这个函数之前,您可以通过调用 strlen_user 宏来获得用户空间字符串的大小:
中找到这些函数。 现在内核已经使用新系统调用完成更新了,接下来看一下从用户空间应用程序中使用这些系统调用需要执行的操作。使用新的内核系统调用有两种方法。第一种方法非常方便(但是在产品代码中您可能并不希望使用),第二种方法是传统方法,需要多做一些工作。 使用第一种方法,您可以通过 syscall函数调用由其索引所标识的新函数。使用 syscall 函数,您可以通过指定它的调用索引和一组参数来调用系统调用。例如,清单 5 显示的简单应用程序就使用其索引调用了 sys_getjiffies。 清单 5. 使用 syscall 调用系统调用
syscall 函数使用了系统调用表中使用的索引作为第一个参数。如果还有其他参数需要传递,可以加在调用索引之后。大部分系统调用都包括了一个 SYS_ 符号常量来指定自己到 __NR_索引的映射。例如,使用 syscall调用 __NR_getpid索引:
syscall函数特定于架构,使用一种机制将控制权交给内核。其参数是基于 __NR索引与 /usr/include/bits/syscall.h 提供的 SYS_符号之间的映射(在编译 libc 时定义)。永远都不要直接引用这个文件;而是要使用 /usr/include/sys/syscall.h 文件。 传统的方法要求我们创建函数调用,这些函数调用必须匹配内核中的系统调用索引(这样就可以调用正确的内核服务),而且参数也必须匹配。Linux 提供了一组宏来提供这种功能。 _syscallN宏是在 /usr/include/linux/unistd.h 中定义的,格式如下:
_syscall宏最多可定义 6 个参数(不过此处只显示了 3 个)。 现在,让我们来看一下如何使用 _syscall宏来使新系统调用对于用户空间可见。清单 6 显示的应用程序使用了 _syscall宏定义的所有系统调用。 清单 6. 将 _syscall 宏 用于用户空间应用程序开发
__NR索引在这个应用程序中是必需的,因为 _syscall宏使用了 func-name来构造 __NR索引( getjiffies-> __NR_getjiffies)。其结果是您可以使用它们的名字来调用内核函数,就像其他任何系统调用一样。 系统调用是请求内核中服务的一种有效方法。使用这种方法的最大问题就是它是一个标准接口,很难将新的系统调用增加到内核中,因此可以通过其他方法来实现类似服务。如果您无意将自己的系统调用加入公共的 Linux 内核中,那么系统调用就是将内核服务提供给用户空间的一种方便而且有效的方法。 让您的服务对用户空间可见的另外一种方法是通过 /proc 文件系统。/proc 文件系统是一个虚拟文件系统,您可以通过它来向用户提供一个目录和文件,然后通过文件系统接口(读、写等)在内核中为新服务提供一个接口。 Linux 内核提供了一种非常有用的方法来跟踪某个进程所调用的系统调用(以及该进程所接收到的信号)。这个工具就是 strace,它可以在命令行中执行,使用希望跟踪的应用程序作为参数。例如,如果您希望了解在执行 date 命令时都执行了哪些系统调用,可以键入下面的命令:
date 命令过程中所执行的各个系统调用。您会看到加载共享库、映射内存,最后跟踪到的是在标准输出中生成日期信息:
syscall_trace的特定字段集(它导致 do_syscall_trace函数的调用)时,将在内核中完成跟踪。您还可以看到跟踪调用是 ./linux/arch/i386/kernel/entry.S 中系统调用请求的一部分(请参看 syscall_trace_entry)。
文件系统项来提供用户/内核间的交互。不过当速度因素非常重要时,系统调用则是使应用程序获得最佳性能的理想方法。请参看 参考资料 的内容进一步了解 SCI。 参考资料 学习 您可以参阅本文在 developerWorks 全球网站上的 英文原文。 在 “Access the Linux kernel using the /proc filesystem”(developerWorks,2006 年 3 月)一文中,学习如何使用 /proc 文件系统开发内核代码,实现用户空间与内核空间之间的通信。 请阅读来自 Manugarg 的 “Sysenter Based System Call Mechanism in Linux 2.6”,详细了解用户空间应用程序和内核之间的系统调用。这篇文章着重介绍了 2.6 版本内核所提供的转换机制。 本文详细介绍了用户空间和内核之间的 汇编语言链接。 GNU CLibrary(glibc)是 GNU C 的标准库。您可以找到 Linux 和其他各种操作系统上使用的 glibc。 GNU CLibrary 遵守很多标准,包括 ISO C 99、POSIX 和 UNIX98。在 GNU Project 上您可以找到更多相关内容。 Linux syscalls 手册 提供了 Linux 中可以使用的系统调用的完整列表。 Wikipedia 提供了一个 有关系统调用的有趣观点,包括相关历史和典型实现。 虽然有点过时,但是提供了 内核应用程序编程接口(API),它归档了很多(在内核内)通用的内核函数。 在 developerWorks Linux 专区 中可找到面向 Linux 开发人员的更多资源。 随时关注 developerWorks 技术活动和网络广播。 讨论 通过参与 developerWorks blog 加入 developerWorks 社区。 关于作者
|
相关文章推荐
- linux文件操作笔记(1)——系统调用、库函数
- 操作系统-Linux添加系统调用
- arm-linux3.0自定义系统调用
- Linux系统调用
- 走马观花: Linux 系统调用 open 七日游(五)
- Linux系统调用的实现机制分析
- linux 系统中调用执行脚本
- Linux系统调用列表(收藏)
- linux总结应用之四 系统调用函数
- Linux系统调用及用户编程接口(API)(二)
- linux2.6内核系统调用的增加方法
- 学习笔记 --- LINUX 应用调试之使用strace命令追踪系统调用
- Linux系统调用
- linux内核不用系统调用获取时间的函数kernel_mktime
- linux中mmap系统调用原理分析与实现
- Linux系统调用列表
- 浅析linux中open系统调用
- Linux2.6 --系统调用处理程序
- Linux系统调用的实现机制分析
- Linux下的系统调用列表