系统与进程——操作系统概念和进程概念

目录

冯诺依漫体系结构

操作系统概念

(1)操作系统概念

(2)操作系统用途

(3)操作系统功能实现原理

(4)操作系统组织结构图

(5)系统调用&库函数

进程概念

(1)程序与进程的概念

(2)操作系统如何管理进程

(3)描述(PCB)

(4)组织

(5)创建子进程

(6)僵尸进程&僵尸状态(Z)

(7)孤儿进程

(8)环境变量

(9)进程虚拟地址空间

(10)进程优先级问题

冯诺依漫体系结构

(1)冯诺依曼结构体系由输入设备、输出设备、控制器、存储器、运算器组成

(2)中央处理器功能:算数运算与逻辑运算

(3)存储器:将数据进行短期存储,等同于内存

所有数据都是采用2进制进行存储的(电流的高低电平模拟二进制数据)
运算产生的数据都存储在内存当中
操作系统概念
(1)操作系统概念
操作系统 = 操作系统内核 + 应用集合

操作系统:计算机系统的一个基本程序集合,成为操作系统(os)
操作系统内核:代码程序,用于对计算机的进程、文件、驱动、内存等进行管理
应用集合:依附操作系统内核实现具体功能的程序
(2)操作系统用途
操作系统用于管理计算机的软硬件资源

软件资源:进程资源、驱动资源
硬件资源:CPU、内存、硬盘、
(3)操作系统功能实现原理
操作系统通过管理实现上述功能

管理 = 描述(结构体)+ 组织(串联结构体)

(4)操作系统组织结构图

(5)系统调用&库函数
系统函数:操作系统提供的函数,被称之为系统调用函数

使用时参数多,难度大,不适合新手

库函数:C标准库提供的函数,被称之为库函数。库函数的代码实现当中调用了系统调用函数

使用时参数少,难度低,适合新手(例如C语言中的printf)

进程概念
(1)程序与进程的概念
程序:源代码经过编译产生的可执行文件,这个文件是静态的

进程:程序运行起来的实例,是动态运行的

(2)操作系统如何管理进程
进程的管理 = 描述(PCB)+ 组织的方式(链表)

(3)描述(PCB)
描述用于进程在Linux中显示自身相关信息,称之为进程控制块(PCB)

struct task_struct{
进程标识符(进程号)
进程状态
程序计数器
上下文指针
内存指针
记账信息
IO信息
...
}
1.进程标识符:在当前机器能够唯一表示一个进程

进程号(PID) 父进程号(PPID)

2. 获取进程号方法

(1)指令方法:

ps aux | [管道查询关键字] 指令方法一
ps -ef | [管道查询关键字] 指令方法二(在方法一的基础上怎增加父进程号)
(2)代码方法:

getpid(); 获取当前进程进程号
getppid(); 获取当前进程的父进程号
3.进程调度

问: 如果一个进程需要执行自己的代码,就需要向计算机获取CPU资源,但是CPU数量远小于进程数量,该如何来调度?

答:进程通过被操作系统调度来获取CPU资源,CPU运行程序,时按照汇编指令进行运算的

调度策略:

1.先来先服务

2.短作业优先

3.长作业优先

4.优先级优先

5.时间片轮转(5-800毫秒)

4.进程状态

(R)运行:进程正在CPU上运行:正在使用CPU来执行自己的代码
就绪:进程已经具备运行能力,但CPU还没分配过来:进行已经准备就绪,等待操作系统调用
阻塞:进程因等待某件事发生而暂时不能运行:例如等待IO输入,调度某些阻塞接口
(S)可中断睡眠状态(代码):进程正在睡眠(被阻塞),等待资源唤醒,或其他进程信号唤醒,进入运行队列
(D)不可中断睡眠状态:通常等待一个IO结束(输入输出结束)
(T)暂停(ctrl+z)(代码):暂停进程
(t)跟踪:调试程序的时候可以看到
(X)死亡状态:用户无法看到,当PCB被内核释放后进程被置为X,然后进程退出
(Z)僵尸状态:子进程先于父进程退出,子进程变为僵尸状态
(+)前台进程:进程状态后面跟着的加号表示该进程为前台进程
5.进程执行方式

抢占式执行:当程序准备好后进入就绪队列,即可被操作系统调度运行

