五、进程状态
1.进程状态定义
一个进程从创建而产生至撤销而消亡的整个生命期间,有时占有处理器执行,有时虽可运行但分不到处理器、有时虽有空闲处理器但因等待某个事件的发生而无法执行,这说明进程和程序不相同,它是活动的且有状态变化的,能够体现一个进程的生命状态,可以用一组状态来描述:
内核源代码里面的状态定义:
1. /* 2. * The task state array is a strange "bitmap" of 3. * reasons to sleep. Thus "running" is zero, and 4. * you can test for combinations of others with 5. * simple bit tests. 6. */ 7. static const char * const task_state_array[] = {//进程也叫做任务 8. "R (running)", /* 0 */ 9. "S (sleeping)", /* 1 */ 10. "D (disk sleep)", /* 2 */ 11. "T (stopped)", /* 4 */ 12. "t (tracing stop)", /* 8 */ 13. "X (dead)", /* 16 */ 14. "Z (zombie)", /* 32 */ 15. };
通过不同状态来对进程进行区分,从而对进程进行分类。Linux进程的状态信息保存在进程的task_struct中。
2.进程状态分类
使用如下两条命令都可以查看进程当前状态:
ps aux
ps axj
查看到的进程状态:
(1)R-运行状态
R(Running):要么在运行中,要么在运行队列里,所以R状态并不意味着进程一定在运行中,因此系统中可能同时存在多个R状态进程。
如下代码statusType.c:
1. #include<stdio.h> 2. int main() 3. { 4. while(1); 5. return 0; 6. }
运行起来之后,一直处于运行状态,会发现是R+ 状态,其中+表示在前台运行:
如果运行时在后面加&,就会在后台运行,就变成R状态了:
后台运行的进程只能用kill -9 进程号来杀掉了:
在运行状态的进程,是可以被CPU调度的,当操作系统切换进程时,就会直接在运行队列里选取R状态进程。
(2)S-浅睡眠状态
S(Sleeping) :进程正在等待某事件完成,可以被唤醒,也可被杀死,浅睡眠状态也叫做可中断睡眠。
比如如下代码:
status.c
1. int main() 2. { 3. printf("hello linux\n"); 4. sleep(20); 5. 6. return 0; 7. }
在运行后20s内查看status进程的状态,发现为S+,执行kill命令后,该进程被杀死:
(3)D-深睡眠状态
D(Disk sleep):进程正在等待IO,不能被杀死,必须自动唤醒才能恢复,也叫不可中断睡眠状态。
进程等待IO时,比如对磁盘写入,正在写入时,进程处于深度睡眠状态,需要等待磁盘将是否写入成功的信息返回给进程,因此此时进程不会被杀掉
(4)T-停止状态
T(Stopped):可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送SIGCONT 信号让进程继续运行。
运行起来的status进程,通过SIGSTOP信号被暂停了,状态由S+变为T:
又通过SIGCONT信号恢复了,状态由T变为S:
kill -l命令可列出操作系统中所有信号,其中18就是SIGCONT信号,19就是SIGSTOP信号:
因此上述kill SIGCONT 进程号 也可以用kill -18 进程号来代替,kill SIGSTOP 进程号 也可以写成kill -19 进程号来代替。
(5)Z-僵尸状态
当进程退出时,所占用的资源不是立即被释放,而是要暂时保存进程的所有退出信息来辨别进程死亡的原因(比如代码有问题、被操作系统杀死等),这些数据都保存在task_struct中,供父进程或系统读取,这就是僵尸状态存在的原因。
当进程退出并且父进程没有读取到子进程退出的返回码时就会产生僵尸进程。僵尸进程会以终止状态保持在进程表中,并且会一直等待父进程读取退出状态码。
如下代码,statusZombie.cc:
1. #include<iostream> 2. #include<unistd.h> 3. using namespace std; 4. 5. int main() 6. { 7. pid_t id = fork(); 8. if(id == 0) 9. { 10. while(1) 11. { 12. cout << "child is running" << endl; 13. sleep(20); 14. } 15. } 16. else 17. { 18. cout << "father" << endl; 19. sleep(50); 20. } 21. return 0; 22. }
Makefile:
1. statusZombie:statusZombie.cc 2. g++ -o $@ $^ 3. .PHONY:clean 4. clean: 5. rm -f statusZombie
使用如下监控进程脚本
while :; do ps axj | head -1 && ps ajx | grep 进程名 | grep -v grep;sleep 1; echo "####################"; done
来监控进程状态,进程运行之后,父进程和子进程的状态变成了S:
杀掉子进程后,子进程的状态变成了Z状态:
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程就进入Z状态。
(6)X-死亡状态
这个状态只是一个返回状态,在任务列表里看不到这个状态。因为当进程退出时,释放进程所占用的资源时一瞬间就释放完了,所以死亡状态看不到。
3.僵尸进程危害
从僵尸状态我们知道了僵尸进程退出时会等待父进程或系统读取其返回码来辨别进程死亡的原因。这就像我们在写代码时,main函数的返回值都是0:
1. #include<stdio.h> 2. int main() 3. { 4. //code 5. return 0; 6. }
返回值0就是为了告诉操作系统代码顺利执行结束,可以使用echo $?来获取进程最后一次退出时的退出码:
当子进程退出,而父进程还在运行,但是父进程没有读取子进程的退出信息,子进程就进入了僵尸状态。
如下面代码zombieProcess.c,子进程在打印5次之后退出,父进程没有读取子进程的退出信息,此时子进程就变成僵尸状态:
1. #include<stdio.h> 2. #include<stdlib.h> 3. #include<unistd.h> 4. int main() 5. { 6. pid_t id = fork(); 7. if(id == 0)//child 8. { 9. int count = 5; 10. while(count) 11. { 12. printf("child PID:%d,PPID:%d,count:%d\n",getpid(),getppid(),count); 13. sleep(1); 14. count--; 15. } 16. printf("child is quiting\n"); 17. exit(1); 18. } 19. else if(id >0)//father 20. { 21. while(1) 22. { 23. printf("father PID:%d,PPID:%d\n",getpid(),getppid()); 24. sleep(1); 25. } 26. } 27. else//fork error 28. { 29. //do nothing 30. } 31. return 0; 32. }
使用监控脚本就可以看到子进程的状态就变成了僵尸状态:
僵尸进程危害:
(1)进程的退出状态必须被维持下去,因为它要把退出信息告诉父进程,如果父进程一直不读取,那么子进程就一直处于僵尸状态
(2)由于进程基本信息是保存在task_struct中的,如果僵尸状态一直不退出,只要父进程没有读取子进程退出信息,那么PCB一直都需要维护。
(3)如果一个父进程创建了多个子进程,并且不回收,那么就要维护多个task_struct 数据结构,会造成内存资源的浪费
(4)僵尸进程申请的资源无法进行回收,那么僵尸进程越多,实际可用的资源就越少,也就是说,僵尸进程会导致内存泄漏
六、孤儿进程
僵尸进程是子进程先退出,但是父进程没有读取子进程的退出信息。
假如父进程先退出,子进程后退出,此时子进程处于僵尸状态,没有父进程来读取它的退出信息,此时子进程就称为孤儿进程。
如下代码orphanProcess.c,父进程在5秒后终止退出,子进程并没有退出:
1. #include<stdio.h> 2. #include<unistd.h> 3. #include<stdlib.h> 4. int main() 5. { 6. pid_t id = fork(); 7. if(id ==0)//child 8. { 9. while(1) 10. { 11. printf("child\n"); 12. sleep(2); 13. } 14. } 15. else//father 16. { 17. sleep(5); 18. printf("father is quiting\n"); 19. exit(1);//父进程5秒后终止 20. } 21. return 0; 22. }
启动监控脚本,查看到父进程退出后,子进程就变成了孤儿进程,但是子进程的PPID变成了1,即子进程的父进程变成了1号进程:
1号进程是什么进程呢?
1号进程是init进程,也叫做操作系统进程,当出现孤儿进程的时候,孤儿进程就会被1号int进程领养,当孤儿进程进入僵尸状态时,就由1号init进程回收。
为什么孤儿进程会被1号进程领养呢?
如果孤儿进程要退出时,需要被回收, 那么需要一个进程回收它,所以孤儿进程被1号init进程领养,也就能被1号init进程回收了。
七、进程优先级
1.概念
进程的优先级就是CPU资源分配的先后顺序 ,即进程的优先权,优先权高的进程有优先执行权力。
还有一些其他概念:
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
- 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
- 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
2.为什么要有进程优先级
因为CPU资源是有限的,一个CPU只能同时运行一个进程,当系统中有多个进程时,就需要进程优先级来确定进程获取CPU资源的能力。
另外,配置进程优先权对多任务环境的linux很有用,可以改善系统性能。还可以把进程运行到指定的CPU上,这就把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
3. 查看系统进程
使用:
ps -l
命令查看系统进程:
可以看到
- UID : 代表执行者的身份,表明该进程由谁启动
- PID : 代表这个进程的代号
- PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
- PRI :代表这个进程可被执行的优先级,其值越小越早被执行
- NI :代表这个进程的nice值
4.PRI和NI
- PRI是进程的优先级,也就是就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
- NI就是nice值,表示进程可被执行的优先级的修正数值
- PRI值越小越快被执行,加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
- 当nice值为负值时,该程序优先级值将变小,即其优先级会变高,则其越快被执行
- Linux下调整进程优先级,就是调整进程nice值
- nice其取值范围是-20至19,一共40个级别。
注意: nice值不是进程的优先级,是进程优先级的修正数据,会影响到进程的优先级变化。
5.使用top命令更改进程优先级
(1)更改NI值
先运行一个进程,使用
ps -l
查看进程号、优先级及NI值,比如执行./forkProcess_getpid进程:
可以查看到优先级为80,NI值为0:
在运行top命令之后,输入r,就会有PID to renice,此时输入进程号5255,再输入NI值,此处设为10:
然后查看进程的优先级和NI值,优先级变成了90,NI值变成了10:
说明优先级和NI值已经被改了。由此也能验证:
PRI(new) = PRI(old)+nice
PRI(old)一般都是80,这就是为什么没有修改NI值之前,用ps -al命令查看到的进程的PRI都是80的原因。
(2)NI的取值范围
现在验证一下NI(nice)的取值范围,假如将NI的值设为100:
再查看进程的优先级和NI值,发现NI值变成了19,优先级增加了19:
这说明NI的上限就是19,那么下限呢?此时PID变成了12452,
将NI值改为-100:
发现NI值变成了-20,说明本次 的NI值变成了-20,优先级减小了20:
这说明NI的取值范围为-20~19,一共40个级别。
(3)NI取值范围较小的原因
因为优先级再怎么设置,也只能是一种相对的优先级,不能出现绝对的优先级,否则会出现很严重的进程“饥饿问题”,即某个进程长时间得不到CPU资源,而调度器需要较为均衡地让每个进程享受到CPU资源。
八、环境变量
1.概念
环境变量(environment variables)指操作系统中用来指定操作系统运行环境的一些参数。例如:在编写C/C++代码的时候,在链接的时候,从来不知道所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
2.常见环境变量
- PATH : 指定命令的搜索路径
- HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
- SHELL : 当前Shell,它的值通常是/bin/bash。
3.如何查看环境变量
我们运行可执行程序时,都需要在可执行程序前面加上./才能执行:
但是在执行系统命令时,为什么不需要在前面加上./呢?
命令、程序、工具,本质都是可执行文件,./的作用就是帮系统确认对应的程序在哪里,由于环境变量的存在,所以执行系统命令时,不需要在系统命令前加./
查看环境变量的方法:
echo $PATH
系统通过PATH进行路径查找,查找规则就是,在PATH中先在第一个路径中找,找不到就在第二个路径中找,再找不到就在第三个路径中找……,如果找到了就不往下找了,直接将找到的路径下的程序运行起来,这就完成了路径查找。即系统执行命令时,操作系统通过环境变量PATH,去搜索对应的可执行程序路径。
如何让forkProgress执行时不带./,跟执行系统命令一样,有2种做法:
- 把forkProgress命令拷贝到以上6种任意一个路径里,不过这种做法不推荐,会污染命令池
- 把当前路径添加到PATH环境变量中
平时安装软件,就是把软件拷贝到系统环境变量中特定的命令路径下就完成了,安装的过程其实就是拷贝的过程。
不能直接把当前路径赋值给PATH,否则上面的6种路径就全没了。可以使用export导入环境变量:
export PATH=$PATH:程序路径
查找到 forkProcess的路径:
添加环境变量:
现在在其他路径下也可以执行该可执行程序了,比如在家目录下执行:
4.和环境变量相关的命令
环境变量的本质是操作系统在内存/磁盘上开辟的空间,用来保存系统相关的数据。在语言上定义环境变量的本质是在内存中开辟空间,存放key、value值,即变量名和数据。
- echo:显示某个环境变量值
- export:设置一个新的环境变量
- env:显示所有环境变量
- set:显示本地定义的shell变量和环境变量
- unset:清除环境变量
用echo显示某个变量的值:
export设置一个新的环境变量, 前面已经设置过了:
env显示所有环境变量:
set显示环境变量:
unset清除环境变量: