news 2026/6/15 17:29:24

【Linux入门】从0开始认识进程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Linux入门】从0开始认识进程

本文主要内容包括:

  • 认识冯诺依曼体系结构,理解现代计算机的基本工作原理
  • 学习操作系统的概念与定位,理解管理的本质思想
  • 深入理解进程概念,认识 PCB(进程控制块)及其作用
  • 学习进程状态,掌握进程创建过程,理解僵尸进程和孤儿进程的形成原因及危害
  • 了解 Linux 进程调度机制,掌握进程优先级相关概念
  • 理解进程的竞争性与独立性,区分并发与并行
  • 理解进程切换过程,认识 Linux 2.6 Kernel 中 O(1) 调度算法架构
  • 学习环境变量相关知识,掌握常见环境变量及相关命令
  • 熟悉 getenv、setenv 等环境变量操作函数
  • 理解 C 语言内存空间分布规律
  • 了解进程内存映像与应用程序之间的区别
  • 认识虚拟地址空间及其存在意义

一、冯诺依曼体系结构

冯诺依曼体系结构是现代计算机最常见的组织方式。我们平时接触的笔记本、服务器,大体都符合这套模型。它把计算机划分成几个基本部分:

  • 输入单元,比如键盘、鼠标、扫描仪
  • 中央处理器 CPU,负责运算和控制
  • 输出单元,比如显示器、打印机
  • 存储器,也就是内存

这里要特别注意:这里说的存储器指的是内存,不是硬盘

冯诺依曼体系最核心的规则是:

  • CPU 只能直接和内存打交道
  • 外设也只能先把数据写入内存,或者从内存取数据
  • 所有设备的数据传输,本质上都绕不开内存

也就是说,所有设备都只能直接和内存交互

注意,理解冯诺依曼不能只停留在硬件名词上,还要能把它和软件数据流联系起来。比如 QQ 聊天时:

  • 你输入消息,先进入内存
  • CPU 处理消息内容
  • 消息通过网络协议发出去
  • 对方收到后,数据先落到内存,再交给对应程序读取

如果发送文件,过程也是一样的,只是传输的数据量更大,路径更长。


二、操作系统(Operator System)

2-1 概念

操作系统是整个计算机系统中最基础的一类软件。广义上说,它包含两部分:

  • 内核,比如进程管理、内存管理、文件管理、驱动管理
  • 其他系统级程序,比如函数库、shell 等

操作系统对外看起来像一个整体,但内部其实分工明确。它最重要的任务不是替代应用程序,而是给应用程序提供运行环境。


2-2 设计OS的目的

操作系统存在的意义可以分成两个方向:

  • 对下:和硬件交互,管理所有软硬件资源
  • 对上:给用户程序提供一个稳定、统一、好用的执行环境

这意味着操作系统既要管硬件,又要服务上层程序。


2-3 核心功能

操作系统在整个软硬件架构中的定位,可以概括成一句话:

它是一款专门负责管理的软件

它要管理的对象很多,比如:

  • CPU
  • 内存
  • 硬盘
  • 文件
  • 进程
  • 设备

所以理解操作系统,最重要的不是把它想成一个神秘的大黑盒,而是把它看成一个管理者。


2-4 如何理解 管理

管理这件事,最容易理解的方法,就是拿现实中的组织结构做类比。

比如学校里:

  • 学生是被管理对象
  • 辅导员负责日常管理
  • 校长负责更高层次的组织和决策

管理一个对象,通常分两步:

  1. 先描述它
  2. 再组织它

操作系统管理硬件和进程,也遵循同样的思路:

  1. 用结构体描述对象属性
  2. 用链表、树等数据结构组织这些对象

描述起来用结构体,组织起来用链表或其他高效数据结构


2-5 系统调用和库函数概念

操作系统不会把所有能力都直接暴露给用户程序。它会提供一部分基础接口给上层使用,这些接口叫做系统调用

系统调用的特点是:

  • 功能比较底层
  • 使用门槛高
  • 接口偏基础

为了方便开发,很多系统调用会被进一步封装成更好用的库函数

简单理解:

  • 系统调用负责和内核直接打交道
  • 库函数负责把复杂接口包装得更容易用

三、进程

3-1 基本概念与基本操作

教材对进程的定义:程序的一个执行实例,也就是正在执行的程序。

从内核的角度看,进程是:

分配系统资源的实体

现在可以先把进程理解成:

内核数据结构 task_struct + 程序代码和数据

也就是说,进程不是单纯的一段代码,而是代码和内核管理信息的组合。


3-1-2 描述进程-PCB

进程的信息不会散落在内核中,而是统一放在一个专门的数据结构里,这个结构叫做进程控制块 PCB

在 Linux 中,PCB 对应的就是task_struct

task_struct 里会包含进程的很多属性
如:

  • 进程标识符
  • 状态
  • 优先级
  • 程序计数器
  • 内存指针
  • 上下文数据
  • I/O 状态信息
  • 记账信息

可以把它理解成进程的档案袋,进程要被管理,先得有档案。


3-1-3 task_struct

Linux 内核中,进程就是靠 task_struct 描述的。它会被装载到内存里,里面保存了进程的完整信息。

常见字段为:

  • 标识符:区分不同进程
  • 状态:表示当前处于运行、睡眠、停止等哪种状态
  • 优先级:决定调度先后
  • 程序计数器:下一条将要执行的指令地址
  • 内存指针:代码段、数据段以及共享内存的相关信息
  • 上下文数据:寄存器现场
  • I/O 状态信息:文件列表、I/O 请求等
  • 记账信息:CPU 时间、执行时间限制等

所有进程在内核里还会以双链表的形式组织起来,方便统一管理。


3-1-4 查看进程

查看进程信息有两种常见方式:

  1. 直接看/proc文件系统
  2. 使用topps这类用户态工具

比如 PID 为 1 的进程信息,可以查看/proc/1目录。


3-1-5 通过系统调用获取进程标识符

进程最重要的两个标识符是:

  • PID:进程号
  • PPID:父进程号
#include<stdio.h>#include<sys/types.h>#include<unistd.h>intmain(){printf("pid: %d\n",getpid());printf("ppid: %d\n",getppid());return0;}

这两个接口非常常用,尤其在父子进程、守护进程、进程树分析里。


3-1-6 通过系统调用创建进程-fork初识

创建子进程要用fork

基本概念

  • fork有两个返回值
  • 父子进程代码共享
  • 数据各自拥有一份
  • 采用写时拷贝机制
#include<stdio.h>#include<sys/types.h>#include<unistd.h>intmain(){intret=fork();printf("hello proc : %d!, ret: %d\n",getpid(),ret);sleep(1);return0;}

通常会结合if做分流:

#include<stdio.h>#include<sys/types.h>#include<unistd.h>intmain(){intret=fork();if(ret<0){perror("fork");return1;}elseif(ret==0)// 子进程{printf("I am child : %d!, ret: %d\n",getpid(),ret);}else// 父进程{printf("I am father : %d!, ret: %d\n",getpid(),ret);}sleep(1);return0;}

注意

  • 为什么fork会有两个返回值
  • 返回值如何分别给父进程和子进程

这两个问题后面会深入讲进程复制机制。


3-2 进程状态

3-2-1 Linux内核源代码

Linux 内核里,进程状态不是单一的,而是有多种状态。

常见状态包括:

  • R:运行状态
  • S:睡眠状态
  • D:磁盘休眠状态
  • T:停止状态
  • X:死亡状态
  • Z:僵尸状态

3-2-2 进程状态查看

查看进程状态常用命令是:

  • ps aux
  • ps axj

其中:

  • a:显示终端上的所有进程
  • x:显示没有控制终端的进程
  • j:显示进程组 ID、会话 ID、父进程 ID 等作业控制信息
  • u:以用户为中心显示详细信息

3-2-3 Z(zombie)-僵尸进程

