精华内容
下载资源
问答
  • 操作系统实验一

    千次阅读 2020-05-20 21:13:48
    实验课程名称 操作系统实验 成绩 实验项目名称 进程管理与进程通信 指导老师 张艳玲 实验一 进程管理与进程通信 一、实验目的 1、掌握进程的概念,明确进程的含义。 2、认识并了解进程并发执行的实质,进程的阻塞与...

    广州大学学生实验报告
    开课学院:计算机科学与网络工程学院
    实验室:计算机软件实验室 2020 年5月20 日
    学院 计算机科学与教育软件学院 年级/专业/班 计科183 姓名 张健介 学号 1806100162
    实验课程名称 操作系统实验 成绩
    实验项目名称 进程管理与进程通信 指导老师 张艳玲

    实验一 进程管理与进程通信
    一、实验目的
    1、掌握进程的概念,明确进程的含义。
    2、认识并了解进程并发执行的实质,进程的阻塞与唤醒,终止与退出的过程。
    3、熟悉进程的睡眠、同步、撤消等进程控制方法。
    4、分析进程竞争资源的现象,学习解决进程互斥的方法 。
    5、了解什么是信号,利用信号量机制熟悉进程间软中断通信的基本原理,
    6、熟悉消息传送的机理 ,共享存储机制 。

    二、实验内容
    1、编写一段程序,使用系统调用fork( )创建两个子进程。当此程序运行时,在系统中有一个父进程和两个子进程并发执行,观察实验结果并分析原因。
    2、用fork( )创建一个进程,再调用exec( ),用新的程序替换该子进程的内容,利用wait( )来控制进程执行顺序,掌握进程的睡眠、同步、撤消等进程控制方法,并根据实验结果分析原因。
    3、编写一段多进程并发运行的程序,用lockf( )来给每一个进程加锁,以实现进程之间的互斥,观察并分析出现的现象及原因。
    4、编写程序:用fork( )创建两个子进程,再用系统调用signal( )让父进程捕捉键盘上来的中断信号(即按^c键);捕捉到中断信号后,父进程用系统调用kill( )向两个子进程发出信号,子进程捕捉到信号后分别输出下列信息后终止:
    Child process1 is killed by parent!
    Child process2 is killed by parent!
    父进程等待两个子进程终止后,输出如下的信息后终止:
    Parent process is killed!
    分析利用信号量机制中的软中断通信实现进程同步的机理。
    5、使用系统调用msgget( ),msgsnd( ),msgrev( ),及msgctl( )编制一长度为1k的消息发送和接收的程序,并分析消息的创建、发送和接收机制及控制原理。
    6、编制一长度为1k的共享存储区发送和接收的程序,并设计对该共享存储区进行互斥访问及进程同步的措施,必须保证实现正确的通信。

    三、实验原理
    1、进程创建与进程并发执行
    Linux中,进程既是一个独立拥有资源的基本单位,又是一个独立调度的基本单位。一个进程实体由若干个区(段)组成,包括程序区、数据区、栈区、共享存储区等。每个区又分为若干页,每个进程配置有唯一的进程控制块PCB,用于控制和管理进程。
    系统为每个进程配置了一张进程区表。表中,每一项记录一个区的起始虚地址及指向系统区表中对应的区表项。核心通过查找进程区表和系统区表,便可将区的逻辑地址变换为物理地址。
    进程是进程映像的执行过程,也就是正在执行的进程实体。它由三部分组成:
    (1)用户级上、下文。主要成分是用户程序;
    (2)寄存器上、下文。由CPU中的一些寄存器的内容组成,如PC,PSW,SP及通用寄存器等;
    (3)系统级上、下文。包括OS为管理进程所用的信息,有静态和动态之分。

    进程创建所涉及的系统调用:
    fork( ) 创建一个新进程。
    系统调用格式: pid=fork( )
    参数定义:int fork( )
    fork( )返回值意义如下:
    0:在子进程中,pid变量保存的fork( )返回值为0,表示当前进程是子进程。
    大于0:在父进程中,pid变量保存的fork( )返回值为子进程的id值(进程唯一标识符)。
    -1:创建失败。
    如果fork( )调用成功,它向父进程返回子进程的PID,并向子进程返回0,即fork( )被调用了一次,但返回了两次。此时OS在内存中建立一个新进程,所建的新进程是调用fork( )父进程(parent process)的副本,称为子进程(child process)。子进程继承了父进程的许多特性,并具有与父进程完全相同的用户级上下文。父进程与子进程并发执行。
    核心为fork( )完成以下操作:
    (1)为新进程分配一进程表项和进程标识符
    进入fork( )后,核心检查系统是否有足够的资源来建立一个新进程。若资源不足,则fork( )系统调用失败;否则,核心为新进程分配一进程表项和唯一的进程标识符。
    (2)检查同时运行的进程数目
    超过预先规定的最大数目时,fork( )系统调用失败。
    (3)拷贝进程表项中的数据
    将父进程的当前目录和所有已打开的数据拷贝到子进程表项中,并置进程的状态为“创建”状态。
    (4)子进程继承父进程的所有文件
    对父进程当前目录和所有已打开的文件表项中的引用计数加1。
    (5)为子进程创建进程上、下文
    进程创建结束,设子进程状态为“内存中就绪”并返回子进程的标识符。
    (6)子进程执行
    虽然父进程与子进程程序完全相同,但每个进程都有自己的程序计数器PC(注意子进程的PC开始位置),然后根据pid变量保存的fork( )返回值的不同,执行了不同的分支语句。

    2、进程的睡眠、同步、撤消等进程控制
    用fork( )创建一个进程,再调用exec( )用新的程序替换该子进程的内容,然后利用wait( )来控制进程执行顺序。
    (1)exec( )系列
    系统调用exec( )系列,也可用于新程序的运行。fork( )只是将父进程的用户级上下文拷贝到新进程中,而exec( )系列可以将一个可执行的二进制文件覆盖在新进程的用户级上下文的存储空间上,以更改新进程的用户级上下文。exec( )系列中的系统调用都完成相同的功能,它们把一个新程序装入内存,来改变调用进程的执行代码,从而形成新进程。如果exec( )调用成功,调用进程将被覆盖,然后从新程序的入口开始执行,这样就产生了一个新进程,新进程的进程标识符id 与调用进程相同。
    exec( )没有建立一个与调用进程并发的子进程,而是用新进程取代了原来进程。所以exec( )调用成功后,没有任何数据返回,这与fork( )不同。exec( )系列系统调用在UNIX系统库unistd.h中,共有execl、execlp、execle、execv、execvp五个,其基本功能相同,只是以不同的方式来给出参数。
    一种是直接给出参数的指针,如:
    int execl(path,arg0[,arg1,…argn],0);
    char *path,*arg0,*arg1,…,*argn;
    另一种是给出指向参数表的指针,如:
    int execv(path,argv);
    char *path,*argv[ ];
    具体使用可参考有关书。
    (2)exec( )和fork( )联合使用
    系统调用exec和fork( )联合使用能为程序开发提供有力支持。用fork( )建立子进程,然后在子进程中使用exec( ),这样就实现了父进程与一个与它完全不同子进程的并发执行。
    一般,wait、exec联合使用的模型为:
    int status;

    if (fork( )= =0)
    {
    …;
    execl(…);
    …;
    }
    wait(&status);
    (3)wait( )
    等待子进程运行结束。如果子进程没有完成,父进程一直等待。wait( )将调用进程挂起,直至其子进程因暂停或终止而发来软中断信号为止。如果在wait( )前已有子进程暂停或终止,则调用进程做适当处理后便返回。
    系统调用格式:
    int wait(status) 
    int *status;
    其中,status是用户空间的地址。它的低8位反应子进程状态,为0表示子进程正常结束,非0则表示出现了各种各样的问题;高8位则带回了exit( )的返回值。exit( )返回值由系统给出。
    核心对wait( )作以下处理:
    1)首先查找调用进程是否有子进程,若无,则返回出错码;
    2)若找到一处于“僵死状态”的子进程,则将子进程的执行时间加到父进程的执行时间上,并释放子进程的进程表项;
    3)若未找到处于“僵死状态”的子进程,则调用进程便在可被中断的优先级上睡眠,等待其子进程发来软中断信号时被唤醒。
    (4)exit( )
    终止进程的执行。
    系统调用格式:
        void exit(status)
       int status;
    其中,status是返回给父进程的一个整数,以备查考。
    为了及时回收进程所占用的资源并减少父进程的干预,UNIX/LINUX利用exit( )来实现进程的自我终止,通常父进程在创建子进程时,应在进程的末尾安排一条exit( ),使子进程自我终止。exit(0)表示进程正常终止,exit(1)表示进程运行有错,异常终止。
    如果调用进程在执行exit( )时,其父进程正在等待它的终止,则父进程可立即得到其返回的整数。核心须为exit( )完成以下操作:
    1)关闭软中断
    2)回收资源
    3)写记帐信息
    4)置进程为“僵死状态”

    3、多进程通过加锁互斥并发运行
    用lockf( )来给每一个进程加锁,以实现多进程之间的互斥。
    所涉及的系统调用:lockf(files,function,size),用作锁定文件的某些段或者整个文件。
    本函数的头文件为
    #include “unistd.h”
    参数定义:
    int lockf(files,function,size)
    int files,function;
    long size;
    其中:files是文件描述符;function是锁定和解锁:1表示锁定,0表示解锁。size是锁定或解锁的字节数,为0,表示从文件的当前位置到文件尾。

    4、进程间通过信号机制实现软中断通信
    (1)信号的基本概念
    每个信号都对应一个正整数常量(称为signal number,即信号编号。定义在系统头文件<signal.h>中),代表同一用户的诸进程之间传送事先约定的信息的类型,用于通知某进程发生了某异常事件。每个进程在运行时,都要通过信号机制来检查是否有信号到达。若有,便中断正在执行的程序,转向与该信号相对应的处理程序,以完成对该事件的处理;处理结束后再返回到原来的断点继续执行。实质上,信号机制是对中断机制的一种模拟,故在早期的UNIX版本中又把它称为软中断。
    信号与中断的相似点:
    1)采用了相同的异步通信方式;
    2)当检测出有信号或中断请求时,都暂停正在执行的程序而转去执行相应的处理程序;
    3)都在处理完毕后返回到原来的断点;
    4)对信号或中断都可进行屏蔽。
    信号与中断的区别:
    1)中断有优先级,而信号没有优先级,所有的信号都是平等的;
    2)信号处理程序是在用户态下运行的,而中断处理程序是在核心态下运行;
    (3)中断响应是及时的,而信号响应通常都有较大的时间延迟。
    信号机制具有以下三方面的功能:
    1)发送信号。发送信号的程序用系统调用kill( )实现;
    2)预置对信号的处理方式。接收信号的程序用signal( )来实现对处理方式的预置;
    3)收受信号的进程按事先的规定完成对相应事件的处理。
    (2)信号的发送
    信号的发送,是指由发送进程把信号送到指定进程的信号域的某一位上。如果目标进程正在一个可被中断的优先级上睡眠,核心便将它唤醒,发送进程就此结束。一个进程可能在其信号域中有多个位被置位,代表有多种类型的信号到达,但对于一类信号,进程却只能记住其中的某一个。
    进程用kill( )向一个进程或一组进程发送一个信号。
    (3)对信号的处理
    当一个进程要进入或退出一个低优先级睡眠状态时,或一个进程即将从核心态返回用户态时,核心都要检查该进程是否已收到软中断。当进程处于核心态时,即使收到软中断也不予理睬;只有当它返回到用户态后,才处理软中断信号。对软中断信号的处理分三种情况进行:
    1)如果进程收到的软中断是一个已决定要忽略的信号(function=1),进程不做任何处理便立即返回;
    2)进程收到软中断后便退出(function=0);
    3)执行用户设置的软中断处理程序。
    (4)所涉及的中断调用
    (1)kill( )
    系统调用格式:int kill(pid,sig)
    参数定义:int pid,sig;
    其中,pid是一个或一组进程的标识符,参数sig是要发送的软中断信号。
    1)pid>0时,核心将信号发送给进程pid。
    2)pid=0时,核心将信号发送给与发送进程同组的所有进程。
    3)pid=-1时,核心将信号发送给所有用户标识符真正等于发送进程的有效用户标识号的进程。
    (2)signal( )
    预置对信号的处理方式,允许调用进程控制软中断信号。
    系统调用格式
    signal(sig,function)
    头文件为
      #include <signal.h>
    参数定义
    signal(sig,function)
    int sig;
    void (*func) ( )
    其中sig用于指定信号的类型,sig为0则表示没有收到任何信号,余者如下表:

    值 名 字 说 明
    01 SIGHUP 挂起(hangup)
    02 SIGINT 中断,当用户从键盘按c键或break键时
    03 SIGQUIT 退出,当用户从键盘按quit键时
    04 SIGILL 非法指令
    05 SIGTRAP 跟踪陷阱(trace trap),启动进程,跟踪代码的执行
    06 SIGIOT IOT指令
    07 SIGEMT EMT指令
    08 SIGFPE 浮点运算溢出
    09 SIGKILL 杀死、终止进程
    10 SIGBUS 总线错误
    11 SIGSEGV 段违例(segmentation violation),进程试图去访问其虚地址空间以外的位置
    12 SIGSYS 系统调用中参数错,如系统调用号非法
    13 SIGPIPE 向某个非读管道中写入数据
    14 SIGALRM 闹钟。当某进程希望在某时间后接收信号时发此信号
    15 SIGTERM 软件终止(software termination)
    16 SIGUSR1 用户自定义信号1
    17 SIGUSR2 用户自定义信号2
    18 SIGCLD 某个子进程死
    19 SIGPWR 电源故障

    function:在该进程中的一个函数地址,在核心返回用户态时,它以软中断信号的序号作为参数调用该函数,对除了信号SIGKILL,SIGTRAP和SIGPWR以外的信号,核心自动地重新设置软中断信号处理程序的值为SIG_DFL,一个进程不能捕获SIGKILL信号。
    function 的解释如下:
    1)function=1时,进程对sig类信号不予理睬,亦即屏蔽了该类信号;
    2)function=0时,缺省值,进程在收到sig信号后应终止自己;
    3)function为非0,非1类整数时,function的值即作为信号处理程序的指针。

    5、消息的发送与接收
    使用系统调用msgget( ),msgsnd( ),msgrev( ),及msgctl( )编制一长度为1k的消息发送和接收的程序。
    消息(message)是一个格式化的可变长的信息单元。消息机制允许由一个进程给其它任意的进程发送一个消息。当一个进程收到多个消息时,可将它们排成一个消息队列。消息使用二种重要的数据结构:一是消息首部,其中记录了一些与消息有关的信息,如消息数据的字节数;二个消息队列头表,其每一表项是作为一个消息队列的消息头,记录了消息队列的有关信息。
    (1)消息机制的数据结构
    (1)消息首部
    记录一些与消息有关的信息,如消息的类型、大小、指向消息数据区的指针、消息队列的链接指针等。
    (2)消息队列头表
    其每一项作为一个消息队列的消息头,记录了消息队列的有关信息如指向消息队列中第一个消息和指向最后一个消息的指针、队列中消息的数目、队列中消息数据的总字节数、队列所允许消息数据的最大字节总数,还有最近一次执行发送操作的进程标识符和时间、最近一次执行接收操作的进程标识符和时间等。
    (3) 消息队列的描述符
    UNIX中,每一个消息队列都有一个称为关键字(key)的名字,是由用户指定的;消息队列有一消息队列描述符,其作用与用户文件描述符一样,也是为了方便用户和系统对消息队列的访问。
    涉及的系统调用
    (1) msgget( )
    创建一个消息,获得一个消息的描述符。核心将搜索消息队列头表,确定是否有指定名字的消息队列。若无,核心将分配一新的消息队列头,并对它进行初始化,然后给用户返回一个消息队列描述符,否则它只是检查消息队列的许可权便返回。
    系统调用格式:
    msgqid=msgget(key,flag)
    该函数使用头文件如下:
    #include<sys/types.h>
    #include<sys/ipc.h>
    #include<sys/msg.h>
    参数定义
    int msgget(key,flag)
    key_t key;
    int flag;
    其中:
    key是用户指定的消息队列的名字;flag是用户设置的标志和访问方式。如 IPC_CREAT |0400 是否该队列已被创建。无则创建,是则打开;
    IPC_EXCL |0400 是否该队列的创建应是互斥的。
    msgqid 是该系统调用返回的描述符,失败则返回-1。
    (2) msgsnd()
    发送一消息。向指定的消息队列发送一个消息,并将该消息链接到该消息队列的尾部。
    系统调用格式:
    msgsnd(msgqid,msgp,size,flag)
    该函数使用头文件如下:
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>
    参数定义:
    int msgsnd(msgqid,msgp,size,flag)
    I int msgqid,size,flag;
    struct msgbuf * msgp;
    其中msgqid是返回消息队列的描述符;msgp是指向用户消息缓冲区的一个结构体指针。缓冲区中包括消息类型和消息正文,即
    {
    long mtype; /消息类型/
    char mtext[ ]; /消息的文本/
    }
    size指示由msgp指向的数据结构中字符数组的长度;即消息的长度。这个数组的最大值由MSG-MAX( )系统可调用参数来确定。flag规定当核心用尽内部缓冲空间时应执行的动作:进程是等待,还是立即返回。若在标志flag中未设置IPC_NOWAIT位,则当该消息队列中的字节数超过最大值时,或系统范围的消息数超过某一最大值时,调用msgsnd进程睡眠。若是设置IPC_NOWAIT,则在此情况下,msgsnd立即返回。
    对于msgsnd( ),核心须完成以下工作:
    1)对消息队列的描述符和许可权及消息长度等进行检查。若合法才继续执行,否则返回;
    2)核心为消息分配消息数据区。将用户消息缓冲区中的消息正文,拷贝到消息数据区;
    3)分配消息首部,并将它链入消息队列的末尾。在消息首部中须填写消息类型、消息大小和指向消息数据区的指针等数据;
    4)修改消息队列头中的数据,如队列中的消息数、字节总数等。最后,唤醒等待消息的进程。
    (3) msgrcv( )
    接受一消息。从指定的消息队列中接收指定类型的消息。
    系统调用格式:
    msgrcv(msgqid,msgp,size,type,flag)
    本函数使用的头文件如下:
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>
    参数定义:
    int msgrcv(msgqid,msgp,size,type,flag)
    int msgqid,size,flag;
    struct msgbuf *msgp;
    long type;
    其中,msgqid,msgp,size,flag与msgsnd中的对应参数相似,type是规定要读的消息类型,flag规定倘若该队列无消息,核心应做的操作。如此时设置了IPC_NOWAIT标志,则立即返回,若在flag中设置了MS_NOERROR,且所接收的消息大于size,则核心截断所接收的消息。
    对于msgrcv系统调用,核心须完成下述工作:
    1)对消息队列的描述符和许可权等进行检查。若合法,就往下执行;否则返回;
    2)根据type的不同分成三种情况处理:
    type=0,接收该队列的第一个消息,并将它返回给调用者;
    type为正整数,接收类型type的第一个消息;
    type为负整数,接收小于等于type绝对值的最低类型的第一个消息。
    3)当所返回消息大小等于或小于用户的请求时,核心便将消息正文拷贝到用户区,并从消息队列中删除此消息,然后唤醒睡眠的发送进程。但如果消息长度比用户要求的大时,则做出错返回。
    (4) msgctl( )
    消息队列的操纵。读取消息队列的状态信息并进行修改,如查询消息队列描述符、修改它的许可权及删除该队列等。
    系统调用格式:
    msgctl(msgqid,cmd,buf);
    本函数使用的头文件如下:
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>
    参数定义:
    int msgctl(msgqid,cmd,buf);
    int msgqid,cmd;
    struct msgqid_ds *buf;
    其中,函数调用成功时返回0,不成功则返回-1。buf是用户缓冲区地址,供用户存放控制参数和查询结果;cmd是规定的命令。命令可分三类:
    1)IPC_STAT。查询有关消息队列情况的命令。如查询队列中的消息数目、队列中的最大字节数、最后一个发送消息的进程标识符、发送时间等;
    2)IPC_SET。按buf指向的结构中的值,设置和改变有关消息队列属性的命令。如改变消息队列的用户标识符、消息队列的许可权等;
    3)IPC_RMID。消除消息队列的标识符。
    msgqid_ds 结构定义如下:
    struct msgqid_ds
    { struct ipc_perm msg_perm; /许可权结构/
    short pad1[7]; /由系统使用/
    ushort msg_qnum; /队列上消息数/
    ushort msg_qbytes; /队列上最大字节数/
    ushort msg_lspid; /最后发送消息的PID/
    ushort msg_lrpid; /最后接收消息的PID/
    time_t msg_stime; /最后发送消息的时间/
    time_t msg_rtime; /最后接收消息的时间/
    time_t msg_ctime; /最后更改时间/
    };
    struct ipc_perm
    { ushort uid; /当前用户/
    ushort gid; /当前进程组/
    ushort cuid; /创建用户/
    ushort cgid; /创建进程组/
    ushort mode; /存取许可权/
    { short pid1; long pad2;} /由系统使用/
    }

    6、进程的共享存储区通信
    编制一长度为1k的共享存储区发送和接收的程序。
    (1)共享存储区机制的概念
    共享存储区(Share Memory)是UNIX系统中通信速度最高的一种通信机制。该机制可使若干进程共享主存中的某一个区域,且使该区域出现(映射)在多个进程的虚地址空间中。另一方面,一个进程的虚地址空间中又可连接多个共享存储区,每个共享存储区都有自己的名字。当进程间欲利用共享存储区进行通信时,必须先在主存中建立一共享存储区,然后将它附接到自己的虚地址空间上。此后,进程对该区的访问操作,与对其虚地址空间的其它部分的操作完全相同。进程之间便可通过对共享存储区中数据的读、写来进行直接通信。图示列出二个进程通过共享一个共享存储区来进行通信的例子。其中,进程A将建立的共享存储区附接到自己的AA’区域,进程B将它附接到自己的BB’区域。

    进程A的虚空间 内存空间 进程B的虚空间

                A
    
                A’
    
    
    
    应当指出,共享存储区机制只为进程提供了用于实现通信的共享存储区和对共享存储区进行操作的手段,然而并未提供对该区进行互斥访问及进程同步的措施。因而当用户需要使用该机制时,必须自己设置同步和互斥措施才能保证实现正确的通信。
    

    (2)涉及的系统调用
    1)shmget( )
    创建、获得一个共享存储区。
    系统调用格式:
    shmid=shmget(key,size,flag)
    该函数使用头文件如下:
    #include<sys/types.h>
    #include<sys/ipc.h>
    #include<sys/shm.h>
    参数定义
    int shmget(key,size,flag);
    key_t key;
    int size,flag;
    其中,key是共享存储区的名字;size是其大小(以字节计);flag是用户设置的标志,如IPC_CREAT。IPC_CREAT表示若系统中尚无指名的共享存储区,则由核心建立一个共享存储区;若系统中已有共享存储区,便忽略IPC_CREAT。
    附:
    操作允许权 八进制数
    用户可读 00400
    用户可写 00200
    小组可读 00040
    小组可写 00020
    其它可读 00004
    其它可写 00002

    控制命令 值
    IPC_CREAT 0001000
    IPC_EXCL 0002000
    例:shmid=shmget(key,size,(IPC_CREAT|0400))
    创建一个关键字为key,长度为size的共享存储区
    2)shmat( )
    共享存储区的附接。从逻辑上将一个共享存储区附接到进程的虚拟地址空间上。
    系统调用格式:
    virtaddr=shmat(shmid,addr,flag)
    该函数使用头文件如下:
    #include<sys/types.h>
    #include<sys/ipc.h>
    #include<sys/shm.h>
    参数定义
    char *shmat(shmid,addr,flag);
    int shmid,flag;
    char * addr;
    其中,shmid是共享存储区的标识符;addr是用户给定的,将共享存储区附接到进程的虚地址空间;flag规定共享存储区的读、写权限,以及系统是否应对用户规定的地址做舍入操作。其值为SHM_RDONLY时,表示只能读;其值为0时,表示可读、可写;其值为SHM_RND(取整)时,表示操作系统在必要时舍去这个地址。该系统调用的返回值是共享存储区所附接到的进程虚地址viraddr。
    3)shmdt( )
    把一个共享存储区从指定进程的虚地址空间断开。
    系统调用格式:
    shmdt(addr)
    该函数使用头文件如下:
    #include<sys/types.h>
    #include<sys/ipc.h>
    #include<sys/shm.h>
    参数定义
    int shmdt(addr);
    char addr;
    其中,addr是要断开连接的虚地址,亦即以前由连接的系统调用shmat( )所返回的虚地址。调用成功时,返回0值,调用不成功,返回-1。
    4)shmctl( )
    共享存储区的控制,对其状态信息进行读取和修改。
    系统调用格式:
    shmctl(shmid,cmd,buf)
    该函数使用头文件如下:
    #include<sys/types.h>
    #include<sys/ipc.h>
    #include<sys/shm.h>
    参数定义
    int shmctl(shmid,cmd,buf);
    int shmid,cmd;
    struct shmid_ds *buf;
    其中,buf是用户缓冲区地址,cmd是操作命令。命令可分为多种类型:
    第一种:用于查询有关共享存储区的情况。如其长度、当前连接的进程数、共享区的创建者标识符等;
    第二种:用于设置或改变共享存储区的属性。如共享存储区的许可权、当前连接的进程计数等;
    第三种:对共享存储区的加锁和解锁命令;
    第四种:删除共享存储区标识符等。
    上述的查询是将shmid所指示的数据结构中的有关成员,放入所指示的缓冲区中;而设置是用由buf所指示的缓冲区内容来设置由shmid所指示的数据结构中的相应成员。

    四、实验中用到的系统调用函数(包括实验原理中介绍的和自己采用的),自己采用的系统调用函数要按照指导书中的格式说明进行介绍。
    本实验的用到的主要系统调用函数:
    Fork, exec, wait, exit, getpid, sleep, lockf, kill, signal, read, write, msgget, msgsnd, msgrcv, msgctl,shmget, shmat, shmdt, shmctl。
    五、实验步骤(要求写出实验过程和思路。)
    1.父进程创建两个子进程,然后三个进程并发执行,父进程输出a,子进程a输出b,子进程b输出c,多次运行程序,并通过结果观察其规律。

    2.父进程创建一个子进程,如果子进程创建成功,若此时是父进程正在运行,则父进程进行等待操作,将cpu教给子进程,子进程获取cpu后,开始运行,用系统中bin目录下的ls命令程序装入子进程运行的地址,即用ls命令程序代替子进程。当子进程运行完毕,父进程输出,程序运行完毕。

    3.父进程创建两个子进程,若是父进程运行,则输出parent 1-500,若子进程运行,则输出son1-500或者daughter1-500,分别用加锁和不加锁的程序进行测试。

    4.用fork( )创建两个子进程,再用系统调用signal( )让父进程捕捉键盘上来的中断信号(即按^c键);捕捉到中断信号后,父进程用系统调用kill( )向两个子进程发出信号,子进程捕捉到信号后分别输出下列信息后终止:
    Child process1 is killed by parent!
    Child process2 is killed by parent!
    父进程等待两个子进程终止后,输出如下的信息后终止:
    Parent process is killed!

    5.在两个终端上创建两个进程,分别是client负责发送消息,server负责接受消息。

    6.建立两个进程server和client,开辟一个共享资源区,双方都处于不断同步的状态,client等待资源区被server修改,而server修改后,也在等待client修改资源区,client察觉资源区被修改,也会继续修改,就这样进行循环。

    六、实验数据及源代码(学生必须提交自己设计的程序源代码,并有注释,源代码电子版也一并提交),包括思考题的程序。
    1)

    #include <unistd.h>
    #include <sys/types.h>
    #include <stdio.h>
    int main(int argc,char *argv[])
    {
        int pa;  //进程a
        int pb;  //进程b
        while((pa=fork())==-1); //等待子进程a建立完成
        if(pa==0)
          {
             putchar('b');
          }
        else
          {
             while((pb=fork())==-1);
             if(pb==0)
               {
                  putchar('c');
               }
             else
               {
                 putchar('a');
               }
          }
    }
    

    2)

    #include <stdlib.h>
    #include<stdio.h>
    #include<sys/wait.h>
    #include<unistd.h>
    int main(int argc,char* argv[])
    {
      int pid;
      pid=fork();
      switch(pid)
      {
        case -1:
                printf("fork fail!\n");
                exit(1);
        case 0:
                execl("/bin/ls","-1","-color",NULL);
                printf("exec fail!\n");
                exit(1);
        default:
                wait(NULL);
                printf("ls completed!\n");
                exit(0);
    
      }
    }
    

    3)

    #include<stdio.h>
    #include<stdlib.h>
    #include<sys/wait.h>
    #include<unistd.h>
    
    int main(int argc,char* argv[])
    {
         int p1,p2,i;
         p1=fork();
         p2=fork();
         if(p1==0)
           {
               lockf(1,1,0);     //加锁
               for(i=0;i<500;i++)
                   printf("parent %d\n",i);
               lockf(1,0,0);   //解锁
               wait(0);  //保证子进程终止前,父进程不终止
               exit(0);
           }
         else
          {
               if(p2==0)
                 {
                     lockf(1,1,0);
                     for(i=0;i<500;i++)
                           printf("son %d\n",i);
                     lockf(1,0,0);
                     wait(0);  
                     exit(0);
                 }
               else
                {
                      lockf(1,1,0);
                      for(i=0;i<500;i++)
                           printf("daughter %d\n",i);
                      lockf(1,0,0);
                      exit(0);
                }
          }
    }
    

    4)

    #include<stdio.h>
    #include<signal.h>
    #include<unistd.h>
    #include<sys/wait.h>
    #include<stdlib.h>
    
    int pid1,pid2;
    int EndFlag=0;
    int pf1=0;
    int pf2=0;
    
    void IntDelete()
    {
       kill(pid1,16);
       kill(pid2,17);
    }
    
    void Int1()
    {
    printf("child process 1 is killed by parent!\n");
    exit(0);
    }
    void Int2()
    {
    printf("child process 2 is killed by parent!\n");
    exit(0);
    }
    int main(int argc,char*argv[])
    {
       int exitpid;
       if(pid1=fork())
          {
               if(pid2=fork())
                  {
                      signal(SIGINT,IntDelete);
                      waitpid(-1,&exitpid,0);
                      waitpid(-1,&exitpid,0);
                      printf("parent process is killed\n");
                      exit(0);
                  }
               else
                  {
                       signal(SIGINT,SIG_IGN);
                       signal(17,Int2);
                       pause();
                  }
          }
        else
          {
             signal(SIGINT,SIG_IGN);
             signal(16,Int1);
             pause();
          }
    
    }
    

    5)
    Server:

    #include<stdio.h>
    #include<stdlib.h>
    #include<sys/types.h>
    #include<sys/msg.h>
    #include<sys/ipc.h>
    #define MSGKEY 75
    struct msgform
    {
    long mtype;
    char mtextp[1000];
    }msg;
    int msgqid;
    void server()
    {
      msgqid=msgget(MSGKEY,0777|IPC_CREAT);
      do
       {
         msgrcv(msgqid,&msg,1030,0,0);
         printf("(sever)received\n");
       }while(msg.mtype!=1);
      msgctl(msgqid,IPC_RMID,0);
      exit(0);
    }
    int main(int argc,char* argv[])
    {
      server();
    }
    

    client:

    #include<stdio.h>
    #include<stdlib.h>
    #include<sys/types.h>
    #include<sys/msg.h>
    #include<sys/ipc.h>
    #define MSGKEY 75
    struct magform
    {
    long mtype;
    char mtext[1000];
    }msg;
    int msgqid;
    void client()
    {
       int i;
       msgqid=msgget(MSGKEY,0777);
       for(i=10;i>=-1;i--)
          {
            msg.mtype=i;
            printf("(client)sent\n");
            msgsnd(msgqid,&msg,1024,0);
          }
       exit(0);
    }
    int main(int argc,char* argv[])
    {
    client();
    }
    

    ~
    6)

    #include<stdio.h>
    #include<stdlib.h>
    #include<sys/types.h>
    #include<sys/shm.h>
    #include<sys/ipc.h>
    #include<sys/wait.h>
    
    #include<unistd.h>
    #define SHMKEY 75
    int shmid,i;
    int *addr;
    void client()
    {
      int i;
      shmid=shmget(SHMKEY,1024,0777);
      addr=shmat(shmid,0,0);
      for(i=9;i>=0;i--)
        {
          while(*addr!=-1)
             printf("(client)sent\n");
          *addr=i;
        }
    exit(0);
    }
    void server()
    {
       shmid=shmget(SHMKEY,1024,0777|IPC_CREAT);
       addr=shmat(shmid,0,0);
       do
       {
        *addr=-1;
        while(*addr==-1);
            printf("*(server)receive\n");
       }while(*addr);
       shmctl(shmid,IPC_RMID,0);
       exit(0);
    
    }
    
    int main(int argc,char*argv[])
    {
      while((i=fork())==-1);
      if(!i) server();
      system("ipcs -m");
      while((i=fork())==-1);
      if(!i) client();
      wait(0);
      wait(0);
    }
    

    七、实验结果分析(截屏的实验结果,与实验结果对应的实验分析)
    1、实验结果与实验程序、实验步骤、实验原理、操作系统原理的对应分析;
    2、不同条件下的实验结果反应的问题及原因;
    3、实验结果的算法时间、效率、鲁棒性等性能分析。

    结果:
    刚开始的结果都是acb。
    在这里插入图片描述
    但是这不太符合并发执行的进程结果,所以后来我把虚拟机的处理器内核数量从1提高到3就会出现随机的结果啦!
    在这里插入图片描述
    分析:从结果来看,父进程和两个子进程的并发执行,结果是随机的,即abc的顺序都有可能,也就是说,并发执行的子进程之间,在没有优先级的限制条件下,争夺cpu的情况是随机的,在代码中,可能存在进程建立时间和代码运行输出等时间的一个不对等性,但是进程获取的时间片是随机的,故而会输出随机的不同结果。
    思考题:
    2.
    结果:
    利用wait()函数实现同步:

    在这里插入图片描述
    未用wait()函数实现同步:
    在这里插入图片描述
    分析:第一个结果是利用睡眠等待等操作实现同步,第二幅图的结果是未用等待操作导致不同步的结果,由第一幅图可知,子进程创建成功,且运行了ls命令程序,多次运行程序也是同一个结果,而当取消wait()的时候,顺序输出便错乱了,说明该程序父进程和子进程实现了同步。
    3.
    结果:
    1)不用lockf()进行进程加锁:
    在这里插入图片描述
    利用lockf()给进程加锁:
    在这里插入图片描述
    分析:
    Lockf(1,1,0)加锁输出设备,而Lockf(1,0,0)解锁输出设备,未用lockf进行进程加锁,三个循环之间会交错进行,输出也会出现交错状态,但加锁后则不会,所以在lockf(1,1,0)和lockf(1,0,0)之间的for循环输出中,不会被间断。但加锁前的三个进程间的存在并发执行,故而parent,daughter,son这三个循环的输出顺序随机,但循环不会被间断。
    分析:

    结果:
    在这里插入图片描述
    分析:
    父进程接收到ctrl+c信号才会调用kill()向两个子进程发出信号。子进程接收到信号才会打印。
    5)
    结果:
    在这里插入图片描述
    在这里插入图片描述

    分析:首先运行服务代码test5_s,server会进入等待状态,继而运行客户代码test5_c像服务器发出消息,并输出(client)sent,继而运行服务器的终端收到消息,输出(server)received。

    6)
    结果:
    在这里插入图片描述
    分析:
    Client和server两个进程拥有共享资源区,开始时,client修改了资源区内容,然后等待共享区的内容被修改为-1,因此是等待状态,而server修改了共享区的内容后,client察觉到内容变为-1,故而又继续修改,不断循环,直到client修改为-1。

    思考题:

    1、进程创建与进程并发执行
    (1)系统是怎样创建进程的?
    1.申请空白PCB(进程控制块);
    2.为新进程分配资源;
    3.初始化PCB;
    4.就新进程插入就绪队列;
    (2)当首次调用新创建进程时,其入口在哪里?
    fork()函数被调用一次,但返回两次;两次返回区别在于:子程序返回值是0,而父进程返回值是子进程的ID。子进程和父进程运行相同的代码,但是有自己的数据空间。

    (3)程序的多次运行结果为什么不同?如何控制实验结果的随机性?
    多个进程的并发执行,每个进程都有先获取cpu的可能性,故而哪一个进程先执行是随机的。可以通过等待,睡眠的等操作来实现多个进程的同步。
    (4)利用strace 和ltrace -f -i -S ./executable-file-name查看程序执行过程,并分析原因,画出进程家族树。

    2、进程的睡眠、同步、撤消等进程控制
    (1)可执行文件加载时进行了哪些处理?
    进程用exec( )装入命令ls ,exec( )后,子进程的代码被ls的代码取代,这时子进程的PC指向ls的第1条语句,开始执行ls的命令代码
    (2)什么是进程同步?wait( )是如何实现进程同步的?
    进程同步是指多个相关进程在执行次序上进行协调,以使并发执行的主进程之间有效的共享资源和相互合作,从而使程序的执行具有可再现性。
    首先程序在调用fork()创建了一个子进程后,马上调用wait(),使父进程在子进程调用之前一直处于睡眠状态,这样使子进程先运行,子进程运行exec()装入命令后,然后调用wait(0),使子进程和父进程并发执行,实现了进程同步。
    (3)wait( )和exit()是如何控制实验结果的随机性的?
    可以看出在使用了exec()函数后程序使用了ls的命令,列出/bin/目录下的文件信息,执行完execl()函数后,子进程调用exit()函数,退出当前进程,我们可以发现在使用wait()函数后,父进程永远将在其他的子进程完成之后才执行,所以在输出的结果中我们可以看到最后输出的将是父进程的信息,这样进而可以控制实验结果的随机性。
    3、多进程通过加锁互斥并发运行
    (1)进程加锁和未上锁的输出结果相同吗? 为什么?
    大致与未上锁的输出结果相同,也是随着执行时间不同,输出结果的顺序有所不同。
    不同进程之间不存在共享临界资源问题,所以加锁与不加锁的效果大致相同。
    4、进程间通过信号机制实现软中断通信
    (1)为了得到实验内容要求的结果,需要用到哪些系统调用函数来实现及进程间的通信控制和同步?
    Signal,kill,wait,exit,lockf
    (1)kill( )和signal( )函数在信号通信中的作用是什么?如果分别注释掉它们,结果会如何?
    结果都不会有输出,因为注释kill的情况下,父进程在等待子进程的信号,子进程也在等待父进程的信号,进入的死锁状态。而注释掉signal的情况下,子进程无法接受道父进程的信号,也无法向父进程发送信号,所以子进程结束后,父进程一直处于等待状态。
    5、消息的发送与接收
    (1)为了便于操作和观察结果,需要编制几个程序分别用于消息的发送与接收?
    为了便于操作和观察结果,编制了两个程序client.c和server.c,分别用于信息的发送与接受。
    (2)这些程序如何进行编辑、编译和执行?为什么?
    使用vim工具:vim test5_c.c vim test5_s.c分别编辑两个文件的代码
    使用gcc: gcc -o test5_c test5_c.c和gcc -o test5_s test5_s.c
    最后在两个终端分别先后运行./test5_s和./test5_c

    (3)如何实现消息的发送与接收的同步?
    分别在两个终端建立两个进程server和client,client发送信号,server接受信号,先运行server,后运行client。在server接受到消息后才会输出。

    6、进程的共享存储区通信
    (1)为了便于操作和观察结果,需要如何合理设计程序来实现子进程间的共享存储区通信?
    建立两个进程server和client,开辟一个共享资源区,双方都处于不断同步的状态,client等待资源区被server修改,而server修改后,也在等待client修改资源区,client察觉资源区被修改,也会继续修改,就这样进行循环多次。

    (2)比较消息通信和共享存储区通信这两种进程通信机制的性能和优缺点。
    答:由于两种机制实现的机理和用处都不一样,难以直接进行时间上的比较。如果比较其性能,应更加全面的分析。
    (1)消息队列的建立比共享区的设立消耗的资源少。前者只是一个软件上设定的问题,后者需要对硬件的操作,实现内存的映像,当然控制起来比前者复杂。如果每次都重新进行队列或共享的建立,共享区的设立没有什么优势。
    (2)当消息队列和共享区建立好后,共享区的数据传输,受到了系统硬件的支持,不耗费多余的资源;而消息传递,由软件进行控制和实现,需要消耗一定的cpu的资源。从这个意义上讲,共享区更适合频繁和大量的数据传输。
    (3)消息的传递,自身就带有同步的控制。当等到消息的时候,进程进入睡眠状态,不再消耗cpu资源。而共享队列如果不借助其他机制进行同步,接收数据的一方必须进行不断的查询,白白浪费了大量的cpu资源。可见,消息方式的使用更加灵活。

    展开全文
  • 操作系统实验一之引导程序

    千次阅读 2016-09-05 09:07:16
    操作系统实验一之引导程序

    一, 实验内容

    改写bootsect.s和setup.s, 完成如下主要功能:
    
    1, bootsect.s能够在屏幕上打印一段提示信息"XXX is booting...", 其中XXX是你给自己的操作系统起的名字,例如LZJos、Sunix等.
    
    2, bootsect.s能够完成setup.s的载入, 并跳转到setup.s开始地址执行. 
    
    3, setup.s能够像屏幕输出一行信息 "Now we are in SETUP" 
    
    4, setup.s能获取至少一个基本的硬件参数(如内存参数、显卡参数、硬盘参数等),将其存放在内存的特定地址,并输出到屏幕上。
    
    5,setup.s不再加载linux内核, 保持上述信息显示到屏幕上即可
    

    二, 实验基础知识

    1。启动流程  
    这里写图片描述

    三, 实验步骤

    1, 完成bootsect的屏幕输出功能

      由于不需要加载linux内核,所以就不需要原始的linux代码那么复杂,比如: 将bootsect自身移动到0x90000处等操作,可以忽略的。
    

      要显示字符串,那么字符串显示到屏幕的哪里呢?当然是当前光标的位置了!所以第一步就要先读取光标的位置,这可以利用10号中断的3号子程序来完成。要显示字符串,可以利用10号功能的13号子程序来完成,需要注意的是一定要边显示字符边移动光标,最终的光标要移动到字符串的末尾处。最后要注意用0xAA55来标记引导扇区。代码如下:

    entry _start
    _start:
    ! 首先利用10号中断的3号功能来读取光标位置
        mov    ah,#0x03        
        xor    bh,bh            
        int    0x10
    
    ! 再利用10号中断的13号功能显示字符串
        mov    cx,#50            ! 加上回车和换行,字符串一共包含50个字符,所以设置cx为50
        mov    bx,#0x0007
        mov    bp,#msg1
        mov     ax,#0x07c0
        mov     es,ax            ! es:bp=显示字符串的地址
        mov    ax,#0x1301        
        int    0x10
    
    Inf_loop:
        jmp Inf_loop            ! 无限循环
    
    ! msg1处放置要显示的字符串
    msg1:
        .byte 13,10            ! 换行+回车
        .ascii "AXF OS is booting, my name is Aixiangfei ..."
        .byte 13,10,13,10        ! 两对换行+回车
    
    ! 下面是启动盘具有有效引导扇区的标志. 仅供BIOS中的程序加载扇区时识别使用。
    ! 它必须位于引导扇区的最后两个字节中.
    .org 510
    boot_flag:
        .word 0xAA55    ! 引导扇区的标记就是0XAA55

    编译和运行: 进入~/oslab/linux-0.11/boot/目录,编译并连接:

    as86 -0 -a -o bootsect.o bootsect.s
    ld86 -0 -s -o bootsect bootsect.o

      参数说明:-0(注意:这是数字0,不是字母O)表示生成8086的16位目标程序,-a表示生成与GNU as和ld部分兼容的代码,-s告诉链接器ld86去除最后生成的可执行文件中的符号信息。

      如果这两个命令没有任何输出,说明编译与链接都通过了。需要留意的生成的bootsect的大小是544字节,而引导程序必须要正好占用一个磁盘扇区,即512个字节。造成多了32个字节的原因是ld86产生的是Minix可执行文件格式,这样的可执行文件处理文本段、数据段等部分以外,还包括一个Minix可执行文件头部。所以最后必须要把这多余的32个字节删掉,可以用linux自带的工具dd来完成:

    dd bs=1 if=bootsect of=Image skip=32

      去掉这32个字节后,将生成的文件拷贝到linux-0.11目录下,并一定要命名为“Image”(注意大小写)。然后就可以run了

    2, setup的载入

      首先要确定setup是在磁盘的0磁道2扇区,linux 0.11中的setup占了4个扇区,而我们最后要写的setup显然没有那么复杂,所以可以只用1个扇区就可以了。另外,由于bootsect位于0x7c00处,占用512个字节,所以可以将setup载入到0x7e00处。要想从磁盘中载入数据到内存,可以利用BIOS提供的13号中断轻松完成。最后就直接用jumi指令跳转到0x7e00处即可。对前面的bootsect.s扩展之后的完整代码如下:

    SETUPLEN = 1
    SETUPSEG = 0x07e0
    
    entry _start
    _start:
    ! 首先利用10号中断的3号功能来读取光标位置
        mov    ah,#0x03        
        xor    bh,bh            
        int    0x10
    
    ! 再利用10号中断的13号功能显示字符串
        mov    cx,#50            ! 加上回车和换行,字符串一共包含50个字符,所以设置cx为50
        mov    bx,#0x0007
        mov    bp,#msg1
        mov     ax,#0x07c0
        mov     es,ax            ! es:bp=显示字符串的地址
        mov    ax,#0x1301        
        int    0x10
    
    load_setup:
        mov    dx,#0x0000        ! 设置驱动器和磁头(drive 0, head 0): 软盘0磁头
        mov    cx,#0x0002        ! 设置扇区号和磁道(sector 2, track 0):0磁头、0磁道、2扇区
        mov    bx,#0x0200        ! 设置读入的内存地址:BOOTSEG+address = 512,偏移512字节
        mov    ax,#0x0200+SETUPLEN    ! 设置读入的扇区个数(service 2, nr of sectors),
                        ! SETUPLEN是读入的扇区个数,Linux 0.11设置的是4,
                        ! 我们不需要那么多,我们设置为1
        int    0x13            ! 应用0x13号BIOS中断读入1个setup.s扇区
        jnc    ok_load_setup        ! 读入成功,跳转到ok_load_setup: ok - continue
        mov    dx,#0x0000        ! 软驱、软盘有问题才会执行到这里
        mov    ax,#0x0000        ! 否则复位软驱
        int    0x13
        j    load_setup        ! 重新循环,再次尝试读取
    
    ok_load_setup:
        jmpi    0,SETUPSEG
    
    ! msg1处放置要显示的字符串
    msg1:
        .byte 13,10            ! 换行+回车
        .ascii "AXF OS is booting, my name is Aixiangfei ..."
        .byte 13,10,13,10        ! 两对换行+回车
    
    ! 下面是启动盘具有有效引导扇区的标志. 仅供BIOS中的程序加载扇区时识别使用。
    ! 它必须位于引导扇区的最后两个字节中.
    .org 510
    boot_flag:
        .word 0xAA55    ! 引导扇区的标记就是0XAA55

    3, 完成setup的屏幕输出功能
      这个很简单,跟bootsect是一样的。代码如下:

    entry _start
    _start:
    ! 首先利用10号中断的3号功能来读取光标位置
        mov    ah,#0x03
        xor    bh,bh
        int    0x10
    
    ! 再利用10号中断的13号功能显示字符串
        mov    cx,#26
        mov    bx,#0x0007
        mov    bp,#msg
        mov ax,cs
        mov    es,ax
        mov    ax,#0x1301
        int    0x10
    
    Inf_loop:
        jmp Inf_loop            ! 无限循环
    
    msg:
        .byte 13,10
        .ascii "Now we are in SETUP."
        .byte 13,10,13,10
    
    
    .org 510
    boot_flag:
        .word 0xAA55

      编译和与运行:现在有两个文件都要编译、链接。一个个手工编译,效率低下,所以借助Makefile是最佳方式。linux 0.11中Makefile文件已经帮我们把这件事做好了。进入liux-0.11目录后,使用命令:

    $ make BootImage
      但是我们发现竟然出现了错误:
      这里写图片描述
      原因:这是因为make根据Makefile的指引执行了tools/build.c,它是为生成整个内核的镜像文件而设计的,没考虑我们只需要bootsect.s和setup.s的情况。build.c从命令行参数得到bootsect、setup和system内核的文件名,将三者做简单的整理后一起写入Image。其中system是第三个参数(argv[3])。当“make all”或者“makeall”的时候,这个参数传过来的是正确的文件名,build.c会打开它,将内容写入Image。而“make BootImage”时,传过来的是字符串”none”。所以,修改build.c的思路就是当argv[3]是”none”的时候,只写bootsect和setup,忽略所有与system有关的工作,或者在该写system的位置都写上“0”。
      修改build.c文件很简单,只需要把第178到183这几行代码删除即可!
    这里写图片描述
     
    4, 读取硬件参数

      这个部分是最复杂的了。需要打印的硬件信息有:光标位置,内存大小,磁盘的柱面数,磁头数,每磁道的扇区数。在这个实验中,这些参数的信息可以保存在内存中的任意位置,在linux 0.11中,这些参数信息是被保存到了0x90000处,所以不妨跟linux 0.11一样。

    (1)获取硬件参数

      获得光标位置信息,这个很简单,只需要调用13号中断的3号子程序就可以得到,前面已经用过了的。

      获得内存大小,可以调用用15号中断的88号子程序得到,也很简单。

      与磁盘相关的信息稍微复杂一点,这些信息被保存在0x0000:0x0104地址处的16个字节的中,这16个字节的信息叫做“磁盘参数表”。所以获得磁盘信息的方法就是复制数据。

    (2)数字转字符

      现在已经将这些硬件参数取出来放在了0x90000处,接下来的工作是将这些参数显示在屏幕上。这些参数都是一些无符号整数,所以需要做的主要工作是用汇编程序在屏幕上将这些整数用16进制的形式显示出来。

      因为十六进制与二进制有很好的对应关系(每4位二进制数和1位十六进制数存在一一对应关系),显示时只需将原二进制数每4位划成一组,按组求对应的ASCII码送显示器即可。ASCII码与十六进制数字的对应关系为:0x30~0x39对应数字0~9,0x41~0x46对应数字a~f。从数字9到a,其ASCII码间隔了7h,这一点在转换时要特别注意。为使一个十六进制数能按高位到低位依次显示,实际编程中,需对bx中的数每次循环左移一组(4位二进制),然后屏蔽掉当前高12位,对当前余下的4位(即1位十六进制数)求其ASCII码,要判断它是0~9还是a~f,是前者则加0x30得对应的ASCII码,后者则要加0x37才行,最后送显示器输出。以上步骤重复4次,就可以完成bx中数以4位十六进制的形式显示出来。
      因为在输出的时候需要调用多次,所以最好把这个功能写成一个函数print_bx,方便使用。既然要用到函数,故一定要先设置好栈。为了方便,还可以写一个函数print_nl实现换行的功能。
      最终setup.s代码如下:

    INITSEG  = 0x9000   ! we move boot here - out of the way
    SYSSEG   = 0x1000   ! system loaded at 0x10000 (65536).
    SETUPSEG = 0x9020   ! this is the current segment
    .globl begtext, begdata, begbss, endtext, enddata, endbss
    .text
    begtext:
    .data
    begdata:
    .bss
    begbss:
    .text
    
    entry start
    start:
    
        mov ax,#SETUPSEG
        mov es,ax
    ! init ss:sp
            mov ax,#INITSEG
        mov ss,ax
        mov sp,#0xFF00
    ! *************print we are in step**************
    
            mov ah,#0x03        ! read cursor pos
        xor bh,bh
        int 0x10
    
        mov cx,#28
        mov bx,#0x0007      ! page 0, attribute 7 (normal)
        mov bp,#msg_log
        mov ax,#0x1301      ! write string, move cursor
        int 0x10 
    
    !*******************************hard parm******************************
        mov ax,#INITSEG ! this is done in bootsect already, but...
        mov ds,ax
        mov ah,#0x03    ! read cursor pos
        xor bh,bh
        int 0x10        ! save it in known place, con_init fetches
        mov [0],dx      ! it from 0x90000.
    ! Get memory size (extended mem, kB)
        mov ah,#0x88
        int 0x15
        mov [2],ax
    
    ! Get hd0 data
    
        mov ax,#0x0000
        mov ds,ax
        lds si,[4*0x41]
        mov ax,#INITSEG
        mov es,ax
        mov di,#0x0004
        mov cx,#0x10
        rep
        movsb
    
    ! *************************show msg
    
        mov ax,#SETUPSEG
        mov es,ax
        mov ax,#INITSEG ! this is done in bootsect already, but...
        mov ds,ax
    
    
    ! **************print cursor position***********
            mov ah,#0x03        ! read cursor pos
        xor bh,bh
        int 0x10
    
        mov cx,#18
        mov bx,#0x0007      ! page 0, attribute 7 (normal)
        mov bp,#msg_cursor
        mov ax,#0x1301      ! write string, move cursor
        int 0x10 
    ! *****************cursor position**************
            mov ax,#INITSEG ! this is done in bootsect already, but...
        mov ds,ax
        mov dx,[0]
        call    print_hex 
    ! *******************print memory size****************       
            mov ah,#0x03        ! read cursor pos
        xor bh,bh
        int 0x10
    
        mov cx,#14
        mov bx,#0x0007      ! page 0, attribute 7 (normal)
        mov bp,#msg_memory
        mov ax,#0x1301      ! write string, move cursor
        int 0x10 
    ! *****************memory size**************
            mov ax,#INITSEG ! this is done in bootsect already, but...
        mov ds,ax
        mov dx,[2]
        call    print_hex 
    ! *****************print KB************
        mov ah,#0x03        ! read cursor pos
        xor bh,bh
        int 0x10
    
        mov cx,#2
        mov bx,#0x0007      ! page 0, attribute 7 (normal)
        mov bp,#msg7
        mov ax,#0x1301      ! write string, move cursor
        int 0x10 
    ! ****************print Cyles****************
        mov ah,#0x03        ! read cursor pos
        xor bh,bh
        int 0x10
    
        mov cx,#8
        mov bx,#0x0007      ! page 0, attribute 7 (normal)
        mov bp,#msg_cyles
        mov ax,#0x1301      ! write string, move cursor
        int 0x10 
    ! ********************Cyles size************************
            mov ax,#INITSEG ! this is done in bootsect already, but...
        mov ds,ax
        mov dx,[4]
        call    print_hex 
    ! ****************print Heads****************
        mov ah,#0x03        ! read cursor pos
        xor bh,bh
        int 0x10
    
        mov cx,#8
        mov bx,#0x0007      ! page 0, attribute 7 (normal)
        mov bp,#msg_heads
        mov ax,#0x1301      ! write string, move cursor
        int 0x10 
    ! ********************Heads size************************
            mov ax,#INITSEG ! this is done in bootsect already, but...
        mov ds,ax
        mov dx,[6]
        call    print_hex 
    ! ****************print msg_sectors****************
        mov ah,#0x03        ! read cursor pos
        xor bh,bh
        int 0x10
    
        mov cx,#10
        mov bx,#0x0007      ! page 0, attribute 7 (normal)
        mov bp,#msg_sectors
        mov ax,#0x1301      ! write string, move cursor
        int 0x10 
    ! ********************msg_sectors size************************
            mov ax,#INITSEG ! this is done in bootsect already, but...
        mov ds,ax
        mov dx,[12]
        call    print_hex 
    
    L6:
            jmp L6
    
    print_hex:       
        mov    cx,#4        ! 4???????
    print_digit:
        rol    dx,#4        ! ?????4???? !! ?dx??4?????4????
        mov    ax,#0xe0f    ! ah = ???????al = ???(4???)???
        and    al,dl        ! ?dl??4????
        add    al,#0x30     ! ?al????????0x30
        cmp    al,#0x3a
        jl     outp     !??????????
        add    al,#0x07     !?a?f????7
    outp: 
        int    0x10
        loop   print_digit
        ret
    print_nl:
        mov    ax,#0xe0d     ! CR
        int    0x10
        mov    al,#0xa     ! LF
        int    0x10
        ret
    
    ! ??wo are new in setup.s  ==28
    msg_log:
        .byte 13,10
        .ascii "we are now in setup..."
        .byte 13,10,13,10
    msg_cursor:
            .byte 13,10
        .ascii "Cursor position:"
    msg_memory:
            .byte 13,10
        .ascii "Memory Size:"
    msg_cyles:
            .byte 13,10
        .ascii "Cyles:"
    
    msg_heads:
            .byte 13,10
        .ascii "Heads:"
    msg_sectors:
            .byte 13,10
        .ascii "Sectors:"
    msg7:
            .ascii "KB"
    
    .text
    endtext:
    .data
    enddata:
    .bss
    endbss:

    实验结果如下:
    这里写图片描述

    展开全文
  • 阅读分别运行用API接口函数getpid()直接调用和汇编中断调用两种方式调用Linux操作系统的同个系统调用getpid的程序(请问getpid的系统调用号是多少?linux系统调用的中断向量号是多少?)。 2、上机完成习题1.13。 ...

    实验题目

    一、 (系统调用实验)了解系统调用不同的封装形式。
    1、参考下列网址中的程序。阅读分别运行用API接口函数getpid()直接调用和汇编中断调用两种方式调用Linux操作系统的同一个系统调用getpid的程序(请问getpid的系统调用号是多少?linux系统调用的中断向量号是多少?)。
    2、上机完成习题1.13。
    3、阅读pintos操作系统源代码,画出系统调用实现的流程图。

    二、 (并发实验)根据以下代码完成下面的实验。
    1、编译运行该程序(cpu.c),观察输出结果,说明程序功能。
    (编译命令: gcc -o cpu cpu.c –Wall)(执行命令:./cpu)
    2、再次按下面的运行并观察结果:执行命令:./cpu A & ; ./cpu B & ; ./cpu C & ; ./cpu D &程序cpu运行了几次?他们运行的顺序有何特点和规律?请结合操作系统的特征进行解释。

    三、 (内存分配实验)根据以下代码完成实验。
    1、 阅读并编译运行该程序(mem.c),观察输出结果,说明程序功能。(命令: gcc -o mem mem.c –Wall)
    2、 再次按下面的命令运行并观察结果。两个分别运行的程序分配的内存地址是否相同?是否共享同一块物理内存区域?为什么?命令:./mem &; ./mem &

    四、 (共享的问题)根据以下代码完成实验。
    1、 阅读并编译运行该程序,观察输出结果,说明程序功能。(编译命令:gcc -o thread thread.c -Wall)(执行命令1:./thread 1000)
    2、 尝试其他输入参数并执行,并总结执行结果的有何规律?你能尝试解释它吗?(例如执行命令2:./thread 100000)(或者其他参数。)
    3、 提示:哪些变量是各个线程共享的,线程并发执行时访问共享变量会不会导致意想不到的问题。

    实验解答

    一、
    1、 getpid的系统调用号位14H,Linux系统调用的中断向量号是80H。
    所给代码的程序运行结果如下
    在这里插入图片描述

    2、Linux系统调用实现
    (1)使用C语言程序代码如下

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    int main(int argc, char *argv[]){
    	write(1,"hello world\n",11);
    	return 0;
    }
    
    

    程序运行结果如下
    在这里插入图片描述

    (2)使用汇编语言程序代码如下

    #include <stdio.h>
    #include <unistd.h>
    void main (){
    char const *MASSAGE = “hello world\n”;
    int len = 11;
    int result = 0;
    asm volatile(
    “mov %2, %%edx:\n\r”
    “mov %1, %%ecx;\n\r”
    “mov $1, %%ebx;\n\r”
    “mov $4, %%eax;\n\r”
    “int $0x80”
    :”=m”(result)
    :”m”(msg),”r”(len)
    :”%%eax”
    );
    }
    
    

    程序运行结果如下
    在这里插入图片描述

    3、通过源代码可知,pintos操作系统的系统调用实现流程图如下
    在这里插入图片描述

    二、
    1、使用题中给的编译命令和执行命令,运行结果如下。
    在这里插入图片描述

    根据运行结果并结合代码可以看出,该程序的功能是输出命令行参数的内容。并且如果没有输入参数,那么会输出提示信息——如上图所示。
    对原代码中问题的解决:common.h头文件改为unistd.h。并且吧spin()函数改为speel()函数。
    2、使用题中给的执行命令,运行结果如下。
    在这里插入图片描述

    可以看到有四个程序在同时运行,四个程序的进程号是相邻的。并且输出顺序和参数的输入顺序(ABCD)是不一致的,并且输出顺序是在不断变化的。并且输出是在不断进行的,每次会输出四个字母。
    究其原因,四个程序在宏观上是同时运行的,微观上是在CPU中交替运行的。因为四个程序的优先级相同,先进行的程序不一定是先结束,所以这四个程序同时运行时,结束的顺序是不确定的,进而导致输出顺序是不确定的。
    三、
    1、使用所给的编译命令和执行命令,运行结果如下。
    在这里插入图片描述

    首先,打印程序进程号并且输出指针p被分配的内存地址。然后给p所指的内存单元赋值为0。然后在一个每次延时一秒的循环中,对p指向的内存单元中的值进行递增1的操作。并且输出进程号和当前p指向的内存单元中的值。
    2、使用所给的编译命令和执行命令,运行结果如下。
    在这里插入图片描述

    从运行结果来看,两个程序中的指针p指向的内存地址不同并且相距较远。进而可知,两个独立运行的程序分配的内存地址不同,并且不共享一块物理地址。
    这是因为操作系统虚拟化了内存,每个进程访问自己的私有虚拟地址空间,然后操作系统通过某种方式把虚拟地址空间映射到机器的物理内存。这样一个程序的内存引用不会影响其他进程(包括OS本身)的地址空间。
    四、
    1、使用所给的编译命令和执行命令,运行结果如下。
    在这里插入图片描述

    输出的结果中,起始值为0,而最终值2000为输入参数1000的两倍。
    程序使用pthread.create()函数创建了两个线程。每个线程中运行一个worker()函数,该函数的作用是循环递增,循环次数上限为输入的参数。这里输入的参数为1000,两个线程中的worker()函数每个把counter的值“加1”1000次,所以counter的最终值为2000。
    程序的功能是输入参数为N时,程序最终输出值为2N。
    2、不断增大输入的参数的值,会发现得到输出不是2N。
    计数器递增,需要三个指令:计数器的值从存储器加载到寄存器中、递增、存储回内存。这三条指令不是同时执行的,所以这些操作之间的执行顺序是不固定的。而counter、 loops这两个全局变量是被这两个线程共享的,当两个的线程一起被调用时,共享全局变量counter。 多线程程序中,同一个变量可能被多个线程修改。所以当参数变得很大时,可能会因为指令调用顺序和参数共享的缘故导致输出结果的错误。

    程序代码及可执行程序详见git链接

    展开全文
  • &lt;&lt;操作系统&gt;&gt;第实验 操作系统初步 学院:计算机与信息技术 ...阅读分别运行用API接口函数getpid()直接调用和汇编中断调用两种方式调用Linux操作系统的同个系统调用getpid的程序...

    <<操作系统>>第一次实验

    操作系统初步

    学院:计算机与信息技术
    专业:信息安全(保密技术)
    班级:安全1601
    姓名:李超
    学号:16281194
    2019年3月5日
    一、(系统调用实验)了解系统调用不同的封装形式。
    1、参考下列网址中的程序。阅读分别运行用API接口函数getpid()直接调用和汇编中断调用两种方式调用Linux操作系统的同一个系统调用getpid的程序(请问getpid的系统调用号是多少?linux系统调用的中断向量号是多少?)。
    在这里插入图片描述在这里插入图片描述
    系统调用getpid的中断号通过查询syscall_32.tbl表可知为20。系统调用其实就是内核空间里的函数的执行,但是出于提高系统安全性,提高用户程序的可移植性等目的,用户程序是不能直接调用或修改内核空间里的函数的,这个时候就用到了系统调用,它其实就是连接用户空间和内核空间的纽带,内核内函数的具体实现和系统调用函数之间的对应关系通过syscall_32.tbl表来记录。
    在这里插入图片描述
    2、上机完成习题1.13。
    Linux调用C函数形式
    在这里插入图片描述在这里插入图片描述
    汇编形式
    在这里插入图片描述
    这段代码在64位系统中无法直接编译,编译32位程序需要加上”-m32”。但是直接输入会提示错误,需要安装适配库” sudo apt-get install gcc-multilib g+±multilib module-assistant”。

    3、阅读pintos操作系统源代码,画出系统调用实现的流程图。
    在这里插入图片描述
    二、(并发实验)根据以下代码完成下面的实验。
    要求:
    1、 编译运行该程序(cpu.c),观察输出结果,说明程序功能。
    (编译命令: gcc -o cpu cpu.c –Wall)(执行命令:./cpu)
    2、再次按下面的运行并观察结果:执行命令:./cpu A & ; ./cpu B & ; ./cpu C & ; ./cpu D &程序cpu运行了几次?他们运行的顺序有何特点和规律?请结合操作系统的特征进行解释。

    1 #include <stdio.h>
    2 #include <stdlib.h>
    3 #include <sys/time.h>
    4 #include <assert.h>
    5 #include “common.h”
    6
    7 int
    8 main(int argc, char *argv[])
    9 {
    10 if (argc != 2) {
    11 fprintf(stderr, “usage: cpu \n”);
    12 exit(1);
    13 }
    14 char *str = argv[1];
    15 while (1) {
    16 spin(1);
    17 printf("%s\n", str);
    18 }
    19 eturn 0;
    20
    在这里插入图片描述
    经过上面的实验,在cpu在一个周期内运行了4次但是由于测不准原理,无法准确的确认时间开销,但是可以大致计算吞吐量。
    输出顺序与系统调度有关系,如果是单道或者轮转,那输出肯定是顺次的,但是由于现在的系统调度比较复杂,如果是随机调度,输出顺序就是不一定的。

    三、(内存分配实验)根据以下代码完成实验。
    要求:
    1、阅读并编译运行该程序(mem.c),观察输出结果,说明程序功能。(命令: gcc -o mem mem.c –Wall)
    2、再次按下面的命令运行并观察结果。两个分别运行的程序分配的内存地址是否相同?是否共享同一块物理内存区域?为什么?命令:./mem &; ./mem &
    1 #include <unistd.h>
    2 #include <stdio.h>
    3 #include <stdlib.h>
    4 #include “common.h”
    5
    6 int
    7 main(int argc, char *argv[])
    8 {
    9 int *p = malloc(sizeof(int)); // a1
    10 assert(p != NULL);
    11 printf("(%d) address pointed to by p: %p\n",
    12 getpid(), p); // a2
    13 *p = 0; // a3
    14 while (1) {
    15 Spin(1);
    16 *p = *p + 1;
    17 printf("(%d) p: %d\n", getpid(), *p); // a4
    18 }
    19 return 0;
    在这里插入图片描述
    经过上面的实验两个程序分配的内存地址不同,但是在关闭了地址随机化,分配地址相同。但是具体物理地址不能完全确定,其中有对应关系。是随机的,可能不是物理连续的。

    四、(共享的问题)根据以下代码完成实验。
    要求:
    1、 阅读并编译运行该程序,观察输出结果,说明程序功能。(编译命令:gcc -o thread thread.c -Wall –pthread)(执行命令1:./thread 1000)
    2、 尝试其他输入参数并执行,并总结执行结果的有何规律?你能尝试解释它吗?(例如执行命令2:./thread 100000)(或者其他参数。)
    3、 提示:哪些变量是各个线程共享的,线程并发执行时访问共享变量会不会导致意想不到的问题。

    1 #include <stdio.h>
    2 #include <stdlib.h>
    3 #include “common.h”
    4
    5 volatile int counter = 0;
    6 int loops;
    7
    8 void *worker(void *arg) {
    9 int i;
    10 for (i = 0; i < loops; i++) {
    11 counter++;
    12 }
    13 return NULL;
    14 }
    15
    16 int
    17 main(int argc, char *argv[])
    18 {
    19 if (argc != 2) {
    20 fprintf(stderr, “usage: threads \n”);
    21 exit(1);
    22 }
    23 loops = atoi(argv[1]);
    24 pthread_t p1, p2;
    25 printf(“Initial value : %d\n”, counter);
    26
    27 Pthread_create(&p1, NULL, worker, NULL);
    28 Pthread_create(&p2, NULL, worker, NULL);
    29 Pthread_join(p1, NULL);
    30 Pthread_join(p2, NULL);
    31 printf(“Final value : %d\n”, counter);
    32 return 0;
    33
    在这里插入图片描述在这里插入图片描述
    在分别修改了参数之后,得到的数值不同于两倍,理论值应该是输入值得两倍,但是由于冲突,所以数值会小于两倍。

    实验代码地址

    展开全文
  • 实验名称:操作系统初步(系统调用实验)了解系统调用不同的封装形式实验原理API系统调用两者联系与区别系统调用号系统调用和函数调用的区别软中断int与函数调用call的区别Task 1实验过程实验结果Task 2实验过程Task...
  • 操作系统实验一 熟悉实验环境

    千次阅读 2020-04-18 14:22:24
    操作系统全部笔记目录见:操作系统笔记整理 先把实验楼里的简介放进来,原封不动: x86 模拟器 Bochs Bochs 是个免费且开放源代码的 IA-32(x86)架构 PC 机模拟器。在它模拟出的环境中可以运行 Linux、DOS 和...
  • 操作系统实验一:并发程序设计。包括: 1.使用fork()创建子进程 2.编写代码实现孤儿进程,编写代码创建僵尸进程 3.创建多个线程,在各个线程中打印出堆栈变量的地址, 4.创建相同数量的进程和线程比较进程控制块开销...
  • 操作系统实验一(内核编译,系统调用)

    千次阅读 多人点赞 2014-04-11 23:13:45
    操作系统实验一 ——-系统调用   注: 实验前的准备工作就不一一叙述了。 Ubuntu运行环境:mac osx10.9.1 +VMware Fusion 6.0.2 Ubuntu 版本: 12.10 64位   具体实验流程如下:   1.安装有关编译程序。...
  • 哈工大操作系统实验手册 实验资源与参考 不配环境懒人福利:实验楼 在线课程:操作系统,李治军,哈工大(网易云课堂) 参考阅读:《Linux内核完全注释》——赵炯,《操作系统原理、实现与实践》——李治军,...
  • 操作系统实验一:处理器管理 实验报告 一、实验目的 (1)加深对处理机调度的作用和工作原理的理解。 (2)进一步认识并发执行的实质。 二、实验要求: 本实验要求用高级语言,模拟在单处理器情况下,采用多...
  • 操作系统实验一:进程管理 实验目的 实验内容 操作系统实验一:进程管理 1.实验目的 1.理解进程的概念,明确进程和程序的区别 2.理解并发执行的实质 3.掌握进程的创建、睡眠、撤销等进程控制方法 2.实验内容...
  • 操作系统实验一 进程管理

    千次阅读 多人点赞 2019-05-18 14:59:09
    实验一 进程管理 1.目的和要求 通过实验理解进程的概念,进程的组成(PCB结构),进程的并发执行和操作系统进行进程管理的相关原语(主要是进程的创建、执行、撤消)。 2.实验内容 用C语言编程模拟进程管理,...
  • 计算机操作系统实验一:进程调度 **[目的要求]  用高级语言编写和调试一个进程调度程序,以加深对进程的概念及进程调度算法的理解.  [准备知识]  一、基本概念  1、进程的概念;  2、进程的状态和进程控制块;...
  • 杭电操作系统实验一报告

    千次阅读 2019-03-24 00:13:22
    按理说操作系统实验应该自己做,这样能锻炼自己。鉴于我的报告还是比较有参考价值,能让以后的同学参考一下,就做成md的形式。仅供参考! 实验一报告 一、实验内容: (1)实验名:Linux内核编译及添加系统调用...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 18,689
精华内容 7,475
关键字:

操作系统实验一