本文已参与「新人创作礼」活动,一起开启掘金创作之路。
进程状态
一般系统进程状态:就绪,阻塞,运行。
Linux下主要有:
R运行态: 相当于就绪+运行,并不意味着进程一定在运行中,它表明进程要么在运行中、要么在运行队列中。S睡眠状态(可中断睡眠态):意为着这个进程在等待事件完成。D磁盘休眠状态(不可中断睡眠态)这个状态的进程会等待I/O的结束。T停止状态:可以通过发送SIGSTOP信号给进程来停止进程,这个被暂停的进程可以通过发送SIGCONT信号让进程继续运行,并非什么事都不干,而是在处理某些事件。X死亡状态:这个状态只是一个返回状态,不会再任务列表中看到这个状态。Z僵尸状态/僵死状态:子进程先于父进程退出,父进程对于信号没有处理,此时子进程为僵死状态。
进程终止
kill + pid 终止进程
kill -9 + pid 强制停止/杀死进程(9号信号)
- 无法被杀死的进程:僵尸进程,强杀也无法杀死。
僵尸进程
意为处于僵死状态的进程,强杀也无法停止。
产生原因:
- 子进程先于父进程退出,操作系统检测到子进程的退出,通过信号通知父进程。
- 但是父进程此时正处于运行态(相当于在专心工作),对于此信号没有关注,这时子进程为了记录现场(案发现场),操作系统不会完全释放子进程资源,因为子进程的
PCB包含有退出原因。 - 子进程这时候因为既没有运行,也没有完全退出,因此就处于僵死状态,称为僵尸进程。
危害:占用资源、资源泄露。
处理方法:
-
手动关闭父进程。( 但这只是处理方法,权宜之计,不是解决方法)
-
等待进程。(完全避免僵尸进程的产生)
孤儿进程
产生原因:
- 父进程先于子进程退出,父进程退出后,此时子进程由前台进程(
S+态)成为后台进程(S态)。但其并非没有父进程了,通过查看进程发现,它是被1号进程sbin/init"领养"了。
涉及相关与硬件相关的脱机概念,孤儿进程有可能摇身一变成为
守护进程(精灵进程),我们之后讲解~
进程优先级
优先级就是决定资源的优先分配权的等级划分。
- Q:为什么存在优先级?
- A:让操作系统运行的更加合理!
这里延伸两个相关概念:
交互式进程:一旦有操作,要【优先处理】批处理进程:一直~处理数据,但是对CPU要求并不是很高!
通过ps -elf指令可以查看目前进程的优先级。
优先级的设置:
PRI优先级NI(nice值) 【它的取值范围:-20~19】
修改公式:==PRI = PRI + NI==
因为系统默认PRI无法直接设置,但是可以通过设置NI值,进而按照公式进行调整PRI值。
修改nice值指令:
renice -n <size> -p <pid> //后缀的先后顺序固定
例如:renice -n 10 -p 7635
- PRI越大,优先级越低
- PRI越小,优先级越高
并行与并发
并行:CPU资源足够,多个进程 ==同时== 运行
并发:CPU资源不够,多个进程 ==切换== 运行,同时推进。
独立性:进程间相互独立。 竞争性:资源少,进程多,“狼多肉少”,每个进程之间都具有竞争性。
环境变量
保存系统运行环境参数的变量
目的:保存为环境变量后,运行的更加快速、高效。
常见环境变量:
PATH:指定命令的搜索路径。HOME:指定用户的主工作目录,即用户登陆到Linux系统中时默认的目录。SHELL:当前shell,它的值通常是/bin/bash。
环境变量相关指令:
echo: 通过变量名称查看指定环境变量 (例如echo &MYENV)env查看所有环境变量 (env或env | grep MYENV)set查看环境变量以及临时变量(比env查看显示的多,因为多列举了临时变量等)export声明/设置一个环境变量unset删除一个环境变量
环境变量的设置:
PATH=$PATH:/home/...
- 获取全部:
main(int argc,int char *argv[],char *env[]) //方法一:参数获取extern char **environ //方法二:全局变量获取 - 获取指定:
char *getenv(const char *env_name) //方法三:通过接口获取
请看代码:
//方法一: 通过字符指针数组env[]的参数获取
#include <stdio.h>
int main(int argc,int *argv[],char *env[]){
int i;
for(i = 0;env[i] != NULL;i++){
printf("env[%d] = [%s]\n",i,env[i]);
}
return 0;
}
//方法二 通过全局变量environ来获取,其本质是一个二级指针,相对于本程序是一个外部变量
int main(int argc,int *argv[],char *env[]){
extern char **environ; //在头文件中没有包含,但在库中包含
int i;
for(i = 0;environ[i] != NULL;i++){
printf("environ[%d] = [%s]\n",i,environ[i]);
}
return 0;
}
//方法三 getenv()接口,需要包含头文件 <stdlib.h> 也是最常用的一种!
printf("PATH:[%s]\n",getenv("PATH"));
环境变量的全局特性:
在子进程中获取继承于父进程的环境变量信息(继承特性) 需要在外部定义MYENV。临时变量不可以,而是要export声明为全局变量。
程序地址空间
对于32位操作系统,寻址空间为4G。一般内核占1G,用户占3G 。
地址是什么?是内存的编号,指向内存的一块区域。
虚拟内存地址空间
mm_struct内存描述符,描述出了一个虚假的内存地址空间,其本质是一个结构体,也存放在tack_struct结构体中。
struct mm_struct{
long int size;
code_start;
code_end;
data_start;
data_end;
}
- 页表:就记录虚拟地址与物理地址之间的映射关系,联系了二者并且对(虚拟)地址进行内存访问控制。
物理内存上可能不连续,但虚拟内存连续,二者是通过页表映射位置信息。这种“映射”,是拿到虚拟地址,经过一部分算法计算(页内偏移等因素影响),得到物理地址。而不是记录地址到地址的映射。
其实页表也是一个结构体,因为有了对页表的操作特性,所以也存在“二级页表”、“三级页表”等。
虚拟地址空间的好处:
- 内存充分利用。
- 内存访问控制。
- 保证每个进程的独立性。
写时拷贝技术
优点:
- 节省资源
- 提高子进程的创建效率
- 这种机制,会比
vfork创建进程更节省空间。
过程:
- 子进程拷贝父进程
PCB,同时拷贝了虚拟空间,同时拷贝出的页表也是相同的。 (如果创建的子进程可以修改父进程的数据,就不构成进程独立性,同时会造成栈混乱) - 如果一旦子进程要对内容进行修改,就会在物理内存上从新开辟一个地址,并拷贝存放更改的数据。
- 物理地址上存储数据的地址不同,但在虚拟地址上仍是同一个地址,所以说"数据独有"。
- 因为代码段是只读的,如果要修改就会报错,
只读属性也是页表上记录的,所以说"代码共享"。
内核O(1)调度算法
通过两个队列互相替代的一种内核调度算法。
活动队列
所有时间片还没有耗尽的进程都按照优先级放在该队列。
nr_active: 总共有多少个运行状态的进程queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级。
从该结构中选择一个最合适的进程,我们一般思路大致是这样:
- 从
0下标开始遍历queue[140]。 - 找到第一个非空队列,该队列必定为优先级最高的队列。
- 拿到选中队列的第一个进程,开始运行,调度完成。
- 遍历
queue[140]
虽然这样的程序时间复杂度是常数,但是太低效了。所以引入了位图的表示方法:
bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样就可以大大提高查找效率。
过期队列
过期队列和活动队列结构一模一样,其上放置的进程,都是时间片耗尽的进程。 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算。
实现方式
创建一active指针永远指向活动队列。
创建一expired指针永远指向过期队列。
可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片耗尽时就会入队过期队列。
所以在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程。可以开始进行下一周期的新调度。
小结
在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增加,我们称之为进程调度O(1)算法!
这是一种典型的空间换时间的操作。
【注】objdump -S + <程序名> :查看main函数的汇编代码