一、冯诺依曼体系结构
我们常⻅的计算机,如笔记本。我们不常⻅的计算机,如服务器,⼤部分都遵守冯诺依曼体系。

截⾄⽬前,我们所认识的计算机,都是由⼀个个的硬件组件组成
- 输⼊设备:键盘、⿏标、话筒、摄像头、网卡、磁盘等
- 中央处理器(CPU):运算器+控制器
- 输出设备:显⽰器、磁盘、网卡、打印机等
关于冯诺依曼体系,有以下几点需要注意:
- 这⾥的存储器指的是内存
- 不考虑缓存情况,这⾥的CPU只能对内存进⾏读写,不能访问外设(输⼊或输出设备)。
- 外设(输⼊或输出设备)要输⼊或者输出数据,也只能写⼊内存或者从内存中读取。
- ⼀句话,所有设备都只能直接和内存打交道。
注意: 对冯诺依曼的理解,不能停留在概念上,要深⼊到对软件数据流理解上
例如:登录上qq和某位朋友聊天,从你打开窗⼝,开始给他发消息,到他收到消息之后的数据流动过程。亦或是在qq上发送⽂件呢?

二、操作系统(Operator System)
1、概念
任何计算机系统都包含⼀个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
- 内核(进程管理,内存管理,⽂件管理,驱动管理)
- 其他程序(例如函数库,shell程序等等)

2、设计OS的⽬的
- 对上,为⽤户程序(应⽤程序)提供⼀个良好的执⾏环境。
- 对下,与硬件交互,管理所有的软硬件资源。

从图中我们可以得出结论:
- 操作系统呈现的是软硬件体系结构、层次结构。
- 访问操作系统,必须使用系统调用,其实就是函数,只不过是系统提供的
- 我们的程序,只要你判断出它访问了硬件,那么它必须贯穿整个软硬件体系结构!
3、核⼼功能
- 在整个计算机软硬件架构中,操作系统的定位是:⼀款纯正的"搞管理"的软件
4、如何理解 "管理"
这里使用一个例子来解释管理:

- 这里的管理者就是校长,被管理者就是学生,校长管理学生并非一一进行针对性管理,而是先描述再组织,先将学生的属性描述为单个结构体,再将多个学生连接成链表,转而实现对链表的增删查改。
总结:
- 计算机管理硬件:先描述,在组织
- 描述:⽤struct结构体
- 组织:⽤链表或其他⾼效的数据结构
5、系统调⽤和库函数概念

-
在开发⻆度,操作系统对外会表现为⼀个整体,但是会暴露⾃⼰的部分接⼝,供上层开发使⽤,这部分由操作系统提供的接⼝,叫做系统调⽤。
-
系统调⽤在使⽤上,功能⽐较基础,对⽤户的要求相对也⽐较⾼,所以,有⼼的开发者可以对部分系统调⽤进⾏适度封装,从⽽形成库,有了库,就很有利于更上层⽤户或者开发者进⾏⼆次开发。
那么操作系统是怎么进⾏进程管理的呢?很简单,先把进程描述 起来,再把进程组织起来!
三、进程
1、基本概念与基本操作
- 课本概念:程序的⼀个执⾏实例,正在执⾏的程序等
- 内核观点:担当分配系统资源(CPU时间,内存)的实体。

再通过上图的理解,可以总结为:进程 = PCB(task_struct)+ 自己的代码和数据。
1)描述进程 - PCB
基本概念:
- 进程信息被放在⼀个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
- 课本上称之为PCB(process control block),Linux操作系统下的PCB是:task_struct
task_struct 是 PCB 的⼀种:
- 在Linux中描述进程的结构体叫做task_struct。
- task_struct是Linux内核的⼀种数据结构,它会被装载到RAM(内存)⾥并且包含着进程的信息。
2)task_struct
内容分类:
- 标⽰符:描述本进程的唯⼀标⽰符,⽤来区别其他进程。
- 状态:任务状态,退出代码,退出信号等。
- 优先级:相对于其他进程的优先级。
- 程序计数器:程序中即将被执⾏的下⼀条指令的地址。
- 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下⽂数据:进程执⾏时处理器的寄存器中的数据。
- I ∕ O状态信息:包括显⽰的I/O请求,分配给进程的I∕O设备和被进程使⽤的⽂件列表。
- 记账信息:可能包括处理器时间总和,使⽤的时钟数总和,时间限制,记账号等。
组织进程:
可以在内核源代码⾥找到它。所有运⾏在系统⾥的进程都以task_struct链表的形式存在内核⾥。

