Linux进程概念(1)
本次,我们讲解的是关于Linux的进程的基础概念认识:
首先,我们先从硬件层面谈起:得先了解一个计算机体系结构:
冯诺依曼体系结构:
一、硬件组成
(一)中央处理器(CPU) 组件
- 运算器:执行数据的算术运算(如加减乘除)和逻辑运算(如与或非)。
- 控制器:对计算机硬件部件进行控制,协调各组件工作。
(二)外设
- 输入设备:向计算机输入数据,如鼠标、键盘、摄像头、话筒、磁盘、网卡等。
- 输出设备:将计算机处理结果输出,如显示器、播放硬件、磁盘、网卡等;部分设备兼具输入输出功能。
- 存储器:这里指内存,是硬件级的缓存空间,处于核心地位。
二、工作原理
程序运行时,必须先加载到内存中执行,这是冯·诺依曼体系结构的规定。
三、组件连接
各个硬件单元通过“总线”链接,主要包括系统总线和 I/O 总线。
四、存储分级(存储金字塔)
从上层到下层,价格越来越低、容量越来越大、速度越来越慢、离CPU越来越远,层级如下:
(注:这种分级设计平衡了存储的速度、容量与成本。)ps:这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)
外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取
总结:所有设备都只能直接和内存打交道
举个例子:微信聊天,整个数据流是怎么流动的?
输入设备:键盘,网卡,磁盘.......
输出设备:显示器......
ps:了解完了冯诺依曼体系的原理后,我们再回过头来想想,上次我们写的进度条,默认显示的数据,是可能会缓存起来的,在哪里保存呢?就是在存储器。
谈完硬件层面后,现在,我们来谈一下关于软件层面的:
二、软件层面
一、操作系统
是什么?
操作系统是一款进行管理(对硬件,软件)的软件。
为什么?
1. 资源管理:帮助用户管理好计算机的软硬件资源。
操作系统通过管理底层的软硬件资源(手段),
为用户(使用者,程序员)提供一个良好的执行环境(目的)。
2. 环境提供:为用户提供稳定、高效、安全的运行环境。
操作系统的数据安全与接口
- 操作系统内部包含各种数据,但它不信任任何用户。
- 为保证自身数据安全并为用户提供服务,操作系统以接口的方式给用户提供调用入口,使用户能获取其内部数据。----(这个接口就是系统调用接口)(看上面的图片)
系统调用
- 是操作系统提供的用C实现的内部函数调用。
- 所有访问操作系统的行为,都只能通过系统调用来完成。
操作系统是一个进行管理的软件,那么它是怎么管理的呢?
管理的核心逻辑(如何管理)
先描述再组织
例子背景:一个学校那么多学生,学校领导是如何进行管理的?(简单版本)
需要的人物:
校长(决策者)->操作系统,
辅导员(执行者)-->驱动程序,
学生(被管理者)-->软硬件资源
(层层向上提供信息)
一、“描述-组织”的管理实现路径
以学生管理为例,展现“先描述,再组织”的过程:
- 描述过程:用结构体(如 struct student )定义学生的属性(学院、专业、班级、姓名、性别、身高、体重、籍贯、电话等),将“学生”这一管理对象数据化。- 组织过程:通过数据结构(如链表,借助 struct student *next 实现节点连接)将多个学生的信息组织起来,从而把“对学生的管理”转化为对链表(数据结构)的增删改查。
类似地,其他管理对象也可通过结构体(如 struct person 描述个人信息, struct contact 组织多人联系信息)实现“数据化-结构化”的管理。二、管理的核心逻辑
从上面知道,管理者与被管理者是不需要见面的,管理者在不见被管理者的情况下,如何做好的管理呢?只要能够得到管理信息,就可以在未来进行管理决策,
-->管理的本质:是通过对数据的管理,达到对人的管理。-->管理者和被管理者面都不见,那么我们怎么拿到对应的数据的?是通过执行者拿到的
eg(学校例子中的辅导员)
三、操作系统中的管理映射
在操作系统场景中,任何管理对象最终都可转化为对某种数据结构的增删改查。程序员开发软件时,需通过操作系统提供的接口(系统调用)来访问其内部数据结构,所有对操作系统的操作都依赖系统调用完成。总结:
1. 管理者和被管理者可以不见面,只要能拿到管理信息,就可在未来进行管理决策。
2. 管理的本质是通过对数据的管理,达到对人的管理。
3. 管理者和被管理者表面不见时,可通过执行者来实现对应的数据操作。因此:管理的核心六大字!!!!!
先描述再组织
再谈进程1
ps:现在只是简单版本的认识,等到后面学着学着会逐渐补充完善的。
有了上面的铺垫后,我们现在来了解一下关于进程的知识点。
是什么
- 一个已加载到内存中的程序,称为进程,也可理解为“任务”。
- 正在运行的程序,也叫进程。
二、进程的组成
进程 = 内核PCB数据结构对象 + 自身的代码和数据。(目前简单理解,其实还不算很准确)
**PCB(Process Control Block,进程控制块),Linux操作系统的PCB是task_struct**是描述进程所有属性的结构体对象,包含进程编号、状态、优先级、相关指针信息(如 struct PCB *next 用于链表组织)等属性集合。
三、操作系统对进程的管理逻辑
- 操作系统不仅可以运行一个还可同时运行多个进程,因此必须对进程进行管理,
怎么管理?**“先描述,再组织”**。
- 描述:进程加载到内存时,操作系统会先创建对应的PCB结构体对象,用其属性描述进程。
- 组织:通过PCB中的指针将多个进程组织成单链表(或其他数据结构),对进程的管理最终转化为对该链表的增删改查操作。
四、认知逻辑延伸
- 人们认知事物(包括进程)的方式,是通过属性的集合来识别;当属性足够丰富时,这些属性的组合就标识了具体对象(如进程的PCB属性集合标识了一个进程)。
五、看Linux下进程是如何做的?
在Linux中描述进程的结构体叫做task_struct。
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
Linux中是如何组织进程的,Linux内核中,最基本的组织进程task_struct的方式,采用双向链表组织的。
它的内容:
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
查看进程:
ps axj 查看所有进程
指令:
ls /proc/
指令:
top
指令:
ps aux | grep test |grep -v grep
ps:指令怎么知道进程在哪?
五、理解指令与进程的关系
在Linux系统中,用户输入的指令(如
pwd
、ls
、grep
)本质上是可执行程序或内置命令。当这些指令被执行时,操作系统会为其创建独立的进程。
pwd
和ls
这类指令的进程生命周期非常短暂,执行完成后立即终止。它们的输出通常是静态信息(如当前目录或文件列表),不会涉及复杂的系统状态遍历。六、grep进程的特殊性
grep
是一个用于文本搜索的工具,其工作原理是逐行扫描输入并匹配关键字。当执行grep
命令时:
- 操作系统为其创建新进程。
- 该进程会加载
grep
的代码到内存,并开始执行搜索逻辑。七、自包含过滤问题
当使用
grep
过滤包含自身关键字的进程时(例如ps aux | grep test
),可能出现以下现象:
grep test
本身是一个进程,其命令行参数包含关键字test
。ps aux
会列出所有进程信息,包括grep test
这个进程。- 因此过滤结果会包含
grep
进程自身。
指令:ls /proc/(进程编号) -l
查看进程指令(常用)
ps ajx | head -1 && ps ajx |grep test |grep -v grepps ajx | head -1 展示它的名称,方便我们看它对应的数是属于哪一个 && 并且,一起执行的意思 ps ajx |grep test |grep -v grep 打印出来进程,并过滤一下
系统调用来获取标识符
getpid:获取进程ID,即PID
getppid:获取父进程ID,即PPID
系统调用来创建进程:
fork()
我们可以在Linux下,man查一下。
tips:进去后,/return val (想追踪的信息,如果存在的话,都可以直接定位到该位置)
话不多说,先来段代码来初识一下:
看上面的图片,我们可能会又以下疑问:
问题:
1.为什么fork要给子进程返回0,给父进程返回子进程的pid
2.一个函数是如何做到返回两次的?如何理解?
3.一个变量怎么会有不同内容,如何理解?
接下来,我们就会围绕上面三个问题展开来讲:
一、 fork() 核心行为
fork() 是 Linux 系统调用,用于创建子进程。它会复制父进程的内核数据结构(如 task_struct ,包含 pid 、 ppid 等标识)以及代码等资源,生成与父进程几乎一致的子进程。之后,父、子进程会从 fork() 调用后的代码行开始,各自独立执行。
ps:进程=内核数据结构+代码和数据
进程中的数据是通过写时拷贝来实现的。
所以,我们就可以回答:
1.为什么fork要给子进程返回0,给父进程返回子进程的pid?
为了让父和子执行不同的事情!需要想办法让父和子进程执行不同的代码块。从而让fork具有了不同的返回值。
二、“函数返回两次”的本质
回答问题二:
ps:fork本质上也是一个函数,这里有点像我们的函数里面有个函数进行调用的形式。
fork() 调用后,系统同时存在父进程和子进程两个独立进程:
- 在父进程中, fork() 返回子进程的 PID(大于 0 的整数)。
- 在子进程中, fork() 返回 0。
从“进程执行流”视角看, fork() 仿佛在父、子进程中各返回一次,因此呈现“返回两次”的效果。
三、进程资源与共享逻辑
- 代码共享:父、子进程的代码是共享的(代码通常为只读,可安全共享)。
- 数据独立:数据是各自独立的(若共享数据,修改后会导致父、子进程数据不一致,所以 fork() 会为子进程复制独立的数据副本)。(写识拷贝)那么,如果父子进程被创建好,fork()往后,谁先运行呢?谁先运行,由调度器决定,不确定的。
因此,我们就可以回答第三个问题:3.一个变量怎么会有不同内容,如何理解?(如上)
四、创建子进程的目的与代码分支实现
- 目的:让父、子进程能执行不同的任务,实现多进程协作。
- 代码分支实现:利用 fork() 在父、子进程中返回值不同的特点(父进程返回子进程 PID,子进程返回 0),通过 if-else 判断返回值,让父进程和子进程进入不同的代码块执行(如父进程执行 id > 0 分支,子进程执行 id == 0 分支)。
bash通过创建子进程完成的,怎么创建?fork(),执行解释新命令
什么是bash??
bash”是Bourne-Again Shell的缩写,它是Linux和类Unix系统中最常用的命令行解释器(终端shell)。
简单来说,它的作用是:
- 接收并解释执行用户输入的命令,比如你在终端里输入的 ps 、 grep 这些指令,都是由bash来处理执行的。
区分bash与shell的区别:
Shell 是“命令解释器”的统称,bash 是 Shell 的一种具体实现,也是 Linux 中最常用的 Shell
进程状态
现在,我们来讲一下关于操作系统中常见的进程状态。
新建状态、就绪状态、运行状态、阻塞(阻塞/挂起)状态、终止状态
一、进程状态与生命周期
- 新建状态:进程刚被创建,尚未进入就绪队列。
- 就绪状态:进程已准备好执行,等待CPU调度。
- 运行状态:进程正在CPU上执行指令。
- 阻塞状态:进程因等待I/O事件等原因无法执行,需等待事件完成后回到就绪状态。
- 终止状态:进程执行完毕或异常退出。
- 状态切换:存在“新建→就绪→运行→阻塞→就绪”“运行→终止”等切换路径,体现进程在不同阶段的流转逻辑。
二、进程调度与时间片
问题:一个进程只要把它自己放到CPU上开始运行了,是不是一直要执行完毕,才把自己放下来?
答案是:不是的,它其中会借助一个叫时间片的机制:
- 时间片机制:每个进程都有“时间片”(如10ms),进程在CPU上执行的时间不会无限长,时间片耗尽后会被调度器换下CPU,回到就绪队列,从而让其他进程获得执行机会。
- 调度器与运行队列:通过 struct runqueue (运行队列)管理就绪进程, struct task_struct (进程控制块)记录进程的代码、数据及状态等信息,调度器负责从运行队列中选择进程分配CPU。
被放到运行队列的pcb,/就意味着告诉操作系统,我已经准备好了,可以随时进行调度!
三、并发执行与进程切换
- 并发的实现:在一个时间区间内,所有进程的代码通过“时间片轮转”被轮流执行,从而实现并发效果(宏观上多个进程同时运行)。
- 进程切换:大量进程在CPU上“被放上、被拿下”的动作即为进程切换,这是实现多任务并发的关键操作。
- 阻塞状态:进程因等待外设I/O(如键盘输入、磁盘读写)而无法执行的状态,需等待事件完成后才能回到就绪状态。
- 交换分区(swap):是磁盘上的一块区域,用于内存资源紧张时临时存储进程数据,实现内存与外存的“换入/换出”,保障多进程调度的连续性。
- 内存资源不足的调度
当“操作系统内部内存资源严重不足”时,会通过换出机制(将部分进程数据暂存到交换分区 swap )来“省出内存资源”,保证系统正常运行;当进程需要恢复执行时,再通过换入机制从 swap 分区将数据调回内存。
- 外设I/O的进程交互(以键盘为例)
进程发起键盘I/O请求后,若键盘处于“忙”状态,进程会被加入键盘的等待队列,进入阻塞状态;当键盘完成数据读取后,进程会被唤醒,从阻塞状态切换为就绪状态,等待CPU调度。(进程挂起状态)
总结:操作系统如何通过 struct dev 、 struct task_struct 等结构,结合内存换入换出、进程阻塞唤醒机制,实现外设I/O与内存资源的高效管理。
了解完进程状态后,,我们现在来认识一下:
具体的Linux状态是怎么样的?
*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */ 浅度睡眠 ,当遇到命令时是可以唤醒的
"D (disk sleep)", /* 2 */ 深度睡眠,不回应任何请求
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态
Z僵死状态(Zombies)是一个比较特殊的状态。
产生原因:进程退出时,若父进程未主动回收子进程的退出信息,子进程会进入僵尸态,持续占用 task_struct 等资源。
资源影响:僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。若大量存在,会浪费系统资源。而且还会造成内存泄漏
回收方式:使用wait()系统调用
现实当中的僵尸进程是怎么样的?我们来实践一下:
#include <stdio.h> #include <unistd.h> #include <stdlib.h>int main() {pid_t id = fork();if (id < 0){perror("fork");return 1;}else if (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);}return 0; }
父子进程特殊情况:
- 父进程先退出:子进程的父进程会被系统设置为 1 号进程( init 进程)。
- 孤儿进程:若父进程是 1 号进程,子进程成为孤儿进程,会由系统负责回收其资源(因为孤儿进程最终也会退出,系统需保障资源释放)。
我们改一下上面的代码,就能证明出来:
Linux 内核管理进程:
通过不同的链表(如运行队列链表、睡眠链表等)和树结构,内核可以高效地进行进程调度、资源管理和状态维护;偏移计算则保障了在链表操作中能准确关联进程控制块的完整数据。(如下图)
1. struct node 双向链表结构
- 包含 struct node *next (指向下一节点)和 struct node *prev (指向上一节点),用于构建双向链表。
2. struct task_struct 进程控制块
- 包含进程的所有属性,同时嵌入了 struct node1 link1 和 struct node link 两种链表节点结构,用于将进程加入不同的链表进行管理。
3. 树结构节点 struct node
此外,它可以构建树结构(如红黑树)来组织进程。
二、链表与结构体的偏移计算
- 公式 (task_struct*)((start - &(task_struct*)0->link) -> A 用于计算 link 成员在 task_struct 结构体中的偏移量,通过指针运算实现从链表节点反向获取 task_struct 结构体的完整信息,是 Linux 内核中“容器_of”思想的体现(即通过结构体中某成员的指针,反推整个结构体的起始地址)。
进程优先级管理:
一、优先级的本质与意义
- 是什么:优先级是进程对资源(如CPU)访问的先后顺序规则,与“权限”不同(权限是能否访问资源,优先级是访问的先后)。
- 为什么需要:系统资源有限但进程众多,进程间存在资源竞争关系;操作系统需通过优先级保障进程良性竞争,避免进程长时间得不到资源而出现“饥饿问题”(代码长时间无法推进)。
二、优先级的表示与调整
(这个目前为止,几乎没怎么用过,看看以后有没有机会使用到)
- 关键字段(通过 ps -al 查看):优先级公式:PRI(new)=PRI(old)+nice
ps:nice不是进程优先级,但它会影响进程优先级。
- PRI :进程的优先级(数值越小,优先级越高)。
- NI (nice值):优先级的修正数据,用于调整进程优先级。
- nice值范围: [-20, 19] ,Linux限制用户对优先级的过度调整,确保系统稳定性。
- 优先级计算:例如默认 PRI 为80时,调整 nice 值后, PRI 范围在 [60, 99] 之间( 80 + (NI的调整值) ,NI最小-20、最大19)。
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行NI :代表这个进程的nice值
renice的使用:
三、优先级调整的限制与意图
Linux不允许用户无限制调整优先级,仅在 [-20, 19] 范围内调整,防止进程因优先级过高长期占用CPU,或过低导致饥饿,保障系统整体调度的公平性与效率。
用top命令更改已存在进程的nice
进入top后按“r”–>输入进程PID–>输入nice值
这里就不演示了。
接下来,我们来讲解一下:
操作系统是如何根据优先级进行调度的?
通过位图的形式,我们已经在C++那边讲解过位图的知识点了。(若需要复习,可以去看看)
一、位图( struct bitmap )与快速查找
- 作用:用于快速判断资源(如进程队列是否为空),支持O(1)时间复杂度的查询,是Linux内核高效管理的基础(如判断队列是否为空仅需遍历位图)。
二、进程队列与调度组( struct rungqueue )
- 调度效率:通过将进程按优先级分组到不同队列,结合位图的快速判断,实现高效的进程调度与饥饿问题缓解(确保不同优先级进程都能获得合理的CPU时间)。
好了,本次就分享到这里了,下一次继续。希望大家一起进步!
最后,到了本次鸡汤环节:
我希望,我的希望,有希望。