Linux系统编程-进程篇
- 冯诺依曼体系结构
- 操作系统
- 进程
- Linux下的底层下的组织
- 环境变量--重点
- 进程优先级
- 进程状态转换问题
- 结语
冯诺依曼体系结构
这里的存储器是内存,一般情况下内存不直接与外存打交道,而是通过缓存先将输入/输出 输出到寄存器中,再由寄存器与外存做交互。
当代典型的输入设备:键盘、鼠标、扫描仪、写板等
输出设备:显示器、打印机等。
冯诺依曼的两个重要思想
1.所有数据均采用二进制存储(契合电路,高低电平的特点)
2.数据都保存在存储器中(内存中)
当代计算机的三级缓存
三级缓存与高速缓存(cache)
首先必须明确三级缓存均属于高速缓存
一级缓存:一级缓存通常设置在cpu内部,但是比起寄存器来说还是远一点,因为其放在cpu内部,所以它的空间是很小的,通常为8-16k
二级缓存:二级缓存在CPU之外,因为主板上的空间很大,所以二级缓存比一级缓存大得多,大概是256KB-1MB左右,但是它的速度慢,因为它离CPU比较远。二级缓存通常用作一级缓存与内存的交换空间
三级缓存:其空间更大,但是相应的离cpu也越来越远。
所以从远及近看 CPU〈------〉寄存器〈----》缓存《-----》内存。
缓存中的数据是可以提升的?
比如初始情况下,一个数据位于三级缓存中,但是因为经常使用,所以它会被提升到二级缓存甚至一级缓存,这块很好理解,经常用cpu就让该数据离它更近点。
为了保证CPU访问时有较高的命中率,Cache中的内容应该按一定的算法替换。一种较常用的算法是“最近最少使用算法”(LRU算法),它是将最近一段时间内最少被访问过的行淘汰出局。因此需要为每行设置一个计数器,LRU算法是把命中行的计数器清零,其他各行计数器加1。当需要替换时淘汰行计数器计数值最大的数据行出局。这是一种高效、科学的算法,其计数器清零过程可以把一些频繁调用后再不需要的数据淘汰出Cache,提高Cache的利用率。
LRU为操作系统中提供的算法。
操作系统
操作系统的概念
操作系统时配置在计算机硬件上的第一层软件,是对硬件系统的首次扩充。其主要作用是管理好这些设备,提高它们的利用率和系统的吞吐量,并为用户和应用程序提供一个简单的接口,便于用户使用。
硬件:冯诺依曼中的所有设备
软件:安装软件、卸载软件、升级软件
在系统层面:文件、进程、驱动等
操作系统的组成
操作系统=操作系统内核+一堆应用
操作系统内核:代表某种操作系统的代码的统称。
操作系统作用
操作系统是管理当前设备的软硬件资源的软件。
管理:先描述,再组织。
这块管理:管理不是像宿管阿姨管理一个宿舍那样,更像是一个leader(校长),操作系统具有**“决策权”**。
对上:给用户一个稳定高效的执行文件。
对下:管理好软硬件资源,提供稳定的软硬件资源。
这里还是以学校举例:把学生比作硬件的话,那么宿管就相当于是驱动程序,而操作系统位于驱动程序之上,等于说驱动程序只是按照操作系统的命令去做事而已。这便是真正的管理者。作为一个leader,统筹全局。
Linux下的操作系统体系
进程
进程概念
进程:进程是程序的一次执行,是系统进行资源分配和调度的一个独立单位。
进程特性
1.动态性。由创建而产生,由调度而执行,由撤销而消亡。
2.并发性。是指多个进程实体同时存在于内存中,且能在一段时间内使进程实体和其他进程实体同时执行。(在不同的时间点,交给处理及处理。在同一时间点,任务不会同时进行)。
这块解释一下并发与并行区别:
并发性:在一个处理机上,宏观上有多个进程同时执行的效果,但在微观上,并不是同时执行,只是把时间分为若干段,(类似于时间片轮转算法),使多个进程轮流交替的运行。
并行性:指在同一时刻,多个处理机同时处理不同的任务(进程)。
3.独立性:进程是一个能独立运行,独立获得资源和独立接受调度的基本单位。前提:建立PCB情况下。
4.异步性:进程按独立的,不可预知的速度向前推进。
进程的组成
进程=程序段+数据段+进程控制块(PCB)(PCB常驻内存)
进程与程序区别
1.进程是一个动态的概念,程序是一个静态的概念。
2.进程具有并发性,而程序没有
3.进程是资源分配和处理机调度的独立单位,其并发性受系统制约。
4.一个程序,多次执行,对应多个进程,不同的进程可包含同一组程序。
进程:程序 (1:n)
进程控制块
PCB定义:作为进程实体的一部分,记录了操作系统所需的,用于描述进程的当前情况以及管理进程运行的全部信息,是操作系统中最重要的记录型数据结构。
PCB的作用是使一个在多道程序环境下不能独立运行的程序(含数据)称为一个独立运行的基本单位,一个能与其他进程并发执行的进程。
作用:
1)PCB标志了程序作为独立运行基本单位。
2)能实现间断性运行方式。这很好理解,比如cpu处理进程使的并发性。当在一段时间内A进程的时间片耗尽时,此时需要记录执行到哪里了(保护现场)。保证了进程下次被分到时间片时的恢复现场。
3)提供了进程管理所需要的信息。PCB中记录了程序和数据在内存或外存的始址指针,找到对应的程序和数据。总的来说,PCB里面保存着执行资源的地址。
4)提供进程调度所需要的信息。比如优先级、程序目前的状态,是在阻塞态还是在S(sleep)状态。
5)实现与其他进程的同步和通信。比如一个程序被分为多个进程,此时后一个所需的资源依赖于前一个进程执行完毕,这就涉及到了进程的同步与通信了,是有顺序的,。要不然你执行你的,我执行我的,这样的出来的值到底是什么!!
注:PCB里面存储的程序和数据均是程序和数据的地址,PCB常驻内存。
Linux下的底层下的组织
明确Linux操作系统底层
Linux系统是以C语言编写的,C语言可以存储多个对象的数据类型的结构是什么,当然是结构体
文件=内容+属性
Linux底层管理进程的结构体(描述)
进程=对应的文件+进程属性
struct task_struct
{
}
这个结构体下存了哪些东西:
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。(汇编指令)
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
I/ O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
这个结构体又称为PCB
从操作系统内核的结构看进程(组织)
前面我们提到过:
管理=描述+组织
描述:前面我们已经给出了描述进程的结构体,那在底层,它是怎么实现组织的?
是通过双向链表的方式,通过增、删、查、改的操作来实现进程的调度。
进程管理:先描述,再组织
将程序跑起来,就相当于将程序加载入内存,将程序输出可视化,便是将程序加载到显示器中。
查看进程命令
ls /proc proc目录下存储着当前所运行的进程
进程号
什么是进程号:进程号就相当于是标识进程存在的序号,这个序号是随机产生的,每次产生的进程的进程号都是不同的。它是用来唯一标识进程的。就像每个人自出生起你的身份证号就固定起来了。(唯一标识:在当前主机),比如说两个不同的主机,两个进程号就算相同的话,它也不是同一进程。
通过进程调用获取进程标识符
getpid()获取到当前进程的进程号
getppid()获取的是该进程的父进程的进程号
测试用例:
基本Linux下的所有命令的父进程都是bash。
bash的运行原理:
通过创建子进程,让子进程去完成任务。 为什么?因为通过上面可以看到bash也相当于是操作系统的一个进程,所以具有进程独立性的特点,命令执行基础是要保证自己是安全的。等于说自己先创建一个子进程去完成命令,就算执行不了,崩掉了,此时根据进程独立性特点,它也影响不到父进程。因为bash是Linux程序运行的基础,bash崩了,整个Linux操作系统就崩了。
通过系统调用来创建进程fork()命令
fork系统调用用于创建一个新进程,称为子进程,它与进程(称为系统调用fork的进程)同时运行,此进程称为父进程。创建新的子进程后,两个进程将执行fork()系统调用之后的下一条指令。子进程使用相同的pc(程序计数器),相同的CPU寄存器,在父进程中使用的相同打开文件。
头文件 :#include<unistd.h>
函数参数:pid_t fork(void);
pid_t 就是int类型
它不需要参数并返回一个整数值。下面是fork()返回的不同值。
负值:创建子进程失败。
零:返回到新创建的子进程。
正值:返回给父进程或调用者。该值是新创建的子进程的进程ID 。
fork的一些特性:
返回值。有三个返回值,成功时候两个,失败时候一个。
成功时:给创建子进程的父进程返回创建子进程的进程号。给子进程返回0值。
1.如何理解进程创建
2.fork()为什么会有两个返回值?
声明一点fork()只调用了一次
此时父进程创建出来一次的时候会return一次,子进程的数据流也会返回一次。
由于在复制时复制了 父进程的堆栈段,所以两个进程都停留在fork函数中,等待返回.因为fork函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回,这两次的返回值不同. 从fork函数开始以后的代码父子共享,既父进程要执行这段代码,子进程也要执行这段代码.(子进程获得父进程数据空间,堆和栈的副本. 但是父子进程并不共享这些存储空间部分. 父,子进程共享代码段.) 现在很多现实并不执行一个父进程数据段,堆和栈的完全复制. 而是采用 写时拷贝技术.这些区域有父子进程共享,而且内核地他们的访问权限改为只读的.如果父子进程中任一个试图修改这些区域,则内核值为修改区域的那块内存制作一个副本, 也就是如果你不修改我们一起用,你修改了之后对应修改的那部分。
就是类似于进程的独立性,进程间代码共享,数据私有,此时因为数据要私有一份(数据改变),所以此时父进程会return 一次(return一次子进程的进程号),子进程return一次,(return 0);前提是进程创建成功的前提下。
写时拷贝技术
3.fork()父子的执行顺序和代码和数据的复制问题?
进程数据=代码+数据
父进程创建子进程时,代码共享(因为代码在内存中一般为只读),数据私有(写时拷贝),这也就解释了上面的fork()为什么会有两个不同的返回值,。为什么数据私有呢?是由于进程的特性所决定的,进程具有独立性。
关于执行顺序:当子进程被创建成功时,此时它会被加载到内存的运行队列中,又因为进程的特征具有异步性。(进程以不可预料的结果向前推进),所以这块子进程与父进程的执行顺序是不确定的。由操作系统中的调度器决定
4.子进程是从fork之前还是fork之后开始运行?***
子进程是从fork()之后开始运行的。
父进程调用fork()函数时,其程序计数器中保存的是fork’()之后的下一条汇编指令。
上下文信息:调用fork()完毕之后保存的寄存器内容。
此时子进程去拷贝父进程的PCB,所以此时子进程拿到的便是fork之后的代码,所以子进程的执行是从fork()之后开始的。
Linux下查看进程的ps命令
-A 显示所有进程(同-e)
-a 显示当前终端的所有进程
-u 显示进程的用户信息
-o 以用户自定义形式显示进程信息
-f 显示程序间的关系
-axj 通常可以查看父子间的依赖关系
-aux查看的是当前操作系统的所有信息
Linux环境下的几种状态
R状态–运行状态
测试用例:
首先明确一个概念:R状态是不是程序一定在运行?
S状态 --睡眠状态,可中断的
为什么这块要强调一下它是可中断的睡眠呢?
因为下面还有一个D状态,是不可中断的睡眠方式,后面会详述的。
测试用例:
可以看到,这里是一直在打印.字符
这里可以看到程序处于S状态,为什么呢?我不是一直在打印.这个字符吗?
这是因为cout打印涉及到了I/O,cpu的执行效率是远远大于I/O操作的,可以这么说吧,cpu用了1%的时间打印.这个字符,但99%的时间都在等I/O完成。所以虽然是个while(1)循环,其它的进程也可以照样执行的原因,这是因为进程的并发性的特性所决定的。它一段时间内不会单单只处理一个进程。
D状态 —磁盘休眠状态。
这个不好演示,给大家画个图吧。
T状态–暂停状态
可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可
以通过发送 SIGCONT 信号让进程继续运行。
测试用例:
命令kill -19 + 进程号(PID) 让程序暂停执行
可以看到此时程序已经停止了,但程序依然存在于内存中。 类似于我们平时按下ctrl+z的操作一样,这也是为什么不推荐使用该命令的原因,它只是暂停程序,并没有终止程序
那怎么恢复呢?
kill -18 + 进程号(PID)
X状态 --死亡状态
一般来说我们是看不到死亡进程的,死亡进程会被立即置换出内存,这个过程是及其短暂的,所以很难追踪到。
孤儿进程
孤儿进程的概念。
父进程先退出,子进程就称之为“孤儿进程”
孤儿进程被1号systemed进程领养,当然要有systemed进程回收喽。
孤儿进程的产生条件
父进程先于子进程退出。子进程进入一个死循环,这样这个进程就变为了一个孤儿进程。
测试用例:
此时看现象:
这里解释一下为什么在孤儿进程的情况下仍可以运行bash命令行解释器?
Z状态,僵尸进程***
僵尸进程的概念:
1.僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使
没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
2.僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
3.所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
测试用例:
僵尸进程的危害?
1.进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
2.维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说, Z状态一直不退出, PCB一直都要维护?是的!
3.那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!便会造成内存泄漏
为什么要提出僵尸进程的概念
保存进程的基本退出原因,方便父进程读取,获取退出原因。
进程死亡的话,操作系统通过系统调用来分析进程结束完后的基本信息,(1.是否正常运行完?2.是否异常?异常的原因)
让进程退出之时,不要立即释放资源,而是把资源维持一段时间去供操作系统进行读取。
解决方案
1.重启操作系统(众所周知,重启可以解决计算机90%的问题)–不推荐
2.干掉父进程,这样进程成功由僵尸进程转化为孤儿进程,它就会被1号进程所领养,最后回收资源。(重点)
3.进程等待。
为什么会产生僵尸进程?
父进程创建一个子进程,子进程相对于父进程来说提前退出。子进程在退出的时候会向父进程发送一个信号(SIGCHLD),而父进程对于该信号是忽略处理的,导致子进程在退出时,没有进程来回收子进程的资源(PCB),子进程就变为了僵尸进程。
解释一下什么是前台程序和后台程序
一般来说比如进程状态后有‘+’符号的号,就是前台程序,没有的话就是后台程序。怎么理解前台程序和后台程序呢?
比如你今天电脑中开了许多的app,比如qq、网易云、微信等程序,此时你正在网易云里面看视频(MV),那此时网易云便是你的前台程序,qq、微信,不是不运行了,而是由前台程序变为了后台程序。
在linux’系统中,一般可以用ctrl+c终止的程序称为前台程序,不能的称为后台程序。孤儿进程中的父进程就相当于前台程序(在执行完后,等待子进程结束的时候)。
怎么去让一个程序变为后台运行
./可执行程序名 &
环境变量–重点
什么是环境变量
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
本质:就是将命令的可执行程序的路径保存起来。
环境变量的分类
系统级的
存在所有的环境变量,对所有用户都需要加载
/etc/bashrc ,一般不要对该文件下的内容进行修改。
用户级的
两个文件 ~./ bashrc
~./ bash_profile
使用echo查看环境变量内容
可以看到这就是PATH环境变量下存储的内容,一般存储的是可执行程序的路径。
常见的环境变量
PATH:指定可执行程序的搜索路径 --重点
测试用例:
比如ls命令
总结:在不指定路径的情况下,在命令行输入一个命令,他会在PATH所存储的路径中依次查找,查找到就执行该可执行程序,查找不到就会报错,这也解释了为什么在当前目录下生成的可执行文件为什么要加上./才能执行,因为就告诉编译器我是在执行当前目录下可执行文件
HOME :指定用户的主工作目录(即用户登录到LInux上时,默认的目录)
SHELL:当前的shell,通常是/bin/bash
查看所有的环境变量env命令
修改环境变量
临时修改 使用命令
export +环境变量名称=$环境变量名称:新的路径
$环境变量名称:为了保留之前的环境变量,类似于在之前环境变量的基础上添加,而不是直接修改
临时修改的特性:只在该系统使用时有效,重启后失效。
测试用例:
永久修改 --用户级别的(针对当前用户)
vim ./.bash_profile
source ~/.bash_profile 是让主机重新读取环境变量
这里看到运行成功了
环境变量的加载顺序
先加载系统级的环境变量 /etc/bashrc
再加载用户级中的 ~/.bashrc
最后是 ~/.bash_profile
环境变量的组织格式
环境变量在底层是以指针数组存储的,每个指针指向一个环境变量,以NULL值作为结束标志。
底层是char*envp[] 指针数组,每个指针指向存储的一份环境变量。
main()函数的几大参数
int main(int argc,charargv[],charenvp[]);
argc 为命令行参数的个数,本质上是argv的元素个数,默认情况为1,输出的是该程序的名字。
argv 为命令行参数
envp为环境变量
测试用例:
通过第三方变量environ获取
获取整个环境变量
获取特定环境变量和修改环境变量
getenv
头文件#include<stdlib.h>
函数原型:char* getenv(const char*name);
name为需要查找环境变量的名称。
putenv()函数
头文件 #include<stdlib.h>
函数原型 int putenv(char*string)
返回值 修改成功返回0 ,失败返回非0值
例子:不截图了
调用putenv去修改PATH
int ret=putenv("PATH=/usr/bin");
if(ret==0)
{
cout<<"success"<<endl;
}
else
{
cout<<"change failed"<<endl;
}
进程优先级
区分一下优先级与权限的问题
优先级:cpu资源的分配顺序,就是进程的优先级。
优先权高的具有优先被cpu’先执行的权力。 这很好理解,就像一个医院中来了一个感冒的病人和一个出了车祸的病人一样的,医生不会说因为你先到就给你先看完再去处理出车祸的病人。
Linux操作系统为抢占式的操作系统。
那这里就有人会想了,既然为抢占式操作系统,那是不是优先级低的一直得不到cpu调度?
首先,优先级不是一成不变的,在Linux内部,一个任务的优先级是会随着时间的推移渐渐增长的,所以确保了那些优先级低的也有将来某一天被cpu调度的可能。
权限:强调的是一个任务能不能被操作。
查看系统进程的优先级数
ps -l 采取详细的格式来显示进程状况
ps -al 显示所有终端机下执行的程序,除了阶段作业领导者之外 ,并显示他们的详细的格式。
PRI与NI
PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为: PRI(new)=PRI(old)+nice
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行所以,调整进程优先级,在Linux下,就是调整进程nice值,nice其取值范围是-20至19,一共40个级别。
注:PRI的值不可直接修改,只可以通过修改其nice值达到修改PRI的目的。
修改进程的PRI
这里只介绍一种最简单的,就是top命令,进入Linux 下的任务管理器。进入后按下r,接着输入进程pid(进程号) 接着输入修正Nice值。
必须在root权限下进行。普通用户无法修改PRI。
步骤:
记录z_fork()的pid,修改时候要用
按下r
输入进程pid完成后
我们将NI设为-19,看结果图
可以看到进程的优先级已经由80变为61了。
进程优先级的一些注意
1.一般情况下不要去修改进程的优先级,专业人干专业事,操作系统有自己的调度策略,所以非必要情况下不要去修改。
2.进程的优先级初始化PRI都是80,NI 都是0,我们只可以通过修改NI的方式达到修改PRI的目的。
3.进程优先级是可以提升的。这里的提升不是说用户去提升它,而是操作系统对驻留在内存中的作业的优先级提升,一般随时间线性化增长。
4.进程的优先级数值越大,优先级越低。
5,进程PRI的取值范围,首先看一下NICE的取值范围[-20,19]。所以PRI的取值范围为[60,99];
进程状态转换问题
在Linux’系统下
结语
希望大家多多支持,若博客有什么问题请大家不吝赐教。有博客中不懂的问题可以在评论区给出,我会尽我所能的一个一个给大家回答。本次博客我们进入到了系统编程中了,这只是一个开篇,后序的我会坚持更新。感谢访问!!