【Linux 底层原理】进程(一):从硬件、操作系统到系统调用,理解程序运行机制
一、前言:学习进程之前,先弄清楚操作系统在做什么
之前学习 C/C++ 的时候,我们主要关注的是程序本身:变量存在哪里、函数如何调用、数据结构如何组织,以及代码最终怎样运行。
但当我们开始学习 Linux,就需要换一个角度思考问题:
- 我们编写的程序,最终是怎样被计算机执行的?
- CPU、内存、磁盘和输入输出设备之间是如何协作的?
- 操作系统为什么能够同时管理那么多程序?
- 操作系统面对大量运行中的程序,究竟是怎样进行管理的?
这些问题看似分散,实际上有一条统一的主线。
计算机硬件负责执行和存储,操作系统负责管理资源,而进程正是操作系统管理运行任务的重要对象。
要理解进程,不能只背诵“进程是程序的一次执行实例”。我们需要先理解程序运行的硬件基础,再理解操作系统的定位,最后进入操作系统的管理方式。
本文主要从三个方面展开:
- 冯诺依曼体系结构与计算机中的数据流动。
- 操作系统的定位,以及系统调用和库函数的关系。
- 操作系统的管理哲学:先描述,再组织。
二、冯诺依曼体系结构:程序运行的硬件基础
2.1 一台计算机由哪些部分组成?
我们日常使用的笔记本电脑,以及服务器等计算设备,大多可以从冯诺依曼体系结构的角度理解。
经典的冯诺依曼体系结构主要包括以下组成部分:

- 输入设备:键盘、鼠标等,用于向计算机输入信息。
- 输出设备:显示器、打印机等,用于向外部呈现处理结果。
- 存储器:经典模型中主要指内存,用于存放程序和数据。
- 运算器:负责执行算术运算和逻辑运算。
- 控制器:负责协调和控制计算机的执行过程。
现代计算机还包含磁盘、网卡、显卡等大量硬件组件。实际硬件结构远比经典模型复杂,但冯诺依曼体系结构依然是理解程序运行的重要起点。
这里需要特别关注三个对象:
CPU、内存和外部设备。
CPU 负责执行指令,内存用于存放正在执行的程序及其数据,而键盘、磁盘、网卡等设备则负责提供或接收信息。
它们不是各自独立工作的,而是需要通过一系列数据传输与控制过程协同完成任务。
2.2 为什么程序通常需要先加载到内存?
先思考一个问题:我们编写的 C 程序,最初保存在哪里?
例如,我们在 Linux 中编写一个 hello.c 文件:
#include <stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
经过编译和链接后,我们得到一个可执行文件。
此时,这个可执行文件通常保存在磁盘上。它是一个文件,还没有因为被保存下来就自动开始执行。
当我们运行它时,操作系统需要为程序的执行建立相应的运行环境,并根据需要将程序内容映射或加载到内存中。CPU 随后才能按照执行机制获取指令、处理数据。
为什么要这样设计?
因为 CPU 执行指令时,需要通过计算机的存储层次获取指令和数据。内存为程序执行提供了重要的工作空间,而磁盘主要承担持久化存储的任务。
从性能上看,CPU 的执行速度远高于常规存储设备的访问速度。内存、缓存和寄存器等不同层次的存储结构,正是现代计算机协调性能、容量和成本的重要组成部分。
因此,我们可以先建立一个基本认识:
磁盘主要负责长期保存程序和数据,内存为程序运行提供工作空间,CPU 负责执行指令。
这里的“加载到内存”是帮助理解程序运行的基础模型。现代操作系统还会使用虚拟内存、按需调页、文件映射等机制,并不意味着每次运行都必须把整个可执行文件一次性读入内存。

