SHANGHAI UNIVERSITY
操作系统(一)实验报告
组 号 |
第 4 组 |
学号姓名 |
20120889曹中阳 |
实验序号 |
实验三 |
日期 |
2022年 9 月 10 日 |
一、 实验目的与要求
实验目的:
- 利用Linux提供的系统调用设计程序,加深对进程概念的理解。
- 体会系统进程调度的方法和效果。
- 了解进程之间的通信方式以及各种通信方式的使用。实验要求:
二、实验环境
VMware+Linux发行版 Ubuntu 20.04版
三、实验内容及其设计与实现
3.1 控制部分
3.1.1 编写程序。显示进程的有关标识(进程标识、组标识、用户标识等)。经过5 秒钟 后,执行另一个程序,最后按用户指示(如:Y/N)结束操作。
答:编写程序。使用get*id获取其进程id和父/组/用户等标识,经过5 秒钟后,执行另一个程序prog2.o,在prog2.c中循环接受用户输入的指令按用户指示(如:Y/N)结束操作。两段程序代码如下:
图1.prog1.c(左) prog2.c(右)
编写完成后保存程序,在终端编译、运行第一段程序:
图2.prog1.o/ prog2.o执行情况
终端打印出了pid,ppid等信息。出现“是否要继续执行?Y继续N结束:”可见我们已经开始运行第二个程序,输入Y确定,可见终端循环打印 hello world直至输入N。
3.1.2 参考例程1,编写程序。实现父进程创建一个子进程。体会子进程与父进程分别获得
不同返回值,进而执行不同的程序段的方法。
思考:子进程是如何产生的? 又是如何结束的?子进程被创建后它的运行环境是怎样建立的?
答:首先在终端创建example1.c,将例程一代码输入。在终端编译、运行example2.c,得到如下结果:
图3.example1.c以及执行情况
根据代码运行结果,我们可以得知:(1)当父进程运行到fork()时,它创建了PID=2795的子进程,并分配空间和资源,子进程由此产生。(2)当子进程执行exit(0)时,子进程结束并返回父进程,此时父进程的wait()结束,开始输出子进程结束的信息。(3)当子进程创建后,系统同时开辟了相同大小的2个空间,并将与父进程相同的代码、变量和资源给子进程,由此子进程有了运行环境。由于fork()对父进程的返回值是子进程PID,而对子进程返回值为0,因此即使有相同的代码,子进程仍由于if判断条件而执行了和父进程不同的代码段。
3.1.4 参考例程2,编写程序。父进程通过循环语句创建若干子进程。探讨进程的家族树以及子进程继承父进程的资源的关系。
思考:① 画出进程的家族树。子进程的运行环境是怎样建立的?反复运行此程序看会有什么情况?解释一下。
② 修改程序,使运行结果呈单分支结构,即每个父进程只产生一个子进程。画出进程树,解释该程序。
答: 首先在终端创建example2.c,将例程2代码输入。在终端编译、运行example1.c,得到如下结果:
图4.example2.c以及执行情况
① 进程的家族树:
由于进程pid是连续的,故其可以代表进程创建时间的先后顺序;同时家族书的关系又表示了各子进程的相互关系。分析进程树可知,父进程pid为4434,其运行到语句fork()时,创建子进程4435,这个过程在for循环下,i=0;4435创建完成后父进程4434由于wait语句暂停运行等待子进程返回,而子进程在打印自身信息之后,会回到for循环在i++后继续创建子进程4436,也就是4434的孙进程,此时i=1;同理,4436会在i=2的条件下创建4437进程,而4437进程创建且打印信息后,i++后已无法满足循环结果,因此返回进程4436。此时父进程4436wait()结束,但由于父进程也在for循环中,它会在i=2的条件下创建4438,重复上述过程后返回到4434,由于4434进程也在for循环中,它会在i=1的条件下创建4439……依照该规则,最终形成如上图所示的进程树。
稍等一段时间,然后快速进行2次运行:
图5.example2.c多次执行情况
我们发现运行进程的pid和第一次不一样了,而快速执行2遍时第二次进程的pid恰好接在了第一次运行的pid之后。通过查阅资料得知,虽然进程的pid是可以复用的,但是为防止将新进程误认为是使用同一 ID 的某个已终止的先前进程,Linux 采用延迟重用的算法,在大部分场景下会使得新进程 ID 不同于最近终止进程所使用的 ID。而时间相近的进程创建也会顺序的获得相邻的pid号码了。
② 我们在父进程输出完子进程结束的信息后直接break出循环,就可保证其不再创建另外的子进程。
图6.example2.c修改后及其执行情况
图7.example2.c修改后进程树
画出改变后的程序进程树,分析程序:每当父进程创建了一个子进程时,它本身进入了wait()函数中;子进程虽然无需等待,但是其在信息输出后会进入循环中,成为下一个子进程的父进程,继而进行递归的等待。因此只要在父函数wait()结束并输出信息之后直接跳出循环,就可以防止第二个子程序的创建了。
3.1.5 参考例程3 编程,使用fork( )和exec( )等系统调用创建三个子进程。子进程分别启动不同程序,并结束。反复执行该程序,观察运行结果,结束的先后,看是否有不同次序。
思考:子进程运行其它程序后,进程运行环境怎样变化的?反复运行此程序看会有什么情况?解释一下。
答: 首先在终端创建example3.c,将例程3代码输入。在终端编译、运行example3.o,得到如下结果:
图8.example3.c以及运行结果
运行后我们可以看到:(1)终端依次显示每个子进程内执行程序的结果和返回父进程后的回显(child process X terminated with status),而不是同时显示三个子进程的运行结果后,再依次显示返回父进程后的回显。
从运行结果分析,我们可以得到当一个子进程开始运行时,在其运行周期内,终端上只显示该进程的返回信息,只有当该子进程结束后,终端才显示下一个子进程运行的信息。对此我的理解是,尽管这不代表系统同时只运行一个子进程,但是至少可以说明终端在同一时间只处理一个子进程的输出。这表示各个并发进程之间不会互相影响。
(2)思考:子进程运行其它程序后,进程运行环境怎样变化的?反复运行此程序看会有什么情况?解释一下。
答:反复运行本程序,发现子进程1、2、3的运行先后顺序并不确定,而是具有随机性,可以变化的。但是有趣的是在多次运行中子进程1的信息总是第一个输出在终端上。
分析代码,我们可以知道父进程所做的是依次创建子进程1、2、3,然后等待他们执行完并回显。如果按照面向过程编程,理应在终端按照1、2、3的顺序依次显示子进程运行结果。但是考虑到各个子进程是并发执行的,再结合之前发现的子进程1的结果似乎总是先输出在终端,我们可以这样推断:各个子进程的创建有先后之分,但同时各个子进程中程序的复杂度不一,运行时间有快慢之分,而先运行完的子进程将先输出并先返回父进程,因此造成运行结果不一定与创建顺序一致;也由于子进程之间是并发运行的,受运行环境影响,各次的程序执行时间不会完全一致,造成子程序运行多次时有不同的输出顺序。此外子进程1比较简单,只输出字符串,因此运行时间往往最短,也常在第一个输出。
3.1.6 参考例程4 编程,验证子进程继承父进程的程序、数据等资源。如用父、子进程修改公共变量和私有变量的处理结果;父、子进程的程序区和数据区的位置。
思考:子进程被创建后,对父进程的运行环境有影响吗?解释一下。
答: 子进程被创建后,对父进程的运行环境没有影响。首先在终端创建example4.c,将例程4代码输入。在终端编译、运行example4.o,得到如下结果:
图9.example4.c以及运行结果
分析代码,我们可以知道程序fork后分为父进程和子进程,其中父进程只执行输出字符串的操作,而不修改变量;子进程则将全局变量++,私有变量-1后输出。根据运行结果,我们可以看出子进程确实修改了全局变量和局部变量的值,但是由于子进程是最后结束的,这无法证明父进程的资源有没有修改。因此我们对example4.c稍加修改,使父进程等待子进程返回后再执行输出:
图10.example4.c改进版以及运行结果
分析运行结果,我们可以看到子进程首先修改了全局变量和局部变量的值,并且成功修改并输出了;但是返回父进程后,父进程输出的变量结果仍然是初始时的结果,这说明了(1)子进程在fork()时复制了父进程的资源,而不是直接和父进程共享资源;(2)子进程修改资源后,父进程的变量值仍未改变,说明子进程的创建和运行对于父进程的运行环境没有影响。
3.1.7 参照《实验指导》第五部分中“管道操作的系统调用”。复习管道通信概念,参考例程5,编写一个程序。父进程创建两个子进程,父子进程之间利用管道进行通信。要求能显示父进程、子进程各自的信息,体现通信效果。
思考:①什么是管道?进程如何利用它进行通信的?解释一下实现方法。
③ 修改睡眠时机、睡眠长度,看看会有什么变化。请解释。 ③加锁、解锁起什么作用?不用它行吗?
答:首先在终端创建example5.c,将例程5代码输入。在终端编译、运行example5.o,得到如下结果:
图10.example5.c以及运行结果
分析代码可知,程序先建立一个管道,父进程首先创建子进程p1,再创建子进程p2,而两个子进程中执行操作为先给管道加锁,然后向管道写入字符串后解锁管道。等待五秒后,子进程会输出ppid信息。而在父进程中其会依次执行:首先等待子进程结束,然后尝试读取管道中的内容,如果读取成功则输出管道中的内容。
思考题:(1)管道通信是进程的一种通信方式。Linux 中两个进程可以通过管道来传递消息。进程利用它的方式为首先进程需要使用pipe()系统调用生成一个管道,之后需通信的进程首先给管道上锁保证自己独占,再使用write()向管道写入内容;另一个进程可通过read()获取管道内容。
(2)首先尝试将sleep时间缩短为1s:
图10-1.example5.c以及运行结果
发现有趣结果:虽然子进程P1比子进程P2先执行,但是P2子进程却更先结束。尽管如此,父进程仍先输出了p1的信息。推测由于两个子进程是互斥的,但是运行时间过短,因此可看成并发运行的,当我们每个程序只sleep1s的时候,就有可能存在p2先执行完的情况。但是由于父进程先创建p1再创建p2,因此p1、p2谁先占用管道并锁住它的先后次序与谁先运行结束没有必然联系,因此存在p1先输入管道被父进程读取,p2再输入管道被父进程读取,但是p2先运行结束的情形,因此父进程也就先输出p1的信息,再输出p2的信息。
再次尝试将sleep(5)语句移至解除管道占用之前,这就意味着子进程在写入管道后还会占用5s才会释放:
图10-2.example5.c运行结果
这次我们也看到有趣的结果:p2首先输出自己信息,然后首先占用了管道。我们可以看到父进程首先输出了管道中p2传送的信息。之后p1再传送信息。这样的原因是每个子进程传送完成后将额外占用管道五秒,由于管道加锁后的互斥性,这就导致了p2整个进程做完才会让p1进管道,也就在宏观上失去了并发性。
(3)加锁、解锁的作用是用来保证临界资源每次只有一个进程占用。如果不用它,前提是它不是临界资源,否则多个进程同时访问和改写临界资源,会导致临界资源的值不一致,导致严重错误。但是我尝试注释掉加锁、解锁的代码行,反复运行仍然是正确的,猜想现在计算机速度太快,代码比较简单,导致执行时间很快就完成了,错误没有发生。
此外,在实验完成后,尝试将p2进程的加锁、解锁操作注释掉,同时使p1在锁住管道后sleep10s再写入,然后再解锁管道。按照正常的代码运行,我们本应看到的是p1先写入,然后睡10s后,p2才能开始写入,然后父进程将先接收到p1的信息,再接收到p2的信息。但是实际运行的结果却是:在p1进程sleep的十秒之内,p2直接进入管道完成了写入,并且父进程首先获得了p2放入管道的信息并输出,显然p1在sleep时并没有锁住临界资源。
3.1.8 编程验证:实现父子进程通过管道进行通信。进一步编程,验证子进程结束,由父进程执行撤消进程的操作。测试父进程先于子进程结束时,系统如何处理“孤儿进程”的。
思考:对此作何感想,自己动手试一试?解释一下你的实现方法。
答: 首先参照上一题的例程,编写一个正常的管道通信程序:
图11.prog3.c运行结果
本程序创建一个子进程,然后在子进程中输出子进程与父进程的pid,然后发送信息给父进程,最后输出表示子进程退出;父进程通过wait()在等待子进程结束后撤销该进程,接收到信息后输出。我们发现运行结果确实为程序功能:3047创建3048子进程,之后正常退出。
现在稍微改动程序,删除父进程用于等待并撤销子进程的函数wait();同时为了展示父进程结束后子进程的状态,在信息传送后先不要让子进程exit,而是循环输出自己和父进程的pid,代码如下:
图12.prog4.c运行结果
当执行程序时,我们发现原先是父进程2990创建了子进程2991,但是当通信完成, 父进程2990退出时,子进程仍然在输出,而这时其父进程id变为了1651,也即是其被另一个进程init领养了。
3.1.9编写两个程序一个是服务者程序,一个是客户程序。执行两个进程之间通过消息机制通信。消息标识MSGKEY 可用常量定义,以便双方都可以利用。客户将自己的进程标识(pid)通过消息机制发送给服务者进程。服务者进程收到消息后,将自己的进程号和父进程号发送给客户,然后返回。客户收到后显示服务者的pid 和ppid,结束。以下例程6 基本实现以上功能。这部分内容涉及《实验指导》第五部分中“IPC系统调用”。先熟悉一下,再调试程序。
使用例程6的程序:
图12.ex6server.c/exclient6.c运行结果
运行可知服务器进程3769一直在后台运行,循环接受消息队列中的信息;每次运行服务端程序都创建一个进程,向消息队列发送信息。这里我们定义了一个消息结构为msgform,其中包含一个char数组的结构为msgtext,用int*指针将pid和msgtext连接,之后就可使用msgsnd发送消息,msgrcv接收消息并显示数据和对应的发送方pid号码。
思考:想一下服务者程序和客户程序的通信还有什么方法可以实现?
在计算机网络课程中学习了基于socket中TCP或UDP方式实现消息传送的方法,也可以实现服务端与客户端通信:以TCP为例,这是有连接的通信,需要定义ip地址并占用一个通信端口,在要通信时占用端口,通过listen进行监听,通过send和recv进行信息的首发,其思想与利用管道进行的通信十分相像,下面节选一段计网实验中编写的用于通讯的代码:
图13.使用TCP通信的代码节选(python)
3.1.10编程实现软中断信号通信。父进程设定软中断信号处理程序,向子进程发软中断信号。子进程收到信号后执行相应处理程序。思考:这就是软中断信号处理,有点儿明白了吧?讨论一下它与硬中断有什么区别?
将例程7写入,编译运行:
图14.example7.c及其运行结果
得到了如上运行结果:虽然父进程显示它已经等待了子进程4300的返回,但是我们看到子进程并没有完成打印“A signal from my parent is recieved”的功能。此外以自己的感官感知,也并没有发现父进程等待了10s.猜测使用kill函数时,不仅仅会让子进程执行func(),而且子进程本身的程序也不会被执行了。将子进程稍加修改:然后运行,发现结果不变,印证猜想。
思考:讨论一下它与硬中断有什么区别?
答:硬中断一般是由外设等的IO所要求的中断,处理器使用中断向量表的数据结构处理这些中断。软中断是处理器内部产生的。是由执行指令引起的。
3.1.12讨论:用信号量机制编写一个解决生产者—消费者问题的程序。
(1)首先分析要求,我们使用信号量机制解决问题,故引入<semaphore.h>,该头文件给我们提供了信号量结构sem_t,该结构原型为extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value)),第一个参数为信号量名称的指针,第二个参数表示进程间共享或线程间共享;最后一个参数是初值。我们学习的wait和signal函数这里也提供了,分别是sem_wait和sem_post,参数不在此赘述。
此外,由于生产者、消费者需要共享信号量的数值,而不同进程间只有创建时拷贝了一份资源,其他时候进程之间不会共享资源,因此我们这里不能选择多进程来实现,而是使用多线程。各线程之间可以共享信号量,因此可以利用信号量达成互斥和同步。
(2)数据结构和多线程编程的方针确定,我们借鉴教材上信号量机制章节的伪代码进行函数设计,伪代码如下:
图15.伪代码示意
我们每次都先判断是否有空间/是否满,避免死锁问题,然后判断是否临界资源正在被使用,若均正常则开始自己的生产/消费过程,结束后设置对应信号量即可。这里我使用queue的队列数据结构来表现缓冲区,将其大小设置为5,用进出队列表现生产/消费的过程,代码如下:
图16.pro_con.cpp代码示意
在CPP程序里我们定义了三个mutex,分别表示可用空间(初始化为5,代表缓冲区总大小为5)、已存货物数和临界量1,此外为了让线程得以结束,设置他们的最大循环次数之和为MAX=10000。在主函数中尝试使用2个线程,分别执行两个线程函数。对于这两个线程函数,在每次生产/消费的循环中,都打印生产/消费产品的id号。
编译、执行该程序,结果如下:
图16.pro_con.cpp代码运行结果示意
由运行结果我们可以看到确实是生产者先生产,消费者再消费,并且缓冲区的最大容量为5(生产者无法连续生产超过5个)。
但是程序运行结果中也有一些奇怪之处:首先,我们看到总共执行次数为4998+5003=10001次,而我们定义的最大执行此数之和应该是MAX=10000 。其次,我们可以看到除了最后消费者和生产者各执行2次外,其他情况都是生产者直接把缓冲区填满,然后再由消费者一直取到空,循环往复。在我们的设想中,消费者和生产者应该是随机存取的,只要符合消费者取走的一定是之前被生产过的这一条就可以了。
尝试修改max值,反复运行:
图17.修改MAX为100、15、10后运行结果示意
修改MAX值后多次执行,均发现了总运行次数超过MAX的情况。猜想是两个线程同时抢夺全局变量MAX的值并修改,但是进程是并发执行的,MAX又不是一个信号量,因此线程同步资源可能有时差,正是这个时差导致了执行总次数超过MAX值。为验证猜想,我们定义两个全局变量MAX1、MAX2,分别代表生产者和消费者执行的总次数,再次运行程序:
图17.修改MAX为两个均为10的计数器后运行结果示意
发现生产者和消费者线程的运行结果正常,均为10次。但是还有一个问题解决,就是为何总是生产者一直生产满,消费者才能进去消费,而且会一直消费空呢?尝试再设置一个生产者线程,并且保持MAX1=MAX2=100,也就是2个生产者一共生产100次,一个消费者消费100次;再尝试反过来:即设置两个线程的消费者,一个线程的生产者,并且保持MAX1=MAX2=100。
图18.修改生产者/消费者数量后运行结果
发现只有两个生产者一个消费者时会变成生产一个取走一个的情况,而两个消费者一个生产者时情况没有变化。此外,当消费者和生产者的数量均大于1时,还出现了MAX失效的情况,即生产者和消费者一直在对缓冲区进行读写操作。在询问老师后了解到自己程序的两个缺陷:(1)我使用了while(MAX--)来替代While(1),想规定消费者和生产者总的运行次数,在单线程中这是没有问题的,因为MAX必然是从正数减到0后终止;但是当消费者和生产者大于一个之后,多个线程之间的MAX无法同步,会导致MAX被减到0之后,一个进程又将其减到了负数,之后的while(MAX--)就失去了作用。(2)在我的生产者/消费者程序中,生产者和消费者并不做实际的生产和消费动作,导致他们的运行都非常快,推测这就是为什么出现了一方总是把缓冲区填满之后才被阻塞,而另一方也总是把缓冲区取空才被阻塞。
根据上面的猜测,我们对代码进行修改:
图19.修改后的生产者/消费者函数
我们首先将while(MAX--)更换为两条语句,确保当MAX>=0时才会执行消费者/生产者程序;此外我们对实际生产和消费过程进行模拟,让生产者每次sleep(5),来模拟生产产品的过程,而消费者需要sleep(3)来模拟消费过程。修改完毕后编译执行,首先是单消费者单生产者的情况,然后我们再加入缓冲区buffer,用来检测每次缓冲区里还有几个货物:
图20.修改后运行结果
我们可以看到,这时是生产者生产一个,消费者就消费一个。这符合代码设计,因为生产者需要5s生产一个产品,但消费者只需3s就可以消费,因此正常情况本应如此。之后我们增加生产者的数量:
图21.2个生产者和1个消费者运行结果
根据运行结果我们可以看到,这样做和单生产者/消费者没有区别。因此我们尝试再加入2个生产者:
图22.4个生产者和1个消费者运行结果
我们可以明显看出:首先生产者生产了3件产品,然后第一件产品被消费了,紧接着第四个生产者消费了第四件产品,然后消费者消费了2件,生产者又生产了4件……这种带随机性的运行结果正是正常来说会发生的,程序运行成功!
从一系列设计和修改中,我认识到了只有真实模拟生产和消费过程,才能使运行结果真实。
此外,在实验验收中老师鼓励我再尝试另一种思路:使用多进程实现生产者-消费者问题。查阅网络资料得知 sem_init()中第二个参数为1便可以让该临界资源在不同进程之间传递。因此将程序稍加修改,分成2个cpp文件,一个为生产者进程,另一个为消费者进程,便可实现类似功能:
图23修改后运行结果
图24修改后两cpp文件
其与之前的主要区别在于定义了结构体,用结构体指针来连接共享内存区域,在结构体内定义空、满、互斥等临界量。
3.2研究并讨论
1. 讨论Linux 系统进程运行的机制和特点,系统通过什么来管理进程?
答:进程运行的机制:通过在就绪、获得cpu、阻塞、挂起等不同的状态之间切换实现运行。系统通过PCB管理进程。
2. C 语言中是如何使用Linux 提供的功能的?用程序及运行结果举例说明。
在C语言中,程序通过使用系统调用函数,使用Linux内核态向外部提供的接口来使用其功能。例如我们在之前的题目中参考例程3 编程,使用fork( )和exec( )等系统调用创建三个子进程。这里使用的fork和exec都是系统调用函数,实际上用户是不能直接自行创建子进程的,必须通过系统调用的方式
3. 什么是进程?如何产生的?举例说明。
答:进程是一个执行中程序的实例,是CPU的抽象。进程的产生是通过PCB进行的,假设我们要创建一个进程,该进程将通过管道与父进程通信。首先需要其父进程申请一个空白的PCB,并且写入控制信息和管理信息。然后为进程分配运行时需要的资源,也就是父进程所拥有的变量、通信用的管道等。然后就可以初始化PCB,给他标识符和父进程标识符、处理机信息等。之后该子进程若已经获得所有所需资源,就会转入就绪状态,等待获取CPU的时刻。
- 进程控制如何实现的?举例说明。
进程控制是通过OS内核中的原语来实现的。举例而言,假如父进程要结束一个子进程,这也属于进程控制的一个操作,那么父进程调用kill()函数,OS调用终止原语,首先根据标识符寻找到这个PCB,查看其状态,若为执行则立即终止,再寻找所有孙进程并终止,最后将资源归还给父进程,移除被结束进程的PCB。
- 进程通信方式各有什么特点?用程序及运行结果举例说明。
进程通信可分为低级通信和高级通信两种,其中低级方式的特点是效率较低、通信对用户不透明,例如信号量系统:
以实验报告最后一题举例:编写程序解决生产者、消费者问题,我们使用信号量机制时,每次生产者或消费者都只能在缓冲区放入/取出一个商品,效率较低;从外从代码我们也可以看出,编写程序时我们必须自己设置临界资源mutex,自己实现数据传送和临界资源的增减。
图19 信息量机制
而高级通信机制可以分四类:共享存储器、管道通信、消息传递系统以及客户-服务器。其中管道系统我们已经在实验中接触过了,通过下图的代码(来源为实验例程),我们可以看到管道通信具有的优势是:用户创建管道后直接加锁并进行写入/读出操作,而不用在意锁如何实现互斥;传递的信息不是单个的,而可以是文本、文件,效率更高。
(1)管道系统
图20管道系统 程序与运行结果
(2)消息传递系统
我们在本次的实验中也学习了通过消息队列机制实现客户端、服务器之间的通信(来源为实验例程),其代码和运行结果如下图所示:而消息队列属于一种共享数据结构的通信方式。这里我们定义的数据结构包含长度为256个字节的char数组mtext,通过直接使用msgsnd和msgrcv进行服务器和客户之间的消息传送。
图21消息队列实现客户-服务器通信 程序与运行结果
(3)客户-服务器系统
图21socket套接字实现客户-服务器通信 程序与运行结果
此外我也在计算机网络的课程实验中实现了通过socket套接字进行客户端、服务器之间的通信。分析上图代码与运行结果,我们也可以看出, socket除了需要设置阻塞端口实施监听之外,程序员无需关系如何实现客户与服务器之间的同步问题,并且发送信息也有较高效率。
(4)共享存储器系统
实际上信号量机制也属于共享存储器中的共享数据结构的通信方式,但这是低级通信。基于共享存储区的通信方式则属于高级通信。通常共享内存段由一个进程创建,接下来的读写操作就可以由多个进程参加,进行信息传递。我们尝试使用shm编写一个简单的程序:
图22.使用共享内存方法编写的发送方(左)与接收方(右)代码
在上图的程序中,我们调用shmget使发送方创建一个shmid,也就是共享内存id,并且指定其key值为1000(通过相同的key值接收方就可以实现打开相同的共享内存的操作了)、大小为256字节。然后我们调用shmat函数,将共享内存段附加到进程地址的空间上,建立一个映射msg,然后读取终端的输入放入msg中即可,这就相当于放入了共享内存。发送完后将共享内存从当前进程的映射关系msg中脱离;接收方也利用同样的思路,在将共享内存段附加到进程地址的空间的空间、建立映射后,就可以直接读取其中内容了!这样就通过一个共享的存储区实现了进程通信:
图22.发送方(左)与接收方(右)进程的执行过程
通过运行结果,我们可以看出成功进行了发送和接收,并在发送end后结束。
在编写完上述代码后,我们对比这几种通信方式,可以发现共享存储区的通信有无需建立链接的特点。像我们之前的socket通信、管道通信、消息传递的消息队列等等,都需要有明确的发送和接收者,两者是连接起来的。但是共享存储区的通信里,二者通信的实现是基于它们都知道同一块存储区的key值,从而可以访问该资源。那么这二者其实没有直接连接关系,也没有固定谁是接收方、谁是发送方,而是都具有存取权限。
- 管道通信如何实现?该通信方式可以用在何处?
管道通信的实现:管道被设计为环形的数据结构,本质上是一个系统的缓冲区,一端连接一个进程的输出,另一端连接一个进程的输入。当管道里有信息时,要放入信息的进程必须等待,直到管道中的信息被取走;当管道为空时,要去信息的进程必须等待,直到管道中有信息。当两个进程都结束后,管道也会消失。
管道通信应用场景主要是半双工通信,主要是在有血缘关系的进程间(父子进程或兄弟进程)。
7. 什么是软中断?软中断信号通信如何实现?
软中断是操作系统响应中断指令要求而产生的中断。软中断信号通信是通过进程间调用kill函数实现的。Kill函数会向指定的进程发送一个信号,而signal函数一般定义接收到一个信号后调用什么处理函数进行处理。因此当一个进程使用kill向另一个进程发送信号后,接收进程可以停止自己的程序转而执行中断程序,是为软中断。
四、收获与体会
说明:撰写完成该实验后的收获和体会。
本次实验包含了较多的内容,从最初的认识进程的pid、ppid开始,一直到使用各种方式实现进程间的通信IPC,是一个逐渐深入的过程。到现在为止,我不仅对于进程有了比较具象的概念,而且也在实验和研讨中提前学习到了线程的概念,以及它与进程最主要的区别:共享资源。
在最开始,我们先从父子进程的创建做起,我发现了fork()这个有意思的函数,可以让我们明明在一个程序中却运行了两个甚至多个进程,每个进程还能执行不同的功能。而这个实验也让我体会到了课本上讲到的进程和程序是不同的,进程是资源分配和调度的一个独立单位;之后,我们在使进程间保持通信方面使用了很多的方法,包括信号量、记录型信号量、共享内存、管道通信等等,而这些方法各有其优缺点,也是之前的计算机科学家们探索IPC通信历史的微缩版。在最后的自由编写信号量机制程序解决生产者消费者问题中,我也经过了不少找bug和面对运行结果一头雾水的阶段,索性最后对他们有所了解。
要说体会,大概是:要把各种IPC通信方式试一遍很简单,要深入研究其中一种(信号量机制)是怎么解决问题的就很麻烦了,但是了解其原理确实是有趣的。
标签:IPC,创建,程序,通信,间通信,管道,进程,运行 From: https://www.cnblogs.com/czy-blogs/p/16797062.html