C++之《程序员自我修养》读书总结(12)

《程序员自我修养》读书总结(十二)

Author: Once Day Date: 2026年3月16日

一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦...

漫漫长路,有人对你微笑过嘛...

全系列文章可参考专栏: 书籍阅读_Once-Day的博客-CSDN博客

参考文章:


文章目录

12. 系统调用与API
12.1 系统调用介绍

现代操作系统通常将运行空间划分为用户态与内核态两个层次。应用程序运行在用户态,无法直接访问硬件资源或执行特权指令;而内核态则掌控着内存管理、进程调度、设备驱动等核心功能。系统调用(System Call)正是操作系统内核向用户态程序提供服务的唯一正式入口,它以一组预定义的函数接口形式存在,使得应用程序能够安全、受控地请求内核执行特权操作。文件读写、进程创建、网络通信等日常操作,最终都要通过系统调用来完成。

Linux系统调用的数量相对精简,以x86架构为例,内核提供的系统调用约为三百余个,每个系统调用都有一个唯一的编号,称为系统调用号。这些调用大致可分为以下几类:

类别 典型系统调用 功能说明
文件操作 openreadwriteclose 文件的打开、读写与关闭
进程控制 forkexecvewaitexit 进程的创建、执行与退出
内存管理 brkmmapmunmap 堆空间扩展与内存映射
信号处理 signalsigactionkill 信号的注册、发送与处理
网络通信 socketbindlistenaccept 套接字的创建与网络连接管理

以文件读取为例,应用程序调用read(fd, buf, count)时,该调用通过中断或专用指令陷入内核,内核根据文件描述符fd找到对应的文件对象,将数据从磁盘缓冲区拷贝到用户空间的buf中,随后返回实际读取的字节数。整个过程对应用程序而言是同步阻塞的,底层的磁盘调度、缓存管理等细节被完全屏蔽。

然而,直接使用系统调用进行开发存在明显的不便。系统调用的接口粒度较低,完成一个稍复杂的任务往往需要组合多个调用并处理各种边界情况和错误码。例如,仅实现一个带缓冲的文件逐行读取功能,就需要在read之上手动维护缓冲区、处理部分读取以及换行符分割等逻辑。这正是C运行时库(如glibc)存在的意义之一------它在系统调用之上封装了更易用的接口,如fopenfgetsfprintf等,提供了自动缓冲和格式化能力。

另一个更为根本的问题在于跨平台兼容性。不同操作系统的系统调用在编号、语义和参数约定上几乎完全不同。Linux使用fork创建进程,而Windows对应的是CreateProcessLinux的文件描述符是一个整型数值,Windows则使用句柄(HANDLE)。即便是同属UNIX系谱的macOSLinux,其系统调用号也并不一致。因此,直接基于系统调用编写的程序几乎无法在不同操作系统之间移植。为了解决这一困境,POSIX标准应运而生,它在系统调用之上定义了一套统一的操作系统接口规范,各个兼容POSIX的系统只需保证接口语义一致,从而使应用程序获得源码级别的可移植性。

12.2 系统调用原理

x86体系结构通过特权级(Ring)机制来实现权限隔离,共定义了Ring 0Ring 3四个特权级别,数字越小权限越高。现代操作系统通常只使用其中两个:内核运行在Ring 0,拥有执行特权指令和访问全部内存的能力;应用程序运行在Ring 3,被限制在受保护的用户空间内。当用户态程序需要请求内核服务时,必须通过一种受控的方式完成特权级切换,而不能随意跳转到内核代码。这种受控切换的核心机制就是中断(Interrupt),更准确地说,系统调用利用的是软中断或陷阱指令,由程序主动触发,将控制权移交给内核预先注册好的中断处理程序。

在传统的Linux系统调用实现中,x86平台使用int 0x80指令触发128号软中断。调用前,用户程序需要按照约定将系统调用号存入eax寄存器,参数依次存入ebxecxedxesiediebp中。以write系统调用为例,其汇编实现如下:

asm 复制代码
section .data
    msg db "hello", 0x0a     ; 待输出的字符串,末尾换行
    len equ $ - msg           ; 字符串长度

section .text
    global _start

_start:
    mov eax, 4                ; 系统调用号: sys_write
    mov ebx, 1                ; 文件描述符: stdout
    mov ecx, msg              ; 缓冲区地址
    mov edx, len              ; 写入字节数
    int 0x80                  ; 触发软中断,陷入内核

    mov eax, 1                ; 系统调用号: sys_exit
    mov ebx, 0                ; 退出码
    int 0x80

int 0x80指令执行时,CPU自动完成一系列关键操作:首先根据中断描述符表(IDT)找到对应的内核入口函数system_call,随后将当前的用户态栈指针esp、段寄存器ss以及标志寄存器eflags等压入内核栈 ,并切换到Ring 0特权级。这里涉及一个重要的细节------每个进程在创建时,内核都会为其分配一个独立的内核栈,其栈指针保存在任务状态段(TSS)中。切换到内核态后,CPUTSS中读取内核栈指针esp0并加载,由此完成从用户栈到内核栈的切换。内核处理完请求后,通过iret指令恢复用户态寄存器状态并返回,整个过程可以概括为:

然而int 0x80方式存在明显的性能瓶颈。软中断的触发和返回需要经历完整的中断处理流程,包括查找IDT、保存和恢复大量寄存器等操作,开销较大。为此,IntelPentium II开始引入了sysenter/sysexit指令对,AMD则提供了syscall/sysret指令对,它们绕过中断描述符表,通过预设的模型特定寄存器(MSR)直接完成特权级切换,大幅减少了指令周期。

Linux 2.5内核开始支持这一新机制,并通过vDSOvirtual Dynamic Shared Object)来实现对用户态的透明适配------内核在每个进程的地址空间中映射一小段共享代码页,其中包含当前CPU最优的系统调用入口指令。用户程序调用glibc封装函数时,glibc会跳转到vDSO中的入口点,由其自动选择sysenterint 0x80,无需应用程序关心底层硬件差异。

12.3 Windows API

Linux将系统调用直接暴露给用户程序不同,Windows有意对其底层系统调用(在微软文档中称为系统服务System Services)进行了隔离和封装。Windows的系统调用接口位于ntdll.dll中,以NtZw为前缀,例如NtCreateFileNtReadFile等。这些接口没有公开的官方文档保证,其签名和调用号随版本更新可能发生变化。因此,微软明确不建议应用程序直接调用ntdll.dll中的原生系统服务,而是要求通过上层的Windows API来间接使用内核功能。

Windows API(也常称为Win32 APIWinAPI)是微软为应用程序开发提供的核心编程接口,主要由一组动态链接库实现。其中最基础的三个库构成了Windows子系统的核心:kernel32.dll负责进程、线程与内存管理,user32.dll处理窗口和消息机制,gdi32.dll提供图形绘制能力。这些DLL内部再调用ntdll.dll完成真正的系统调用陷入。按功能领域划分,Windows API大致包含以下类别:

类别 代表函数 功能说明
文件与I/O CreateFileReadFileWriteFile 文件、管道、设备的创建与读写
进程与线程 CreateProcessCreateThreadExitProcess 进程和线程的生命周期管理
内存管理 VirtualAllocHeapAllocMapViewOfFile 虚拟内存分配与文件映射
同步机制 CreateMutexWaitForSingleObjectEnterCriticalSection 互斥量、事件、临界区等同步原语
窗口与消息 CreateWindowExGetMessageDispatchMessage GUI窗口创建与消息循环
图形绘制 GetDCBitBltTextOut 设备上下文与GDI绘图操作
网络通信 WSAStartupsocketsendrecv 基于Winsock的网络编程
注册表 RegOpenKeyExRegSetValueExRegQueryValueEx 系统注册表的读写访问

应用程序使用Windows API而非直接调用系统服务,最核心的原因在于稳定性与兼容性。微软对Windows API层面的接口保持了严格的向后兼容承诺,一个基于Win32 API编写并在Windows XP上编译的程序,通常能够在Windows 11上正常运行。而底层的Nt系列系统服务则不具备这种保证。此外,Windows API在原生系统调用之上提供了大量附加逻辑,例如CreateFile不仅仅是对NtCreateFile的简单转发,还包含路径规范化、安全描述符处理以及兼容性修补等额外工作,这些都简化了开发者的负担。

以文件写入为例,下面的代码展示了使用Windows API将字符串写入文件的典型流程:

c 复制代码
#include <windows.h>

int main(void) {
    HANDLE hFile = CreateFile(
        "output.txt",              // 文件名
        GENERIC_WRITE,             // 写入权限
        0,                         // 不共享
        NULL,                      // 默认安全属性
        CREATE_ALWAYS,             // 总是创建新文件
        FILE_ATTRIBUTE_NORMAL,     // 普通文件属性
        NULL                       // 无模板文件
    );
    if (hFile == INVALID_HANDLE_VALUE) return 1;

    const char *msg = "hello from WinAPI\n";
    DWORD written;
    WriteFile(hFile, msg, strlen(msg), &written, NULL);
    CloseHandle(hFile);
    return 0;
}

Linuxopen/write/close三个系统调用的简洁风格相比,Windows API的函数参数明显更为冗长,CreateFile一个调用就包含七个参数。这种设计风格体现了两种操作系统不同的哲学取向------Linux倾向于提供简洁的原语,由上层组合完成复杂功能;Windows则倾向于在API层面一次性提供尽可能完整的控制能力,将更多的选项集成到单个调用中。

Once Day

也信美人终作土,不堪幽梦太匆匆......
如果这篇文章为您带来了帮助或启发,不妨点个赞👍和关注!
(。◕‿◕。)感谢您的阅读与支持~~~

相关推荐
浅念-1 小时前
C++ 异常
开发语言·数据结构·数据库·c++·经验分享·笔记·学习
2401_884563241 小时前
高性能日志库C++实现
开发语言·c++·算法
czxyvX2 小时前
C++ - 基于多设计模式下的同步&异步日志系统
c++·设计模式
handler012 小时前
基础算法:BFS
开发语言·数据结构·c++·学习·算法·宽度优先
2401_879503412 小时前
C++中的状态模式实战
开发语言·c++·算法
Aawy1202 小时前
自定义字面量实战
开发语言·c++·算法
j_xxx404_2 小时前
LeetCode模拟算法精解II:外观数列与数青蛙
数据结构·c++·算法·leetcode
轩情吖2 小时前
MySQL之表的增删查改
android·开发语言·c++·后端·mysql·adb·
2301_793804692 小时前
C++与硬件交互编程
开发语言·c++·算法