【作业】操作系统实验一:进程和线程

news2025/2/22 13:29:10

文章目录

  • 实验内容
    • 一、进程的创建
      • 1、编辑源程序
      • 2、编辑结果
      • 3、编译和运行程序
      • 4、解释运行结果
    • 二、进程共享
      • 1、运行
      • 2、解释运行结果
    • 三、进程终止
      • 1、运行
      • 2、解释运行结果
    • 四、进程同步
      • 1、运行
      • 2、解释运行结果
    • 五、Linux中子进程映像的重新装入
      • 1、运行
      • 2、解释运行结果
    • 六、线程
      • 1、运行
      • 2、解释运行结果
    • 七、共享资源的互斥访问
      • 1、运行
      • 2、解释运行结果

实验内容

一、进程的创建

编写一段源程序,使用系统调用fork()创建子进程,当此程序运行时,在系统中有父进程和子进程在并发执行。观察屏幕上的显示结果,并分析原因(源代码:forkpid.c)。

1、编辑源程序

在这里插入图片描述

2、编辑结果

在这里插入图片描述

3、编译和运行程序

在这里插入图片描述

4、解释运行结果

假设命令./forkpid创建的进程称为A,则A中的fork()调用会创建一个子进程,称为B

fork()函数只在父进程A中成功被调用一次,但是在A和B两个进程中都会有返回值。成功创建子进程后,在父进程A中,fork的返回值为创建的子进程的pid;在子进程中,fork的返回值为0。

因此,if(p1==0)后面的代码块会在子进程中被执行,而else后面的代码块会在父进程中被执行。由上述运行结果可知,进程的创建关系如下。

pid: 389733 --> 390075(A) --> 390076(B)

那么还有一个问题,上述运行结果中的My parent is 1,岂不是说B进程的父进程pid是1?这不是矛盾了吗?根据我所查阅的资料,这是因为父进程A比子进程B先结束,B中查找自己的父进程时,父进程A已经不在了,会使用pid为1的进程代替。在Linux中,pid号为1的进程是所有进程的祖先进程

参考

  1. https://blog.csdn.net/lein_wang/article/details/81946108 - 为什么父进程id是1
  2. https://www.cnblogs.com/alantu2018/p/8526970.html - linux的 0号进程 和 1 号进程

二、进程共享

父进程创建子进程后,父子进程各自分支中的程序各自私有,其余部分,包括创建前和分支结束后的程序段,均为父子进程共享。(源代码:forkshare_1.c)

1、运行

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

2、解释运行结果

根据运行结果我们很容易看出,进程调度顺序和上一节中一样,是先调度父进程再调度字进程。其中前三个字母xay是父进程的输出,后面三个字母xby则是子进程的输出。

可令人困惑不解的是,子进程不也应该从fork()返回的地方开始运行吗?那为什么'x'会被输出两次?我一时间有些蒙圈。

在查找资料后,我尝试了另一个示例,其中仅仅是将putchar('x')换成了printf("x\n"),而最主要的区别就是多了一个\n。下面是代码及其运行结果。

在这里插入图片描述
在这里插入图片描述

子进程会从fork()返回的地方开始执行,没有错。问题出在stdout缓冲区的机制上,在输出xayxby的示例中,运行putchar('x')语句并没有直接将x写到屏幕上,而仅仅是将x放到了缓冲里。随后运行fork()创建子进程,会将父进程的stdout缓冲区也复制一份,而复制的缓冲区中就包含了刚刚放入的x。这便是两个x从何而来。

而如果打印的内容中包含了\n,就会马上将内容写到屏幕上并刷新缓冲区,这便是为什么第二个示例中x只有一个。

参考:

  1. https://blog.csdn.net/koches/article/details/7787468 - linux中fork–子进程是从哪里开始运行

三、进程终止

如果子进程在其分支结束处使用了进程终止exit()系统调用而终止执行,则不会再执行分支结束后的程序段。(源代码:forkshare_2.c)

1、运行

在这里插入图片描述
在这里插入图片描述

2、解释运行结果

子进程因为在分支中输出b后就exit()结束进程了,因此子进程不会输出后面的y,其它与上一节情况相同,不作多解释。


四、进程同步

当父进程有许多任务要做时,往往会针对每一个任务创建一个子进程去完成,然后再等待每一个子进程的终止。其同步关系是父进程等待子进程 (源代码:wait.c)。

实现的方法是:1)子进程终止时执行exit()向父进程发终止信号,2)父进程使用wait()等待子进程的终止。

