Linux 进程核心原理精讲:从体系结构到实战操作(含 fork / 状态 / 优先级)----《Hello Linux!》(6)

文章目录

前言

进程是操作系统的核心骨架,所有程序的运行本质都是进程的调度与执行。理解进程的底层逻辑,不仅能打通操作系统、硬件与应用程序的关联,更能为排查性能问题、编写高效代码打下基础。

本文将从冯诺依曼体系结构出发,逐步拆解操作系统的核心职责,再深入进程的定义、PCB 结构、状态转换、优先级调度等核心知识点,同时搭配ps/top/fork等实操指令与代码示例,兼顾理论深度与实战性。无论是刚接触 Linux 系统的初学者,还是想夯实底层基础的开发者,都能通过本文理清进程的完整逻辑链,掌握从 "认识进程" 到 "操控进程" 的核心能力。

冯诺依曼体系结构

这里指的存储器是内存,是硬件级别的缓存空间(外存不算在这里)

外设分为输入设备和输出设备

输入设备:鼠标,键盘,磁盘,网卡...

输出设备:显示器,播放器硬件,磁盘,网卡

由此看出:有些设备既有输入功能,又有输出功能

中央处理器(CPU):由运算器和控制器组成

运算器:对数据进行计算任务(算数运算,逻辑运算啥的)

控制器:对计算硬件流程进行一定的控制

他们都是独立的个体!
不考虑缓存情况的话,这个中央处理器是不会跟输入输出设备直接联系的,都是通过存储器进行联系的

这样设计的好处:提高CPU的工作效率(不用等输入输出设备慢慢输,可以趁这个时间搞其他的)
一个程序要运行,就必须先加载到内存中运行的原因:冯诺依曼体系结构的规定

操作系统

概念:操作系统是一款进行管理的软件(可以管理软件和硬件)

要管理这么多东西--注定了操作系统里面存在大量的数据结构(因为先描述再组织)
为什么要有操作系统:

1.可以帮助用户管理好下面的软硬件资源

2.为用户提供一个良好(也就是稳定,高效,安全)的运行环境

也就是操作系统通过管理好底层的软硬件资源(手段),为用户提供一个良好的执行环境(目的)
操作系统里面由大量的数据,但是不想被用户获取

所以:为了保证自己的数据安全,也为了能够给用户提供服务,操作系统以接口的方式给用户提供调用的入口,来使用操作系统内部的数据

这个接口是操作系统自己提供的,用C语言实现的,像这种操作系统自己内部进行的函数调用,称为系统调用

--所有访问操作系统的行为,都只能通过系统调用来完成
操作系统管理数据的方法--先描述再组织(6个字很重要)

(也就是先描述数据属性,再把数据串起来)

在操作系统中,管理任何对象,最终都可以转化成对某种数据结构的增删查改
库函数和系统调用:

库函数是通过系统调用提供的接口实现的

这俩个在写代码的时候都可以用(但是要编程语言支持这个)

进程的概念

一个加载到内存中的程序(也就是正在运行的程序),叫做进程--这个说法不准确哈

进程 = 内核PCB数据结构对象 + 代码和数据

(代码和数据一直在,只有在运行的时候才会产生PCB)

PCB(进程控制块):进程属性的集合,描述了这个进程的所有属性
在Linux内核里,进程有时候也叫做任务

大多数Linux的指令都是bash的子进程
一个操作系统不仅仅只能运行一个进程,可以同时运行多个进程

任何一个"进程"在加载到内存形成真正的进程时,操作系统会先创建描述进程的结构体对象--也就是PCB

对进程做管理其实也就是对PCB做管理

"进程的执行有先后"其实是PCB在排队等执行,而不是代码和数据
具体到Linux是怎么处理进程的:

1.pcb在其中是task_struct结构体,里面包含进程的所有属性(先描述)

(也就是在Linux中描述进程的结构体叫做task_struct)

2.Linux中组织进程:用双向链表组织
如何通过链表节点(令为start)反向定位 task_struct,从而访问其他PCB的数据:

比如task_struct里面跟start同位置的是link

那么定位starttask_struct的方法:

cpp 复制代码
task_struct*( (int)start - &(int)(task_struct*)0->link)
&(int)(task_struct*)0->link--其实也就是看偏移量(看离0地址的task_struct的头有多远)
注意:这里的指针减指针的时候记得强转一下

task_struct内容分类

标示符: 描述本进程的唯一标示符,用来区别其他进程。

状态: 任务状态,退出代码,退出信号等。

优先级: 相对于其他进程的优先级。

程序计数器(也叫做PC指针): 程序中即将被执行的下一条指令的地址。

cpp 复制代码
作用:比如,在程序需要跳转到其他地方的时候,就先保存一下现在的地址,之后返回要用
系统也就是用的这个来得知当前执行到哪行代码了

内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针

上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。

I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。

记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。

其他信息
所有指令的运行都是进程

查看进程

引申:1.如果想要干掉一个进程的话,可以kill -9 PID值(ctrl+c干不掉时用此可以试试)

复制代码
      `kill`有很多种方法,不止`-9`,可以输`kill -l`去看

      2.程序里面遇到`exit(0)`的话是会关闭进程的--也就是关闭程序,跟遇到`return 0`差不多

/proc方法

语法: ls /proc查看当前所有的进程信息

复制代码
    `ls /proc/PID值`看这个进程的信息
bash 复制代码
eg: ls /proc/28128 -l
这里面的cwd表示的是PID对应那个进程的工作目录

ps指令

语法:ps ajx | head -1就是查看系统中进程的状态信息(只用第一行,所以-1)

PPID是当前进程的父进程的ID值 --PPID一般不变,在同一终端下的话

bash 复制代码
PPID一般都是一样的,比如:用命令行来使用Linux的话,父进程都是bash

PID是进程的ID值(是唯一的),如果进程结束,再重新开始,PID大概率会变

--PID值也是个目录其实

COMMAND是进程对应的 "命令名称或启动路径"

语法: ps ajx | grep 文件名可以查询这个文件对应的这些信息

bash 复制代码
这里的grep --color=auto myprocess是grep的进程(因为也带myprocess,所以被搜到了)
如果不想要的话就:ps ajx | grep proc | grep -v grep去过滤(不含grep的才留下)

ps一般这样用:ps ajx | head -1 ; ps ajx | grep 文件名

bash 复制代码
关于;和&&
;是不管前一个命令是否成功,都会执行后一个命令
&&是只有当前面的命令执行成功时,才会执行后面的命令

通过系统调用获取进程标示符

比如:getpid和getppid这俩个系统调用接口(具体的用man指令去查)

进程的特性

1.竞争性

2.独立性:多进程运行期间互不干扰

3.并行:多个进程在多个CPU下分别同时进行运行

4.并发:多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进
并发的逻辑:用的是基于进程切换和基于时间片进行轮换的调度算法

相同优先级的时间片用完之后就会放入waiting队列里面--到时runwait交换一下,相同优先级的不同进程间的顺序跟之前一样--这几个名字借鉴的下面一个问题的

上一个优先级时间片全用完了就轮到下一个优先级开始了

注意:进程在被切换时需要保存上下文和恢复上下文

--所以,进程在从CPU上离开时,要将自己的上下文数据保存好甚至带走

进程切换时,内核会把 CPU 里的寄存器数据(即进程上下文)拷贝到该进程 task_struct关联的内存结构里
引申:函数返回值被外部拿到,是通过的CPU寄存器

--寄存器的作用:提高效率,进程高频数据会被放入寄存器中

--CPU寄存器中保存的是进程相关的数据(其实就是进程的临时数据--进程的上下文)

--寄存器的种类也有很多,比如通用寄存器:eg: eax,ebx,ecx,edx

