必看前言:
文中图片均是经自己亲自调改 生成。
"豆包"仅把图片背景变黑 ,图形 字体均经过校正。
纯手搓无复制
目录
[pid(process id 进程id)](#pid(process id 进程id))
[ppid(parent process id)](#ppid(parent process id))
操作系统(OS)
基本概念
操作系统(Operation System)是一个基本的程序集合,为用户提供友好的执行环境,进行管理软硬件的软件。
操作系统架构图
绝大多数的计算机操作系统的架构图基于冯•诺依曼提出的基本架构,由输入设备、存储器、运算器、控制器和输出设备组成。
五大组件的基本关系如下:

控制器:负责读取指令产生控制信号。从内存读取指令、解码指令,并产生控制信号。
指挥协调其他部件的工作流程。
运算器:负责执行**运算。**算术运算(加减乘除)和逻辑运算(与或非)。
是整个计算机的数据加工中心。
底层
操作系统底层是应用C语言编写的,主要利用struct结构体来管理驱动设备、底层硬件和进程等。
操作系统与库函数的关系
理解操作系统与库函数的关系是区分"系统编程"与"应用编程"的关键。
库函数是操作系统提供给应用程序的"接口"和"工具箱",二操作系统是真正的硬件管理者 。硬件程序通常不直接指挥操作系统而是直接通过调用库函数来请求操作系统提供服务。
内存(RAM)与磁盘(硬盘)
CPU是数据处理器(运算器+控制器),需要读取数据。
♔ 内存是数据的"工作台",磁盘是数据的"仓库"。
CPU读取数据只能从内存中读取指令和数据来执行,++无法直接访问++ 磁盘。因此磁盘里的数据必须*++先被加载到内存中++*,CPU才能处理它们。
进程
前言:此处探讨的进程均是子进程对象概念
基本概念:程序的一个执行实现 ,正在执行的程序等。
内核方面:担当系统分配资源(CPU、内存)的实体。
进程 == 内核结构对象+自己的代码+数据。
再谈操作系统
进程的数据来源
操作系统读取数据只能从内存中读取,内存内数据来自磁盘的"可执行程序+代码段、数据段"。

磁盘内数据被读取到内存中:

OS内的进程块
操作系统内由struct XXX {......}结构体来管理进程。Linux内"XXX"是PCB(进程控制块Process Control Block),PCB间使用双向链表链接。

PCB结构体内拥有代码地址 和**数据地址,**这些指针指向PCB外的进程数据(代码段+数据段)。

因此 进程 == PCB(struct task_struct) + 该进程的代码段和数据段
pid、ppid
id查询方式:
pid(process id 进程id)
每个进程都有自己的pid类似于我们的身份证号码,当进程退出后重新启动其pid就会改变,类似于"人生重开"后的身份证号。
♝像热键:Ctrl+C就是终止进程。
ppid(parent process id)
该进程的父进程id。
查询指令
while :; do ps ajx | head -1 && ps ajx | grep myprocess;sleep 1; done
每1秒查询一次或
cpp
while :; do
clear
echo "====== $(date '+%Y-%m-%d %H:%M:%S') ======"
ps ajx | head -1
ps ajx | grep myprocess
echo "================================"
sleep 1
done
//复制版本:
while :; do clear; echo "====== $(date '+%Y-%m-%d %H:%M:%S') ======"; ps ajx | head -1; ps ajx | grep myprocess; echo "================================"; sleep 1; done
更简洁的查询
父进程
父进程是指通过fork()来创建出来的另一个进程。被创建出来的那个新进程就是这个父进程的子进程。

父进程与子进程构成了树状结构,整个系统的所有进程的根节点,就是内核启动的第一个用户态进程(通常是init或systemd,PID==1)。
-bash进程
介绍:当登录Linux服务器操作系统立马为你创建一个新的进程,这个进程就是-bash。它是一切进程的子进程,因此你可以从任意一个进程的ppid来不断追溯到达-base进程。
-bash进程会显示一个命令提示符(比如: $ # ),等待你输入命令。
-bash进程与init/systemd进程的关系:
init/systemd向下会达到-base进程。
fork函数
原型:pid_t fork(); #pid_t本质是signed int,-2^31 ~ 2^31-1
原理:用于创建一个新的进程使得原进程变作新进程的父进程,两个进程在下面的代码执行过程中调用顺序完全不确定,受很多种因素决定。
返回值:fork调用一次但是会**++返回两次++** (一次在父进程,一次在子进程)
父进程的返回ID > 0,子进程的返回ID == 0。
若新进程++创建失败则返回-1。++
.c文件下:Man 3 fork #查询fork函数
man查询图(仅展示一部分)

使用举例:
cpp
//prc.c
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("启动!\n");
printf("我是原进程pid = %d\n", getpid());
pid_t i = fork();
if(i == 0) //子进程代码
{
printf("我是衍生出来的子进程 pid == %d ppid == %d\n", getpid(), getppid());
}
else if(i > 0) //父进程入口
{
printf("我是原来的进程变为了父进程pid == %d\n", getpid());
}
else{
printf("子进程创建失败\n");
}
return 0;
}
运行结果:

运行图示:

底层
子进程拷贝父进程数据,它们同时指向相同的代码+数据,当二者中任意一者发生写时再资源创建,各自资源独立。

进程状态
三种主要类型:运行 阻塞 挂起
运行和挂起是正常一般的状态,而挂起是极端情况下处理模式。
深入内核
1.运行
进程想要运行本质在于是否位于CPU维护的"调度队列(runqueue)",只有位于调度队列的进程元素才有运行的资格。 ++ps:一个CPU仅维护一个调度队列++
调度队列严格遵守"尾进头出(++FIFO++)"原则。
进程控制块PCB------++struct task_struct++,PCB间使用指针链接最终形成双向链表的结构。
图示如下:

2.阻塞(scanf)
阻塞的来源于I/O流及数据传输相关,本质是"++我需要的东西还没有准备好++",并非"我没有资源"而阻塞。
以I/O中的scanf为例说明阻塞的原理。
同样的device结构体,之间链接节点:
cpp
struct device
{
int id;//设备id
int status;//设备状态
void* data;
struct device* next;// 指向下一个
struct device* prev;// 指向前一个
int type;
struct task_struct* wait_queue;// 等待队列
}

等待队列(wait_queue)是位于链表外的又一队列,例如多个输入的等待。
运行和阻塞间的切换就是链表的"增删查改"。
例如程序运行到scanf时OS的处理:

网状本质
事实:PCB间的链接并非简单的双向链表而是网状复杂结构。
每个PCB(struct task_struct)内嵌套一种结构体(struct list_head),每个list_head内部都拥有两个指针(next + prev),task_struct内嵌套了多个这样的结构体。因此链接(父子 兄弟 全局)进程,构成网状结构。
cpp
struct task_struct {
struct list_head tasks; // 接入全局进程链表
struct list_head children; // 接入父进程的子进程链表
struct list_head sibling; // 接入兄弟进程链表
struct list_head run_list; // 接入调度器运行队列
// ... 还有 400+ 个字段
};
下图不用看懂仅作"网状"的展示:

挂起
极端情况下:像内存空间不足、用户主动暂停及系统休眠,引发的非一般进程状态。
挂起指的是进程被人为/系统地从内存移动到磁盘(swap分区),以释放宝贵的内存资源。
孤儿进程
定义:子进程还未来得及结束时,父进程就已经结束。
孤儿进程被系统受理父id为1(可理解为操作系统),至此此进程变为后台进程。
读懂进程状态
前言:进程状态的查询受运行间隔的影响因此在使用循环体来检测进程状态时要谨慎处理。
描述进程的常量:
cpp
// 数组
static const char* const task_state_array[] = {
"R (running)",
"S (sleeping)",
"D (disk sleep)",
"T (stopped)",
"t (trcing stop)",
"X (dead)",
"Z (zombine)",
};
进程查询
cpp
// 每秒查询一次进程状态
while :; do
echo "====== $(date '+%Y-%m-%d %H:%M:%S') ======"
ps ajx | grep '\./proc' | grep -v grep
echo "================================"
sleep 1
done
Ⅰ暂停

Ⅱ休眠

Ⅲ死亡

进程访问属性
进程的访问属性包括:++权限++ 和++优先级++,决定了进程能否被访问,能被访问的话被访问的顺序如何。
权限
权限决定了进程可以访问哪些资源,由进程的身份决定。
优先级
进程的优先级决定了进程获得CPU资源的能力,由评判优先级的值(PRI)决定,值越低优先级越高越高优先级越低。
查询:
css
ps -al |head -1 && ps -al |grep proc

NI值作为修复值,调整PRI值的大小。PRI == (默认值)80 + NI
当新旧进程调整时NI值会发生变化。
*UID
UID是文件/目录权限中,判断访问者属于拥有者/所属组/其他人。
上图中UID的1000代表wzb用户。
使用ls -ln查询显现UID

性质总结
总体而言进程的性质:
竞争性:进程间存在资源竞争关系与优先级问题。
独立性:多个进程运行时,需要各自独享资源,进程间具备一定的独立性。
并行:多个进程在多个CPU 下分别同时进行,称为并行。
并发:多个进程在同一个CPU下采用进程切换(超迅速切换)的方式使得多个进程得以推进称之为并发。
进程调度
|--------------|------------------|------------------------|
| 类型 | 设计目标 | 核心指标 |
| 实时操作系统(RTOS) | 在规定时间内响应外部事件 | 确定性、可预测性、响应延迟有最大上限 |
| 分时操作系统 | 让多个用户/任务公平地共享CPU | 吞吐量、平均响应时间及公平性 |
在进程中的体现:
进程切换:通过寄存器存储上下文数据且区分是否先前执行过以决策是否使得寄存器存储上下文数据。 ps:实时操作系统基于"时间片"的来实现,最大时间消耗。
如:响应超时 既体现了实时操作系统也体现了分时操作系统。
内核级分析CPU调度算法
结构:
CPU维护一个调度队列(runqueue),本质此队列是一个内部嵌套进程属性变量+struct rqueue_Elmen* array[2]的结构体。
struct rqueue_Elmen*内部含有队列状态(nr_active)、5*32位图(bitmap)和FIFO队列(queue[140])。
队列(queue[140])内每个元素链接为双向链表------形成了由链地址法形成的哈希表:

活跃进程
array[0] 的 nr_active决定此array是++活跃++ 进程函数++过期++ 进程,bitmap位图(5*32 == 160),queue[140]维护的进程队列(哈希表)。
bitmap:160位但实际仅用到了[0,139]记140个元素。
【0,99】分时优先级系统管理(不探讨)。
【100,139】实时优先级系统统筹管理,同样的下标越小的进程优先级越大。
活跃进程与过期进程
构成元素完全一致执行不同的任务要求。
array[0]和array[1] 两者在内存中各自维护自己的调度队列及其属性,明显的进程的调度实际是队列节点的移动,当一个进程在活跃队列执行完/超出时间片规定自动移动到过期进程(同样的FIFO)中。
燃尽式博客 写完不知天地为何物 仿佛天越来越远地越来越近 求关球馆求
(o・ェ・o)
