我是靠谱客的博主 年轻黑裤,最近开发中收集的这篇文章主要介绍Linux内核设计与实现(三)| 系统调用系统调用,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

文章目录

  • 系统调用
    • 1.与内核通信
    • 2.API、POSIX和C库
    • 3.系统调用(syscall)
      • 3.1 系统调用号
      • 3.2 系统调用的性能
    • 4. 系统调用处理程序
      • 4.1 指定恰当的系统调用
      • 4.2 参数传递
    • 5.系统调用的实现
      • 5.1 实现系统调用
      • 5.2 参数验证
    • 6.系统调用上下文
      • 6.1 绑定系统调用的最后步骤
      • 6.2 从用户空间访问系统调用
      • 6.3 为什么不通过系统调用的方式实现

系统调用

1.与内核通信

  • 概述

系统调用在硬件设备和用户空间进程之间提供了一个中间层,主要就是忽略连接细节,保护系统的稳定和内核随时得知那些应用进程在访问那些硬件从而宏观调控多任务的执行等

在Linux中系统调用是用户空间访问内核的唯一手段,除了异常和陷入外,即是内核唯一的合法入口

2.API、POSIX和C库

  • 概述

一般情况下用户空间程序是通过用户空间实现的应用编程接口(API)去编程,以此间接的进行系统调用,而不是直接通过系统调用来编程;在Unix中最流行的应用编程接口是基于POSIX标准的

  • 以调用printf函数为例,看一下应用程序、C库和内核之间的关系

在这里插入图片描述

  • Linux和Unix的调用对比

Linux的系统调用像大多数Unix系统一样,作为C库的一部分提供。C库实现了Unix系统的主要API,包括标准C库函数和系统调用接口。所有的C程序都可以使用C库,而由于C语言本身的特点,其他语言也可以很方便地把它们封装起来使用。此外,C库提供了POSIX的绝大部分API。

3.系统调用(syscall)

  • 概述

上面说到Linux中大多通过调用C库定义的函数来访问系统调用,它们通常需要定义0、1或多个参数还有产生一些副作用(比如有的函数就是返回一个你需要的值,不会使系统发生某种改变,而有些函数则会改变系统状态);一般会通过一个long的返回值来代表成功与否(负值一般表示错误)

以getpid()为例我们看看系统最终如何明确操作

SYSCALL_DEFINE0只是一个宏,定义一个无参数的系统调用,我们要定义一个系统调用就需要函数声明asmlinkage限定词,返回值在内核态为long,在y用户空间为int

SYSCALL_DEFINE0(getpid){
	return task_tgid_vnr (current); // returns current->tgid
}
//SYSCALL_DEFINE0的展开形式
asmlinkage long sys__getpid(void)

3.1 系统调用号

  • 概述

在Linux中每个有效的系统调用被赋予一个唯一的系统调用号,做到一对一的对应,通过号码来指明执行那个系统调用,分配后不会更改,系统调用被删除同样也不会对号码进行回收;如果一个系统调用被删除或停用会被sys_ni_syscall()函数来填补空缺

内核记录了系统调用表已经注册过得系统调用列表,这个表为每一个有效的系统调用指定了唯一的系统调用号。

3.2 系统调用的性能

  • 概述

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

4. 系统调用处理程序

  • 概述

前面我们一直说过用户空间无法直接执行内核代码,无法调用内核的函数,因为内核在受保护的地址,所以,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序在内核空间执行系统调用。

如何通知内核

我们通过软中断来,就是引发一个异常促使系统切换到内核态去执行异常处理程序,异常处理即为系统调用程序了;在X86系统中我们称为system_call()对应异常处理程序第128号,切记用户空间引起异常或陷入内核是以很重要的概念

4.1 指定恰当的系统调用

  • 概述

系统调用陷入内核的方式大庭相径,但是我们需要区分是什么系统调用,那么就会把系统调用号一起传入,在X86中该号会存放在一个eax寄存器并由其传入内核,通过对比一个参数如果大于等于则会返回错误,以此来进行校验

4.2 参数传递

  • 概述

除了系统调用号,外部还会传入一些参数,传递这些参数的方式跟传递系统调用号一样,存入寄存器,存放这些参数在用户空间地址的指针
在这里插入图片描述

返回值

也同样经由寄存器进行传递

5.系统调用的实现

  • 概述

这节我们主要关注如何实现一个系统调用所需的步骤,添加之类的相对容易

5.1 实现系统调用

  • 确定该系统调用的用途,不提倡通过重载的方式进行函数作用的区分
  • 确定系统调用的参数、返回值和错误码
  • 不能破坏向后兼容
  • 不允许单个系统调用具有多个不同的行为
  • 设计接口时,是否做了不必要的限制,限制其通用性
  • 移植性和健壮性是否可以

