目录
一、进程灵魂三问
第一问:操作系统中可不可能存在大量的进程?
答:可能
第二问:进程一定是被操作系统管理的么?
答:必须的
第三问:操作系统如何管理进程?
答:先描述,再组织
1)进程控制块 (概念)
描述进程是将进程的相关信息放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。在课本上进程控制块被称为PCB(process control block),在Linux操作系统中称为task_struct(一个包含程序属性信息的结构体) 。
2)进程概念(百度)
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
3)进程概念(通俗)
当运行程序时,操作系统会将程序代码加载到内存当中并且会创建与之代码对应的各种数据结构,例如:task_struct。简单点说,进程=程序内容+task_struct(真正的进程包含的内容不止于此,这里我们先笼统理解)。我们可以通过在命令行输入 ps asj | grep 可执行程序文件名 来查看系统中运行的进程。
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 while(1)
7 {
8 printf("hello world!\n");
9 sleep(1);
10 }
11 return 0;
12 }
执行上述程序,然后复制一个新的会话框输入指令➡ ps axj | grep 可执行程序文件名 ➡来查看系统运行的进程。
终止进程 crtl + c。
二、进程控制块中的内容分类
进程控制块中内容的分类对应于Linux系统中的 task_struct 。具体分类包含以下内容:
1)标示符: 描述本进程的唯一标示符,用来区别其他进程。
标识符是每个进程独有的,用以区分不同进程;其中PID表示子进程、PPID表示父进程。
运行上述代码,并通过复制会话框观察如下:
不难看出,此时子进程PID为 15693,父进程PID为 3921;然后我们再输入指令 ps axj | grep bash。结果如下:
可以发现此时bash进程的PID为3921,也就是上面./test子进程的父进程的标识符。直接上结论:在命令行上运行的命令,基本上父进程都是bash。
2)状态: 任务状态,退出代码,退出信号等
上一条指令的退出码。
3)优先级: 相对于其他进程的优先级。
cpu可能同时会运行多个进程在内存当中,此时这些进程并不是同时使用cpu的资源的,而是存在一个运行队列,通过调度模块较为均衡的运行各个进程;而每个进程的优先级决定了他们被调度的顺序。
4)程序计数器: 程序中即将被执行的下一条指令的地址。
5)内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
通过内存指针我们可以找到进程所对应的代码。
6)上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
上面提到多个进程组成运行队列被os较为均衡的轮番调度,假设有A、B两个进程正在运行。当A进程被调度时,B进程在等待;两个进程在运行的过程当中轮流切换(进程的代码有可能不是短时间内能够运行完的),由A切换到B再切换至A时如何知道此时A之前运行的数据呢?这里的数据指的就是上下文数据,这些数据是进程切换过程中需要被保存的,目的是为了cpu调度顺利运行各个进程。
7)I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
8)记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
9)其他信息
三、组织进程
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。进程task-struct中的诸多信息包含了程序的诸多属性。
四、查看进程
上面有提到的查看进程指令:
1)ps axj | grep 关键字
2)ps axj | head -1 && ps axj | grep 关键字
此外,存在着一个存放进程标示符的系统文件/proc,如下所示:
我们也可以在/proc系统文件中查看进程属性信息,如下所示:
五、fork初识
在Linux上查看man手册 - man fork。
可以看出其功能是可以创建出一个子进程。为了方便理解我们通过代码来演示以下:
1 #include<iostream>
2 #include<unistd.h>
3
4 int main()
5 {
6 fork();
7 std::cout << "hello proc:" << getpid() << "hello parent:" << getppid() << std::endl;
8 sleep(1);
9 }
代码运行结果如下:
从结果可以看出,fork()后一段代码被执行了两次,也就是说操作系统为之创建了两个进程,他们之间的关系为父子进程(通过上面进程的pid和父进程的pid可以看出)。那么两个进程中的父进程的ppid是哪个进程呢?也就是上面pid为15665的进程。
ps axj | grep 15665 查看结果如下:
★ 如何理解fork创建子进程
像我们在执行的 ./cmd、commit、fork......从操作系统角度来看,这些创建进程的方式并无区别。fork的本质是创建进程,系统中会随之会创建与进程相关的数据结构和程序代码。fork创建的子进程的代码和数据默认情况下会“继承”父进程的代码和数据,内核数据结构task_struct也会以父进程为模板初始化子进程的task_struct。
★ 进程是具有独立性的
在我们使用windows系统中,我们常常会运行多个软件(微信、QQ、浏览器等),相当于cpu同时创建多个进程,当我们在不同软件框中输入或是修改内容时,亦或者是关闭某个软件时并不会影响到其它的软件运行,因此可以得出结论:进程时具有独立性的。
★ 写时拷贝
上面我们提到fork创建子进程的程序代码和数据是“继承”父进程的,也就是说完全相同。那么当我们修改父进程的数据时,子进程的数据也会随之修改么? 从继承角度来看应该是的,但这样的话不就跟进程具有独立性的结论相违背了么?
因此,我们这里引入写时拷贝的概念,当不存在修改数据时,子进程默认继承父进程的一切;而当存在某一进程的数据修改时,存在写时拷贝的技术,以此来达到进程数据具有独立性的目的,具体如下:
★ fork创建子进程的目的
如果fork单单创建子进程是为了和父进程做同样的事情是没有意义的,一般是要让其做与父进程不一样的事情,可以通过if else分流来完成亦或是fork的返回值来完成。其中进程创建成功的话,父进程返回的是子进程的pid,子进程返回的是0;创建进程失败fork返回小于0。下面通过代码实验:
1 #include<iostream>
2 #include<unistd.h>
3
4 int main()
5 {
6 pid_t id = fork();
7 if(id == 0)
8 {
9 //child
10 while(1)
11 {
12 std::cout << "I am child!!!" << getpid() << "parent: " << getppid() << "ret: " << i d << std::endl;
13 sleep(1);
14 }
15 }
16 else if(id > 0)
17 {
18 // parent
19 while(1)
20 {
21 std::cout << "I am parent!!!" << getpid() << "parent: " << getppid() << "ret: " << id << std::endl;
22 sleep(2);
23 }
24 }
25 else
26 {
27 std::cout << "fail!!!" << std::endl;
28 }
29 return 0;
30 }
运行结果如下:可以看出一段程序代码创建了有两个进程在运行,父进程的fork返回值为子进程pid,子进程的fork返回值为0。
父进程和子进程的对应关系是一对多,父子进程一共有两个返回值且都是数据,会发生写时拷贝的情况。
六、进程状态
★ 问:进程的状态信息在哪里?
答:程序控制块(task_struct)。
★ 问:进程状态的意义?
答:方便OS快速的判断进程所处情况,完成特定的功能,比如:调度...本质上是对进程的分类。
进程存在着以下几种状态:
下面我们来一一了解下各个进程的状态信息:
★ R运行状态(running)
当两个及以上的进程运行时,并不是说这些进程都正在运行,而是通过CPU切换调度完成的。R运行状态的进程会处在一个运行队列run_queue当中,随时被CPU调度。处于队列中的是每个进程的task_struct,CPU通过它来找到进程的程序代码。
处于运行状态的进程不一定正在占用CPU。
★ S - sleeping(浅度睡眠、可中断睡眠)、D - disk sleeping(深度睡眠、不可中断睡眠)睡眠状态
进程在CPU中运行和在与外设的交互中的运算处理是不在一个数量级上的,当进程需要与外设交互时的一些操作是比较耗时的,例如:磁盘写入...此时的进程虽然也在OS上运行,但其因速度慢需要将其从run_queue队列脱离,这时的进程会处在一个等待队列。
当等待队列中的进程需要被CPU调度时,会马上将该进程的task_struct添加到运行队列当中,供CPU切换调度。
由此可见,进程并不是只会等待CPU资源。 所谓的进程在运行的时候,有可能因为运行需要,可以会在不同的队列里,在不同的队列里,所处的状态是不一样的。
我们把从运行状态的task_struct(run_queue)放到等待队列中,就叫做挂起等待(阻塞)。从等待队列放到运行队列中让CPU调度就叫做唤醒进程。
如果进程处于D状态,那么是不能够被操作系统杀掉的!假设以下情景,当某一进程需要给磁盘大量写入数据时,此时等待的时间较长;会占用一部分的OS资源,如果OS此时资源紧张,有可能会杀掉某些不重要的等待进程,例如S状态进程;而D状态会保护类似需要大量写入进程不被OS杀掉。但当操作系统中存在大量的D进程时,计算机可能会崩溃宕机。
★ T(stopped)运行状态 - 暂停状态
其与S状态的区别是:S状态时进程虽处在等待队列而未被CPU调用,但其核心数据也有可能会被更新,但T运行状态则是一种完全的暂停。
★ t(tracing stop)跟踪暂停
例如:VS中通过打断点调试代码。
★ X(dead)死亡状态
回收进程资源。
★ Z (zombie)僵尸状态
当进程退出时,需要先经过僵尸状态。僵尸状态存在的原因:辨别进程退出的原因-进程退出的信息,其也是数据并且包含在程序控制块task_struct当中。因此进程退出的流程实际上是先变为僵尸状态,然后才会进入到死亡状态。
★ 验证部分状态
R运行状态。
S运行状态。
该代码需要在显示器上打印,此时改进程大部分时间在S状态,由此可见I/O相比于CPU运算太慢了。
注意:+表示进程在前台运行。
T运行状态。通过kill命令来实现:
执行kill -l 查看命令号。
执行kill -命令号 pid ()将进程切换为 T 状态。
恢复运行 -- kill -18 pid。
此时进程状态没有+,后台运行,无法用crtl+c退出,需要执行指令 kill -9 pid。
当运行程序加载进程时,可以通过 ./可执行文件名 & 来后台运行。fg 命令后台程序调整到前台运行。
Z状态验证。
进程退出时,资源会被其父进程回收,如果违背回收,则该进程将处于僵尸状态。
代码:
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 pid_t id = fork();
7 if(id == 0)
8 {
9 while(1)
10 {
11 printf("I am child!\n");
12 sleep(1);
13 }
14 }
15 else{
16 printf("father do nothing!\n");
17 sleep(50);
18 }
19 return 0;
20 }
做法:
运行上述代码,当父进程(50s)等待时,干掉子进程,此时父进程未结束,子进程不会被资源回收,因此子进程将处于僵尸状态。
★ 孤儿进程
fork创建子进程,当子进程还在运行时干掉父进程,此时的子进程就会变为孤儿进程。其子进程的父进程pid就会变为 1 (操作系统)。
七、进程优先级
★ 基本概念
cpu资源分配的先后顺序,就是指进程的优先权(priority)。
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
查看当前会话框进程:
UID : 代表执行者的身份-执行的用户
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
★ PRI and NI
PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高。
那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值。PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为: PRI(new)=PRI(old)+nice这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行。
所以,调整进程优先级,在Linux下,就是调整进程nice值。nice其取值范围是-20至19,一共40个级别。
★ PRI vs NI
需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
可以理解nice值是进程优先级的修正数据。
★ 用top命令更改已存在进程的nice:
进入top后按“r”–>输入进程PID–>输入nice值–>q退出。
★ nice值为何要是一个相对较小的范围呢?
优先级再怎么设置,也只能是一种相对的优先级,不能出现绝对的优先级,否则会出现很严重的进程“饥饿问题”。
调度器:较为均衡的让每个进程享受CPU资源。
八、进程并行与并发
★ 进程并行
一台计算机有多个CPU,多个进程在不同的CPU上同时运行(多个进程同一时间段同时运行)。
★ 进程并发
一个CPU通过对多个进程快速切换来完成进程运行(多个进程不同时间段分别运行)。