【Linux】进程概念(上):从冯诺依曼到进程入门
前言:进程是操作系统的核心概念之一,是我们必学的内容之一。我们的日常生活中使用的app,或是后台运行的程序,其本质都是一个个被OS管理的进程。本篇从冯诺依曼体系结构出发,一步步揭开进程的神秘面纱,进程到底是何方神圣?
一、冯诺依曼体系结构
要说进程,首先得知道计算机的基础架构。我们日常使用的笔记本、服务器等设备,大多遵循冯诺依曼体系,其核心思想可以概括为:所有的设备都只能直接和内存打交道,计算机硬件间数据交互的媒介是内存。
冯诺依曼体系的硬件组成包括:
- 输入设备:键盘、鼠标、扫描仪等,负责将数据传入系统
- 中央处理器(CPU):包括运算器和控制器,是执行指令的核心;
- 存储器:此处特指内存,用于临时存储数据和指令;
- 输出设备:显示器、打印机等,负责将处理结果输出。
图片解释案例–以QQ聊天为例:
当我们发送一条信息时,信息的流动严格遵循冯诺依曼规则:
- 你在键盘输入消息(输入设备),数据先被写入保存;
- CPU从内存中读取消息数据,进行处理(如封装、加密等)
- 处理后的消息再次被写入内存,再通过网卡(输出设备)发送给对方;
- 对方的QQ接收信息后,先存入其设备内存,再由CPU处理并通过显示器(输出设备)展示。
简洁一点来说就是,信息(数据)从本地磁盘读入内存,经CPU处理后通过网络发送,接收方则将数据从内存写入其磁盘。
二、操作系统:进程的“老大”(1号进程)
2.1概念:
OS:任何计算机系统都包含一个基本的程序集合,成为操作系统。操作系统的定位是一款“搞管理”的软件!
可以笼统的理解,OS包括:
- 内核:进程管理、内存管理、文件管理、驱动管理
- 其他程序:如函数库,shell程序等
2.2操作系统的两层定位
- 对下,与硬件交互,管理CPU、内存、磁盘等资源
- 对上,为用户程序(应用程序)提供一个良好的执行环境
2.4如何理解“管理”?
操作系统的“管理”,本质是对被管理对象的组织与描述:
- 组织:用链表、树等结构将对象有序排列,方便高效管理
- 描述:用数据结构(如struct)记录对象属性,例如用task_struct描述进程
如以下的一个案例:大学校长–大学辅导员–学生:辅导员用表格(描述)记录学生,再按班级(组织)分类管理
三、进程:程序的“执行实例”
3.1什么是进程?
- 课本概念:进程是程序的一个执行实例,正在执行的程序等;
- 内核观点:担当分配系统资源(CPU时间、内存等)的实体
- 这里要说的:
进程=内核数据结构(task_struct)+程序代码+数据
3.2进程的唯一标识:PCB
进程控制块(PCB):PCB是描述进程的核心数据结构,在Linux中成为task_struct
。它包含进程的关键信息:
- 标识符(PID):唯一区分进程的ID;
- 状态:任务状态、推出代码、退出信号等
- 优先级:相对于其他进程的优先级
- 程序计数器(PC):下一条要执行的指令地址
- 内存指针:指向程序代码和数据的位置
- 上下文数据:CPU寄存器中的数据,用于进程切换后恢复进程执行
所有进程的task_struct以双链表形式组织
,方便操作系统遍历和管理
3.2.1查看进程信息:
-
通过
/proc
系统文件夹查看:(查看一号进程)
-
大多数进程信息同样可以使用
top和ps
这些用户级工具获取
代码:在终端输入:vim test.c
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>int main()
{while(1){printf("这是一个进程")sleep(1);}return 0;
}
指令:
//通用命令
ps aux | ps axj 命令
- a:显示一个终端所有的进程,包括其他用户的进程
- x:显示没有控制终端的进程,例如后台运行的进程
- j:显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息
- u:以用户为中心的格式显示进程信息,提供进程的详细信息,如用户、CPU和内存使用情况
//这里是查看一号进程:
ps aux | grep test | grep -v grep
3.2.3通过系统调用获取进程标识符
- 父进程ID:PPID;
- 进程ID:PPID
在此之前,我们先认识一个函数getpid
,在命令行输入man getpid
可以看到这个函数的详情。
了解以后,我们就可以初步看看,怎么获取进程ID了:
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>int main()
{while(1){printf("我的进程ID是:%d\n",getpid());printf("我的父进程ID是:%d\n",getppid());sleep(10);}return 0;
}
3.2.4通过系统调用创建进程:初识fork
fork是什么?fork的作用是创建一个新进程,新进程称为子进程,原来那个进程称为父进程。在命令行输入man fork
,可以看到fork的用法。
man fork
以后可以看到:
- fork有两个返回值,从父进程的角度看,
fork
会返回子进程的进程 ID(PID,一个正整数);而从子进程的角度看,fork
会返回 0 - 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
接下来简单实现一下fork的功能:
代码:
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>int main()
{int ret=fork();//创建失败if(ret<0){perror("fork");return 1;}//子进程else if(ret==0){printf("I am child:%d!,ret:%d\,",getpid(),ret);}//父进程else{printf("I am father:%d!,ret:%d\n",getppid(),ret);}sleep(5);return 0;}
3.3进程状态:进程的”一生之旅“
3.3.1进程状态名称:
一个进程能有几个状态(在Linux内核里,进程有时候也叫做任务),这些状态在kernel
源代码中这么定义:
- R运行状态(running):要么正在执行,要么在运行队列中等待
- S睡眠状态(sleeping):等待事件完成(可中断睡眠,如等待用户输入)
- D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠,即等待IO完成,此时进程不能被杀死
- T停止状态(stopped):收到
SIGSTOP
信号后暂停,可通过SIGCONT
恢复 - X死亡状态(dead):进程彻底结束,无法在任务列表中看到
3.3.2 Z(zombie)–僵尸进程
- 僵尸进程:当子进程退出并且父进程没有读取子进程退出的返回代码时,就会产生僵尸进程。
- 僵尸进程会以终止状态保持在进程表中,并且会一直等待父进程读取退出状态码
- 简单来说就是,只要子进程退出,父进程在运行,并且没有读取子进程的状态,子进程就会进入Z状态
简单举个例子:
#include<stdio.h>
#include<stdlib.h>int main()
{ pid_t ret = fork();if(ret<0){printf("fork error\n");return 1;}else if(ret > 0){printf("parent[%d] is sleeping...\n",getpid());sleep(50);}else{printf("child:[%d],ready to zombie...\n",getpid());sleep(5);exit(EXIT_SUCCESS);}return 0;
}
测试语句:
while :; do ps aux | grep zombie | grep -v grep; sleep 1; echo "------------------------"; done
如上图我们可以看到,父进程不读取,子进程确实会变成僵尸进程,那僵尸进程会导致什么后果呢?
- 父进程不读取,子进程会一直处于
Z状态
Z状态
一直不退出,PCB一直都要维护- 父进程一直不回收子进程,同时创建很多个子进程,会导致资源的浪费,且会导致资源泄露
3.2.3 孤儿进程
孤儿进程:父进程提前退出,子进程被1号进程init
收养,由系统负责回收,避免成为僵尸进程
举个例子:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h> int main() {// 创建子进程pid_t pid = fork();// 判断fork是否成功if (pid < 0) {perror("fork failed"); return 1; }// 子进程else if (pid == 0) {printf("子进程: PID=%d, 父进程PID=%d\n", getpid(), getppid());sleep(10);printf("子进程: 10秒后,父进程PID=%d (已被领养)\n", getppid());}// 父进程else {printf("父进程: PID=%d, 子进程PID=%d\n", getpid(), pid);sleep(3);printf("父进程: 即将退出\n");// 父进程主动退出exit(0);}return 0;
}
显而易见:父进程提前退出的子进程确实被1号进程收养了,避免了成为僵尸进程
3.3 进程优先级
进程的的优先权即cpu资源分配的先后顺序,优先权高的进程有优先执行的权力。
3.3.1查看系统进程
在Linux或Unix系统中,用ps -l
命令输出内容:
图中的被圈起来的代表什么呢?
- UID:代表执行者的身份(root用户为0,普通用户从1000开始)
- PID:代表这个进程的代号
- PPID:父进程代号
- PRI:代表这个进程可被执行的优先级,其值越小越早执行
- NI:代表这个进程的nice值(nice值即进程的优先级数值,数值范围是
-20~+19
)
【注】nice值不是进程优先级,它代表的是进程优先级的数值,可以理解为nice值是进程优先级的修正数据
3.3.2 查看进程优先级的命令
用top命令更改已存在进程的nice:
- 在终端输入:
top回车
->r回车
->输入PID
->输入nice值
把nice值改成10,改前:
改后:
【注】nice值的取值范围是:-20~+19
即无论你设置到大于19,小于-20,都会是在这个范围,就比如说,你设置的是+30,那么它最大不能超过19,他就只会到19了。
3.3.3 竞争、独立、并行、并发
- 竞争性:系统进程数目众多,而CPU资源只有少量,所以进程之间是具有竞争属性的
- 独立性:多进程运行,需独立享有各种资源,多进程间互不干扰
- 并行:多个进程在多个CPU下分别,同时进行运行,称之为并行
- 并发:多个进程在一个CPU下采用进程切换的方式,在一段时间内,让多个进程都得以推进,称之为并发。
3.3.4 进程切换(process switching)
基本概念:是指操作系统从一个正在运行的进程中暂停,保存其运行状态,并恢复另一个进程的运行状态,使其开始执行的过程
进程切换的原因:
- 时间片耗尽:在采用
时间片轮转调度算法
的系统中,每个进程被分配一个固定的时间片来使用 CPU - 更高优先级进程就绪:如果系统采用
优先级调度算法
,当有更高优先级的进程进入就绪队列时,正在运行的低优先级进程会被暂停,操作系统切换到高优先级进程执行,确保重要或紧急的任务能够优先得到处理 - 等待资源:当进程需要等待某些资源(如 I/O 操作完成、等待信号量等)而无法继续执行时,它会主动放弃 CPU,进入等待状态
进程切换的过程:
- 保存当前进程状态:操作系统会将当前正在运行进程的相关信息保存起来,主要包括程序计数器(PC)的值,它记录了进程下一条要执行的指令地址;CPU 寄存器中的数据,如通用寄存器、状态寄存器等,这些数据反映了进程当前的运算状态和控制信息;进程的上下文环境,包括栈指针等信息。这些信息会被存储在进程的进程控制块(PCB)中。
- 更新进程状态:将当前进程的状态从 “运行” 状态改为 “就绪” 状态或 “等待” 状态,具体取决于进程切换的原因。同时,从就绪队列中选择一个合适的进程,将其状态从 “就绪” 状态改为 “运行” 状态。
- 恢复新进程状态:从即将运行进程的 PCB 中读取之前保存的状态信息,恢复该进程的程序计数器、CPU 寄存器等数据,让 CPU 能够从上次中断的地方继续执行该进程的指令。
图示:
3.3.5 进程调度:内核进程O(1)调度队列
【注】活跃进程是时间片还没用完的进程,过期进程是时间片耗尽的进程,还在等待时间片的进程
优先级
- 普通优先级:100~139(我们所说的进程都是普通优先级)
- 实时优先级:0~99(系统管)
活动队列(active)
- 时间片还没有结束的所有进程都按照优先级放在该队列
nr_active
:总共有多少个运行状态的进程queue(140)
:一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以, 数组下标就是优先级!
那么选择一个进程,过程应该是怎么养的呢?
- 从0下标开始遍历queue(140)
- 找到第一个非空队列,该队列必定是优先级最高的队列
- 拿到选中队的第一个进程,开始运行,调度完成
- 遍历queue(140)时间复杂度是常数(低效!)
则有:bitmap(5)
:一共140个优先级,一共140个进程队列,为了提⾼查找非空队列的效率,就可以用5 * 32个比特位表示队列是否为空,这样,便可以大大提⾼查找效率!
过期队列
- 结构和活动队列一模一样
- 过期队列上方的都是时间片耗尽的进程
- 当活动队列上的进程都被处理完毕后,对过期队列的进程进行时间片重新计算
active指针和expired指针
- active指针永远指向活动队列
- expired指针永远指向过期队列
- 活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的;
- 在合适的时候,只要交换active指针和expired指针的内容,就相当于又具有了一批新的活跃进程!