本次实验全部基于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执行结果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 | 所执行的指令 |
执行结果如下: ps aux执行结果
可以看到,./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 | 命令名/命令行 |
执行结果如下: top执行结果
Q3.
使用fork系统调用,创建如下进程树,并使每个进程输出自己的ID和父进程的ID。观察进程的执行顺序和运行状态的变化。
A3.
实现进程树需要通过fork系统调用来实现,首先需要熟悉fork的具体作用。
fork在英文中是”分叉”的意思,fork函数启动一个新的进程,这个进程几乎是当前进程的一个拷贝:子进程和父进程使用相同的代码段,子进程复制父进程的堆栈段和数据段。
所以也就是说,执行一次fork函数,有两个返回值。根据返回值的不同可以区别父进程(>0)和子进程(=0)。
根据进程树的结构,p1有两个子进程p2和p3,同时p2也有两个子进程p4和p5,所以我所编写的代码如下:
1 |
|
程序的输出结果如下,满足题目的进程树的架构: 进程树模拟
Q4.
修改上述进程树中的进程,使得所有进程都循环输出自己的ID和父进程的ID。然后终止p2进程(分别采用kill -9 、自己正常退出exit()、段错误退出),观察p1、p3、p4、p5进程的运行状态和其他相关参数有何改变。
A4.
第四题应该是这几个题中最难的一个,综合了前三个题的知识应用,必须有对进程的充分认识和fork系统调用的理解才能实现这些功能,我的视线具体如下:
代码部分需要在第三题的基础上进行修改,代码如下:
1 |
|
即需要在刚进入p1的时候记录p1的pid和ppid以便后续打印输出,p2进程的输出部分需要控制在p5进程的父进程部分,如果在刚进入p2就执行循环输出,p4和p5进程就会因此无法创建。
程序的输出结果如下: 循环输出结果
下面分别采用kill -9 、自己正常退出exit()、段错误退出来终止p2进程。
kill -9
kill -9属于手动中断进程,通过此命令中断p2的结果如下: kill命令中断
在p2被中断之后,其子进程p4和p5的参数ppid的值随即发生改变。
exit()
exit()函数属于安放在代码中的正常退出函数,将其放入p2循环打印的代码中,控制其在特定次数后执行便可以达到我们的预期,结果如下: exit命令中断
可以看到,由于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