第2章:Linux系统调用
Source: Dev.to
Source: …
Linux 系统调用 – 通往内核的“前门”
本文是 终极容器安全系列 的一部分,该系列是一个结构化的多篇指南,涵盖从基础概念到运行时防护的容器安全。有关系列结构、范围以及更新计划的概览,请参阅系列介绍帖 这里。
1. Linux 执行的“世界”
| 世界 | 描述 |
|---|---|
| 用户空间 | 运行面向用户的应用程序的地方(Web 服务器、浏览器、编辑器、CLI 工具、后台服务等)。这是一个受限区域——应用程序不能直接访问硬件或管理关键系统资源。这种限制提升了稳定性:即使某个应用崩溃,整个操作系统通常仍能保持运行。 |
| 内核空间 | Linux 内核所在的区域。它控制内存、进程、调度、硬件、驱动、文件系统、网络、安全等。它直接与 CPU、RAM、磁盘及其他硬件交互,拥有全部特权。 |
2. 系统调用在何处起作用?
应用程序在用户空间运行,拥有 较低的权限。
如果应用需要执行必须由内核特权才能完成的操作——例如:
- 打开文件
- 读取/写入数据
- 创建进程
- 分配内存
- 发送网络流量
- 获取当前时间
——它必须请求内核来完成。
该请求通过 系统调用接口(也称 syscall 接口)发出。
定义(通俗说法) – 系统调用是一种让用户空间应用程序以安全、受控的方式向 Linux 内核请求服务的编程手段。
为什么要区分?
- 安全性与稳定性 – 用户程序不能直接触碰硬件或内核内存;那样会非常危险。
- 受控的入口点 – 系统调用提供了一组有限且经过审查的入口,进入内核。
并非所有操作都需要内核。例如,字符串分词完全在用户空间完成。而涉及文件、设备、网络或进程管理的任何操作,都必须通过系统调用。
Linux 自带 300 多个系统调用(具体数量随内核版本和 CPU 架构而异)。
3. 常见系统调用(示例)
| 程序想要做的事 | 系统调用 |
|---|---|
| 读取文件 | read() |
| 写入文件 | write() |
| 打开文件 | open() |
| 启动新程序 | execve() |
| 创建进程 | fork() |
| 分配内存 | mmap() |
| 发送网络数据 | send() |
| 获取当前时间 | clock_gettime() |
完整列表可通过手册页查看:man 2 syscalls。
4. 系统调用的高级视图
从程序员的角度看,系统调用看起来像普通函数调用,但在内部它会执行一次 受控的内核模式切换。
典型流程
- 用户应用调用标准库函数(例如
read())。 - 该函数使用 系统调用号 触发一次系统调用。
- CPU 从 用户模式 切换到 内核模式。
- Linux 内核执行请求的操作。
- 控制权返回给应用,并携带结果(或错误)。
示例:read(fd, buffer, size) 会触发内核对该文件描述符的 read 实现,并返回读取的字节数(出错时返回 ‑1,错误细节存于 errno)。
5. 在高级语言中使用系统调用
作为应用开发者,你很少会直接“裸”调用系统调用。通常会使用 包装函数:
| 语言 | 包装函数来源 |
|---|---|
| C / C++ | glibc(如 read()、write()、open()) |
| Go | syscall 包(或更高级的 os 包) |
这些包装函数会:
- 验证并整理参数
- 执行切换到内核模式的操作
- 以熟悉的方式返回结果
6. 最小化 C 示例 – 向 stdout 打印
#include
int main(void) {
const char msg[] = "Hello, World!\n";
/* write() is a glibc wrapper around the write syscall */
write(1, msg, sizeof(msg) - 1); /* fd 1 = stdout */
return 0;
}
步骤逐步发生了什么?
write(1, msg, sizeof(msg) - 1)从用户空间被调用。write()(来自 glibc)准备系统调用(将系统调用号和参数放入相应的寄存器)。- CPU 通过系统调用接口切换到内核模式。
- 内核进行验证:
- 文件描述符 1 是否有效,
- 进程是否被允许写入该描述符,
msg是否指向可访问的内存。
- 内核将字节写入 stdout(通常是你的终端)。
- 内核返回写入的字节数;执行在用户空间恢复。
即使代码看起来很简单,重要的收获是 任何对文件、进程、网络、内存映射等的交互都必须通过系统调用。
7. 容器与系统调用
容器不过是运行在宿主 Linux 内核上的进程。
- 容器 没有独立的内核;它们共享宿主内核。
- 系统调用是容器进程与该内核交互的 唯一方式。
因此,容器所做的一切——读取文件、打开套接字、创建进程——都要经过系统调用。应用代码使用系统调用的方式在宿主机或容器内部是完全相同的。
8. 安全影响
由于容器依赖宿主内核,系统调用成为强大的安全控制点:
- 如果进程能够调用强大的系统调用,它就可能执行强大(且可能危险)的操作。
- 最小特权 很重要:并非所有应用都需要所有系统调用。
- 通过 限制容器化应用可以使用的系统调用,可以降低攻击面。
底线: 如果攻击者侵入了容器化应用,他们能造成的破坏在很大程度上取决于该进程被允许使用的系统调用(以及权限)。
因此,容器加固通常侧重于 减少内核暴露——例如使用 seccomp 配置文件、AppArmor、SELinux 或其他机制来限制容器可能调用的系统调用。
9. 接下来是什么?
接下来的章节将在此基础上进一步说明:
- 容器如何提供隔离、资源管理和安全边界
- 运行时保护技术(seccomp、能力、命名空间等)
- 针对真实工作负载的实用加固步骤
敬请期待!
容器安全控制
- seccomp – 限制系统调用。
- Capabilities – 删除不必要的特权。
- Namespaces & cgroups – 提供隔离和资源限制。
在后续章节中,我们将直接基于此概念,展示容器如何创建边界以及如何加强这些边界。
更多资源
本文是 Ultimate Container Security Series 系列中的一篇,旨在以实用的方式组织和阐释容器安全概念。如果您想了解相关主题或查看接下来的内容,请参阅 系列简介,获取完整路线图。