2.3 理解数据流动:以键盘输入为例
仅仅记住 CPU、内存和外设的名称还不够,我们还需要理解它们之间的数据是怎样流动的。
假设我们在 Linux 终端中输入:
echo hello
从宏观角度看,可以把这个过程理解为以下几个阶段。
第一阶段:键盘产生输入。
当我们按下键盘上的按键时,键盘硬件会产生相应的输入信息。操作系统通过输入设备相关的驱动和处理机制接收这些信息。
第二阶段:操作系统处理输入。
输入信息经过硬件与操作系统相关的处理流程,进入相应的软件缓冲区或终端输入处理机制。具体经过哪些缓冲区,取决于设备、驱动和终端配置。
第三阶段:程序读取输入并执行。
终端程序根据用户输入的内容进行处理。当我们执行 echo hello 时,Shell 会解析命令,并通过相应的执行机制启动或调用命令程序。
第四阶段:产生输出。
echo 产生字符串 hello,输出数据通过操作系统提供的输入输出机制传递到终端,最终由显示设备呈现出来。
我们可以把整体流程简化为:
键盘
|
v
输入设备与驱动
|
v
操作系统的输入处理机制
|
v
终端 / Shell 处理
|
v
命令执行与输出
|
v
操作系统的输出机制
|
v
显示设备
这张图展示的是宏观的数据处理流程,不代表每一步都对应一次独立的内存复制。
需要注意,数据流动不等于所有数据都必须由 CPU 亲自搬运一遍。在真实系统中,设备控制器、DMA、缓冲区以及不同的 I/O 机制都可能参与数据传输。
对于初学阶段,我们首先需要理解的是:程序执行、输入输出和硬件设备并不是相互孤立的,它们需要操作系统和硬件机制协同完成工作。
2.4 为什么需要操作系统参与?
假设我们编写一个程序,需要读取文件、接收键盘输入、向屏幕输出内容。
如果每个应用程序都必须自己了解每一种硬件的工作方式,直接控制设备的寄存器,并自行处理资源分配,那么程序开发会变得非常复杂。
而且,如果多个程序同时使用同一个设备或争夺有限的内存资源,又该由谁来协调?
这就引出了操作系统。
操作系统将大量通用的硬件管理、资源分配和访问控制机制集中起来,为上层程序提供统一的使用方式。
应用程序不需要了解所有硬件的内部细节,也不应该随意控制整台计算机的资源。
它可以通过操作系统提供的接口提出请求,再由操作系统按照相应规则处理。
这就是我们接下来要理解的内容。
三、操作系统:连接应用程序与硬件的管理者
3.1 操作系统到底是什么?
从广义上看,操作系统通常包括内核以及相关系统程序和工具。
其中,内核是操作系统最核心的部分,负责进程管理、内存管理、文件管理、设备驱动等重要工作。
我们在 Linux 中学习的进程管理,主要就是围绕 Linux 内核展开的。
可以将计算机的软件结构简化为:

这张图是帮助理解的简化模型。库函数不一定每次都会调用系统调用,驱动程序也不一定总是以独立的层次出现在每条执行路径中。
不过,它说明了一个重要事实:
应用程序通常通过操作系统提供的受控接口使用系统资源,而不是直接掌管整个硬件。
3.2 操作系统的两个主要目标
从课件的角度来看,操作系统的目标可以概括为两个方面。
对下:管理和协调计算机资源。
操作系统需要管理 CPU 的使用、内存的分配、文件的访问以及设备的使用等。
例如,当多个程序同时运行时,CPU 的执行时间是有限的,内存容量也是有限的。操作系统需要协调这些资源,让不同的程序能够按照规则获得执行机会。
对上:为应用程序提供良好的运行环境。
应用程序不需要每次读取文件时都重新编写一套磁盘控制逻辑,也不需要为每一种网卡都实现一套完整的硬件控制机制。
操作系统通过相应的接口和机制,将底层的复杂性封装起来,让应用程序可以使用相对统一的方式完成工作。
这两个目标并不是彼此独立的。
操作系统正是通过管理底层资源,才能为上层程序提供相对稳定、可靠的运行环境。
3.3 为什么操作系统不能让应用程序随意操作硬件?
我们可以把操作系统想象成一个大型图书馆。
图书馆里有大量图书、阅览座位和设备。每个读者都可以使用这些资源,但不能因为自己想看某本书,就随意拿走所有藏书,也不能擅自破坏其他读者正在使用的设备。
图书馆需要制定规则、记录资源状态,并按照一定的方式管理使用过程。
操作系统也有类似的职责。
如果一个普通应用程序可以不受限制地修改其他程序的内存、随意访问设备或者直接控制整个系统,那么系统的安全性和稳定性就很难得到保障。
因此,现代操作系统通常区分用户态和内核态,并通过权限控制和受控接口限制应用程序对系统资源的访问。
这不是为了故意增加开发难度,而是为了让多个程序能够在同一台计算机上相对安全地运行。
四、系统调用与库函数:应用程序怎样请求操作系统服务?
理解了操作系统的定位,接下来就要回答一个具体问题:
应用程序想要使用操作系统的功能,究竟应该怎么做?
这就涉及系统调用与库函数。
4.1 什么是系统调用?
系统调用是应用程序请求操作系统内核提供服务的重要接口。
例如,在 Linux 中,我们经常会接触到:
fork():创建进程。read():读取数据。write():写入数据。open():打开文件。close():关闭文件。
这些接口让应用程序能够请求内核执行相应的操作。
以文件写入为例,应用程序不能随意修改磁盘上的任意位置,而是需要通过操作系统提供的文件访问机制完成操作。操作系统会结合文件描述符、访问权限和具体 I/O 路径等信息进行处理。
系统调用的意义在于,它为应用程序提供了受控的内核服务入口。
需要注意,系统调用并不等于普通的 C 函数调用。系统调用通常涉及从用户态进入内核态的机制,并由内核处理相应请求。
4.2 什么是库函数?
我们在 C 语言中经常使用:
printf("hello world\n");
这里的 printf() 是 C 标准库提供的格式化输出函数。
它负责按照指定格式组织输出内容,并通过相应的底层机制完成输出。
在 Linux 中,printf() 通常会使用标准 I/O 缓冲机制,并在需要时通过底层的 write() 等机制输出数据。
这里必须区分两个概念:
printf()是库函数。write()是 Linux 提供的系统调用接口。
它们不是同一个层次的东西。
库函数可以封装更方便的功能,系统调用则为应用程序提供请求内核服务的入口。
此外,并不是每个库函数都会调用系统调用。例如,一些字符串处理函数完全可以在用户态完成工作。
4.3 用一条输出语句理解调用关系
当我们执行:
printf("hello world\n");
可以从宏观上理解为:
- 程序调用
printf()。 - C 标准库对输出内容进行格式化,并根据需要使用缓冲机制。
- 当需要向终端等目标输出数据时,库函数可能通过
write()等系统调用请求内核服务。 - 内核根据相应的文件描述符、设备和 I/O 机制处理请求。
- 输出最终通过相应的设备路径呈现给用户。
这不是说每次调用 printf() 都一定立即执行一次 write()。如果使用了缓冲机制,实际输出可能被推迟到缓冲区刷新时。
我们真正要理解的是:库函数负责提供方便的编程接口,系统调用负责让应用程序请求内核服务。
通过这种分层设计,应用程序不必重复实现大量底层机制,也不必直接控制所有硬件细节。
五、操作系统的管理哲学:先描述,再组织
前面的内容解释了操作系统为什么存在,以及应用程序如何使用它提供的服务。
现在来到本篇最重要的部分:
操作系统究竟怎样管理大量的对象?
课件中给出了一个非常重要的思路:先描述,再组织。
这也是后续理解 Linux 进程管理的基础。
5.1 第一步:先描述被管理对象
假设学校需要管理几千名学生。
如果只知道学校里有很多学生,却不知道每个人是谁、属于哪个班级、成绩如何,那么管理工作就无法开展。
学校需要记录每个学生的相关信息,例如:
- 学号。
- 姓名。
- 班级。
- 年龄。
- 成绩。
这样,每个学生就不再只是一个模糊的个体,而是有了一组可以被记录、查询和处理的属性。
在程序中,我们可以使用结构体描述学生:
struct Student
{
int id;
char name[32];
int age;
float score;
};
这里的 struct Student 描述了学生对象具有哪些属性。
需要注意,结构体定义本身只是规定了数据的组织形式,并不会自动创建所有学生对象。只有在定义变量、动态分配内存等操作发生后,才会有相应的对象实例。
例如:
struct Student s1 = {1001, "Tom", 19, 90.0f};
struct Student s2 = {1002, "Jerry", 20, 85.0f};
现在,两个学生的信息就可以分别保存在相应的结构体对象中。
这种做法把现实中的管理对象抽象成计算机可以处理的数据。
所谓“描述”,就是把管理对象的重要属性整理成明确的数据结构。
5.2 第二步:再组织被管理对象
如果学校只有两名学生,分别保存两个结构体对象就已经足够直观。
但假设学生数量达到几千甚至几万人,我们还需要频繁进行查找、遍历、添加和删除等操作。
这时,仅仅描述每个学生还不够。
我们还需要考虑:怎样把这些对象组织起来,方便后续管理?
例如,可以使用链表组织学生节点:
struct Student
{
int id;
char name[32];
int age;
float score;
struct Student* next;
};
其中,next 指针指向下一个学生节点。
多个节点可以通过指针连接起来,形成一条链表。
head
|
v
[学生1] -> [学生2] -> [学生3] -> NULL
这样,我们就可以从链表头部出发,逐个访问学生节点,并在满足相应条件时执行查询、插入或删除等操作。

