原文参考:https://blog.csdn.net/zhybiancheng/article/details/120312400
一、操作系统如何进行有效管理?
在讲操作系统如何是管理的之前,可以先说一说什么叫做管理?
我们在学校中常说辅导员管理整个年级,那么辅导员管理的整个年级到底指的是啥?管理其实本质上是在管理一些有效的数据。辅导员是在管理整个年级中反馈给辅导员的数据信息,而每一个班级反馈的数据信息形成了整个年级的数据信息,每一个班级中的每一个学生反馈的数据信息形成的数据信息组成了整个班级的信息。在辅导员收到这些数据信息的时候,就会进行一些措施去执行。这样就形成了整个管理的过程。
那说了这么多,我们再回来说说操作系统是如何进行管理的。同样操作系统也是通过收到来自计算机中各种数据来进行管理的。那么这些数据从哪里来的呢?这就是操作系统中最重要的概念了,即抽象出各种概念,通过结构体的形式描述出来,最后通过数据结构的形式组织起来。
数据的来源就是操作系统中的众多结构体,为了描述出结构体,操作系统给结构体定义了很多的属性,这样结构体就可以返回很多操作系统所需要的有效信息了。如此多的结构体返回有效的数据信息的时候操作系统还是很头疼,所以此时数据结构就起到了至关重要的作用,即将这些结构体通过不同的数据结构组织起来,通过使用不同的数据结构组织起来的数据所实现的效果是不一样的,可以通过具体的要求来进行选择,如如果想要快速找到元素,可以使用树这种结构。如果想要进行快速地插入元素可以使用链表的形式…
有六个字总结操作系统是如何进行管理的:先描述,再组织。
二、进程管理-初识进程和PCB
1.基本概念
操作系统中有很多抽象出的概念,而进程就是其中最深刻的概念之一。
进程的定义:一个执行中程序的实例,即一个正在执行的程序。
如果站在内核的角度来看:进程是分配系统资源的单位。
2.描述进程-进程控制块(PCB)
前面说了一个抽象的概念需要一个具体的结构体来进行描述的。进程中的信息就被放在了一个叫做进程控制块(PCB)的结构体中。
在不同的操作系统下进程控制块的名称不同(就好像不同地方的人称呼某一个东西会有不同的叫法一样),在Linux操作系统中PCB的具体名称是:task_struct。
当一个程序被加载到内存中要开始执行的时候,操作系统同时会给该进程分配一个PCB,在Linux中就是task_struct这里面包含了所有关于进程的数据信息。所以CPU对task_struct进行管理就相当于对进程进行管理。
2.1 task_struct
task_struct是Linux内核的一种数据结构,它会被装载到RAM里并包含进程的信息。每个进程都把它的信息放在task_struct这个数据结构里面,而task_struct包含以下内容:
[
](https://blog.csdn.net/zhybiancheng/article/details/120312400)
标识符:与进程相关的唯一标识符,用来区别其他进程
状态:进程会有不同的状态,如运行,停止等等
优先级:相对于其他进程的优先顺序
程序计数器:程序中即将执行的下一条指令的地址
内存指针:包括程序代码和进程相关数据的是很
上下文信息:进程执行时CPU的寄存器中的数据
IO状态信息: 包括显示的I/O请求,分配给进程的I/O设备和正在被进程使用的文件列表。
记账信息:可能包括处理器时间总和,使用的时钟总数,时间限制,记账号等
[
](https://blog.csdn.net/zhybiancheng/article/details/120312400)
1.进程标示符: 描述本进程的唯一标示符,用来区别其他进程。
也就是进程的PID,PID是操作系统中唯一标识的进程号。
有两个获得进程PID的方式:
1.1.可以使用ps aux查看进程的信息。
1.2.可以使用系统接口得到进程PID和父进程的PPID
#include <stdio.h>#include <unistd.h>int main() {printf("pid=%d, ppid=%d\n", getpid(), getppid()); // 进程号和父进程号return 0;}
2.进程状态
后面会详细讲解每一个状态,这里先大概地介绍一下。
3.优先级
因为CPU资源有限,而进程却有很多个,所以需要优先级这个属性去决定了进程拿到资源的顺序。**
4. 程序计数器: 程序中即将被执行的下一条指令的地址。
CPU有三个工作:取指令,分析指令和执行指令。CPU中的指令寄存器每一次都会保存下一条指令的地址,以此来进行指令判断。
5. 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
6.上下文数据
通常操作系统内核使用一种叫做「上下文切换」的方式来实现控制流。
实行这种机制是因为CPU只有一套寄存器,所有只能有将一个进程的存储数据放入寄存器中计算,从而形成了上下文数据。但是同时有多个进程的时候,操作系统为了使得CPU的利用率最高,所以会让进程之间来回的切换,一般进程切换有两种情况。
6.1 第一种情况:我们称两个执行流在执行的时间上与另一个执行流有重叠的部分,就称这个两个执行流在 「并发的运行」。一个进程在和其他的进程轮流轮流运行成为 「多任务」。一个进程执行它的控制流的那一段时间叫做 「时间片」。简单来说,每一个进程都会有最多执行的时间限制,如果执行时间超过了时间片,就会自动的退出执行。
6.2 第二种情况:当操作系统内核,发现一个优先级更高的进程的时候,该优先级更高的进程就会「抢占」当前进程的位置,然后执行优先级更高的进程。等到该进程执行完后,在执行被 「抢占」 的进程。这种决策方式叫做 「调度」。
以上两种情况,都会使得进程莫名其妙的退出CPU的执行,但是下次CPU还想接着上一次执行的地方继续执行那个莫名其妙退出的进程,所以就需要在进程退出之前,在task_struct保留下上一次执行的数据,方便下一次再被执行。
7. I/ O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
[
](https://blog.csdn.net/zhybiancheng/article/details/120312400)
3.组织进程
可以在内核源代码里找到task_struct。所有运行在系统里的进程都以task_struct双链表链表的形式存在内核里。如果在情况复杂的情况下,双链表中的节点也有可能存在在其他的数据结构中,例如队列等。
4.查看进程
查看进程有三种方式:
4.1.通过系统目录
4.2.通过ps命令
第二种方式:可以使用命令
ps aux # 查看系统中所有的进程信息 ps axj # 可以查看进程的父进程号


第三种方式:使用top命令
top # 动态的查看进程的信息,其中的信息默认3秒回更新
5.创建进程-fork()
创建进程有两种创建方式:
1.使用./运行某一个可执行程序,这种是最常见的方式
2.使用系统调用接口创建进程,即使用fork()
当时用fork()函数之后,就在原来的进程中创建了一个子进程,在fork()之前的代码只被父进程执行,在fork()之后的代码有父子进程一起执行。
创建的子进程和父进程几乎一模一样,子进程和父进程的共享地址空间,子进程可以或者父进程中所有的文件,只有PID是父子进程最大的不同。
利用fork创建一个进程来举个例子:
#include <stdio.h>#include <unistd.h>int main(){pid_t pid = fork();if (pid < 0) {printf("error");}if (pid == 0) {printf("i am a child process\n"); // 输出} else {printf("i am a father process\n"); // 输出}return 0;}
如果fork成功创建了一个进程,那么上面的代码就会输出i am a child process i am a father process这两句话。
这里面有很多有意思的点:
- fork函数调用一次,返回两次。
上面的代码是如何实现执行两个不同的分支语句的呢?其实是因为fork函数会返回两个返回值,一个是子进程会返回0,一个是父进程会返回子进程的PID。所以会同时进程两个分支语句中。
- 并发执行
父子进程是两个并发运行的独立程序。前面说过并发,就是两个执行流在执行的时间上有重叠的部分。也就是说父子进程谁先被调度是不能确定的。
- 相同但是独立的地址空间
两个进程其实地址空间是一样的,但是它们都有自己私有的地址空间,所以父子进程的运行都是独立的,一个进程中的内存不会影响另一个进程中的内存。
- 共享文件
子进程继承了父进程所有打开的文件,所以父进程调用fork的时候,stdout文件呢是打开的,所以子进程中执行的内容也可以输出到屏幕上
下面这个例子有一些的复杂,你可以检验一下自己有没有真正理解fork()
#include <stdio.h>#include <unistd.h>#include <sys/types.h>int main(){fork();fork();printf("hello\n");exit(0);return 0;}
三、进程状态
1. 查看进程的状态
2.不同的进程状态
进程有很多的不同的状态,在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 */ };
- R运行状态
处于R状态的进程有的在cpu中执行,但是很多都是在运行队列中等待运行,也就是该进程允许被调度。
模拟实现:
可以运行任意一个可运行的程序,即可出现R状态。
- S 睡眠状态
这种状态是一种浅度睡眠,此时的进程是在被阻塞的状态中,等待着条件的满足过后进程才可以运行。在这种状态下可以被信号激活,也可以被信号杀死。
模拟实现:
可以使用sleep() 系统调用接口使得一个进程睡眠
#include <stdio.h>int main(){while (1){printf("hello world\n");sleep(100); // 睡眠100秒}return 0;}
- D磁盘休眠状态
这种状态是一种深度休眠的状态,在这种状态下即使是操作系统发送信号也不可以杀死进程,只能等待进程自动唤醒才可以。
模拟实现:
这种情况没法模拟,一般都是一个进程正在对IO这样的外设写入或者读取的时候,为了防止操作系统不小心杀掉这个进程,所以特地创建出一个状态保护这种进程。
- T停止状态
可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
模拟实现:
可以使用信号
kill -SIGSTOP PID // 停止进程kill -SIGSONT PID // 继续进程

- X死亡状态
进程停止执行,进程不能投入运行。通常这种状态发生在接受到SIGSTOP、SIGTSTP、SIGTTIN、SIGOUT等信号的时候。
模拟实现:
可以使用kill -9 PID即可杀死一个进程
- z僵死状态
后面会详细讲解
3. 孤儿进程
如果父进程比子进程先退出,那么此时子进程就叫做孤儿进程。而操作系统不会让这个子进程孤苦伶仃的运行在操作系统中,所以此时孤儿进程会被init进程(也就是1号进程,即所有进程的祖先)领养,从此以后孤儿进程的状态和最后的PCB空间释放都是由init进程负责了。
模拟实现:
模拟实现让父进程比子进程提前退出。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if (pid == 0) { // 子进程一直执行
while (1) {
printf("I am a child, pid=%d, ppid=%d\n", getpid(), getppid());
sleep(1);
}
} else {
int count = 5; // 父进程执行5次
while (count --) {
printf("I am a father, pid=%d, ppid=%d\n", getpid(), getppid());
sleep(1);
}
exit(1);
}
return 0;
}
使用shell脚本监控进程信息
# 每隔一秒显示进程的信息
while :; do ps axj | head -1 && ps axj | grep a.out | grep -v grep; sleep 1; done

父进程退出后,自己子进程被1号init进程收养。
4. 僵尸进程
4.1.为什么会出现僵尸进程?
前面说过进程的作用是为了给操作系统提供信息的,所以在进程调用结束之后,应该将该进程完成的任务情况汇报(eixt code)给操作系统,但是进程在执行完之后已经结束了,所以此时进程的状态就是僵尸状态。
4.2.僵尸进程的概念
僵尸进程:即进程已经结束了,但是父进程没有使用wait()系统调用,此时父进程不能读取到子进程退出返回的信息,此时就该进程就进入僵死状态。
4.3.僵尸进程的危害
进程已经结束了,但是进程控制块PCB却还是没有被释放,这时就会浪费这一块资源空间。所以会导致操作系统的内存泄漏。
4.4.如何消灭僵尸进程?
僵死状态需要父进程发出wait()系统调用终止进程,如果父进程不终止进程,那么此时要消灭僵尸进程只能通过找到僵尸进程的父进程,然后kill掉这个父进程,然后僵尸进程就会成为孤儿进程,此时由init进程领养这个进程然后杀死这个僵尸进程。
模拟实现:
模拟实现让子进程比父进程提前退出。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if (pid == 0) {
int count = 5; // 子进程运行5次
while (count --) {
printf("I am a child, pid=%d, ppid=%d\n", getpid(), getppid());
sleep(1);
}
exit(0);
} else { // 父进程一直运行
while (1) {
printf("I am a father, pid=%d, ppid=%d\n", getpid(), getppid());
sleep(1);
}
}
return 0;
}
可以使用shell脚本监控
# 每隔一秒就查看进程的信息
while :; do ps aux | head -1 && ps aux | grep a.out | grep -v grep; sleep 1; done
5. 进程状态的转化
四、进程优先级
1.进程优先级的概念
1.1.进程优先级的定义
进程优先级为进程获取cpu资源分配的先后顺序,即进程的优先权,优先级高的进程可以有优先执行的权力。
1.2.进程优先级的意义
之所以会存在进程优先级,是因为cpu本身的资源分配是有限的,一个cpu一次只能run一个进程,但是一个操作系统中可能会有成千上百的进程,所以需要存在进程优先级来确定每一个进程获得cpu资源分配的顺序。
2.1.查看进程优先级
在linux或者unix系统中,用ps –al或者ps -l命令则会类似输出以下几个内容:
其中我们来了解几组关于进程优先级的相关信息:
- UID:执行者的身份,用户标识符
- PID:进程的编号
- PPID:进程的父进程的编号
- PRI:进程可被执行的优先级,PRI越小代表优先级越高
- NI:进程的nice值,代表进程优先级的修改数值
3.PRI and NI
PRI和NI是一组对应的概念。NI的取值会影响到PRI的最终值。
PRI代表进程被CPU执行的先后顺序,并且 PRI越小进程的优先级越高。NI代表nice值,表示进程的优先级的修改数值。所以两者之间有一个计算的公式:(new)PRI = (old)PRI + NI。
注意:
1.PRI在系统中默认初始化为80。
2.NI的取值范围为-20 ~ 19,一共40个级别。
3.当NI为正值,PRI增大,进程优先级变高。当NI为负值,PRI变小,进程优先级变小。
例如:
例1:
默认进程的PRI为80,当前的nice值为0,所以最终的PRI为80。
例2:
默认进程的PRI为80,当前的nice值为10,所以最终的PRI为90。
例3:
默认进程的PRI为80,当前的nice值为-10,所以最终的PRI为70。注意当要调整进程的nice值为负值的时候,由于要提升进程的优先级,所以需要超级用户root的权限才可以。
总结:在Linux环境下,我们一般说调整进程的优先级,就是在调整nice值。nice值决定性的影响到进程优先级。
4.更改nice值
4.1.通过top命令更改nice值
top命令是一个可以动态查看进程信息的命令(很像windows中的任务管理器)
4.2.通过renice命令更改nice值
修改进程的优先级也可以直接使用renice命令。
语法格式:renice nice值 进程PID


