北邮操作系统进程管理实验报告.docx

上传人:b****1 文档编号:3276805 上传时间:2023-05-05 格式:DOCX 页数:21 大小:582.41KB
下载 相关 举报
北邮操作系统进程管理实验报告.docx_第1页
第1页 / 共21页
北邮操作系统进程管理实验报告.docx_第2页
第2页 / 共21页
北邮操作系统进程管理实验报告.docx_第3页
第3页 / 共21页
北邮操作系统进程管理实验报告.docx_第4页
第4页 / 共21页
北邮操作系统进程管理实验报告.docx_第5页
第5页 / 共21页
北邮操作系统进程管理实验报告.docx_第6页
第6页 / 共21页
北邮操作系统进程管理实验报告.docx_第7页
第7页 / 共21页
北邮操作系统进程管理实验报告.docx_第8页
第8页 / 共21页
北邮操作系统进程管理实验报告.docx_第9页
第9页 / 共21页
北邮操作系统进程管理实验报告.docx_第10页
第10页 / 共21页
北邮操作系统进程管理实验报告.docx_第11页
第11页 / 共21页
北邮操作系统进程管理实验报告.docx_第12页
第12页 / 共21页
北邮操作系统进程管理实验报告.docx_第13页
第13页 / 共21页
北邮操作系统进程管理实验报告.docx_第14页
第14页 / 共21页
北邮操作系统进程管理实验报告.docx_第15页
第15页 / 共21页
北邮操作系统进程管理实验报告.docx_第16页
第16页 / 共21页
北邮操作系统进程管理实验报告.docx_第17页
第17页 / 共21页
北邮操作系统进程管理实验报告.docx_第18页
第18页 / 共21页
北邮操作系统进程管理实验报告.docx_第19页
第19页 / 共21页
北邮操作系统进程管理实验报告.docx_第20页
第20页 / 共21页
亲,该文档总共21页,到这儿已超出免费预览范围,如果喜欢就下载吧!
下载资源
资源描述

北邮操作系统进程管理实验报告.docx

《北邮操作系统进程管理实验报告.docx》由会员分享,可在线阅读,更多相关《北邮操作系统进程管理实验报告.docx(21页珍藏版)》请在冰点文库上搜索。

北邮操作系统进程管理实验报告.docx

北邮操作系统进程管理实验报告

 

操作系统实验课程报告

 

课题:

进程管理实验

 

姓名张涛

学院计算机学院

班级2011211311

学号2011211419

2013年11月10日

1.实验目的:

(1)加深对进程概念的理解,明确进程和程序的区别;

(2)进一步认识并发执行的实质;

(3)分析进程争用资源的现象,学习解决进程互斥的方法;

(4)了解Linux系统中进程通信的基本原理。

2.实验预备内容

(1)阅读Linux的sched.h源码文件,加深对进程管理概念的理解。

这个文件长达2616行,这里截取第1221~1548行抄录在实验报告最后,即结构体task_struct,地位相当于PCB。

下面对几个比较重要的参数,结合本人的了解以及网上查阅的资料做一点解释。

中括号内的数字为代码行号,下同。

volatile long state:

【1222】进程状态字,表示进程当前的状态(运行、就绪、等待、僵死、暂停、交换),分别对应已定义好的常量;

TASK_RUNING:

正在运行或可运行状态;

TASK_INTERRUPTIBLE:

可打断睡眠状态;

TASK_UNINTERRUPTIBLE:

不可打断睡眠状态;

TASK_ZOMBLE:

僵死状态;

TASK_STOPPED:

暂停状态;

交换状态。

void *stack:

【1223】进程所使用的栈空间;

unsigned int flags:

【1225】进程标志(创建、关闭、跟踪、被跟踪、内核dump等),同样对应已定义好的常量;

unsigned int rt_priority:

【1237】表示本进程的实时优先级;

const struct sched_class*sched_class、struct sched_entityse:

【1239,1240】分别是调度类和调度实体,这两个结构包含了用于任务调度的完整的信息(进程信息、调度策略等);

unsigned int policy:

【1260】进程的调度策略标志,有三种调度标志:

SCHED_OTHER:

普通进程的调度策略,基于优先权的轮转法;

SCHED_FIFO:

实时进程的调度策略,基于先进先出的算法;

SCHED_RR:

实时进程的调度策略,基于优先权的轮询法。

struct list_headtasks:

【1274】任务队列,为一双向循环链表;

int pdeath_signal:

【1282】父进程终止时产生的信号;

pid_tpid:

【1294】进程标识符,操作系统每创建一个新的进程就要为这个新进程分配一个进程控制块(PCB),系统内核通过pid区分这些进程的;

struct task_struct*real_parent:

【1307】本进程的父进程的PCB;

struct list_headchildren:

【1312】本进程的子进程列表;

struct list_headptraced:

【1321】本进程正在使用ptrace监视的进程列表;

struct thread_structthread:

【1375】本进程下属的线程集;

struct signal_struct*signal、struct sighand_struct*sighand:

【1383,1384】分别是进程运行时产生的信号以及信号处理模块。

(2)阅读Linux的fork()源码文件,分析进程的创建过程。

do_fork()函数应该与fork()函数一样的,这个文件长达1287行,这里截取第835~1265

行(主要包括copy_process()函数与do_fork()函数)抄录在实验报告最后,并通过这两个函数简单分析一下进程的创建。

copy_process()函数的作用是克隆当前进程(包括下述所有需要被复制的信息),但是不负责运行。

首先,在内存中分配一个新的task_struct数据结构【852】,并将当前的task_struct复制给它,以代表新产生的进程的PCB【877】,用户进程数加1【889,890】。

然后进行对新进程的定义,包括将新进程定义为未执行【907】,将进程标志和PID复制给新进程【908,909】,初始化本进程的子进程链表【917】,以及初始化CPU计数、读/写字节计数和读/写系统调用计数【926-932】,复制文件系统信息、信号及信号处理机制、内存、线程集及命名空间等等【961-982】。

最后,设置对新进程的调度策略【1017】,完成与CPU使用以及与父进程之间关系相关的设定【1031-1050】等等。

最后是返回新进程及错误处理部分【1120-1161】。

其实我们可以发现,copy_process()函数做了许多的事,接下来的do_fork()函数反而显得相对简单。

首先将copy_process()能做的工作全部做完【1224】,然后根据情况决定新进程的唤醒/暂停【1245-1248】,最后,向父进程返回子进程的PID【1264】,一个新进程就产生了。

总之,进程的创建可以大致总结为以下三个过程:

第一,建立新的进程控制结构并初始化PCB;第二,对相关的内核数据结构进行复制或赋值,使之成为进程映象;第三,设置调度策略并启动调度程序,给予新进程运行的机会。

3.实验环境

此实验采用的是Win8下虚拟机VMware-workstation10软件中的ubuntu-10.10-desktop-i386版本。

/******************************************************************************

该虚拟机为ubuntu,获得win8中512MB内存,15G存储空间。

ubuntu用户操作名名tao。

******************************************************************************/

4.实验步骤

4.1.1题目要求:

编写一段程序,使用系统调用fork()创建两个子进程。

当此程序运行时,在系统中有一个父进程和两个子进程活动。

让每一个进程在屏幕上显示一个字符:

父进程显示字符“a”,子进程分别显示字符“b”和“c”。

试观察记录屏幕上的显示结果,并分析原因。

4.1.2程序设计说明:

利用fok()创建子进程

4.1.3源代码:

4.1.4运行结果:

4.1.5分析:

从截图可见运行结果有abc,bac,acb等。

这是因为三个进程间没有同步措施,所以父进程和两个子进程的输出次序带有随机性。

因此实际上三者的各种组合都可能出现。

当pid>0表示为父进程,=0子进程,<0创建失败,由此可以根据返回值来确定不同操作。

