Linux系统调用原理与实现详解

📅 发布时间:2026/7/7 0:52:08 👁️ 浏览次数:
Linux系统调用原理与实现详解
1. Linux系统调用概述在计算机系统中各种硬件资源都是有限的。现代多任务操作系统上同时运行的多个进程都需要访问这些资源但为了更好的管理这些资源进程是不被允许直接操作的。所有对这些资源的访问都必须由操作系统控制。操作系统是使用这些资源的唯一入口而这个入口就是操作系统提供的系统调用System Call。在Linux中系统调用是用户空间访问内核的唯一手段除异常和陷入外它们是内核唯一的合法入口。注意一般情况下应用程序通过应用编程接口(API)而不是直接通过系统调用来编程。在Unix世界最流行的API是基于POSIX标准的。操作系统一般是通过中断从用户态切换到内核态。中断就是一个硬件或软件请求要求CPU暂停当前的工作去处理更重要的事情。比如在x86机器上可以通过int指令进行软件中断而在磁盘完成读写操作后会向CPU发起硬件中断。中断有两个重要的属性中断号用来标识不同的中断中断处理程序不同的中断具有不同的中断处理程序在操作系统内核中维护着一个中断向量表Interrupt Vector Table这个数组存储了所有中断处理程序的地址而中断号就是相应中断在中断向量表中的偏移量。2. 为什么需要系统调用Linux内核中设置了一组用于实现系统功能的子程序称为系统调用。系统调用和普通库函数调用非常相似但有以下关键区别特性系统调用普通库函数提供者操作系统核心函数库或用户自己运行态内核态用户态权限高低一般的进程是不能访问内核的。它不能访问内核所占内存空间也不能调用内核函数。CPU硬件决定了这些这就是为什么它被称作保护模式。为了和用户空间上运行的进程进行交互内核提供了一组接口。透过该接口应用程序可以访问硬件设备和其他操作系统资源。这组接口在应用程序和内核之间扮演了使者的角色应用程序发送各种请求而内核负责满足这些请求(或者让应用程序暂时搁置)。系统调用在用户空间进程和硬件设备之间添加了一个中间层。该层主要作用有三个硬件抽象为用户空间提供了一种统一的硬件的抽象接口。比如当需要读些文件的时候应用程序就可以不去管磁盘类型和介质甚至不用去管文件所在的文件系统到底是哪种类型。系统安全保证了系统的稳定和安全。作为硬件设备和应用程序之间的中间人内核可以基于权限和其他一些规则对需要进行的访问进行裁决。虚拟化支持每个进程都运行在虚拟系统中而在用户空间和系统的其余部分提供这样一层公共接口也是出于这种考虑。3. API、POSIX与C库的关系一般情况下应用程序通过应用编程接口(API)而不是直接通过系统调用来编程。这点很重要因为应用程序使用的这种编程接口实际上并不需要和内核提供的系统调用一一对应。一个API定义了一组应用程序使用的编程接口。它们可以实现成一个系统调用也可以通过调用多个系统调用来实现而完全不使用任何系统调用也不存在问题。在Unix世界中最流行的应用编程接口是基于POSIX标准的其目标是提供一套大体上基于Unix的可移植操作系统标准。POSIX是说明API和系统调用之间关系的一个极好例子。Linux的系统调用像大多数Unix系统一样作为C库的一部分提供。C库实现了Unix系统的主要API包括标准C库函数和系统调用。所有的C程序都可以使用C库而由于C语言本身的特点其他语言也可以很方便地把它们封装起来使用。从程序员的角度看系统调用无关紧要他们只需要跟API打交道就可以了。相反内核只跟系统调用打交道库函数及应用程序是怎么使用系统调用不是内核所关心的。4. 系统调用的实现原理4.1 基本机制Linux下的系统调用是通过0x80中断实现的。但操作系统会有多个系统调用Linux下有319个系统调用而对于同一个中断号是如何处理多个不同的系统调用的最简单的方式是对于不同的系统调用采用不同的中断号但是中断号明显是一种稀缺资源Linux显然不会这么做。系统调用的实现需要解决两个核心问题系统调用的函数名称转换系统调用的参数传递实际上Linux中每个系统调用都有相应的系统调用号作为唯一的标识内核维护一张系统调用表sys_call_table表中的元素是系统调用函数的起始地址而系统调用号就是系统调用在调用表的偏移量。在x86上系统调用号是通过eax寄存器传递给内核的。比如fork的实现用户空间的程序无法直接执行内核代码。它们不能直接调用内核空间中的函数因为内核驻留在受保护的地址空间上。如果进程可以直接在内核的地址空间上读写的话系统安全就会失去控制。所以应用程序应该以某种方式通知系统告诉内核自己需要执行一个系统调用希望系统切换到内核态这样内核就可以代表应用程序来执行该系统调用了。4.2 系统调用表与系统调用号在Linux中每个系统调用被赋予一个系统调用号。这样通过这个独一无二的号就可以关联系统调用。当用户空间的进程执行一个系统调用的时候这个系统调用号就被用来指明到底是要执行哪个系统调用。系统调用号相当关键一旦分配就不能再有任何变更否则编译好的应用程序就会崩溃。Linux有一个未实现系统调用sys_ni_syscall()它除了返回-ENOSYS外不做任何其他工作这个错误号就是专门针对无效的系统调用而设的。内核记录了系统调用表中的所有已注册过的系统调用的列表存储在sys_call_table中。它与体系结构有关一般在entry.s中定义。这个表中为每一个有效的系统调用指定了惟一的系统调用号。system_call()函数通过将给定的系统调用号与NR_syscalls做比较来检查其有效性。如果它大于或者等于NR_syscalls该函数就返回-ENOSYS。否则就执行相应的系统调用。4.3 参数传递与验证除了系统调用号以外大部分系统调用都还需要一些外部的参数输入。所以在发生异常的时候应该把这些参数从用户空间传给内核。最简单的办法就是像传递系统调用号一样把这些参数也存放在寄存器里。在x86系统上ebx, ecx, edx, esi和edi按照顺序存放前五个参数需要六个或六个以上参数时用一个单独的寄存器存放指向所有这些参数在用户空间地址的指针返回值通过eax寄存器传递系统调用必须仔细检查它们所有的参数是否合法有效。最重要的一种检查就是检查用户提供的指针是否有效。内核提供了两个方法来完成必须的检查和内核空间与用户空间之间数据的来回拷贝copy_to_user()向用户空间写入数据copy_from_user()从用户空间读取数据注意内核无论何时都不能轻率地接受来自用户空间的指针这两个方法中必须有一个被调用。5. 系统调用的执行流程5.1 系统调用上下文内核在执行系统调用的时候处于进程上下文。current指针指向当前任务即引发系统调用的那个进程。在进程上下文中内核可以休眠内核可以被抢占这两点都很重要。能够休眠说明系统调用可以使用内核提供的绝大部分功能。在进程上下文中能够被抢占其实表明像用户空间内的进程一样当前的进程同样可以被其他进程抢占。5.2 系统调用示例read()让我们以read()系统调用为例看看具体的执行流程操作系统使用系统调用表将系统调用编号翻译为特定的系统调用read()系统调用编号是3所以sys_read()位于系统调用表的第四个条目中从地址sys_call_table (3 * word_size)读取数据得到sys_read()的地址控制权转交给sys_read()函数sys_read()会找到关联到fd编号的文件结构体调用与文件相关的read()函数来真正从文件中读取数据并返回从sys_read()返回将控制权切换给ret_from_sys恢复用户进程的状态并将控制权交还给用户程序6. 添加自定义系统调用6.1 通过修改内核源代码添加系统调用要在Linux内核中添加一个新的系统调用需要以下步骤编写系统调用响应函数asmlinkage int sys_mycall(int number) { printk(这是我添加的第一个系统调用); return number; }在系统调用表中添加对应项在arch/x86/syscalls/syscall_32.tbl或syscall_64.tbl中找到未使用的系统调用号添加新条目如223 i386 mycall sys_mycall重新编译内核6.2 利用内核模块添加系统调用也可以通过内核模块的方式动态添加系统调用编写内核模块代码#include linux/kernel.h #include linux/module.h #define __NR_testsyscall 191 extern void *sys_call_table[]; asmlinkage int testsyscall() { printk(hello world\n); return 0; } int init_module() { sys_call_table[__NR_testsyscall] testsyscall; printk(system call testsyscall() loaded success\n); return 0; } void cleanup_module() { /* 恢复原来的系统调用表 */ }编译并加载模块6.3 使用新的系统调用在用户空间程序中可以通过以下方式使用新的系统调用#define __NR_testsyscall 191 _syscall0(int, testsyscall) int main() { testsyscall(); return 0; }7. 常见系统调用分类Linux系统调用可以分为以下几大类进程控制fork() - 创建新进程exit() - 终止进程wait() - 等待子进程终止文件系统控制open() - 打开文件close() - 关闭文件read() - 读取文件write() - 写入文件系统控制ioctl() - 设备控制reboot() - 重新启动系统sysinfo() - 获取系统信息内存管理brk() - 改变数据段空间分配mmap() - 映射内存munmap() - 取消内存映射网络管理socket() - 创建套接字bind() - 绑定套接字connect() - 连接套接字进程间通信pipe() - 创建管道shmget() - 获取共享内存msgget() - 获取消息队列在实际开发中理解系统调用的工作原理和实现机制对于深入理解Linux操作系统至关重要。通过分析系统调用的执行流程我们可以更好地理解用户空间和内核空间之间的交互方式以及操作系统如何管理和保护系统资源。