复制代码
                                             栈帧相关寄存器   `eg:ebp esp eip`

                                            状态寄存器          `eg: status`

通过系统调用创建进程-fork

fork也是一个系统调用 可以通过man fork去查他的用法

引申:系统调用接口不能在命令行那里用

简述一下用法:

1.返回值:(是pid_t类型的)

2.函数声明:
fork之后会产生一个子进程,原来那个变成父进程

父子进程的代码是共享的,但是数据和PCB不是

数据的话,在子进程想修改数据(修改的跟父进程不一样时)时,就会进行写时拷贝,把那一点想修改的数据单独开辟空间,其他的数据还是共享的 --父进程也同理
关于fork引申出的几个问题:

1.为什么成功时,fork给子进程返回0,给父进程返回的是子进程的pid值

原因:一个父进程可以有多个子进程,返回pid值的话才能分清楚是哪个

复制代码
    返回不同的返回值,可以让不同的执行流执行不同的代码块

  (借此特点,用完fork 之后通常要用 if 进行分流,来分开子进程和父进程)

2.一个函数是如何做到返回两次的(说的fork)

原因:fork函数内部的进程:

cpp 复制代码
1.先创建子进程PCB
2.填充PCB对应的内容
3.让子进程和父进程指向同样的代码啥的
然后就return 结果了(这一步的时候两个就分开执行了)

3.一个变量怎么会有不同的内容pid_t id = fork(); --以后再给出原因

4.如果父子进程用fork搞好了之后,谁先运行

这个由调度器决定,不确定的--兄弟进程谁先运行也是由调度器决定

cpp 复制代码
调度器:负责把进程搞到CPU上运行

bash创建子进程就是用了fork(执行外部命令,比如ls时,bash就会创建子进程)

进程状态

进程状态分为运行状态,阻塞状态和挂起状态等(还有比如阻塞挂起状态啥的)
运行态:会有个运行队列把进程穿起来,在运行队列里的进程都处于运行态(没给CPU的也算)
阻塞状态:每个设备都会有一个等待队列,在这个里面的处于阻塞状态,等设备写入到进程的数据里面,就会被交给运行队列(此时进程整体还是都在内存里的)
挂起状态:在阻塞状态的基础上,操作系统内部的内存资源严重不足了,就会把等待队列里面PCB对应的代码和数据先放在外存里面(PCB还是在内存里),此时这个进程就处于挂起状态(此时进程依然存在)

关于此的几个零碎的知识点

一个进程只要开始运行就会执行完毕才会停止吗--不是的

进程有个时间片的概念--时间片是操作系统分配给每个进程在 CPU 上运行的一段有限时间

在一个时间段内,所有的进程代码都会被执行(也叫做并发执行)

把进程从CPU上放上去,在取下来的操作叫做进程切换

具体到Linux的进程状态

R(running):是运行态

S(sleeping):是阻塞状态中的浅度睡眠--eg:进程等键盘写入数据

cpp 复制代码
引申: 程序看似一直在printf的话,其实是S状态
--因为运行进程是很快的,其实是一直在等IO设备执行完毕,然后继续运行进程的
     程序那步是eg:sleep(1)--其实也是s状态

D(disk sleep):是阻塞状态中的深度睡眠

cpp 复制代码
处于D状态的话是进程正在等待磁盘写入完毕--此时操作系统是不会去杀这个进程的
--不然会数据丢失

引申:操作系统在内存不足时会把不好的进程给杀掉

T(stopped):停止状态--跟阻塞状态的区别就是T可能是停下来等写入的,也有可能是单纯停下来的

t(tracing stop):也是停止状态--跟T差不多--比如像gdb遇到断点停下时时就会出现t

X(dead):死亡状态

Z(zombie):僵尸状态

关于挂起状态的话,操作系统是不会跟你说这个状态的

像这种状态里面出现+的话是表示进程在前台运行,此时用不了命令行解释