它的形成过程是:

  • 子进程已经退出
  • 父进程还活着
  • 父进程没有调用waitwaitpid回收子进程状态
  • 子进程就会进入 Z 状态

僵尸进程本身不再运行,但它的退出信息还留在进程表中,等待父进程读取。

例子:

#include<stdio.h>#include<stdlib.h>#include<unistd.h>intmain(){pid_tid=fork();if(id<0){perror("fork");return1;}elseif(id>0){printf("parent[%d] is sleeping...\n",getpid());sleep(30);}else{printf("child[%d] is begin Z...\n",getpid());sleep(5);exit(EXIT_SUCCESS);}return0;}

僵尸进程危害

僵尸进程会一直占着进程表中的一部分信息,因为它的退出状态还要保留给父进程查看。

这意味着:

  • PCB 相关信息不能立刻释放
  • 如果父进程长期不回收,资源会被浪费
  • 大量僵尸进程会造成系统压力

注意,这类泄漏不是内存块忘了释放,而是进程状态和控制块长期无法回收。


3-2-5 孤儿进程

如果父进程先退出,子进程还在运行,那么这个子进程就叫做孤儿进程

孤儿进程不会一直无人管理,它会被 1 号进程,也就是initsystemd进程领养,后续由它负责回收。

#include<stdio.h>#include<unistd.h>#include<stdlib.h>intmain(){pid_tid=fork();if(id<0){perror("fork");return1;}elseif(id==0){printf("I am child, pid : %d\n",getpid());sleep(10);}else{printf("I am parent, pid: %d\n",getpid());sleep(3);exit(0);}return0;}

孤儿进程本身不是错误,但要理解它的归宿是被系统接管。


3-3 进程优先级

3-3-1 基本概念

CPU 资源分配的先后顺序,就是进程的优先级。

优先级高的进程,通常更早获得执行机会。

这在多任务系统里非常重要,因为 CPU 资源有限,进程很多,必须通过优先级来协调竞争。


3-3-2 查看系统进程

ps -l的输出中,常见字段有:

  • UID:执行者身份
  • PID:进程号
  • PPID:父进程号
  • PRI:实际执行优先级
  • NI:nice 值

3-3-3 PRI and NI

PRI可以理解为真正的优先级,值越小越优先执行。

NI是 nice 值,可以理解成优先级修正值。

二者关系可以写成:

PRI(new)=PRI(old)+nice

所以:

  • nice 值越小,优先级越高
  • nice 值越大,优先级越低

Linux 中 nice 的取值范围通常是-2019


3-3-4 PRI vs NI

区分:

  • PRI是进程优先级
  • NI不是优先级本身
  • NI是影响优先级的修正值

也就是说,nice 值只是调整项,不是最终值。


3-3-5 查看进程优先级的命令

可以用top修改已有进程的 nice 值:

  • 先输入top
  • 再按r
  • 输入进程 PID
  • 再输入新的 nice 值

其他命令还有:

  • nice
  • renice

相关系统函数:

#include<sys/time.h>#include<sys/resource.h>intgetpriority(intwhich,intwho);intsetpriority(intwhich,intwho,intprio);

3-3-6 竞争、独立、并行、并发

概念含义
竞争性多个进程争抢有限 CPU 资源
独立性多个进程互不干扰,各自运行
并行多个进程在多个 CPU 上同时运行
并发多个进程在一个 CPU 上通过切换推进执行

并行强调真正同时执行并发强调一段时间内都能推进


3-4 进程切换

进程切换的本质是 CPU 上下文切换,也就是:

  • 保存当前任务的寄存器现场
  • 恢复下一个任务的寄存器现场
  • CPU 转去执行新的任务

注意,时间片是分时系统里非常重要的概念。每个进程都会被分配一定的 CPU 时间片,时间片到了,就会被系统切走。


3-4 Linux2.6内核进程O(1)调度队列

Linux 2.6 时代的调度器采用过 O(1) 调度思想,目标是:查找最合适进程的时间复杂度尽量保持常数级