当然,链表并不是唯一的组织方式。
如果我们需要快速查找学生,也可以考虑哈希表;如果需要按照某种顺序维护数据,也可以考虑平衡搜索树。
描述解决的是“对象有哪些属性”,组织解决的是“对象之间如何关联,以及怎样被高效管理”。
二者缺一不可。
5.3 把这个思想放回操作系统
现在将视角从学校切换到 Linux。
操作系统需要管理大量进程。每个进程都有自己的标识、状态、资源使用情况以及执行上下文等信息。
如果操作系统只知道“当前有很多程序在运行”,却没有办法记录和组织每个进程的信息,那么它就无法有效地进行调度、资源管理和状态维护。
因此,Linux 内核使用专门的数据结构描述进程。
在 Linux 中,这个重要的数据结构就是:
struct task_struct;
task_struct 是 Linux 内核用于描述任务的重要结构体类型,其中包含了进程管理所需要的各种信息。
需要说明的是,task_struct 并不是用户自己定义的普通业务结构体,而是 Linux 内核实现的一部分。它的具体字段和组织方式与内核版本有关。
在后续学习中,我们会逐渐看到它所涉及的进程标识、状态、调度信息、内存管理信息等内容。
而操作系统不仅需要描述单个进程,还需要将大量进程纳入内核的管理机制。
Linux 内核会通过链表、树、队列等多种数据结构和相关管理机制组织不同对象。具体使用哪一种结构,取决于管理任务和内核实现,并不是所有对象都简单地放在同一条链表中。
因此,我们可以把操作系统的管理思路概括为:
大量管理对象
|
v
提取对象属性
|
v
建立相应的数据结构
|
v
组织对象及其关系
|
v
执行查询、调度、分配和回收等管理操作
这就是“先描述,再组织”的核心含义。
5.4 为什么这个思想值得反复理解?
学到这里,可能会觉得这只是一个普通的管理方法。
但如果结合我们以前学过的 C/C++,就会发现它与数据结构的设计思想有很强的联系。
在 C 语言中,我们使用结构体组织数据;在数据结构中,我们通过链表、树、哈希表等方式组织对象;在 Linux 内核中,也需要使用相应的数据结构描述和管理进程及其他系统对象。
它们并不是完全相同的东西,但都体现了一个共同思路:
先明确需要处理的对象及其属性,再根据实际需求选择合适的数据组织方式,最后在这些结构上实现相应的操作。
这也解释了为什么学习 Linux 之前,掌握 C 语言、指针和数据结构非常重要。
当我们以后阅读 task_struct 的代码时,不应该只把它看成一大段陌生的内核代码。
我们应该先问:
- 它描述的对象是什么?
- 为什么需要这些属性?
- 这些对象之间有什么关系?
- 内核通过什么数据结构组织它们?
- 这种组织方式如何支持后续的管理操作?
当我们能够从这些角度阅读代码时,理解内核实现就会更有方向。
六、本篇总结
通过本文,我们建立了理解 Linux 进程管理所需要的第一层基础。
首先,从冯诺依曼体系结构出发,我们理解了 CPU、内存和外部设备之间的基本关系,以及程序运行为什么离不开硬件和操作系统的协作。
其次,我们认识到操作系统不仅负责管理底层资源,还为上层应用程序提供相对统一的运行环境。系统调用和库函数则分别处于不同的软件层次,帮助应用程序使用相关功能。
最后,我们回到操作系统管理本身,理解了“先描述,再组织”的核心思想。
对于 Linux 进程管理来说,这个思想尤其重要:操作系统必须能够描述每个进程,并通过相应的数据结构和管理机制组织这些进程,才能进一步完成调度、资源分配和状态维护。
下一篇,我们将正式进入进程的基本概念,重点理解:
- 程序与进程有什么区别?
- 为什么一个程序运行起来后,需要额外的管理信息?
- Linux 为什么使用
task_struct描述进程? - PID、PPID 和进程控制块分别承担什么作用?
从这里开始,我们就正式进入 Linux 进程管理的核心内容。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)