操作系统原理 | 实验2-进程控制(整理实验报告及知识点)

从今天开始,记录本科一些课程的学习历程。本文记录的是操作系统原理实验课的第二个实验,实验源代码及知识点讲解大多来源于网上,文末将附相关链接。#自存# #侵删#

目录

  • 实验内容
  • 实验知识点
    • 进程 & 并发性 & 进程标识符
    • fork()
    • exce系列
    • waitpid()
    • exit()
  • 实验源代码
    • // fork1.c
    • // fork2.c
  • 实验结果与分析
    • fork1.c
    • fork2.c

实验内容

  • 写一段程序,使用系统调用fork()来创建两个子进程,并由父进程重复显示字符某字符串和自己的标识数,而子进程则重复显示某字符串和自己的标识数。
  • 编写一段程序,使用系统调用fork()来创建一个子进程。子进程通过系统调用exec()更换自己的执行代码,显示新的代码后,调用exit()结束。而父进程则调用waitpid()等待子进程结束,并在子进程结束后显示子进程的标识符,然后正常结束。

实验知识点

进程 & 并发性 & 进程标识符

对于进程的定义有多种,其中较经典的定义有:

  • 进程是程序的一次执行;

  • 进程是一个程序及其数据在处理机上顺序执行所发生的活动;

  • 进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。

并发性是进程的一个重要特征,也是OS的重要特征。引入进程的目的也正是为了使其进程实体能和其他进程实体并发执行。

程序(没有建立PCB)是不能参与并发执行的。

并发的实质是一个处理器在几个进程之间的多路复用 (多路复用比较出名的有时分复用,根据时间片调用不用的进程),是对有限的物理资源强制行使多用户共享,消除计算机部件之间的互等现象,以提高系统资源利用率。

宏观上,并发性反映一个时间段中几个进程都在同一处理器上处于运行还未运行结束的状态;微观上,任一时刻仅有一个进程在处理器上运行。

创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。

进程标识符是进程控制块(PCB)中的信息之一,用于唯一地表示一个进程。每一个进程都有一个唯一的数字标识符(即内部标识符),它通常是一个进程的序号,是由OS设置的。

进程的内部标识符可以通过getpid()函数获得,还有一个记录父进程pid的变量,可以通过getppid()函数获得变量的值。

fork()

#include

特性总结:一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值(包括代码、数据和分配给进程的资源)都复制到新的新进程中。相当于克隆了一个自己,两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。

fork只拷贝下一个要执行的代码到新的进程,而不是从“include#…”开始拷贝的。

它仅仅被调用一次,却能够返回两次(父进程和子进程分别返回),并且它可能有三种不同的返回值:在父进程中,fork返回新创建子进程的进程ID;在子进程中,fork返回0;如果出现错误,fork返回一个负值。通过fork返回的值可以判断当前进程是子进程还是父进程。

exce系列

#include

exce系统调用新程序覆盖调用它的进程的地址空间。exce把一个新的程序装入调用进程的内存空间,来改变调用进程的执行代码。此时,系统把代码段替换成新程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段和堆栈段,唯一留下的只有进程号。对系统而言,还是同一个进程,只不过运行另一个可执行程序。

fork()和exce()组合是典型的Linux新进程产生模式,通常先用fork()创建新进程,然后新进程通过调用exce()系列执行自己的任务。

exec系列有:excel、excelp、excele、execv、execvp、execve。其中l表示长格式,v代表利用argv传参,e代表从envp传递环境变量,p代表从PATH指定路径搜索文件。以上六个函数的共同特点是,运行成功无返回(因为程序已被置换),运行失败返回-1,失败原因存于errno中。execve是系统调用函数,其他五个函数都是在用户空间中实现的,实际最终也是调用execve实现最终功能。

waitpid()

#include
#include

这个函数会暂时停止目前进程的执行,直到有信号来到或子进程结束。waidpid()的返回值:如果执行成功,则返回子进程识别码,如果错误发生,则返回-1。失败原因存于errno中。

exit()

进程有生命周期,可通过给进程发送信号强行终止运行的进程,也可当完成任务后进程执行函数自动退出而消亡。无论在程序中什么位置,只要执行到exit系统调用,进程就会停止剩下的所有操作,清除包括PCB在内的各种数据结构,并终止本进程的运行。exit(status)的参数用来传递进程结束时的状态。0表示没意外的正常结束,其他的数值表示出现了错误,进程非正常结束。

调用exit()之前会检查文件的打开情况,把文件缓冲区的内容写回文件。如果要保证数据的完整性,要用exit()而不是_exit()。

实验源代码

// fork1.c