基本结构

每个 CPU 对应一个 runqueue。

如果有多个 CPU,就要考虑负载均衡问题。

调度队列分为两个核心数组:

  • active:活动队列
  • expired:过期队列

优先级划分

  • 普通优先级:100 到 139
  • 实时优先级:0 到 99

普通进程最常见。

活动队列

时间片没有用完的进程都放在活动队列中。

  • nr_active:活动进程数
  • queue[140]:每个优先级一个队列
  • 同优先级按 FIFO 排队

查找过程大致是:

  1. 从低下标开始遍历
  2. 找到第一个非空队列
  3. 取出队首进程执行

为了提高效率,还会配合 bitmap 记录哪些队列非空。

过期队列

时间片用完的进程会进入过期队列。

当活动队列处理完后,再把过期队列中的进程重新计算时间片,交换 active 和 expired 指针,继续下一轮调度。

所以整个查找过程可以保持接近 O(1) 的效率。


四、命令行参数和环境变量

4-1 基本概念

环境变量是操作系统中用来描述运行环境的一组参数。

它有几个典型特点:

  • 和系统配置有关
  • 具有全局属性
  • 会被子进程继承

比如编译和链接时,程序能找到动态库或静态库,很大程度上就和环境变量有关。


4-2 常见环境变量

常见环境变量包括:

  • PATH:命令搜索路径
  • HOME:当前用户主目录
  • SHELL:当前使用的 shell,一般是/bin/bash

4-3 查看环境变量方法

查看某个环境变量:

echo$NAME

比如:

  • echo $PATH
  • echo $HOME

关于 PATH 的理解

如果一个程序不在系统默认路径里,通常要写完整路径才能执行。

把程序所在目录加入PATH后,就可以直接运行。


4-4 和环境变量相关的命令

常见命令有:

  1. echo:查看变量值
  2. export:导出环境变量
  3. env:显示所有环境变量
  4. unset:清除环境变量
  5. set:显示 shell 变量和环境变量

4-5 环境变量的组织方式

每个程序启动时,都会收到一张环境表。

这张环境表本质上是一个字符指针数组:

  • 每个元素指向一个环境字符串
  • 每个字符串都以\0结尾

4-6 通过代码如何获取环境变量

方法一:通过命令行参数传入的 env

#include<stdio.h>intmain(intargc,char*argv[],char*env[]){inti=0;for(;env[i];i++){printf("%s\n",env[i]);}return0;}

方法二:通过全局变量 environ

#include<stdio.h>intmain(intargc,char*argv[]){externchar**environ;inti=0;for(;environ[i];i++){printf("%s\n",environ[i]);}return0;}

environ没有包含在常规头文件中,所以使用前要extern声明。


4-7 通过系统调用获取或设置环境变量

常用函数是:

  • getenv
  • putenv

示例:

#include<stdio.h>#include<stdlib.h>intmain(){printf("%s\n",getenv("PATH"));return0;}

getenv用来获取指定环境变量的值。


4-8 环境变量通常是具有全局属性的

环境变量可以被子进程继承。

#include<stdio.h>#include<stdlib.h>intmain(){char*env=getenv("MYENV");if(env){printf("%s\n",env);}return0;}

如果只设置:

MYENV="helloworld"

但不export,程序通常拿不到。

如果执行:

exportMYENV="hello world"

再运行程序,就能读取到。

这说明:导出的环境变量会进入环境表,并被子进程继承


五、程序地址空间

核心:

  • 程序地址空间
  • 进程地址空间
  • 虚拟地址
  • 物理地址

5-1 研究平台

这里讨论的环境是:

  • kernel 2.6.32
  • 32 位平台

地址空间布局和内核行为会受平台影响。


5-2 程序地址空间查看

验证不同区域的地址分布:

#include<stdio.h>#include<unistd.h>#include<stdlib.h>intg_unval;intg_val=100;intmain(intargc,char*argv[],char*env[]){constchar*str="helloworld";printf("code addr: %p\n",main);printf("init global addr: %p\n",&g_val);printf("uninit global addr: %p\n",&g_unval);staticinttest=10;char*heap_mem=(char*)malloc(10);char*heap_mem1=(char*)malloc(10);char*heap_mem2=(char*)malloc(10);char*heap_mem3=(char*)malloc(10);printf("heap addr: %p\n",heap_mem);printf("heap addr: %p\n",heap_mem1);printf("heap addr: %p\n",heap_mem2);printf("heap addr: %p\n",heap_mem3);printf("test static addr: %p\n",&test);printf("stack addr: %p\n",&heap_mem);printf("stack addr: %p\n",&heap_mem1);printf("stack addr: %p\n",&heap_mem2);printf("stack addr: %p\n",&heap_mem3);printf("read only string addr: %p\n",str);for(inti=0;i<argc;i++){printf("argv[%d]: %p\n",i,argv[i]);}for(inti=0;env[i];i++){printf("env[%d]: %p\n",i,env[i]);}return0;}

通过打印可以看出:

  • 代码段有自己的地址
  • 全局变量有自己的地址
  • 堆和栈分布不同
  • 字符串常量在只读区
  • argv 和 env 也都有自己的位置

这说明进程内存布局是有规律的。


5-3 虚拟地址

如:

#include<stdio.h>#include<unistd.h>#include<stdlib.h>intg_val=0;intmain(){pid_tid=fork();if(id<0){perror("fork");return0;}elseif(id==0){g_val=100;printf("child[%d]: %d : %p\n",getpid(),g_val,&g_val);}else{sleep(3);printf("parent[%d]: %d : %p\n",getpid(),g_val,&g_val);}sleep(1);return0;}

输出会出现一个很关键的现象:

  • 父子进程变量地址一样
  • 但变量值不一样

这说明:

  • 这个地址不是物理地址
  • 它是虚拟地址

所以我们在 C 和 C++ 里看到的地址,全部都是虚拟地址。物理地址对用户是不可见的,由操作系统统一管理。


5-4 进程地址空间

以前常说程序地址空间,但更准确的说法其实是进程地址空间

原因:

  • 每个进程都有自己独立的地址空间
  • 同一个虚拟地址,在不同进程里可以映射到不同物理地址
  • 进程之间互不干扰

进程地址空间由操作系统通过页表进行映射和管理。

描述进程地址空间的核心结构体是:

  • mm_struct:内存描述符

task_struct中,有一个指向mm_struct的指针。


5-5 虚拟内存管理

task_structmm_struct的关系,可以理解为:

  • task_struct管进程整体
  • mm_struct管进程地址空间

mm_struct描述的是整个用户空间。每个进程都有自己的mm_struct,这样才能拥有独立地址空间。

常见字段包括:

  • mmap:指向虚拟区间链表
  • mm_rb:红黑树根
  • task_size:进程虚拟地址空间大小
  • start_codeend_code:代码段范围
  • start_dataend_data:数据段范围
  • start_brkbrk:堆区范围
  • start_stack:栈区起始
  • arg_startarg_end:命令行参数区域
  • env_startenv_end:环境变量区域

vm_area_struct

Linux 用vm_area_struct表示一个独立的虚拟内存区域,也叫 VMA。

一个进程通常会有多个 VMA,分别表示:

  • 代码区
  • 数据区
  • 堆区
  • 栈区
  • 文件映射区
  • 共享内存区

vm_area_struct的一些典型字段:

  • vm_start:起始地址
  • vm_end:结束地址
  • vm_nextvm_prev:链表前后指针
  • vm_rb:红黑树节点
  • vm_mm:所属的 mm_struct
  • vm_flags:标志位
  • vm_file:映射文件
  • vm_private_data:私有数据

5-6 为什么要有虚拟地址空间

如果程序直接操作物理内存,会有什么问题?

1. 安全风险