5.2 参数验证

  • 仔细检查所有传入参数是否合法且有效(举例来说,与文件IO相关的系统调用必须检查文件描述符是否有效。与进程相关的函数必须检查提供的PID是否有效。必须检查每个参数,保证它们不但合法有效,而且正确。进程不应当让内核去访问那些它无权访问的资源。)
  • 对于有效性,我们必须检查用户提供的指针是否有效,与提供者的访问级别是否对应那么内核在接收用户的指针之前必须保证
  • 指针指向的内存区域属于用户空间。进程决不能哄骗内核去读内核空间的数据。
  • 指针指向的内存区域在进程的地址空间里。进程决不能哄骗内核去读其他进程的数据。
  • 如果是读,该内存应被标记为可读;如果是写,该内存应被标记为可写;如果是可执行,该内存被标记为可执行。进程决不能绕过内存访问限制。
  • 内核提供两个方法来检查内核空间和用户空间之间数据的来回拷贝,两者都会引起阻塞,因为需要的数据不在物理内存就要进行缺页处理啊
  • 向用户空间写入数据,提供了copy_to_user(进程空间的目的内存地址、内核空间的源地址、需要拷贝的数据长度)
  • 从用户空间读取数据,内核提供了copy_from_user(内核空间的目的内存地址、用户空间的源地址、需要拷贝的数据长度)

两个方法的伪代码

sYsCALL_DEFINE3(silly_copy,
				unsigned long *, src,
				unsigned long * , dst,
				unsigned long len)
{
	unsigned long buf;
	/*将用户地址空间中的src拷贝进buf*/
	if (copy_from_user ( &buf, src,len))
		return -EFAULT;
	/*将buf拷贝进用户地址空间中的dst*/
	if (copy_to_user(dst,&buf,len))
		return -EFAULT;
	/*返回考贝的数据量*/
	return len;
}
  • 检查是否有合法权限,检查特点资源的特殊权限即调用者调用capable()函数来检查自己是否有权力对指定的资源进行操作,举个例子,capable(CAP_ SYS_NICE)可以检查调用者是否有权改变其他进程的nice值。

6.系统调用上下文

  • 概述

我们前面说过,内核在执行系统调用的时候处于进程上下文,current指向当前进程,在用户进程的眼里就在这里停止了,这个用户进程就是引起系统调用的进程。当系统调用返回的时候,控制权仍然在system_call()中,它最终会负责切换到用户空间,并让用户进程继续执行下去。

注意

在进程上下文中,内核可以休眠(比如在系统调用阻塞或显式调用schedule()的时候)并且可以被抢占。这两点都很重要。

  • 首先,能够休眠说明系统调用可以使用内核提供的绝大部分功能
  • 在进程上下文能够被抢占其实表明,像用户空间内的进程一样,当前的进程同样可以被其他进程抢占。因为新的进程可以使用相同的系统调用,所以必须小心,保证该系统调用是可重入的。

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

  • 概述

这里我们就简单描述一下,一个系统调用写完后如何注册成可被调用的

  1. 在系统调用表创建表项,一般这个表是由0开始的,分配后的位置-1即为系统调用号
  2. 被编译进内核映像,放入相关文件

6.2 从用户空间访问系统调用

  • 概述

通常,系统调用靠C库支持。用户程序通过包含标准头文件并和C库链接,就可以使用系统调用(或者调用库函数,再由库函数实际调用)。在Linux中本身会提供一组宏,用于直接对系统调用进行访问,会设置寄存器并调用陷入指令,参数可以传递0~6个,对于参数必须识别,这样我们才能知道多少参数按照什么次序压入寄存器

例子

对于_syscall3前两个参数就是对应系统调用的返回值类型,第二个参数就是系统调用的名称

//open系统调用定义为:
long open (const char *filename,int flags, int mode)
//而不靠库支持,直接调用此系统调用的宏的形式为:可看到数字3即为3个参数
#define NR_open5
_syscall3(long,open,const char*,filename,int, flags,int,mode)

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

  • 概述

我们不提倡新建一个系统调用,Linux会尽量避免每出现一种新的抽象就简单的加入一个新的系统调用,因为有更多平替的方式来替换这种大材小用的方式

平替方式

  • 实现一个设备节点,并对此实现read()和 write()。使用ioctI()对特定的设置进行操作或者对特定的信息进行检索。
  • 像信号量这样的某些接口,可以用文件描述符来表示,因此也就可以按上述方式对其进行操作。
  • 把增加的信息作为一个文件放在sysfs的合适位置。

最后

以上就是年轻黑裤为你收集整理的Linux内核设计与实现(三)| 系统调用系统调用的全部内容,希望文章能够帮你解决Linux内核设计与实现(三)| 系统调用系统调用所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(48)

评论列表共有 0 条评论

立即
投稿
返回
顶部