注意: 我们历史上执行的所有指令,工具,自己的程序,运行起来都是进程!
3)查看进程信息
- 进程的信息可以通过
/proc系统⽂件夹查看
举例:查看进程ID为 29334 的信息
powershell
ll /proc/29334

- 进程信息也可以使⽤ps来获取
示例:
cpp
// myprocess.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
while(1)
{
sleep(1);
printf("我是一个进程!我的pid: %d,我的父进程id:%d\n",getpid(),getppid());
}
return 0;
}
运行结果:

例1:筛选并输出包含 "myprocess" 关键字的进程信息
powershell
ps ajx | head -1 && ps ajx | grep myprocess

例2:筛选出进程ID 为 8081 的进程信息
powershell
ps ajx | head -1 && ps ajx | grep 29547 | grep -v grep

我们发现这里的父进程ID的信息是 bash,也就是命令行解释器,并且随着我们中断在运行,父进程一直保持不变。实际上OS会给每一个登录用户,都分配一个bash。
4)通过系统调⽤获取进程标识符
函数原型:

示例:
cpp
// myprocess.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
printf("我是一个进程!我的pid: %d,我的父进程id:%d\n",getpid(),getppid());
return 0;
}
运行结果:

5)通过系统调⽤创建进程 - fork
函数原型:

-
fork 有两个返回值:父进程中返回子进程的 PID,子进程中返回 0,失败返回-1。
-
⽗⼦进程代码共享,数据各⾃开辟空间,私有⼀份(采⽤写时拷⻉)

-
fork 之后通常要⽤
if进⾏分流
示例:
cpp
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
printf("父进程开始运行,pid: %d\n",getpid());
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
// child
sleep(1);
printf("我是一个子进程! 我的pid:%d, 我的父进程id:%d\n",getpid(),getppid()); }
else
{
// father
sleep(1);
printf("我是一个父进程!我的pid:%d,我的父进程id:%d\n",getpid(),getppid());
}
return 0;
}
运行结果:

这里你可能会有以下几个疑问?
- 为什么fork给父子返回各自的不同返回值?
因为父子进程的比例一般是1:n,一个父进程可能有多个子进程,父进程需要通过pid来确认子进程,而多个子进程只有一个共同父亲,因此只需要一个pid即可。
- 为什么一个函数会返回两次?

结论: 进程具有独立性!

示例:
cpp
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int gval = 100;
int main()
{
printf("父进程开始运行,pid: %d\n",getpid());
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
// child
printf("我是一个子进程! 我的pid:%d, 我的父进程id:%d\n,gval:%d\n",getpid(),getppid(),gval);
sleep(5);
while(1)
{
sleep(1);
printf("子进程修改变量:%d->%d",gval,gval+10);
gval += 10;
printf("我是一个子进程!我的pid:%d,我的父进程id:%d\n",getpid(),getppid());
}
}
else
{
// father
while(1)
{
sleep(1);
printf("我是一个父进程!我的pid:%d,我的父进程id:%d,gval:%d\n",getpid(),getppid(),gval);
}
}
printf("进程开始运行,pid:%d\n",getpid());
return 0;
}
运行结果:

2、进程状态
1)运行、阻塞、挂起