如果每个进程都能直接访问物理内存,那么:

  • 程序可以随便读写系统内存
  • 木马和病毒更容易破坏系统
  • 进程之间也更容易互相干扰

显然不安全。

2. 地址不确定

程序编译好后存放在磁盘上,运行时才被加载到内存。

如果直接用物理地址,那么每次装载的位置都可能不同:

  • 第一次运行,内存可能很空
  • 第二次运行,内存已经有很多程序了
  • 地址每次都可能变化

这样程序就很难稳定运行。

3. 效率低下

如果直接使用物理内存,进程会以整体块的形式管理。

当内存不足时,想把不常用的程序换出到磁盘,就得整块搬移,效率很低。


虚拟地址空间的好处

有了虚拟地址空间和页表机制之后,问题就解决了很多:

  • 进程看起来都有独立的地址空间
  • 进程之间互不干扰
  • 物理内存可以被统一调度
  • 内存管理和进程管理可以解耦
  • mallocnew时,未必立刻分配物理内存
  • 需要访问时才真正触发分配,这叫延迟分配

虚拟地址空间的本质,是让进程看到一个稳定、连续、安全的内存空间


总结

  • 计算机所有设备最终都要和内存打交道
  • 操作系统的核心任务是管理
  • 进程是操作系统管理资源的基本单位
  • task_struct 是进程的核心描述结构
  • fork 可以创建子进程
  • 进程有运行、睡眠、僵尸、孤儿等状态
  • 优先级和调度决定进程执行顺序
  • 环境变量会被进程继承
  • 程序看到的是虚拟地址,不是物理地址
  • 虚拟地址空间让进程更安全、更稳定、更高效

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/15 17:25:04

搜索技能——anysearch技能

技能用途 anysearch 技能主要用于&#xff1a; 通用搜索&#xff1a;跨多个搜索引擎进行综合搜索结果聚合&#xff1a;整合多个平台的搜索结果信息检索&#xff1a;快速找到所需信息 安装方式 请先检查是否已安装 SkillHub 商店&#xff0c;若未安装&#xff0c;请根据 http…

作者头像 李华
网站建设 2026/6/13 14:02:32

人工智能专业术语详解(K)

在以字母K开头的术语中&#xff0c;人工智能领域呈现出三条截然不同却又彼此呼应的知识脉络&#xff1a;K-Means Clustering&#xff08;K均值聚类&#xff09; 将无监督学习的分组问题转化为一个迭代优化的几何过程&#xff1b;K-Nearest Neighbors&#xff08;K最近邻&#x…

作者头像 李华
网站建设 2026/6/15 17:16:36

【华为】vlan+NAT(Easy-IP 与 NAT Server)+ OSPF 动态路由综合配置

基础配置&#xff1a;命名与接口IP配置配置时不弹日志&#xff0c;界面干净进入系统配置模式关闭设备日志&#xff08;不推荐生产环境用&#xff09;修改设备名字Console 口永不超时&#xff0c;方便调试退出配置视图undo terminal monitor system-view undo info-center enabl…

作者头像 李华
网站建设 2026/6/12 17:42:50

深入理解Java内存模型:提升并发编程效率的关键

在现代软件开发中&#xff0c;随着多核处理器的普及&#xff0c;多线程编程已成为提升程序性能的关键手段。然而&#xff0c;多线程编程也带来了诸多挑战&#xff0c;其中最核心的问题之一就是如何正确处理共享数据的访问。Java内存模型&#xff08;Java Memory Model, JMM&…

作者头像 李华
网站建设 2026/6/13 23:58:04

从虚拟机到物理机:在CentOS 7单机上搭建OpenStack私有云的完整踩坑实录

从虚拟机到物理机&#xff1a;在CentOS 7单机上搭建OpenStack私有云的完整踩坑实录当你想在有限的硬件资源上体验企业级云计算平台时&#xff0c;OpenStack无疑是最佳选择之一。但官方文档往往假设你拥有多台服务器和充足资源&#xff0c;这让许多技术爱好者在旧服务器或高性能…

作者头像 李华