Linux —进程概念

目录

一、认识冯·诺依曼体系结构

冯·诺依曼体系定义了计算机的五大核心部件,设备之间是相互链接的:

  • 输入设备:键盘、鼠标、摄像头、硬盘、网卡等,负责把数据写入存储器(内存)
  • 输出设备:显示器、声卡、硬盘、网卡等,是从存储器读取数据
  • 存储器 :核心就是内存,本质上是一个巨大的缓存区,数据和程序都要先到这里,CPU才能处理
  • 运算器:负责加减乘除、逻辑判断等所有计算工作
  • 控制器:整个计算机的指挥中心,协调所有部件按步骤干活

重点:冯诺依曼结构图就表明了存储器(内存)是整个数据交互的中心枢纽

核心体现:

  1. CPU不直接和外设打交道,只和内存打交道
  2. 外设的输入/输出数据,也必须先交给内存,再由CPU处理或写回

💡关键理解

  1. 数据流动的本质是拷贝:数据不是直接 "瞬移" 到另一个设备上,而是从一个设备复制到另一个设备。比如你打字,键盘把信号拷贝到内存,CPU从内存读取计算,将结果拷贝到内存,最后拷贝到显示器上

  2. 数据在设备间拷贝的快慢,直接决定了电脑的整体运行效率

补充:存储金字塔

这张金字塔讲的是计算机里不同存储设备的层级关系,记住这个规律:
离 CPU 越近 → 速度越快、容量越小、成本越高

冯诺依曼体系为什么要这么设计?

硬盘是离CPU最远的设备,可以说速度是非常慢的。但是CPU的速度是非常快的,这就导致两者的时间差很大,直接让CPU等硬盘会浪费大量的时间,所以设计了缓存(内存)来作中转站,把内存作为中间层,就可以用"缓冲"的方式解决速度差问题:外设先把数据放到内存,CPU再按需读取,处理完再写回内存,外设再慢慢输出,互不阻塞

二、操作系统

1.操作系统的本质

操作系统是管理软硬件资源的软件。对上给用户和应用程序提供统一、简单的接口,不用直接操作复杂的硬件,对下管理所有的硬件资源(CPU、内存、磁盘、网卡等)

💡关键 :**用户/程序永远不能直接操作硬件,必须通过操作系统!**就像你去银行存钱,不能直接进金库,必须通过柜员(操作系统)办理,银行谁也不相信只相信自己,OS也是只相信自己

2.操作系统管理的核心逻辑: 先描述,再组织

例子:老师是如何 "管理" 学生的?

1.先描述

要管理一个对象(学生/进程/硬件设备),首先得先用数据结构把它的属性信息描述出来

管理学生:用struct stu结构体,记录姓名,学号,成绩等信息进行描述。

管理进程:操作系统会用struct task_struct这样的结构体,记录进程的ID,状态,优先级,内存地址等信息。

管理硬件:比如strcut dev结构体,记录设备的类型,厂商,状态等。

简单说:你要管理它,就得先把它的信息用数据结构 "装起来"

  1. 再组织

描述完对象后,操作系统会用链表,队列,哈希表等数据结构,把这些结构体组织起来,方便统一的管理。

管理学生:用链表struct stu* head把所有的学生串起来,方便增删改查。

管理进程:用进程链表,进程队列,把所有的进程按照状态或其他进行组织,方便操作系统调度

💡本质:操作系统对所有资源的管理,本质上都是对数据的管理,也就是数据结构! 这也是为什么学操作系统前要先学数据结构的原因。

3.操作系统的分层架构

由下到上:硬件 → 驱动 → 操作系统内核 → 系统调用 → 库函数 / Shell → 可执行程序 → 用户,每一层都在为上层提供服务,同时隐藏了下层的复杂性。分层抽象,每一层只关心自己的事情,不管下一层的实现细节。

三、深入理解进程概念

1.进程概念