1、运行

在这里插入图片描述
在这里插入图片描述

2、解释运行结果

前面三个程序都是父进程输出的 va在前而子进程输出的b在后,这次ba换子进程的输出在前了。

父进程中掉用wait(0)函数会立即阻塞自己,并等待子进程的退出。注意阻塞和空循环等待是有区别的,进程阻塞自己后会交出cpu的控制权。

不过,wait(0)exit(0)中都有一个0,这个0是什么意思呢?其实这两个零并不是同一个东西,wait的函数头原型是int wait(int *status),参数是一个int类型的指针,wait(0)其实相当于wait(NULL),这个参数为NULL表示我们不关心子进程是如何退出的,只要退出就行。而如果你写成wait(1),那么编译时候就会报错了,因为类型不匹配。

exit(0)的参数是退出码,你完全可以使用任性地使用exit(123)退出子进程,并在父进程中仍然以wait(0)接收子进程的终止信号。

参考:

  1. https://zhuanlan.zhihu.com/p/549981187 - 入门篇:进程等待函数wait详解
  2. https://blog.csdn.net/qustdjx/article/details/7704323 - wait()以及wait(&status)\ waitpid()
  3. https://zhuanlan.zhihu.com/p/647776823 - Linux Shell 中的各种退出码
  4. https://www.cnblogs.com/shikamaru/p/5359731.html - exit(0)、exit(1)、和return

五、Linux中子进程映像的重新装入

创建一个子进程,并给它加载程序,其功能是显示“I am a child”。设被加载的程序路径名为./child。分析:由于子进程需要加载的程序比较简单,不带参数,所以可以使用execl()实现加载。

1、运行

./child_parent.c:

在这里插入图片描述

./child.c

在这里插入图片描述

在这里插入图片描述

2、解释运行结果

我在./child_parent.c文件的execl()语句后加了一个printf()语句,这样也行能够更好地体现execl的运行机制。

上述源代码中共有两个printf语句,但最终只有./child.c中的printf语句产生了输出。这是因为,execl加载进程时并不会新建一个进程,而是使用传入路径中的程序覆盖当前进程,并从新进程的main函数开始执行。覆盖后,使用fork()创建的那个程序,当然也包括其中的printf("I am child A\n"),已经不存在了。

使用execl,子进程可以更好地干自己的活,而不只是作为父进程的拷贝。

参考

  1. https://blog.csdn.net/bao_bei/article/details/48287945 - Linux下execl函数学习_linux的execl函数

六、线程

Linux 系统下的多线程遵循 POSIX 线程接口,称为 pthread。编写 Linux 下的多线程程式,需要使用头文档 pthread.h,连接时需要使用库 libpthread。顺便说一下,Linux 下pthread 的实现是通过系统调用 clone()来实现的。clone()是 Linux 所特有的系统调用,他的使用方式类似 fork,关于 clone()的周详情况,有兴趣的读者能够去查看有关文档说明。下面我们展示一个最简单的多线程程序 pthread_create.c。

1、运行

在这里插入图片描述

在这里插入图片描述

2、解释运行结果

// 线程创建函数的函数头
int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*strat_routine)(void *), void *arg)

返回值(int):创建成功时返回0,失败时返回错误码。

参数:

  • thread:前面先创建了空的线程标识符pthread_t id1,这里传入&id1告诉函数要将新建线程的标识符放到哪里。可以对比scanf("%d", &x)的用法。
  • attr:线程的属性,传入NULL表示默认。
  • start_routine:线程要执行的函数的指针,上述示例代码中实参前的(void*)大概是强制类型转换为指针的意思。函数头中那一串void *(*strat_routine)(void *)你可能看着有点晕,感觉自己的C语言功底不太够用了,抱歉我也是。
  • arg:线程要执行的函数所需要的参数,传入NULL意思是不需要参数。

我在main函数return前添加了一个printf语句。

pthread_join的作用是让主线程进入阻塞态,等待子线程运行结束后,主线程再接着运行。否则,从子线程创建时开始,主线程与两个子线程就开始并发执行,当主线程很快运行结束并return后,整个进程也会结束,即子线程跟着结束,都来不及在屏幕上打印信息。

可以看到输出中交替打印两个字符串各四次后,最后打印出来的是This is main thread.

参考

  1. https://blog.csdn.net/sevens_0804/article/details/102823184 - Linux多线程操作pthread_t
  2. https://www.jb51.net/article/176510.htm - 简单了解C语言中主线程退出对子线程的影响

