Linux进程概念

本节将从以下几个方面进行介绍

1,认识冯诺依曼系统

2,操作系统概念与定位

3,深入理解进程概念,了解PCB

4,学习进程状态,学会创建进程,掌握僵尸进程和孤儿进程,及其形成原因和危害

5,了解进程调度,Linux进程优先级,理解进程竞争性与独立性,理解并行与并发

6,理解环境变量,熟悉常见环境变量及相关指令, getenv/setenv函数

7,理解C内存空间分配规律,了解进程内存映像和应用程序区别, 认识地址空间。

8,选学Linux2.6 kernel,O(1)调度算法架构

一,认识冯诺依曼系统结构

1,电脑中的储存结构

从电脑最外层到cpu大致可分为几层

最外层的磁盘/硬盘,到内存,高速缓存,cpu寄存器,越靠近cpu的设备造价越高,运行速度相对也越快

2,cpu读取数据的过程

在老式的电脑中,cpu通常直接访问外设,简称DMA,但这样会大大拉低整体的运行速度,类似于木桶原理。

整体运行速度往往由过程中处理最慢的设备决定,因此cpu一般只与内存或者cpu寄存器交互,这样能大大提高生产效率。基于冯诺依曼体系结构造出来的电脑能做到价格相对低效率也相对不错,性价比得到提高。

3,程序运行原理

程序 = 代码 + 数据

程序在cpu运算前会先进入内存中,然后cpu才从内存中拿取数据。但是程序在进入内存前会生成一个exe文件,这个文件只能保存在磁盘中

二,操作系统的概念与定位

1,初识操作系统

操作系统是一款软硬件资源管理的软件,是开机启动的第一个软件,它具有给用户提供稳定,高效安全的使用环境为目的。

驱动程序:

每个外设都有相对的驱动,在用鼠标或者键盘这些外设前,驱动会提前安装好。

2,操作系统的运行原理

那么系统到底是如何管理好这些数据的呢?

首先计算机采用分层管理的模式,将数据汇总至寄存器,再将数据以类似struct结构分别将属性,大小等各个数据分类,变成类似于excel表格的形式提交给系统做处理。

而数据又通过链式结构一个个链接起来,类似于队列进行节点式的数据访问。任何数据管理都可以通过先描述,再组织的形式进行管理。

 

操作系统的管理,核心是:

1,进程管理

2,内存管理

3,文件/io管理

4,驱动管理

但是计算机是不信任用户的,不允许用户直接访问底层数据,只能通过系统调用口进行访问。类似于银行系统。这些业务都是通过一个个小窗口给他人提供服务。

3,操作系统的各个结构

(1)外壳程序

当普通用户想要调用系统内部功能时,由于并不熟悉操作系统,故需要一个外壳,也就是翻译官来让普通用户和操作系统内部进行对话,类似于取银行办理业务时大堂经理的功能,指引你去办理各种不同的业务。

站在系统开发人员的角度山,可以直接调用系统接口,将系统接口封装成为各种各样好用的函数,打包形成库!!!所有开发者,用很多功能,不用自己写,直接调用库函数即可。

4,系统调用和库函数的概念

在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分 由操作系统提供的接口,叫做系统调用。

系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统 调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发

三,深入理解进程以及pcb概念

1,PCB概念

数据量庞大,为了更好的管理数据,用pcb来标记每一个文件数据的属性,pcb类似于struct结构,其中成员变量都是一个文件的所有属性。

进程 = 可执行程序 + 内核数据结构!!!!!

描述进程-PCB:

进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。

课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct

task_struct-PCB的一种:

在Linux中描述进程的结构体叫做task_struct。

task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息

task_ struct内容分类:

标示符: 描述本进程的唯一标示符,用来区别其他进程。

状态: 任务状态,退出代码,退出信号等。

优先级: 相对于其他进程的优先级。 程序计数器: 程序中即将被执行的下一条指令的地址。

内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针

上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。

I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。

记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。

其他信息

链接方式:

通过计算偏移量查找链表首地址。

2,linux中进程可视化

当然,在linux中我们可以确切的看到进程运行时的样子

首先单击选中当前会话

通过这个查看进程列表

再通过这个查看在运行中的进程

两者合并可查看具体信息

查看进程pid

进程终止的方式

3,父进程与子进程

一般子进程会随着每一次运行刷新他的uid,而父进程(bash类似的命令行解释器)却一直保持不变,由此可知子进程是由父进程为模板,从而生成的uid,我们可以用以下程序证明

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
 printf("pid: %d\n", getpid());
 printf("ppid: %d\n", getppid());
 return 0;
}