操作系统中,进程可以同时存在非常多!操作系统得对进程进行管理 (先描述,再组织)

  • 要管理进程,首先得用数据结构把进程的信息 "描述" 出来,这就是PCB(process control block 进程控制块)
  • 再用链表/队列把所有PCB组织起来,方便操作系统管理调度

PCB里面都有啥?

在Linux下,struct PCB实际上是struct task_struct包含:

  1. 进程标识信息:PID(进程ID),PPID(父进程ID)每个ID都是唯一的
  2. 进程状态:运行,就绪,阻塞,挂起
  3. 资源信息:打开的文件描述符,内存地址空间,CPU寄存器上下文
  4. 链接信息:struct task_struct* next,把所有进程串起来,方便管理

进程的本质:进程=内核task_struct结构体+程序的代码和数据

PID:进程的唯一标识

  • 每个进程都有自己的唯一标识PID,存在task_struct
  • 程序可以通过系统调用getpid()获取自己的进程PID,通过系统调用getppid()获得父进程的PID
  • 每次运行同一个程序,都会创建新的进程,PID也会不同

2.创建进程

图文解释:

在程序运行前,操作系统内核首先从磁盘被加载到内存并常驻。这是因为操作系统作为硬件和用户之间的"中间层",负责提供内存分配,进程创建,CPU调度,中断处理等核心服务。若操作系统只在磁盘上,每次服务请求都需要读取磁盘,系统性能无法满足运行需求。随后,用户程序的可执行文件(如processbar)被加载到内存,形成代码段和数据段。此时,操作系统为该程序创建PCB,记录其PID,状态,内存指针等信息,并将其加入到系统进程管理链表。只有当PCB创建完成后,程序才真正成为一个被操作系统管理的进程,可被CPU调度执行。

从磁盘到运行的顺序:

为什么是这个顺序?

如果先创建PCB在加载到内存 :PCB里面的内存指针一开始是 "空的",没有指向任何有效地址,操作系统无法知道这个进程的代码和数据在哪里,就无法调度CPU执行
先加载再创建PCB:PCB的内存指针就可以直接指向已经加载好的内存区域了,操作系统拿到PCB就能直接找到进程的所有资源,完成调度,执行,管理。

总结:

  • 硬盘上的processbar二进制文件,只是一段静态的代码和数据
  • 当运行的时候,操作系统会把它加载到内存,创建一个进程,为它分配资源,让CPU执行它的指令

3.进程状态

Linux内核用struct task_struct里的state字段标识进程状态,内核里定义了对应的字符串数组:

c 复制代码
static const char* const tast_state_array[]={
	"R(running)",      //0  --运行/就绪状态
	"S(sleeping)",     //1  --可中断睡眠
	"D(disk sleep)",   //2  --不可中断睡眠(深度睡眠)
	"T(stopped)",      //4  --暂停状态(可恢复)
	"t(tarcing stop)", //8  --调试暂停(gdb断点)
	"X(dead)",         //16 --进程完全退出(被回收)
	"Z(zombie)"        //32 --僵尸进程(已退出,等待父进程回收)
}
状态 符号 核心特点 典型场景
运行态 R 进程要么正在CPU上运行,要么在运行队列里面等待调度(就绪) 所有正在占用CPU,或者排队等CPU的进程
可中断睡眠 S 进程等待资源(IO资源等)可被信号唤醒 read()/scanf()阻塞,等待用户输入的进程
不可中断睡眠 D 深度睡眠,不响应信号,只能自己醒来 磁盘IO读写时(如写文件),怕被打断导致数据损坏
暂停态 T 进程被暂停,不允许也不退出,可被信号恢复 kill -STOP暂停进程,kill -CONT恢复进程
调试暂停 t 调试器(gdb)打断点暂停 进程停在断点处
死亡态 X 进程完全退出了,PCB被内核回收,不存在了 进程被正常回收后,就会进入这个状态,只是比较短暂,难以捕捉

1. 僵尸进程

成因 :子进程先执行完退出,父进程还活着,但是没有调用wait()/waitpid()读取子进程的退出信息
特点 :子进程的代码和数据已经释放,但PCB进程控制块会保留,状态显示为Z+
危害 :占用系统资源(PCB),如果大量僵尸进程堆积,会导致内核无法创建新进程(因为PID和PCB资源有限),会出现 "内存泄漏问题"
解决 :父进程调用wait()/waitpid()回收,或父进程退出后由init(PID=1)进程领养回收

示例:

c 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
	pid_t id=fork();
	if(id>0)
	{
	  //father
	  printf("我是父进程,PID=%d,PPID=%d\n",getpid(),getppid());
	  sleep(10);
	}
	else if(id==0)
	{
	  //child
	  printf("我是子进程,PID=%d,PPID=%d\n",getpid(),getppid());
	}
	return 0;
}    

让父进程休眠10s,子进程先退出,此时父进程还在休眠,没有及时回收子进程,会出现僵尸进程,10s后才会回收掉子进程

2. 孤儿进程

成因:父进程先退出,子进程还在运行,变成了"没人管的孩子"

特点:会被系统的init(PID=1) 的进程领养,变成init的子进程,退出时会被init自动回收,不会变成僵尸进程

意义:避免子进程退出后没人回收,导致资源泄漏

示例:

c 复制代码
int main()
{
	pid_t id=fork();
	if(id>0)
	{
		//father
		printf("我是父进程,PID=%d,PPID=%d\n",getpid(),getppid());
	}
	else if(id==0)
	{
		//child
		int cnt=5;
		while(cnt--)
		{
		   printf("我是子进程,PID=%d,PPID=%d\n",getpid(),getppid());
		   sleep(2);                                                                  
		}
	}
	return 0;
}

运行结果:现象

为什么PPID会变成1?

从代码中可以看到,父进程是先结束的,当父进程(PID 6005)退出后,子进程(PID 6006)就失去了它的父进程,变成了一个孤儿进程。Linux系统规定:所有的孤儿进程,都会被系统的init(PID=1)的进程收养,所以子进程的父进程ID(PPID)会有6005变成1。
为什么命令提示符会再次出现?
谁占用终端,谁就拥有命令提示符。 最开始输入./testexe→bash运行这个程序,bash把终端控制权交给该进程,此时不会再显示命令提示符,fork出父子进程,父进程一退出,终端控制权立刻还给了bash,bash拿到控制权,马上打印命令提示符,但是不影响子进程的输出,所以就出现了上述的现象

3.调度、阻塞和挂起

状态流转:

就绪 → 运行:CPU调度器选中它,给它分配CPU时间

运行 → 就绪:时间片用完,被系统强制切换

运行 → 阻塞:进程主动等待事件(比如read读键盘),主动放弃CPU

阻塞 → 就绪:等待的事件发生(比如用户按下键盘,读取到键盘资源),被唤醒,重新进入就绪队列

阻塞状态

  • 当进程需要等待I/O(比如读键盘、读磁盘)时,会主动进入阻塞状态
  • 它不会留在CPU的运行队列里,而是被移到对应设备的等待队列(wait_queue)里。(比如读键盘的进程,会进入键盘设备的wait_queue)
  • 当设备完成I/O(比如用户按下键盘),设备驱动会唤醒等待队列里的进程,把它移回到CPU的运行队列,重新变成就绪状态

每个设备都有自己的wait_queue,就是用来管理等待它的进程的

挂起状态 :内存不足时的"磁盘交换"

挂起状态(Swap),解决的时"内存不够用怎么办"的问题:

  • 当内存不足时,操作系统会暂时把不活跃的进程的代码和数据,换出到磁盘的swap分区(虚拟内存)
  • 进程的task_struct(PCB,进程控制块)会留在内存里,但进程本身变成了挂起状态
  • 当进程需要运行时,再把它的数据从磁盘换回内存,恢复成就绪/运行状态

