操作系统——进程控制

本次实验全部基于Ubuntu 16.04完成
代码托管于GitHub: https://github.com/hnjia00/OS2019

最近的课程正在讲述进程有关的知识,老师说进程是面试的时候面试官最喜欢提问的话题,也是区分科班操作系统出身学生的一个标准,所以这部分内容和习题的实践性要远强于第一次。

Q1.

打开一个vi进程。通过ps命令以及选择合适的参数,只显示名字为vi的进程。寻找vi进程的父进程,直到init进程为止。记录过程中所有进程的ID和父进程ID。将得到的进程树和由pstree命令的得到的进程树进行比较。

A1.

第一题还是以基本操作为主,是一个按部就班的过程,下面就来逐步讲述。

  1. 打开vi进程只需在终端直接输入vi,执行结果如下:

Alt text

  1. 接下来启动另一个terminal,通过命令 ps -A 查找 vi 的进程信息,结果如下:

Alt text
Alt text

  1. 在找到vi的进程号 pid 之后,可以继续从 ppid 处得到进程的父进程id,通过命令ps -l逐步寻找vi的父进程,寻找步骤如下:

Alt text

  1. 在得到vi的进程调用序列之后,通过pstree命令来查看所有的进程树如下图所示,可以发现进程树命令和逐层寻找得到的结果相同,均为如下序列。

    1
    Systemd->lightdm->lightdm->gnome-terminal->bash->vi

    这里还要额外提一句,使用 Systemd 就不需要再用 init 了。这是因为init进程有两个缺点:启动时间长且启动脚本复杂。
    Systemd 就是为了解决这些问题而诞生的。它的设计目标是,为系统的启动和管理提供一套完整的解决方案。

Alt text
Alt text

Q2.

编写程序,首先使用fork系统调用,创建子进程。在父进程中继续执行空循环操作;在子进程中调用exec打开vi编辑器。然后在另外一个终端中,通过ps–Al命令、ps aux或者top等命令,查看vi进程及其父进程的运行状态,理解每个参数所表达的意义。选择合适的命令参数,对所有进程按照cpu占用率排序。

A2.

  1. 首先需要编写一个用于实现题干功能的C语言代码。
    分析题目可以看到,首先需要使用 fork() 系统调用创建子进程,其次根据fork的返回值相应的在父/子进程中填入对应的功能代码,我的代码具体如下,程序在父进程中执行空循环操作,在子进程中通过系统调用 execlp() 来启动vi进程,执行结果通过在另外的终端中的命令得以查看。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    #include <sys/types.h>
    #include <unistd.h>
    #include <stdio.h>

    int main(){
    pid_t pid;
    pid = fork();
    //父进程: pid>0
    if(pid > 0) while(1);
    //子进程: pid=0
    else if(pid == 0){
    int ret;
    ret = execlp("vi","",NULL);

    if (ret == -1){
    perror ("execl");
    printf("excel error\n");
    }
    }
    else if(pid == -1){
    perror("fork");
    printf("fork error\n");
    }

    }

ps-Al

执行 ps -Al命令的执行结果如下:
Alt text
其中,各个参数的解释如下:

参数 说明
F flag
S 程序的状态
UID 执行者身份
PID 进程ID
PPID 父进程ID
C 使用的CPU资源百分比
PRI 进程的执行优先权
NI 进程的nice值
ADDR 内核函数
SZ 占用内存的大小
WCHAN 进程正在睡眠的内核函数名称
TTY 登入者的终端机位置
TIME 使用掉的CPU时间
CMD 下达指令的名称

ps aux

执行 ps aux 命令可以打印使进程按如下格式输出:

1
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND

其中,各个参数的解释如下:

