Linux内核设计与实现---系统调用

系统调用

  • 1 API、POSIX和C库
  • 2 系统调用
    • 系统调用号
  • 3 系统调用处理程序
    • 指定恰当的系统调用
    • 参数传递
  • 4 系统调用的实现
      • 参数验证
  • 5 系统调用上下文
    • 绑定一个系统调用的最后步骤
    • 从用户空间访问系统调用
    • 为什么不通过系统调用的方式实现

1 API、POSIX和C库

API:应用编程接口。一个API定义了一组应用程序使用的编程接口。它们可以实现成一个系统调用,也可以通过调用多个系统调用来实现,而完全不使用任何系统调用也不存在问题。在Unix世界中,最流行的应用编程接口是基于POSIX标准的。Linux是与POSIX兼容的。

Linux的系统调用像大多数Unix一样,作为C库的一部分。C库实现了Unix系统的主要API,包括标准C库函数和系统调用。

Unix的系统调用抽象出了完成某种确定目的的函数。至于这些函数怎么用完全不需要内核去关心,提供机制(需要提供什么功能)而不是策略(怎样实现这些功能)。

2 系统调用

系统调用通常通过函数进行调用,在Linux中常称作syscalls。系统调用还会通过一个long类型的返回值来表示成功或者失败,使用long类型是为了与64位的硬件体系结构保持兼容。

函数声明中如果用asmlinkage限定词,表示通知编译器仅从栈中提取函数的参数,所有的系统调用都需要这个限定词。Linux所有的系统调用都应该遵守的命名规则:系统调用getpid()在内核中被定义为sys_getpid(),要在系统调用加上sys_。

系统调用号

Linux中,每个系统调用都被赋予一个系统调用号。这样,通过这个独一无二的号就可以关联系统调用。当用户空间的进程执行一个系统调用的时候,这个系统调用号就被用来指明到底是要执行那个系统调用,进程不会提及系统调用的名称。

系统调用号一旦分配就不能再有任何变更,否则编译好的应用程序就会崩溃。此外,如果一个系统调用被删除,它所占的系统调用号也不允许被回收利用,否则,以前编译过的代码再调用这个系统调用,实际上调用的就是另一个系统调用了。Linux有一个系统调用sys_ni_syscall(),它除了返回ENOSYS外不做任何其他的工作,这个错误号就是专门针对无效的系统调用而设的。

内核记录了系统调用表中的所有已被注册过的系统调用的列表,存储在sys_call_table中。它与结构体系有关,需要将系统调用分别注册到每个需要支持的体系结构去,一般定义在entry.s中。2.6.10版本的位置在arch/结构体系/kernel/entry.S。这个表中为每一个有效的系统调用指定了唯一的系统调用号。
m32r体系结构:
在这里插入图片描述

3 系统调用处理程序

用户空间的程序无法直接执行内核代码,他们不能直接调用内核空间中的函数,所以,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,通知内核的机制是靠软中断实现的:通过一个异常来促使系统切换到内核态去执行异常处理程序。此时的异常处理程序就是系统调用程序。x86系统上的软中断是int 0x80指令产生的。这条指令会触发一个异常导致系统切换到内核态并执行第128号异常处理程序,而该程序正是系统调用处理程序,叫system_call()。x86处理器增加了一条叫做sysenter的指令。与int指令相比,这条指令提供了更快、更专业的陷入内核执行系统调用的方式。

指定恰当的系统调用

因为所有的系统调用陷入内核方式都一样,因此必须把系统调用号一并传给内核,告诉内核去执行什么系统调用。在x86上,系统调用号是通过eax寄存器传递给内核的。在陷入内核之前,用户空间就把相应系统调用所对应的号放入eax中。这样系统调用处理程序一旦运行,就可以从eax中得到数据。

system_call()函数通过将给定的系统调用号与NR_syscalls做比较来检查其有效性。如果大于等于NR_syscalls,该函数返回ENOSYS。否则,就执行相应的系统调用。

call *sys_call_table(,%eax,4)

由于系统调用表中的表项是32位(4字节)类型存放的,所以内核需要将给定的系统调用号乘以4,然后用所得的结构在该表中查询其位置。
在这里插入图片描述

参数传递