缺点:磁盘的读写速度远慢于内存,会导致系统性能下降(这就是"用时间换空间")

总结:

概念 理解
task_struct Linux里描述进程的结构体(PCB),包含进程状态、PID、优先级等所有信息
运行队列runqueue 存放所有就绪进程的队列,CPU调度器从这里选进程
等待队列wait_queue 存放等待I/O/事件的进程队列,每个设备都有自己的等待队列
时间片轮转 让多个进程再单核CPU上"同时运行"的调度方式
阻塞状态 进程主动等待事件,放弃CPU,进入设备等待队列
挂起状态 内存不足时,进程数据被换到磁盘swap分区,用时间换空间

4. 进程调度

进程调度的本质:操作系统怎么"同时" 跑多个进程?

1.调度的核心机制:进程队列

  • 操作系统会把所有的进程的struct task_struct,放到不同的队列里(运行队列,就绪队列,阻塞队列)
  • CPU每次只能执行一个进程,它会按时间片轮转,轮流执行队列里的进程

2.动态运行的核心:进程在不同的队列间切换
只要我们的进程task_struct将来在不同的队列中,进程就可以访问不同的资源

  • 进程处于运行队列时,CPU正在执行它的指令,它就能访问CPU资源了
  • 进程处于就绪队列时,它已经准备好运行,只等CPU调度
  • 进程处于阻塞队列时,它在等待IO(比如读文件,网络请求),CPU会切换去执行其他进程

💡对用户来说,多个进程好像在"同时"运行,但本质上时CPU在快速切换这些task_struct,这些都是"并发"给用户带来的错觉

并行 :同一时刻,多个进程真的在同时运行(前提是多核CPU,每个进程用各自的CPU,互不干涉)
并发:同一时间段内,多个进程轮流使用同一个CPU,通过快速切换,让用户感觉它们在"同时"运行(单核CPU,或者进程数量超过CPU数量,CPU靠时间片轮转调度,给进程轮流分配执行权)

5. Linux进程优先级

1.什么是进程优先级?

进程优先级,就是进程获取CPU资源的先后顺序

在Linux中,优先级数字越小优先级越高 ,进程的优先级信息,存储在task_struct(进程控制块)中,由内核维护

2.为什么要有优先级?

核心原因是资源有限

  • CPU资源是有限的,但系统中同时运行的进程很多
  • 如果没有优先级调度,系统可能会出现 "饥饿问题"---某些进程长时间得不到CPU时间片,永远无法运行
  • 优先级调度的目的是在"保证基本公平"的情况下,优先调度更重要的进程,同时避免其他进程饿死

3. Linux优先级的查看与修改

1.查看优先级的命令:ps -al查看所有终端的进程

列表头中的
PRI:表示进程优先级(默认都是80)
NI:nice值(这里是0)

nice值有范围限制:范围是-20,19,一共40个值,Linux的优先级是通过PRInice调整的,优先级PRI = PRI默认值80 + nice的则优先级PRI的范围在60,99,数字越小优先级越高,其中只有root能设置负的nice值,普通用户只能设置0~19,防止恶意程序抢占CPU

2.如何修改进程优先级?
int nice(int inc); 程序运行时自己修改优先级
参数 inc 不是直接设置的新nice值,而是增量

示例:在进程中修改

bash 复制代码
nice(5)  #当前的nice值+5(优先级降低)
nice(-2) #当前的nice值-2(优先级提高,需要root权限/指令提权)

返回值:成功返回修改后的新nice值,失败返回-1并设置errno

renice [新nice值] -p [PID] 进程已经跑起来了,在终端里手动修改它的优先级

示例:

bash 复制代码
renice 5 -p 1234 		#将PID为1234的进程nice值改为5
sudo renice -5 -p 1234  #将PID为1234的进程的nice值改为-5

