冯诺依曼体系结构(Von Neumann Architecture)是计算机的基本设计理念之一,由美国数学家约翰·冯·诺依曼于1945年提出,也被称为“冯诺依曼模型”或“冯诺依曼计算机体系结构”。它的核心思想是将程序和数据存储在计算机的内存中,并通过中央处理单元(CPU)执行程序。冯诺依曼体系结构至今仍然是大多数计算机的基础架构。
CPU
):
CU
):负责指挥计算机各部分的工作。
ALU
):进行算术和逻辑运算。
RAM
):
注意:
为什么体系结构中要存在内存?
CPU处理速度非常快,但是输入数据的速度相较于CPU的速度是非常慢的,这就导致了很多时候CPU都在等待数据的输入,严重浪费了CPU的性能,所以增加内存,让CPU直接跟内存交换数据,充分发挥CPU的性能。(内存输入输出的数据的速度是非常快的)
计算机存储金字塔:
冯诺依曼瓶颈:
冯诺依曼架构存在一个著名的问题,即“冯诺依曼瓶颈”(Von Neumann Bottleneck)。这是由于程序和数据共享同一个内存系统,CPU在执行指令时需要频繁地从内存读取指令和数据,导致内存的读写速度成为限制计算机性能的瓶颈。随着计算机硬件的不断发展,解决冯诺依曼瓶颈的问题成为计算机体系结构研究的一个重要方向。
总的来说,冯诺依曼体系结构让计算机保持一定处理速度的同时,降低了计算机的成本,使得计算机能够进入各家各户,为之后互联网的发展奠定了基础。
操作系统(Operating System,简称OS)是管理计算机硬件与软件资源的系统软件,它为应用程序提供了一个运行环境,并为用户提供与计算机硬件交互的接口。
操作系统包括:
一般而言,操作系统指的是内核。
设计目的:
系统调用与库函数:
操作系统会暴露部分接口供上层开发者使用,这部分接口就是系统调用。
系统调用的功能比较基础,对使用者要求较高,所以一部分开发者将系统调用的接口进行封装,从而形成了库,有利于开发者进行二次开发。
进程(Process)是计算机中正在执行的程序的一个实例。它是操作系统资源分配和调度的基本单位,是操作系统管理计算机硬件和软件资源时的核心概念之一。
程序与进程的区别:
内存在同一时间会有成百上千的被加载进来的程序,操作系统是需要对其进行管理的。
操作系统会给每个代码和数据块建立一个struct
结构体(进程控制块),结构体存的是对代码和数据块的信息,也有相应的指针指向对应的代码和数据。许多这样的结构体组成双链表,也就是进程列表。
进程控制块PCB→Process Control Block
Linux系统中PCB
是task_struct
PCB相关内容:https://www.cnblogs.com/tongyan2/p/5544887.html
总结:进程 = PCB(task_struct) + 对应的代码和数据
进程信息被放在进程控制块中,可以理解为进程属性的集合。操作系统要对进程进行管理,其实就是对描述进程的task_struct形成的数据结构进行增删查改。
注:我们在Linux执行的指令、工具、程序,运行起来都是进程
getpid() //获取进程pid
getppid() //获取父进程pid
pid
就是进程的标识符(编号);我们可以使用 man 2 getpid
来查看相关信息。
其返回值就是一个整型变量,成功调用返回一个大于0的整数,失败就返回-1。
ps axj | head -1 ; ps axj | grep mypro //查看mypro进程相关信息
由于grep
本来也是个进程,查找mypro
进程,grep
进程就会带上mypro
的关键字,那么查出来的进程也会有grep
进程,要屏蔽的话就使用grep -v
反向匹配
ps axj | head -1 ; ps axj | grep mypro | grep -v grep
ls /proc
ls /proc
以文件的方式查看进程,proc
目录记录进程信息,每个数字目录代表一个正在运行的进程,进程结束后,对应的目录文件就会删除。
我们先运行一个pid
为70816
的进程。
再使用ls /proc
查看进程信息,可以看到有70816
的文件夹。
让我们直接查看70816
的文件夹。
exe
代表我们的可执行文件,如果将其删除不会对正在运行的进程造成影响,因为删除的是磁盘上的程序,可执行文件已经被加载在内存里了。
cmd
(current work directory
)是进程所在的当前文件路径,我们的程序在创建文件时默认是在当前路径下创建的,而当前路径就是通过cmd
获取的。
我们可以使用chdir
改变当前进程的文件路径,以下为相关信息。
结束进程:ctrl+c
或 kill -9 pid
父进程pid不变现象:
多次启动进程时,会发现其父进程的pid不变,这是因为该父进程其实就是bash
进程,我们执行的程序或者指令大多都是bash
的子进程。
当子进程出问题,不会影响bash
进行,因为进程具有独立性;当我们启动xshell
时,系统自动生成bash
进程。
创建进程需要使用系统调用fork
函数。
#include <unistd.h>
pid_t fork();
fork
系统调用,没有参数,有两个返回值
fork
在创建进程成功时,给父进程返回子进程的pid,给子进程返回0,失败时返回-1给父进程(没有子进程创建)。这样做的原因是因为父进程与子进程的关系是一对多的关系,将子进程的pid返回给父进程让其可以区分不同的子进程。
fork的使用示例:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
//子进程
while(1)
{
sleep(1);
printf("I am a child process, my pid: %d\n", getpid());
printf("\n");
}
}
else
{
//父进程
while(1)
{
sleep(1);
printf("I am a fathermak process, my pid: %d\n", getpid());
}
}
return 0;
}
从上面的现象我们可以看出,id == 0 和 id > 0 同时成立,这是因为当我们创建完子进程后,其实已经存在了两个进程,会共用fork之后的代码,对于父进程 id > 0,对于子进程 id == 0 ,在同一时间内两个进程执行不同的代码,就会产生上图的结果。
为什么fork
会返回两个值?
在fork函数内部,执行到最后的return语句时,子进程已经创建好了,两个进程就会同时执行return语句,fork就能返回两个值,我们以前认知的一个函数只能有一个返回值是在同一个进程的条件下才成立的。
为什么 id == 0
和 id > 0
同时成立?
进程具有独立性。父子进程不同的PCB、代码虽然共享,但是只读的,不会影响独立性
父进程和子进程共用代码和数据,当任意进程尝试修改数据,操作系统在数据被修改前拷贝一份,让目标进程修改这份数据的拷贝。通过这种写时拷贝技术让不同进程的数据独立,从而保证进程的独立性。
进程状态说明了进程当前的行为
进程状态就是task_struct
中的一个整数变量
各种进程状态转换图
一个进程会有如下的很多状态
CPU能执行的进程只有一个,但是进程会有很多个,所以这些进程就需要排队等待执行,而这个队列就是调度队列。一个CPU只能有一个调度队列。
以下为基本调度结构:
注:task_struct
既可以属于全局中的PCB
双链表,也可以属于调度队列中的队列,跟以前一个节点只属于一个数据结构稍有不同。
调度算法之一:FIFO
,先进先出。CPU
依次选择一个task_struct
来执行。
进程的运行阻塞挂起状态
运行:但是只要一个进程在调度队列中,它就是running
状态。换句话说,进程是运行状态,要么它持有CPU
,要么就是完全准备好随时被调度。
阻塞:进程等待某种设备或者资源就绪。如:C语言的scanf
C++的cin
,这些设备可以是键盘、显示器、网卡、磁盘、摄像头、话筒等
运行和阻塞的切换
操作系统中不仅有调度队列,也有其他的队列,比如设备队列。我们举scnaf
例子来说明进程运行与阻塞状态的改变。
一个进程要
scanf
, 操作系统要去等待键盘输入,进程无法执行,将该进程从调度队列中拿掉,链接到其他队列(特定设备的等待队列 ),该进程不会被调度,此时进程就处于阻塞状态,当键盘输入数据完毕, 进程又会链接到调度队列,进程状态变为运行状态
进程状态的变化、表现之一,就是在不同的队列中进行流动,本质都是数据结构的增删查改。
挂起状态
swap
分区以腾出内存空间资源,此时进程变为阻塞挂起状态。
PCB
对应的代码和数据交换到磁盘的swap
分区,此时进程变为运行挂起状态。
一个PCB是如何做到属于进程列表(全局双链表)又属于调度队列或者其他队列?
双链表一般定义,结构体里定义当前结构体的指针。
struct Node
{
int data;
struct Node* next;
struct Node* prev;
}
在task_struct
中,将next
指针和prev
指针再封装成list_head
的结构体作为task_struct
的成员变量。
struct list_head
{
struct list_head* next;
struct list_head* prev;
}
struct task_struct
{
......
list_head links;
}
list_head
通过偏移量去找到task_struct
的其他成员,或者说去构建一个task_struct
的指针。
&((struct task_stuct*)0→links) //links在`ask_struct的统一偏移量
(struct task_struct*)(next/list - 偏移量) //构建出一个`task_struct`的指针
我们可以通过构建出来的指针去访问task_struct
的其他成员。而一个task_struct
里会有多个list_head
类型的成员,通过各个list_head
类型的成员相互连接task_struct
,这样就做到了一个task_struct
既属于进程列表又属于调度队列或者其他队列
在Linux内核源码中,task_state_array
指针数组用于存放进程的各种状态。
static const char* const task_state_array[] = {
"R(running)",
"S(sleeping)",
"D(disk sleeping)",
"T(stopped)",
"t(tracing stop)",
"x(dead)",
"Z(zombie)"
}
R+ 代表进程是运行状态,+代表是前台运行,没有就是后台运行
内存泄漏问题
孤儿进程
如果在父子进程的关系中,父进程先于子进程结束,子进程就会变成孤儿进程。孤儿进程会被1号进程(操作系统)领养。
孤儿进程如果不被领养则会导致内存泄漏的问题。
进程变成孤儿进程会变成后台进程,一般的ctrl+c不能结束孤儿进程,结束孤儿kill -9 pid
指令。
1号进程
进程优先级指的是进程获取CPU资源的先后顺序,就好比学生在食堂排队打饭。优先级高的进程有优先占用CPU执行的权利。
进程优先级的存在可以让CPU有序的执行各个进程,发挥CPU的性能优势。
进程优先级是一种数字,其值越低,优先级越高。
注:现在大多数的操作系统是基于时间片的操作系统,考虑公平性,优先级可能会变化,但是幅度不会太大。
UID
(users id
),用于标识用户的特定数字,让操作系统可以区分拥有者、所属组和Other
PRI
,默认值为80;进程优先级的修正数据NI
,默认值为0;
PRI(new) = PRI(default) + nice
NI
的范围是[-20, 19],由此推出进程优先级的范围[60, 99]。
进程级先级的设立要合理,不然可能会导致低优先级的进程得不到CPU
的使用权,导致进程饥饿。
进程优先级的设置
进程优先级可以随意改低,但是如果要提高进程优先级,也就是将NI
值改小,需要sudo
提权。
top
指令
top
进入任务管理器r
,然后输入你要修改进程的pid
和修改的NI
值nice
指令,修改未运行的进程优先级
nice -n <nice_value> <command>
nice_value
是你希望的NI
值command
是你要执行的指令或程序renice
指令,既可以修改未运行的进程的优先级,也可以修改已经运行的进程的优先级
renice NI [[-p] pid ...] [[-g] pgrp ...] [[-u] user ...]
NI
:修改后的NI值-p pid
:需要修改优先级的目标进程pid
-g pgrp
:需要修改优先级的目标进程组的id
-u user
:指定进程拥有者为user
的进程来修改优先级竞争、独立、并行、并发
进程占有CPU,不会把代码执行完,操作系统会分配时间片,只让该进程执行特定的时间,CPU就会切换到另一个进程执行,同样执行特定的时间,经过排队重新执行死循环进程。
死循环进程不会一直占有
CPU
,与其他进程轮流执行的。
CPU
(中央处理单元)是计算机的核心部件,负责执行程序中的指令并进行数据处理。寄存器是CPU
内部的一个小型高速存储单元,用于存储临时数据、指令和操作数。
进程在使用CPU
时,寄存器保留的是进程的上下文数据。
进程在切换时,首先被进程从CPU
上剥离下来,然后将进程的上下文数据,既CPU寄存器的内容保存在task_struct
或TSS
任务状态段,如果重新执行进程,需要恢复进程的上下文数据。
进程切换的核心就是保留和恢复当前进程的硬件上下文数据,既CPU内寄存器的内容。
参考:玩转Linux内核进程调度,这一篇就够(所有的知识点)
Linux2.6内核中进程队列的数据结构
queue[140]
是一个队列数组,0-99号队列是实时进程,这类进程优先被调度,不在讨论范围内。(队列数组本质是开散列的哈希表)
CPU
资源,导致优先级较低的进程无法执行。
active
指针和expired
指针交换。
O(1)
的查找进程。
总的来说,这样的进程队列数据结构实现O(1)的调度算法,可以更大幅度地利用CPU资源,提高整体效率。
环境变量(environmentvariables)⼀般是指在操作系统中用来指定操作系统运行环境的⼀些参数
如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,⽣成可执⾏程序,原因就是有相关环境变量帮助编译器进行查找。
环境变量通常具有某些特殊⽤途,还有在系统当中通常具有全局特性
main
的命令行参数是程序实现不同功能的方法,是指令选项的实现原理。
int main(int argc, char* argv[])
{
for(int i = 0; i < argc; ++i)
printf("argv[%d]: %s\n", i, argv[i]);
return 0;
}
我们在终端输入的指令和选项时,bash会以空格为标识符来划分各个字符串,从而存入main
参数的指针数组,在main
内部根据不同的选项来实现不同的功能。
进程拥有一张argv
表,用于实现选项功能。
我们所写的二进制程序与系统的指令没有本质区别。而执行自定义程序需要指明路径,执行指令却不需要,原因在于系统存在环境变量保留有默认路径。
Linux存在环境变量PATH
,用于标识系统指令的默认搜索路径。
env #用于查询环境变量
echo $PATH #查询PATH环境变量
环境变量是内存级变量,bash启动时会读取环境变量形成一张环境变量表。
我们输入指令时,bash将其构建成命令行参数表, 将其解析后在环境变量表搜索路径,在路径后拼上你的指令名,然后创建子进程执行指令。
环境变量是从系统的配置文件来的,bash在启动时就会读取配置文件形成环境变量表。
HOME
:当前用户的家目录路径
USER
: 当前用户
LOGNAME
:登录名
su -
就会切换当前用户和登录名,相当于重新登录。
HISTSIZE
:记录最近输入指令的条数
PWD
:当前工作目录
OLDPWD
:上一次工作目录
cd -
指令可以在新旧工作目录来回切换
export MYENV=xxxx # 导入环境变量
unset MYENV # 取消环境变量
main
的参数最多有三个,是父进程bash传递的
int main(int argc, char* argv[], char* env[])
方法一:获取父进程(bash)的环境变量
#include <stdio.h>
int main(int argc, char* argv[], char* env[])
{
for(int i = 0; env[i]; ++i)
printf("env[%d]-> %s\n", i, env[i]);
return 0;
}
环境变量是可以被子进程继承的;环境变量具有全局特性。
方法二:
getenv("xxx") //获取指定环境变量
方法三:
在 Linux , environ
是一个环境变量的数组,包含了当前进程的环境变量。
environ - user environment
具体信息
使用案例
#include <stdio.h>
#include <unistd.h>
extern char** environ;
int main(int argc, char* argv[], char* env[])
{
for (size_t i = 0; environ[i]; i++)
printf("environ[%ld]-> %s\n", i, environ[i]);
return 0;
}
环境变量最重要的一个特性就是全局性,可以被被各个进程使用。
补充知识:
export
命令是内建命令(built-in command),bash不创建子进程而由bash亲自来执行。export
的作用是导入环境变量,如果是创建子进程来执行,由于进程的独立性,子进程是不能向父进程传输数据的,export
就无法导入环境变量
程序地址空间,严格来说应该是进程地址空间(虚拟地址空间),一个系统级别的概念,不是语言层的概念。它不是实际物理内存。
static
变量其实就是全局变量,只不过受到了main
的限制,只能在main
里面访问。
证明内存地址是虚拟地址。
#include <stdio.h>
#include <unistd.h>
int gval = 100;
int main()
{
__pid_t id = fork();
if(id == 0)
{
while(1)
{
printf("子进程: gval: %d, &gval: %p, pid: %d\n", gval, &gval, getpid());
gval++;
sleep(1);
}
}
else
{
while(1)
{
printf("父进程: gval: %d, &gval: %p\n", gval, &gval);
sleep(1);
}
}
return 0;
}
同一地址空间下的变量不可能同时是两个值,只能说明内存地址是虚拟地址。
原因:
注:用户无法查看实际的物理地址。
操作系统管理进程地址空间采取画饼时管理,让每一个进程都认为自己独占所有物理内存。
虚拟地址空间也是一个结构体,在Linux上是
mm_struct
如何在mm_struct
划分栈和堆等空间?
记录区域的开始和结束,调整区域就修改其开始和结束。 区域划分需要确定区域的开始和结束。
stuct mm_struct
{
long code_start, code_end;
long init_start, init_end;
//...
}
由于每个不同进程的虚拟内存区域功能和内部机制都不同,因此⼀个进程使⽤多个vm_area_struct
结构来分别表示不同类型的虚拟内存区域(栈、堆、静态区等)。
有关虚拟地址空间结构体更深的认识:
虚拟地址是一个结构体,在开辟空间初始化时,其变量的值从内存加载的代码和数据的大小来,加载程序到内存时会申请物理空间,同时在虚拟地址空间也申请相应大小的空间(调整区域划分)。
为什么会存在虚拟地址空间?
一般而言,在创建进程时先要有内核数据结构,再加载代码和数据,但是也可以不加载代码,只有task_struct、mm_struct;
进程挂起更深刻的认识:
Have a good day😏
See you next time, guys!😁✨🎞