2)进程状态分类
- 进程状态就是 task_struct 内的一个整数
下⾯的状态在kernel源代码⾥定义:
cpp
/*
*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(运⾏状态):并不意味着进程⼀定在运⾏中,它表明进程要么是在运⾏中要么在运⾏队列⾥。
- S(睡眠状态):可中断睡眠(浅睡眠),当进程运行时可以通过ctrl+c 终止掉。
- D(磁盘休眠状态):不可中断睡眠状态(深睡眠),这个状态一般伴随着磁盘的写入写出,如果进程被杀掉,就可能造成数据的丢失。与S都属于阻塞状态。
- T(暂停状态):可以通过发送 SIGSTOP 信号给进程来停⽌进程,这个被暂停的进程也可以通过发送 SIGCONT 信号让进程继续运⾏。
- t(追踪状态):当进程进入debug调试状态时,遇到断点,此时进程就被暂停了。
- X(死亡状态):这个状态只是⼀个返回状态,你不会在任务列表⾥看到这个状态。
- Z(僵尸状态):为了获取退出信息。
3)进程状态查看
powershell
ps aux/ajx 命令
- a:显示一个终端所有的进程,包括其他用户的进程。
- x:显示没有控制终端的进程,例如后台运行的守护进程。
- j:显示进程归属的进程组ID、会话ID、⽗进程ID,以及与作业控制相关的信息
- u:以⽤户为中⼼的格式显⽰进程信息,提供进程的详细信息,如⽤户、CPU和内存使⽤情况等
- 查看R状态
示例:
cpp
#include<stdio.h>
int main()
{
while(1)
{
printf("hello world\n");
}
return 0;
}
程序运行后,查看当前进程的状态:
powershell
ps ajx | head -1; ps ajx | grep myprocess

可以看到当前的进程是R+,R表示运行状态,+表示在前台运行。
- 查看S状态
示例:
cpp
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
printf("我是一个进程,pid:%d\n",getpid());
int x;
scanf("%d",&x);
return 0;
}

程序运行后等待键盘输入时,查看当前进程的状态:
powershell
ps ajx | head -1; ps ajx | grep myprocess

可以看到此时是S状态,也就是浅睡眠状态。
- 查看 t 状态
示例:
cpp
// myprocess.c
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
printf("我是一个进程,pid:%d\n",getpid());
int x;
scanf("%d",&x);
return 0;
}
编译时加入调试信息:gcc myprocess.c -o myprocess -g
进行调试:gdb myprocess
给第9行打上断点:b 9
程序执行到断点处,此时查看当前进程的状态:
powershell
ps ajx | head -1; ps ajx | grep myprocess

可以看到执行到断点处时,此时的状态是 t ,也就是追踪状态。
- 查看 T 状态
示例:
cpp
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
printf("我是一个进程,pid:%d\n",getpid());
while(1)
{
printf("hello world\n");
}
return 0;
}
程序运行中按 ctrl+z,此时查看进程的状态:
powershell
ps ajx | head -1; ps ajx | grep myprocess

可以看到此时的状态是T,也就是暂停状态。
4)Z(zombie) - 僵⼫进程
-
僵尸状态(Zombies)是⼀个⽐较特殊的状态。当子进程退出并且⽗进程没有读取到⼦进程退出的返回代码时就会产⽣僵⼫进程。
-
僵尸进程会以终⽌状态保持在进程表中,并且会⼀直在等待⽗进程读取退出状态代码。
-
所以,只要⼦进程退出,⽗进程还在运⾏,但⽗进程没有读取⼦进程状态,⼦进程就进⼊Z状态。
示例:
cpp
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
// child
int count = 5;
while(count)
{
printf("我是子进程,我正在运行:%d\n",count);
sleep(1);
count--;
}
}
else
{
while(1)
{
printf("我是父进程,我正在运行...\n");
sleep(1);
}
}
return 0;
}
运行结果:

运行程序的同时,查看进程状态:
powershell
while :; do ps ajx | head -1; ps ajx | grep myprocess; sleep 1; done
可以看到当子进程执行完代码后,由于没有被父进程读取到退出的信息,就变成了Z状态。
5)僵⼫进程危害
-
进程的退出状态必须被维持下去,因为他要告诉⽗进程有关他结果相关的信息。可⽗进程如果⼀直不读取,那⼦进程就⼀直处于Z状态!
-
维护退出状态本⾝就是要⽤数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态⼀直不退出,PCB⼀直都要维护!
-
如果⼀个⽗进程创建了很多⼦进程,就是不回收,就会造成内存资源的浪费(内存泄漏),因为数据结构对象本⾝就要占⽤内存。
6)孤⼉进程
-
父子进程关系中,如果父进程先退出,子进程要被1号进程领养,这个被领养的进程(子进程),叫做孤儿进程。
-
孤⼉进程被1号init/systemd进程领养,也要由init/systemd进程回收。
示例:
cpp
// myprocess.c
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
// child
while(1)
{
printf("我是一个子进程, pid:%d, ppid: %d\n", getpid(),getppid());
sleep(1);
}
}
else
{
// father
int cnt = 5;
while(cnt)
{
printf("我是一个父进程, pid: %d, ppid: %d\n", getpid(), getppid());
cnt--;
sleep(1);
}
}
return 0;
}
运行结果:

执行程序后,查看当前进程状态:
powershell
while :; do ps ajx | head -1 ; ps ajx | grep myprocess ; sleep 1; done

可以看到此时子进程的父进程是1号进程,也就说明此进程是孤儿进程。
注意:孤儿进程无法通过ctrl + c进行终止,只能通过 kill 命令来杀死进程。
powershell
kill -9 17065
3、进程优先级
1)基本概念
-
进程优先级是指进程得到CPU资源的先后顺序。
-
优先权⾼的进程有优先执⾏权利。配置进程优先权对多任务环境的 linux 很有⽤,可以改善系统性能。
-
还可以把进程运⾏到指定的CPU上,这样⼀来,把不重要的进程安排到某个CPU,可以⼤大改善系统整体性能。
2)查看系统进程
cpp
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
// child
while(1)
{
printf("我是一个子进程, pid:%d, ppid: %d\n", getpid(),getppid());
sleep(1);
}
}
else
{
// father
while(1)
{
printf("我是一个父进程, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
return 0;
}
运行程序,查看当前的系统进程:
powershell
ps -l | head -1 && ps -al | grep myprocess

- UID:执⾏者的⾝份
- PID:进程的代号
- PPID:进程对应⽗进程的代号
- PRI:进程可被执⾏的优先级,其值越⼩越早被执⾏
- NI:进程的nice值
3)PRI && NI
-
PRI即进程的优先级,或者通俗点说就是程序被CPU执⾏的先后顺序,此值越⼩进程的优先级别越⾼
-
NI就是我们所说的nice值,其表⽰进程可被执⾏的优先级的修正数值
-
PRI值越⼩越快被执⾏,那么加⼊nice值后,将会使得PRI变为:PRI(new)=PRI(默认)+nice ,PRI默认为80。
-
当nice值为负值的时候,那么会将该程序PRI变⼩,即其优先级会变⾼,则其越快被执⾏
-
调整进程优先级,在Linux下,就是调整进程nice值
-
nice其取值范围是 [-20,19],⼀共40个级别,那么Linux进程的优先级范围就是[60,99]。
4)查看、更改进程优先级
- top指令
示例:
cpp
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
while(1)
{
sleep(1);
printf("我是一个父进程,pid: %d, ppid: %d\n",getpid(), getppid());
}
return 0;
}
运行结果:

1.输入top后按 r,输⼊进程PID后按回车

2.输⼊新nice值

然后查看再当前进程的优先级:
powershell
ps -al | head -1 && ps -al | grep myprocess

可以发现,我们刚才将nice值修改为10,让进程的优先级值变为了90,相当于降低了当前进程的优先级。
- nice指令:启动进程时设置优先级
举例:将程序myprocess的nice值设置为 10
powershell
nice -n 10 ./myprocess

- renice指令:调整已运行进程的优先级
举例:将程序myprocess的nice值设置为 -10
powershell
sudo renice -n -10 -p 17953

5)竞争、独⽴、并⾏、并发
-
竞争性:系统进程数⽬众多,⽽CPU资源只有少量,甚⾄1个,所以进程之间是具有竞争属性的。为了⾼效完成任务,更合理竞争相关资源,便具有了优先级。
-
独⽴性:多进程运⾏,需要独享各种资源,多进程运⾏期间互不⼲扰
-
并⾏:多个进程在多个CPU下分别,同时进⾏运⾏,这称之为并⾏
-
并发:多个进程在⼀个CPU下采⽤进程切换的⽅式,在⼀段时间之内,让多个进程都得以推进,称之为并发
4、进程切换
1)死循环进程如何运行
- 一旦一个进程占有CPU,会把自己的代码跑完吗?不会!因为存在时间片。
- 死循环进程,不会一直都占有CPU,因为当他的时间片结束后,就会自动切换下一个进程。
2)CPU && 寄存器
- 寄存器就是CPU内部的临时空间
- 寄存器 != 寄存器里面的数据

3)如何切换
关于进程如何切换可以用一个大学期间当兵的例子来讲述:

这里的我就是进程,学校就是CPU,当我去当兵时,学校就会保存学籍(上下文数据),当我回校后,学校就会恢复学籍(上下文数据)。
那么具体是怎样的呢?

- 进程切换最核心的就是,保存和恢复当前进程的硬件上下文数据,即CPU内寄存器的内容!
- 那么当前进程要把自己的进程硬件上下文数据保存起来,保存到哪里了呢?保存到进程的task_struct里面的一个结构体对象TSS(任务状态段)。
参考Linux内核0.11代码:

注意:
时间⽚:当代计算机都是分时操作系统,每个进程都有它合适的时间⽚(其实就是⼀个计时器)。时间⽚到达,进程就被操作系统从CPU中剥离下来。
5、Linux2.6内核进程 - O(1)调度队列

上图是Linux2.6内核中进程队列的数据结构,之间关系也已经给⼤家画出来,⽅便⼤家理解
1)⼀个CPU拥有⼀个runqueue
- 如果有多个CPU就要考虑进程个数的负载均衡问题
2)优先级
- 普通优先级:[100,139],40个刚好对应nice值。
- 实时优先级:[0,99]
3)活动队列
- 时间⽚还没有结束的所有进程都按照优先级放在该队列
- nr_active:总共有多少个运⾏状态的进程
- queue[140]:⼀个元素就是⼀个进程队列,相同优先级的进程按照FIFO规则进⾏排队调度,所以,数组下标就是优先级!
- 从该结构中,选择⼀个最合适的进程,过程是怎么的呢?
1.从0下标开始遍历queue[140]
2.找到第⼀个⾮空队列,该队列必定为优先级最⾼的队列
3.拿到选中队列的第⼀个进程,开始运⾏,调度完成!
4.遍历queue[140]时间复杂度是常数!但还是太低效了! - bitmap[5]:⼀共140个优先级,⼀共140个进程队列,为了提⾼查找⾮空队列的效率,就可以⽤5*32个⽐特位表⽰队列是否为空(1/0),这样,便可以⼤大提⾼查找效率!

4)过期队列
- 过期队列和活动队列结构⼀模⼀样
- 过期队列和活动队列结构一模一样
- 过期队列上放置的进程,都是时间⽚耗尽的进程
- 当活动队列上的进程都被处理完毕之后,对过期队列的进程进⾏时间⽚重新计算
5)active指针和expired指针
- active指针永远指向活动队列
- expired指针永远指向过期队列
- 活动队列上的进程会越来越少,过期队列上的进程会越来越多,没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于又具有了⼀批新的活动进程!
6)总结
- 在系统当中查找⼀个最合适调度的进程的时间复杂度是⼀个常数,不随着进程增多⽽导致时间成本增加,我们称之为进程调度O(1)算法!