七、共享资源的互斥访问

创建两个线程来实现对一个数的递加 pthread_example.c

1、运行

例程1

代码太长,截图不便,我直接贴文本吧。

#include<pthread.h>
#include<stdlib.h>
#include<stdio.h>
#include<sys/time.h>
#include<string.h>
#include<unistd.h>
#define MAX 10

pthread_t thread[2];
pthread_mutex_t mut;
int number=0;
int i;

void thread1(){
	printf("thread1: I'm thread 1\n");
	for(i=0;i<MAX;i++){
		printf("thread1: number=%d\n",number);
		pthread_mutex_lock(&mut);
		number++;
		pthread_mutex_unlock(&mut);
		sleep(2);
	}
	pthread_exit(NULL);
}
void thread2(){
	printf("thread2: I'm thread 2\n");
	for(i=0;i<MAX;i++){
		printf("thread2: number=%d\n",number);
		pthread_mutex_lock(&mut);
		number++;
		pthread_mutex_unlock(&mut);
		sleep(3);
	}
	pthread_exit(NULL);
}
void thread_create(){
	int temp;
	memset(&thread,0,sizeof(thread));
	
	if( (temp=pthread_create(&thread[0],NULL,(void*)thread1,NULL)) != 0){
		printf("create thread1 failed!\n");
	}else{
		printf("create thread1 success!\n");
	}

	if( (temp=pthread_create(&thread[1],NULL,(void*)thread2,NULL)) != 0){
		printf("create thread2 failed!\n");
	}else{
		printf("create thread2 success!\n");
	}
}
void thread_wait(){
	if(thread[0]!=0){
		pthread_join(thread[0],NULL);
		printf("thread1 end!\n");
	}
	if(thread[1]!=0){
		pthread_join(thread[1],NULL);
		printf("thread2 end!\n");
	}						        
}
int main(){
	pthread_mutex_init(&mut,NULL);
	printf("I am main, I am creating thread!\n");
	thread_create();
	printf("I am main, I am waiting thread end!\n");
	thread_wait();

	return 0;
}

在这里插入图片描述

2、解释运行结果

进程是资源分配的基本单位,线程是调度的基本单位。上述代码中,int numberint i都是被两个线程所共享的变量。利用mutex可以实现对共享资源的互斥访问,这个比较容易理解,但是上面的运行结果中,可能有些令人疑惑的地方。

1、为什么会打印两次"number=0",这正常吗?

是正常的,比如在如下图所示的执行顺序(但不唯一)中,就会出现打印两次"number=0"然后打印"number=2"的情况。注意sleep()会直接让当前进程阻塞,因此下图中在sleep处都是拐点。

在这里插入图片描述

2、为什么thread1连续输出了两个数"number=6"和"number=7"?

对进程的同步与互斥有些模糊时,容易产生这样的疑问。运行结果中,大部分输出都是thread1和thread2交替的,只有6和7这里是同一个线程连续输出两次,以至于觉得本应该交替输出、而连续输出是异常的。

其实不是,上述thread1的代码中是sleep(2),而thread2的代码中是sleep(3)。如果你将thread1中的sleep(2)修改成sleep(1),可以观察到大部分时候都是thread1在连续输出。总之,代码中只是使用mutex实现了对共享资源的互斥访问而已,并没有实现同步。

3、验证lock与unlock的作用。

到这里,我只知道mutex可以实现对共享资源(全局变量number)的互斥访问,程序也确实看起来正常运行。不过,它只循环了10次呢。而且不知道你有没有发现一个问题,另一个全局变量i,不也同样是thread1和thread2两个线程的共享资源吗?

让我们将总循环次数提到10万吧:#define MAX=100000,同时将阻塞时间缩到sleep(0.001)让它运行得快一点。然后重新编译并运行一下我们的代码:

例程2

在这里插入图片描述

看!最后结果是number=100018,而并不是我们所设置的100000,那么很可能就是由于对于共享变量 i 的访问发生了冲突。我将例程2运行了5次,记录每次最后的number值如下:

100018 100017 100011 100024 100009

为了验证确实是变量 i 导致的问题,而不是其它的什么原因——比如上帝在你的计算机里边掷骰子,我们不妨给 i 也加上互斥。

例程3

我新增了一个新的用于互斥的变量mut_i,并在两个线程函数的循环中都改用lockunlock来保护记录循环迭代的i++语句。

在这里插入图片描述
在这里插入图片描述