并发:多个进程在一个CPU下采用进程切换的方式,各自独占CPU运行各自的代码一小段时间,交替运行,让多个进程都得以推进,称之为并发。

并行:多个进程在多个CPU下各自运行各自的代码,称之为并行

扩展:在一个多核CPU当中,并发和并行是混合存在的

6.程序计数器:

保存程序下一条执行的指令

当多个进程之间使用时间片轮转的进程调度方式时,当一个进程的调度时间片结束时,会记录即将执行的下一条指令,当该进程再一次被时间片轮转调度时,即可继续运行程序

7.上下文信息:

保存寄存器当中的内容

记录进程切换时寄存器内保存的信息,当恢复现场时则直接利用上下文信息继续运行程序 (在多线程的系统中,操作系统调度进程,获取CPU之后,进行恢复现场,继续执行后面的代码)

8.内存指针

指向 ” 程序地址空间 “,程序地址空间中的每一个地址都有一个地址编号(0x00000000~0xFFFFFFFF),地址的读取根据大小端从下而上取第一个字节的地址编号。

大端机器和小端机器
小端:低位存在低地址(x86机器都是小端机器)

大端:低位存在高地址

9.记账信息

记录进程运行时使用CPU的时长,占用内存的大小

10.IO信息

保存进程打开文件的信息

一个进程被创建出来,默认可以使用标准输入、标准输出、标准错误(stdin、stdout、styderr)三个文件(Linux中一切皆文件),三个文件的位置为:

/proc/[进程号]/fd 文件内有0、1、2三行
0:标准输入scanf 1:标准输出printf 2:标准错误perror
这三个文件与进程保持同步,当进程结束后,三个文件消失

(4)组织
多个task结构体通过双向链表连接起来

(5)创建子进程
1.fork()

作用:让一个正在运行的进程调用该函数,可以让运行的进程创建出来一个子进程,二者是父子关系

注意:由父进程程序中创建的子进程,执行代码时从fork()的下一条指令开始向下执行

原因:当运行到fork指令时,子进程先创建一个struct结构体,然后去复制父进程结构体中的全部内容,其中父进程结构体中的程序技术器记录了fork的下一条指令,所以复制过去的子进程也是从fork的下一条指令开始执行的。

参数:没有参数

#include<unistd.h>
pid_t fork(void);
2.fork()的返回值

子进程创建成功:返回两次,父进程返回大于0的数字(该数字为子进程的PID),子进程返回等于0

子进程创建失败:返回-1

pid_t xxx = fork() 用xxx来接收fork返回值
3.原理

子进程是拷贝父进程的PCB

结论:(1)父子进程代码共享,因为子进程拷贝父进程的PCB,所以拥有相同的代码

(2)数据都有(站区、堆区的数据),以为各自有各自的程序地址空间

(3)父进程将子进程创建出来后,子进程就是一个独立的进程,被操作系的独立进行调度,调度时父子进程抢占式执行。(父进程创建子进程成功后,父子进程被调度顺序不一定)

4.既然父进程的代码是相同的,那么子进程是从哪一行代码开始执行的?

在父进程程序中创建的子进程,执行代码时从fork()的下一条指令开始向下执行

原因:当运行到fork指令时,子进程先创建一个struct结构体,然后去复制父进程结构体中的全部内容,其中父进程结构体中的程序技术器记录了fork的下一条指令,所以复制过去的子进程也是从fork的下一条指令开始执行的。

5.针对fork的返回值,让父子进程执行不一样的代码块

将返回值分为三种情况,第一种情况为-1,此时进程没有成功创建;第二种情况为0,此时为子进程;第三种情况为大于0的数,此时为父进程返回的子进程号。

利用if-else语句可以分别对这三种情况对应的未创建、父进程、子进程分别执行不同的程序代码。

6.扩展:正常在命令行当中启动的程序,他的父进程是谁呢?

bath:是一个命令行解释器(命令行解释器本质也是一个进程)

在命令行中启动一个进程,原理是:命令行解释器的进程,创建子进程,让子进程进行程序替换,替换为目标程序。

在命令行运行命令时,是通过bath创建的子进程来执行该命令或程序,当bath的子进程被释放掉时,即可继续执行命令行,此时如果bath创建的子进程的子进程仍在运行(孤儿进程),则两者互不影响。

