本次实验全部基于Ubuntu 16.04完成
代码托管于GitHub: https://github.com/hnjia00/OS2019
最近的课程正在讲述进程有关的知识,老师说进程是面试的时候面试官最喜欢提问的话题,也是区分科班操作系统出身学生的一个标准,所以这部分内容和习题的实践性要远强于第一次。
Q1.
打开一个vi进程。通过ps命令以及选择合适的参数,只显示名字为vi的进程。寻找vi进程的父进程,直到init进程为止。记录过程中所有进程的ID和父进程ID。将得到的进程树和由pstree命令的得到的进程树进行比较。
A1.
第一题还是以基本操作为主,是一个按部就班的过程,下面就来逐步讲述。
- 打开vi进程只需在终端直接输入vi,执行结果如下:
- 接下来启动另一个terminal,通过命令
ps -A
查找 vi 的进程信息,结果如下:
- 在找到vi的进程号 pid 之后,可以继续从 ppid 处得到进程的父进程id,通过命令
ps -l
逐步寻找vi的父进程,寻找步骤如下:
在得到vi的进程调用序列之后,通过pstree命令来查看所有的进程树如下图所示,可以发现进程树命令和逐层寻找得到的结果相同,均为如下序列。
1
Systemd->lightdm->lightdm->gnome-terminal->bash->vi
这里还要额外提一句,使用 Systemd 就不需要再用
init了。这是因为init进程有两个缺点:启动时间长且启动脚本复杂。
Systemd 就是为了解决这些问题而诞生的。它的设计目标是,为系统的启动和管理提供一套完整的解决方案。
Q2.
编写程序,首先使用fork系统调用,创建子进程。在父进程中继续执行空循环操作;在子进程中调用exec打开vi编辑器。然后在另外一个终端中,通过ps–Al命令、ps aux或者top等命令,查看vi进程及其父进程的运行状态,理解每个参数所表达的意义。选择合适的命令参数,对所有进程按照cpu占用率排序。
A2.
- 首先需要编写一个用于实现题干功能的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
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
命令的执行结果如下:
其中,各个参数的解释如下:
参数 | 说明 |
---|---|
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 | 所执行的指令 |
执行结果如下:
可以看到,./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 | 命令名/命令行 |
执行结果如下:
Q3.
使用fork系统调用,创建如下进程树,并使每个进程输出自己的ID和父进程的ID。观察进程的执行顺序和运行状态的变化。
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
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);
}
程序的输出结果如下,满足题目的进程树的架构:
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
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进程就会因此无法创建。
程序的输出结果如下:
下面分别采用kill -9 、自己正常退出exit()、段错误退出来终止p2进程。
kill -9
kill -9属于手动中断进程,通过此命令中断p2的结果如下:
在p2被中断之后,其子进程p4和p5的参数ppid的值随即发生改变。
exit()
exit()函数属于安放在代码中的正常退出函数,将其放入p2循环打印的代码中,控制其在特定次数后执行便可以达到我们的预期,结果如下:
可以看到,由于p2先于其父进程p1结束,随即就变成了僵尸进程defunct状态
段错误
所谓段错误,一般是访问了未申请的内存或非法的内存时产生的,概括点说在代码中一般是由指针的不当使用引起的。
为了引起段错误,我在代码中设置了如下代码段:
其执行结果如下:
参数变化
对于每种错误我均使用了ps命令来查看进程参数信息,最后发现三种情况所造成的进程中断所带来的参数变化是一样的,p2进程的参数变化如下:
从图中可以看到:
- 程序的状态由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