除了系统调用号外,大部分系统调用都还需要一些外部的参数输入。所以,在发送异常的时候,应该把这些参数从用户空间传给内核。最简单的办法就是像传递系统调用号一样:把这些参数存放到寄存器里。在x86系统上,ebx、ecx、edx、edi和esi按照顺序存放前五个参数。需要6个或6个以上参数的,应该用一个单独的寄存器存放指向所有这些参数在用户空间地址的指针。

用户空间返回值也通过寄存器传递,在x86系统上,它存放在eax寄存器中。

4 系统调用的实现

实现一个新的系统调用的第一步是决定它的用途。它要做什么?每个系统调用都应该有一个明确的用途。在Linux中不提倡采用多用途的系统调用(一个系统调用根据参数来选择完成不同的工作)。

新系统调用的参数、返回值和错误码又该是什么?系统调用的接口应该简洁,参数尽可能的少。

设计接口的时候要尽量为将来多做考虑。

当写一个系统调用的时候,要时刻注意可移植性和健壮性,不但要考虑当前,还要为将来做打算。

参数验证

系统调用必须仔细检查它们所有的参数是否合法有效。系统调用在内核空间执行,如果任由用户将不合法的输入传递给内核,那么系统的安全和稳定将面临巨大的考验。

内核提供了两个方法来完成必须的检查和内核空间与用户空间之间数据的来回拷贝。为了向用户空间写入数据,内核提供了copy_to_uesr(),它需要三个参数。第一个参数是进程空间中的目的内存地址。第二个是内核空间内的源地址。最后一个参数是需要拷贝的数据长度。为了从用户空间读取数据,内核提供了copy_from_user()。该函数把第二个参数指定的位置上的数据拷贝到第一个参数指定的位置上,拷贝的数据长度由第三个参数决定。

注意,内核无论何时都不能轻率地接受来自用户空间的指针!

5 系统调用上下文

在执行系统调用的时候处于进程上下文。current指针指向当前任务,即引发系统调用的那个进程。
在进程上下文中,内核可以休眠并且可以被抢占。当系统调用返回的时候,控制权仍然在system_call()中,它最终负责切换到用户空间并让用户进程继续执行下去。

绑定一个系统调用的最后步骤

当编写完一个系统调用后,把它注册成一个正式的系统调用是件琐碎的工作:

  • 首先,在系统调用表的最后加入一个表项。每种支持该系统调用的硬件体系都必须做这样的工作。从0开始算起,系统调用在该表中的位置就是它的系统调用号。如第10个系统调用分配到的系统调用号为9。
  • 对于所支持的各种体系结构,系统调用号都必须定义于include/asm/unistd.h中。
  • 系统调用必须被编译进内核映像(不能编译成模块)。这只要把它放进kernel/下的一个相关文件就行了。

我们通过一个虚构的系统调用foo()来仔细观察这些步骤。首先,我们把sys_foo加入到系统调用表中去。对于大多数体系结构中,该表位于entry.s中,形式如下:
请添加图片描述
我们把新的系统调用加到这个表的末尾:

.long sys_foo

虽然没有明确地指定编号,但我们加入这个系统调用被次序分配给了285这个系统调用号。对于每种需要支持的体系结构,我们都必须将自己的系统调用加入到其系统调用表中去。每种体系结构不需要对应相同的系统调用号。系统调用号是专属体系结构ABI(应用程序二进制结构)的部分。

接下来,我们把系统调用号加入到include/asm/unistd.h中,它的格式如下
请添加图片描述
然后,我们在该列表中加入下面这行:

#define _NR_foo 			285

同时#define NR_syscalls 285这个要改成#define NR_syscalls 286。因为系统调用处理程序system_call()函数通过将给定的系统调用号与NR_syscalls做比较来检查其有效性。如果大于等于NR_syscalls,该函数返回ENOSYS。否则,就执行相应的系统调用。

最后,我们来实现foo()系统调用。无论何种配置,该系统调用都必须编译到核心的内核映像中去,所以我们把它放在kernel/sys.c文件中。再次编译内核就可以了。

从用户空间访问系统调用

Linux提供了一组宏,用于直接对系统调用进行访问。它会设置好寄存器并调用陷入指令。这些宏是_syscalln(),其中n的范围是0到6,代表需要传递给系统调用的参数个数。举个例子,open系统调用的 定义为:

