操作系统实验1 并发程序设计(二)

简介: 操作系统实验1 并发程序设计

完成了对各个部分的介绍后,接下来将记录hello_loop进程的用户空间。为了更准确的绘制用户空间布局图,可以使用cat /proc/pid/maps命令查看进程中的空间分布。结果如下:


369d094aec924caa8ec6cde82e018284.png


图 12 用户空间分布

 每一行的数据依次代表着本段内存映射的虚拟地址空间范围,权限,本段映射地址在文件中的偏移,所映射的文件所属设备的设备号,文件的索引节点号,所映射的文件名。

 通过观察输出,可得出如下判断:


第一行的地址最低,并且只可读,不可写不可执行,因此可能是ELF头;

第二行可读可执行不可写,因此大概率为代码段

第三第四行只可读,并且夹在第五行(可读可写数据区)与第二行间(代码段),因此为只读数据段

第五行可读可写,因此为数据区

第六行是堆

第七行到第十二行,第十四行到第十八行为内存映射段

第二十行为栈区

第二十一行到第二十三行为系统调用区

综上,可做出如下进程用户空间布局图:


ca57ca9c6c0544d3b2b2ec4d5d5d160b.png


图 13 进程空间布局图


4 使用kill命令终止hello_loop进程


 由于hello_loop进程是死循环,我们可以使用kill命令来杀死这个进程,在命令行中输入kill+pid对进程进行杀死。


a176ce3e9dc64a38b51c18ac79574ee3.png


图 14 杀死hello_loop进程

 使用kill之后可以看到原本循环输出“loop”的界面已经停止,并输出了“Terminated”。


bcb6ebbb768442bcbe8752e7725f5a22.png


图 15 进程被杀死


5 使用fork创建子进程


 为了使并发执行的每个程序(含数据)能够独立运行以避免不可再现性(多次运行结果不同),需要对运行中的程序添加标识来供操作系统管理。

 在操作系统中,与此匹配的数据结构称为进程控制块(Process Control Block, PCB),系统利用PCB来描述进程的基本情况和活动过程,进而控制和管理进程。这样,由程序段、相关数据段以及PCB三部分构成的实体便是所谓——进程实体(又称进程映像(process image))。通常所说的进程即是进程实体。

 在接下来的实验中,创建进程指的是创建进程实体中的PCB;撤销进程指的是撤销进程的PCB。


5.1 创建1层50个子进程


使用fork创建如下图的一层50个子进程。


71e74ac8182f47cfa044e32e1887e73f.png


图 16 1层50个子进程

编写代码如下图:


b4c3a7f0aaa2433bacb3a869088a98e4.png


图 17 1层50个子进程代码图

 由于生成子进程的时候,是将父进程现有的数据复制一份给子进程,让子进程从生成位置开始继续运行。故每一个新生成的子进程,都拥有变量father_pid且该值等于parent process的PID,那么,在循环中,也就只有父进程会进行fork。

 我们可以通过进程树等方式来验证我们的结果,首先,通过“pstree”查看进程树,可以看到如下输出:


f258fffa801e4500a9fb44da5f51c453.png


图 18 1层50个子进程的进程树

 如图 18,我们的进程成功创建了50个子进程。此外,我们还可以通过对进程进行计数,完成对结果的验证,我们对进程中含有“fork1”的进程进行计数,结果如图 19。


183cf46993254be3a305885a3928e905.png


图 19 1层50个子进程的进程数

 可以看到,一共有51个进程,其中,有一个为父进程,则剩下的50个为子进程。

 最后,我们也可以通过查看进程信息,来观察进程间的关系,来验证我们的实验结果,具体如图 20:


301c74404e1b4fcbb6e0df8de4a928b7.png


图 20 1层50个子进程的进程间关系

 可以看到,第一个进程为父进程,进程号为2972,其余子进程有各自的进程号,但其父进程都为2972。因此,我们使用了三个方法验证了我们的实验。


5.2 创建50层1个子进程


使用fork创建如下图的50层1个子进程。


0e3835a5f99b4951b2e8eec2e9f07135.png


图 21 50层1个子进程

编写代码如下图:


df3c2b7c0f8b4174b46027f28c30b7de.png