6.进程切换

Linux进程切换的核心原理:上下文保护与恢复

1.什么是进程上下文?

进程在CPU上运行时,所有临时状态都存在CPU寄存器里,比如:

  • eax/ebx/ecx等通用寄存器(存计算结果,临时变量)
  • eip(指令指针,存下一条要执行的指令地址)
  • CR0~CR4控制寄存器(存进程的内存管理,状态信息)

CPU内部的所有寄存器中的临时数据,就叫做进程的上下文

2.进程切换的核心矛盾

CPU寄存器是硬件,只有一套,但系统里有很多进程。

矛盾点:进程1运行时,寄存器里全是它的数据,时间片用完了,要切换到进程2运行,如果直接覆盖寄存器,进程1的状态就全丢了,下次再切回来时,它根本不知道自己执行到哪里了,又要重头来,所以进程切换最关键的一步就是上下文的保存与恢复

3.进程切换的完整流程

1.保存上下文(切出进程1)

当进程1的时间片用完时,系统会把CPU寄存器里的所有数据(进程1的上下文),全部复制到进程1的task_struct(PCB进程控制块)里的"上下文数据"区域保存起来,这样进程1的状态就完整存好了,它可以被放到就绪队列里排队,等待下一次调度。
2.恢复上下文(切入进程2)

当调度器选中进程2运行时,系统会从进程2的task_struct里,把它之前保存的上下文数据,全部写回到CPU寄存器里,CPU寄存器恢复成进程2上次运行结束时的状态,进程2就可以接着之前的状态继续执行了。

四、环境变量

1. 命名行参数

1.int main(int argc, char* argv[])是什么?
argc是命令行参数的个数(包括程序本身在内)
argv是一个字符串数组,存了argc个命令参数,数组最后自动以NULL结尾

示例:

bash 复制代码
./myprocess -a -b -c

argc=4 (因为有./myprocess-a-b-c四个参数)
argv[0]=./myprocess(程序路径+名称)
argv[1]=-a
argv[2]=-b
argv[3]=-c
argv[4]=NULL

图文解释:在终端输入命令行./myprocess -a -b -c首先会背父进程bash接收,bash会解析这行命令,将它拆分成多个字符串,构建出argv数组,然后通过exec*系列系统调用传递给新创建的myporcess进程,所以argc/argv本质上是父进程传递给子进程的命令行参数

2.命令行参数的意义:像ls -l这类带选项的命令,都是通过解析argv来实现的,后续我们也可以自己实现一个简单的shell

2. 环境变量

1.什么是环境变量?

环境变量是bash进程里的全局配置信息,相当于系统给程序的"全局备忘录",用来告诉程序一些关键信息,比如:
PATH:命令搜索路径,bash会按这里的目录顺序找可执行文件
HOME:当前用户的家目录
PWD:当前所在的工作目录
SHELL:使用的shell类型
HISTSIZE:历史命令能保存的最大条数

2.查看和修改环境变量

env查看所有的环境变量

echo $环境变量名:查看单个变量的值

export name=value:创建/修改环境变量

把一个变量升级为环境变量,这样它就可以被当前Shell的子进程继承

HelloWorld声明成环境变量,可以通过查看单个变量的方式查看

在环境变量表中也能查看到HelloWorld变量,说明已经创建成功了

加export :环境变量(自己和子进程都能用),这个变量会被放进环境表 ,当前Shell能看见,子进程(比如你运行的C程序,bash脚本)也能继承

当不加export

也可以通过查看单个变量的方式查看

不加export :本地变量(只给自己用),这个变量只存在于当前bash进程里,当前Shell能看见,echo $name能输出,但子进程看不见

总结不加export(本地变量--局部),加export(环境变量--全局) ,两种变量都能用echo $xxx查看,

unset name删除环境变量

把一个环境变量从当前Shell中移除