当命令行正在运行时,一般情况下会阻塞bath进程

前台进程:阻塞bath运行的进程

后台进程:不阻塞bath运行的进程

(6)僵尸进程&僵尸状态(Z)
1.僵尸状态&僵尸进程产生的原因

子进程先于父进程退出,在退出时会告知父进程(信号),父进程接收到信号后忽略处理,父进程并没有回收子进程的退出状态信息,从而导致子进程变成僵尸进程

退出状态信息:1.退出码

2.退出信号

3.coredump标志位

2.僵尸进程的危害

子进程的PCB(task_struct结构体)没有被操作系统内核释放,导致内存泄漏(使用ps进程查询指令可以查看相关信息)

3.僵尸进程的解决方案

(1)终止其父进程

(2)重启操作系统

(3)进程等待(进程控制)

扩展:kill命令(与后面学习的进程信号强相关)

kill [pid] 终止一个正在运行的进程
打印:Terminated
kill -9 [pid] 强杀一个进程
打印:Killed
注意:一个进程使用kill命令只能生效一次

(7)孤儿进程
1.产生原因

父进程先于子进程退出,子进程就变成了孤儿进程

2.进程模拟

利用分支语句为父进程和子进程编写不同的执行代码,父进程单次打印后退出,子进程循环打印。当父进程退出后,子进程的父进程号变为1,即1号进程。

3.孤儿进程退出信息由谁来回收

孤儿进程退出时,退出状态信息由1号进程进行回收

4.孤儿进程有危害

孤儿进程没有任何危害,1好进程会回收孤儿进程的退出状态信息

5.补充

1号进程:操作系统的init进程,操作系统很多进程都是由该进程创建出来的

有孤儿进程,但是没有孤儿状态

(8)环境变量
1.什么是环境变量

环境变量是指操作系统中用来指定操作系统运行的一些参数(操作系统通过环境变量来找到运行时的一些资源)

2.常见的环境变量

PATH:指定可执行程序的搜索路径,程序员执行的命令之所以能找到,是环境变量的作用
HOME:登录到Linux操作系统的用户家目录
shell:当前的命令行解释器,默认是“/bin/bash”。(本质上是一个可执行程序,运行起来也是一个可执行程序)
3.查看环境变量

env 查看全部环境变量值
echo $[环境变量名] 查看单个环境变量值
环境变量名:环境变量值1:环境变量值2:环境变量值3:...

4.环境变量对应的文件

系统级文件(不推荐):针对各个用户都起作用(root用户修改),不推荐因为会影响到其他用户。

/etc/bashrc
用户级文件(推荐):只对用户自己的环境变量做出修改,只影响自己

~/.bashrc
~/.bash_profile
三个环境变量文件的层级关系

用户
~/.bash_profile
~/.bashrc
/etc/bashrc
5.修改环境变量

export [环境变量名] = $[环境变量名]:[新添加的环境变量内容] 标准范式
命令行中直接修改(临时生效,重启终端恢复默认)
export [环境变量名] = [新添加的环境变量内容] 新增环境变量修改格式
export [环境变量名] = $[环境变量名]:[新添加的环境变量内容] 修改老的环境变量格式
文件中需修改(永久生效)
export [环境变量名] = [新添加的环境变量内容] 新增环境变量修改格式
export [环境变量名] = $[环境变量名]:[新添加的环境变量内容] 修改老的环境变量格式
补充:文件生效指令:source [文件名]

6.环境变量的组织方式

环境变量是以字符指针数组的方式进行组织的,最后的元素以NULL结尾(程序依次拿取环境变量直到NULL)

7.代码获取环境变量

方法一:main函数的参数

结构说明:

(1)命令行参数
命令行参数的个数:argc
命令行参数的值:argv[ ]
(2)环境变量:envp[ ]

int main(int argc,char* argv[],char* envp[])

示例:
int main(int argc,char* argv[],char* envp[])
{
for(int i = 0;envp[i] != NULL;i++)
{
printf("---[%s]\n",envp[i]);
}
return 0;
}
方法二:environ(二级指针)

extern char** environ;

