跳转至

操作系统基础

学习目标

学完本章后,学习者应该能够:

  1. 理解操作系统在应用程序和硬件之间承担的职责。
  2. 区分用户态、内核态和系统调用。
  3. 解释为什么一次普通文件读写或网络请求需要操作系统参与。
  4. 能把 Go 后端服务和操作系统资源联系起来。

操作系统解决什么问题

应用程序不能直接随意操作 CPU、内存、磁盘和网卡。否则一个程序写错,就可能破坏其他程序的数据,甚至让整台机器不可用。

操作系统负责在应用程序和硬件之间建立秩序:

职责 后端工程中的体现
进程管理 启动服务、停止服务、信号处理
内存管理 虚拟内存、堆栈、OOM
文件管理 日志、配置、上传文件、文件描述符
网络管理 socket、端口、TCP 连接
权限管理 用户权限、文件权限、容器能力
资源隔离 cgroups、namespace、容器资源限制

从后端工程师的角度看,操作系统就是服务运行时的资源管理者。

用户态与内核态

CPU 执行程序时通常分为两类权限状态:

  • 用户态:普通应用程序运行的位置,权限受限。
  • 内核态:操作系统内核运行的位置,可以访问硬件和核心资源。

Go 代码大多数时候运行在用户态。但当程序需要读文件、发网络包、创建线程、申请内存映射时,就需要通过系统调用进入内核态。

系统调用

系统调用是应用程序请求内核帮忙做事的接口。

常见系统调用包括:

  • open:打开文件。
  • read:读取文件或 socket。
  • write:写入文件或 socket。
  • close:关闭文件描述符。
  • socket:创建网络 socket。
  • accept:接受新连接。
  • mmap:建立内存映射。
  • clone / fork:创建执行单元。

Go 标准库把很多系统调用包装成了更易用的 API:

file, err := os.Open("app.log")
if err != nil {
    return err
}
defer file.Close()

这段 Go 代码看起来很简单,但底层会涉及打开文件、分配文件描述符和关闭文件描述符。

Go 后端实际应用例子

例子一:忘记关闭文件导致描述符泄漏

文件和网络连接都依赖文件描述符。如果请求中反复打开文件但不关闭,最终可能出现 too many open files

func ReadConfig(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close()

    return io.ReadAll(f)
}

defer f.Close() 不是格式问题,而是资源释放问题。后续文件系统与 IO 章节会继续展开。

例子二:HTTP 请求最终也是系统资源

一个 HTTP Server 监听端口,本质上是进程持有一个监听 socket:

srv := &http.Server{
    Addr:         ":8080",
    ReadTimeout:  5 * time.Second,
    WriteTimeout: 10 * time.Second,
}

if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
    return err
}

监听端口、接受连接、读写 socket 都需要操作系统参与。理解这一点后,连接泄漏、端口占用、文件描述符耗尽就不再是神秘问题。

常见误区

  • 误区一:操作系统只是运维需要懂。

后端服务运行在操作系统上。端口、文件描述符、信号、内存限制都会影响代码行为。

  • 误区二:系统调用只是底层细节。

系统调用多了会有成本。高并发场景下,频繁读写小文件、频繁创建连接、频繁跨用户态和内核态切换都可能影响性能。

  • 误区三:Go 标准库屏蔽了所有系统差异。

标准库提供了统一接口,但不同操作系统的文件路径、信号、网络行为、权限模型仍然存在差异。

实战任务

观察当前机器上一个 Go 服务或任意长期运行进程:

  1. 找到它的进程 ID。
  2. 判断它是否监听端口。
  3. 找出它打开了多少文件或连接。
  4. 思考这些资源如果不释放,会出现什么问题。
Get-Process
netstat -ano
ps aux
ss -lntp
lsof -p <pid>
参考答案

Windows 上可以先用 Get-Process 找到目标进程,再用 netstat -ano 根据 PID 查看监听端口或连接。Linux / macOS 上可以用 ps aux 找 PID,ss -lntp 看监听端口,lsof -p <pid> 看进程打开的文件、socket 和动态库。

如果文件或连接持续增长且没有释放,可能导致文件描述符耗尽。表现包括无法接受新连接、无法打开日志文件、数据库连接创建失败,常见报错是 too many open files

面试题

1. 操作系统对后端服务最重要的职责是什么?

参考答案

操作系统负责管理 CPU、内存、文件、网络、进程和权限。对后端服务来说,它决定进程如何被调度、内存如何分配、端口如何监听、文件如何读写、连接如何建立,以及资源达到限制时会发生什么。

后端工程师不需要掌握所有内核实现,但要理解这些资源如何影响服务稳定性和性能。

2. 什么是用户态和内核态?

参考答案

用户态是普通应用程序运行的状态,权限受限,不能直接操作硬件和关键内核数据。内核态是操作系统内核运行的状态,拥有更高权限,可以管理硬件和系统资源。

应用程序需要读写文件、创建 socket、申请内存映射等操作时,会通过系统调用从用户态进入内核态,请求内核完成工作。

3. 系统调用为什么有成本?

参考答案

系统调用涉及从用户态切换到内核态,再从内核态返回用户态,这个过程需要保存和恢复上下文,还可能触发调度、等待 IO 或锁竞争。

单次系统调用成本通常可接受,但高频、小粒度、无批处理的系统调用会放大开销。例如频繁写小日志、频繁创建短连接,都可能让系统调用成本变成性能问题。