/******************************************************************************

程序一

4.1.6题目要求:

修改已经编写的程序,将每个进程输出一个字符改为每个进程输出一句话,再观察程序执行时屏幕上出现的现象,并分析原因。

4.1.7程序设计说明:

直接改自第一个程序。

其间加sleep()是为了规范下格式。

详见源代码。

4.1.8源代码:

4.1.9运行结果:

4.2.0分析:

可见运行情况和第一个程序是很像的。

由于父进程sleep(4)的存在,所以每次都是先打印完子进程的内容后再输出父进程内容。

/******************************************************************************

程序2:

4.2.1题目要求:

如果在程序中使用系统调用lockf()来给每一个进程加锁,可以实现进程之间的互斥,观察并分析出现的现象。

4.2.2程序设计说明:

此题主要是使用,lockf()函数,并用输出结果进行对比,从而可以观察到lockf()函数实际的用处。

但是此题若只输出一句话,则无法观察到分次执行的效果,但若每个进程输出两句话则可出现执行混乱的现象。

查阅资料:

lockf()函数允许将文件区域用作信号量(监视锁),或用于控制对锁定进程的访问(强制模式记录锁定)。

试图访问已锁定资源的其他进程将返回错误或进入休眠状态,直到资源解除锁定为止。

当关闭文件时,将释放进程的所有锁定,即使进程仍然有打开的文件。

当进程终止时,将释放进程保留的所有锁定。

/*****************************************************************************

Lockf(intfd,intcmd,off_tlen)函数的使用方法:

①:

参数fd是打开文件的文件描述符。

通过此函数调用建立锁定,文件描述符必须使用只写权限(O_WRONLY)或读写权限(O_RDWR)打开。

如果调用进程是具有PRIV_LOCKRDONLY权限的组的成员,它也可以使用lockf()来锁定使用只读权限(O_RDONLY)打开的文件。

②:

cmd是指定要采取的操作的控制值,允许的值定义如下:

#defineF_ULOCK0//解锁

#defineF_LOCK1//互斥锁定区域

#defineF_TLOCK2//测试互斥锁定区域

#defineF_TEST3//测试区域

③:

len:

要锁定或解锁的连续字节数:

要锁定的资源从文件中当前偏移量开始,对于正len将向前扩展,对于负len向后扩展(直到但不包括当前偏移量的前面的字节数)。

如果len=0,则锁定从当前偏移量到文件结尾的区域(即从当前偏移量到现有或任何将来的文件结束标志)。

******************************************************************************/

4.2.3源代码:

4.2.4运行结果:

若是去掉lockf(),则运行结果如下

4.2.5分析:

由结果分析易得:

加了lockf后,尽管两个子进程的先后顺序仍然无法确认,但是可以肯定的是加上lock后,每次执行lock间锁定的内容时,程序中是无法产生中断的,所以,lock具有对输出流的锁定作用。

/******************************************************************************

4.3进程的软中断通信

程序3:

4.3.1题目要求:

(a)使用系统调用fork()创建两个子进程,再用系统调用signal()让父进程捕捉键盘上来的中断信号(即按Del/CTRL+C键).当捕捉到中断信号后,父进程用系统调用kill()向两个子进程发出信号,子进程捕捉到信号后分别输出下列信息后终止:

ChildProcess1iskilledbyParent!

ChildProcess2iskilledbyParent!

父进程等待两个子进程终止后,输出如下的信息后终止:

ParentProcessiskilled!

4.3.2程序设计说明:

1)题目解析和问题解决

一,要求Del后引发父进程的动作。

实际就是触发软中断SIGINT。

二,要求在中断到来前两子进程处于等待状态,中断到来后立刻动作。

对此,我自定义了两个函数my_wait()函数和my_stop()函数,通过对flag标志位的操作来实现。

flag为真时等待,中断到来后通过my_stop()函数使flag改为假,从而退出等待的死循环。

虽然这样效率很低,但是在这个小程序里还是可以的。

三,至于如何控制父进程杀死子进程后再自杀,我用了signal()函数预留给用户自定义的10和12号信号,即SIGUSR1和SIGUSR2。

让两个子进程分别监听这两个信号,父进程被触发后分别向两个子进程发出这两个信号杀死他们,然后再退出即可。

2)知识点

补充学习到的函数的定义。

/**************************************************************************

1、Signal(intsignnum,sig_thandler)函数的使用方法:

  

参数说明:

 

第一个参数signum指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。

 

第二个参数handler描述了与信号关联的动作。

返回值:

返回先前的信号处理函数指针,如果有错误则返回SIG_ERR(-1)。

2、kill(pid_tpid,intsig)函数的使用方法:

参数说明:

pid:

可能选择有以下四种

1.pid大于零时,pid是信号欲送往的进程的标识。

2.pid等于零时,信号将送往所有与调用kill()的那个进程属同一个使用组的进程。

3.pid等于-1时,信号将送往所有调用进程有权给其发送信号的进程,除了进程1(init)。

4.pid小于-1时,信号将送往以-pid为组标识的进程。

sig:

准备发送的信号代码,假如其值为零则没有任何信号送出,但是系统会执行错误检查,通常会利用sig值为零来检验某个进程是否仍在执行。

返回值说明:

成功执行时,返回0。

失败返回-1。

3、signal(SIGINT,SIG_IGN)和signal(SIGQUIT,SIG_IGN)的区别:

SIGINT是信号名称,你可以将之替换为其他的你想要的任何信号,SIG_IGN是函数指针。

这条代码的意思是如果发生SIGINT信号则中断,转去执行SIG_IGN函数。

同理:

signal(SIGQUIT,SIG_IGN)

**************************************************************************/