示例:
int main()
{
extern char** environ;
for(int i = 0;environ[i] != NULL;i++)
{
printf("---[%s]\n",environ[i]);
}
return 0;
}
方法三:getenv(获取单个环境变量)

#include <stdlib.h>

char *getenv()

示例:
int main()
{
printf("---PATH =%s\n",getenv("PATH"));
return 0;
}
(9)进程虚拟地址空间
1.c语言当中的程序地址空间图

2.代码

情况一:父子进程打印全局变量A的值和地址

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

int A=10;
int main()
{
pid_t B = fork();
if(B<0){
return 0;
}else if(B == 0){
printf("i am child,A=%d,address=%d",A,&A);
}else(){
printf("i am father,A=%d,address=%d",A,&A);
}
return 0;
}

情况二:父子进程打印各自修改全局变量A后的值和地址

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

int A=10;
int main()
{
pid_t B = fork();
if(B<0){
return 0;
}else if(B == 0){
printf("i am child,A=%d,address=%d",A,&A);
}else(){
printf("i am father,A=%d,address=%d",A,&A);
}
return 0;
}
结果:情况一下,父子进程打印的值和地址相同;情况二下父子进程打印的值不同,但地址相同

结论:(1)父子进程打印的变量不是同一个变量

(2)变量使用的地址不是物理地址,是Linux地址下的虚拟地址

3.虚拟地址

在C/C++语言中所看到的地址,全部都是虚拟地址,由OS(操作系统)统一将程序中的虚拟地址转化为物理地址,用户无法看到物理地址。

操作系统利用虚拟地址可以提高内存的使用率

4.进程虚拟地址

操作系统为每一个进程虚拟一个4G的虚拟地址空间(32位操作系统),程序在访问内存的时候,使用的是虚拟地址进行访问
对于操作系统虚拟出来的地址,并不能直接存储数据,存储数据还是在真实的物理内存当中,所以操作系统需要将虚拟地址转化为物理地址进行访问(页表)
扩展:为什么操作系统要给每一个进程都许你一个进程地址空间?为什么不直接访问物理内存来达到更快的速度?

解释:因为多个进程访问同一个物理地址空间会造成不可控,在有限的内存空间中,进程不清楚哪个内存被其他进程使用,哪个进程是空闲的。所以在这种情况下随意使用会导致多个进程访问同一个物理内存出现混乱。

所以内存由操作系统统一管理起来,在不能使用预先分配内存的方式下,给每个进程分配了4G的地址(虚拟内存),不预先分配内存是因为操作系统也不清楚进程真正要保存多少数据以及使用多久。

最后每个进程都“无感”的拿到了4G的虚拟地址空间

5.页表

页号 页内偏移
作用:用于连接虚拟地址空间和物理地址空间

映射关系:虚拟地址空间分成一页一页(页的大小为4096),物理内存分成一块一块,使用页表将页和块映射在一起

根据虚拟地址+页表查找物理地址:

虚拟地址 = 页号 + 页内偏移
页号 = 虚拟地址 / 页的大小
业内偏移 = 虚拟地址 % 页的大小
分段式

段号 段的起始地址
虚拟地址=段号+段内偏移
段表 段号:段的起始地址
段页式(一个段表+多个页表)

段号 页表的起始位置
页号 块号
虚拟地址=段号+页号+页内偏移
段表 段号:页内的起始位置
页表 页号:块号
(10)进程优先级问题
PRI:进程优先级,数值越小越优先被执行
NI:nice值,进程可被执行的优先级的修正数值,当nice值为负值时,程序会将优先级值变小,其优先级会变高,越先被执行
Linux操作系统下,调整进程优先级就是i调整进程nice值
nice取值范围为-20至19,一共40个级别
修改进程优先级方法(root用户使用)

输入top;按“r”后输入进程PID;再输入想要的nice值

————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/w2583467558/article/details/130168109

版权声明:
作者:SE_Yang
链接:https://www.cnesa.cn/2115.html
来源:CNESA
文章版权归作者所有,未经允许请勿转载。

THE END
打赏
海报
系统与进程——操作系统概念和进程概念
目录 冯诺依漫体系结构 操作系统概念 (1)操作系统概念 (2)操作系统用途 (3)操作系统功能实现原理 (4)操作系统组织结构图 (5)系统调用&库函数 进……
<<上一篇
下一篇>>