Linux基于centOS7【内存与OS的随谈】,进程初学【PCB】【fork】【进程排队】

冯诺依曼体系结构——存储器

存储器主要指的是内存,它有个特点就是掉电易失

磁盘等其它输入和输出设备

为什么要在计算机体系结构中要存在内存

我们知道,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 为中间结构体的地址

  1. &n:表示获取变量n的内存地址。

  2. ((task_struct*)0):这是一个类型转换操作。0被强制转换为task_struct类型的指针,变成0x0000 0000 0000 0000。

  3. ->:这是C语言中指针的成员访问操作符。它允许你通过指针访问结构体的成员。

  4. n:这是结构体task_struct中的一个成员变量。

整个表达式&n - &((task_struct*)0->n)的意思是,它计算了两个地址之间的差值。第一个地址是变量n的地址,第二个地址是通过将0转换为task_struct类型的指针并访问它的成员n得到的地址。这个差值用来表示成员n在结构体task_struct中的偏移量。

也就是说把0号地址当成了开始处,到对象0的n处的话,就是偏移

哪个进程需要进行的话,就将它到相应的队列里,然后更改对应的链表指针指向即可

以上便是本次博文的学习内容,如有错误,还望大佬指定,谢谢阅读


 

  • 22
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值