3)父子父同步的实现

在进程同步中,使用exit()和wait()实现了父进程等子进程终止的同步,但是这种同步方法不能实现子进程对父进程的等待。

要实现子进程对父进程的等待可以使用父进程向子进程发软中断信号,子进程接收信号的方式实现。

这两种同步方式相结合,可以实现父→子→父的同步序列。

实现父→子→父同步的步骤如下:

⑴子进程使用signal()预置软中断处理函数,然后等待父进程发软中断信号;

⑵父进程中使用kill()发软中断信号给子进程,再用wait(0)等待子进程结束;

⑶子进程接收到软中断信号后转去执行中断处理函数

⑷子进程在中断处理返回后,使用exit(0)终止执行,向父进程发终止信息。

⑸父进程使用wait(0)接收到子进程的终止信息后结束等待,并终止自己的程序的执行。

 

4.3.3源代码:

4.3.4运行结果:

4.3.5分析:

如上图,可见两个子进程的执行顺序还是随机的。

/*****************************************************************************

程序3.2:

4.3.1题目要求:

(b)在上面的程序中增加语句signal(SIGINT,SIG_IGN)和signal(SIGQUIT,SIG_IGN),观察执行结果,并分析原因。

4.3.2程序设计说明:

signal(SIGTINT,SIG_IGN);//后台进程读中断信号,默认挂起

signal(SIGQUIT,SIG_IGN);//程序终止信号,默认操作写dump-core文件

4.3.3源代码:

(略)

signal(SIGINT,SIG_IGN);

signal(SIGQUIT,SIG_IGN);

到两个子进程中

4.3.4运行结果及分析:

分析:

程序1:

(使用kill()&signal())

此种情况下,当进程为父进程时,则需要用signal()捕捉,从键盘输入的ctrl+c信号,而wait_del()函数的作用则是用来模拟进程进行,若没有此函数则由于运行时间过短,进程可能还没捕捉到信号就已经退出进程了,从而达不到中断的效果了。

所以用了一个循环while(flag);模拟效果。

然后再kill掉两个子进程,然后用wait()函数等待子进程结束,在自杀。

若为子进程,则用signal()捕捉父进程发过来的自定义信号,同样用wait_del()函数来模拟进程进行的效果。

但由于子进程中没有对ctrl+c进行函数处理,所以直接就结束掉了,而父进程由于有了signal(sigint,change_state);函数的存在,则对ctrl+c函数进行处理,则父进程不会结束。

程序2:

(使用)了函数signal(SIGINT,SIG-IGN)

这个函数的意思就是忽略了ctrl+c这个信号的作用。

则将ctrl+c信号忽略掉,所以若将这个语句加到两个子进程的语句段中,全部会打印出话。

(使用)了函数signal(SIGQUIT,SIG-IGN)

这个函数的作用就是当遇到QUIT即ctrl+\信号时,会忽略此信号,不做处理,而实际处理ctrl+\后会出现以下结果:

QUIT(coredumples);

/******************************************************************************

4.4进程的管道通信

程序4:

4.4.1题目要求:

编制一段程序,实现进程的管道通信。

使用系统调用pipe()建立一条管道线,两个子进程P1和P2分别向管道各写一句话:

Child1issendingamessage!

Child2issendingamessage!

而父进程则从管道中读出来自于两个子进程的信息,显示在屏幕上。

要求父进程先接收子进程P1发来的消息,然后再接收子进程P2发来的消息。

4.4.2程序设计说明:

为了避免两个进程抢占管道,我把第二个子进程延时了,并且对管道加锁,从而形成独占,避免冲突产生。

而父进程只需用之前的wait()函数即可确保在两个子进程后执行。

 

查阅资料:

/******************************************************************************

管道的创建:

#include

intpipe(intfd[2])

该函数创建的管道的两端处于一个进程中间,在实际应用中没有太大意义,因此,一个

进程在由pipe()创建管道后,一般再fork一个子进程,然后通过管道实现父子进程间的

通信(因此也不难推出,只要两个进程中存在亲缘关系,这里的亲缘关系指的是具有共同的

祖先,都可以采用管道方式来进行通信)。

管道的读写规则:

管道两端可分别用描述字fd[0]以及fd[1]来描述,需要注意的是,管道的两端是固定了任务的。

即一端只能用于读,由描述字fd[0]表示,称其为管道读端;另一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。

如果试图从管道写端读取数据,或者向管道读端写入数据都将导致错误发生。

一般文件的I/O函数都可以用于管道,如close、read、write等等。

******************************************************************************/

4.4.3源代码:

 

4.4.4运行结果:

管道通信相对而言就没有什么难的了。

系统调用pipe()把一个二元组初始化为管道,一头进一头出(这里是tube[1]进,tube[0]出)。

子进程P1,P2都是往管道输入的,父进程会从管道读取信息。

sprintf(wb,"Child1issendingamessage!

\n");

write(tube[1],wb,50);

两个子进程的上面截取的两句话,是向管道中输入字符串。

read(tube[0],rb,50);

父进程从管道中读取字符串。

为了防止两个进程抢占管道,一方面把第二个子进程稍做延迟(sleep

(1)),另一方面用前边用过的lockf()锁死管道,形成独占,避免冲突发生。

 

/******************************************************************************

5思考

1)系统是怎么创建进程的?

答:

新进程通过克隆老进程或是当前进程来创建。

系统调用fork或clone可以创建新任务,复制发生在核心状态下地核心中。

在类UNIX系统中,除了根进程之外,如果想要在系统之中创建一个新的进程,唯一的方法就是利用一个已存在的进程通过系统调用fork()函数来实现,fork()函数被调用时,它会将调用它的进程复制出一个副本(一般会共享代码段,其它的数据段和堆栈等会复制个新的),原进程是复制得到进程的父进程,两个进程除了进程号不同,是一模一样,fork 调用结束后将 0 返回给子进程,将子进程的进程号返回给父进程,在调用fork函数之后,父子进程都将从位于fork函数的调用点之后的指令处开始执行,因为是两个不同的进程,它们的执行是并行的,先后次序是不定的。

(参考《Linux内核完全解析》和编程爱好者网站相关文章)

2)可执行文件加载时进行了哪些处理?

答:

当操作系统装载一个可执行文件的时候,首先操作系统判断该文件是否是一个合法的可执行文件。

如果是操作系统将按照段表中的指示为可执行程序分配地址空间。

加载文件最重要的是完成两件事:

加载程序段和数据段到内存,以及进行外部定义符号的重定位。

下面是ELF文件为例的可执行文件加载处理:

1,内核读文件的头部,然后根据文件的数据指示分别读入各种数据结构,找到可加载的段加载到内存中。

2,内核分析出文件对应的动态连接器名称并加载动态连接器。

3,内核在新进程的堆栈中设置一些标记-值对,以指示动态连接器的相关操作。

4,内核把控制传递给动态连接器。

5,动态连接器检查程序对外部文件(共享库)的依赖性,并在需要时对其进行加载。

6,动态连接器对程序的外部引用进行重定位。

7,动态连接器进行程序运行的初始化。

8,动态连接器把控制传递给程序,从文件头部定义的程序进入点开始执行。

(参考execve()的内核代码和网上相关文章)

3)当首次调用新创建进程时,其入口在哪里?

答:

当首次调用新创建进程时,入口是fork()之后的语句。

fork系统调用创建的子进程继承了原进程的context,也就是说fork调用成功后,子进程与父进程并发执行相同的代码。

但由于子进程也继承了父进程的程序指针,所以子进程是从fork()后的语句开始执行(也就是新进程调用的入口)。

另外fork在子进程和父进程中的返回值是不同的。

在父进程中返回子进程的PID,而在子进程中返回0。

所以可以在程序中检查PID的值,使父进程和子进程执行不同的分支。

(参考网上相关文章)

4)进程通信有什么特点?

答:

(参考网上文章)

linux下进程间通信的几种主要手段简介:

管道(Pipe)及有名管道(namedpipe):

管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,

展开阅读全文
相关资源
猜你喜欢
相关搜索
资源标签

当前位置:首页 > 人文社科 > 法律资料

copyright@ 2008-2023 冰点文库 网站版权所有

经营许可证编号:鄂ICP备19020893号-2