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,谁修改谁重新映射,但虚拟地址还是最初的那个

相关推荐
iuu_star1 小时前
Vue+FastAPI 项目宝塔Linux部署指南
linux·运维·fastapi
杜哥无敌1 小时前
FreeSSHd vs FileZilla Server vs SFTPGo:Windows SFTP服务器易用性终极横向测评
运维·服务器·windows
楼田莉子1 小时前
仿Muduo的高并发服务器:Channel模块与Poller模块
linux·服务器·c++·学习·设计模式
zhouwy1131 小时前
Linux网络编程从入门到精通
linux·c++
zhangrelay1 小时前
ROS Kinetic-信号与系统-趣味案例
linux·笔记·学习·ubuntu
IMPYLH1 小时前
Linux 的 tail 命令
linux·运维·服务器·bash
生成论实验室1 小时前
《事件关系阴阳博弈动力学:识势应势之道》第五篇:安全关键关系——故障、障碍与冲突
运维·服务器·人工智能·安全·架构
weixin_446260851 小时前
应用实战篇:利用 DeepSeek V4 构建生产级 AI 应用的全流程与最佳实践
大数据·linux·人工智能
maosheng11461 小时前
RHCE的第一次笔记
服务器·网络·笔记