我同样将这个修改后的例程3运行了5次,其中有四次是正确的结果number=100000。可很遗憾,我也不知道在第一次运行中为什么会出现连续两个number=99999,不过我觉得这不一定是资源互斥中所导致的问题。

总之,凡有赋值的操作在多线程环境都要加锁,不论是上述的number++,还是i++。因为,它们都不是原子操作,从机器指令的层面来看,一个高级语言中的i++包含多个步骤,而一条语句还没执行完,可能就已经发生中断,转而去执行另一个线程了。

4、在例程2和例程3的执行结果中,每次最终的number值都是偏大,为什么不会偏小呢?

例程4

下面我解除了对于变量 number 的互斥保护,而保持对 i 的互斥保护。

在这里插入图片描述

在这里插入图片描述

可以看到我连续运行五次结果中,number 每次都是比10万要偏小。这个具体的细节,其实回忆一下以前数据库并发控制中丢失修改导致的数据不一致问题,就明白了,情况如下图所示。而当 i 偏小的时候,i 的递增次数就小于实际迭代次数,于是导致了 number 的结果偏大。

在这里插入图片描述

参考

  1. https://blog.csdn.net/JMW1407/article/details/108318960 - int i =1 是原子操作吗?i++是原子操作吗?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1225461.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

操作系统:进程(一)

进程的基本概念 一般的解释是&#xff1a;进程是程序的一个执行实例&#xff0c;是正在执行的程序。我们写的程序编译后是一段二进制的文件。启动的时候加载到系统里面执行&#xff0c;就是以进程的形式执行。也就是说&#xff0c;我们编译后的可执行程序是一个静态的概念&…

035、目标检测-物体和数据集

之——物体检测和数据集 目录 之——物体检测和数据集 杂谈 正文 1.目标检测 2.目标检测数据集 3.目标检测和边界框 4.目标检测数据集示例 杂谈 目标检测是计算机视觉中应用最为广泛的&#xff0c;之前所研究的图片分类等都需要基于目标检测完成。 在图像分类任务中&am…

python图

有向图&#xff1a;图中的每条边都有方向的图叫有向图。此时&#xff0c;边的两个顶点有次序关系&#xff0c;有向边 < u,v>成为从顶点u到顶点v的一条弧&#xff0c;u成为弧尾&#xff08;始点&#xff09;&#xff0c;v成为弧头&#xff08;终点&#xff09;&#xff0c…

rabbitmq默认交换机锁绑定的routingkey-待研究

例如这个是我的一个消息队列&#xff0c;它默认绑定的交换机是 什么类型呢? 看到这个图&#xff0c;感觉应该是一个默认的交换机&#xff0c;因为是default exchange 于是来到交换机来看看其他默认的交换机&#xff1a; 这里可以看到默认的交换机是direct&#xff08;应该没…

1.0 Zookeeper 教程

分类 Zookeeper 教程 ZooKeeper 是 Apache 软件基金会的一个软件项目&#xff0c;它为大型分布式计算提供开源的分布式配置服务、同步服务和命名注册。 ZooKeeper 的架构通过冗余服务实现高可用性。 Zookeeper 的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来&…

Android resource/drawable转换成Uri,Kotlin

Android resource/drawable转换成Uri&#xff0c;Kotlin private fun convertResource2Uri(resId: Int): Uri {return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE "://" resources.getResourcePackageName(resId) / resources.getResourceTypeName(resI…

、如何在企业签名、超级签名、tf签名之间做选择

企业签名 (Enterprise Signing): 用途&#xff1a; 适用于企业内部发布应用&#xff0c;不需要经过App Store审核&#xff0c;可以通过企业内部渠道直接分发给员工或内部用户。限制&#xff1a; 仅限于企业内部使用&#xff0c;无法在App Store上发布或向外部用户分发。 超级签…

python趣味编程-5分钟实现一个打字速度测试(含源码、步骤讲解)

Python速度打字测试是用 Python 编程语言编写的,速度打字测试 Python项目理念,我们将构建一个令人兴奋的项目,通过它您可以 检查 甚至 提高 您的打字速度。 为了创建图形用户界面(GUI),我们将使用 用于处理图形的pygame库。 Python 打字速度测试有利于学生或初学者提高…

生命科学领域 - FAIR原则和如果使数据FAIR化

2016年&#xff0c;《Scientific Data》发表了《科学数据管理和监督的FAIR指导原则》&#xff08;FAIR Guiding Principles for scientific data management and stewardship&#xff09;。文章旨在提供指导方针&#xff0c;以提高数字资产的可发现性、可访问性、互操作性和重用…