查看进程的方式:

  当进程运行时删除子进程的uid,进程不会立马终止,而是删除磁盘里的文件,而加载到内存里的文件并不会删除掉,直到运行结束时候释放

  默认情况下,进程所处的文件位置就是当前位置。进程的本质:

4,系统调用函数fork()

fork基本函数用法:

(1)只有父进程会执行fork()函数前的代码,fork函数后父进程子进程都会执行

(2)fork函数执行后,会有两个返回值,父进程返回子进程的pid,子进程返回0

(3)那么为什么要子进程呢?因为子进程和父进程可以联动执行代码,比如边播放边移动的玩具

(4)那如何让子进程和父进程执行不同的命令呢?通过判断子进程和父进程的代码返回值判断谁是父谁是子,执行不同的代码片段

用以下代码即可判断fork出现两个返回值:

当进程产生时,实际上内存只有一份代码,但是fork函数却能返回两个值,因为实际上内存里的是父进程,而子进程和父进程公用一份内存,所以两者地址是一样的。

 但是当父子进程都进入队列生成对应的pcb时,他们被调用的顺序是不固定的,由各自调度信息决定(时间片,优先等级等)+ 调度算法共同决定

整个fork函数调度过程如下:

子进程和父进程之间是独立的,kill子进程或者父进程后不会影响另外一个,因为各自的pcb是不同的,互相独立的。

但是这一切都是基于子进程和父进程各自把代码拷贝了一份,但是存在的问题就是,如果变量过多,需要修改其中一两个变量的时候,拷贝会浪费大量的空间,所以操作系统用写时拷贝的技术尽可能的节约空间。所以一个变量会有不同的值

四,进程状态

实际上理解:

状态实际就是整形变量,改的时候就是对应的整形变量改了。

1,运行状态

运行状态就是一个cpu维护一个队列,队列中有很多进程在列表中的状态。

2, 阻塞状态

阻塞状态就是当加载进程时软件/硬件方还未就绪,比如调用scanf函数时,迟迟不从键盘上输入任何值,输入还未就绪,这时候是没法在屏幕上显示出来。这个就是侠义上的阻塞状态。

操作系统时时会查看进程是否就绪,从而告诉你当前状态,是否阻塞等,因此cpu判断队列进程状态时通过变量查看的。

在操作系统中会存在很多种队列,有正在运行的队列也有等待队列,而未加载而又储存在等待队列中的进程就是阻塞状态。而将阻塞状态的进程加入运行队列的过程叫做唤醒

3,挂起状态

如果一个进程当前被阻塞了,那么它在等待还没有就绪的时候,该进程时无法调度的,但是如果此时,OS内的内存资源已经严重不足了,那么它就会从内存中的队列存到磁盘去了,而且针对所有阻塞状态的队列。这个状态就叫挂起状态。

在电脑中有一个文件叫swap文件,起到作用就是把内存的进程放入磁盘。当有内存了,这个队列又会调度进来。

进程加&变成后台进程

4,各种运行时的状态

“R”:运行状态

“s”:sleeping(浅度睡眠)可以被终止,会对外部信号做出反应

“D”:(针对磁盘设计的)deep sleeping(深度睡眠)无法终止!OS也不行,直到运行完

5,僵尸状态

僵尸状态是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。

代码验证:

返回结果:

后果:当子进程退出时没有被父进程回收,就会出现<defunct>状态,即失效状态,造成的后果就是不及时回收就会造成内存泄漏

6,孤儿状态

孤儿状态指的是父进程先退出,被父进程的父亲bash回收,从而只留下子进程没人查看他的状态,最后子进程被1号init所接管的状态。

验证代码:

最后结果:

五,进程优先级

进程排队的本质是因为计算机资源有限,进程数量远远大于计算机cpu数量,因此排队的出现即会出现优先级问题,谁先运行谁后运行呢?

1,查看进程优先级

在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:

其中:

UID : 代表执行者的身份

PID : 代表这个进程的代号

PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号

PRI :代表这个进程可被执行的优先级,其值越小越早被执行

NI :代表这个进程的nice值

PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高

那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值 PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice

这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行 所以,调整进程优先级,

在Linux下,就是调整进程nice值 nice其取值范围是-20至19,一共40个级别

2,更改进程优先级

先top。

进入top后按“r”–>输入进程PID–>输入nice值。

进程并非一直占用cpu运行,而是根据时间片,每过一段时间从上面剥离下来,切换另外一个进程。所以称这个方式是基于时间片轮转式抢占内核

进程被切换时,进程的pcb会转存cpu寄存器内的数据内容,然后cpu寄存器内的数据会被新的进程数据给覆盖。实际上可以通俗的理解为把cpu寄存器的内容放进了内存中

