完成了对各个部分的介绍后,接下来将记录hello_loop进程的用户空间。为了更准确的绘制用户空间布局图,可以使用cat /proc/pid/maps命令查看进程中的空间分布。结果如下:
图 12 用户空间分布
每一行的数据依次代表着本段内存映射的虚拟地址空间范围,权限,本段映射地址在文件中的偏移,所映射的文件所属设备的设备号,文件的索引节点号,所映射的文件名。
通过观察输出,可得出如下判断:
第一行的地址最低,并且只可读,不可写不可执行,因此可能是ELF头;
第二行可读可执行不可写,因此大概率为代码段
第三第四行只可读,并且夹在第五行(可读可写数据区)与第二行间(代码段),因此为只读数据段
第五行可读可写,因此为数据区
第六行是堆
第七行到第十二行,第十四行到第十八行为内存映射段
第二十行为栈区
第二十一行到第二十三行为系统调用区
综上,可做出如下进程用户空间布局图:
图 13 进程空间布局图
4 使用kill命令终止hello_loop进程
由于hello_loop进程是死循环,我们可以使用kill命令来杀死这个进程,在命令行中输入kill+pid对进程进行杀死。
图 14 杀死hello_loop进程
使用kill之后可以看到原本循环输出“loop”的界面已经停止,并输出了“Terminated”。
图 15 进程被杀死
5 使用fork创建子进程
为了使并发执行的每个程序(含数据)能够独立运行以避免不可再现性(多次运行结果不同),需要对运行中的程序添加标识来供操作系统管理。
在操作系统中,与此匹配的数据结构称为进程控制块(Process Control Block, PCB),系统利用PCB来描述进程的基本情况和活动过程,进而控制和管理进程。这样,由程序段、相关数据段以及PCB三部分构成的实体便是所谓——进程实体(又称进程映像(process image))。通常所说的进程即是进程实体。
在接下来的实验中,创建进程指的是创建进程实体中的PCB;撤销进程指的是撤销进程的PCB。
5.1 创建1层50个子进程
使用fork创建如下图的一层50个子进程。
图 16 1层50个子进程
编写代码如下图:
图 17 1层50个子进程代码图
由于生成子进程的时候,是将父进程现有的数据复制一份给子进程,让子进程从生成位置开始继续运行。故每一个新生成的子进程,都拥有变量father_pid且该值等于parent process的PID,那么,在循环中,也就只有父进程会进行fork。
我们可以通过进程树等方式来验证我们的结果,首先,通过“pstree”查看进程树,可以看到如下输出:
图 18 1层50个子进程的进程树
如图 18,我们的进程成功创建了50个子进程。此外,我们还可以通过对进程进行计数,完成对结果的验证,我们对进程中含有“fork1”的进程进行计数,结果如图 19。
图 19 1层50个子进程的进程数
可以看到,一共有51个进程,其中,有一个为父进程,则剩下的50个为子进程。
最后,我们也可以通过查看进程信息,来观察进程间的关系,来验证我们的实验结果,具体如图 20:
图 20 1层50个子进程的进程间关系
可以看到,第一个进程为父进程,进程号为2972,其余子进程有各自的进程号,但其父进程都为2972。因此,我们使用了三个方法验证了我们的实验。
5.2 创建50层1个子进程
使用fork创建如下图的50层1个子进程。
图 21 50层1个子进程
编写代码如下图:
图 22 50层1个子进程代码图
第一个进程首先将initial_id设置为自己本身的PID值,由于fork()的内容皆在初始化initial_id之后,故每个子进程栈里的𝑖𝑛𝑖𝑡𝑖𝑎𝑙_𝑝𝑖𝑑都将是初始父进程的PID。接着在for循环中改变深度,每轮循环的时候,验证
If(initial_pid+i==getpid())
若是则生成新的子进程,然后将fork返回值赋予pid,用于使父进程退出for循环。
我们可以通过进程树等方式来验证我们的结果,首先,通过“pstree”查看进程树,可以看到如下输出:
图 23 50层1个子进程的进程树
如图 23我们的进程成功创建了50个子进程。但由于显示有限,不能全部显示所有的进程。我们将通过对进程进行计数,进一步完成对结果的验证,我们对进程中含有“fork2”的进程进行计数,结果如图 24。
图 24 50层1个子进程的进程数
可以看到,一共有51个进程,其中,有一个为父进程,则剩下的50个为子进程。
最后,我们也可以通过查看进程信息,来观察进程间的关系,来验证我们的实验结果,具体如下:
图 25 50层1个进程的进程间关系
可以看到,除第一个父进程外,每一个进程都是上一个进程的子进程,除最后一个进程外,每一个进程都是下一个进程的父进程。因此,我们验证了我们的实验。
5.3 创建6层完全二叉树式进程
使用fork创建如下图的6层完全二叉树式子进程。
图 26 6层完全二叉树式子进程
编写代码如下图:
图 27 6层完全二叉树式子进程代码图
思路上顺着上面两个程序的思路,我们充分利用好fork返回给父进程和子进程数值不同的这个特性,确保父进程先后生成子进程2个的时候,第一个生成的子进程不会执行for的后半段,导致生成额外的不被预期的子进程。
我们可以通过进程树等方式来验证我们的结果,首先,通过“pstree”查看进程树,可以看到如下输出:
图 28 6层完全二叉树式子进程的进程树
如图 28我们的进程成功创建了二叉树式的子进程。但由于显示有限,不能全部显示所有的进程。我们将通过对进程进行计数,进一步完成对结果的验证,我们对进程中含有“fork3”的进程进行计数,结果如图 29。
图 29 6层完全二叉树进程的进程数
由于这是一个6层的完全二叉树,故应有2^7-1=127。因此我们的实验正确。最后,我们也可以通过查看进程信息,来观察进程间的关系,来验证我们的实验结果,具体如下:
图 30 6层完全二叉树的进程间关系
可以看到,第一个父进程节点的进程号为3452,其余进程都呈现完全二叉树的结构。因此,我们验证了我们的实验。
6 孤儿进程与僵尸进程的创建
6.1 孤儿进程的创建与观察
孤儿进程(Orphan Process)指的是在其父进程执行完成或被终止后仍继续运行的一类进程。在Linux操作系统中,为避免孤儿进程退出时无法释放所占用的资源而僵死,任何孤儿进程产生时都会立即为系统进程init或systemd自动接收为子进程。
在此,我利用fork设计了如下的代码以实现孤儿进程:
图 31 孤儿进程代码
子进程和父进程都会需要getchar来结束运行,若如预期,则运行程序后,第一次按下回车会结束父进程,子进程成为孤儿进程,并被init进程收养;再次按下回车,子进程死亡。
通过使用ps,可以观察到如下结果:
图 32 孤儿进程的ps观察
可以发现,进程并没有如期被init收养,而是被一个进程号为967的进程收养了。我们使用top来观察这个进程。
图 33 进程号为967的进程信息
如图 33,可以发现,这个进程为systemd系统进程。通过查看源码:
图 34 systemd源码
如图 34,可以发现,这里使用了一个比较特殊的prctl机制。经查阅Linux文档,可以发现1为prctl的第二个参数,当第二个参数若为非零时,调用进程设置child subreaper属性,此时孤儿进程成会被祖先中距离最近的supreaper进程收养。不会被init收养。避免孤儿进程被systemd收养,从而观测到进程被init收养的情况,可以使用命令行进行操作即可,具体结果如下:
图 35 命令行下的孤儿进程
可以看到,孤儿进程被pid为1的init收养了。
6.2 僵尸进程的创建与观察
当子进程退出时,没有被父进程以wait ()或者waitpid ()回收,那么子进程变为僵尸进程(zombie process),状态栏会显示defunct。
正常的进程在死亡之后,4G(32bit下为4G)的进程地址空间会主动释放,但是PCB仍然残留在内核中,等待父进程通过PCB了解它的死亡信息,也就是子进程的status。
编写代码如下:
图 36 僵尸进程代码
如上,在父进程通过fork生成子进程后,子进程结束而父进程进入15秒睡眠,此时子进程成为僵尸进程,状态为defunct,空留PCB于操作系统中。直到父进程醒来,
程序在运行10s后将产生僵尸进程,此时使用ps进行观察,结果如下:
图 37 僵尸进程状态
可以看到,在父进程睡眠的10秒内,子进程以僵尸进程身份存在。而在父进程苏醒(并立刻死亡)后,子进程被正确回收。图 37中,第二次执行ps j |grep zombie已经只剩下grep–color=auto zombie了。PCB被正确回收。
7 阻塞与运行状态的观察
可以编写代码如下所示。其思路上利用time (0)来获取时间,运行过程中一直陷入无限的for循环中,直到时长达到5秒,否则不退出。
图 38 阻塞与运行代码图
将该程序运行,可以观察到如下输出:
图 39 阻塞与运行的输出
为了使用top对该进程进行观察,首先利用ps获取该进程的进程号如下:
图 40 ps获取该进程的进程号
可以发现,该进程的进程号为4270,接下来我们使用top观察该进程。阻塞时的运行情况如下:
图 41 阻塞时运行情况
运行时运行情况如下:
图 42 运行时运行情况如下
可以发现在进程休眠时cpu占用为0,在运行时则有比较高的cpu占用。成功实现了阻塞与运行时的观察。
四、 实验结论或体会
通过本次实验,我学习了如何在Linux下编译并运行C代码,学习了函数fork()、execl()、exit()、getpid()和waitpid()的功能和用法。我学习了内存中是如何存储进程的相关代码文件,也通过实验了解了进程的创建、撤销和运行加深了我对进程概念和进程并发执行的理解,这使我能够区分进程与程序。
由于有着比较好的Linux基础,本次实验并没有遇到比较棘手的困难,有不了解的地方也通过询问上网查阅资料以及询问助教得以妥善解决。但是在本次实验中,我学会了如何使用命令行,在之前的学习中我都以为通过Linux的图形化界面调出来的就是命令行。通过这次实验的学习,我明白了,直接调出来的是伪命令行,而实际的命令行需要通过Ctrl+Alt+F3进行切换。