4.系统调用

系统调用

内核提供了用户进程和内核进行交互的一组接口,称这些接口为系统调用。通过接口的方式访问系统,保证系统的稳定可靠,避免应用程序恣意妄行。

1.与内核通信

系统调用在用户空间进程和硬件设备间添加了一个中间层。其主要有三个功能:

  • 为用户空间提供一个硬件的抽象接口,不需要管底层细节
  • 保护系统的稳定和安全,对访问进行判决,用户程序危害系统
  • 每个进程运行在各自的虚拟系统,内核需要了解进程信息才能实现多任务和虚拟内存(从系统管理的角度出发)

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

2.系统调用

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

下面给出实例,getpid()系统调用,返回进程的PID,内核中实现如下:

SYSCALL_DEFINE0(getpid)
{
    return task_tpid_vnr(current);  // returns current->tgid
}

其中SYSCALL_DEFINE0只是一个宏,它定义一个无参数的系统调用(因此数字为0),展开代码如下:

asmlinkage long sys_getpid(void)

注意函数声明中的asmlinkage限定词是一个编译指令,通知编译器仅从栈中提取该函数的参数,所有的系统调用都需要加该限定词

系统调用号

在Linux中,每个系统调用被赋予一个系统调用号,通过这个独一无二的号就可以关联系统调用,指明到底执行哪个系统调用。

系统调用号非常重要,一旦分配,就不能修改,如果一个系统调用删除,它所占用的系统调用号也不允许被回收利用(向以前版本兼容的目的),同时还有一个“未实现”的系统调用sys_ni_syscall(),除了返回-ENOSYS不做其他任何工作,专门用于填补失效的系统调用。所有的系统调用号对应的系统调用存储在sys_call_table中。

3.系统调用处理程序

用户空间的程序无法直接执行内核代码,它们不能直接调用内核空间中的函数,因为内核驻留在受保护的地址空间上。如果进程直接在内核地址空间上读写,系统的安全性和稳定性将不复存在。

通知内核的机制是靠软中断实现的:通过引发一个异常促使系统切换到内核态去执行异常处理程序,这个异常处理程序就是系统调用处理程序,这个处理程序名字叫system_call()x86处理器常用方法:

  • INT128:通过init $0x80指令除非中断,这个指令会除非一个异常导致系统切换到内核态执行第128号异常处理程序,该程序就是系统调用程序
  • sysenter指令:比int中断指令更快更专业陷入内核执行系统调用

指定系统调用

上面讲了如何执行系统调用,但是没有指定系统调用号。对于x86,系统调用号通过eax寄存器传递给内核。在陷入内核之前,用户空间就把相应系统调用所对应的号存入eax寄存器中。

参数传递

除了系统调用号,大部分系统调用还需要传递外部参数,方法和调用号传递类似,也是存放在寄存器中,给用户返回值同样通过寄存器返回,在x86上,仍然保持在eax寄存器中。

系统调用流程

参数验证

因为系统调用运行于内核空间,因此需要对用户进程传入的参数进行验证,最重要的检查就是检查用户提供的指针是否有效,内核必须保证:

  • 指针指向的内存区域属于用户空间。(用户不能哄骗内核去读取内核空间数据)
  • 指针指向的内存区域属于该进程的地址空间。(进程不能哄骗内核去读其他进程的数据)
  • 如果是读,内存应标记为可读;如果是写,内存应该标记为写;如果是可执行,则内存标记为可执行。(进程决不能绕开内存访问限制)

为了向用户空间写入数据,内核提供了copy_to_user(),输入参数有三个:srcdst字节数;为了从用户空间读取数据,内核提供了copy_from_user(),参数和copy_to_user()类似。

4.系统调用上下文

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

在进程上下文中,内核可以休眠并且可以被抢占。当系统调用返回的时候,控制权仍然在system_call()中,它最终负责切换到用户空间,并让用户进程继续执行下去。

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

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

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

问题:

  • 需要一个系统调用号,需要内核处于开发版本阶段由官方分配
  • 系统调用加入稳定内核后就被固化了,接口不允许做改动
  • 需要将系统调用分别注册到每个需要支持的体系结构中
  • 在脚本中不容易调用系统调用,也不能从文件系统直接访问系统调用
  • 由于需要系统调用号,因此在主内核树之外很难维护和使用系统调用
  • 若仅仅进行简单的信息交换,系统调用就大材小用了

小结

对于系统调用,我们需要明白执行调用的整个过程:触发异常或中断陷入内核空间,传递系统调用号和参数,执行正确的系统调用函数,再把返回值带回用户空间。其中传递系统调用号和参数,以及返回值均是通过读写寄存器。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 安大大 + 原创作品转载请注明出处 + 《Linux操作系统分析》MOOC课程 用户态、内核态和中断处理过程 程序...
    夏天的篮球阅读 5,734评论 0 0
  • 一、温故而知新 1. 内存不够怎么办 内存简单分配策略的问题地址空间不隔离内存使用效率低程序运行的地址不确定 关于...
    SeanCST阅读 12,385评论 0 27
  • 又来到了一个老生常谈的问题,应用层软件开发的程序员要不要了解和深入学习操作系统呢? 今天就这个问题开始,来谈谈操...
    tangsl阅读 9,631评论 0 23
  • 今天这篇文章是写关于青春的世界里,男生女生心中关于喜欢的秘密。青春时代,那种喜欢就像开在夜间的昙花一般,美好又神秘...
    七小琪儿阅读 2,854评论 0 2
  • Linux下安装MySQL 环境:1、操作系统:CentOS release 6.8 (Final)2、安装版本:...
    剑御阅读 4,282评论 0 2