【读书笔记】Linux内核设计与实现--系统调用

系统调用的接口实现主要是为了保证系统稳定可靠,避免应用程序恣意妄行。

1.与内核通信

系统调用在用户空间进程和硬件设备之间添加了一个中间层。
作用如下:

  1. 为用户空间提供了一种硬件的抽象接口;
  2. 保证了系统的稳定和安全;
  3. 每个进程都运行在虚拟系统中,而在用户空间和系统的其余部分提供这样一层公共接口。

ps:在Linux系统中,系统调用是用户空间访问内核的唯一手段;除异常和陷入外,它们是内核唯一的合法入口。

2.API、POSIX和C库

一般情况下,应用程序通过在用户空间实现的应用编程接口(API)而不是直接通过系统调用来编程。
ps:应用程序使用这种编程接口实际上并不需要和内核提供的系统调用对应。

如图加深理解:
在这里插入图片描述
关于unix的接口设计有一句格言“提供机制(需要提供什么功能)而不是策略(怎样实现这些功能)”。即Unix的系统调用抽象出用于完成某种确定的目的的函数,至于这些函数怎么用完全不需要内核去关心。

3.系统调用

要访问系统调用(在Linux中常称作syscall),通常通过C库中定义的函数调用来进行。
系统调用在出现错误的时候C库会把错误码写入errno全局变量。通过调用oerror()库函数可以把该变量翻译成用户可以理解的错误字符串。

Q:如何定义系统调用?
A:为了保证32位和64位系统的兼容,系统调用在用户空间和内核空间有不同的返回值类型,在用户空间为int,在内核空间为long。函数名也以sys_为前缀,如:系统调用getpid()在内核中被定义为sys_getpid()。

3.1 系统调用号–对应系统调用函数

在Linux中,每个系统调用被赋予一个系统调用号(独一无二的)。
当用户空间的进程执行一个系统调用的时候,这个系统调用号就用来指明到底是要执行哪个系统调用,进程不会提及系统调用的名称(因此系统调用号很重要)。

Linux有一个“未实现”系统调用sys_ni_syscall(),它除了返回-ENOSYS外不做任何其他工作。

内核记录了系统调用表中的所有已注册过的系统调用的列表,存储在sys_call_table中。这个表为每一个有效的系统调用指定了唯一的系统调用号。

3.2 系统调用的性能

Linux系统调用比其他许多操作系统执行得要快。
why?
A:Linux很短的上下文切换时间是一个重要的原因,进出内核都被优化得简洁高效;
其次是系统调用处理程序和每个系统调用本身也都非常简洁。

4.系统调用处理程序

用户空间的程序无法直接执行内核代码。
应用程序应该以某种方式通知系统,让内核代表应用程序在内核空间执行系统调用。
通知内核的机制是靠软中断实现的:通过引发一个异常来促使系统切换到内核态去执行异常处理程序–系统调用处理程序。

4.1 指定恰当的系统调用

因为所有系统调用陷入内核的方式都一样,所以仅仅是陷入内核空间是不够的。必须把系统调用号一并传给内核。
在这里插入图片描述

4.2 参数传递

除了系统调用号以外,大部分系统调用都还需要一些外部的参数输入。所以,在发生陷入的时候,应该把这些参数从用户空间传给内核。最简单的办法就是像传递系统调用号一样,把这些参数也存放在寄存器里。

5.系统调用的实现

5.1 实现系统调用

实现一个新的系统调用的第一步是决定它的用途。每个系统调用都应该有一个明确的用途。在Linux中不提倡采用多用途的系统调用(一个系统调用通过传递不同的参数值来选择完成不同的工作)。

5.2 参数验证

系统调用必须仔细检查它们所有的参数是否合法有效。避免用户将不合法的输入传递给内核。
必须检查每个参数,保证它们不但合法有效,而且正确。进程不应当让内核去访问那些它无权访问的资源。
最重要的一种检查就是检查用户提供的指针是否有效。
在接收一个用户空间的指针之前,内核必须保证如下:

  1. 指针指向的内存区域属于用户空间。进程决不能哄骗内核去读内核空间的数据;
  2. 指针指向的内存区域在进程的地址空间里。进程决不能哄骗内核去读其他进程的数据;
  3. 如果是读,该内存应被标记为可读;如果是写,该内存应被标记为可写;如果是可执行,该内存被标记为可执行。进程决不能绕过内存访问限制。

