进程概念
冯诺依曼体系结构
- 冯诺依曼体系架构图
输入设备:键盘、鼠标、扫描仪、网卡等
输出设备:屏幕、网卡、打印机等
存储器:是内存,而非磁盘
中央处理器:又称CPU,由运算器和控制器构成,可以处理算数运算和逻辑运算
注:
- 不考虑缓存情况,这里的CPU只能对内存进行操作,不能访问外设
- 外设在写入数据或输出数据的时候,只能写到内存或者从内存读取数据
- 所有设备都只能直接和内存打交道
试试
- 从冯诺依曼体系结构看数据流转
举例:微信聊天数据传输过程
①A向B发送信息“Hello”主要经历以下4个步骤:
②B接收A发送的信息:
接下来B回复A以及A接收B的信息与上述过程类似,这里不再描述。
- 两个重要思想
所有的数据都是按照二进制存储的,计算机处理的数据都是二进制
运算产生的数据都是存储在内存中
操作系统概述
操作系统是什么
- 操作系统 = 操作系统内核 + 一堆应用
内核 :也是代码程序,代码的作用是进行进程管理、内存管理、文件管理、驱动管理等
一堆应用:依附在系统内核上完成某些功能
操作系统做什么
- 管理计算机的各种软硬件资源
硬件资源:CPU、内存、硬盘、网卡、显示器等
软件资源:进程资源、驱动程序等
如何做
六字真言
先描述再组织
描述:通过结构体
组织:串联结构体
操作系统组织结构图
系统调用&&库函数
- 系统调用函数
操作系统内核提供的函数 - 库函数
C标准库提供的函数
为什么库函数的代码实现中会调用系统调用函数??
毋庸置疑的,因为库函数的操作也是要基于操作系统的,而涉及到操作系统就势必会涉及到计算机的各种软硬件资源,那么操作这些资源就要用到内核提供的系统调用函数
为什么有了系统调用函数,还要有库函数?
有些系统调用函数的函数参数比较生僻,对程序员的要求比较高,所以一些“大牛”就将难用的系统调用函数进行封装,再提供给其他程序员
进程相关概念
程序&& 进程
- 程序
源代码经过编译产生的可执行文件,这个文件是静态的 - 进程
程序运行起来的实例,是动态在运行的
OS如何管理进程
先描述再组织
描述:PCB
组织:链表或其他高效的数据结构
PCB
进程号pid
是进程的标识符
作用:标识当前OS中的一个进程
每个进程的进程号是唯一的
进程号可以重复使用
==理解:==每产生一个进程,OS就会给该进程提供一个进程号,当该进程结束时,对应的进程号就会被OS回收,而该进程号可以提供给其他进程(重复使用)
实践环节
- Linux操作系统查看进程号方式:
ps:查看当前OS中进程信息的命令
ps aux (一般搭配管道过滤来查看特定进程的进程号)
ps -ef - 编写程序,运行后查看进程的pid
要让自己的进程处于持续运行的状态
使用getpid()函数查看进程号
关于getpid()函数的细节如下图所示
#include<stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("my pid is %d\n",getpid());
sleep(3);
}
return 0;
}
当然,也可以通过ps命令来查看进程的进程号
进程状态
- 运行态
进程占用CPU,并在CPU上运行(进程正在使用CPU来执行自己的代码) - 就绪态
进程已经具备了运行条件,但是OS还没有为其分配CPU(进程已经将运行前的准备工作全部做好了,就等着OS调用) - 阻塞态
进程因 等待某事件发生而 暂时不能运行(e.g:IO输入、调用某些阻塞接口)
三者之间的转换图
进程是抢占式执行的:原则上,谁准备好了,谁就可以执行
理解:
CPU数量少,进程数量多,OS在调度的时候需做做到“雨露均沾”。让每一个进程都能运行,但是OS在调度的时候,是从 就绪队列中获取进程。也就是说,谁准备好好,就可以调度谁。进程为了执行自己的代码,都是抢占式执行,不会相互谦让
因此进程会有不同的状态,其中的原因之一就是“狼多肉少”
OS在调度的时候有各种算法
- 先来先服务
- 短作业优先
- 高优先级优先
- 时间片轮转(毫秒级别:5—800)
给每一个就绪状态的进程在分配CPU资源的时候分配一个时间片,在时间片内,进程可以拿着CPU来运行自己的代码。时间片到了,就会被剥离CPU资源
并发 && 并行
并发
:多个进程 在 一个CPU下,采用进程切换的方式,各自独占CPU运行各自的代码,交替运行,让多个进程都得以推进并行
:多个进程 在 多个CPU下,同时运行各自的代码
细分的进程状态
介绍ps aux命令查看到的信息含义:
STAT的状态信息后面的+的含义:
-
R:运行状态
可能是在执行代码,也可能是在运行队列(就绪队列)
在就绪队列 + 运行状态的进程,STAT(进程状态)都是R
-
S:可中断睡眠状态
进程正在睡眠(被阻塞),等待资源到来时唤醒,也可以通过其他进程信号或时钟中断唤醒 ,进入运行队列
-
D:不可中断睡眠状态(磁盘休眠状态)
通常是等待某一个IO的结束(也就是输入输出结束) -
T:暂停状态(Ctrl+Z)
在Linux下不要使用Ctrl + z来结束进程,不是结束,而是暂停
-
t : 跟踪状态
-
调试程序的时候可以看到
进入gdb调试
查看进程状态信息
- X:死亡状态
这个状态用户是看不到的,在PCB被内核释放的时候,进程会被置为X,紧接着进程就退出了 - Z:僵尸状态
子进程先于父进程退出,子进程就变成了僵尸进程
具体内容在下文僵尸进程中详细展开
程序计数器
预备知识:
程序计数器的作用就是记录当前进程下一步要执行的指令
本质:为下一次执行做准备
上下文信息
作用:保存寄存器当中的信息
理解:
为什么要保存寄存器的信息?
寄存器是OS的资源,在进程切换的过程中如果不保存寄存器的信息,等到该进程再次被CPU调度的时候,可能会由于寄存器的内容被其他进程改变而导致程序运行错误
内存指针
指向“程序地址空间”
这里的程序地址空间实质上是OS虚拟出来的虚拟地址空间
记账信息
记录使用CPU时长、占用内存大小
IO信息
保存进程打开的文件信息
每一个进程被创建的时候都会默认打开三个文件:
- stdin:标准输入 : scanf getchar
- stdout:标准输出 : printf
- stderr:标准错误 perror
验证:
一个进程被创建出来后,OS会以进程的进程号创建一个文件夹,该文件夹下的内容都是该进程的相关内容,这些内容都放在/proc目录下
在/proc/进程号/fd目录下有三个文件 0(标准输入域) 1(标准输出) 2(标准错误)(软连接文件,相当于Windows中的快捷方式)
①创建出一个进程
②在/proc目录下找到并进入以进程号命名的文件夹
③进入fd文件夹,找到三个命名为012的文件
创建子进程
fork函数
- 函数原型
pid_t fork()
系统调用函数,在使用的时候需要包含头文件#include<unistd.h> - 函数返回值
①创建子进程成功
会有两个返回值,一个返回给父进程,另一个在子进程中返回
②创建子进程失败
返回-1
针对fork的返回值,如何让父子进程执行不一样的代码块???
采用分支语句,根据fork的返回值确定相应的代码块。
e.g
#include<stdio.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if(-1 == pid)
{
//说明创建子进程失败
return -1;
}
else if(0 == pid)
{
//child
printf("I am child,my pid is %d,ppid is %d\n",getpid(),getppid());
sleep(5);
}
else
{
// parent
printf("I am father,my pid is %d,ppid is %d\n",getpid(),getppid());
sleep(5);
}
return 0;
}
正常在命令行当中启动的进程,它的父进程是bash(就是上图father的ppid)
父子进程代码共享
①子进程拷贝父进程的PCB
②父子进程的数据独有(各自有各自的进程虚拟地址空间)
进程独立性:多个进程运行,需要独享各种资源,各自拥有各自的进程地址空间,互相在执行的时候,数据不会窜,互不干扰
注意:父进程在创建成功子进程后,父子进程各自在运行的时候,是互不干扰的,各玩各的(尽管代码一样,他们也是自己执行自己的代码)
重点理解: 子进程与父进程代码相同,那么子进程从哪里开始执行代码?
答案是:从fork之后的汇编指令开始执行
原因:父进程在执行完毕fork函数的时候,子进程才会被创建出来。此时,父子进程竞争使用CPU来运行自己的代码,而在一个进程被剥离CPU的时候,程序计数器就会记录下一条要执行的指令。因此,子进程的程序计数器起始记录的一定是fork函数执行完毕后的第一条汇编指令(其实就是将函数返回值移动到某个寄存器的汇编指令)
子进程的作用
让子进程去执行某些代码
父进程想把“脏活累活”(可能导致程序崩溃)都交给子进程去处理
僵尸进程
概念
子进程先于父进程退出,子进程就会变成僵尸进程
形成原因
子进程在退出的时候,会告知父进程,父进程忽略处理,父进程并没有回收子进程的退出状态信息。
详细过程如下图:
代码模拟
#include <stdio.h>
#include <unistd.h>
//僵尸进程:子进程先于父进程退出
int main()
{
int ret = fork();
if(ret<0)
{
return -1;
}
else if(ret == 0)
{
//子进程代码
printf("the child process exit!\n");
}
else
{
//父进程代码
while(1)
{
printf("I am parent process!\n");
sleep(1);
}
}
}
运行结果:
危害
子进程的PCB没有被释放,会造成内存泄露
解决方案
- 杀死父进程,子进程由僵尸进程转变为孤儿进程(不推荐)
- 重启OS(不推荐)
- 父进程进行进程等待(推荐)
具体进程等待是什么,会在下一篇章进程等待篇详细介绍
扩展内容
- kill命令可以终止一个进程
kill pid 普通终止
kill -9 pid 强杀
注:程序员针对僵尸进程,使用kill命令是不能杀死的。一个系统当中如果僵尸进程太多,浪费的内存就越大
孤儿进程
概念
父进程先于子进程退出,子进程就会变成孤儿进程
注意:
没有孤儿状态!!!
形成原因
父进程先于子进程退出后,既定回收子进程的父进程就不在了,所以子进程就变成孤儿了。所以称之为孤儿进程
模拟代码
#include <stdio.h>
#include <unistd.h>
//孤儿进程:父进程先于子进程退出
int main()
{
int ret = fork();
if(ret<0)
{
return -1;
}
else if(ret == 0)
{
//子进程代码
while(1)
{
printf("I am child process!\n");
printf("pid:%d ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
else
{
//父进程代码
sleep(1);
printf("I am parent process!\n");
printf("pid:%d ppid:%d\n",getpid(),getppid());
}
}
退出信息的回收
孤儿进程会被1号进程领养
什么是1号进程?
/user/lib/systemd/ : OS启动的第一个进程,后续有很多进程都是由该进程创建出来的或者是由该进程的子孙创建出来的OS初始化的一些工作。
孤儿进程有危害吗?
孤儿进程没有危害
原因:孤儿进程在正常退出后,被一号进程领养,不会形成僵尸进程
环境变量
概念
在OS中用来指定OS运行的一些参数。也就是说,OS通过环境变量来找到运行时的一些资源。执行命令的时候,帮助用户找到该命令在哪一个位置
e.g: 链接的时候,帮助链接器找到动态库(标准库)(注:用户自己编写的动态库,需要自己指定环境变量)
常见的环境变量
- PATH
指定 可执行程序的 搜索路径。程序员执行的命令之所以能够被找到,就是环境变量的作用
验证:
使用 which + 命令查找该命令所在的路径e.g:which ls
查找ls的路径,和PATH中所有路径比对,会找到匹配的路径
- HOME
登录到Linux操作系统的家目录 - SHELL
当前的命令行解释器,默认是"/bin/bash"
查看当前环境变量
-
使用env命令来查看
-
使用echo$[环境变量名称]
环境变量的组织方式
环境变量名称 = 环境变量的值(使用:进行间隔)
理解:
系统当中的环境变量是有多个的
每一个环境变量的组织方式都是key(环境变量名称)= value(环境变量的值,多个值之间用:隔开)
环境变量对应的文件
-
系统级文件
-
用户级文件
修改环境变量
- 命令范式
export 环境变量名称 = $环境变量名称 :新添加的环境变量内容 - 修改方式
①命令行当中直接修改
②文件中修改
- 扩展
如何让自己的程序。不加 ./ 直接使用程序 名称执行??
两种方式:
①将我们的程序放在/user/bin下面(不推荐)
②设置环境变量
在PATH环境变量当中增加可执行程序的路径
环境变量的组织方式
通过字符指针数组的方式组织,数组最后的元素以NULL结尾(当程序拿到环境变量的时候,读取到NULL,说明已经读取完毕)
char* env[] :本质上是一个数组,数组的元素是char*,每一个char*都指向一个环境变量(key = value)
代码获取环境变量
- 通过main函数的参数获取
main函数参数的含义:
可以在main函数内通过循环的方式打印输出环境变量的内容(循环条件:env[i] != NULL)
验证:for循环打印的内容和命令行直接输入env的结果一致
#include<stdio.h>
#include <unistd.h>
int main(int argc,char* argv[],char* env[])
{
int i = 0;
for(i = 0;env[i]!=NULL;i++)
{
printf("%s\n",env[i]);
}
return 0;
}
函数执行结果:
使用env命令
两者完全一致!!!
下面来谈一谈main函数的前两个参数
#include<stdio.h>
#include <unistd.h>
int main(int argc,char* argv[],char* env[])
{
printf("参数个数为%d\n",argc);
int i =0;
for(;argv[i]!=NULL;i++)
{
printf("%s\n",argv[i]);
}
// int i = 0;
// for(i = 0;env[i]!=NULL;i++)
// {
// printf("%s\n",env[i]);
// }
return 0;
}
3. environ
extern char** environ:这个是全局的外部变量,在lib.so当中定义,使用的时候需要extern关键字
验证:
#include<stdio.h>
#include <unistd.h>
int main()
{
extern char** environ;
int i = 0;
for(;environ[i]!=NULL;i++)
{
printf("%s\n",environ[i]);
}
return 0;
}
3. getenv
验证:
查看PTAH环境变量的内容
#include<stdio.h>
#include<stdlib.h>
int main()
{
char* ret = NULL;
ret = getenv("PATH");
printf("%s\n",ret);
return 0;
}
对比echo$PATH一样的效果
进程虚拟地址空间
C语言当中的程序地址空间图(32位操作系统为例)
内存访问的基本单位是字节!!一个内存地址对应一个字节
代码
①单个进程打印全局变量的地址
#include<stdio.h>
int g_val = 10;
int main()
{
printf("g_val = %d,&g_val = 0x%p\n",g_val,&g_val);
return 0;
}
②创建子进程,父子进程都打印全局变量的地址
#include<stdio.h>
#include<unistd.h>
int g_val = 10;
int main()
{
pid_t pid = fork();
if(-1 == pid)
{
return -1;
}
else if(0 == pid)
{
//child
printf("I am child\n");
printf("g_val = %d,&g_val = 0x%p\n",g_val,&g_val);
}
else
{
//father
sleep(1);
printf("I am father\n");
printf("g_val = %d,&g_val = 0x%p\n",g_val,&g_val);
}
return 0;
}
③子进程修改全局变量的值,父子进程分别打印全局变量的地址和值
#include<stdio.h>
#include<unistd.h>
int g_val = 10;
int main()
{
pid_t pid = fork();
if(-1 == pid)
{
return -1;
}
else if(0 == pid)
{
//child
g_val = 100;
printf("I am child\n");
printf("g_val = %d,&g_val = 0x%p\n",g_val,&g_val);
}
else
{
//father
sleep(1);
printf("I am father\n");
printf("g_val = %d,&g_val = 0x%p\n",g_val,&g_val);
}
return 0;
}
对比上面的代码,我们发现
虚拟地址
我们在用C/C++语言所看到的地址,全部都是虚拟地址。物理地址用户一般情况下看不到,由OS统一管理
OS负责将程序当中的虚拟地址转换为物理地址
进程虚拟地址空间
具体分配情况
三个经典问题
-
为什么OS要给每一个进程虚拟出来一个地址空间,直接访问物理内存,不是更快??
-
为什么不能采用预先直接分配内存的方式给进程???
-
为什么32位操作系统,给进程分配的是4G的虚拟地址空间??
页表
-
映射关系
虚拟地址空间分为一页一页
物理内存分为一块一块
使用页表将页和块映射起来 -
如何通过虚拟地址+页表的方式 找到具体的物理地址??
-
为什么内存分配的时候,不采用连续分配的方式,而是一页一页离散的分配方式??
防止内存碎片 -
其他两种将虚拟地址转换为物理地址的方式
-
fork在创建子进程的时候吗,会不会拷贝页表?
进程优先级问题
前提引入
CPU资源分配的先后顺序,就是指进程的优先权
ps -l 命令显示的内容(着重介绍两项)
PRI && NI
- PRI值越小越快被执行,加入nice值后,PRI(new) = PRI(old) + nice
- nice的取值范围是[-20,19],共40个级别
- 调整进程优先级在Linux下,就是调整进程的nice值
PRI VS NI
- 进程nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程优先级的变化
- nice值是进程优先级的修正数据
top命令修改nice值
1、 使用top命令
2、进入top后按“r”–>输入进程PID—>输入nice值
为什么要有优先级
系统进程多,CPU少,进程之间具有竞争性。为了高效完成任务,更合理竞争相关资源,于是便有了优先级
以上就是关于进程概念方面的一些总结~~若有不准确的地方,欢迎各位评论私信!!
各位看官觉得有收获可以一键三连嗷~