系统调用的过程

来自APUE和TLPI读书笔记

一个系统调用是怎么发生的呢?典型步骤如下:

  • 调用标准库中的函数,例如C语言中,申请内存时,会使用如下代码:

    #include <stdlib.h>
    #include <string.h>
    #include <stdio.h>
    
    int main() {
    char *s = malloc(sizeof("hello"));
    printf("s -> %s\n", s);
    memcpy(s, "world", 5);
    printf("s -> %s\n", s);
    free(s);
    }
    

其中 malloc 就是一个标准库中的函数,它其实是一个系统调用的wrapper。

  • malloc 所包裹的系统调用函数(Linux中一般是 sbrk)不一定每一次都会被调用,因为通常来说,为了提高内存分配的性能, 标准库里的wrapper函数会对所申请的内存进行缓存。此外,内核所提供的系统调用,也会规定好以一种什么样的方式进行调用, 哪个参数该放哪个寄存器上等等。标准库里的函数负责把这些准备好。

  • 由于所有的系统调用都是以同一种方式进入内核,而每个系统调用都有一个编号,这个编号是内核用来分辨是哪个系统调用用的, wrapper函数把系统调用的编号copy到寄存器 %eax

  • wrapper函数执行一个 trap(一般翻译为中断) 进入内核模式,比如以前的Linux中,一般是执行 int 0x80,当然现在的CPU 有一个更高效的方式。

  • 当进入内核态之后,操作系统完成这么几件事情:

    • 保存当前的寄存器里的值
    • 检查系统调用编号是否正确
    • 执行系统调用的代码
    • 执行完成之后恢复寄存器的值
    • 返回到用户态的wrapper函数中,所执行系统调用的地方的代码指令处
  • wrapper函数对系统调用返回的结果进行检查,如果出错了,就设置好 errno 这个值。


这就是一个典型的C程序进行系统调用的过程,其他语言也是类似,只不过所使用的标准库函数不一定同名,系统调用返回之后的 处理方式也未必相同(比如未必是使用 errno 而是抛异常等)。


更多文章
  • 独立运营博客一年的一些数据分享
  • To B(usiness) 和 To C(ustomer)
  • 常见的软件架构套路
  • Cookie 中的secure和httponly属性
  • Google Ads使用体验
  • Go的custom import path
  • 如何挖掘二级子域名?
  • Go Module 简明教程
  • 写了一个Telegram Bot:自动化分享高质量内容
  • ArchLinux 怎么降级 package ?
  • Vim打开很慢,怎么找出最慢的插件?怎么解决?
  • Web后端工程师进阶指南(2018)
  • macOS ansible 遇到 rsync: --chown=www-data: unknown option
  • How to implement fork syscall in Golang?
  • 关于运营的思考-运营要怎么做?