图 22 50层1个子进程代码图

 第一个进程首先将initial_id设置为自己本身的PID值,由于fork()的内容皆在初始化initial_id之后,故每个子进程栈里的𝑖𝑛𝑖𝑡𝑖𝑎𝑙_𝑝𝑖𝑑都将是初始父进程的PID。接着在for循环中改变深度,每轮循环的时候,验证


If(initial_pid+i==getpid())

 若是则生成新的子进程,然后将fork返回值赋予pid,用于使父进程退出for循环。

 我们可以通过进程树等方式来验证我们的结果,首先,通过“pstree”查看进程树,可以看到如下输出:


8d5ebb7bfc0a443082239b4faac45809.png


图 23 50层1个子进程的进程树

 如图 23我们的进程成功创建了50个子进程。但由于显示有限,不能全部显示所有的进程。我们将通过对进程进行计数,进一步完成对结果的验证,我们对进程中含有“fork2”的进程进行计数,结果如图 24。


c04eea398b7f46f78312eafb4f886e40.png


图 24 50层1个子进程的进程数

 可以看到,一共有51个进程,其中,有一个为父进程,则剩下的50个为子进程。

 最后,我们也可以通过查看进程信息,来观察进程间的关系,来验证我们的实验结果,具体如下:


bbb7949a56c74390b35113845fb36386.png


图 25 50层1个进程的进程间关系

 可以看到,除第一个父进程外,每一个进程都是上一个进程的子进程,除最后一个进程外,每一个进程都是下一个进程的父进程。因此,我们验证了我们的实验。


5.3 创建6层完全二叉树式进程


使用fork创建如下图的6层完全二叉树式子进程。


eeb65a523c4b465d90e2a4f6603c10d8.png


图 26 6层完全二叉树式子进程

编写代码如下图:


b71d81434569428f804e0f042a3c264e.png


图 27 6层完全二叉树式子进程代码图

 思路上顺着上面两个程序的思路,我们充分利用好fork返回给父进程和子进程数值不同的这个特性,确保父进程先后生成子进程2个的时候,第一个生成的子进程不会执行for的后半段,导致生成额外的不被预期的子进程。

 我们可以通过进程树等方式来验证我们的结果,首先,通过“pstree”查看进程树,可以看到如下输出:


8636003559d947678ba8b27db055dcd5.png


图 28 6层完全二叉树式子进程的进程树

 如图 28我们的进程成功创建了二叉树式的子进程。但由于显示有限,不能全部显示所有的进程。我们将通过对进程进行计数,进一步完成对结果的验证,我们对进程中含有“fork3”的进程进行计数,结果如图 29。


c96e309d65e84504bfe7353f0b8bd391.png


图 29 6层完全二叉树进程的进程数

 由于这是一个6层的完全二叉树,故应有2^7-1=127。因此我们的实验正确。最后,我们也可以通过查看进程信息,来观察进程间的关系,来验证我们的实验结果,具体如下:


04e9935bc9a144c9ad0e76d79910cd5b.png


图 30 6层完全二叉树的进程间关系

 可以看到,第一个父进程节点的进程号为3452,其余进程都呈现完全二叉树的结构。因此,我们验证了我们的实验。


6 孤儿进程与僵尸进程的创建


6.1 孤儿进程的创建与观察


 孤儿进程(Orphan Process)指的是在其父进程执行完成或被终止后仍继续运行的一类进程。在Linux操作系统中,为避免孤儿进程退出时无法释放所占用的资源而僵死,任何孤儿进程产生时都会立即为系统进程init或systemd自动接收为子进程。

 在此,我利用fork设计了如下的代码以实现孤儿进程:


70a4444d2d7247e1952678c6fb3bbd69.png


图 31 孤儿进程代码

 子进程和父进程都会需要getchar来结束运行,若如预期,则运行程序后,第一次按下回车会结束父进程,子进程成为孤儿进程,并被init进程收养;再次按下回车,子进程死亡。

 通过使用ps,可以观察到如下结果:


0c0418e9e4204913b4567c80f045e03d.png


图 32 孤儿进程的ps观察

 可以发现,进程并没有如期被init收养,而是被一个进程号为967的进程收养了。我们使用top来观察这个进程。


bd6d93a022a74d7eaa97b2271fc8205f.png


图 33 进程号为967的进程信息

 如图 33,可以发现,这个进程为systemd系统进程。通过查看源码:


de0fb0c39a3446b289ae7d940ab25d35.png


图 34 systemd源码

 如图 34,可以发现,这里使用了一个比较特殊的prctl机制。经查阅Linux文档,可以发现1为prctl的第二个参数,当第二个参数若为非零时,调用进程设置child subreaper属性,此时孤儿进程成会被祖先中距离最近的supreaper进程收养。不会被init收养。避免孤儿进程被systemd收养,从而观测到进程被init收养的情况,可以使用命令行进行操作即可,具体结果如下:


5a0185435f914f0d80e4103afa2065c0.png


图 35 命令行下的孤儿进程

 可以看到,孤儿进程被pid为1的init收养了。


6.2 僵尸进程的创建与观察


 当子进程退出时,没有被父进程以wait ()或者waitpid ()回收,那么子进程变为僵尸进程(zombie process),状态栏会显示defunct。

 正常的进程在死亡之后,4G(32bit下为4G)的进程地址空间会主动释放,但是PCB仍然残留在内核中,等待父进程通过PCB了解它的死亡信息,也就是子进程的status。

 编写代码如下:


a9fec64d8ef944b18caa2ed1a9ea8c3d.png


图 36 僵尸进程代码

 如上,在父进程通过fork生成子进程后,子进程结束而父进程进入15秒睡眠,此时子进程成为僵尸进程,状态为defunct,空留PCB于操作系统中。直到父进程醒来,

 程序在运行10s后将产生僵尸进程,此时使用ps进行观察,结果如下:


58cef1e9c38d49bc8140451b96bf315a.png


图 37 僵尸进程状态

 可以看到,在父进程睡眠的10秒内,子进程以僵尸进程身份存在。而在父进程苏醒(并立刻死亡)后,子进程被正确回收。图 37中,第二次执行ps j |grep zombie已经只剩下grep–color=auto zombie了。PCB被正确回收。


7 阻塞与运行状态的观察


 可以编写代码如下所示。其思路上利用time (0)来获取时间,运行过程中一直陷入无限的for循环中,直到时长达到5秒,否则不退出。


fed27571fd9a4ac5b1f0cde575a20062.png


图 38 阻塞与运行代码图

 将该程序运行,可以观察到如下输出:


5080686db1214bf592f0e645f8c8f8e0.png


图 39 阻塞与运行的输出

 为了使用top对该进程进行观察,首先利用ps获取该进程的进程号如下:


b45a4818b04f4f11859a7e730fa407b9.png

图 40 ps获取该进程的进程号

 可以发现,该进程的进程号为4270,接下来我们使用top观察该进程。阻塞时的运行情况如下:


7caafa3d12954102a1956f830f0be30f.png


图 41 阻塞时运行情况

 运行时运行情况如下:


c44d8de581eb4b7cb09debe71191802c.png

图 42 运行时运行情况如下

 可以发现在进程休眠时cpu占用为0,在运行时则有比较高的cpu占用。成功实现了阻塞与运行时的观察。


四、 实验结论或体会


 通过本次实验,我学习了如何在Linux下编译并运行C代码,学习了函数fork()、execl()、exit()、getpid()和waitpid()的功能和用法。我学习了内存中是如何存储进程的相关代码文件,也通过实验了解了进程的创建、撤销和运行加深了我对进程概念和进程并发执行的理解,这使我能够区分进程与程序。

 由于有着比较好的Linux基础,本次实验并没有遇到比较棘手的困难,有不了解的地方也通过询问上网查阅资料以及询问助教得以妥善解决。但是在本次实验中,我学会了如何使用命令行,在之前的学习中我都以为通过Linux的图形化界面调出来的就是命令行。通过这次实验的学习,我明白了,直接调出来的是伪命令行,而实际的命令行需要通过Ctrl+Alt+F3进行切换。