3,进程的其他概念

竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高 效完成任务,更合理竞争相关资源,便具有了优先级

独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰

并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行

并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为 并发

4,linux内核队列调度规则

蓝色部分对应的是active活跃队列,放已经在队列排队的进程,优先执行,红色部分对应的是等待队列,后插入的放入,无论优先级有多高也放进这里。

普通优先级:100~139(我们都是普通的优先级,想想nice值的取值范围,可与之对应!)

实时优先级:0~99(不关心)

时间片还没有结束的所有进程都按照优先级放在该队列 nr_active: 总共有多少个运行状态的进程 queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下 标就是优先级!

从该结构中,选择一个最合适的进程,过程是怎么的呢?

1. 从0下表开始遍历queue[140]

2. 找到第一个非空队列,该队列必定为优先级最高的队列

3. 拿到选中队列的第一个进程,开始运行,调度完成!

4. 遍历queue[140]时间复杂度是常数!但还是太低效了

5,active指针和expired指针

active指针永远指向活动队列

expired指针永远指向过期队列

可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在 的。

没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活 动进程

6,过期队列

过期队列和活动队列结构一模一样

过期队列上放置的进程,都是时间片耗尽的进程 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算

六,环境变量

环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数

如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但 是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找

环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性

1,常见环境变量

PATH : 指定命令的搜索路径

HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)

SHELL : 当前Shell,它的值通常是/bin/bash。

main函数的两个参数:

argc:是变量个数

char* argv【】:是一个指针数组,指向调用的函数

以空格为分隔符,分开不同字符串


2,利用main函数传参性质来模拟实现一个计算机

3,模拟实现一个touch

4,环境变量的使用

将目标路径放进后面

相关指令:

1. echo: 显示某个环境变量值

2. export: 设置一个新的环境变量

3. env: 显示所有环境变量

4. unset: 清除环境变量

5. set: 显示本地定义的shell变量和环境变量

环境变量组织方式:

每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串

5,通过代码获取环境变量

刚开始创建代码,shell会为我们创建一个环境变量表

env指针数组:

系统启动我们程序时,会传给我们两张表:
1,命令行参数表

2,环境变量表

6,main函数的第三个参数

(获取环境变量的第二种方式)env用于获取环境变量

(1)那么这个环境变量是谁传的呢?

命令行启动的进程都是bash/shell创建的子进程,所以子进程的命令行参数和环境变量是父进程bash传的!!

(2)那么父进程的环境变量信息从何而来?

每一次进行重新登陆,都会给父进程生成新的解释器,并且新的解释器会自动读取形成自己的环境变量信息。

会配置未见内容,自动生成一个bash_profile,里面自动配置好了环境变量

(3)自己配置环境变量

这是本地变量,并没有导入到bash的环境变量表中

导出环境变量到bash

也可以直接导成环境变量,但是这样重启还是会被覆盖

所以直接写道bash_profile中,即可永久使用

总结:环境变量是可以被继承,父进程给子进程,再给孙子继承,都是同样的环境变量

七,读取环境变量的第三种方法

通过第三方变量environ获取,是一个全局变量

验证:

本地变量和环境变量的区别是?

本地变量只在bash内部有效,不会被子进程继承下去!

环境变量通过让所有子进程继承的方式,实现自身的全局性!

8,内建命令

当创建本地变量时,调用echo时却能正常执行

因为大部分命令都储存在磁盘中,以子进程的方式存在,但是linux中有一种命令需要bash自己执行,因此会调用本地变量,这种叫做内建命令

9,通过系统调用获取或设置环境变量

#include <stdio.h>
#include <stdlib.h>
int main()
{
 printf("%s\n", getenv("PATH"));
 return 0;
}

导出环境变量 export MYENV="hello world" 再次运行程序,发现结果有了!

七,程序地址空间

布局图:
 

1,证明空间分布

代码区

常量区

已初始化全局数据区

未初始化全局数据区

堆区

栈区

结果验证:

2,证明堆向上生长

当定义一个struct时,同样在堆开辟一片空间,然后从地址最大处向地址小处存放数据

验证:

很明显是向变大的方向生长

3,证明栈向下生长

栈区是一次性开辟好数组大小,然后从最底下向上存放数据。

证明:

存放的地址依次在减少

当定义一个int变量时,占四个字节,每个字节都有一个地址,访问数据时访问的是第一个地址,然后根据类型判断向下访问多少字节。所以是起始处+偏移量的访问形式。

为什么static修饰的变量不能修改?

因为static修饰的代码放入了静态变量区,因为离代码区比较近,所以也不可修改。

4,命令行参数存放位置

证明:

地址依次增长。

