目录
进程状态
R (running)运行状态与s休眠状态:
disk sleep(深度睡眠状态)
T (stopped)(暂停状态)
t----tracing stop(追踪状态)
X死亡状态(dead)
Z(zombie)-僵尸进程
孤儿进程
进程优先级
时间片
命令行参数
环境变量
PATH
PWD
HOME
ENV(environment)
进程地址空间
虚拟地址:
物理地址:
Linux进程空间
进程状态
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在 Linux内核里,进程有时候也叫做任务。
首先对于书本上对进程的状态的定义也都是大差不差,基本上都或有这样一个图来说明进程状态以及它们之间的关系。
那么对于我们来说,在底层实现中进程转状态就是Pcb中的一个字段,即就是pcb中的一个变量用来表示状态。
在认识这么多状态之前,我们先认识三个状态,运行状态,阻塞状态,挂起状态。
运行状态:
对于一般单核系统的用户,一个cpu对应一个调度队列(运行队列), 当我们的程序运行时,在内存当中完成好了pcb的创建,若果要运行,就会把该pcb链入我们的运行队列当中。只要在运行队列的进程,都是处于运行状态的。
阻塞状态:
当我们在编写代码的过程中,一般或多或少的取访问系统了其他资源。(例如我们要从键盘中读取数据)但当程序在运行过程中,回显要我们输入数据时,我们不去输入(我们访问的资源没有就绪),此时的资源不具备访问能力,此时的进程无法继续往下进行,之后操作系统了解到,就会把我们的pcb重新链入到设备所维护的等待队列时,即处于阻塞状态
当进程处于阻塞状态时我们就可以看到程序卡住了。
了解到以上两种状态,我们基本已经知道了进程状态的本质其实就是:1.更改pcb中的state的整型变量.2.将他们的pcb链入不同的队列。
从阻塞队列移入导运行队列我们就称为唤醒。
挂起状态:
如果当一个进程处于阻塞状态,即处于某个对应的设备的等待队列中,访问不到资源,此时进程无法被调度,但此时如果操作系统的内存资源严重不足时,为了解决此时内存严重不足的问题,操作系统就会将此进程的代码和数据从内存中拿出去,交到磁盘中,我们称这个进程被挂起了。
其次对于进程的代码和数据的移出是针对所有当前阻塞的进程。
对于linux源码中的pcb(task_struct),中就是以一个变量state作为进程的状态,所以上图的状态变换,本质上就是修改这个变量state,如下:
下面的状态在kernel源代码里定义:
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
现在我们就可以来看看linux里面具体的一些状态:
写一个简单的循环代码,编译执行之后,还是通过查看进程信息来观察他:
R (running)运行状态与s休眠状态:
可以看到进程信息上有一栏stat---就是state表示进程信息
这里的s+ 就是sleep状态,也就是是阻塞状态,可是为什么时阻塞状态,代码不是好好的运行吗?
实际上这里我们一直打印信息,对于显示器来说,并不是时刻准备好了去显示我们要打印的信息,也就是还在对应等待队列中。
而在现实当中也确实时s状态占大多数 。
如果一直想看到的是进程是s(running)状态,那么我们就不去访问其他资源,直接运行一个我们的白板程序,那么就可以看到是S状态:
对于这里的+号,我们可能并不知道是什么意思,实际上对于只要运行起来的程序,我们无法再去使用命令行程序,只有将他结束之后,才能继续使用,这样的程序我们将它称作前台进程。
而对于我们,想要将进程运行起来并可以继续让我们执行其他指令,我们可以在加载程序时,加一个&符号,这样运行的进程就是后台进程了。
./myproc & //后台程序
后台程序不会有+号了:
对于后台程序,我们还是可以使用我们的命令行指令。当我们想要结束掉后台进程时,别忘了用kill -9干掉我们的进程。
disk sleep(深度睡眠状态)
上文说过休眠状态就是阻塞状态,我们将这种休眠状态称之为阻塞状态,于此对应有深度休眠状态,浅度睡眠的进程的会对外部的信号做出响应(比如可以直接ctrl c被终止掉)。
而深度睡眠(disk sleep)状态也是阻塞状态,这种状态的程序是针对磁盘设计的,这种状态的进程,操作系统是不能杀掉的。原因如下:
操作系统在没办法的时候,会通过杀掉进程节省资源,但当一个进程正在向磁盘中写入关键数据时,如果杀掉该进程,那么写入磁盘就会失败导致数据丢失,为了应对这种情况设置了深度睡眠的状态,操作系统遇到这种状态的,就不会去杀掉他。当工作完成之后,会自动醒来。否则只能等(关机也解决不掉我)。
一般我们时间不到的。
T (stopped)(暂停状态)
在linux中我们可以通过kill -l查看信号,之后就可以执行对应的指令完成出对应的操作,如kill -9
此事件而已看到有两个信号19 SIGSTOP就是暂停进程,18 SIGCONT,继续进程。
获取进程的 id之后,通过指令我们可以改变进程的状态:执行暂停之后,进程停止。
此时看一下进程的星系:
状态是T。继续执行kill -SIGCONT pid之后,进程又继续开始了运行,状态为s。
对于这里的前台进程,暂停之后就会是后台进程,因为我们还要去使用命令行。
t----tracing stop(追踪状态)
这种状态进程我们可以通过使用gdb来观察:
在调试我们的程序时候,打断点,调式程序,遇到断点停止下来,此时就可以看到有个t进程,追踪停下时。
可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可 以通过发送 SIGCONT 信号让进程继续运行。
。
X死亡状态(dead)
这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
Z(zombie)-僵尸进程
僵死状态(Zombies)是一个比较特殊的状态。当进程有异常退出并且父进程或者os 没有读取到子进程,但是os此时还会维护这个进程的pcb,此时这个子进程并不是退出,而是以僵尸进程存在着,僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
孤儿进程
对于进程的退出,是怎么让操作系统知道的呢?其实在子进程退出时,程序终止前会将自己的退出信息写入到Pcb当中,父进程或者os读取其中的退出信息,说明进程退出了。但是
父进程如果提前退出,子进程退出时没有人来确定子进程退出信息,导致僵尸状态,此时的子进程就称之为“孤儿进程” ,孤儿进程最终被被1号init进程(bash)领养,当然最终会被init进程回收。
进程优先级
我们知道进程在运行之前,需要到运行队列中等待,而对于排队,其实也就是他们的优先级决定他们谁在前谁在后,因此每一个进程都有自己与之对应的优先级去决定自己的排队顺序。
那么我们其实已经知道了task_struct就是linxu下的pcb,而对于进程的优先级就是用一个整型数字表示的。
Linux进程的优先级数值范围是:60~99。
Linux进程的默认优先级一般都是80。
现在我们就可以来查看一下我们的进程的优先级:
我们随手写了个代码将他运行起来:
ps -l//查看进程信息
ps -la//详细查看所有进程信息
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
可以看到在PRI(priority)这一栏,就是我们的进程优先级,且默认值为80.
其次对于Linux是支持我们动态调整PRI的,我们可以去修改,那么如何修改呢?
在Linux进程的pcb里面,会存在一个nice值:我们将它称为进程优先级的修正数据。
我们并不是直接就去修改优先级,而是通过nice值的运算来确定:
公式:priority(新的)=priority(旧的)+nice值。
修改方法也有很多,我们用最简单的:
首先top(启动任务管理器)
top
输入r
此时可以看到上面的一行会显示有renice(修改),我们在输入我们要改的pid
之后就会提示我们要将他的nice值改为多少呢?我们这里将他修改为10:
此时可以看到我们的这个进程的PRI变成了90,且旁边的NI就表示当前的nice值,为10.
即90(新的优先级)=80(旧的优先级)+10(nice)
此时的优先级其实是变低了,那么如何变高呢?当我们继续修改时此时系统会报错,禁止修改。
我们需要sudo top,获取root权限,之后便可以修改了。
即70(新的优先级)=80(旧的优先级)-10(nice)。
可以看到我们的优先级总是以旧的(默认的80)与nice计算从而得到的。
因为进程的的优先级范围是60-99,因此nice最小-20,最大39,当输入太大或太小的修改值,始终都默认是最小-20,或39来计算,并不会超越。
其次虽然能修改进程的优先级,但我们肯定是不建议去修改进程的优先级:
os为了使进程在调度时较为均衡让每一个进程得到调度,容易导致优先级较低的进程,长时间得不到cpu的资源-------导致进程饥饿 。
其次获取进程的优先级不止这些:
getpriority
renice
其次对于进程优先级也有与之相关的概念:
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高 效完成任务,更合理竞争相关资源,便具有了优先级
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为 并发
时间片
对于我们进程的运行,每一个进程不是得到了cpu的资源就一直占用cpu,每隔一段时间,进程就会被强制剥离下来,我们称之为是时间片,是固定不变的,在task_strcut就是是一个全局变量。
而Linux系统是支持进程之间进行cpu资源抢占,故此Linux内核也是基于时间片的轮转式抢占内核。
命令行参数
在了解命令行参数之前,我们先了解一下main函数,我们早在学习c语言的时候就了解到了main函数,作为一个主函数,通过主函数我们调用我们写的其他函数结构体等。而main函数也作为一个函数,也是需要被调用,实际上main函数也是有参数的,并且有两个参数,而这两个参数我们基本不用,但是今天我们需要了解到,这两个参数实际上就是命令行参数
#include<stdio.h>
int main(int argc,char *argv[])
{
int i=0;
for(;i<argc;i++)
{
printf("%d:%s",i,argv[i]);
}
return 0;
}
我们现在运行,就只有一个0:./proc,代表运行我们的可执行程序。
当我们分别传入参数-a,--a --b --version这两个选项时,出现以下结果。
对于这两个命令行参数:
int argc:这个代表的是在shell命令行上,以空格作为分隔符,从下标零开始,一共有几个字符串,比如这里:./proc -a --b --help --version,传入整个字符串,编译运行之后基会被分割成五个字符串(0~4),之后就被打印出来。
char *argv[ ]:没错这个就是一个指针数组,里面存放了char*指针,也就是用来存放字符串,即存放被分割的字符串,但实际上它存放的个数会比我们分隔之后的个数多一,用来存放null。
那么这是用来干嘛的呢?
通过命令行参数我们可以自己写程序来实现我们需要的指令:
如下,我们去书写一个程序,通过在运行这个程序时添加参数,实现一种指令效果,表示两个整数的相加,相减,相乘或相除。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(int argc,char *argv[])
{
//我们自己实现一个我们的指令
//使用这个指令,至少被分割之后有四个字符串
if(argc!=4)
{
//如果指令格式不符合,打印指令使用手册
printf("Use error\nUsage:%s op[-add|sub|mul|div] data1 data2",argv[0]);
//注意这里的arg[0]是因为指令无论如何,第一个必须是程序名,比如我们这里就是proc
return 1;
}
//如果运行程序到这里,说明指令的输入无误,此时这里应该是有四个(命令行参数)字符串,第一个是程序名,第二个是操作,第三个是数据一,第四个是数据二
//接下来我们去实现这个指令
int x=atoi(argv[2]);int y=atoi(argv[3]);
if(strcmp(argv[1],"add")==0)
{
printf("%d+%d=%d\n",x,y,x+y);
}else if(strcmp(argv[1],"sub")==0)
{
printf("%d-%d=%d\n",x,y,x-y);
}else if(strcmp(argv[1],"mul")==0)
{
printf("%d*%d=%d\n",x,y,x*y);
}else if(strcmp(argv[1],"div")==0)
{
printf("%d/%d=%d\n",x,y,x/y);
}else{
printf("You should ues command in corret way:\nnUsage:%s op[-add|sub|mul|div] data1 data2",argv[0]);
}
错误输入:
指令效果:
通过这种方式我们自己实现了一种指令用来计算两个整数的加减乘除。
学习到了上述命令行参数的使用,那么我们就能理解到了,我们在xshell中使用的这些指令是如何实现的,实际上这些指令的操作,就是写的函数被调用,指令对应的选项也就是一个字符串,将字符串传递到mian函数中,就是命令行参数了。
环境变量
PATH
对于一个操作系统,它是有很多的环境变量的,例如首先我们知道要执行一个命令必须先找到这个指令在哪里,而在linux操作系统中,有一个环境变量,他会记录我们的指令的搜索路径,他就是PATH。
在linux中我们可以通过echo打印出环境变量里面存放的内容,因为不是把它当作一个字符串,因此添加$:
echo $PATH
可以看到打印出了一串路径,着这个路径是由一个个子路径组成,我们的指令ls在这个环境中去查找, 先从最开始的路径搜索,找不到就下一路径。
只有当我们的可执行程序的路径在PATH(搜索路径)中,我们就可以调用我们的可执行程序,否则无法使用。
例如我们上述写的proc程序,我们将该路径追加到PATH中:
可以看到指令不要再添加./来搜索这个可执行程序,直接输入可执行程序即可。
删除的时候就重新给PATH赋值路径:
当然我们也可以直接选择将我们的文件添加到PATH路径当中,不过这需要权限,sudo之后添加进来,我们的程序就可以直接运行了。
PWD
操作系统是怎么知道我身处那个路径底下?其实在操作系统中存在一个叫PWD的环境变量,
PWD就是给我们的bash专门记录路径的一个环境变量,而我们的指令pwd就只用读取PWD里面的内面打印出来即可。
HOME
系统是如何知道我们是那一个用户呢?whoami是怎么知道的?当我们登录的时候,系统会根据用户名和密码形成环境变量(不止一个),比如说这里的HOME,根据用户名初始化我们的HOME,之后在进入系统之前执行cd $HOME,就实现了用户的记录,而这里的whiami就是直接读取HAME里的内容。
ENV(environment)
这么多环境变量,我们如何知道哪些是环境变量,在系统中,还提供了一个环境变量env,这个环境变量就可以直接查到所有的环境变量。
其次我们还可以通过函数getenv,输入名称,获取环境变量的内容:
通过这种方式我们可以决定让那种用户执行哪些程序等。
进程地址空间
首先我们要明确两个概念:虚拟地址与物理地址
虚拟地址:
是程序运行时访问存储器所使用的逻辑地址,操作系统会给每个进程分配一个虚拟地址空间,每个进程都拥有一个自己的地址空间。
虚拟地址是虚拟内存的一种方式,它在磁盘上划分出一块空间由操作系统管理3。虚拟地址将多个物理内存碎片和部分磁盘空间重定义为连续的地址空间,以此让程序认为自己拥有连续可用的内存。
虚拟地址是4G虚拟地址空间中的地址,如果CPU寄存器中的分页标志位被设置,CPU会自动根据页目录和页表中的信息,把虚拟地址转换成物理地址。虚拟地址注册是一种注册方式,不同于程序运行时的虚拟地址。
在我们平常写代码时,我们使用的都是虚拟地址,我们看到的也都是虚拟地址,例如在学习c语言与c++时,我们就了解到了内存管理(虚拟内存管理):
其中的内存分配如图所示,其中栈是向下增长,堆是向上增长,两者之间还有一大片空间,用来连接动态库之后映射。
物理地址:
物理地址是指内存中真实的地址,由CPU生成,用来访问实际的内存单元1。在网络中,物理地址也可以指MAC地址,用于唯一标识一个网卡23。物理地址的表示方法有不同的方式,例如在实地址方式下,物理地址是通过段地址乘以16加上偏移地址得到的4。物理地址和逻辑地址的转换是由内存管理单元(MMU)来完成的
在此之前我们了解到有一些函数他有两个返回参数,当查看地址的时候,(如getpid),我么会看到这两个地址是一样的,那么不可能物理地址,因为物理地址是不能会重复的,那么他就是虚拟地址,那么物理地址与虚拟地址是怎么对应的呢,他们之间的关系是什么呢?
Linux进程空间
知道物理地址与虚拟地址,而对于我们的每一个进程,在运行时都会有自己的地址空间,那毫无疑问是虚拟地址。
例如我们现在有一个进程,那么对于这个进程也是有它自己的地址,而进程在pcb中保存一些字段来表示自己的地址,但此时也只是虚拟地址,而虚拟地址并没有真真能保存数据的能力,这都是要靠物理地址,因此对于我们来讲,数据想要真正存放在物理地址当中,就需要我们用虚拟地址找到物理地址,因此每一个进程都有自己维护的一个映射表,用来映射虚拟地址与物理地址。如下图所示:
每一个进程都是以这样的方式去确定物理地址,他们都有自己对应的页表。父进程创建子进程,子进程一父进程为模板,同时也会把这一整个结构拷贝给子进程,子进程再增加自己的内容。
当然我们子进程也会有拥有自己的页表,它的逻辑地址可能有和父进程的逻辑地址指向的物理地址是相同的,因此一个一个函数可能有两个返回值,且他们的逻辑地址是一样。
首先对于区域空间,操作系统给每个进程花了一张大饼表示这么大的虚拟空间你都可以用(但实际根本不会多用),虚拟地址区域也会被划分,因为地址空间也需要被os管理起来,每一个进程都要有地址空间,因此一定要对地址空间管理,如何管理?--先描述,在组织。地址空间一定会被存放到内核结构体中,在linux中就是mm_struct中存放着,因此进程被创建时,除了创建对应的pcb,还要有对应的地址空间管理的内核数据结构,里面存放着地址空间管理的相关数据:
也就是我们上面对于内存管理的那张图的结构体描述(其中每一个物理与逻辑地址映射关系还会有权限字段,标记字段等)。
而我们的虚拟地址空间加页表就可以对我们的数据操作(访存)进行检查。
在进程加载到内存当中,并不是全部加载到内存当中,只有在使用的时候去加载使用的那一些
。
概念扩展:缺页中断,当我们虚拟地址映射的逻辑地址处的数据不在了,而操作系统就是要访问,此时会将访问状态暂停,重新开辟一片空间,将要访问的代码数据加载到其中,把对应的虚拟地址填充到该位置,标志位给位1,暂停的代码不暂停,继续访问。
当然在我们创建子进程时。也会存在创建失败:
1.系统中有太多进程
2.实际用户的进程超过了限制