Linux进程状态、进程优先级、环境变量、进程地址空间
文章目录
- 说在前面
- 进程状态
- 运行态--->R
- 浅度睡眠态--->S
- 深度睡眠--->D
- 暂停状态--->T
- 僵尸状态--->Z
- 死亡状态--->X
- 孤儿进程相关概念
- 进程优先级的概念
- PRI和NICE值
- 如何设置进程的优先级
- 环境变量
- 怎么理解环境变量
- 如何查看系统的环境变量
- 使用命令修改环境变量
- 使用代码来获取环境变量
- 程序地址空间
- 进程地址空间的概念
- 利用代码来验证对应进程地址空间分布图
- 看看源代码的描述
- "奇怪"的fork
- fork为什么有两个返回值?
- 写时拷贝
- 总结
说在前面
今天我们继续更新Linux相关的知识。今天的知识非常多,也非常的难以理解。 今天的知识是往后我们在Linux环境下进行系统编程的重要基础。 理解了下面的相关的概念,相信你会对Linux系统会有更深刻的理解。废话不多说,下面我们正式进入今天的内容。
进程状态
前面我们知道了,操作系统是需要对每一个进程进行管理的!而我们知道,操作系统对于进程的管理本质上是对描述进程的进程控制块进行管理。在Linux下叫做task_struct 正如一个人有出生,少年,青年,老年等等各种状态。进程也是如此。那么Linux是如何为进程划分状态的呢? 我们来稍微看一看源代码是如何划分的
//Linux对进程状态的划分的数组
static const char *task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"T (tracing stop)", /* 8 */
"Z (zombie)", /* 16 */
"X (dead)" /* 32 */
};
接下来我们就对数组种出现的每一个状态进行分析。
运行态—>R
先来看第一个R状态,R就是run的缩写。也就是说,当前这个进程处于运行状态! 但是这里的运行状态并不意味这说这个进程正在运行!而是说明这个进程处于运行队列上! 这里要特别注意。Linux系统为了能够很好调度进程,设置了许多的队列。其中运行队列上的进程就是我们所说的R状态进程! 在内核的task_struct里面必然会有一个运行队列,而在对应的PCB(task_struct)里就会存在一个指向这个队列的指针:
//内核里面必然会有如下类似的结构
struct run_queue
{
//....一系列的属性
};
//
struct task_struct
{
//....一系列属性
//必然存在一个指针指向一个运行队列
struct run_queue* p;
};
但是由于现代计算机的运算速度实在是太快了,所以我们很难演示出R状态。所以在这里我就不用代码进行演示了。
浅度睡眠态—>S
接下来我们来看浅度睡眠状态—>S状态 我们知道,所谓的睡眠状态很好理解。就是进程不继续执行了,在等待某种资源的就绪! 最典型的就是我们使用printf向显示器打印数据
*演示进程S状态
* */
#include<stdio.h>
int main()
{
while(1)
{
printf("hello Linux\n");
}
return 0;
}
在我们看来,程序正在死循环飞速打印hello Linux,但是我们使用命令来观察和进程的状态得到如下的情况图:
但是明显这里的进程状态是S,也就是说进程多数情况下是处于等待I/O设备的情况! 因为cpu的速度实在太快了,所以我们多数情况下观察到的都是处于S状态,可能偶尔运气好可以看见R状态。这个就是S状态。接下来我们介绍深度睡眠状态—>D状态
深度睡眠—>D
前面我们介绍了浅度睡眠S状态,既然是浅度睡眠,那么就意味这操作系统可以把这个进程唤醒,也可以把这个进程杀死。 但是不妨考虑如下的场景:
一个进程需要对磁盘进行写操作,接下来磁盘拿着进程交给它的数据进行写入。进程就开始进入睡眠状态等待磁盘写入完成后返回的信号。 这时,繁忙的操作系统看到有一个进程在睡眠,可能就会把这个进程误杀。此时,写入完成的磁盘回头寻找进程的时候就寻找不到了。假设这个时候,磁盘写入失败,需要向原先的进程反馈失败好获取数据继续重写。 但是不幸的是,此时的进程已经不存在了。也就意味这数据丢失了! 如果这是发生在银行这种系统中的话,数据丢失就会造成不可估计的损失!
所以,为了解决这种情况。Linux引入了深度睡眠—>D,深度睡眠也可以称为"任务中断睡眠",D状态的进程是连操作系统都没有权力杀死的进程! 而且,如果一台电脑中,D状态的进程很多。那么这台电脑可能都没办法正常关机。同样,由于D状态的进程的特性,我们没办法很好地用代码进行模拟。
暂停状态—>T
接下来介绍一个Linux里面存在感不是特别强的进程状态:---->
暂停状态–>T状态,那么这个状态可以通过使用信号机制来实现:
kill -l //查看对应的信号
kill -19 +进程pid //向对应进程发送停止信号
比如,我们给我们的main进程发送停止信号,使用监控脚本观察状态:
可以看到这里确实是变成T状态了。而如果我们继续发送18信号,那么这个进程就又继续运行了。
//发送信号使得进程继续运行
kill -18 main
相对来说,这个进程状态考察的不是很多。也不是特别常用。所以大家了解有这样一个进程的状态就好了。
僵尸状态—>Z
接下来,我们就来讲一个比较常考也比较重要的进程状态—>僵尸状态。联系现实生活,假如在某个人非正常死亡。那么这个第一时间并不是就去下葬了。而是要通过法医和警察经过一系列的取证和分析。最后确定死因以后才可以下葬。 对应的,在Linux里面,如果一个进程今后不会再被使用。那么这个进程第一时间不是就进入死亡状态,而是进入僵尸状态!那么接下来,如果对应的父进程没有回收子进程。那么这样的一个进程就称之为僵尸进程! 而僵尸进程最严重的问题就是会存在内存泄露!而具体的处理方式和善后工作,我们会在后面进行详细介绍。
下面我们来用一段代码来简单模拟僵尸进程:
//模拟僵尸进程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int cnt=4;
int main()
{
pid_t id=fork();
if(id==0)
{
//child
while(cnt)
{
printf("我是子进程,我还有%dS退出\n",cnt--);
sleep(1);
}
exit(0);
sleep(2);
}
else
{
//parent
while(1)
{
sleep(1);
}
}
return 0;
}
可以看到,这里的pid小的大的那一个,也就是子进程确实变成了僵尸状态。那么父进程要怎么处理僵尸子进程,换句话说。子进程在变成僵尸之前,需要像父进程返回什么?
我们不妨从main函数开始。从我们开始写代码到今天,我们在main函数里面总是会在末尾加上return 0,但是这个0到底有什么作用。或者说,这个0到底是return 给谁的。
#include<stdio.h>
int main()
{
//......
return 0;//这个return 0到底是给谁的?
}
我们知道一个程序变成进程,那么最后的结果无非就如下三种:
1.程序跑完,结果正确
2.程序跑完,结果错误
3.程序崩溃,提前中止退出。
那么我们知道,进程必然会退出。但是对于上层而言,上层并不知道进程退出的具体情况。而return 语句就是起到了反馈信息的作用!
以main函数为例:这个return 0是返回给操作系统的。我们可以使用echo命令来获取进程的退出码:
echo $?
补充:这个退出码的信息通常是放在对应进程的PCB(Linux下是task_struct)中的。
死亡状态—>X
前面我们讲了僵尸状态,最后一个状态就是死亡状态了。进入这个状态的进程就会真正被回收。而进入僵尸状态的进程,代码和数据会被回收,但是对应的task_struct是不会被回收的!但是一旦进程变成了X状态,那么对应的task_struct也就会被操作系统释放。
孤儿进程相关概念
前面我们演示了子进程先于父进程退出就会出现僵尸进程。那么如果是父进程先退出呢?来看下面这么一段代码:
/*
*模拟孤儿进程
*
* */
#include<stdio.h>
#include<unistd.h>
int cnt=4;
int main()
{
pid_t id=fork();
if(id)
{
//parent
while(cnt)
{
printf("我是父进程,我还有%d秒就要退出了,我的pid是%d\n",cnt--,getpid());
sleep(1);
}
}
else
{
//child
while(1)
{
printf("我是子进程,我的pid=%d,我的ppid是%d\n",getpid(),getppid());
sleep(1);
}
}
return 0;
}
运行起来以后,整个程序的结果如下:
当时间到了5秒的时候,父进程退出,子进程就成了孤儿进程。此时子进程就会自动被1号进程领养。而1号进程就是操作系统!此时子进程就变成了一个后台进程。而我们ctrl+c只能够终止前台进程。 虽然这个时候我们照样可以输入指令操作。但是很不美观。这个时候就只能用kill命令来杀死后台进程了。而我们在命令行上用的指令,本质也是一个进程。而这些指令的父进程就是bash进程!
进程优先级的概念
我们知道,进程是有优先级的。比如系统的进程优先级就是要比用户的进程优先级高。而Linux对于进程的也划分了优先级。 对于现代计算机来说,有一个时间片的概念。所谓时间片,就是说cpu给固定的时间给一个进程运行。而大多数计算机都已经支持抢占式内核。一旦有有优先级高的进程到来,无论时间片是否用完,这个进程都会被剥夺下来。 那么Linux是如何确定进程的优先级的呢?
PRI和NICE值
事实上,Linux的优先级由两个部分构成分别是priority和nice值构成。而通常第一个值是固定的不能更改。而能够修改的只有第二个数值nice值!
如何设置进程的优先级
修改优先级的命令如下:
sudo top +r+pid+设置数值
这里的pri代表的就是进程main的优先级,接下来我们修改nice值为-20
我们可以看到,这里main进程的优先级被修改成了60。而我们注意到进程默认的优先数是80,每次修改完后,进行下一次修改的时候,这个priority都会恢复到默认的80。Linux不允许用户无节制设置nice值。合法的nice值范围是-20—19,priority值越小的进程优先级越高。
环境变量
接下来我们来谈一谈环境变量,相信学习过java的小伙伴对这个环境变量应该是很熟悉的。当安装了java的环境变量以后,那么就可以在任何地方编译java的源代码。 那么这个环境变量究竟是何方神圣呢?换言之,我们要如何理解环境变量呢?
怎么理解环境变量
在正式介绍环境变量之前,我们不妨先这样想一想。为什么我们自己的main程序成为main进程需要带上路径,但是执行系统的指令却不需要呢?
可以看到,这里的报错指的是未找到命令。一个程序要运行,首先系统要先找到这个程序的代码和数据。很显然,系统找不到main代码的数据和路径。 而环境变量起到的作用就是指路人的作用。
下面我们就来看如何查看系统的环境变量
如何查看系统的环境变量
//查看系统所有的环境变量可以使用env指令
env
//而如果需要输出系统默认搜索的路径,可以使用echo命令
echo $PATH //显示系统默认的路径
这是我的服务器的环境变量列表
这是我的系统的默认搜索路径:
显然,因为系统把所有的指令的路径都添加进了环境变量,所以系统命令无需带路径也可以直接运行。接下来,我们也尝试让我们自己的进程可以和系统命令一样直接使用。
使用命令修改环境变量
这里我就介绍一种比较合理的方法:系统的环境变量都是以:作为分割符的,所以我们可以这样添加环境变量:
PATH=$PATH:/home/chy/test/test/linux_code/blog_code
这时候我们就可以和系统命令一样不带路径运行了。不过这种方式在下一次系统重启的时候就失效了。
使用代码来获取环境变量
除了可以用指令,我们还可以用代码来获取环境变量。这里介绍一个知识:main函数实际是可以带参数的。
#include<stdio.h>
//argc是命令行参数,agrv是命令行字符串
int main(int argc,char* argv[])
{
for(int i=0;i<argc;++i)
{
printf("%s\n",argv[i]);
}
return 0;
}
而实际上,main函数是可以带三个参数的。如下:
#include<stdio.h>
//第三个参数就是环境变量
int main(int argc,char* argv[],char* env[])
{
int i=0;
for(;env[i];++i)
{
printf("%s\n",env[i]);
}
}
整个env数组的结构大致如下。最后一个必然是以NULL结尾。
第二种方式:通过C语言的定义的全局变量environ来使用
//用c语言自带的environ变量获取环境变量
#include<stdio.h>
int main()
{
extern char** environ;
int i=0;
while(environ[i])
{
printf("%s\n",environ[i++]);
}
return 0;
}
运行结果如下:
方法三:使用getenv()来获取环境变量。我们先来看一看getenv()的函数说明
所以严格来说,getenv还是c语言的接口。接下来我们就写一个专属于我的代码:
/*
* 使用getenv接口来获取环境变量*/
#include<stdio.h>
#include<strings.h>
#include<stdlib.h>
int main()
{
char* p=getenv("USER");
if(strcasecmp(p,"chy"))
{
printf("权限拒绝\n");
return 0;
}
printf("权限允许\n");
return 0;
}
使用root用户进行访问
当前我们的代码连root用户都给拦住了!这就是环境变量的作用,which命令能够找到对应系统指令的路径的原因也都依赖环境变量!
程序地址空间
在学习C语言阶段,我们都曾经学习过C语言程序的地址空间分布是如下图的:
但其实这种说法是不准确的,准确来讲地址空间并不属于语言的范畴。更确切地说,这里的地址空间不能叫做程序地址空间!而是进程地址空间!
进程地址空间的概念
接下来我们就来正式引进进程地址空间的概念。首先,我们要明确一点,进程地址空间并不是内存!而是操作系统为每一个个进程画的一个“大饼”。这么说可能有点突兀。不妨联系一下实际。 我们在使用电脑的时候,同时打开了qq,微信,酷狗音乐等等软件。而这些程序被加载到内存里面就成了一个个的进程。假如我的qq未响应。那么并不会影响我的微信和酷狗音乐。换言之,进程和进程间并不互相影响。也就是说,进程之间具有独立性! 那么操作系统为了保持这种独立性,就提出了地址空间这种概念。每次都给予进程所需的资源,让每一个进程觉得自己是独享整个系统的存储空间的!就好比把钱存入银行。我们并没有感觉银行在欺骗我们,我们还是得到了应该有的钱。但是我们的钱早就不知到被银行拿去如何规划了。进程地址空间也是如此。
利用代码来验证对应进程地址空间分布图
接下来我们就通过代码 来验证地址空间:
/*验证进程地址空间的分布*/
#include<stdio.h>
#include<stdlib.h>
int un_g_val;
int g_val=100;
int main(int argc,char* argv[],char* env[])
{
printf("code add %p\n",&main);
printf("uinit global val %p\n",&un_g_val);
printf("init global %p\n",&g_val);
char* m1=(char*)malloc(10);
char* m2=(char*)malloc(10);
char* m3=(char*)malloc(10);
char* m4=(char*)malloc(10);
printf("heap add %p\n",m1);
printf("heap add %p\n",m2);
printf("heap add %p\n",m3);
printf("heap add %p\n",m4);
printf("stack add %p\n",&m1);
printf("stack add %p\n",&m2);
printf("stack add %p\n",&m3);
printf("stack add %p\n",&m4);
for(int i=0;i<argc;++i)
{
printf("argv add %p\n",argv[i]);
}
int i=0;
for(;env[i];++i)
{
printf("env add %p\n",env[i]);
}
return 0;
}
运行结果如下:
我们可以看到堆和栈之间的空间出现了很大的镂空。不过确实这些地址是符合进程地址空间的描述的。
看看源代码的描述
在看源代码之前,我们可以联系生活中的例子。小时候,你和你的同桌大概率划过38线,那么进程空间肯定类似与38线规定了一个起始和结束。
//内核可能设计的结构
struct area
{
long start;
long end;
};
struct heaparea
{
.//..类似的结构
long heap_start;
long heap_end;
}
那么接下来我们来看一看对应的源代码:
这是mm_struct的内容
而mm_struct的vm_area_struct又是一个关键的结构,我们跳转过去:
由此可以见的,我们预想的结构和实际源代码的结构有一定的相同之处。
"奇怪"的fork
接下来,我们就来谈一谈前面这个有点"奇怪"的fork()。来看下面这段代码:
/*在谈fork*/
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int x=4;
int main()
{
int t=6;
pid_t id=fork();
if(id==0)
{
//子进程
while(1)
{
printf("我是子进程,我还有%d秒就要改数据了,x的值是%d,x的地址是%p\n",t--,x,&x);
if(!t) x=5;
sleep(1);
}
}
else
{
//父进程
while(1)
{
printf("我是父进程,x的值是%d,x的地址是%p\n",x,&x);
sleep(1);
}
return 0;
}
}
首先,从语言的角度来看,C语言不可能会存在两个死循环同时执行!,另外对于一个C语言函数来说,一个函数不可能有两个返回值。最离谱的是这里的x地址相同,但是却打印 出了不同的值! 为什么会出现这么奇怪的现象呢?
fork为什么有两个返回值?
首先,我们先来看fork的返回值。fork自身使用C语言编写的。C语言函数不能支持一次返回两个值,也就意味这fork返回两个值的的背后。一定是fork曾经return两次! 那么为什么fork会返回两次呢?这就和fork的机制有关系了。首先fork函数给父进程返回新创建的子进程id,而如果是子进程则返回0。这么设计的理由是Linux中父子进程是1:n的关系,所以父进程需要一个唯一确定的标识符来标识它的子进程。
那么 我峨嵋你不妨来分析fork之后,操作系统做了什么:
而我们看到if和else同时执行,原因是fork之后,父子进程相互独立了。也就是说这里相当于是两个进程在并发运行。所以才会出现这种从C语言角度不可能存在的事情。
写时拷贝
讲完了fork函数两个返回值的原因。我们再来看看上面实现的现象。明明变量x的值发生了改变,但是对应的x的地址确实相同的!
假如说这个地址就是真实变量存储的物理内存地址的话。同一个内存地址不可能有两个不同的值!也就是说,我们这里打印出来的地址并非是真实的物理内存地址! 那么当我在修改这个变量的值的时候,会发生什么呢?
在我们没有没有进行对数据的写入的时候,那么父子进程的虚拟地址和真实物理内存地址的映射是相同的:,而当子进程写入的时候,就会发生写时拷贝。类似于C++里面的string类的深拷贝。下图就是写时拷贝的具体过程。
这就解释了前面的奇怪的现象。
总结
以上就是这个文章的主要内容。知识点多且难度较大。但是理顺每一个知识点以后,对这个Linux进程的理解会更加深刻。如果有不足的地方可以指出错误。 希望大家可以一起共同进步。