操作系统第二次实验
操作系统实验二 进程控制
16281049 王晗炜 计科1601
实验题目
-
打开一个vi进程。通过ps命令以及选择合适的参数,只显示名字为vi的进程。寻找vi进程的父进程,直到init进程为止。记录过程中所有进程的ID和父进程ID。将得到的进程树和由pstree命令的得到的进程树进行比较。
-
使用ps逐步查找
-
首先在所在目录下使用vi打开一个文本文件,启动vi进程:
-
之后在终端中使用ps命令配合grep命令查找该进程:
-
在这里-ef表示显示出进程的完整信息,以下依次为结果各列对应的项:
- UID:程序被该 UID 所拥有
- PID:就是这个程序的ID
- PPID:则是其上级父程序的ID
- C:CPU使用的资源百分比
- STIME:系统启动时间
- TTY:登入者的终端机位置
- TIME:使用掉的CPU时间。
- CMD:所下达的是什么指令
-
根据其父进程继续查找,直到找到进程init:
如图所示进程1便是我们要找到了init进程,至此查询结束。
-
-
使用pstree查找
-
直接在终端中查询init进程的进程树:
-
如图所示其子进程众多,我们转而查询进程1017的进程树:
-
所标记的即为我们需要查找的子树
-
-
二者比较
使用ps命令查找是从当前进程一步步寻找父进程,是一个自底向上的过程,而pstree指令与之相反,是一个自顶向下的过程,前者查找的信息较为全面且精确,但需要操作的步数较多,而后者给出的信息虽然较少,但结构一目了然,不过在子进程较多的情况下寻找所需信息也较为复杂。因此这两种方法应结合实际情况具体分析选择使用。
-
-
编写程序,首先使用fork系统调用,创建子进程。在父进程中继续执行空循环操作;在子进程中调用exec打开vi编辑器。然后在另外一个终端中,通过ps –Al命令、ps aux或者top等命令,查看vi进程及其父进程的运行状态,理解每个参数所表达的意义。选择合适的命令参数,对所有进程按照cpu占用率排序。
-
按照题目要求编写程序,一下为源代码
2-2.c
:#include <sys/types.h> #include <unistd.h> #include <stdio.h> int main(){ pid_t pid; pid = fork(); if (pid > 0){ printf ("I am the parent of pid=%d!\n", pid); while(1); } else if (!pid){ printf ("I am the baby!\n"); int ret; ret = execl ("/usr/bin/vi", "vi","/home/anjinggufa/桌面/lab2/2-2.c", NULL); if (ret == -1) perror ("execl"); } else if (pid == -1) perror ("fork"); return 0; }
-
在终端下使用gcc编译并运行该程序:
-
之后使用ps命令查询父进程的进程号:
-
使用top命令查看二者的进程运行状况:
在top命令中p参数表示需要查询的进程号,这里只查询了2-2程序中创建了两个进程,对于查询的结果我们可知以下参数的含义为:
- PID: 进程描述符
- USER: 进程的拥有者
- PRI:进程的优先级
- NI: nice level
- SIZE: 进程拥有的内存(包括code segment + data segment + stack segment)
- RSS: 物理内存使用
- VIRT(virtul memory usage): 进程需要的虚拟内存大小
- RES(resident memory usage): 常驻内存
- SHARE: 和其他进程共享的物理内存空间
- STAT:进程的状态,有 S=sleeping,R=running,T=stopped or traced,D=interruptible sleep(不可中断的睡眠状态),Z=zombie。
- %CPU: CPU使用率
- %MEM: 物理内存的使用
- TIME: 进程占用的总共cpu时间
- COMMAND:进程的命令
由此我们可以知道父进程正在运行状态,而子进程处于休眠状态,所以两个进程相对的CPU占用率父进程为100%,要高于子进程。
-
-
使用fork系统调用,创建如下进程树,并使每个进程输出自己的ID和父进程的ID。观察进程的执行顺序和运行状态的变化。
-
根据题目要求以及进程树的结构,编写程序
2-3.c
:#include <sys/types.h> #include <unistd.h> #include <stdio.h> int main(){ pid_t pid; pid = fork(); if (pid > 0){ fork(); printf ("My pid is %d. I am the parent of pid=%d!\n",getpid(),getppid()); } else if (!pid){ pid = fork(); if(pid > 0) fork(); printf ("My pid is %d. I am the parent of pid=%d!\n",getpid(),getppid()); } else if (pid == -1) perror ("fork"); return 0; }
-
在终端中使用gcc编译并运行:
-
结果分析
根据进程号分析,我们可以得到P1~P5对应的进程号:
P1 P2 P3 P4 P5 5061 5062 5063 5064 5065 由终端上字符串打印的次序我们可以得知各个进程运行打印语句顺序为P1=>P3=>P4=>P2=>P5。
通过观察打印的结果我们可以发现一个异常,原本P5的父进程应该为P2,进程号为5062,但这里显示P5的父进程号为1120,与P2不符。
随后自己在网上查阅了有关fork调用的相关资料,进一步探究其工作机制。fork函数为一个系统调用函数,其作用为创建一个子进程,这个子进程与父进程一样,拥有一套与父进程相同的变量,相同的一套代码,这里可以粗浅的理解为子进程又复制了一份main函数。然后fork函数会返回两个不同的值,给子进程返回0,给父进程返回子进程的进程号。我们也就是用这个特性来区别父进程和子进程。而如果fork函数调用失败则会返回-1。
子进程从fork()的位置开始执行,也就是说前面的代码不走,但是拥有之前的变量以及变量的值,与父进程的值一样。当父进程比子进程提前退出时,子进程会成为孤儿进程,系统会将重置孤儿进程的父进程到init进程,ppid == 1。而在这里我们并没有发现进程号1而是出现了一个1120进程。随后我们用ps命令查询该进程:
可知这里出现的进程名为upstart进程,通过查阅资料我们又能发现现在的ubantu操作系统使用了upstart代替了原来的init,如果在之前的版本中,可能返回的就是1,而不是这里的upstart对应的进程号。如此孤儿进程的收养问题便能得到解决。
而P5又为什么会变为孤儿进程呢?我对此做出了有一个猜测:父进程的运行速度过快,在P5打印出父进程号之前就已经运行结束,于是P5便被upstart收养了。基于此猜测我们进一步对其进行验证,使用sleep()函数对每个进程进行一定时间的休眠,在此编译运行得到结果,以下给出修改后的代码
2-3_m.c
:#include <sys/types.h> #include <unistd.h> #include <stdio.h> int main(){ pid_t pid; pid = fork(); if (pid > 0){ fork(); printf ("My pid is %d. I am the parent of pid=%d!\n",getpid(),getppid()); sleep(1); } else if (!pid){ pid = fork(); if(pid > 0) fork(); printf ("My pid is %d. I am the parent of pid=%d!\n",getpid(),getppid()); sleep(1); } else if (pid == -1) perror ("fork"); return 0; }
在终端下编译运行:
这里得到的进程号便和预期的一致,也便验证了自己的猜想。
如此进程的运行顺序和状态便观察完毕。
-
-
修改上述进程树中的进程,使得所有进程都循环输出自己的ID和父进程的ID。然后终止p2进程(分别采用kill -9 、自己正常退出exit()、段错误退出),观察p1、p3、p4、p5进程的运行状态和其他相关参数有何改变。
-
根据题意修改上一题中的代码,得到
2-4.c
:#include <sys/types.h> #include <unistd.h> #include <stdio.h> int main(){ pid_t pid; pid = fork(); if (pid > 0){ fork(); while(1){ printf ("%d I am the parent of pid=%d!\n",getpid(),getppid()); sleep(10); } } else if (!pid){ pid = fork(); if(pid > 0) fork(); while(1){ printf ("%d I am the parent of pid=%d!\n",getpid(),getppid()); sleep(10); } } else if (pid == -1) perror ("fork"); return 0; }
-
在终端上使用gcc编译并运行:
-
随后首先使用kill -9命令终止P2进程:
可见kill进程执行后进程号仍然保留,而子进程则如上题一样被upstrat收养。
在Linux中kill和kill -9,两个命令在linux中都有杀死进程的效果,执行kill命令,系统会发送一个SIGTERM信号给对应的程序。当程序接收到该signal信号后,将会发生以下事情:
- 程序立刻停止
- 当程序释放相应资源后再停止
- 程序可能仍然继续运行
大部分程序接收到SIGTERM信号后,会先释放自己的资源,然后再停止。但是也有程序可能接收信号后,做一些其他的事情,也就是说SIGTERM多半是会被阻塞的。
然而kill -9命令,系统给对应程序发送的信号是SIGKILL,即exit。exit信号不会被系统阻塞,所以kill -9能顺利杀掉进程。基于此猜测执行exit进程后的结果和kill -9一样。
-
使用exit终止P2进程
因为exit只能在源程序中调用,这里对原先的程序做了一些修改,代码如下:
#include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(){ pid_t pid; pid = fork(); if (pid > 0){ fork(); while(1){ printf ("%d I am the child of pid=%d!\n",getpid(),getppid()); sleep(10); } } else if (!pid){ pid = fork(); if(pid > 0){ pid = fork(); printf ("%d I am the child of pid=%d!\n",getpid(),getppid()); sleep(10); if(pid > 0) exit(0); } while(1){ printf ("%d I am the child of pid=%d!\n",getpid(),getppid()); sleep(10); } } else if (pid == -1) perror ("fork"); return 0; }
之后再次编译运行:
之后再观察其打印结果:
可知其结果与以上一样,验证了以上的想法:kill -9 使用的就是系统中调用的exit。
-
使用段错误终止P2进程
这里段错误引发仍需在程序中进行改动,这里直接构造一个野指针造成一个内存访问错误从而实现段错误,代码改动如下:
#include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(){ pid_t pid; pid = fork(); if (pid > 0){ fork(); while(1){ printf ("My pid is %d I am the child of pid=%d!\n",getpid(),getppid()); sleep(10); } } else if (!pid){ pid = fork(); if(pid > 0){ pid = fork(); printf ("My pid is %d I am the child of pid=%d!\n",getpid(),getppid()); sleep(10); if (pid > 0){ int* p; *p = 1; } } while(1){ printf ("My pid is %d I am the child of pid=%d!\n",getpid(),getppid()); sleep(10); } } else if (pid == -1) perror ("fork"); return 0; }
再次编译并运行:
首次打印一切正常,等待10s过后观察再次打印的结果:
可见结果也和前两次一样,至此我们可以得出结论这三种结束进程的方式都不会终止子进程,而是将其变为孤儿进程最后被upstart收养。
附:实验源代码
-