相关文章
|
4月前
|
负载均衡 算法 Linux
操作系统的演化之旅:从单任务到多任务再到现代并发处理
【7月更文挑战第28天】在数字世界的心脏,操作系统(OS)是支撑一切软件运行的基础。本文将带领读者穿梭于操作系统的发展史,揭示从简单单任务处理到复杂多任务和现代并发处理技术的演进过程。我们将探讨如何通过这些技术提高计算机资源的利用效率,并分析它们对现代软件开发实践的影响。
49 5
|
2月前
|
Web App开发 Linux iOS开发
操作系统的演变:从单任务到多核并发
在数字时代的浪潮中,操作系统作为计算机硬件与应用程序之间的桥梁,其发展历史充满了创新与变革。本文将带领读者穿越时空,探索操作系统如何从简单的单任务处理演化为今天能够高效管理多核处理器的复杂系统。我们将一窥各个时代下操作系统的设计哲学,以及它们是如何影响现代计算的方方面面。加入我们的旅程,一起见证技术的力量如何在每次迭代中重塑世界。
40 7
|
4月前
|
弹性计算 运维
阿里云操作系统智能助手OS Copilot实验测评报告
**OS Copilot 产品体验与功能反馈摘要** 运维人员发现OS Copilot易上手,文档清晰,助其高效排查故障(8/10分)。愿意推荐并参与开源开发。亮点在于知识问答,能快速筛选答案。相较于竞品,优点是新手友好、文档清晰,但功能扩展性待增强。期望增加系统错误排查与解决方案,并集成ECS等,以优化系统安装流程。
阿里云操作系统智能助手OS Copilot实验测评报告
|
3月前
|
人工智能 监控 虚拟化
操作系统的演变:从单任务到多任务,再到并发和分布式
随着计算技术的发展,操作系统经历了从简单的单任务处理到复杂的多任务、并发处理,再到现代的分布式系统的转变。本文将探索这一演变过程中的关键里程碑,以及它们如何塑造我们今天使用的计算机系统的架构和性能。
|
4月前
|
算法 安全 Linux
操作系统的演变之旅:从单任务到多任务和并发处理
【7月更文挑战第30天】操作系统作为计算机硬件与软件之间的桥梁,其发展历程充满了创新与变革。本文将通过一次虚拟的时光旅行,探索操作系统从最初的单任务处理到现代多任务和并发处理的转变。我们将一窥操作系统如何适应计算需求的演进,并讨论这一过程中出现的关键概念和技术。
37 3
|
4月前
|
人工智能 分布式计算 物联网
操作系统的演变:从单任务到多任务再到并发和分布式
在数字时代的浪潮中,操作系统作为计算机硬件与应用程序之间的桥梁,其发展史是一部技术革新与需求演进的史诗。本文将带领读者穿梭于操作系统的时空隧道,从早期简单而原始的单任务系统出发,一路见证它如何逐步进化为支持多任务、并发执行乃至分布式计算的复杂系统。我们将一探究竟,是什么推动了这些转变,它们又是如何影响我们日常的技术实践与生活的。
62 1
|
4月前
|
弹性计算 运维 自然语言处理
阿里云操作系统智能助手OS Copilot实验测评报告
OS Copilot是针对Linux的智能助手,助力学习、运维及编程。用户界面直观,自然语言交互方便新手。官方文档详尽,但初次配置略复杂,适合学生和开发者。在提高代码编写和调试效率、系统学习上得分高,功能亮点包括代码生成、问答和命令执行。用户期待更多操作系统支持、自动错误分析和系统排查功能。
184 3
|
4月前
|
Web App开发 物联网 Unix
操作系统的演变:从单任务到多任务再到并发与分布式
本文旨在探讨操作系统的发展历程,着重分析其从处理单一任务的原始阶段,经历多任务处理能力的增强,直至支持并发计算和分布式架构的现代转型。我们将追溯关键时间节点,审视技术创新如何塑造了今日操作系统的复杂性与多样性,并预测未来可能的发展趋势。
|
3月前
|
物联网 Unix Linux
操作系统的演变:从单任务到多任务再到现代并发
操作系统作为计算机的核心软件,其设计和架构的演变反映了计算需求和技术的进步。本文将带领读者穿越时间线,探索操作系统从最初的单任务处理,发展到多任务处理,直至当代复杂的并发和分布式处理系统的历程。我们将一窥各个时代下操作系统的设计哲学、关键技术以及它们如何塑造了今日的数字世界。
61 0
|
14天前
|
安全 Linux 数据安全/隐私保护
Vanilla OS:下一代安全 Linux 发行版
【10月更文挑战第30天】
37 0
Vanilla OS:下一代安全 Linux 发行版