long open(const char *filename,int flags,int mode);

直接调用该系统调用的宏形式为:

_syscall3(long,open,const char *,filename,int flags,int mode)

这样程序就可直接使用open。对于每个宏,都有2+2*n个参数,第一个参数是返回值类型。第二个参数是系统调用的名称。

为什么不通过系统调用的方式实现

建立一个新的系统调用非常容易,但却绝不倡导。

建立一个新的系统调用的好处:

  • 系统调用创建容易且使用方便
  • Linux系统调用的高性能显而易见

问题是:

  • 你需要一个系统调用号,而这需要在一个内核处于开发版本的时候由官方分配给你
  • 系统调用被加入稳定内核后就被固化了
  • 需要将系统调用分别注册的每个需要的体系结构去
  • 在脚本中不容易调用系统调用,也不能从文件系统直接访问系统调用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/379671.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

手动去设置HTTP响应行、响应头、响应体

①手动去设置HTTP响应行中的状态码,这里用到了response的setStatus(int sc);这个方法 package com.itheima.line;import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpSer…

基本的二分查找、寻找第一个和最后一个数的二分查找

二分查找1 二分查找的框架2 寻找一个数(基本的二分搜索)3 寻找左侧边界的二分搜索4 寻找右侧边界的二分查找5 合并二分查找场景:有序数组寻找一个数、寻找左侧边界(有序数组第一个等目标数的下标)、寻找右侧边界&#…

Linux内核设计与实现---中断和中断处理程序

中断和中断处理程序1 中断异常2 中断处理程序上半部与下半部的对比3 注册中断处理程序释放中断处理程序4 编写中断处理程序重入和中断处理程序共享的中断处理程序中断处理程序实例5 中断上下文6 中断处理机制的实现7 中断控制禁止和激活中断禁止指定中断线中断系统的状态8 总结…

response细节点

一、 1)、response获得的流不需要手动关闭,Tomcat容器会帮你自动关闭 2)、getWriter和getOutputStream不能同时调用 //error package com.itheima.content;import java.io.IOException; import javax.servlet.ServletException; import java…

linux内核设计与实现---下半部和推后执行的工作

下半部和推后执行的工作1 下半部为什么要用下半部下半部的环境内核定时器2 软中断软中断的实现软中断处理程序执行软中断使用软中断3 tasklettasklet的实现使用taskletksoftirqd4 工作队列工作队列的实现工作、工作队列和工作者线程之间的关系使用工作队列5 下半部机制的选择6 …

Mac VSCode配置C语言环境(可以调试)

Mac VSCode配置C语言环境c_cpp_properties.jsontasks.jsonlaunch.json新建一个文件夹&#xff0c;用vscode&#xff0c;然后再新建一个test.c文件。 #include <stdio.h>int main(void) {int a1,b1;int cab;printf("%d\n",c);return 0; }这篇文章说怎么配置c_c…

vShpere Client在win 7 RC下和2008下 无法正常连接esx主机之解决办法

vShpere Client在win 7 RC下和2008下 无法正常连接esx主机之解决办法 在win7下和2008下打开client后连接esx主机会出现2个错误提示, 第一个是 第二个是 然后就连接失败了,开始以为是CC的esx主机安装有问题,后来找了找,借助了强大google工具,终于找到解决办法.解决办法如下: 1.从…

localhost与127.0.0.1之间的关系更改

其实localhost的默认IP地址为127.0.0.1&#xff0c;因为这是一种映射关系。 更改步骤如下&#xff1a; C:\Windows\System32\drivers\etc 下的hosts 打开hosts可以看到 更改即可

Linux内核设计与实现---内核同步方法

内核同步方法1 原子操作原子整数操作原子性与顺序性的比较原子位操作2 自旋锁自旋锁是不可递归的其他针对自旋锁的操作自旋锁和下半部3 读-写自旋锁4 信号量创建和初始化信号量使用信号量5 读-写信号量6 自旋锁和信号量7 完成变量8 互斥锁互斥锁API9 禁止抢占10 顺序和屏障1 原…

UNIX环境高级编程---进程间通信总结

