冯诺依曼体系结构——存储器
存储器主要指的是内存,它有个特点就是掉电易失
磁盘等其它输入和输出设备
为什么要在计算机体系结构中要存在内存
我们知道,CPU的处理速度很快很快,但输入设备,以及输出设备,是相对很慢的两个结构,CPU处理完某件事之后,等待着输入设备再次将程序送进来进行计算,但是输入设备又不能很快的送达,因此,在等待的这段时间内,CPU是处于一个闲置的的状态,完全浪费了CPU高速处理事件的能力。这就需要一个物件积存一定量的任务,等到满足某个条件的时候,再将它送给CPU
OS——管理者
为什么要有操作系统:
操作系统是在计算机出来之后慢慢才有的,没有操作系统,就要自己控制硬件,人工管理硬件,效率低,它是管理者
就好比,你在渲染视频,你需要左手一边渲染,右手人工转动风扇,防止机体温度过高,导致线路烧坏
也好比,你在打王者荣耀,你需要一只手一直操控界面,另一只手要扯弄着网卡
有了OS的管理,我们才会方便很多
所以从为什么需要OS可以转换为为什么要有OS的管理
为什么要OS的管理
OS通过对下管理好软硬件资源,从而对上提供一个良好的运行环境(稳定,高效,安全)
前者是手段,后者是目的,所以我们才需要操作系统,因为人工管实在是太慢了
操作系统管理软硬件方式
先描述,在组织
即先描述好软硬件的基本属性,自身状态等内容,将这些属性抽象成结构体或者类类型,从而得以组织起来
再用适合的数据结构,如:链表,队列等,将其串联起来
这样只要OS需要某个硬件干什么,就先告诉驱动去办,搞完后再回头通过修改存储有各种硬件的数据结构里的数值,从而达到更新效果
如果保证OS的安全
系统调用接口
为了保证操作系统的安全,即防止某些用户会对操作系统里的数据进行非法行为,操作系统就在用户和操作系统之间再设计一层系统调用接口,这里的系统调用接口,就是操作系统向上提供的调用接口,这些接口的存在不允许用户直接访问操作系统,只能通过它们才能访问。
这些接口也相当于用C语言设计的函数,由操作系统提供
就好比银行自动柜台,它不会直接将钱币暴露给你,而是通过柜台,一步步验证你的身份信息,密码等相关内容,将你所需要的一定数目的金额发放出来。所有的柜台都是银行提供的,所有的动作银行都能监控,这些柜台就相当于系统调用接口
经过OS的函数,因为OS不信任任何东西,这个函数的底层一定会封装系统调用,比如printf(), 只要是会影响到底层硬件的函数,一定会包含系统调用接口
系统调用接口的实例
库函数由用户层提供,并不一定所有的库函数都会调用系统调用,即用户可以直接跨过用户操作接口使用系统调用接口。
而用户操作接口就相当于系统调用接口的封装,以便用户可以直接调用等等,然后很多接口又封装成lib,比如printf 和 scanf 被封装在C++/C标准库里
真正实践了OS通过对下管理好软硬件资源,从而对上提供一个良好的服务(稳定,高效,安全)
越级访问
只要库函数调用了系统调用,它两就是上下层关系,库函数在上,系统调用在下
所有用户都不能直接越过OS就访问软硬件,驱动程序,即不能够越级访问,如果越级访问了,那么我还要OS的管理干嘛呢,如果用户直接越过OS就访问软硬件,那么用户会不会对我的软硬件干坏事呢?所以不允许用户直接越级访问
进程
我们编译后的二进制文件是放在外设磁盘中,要运行的时候加载到内存里,然后被CPU运行计算
我们可以启动多个程序,意味着将多个exe加载到内存
OS如何管理加载到内存的多个程序 ?
PCB(task_struct)
先描述,在组织
加载到内存的exe,OS刚开始的时候并不认识
OS为了更好的管理每一个加载进内存的exe, OS必须为每一个进程创建一个描述该进程的结构体变量或者对象 ,然后在将属性记录下来,这些结构体变量或者对象就被称为PCB
struct PCB{
//状态
//标识符
//优先级
//内存指针字段
// ...
//几乎所有的属性字段
//struct PCB* next;
}
PCB:进程控制块
所以,某个程序运行时需要的字节大小,其实在内存时都会给这个程序多开,多出来的一般就会存放这个进程的PCB
进程的管理跟变成二进制的代码没有任何关系,跟与之形成的PCB有关系,对进程的管理转换为对PCB的管理
为什么程序加载到内存之后,变成进程之后,我们要给每一个进程形成一个PCB的对象呢?
因为OS要进行管理
所以进程 = 内核PCB对象 + 可执行程序
内核数据结构 + 可执行程序 也可称为进程
进程排队
我们一般所说的让进程排队之列的,本质上是指PCB在排队,并不是可执行程序在排队
一般用队列实现,所以进程能被动态的调度,本质上就是把进程PCB放入到运行队列里,让CPU调度
进程排队的管理
所有对进程的控制和操作都只和进程的PCB有关,跟进程的可执行程序没有关系!!!
PCB不仅可以放到链接里,还可以放到其它容器里
PCB在Linux叫做 task_struct,属于OS的数据
task_struct内容分类
- 标识符:标示符: 描述本进程的唯一标示符,用来区别其他进程,PID.
- 状态: 任务状态(是运行的,还是阻塞的等等),退出代码,退出信号等。
- 优先级:相对于其他进程的优先级,以此来确认谁先得到CPU运算。
- 程序计数器: 程序中即将被执行的下一条指令的地址,程序寄存器。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据 。
- I/O状态信息: 包括显示的I/O请求,分配给进程的 I/O 设备和被进程使用的文件列表
- 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
程序计数器:
CPU干的活就是取指令->分析指令->执行指令
CPU内部存在一种寄存器,里面存着eip/PC指针
PC指针指向的是当前正在执行指令的下一条指令的地址
判断,循环,跳转,本质就是修改PC指针
同时pc指向哪一个进程的代码,就表示哪一个进程被调度执行,pc指针就能成为程序计数器
进程实例
ps axj 查看进程指令
命令本身指令,几乎所有的独立运行的指令,都是程序,运行起来也要变成进程
PCB是在OS内进行维护的,它属于内核数据结构,所以要获取PCB的数据,就必须经过OS的调用
PID就是PCB(task_struct) 的标识符,getpid(),用于获取子进程编号
一般在Linux中,每个子进程都有父进程,父进程就是PPID,getppid(),获得的就是父进程id
每一次启动进程的pid几乎都会变化,因为已经变成新进程了
但是父进程却一直都没变,所有的进程都是bash的子进程
进程信息也可以通过文件目录查询到
ll /proc
这些蓝色字体就是进程的PID,LINUX会将进程相关的信息,以PID为命名的目录形式,把进程的属性放到该目录下
磁盘文件与内存文件区别
一个进程启动了,将它的可执行文件删掉之后,我们能发现这个进程还在跑
因为在运行一个程序时,本质是将磁盘的内容拷贝到内存,删除的是磁盘里的内容,但我内存里拷贝的那份还在跑,当我退出的时候,这个进程才彻底消失并且再也找不到了
cwd:当前工作目录
在我们创建在编译器用fopen等函数,创建文件的时候,编译器就会根据cwd为其保证是在当前目录底下的
那么如何改变当前目录呢?
更改当前目录
用change dir
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main(){
printf("这个进程的pid:%d\n",getpid());
printf("现在更改它的当前工作目录\n");
sleep(10);
chdir("我想要的目录");
ptintf("更改当前工作目录之后!\n");
FILE* fp = fopen("test.txt","w");
if(fp == NULL)
return 1;
fclose(fp);
}
所以当前目录是什么咋咋咋之类的,一定是因为当前程序运行起来变成进城之后,PCB里有cwd,从而能够认识到当前目录是什么
通过系统调用创建进程-fork
返回值pid_t ,它是一个整数,fork函数就是创建子进程的
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4
5 int main(){
6 printf("在使用fork之前,我是一个进程了,我现在的pid是:%d,ppid是:%d\n",getpid(),getppid());
7 fork();
8 printf("在使用fork之后,我是一个进程了,我现在的pid是:%d,ppid是:%d\n",getpid(),getppid());
9 sleep(3); //防止乱序,让它休眠3秒
10 return 0;
11 }
我们发现在使用fork之前这条语句,每次运行只打印一遍,而在使用fork之后这条语句打印了两遍
也就是在fork之后,就分成了两个分支,两个分支都会走
printf("在使用fork之后,我是一个进程了,我现在的pid是:%d,ppid是:%d\n",getpid(),getppid());
但是经历了fork之后,第一个进程跟原本的进程是一致的,第二个进程是第一个进程的子进程
fork之后,父和子进程都会进行
那我原本的进程1546是谁,我们应该如何查看
使用命令:
ps ajx | head -1 && ps ajx | grep 1546
很明显,它是我们的bash进程
fork返回值
pid_t id=fork(); printf("在使用fork之后,我是一个进程了, 我现在的pid是:%d,ppid是:%d,retuen id是:%d\n", getpid(),getppid(),id);
返回值说明
在成功的时候,fork之后的,对父进程返回PID,对子进程返回0
如果失败了,就返回-1给父进程,不创建子进程
不同进程可以同时跑动
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main() {
printf("在使用fork之前,我是一个进程了,
我现在的pid是: %d,ppid是: %d\n", getpid(), getppid());
sleep(3);
pid_t id = fork();
if (id == -1) return 1;
else if (id == 0) {
while (1) {
printf("在使用fork之后,我是一个进程了,
我现在的pid是: %d,ppid是: %d, retuen id是: %d\n", getpid(), getppid(), id);
sleep(1);
}
}
else {
while (1) {
printf("在使用fork之后,我是一个进程了,
我现在的pid是: % d,ppid是: % d, retuen id是: % d\n", getpid(), getppid(), id);
sleep(1);
}
}
sleep(3); //防止乱序,让它休眠3秒
return 0;
}
可以看到系统在不断的编译着两个死循环,这两进程又是父子进程
即父进程再跑,子进程也在跑
为什么fork的时候,两类代码都能跑
我们的每一类进程 = 内核数据结构 + 可执行程序的代码和数据
fork函数是父进程自己执行的,创建一个进程的时候,系统就会多一个进程
创建出了子进程,意味着创建出了子进程的task_struct,但是这个时候,我们又没有相应的可执行程序的代码和数据给子进程,所以它会指向父进程的代码和数据指向 ,但是父进程可以进行代码分流
父进程会将自己PCB里面的很多属性拷贝给子进程,从而才能使子进程跟父进程看到同样的代码,父进程并不是100%给子进程
用相同的代码,执行出不同的结果的原因是,父进程的代码进行了分流
Q:给父进程返回PID,给子进程返回0,为什么会这样
A:因为一个子进程只有一个父进程,子进程可以很容易的找到父进程,但是一个父进程可以有多个子进程,为了方便找到那个子进程,就需要返回那个子进程的编号,即PID
Q :fork为什么会返回两次
A :在fork函数里面,当已经运行到了最后开始执行return的时候,这个函数核心逻辑就已经做完了,也就是说子进程的创建,子进程PCB的拷贝复制,已经指向同一块代码的时候就已经完成了,此刻的他们已经运行他们各自的return了,也就是说代码在返回之前就裂开了
Q :id怎么可能同一个变量,既等于0,又大于0
A :进程是具有独立性的,互相不能互相影响,父子进程也是如此
OS能够保证进程之间的独立性,当某方想要修改其中代码的属性的时候,比如子进程想将代码里的a变量改为自己想要的值,OS为了父进程不受影响,OS会拷贝一份数据,交给子进程,让子进程拿着这份数据去玩,不要打扰父进程,同样,反过来也是如此,这种就叫做写时拷贝
写时拷贝带来的结果便是,子进程和父进程会使用两个不同的空间
返回的本质,就是写入
在Linux中,相同变量名可以表示不同的内存
一次创建多个进程
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
const int num = 10;
void Worker(){
int cnt = 12;
while(cnt)
{
printf("child %d is running ,cnt: %d\n",getgid(),cnt);
cnt --;
sleep(1);
}
}
int main(){
for(int i =0;i<num;i++){
pid_t id = fork();
if(id<0) break;
if(id == 0)
{
Worker();
exit(0); //结束一个进程,这里就是结束子进程,从而不妨碍下次的子进程
}
printf("father create child ,child's PID is %d\n",id);
sleep(1);
}
//只有父进程才能走到这里
sleep(10);
return 0;
}
进程的状态
1,进程排队
进程不是一直运行的
它可能在等待某些资源,比如scanf( ) ,在输入数值之前,系统就相当于在等待数值资源,进程因此也没在运行
进程放在了CPU上,也不是一直会运行的,
写个死循环加载到CPU上,CPU不会放任这个死循环就一直让它搞事情,CPU有个叫时间片的东西,只要你的进程超过一定的阈值,它就会将这个进程拿下来
进程排队,一定是等待某种资源,这里的排队是指 task_struct(PCB) 在排队
一个task_struct 可以被链入多种数据结构中
这里那链表为例子
struct listnode
{
struct listnode* next;
struct listnode* prev;
}
每个PCB之间排队的时候又不是直接头对尾这样排,而是中间嵌入一个结构体,存放前驱和后驱指针,用它来进行排队,这样排队的占比大小又会减少
通过链表链接,我们是能够知道listnode的地址值的,但是我们却不能直接的知道我们每个PCB的开头值,那么我们可以
令 &n 为中间结构体的地址
&n
:表示获取变量n
的内存地址。
((task_struct*)0)
:这是一个类型转换操作。0
被强制转换为task_struct
类型的指针,变成0x0000 0000 0000 0000。
->
:这是C语言中指针的成员访问操作符。它允许你通过指针访问结构体的成员。
n
:这是结构体task_struct
中的一个成员变量。整个表达式
&n - &((task_struct*)0->n)
的意思是,它计算了两个地址之间的差值。第一个地址是变量n
的地址,第二个地址是通过将0
转换为task_struct
类型的指针并访问它的成员n
得到的地址。这个差值用来表示成员n
在结构体task_struct
中的偏移量。
也就是说把0号地址当成了开始处,到对象0的n处的话,就是偏移
哪个进程需要进行的话,就将它到相应的队列里,然后更改对应的链表指针指向即可
以上便是本次博文的学习内容,如有错误,还望大佬指定,谢谢阅读