HelloWorld从环境变量表中移除后,就找不到它的值了,已经不存在了

3.环境变量的完整生命周期

1.磁盘上的源头:配置文件

环境变量的初始定义都在这些文件里:

  • 系统级/etc/bashrc/etc/profile(所有用户生效)
  • 用户级~/.bashrc~/.bash_profile(当前用户生效)

当你登录系统时,bash会读取这些文件,把变量加载到自己的内存中

2.bash进程里的存储:environ

bash会把所有的环境变量组织成一个char *environ[]字符数组:

每个元素是键=值的形式的字符串(比如PATH=/usr/bin:......)

数组最后以NULL结尾,和argv数组的结构完全一样

这个数组是bash进程的内存级数据,用env命令看到的就是它

3.子进程的继承:从bash./myprocess

当bash执行./myprocess时:

  1. bash会创建子进程
  2. 把自己的argv[](命令行参数)和environ[](环境变量表)完整复制一份,传给子进程的main函数参数argv[]和envp[]
c 复制代码
int main(int argc, char* argv[], char* envp[])
{
    for(int i=0; envp[i]; i++) //可以通过envp来查看环境变量
    	printf("%s\n", envp[i]);
    return 0;
}
  1. 所以子进程启动后,能直接拿到父进程的所有环境变量,这就是"继承性"

程序还可以通过extern char** environ全局变量来查看环境变量

示例:

c 复制代码
#include<stdio.h>
extern char** environ;//声明系统全局的环境变量表
int main()
{
	for(int i=0;environ[i];i++)
		printf("env[%d]->%s\n",i,environ[i]);
	return 0;
}

编译运行后,会打印出和终端命令env一模一样的环境变量内容,因为它直接访问了bash传给它的environ

4.C语言操作环境变量的标注函数getenv和putenv
getenv:获取指定环境变量的值

c 复制代码
#include<stdio.h>
char* getenv(const char *name); //函数原型

用法:

c 复制代码
char* path=getenv("PATH");
printf("PATH=%s\n",path); //给它一个环境变量名,返回对应的值(只读),找不到返回NULL

putenv:添加/修改环境变量

c 复制代码
#include<stdio.h>
int putenv(char* string);//函数原型

作用:传入一个name=value格式的字符串,如果环境变量已经存在,则覆盖修改;若不存在,则新增

c 复制代码
putenv("Yes_No=123");

五、地址空间

1.页表和虚拟地址空间

根据代码看现象

矛盾点:父子进程的g_val地址完全一样,但值不一样,这说明它们绝对不是同一个物理变量

1.核心原理:虚拟地址vs物理地址

我们在C/C++代码里面看到的所有地址,其实都是虚拟地址。物理地址由操作系统和MMU(内存管理单元)统一管理,用户进程根本看不到,父进程的&g_val虚拟地址相同 ,但通过页表映射到了不同的物理地址

为什么地址会一样?
fork()创建子进程时,会完整复制父进程的虚拟地址空间和页表,所以父子进程的虚拟地址和布局完全一样,变量的虚拟地址自然也一样,最初它们的页表映射的是同一块物理内存(写时拷贝机制)。

写时拷贝:当子进程修改g_val=100时,触发了写时拷贝,操作系统会给子进程分配一块新的物理内存,把父进程那块的物理内存数据复制过来,修改子进程的页表,让0x60104c这个虚拟地址重新映射到新的物理内存,父进程的页表映射则不变,所以看见的还是100,谁修改谁重新映射,但虚拟地址还是最初的那个

相关推荐
大树8812 小时前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠12 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质12 小时前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush412 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行52012 小时前
Linux 11 动态监控指令top
linux
小宇宙Zz13 小时前
Maven依赖冲突
java·服务器·maven
Inhand陈工13 小时前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智14 小时前
ARP代理--工作原理
运维·网络·arp·arp代理
不会C语言的男孩14 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
shushangyun_14 小时前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化