在后台运行的进程要杀掉的话,ctrl+c没用,要用kill那种方法

想让他在后台运行的话,要在运行时加& eg: ./text & 或者把进程用kill暂停再运行之后,也会成在后台运行的

僵尸进程

子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态

僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码
僵尸状态的危害:

会导致内存一直被占用(尤其是PCB不能被释放)--导致内存泄漏

孤儿进程

如果父进程比子进程早结束的话,子进程的父进程就会被改成PID为1的那个进程(init 进程)(也就是操作系统),此时这个子进程就叫做孤儿进程

如果不这样搞的话,孤儿进程就没人管了,没人将他从Z状态搞出去了

(孤儿进程本身不是一直是Z状态哈,进Z状态的条件跟其他人一样)

进程优先级

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

必要性:进程要去抢CPU,CPU有限--所以要有优先级
优先级要跟权限区分:

优先级是谁先谁后 权限是谁能谁不能
如果进程长期得不到CPU运行的话,该进程就一直止步不前--这是进程的饥饿问题

查看系统进程

ps -l查看这个终端的进程

ps -al查看拥有这个终端的用户底下所有终端的所有进程

UID : 代表执行者的身份

cpp 复制代码
ls那个指令加-n的话,就会把执行者的身份用数字的这种方式展示

PRI :代表这个进程可被执行的优先级,其值越小越早被执行

NI :代表这个进程的nice值--是进程优先级的修正数据

关于PRI和NI

nice取值范围是[-20,19]

PRI的取值范围一般是[60,99]

加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice--上图显示的就是PRI(new)

--注意:PRI(old)永远是80

top指令更改nice

top要超级用户或者sudo才行哈 --一般不建议改优先级!
使用方法:

输入top,然后按r,就会这样

然后再输入想改的进程的PID值,回车

输入想要的nice值,回车,然后就OK了

系统如何通过优先级进行调度

cpp 复制代码
比如有个runqueue:
struct runqueue
{
bitMap isempty;
//位图,bitMap也是自定义的结构体,里面有eg: char bitmap[140]
//数组里面的[100,139]一共40个位图--对应40个优先级
task_struct** run;
task_struct** wait;
//run里面进程被调度完了就swap(&run,&wait)这样
task_struct* running[140];//这样就形成一个队列
task_struct* waiting[140];
//一般都是[0,99]给其他种类的进程存(没学),[100,139]给这里的进程(PRI从60到99的)存
}

每个task_struct里面又有双向链表的特性,把PRI值相同的进程给串起来

想要快速查找最高优先级进程的话:用到位图

--eg:优先级为60的进程加一个,就在100下标那个位图给+1

--在找时,就要位图等不等于0,不等于的话就说明有这个优先级的

--这也就是Linux的O(1)级别的调度算法

--如果嫌弃位图太多,还可以eg:一个位图代表8个优先级--每四个二进制表示一个优先级这样

相关推荐
Lethehong6 小时前
从零到精通:Docker容器化完整指南与实战练习
运维·docker·容器
一个不知名程序员www6 小时前
算法学习入门---二叉树
c++·算法
小李小李快乐不已6 小时前
数组&&矩阵理论基础
数据结构·c++·线性代数·算法·leetcode·矩阵
利刃大大6 小时前
【JavaSE】十五、线程同步wait | notify && 单例模式 && 阻塞队列 && 线程池 && 定时器
java·单例模式·线程池·定时器·阻塞队列
feifeigo1236 小时前
SVM分类在高光谱遥感图像分类与预测中的应用
算法·支持向量机·分类
永恒-龙啸6 小时前
spooling假脱机输入输出模拟
c语言·开源·github
kevin_水滴石穿6 小时前
docker-compose.yml案例
java·服务器·开发语言
coderxiaohan6 小时前
【C++】用哈希表封装unordered_map和unordered_set
开发语言·c++·散列表
三川6986 小时前
AVL树的学习
数据结构·学习·算法