内核提供了两个方法来完成必须的检查和内核空间与用户空间之间数据的来回拷贝。如下表所示:

方法功能说明返回值
copy_to_user()向用户空间写入数据第一个参数是进程空间中的目的内存地址,第二个是内核空间内的源地址,最后一个参数是需要拷贝的数据长度(字节数)执行失败:没能完成拷贝的数据的字节数; 成功:0; 当出现上述错误时候,系统调用返回标准-EFAULT
copy_from_user()从用户空间读取数据参数也是三个,和copy_to_user相似,第二个参数指定的位置上的数据拷贝到第一个参数指定的位置上,拷贝的数据长度由第三个参数决定同上

ps:copy_to_user()和copy_from_user()都有可能引起阻塞。当包含用户数据的页被换出到硬盘上而不是在物理内存上的时候,就会发生阻塞。此时,进程会休眠,直到缺页处理程序将该页从硬盘重新换回物理内存。

最后一项检查针对是否有合法权限
新版本的Linux内核中提供了一个更细粒度的“权能”机制,新的系统允许检查针对特定资源的特殊权限。调用者可以使用capable()函数来检查是否有权能对指定的资源进行操作,如果它返回非0值,调用者就有权进行操作,返回0则无权操作。
eg:

if(!capable(CAP_SYSY_BOOT))	/* 启动系统的系统管理员 */
	return -EPERM;

参考<linux/capability.h>包含一份所有权能和其对于的权限的列表。

6.系统调用上下文

内核在执行系统调用的时候处于进程上下文。 current指针指向当前任务,即引发系统调用的那个进程。

在进程上下文中内核可以休眠(比如在系统调用阻塞或显式调用schedule()的时候)并且可以被抢占

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

Q:当编写完一个系统调用后,如何把它注册成一个正式的系统调用?
A:

  1. 在系统调用表的最后加入一个表项(对于大多数体系结构来说,该表位于entry.s文件中)。每种支持该系统调用的硬件体系都必须做这样的工作。从0开始算起,系统调用在该表中的位置就是它的系统调用号;
  2. 对于所支持的各种体系结构,系统调用号都必须定义于<asm/unistd.h>中,文件中一般每隔5个表项就加入一个调用好注释的习惯,这可以在查找系统调用对应的调用号时提供方便;
  3. 系统调用必须被编译进内核映象(不能被编译成模块)。这只要把它放进kernel/下的一个相关文件中就可以了,比如sys.c,它包含了各种各样的系统调用。

6.2 从用户空间访问系统调用–C库/Linux宏

通常,系统调用靠C库支持。用户程序通过包含标准头文件并和C库链接,就可以使用系统调用(或者调用库函数,再由库函数实际调用)。
Linux本身提供了一组宏,用于直接对系统调用进行访问(不再需要引入C库的头文件),它会设置好寄存器并调用陷入指令。
这些宏是_syscalln(),其中n的范围从0到6,代表需要传递给系统调用的参数个数,这是因为该宏必须了解到有多少参数按照什么次序压入寄存器。
eg:
open()的系统调用定义是:

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

不靠C库支持,通过宏直接调用的形式为:

#define NR_open 5		/* <asm/unistd.h>中定义的系统调用号 */
_syscall3(long, open, const char*, filename, int, flags, int, mode)  

对于每个宏来说,都有2+2xn个参数。
第一个参数对应着系统调用的返回值类型。
第二个参数是系统调用的名称。
后面的就是按照系统调用参数的顺序排列的每个参数的类型和名称。

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

Linux系统尽量避免每出现一种新的抽象就简单的加入一个新的系统调用。
通常采用如下替换想要实现的系统调用:
实现一个设备节点,并对此实现read()和write()。使用ioctl对特定的设置进行操作或者对特定的信息进行检索。

  1. 像信号量这样的某些接口,可以用文件描述符来表示,因此也就可以按上述方式对其进行操作;
  2. 把增加的信息作为一个文件放在sysfs的合适位置。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wang 恒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值