进程间通信1 管道匿名管道命名管道2 消息队列3 信号量POSIX信号量有名信号量无名信号量有名信号量和无名信号量的公共操作4 共享内存5 信号相关函数6 套接字针对 TCP 协议通信的 socket 编程模型针对 UDP 协议通信的 socket 编程模型针对本地进程间通信的 socket 编程模型总结L…

搜索---广度优先遍历、深度优先遍历、回溯法

参考文章&#xff1a;https://github.com/CyC2018/CS-Notes/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3%20-%20%E6%90%9C%E7%B4%A2.md 广度优先搜索&#xff08;BFS&#xff09; 广度优先搜索是按层来处理顶点的&#xff0c;距离开始点最近的那些顶点首先被访问&#…

如何更改Visual Studio 2008中类文件引用的默认名称空间?

在编写程序的时候&#xff0c;如果某些名称空间经常用到&#xff0c;每次创建一个文件的时候&#xff0c;都需要手工添加名称空间&#xff0c;是不是很烦人呢&#xff1f;多说人会回答&#xff1a;是的。如果新建文件的时候就自动加上自己需要的名称空间该多好啊。&#xff1a;…

Linux内核设计与实现---内存管理

内存管理1 页2 区3 获得页获得填充为0的页释放页4 kmalloc()gfp_mask标志kfree()5 vmalloc()6 slab层slab层的设计7 slab分配器的接口8 在栈上的静态分配9 高端内核的映射永久映射临时映射10 每个CPU的分配11 新的每个CPU的接口编译时的每个CPU数据运行时每个CPU数据12 使用每个…

多语言开发 之 通过基页类及Session 动态响应用户对语言的选择

在用户通过UserLogin.aspx登录系统时 提供其对语言的选择选择后 将所选存入Session 以便登录系统后的其他页面进行按语言显示当然相关页面需要支持多语言具体信息可参看使用 根据语言环境不同 而显示不同的 资源本地化 ASP.NET 网页 App_Code下定义基页类 BasePage.cs Codeusin…

Linux内核设计与实现---虚拟文件系统

虚拟文件系统1 通用文件系统2 文件系统抽象层3 Unix文件系统4 VFS对象及其数据结构其他VFS对象5 超级快对象超级块操作6 索引节点对象索引节点操作7 目录项对象目录项状态目录项缓存目录项操作8 文件对象9 和文件系统相关的数据结构10 和进程相关的数据结构11 Linux中的文件系统…

Java里面的几种路径的区别

1&#xff0c;相对路径 相对路径就是指由这个文件所在的路径引起的跟其它文件&#xff08;或文件夹&#xff09;的路径关系。 也就是说&#xff1a; 对于如图所示&#xff1a;一news.html为例 在WEB15工程下的WebContent下的WEB-INF下的news.html 当我访问的news.html的时候…

Linux内核设计与实现---块I/O层

块I/O层1 解刨一个块设备2 缓冲区和缓冲区头3 bio结构体新老方法对比4 请求队列5 I/O调度程序I/O调度程序的工作Linus电梯最终期限I/O调度程序预测I/O调度程序完全公正的排队I/O调度程序空操作的I/O调度程序I/O调度程序的选择系统中能够 随机访问 固定大小数据片的设备被称为块…

算法---数

数1 最大公约数2 最小公约数3 进制转换4 阶乘统计阶乘尾部0的个数5 字符串加法减法二进制加法6 多数投票问题数组中出现次数多于n/2的元素7 相遇问题改变数组元素使所有元素都相同1 最大公约数 欧几里得算法&#xff1a;两个整数的最大公约数等于其中较小的那个数和两数相除余…

Linux内核设计与实现---进程地址空间

进程地址空间1 内存描述符分配内存描述符销毁内存描述符mm_struct与内核线程2 内存区域VMA标志VMA操作内存区域的树形结构和内存区域的链表结构3 操作内存区域find_vma()find_vma_prev()find_vma_intersection()4 mmap()和do_mmap()&#xff1a;创建地址空间mmap&#xff08;&a…

JavaScript中带有示例的Math.log10()方法

JavaScript | Math.log10()方法 (JavaScript | Math.log10() Method) Math operations in JavaScript are handled using functions of math library in JavaScript. In this tutorial on Math.log10() method, we will learn about the log10() method and its working with e…