计算机硬件资源有限,多个程序需要共享 CPU、内存、I/O 设备。如果每个程序都直接操作硬件,会带来资源冲突和安全问题。内核作为系统核心,负责管理所有硬件资源,为应用程序提供统一、安全的访问接口。
Linux 采用宏内核 (Monolithic Kernel) 架构,核心功能(进程调度、内存管理、文件系统、设备驱动)都运行在内核空间。但与传统的宏内核不同,Linux 支持可加载内核模块 (LKM),允许在运行时动态加载和卸载功能。
系统调用是用户程序进入内核的唯一入口。当应用程序需要执行特权操作(如读写文件、创建进程)时,通过 int 0x80 或 syscall 指令触发软中断,CPU 切换到内核态,执行相应的内核函数。
// read 系统调用:从文件描述符读取数据
ssize_t read(int fd, void *buf, size_t count) {
// 1. 参数检查(用户态)
if (count < 0 || buf == NULL) {
return -1;
}
// 2. 触发系统调用(软中断 / syscall 指令)
long ret;
asm volatile (
"syscall"
: "=a"(ret)
: "a"(__NR_read), "D"(fd), "S"(buf), "d"(count)
: "rcx", "r11", "memory"
);
// 3. 处理返回值
if (ret < 0) {
errno = -ret;
return -1;
}
return ret;
}
传统的 SysV init 使用 shell 脚本按顺序启动服务,依赖关系靠脚本中的数字优先级(如 S10network、S20sshd)。这种方式有两个问题:串行启动导致启动慢,依赖关系难以管理(循环依赖、遗漏依赖)。
systemd 的核心概念是 Unit,每个 Unit 是一个配置单元(服务、套接字、设备、挂载点等)。Unit 之间通过 依赖关系(Requires、Wants、After)和 触发关系(socket 激活、路径激活)连接。
Socket 激活是 systemd 的核心创新之一:服务可以不在启动时立即运行,而是由 systemd 监听该服务的 socket,当有连接请求时才启动服务。这实现了按需启动,减少了系统启动时的资源占用。
# /etc/systemd/system/sshd.service
[Unit]
Description=OpenSSH Daemon
After=network.target # 在 network 目标之后启动
Wants=sshd-keygen.service # 可选依赖
[Service]
Type=forking
ExecStart=/usr/sbin/sshd -D # 启动命令
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=10s
[Install]
WantedBy=multi-user.target # 属于 multi-user 目标
在同一个操作系统上运行多个服务时,我们希望它们相互隔离:一个服务崩溃不应该影响其他服务,一个服务也不能占用过多资源。传统 Linux 提供了 chroot 进行文件系统隔离,但隔离能力有限。namespace 和 cgroup 是 Linux 内核提供的两种核心机制,构成了容器技术的基础。
Linux 内核目前支持 8 种 namespace,每种隔离一种全局资源:
| namespace | 隔离资源 | 作用 |
|---|---|---|
| mount (mnt) | 挂载点 | 每个进程拥有独立的文件系统视图 |
| PID | 进程 ID | 子进程看不到父进程的 PID,容器内 PID 从 1 开始 |
| net | 网络栈 | 独立的网络设备、路由表、防火墙规则 |
| ipc | 进程间通信 | 独立的 System V IPC 和 POSIX 消息队列 |
| uts | 主机名 | 独立的主机名和域名 |
| user | 用户 ID | 非 root 用户可在容器内映射为 root |
| cgroup | cgroup 视图 | 独立的 cgroup 层次结构 |
| time | 系统时间 | 独立的系统时间(Linux 5.6+) |
cgroup (control group) 提供资源控制能力,主要包括:
# 创建新的 namespace (PID + 网络 + 挂载)
unshare --pid --net --mount --fork /bin/bash
# 在 cgroup 中限制 CPU 和内存
# 创建 cgroup 子系统目录
mkdir -p /sys/fs/cgroup/cpu/mycontainer
mkdir -p /sys/fs/cgroup/memory/mycontainer
# 设置 CPU 份额 (512 = 50% 的一个核)
echo 512 > /sys/fs/cgroup/cpu/mycontainer/cpu.shares
# 设置内存限制 (512MB)
echo 536870912 > /sys/fs/cgroup/memory/mycontainer/memory.limit_in_bytes
# 将当前进程加入 cgroup
echo $$ > /sys/fs/cgroup/cpu/mycontainer/tasks
echo $$ > /sys/fs/cgroup/memory/mycontainer/tasks
Unix/Linux 最核心的设计哲学之一是 「一切皆文件」 —— 普通文件、目录、设备、网络连接、管道等都被抽象为文件。这意味着统一的操作接口(打开、关闭、读写、控制),但进程如何管理这些打开的文件?答案是文件描述符 (File Descriptor)。
每个进程维护一个文件描述符表,每一项指向一个内核文件对象。文件对象包含文件偏移量、文件状态标志、inode 引用等。多个文件描述符可以指向同一个文件对象,实现共享文件偏移。
重定向的本质是修改文件描述符表,使其指向不同的内核文件对象。例如 exec 2>error.log 将 stderr(fd 2)指向 error.log 的文件对象。管道使用 pipe() 系统调用创建一对文件描述符,一个用于读,一个用于写,实现进程间通信。
// 使用 pipe 实现父子进程通信
int fd[2];
pipe(fd); // fd[0] 读端, fd[1] 写端
pid_t pid = fork();
if (pid == 0) {
// 子进程:关闭写端,从管道读
close(fd[1]);
char buf[256];
read(fd[0], buf, sizeof(buf));
printf("子进程收到: %s\n", buf);
} else {
// 父进程:关闭读端,向管道写
close(fd[0]);
write(fd[1], "Hello from parent", 20);
}
传统的 Unix 权限模型将用户分为root (超级用户)和普通用户。root 拥有所有特权,可以执行任何操作。但实际场景中,一个程序可能只需要某项特权(如绑定低端口),却因为运行在 root 下而获得了全部特权。这带来了巨大的安全风险——一旦程序存在漏洞,攻击者可获得整个系统的控制权。
传统权限模型使用 9 个权限位(读 r、写 w、执行 x),分别针对用户、组、其他。setuid 允许普通用户以文件所有者的身份运行程序(如 /usr/bin/passwd 以 root 身份修改密码)。
Capability 机制将 root 特权拆分为独立的能力,例如:
| Capability | 特权描述 | 示例 |
|---|---|---|
| CAP_NET_BIND_SERVICE | 绑定低于 1024 的端口 | 非 root 运行 web 服务器 |
| CAP_SYS_ADMIN | 系统管理操作 | 挂载文件系统、设置主机名 |
| CAP_DAC_OVERRIDE | 绕过文件权限检查 | 读取任意文件 |
| CAP_NET_RAW | 使用原始套接字 | ping 命令需要 |
| CAP_SYS_BOOT | 系统重启 | reboot 命令 |
| CAP_KILL | 向任意进程发送信号 | kill 命令 |
# 查看程序的 capability
getcap /usr/bin/ping
# 输出: /usr/bin/ping = cap_net_raw+ep
# 给程序添加 capability (如允许非 root 绑定 80 端口)
sudo setcap 'cap_net_bind_service+ep' /usr/bin/nginx
# 查看当前进程的 capability
cat /proc/$$/status | grep Cap
# 输出: CapInh: 0000000000000000
# CapPrm: 0000000000000000
# CapEff: 0000000000000000
# 使用 capsh 启动具有特定 capability 的 shell
sudo capsh --caps='cap_net_raw+eip' -- -c 'ping 8.8.8.8'
Shell 是用户与操作系统交互的命令行界面,也是脚本编程环境。它可以执行用户输入的命令、自动化重复任务、组合多个工具完成复杂操作。Shell 的核心价值在于将一个个独立的命令组合成强大的管道,体现了 Unix 「小工具组合」的设计哲学。
Shell 脚本的执行流程:
子 shell 是 Shell 编程的核心机制:当执行外部命令或管道时,Shell 会 fork() 一个子进程,在子进程中执行命令。子 shell 继承父 shell 的环境变量,但对变量的修改不会影响到父 shell。
#!/bin/bash
# 脚本:统计日志中错误出现频率
# 定义变量
LOG_FILE="/var/log/syslog"
ERROR_PATTERN="ERROR"
# 检查文件是否存在
if [[ ! -f $LOG_FILE ]]; then
echo "日志文件不存在: $LOG_FILE"
exit 1
fi
# 使用管道组合命令:grep → sort → uniq -c → sort -nr
grep "$ERROR_PATTERN" $LOG_FILE | \
sort | \
uniq -c | \
sort -nr
# 管道中的每个命令都在独立的子 shell 中执行
# 管道符 | 创建了进程间通信通道