5,进程地址空间

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int g_val = 100;

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //child
        int cnt = 5;
        while(1)
        {
            printf("child, Pid: %d, Ppid: %d, g_val: %d, &g_val=%p\n", getpid(), getppid(), g_val, &g_val);
            sleep(1);
            if(cnt == 0)
            {
                g_val=200;
                printf("child change g_val: 100->200\n");
            }
            cnt--;
        }
    }
    else
    {
        //father
        while(1)
        {
            printf("father, Pid: %d, Ppid: %d, g_val: %d, &g_val=%p\n", getpid(), getppid(), g_val, &g_val);
            sleep(1);
        }
    }
    return 0;
}

这样运行的结果:

子进程和父进程的地址居然是一样的!?

我们在c/c++得到的地址不是物理地址,这是虚拟地址!

解释:

首先每一个进程都会有进程地址空间的存在,为了让进程虚拟地址能找到物理地址,中间需要一个表来维护:(本质上是一种页表映射结构)

表的左侧是虚拟地址,右侧是物理地址:

在子进程继承的时候,会先将父进程的结构拷贝一份,再加上自己个性化的部分,页表也是一样拷贝一份过来。当拷贝变量过来的时,父子的虚拟地址都指向同一个物理地址,故变量相同。

但是当拷贝变量过来的时,父子的虚拟地址都指向同一个物理地址,故变量相同。但是这样就不能通过子类修改值,只能将100再写时拷贝一份,将拷贝的地址给子进程的虚表,这样就能修改了。

总结:
所以写诗拷贝发生再内存中,由操作系统来做,不影响上层语言。

而进程本身,其实是一个个结构体的区域化分,结构体中存放着各个类型的起始地址和结束地址

进程地址空间不是内存,而是一种结构体,而进程形成的地址空间,进程会有一个指针指向地址空间。

6,空间的概念

cpu对内存进行写入时,会访问各种寄存器,如地址寄存器,控制寄存器等。我们知道机器只识别二进制,1对应高电平,0表示低电平,可以理解为里面有无数的电池,充电的电池表示1,没充电的表示0。然后cpu通过32根总线进行传二进制,故有2的32次方种排列。

7,c/c++中变量的概念

如果变量名成了可执行程序,还有变量名的概念吗?
不会,他会变成地址!

8,为什么要有地址空间

首先每个进程都有自己的地址空间

1,让进程以统一的视角看待内存,所以任意一个进程,可以通过地址空间+页表将乱序内存数据变的有序,让栈区数据放在栈通过页表映射的位置,堆区去堆区应该在的位置,分门规划好!

2,在页表中还有每个进程对应的权限表,对内存访问权限进行限制

3,但是为什么要有这么一层软件层呢?

实际上页表是对原进程进行管理和防止,比如他只有读的权限,进程却想进行写入,这样就会被拦截,保证内存的安全,也能防止越界非法访问,保证安全性。

4,页表是怎么知道将进程放在什么位置,如何放到内存的呢?

进程只有在运行时才会被页表读取到,才能进程各种转换,访问等。而这是一种在cpu上的寄存器——CR3实现的,他能获得页表的地址,链接进程和页表,其中存的是页表的物理地址

而在进程切换时,当前进程会把CR3内的页表地址带走,新的进程会将页表地址再放入。

5,进程的挂起状态

页表除了能链接虚拟物理地址,控制访问权限外,他还有一张表存放的时进程在内存中的状态,其中表面了进程是否分配了内存,是否有内容等。

缺页中断:

当存放进程状态的表表示一个进程没有被分配空间也没有内容时,进程会被暂停访问,但是要运行时系统会帮我们申请内存并且放入内容,这个过程就叫缺页中断

6,页表在进行虚拟地址到物理地址转换时,父进程/操作系统时无法知晓这一过程的,所以它最后一个意义就是可以将进程管理和内存管理进行解耦

9,进程具有独立性

每个进程具有独立的pcb和页表,因此有独立的内核结构,映射关系通过页表转换导致在内存中的位置不一样

八,struct_mm空间

如果想在栈上独立开辟一小段空间,他会开辟一个叫做vim area_struct的结构体,存放独立数据,然后他会有next指针将所有建立的独立空间链接起来,通过mm_struct中的指针指向第一个vim area_struct的头节点地址。mm_struct叫做内存描述符,area_struct叫做线性空间。

1,task_struct的pcb

问题:如果内存地址空间仅仅只是分层明确的结构的话,会存在诸多问题,例如空间碎片化,空间浪费等,这时候我们需要将简单的地址划分弄得更加精确

打开他的pcb,我们就能明确看到这里存了诸多的指针,划分开堆区,栈区,共享区等,明确

本节完!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值