#include 
#include 
#include   // sys是系统路径。这个头文件包含了Unix/Linux系统的基本系统数据类型,含有size_t,time_t,pid_t等类型。
#include   // 这是一个头文件封装了类UNIX系统下的很多固定名称的system_call系统调用。这个函数是依赖于编译器和操作系统的。
int main()
{                              
	pid_t pid;  //pid表示fork函数返回的值
	int i, j, k;
	j = 100;
	k = 100;
	
	for(i = 0; i < 2; i++)
	{
		pid = fork();
		if(pid < 0 || pid == 0)
		{
			break;  // 如果是子进程或创建进程失败,则退出。
		}  
	}
		
	if(pid == -1)
	{
		perror("fail to fork!\n");  // 把一个描述性错误消息输出到标准错误 stderr。首先输出字符串 str,后跟一个冒号,然后是一个空格。
		exit(1);  // 退出整个程序,终止进程。返回0代表程序正常退出,返回1等其他数字通常代表异常终止。
	}
	else if(pid == 0)
	{
		while(j)
		{
			printf("我是子进程,标识数是%d\n", getpid());  // getpid()功能是取得进程识别码(即标识数)。
			sleep(1);                                                           
			j--;
		}
		exit(0);
	}
	else
	{
		while(k)
		{
			printf("我是父进程,标识数是%d\n", getpid());  
			sleep(1);
			k--;
		}
		exit(0);
	}
	return 0;
}

// fork2.c

#include 
#include  
#include 
#include  
int main()                                      
{
	int p = fork(); 
	printf("%d: Attempted to create a child process.\n", getpid());
	
	if (p < 0)
	{ 
		printf("%d: Failed to create a child process.\n", getpid()); 
		exit(1);  // 返回1等其他数字通常代表异常终止
	}
	else if (p == 0) 
	{
		printf("%d: This is the child process.\n", getpid());
		char* const args[] = {"ls","-al","/etc/passwd",0};
		execvp("ls", args);
		puts("This shouldn't be printed out.");  // puts(s)函数用来输出字符串并换行,s为字符串变量(字符串数组名或字符串指针)
		exit(0);
	}
	else 
	{
		printf("The PID of the child process is %d.\n", p);
		int* s; waitpid(p, s, 0);
		printf("%d: This is the parent process. The child process %d has exited.\n", getpid(), p);
	}
	return 0;
}

实验结果与分析

fork1.c

①若设置父进程、两个子进程都循环打印字符串100次,并且有“sleep(1)”的语句,则输出顺序为父进程、子进程2、子进程1,循环这样的输出直至结束。操作系统原理 | 实验2-进程控制(整理实验报告及知识点)_第1张图片

②同①,但注释掉“sleep(1)”语句,则输出顺序为先输出100次父进程的信息,接着开始输出子进程的信息——先输出一串子进程2的信息,然后子进程1和子进程2交替输出,最后输出一串子进程1的信息。
操作系统原理 | 实验2-进程控制(整理实验报告及知识点)_第2张图片操作系统原理 | 实验2-进程控制(整理实验报告及知识点)_第3张图片
③若设置循环打印字符串10次,且注释掉“sleep(1)”语句,结果如下:
操作系统原理 | 实验2-进程控制(整理实验报告及知识点)_第4张图片
④若修改为无限循环打印,且注释掉“sleep(1)”,则一开始执行的都是父进程,到后来也有三个进程一起抢占的情况,也有某个子进程独占的情况。因为是无限循环,且执行速度很快,可用CTRL+C中断进程(相当于发送terminal信息到当前程序),观察输出结果。实验截图略去。

这里输出的标识数指的是进程的内部标识符。从实验结果可看出,标识符是一个累加整数。结束一个进程后,重新调度它,该进程的标识数也会变化,会比之前的大。子进程的标识数紧跟在父进程后(父进程的标识数是3265,子进程1的标识数是3266,子进程2的标识数是3267)。// 这条存疑,好像不一定是紧跟!

父进程与子进程是抢占资源调度并发执行的,但是我对具体的运行机制还是有些疑惑。为何有时会按照一定的顺序执行,有时会交替抢占,有时是无规律抢占,有时是某进程独占?是什么依据呢?
#待解决#

fork2.c

操作系统原理 | 实验2-进程控制(整理实验报告及知识点)_第5张图片
父进程(PID = 4861)调用fork()创建了子进程(PID = 4862)。子进程用execvp()运行了ls -al,用于显示所有文件与文件夹的详细信息。没有输出“This shouldn’t be printed out.”,表明系统调用execvp()会替换调用该系统调用的子进程的代码段。程序运行正常。


参考:
【1】https://wenku.baidu.com/view/18374d9f69dc5022aaea00f9.html // 实验参考
【2】https://blog.csdn.net/COFACTOR/article/details/106106801 // 实验参考
【3】https://blog.csdn.net/qq_31939617/article/details/80478663 // linux ls -al 各项说明
【4】https://blog.csdn.net/kxjrzyk/article/details/81603049 // fork()函数详解
【5】https://blog.csdn.net/tong_xin2010/article/details/38469471 // fork()函数详解
【6】https://blog.csdn.net/m0_37834471/article/details/83661189 // 并发进程
【7】https://blog.csdn.net/u012829687/article/details/33356433 // 并发进程

你可能感兴趣的:(操作系统原理)