微信小程序会议OA首页-开发说明创建项目关于flex布局关于尺寸单位(rpx)关于WXS轮播图会议信息

目录 1. 创建项目 2. 关于flex布局 3. 关于尺寸单位&#xff08;rpx&#xff09; 4. 关于WXS 4. 轮播图 5. 会议信息 1. 创建项目 基于微信原生开发工具&#xff0c;稳定版 Stable Build (1.06.22010310) 创建项目前&#xff0c;请确定有小程序测试账号 使用向导创建一个…

ClickHouse建表优化

1. 数据类型 1.1 时间字段的类型 建表时能用数值型或日期时间型表示的字段就不要用字符串&#xff0c;全String类型在以Hive为中心的数仓建设中常见&#xff0c;但ClickHouse环境不应受此影响。 虽然ClickHouse底层将DateTime存储为时间戳Long类型&#xff0c;但不建议存储Long…

通过bat脚本控制Oracle服务启动停止

1、将Oracle服务全部设置为手动启动 初始安装Oracle之后服务启动状态&#xff1a; 2、服务功能介绍 3、构建服务启动/停止bat脚本 注意&#xff1a;编码选择ANSI(如果编码不是ANSI运行脚本会显示乱码) echo off :main cls echo 当前Oracle服务状态&#xff1a; for /f &quo…

Java中如何通过路径表达式找值:XPath和JsonPath以及SpEL详解及对比

大家好&#xff0c;我是G探险者。 我们编程时&#xff0c;在前后端数据交互和传输过程中&#xff0c;往往需要对报文中的某个字段或者某个标签的值进行解析读取&#xff0c;报文通常是以json或者xml作为数据交换格式&#xff0c;而json和xml这两种格式的报文结构都是具备一定的…

关于Android音效播放,【备忘】

主要还是希望开箱即用。所以才有了这篇&#xff0c;也是备忘。 以下代码适合Android5.0版本以后 private SoundPool soundPool;//特效播放private Map<String,Integer> soundPoolMap;// Builder buildernew SoundPool.Builder();builder.setMaxStreams(4);///最大…

springboot引入第三方jar包放到项目目录中,添加web.xml

参考博客&#xff1a;https://www.cnblogs.com/mask-xiexie/p/16086612.html https://zhuanlan.zhihu.com/p/587605618 1、在resources目录下新建lib文件夹&#xff0c;将jar包放到lib文件夹中 2、修改pom.xml文件 <dependency><groupId>com.lanren312</grou…

(Matalb回归预测)PSO-BP粒子群算法优化BP神经网络的多维回归预测

目录 一、程序及算法内容介绍&#xff1a; 基本内容&#xff1a; 亮点与优势&#xff1a; 二、实际运行效果&#xff1a; 三、部分程序&#xff1a; 四、完整程序数据说明文档下载&#xff1a; 一、程序及算法内容介绍&#xff1a; 基本内容&#xff1a; 本代码基于Matalb…

3D全景视角,足不出户感知真实场景的魅力

近年来&#xff0c;随着科技的快速发展&#xff0c;普通的平面静态视角已经无法满足我们了&#xff0c;不管是视角框架的限制还是片面的环境展示&#xff0c;都不足以让我们深入了解场景环境。随着VR全景技术的日益成熟&#xff0c;3D全景技术的出现为我们提供了全新的视觉体验…

Thrift协议详解

前言特点高效性的体现可拓展性的体现 应用场景示例拓展其他常用协议接口描述语言&#xff08;IDL&#xff09;TBinaryProtocolTCompactProtocolTDebugProtocolTDenseProtocolTJSONProtocol 前言 Thrift协议是一种接口描述语言和二进制通讯协议&#xff0c;它被用来定义和创建跨…

基于传统Session的登录

前言&#xff1a; 本人的一些简历上要回答的点。所以再此整理。 亮点&#xff1a; 使用Filter过滤器进行未登录状态自动跳转到登录页面的拦截&#xff0c;实现统一的权限管理。 1 登陆功能 1.1实体类和结果类 前端页面 约定 res.data.code为1时是登录成功。 数据库的empl…

wangEditor富文本编辑器使用

一、官网 开源 Web 富文本编辑器&#xff0c;开箱即用&#xff0c;配置简单 二、下载安装 npm install --save wangeditor/editor-for-vue 三、在vue中使用 3.1、抽离组件editor.vue 在工程的components目录下新建组件editor <template><div><Toolbar:edi…