参数 说明
USER 行程拥有者
PID pid
%CPU 占用的 CPU 使用率
%MEM 占用的记忆体使用率
VSZ 占用的虚拟记忆体大小
RSS 占用的记忆体大小
TTY 终端的次要装置号码
STAT 该行程的状态(D=不可中断的睡眠状态,R=运行,S=睡眠,T=跟踪/停止,Z=僵尸进程)
START 行程开始时间
TIME 执行的时间
COMMAND 所执行的指令

执行结果如下:
Alt text
可以看到,./fork-exec进程一直在运行,且占用了97.7%的CPU资源,这应该全部归功于空循环。

top

top命令用于实时显示进程的动态,按照CPU的占有量降序排序,进程信息区统计信息区域的下方显示了各个进程的详细信息,各列的含义如下:

参数 说明
PID 进程id
USER 进程所有者的用户名
PR 优先级
NI nice值。负值表示高优先级,正值表示低优先级
VIRT 进程使用的虚拟内存总量,单位kb
RES 进程使用的、未被换出的物理内存大小,单位kb
SHR 共享内存大小,单位kb
S 进程状态(D=不可中断的睡眠状态,R=运行,S=睡眠,T=跟踪/停止,Z=僵尸进程)
%CPU 上次更新到现在的CPU时间占用百分比
%MEM 进程使用的物理内存百分比
TIME+ 进程使用的CPU时间总计,单位1/100秒
COMMAND 命令名/命令行

执行结果如下:
Alt text

Q3.

使用fork系统调用,创建如下进程树,并使每个进程输出自己的ID和父进程的ID。观察进程的执行顺序和运行状态的变化。
Alt text

A3.

实现进程树需要通过fork系统调用来实现,首先需要熟悉fork的具体作用。

fork在英文中是”分叉”的意思,fork函数启动一个新的进程,这个进程几乎是当前进程的一个拷贝:子进程和父进程使用相同的代码段,子进程复制父进程的堆栈段和数据段。

所以也就是说,执行一次fork函数,有两个返回值。根据返回值的不同可以区别父进程(>0)和子进程(=0)。

根据进程树的结构,p1有两个子进程p2和p3,同时p2也有两个子进程p4和p5,所以我所编写的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include<stdio.h>
#include<unistd.h>
#include<stdbool.h>
#include<sys/types.h>

int main(int argc, char *argv) {
/*
先打印根节点
*/
pid_t p1;
printf("p1 pid: %d, ppid: %d\n", getpid(),getppid());
if(p1 == 0){
/*
进入进程p1
*/
pid_t p3;
/*
先创建p3
*/
p3 = fork();
if(p3 == 0)
/*
j进入进程p3
*/
printf("p3 pid: %d, ppid: %d\n", getpid(),getppid());
else if(p3 > 0){
/*
进入p3的父进程,也就是p1。
接下来创建进程p2
*/
pid_t p2;
p2 = fork();
if(p2==0){
/*
进入进程p2,接下来先创建p4
*/
printf("p2 pid: %d, ppid: %d\n", getpid(),getppid());
pid_t p4;
p4 = fork();
if(p4 == 0){
/*
进入进程p4
*/
printf("p4 pid: %d, ppid: %d\n", getpid(),getppid());
}
else if(p4 >0){
/*
此处位于p4的父进程,即p2,继续创建p5
*/
pid_t p5;
p5 = fork();
if(p5==0)
printf("p5 pid: %d, ppid: %d\n", getpid(),getppid());
}
}
}
}
sleep(1);
}

程序的输出结果如下,满足题目的进程树的架构:
Alt text

Q4.

修改上述进程树中的进程,使得所有进程都循环输出自己的ID和父进程的ID。然后终止p2进程(分别采用kill -9 、自己正常退出exit()、段错误退出),观察p1、p3、p4、p5进程的运行状态和其他相关参数有何改变。

A4.

第四题应该是这几个题中最难的一个,综合了前三个题的知识应用,必须有对进程的充分认识和fork系统调用的理解才能实现这些功能,我的视线具体如下:

代码部分需要在第三题的基础上进行修改,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<stdbool.h>
#include<sys/types.h>

int main(int argc, char *argv) {
/*
先打印根节点
*/
pid_t p1;

if(p1 == 0){
/*
进入进程p1
*/
int p1pid,p1ppid;
p1pid = getpid();
p1ppid = getppid();
//printf("p1 pid: %d, ppid: %d\n", getpid(),getppid());
pid_t p3;
/*
先创建p3
*/
p3 = fork();
if(p3 == 0){
/*
j进入进程p3
*/
int i;
for(i=0;i<10;i++){
printf("p3 pid: %d, ppid: %d\n", getpid(),getppid());
sleep(1);
}
return 0;
}
else if(p3 > 0){
/*
进入p3的父进程,也就是p1。
接下来创建进程p2
*/
pid_t p2;
p2 = fork();
if(p2==0){
/*
进入进程p2,接下来先创建p4
*/
pid_t p4;
p4 = fork();
if(p4 == 0){
/*
进入进程p4
*/
int i;
for(i=0;i<10;i++){
printf("p4 pid: %d, ppid: %d\n", getpid(),getppid());
sleep(1);
}
return 0;

}
else if(p4 >0){
/*
此处位于p4的父进程,即p2,继续创建p5
*/
pid_t p5;
p5 = fork();
if(p5==0){
//p5
int i;
for(i=0;i<10;i++){
printf("p5 pid: %d, ppid: %d\n", getpid(),getppid());
sleep(1);
}
return 0;
}

else{
//p2
int i;
for(i=0;i<10;i++){
//通过exit()终止p2
if(i==2)
exit(1);
//段错误
//if(i==5)
//{
// int *p=NULL;
// *p=0;
//}
printf("p2 pid: %d, ppid: %d\n", getpid(),getppid());
sleep(1);
}
return 0;
}
}
}
}
int i;
for(i=0;i<10;i++){
printf("p1 pid: %d, ppid: %d\n", getpid(),getppid());
sleep(1);
}
return 0;
}
sleep(1);
}

即需要在刚进入p1的时候记录p1的pid和ppid以便后续打印输出,p2进程的输出部分需要控制在p5进程的父进程部分,如果在刚进入p2就执行循环输出,p4和p5进程就会因此无法创建。

程序的输出结果如下:
Alt text

下面分别采用kill -9 、自己正常退出exit()、段错误退出来终止p2进程。

kill -9

kill -9属于手动中断进程,通过此命令中断p2的结果如下:
Alt text
在p2被中断之后,其子进程p4和p5的参数ppid的值随即发生改变。

exit()

exit()函数属于安放在代码中的正常退出函数,将其放入p2循环打印的代码中,控制其在特定次数后执行便可以达到我们的预期,结果如下:
Alt text
可以看到,由于p2先于其父进程p1结束,随即就变成了僵尸进程defunct状态

段错误

所谓段错误,一般是访问了未申请的内存或非法的内存时产生的,概括点说在代码中一般是由指针的不当使用引起的。
为了引起段错误,我在代码中设置了如下代码段:
Alt text
其执行结果如下:
Alt text

参数变化

对于每种错误我均使用了ps命令来查看进程参数信息,最后发现三种情况所造成的进程中断所带来的参数变化是一样的,p2进程的参数变化如下:
Alt text
从图中可以看到:

  • 程序的状态由S变为Z,即僵死
  • 占用内存大小变为0
  • WCHAN由hrtime变为‘-’,进程停止
  • cmd部分多了僵尸进程标识符

参考文献

linux命令ps aux|grep xxx详解 https://www.cnblogs.com/robertoji/p/5555449.html

linux的top命令参数详解 https://www.cnblogs.com/LeoBoy/p/7976612.html

ps命令执行后各项参数的含义 https://blog.csdn.net/tcpipstack/article/details/8541980

小手一抖⬇️